diff --git a/.circleci/config.yml b/.circleci/config.yml deleted file mode 100644 index f753ba2685..0000000000 --- a/.circleci/config.yml +++ /dev/null @@ -1,48 +0,0 @@ -version: 2 -jobs: - agent-build: - docker: - - image: bcgovimages/von-image:py36-1.15-1 - steps: - - checkout - - restore_cache: - keys: - - v5-pip-dependencies-{{ .Branch }}-{{ checksum "requirements.txt" }}-{{ checksum "requirements.dev.txt" }}-{{ checksum "requirements.bbs.txt" }} - - v5-pip-dependencies-{{ .Branch }}- - - run: - name: Install Python Dependencies - command: | - pip install \ - --user \ - -r requirements.txt \ - -r requirements.askar.txt \ - -r requirements.bbs.txt \ - -r requirements.dev.txt - - - save_cache: - paths: - - /home/indy/.local/lib/python3.6/site-packages - key: v5-pip-dependencies-{{ .Branch }}-{{ checksum "requirements.txt" }}-{{ checksum "requirements.dev.txt" }}-{{ checksum "requirements.bbs.txt" }}-{{ checksum "requirements.askar.txt" }} - - - run: - name: Run Agent Tests - command: | - [ ! -d test-reports ] && mkdir test-reports - python -m pytest - - - run: - name: Push to Codecov.io - command: | - bash <(curl -s https://codecov.io/bash) -f test-reports/coverage.xml - - - store_test_results: - path: test-reports - - - store_artifacts: - path: test-reports - -workflows: - version: 2 - aries_cloudagent: - jobs: - - agent-build diff --git a/.commitlint.config.js b/.commitlint.config.js new file mode 100644 index 0000000000..c34aa79d07 --- /dev/null +++ b/.commitlint.config.js @@ -0,0 +1,3 @@ +module.exports = { + extends: ['@commitlint/config-conventional'] +}; diff --git a/.dockerignore b/.dockerignore index 910edaa6ff..7ea06888de 100644 --- a/.dockerignore +++ b/.dockerignore @@ -6,4 +6,6 @@ build docs dist test-reports -.python-version \ No newline at end of file +.python-version +docker +env diff --git a/actions/run-indy-tails-server/action.yml b/.github/actions/run-indy-tails-server/action.yml similarity index 100% rename from actions/run-indy-tails-server/action.yml rename to .github/actions/run-indy-tails-server/action.yml diff --git a/actions/run-integration-tests/action.yml b/.github/actions/run-integration-tests/action.yml similarity index 84% rename from actions/run-integration-tests/action.yml rename to .github/actions/run-integration-tests/action.yml index 474f348ac2..61784bb6ba 100644 --- a/actions/run-integration-tests/action.yml +++ b/.github/actions/run-integration-tests/action.yml @@ -20,9 +20,12 @@ runs: - name: run-integration-tests-acapy # to run with external ledger and tails server run as follows (and remove the ledger and tails actions from the workflow): # run: LEDGER_URL=http://test.bcovrin.vonx.io PUBLIC_TAILS_URL=https://tails.vonx.io ./run_bdd ${{ inputs.TEST_SCOPE }} - run: LEDGER_URL=${{inputs.IN_LEDGER_URL}} PUBLIC_TAILS_URL=${{inputs.IN_PUBLIC_TAILS_URL}} ./run_bdd ${{ inputs.TEST_SCOPE }} + run: ./run_bdd ${{ inputs.TEST_SCOPE }} shell: bash env: + LEDGER_URL: ${{ inputs.IN_LEDGER_URL }} + PUBLIC_TAILS_URL: ${{ inputs.IN_PUBLIC_TAILS_URL }} + LOG_LEVEL: warning NO_TTY: "1" working-directory: acapy/demo branding: diff --git a/actions/run-von-network/action.yml b/.github/actions/run-von-network/action.yml similarity index 100% rename from actions/run-von-network/action.yml rename to .github/actions/run-von-network/action.yml diff --git a/.github/workflows/blackformat.yml b/.github/workflows/blackformat.yml index a9ae5dfdee..1885ba40c4 100644 --- a/.github/workflows/blackformat.yml +++ b/.github/workflows/blackformat.yml @@ -10,7 +10,9 @@ jobs: name: lint runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - uses: actions/setup-python@v2 + - uses: actions/checkout@v3 + - uses: actions/setup-python@v4 + with: + python-version: "3.9" - name: Black Code Formatter Check uses: psf/black@stable diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index e240dd8308..e6f15917a0 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -10,16 +10,20 @@ jobs: CodeQL-Build: # CodeQL runs on ubuntu-latest and windows-latest runs-on: ubuntu-latest + if: (github.event_name == 'pull_request' && github.repository == 'hyperledger/aries-cloudagent-python') || (github.event_name != 'pull_request') + + permissions: + security-events: write steps: - name: Checkout repository - uses: actions/checkout@v2 + uses: actions/checkout@v3 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@v1 + uses: github/codeql-action/init@v2 with: languages: python - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v1 + uses: github/codeql-action/analyze@v2 diff --git a/.github/workflows/deploy-development.yml b/.github/workflows/deploy-development.yml new file mode 100644 index 0000000000..95eaa1629a --- /dev/null +++ b/.github/workflows/deploy-development.yml @@ -0,0 +1,54 @@ +name: Deploy Development to App Runner +on: + push: + branches: [development] # Trigger workflow on git push to main branch + workflow_dispatch: # Allow manual invocation of the workflow + inputs: + branch: + description: deploy given branch + required: true + type: string + default: development + +jobs: + deploy: + runs-on: ubuntu-latest + # These permissions are needed to interact with GitHub's OIDC Token endpoint. + permissions: + id-token: write + contents: read + environment: development + + steps: + - name: Checkout Code + uses: actions/checkout@v3 + with: + persist-credentials: false + ref: ${{ github.event.inputs.branch }} + + - name: Configure AWS credentials + id: aws-credentials + uses: aws-actions/configure-aws-credentials@v4 + with: + # Use GitHub OIDC provider + role-to-assume: ${{ secrets.AWS_ASSUME_ROLE_ARN }} + aws-region: ${{ secrets.AWS_REGION }} + + - name: Login to Amazon ECR + id: login-ecr + uses: aws-actions/amazon-ecr-login@v2 + + - name: Set image tage as current date & time + id: date + run: echo "IMAGE_TAG=$(date +'%Y-%m-%dT%H-%M-%S')" >> $GITHUB_ENV + + - name: Build, tag, and push image to Amazon ECR + id: build-image + env: + ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }} + ECR_REPOSITORY: dev/acapy + PKG_TOKEN: ${{ secrets.PKG_TOKEN }} + run: | + docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG -t $ECR_REGISTRY/$ECR_REPOSITORY:latest . + docker push $ECR_REGISTRY/$ECR_REPOSITORY --all-tags + echo "Pushed $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG to AWS ECR" \ No newline at end of file diff --git a/.github/workflows/deploy-production.yml b/.github/workflows/deploy-production.yml new file mode 100644 index 0000000000..015fbf65f9 --- /dev/null +++ b/.github/workflows/deploy-production.yml @@ -0,0 +1,54 @@ +name: Deploy Production to App Runner +on: + push: + branches: [main] # Trigger workflow on git push to main branch + workflow_dispatch: # Allow manual invocation of the workflow + inputs: + branch: + description: deploy given branch + required: true + type: string + default: main + +jobs: + deploy: + runs-on: ubuntu-latest + # These permissions are needed to interact with GitHub's OIDC Token endpoint. + permissions: + id-token: write + contents: read + environment: production + + steps: + - name: Checkout Code + uses: actions/checkout@v3 + with: + persist-credentials: false + ref: ${{ github.event.inputs.branch }} + + - name: Configure AWS credentials + id: aws-credentials + uses: aws-actions/configure-aws-credentials@v4 + with: + # Use GitHub OIDC provider + role-to-assume: ${{ secrets.AWS_ASSUME_ROLE_ARN }} + aws-region: ${{ secrets.AWS_REGION }} + + - name: Login to Amazon ECR + id: login-ecr + uses: aws-actions/amazon-ecr-login@v2 + + - name: Set image tage as current date & time + id: date + run: echo "IMAGE_TAG=$(date +'%Y-%m-%dT%H-%M-%S')" >> $GITHUB_ENV + + - name: Build, tag, and push image to Amazon ECR + id: build-image + env: + ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }} + ECR_REPOSITORY: prod/acapy + PKG_TOKEN: ${{ secrets.PKG_TOKEN }} + run: | + docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG -t $ECR_REGISTRY/$ECR_REPOSITORY:latest . + docker push $ECR_REGISTRY/$ECR_REPOSITORY --all-tags + echo "Pushed $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG to AWS ECR" \ No newline at end of file diff --git a/.github/workflows/integrationtests.yml b/.github/workflows/integrationtests.yml index db62b14a34..c0ec41eee3 100644 --- a/.github/workflows/integrationtests.yml +++ b/.github/workflows/integrationtests.yml @@ -13,15 +13,15 @@ jobs: if: (github.event_name == 'pull_request' && github.repository == 'hyperledger/aries-cloudagent-python') || (github.event_name != 'pull_request') steps: - name: checkout-acapy - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: path: acapy #- name: run-von-network - # uses: ./acapy/actions/run-von-network + # uses: ./acapy/.github/actions/run-von-network #- name: run-indy-tails-server - # uses: ./acapy/actions/run-indy-tails-server + # uses: ./acapy/.github/actions/run-indy-tails-server - name: run-integration-tests - uses: ./acapy/actions/run-integration-tests + uses: ./acapy/.github/actions/run-integration-tests # to run with a specific set of tests include the following parameter: # with: # TEST_SCOPE: "-t @T001-RFC0037" diff --git a/.github/workflows/nightly-tests.yml b/.github/workflows/nightly-tests.yml new file mode 100644 index 0000000000..5c180f8fdd --- /dev/null +++ b/.github/workflows/nightly-tests.yml @@ -0,0 +1,40 @@ +name: Nightly Tests + +on: + schedule: + - cron: '0 0 * * *' + workflow_dispatch: + +jobs: + tests: + if: github.repository == 'hyperledger/aries-cloudagent-python' || github.event_name == 'workflow_dispatch' + name: Tests + strategy: + fail-fast: false + matrix: + os: ["ubuntu-latest"] + python-version: ["3.7", "3.8", "3.9", "3.10"] + include: + - os: "ubuntu-20.04" + python-version: "3.6" + uses: ./.github/workflows/tests.yml + with: + python-version: ${{ matrix.python-version }} + os: ${{ matrix.os }} + + tests-indy: + if: github.repository == 'hyperledger/aries-cloudagent-python' || github.event_name == 'workflow_dispatch' + name: Tests (Indy) + strategy: + fail-fast: false + matrix: + os: ["ubuntu-latest"] + python-version: ["3.7", "3.8", "3.9", "3.10"] + include: + - os: "ubuntu-20.04" + python-version: "3.6" + uses: ./.github/workflows/tests-indy.yml + with: + python-version: ${{ matrix.python-version }} + os: ${{ matrix.os }} + indy-version: "1.16.0" diff --git a/.github/workflows/pip-audit.yml b/.github/workflows/pip-audit.yml new file mode 100644 index 0000000000..486a36e0fb --- /dev/null +++ b/.github/workflows/pip-audit.yml @@ -0,0 +1,24 @@ +name: pip-audit + +on: + workflow_dispatch: + +permissions: + contents: read + +jobs: + selftest: + runs-on: ubuntu-latest + if: (github.event_name == 'pull_request' && github.repository == 'hyperledger/aries-cloudagent-python') || (github.event_name != 'pull_request') + steps: + - uses: actions/checkout@v3 + - name: install + run: | + python -m venv env/ + source env/bin/activate + python -m pip install --upgrade pip + python -m pip install . + - uses: pypa/gh-action-pip-audit@v1.0.0 + with: + virtual-environment: env/ + local: true diff --git a/.github/workflows/pr-tests.yml b/.github/workflows/pr-tests.yml new file mode 100644 index 0000000000..851ea9cf35 --- /dev/null +++ b/.github/workflows/pr-tests.yml @@ -0,0 +1,20 @@ +name: PR Tests + +on: + pull_request: + +jobs: + tests: + name: Tests + uses: ./.github/workflows/tests.yml + with: + python-version: "3.6" + os: "ubuntu-20.04" + + tests-indy: + name: Tests (Indy) + uses: ./.github/workflows/tests-indy.yml + with: + python-version: "3.6" + indy-version: "1.16.0" + os: "ubuntu-20.04" diff --git a/.github/workflows/publish-indy.yml b/.github/workflows/publish-indy.yml new file mode 100644 index 0000000000..0fd219e0f6 --- /dev/null +++ b/.github/workflows/publish-indy.yml @@ -0,0 +1,113 @@ +name: Publish ACA-Py Image (Indy) +run-name: Publish ACA-Py ${{ inputs.tag || github.event.release.tag_name }} Image (Indy ${{ inputs.indy_version || '1.16.0' }}) +on: + release: + types: [published] + + workflow_dispatch: + inputs: + indy_version: + description: 'Indy SDK Version' + required: true + default: 1.16.0 + type: string + tag: + description: 'Image tag' + required: true + type: string + platforms: + description: 'Platforms - Comma separated list of the platforms to support.' + required: true + default: linux/amd64 + type: string + ref: + description: 'Optional - The branch, tag or SHA to checkout.' + required: false + type: string + +# Note: +# - ACA-Py with Indy SDK image builds do not include support for the linux/arm64 platform. +# - See notes below for details. + +env: + INDY_VERSION: ${{ inputs.indy_version || '1.16.0' }} + + # Images do not include support for the linux/arm64 platform due to a known issue compiling the postgres plugin + # - https://github.com/hyperledger/indy-sdk/issues/2445 + # There is a pending PR to fix this issue here; https://github.com/hyperledger/indy-sdk/pull/2453 + # + # linux/386 platform support has been disabled pending a permanent fix for https://github.com/hyperledger/aries-cloudagent-python/issues/2124 + # PLATFORMS: ${{ inputs.platforms || 'linux/amd64,linux/386' }} + PLATFORMS: ${{ inputs.platforms || 'linux/amd64' }} + +jobs: + publish-image: + strategy: + fail-fast: false + matrix: + python-version: ['3.6', '3.9'] + + name: Publish ACA-Py Image (Indy) + runs-on: ubuntu-latest + steps: + - name: Checkout Code + uses: actions/checkout@v3 + with: + ref: ${{ inputs.ref || '' }} + + - name: Gather image info + id: info + run: | + echo "repo-owner=${GITHUB_REPOSITORY_OWNER,,}" >> $GITHUB_OUTPUT + + - name: Cache Docker layers + uses: actions/cache@v3 + with: + path: /tmp/.buildx-cache + key: ${{ runner.os }}-buildx-${{ github.sha }} + restore-keys: | + ${{ runner.os }}-buildx- + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + + - name: Log in to the GitHub Container Registry + uses: docker/login-action@v2 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Setup Image Metadata + id: meta + uses: docker/metadata-action@v4 + with: + images: | + ghcr.io/${{ steps.info.outputs.repo-owner }}/aries-cloudagent-python + tags: | + type=raw,value=py${{ matrix.python-version }}-indy-${{ env.INDY_VERSION }}-${{ inputs.tag || github.event.release.tag_name }} + + - name: Build and Push Image to ghcr.io + uses: docker/build-push-action@v3 + with: + push: true + context: . + file: docker/Dockerfile.indy + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + target: main + build-args: | + python_version=${{ matrix.python-version }} + indy_version=${{ env.INDY_VERSION }} + acapy_version=${{ inputs.tag || github.event.release.tag_name }} + cache-from: type=local,src=/tmp/.buildx-cache + cache-to: type=local,dest=/tmp/.buildx-cache-new,mode=max + platforms: ${{ env.PLATFORMS }} + + # Temp fix + # https://github.com/docker/build-push-action/issues/252 + # https://github.com/moby/buildkit/issues/1896 + - name: Move cache + run: | + rm -rf /tmp/.buildx-cache + mv /tmp/.buildx-cache-new /tmp/.buildx-cache diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000000..bb057f432e --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,97 @@ +name: Publish ACA-Py Image +run-name: Publish ACA-Py ${{ inputs.tag || github.event.release.tag_name }} Image +on: + release: + types: [published] + + workflow_dispatch: + inputs: + tag: + description: 'Image tag' + required: true + type: string + platforms: + description: 'Platforms - Comma separated list of the platforms to support.' + required: true + default: linux/amd64 + type: string + ref: + description: 'Optional - The branch, tag or SHA to checkout.' + required: false + type: string + +env: + # linux/386 platform support has been disabled pending a permanent fix for https://github.com/hyperledger/aries-cloudagent-python/issues/2124 + # PLATFORMS: ${{ inputs.platforms || 'linux/amd64,linux/arm64,linux/386' }} + PLATFORMS: ${{ inputs.platforms || 'linux/amd64' }} + +jobs: + publish-image: + strategy: + fail-fast: false + matrix: + python-version: ['3.6', '3.9'] + + name: Publish ACA-Py Image + runs-on: ubuntu-latest + steps: + - name: Checkout Code + uses: actions/checkout@v3 + with: + ref: ${{ inputs.ref || '' }} + + - name: Gather image info + id: info + run: | + echo "repo-owner=${GITHUB_REPOSITORY_OWNER,,}" >> $GITHUB_OUTPUT + + - name: Cache Docker layers + uses: actions/cache@v3 + with: + path: /tmp/.buildx-cache + key: ${{ runner.os }}-buildx-${{ github.sha }} + restore-keys: | + ${{ runner.os }}-buildx- + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + + - name: Log in to the GitHub Container Registry + uses: docker/login-action@v2 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Setup Image Metadata + id: meta + uses: docker/metadata-action@v4 + with: + images: | + ghcr.io/${{ steps.info.outputs.repo-owner }}/aries-cloudagent-python + tags: | + type=raw,value=py${{ matrix.python-version }}-${{ inputs.tag || github.event.release.tag_name }} + + - name: Build and Push Image to ghcr.io + uses: docker/build-push-action@v3 + with: + push: true + context: . + file: docker/Dockerfile + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + target: main + build-args: | + python_version=${{ matrix.python-version }} + acapy_version=${{ inputs.tag || github.event.release.tag_name }} + cache-from: type=local,src=/tmp/.buildx-cache + cache-to: type=local,dest=/tmp/.buildx-cache-new,mode=max + platforms: ${{ env.PLATFORMS }} + + # Temp fix + # https://github.com/docker/build-push-action/issues/252 + # https://github.com/moby/buildkit/issues/1896 + - name: Move cache + run: | + rm -rf /tmp/.buildx-cache + mv /tmp/.buildx-cache-new /tmp/.buildx-cache diff --git a/.github/workflows/pythonpublish.yml b/.github/workflows/pythonpublish.yml index 21f2f01de1..b42e56685b 100644 --- a/.github/workflows/pythonpublish.yml +++ b/.github/workflows/pythonpublish.yml @@ -8,19 +8,19 @@ jobs: deploy: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v1 - - name: Set up Python - uses: actions/setup-python@v1 - with: - python-version: '3.x' - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install setuptools wheel twine - - name: Build and publish - env: - TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }} - TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} - run: | - python setup.py sdist bdist_wheel - twine upload dist/* + - uses: actions/checkout@v3 + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: "3.x" + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install setuptools wheel twine + - name: Build and publish + env: + TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }} + TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} + run: | + python setup.py sdist bdist_wheel + twine upload dist/* diff --git a/.github/workflows/tests-indy.yml b/.github/workflows/tests-indy.yml new file mode 100644 index 0000000000..7e69e76b30 --- /dev/null +++ b/.github/workflows/tests-indy.yml @@ -0,0 +1,58 @@ +name: Tests (Indy) + +on: + workflow_call: + inputs: + python-version: + required: true + type: string + indy-version: + required: true + type: string + os: + required: true + type: string + +jobs: + tests: + name: Test Python ${{ inputs.python-version }} on Indy ${{ inputs.indy-version }} + runs-on: ${{ inputs.os }} + steps: + - uses: actions/checkout@v3 + + - name: Cache image layers + uses: actions/cache@v3 + with: + path: /tmp/.buildx-cache-test + key: ${{ runner.os }}-buildx-test-${{ github.sha }} + restore-keys: | + ${{ runner.os }}-buildx-test- + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + + - name: Build test image + uses: docker/build-push-action@v3 + with: + load: true + context: . + file: docker/Dockerfile.indy + target: acapy-test + tags: acapy-test:latest + build-args: | + python_version=${{ inputs.python-version }} + indy_version=${{ inputs.indy-version }} + cache-from: type=local,src=/tmp/.buildx-cache-test + cache-to: type=local,dest=/tmp/.buildx-cache-test-new,mode=max + + # Temp fix + # https://github.com/docker/build-push-action/issues/252 + # https://github.com/moby/buildkit/issues/1896 + - name: Move cache + run: | + rm -rf /tmp/.buildx-cache-test + mv /tmp/.buildx-cache-test-new /tmp/.buildx-cache-test + + - name: Run pytest + run: | + docker run --rm acapy-test:latest diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000000..919d1e4f21 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,35 @@ +name: Tests + +on: + workflow_call: + inputs: + python-version: + required: true + type: string + os: + required: true + type: string + +jobs: + tests: + name: Test Python ${{ inputs.python-version }} + runs-on: ${{ inputs.os }} + steps: + - uses: actions/checkout@v3 + - name: Set up Python ${{ inputs.python-version }} + uses: actions/setup-python@v4 + with: + python-version: ${{ inputs.python-version }} + cache: 'pip' + cache-dependency-path: 'requirements*.txt' + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip3 install --no-cache-dir \ + -r requirements.txt \ + -r requirements.askar.txt \ + -r requirements.bbs.txt \ + -r requirements.dev.txt + - name: Tests + run: | + pytest diff --git a/.gitignore b/.gitignore index 044eff6da1..c3e7baa1ad 100644 --- a/.gitignore +++ b/.gitignore @@ -154,6 +154,7 @@ Temporary Items ### .idea/* +**/.idea/* ### ### Windows @@ -188,4 +189,8 @@ _build/ **/*.iml # Open API build -open-api/.build \ No newline at end of file +open-api/.build + +# poetry +poetry.lock +pyproject.toml \ No newline at end of file diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000000..6c0600e70f --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,19 @@ +--- +repos: + - repo: https://github.com/alessandrojcm/commitlint-pre-commit-hook + rev: v2.2.0 + hooks: + - id: commitlint + stages: [commit-msg] + args: ["--config", ".commitlint.config.js"] + additional_dependencies: ['@commitlint/config-conventional'] + - repo: https://github.com/psf/black + rev: 22.3.0 + hooks: + - id: black + stages: [commit] + - repo: https://github.com/pycqa/flake8.git + rev: 3.9.0 + hooks: + - id: flake8 + stages: [commit] diff --git a/.readthedocs.yaml b/.readthedocs.yaml new file mode 100644 index 0000000000..c68ede4684 --- /dev/null +++ b/.readthedocs.yaml @@ -0,0 +1,10 @@ +version: 2 + +build: + os: "ubuntu-20.04" + tools: + python: "3.9" + +sphinx: + builder: dirhtml + configuration: docs/conf.py diff --git a/AdminAPI.md b/AdminAPI.md index f677daabdb..a5f537a7b4 100644 --- a/AdminAPI.md +++ b/AdminAPI.md @@ -2,72 +2,72 @@ ## Using the OpenAPI (Swagger) Interface -ACA-Py provides an OpenAPI-documented REST interface for administering the agent's internal state and sparking communication with connected agents. +ACA-Py provides an OpenAPI-documented REST interface for administering the agent's internal state and initiating communication with connected agents. -To see the specifics of the supported endpoints as well as the expected request and response formats it is recommended to run the `aca-py` agent with the `--admin {HOST} {PORT}` and `--admin-insecure-mode` command line parameters, which exposes the OpenAPI UI on the provided port for interaction via a web browser. Production deployments should run the agent with `--admin-api-key {KEY}` and add the `X-API-Key: {KEY}` header to all requests instead of running the agent with the `--admin-insecure-mode` parameter. +To see the specifics of the supported endpoints, as well as the expected request and response formats, it is recommended to run the `aca-py` agent with the `--admin {HOST} {PORT}` and `--admin-insecure-mode` command line parameters. This exposes the OpenAPI UI on the provided port for interaction via a web browser. For production deployments, run the agent with `--admin-api-key {KEY}` and add the `X-API-Key: {KEY}` header to all requests instead of using the `--admin-insecure-mode` parameter. ![Admin API Screenshot](/docs/assets/adminApi.png) To invoke a specific method: - * scroll to and find that endpoint; - * click on the endpoint name to expand its section of the UI; - * click on the Try it out button; - * fill in any data necessary to run the command; - * click Execute; - * check the response to see if the request worked as expected. +* Scroll to and find that endpoint; +* Click on the endpoint name to expand its section of the UI; +* Click on the Try it out button; +* Fill in any data necessary to run the command; +* Click Execute; +* Check the response to see if the request worked as expected. -The mechanical steps are easy, it’s fourth step from the list above that can be tricky. Supplying the right data and, where JSON is involved, getting the syntax correct - braces and quotes can be a pain. When steps don’t work, start your debugging by looking at your JSON. You may also choose to use a REST client like Postman or Insomnia which will provide syntax highlighting and other features to simplify the process. +The mechanical steps are easy; however, the fourth step from the list above can be tricky. Supplying the right data and, where JSON is involved, getting the syntax correct—braces and quotes can be a pain. When steps don't work, start your debugging by looking at your JSON. You may also choose to use a REST client like Postman or Insomnia, which will provide syntax highlighting and other features to simplify the process. -Because API methods will often kick off asynchronous processes, the JSON response provided by an endpoint is not always sufficient to determine the next action. To handle this situation as well as events triggered due to external inputs (such as new connection requests), it is necessary to implement a webhook processor, as detailed in the next section. +Because API methods often initiate asynchronous processes, the JSON response provided by an endpoint is not always sufficient to determine the next action. To handle this situation, as well as events triggered by external inputs (such as new connection requests), it is necessary to implement a webhook processor, as detailed in the next section. -The combination of an OpenAPI client and webhook processor is referred to as an ACA-Py Controller and is the recommended method to define custom behaviours for your ACA-Py-based agent application. +The combination of an OpenAPI client and webhook processor is referred to as an ACA-Py Controller and is the recommended method to define custom behaviors for your ACA-Py-based agent application. ## Administration API Webhooks When ACA-Py is started with the `--webhook-url {URL}` command line parameter, state-management records are sent to the provided URL via POST requests whenever a record is created or its `state` property is updated. -When a webhook is dispatched, the record `topic` is appended as a path component to the URL, for example: `https://webhook.host.example` becomes `https://webhook.host.example/topic/connections` when a connection record is updated. A POST request is made to the resulting URL with the body of the request comprised by a serialized JSON object. The full set of properties of the current set of webhook payloads are listed below. Note that empty (null-value) properties are omitted. +When a webhook is dispatched, the record `topic` is appended as a path component to the URL. For example, `https://webhook.host.example` becomes `https://webhook.host.example/topic/connections` when a connection record is updated. A POST request is made to the resulting URL with the body of the request comprising a serialized JSON object. The full set of properties of the current set of webhook payloads are listed below. Note that empty (null-value) properties are omitted. -#### Pairwise Connection Record Updated (`/connections`) +### Pairwise Connection Record Updated (`/connections`) - * `connection_id`: the unique connection identifier - * `state`: `init` / `invitation` / `request` / `response` / `active` / `error` / `inactive` - * `my_did`: the DID this agent is using in the connection - * `their_did`: the DID the other agent in the connection is using - * `their_label`: a connection label provided by the other agent - * `their_role`: a role assigned to the other agent in the connection - * `inbound_connection_id`: a connection identifier for the related inbound routing connection - * `initiator`: `self` / `external` / `multiuse` - * `invitation_key`: a verification key used to identify the source connection invitation - * `request_id`: the `@id` property from the connection request message - * `routing_state`: `none` / `request` / `active` / `error` - * `accept`: `manual` / `auto` - * `error_msg`: the most recent error message - * `invitation_mode`: `once` / `multi` - * `alias`: a local alias for the connection record +* `connection_id`: the unique connection identifier +* `state`: `init` / `invitation` / `request` / `response` / `active` / `error` / `inactive` +* `my_did`: the DID this agent is using in the connection +* `their_did`: the DID the other agent in the connection is using +* `their_label`: a connection label provided by the other agent +* `their_role`: a role assigned to the other agent in the connection +* `inbound_connection_id`: a connection identifier for the related inbound routing connection +* `initiator`: `self` / `external` / `multiuse` +* `invitation_key`: a verification key used to identify the source connection invitation +* `request_id`: the `@id` property from the connection request message +* `routing_state`: `none` / `request` / `active` / `error` +* `accept`: `manual` / `auto` +* `error_msg`: the most recent error message +* `invitation_mode`: `once` / `multi` +* `alias`: a local alias for the connection record -#### Basic Message Received (`/basicmessages`) +### Basic Message Received (`/basicmessages`) - * `connection_id`: the identifier of the related pairwise connection - * `message_id`: the `@id` of the incoming agent message - * `content`: the contents of the agent message - * `state`: `received` +* `connection_id`: the identifier of the related pairwise connection +* `message_id`: the `@id` of the incoming agent message +* `content`: the contents of the agent message +* `state`: `received` -#### Forward Message Received (`/forward`) +### Forward Message Received (`/forward`) Enable using `--monitor-forward`. - * `connection_id`: the identifier of the connection associated with the recipient key - * `recipient_key`: the recipient key of the forward message (`to` field of the forward message) - * `status`: The delivery status of the received forward message. Possible values: - * `sent_to_session`: Message is sent directly to the connection over an active transport session - * `sent_to_external_queue`: Message is sent to external queue. No information is known on the delivery of the message - * `queued_for_delivery`: Message is queued for delivery using outbound transport (recipient connection has an endpoint) - * `waiting_for_pickup`: The connection has no reachable endpoint. Need to wait for the recipient to connect with return routing for delivery - * `undeliverable`: The connection has no reachable endpoint, and the internal queue for messages is not enabled (`--enable-undelivered-queue`). +* `connection_id`: the identifier of the connection associated with the recipient key +* `recipient_key`: the recipient key of the forward message (`to` field of the forward message) +* `status`: The delivery status of the received forward message. Possible values: + * `sent_to_session`: Message is sent directly to the connection over an active transport session + * `sent_to_external_queue`: Message is sent to an external queue. No information is known on the delivery of the message + * `queued_for_delivery`: Message is queued for delivery using outbound transport (recipient connection has an endpoint) + * `waiting_for_pickup`: The connection has no reachable endpoint. Need to wait for the recipient to connect with return routing for delivery + * `undeliverable`: The connection has no reachable endpoint, and the internal queue for messages is not enabled (`--enable-undelivered-queue`). -#### Credential Exchange Record Updated (`/issue_credential`) +### Credential Exchange Record Updated (`/issue_credential`) * `credential_exchange_id`: the unique identifier of the credential exchange * `connection_id`: the identifier of the related pairwise connection @@ -88,37 +88,37 @@ Enable using `--monitor-forward`. * `auto_issue`: (boolean) whether to automatically issue the credential * `error_msg`: the previous error message -#### Presentation Exchange Record Updated (`/present_proof`) +### Presentation Exchange Record Updated (`/present_proof`) - * `presentation_exchange_id`: the unique identifier of the presentation exchange - * `connection_id`: the identifier of the related pairwise connection - * `thread_id`: the thread ID of the previously received presentation proposal or offer - * `initiator`: present-proof exchange initiator: `self` / `external` - * `state`: `proposal_sent` / `proposal_received` / `request_sent` / `request_received` / `presentation_sent` / `presentation_received` / `verified` - * `presentation_proposal_dict`: the presentation proposal message - * `presentation_request`: (Indy) presentation request (also known as proof request) - * `presentation`: (Indy) presentation (also known as proof) - * `verified`: (string) whether the presentation is verified: `true` or `false` - * `auto_present`: (boolean) prover choice to auto-present proof as verifier requests - * `error_msg`: the previous error message +* `presentation_exchange_id`: the unique identifier of the presentation exchange +* `connection_id`: the identifier of the related pairwise connection +* `thread_id`: the thread ID of the previously received presentation proposal or offer +* `initiator`: present-proof exchange initiator: `self` / `external` +* `state`: `proposal_sent` / `proposal_received` / `request_sent` / `request_received` / `presentation_sent` / `presentation_received` / `verified` +* `presentation_proposal_dict`: the presentation proposal message +* `presentation_request`: (Indy) presentation request (also known as proof request) +* `presentation`: (Indy) presentation (also known as proof) +* `verified`: (string) whether the presentation is verified: `true` or `false` +* `auto_present`: (boolean) prover choice to auto-present proof as verifier requests +* `error_msg`: the previous error message -## API Standard Behaviour +## API Standard Behavior The best way to develop a new admin API or protocol is to follow one of the existing protocols, such as the Credential Exchange or Presentation Exchange. The `routes.py` file contains the API definitions - API endpoints and payload schemas (note that these are not the Aries message schemas). -The payload schemas are defined using [marshmallow](https://marshmallow.readthedocs.io/) and will be validated automatically when the API is executed (using a middleware). (This raises a status `422` HTTP response with an error message if the schema validation fails.) +The payload schemas are defined using [marshmallow](https://marshmallow.readthedocs.io/) and will be validated automatically when the API is executed (using middleware). (This raises a status `422` HTTP response with an error message if the schema validation fails.) -API endpoints are defined using [aiohttp_apispec](https://github.com/maximdanilchenko/aiohttp-apispec) tags (e.g. `@doc`, `@request_schema`, `@response_schema` etc.) which define the input and output parameters of the endpoint. API url paths are defined in the `register()` method and added to the swagger page in the `post_process_routes()` method. +API endpoints are defined using [aiohttp_apispec](https://github.com/maximdanilchenko/aiohttp-apispec) tags (e.g. `@doc`, `@request_schema`, `@response_schema` etc.) which define the input and output parameters of the endpoint. API URL paths are defined in the `register()` method and added to the Swagger page in the `post_process_routes()` method. -The API's should return the following HTTP status: +The APIs should return the following HTTP status: - * HTTP 200 for successful API completion, with appropriate response - * HTTP 400 (or appropriate 4xx code) (with an error message) for errors on input parameters (i.e. the user can retry with different parameters and potentially get a successful API call) - * HTTP 404 if a record is expected and not found (generally for GET requests that fetch a single record) - * HTTP 500 (or appropriate 5xx code) if there is some other processing error (i.e. won't make any difference what parameters the user tries) with an error message +* HTTP 200 for successful API completion, with an appropriate response +* HTTP 400 (or appropriate 4xx code) (with an error message) for errors on input parameters (i.e., the user can retry with different parameters and potentially get a successful API call) +* HTTP 404 if a record is expected and not found (generally for GET requests that fetch a single record) +* HTTP 500 (or appropriate 5xx code) if there is some other processing error (i.e., it won't make any difference what parameters the user tries) with an error message -.. and should not return: +...and should not return: - * HTTP 500 with a stack trace due to untrapped error (we should handle error conditions with a 400 or 404 response, and catch errors and provide a meaningful error message) +* HTTP 500 with a stack trace due to an untrapped error (we should handle error conditions with a 400 or 404 response and catch errors, providing a meaningful error message) diff --git a/AnoncredsProofValidation.md b/AnoncredsProofValidation.md new file mode 100644 index 0000000000..aa4a599189 --- /dev/null +++ b/AnoncredsProofValidation.md @@ -0,0 +1,83 @@ +# Anoncreds Proof Validation in ACA-Py + +ACA-Py performs pre-validation when verifying Anoncreds presentations (proofs). Some scenarios are rejected (such as those indicative of tampering), while some attributes are removed before running the anoncreds validation (e.g., removing superfluous non-revocation timestamps). Any ACA-Py validations or presentation modifications are indicated by the "verify_msgs" attribute in the final presentation exchange object. + +The list of possible verification messages can be found [here](https://github.com/hyperledger/aries-cloudagent-python/blob/main/aries_cloudagent/indy/verifier.py#L24), and consists of: + +```python +class PresVerifyMsg(str, Enum): + """Credential verification codes.""" + + RMV_REFERENT_NON_REVOC_INTERVAL = "RMV_RFNT_NRI" + RMV_GLOBAL_NON_REVOC_INTERVAL = "RMV_GLB_NRI" + TSTMP_OUT_NON_REVOC_INTRVAL = "TS_OUT_NRI" + CT_UNREVEALED_ATTRIBUTES = "UNRVL_ATTR" + PRES_VALUE_ERROR = "VALUE_ERROR" + PRES_VERIFY_ERROR = "VERIFY_ERROR" +``` + +If there is additional information, it will be included like this: `TS_OUT_NRI::19_uuid` (which means the attribute identified by `19_uuid` contained a timestamp outside of the non-revocation interval (this is just a warning)). + +A presentation verification may include multiple messages, for example: + +```python + ... + "verified": "true", + "verified_msgs": [ + "TS_OUT_NRI::18_uuid", + "TS_OUT_NRI::18_id_GE_uuid", + "TS_OUT_NRI::18_busid_GE_uuid" + ], + ... +``` + +... or it may include a single message, for example: + +```python + ... + "verified": "false", + "verified_msgs": [ + "VALUE_ERROR::Encoded representation mismatch for 'Preferred Name'" + ], + ... +``` + +... or the `verified_msgs` may be null or an empty array. + +## Presentation Modifications and Warnings + +The following modifications/warnings may be made by ACA-Py, which shouldn't affect the verification of the received proof: + +- "RMV_RFNT_NRI": Referent contains a non-revocation interval for a non-revocable credential (timestamp is removed) +- "RMV_GLB_NRI": Presentation contains a global interval for a non-revocable credential (timestamp is removed) +- "TS_OUT_NRI": Presentation contains a non-revocation timestamp outside of the requested non-revocation interval (warning) +- "UNRVL_ATTR": Presentation contains attributes with unrevealed values (warning) + +## Presentation Pre-validation Errors + +The following pre-verification checks are performed, which will cause the proof to fail (before calling anoncreds) and result in the following message: + +```plaintext +VALUE_ERROR:: +``` + +These validations are all performed within the [Indy verifier class](https://github.com/hyperledger/aries-cloudagent-python/blob/main/aries_cloudagent/indy/verifier.py) - to see the detailed validation, look for any occurrences of `raise ValueError(...)` in the code. + +A summary of the possible errors includes: + +- Information missing in presentation exchange record +- Timestamp provided for irrevocable credential +- Referenced revocation registry not found on ledger +- Timestamp outside of reasonable range (future date or pre-dates revocation registry) +- Mismatch between provided and requested timestamps for non-revocation +- Mismatch between requested and provided attributes or predicates +- Self-attested attribute provided for a requested attribute with restrictions +- Encoded value doesn't match raw value + +## Anoncreds Verification Exceptions + +Typically, when you call the anoncreds `verifier_verify_proof()` method, it will return a `True` or `False` based on whether the presentation cryptographically verifies. However, in the case where anoncreds throws an exception, the exception text will be included in a verification message as follows: + +```plaintext +VERIFY_ERROR:: +``` diff --git a/CHANGELOG.md b/CHANGELOG.md index 3c55be3474..b10863afaf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,642 @@ +# 0.8.2 + +## June 29, 2023 + +Release 0.8.2 contains a number of minor fixes and updates to ACA-Py, including +the correction of a regression in Release 0.8.0 related to the use of plugins +(see [\#2255]). Highlights include making it easier to use tracing in a +development environment to collect detailed performance information about what +is going in within ACA-Py. + +This release pulls in [indy-shared-rs] Release 3.3 which fixes a serious issue in AnonCreds verification, as described in issue [\#2036], where the verification of a presentation with multiple revocable credentials fails when using [Aries Askar] and the +other shared components. This issue occurs only when using [Aries Askar] and [indy-credx Release 3.3]. + +An important new feature in this release is the ability to set some instance +configuration settings at the tenant level of a multi-tenant deployment. See PR +[\#2233]. + +There are no breaking changes in this release. + +[\#2255]: https://github.com/hyperledger/aries-cloudagent-python/pull/2255 +[\#2233]: https://github.com/hyperledger/aries-cloudagent-python/pull/2233 +[\#2036]: https://github.com/hyperledger/aries-cloudagent-python/issues/2036 +[indy-shared-rs]: https://github.com/hyperledger/indy-shared-rs +[Aries Askar]: https://github.com/hyperledger/aries-askar +[indy-credx Release 3.3]: https://github.com/hyperledger/indy-shared-rs/releases/tag/v0.3.3 + +### Categorized List of Pull Requests + +- Connections Fixes/Updates + - Resolve definitions.py fix to fix backwards compatibility break in plugins [\#2255](https://github.com/hyperledger/aries-cloudagent-python/pull/2255) [usingtechnology](https://github.com/usingtechnology) + - Add support for JsonWebKey2020 for the connection invitations [\#2173](https://github.com/hyperledger/aries-cloudagent-python/pull/2173) [dkulic](https://github.com/dkulic) + - fix: only cache completed connection targets [\#2240](https://github.com/hyperledger/aries-cloudagent-python/pull/2240) [dbluhm](https://github.com/dbluhm) + - Connection target should not be limited only to indy dids [\#2229](https://github.com/hyperledger/aries-cloudagent-python/pull/2229) [dkulic](https://github.com/dkulic) + - Disable webhook trigger on initial response to multi-use connection invitation [\#2223](https://github.com/hyperledger/aries-cloudagent-python/pull/2223) [esune](https://github.com/esune) +- Credential Exchange (Issue, Present) Updates + - Pass document loader to jsonld.expand [\#2175](https://github.com/hyperledger/aries-cloudagent-python/pull/2175) [andrewwhitehead](https://github.com/andrewwhitehead) +- Multi-tenancy fixes/updates + - Allow Configuration Settings on a per-tenant basis [\#2233](https://github.com/hyperledger/aries-cloudagent-python/pull/2233) [shaangill025](https://github.com/shaangill025) + - stand up multiple agents (single and multi) for local development and testing [\#2230](https://github.com/hyperledger/aries-cloudagent-python/pull/2230) [usingtechnology](https://github.com/usingtechnology) + - Multi-tenant self-managed mediation verkey lookup [\#2232](https://github.com/hyperledger/aries-cloudagent-python/pull/2232) [usingtechnology](https://github.com/usingtechnology) + - fix: route multitenant connectionless oob invitation [\#2243](https://github.com/hyperledger/aries-cloudagent-python/pull/2243) [TimoGlastra](https://github.com/TimoGlastra) + - Fix multitenant/mediation in demo [\#2075](https://github.com/hyperledger/aries-cloudagent-python/pull/2075) [ianco](https://github.com/ianco) +- Other Bug and Documentation Fixes + - Assign ~thread.thid with thread_id value [\#2261](https://github.com/hyperledger/aries-cloudagent-python/pull/2261) [usingtechnology](https://github.com/usingtechnology) + - Fix: Do not replace public verkey on mediator [\#2269](https://github.com/hyperledger/aries-cloudagent-python/pull/2269) [mkempa](https://github.com/mkempa) + - Provide an optional Profile to the verification key strategy [\#2265](https://github.com/hyperledger/aries-cloudagent-python/pull/2265) [yvgny](https://github.com/yvgny) + - refactor: Extract verification method ID generation to a separate class [\#2235](https://github.com/hyperledger/aries-cloudagent-python/pull/2235) [yvgny](https://github.com/yvgny) + - Create .readthedocs.yaml file [\#2268](https://github.com/hyperledger/aries-cloudagent-python/pull/2268) [swcurran](https://github.com/swcurran) + - feat(did creation route): reject unregistered did methods [\#2262](https://github.com/hyperledger/aries-cloudagent-python/pull/2262) [chumbert](https://github.com/chumbert) + - ./run_demo performance -c 1 --mediation --timing --trace-log [#2245](https://github.com/hyperledger/aries-cloudagent-python/pull/2245) [usingtechnology](https://github.com/usingtechnology) + - Fix formatting and grammatical errors in different readme's [\#2222](https://github.com/hyperledger/aries-cloudagent-python/pull/2222) [ff137](https://github.com/ff137) + - Fix broken link in README [\#2221](https://github.com/hyperledger/aries-cloudagent-python/pull/2221) [ff137](https://github.com/ff137) + - fix: run only on main, forks ok [\#2166](https://github.com/hyperledger/aries-cloudagent-python/pull/2166) [anwalker293](https://github.com/anwalker293) + - Update Alice Wants a JSON-LD Credential to fix invocation [\#2219](https://github.com/hyperledger/aries-cloudagent-python/pull/2219) [swcurran](https://github.com/swcurran) +- Dependencies and Internal Updates + - Bump requests from 2.30.0 to 2.31.0 in /demo/playground/scripts dependenciesPull requests that update a dependency file [\#2238](https://github.com/hyperledger/aries-cloudagent-python/pull/2238) [dependabot bot](https://github.com/dependabot) + - Upgrade codegen tools in scripts/generate-open-api-spec and publish Swagger 2.0 and OpenAPI 3.0 specs [\#2246](https://github.com/hyperledger/aries-cloudagent-python/pull/2246) [ff137](https://github.com/ff137) +- ACA-Py Administrative Updates + - Propose adding Jason Sherman [usingtechnology](https://github.com/usingtechnology) as a Maintainer [\#2263](https://github.com/hyperledger/aries-cloudagent-python/pull/2263) [swcurran](https://github.com/swcurran) + - Updating Maintainers list to be accurate and using the TOC format [\#2258](https://github.com/hyperledger/aries-cloudagent-python/pull/2258) [swcurran](https://github.com/swcurran) +- Message Tracing/Timing Updates + - Add updated ELK stack for demos. [\#2236](https://github.com/hyperledger/aries-cloudagent-python/pull/2236) [usingtechnology](https://github.com/usingtechnology) +- Release management pull requests + - 0.8.2 [\#2285](https://github.com/hyperledger/aries-cloudagent-python/pull/2285) [swcurran](https://github.com/swcurran) + - 0.8.2-rc2 [\#2284](https://github.com/hyperledger/aries-cloudagent-python/pull/2283) [swcurran](https://github.com/swcurran) + - 0.8.2-rc1 [\#2282](https://github.com/hyperledger/aries-cloudagent-python/pull/2282) [swcurran](https://github.com/swcurran) + - 0.8.2-rc0 [\#2260](https://github.com/hyperledger/aries-cloudagent-python/pull/2260) [swcurran](https://github.com/swcurran) + +# 0.8.1 + +## April 5, 2023 + +Version 0.8.1 is an urgent update to Release 0.8.0 to address an inability to +execute the `upgrade` command. The `upgrade` command is needed for 0.8.0 Pull +Request [\#2116] - "UPGRADE: Fix multi-use invitation performance", which is +useful for (at least) deployments of ACA-Py as a mediator. In the release, the +upgrade process is revamped, and documented in [Upgrading ACA-Py]. + +Key points about upgrading for those with production, pre-0.8.1 ACA-Py deployments: + +- Upgrades now happen **automatically** on startup, when needed. +- The version of the last executed upgrade, even if it is a "no change" upgrade, + is put into secure storage and is used to detect when future upgrades are needed. + - Upgrades are needed when the running version is greater than the version is + secure storage. +- If you have an existing, pre-0.8.1 deployment with many connection records, +there may be a delay in starting as an upgrade will be run that loads and saves +every connection record, updating the data in the record in the process. + - A mechanism is to be added (see [Issue #2201]) for preventing an upgrade + running if it should not be run automatically, and requires using the + `upgrade` command. To date, there has been no need for this feature. +- See the [Upgrading ACA-Py] document for more details. + +### Postgres Support with Aries Askar + +Recent changes to [Aries Askar] have resulted in Askar supporting Postgres +version 11 and greater. If you are on Postgres 10 or earlier and want to upgrade +to use Askar, you must migrate your database to Postgres 10. + +We have also noted that in some container orchestration environments such as +[Red Hat's OpenShift] and possibly other [Kubernetes] distributions, Askar using +[Postgres] versions greater than 14 do not install correctly. Please monitor +[Issue \#2199] for an update to this limitation. We have found that Postgres 15 does +install correctly in other environments (such as in `docker compose` setups). + +[\#2116]: https://github.com/hyperledger/aries-cloudagent-python/issues/2116 +[Upgrading ACA-Py]: ./UpgradingACA-Py.md +[Issue #2201]: https://github.com/hyperledger/aries-cloudagent-python/issues/2201 +[Aries Askar]: https://github.com/hyperledger/aries-askar +[Red Hat's OpenShift]: https://www.openshift.com/products/container-platform/ +[Kubernetes]: https://kubernetes.io/ +[Postgres]: https://www.postgresql.org/ +[Issue \#2199]: https://github.com/hyperledger/aries-cloudagent-python/issues/2199 + +### Categorized List of Pull Requests + +- Fixes for the `upgrade` Command + - Change upgrade definition file entry from 0.8.0 to 0.8.1 [\#2203](https://github.com/hyperledger/aries-cloudagent-python/pull/2203) [swcurran](https://github.com/swcurran) + - Add Upgrading ACA-Py document [\#2200](https://github.com/hyperledger/aries-cloudagent-python/pull/2200) [swcurran](https://github.com/swcurran) + - Fix: Indy WalletAlreadyOpenedError during upgrade process [\#2196](https://github.com/hyperledger/aries-cloudagent-python/pull/2196) [shaangill025](https://github.com/shaangill025) + - Fix: Resolve Upgrade Config file in Container [\#2193](https://github.com/hyperledger/aries-cloudagent-python/pull/2193) [shaangill025](https://github.com/shaangill025) + - Update and automate ACA-Py upgrade process [\#2185](https://github.com/hyperledger/aries-cloudagent-python/pull/2185) [shaangill025](https://github.com/shaangill025) + - Adds the upgrade command YML file to the PyPi Release [\#2179](https://github.com/hyperledger/aries-cloudagent-python/pull/2179) [swcurran](https://github.com/swcurran) +- Test and Documentation + - 3.7 and 3.10 unittests fix [\#2187](https://github.com/hyperledger/aries-cloudagent-python/pull/2187) [Jsyro](https://github.com/Jsyro) + - Doc update and some test scripts [\#2189](https://github.com/hyperledger/aries-cloudagent-python/pull/2189) [ianco](https://github.com/ianco) + - Create UnitTests.md [\#2183](https://github.com/hyperledger/aries-cloudagent-python/pull/2183) [swcurran](https://github.com/swcurran) + - Add link to recorded session about the ACA-Py Integration tests [\#2184](https://github.com/hyperledger/aries-cloudagent-python/pull/2184) [swcurran](https://github.com/swcurran) +- Release management pull requests + - 0.8.1 [\#2207](https://github.com/hyperledger/aries-cloudagent-python/pull/2207) [swcurran](https://github.com/swcurran) + - 0.8.1-rc2 [\#2198](https://github.com/hyperledger/aries-cloudagent-python/pull/2198) [swcurran](https://github.com/swcurran) + - 0.8.1-rc1 [\#2194](https://github.com/hyperledger/aries-cloudagent-python/pull/2194) [swcurran](https://github.com/swcurran) + - 0.8.1-rc0 [\#2190](https://github.com/hyperledger/aries-cloudagent-python/pull/2190) [swcurran](https://github.com/swcurran) + +# 0.8.0 + +## March 14, 2023 + +0.8.0 is a breaking change that contains all updates since release 0.7.5. It +extends the previously tagged `1.0.0-rc1` release because it is not clear when +the 1.0.0 release will be finalized. Many of the PRs in this release were previously +included in the `1.0.0-rc1` release. The categorized list of PRs separates those +that are new from those in the `1.0.0-rc1` release candidate. + +There are not a lot of new Aries Framework features in this release, as the +focus has been on cleanup and optimization. The biggest addition is the +inclusion with ACA-Py of a universal resolver interface, allowing an instance to +have both local resolvers for some DID Methods and a call out to an external +universal resolver for other DID Methods. Another significant new capability is +full support for Hyperledger Indy transaction endorsement for Authors and +Endorsers. A new repo +[aries-endorser-service](https://github.com/hyperledger/aries-endorser-service) +has been created that is a pre-configured instance of ACA-Py for use as an +Endorser service. + +A recently completed feature that is outside of ACA-Py is a script to migrate +existing ACA-Py storage from Indy SDK format to Aries Askar format. This +enables existing deployments to switch to using the newer Aries Askar +components. For details see the converter in the +[aries-acapy-tools](https://github.com/hyperledger/aries-acapy-tools) repository. + +### Container Publishing Updated + +With this release, a new automated process publishes container images in the +Hyperledger container image repository. New images for the release are +automatically published by the GitHubAction Workflows: [publish.yml] and +[publish-indy.yml]. The actions are triggered when a release is tagged, so no +manual action is needed. The images are published in the [Hyperledger Package +Repository under aries-cloudagent-python] and a link to the packages added to +the repositories main page (under "Packages"). Additional information about the +container image publication process can be found in the document [Container +Images and Github Actions]. + +The ACA-Py container images are based on [Python 3.6 and 3.9 `slim-bullseye` +images](https://hub.docker.com/_/python), and are designed to support `linux/386 +(x86)`, `linux/amd64 (x64)`, and `linux/arm64`. However, for this release, the +publication of multi-architecture containers is disabled. We are working to +enable that through the updating of some dependencies that lack that capability. +There are two flavors of image built for each Python version. One contains only +the Indy/Aries Shared Libraries only ([Aries +Askar](https://github.com/hyperledger/aries-askar), [Indy +VDR](https://github.com/hyperledger/indy-vdr) and [Indy Shared +RS](https://github.com/hyperledger/indy-shared-rs), supporting only the use of +`--wallet-type askar`). The other (labelled `indy`) contains the Indy/Aries +shared libraries and the Indy SDK (considered deprecated). For new deployments, +we recommend using the Python 3.9 Shared Library images. For existing +deployments, we recommend migrating to those images. + +Those currently using the container images published by [BC Gov on Docker +Hub](https://hub.docker.com/r/bcgovimages/aries-cloudagent) should change to use +those published to the [Hyperledger Package Repository under +aries-cloudagent-python]. + +[Hyperledger Package Repository under aries-cloudagent-python]: https://github.com/orgs/hyperledger/packages?repo_name=aries-cloudagent-python +[publish.yml]: https://github.com/hyperledger/aries-cloudagent-python/blob/main/.github/workflows/publish.yml +[publish-indy.yml]: https://github.com/hyperledger/aries-cloudagent-python/blob/main/.github/workflows/publish-indy.yml +[Container Images and Github Actions]: https://github.com/hyperledger/aries-cloudagent-python/blob/main/ContainerImagesAndGithubActions.md + +## Breaking Changes and Upgrades + +### PR [\#2034](https://github.com/hyperledger/aries-cloudagent-python/pull/2034) -- Implicit connections + +The break impacts existing deployments that support implicit connections, those +initiated by another agent using a Public DID for this instance instead of an +explicit invitation. Such deployments need to add the configuration parameter +`--requests-through-public-did` to continue to support that feature. The use +case is that an ACA-Py instance publishes a public DID on a ledger with a +DIDComm `service` in the DIDDoc. Other agents resolve that DID, and attempt to +establish a connection with the ACA-Py instance using the `service` endpoint. +This is called an "implicit" connection in [RFC 0023 DID +Exchange](https://github.com/hyperledger/aries-rfcs/blob/main/features/0023-did-exchange/README.md). + +### PR [\#1913](https://github.com/hyperledger/aries-cloudagent-python/pull/1913) -- Unrevealed attributes in presentations + +Updates the handling of "unrevealed attributes" during verification of AnonCreds +presentations, allowing them to be used in a presentation, with additional data +that can be checked if for unrevealed attributes. As few implementations of +Aries wallets support unrevealed attributes in an AnonCreds presentation, this +is unlikely to impact any deployments. + +### PR [\#2145](https://github.com/hyperledger/aries-cloudagent-python/pull/2145) - Update webhook message to terse form by default, added startup flag --debug-webhooks for full form + +The default behavior in ACA-Py has been to keep the full text of all messages in +the protocol state object, and include the full protocol state object in the +webhooks sent to the controller. When the messages include an object that is +very large in all the messages, the webhook may become too big to be passed via +HTTP. For example, issuing a credential with a photo as one of the claims may +result in a number of copies of the photo in the protocol state object and +hence, very large webhooks. This change reduces the size of the webhook message +by eliminating redundant data in the protocol state of the "Issue Credential" +message as the default, and adds a new parameter to use the old behavior. + +### UPGRADE PR [\#2116](https://github.com/hyperledger/aries-cloudagent-python/pull/2116) - UPGRADE: Fix multi-use invitation performance + +The way that multiuse invitations in previous versions of ACA-Py caused +performance to degrade over time. An update was made to add state into the tag +names that eliminated the need to scan the tags when querying storage for the +invitation. + +If you are using multiuse invitations in your existing (pre-`0.8.0` deployment +of ACA-Py, you can run an `upgrade` to apply this change. To run upgrade from +previous versions, use the following command using the `0.8.0` version of +ACA-Py, adding you wallet settings: + +`aca-py upgrade --from-version=v0.7.5 --upgrade-config-path ./upgrade.yml` + +### Categorized List of Pull Requests + +- Verifiable credential, presentation and revocation handling updates + - **BREAKING:** Update webhook message to terse form [default, added startup flag --debug-webhooks for full form [\#2145](https://github.com/hyperledger/aries-cloudagent-python/pull/2145) by [victorlee0505](victorlee0505) + - Add startup flag --light-weight-webhook to trim down outbound webhook payload [\#1941](https://github.com/hyperledger/aries-cloudagent-python/pull/1941) [victorlee0505](https://github.com/victorlee0505) + - feat: add verification method issue-credentials-2.0/send endpoint [\#2135](https://github.com/hyperledger/aries-cloudagent-python/pull/2135) [chumbert](https://github.com/chumbert) + - Respect auto-verify-presentation flag in present proof v1 and v2 [\#2097](https://github.com/hyperledger/aries-cloudagent-python/pull/2097) [dbluhm](https://github.com/dbluhm) + - Feature: enabled handling VPs (request, creation, verification) with different VCs [\#1956](https://github.com/hyperledger/aries-cloudagent-python/pull/1956) ([teanas](https://github.com/teanas)) + - fix: update issue-credential endpoint summaries [\#1997](https://github.com/hyperledger/aries-cloudagent-python/pull/1997) ([PeterStrob](https://github.com/PeterStrob)) + - fix claim format designation in presentation submission [\#2013](https://github.com/hyperledger/aries-cloudagent-python/pull/2013) ([rmnre](https://github.com/rmnre)) + - \#2041 - Issue JSON-LD has invalid Admin API documentation [\#2046](https://github.com/hyperledger/aries-cloudagent-python/pull/2046) ([jfblier-amplitude](https://github.com/jfblier-amplitude)) + - Previously flagged in release 1.0.0-rc1 + - Refactor ledger correction code and insert into revocation error handling [\#1892](https://github.com/hyperledger/aries-cloudagent-python/pull/1892) ([ianco](https://github.com/ianco)) + - Indy ledger fixes and cleanups [\#1870](https://github.com/hyperledger/aries-cloudagent-python/pull/1870) ([andrewwhitehead](https://github.com/andrewwhitehead)) + - Refactoring of revocation registry creation [\#1813](https://github.com/hyperledger/aries-cloudagent-python/pull/1813) ([andrewwhitehead](https://github.com/andrewwhitehead)) + - Fix: the type of tails file path to string. [\#1925](https://github.com/hyperledger/aries-cloudagent-python/pull/1925) ([baegjae](https://github.com/baegjae)) + - Pre-populate revoc\_reg\_id on IssuerRevRegRecord [\#1924](https://github.com/hyperledger/aries-cloudagent-python/pull/1924) ([andrewwhitehead](https://github.com/andrewwhitehead)) + - Leave credentialStatus element in the LD credential [\#1921](https://github.com/hyperledger/aries-cloudagent-python/pull/1921) ([tsabolov](https://github.com/tsabolov)) + - **BREAKING:** Remove aca-py check for unrevealed revealed attrs on proof validation [\#1913](https://github.com/hyperledger/aries-cloudagent-python/pull/1913) ([ianco](https://github.com/ianco)) + - Send webhooks upon record/credential deletion [\#1906](https://github.com/hyperledger/aries-cloudagent-python/pull/1906) ([frostyfrog](https://github.com/frostyfrog)) + +- Out of Band (OOB) and DID Exchange / Connection Handling / Mediator + - UPGRADE: Fix multi-use invitation performance [\#2116](https://github.com/hyperledger/aries-cloudagent-python/pull/2116) [reflectivedevelopment](https://github.com/reflectivedevelopment) + - fix: public did mediator routing keys as did keys [\#1977](https://github.com/hyperledger/aries-cloudagent-python/pull/1977) ([dbluhm](https://github.com/dbluhm)) + - Fix for mediator load testing race condition when scaling horizontally [\#2009](https://github.com/hyperledger/aries-cloudagent-python/pull/2009) ([ianco](https://github.com/ianco)) + - **BREAKING:** Allow multi-use public invites and public invites with metadata [\#2034](https://github.com/hyperledger/aries-cloudagent-python/pull/2034) ([mepeltier](https://github.com/mepeltier)) + - Do not reject OOB invitation with unknown handshake protocol\(s\) [\#2060](https://github.com/hyperledger/aries-cloudagent-python/pull/2060) ([andrewwhitehead](https://github.com/andrewwhitehead)) + - fix: fix connection timing bug [\#2099](https://github.com/hyperledger/aries-cloudagent-python/pull/2099) ([reflectivedevelopment](https://github.com/reflectivedevelopment)) + - Previously flagged in release 1.0.0-rc1 + - Fix: `--mediator-invitation` with OOB invitation + cleanup [\#1970](https://github.com/hyperledger/aries-cloudagent-python/pull/1970) ([shaangill025](https://github.com/shaangill025)) + - include image\_url in oob invitation [\#1966](https://github.com/hyperledger/aries-cloudagent-python/pull/1966) ([Zzocker](https://github.com/Zzocker)) + - feat: 00B v1.1 support [\#1962](https://github.com/hyperledger/aries-cloudagent-python/pull/1962) ([shaangill025](https://github.com/shaangill025)) + - Fix: OOB - Handling of minor versions [\#1940](https://github.com/hyperledger/aries-cloudagent-python/pull/1940) ([shaangill025](https://github.com/shaangill025)) + - fix: failed connectionless proof request on some case [\#1933](https://github.com/hyperledger/aries-cloudagent-python/pull/1933) ([kukgini](https://github.com/kukgini)) + - fix: propagate endpoint from mediation record [\#1922](https://github.com/hyperledger/aries-cloudagent-python/pull/1922) ([cjhowland](https://github.com/cjhowland)) + - Feat/public did endpoints for agents behind mediators [\#1899](https://github.com/hyperledger/aries-cloudagent-python/pull/1899) ([cjhowland](https://github.com/cjhowland)) + +- DID Registration and Resolution related updates + - feat: allow marking non-SOV DIDs as public [\#2144](https://github.com/hyperledger/aries-cloudagent-python/pull/2144) [chumbert](https://github.com/chumbert) + - fix: askar exception message always displaying null DID [\#2155](https://github.com/hyperledger/aries-cloudagent-python/pull/2155) [chumbert](https://github.com/chumbert) + - feat: enable creation of DIDs for all registered methods [\#2067](https://github.com/hyperledger/aries-cloudagent-python/pull/2067) ([chumbert](https://github.com/chumbert)) + - fix: create local DID return schema [\#2086](https://github.com/hyperledger/aries-cloudagent-python/pull/2086) ([chumbert](https://github.com/chumbert)) + - feat: universal resolver - configurable authentication [\#2095](https://github.com/hyperledger/aries-cloudagent-python/pull/2095) ([chumbert](https://github.com/chumbert)) + - Previously flagged in release 1.0.0-rc1 + - feat: add universal resolver [\#1866](https://github.com/hyperledger/aries-cloudagent-python/pull/1866) ([dbluhm](https://github.com/dbluhm)) + - fix: resolve dids following new endpoint rules [\#1863](https://github.com/hyperledger/aries-cloudagent-python/pull/1863) ([dbluhm](https://github.com/dbluhm)) + - fix: didx request cannot be accepted [\#1881](https://github.com/hyperledger/aries-cloudagent-python/pull/1881) ([rmnre](https://github.com/rmnre)) + - did method & key type registry [\#1986](https://github.com/hyperledger/aries-cloudagent-python/pull/1986) ([burdettadam](https://github.com/burdettadam)) + - Fix/endpoint attrib structure [\#1934](https://github.com/hyperledger/aries-cloudagent-python/pull/1934) ([cjhowland](https://github.com/cjhowland)) + - Simple did registry [\#1920](https://github.com/hyperledger/aries-cloudagent-python/pull/1920) ([burdettadam](https://github.com/burdettadam)) + - Use did:key for recipient keys [\#1886](https://github.com/hyperledger/aries-cloudagent-python/pull/1886) ([frostyfrog](https://github.com/frostyfrog)) + +- Hyperledger Indy Endorser/Author Transaction Handling + - Update some of the demo Readme and Endorser instructions [\#2122](https://github.com/hyperledger/aries-cloudagent-python/pull/2122) [swcurran](https://github.com/swcurran) + - Special handling for the write ledger [\#2030](https://github.com/hyperledger/aries-cloudagent-python/pull/2030) ([ianco](https://github.com/ianco)) + - Previously flagged in release 1.0.0-rc1 + - Fix/txn job setting [\#1994](https://github.com/hyperledger/aries-cloudagent-python/pull/1994) ([ianco](https://github.com/ianco)) + - chore: fix ACAPY\_PROMOTE-AUTHOR-DID flag [\#1978](https://github.com/hyperledger/aries-cloudagent-python/pull/1978) ([morrieinmaas](https://github.com/morrieinmaas)) + - Endorser write DID transaction [\#1938](https://github.com/hyperledger/aries-cloudagent-python/pull/1938) ([ianco](https://github.com/ianco)) + - Endorser doc updates and some bug fixes [\#1926](https://github.com/hyperledger/aries-cloudagent-python/pull/1926) ([ianco](https://github.com/ianco)) + +- Admin API Additions + - fix: response type on delete-tails-files endpoint [\#2133](https://github.com/hyperledger/aries-cloudagent-python/pull/2133) [chumbert](https://github.com/chumbert) + - OpenAPI validation fixes [\#2127](https://github.com/hyperledger/aries-cloudagent-python/pull/2127) [loneil](https://github.com/loneil) + - Delete tail files [\#2103](https://github.com/hyperledger/aries-cloudagent-python/pull/2103) [ramreddychalla94](https://github.com/ramreddychalla94) + +- Startup Command Line / Environment / YAML Parameter Updates + - Update webhook message to terse form [default, added startup flag --debug-webhooks for full form [\#2145](https://github.com/hyperledger/aries-cloudagent-python/pull/2145) by [victorlee0505](victorlee0505) + - Add startup flag --light-weight-webhook to trim down outbound webhook payload [\#1941](https://github.com/hyperledger/aries-cloudagent-python/pull/1941) [victorlee0505](https://github.com/victorlee0505) + - Add missing --mediator-connections-invite cmd arg info to docs [\#2051](https://github.com/hyperledger/aries-cloudagent-python/pull/2051) ([matrixik](https://github.com/matrixik)) + - Issue \#2068 boolean flag change to support HEAD requests to default route [\#2077](https://github.com/hyperledger/aries-cloudagent-python/pull/2077) ([johnekent](https://github.com/johnekent)) + - Previously flagged in release 1.0.0-rc1 + - Add seed command line parameter but use only if also an "allow insecure seed" parameter is set [\#1714](https://github.com/hyperledger/aries-cloudagent-python/pull/1714) ([DaevMithran](https://github.com/DaevMithran)) + +- Internal Aries framework data handling updates + - fix: resolver api schema inconsistency [\#2112](https://github.com/hyperledger/aries-cloudagent-python/pull/2112) ([TimoGlastra](https://github.com/chumbert)) + - fix: return if return route but no response [\#1853](https://github.com/hyperledger/aries-cloudagent-python/pull/1853) ([TimoGlastra](https://github.com/TimoGlastra)) + - Multi-ledger/Multi-tenant issues [\#2022](https://github.com/hyperledger/aries-cloudagent-python/pull/2022) ([ianco](https://github.com/ianco)) + - fix: Correct typo in model -- required spelled incorrectly [\#2031](https://github.com/hyperledger/aries-cloudagent-python/pull/2031) ([swcurran](https://github.com/swcurran)) + - Code formatting [\#2053](https://github.com/hyperledger/aries-cloudagent-python/pull/2053) ([ianco](https://github.com/ianco)) + - Improved validation of record state attributes [\#2071](https://github.com/hyperledger/aries-cloudagent-python/pull/2071) ([rmnre](https://github.com/rmnre)) + - Previously flagged in release 1.0.0-rc1 + - fix: update RouteManager methods use to pass profile as parameter [\#1902](https://github.com/hyperledger/aries-cloudagent-python/pull/1902) ([chumbert](https://github.com/chumbert)) + - Allow fully qualified class names for profile managers [\#1880](https://github.com/hyperledger/aries-cloudagent-python/pull/1880) ([chumbert](https://github.com/chumbert)) + - fix: unable to use askar with in memory db [\#1878](https://github.com/hyperledger/aries-cloudagent-python/pull/1878) ([dbluhm](https://github.com/dbluhm)) + - Enable manually triggering keylist updates during connection [\#1851](https://github.com/hyperledger/aries-cloudagent-python/pull/1851) ([dbluhm](https://github.com/dbluhm)) + - feat: make base wallet route access configurable [\#1836](https://github.com/hyperledger/aries-cloudagent-python/pull/1836) ([dbluhm](https://github.com/dbluhm)) + - feat: event and webhook on keylist update stored [\#1769](https://github.com/hyperledger/aries-cloudagent-python/pull/1769) ([dbluhm](https://github.com/dbluhm)) + - fix: Safely shutdown when root\_profile uninitialized [\#1960](https://github.com/hyperledger/aries-cloudagent-python/pull/1960) ([frostyfrog](https://github.com/frostyfrog)) + - feat: include connection ids in keylist update webhook [\#1914](https://github.com/hyperledger/aries-cloudagent-python/pull/1914) ([dbluhm](https://github.com/dbluhm)) + - fix: incorrect response schema for discover features [\#1912](https://github.com/hyperledger/aries-cloudagent-python/pull/1912) ([dbluhm](https://github.com/dbluhm)) + - Fix: SchemasInputDescriptorFilter: broken deserialization renders generated clients unusable [\#1894](https://github.com/hyperledger/aries-cloudagent-python/pull/1894) ([rmnre](https://github.com/rmnre)) + - fix: schema class can set Meta.unknown [\#1885](https://github.com/hyperledger/aries-cloudagent-python/pull/1885) ([dbluhm](https://github.com/dbluhm)) + +- Unit, Integration, and Aries Agent Test Harness Test updates + - Additional integration tests for revocation scenarios [\#2055](https://github.com/hyperledger/aries-cloudagent-python/pull/2055) ([ianco](https://github.com/ianco)) + - Previously flagged in release 1.0.0-rc1 + - Fixes a few AATH failures [\#1897](https://github.com/hyperledger/aries-cloudagent-python/pull/1897) ([ianco](https://github.com/ianco)) + - fix: warnings in tests from IndySdkProfile [\#1865](https://github.com/hyperledger/aries-cloudagent-python/pull/1865) ([dbluhm](https://github.com/dbluhm)) + - Unit test fixes for python 3.9 [\#1858](https://github.com/hyperledger/aries-cloudagent-python/pull/1858) ([andrewwhitehead](https://github.com/andrewwhitehead)) + - Update pip-audit.yml [\#1945](https://github.com/hyperledger/aries-cloudagent-python/pull/1945) ([ryjones](https://github.com/ryjones)) + - Update pip-audit.yml [\#1944](https://github.com/hyperledger/aries-cloudagent-python/pull/1944) ([ryjones](https://github.com/ryjones)) + +- Dependency, Python version, GitHub Actions and Container Image Changes + - Remove CircleCI Status since we aren't using CircleCI anymore [\#2163](https://github.com/hyperledger/aries-cloudagent-python/pull/2163) [swcurran](https://github.com/swcurran) + - Update ACA-Py docker files to produce OpenShift compatible images [\#2130](https://github.com/hyperledger/aries-cloudagent-python/pull/2130) [WadeBarnes](https://github.com/WadeBarnes) + - Temporarily disable multi-architecture image builds [\#2125](https://github.com/hyperledger/aries-cloudagent-python/pull/2125) [WadeBarnes](https://github.com/WadeBarnes) + - Fix ACA-py image builds [\#2123](https://github.com/hyperledger/aries-cloudagent-python/pull/2123) [WadeBarnes](https://github.com/WadeBarnes) + - Fix publish workflows [\#2117](https://github.com/hyperledger/aries-cloudagent-python/pull/2117) [WadeBarnes](https://github.com/WadeBarnes) + - fix: indy dependency version format [\#2054](https://github.com/hyperledger/aries-cloudagent-python/pull/2054) ([chumbert](https://github.com/chumbert)) + - ci: add gha for pr-tests [\#2058](https://github.com/hyperledger/aries-cloudagent-python/pull/2058) ([dbluhm](https://github.com/dbluhm)) + - ci: test additional versions of python nightly [\#2059](https://github.com/hyperledger/aries-cloudagent-python/pull/2059) ([dbluhm](https://github.com/dbluhm)) + - Update github actions dependencies \(for node16 support\) [\#2066](https://github.com/hyperledger/aries-cloudagent-python/pull/2066) ([andrewwhitehead](https://github.com/andrewwhitehead)) + - Docker images and GHA for publishing images [\#2076](https://github.com/hyperledger/aries-cloudagent-python/pull/2076) ([dbluhm](https://github.com/dbluhm)) + - Update dockerfiles to use python 3.9 [\#2109](https://github.com/hyperledger/aries-cloudagent-python/pull/2109) ([ianco](https://github.com/ianco)) + - Updating base images from slim-buster to slim-bullseye [\#2105](https://github.com/hyperledger/aries-cloudagent-python/pull/2105) ([pradeepp88](https://github.com/pradeepp88)) + - Previously flagged in release 1.0.0-rc1 + - feat: update pynacl version from 1.4.0 to 1.50 [\#1981](https://github.com/hyperledger/aries-cloudagent-python/pull/1981) ([morrieinmaas](https://github.com/morrieinmaas)) + - Fix: web.py dependency - integration tests & demos [\#1973](https://github.com/hyperledger/aries-cloudagent-python/pull/1973) ([shaangill025](https://github.com/shaangill025)) + - chore: update pydid [\#1915](https://github.com/hyperledger/aries-cloudagent-python/pull/1915) ([dbluhm](https://github.com/dbluhm)) + +- Demo and Documentation Updates + - [fix] Removes extra comma that prevents swagger from accepting the presentation request [\#2149](https://github.com/hyperledger/aries-cloudagent-python/pull/2149) [swcurran](https://github.com/swcurran) + - Initial plugin docs [\#2138](https://github.com/hyperledger/aries-cloudagent-python/pull/2138) [ianco](https://github.com/ianco) + - Acme workshop [\#2137](https://github.com/hyperledger/aries-cloudagent-python/pull/2137) [ianco](https://github.com/ianco) + - Fix: Performance Demo [no --revocation] [\#2151](https://github.com/ hyperledger/aries-cloudagent-python/pull/2151) [shaangill025](https://github.com/shaangill025) + - Fix typos in alice-local.sh & faber-local.sh [\#2010](https://github.com/hyperledger/aries-cloudagent-python/pull/2010) ([naonishijima](https://github.com/naonishijima)) + - Added a bit about manually creating a revoc reg tails file [\#2012](https://github.com/hyperledger/aries-cloudagent-python/pull/2012) ([ianco](https://github.com/ianco)) + - Add ability to set docker container name [\#2024](https://github.com/hyperledger/aries-cloudagent-python/pull/2024) ([matrixik](https://github.com/matrixik)) + - Doc updates for json demo [\#2026](https://github.com/hyperledger/aries-cloudagent-python/pull/2026) ([ianco](https://github.com/ianco)) + - Multitenancy demo \(docker-compose with postgres and ngrok\) [\#2089](https://github.com/hyperledger/aries-cloudagent-python/pull/2089) ([ianco](https://github.com/ianco)) + - Allow using YAML configuration file with run\_docker [\#2091](https://github.com/hyperledger/aries-cloudagent-python/pull/2091) ([matrixik](https://github.com/matrixik)) + - Previously flagged in release 1.0.0-rc1 + - Fixes to acme exercise code [\#1990](https://github.com/hyperledger/aries-cloudagent-python/pull/1990) ([ianco](https://github.com/ianco)) + - Fixed bug in run\_demo script [\#1982](https://github.com/hyperledger/aries-cloudagent-python/pull/1982) ([pasquale95](https://github.com/pasquale95)) + - Transaction Author with Endorser demo [\#1975](https://github.com/hyperledger/aries-cloudagent-python/pull/1975) ([ianco](https://github.com/ianco)) + - Redis Plugins \[redis\_cache & redis\_queue\] related updates [\#1937](https://github.com/hyperledger/aries-cloudagent-python/pull/1937) ([shaangill025](https://github.com/shaangill025)) + +- Release management pull requests + - 0.8.0 release [\#2169](https://github.com/hyperledger/aries-cloudagent-python/pull/2169) ([swcurran](https://github.com/swcurran)) + - 0.8.0-rc0 release updates [\#2115](https://github.com/hyperledger/aries-cloudagent-python/pull/2115) ([swcurran](https://github.com/swcurran)) + - Previously flagged in release 1.0.0-rc1 + - Release 1.0.0-rc0 [\#1904](https://github.com/hyperledger/aries-cloudagent-python/pull/1904) ([swcurran](https://github.com/swcurran)) + - Add 0.7.5 patch Changelog entry to main branch Changelog [\#1996](https://github.com/hyperledger/aries-cloudagent-python/pull/1996) ([swcurran](https://github.com/swcurran)) + - Release 1.0.0-rc1 [\#2005](https://github.com/hyperledger/aries-cloudagent-python/pull/2005) ([swcurran](https://github.com/swcurran)) + +# 0.7.5 + +## October 26, 2022 + +0.7.5 is a patch release to deal primarily to add [PR #1881 DID Exchange in +ACA-Py 0.7.4 with explicit invitations and without auto-accept +broken](https://github.com/hyperledger/aries-cloudagent-python/pull/1881). A +couple of other PRs were added to the release, as listed below, and in +[Milestone 0.7.5](https://github.com/hyperledger/aries-cloudagent-python/milestone/6). + +### List of Pull Requests + +- Changelog and version updates for version 0.7.5-rc1 [\#1985](https://github.com/hyperledger/aries-cloudagent-python/pull/1985) ([swcurran](https://github.com/swcurran)) +- Endorser doc updates and some bug fixes [\#1926](https://github.com/hyperledger/aries-cloudagent-python/pull/1926) ([ianco](https://github.com/ianco)) +- Fix: web.py dependency - integration tests & demos [\#1973](https://github.com/hyperledger/aries-cloudagent-python/pull/1973) ([shaangill025](https://github.com/shaangill025)) +- Endorser write DID transaction [\#1938](https://github.com/hyperledger/aries-cloudagent-python/pull/1938) ([ianco](https://github.com/ianco)) +- fix: didx request cannot be accepted [\#1881](https://github.com/hyperledger/aries-cloudagent-python/pull/1881) ([rmnre](https://github.com/rmnre)) +- Fix: OOB - Handling of minor versions [\#1940](https://github.com/hyperledger/aries-cloudagent-python/pull/1940) ([shaangill025](https://github.com/shaangill025)) +- fix: Safely shutdown when root_profile uninitialized [\#1960](https://github.com/hyperledger/aries-cloudagent-python/pull/1960) ([frostyfrog](https://github.com/frostyfrog)) +- feat: 00B v1.1 support [\#1962](https://github.com/hyperledger/aries-cloudagent-python/pull/1962) ([shaangill025](https://github.com/shaangill025)) +- 0.7.5 Cherry Picks [\#1967](https://github.com/hyperledger/aries-cloudagent-python/pull/1967) ([frostyfrog](https://github.com/frostyfrog)) +- Changelog and version updates for version 0.7.5-rc0 [\#1969](https://github.com/hyperledger/aries-cloudagent-python/pull/1969) ([swcurran](https://github.com/swcurran)) +- Final 0.7.5 changes [\#1991](https://github.com/hyperledger/aries-cloudagent-python/pull/1991) ([swcurran](https://github.com/swcurran)) + +# 0.7.4 + +## June 30, 2022 + +> :warning: **Existing multitenant JWTs invalidated when a new JWT is +generated**: If you have a pre-existing implementation with existing Admin API +authorization JWTs, invoking the endpoint to get a JWT now invalidates the +existing JWT. Previously an identical JWT would be created. Please see this +[comment on PR \#1725](https://github.com/hyperledger/aries-cloudagent-python/pull/1725#issuecomment-1096172144) +for more details. + +0.7.4 is a significant release focused on stability and production deployments. +As the "patch" release number indicates, there were no breaking changes in the +Admin API, but a huge volume of updates and improvements. Highlights of this +release include: + +- A major performance and stability improvement resulting from the now +recommended use of [Aries Askar](https://github.com/bcgov/aries-askar) instead +of the Indy-SDK. +- There are significant improvements and tools for dealing with +revocation-related issues. +- A lot of work has been on the handling of Hyperledger Indy transaction +endorsements. +- ACA-Py now has a pluggable persistent queues mechanism in place, with Redis +and Kafka support available (albeit with work still to come on documentation). + +In addition, there are a significant number of general enhancements, bug fixes, +documentation updates and code management improvements. + +This release is a reflection of the many groups stressing ACA-Py in production +environments, reporting issues and the resulting solutions. We also have a very +large number of contributors to ACA-Py, with this release having PRs from 22 +different individuals. A big thank you to all of those using ACA-Py, raising +issues and providing solutions. + +### Major Enhancements + +A lot of work has been put into this release related to performance and load +testing, with significant updates being made to the key "shared component" +ACA-Py dependencies ([Aries Askar](https://github.com/bcgov/aries-askar), [Indy +VDR](https://github.comyperledger/indy-vdr)) and [Indy Shared RS (including +CredX)](https://github.com/hyperledger/indy-shared-rs). We now recommend using +those components (by using `--wallet-type askar` in the ACA-Py startup +parameters) for new ACA-Py deployments. A wallet migration tool from indy-sdk +storage to Askar storage is still needed before migrating existing deployment to +Askar. A big thanks to those creating/reporting on stress test scenarios, and +especially the team at LISSI for creating the +[aries-cloudagent-loadgenerator](https://github.com/lissi-id/aries-cloudagent-loadgenerator) +to make load testing so easy! And of course to the core ACA-Py team for +addressing the findings. + +The largest enhancement is in the area of the endorsing of Hyperledger Indy +ledger transactions, enabling an instance of ACA-Py to act as an Endorser for +Indy authors needing endorsements to write objects to an Indy ledger. We're +working on an [Aries Endorser +Service](https://github.com/bcgov/aries-endorser-service) based on the new +capabilities in ACA-Py, an Endorser to be easily operated by an organization, +ideally with a controller starter kit supporting a basic human and automated +approvals business workflow. Contributions welcome! + +A focus towards the end of the 0.7.4 development and release cycle was on the +handling of AnonCreds revocation in ACA-Py. Most important, a production issue +was uncovered where by an ACA-Py issuer's local Revocation Registry data could +get out of sync with what was published on an Indy ledger, resulting in an +inability to publish new RevRegEntry transactions -- making new revocations +impossible. As a result, we have added some new endpoints to enable an update to +the RevReg storage such that RevRegEntry transactions can again be published to +the ledger. Other changes were added related to revocation in general +and in the handling of tails files in particular. + +The team has worked a lot on evolving the persistent queue (PQ) approach +available in ACA-Py. We have landed on a design for the queues for inbound and +outbound messages using a default in-memory implementation, and the ability to +replace the default method with implementations created via an ACA-Py plugin. +There are two concrete, out-of-the-box external persistent queuing solutions +available for [Redis](https://github.com/bcgov/aries-acapy-plugin-redis-events) +and [Kafka](https://github.com/sicpa-dlab/aries-acapy-plugin-kafka-events). +Those ACA-Py persistent queue implementation repositories will soon be migrated +to the Aries project within the Hyperledger Foundation's GitHub organization. +Anyone else can implement their own queuing plugin as long as it uses the same +interface. + +Several new ways to control ACA-Py configurations were added, including new +startup parameters, Admin API parameters to control instances of protocols, and +additional web hook notifications. + +A number of fixes were made to the Credential Exchange protocols, both for V1 +and V2, and for both AnonCreds and W3C format VCs. Nothing new was added and +there no changes in the APIs. + +As well there were a number of internal fixes, dependency updates, documentation +and demo changes, developer tools and release management updates. All the usual +stuff needed for a healthy, growing codebase. + +### Categorized List of Pull Requests + +- Hyperledger Indy Endorser related updates: + - Fix order of operations connecting faber to endorser [\#1716](https://github.com/hyperledger/aries-cloudagent-python/pull/1716) ([ianco](https://github.com/ianco)) + - Endorser support for updating DID endpoints on ledger [\#1696](https://github.com/hyperledger/aries-cloudagent-python/pull/1696) ([frostyfrog](https://github.com/frostyfrog)) + - Add "sent" key to both Schema and Cred Defs when using Endorsers [\#1663](https://github.com/hyperledger/aries-cloudagent-python/pull/1663) ([frostyfrog](https://github.com/frostyfrog)) + - Add cred_def_id to metadata when using an Endorser [\#1655](https://github.com/hyperledger/aries-cloudagent-python/pull/1655) ([frostyfrog](https://github.com/frostyfrog)) + - Update Endorser documentation [\#1646](https://github.com/hyperledger/aries-cloudagent-python/pull/1646) ([chumbert](https://github.com/chumbert)) + - Auto-promote author did to public after endorsing [\#1607](https://github.com/hyperledger/aries-cloudagent-python/pull/1607) ([ianco](https://github.com/ianco)) + - DID updates for endorser [\#1601](https://github.com/hyperledger/aries-cloudagent-python/pull/1601) ([ianco](https://github.com/ianco)) + - Qualify did exch connection lookup by role [\#1670](https://github.com/hyperledger/aries-cloudagent-python/pull/1670) ([ianco](https://github.com/ianco)) + - Use provided connection_id if provided [\#1726](https://github.com/hyperledger/aries-cloudagent-python/pull/1726) ([ianco](https://github.com/ianco)) + +- Additions to the startup parameters, Admin API and Web Hooks + - Improve typing of settings and add plugin settings object [\#1833](https://github.com/hyperledger/aries-cloudagent-python/pull/1833) ([dbluhm](https://github.com/dbluhm)) + - feat: accept taa using startup parameter --accept-taa [\#1643](https://github.com/hyperledger/aries-cloudagent-python/pull/1643) ([TimoGlastra](https://github.com/TimoGlastra)) + - Add auto_verify flag in present-proof protocol [\#1702](https://github.com/hyperledger/aries-cloudagent-python/pull/1702) ([DaevMithran](https://github.com/DaevMithran)) + - feat: query connections by their_public_did [\#1637](https://github.com/hyperledger/aries-cloudagent-python/pull/1637) ([TimoGlastra](https://github.com/TimoGlastra)) + - feat: enable webhook events for mediation records [\#1614](https://github.com/hyperledger/aries-cloudagent-python/pull/1614) ([TimoGlastra](https://github.com/TimoGlastra)) + - Feature/undelivered events [\#1694](https://github.com/hyperledger/aries-cloudagent-python/pull/1694) ([mepeltier](https://github.com/mepeltier)) + - Allow use of SEED when creating local wallet DID Issue-1682 Issue-1682 [\#1705](https://github.com/hyperledger/aries-cloudagent-python/pull/1705) ([DaevMithran](https://github.com/DaevMithran)) + - Feature: Add the ability to deny specific plugins from loading [\#1737](https://github.com/hyperledger/aries-cloudagent-python/pull/1737) ([frostyfrog](https://github.com/frostyfrog)) + - feat: Add filter param to connection list for invitations [\#1797](https://github.com/hyperledger/aries-cloudagent-python/pull/1797) ([frostyfrog](https://github.com/frostyfrog)) + - Fix missing webhook handler [\#1816](https://github.com/hyperledger/aries-cloudagent-python/pull/1816) ([ianco](https://github.com/ianco)) + +- Persistent Queues + - Redis PQ Cleanup in preparation for enabling the uses of plugin PQ implementations \[Issue\#1659\] [\#1659](https://github.com/hyperledger/aries-cloudagent-python/pull/1690) ([shaangill025](https://github.com/shaangill025)) + +- Credential Revocation and Tails File Handling + - Fix handling of non-revocable credential when timestamp is specified \(askar/credx\) [\#1847](https://github.com/hyperledger/aries-cloudagent-python/pull/1847) ([andrewwhitehead](https://github.com/andrewwhitehead)) + - Additional endpoints to get revocation details and fix "published" status [\#1783](https://github.com/hyperledger/aries-cloudagent-python/pull/1783) ([ianco](https://github.com/ianco)) + - Fix IssuerCredRevRecord state update on revocation publish [\#1827](https://github.com/hyperledger/aries-cloudagent-python/pull/1827) ([andrewwhitehead](https://github.com/andrewwhitehead)) + - Fix put_file when the server returns a redirect [\#1808](https://github.com/hyperledger/aries-cloudagent-python/pull/1808) ([andrewwhitehead](https://github.com/andrewwhitehead)) + - Adjust revocation registry update procedure to shorten transactions [\#1804](https://github.com/hyperledger/aries-cloudagent-python/pull/1804) ([andrewwhitehead](https://github.com/andrewwhitehead)) + - fix: Resolve Revocation Notification environment variable name collision [\#1751](https://github.com/hyperledger/aries-cloudagent-python/pull/1751) ([frostyfrog](https://github.com/frostyfrog)) + - fix: always notify if revocation notification record exists [\#1665](https://github.com/hyperledger/aries-cloudagent-python/pull/1665) ([TimoGlastra](https://github.com/TimoGlastra)) + - Fix for AnonCreds non-revoc proof with no timestamp [\#1628](https://github.com/hyperledger/aries-cloudagent-python/pull/1628) ([ianco](https://github.com/ianco)) + - Fixes for v7.3.0 - Issue [\#1597](https://github.com/hyperledger/aries-cloudagent-python/issues/1597) [\#1711](https://github.com/hyperledger/aries-cloudagent-python/pull/1711) ([shaangill025](https://github.com/shaangill025)) + - Fixes Issue 1 from [\#1597](https://github.com/hyperledger/aries-cloudagent-python/issues/1597): Tails file upload fails when a credDef is created and multi ledger support is enabled + - Fix tails server upload multi-ledger mode [\#1785](https://github.com/hyperledger/aries-cloudagent-python/pull/1785) ([ianco](https://github.com/ianco)) + - Feat/revocation notification v2 [\#1734](https://github.com/hyperledger/aries-cloudagent-python/pull/1734) ([frostyfrog](https://github.com/frostyfrog)) + +- Issue Credential, Present Proof updates/fixes + - Fix: Present Proof v2 - check_proof_vs_proposal update to support proof request with restrictions [\#1820](https://github.com/hyperledger/aries-cloudagent-python/pull/1820) ([shaangill025](https://github.com/shaangill025)) + - Fix: present-proof v1 send-proposal flow [\#1811](https://github.com/hyperledger/aries-cloudagent-python/pull/1811) ([shaangill025](https://github.com/shaangill025)) + - Prover - verification outcome from presentation ack message [\#1757](https://github.com/hyperledger/aries-cloudagent-python/pull/1757) ([shaangill025](https://github.com/shaangill025)) + - feat: support connectionless exchange [\#1710](https://github.com/hyperledger/aries-cloudagent-python/pull/1710) ([TimoGlastra](https://github.com/TimoGlastra)) + - Fix: DIF proof proposal when creating bound presentation request \[Issue\#1687\] [\#1690](https://github.com/hyperledger/aries-cloudagent-python/pull/1690) ([shaangill025](https://github.com/shaangill025)) + - Fix DIF PresExch and OOB request_attach delete unused connection [\#1676](https://github.com/hyperledger/aries-cloudagent-python/pull/1676) ([shaangill025](https://github.com/shaangill025)) + - Fix DIFPresFormatHandler returning invalid V20PresExRecord on presentation verification [\#1645](https://github.com/hyperledger/aries-cloudagent-python/pull/1645) ([rmnre](https://github.com/rmnre)) + - Update aries-askar patch version to at least 0.2.4 as 0.2.3 does not include backward compatibility [\#1603](https://github.com/hyperledger/aries-cloudagent-python/pull/1603) ([acuderman](https://github.com/acuderman)) + - Fixes for credential details in issue-credential webhook responses [\#1668](https://github.com/hyperledger/aries-cloudagent-python/pull/1668) ([andrewwhitehead](https://github.com/andrewwhitehead)) + - Fix: present-proof v2 send-proposal [issue\#1474](https://github.com/hyperledger/aries-cloudagent-python/issues/1474) [\#1667](https://github.com/hyperledger/aries-cloudagent-python/pull/1667) ([shaangill025](https://github.com/shaangill025)) + - Fixes Issue 3b from [\#1597](https://github.com/hyperledger/aries-cloudagent-python/issues/1597): V2 Credential exchange ignores the auto-respond-credential-request + - Revert change to send_credential_ack return value [\#1660](https://github.com/hyperledger/aries-cloudagent-python/pull/1660) ([andrewwhitehead](https://github.com/andrewwhitehead)) + - Fix usage of send_credential_ack [\#1653](https://github.com/hyperledger/aries-cloudagent-python/pull/1653) ([andrewwhitehead](https://github.com/andrewwhitehead)) + - Replace blank credential/presentation exchange states with abandoned state [\#1605](https://github.com/hyperledger/aries-cloudagent-python/pull/1605) ([andrewwhitehead](https://github.com/andrewwhitehead)) + - Fixes Issue 4 from [\#1597](https://github.com/hyperledger/aries-cloudagent-python/issues/1597): Wallet type askar has issues when receiving V1 credentials + - Fixes and cleanups for issue-credential 1.0 [\#1619](https://github.com/hyperledger/aries-cloudagent-python/pull/1619) ([andrewwhitehead](https://github.com/andrewwhitehead)) + - Fix: Duplicated schema and cred_def - Askar and Postgres [\#1800](https://github.com/hyperledger/aries-cloudagent-python/pull/1800) ([shaangill025](https://github.com/shaangill025)) + +- Mediator updates and fixes + - feat: allow querying default mediator from base wallet [\#1729](https://github.com/hyperledger/aries-cloudagent-python/pull/1729) ([dbluhm](https://github.com/dbluhm)) + - Added async with for mediator record delete [\#1749](https://github.com/hyperledger/aries-cloudagent-python/pull/1749) ([dejsenlitro](https://github.com/dejsenlitro)) + +- Multitenacy updates and fixes + - feat: create new JWT tokens and invalidate older for multitenancy [\#1725](https://github.com/hyperledger/aries-cloudagent-python/pull/1725) ([TimoGlastra](https://github.com/TimoGlastra)) + - Multi-tenancy stale wallet clean up [\#1692](https://github.com/hyperledger/aries-cloudagent-python/pull/1692) ([dbluhm](https://github.com/dbluhm)) + +- Dependencies and internal code updates/fixes + - Update pyjwt to 2.4 [\#1829](https://github.com/hyperledger/aries-cloudagent-python/pull/1829) ([andrewwhitehead](https://github.com/andrewwhitehead)) + - Fix external Outbound Transport loading code [\#1812](https://github.com/hyperledger/aries-cloudagent-python/pull/1812) ([frostyfrog](https://github.com/frostyfrog)) + - Fix iteration over key list, update Askar to 0.2.5 [\#1740](https://github.com/hyperledger/aries-cloudagent-python/pull/1740) ([andrewwhitehead](https://github.com/andrewwhitehead)) + - Fix: update IndyLedgerRequestsExecutor logic - multitenancy and basic base wallet type [\#1700](https://github.com/hyperledger/aries-cloudagent-python/pull/1700) ([shaangill025](https://github.com/shaangill025)) + - Move database operations inside the session context [\#1633](https://github.com/hyperledger/aries-cloudagent-python/pull/1633) ([acuderman](https://github.com/acuderman)) + - Upgrade ConfigArgParse to version 1.5.3 [\#1627](https://github.com/hyperledger/aries-cloudagent-python/pull/1627) ([WadeBarnes](https://github.com/WadeBarnes)) + - Update aiohttp dependency [\#1606](https://github.com/hyperledger/aries-cloudagent-python/pull/1606) ([acuderman](https://github.com/acuderman)) + - did-exchange implicit request pthid update & invitation key verification [\#1599](https://github.com/hyperledger/aries-cloudagent-python/pull/1599) ([shaangill025](https://github.com/shaangill025)) + - Fix auto connection response not being properly mediated [\#1638](https://github.com/hyperledger/aries-cloudagent-python/pull/1638) ([dbluhm](https://github.com/dbluhm)) + - platform target in run tests. [\#1697](https://github.com/hyperledger/aries-cloudagent-python/pull/1697) ([burdettadam](https://github.com/burdettadam)) + - Add an integration test for mixed proof with a revocable cred and a n… [\#1672](https://github.com/hyperledger/aries-cloudagent-python/pull/1672) ([ianco](https://github.com/ianco)) + - Fix: Inbound Transport is_external attribute [\#1802](https://github.com/hyperledger/aries-cloudagent-python/pull/1802) ([shaangill025](https://github.com/shaangill025)) + - fix: add a close statement to ensure session is closed on error [\#1777](https://github.com/hyperledger/aries-cloudagent-python/pull/1777) ([reflectivedevelopment](https://github.com/reflectivedevelopment)) + - Adds `transport_id` variable assignment back to outbound enqueue method [\#1776](https://github.com/hyperledger/aries-cloudagent-python/pull/1776) ([amanji](https://github.com/amanji)) + - Replace async workaround within document loader [\#1774](https://github.com/hyperledger/aries-cloudagent-python/pull/1774) ([frostyfrog](https://github.com/frostyfrog)) + +- Documentation and Demo Updates + - Use default wallet type askar for alice/faber demo and bdd tests [\#1761](https://github.com/hyperledger/aries-cloudagent-python/pull/1761) ([ianco](https://github.com/ianco)) + - Update the Supported RFCs document for 0.7.4 release [\#1846](https://github.com/hyperledger/aries-cloudagent-python/pull/1846) ([swcurran](https://github.com/swcurran)) + - Fix a typo in DevReadMe.md [\#1844](https://github.com/hyperledger/aries-cloudagent-python/pull/1844) ([feknall](https://github.com/feknall)) + - Add troubleshooting document, include initial examples - ledger connection, out-of-sync RevReg [\#1818](https://github.com/hyperledger/aries-cloudagent-python/pull/1818) ([swcurran](https://github.com/swcurran)) + - Update POST /present-proof/send-request to POST /present-proof-2.0/send-request [\#1824](https://github.com/hyperledger/aries-cloudagent-python/pull/1824) ([lineko](https://github.com/lineko)) + - Fetch from --genesis-url likely to fail in composed container [\#1746](https://github.com/hyperledger/aries-cloudagent-python/pull/1739) ([tdiesler](https://github.com/tdiesler)) + - Fixes logic for web hook formatter in Faber demo [\#1739](https://github.com/hyperledger/aries-cloudagent-python/pull/1739) ([amanji](https://github.com/amanji)) + - Multitenancy Docs Update [\#1706](https://github.com/hyperledger/aries-cloudagent-python/pull/1706) ([MonolithicMonk](https://github.com/MonolithicMonk)) + - [\#1674](https://github.com/hyperledger/aries-cloudagent-python/issue/1674) Add basic DOCKER_ENV logging for run_demo [\#1675](https://github.com/hyperledger/aries-cloudagent-python/pull/1675) ([tdiesler](https://github.com/tdiesler)) + - Performance demo updates [\#1647](https://github.com/hyperledger/aries-cloudagent-python/pull/1647) ([ianco](https://github.com/ianco)) + - docs: supported features attribution [\#1654](https://github.com/hyperledger/aries-cloudagent-python/pull/1654) ([TimoGlastra](https://github.com/TimoGlastra)) + - Documentation on existing language wrappers for aca-py [\#1738](https://github.com/hyperledger/aries-cloudagent-python/pull/1738) ([etschelp](https://github.com/etschelp)) + - Document impact of multi-ledger on TAA acceptance [\#1778](https://github.com/hyperledger/aries-cloudagent-python/pull/1778) ([ianco](https://github.com/ianco)) + +- Code management and contributor/developer support updates + - Set prefix for integration test demo agents; some code cleanup [\#1840](https://github.com/hyperledger/aries-cloudagent-python/pull/1840) ([andrewwhitehead](https://github.com/andrewwhitehead)) + - Pin markupsafe at version 2.0.1 [\#1642](https://github.com/hyperledger/aries-cloudagent-python/pull/1642) ([andrewwhitehead](https://github.com/andrewwhitehead)) + - style: format with stable black release [\#1615](https://github.com/hyperledger/aries-cloudagent-python/pull/1615) ([TimoGlastra](https://github.com/TimoGlastra)) + - Remove references to play with von [\#1688](https://github.com/hyperledger/aries-cloudagent-python/pull/1688) ([ianco](https://github.com/ianco)) + - Add pre-commit as optional developer tool [\#1671](https://github.com/hyperledger/aries-cloudagent-python/pull/1671) ([dbluhm](https://github.com/dbluhm)) + - run_docker start - pass environment variables [\#1715](https://github.com/hyperledger/aries-cloudagent-python/pull/1715) ([shaangill025](https://github.com/shaangill025)) + - Use local deps only [\#1834](https://github.com/hyperledger/aries-cloudagent-python/pull/1834) ([ryjones](https://github.com/ryjones)) + - Enable pip-audit [\#1831](https://github.com/hyperledger/aries-cloudagent-python/pull/1831) ([ryjones](https://github.com/ryjones)) + - Only run pip-audit on main repo [\#1845](https://github.com/hyperledger/aries-cloudagent-python/pull/1845) ([ryjones](https://github.com/ryjones)) + +- Release management pull requests + - 0.7.4 Release Changelog and version update [\#1849](https://github.com/hyperledger/aries-cloudagent-python/pull/1849) ([swcurran](https://github.com/swcurran)) + - 0.7.4-rc5 changelog, version and ReadTheDocs updates [\#1838](https://github.com/hyperledger/aries-cloudagent-python/pull/1838) ([swcurran](https://github.com/swcurran)) + - Update changelog and version for 0.7.4-rc4 [\#1830](https://github.com/hyperledger/aries-cloudagent-python/pull/1830) ([swcurran](https://github.com/swcurran)) + - Changelog, version and ReadTheDocs updates for 0.7.4-rc3 release [\#1817](https://github.com/hyperledger/aries-cloudagent-python/pull/1817) ([swcurran](https://github.com/swcurran)) + - 0.7.4-rc2 update [\#1771](https://github.com/hyperledger/aries-cloudagent-python/pull/1771) ([swcurran](https://github.com/swcurran)) + - Some ReadTheDocs File updates [\#1770](https://github.com/hyperledger/aries-cloudagent-python/pull/1770) ([swcurran](https://github.com/swcurran)) + - 0.7.4-RC1 Changelog intro paragraph - fix copy/paste error [\#1753](https://github.com/hyperledger/aries-cloudagent-python/pull/1753) ([swcurran](https://github.com/swcurran)) + - Fixing the intro paragraph and heading in the changelog of this 0.7.4RC1 [\#1752](https://github.com/hyperledger/aries-cloudagent-python/pull/1752) ([swcurran](https://github.com/swcurran)) + - Updates to Changelog for 0.7.4. RC1 release [\#1747](https://github.com/hyperledger/aries-cloudagent-python/pull/1747) ([swcurran](https://github.com/swcurran)) + - Prep for adding the 0.7.4-rc0 tag [\#1722](https://github.com/hyperledger/aries-cloudagent-python/pull/1722) ([swcurran](https://github.com/swcurran)) + - Added missed new module -- upgrade -- to the RTD generated docs [\#1593](https://github.com/hyperledger/aries-cloudagent-python/pull/1593) ([swcurran](https://github.com/swcurran)) + - Doh....update the date in the Changelog for 0.7.3 [\#1592](https://github.com/hyperledger/aries-cloudagent-python/pull/1592) ([swcurran](https://github.com/swcurran)) + # 0.7.3 -## December 22, 2021 +## January 10, 2022 This release includes some new AIP 2.0 features out (Revocation Notification and Discover Features 2.0), a major new feature for those using Indy ledger (multi-ledger support), diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 4d3508f609..51d59a2c0b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -12,3 +12,21 @@ end-user and developer demos in the repo should include updates or extensions to If you would like to propose a significant change, please open an issue first to discuss the work with the community. Contributions are made pursuant to the Developer's Certificate of Origin, available at [https://developercertificate.org](https://developercertificate.org), and licensed under the Apache License, version 2.0 (Apache-2.0). + +## Development Tools + +### Pre-commit + +A configuration for [pre-commit](https://pre-commit.com/) is included in this repository. This is an optional tool to help contributors commit code that follows the formatting requirements enforced by the CI pipeline. Additionally, it can be used to help contributors write descriptive commit messages that can be parsed by changelog generators. + +On each commit, pre-commit hooks will run that verify the committed code complies with flake8 and is formatted with black. To install the flake8 and black checks: + +``` +$ pre-commit install +``` + +To install the commit message linter: + +``` +$ pre-commit install --hook-type commit-msg +``` diff --git a/ContainerImagesAndGithubActions.md b/ContainerImagesAndGithubActions.md new file mode 100644 index 0000000000..25bfca9dc8 --- /dev/null +++ b/ContainerImagesAndGithubActions.md @@ -0,0 +1,119 @@ +# Container Images and Github Actions + +Aries Cloud Agent - Python is most frequently deployed using containers. From +the first release of ACA-Py up through 0.7.4, much of the community has built +their Aries stack using the container images graciously provided by BC Gov and +hosted through their `bcgovimages` docker hub account. These images have been +critical to the adoption of not only ACA-Py but also Hyperledger Aries and SSI +more generally. + +Recognizing how critical these images are to the success of ACA-Py and +consistent with Hyperledger's commitment to open collaboration, container images +are now built and published directly from the Aries Cloud Agent - Python project +repository and made available through the [Github Packages Container +Registry](https://ghcr.io). + + +## Image + +This project builds and publishes the `ghcr.io/hyperledger/aries-cloudagent-python` image. +Multiple variants are available; see [Tags](#tags). + +### Tags + +ACA-Py is a foundation for building decentralized identity applications; to this +end, there are multiple variants of ACA-Py built to suit the needs of a variety +of environments and workflows. There are currently two main variants: + +- "Standard" - The default configuration of ACA-Py, including: + - Aries Askar for secure storage + - Indy VDR for Indy ledger communication + - Indy Shared Libraries for AnonCreds +- "Indy" - The legacy configuration of ACA-Py, including: + - Indy SDK Wallet for secure storage + - Indy SDK Ledger for Indy ledger communication + - Indy SDK for AnonCreds + +These two image variants are largely distinguished by providers for Indy Network +and AnonCreds support. The Standard variant is recommended for new projects. +Migration from an Indy based image (whether the new Indy image variant or the +original BC Gov images) to the Standard image is outside of the scope of this +document. + +The ACA-Py images built by this project are tagged to indicate which of the +above variants it is. Other tags may also be generated for use by developers. + +Below is a table of all generated images and their tags: + +Tag | Variant | Example | Description | +------------------------|----------|--------------------------|-------------------------------------------------------------------------------------------------| +py3.6-X.Y.Z | Standard | py3.6-0.7.4 | Standard image variant built on Python 3.6 for ACA-Py version X.Y.Z | +py3.7-X.Y.Z | Standard | py3.7-0.7.4 | Standard image variant built on Python 3.7 for ACA-Py version X.Y.Z | +py3.8-X.Y.Z | Standard | py3.8-0.7.4 | Standard image variant built on Python 3.8 for ACA-Py version X.Y.Z | +py3.9-X.Y.Z | Standard | py3.9-0.7.4 | Standard image variant built on Python 3.9 for ACA-Py version X.Y.Z | +py3.10-X.Y.Z | Standard | py3.10-0.7.4 | Standard image variant built on Python 3.10 for ACA-Py version X.Y.Z | +py3.7-indy-A.B.C-X.Y.Z | Indy | py3.7-indy-1.16.0-0.7.4 | Standard image variant built on Python 3.7 for ACA-Py version X.Y.Z and Indy SDK Version A.B.C | +py3.8-indy-A.B.C-X.Y.Z | Indy | py3.8-indy-1.16.0-0.7.4 | Standard image variant built on Python 3.8 for ACA-Py version X.Y.Z and Indy SDK Version A.B.C | +py3.9-indy-A.B.C-X.Y.Z | Indy | py3.9-indy-1.16.0-0.7.4 | Standard image variant built on Python 3.9 for ACA-Py version X.Y.Z and Indy SDK Version A.B.C | +py3.10-indy-A.B.C-X.Y.Z | Indy | py3.10-indy-1.16.0-0.7.4 | Standard image variant built on Python 3.10 for ACA-Py version X.Y.Z and Indy SDK Version A.B.C | + +### Image Comparison + +There are several key differences that should be noted between the two image +variants and between the BC Gov ACA-Py images. + +- Standard Image + - Based on slim variant of Debian + - Does **NOT** include `libindy` + - Default user is `aries` + - Uses container's system python environment rather than `pyenv` + - Askar and Indy Shared libraries are installed as dependencies of ACA-Py through pip from pre-compiled binaries included in the python wrappers + - Built from repo contents +- Indy Image + - Based on slim variant of Debian + - Built from multi-stage build step (`indy-base` in the Dockerfile) which includes Indy dependencies; this could be replaced with an explicit `indy-python` image from the Indy SDK repo + - Includes `libindy` but does **NOT** include the Indy CLI + - Default user is `indy` + - Uses container's system python environment rather than `pyenv` + - Askar and Indy Shared libraries are installed as dependencies of ACA-Py through pip from pre-compiled binaries included in the python wrappers + - Built from repo contents + - Includes Indy postgres storage plugin +- `bcgovimages/aries-cloudagent` + - (Usually) based on Ubuntu + - Based on `von-image` + - Default user is `indy` + - Includes `libindy` and Indy CLI + - Uses `pyenv` + - Askar and Indy Shared libraries built from source + - Built from ACA-Py python package uploaded to PyPI + - Includes Indy postgres storage plugin + +## Github Actions + +- Tests (`.github/workflows/tests.yml`) - A reusable workflow that runs tests + for the Standard ACA-Py variant for a given python version. +- Tests (Indy) (`.github/workflows/tests-indy.yml`) - A reusable workflow that + runs tests for the Indy ACA-Py variant for a given python and indy version. +- PR Tests (`.github/workflows/pr-tests.yml`) - Run on pull requests; runs tests + for the Standard and Indy ACA-Py variants for a "default" python version. + Check this workflow for the current default python and Indy versions in use. +- Nightly Tests (`.github/workflows/nightly-tests.yml`) - Run nightly; runs + tests for the Standard and Indy ACA-Py variants for all currently supported + python versions. Check this workflow for the set of currently supported + versions and Indy version(s) in use. +- Publish (`.github/workflows/publish.yml`) - Run on new release published or + when manually triggered; builds and pushes the Standard ACA-Py variant to the + Github Container Registry. +- Publish (Indy) (`.github/workflows/publish-indy.yml`) - Run on new release + published or when manually triggered; builds and pushes the Indy ACA-Py + variant to the Github Container Registry. +- Integration Tests (`.github/workflows/integrationtests.yml`) - Run on pull + requests (to the hyperledger fork only); runs BDD integration tests. +- Black Format (`.github/workflows/blackformat.yml`) - Run on pull requests; + checks formatting of files modified by the PR. +- CodeQL (`.github/workflows/codeql.yml`) - Run on pull requests; performs + CodeQL analysis. +- Python Publish (`.github/workflows/pythonpublish.yml`) - Run on release + created; publishes ACA-Py python package to PyPI. +- PIP Audit (`.github/workflows/pipaudit.yml`) - Run when manually triggered; + performs pip audit. diff --git a/DIDMethods.md b/DIDMethods.md new file mode 100644 index 0000000000..fa21e6ee72 --- /dev/null +++ b/DIDMethods.md @@ -0,0 +1,47 @@ +# DID Methods in ACA-Py + +Decentralized Identifiers, or DIDs, are URIs that point to documents that describe cryptographic primitives and protocols used in decentralized identity management. +DIDs include methods that describe where and how documents can be retrieved. +DID methods support specific types of keys and may or may not require the holder to specify the DID itself. + +ACA-Py provides a `DIDMethods` registry holding all the DID methods supported for storage in a wallet + +> :warning: Askar and InMemory are the only wallets supporting this registry. + +## Registering a DID method + +By default, ACA-Py supports `did:key` and `did:sov`. +Plugins can register DID additional methods to make them available to holders. +Here's a snippet adding support for `did:web` to the registry from a plugin `setup` method. + +```python +WEB = DIDMethod( + name="web", + key_types=[ED25519, BLS12381G2], + rotation=True, + holder_defined_did=HolderDefinedDid.REQUIRED # did:web is not derived from key material but from a user-provided respository name +) + +async def setup(context: InjectionContext): + methods = context.inject(DIDMethods) + methods.register(WEB) +``` + +## Creating a DID + +`POST /wallet/did/create` can be provided with parameters for any registered DID method. Here's a follow-up to the +`did:web` method example: + +```json +{ + "method": "web", + "options": { + "did": "did:web:doma.in", + "key_type": "ed25519" + } +} +``` + +## Resolving DIDs + +For specifics on how DIDs are resolved in ACA-Py, see: [DID Resolution](DIDResolution.md). diff --git a/DIDResolution.md b/DIDResolution.md index c1666adab2..82d0b345b6 100644 --- a/DIDResolution.md +++ b/DIDResolution.md @@ -1,25 +1,26 @@ # DID Resolution in ACA-Py + Decentralized Identifiers, or DIDs, are URIs that point to documents that describe cryptographic primitives and protocols used in decentralized identity management. DIDs include methods that describe where and how documents can be retrieved. DID resolution is the process of "resolving" a DID Document from a DID as dictated by the DID method. A DID Resolver is a piece of software that implements the methods for resolving a document from a DID. For example, given the DID `did:example:1234abcd`, a DID Resolver that supports `did:example` might return: -```json= +```json { - "@context": "https://www.w3.org/ns/did/v1", - "id": "did:example:1234abcd", - "verificationMethod": [{ - "id": "did:example:1234abcd#keys-1", - "type": "Ed25519VerificationKey2018", - "controller": "did:example:1234abcd", - "publicKeyBase58": "H3C2AVvLMv6gmMNam3uVAjZpfkcJCwDwnZn6z3wXmqPV" - }], - "service": [{ - "id": "did:example:1234abcd#did-communication", - "type": "did-communication", - "serviceEndpoint": "https://agent.example.com/8377464" - }] + "@context": "https://www.w3.org/ns/did/v1", + "id": "did:example:1234abcd", + "verificationMethod": [{ + "id": "did:example:1234abcd#keys-1", + "type": "Ed25519VerificationKey2018", + "controller": "did:example:1234abcd", + "publicKeyBase58": "H3C2AVvLMv6gmMNam3uVAjZpfkcJCwDwnZn6z3wXmqPV" + }], + "service": [{ + "id": "did:example:1234abcd#did-communication", + "type": "did-communication", + "serviceEndpoint": "https://agent.example.com/8377464" + }] } ``` @@ -29,10 +30,11 @@ In practice, DIDs and DID Documents are used for a variety of purposes but espec ## `DIDResolver` -In ACA-Py, the `DIDResolver` provides the interface to resolve DIDs using registered method resolvers. Method resolver registration happens on startup through the `DIDResolverRegistry`. This registry enables additional resolvers to be loaded via plugin. +In ACA-Py, the `DIDResolver` provides the interface to resolve DIDs using registered method resolvers. Method resolver registration happens on startup in a `did_resolvers` list. This registry enables additional resolvers to be loaded via plugin. -#### Example usage: -```python= +### Example usage + +```python class ExampleMessageHandler: async def handle(context: RequestContext, responder: BaseResponder): """Handle example message.""" @@ -73,20 +75,21 @@ The following is an example method resolver implementation. In this example, we ```python= from aries_cloudagent.config.injection_context import InjectionContext -from aries_cloudagent.resolver.did_resolver_registry import DIDResolverRegistry +from ..resolver.did_resolver import DIDResolver from .example_resolver import ExampleResolver async def setup(context: InjectionContext): """Setup the plugin.""" - registry = context.inject(DIDResolverRegistry) + registry = context.inject(DIDResolver) resolver = ExampleResolver() await resolver.setup(context) - registry.register(resolver) + registry.append(resolver) ``` #### `example_resolver.py` + ```python= import re from typing import Pattern @@ -133,25 +136,26 @@ class ExampleResolver(BaseDIDResolver): ``` #### Errors + There are 3 different errors associated with resolution in ACA-Py that could be used for development purposes. - ResolverError - - Base class for resolver exceptions. + - Base class for resolver exceptions. - DIDNotFound - - Raised when DID is not found using DID method specific algorithm. + - Raised when DID is not found using DID method specific algorithm. - DIDMethodNotSupported - - Raised when no resolver is registered for a given did method. + - Raised when no resolver is registered for a given did method. ### Using Resolver Plugins -In this section, the [Github Resolver Plugin found here](https://github.com/dbluhm/acapy-resolver-github) will be used as an an example plugin to work with. This resolver resolves `did:github` DIDs. +In this section, the [Github Resolver Plugin found here](https://github.com/dbluhm/acapy-resolver-github) will be used as an an example plugin to work with. This resolver resolves `did:github` DIDs. The resolution algorithm is simple: for the github DID `did:github:dbluhm`, the method specific identifier `dbluhm` (a GitHub username) is used to lookup a `index.jsonld` file in the `ghdid` repository in that GitHub users profile. See [GitHub DID Method Specification](http://docs.github-did.com/did-method-spec/) for more details. To use this plugin, first install it into your project's python environment: ```shell -$ pip install git+https://github.com/dbluhm/acapy-resolver-github +pip install git+https://github.com/dbluhm/acapy-resolver-github ``` Then, invoke ACA-Py as you normally do with the addition of: @@ -163,7 +167,8 @@ $ aca-py start \ ``` Or add the following to your configuration file: -```yaml= + +```yaml plugin: - acapy_resolver_github ``` @@ -180,16 +185,19 @@ CMD ["aca-py", "start", "-it", "http", "0.0.0.0", "3000", "-ot", "http", "-e", " ``` To use the above dockerfile: + ```shell -$ docker build -t resolver-example . -$ docker run --rm -it -p 3000:3000 -p 3001:3001 resolver-example +docker build -t resolver-example . +docker run --rm -it -p 3000:3000 -p 3001:3001 resolver-example ``` ### Directory of Resolver Plugins + - [Github Resolver](https://github.com/dbluhm/acapy-resolver-github) - [Universal Resolver](https://github.com/sicpa-dlab/acapy-resolver-universal) - [DIDComm Resolver](https://github.com/sicpa-dlab/acapy-resolver-didcomm) ## References -https://www.w3.org/TR/did-core/ -https://w3c-ccg.github.io/did-resolution/ + + + diff --git a/DevReadMe.md b/DevReadMe.md index 53e8511fcb..e16484ce92 100644 --- a/DevReadMe.md +++ b/DevReadMe.md @@ -18,6 +18,7 @@ See the [README](README.md) for details about this repository and information ab - [Developing](#developing) - [Prerequisites](#prerequisites) - [Running Locally](#running-locally) + - [Logging](#logging) - [Running Tests](#running-tests) - [Running Aries Agent Test Harness Tests](#running-aries-agent-test-harness-tests) - [Development Workflow](#development-workflow) @@ -148,7 +149,7 @@ To enable the [ptvsd](https://github.com/Microsoft/ptvsd) Python debugger for Vi Any ports you will be using from the docker container should be published using the `PORTS` environment variable. For example: ```bash -PORTS="5000:5000 8000:8000 1000:1000" ./scripts/run_docker start --inbound-transport http 0.0.0.0 10000 --outbound-transport http --debug --log-level DEBUG +PORTS="5000:5000 8000:8000 10000:10000" ./scripts/run_docker start --inbound-transport http 0.0.0.0 10000 --outbound-transport http --debug --log-level DEBUG ``` Refer to [the previous section](#Running) for instructions on how to run the software. diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000000..d9c441110c --- /dev/null +++ b/Dockerfile @@ -0,0 +1,28 @@ +FROM bcgovimages/von-image:py36-1.16-1 + +ADD requirements*.txt ./ + +RUN pip3 install --no-cache-dir \ + -r requirements.txt \ + -r requirements.askar.txt \ + -r requirements.bbs.txt \ + -r requirements.dev.txt + +RUN mkdir aries_cloudagent && touch aries_cloudagent/__init__.py +ADD aries_cloudagent/version.py aries_cloudagent/version.py +ADD bin ./bin +ADD README.md ./ +ADD setup.py ./ + +RUN pip3 install --no-cache-dir -e . +ADD aries_cloudagent ./aries_cloudagent + +USER root +RUN apt-get -y update && apt-get -y install nginx + +EXPOSE 5000 + +COPY ./deployment/nginx.conf /etc/nginx/conf.d/default.conf +COPY ./deployment/start.sh ./start.sh + +ENTRYPOINT ["/bin/bash", "./start.sh"] \ No newline at end of file diff --git a/Endorser.md b/Endorser.md index c108fec4d8..b1db873c26 100644 --- a/Endorser.md +++ b/Endorser.md @@ -1,7 +1,5 @@ # Transaction Endorser Support -Note that the ACA-Py transaction support is in the process of code refactor and cleanup. The following documents the current state, but is subject to change. - ACA-Py supports an [Endorser Protocol](https://github.com/hyperledger/aries-rfcs/pull/586), that allows an un-privileged agent (an "Author") to request another agent (the "Endorser") to sign their transactions so they can write these transactions to the ledger. This is required on Indy ledgers, where new agents will typically be granted only "Author" privileges. Transaction Endorsement is built into the protocols for Schema, Credential Definition and Revocation, and endorsements can be explicitely requested, or ACA-Py can be configured to automate the endorsement workflow. @@ -57,5 +55,48 @@ Endorsement: --auto-create-revocation-transactions For Authors, specify whether to automatically create transactions for a cred def's revocation registry. (If not specified, the controller must invoke the endpoints required to create the revocation registry and assign to the cred def.) [env var: ACAPY_CREATE_REVOCATION_TRANSACTIONS] + --auto-promote-author-did + For Authors, specify whether to automatically promote a DID to the wallet public DID after writing to the ledger. [env var: ACAPY_AUTO_PROMOTE_AUTHOR_DID] ``` +## How Aca-py Handles Endorsements + +Internally, the Endorsement functionality is implemented as a protocol, and is implemented consistently with other protocols: + +- a [routes.py](https://github.com/hyperledger/aries-cloudagent-python/blob/main/aries_cloudagent/protocols/endorse_transaction/v1_0/routes.py) file exposes the admin endpoints +- [handler files](https://github.com/hyperledger/aries-cloudagent-python/tree/main/aries_cloudagent/protocols/endorse_transaction/v1_0/handlers) implement responses to any received Endorse protocol messages +- a [manager.py](https://github.com/hyperledger/aries-cloudagent-python/blob/main/aries_cloudagent/protocols/endorse_transaction/v1_0/manager.py) file implements common functionality that is called from both the routes.py and handler classes (as well as from other classes that need to interact with Endorser functionality) + +The Endorser makes use of the [Event Bus](https://github.com/hyperledger/aries-cloudagent-python/blob/main/CHANGELOG.md#july-14-2021) (links to the PR which links to a hackmd doc) to notify other protocols of any Endorser events of interest. For example, after a Credential Definition endorsement is received, the TransactionManager writes the endorsed transaction to the ledger and uses the Event Bus to notify the Credential Defintition manager that it can do any required post-processing (such as writing the cred def record to the wallet, initiating the revocation registry, etc.). + +The overall architecture can be illustrated as: + +![Class Diagram](./docs/assets/endorser-design.png) + +### Create Credential Definition and Revocation Registry + +An example of an Endorser flow is as follows, showing how a credential definition endorsement is received and processed, and optionally kicks off the revocation registry process: + +![Sequence Diagram](./docs/assets/endorse-cred-def.png) + +You can see that there is a standard endorser flow happening each time there is a ledger write (illustrated in the "Endorser" process). + +At the end of each endorse sequence, the TransactionManager sends a notification via the EventBus so that any dependant processing can continue. Each Router is responsible for listening and responding to these notifications if necessary. + +For example: + +- Once the credential definition is created, a revocation registry must be created (for revocable cred defs) +- Once the revocation registry is created, a revocation entry must be created +- Potentially, the cred def status could be updated once the revocation entry is completed + +Using the EventBus decouples the event sequence. Any functions triggered by an event notification are typically also available directly via Admin endpoints. + +### Create DID and Promote to Public + +... and an example of creating a DID and promoting it to public (and creating an ATTRIB for the endpoint: + +![Sequence Diagram](./docs/assets/endorse-public-did.png) + +You can see the same endorsement processes in this sequence. + +Once the DID is written, the DID can (optionally) be promoted to the public DID, which will also invoke an ATTRIB transaction to write the endpoint. diff --git a/Logging.md b/Logging.md index 9c3162ac84..c8f778a801 100644 --- a/Logging.md +++ b/Logging.md @@ -12,11 +12,14 @@ Other log levels fall back to `WARNING`. * `--log-level` - The log level to log on std out. * `--log-file` - Path to a file to log to. +* `--log-handler-config` - Specifies `when`, `interval`, `backupCount` for the `TimedRotatingFileMultiProcessHandler`. These 3 attributes are passed as a `;` seperated string. For example, `when` of d (days), `interval` of 7 and `backupCount` of 1 will be passed as `D;7;1`. Note: `backupCount` of 0 will mean all backup log files will be retained and not deleted at all. More details about these attributes can be found [here](https://docs.python.org/3/library/logging.handlers.html#timedrotatingfilehandler). `TimedRotatingFileMultiProcessHandler` supports the ability to cleanup logs by time and mantain backup logs and a custom JSON formatter for logs. +* `--log-fmt-pattern` - Specifies logging.Formatter pattern to override default patterns. +* `--log-json-fmt` - Specifies whether to use JSON logging formatter or text logging formatter. Defaults to `False`. Example: ```sh -./bin/aca-py start --log-level debug --log-file acapy.log +./bin/aca-py start --log-level debug --log-file acapy.log --log-handler-config "d;7;1" --log-fmt-pattern "%(asctime)s [%(did)s] %(filename)s %(lineno)d %(message)s" --log-json-fmt ``` ## Environment Variables @@ -24,11 +27,14 @@ Example: The log level can be configured using the environment variable `ACAPY_LOG_LEVEL`. The log file can be set by `ACAPY_LOG_FILE`. The log config can be set by `ACAPY_LOG_CONFIG`. +The log rotating file handler config can be set by `ACAPY_LOG_HANDLER_CONFIG`. +The log formatter pattern can be set by `ACAPY_LOG_FMT_PATTERN`. +The log json formatter flag can be set by `ACAPY_LOG_JSON_FMT`. Example: ```sh -ACAPY_LOG_LEVEL=info ACAPY_LOG_FILE=./acapy.log ACAPY_LOG_CONFIG=./acapy_log.ini ./bin/aca-py start +ACAPY_LOG_LEVEL=info ACAPY_LOG_FILE=./acapy.log ACAPY_LOG_CONFIG=./acapy_log.ini ACAPY_LOG_HANDLER_CONFIG="d;7;1" ./bin/aca-py start ``` ## Acapy Config File diff --git a/MAINTAINERS.md b/MAINTAINERS.md index 3306ec0012..69abb1d17e 100644 --- a/MAINTAINERS.md +++ b/MAINTAINERS.md @@ -1,54 +1,95 @@ # Maintainers +## Maintainer Scopes, GitHub Roles and GitHub Teams + +Maintainers are assigned the following scopes in this repository: + +| Scope | Definition | GitHub Role | GitHub Team | +| ---------- | ------------------------ | ----------- | ------------------------------------ | +| Admin | | Admin | [aries-admins] | +| Maintainer | The GitHub Maintain role | Maintain | [aries-cloudagent-python committers] | +| Triage | The GitHub Triage role | Triage | [aries triage] | +| Read | The GitHub Read role | Read | [Aries Contributors] | +| Read | The GitHub Read role | Read | [TOC] | +| Read | The GitHub Read role | Read | [aries-framework-go-ext committers] | + +[aries-admins]: https://github.com/orgs/hyperledger/teams/aries-admins +[aries-cloudagent-python committers]: https://github.com/orgs/hyperledger/teams/aries-cloudagent-python-committers +[aries triage]: https://github.com/orgs/hyperledger/teams/aries-triage +[Aries Contributors]: https://github.com/orgs/hyperledger/teams/aries-contributors +[TOC]: https://github.com/orgs/hyperledger/teams/toc +[aries-framework-go-ext committers]: https://github.com/orgs/hyperledger/teams/aries-framework-go-ext-committers + ## Active Maintainers -| Name | Github | LFID | -| ---------------- | ---------------- | ---------------- | -| Sam Curren | TelegramSam | TelegramSam | -| Stephen Curran | swcurran | swcurran | -| Andrew Whitehead | andrewwhitehead | cywolf | +| GitHub ID | Name | Scope | LFID | Discord ID | Email | Company Affiliation | +| --------------- | ---------------- | ---------- | ---- | ---------- | ------------------------ | ------------------- | +| andrewwhitehead | Andrew Whitehead | Admin | | | cywolf@gmail.com | BC Gov | +| dbluhm | Daniel Bluhm | Admin | | | daniel@indicio.tech | Indicio PBC | +| dh1128 | Daniel Hardman | Admin | | | daniel.hardman@gmail.com | Provident | +| shaangill025 | Shaanjot Gill | Maintainer | | | gill.shaanjots@gmail.com | BC Gov | +| swcurran | Stephen Curran | Admin | | | swcurran@cloudcompass.ca | BC Gov | +| TelegramSam | Sam Curren | Maintainer | | | telegramsam@gmail.com | Indicio PBC | +| TimoGlastra | Timo Glastra | Admin | | | timo@animo.id | Animo Solutions | +| WadeBarnes | Wade Barnes | Admin | | | wade@neoterictech.ca | BC Gov | +| usingtechnology | Jason Sherman | Maintainer | | | tools@usingtechnolo.gy | BC Gov | ## Emeritus Maintainers -| Name | Github | LFID | -|--------------|---------|---------| -| | | | +| Name | GitHub ID | Scope | LFID | Discord ID | Email | Company Affiliation | +|----- | --------- | ----- | ---- | ---------- | ----- | ------------------- | +| | | | | | | | + +## The Duties of a Maintainer + +Maintainers are expected to perform the following duties for this repository. The duties are listed in more or less priority order: + +- Review, respond, and act on any security vulnerabilities reported against the repository. +- Review, provide feedback on, and merge or reject GitHub Pull Requests from + Contributors. +- Review, triage, comment on, and close GitHub Issues + submitted by Contributors. +- When appropriate, lead/facilitate architectural discussions in the community. +- When appropriate, lead/facilitate the creation of a product roadmap. +- Create, clarify, and label issues to be worked on by Contributors. +- Ensure that there is a well defined (and ideally automated) product test and + release pipeline, including the publication of release artifacts. +- When appropriate, execute the product release process. +- Maintain the repository CONTRIBUTING.md file and getting started documents to + give guidance and encouragement to those wanting to contribute to the product, and those wanting to become maintainers. +- Contribute to the product via GitHub Pull Requests. +- Monitor requests from the Hyperledger Technical Oversight Committee about the +contents and management of Hyperledger repositories, such as branch handling, +required files in repositories and so on. +- Contribute to the Hyperledger Project's Quarterly Report. ## Becoming a Maintainer -The Aries Cloud Agent community welcomes contributions. Contributors may progress to become a -maintainer. To become a maintainer the following steps occur, roughly in order. - -- 5 significant changes have been authored by the proposed maintainer and - accepted. -- The proposed maintainer has the sponsorship of at least one other maintainer. - - This sponsoring maintainer will create a PR modifying the list of - maintainers. - - The proposed maintainer accepts the nomination and expresses a willingness - to be a long-term (more than 6 month) committer. - - This would be a comment in the above PR. - - This PR will be communicated in all appropriate communication channels. It - should be mentioned in any maintainer/community call. It should also be - posted to the appropriate mailing list or chat channels if they exist. -- Approval by at least 3 current maintainers within two weeks of the proposal or - an absolute majority of current maintainers. - - These votes will be recorded in the PR modifying the list of maintainers. -- No veto by another maintainer within two weeks of proposal are recorded. - - All vetoes must be accompanied by a public explanation as a comment in the - PR for adding this maintainer - - The explanation of the veto must be reasonable. - - A veto can be retracted, in that case the approval/veto timeframe is reset. - - It is bad form to veto, retract, and veto again. -- The proposed maintainer becomes a maintainer - - Either two weeks have passed since the third approval, - - Or an absolute majority of maintainers approve. - - In either case, no maintainer presents a veto. +This community welcomes contributions. Interested contributors are encouraged to +progress to become maintainers. To become a maintainer the following steps +occur, roughly in order. + +- The proposed maintainer establishes their reputation in the community, + including authoring five (5) significant merged pull requests, and expresses + an interest in becoming a maintainer for the repository. +- A PR is created to update this file to add the proposed maintainer to the list of active maintainers. +- The PR is authored by an existing maintainer or has a comment on the PR from an existing maintainer supporting the proposal. +- The PR is authored by the proposed maintainer or has a comment on the PR from the proposed maintainer confirming their interest in being a maintainer. + - The PR or comment from the proposed maintainer must include their + willingness to be a long-term (more than 6 month) maintainer. +- Once the PR and necessary comments have been received, an approval timeframe begins. +- The PR **MUST** be communicated on all appropriate communication channels, including relevant community calls, chat channels and mailing lists. Comments of support from the community are welcome. +- The PR is merged and the proposed maintainer becomes a maintainer if either: + - Two weeks have passed since at least three (3) Maintainer PR approvals have been recorded, OR + - An absolute majority of maintainers have approved the PR. +- If the PR does not get the requisite PR approvals, it may be closed. +- Once the add maintainer PR has been merged, any necessary updates to the GitHub Teams are made. ## Removing Maintainers -Being a maintainer is not a status symbol or a title to be maintained +Being a maintainer is not a status symbol or a title to be carried indefinitely. It will occasionally be necessary and appropriate to move a maintainer to emeritus status. This can occur in the following situations: @@ -56,15 +97,25 @@ maintainer to emeritus status. This can occur in the following situations: - Violation of the Code of Conduct warranting removal. - Inactivity. - A general measure of inactivity will be no commits or code review comments - for one reporting quarter, although this will not be strictly enforced if + for one reporting quarter. This will not be strictly enforced if the maintainer expresses a reasonable intent to continue contributing. - Reasonable exceptions to inactivity will be granted for known long term leave such as parental leave and medical leave. -- Other unspecified circumstances. +- Other circumstances at the discretion of the other Maintainers. + +The process to move a maintainer from active to emeritus status is comparable to the process for adding a maintainer, outlined above. In the case of voluntary +resignation, the Pull Request can be merged following a maintainer PR approval. If the removal is for any other reason, the following steps **SHOULD** be followed: -Like adding a maintainer the record and governance process for moving a -maintainer to emeritus status is recorded in the github PR making that change. +- A PR is created to update this file to move the maintainer to the list of emeritus maintainers. +- The PR is authored by, or has a comment supporting the proposal from, an existing maintainer or Hyperledger GitHub organization administrator. +- Once the PR and necessary comments have been received, the approval timeframe begins. +- The PR **MAY** be communicated on appropriate communication channels, including relevant community calls, chat channels and mailing lists. +- The PR is merged and the maintainer transitions to maintainer emeritus if: + - The PR is approved by the maintainer to be transitioned, OR + - Two weeks have passed since at least three (3) Maintainer PR approvals have been recorded, OR + - An absolute majority of maintainers have approved the PR. +- If the PR does not get the requisite PR approvals, it may be closed. Returning to active status from emeritus status uses the same steps as adding a new maintainer. Note that the emeritus maintainer already has the 5 required -significant changes as there is no contribution time horizon for those. \ No newline at end of file +significant changes as there is no contribution time horizon for those. diff --git a/MANIFEST.in b/MANIFEST.in index 494b5def3a..ef5132a345 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,4 +1,5 @@ include aries_cloudagent/config/default_logging_config.ini +include aries_cloudagent/commands/default_version_upgrade_config.yml include requirements.txt include requirements.dev.txt include requirements.indy.txt diff --git a/Mediation.md b/Mediation.md index 425996e16e..c301a39e77 100644 --- a/Mediation.md +++ b/Mediation.md @@ -15,6 +15,7 @@ * `--open-mediation` - Instructs mediators to automatically grant all incoming mediation requests. * `--mediator-invitation` - Receive invitation, send mediation request and set as default mediator. +* `--mediator-connections-invite` - Connect to mediator through a connection invitation. If not specified, connect using an OOB invitation. * `--default-mediator-id` - Set pre-existing mediator as default mediator. * `--clear-default-mediator` - Clear the stored default mediator. @@ -72,4 +73,4 @@ See [Aries RFC 0211: Coordinate Mediation Protocol](https://github.com/hyperledg ## Using a Mediator After establishing a connection with a mediator also having mediation granted, you can use that mediator id for future did_comm connections. - When creating, receiving or accepting a invitation intended to be Mediated, you provide `mediation_id` with the desired mediator id. if using a single mediator for all future connections, You can set a default mediation id. If no mediation_id is provided the default mediation id will be used instead. \ No newline at end of file + When creating, receiving or accepting a invitation intended to be Mediated, you provide `mediation_id` with the desired mediator id. if using a single mediator for all future connections, You can set a default mediation id. If no mediation_id is provided the default mediation id will be used instead. diff --git a/Multicredentials.md b/Multicredentials.md new file mode 100644 index 0000000000..1c9e1f72ab --- /dev/null +++ b/Multicredentials.md @@ -0,0 +1,9 @@ +# Multi-Credentials + +It is a known fact that multiple AnonCreds can be combined to present a presentation proof with an "and" logical operator: For instance, a verifier can ask for the "name" claim from an eID and the "address" claim from a bank statement to have a single proof that is either valid or invalid. With the Present Proof Protocol v2, it is possible to have "and" and "or" logical operators for AnonCreds and/or W3C Verifiable Credentials. + +With the Present Proof Protocol v2, verifiers can ask for a combination of credentials as proof. For instance, a Verifier can ask a claim from an AnonCreds **and** a verifiable presentation from a W3C Verifiable Credential, which would open the possibilities of Aries Cloud Agent Python being used for rather complex presentation proof requests that wouldn't be possible without the support of AnonCreds or W3C Verifiable Credentials. + +Moreover, it is possible to make similar presentation proof requests using the or logical operator. For instance, a verifier can ask for either an eID in AnonCreds format or an eID in W3C Verifiable Credential format. This has the potential to solve the interoperability problem of different credential formats and ecosystems from a user point of view by shifting the requirement of holding/accepting different credential formats from identity holders to verifiers. Here again, using Aries Cloud Agent Python as the underlying verifier agent can tackle such complex presentation proof requests since the agent is capable of verifying both type of credential formats and proof types. + +In the future, it would be even possible to put mDoc as an attachment with an and or or logical operation, along with AnonCreds and/or W3C Verifiable Credentials. For this to happen, Aca-Py either needs the capabilities to validate mDocs internally or to connect third-party endpoints to validate and get a response. \ No newline at end of file diff --git a/Multiledger.md b/Multiledger.md index 30c692c878..5d575c9969 100644 --- a/Multiledger.md +++ b/Multiledger.md @@ -14,6 +14,7 @@ More background information including problem statement, design (algorithm) and - [Read Requests](#read-requests) - [For checking ledger in parallel](#for-checking-ledger-in-parallel) - [Write Requests](#write-requests) +- [A Special Warning for TAA Acceptance](#a-special-warning-for-taa-acceptance) - [Impact on other ACA-Py function](#impact-on-other-aca-py-function) ## Usage @@ -104,6 +105,25 @@ If multiple ledgers are configured then `IndyLedgerRequestsExecutor` service ext On startup, the first configured applicable ledger is assigned as the `write_ledger` [`BaseLedger`], the selection is dependant on the order (top-down) and whether it is `production` or `non_production`. For instance, considering this [example configuration](#example-config-file), ledger `bcorvinTest` will be set as `write_ledger` as it is the topmost `production` ledger. If no `production` ledgers are included in configuration then the topmost `non_production` ledger is selected. +## A Special Warning for TAA Acceptance + +When you run in multi-ledger mode, ACA-Py will use the `pool-name` (or `id`) specified in the ledger configuration file for each ledger. + +(When running in single-ledger mode, ACA-Py uses `default` as the ledger name.) + +If you are running against a ledger in `write` mode, and the ledger requires you to accept a Transaction Author Agreement (TAA), ACA-Py stores the TAA acceptance +status in the wallet in a non-secrets record, using the ledger's `pool_name` as a key. + +This means that if you are upgrading from single-ledger to multi-ledger mode, you will need to *either*: + +- set the `id` for your writable ledger to `default` (in your `ledgers.yaml` file) + +*or*: + +- re-accept the TAA once you restart your ACA-Py in multi-ledger mode + +Once you re-start ACA-Py, you can check the `GET /ledger/taa` endpoint to verify your TAA acceptance status. + ## Impact on other ACA-Py function There should be no impact/change in functionality to any ACA-Py protocols. diff --git a/Multitenancy.md b/Multitenancy.md index fbec89481c..f99566b29e 100644 --- a/Multitenancy.md +++ b/Multitenancy.md @@ -22,8 +22,14 @@ This allows ACA-Py to be used for a wider range of use cases. One use case could - [Identifying the wallet](#identifying-the-wallet) - [Authentication](#authentication) - [Getting a token](#getting-a-token) + - [Method 1: Register new tenant](#method-1-register-new-tenant) + - [Method 2: Get tenant token](#method-2-get-tenant-token) - [JWT Secret](#jwt-secret) - [SwaggerUI](#swaggerui) +- [Tenant Management](#tenant-management) + - [Update a tenant](#update-a-tenant) + - [Remove a tenant](#remove-a-tenant) + - [Per tenant settings](#per-tenant-settings) ## General Concept @@ -124,7 +130,7 @@ The main tradeoff between option 1. and 2. is redundancy and control. Option 1. A combination of option 1. and 2. is also possible. In this case, two mediators will be used and the sub wallet mediator will forward to the base wallet mediator, which will, in turn, forward to the ACA-Py instance. -``` +```plaintext +---------------------+ +----------------------+ +--------------------+ | Sub wallet mediator | ---> | Base wallet mediator | ---> | Multi-tenant agent | +---------------------+ +----------------------+ +--------------------+ @@ -193,7 +199,7 @@ For sub wallets, an additional authentication method is introduced using JSON We Example -``` +```jsonc GET /connections [headers="Authorization: Bearer {token}] ``` @@ -203,6 +209,78 @@ The `Authorization` header is in addition to the Admin API key. So if the `admin A token can be obtained in two ways. The first method is the `token` parameter from the response of the create wallet (`POST /multitenancy/wallet`) endpoint. The second option is using the get wallet token endpoint (`POST /multitenancy/wallet/{wallet_id}/token`) endpoint. +#### Method 1: Register new tenant + +This is the method you use to obtain a token when you haven't already registered a tenant. In this process you will first register a tenant then an object containing your tenant `token` as well as other useful information like your `wallet id` will be returned to you. + +Example + +```jsonc +new_tenant='{ + "image_url": "https://aries.ca/images/sample.png", + "key_management_mode": "managed", + "label": "example-label-02", + "wallet_dispatch_type": "default", + "wallet_key": "example-encryption-key-02", + "wallet_name": "example-name-02", + "wallet_type": "askar", + "wallet_webhook_urls": [ + "https://example.com/webhook" + ] +}' +``` + +```sh +echo $new_tenant | curl -X POST "${ACAPY_ADMIN_URL}/multitenancy/wallet" \ + -H "Content-Type: application/json" \ + -H "X-Api-Key: $ACAPY_ADMIN_URL_API_KEY" \ + -d @- +``` + +**`Response`** + +```jsonc +{ + "settings": { + "wallet.type": "askar", + "wallet.name": "example-name-02", + "wallet.webhook_urls": [ + "https://example.com/webhook" + ], + "wallet.dispatch_type": "default", + "default_label": "example-label-02", + "image_url": "https://aries.ca/images/sample.png", + "wallet.id": "3b64ad0d-f556-4c04-92bc-cd95bfde58cd" + }, + "key_management_mode": "managed", + "updated_at": "2022-04-01T15:12:35.474975Z", + "wallet_id": "3b64ad0d-f556-4c04-92bc-cd95bfde58cd", + "created_at": "2022-04-01T15:12:35.474975Z", + "token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ3YWxsZXRfaWQiOiIzYjY0YWQwZC1mNTU2LTRjMDQtOTJiYy1jZDk1YmZkZTU4Y2QifQ.A4eWbSR2M1Z6mbjcSLOlciBuUejehLyytCVyeUlxI0E" +} +``` + +#### Method 2: Get tenant token + +This method allows you to retrieve a tenant `token` for an already registered tenant. To retrieve a token you will need an Admin API key (if your admin is protected with one), `wallet_key` and the `wallet_id` of the tenant. Note that calling the get tenant token endpoint will **invalidate** the old token. This is useful if the old token needs to be revoked, but does mean that you can't have multiple authentication tokens for the same wallet. Only the last generated token will always be valid. + +Example + +```sh +curl -X POST "${ACAPY_ADMIN_URL}/multitenancy/wallet/{wallet_id}/token" \ + -H "Content-Type: application/json" \ + -H "X-Api-Key: $ACAPY_ADMIN_URL_API_KEY" \ + -d { "wallet_key": "example-encryption-key-02" } +``` + +**`Response`** + +```jsonc +{ + "token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ3YWxsZXRfaWQiOiIzYjY0YWQwZC1mNTU2LTRjMDQtOTJiYy1jZDk1YmZkZTU4Y2QifQ.A4eWbSR2M1Z6mbjcSLOlciBuUejehLyytCVyeUlxI0E" +} +``` + In unmanaged mode, the get token endpoint also requires the `wallet_key` parameter to be included in the request body. The wallet key will be included in the JWT so the wallet can be unlocked when making requests to the admin API. ```jsonc @@ -221,6 +299,158 @@ For deterministic JWT creation and verification between restarts and multiple in ### SwaggerUI -When using the SwaggerUI you can click the :lock: icon next to each of the endpoints or the `Authorize` button at the top to set the correct authentication headers. Make sure to also include the `Bearer ` part in the input field. This won't be automatically added. +When using the SwaggerUI you can click the :lock: icon next to each of the endpoints or the `Authorize` button at the top to set the correct authentication headers. Make sure to also include the `Bearer` part in the input field. This won't be automatically added. + +![API Authentication](/docs/assets/adminApiAuthentication.png) -![](/docs/assets/adminApiAuthentication.png) +## Tenant Management + +After registering a tenant which effectively creates a subwallet, you may need to update the tenant information or delete it. The following describes how to accomplish both goals. + +### Update a tenant + +The following properties can be updated: `image_url`, `label`, `wallet_dispatch_type`, and `wallet_webhook_urls` for tenants of a multitenancy wallet. To update these properties you will `PUT` a request json containing the properties you wish to update along with the updated values to the `/multitenancy/wallet/${TENANT_WALLET_ID}` admin endpoint. If the Admin API endoint is protected, you will also include the Admin API Key in the request header. + +Example + +```jsonc +update_tenant='{ + "image_url": "https://aries.ca/images/sample-updated.png", + "label": "example-label-02-updated", + "wallet_webhook_urls": [ + "https://example.com/webhook/updated" + ] +}' +``` + +```sh +echo $update_tenant | curl -X PUT "${ACAPY_ADMIN_URL}/multitenancy/wallet/${TENANT_WALLET_ID}" \ + -H "Content-Type: application/json" \ + -H "x-api-key: $ACAPY_ADMIN_URL_API_KEY" \ + -d @- +``` + +**`Response`** + +```jsonc +{ + "settings": { + "wallet.type": "askar", + "wallet.name": "example-name-02", + "wallet.webhook_urls": [ + "https://example.com/webhook/updated" + ], + "wallet.dispatch_type": "default", + "default_label": "example-label-02-updated", + "image_url": "https://aries.ca/images/sample-updated.png", + "wallet.id": "3b64ad0d-f556-4c04-92bc-cd95bfde58cd" + }, + "key_management_mode": "managed", + "updated_at": "2022-04-01T16:23:58.642004Z", + "wallet_id": "3b64ad0d-f556-4c04-92bc-cd95bfde58cd", + "created_at": "2022-04-01T15:12:35.474975Z" +} +``` + +> An Admin API Key is all that is ALLOWED to be included in a request header during an update. Inluding the Bearer token header will result in a 404: Unauthorized error + +### Remove a tenant + +The following information is required to delete a tenant: + +- wallet_id +- wallet_key +- {Admin_Api_Key} if admin is protected + +Example + +```sh +curl -X POST "${ACAPY_ADMIN_URL}/multitenancy/wallet/{wallet_id}/remove" \ + -H "Content-Type: application/json" \ + -H "x-api-key: $ACAPY_ADMIN_URL_API_KEY" \ + -d '{ "wallet_key": "example-encryption-key-02" }' +``` + +**`Response`** + +```jsonc +{} +``` + +### Per tenant settings + +To allow configurablity of ACA-Py startup parameters/environment variables at a tenant/subwallet level. [PR#2233](https://github.com/hyperledger/aries-cloudagent-python/pull/2233) will provide the ability to update the following subset of settings when creating or updating the subwallet: + +| Labels | | Setting | +|---|---|---| +| ACAPY_LOG_LEVEL | log-level | log.level | +| ACAPY_INVITE_PUBLIC | invite-public | debug.invite_public | +| ACAPY_PUBLIC_INVITES | public-invites | public_invites | +| ACAPY_AUTO_ACCEPT_INVITES | auto-accept-invites | debug.auto_accept_invites | +| ACAPY_AUTO_ACCEPT_REQUESTS | auto-accept-requests | debug.auto_accept_requests | +| ACAPY_AUTO_PING_CONNECTION | auto-ping-connection | auto_ping_connection | +| ACAPY_MONITOR_PING | monitor-ping | debug.monitor_ping | +| ACAPY_AUTO_RESPOND_MESSAGES | auto-respond-messages | debug.auto_respond_messages | +| ACAPY_AUTO_RESPOND_CREDENTIAL_OFFER | auto-respond-credential-offer | debug.auto_resopnd_credential_offer | +| ACAPY_AUTO_RESPOND_CREDENTIAL_REQUEST | auto-respond-credential-request | debug.auto_respond_credential_request | +| ACAPY_AUTO_VERIFY_PRESENTATION | auto-verify-presentation | debug.auto_verify_presentation | +| ACAPY_NOTIFY_REVOCATION | notify-revocation | revocation.notify | +| ACAPY_AUTO_REQUEST_ENDORSEMENT | auto-request-endorsement | endorser.auto_request | +| ACAPY_AUTO_WRITE_TRANSACTIONS | auto-write-transactions | endorser.auto_write | +| ACAPY_CREATE_REVOCATION_TRANSACTIONS | auto-create-revocation-transactions | endorser.auto_create_rev_reg | +| ACAPY_ENDORSER_ROLE | endorser-protocol-role | endorser.protocol_role | + +- `POST /multitenancy/wallet` + + Added `extra_settings` dict field to request schema. `extra_settings` can be configured in the request body as below: + + **`Example Request`** + ``` + { + "wallet_name": " ... ", + "default_label": " ... ", + "wallet_type": " ... ", + "wallet_key": " ... ", + "key_management_mode": "managed", + "wallet_webhook_urls": [], + "wallet_dispatch_type": "base", + "extra_settings": { + "ACAPY_LOG_LEVEL": "INFO", + "ACAPY_INVITE_PUBLIC": true, + "public-invites": true + }, + } + ``` + + ```sh + echo $new_tenant | curl -X POST "${ACAPY_ADMIN_URL}/multitenancy/wallet" \ + -H "Content-Type: application/json" \ + -H "X-Api-Key: $ACAPY_ADMIN_URL_API_KEY" \ + -d @- + ``` + +- `PUT /multitenancy/wallet/{wallet_id}` + + Added `extra_settings` dict field to request schema. + + **`Example Request`** + ``` + { + "wallet_webhook_urls": [ ... ], + "wallet_dispatch_type": "default", + "label": " ... ", + "image_url": " ... ", + "extra_settings": { + "ACAPY_LOG_LEVEL": "INFO", + "ACAPY_INVITE_PUBLIC": true, + "ACAPY_PUBLIC_INVITES": false + }, + } + ``` + + ```sh + echo $update_tenant | curl -X PUT "${ACAPY_ADMIN_URL}/multitenancy/wallet/${WALLET_ID}" \ + -H "Content-Type: application/json" \ + -H "x-api-key: $ACAPY_ADMIN_URL_API_KEY" \ + -d @- + ``` diff --git a/OutboundQueue.md b/OutboundQueue.md deleted file mode 100644 index c8d9c175f2..0000000000 --- a/OutboundQueue.md +++ /dev/null @@ -1,53 +0,0 @@ -# Outbound Queues in ACA-py - -## Background - -By default, messages often stay in ACA-py memory for long periods of time without being delivered. As a result, when the ACA-py Python process is terminated unexpectedly, messages are lost. - -But with recent changes, outbound messages can now be sent to a message queue of your choice instead of being delivered by ACA-py. This queue is external to the ACA-py process, and can be configured for the durability requirements you want. This new concept of an "outbound queue" is intended to be an optional replacement to the current ACA-py outbound transport (i.e. option `-ot`, `--outbound-transport`). - -If you run an outbound queue, you will also need to run a new service, a delivery agent, to actually deliver the message. See more details below. - -## Usage Details - -A new set of commandline options have been added to provide a way for users to "opt in" to use of the outbound queue. These new options are as follows: - -- `-oq`, `--outbound-queue`: specifies the queue connection details. -- `-oqp`, `--outbound-queue-prefix`: defines a prefix to use when generating the topic key. -- `-oqc`, `--outbound-queue-class`: specify the location of a custom queue class. - -Only the first, `--outbound-queue`, is required if you would like to opt into the outbound queue to replace `--outbound-transport`. The input for this option takes the form `[protocol]://[host]:[port]`. So for example, if the queue I want to use is Redis, on host `myredis.mydomain.com` using the default port for Redis, the string would be as follows: `redis://myredis.mydomain.com:6379` - -The second option, `--outbound-queue-prefix`, specifies the queue topic prefix. The queue topic is generated in the following form: `{prefix}.outbound_transport`. The default value for this commandline option is the value `acapy`, so a queue key of `acapy.outbound_transport` is generated in the case of the default settings. ACA-py will send messages to the queue using this generated key as the topic. - -The third option, `--outbound-queue-class`, specifies the queue backend. By default, this is `aries_cloudagent.transport.outbound.queue.redis:RedisOutboundQueue`, which specifies ACA-py's builtin Redis `LIST` backend. Users can define their own class, inheriting from `BaseOutboundQueue`, to implement a queue backend of their choice. This commandline option is the official entrypoint of ACA-py's pluggable queue interface. Developers must specify a Python dotpath to a module importable in the current `PYTHONPATH`, followed by a colon, followed by the name of their custom class. - -## Delivery Agent - -When using `--outbound-queue` instead of `--outbound-transport`, ACA-py no longer delivers the messages to destinations. Instead, a delivery service ([a prototype can be found here](https://github.com/andrewwhitehead/aca-deliver)) would need to be run. This service should pick up a message from the queue and then deliver that message. - -When running `--outbound-queue`, ACA-py serializes messages to be sent to the queue by using MessagePack. MessagePack is a protocol to serialize content into a compact binary format. ACA-py generates keys in MessagePack as follows: -- `endpoint` - specifies the endpoint for the message. -- `headers` - specifies a set of key-value pairs representing message headers. -- `payload` - the raw binary content of the message. - -The delivery service will need to deserialize the binary content on the consuming end. The result will then be a key-value data structure (for example, a `dict` in Python). So the deseralized message, deserialized into a Python `dict` for example, would be in the following form: -``` -{ - "headers": {"Content-Type": "..."}, - "endpoint": "...", - "payload": "..." -} -``` -The delivery agent should process this message and deliver it to the recipient as appropriate. - -## Backend-Specific Notes - -### Redis - -Value for `--outbound-queue-class` to use this backend: -- `aries_cloudagent.transport.outbound.queue.redis:RedisOutboundQueue` - -This is a queue backend, using the `LIST` data type in Redis. When using Redis, the delivery service consuming this queue in order to send outbound messages over transport will need to pop from the left side of the queue (i.e. the Redis `LPOP` command) to get messages in the order they were sent. - -Users will need to configure [Redis persistence](https://redis.io/topics/persistence) to gain message durability benefits in their Redis deployment. Redis by default runs entirely in-memory, so it is subject to the same data loss characteristics as ACA-py unless you also configure it to run in persistence mode. diff --git a/PUBLISHING.md b/PUBLISHING.md index 9f97266bad..828811df2d 100644 --- a/PUBLISHING.md +++ b/PUBLISHING.md @@ -4,27 +4,135 @@ The code to be published should be in the `main` branch. Make sure that all the merged, and decide on the release tag. Should it be a release candidate or the final tag, and should it be a major, minor or patch release, per [semver](https://semver.org/) rules. -Once ready to do a release, create a PR that includes the following updates: +Once ready to do a release, create a local branch that includes the following updates: -1. Update the CHANGELOG.md to add the new release. If transitioning for a Release Candidate to the final release for the tag, do not create a new section -- just drop the "RC" designation. +1. Create a PR branch from an updated `main` branch. -2. include details of the closed PRs included in this release. General process to follow: +2. Update the CHANGELOG.md to add the new release. Only create a new section when working on the first release candidate for a new release. When transitioning from one release candidate to the next, or to an official release, just update the title and date of the change log section. -- Gather the set of PRs since the last release and put them into a list. - - An example query to use to get the list of PRs is: [https://github.com/hyperledger/aries-cloudagent-python/pulls?q=is%3Apr+is%3Amerged+sort%3Aupdated+merged%3A%3E2021-11-15](https://github.com/hyperledger/aries-cloudagent-python/pulls?q=is%3Apr+is%3Amerged+sort%3Aupdated+merged%3A%3E2021-11-15), where the date at the end is the date of the previous release. - - Organize the list into suitable categories, update (if necessary) the PR description and add notes to clarify the changes. - - Add a link to each PR on the PR number. - - A regular expression you can use in VS Code to add the links to the list (assuming each line ends with the PR number) is `#([0-9]*)` (find) and `[#$1](https://github.com/hyperledger/aries-cloudagent-python/pull/$1)` (replace). Use regular expressions in the search, highlight the list and choose "Find in Selection" before replacing. - - Add a narrative about the release above the PR that highlights what has gone into the release. +3. Include details of the merged PRs included in this release. General process to follow: -3. Update the ReadTheDocs in the `/docs` folder by following the instructions in the `docs/README.md` file. That will likely add a number of new and modified files to the PR. Eliminate all of the errors in the generation process, either by mocking external dependencies or by fixing ACA-Py code. If necessary, create an issue with the errors and assign it to the appropriate developer. Experience has demonstrated to use that documentation generation errors should be fixed in the code. +- Gather the set of PRs since the last release and put them into a list. A good + tool to use for this is the + [github-changelog-generator](https://github.com/github-changelog-generator/github-changelog-generator). + Steps: + - Create a read only GitHub token for your account on this page: + [https://github.com/settings/tokens](https://github.com/settings/tokens/new?description=GitHub%20Changelog%20Generator%20token) + with a scope of `repo` / `public_repo`. + - Use a command like the following, adjusting the tag parameters as + appropriate. `docker run -it --rm -v "$(pwd)":/usr/local/src/your-app + githubchangeloggenerator/github-changelog-generator --user hyperledger + --project aries-cloudagent-python --output 0.7.4-rc0.md --since-tag 0.7.3 + --future-release 0.7.4-rc0 --release-branch main --token ` + - In the generated file, use only the PR list -- we don't include the list of + closed issues in the Change Log. -4. Update the version number listed in [aries_cloudagent/version.py](aries_cloudagent/version.py) and, prefixed with a "v" in [open-api/openapi.json](open-api/openapi.json) (e.g. "v0.7.2" in the openapi.json file, and "0.7.2" in the version.py file). The incremented version number should adhere to the [Semantic Versioning Specification](https://semver.org/#semantic-versioning-specification-semver) based on the changes since the last published release. For Release Candidates, the form of the tag is "0.7.2-rc0". +In some cases, the approach above fails because of too many API calls. An +alternate approach to getting the list of PRs in the right format is to use this +scary `sed` pipeline process to get the same output.¥ + +- Put the following commands into a file called `changelog.sed` + +``` bash +/Approved/d +/updated /d +/^$/d +/^ [0-9]/d +s/was merged.*// +/^@/d +s# by \(.*\) # [\1](https://github.com/\1)# +s/^ // +s# \#\([0-9]*\)# [\#\1](https://github.com/hyperledger/aries-cloudagent-python/pull/\1) # +s/ / /g +/^Version/d +/tasks done/d +s/^/- / +``` + +- Navigate in your browser to the paged list of PRs merged since the last + release (using in the GitHub UI a filter such as `is:pr is:merged sort:updated + merged:>2022-04-07`) and for each page, highlight, and copy the text + of only the list of PRs on the page to use in the following step. +- For each page, run the command + `sed -e :a -e '$!N;s/\n#/ #/;ta' -e 'P;D' < [![pypi releases](https://img.shields.io/pypi/v/aries_cloudagent)](https://pypi.org/project/aries-cloudagent/) -[![CircleCI](https://circleci.com/gh/hyperledger/aries-cloudagent-python.svg?style=shield)](https://circleci.com/gh/hyperledger/aries-cloudagent-python) [![codecov](https://codecov.io/gh/hyperledger/aries-cloudagent-python/branch/main/graph/badge.svg)](https://codecov.io/gh/hyperledger/aries-cloudagent-python) @@ -10,7 +9,7 @@ ## Overview -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)](https://trustoverip.org/wp-content/uploads/sites/98/2020/05/toip_050520_primer.pdf) using [DIDComm messaging](https://github.com/hyperledger/aries-rfcs/tree/main/concepts/0005-didcomm) and [Hyperledger Aries](https://www.hyperledger.org/use/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. +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)](https://trustoverip.org/wp-content/uploads/2020/05/toip_050520_primer.pdf) using [DIDComm messaging](https://github.com/hyperledger/aries-rfcs/tree/main/concepts/0005-didcomm) and [Hyperledger Aries](https://www.hyperledger.org/use/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. ACA-Py is built on the Aries concepts and features that make up [Aries Interop Profile (AIP) 1.0](https://github.com/hyperledger/aries-rfcs/tree/main/concepts/0302-aries-interop-profile#aries-interop-profile-version-10), and most of the features in [AIP 2.0](https://github.com/hyperledger/aries-rfcs/tree/main/concepts/0302-aries-interop-profile#aries-interop-profile-version-20). [ACA-Py’s supported Aries protocols](https://github.com/hyperledger/aries-cloudagent-python/blob/main/SupportedRFCs.md) include, most importantly, protocols for issuing, verifying, and holding verifiable credentials using both [Hyperledger Indy AnonCreds](https://hyperledger-indy.readthedocs.io/projects/sdk/en/latest/docs/design/002-anoncreds/README.html) verifiable credential format, and the [W3C Standard Verifiable Credential](https://www.w3.org/TR/vc-data-model/) format using JSON-LD with LD-Signatures and BBS+ Signatures. @@ -66,6 +65,8 @@ There is an [architectural deep dive webinar](https://www.youtube.com/watch?v=FX ![drawing](./aca-py_architecture.png) +You can extend Aca-Py using plug-ins, which can be loaded at runtime. Plug-ins are mentioned in the [webinar](https://docs.google.com/presentation/d/1K7qiQkVi4n-lpJ3nUZY27OniUEM0c8HAIk4imCWCx5Q/edit#slide=id.g5d43fe05cc_0_145) and are [described in more detail here](/docs/GettingStartedAriesDev/PlugIns.md). + ### Installation and Usage An ["install and go" page for developers](https://github.com/hyperledger/aries-cloudagent-python/blob/main/DevReadMe.md) is available if you are comfortable with Trust over IP and Aries concepts. ACA-Py can be run with Docker without installation (highly recommended), or can be installed [from PyPi](https://pypi.org/project/aries-cloudagent/). In the [/demo directory](/demo) there is a full set of demos for developers to use in getting started, and the [demo read me](/demo/README.md) is a great starting point for developers to use an "in-browser" approach to run a zero-install example. The [Read the Docs](https://aries-cloud-agent-python.readthedocs.io/en/latest/) overview is also a way to reference the modules and APIs that make up an ACA-Py instance. @@ -78,6 +79,19 @@ An ACA-Py instance puts together an OpenAPI-documented REST interface based on t Technical note: the administrative API exposed by the agent for the controller to use must be protected with an API key (using the --admin-api-key command line arg) or deliberately left unsecured using the --admin-insecure-mode command line arg. The latter should not be used other than in development if the API is not otherwise secured. +## Troubleshooting + +There are a number of resources for getting help with ACA-Py and troubleshooting +any problems you might run into. The [Troubleshooting](Troubleshooting.md) +document contains some guidance about issues that have been experienced in the +past. Feel free to submit PRs to supplement the troubleshooting document! +Searching the [ACA-Py GitHub +issues](https://github.com/hyperledger/aries-cloudagent-python/issues) will +often uncover challenges that others have experienced, often with answers to +solving those challenges. As well, there is the "aries-cloudagent-python" +channel on the Hyperledger Discord chat server ([invitation +here](https://discord.gg/hyperledger)). + ## Credit The initial implementation of ACA-Py was developed by the Government of British Columbia’s Digital Trust Team in Canada. To learn more about what’s happening with decentralized identity and digital trust in British Columbia, a new website will be launching and the link will be made available here. diff --git a/RedisPlugins.md b/RedisPlugins.md new file mode 100644 index 0000000000..da4ee090bb --- /dev/null +++ b/RedisPlugins.md @@ -0,0 +1,266 @@ +# ACA-Py Redis Plugins +# [aries-acapy-plugin-redis-events](https://github.com/bcgov/aries-acapy-plugin-redis-events/blob/master/README.md) [`redis_queue`] + + +It provides a mechansim to persists both inbound and outbound messages using redis, deliver messages and webhooks, and dispatch events. + +More details can be found [here](https://github.com/bcgov/aries-acapy-plugin-redis-events/blob/master/README.md). + +### Plugin configuration [`yaml`] +``` +redis_queue: + connection: + connection_url: "redis://default:test1234@172.28.0.103:6379" + + ### For Inbound ### + inbound: + acapy_inbound_topic: "acapy_inbound" + acapy_direct_resp_topic: "acapy_inbound_direct_resp" + + ### For Outbound ### + outbound: + acapy_outbound_topic: "acapy_outbound" + mediator_mode: false + + ### For Event ### + event: + event_topic_maps: + ^acapy::webhook::(.*)$: acapy-webhook-$wallet_id + ^acapy::record::([^:]*)::([^:]*)$: acapy-record-with-state-$wallet_id + ^acapy::record::([^:])?: acapy-record-$wallet_id + acapy::basicmessage::received: acapy-basicmessage-received + acapy::problem_report: acapy-problem_report + acapy::ping::received: acapy-ping-received + acapy::ping::response_received: acapy-ping-response_received + acapy::actionmenu::received: acapy-actionmenu-received + acapy::actionmenu::get-active-menu: acapy-actionmenu-get-active-menu + acapy::actionmenu::perform-menu-action: acapy-actionmenu-perform-menu-action + acapy::keylist::updated: acapy-keylist-updated + acapy::revocation-notification::received: acapy-revocation-notification-received + acapy::revocation-notification-v2::received: acapy-revocation-notification-v2-received + acapy::forward::received: acapy-forward-received + event_webhook_topic_maps: + acapy::basicmessage::received: basicmessages + acapy::problem_report: problem_report + acapy::ping::received: ping + acapy::ping::response_received: ping + acapy::actionmenu::received: actionmenu + acapy::actionmenu::get-active-menu: get-active-menu + acapy::actionmenu::perform-menu-action: perform-menu-action + acapy::keylist::updated: keylist + deliver_webhook: true +``` +- `redis_queue.connection.connection_url`: This is required and is expected in `redis://{username}:{password}@{host}:{port}` format. +- `redis_queue.inbound.acapy_inbound_topic`: This is the topic prefix for the inbound message queues. Recipient key of the message are also included in the complete topic name. The final topic will be in the following format `acapy_inbound_{recip_key}` +- `redis_queue.inbound.acapy_direct_resp_topic`: Queue topic name for direct responses to inbound message. +- `redis_queue.outbound.acapy_outbound_topic`: Queue topic name for the outbound messages. Used by Deliverer service to deliver the payloads to specified endpoint. +- `redis_queue.outbound.mediator_mode`: Set to true, if using Redis as a http bridge when setting up a mediator agent. By default, it is set to false. +- `event.event_topic_maps`: Event topic map +- `event.event_webhook_topic_maps`: Event to webhook topic map +- `event.deliver_webhook`: When set to true, this will deliver webhooks to endpoints specified in `admin.webhook_urls`. By default, set to true. + +### Usage + +#### With Docker +Running the plugin with docker is simple. An +example [docker-compose.yml](https://github.com/bcgov/aries-acapy-plugin-redis-events/blob/master/docker/docker-compose.yml) file is available which launches both ACA-Py with redis and an accompanying Redis cluster. + +```sh +$ docker-compose up --build -d +``` +More details can be found [here](https://github.com/bcgov/aries-acapy-plugin-redis-events/blob/master/docker/README.md). + +#### Without Docker +Installation +``` +pip install git+https://github.com/bcgov/aries-acapy-plugin-redis-events.git +``` +Startup ACA-Py with `redis_queue` plugin loaded +``` +docker network create --subnet=172.28.0.0/24 `network_name` +export REDIS_PASSWORD=" ... As specified in redis_cluster.conf ... " +export NETWORK_NAME="`network_name`" +aca-py start \ + --plugin redis_queue.v1_0.events \ + --plugin-config plugins-config.yaml \ + -it redis_queue.v1_0.inbound redis 0 -ot redis_queue.v1_0.outbound + # ... the remainder of your startup arguments +``` + +Regardless of the options above, you will need to startup `deliverer` and `relay`/`mediator` service as a bridge to receive inbound messages. Consider the following to build your `docker-compose` file which should also start up your redis cluster: +- Relay + Deliverer + ``` + relay: + image: redis-relay + build: + context: .. + dockerfile: redis_relay/Dockerfile + ports: + - 7001:7001 + - 80:80 + environment: + - REDIS_SERVER_URL=redis://default:test1234@172.28.0.103:6379 + - TOPIC_PREFIX=acapy + - STATUS_ENDPOINT_HOST=0.0.0.0 + - STATUS_ENDPOINT_PORT=7001 + - STATUS_ENDPOINT_API_KEY=test_api_key_1 + - INBOUND_TRANSPORT_CONFIG=[["http", "0.0.0.0", "80"]] + - TUNNEL_ENDPOINT=http://relay-tunnel:4040 + - WAIT_BEFORE_HOSTS=15 + - WAIT_HOSTS=redis-node-3:6379 + - WAIT_HOSTS_TIMEOUT=120 + - WAIT_SLEEP_INTERVAL=1 + - WAIT_HOST_CONNECT_TIMEOUT=60 + depends_on: + - redis-cluster + - relay-tunnel + networks: + - acapy_default + deliverer: + image: redis-deliverer + build: + context: .. + dockerfile: redis_deliverer/Dockerfile + ports: + - 7002:7002 + environment: + - REDIS_SERVER_URL=redis://default:test1234@172.28.0.103:6379 + - TOPIC_PREFIX=acapy + - STATUS_ENDPOINT_HOST=0.0.0.0 + - STATUS_ENDPOINT_PORT=7002 + - STATUS_ENDPOINT_API_KEY=test_api_key_2 + - WAIT_BEFORE_HOSTS=15 + - WAIT_HOSTS=redis-node-3:6379 + - WAIT_HOSTS_TIMEOUT=120 + - WAIT_SLEEP_INTERVAL=1 + - WAIT_HOST_CONNECT_TIMEOUT=60 + depends_on: + - redis-cluster + networks: + - acapy_default + ``` +- Mediator + Deliverer + ``` + mediator: + image: acapy-redis-queue + build: + context: .. + dockerfile: docker/Dockerfile + ports: + - 3002:3001 + depends_on: + - deliverer + volumes: + - ./configs:/home/indy/configs:z + - ./acapy-endpoint.sh:/home/indy/acapy-endpoint.sh:z + environment: + - WAIT_BEFORE_HOSTS=15 + - WAIT_HOSTS=redis-node-3:6379 + - WAIT_HOSTS_TIMEOUT=120 + - WAIT_SLEEP_INTERVAL=1 + - WAIT_HOST_CONNECT_TIMEOUT=60 + - TUNNEL_ENDPOINT=http://mediator-tunnel:4040 + networks: + - acapy_default + entrypoint: /bin/sh -c '/wait && ./acapy-endpoint.sh poetry run aca-py "$$@"' -- + command: start --arg-file ./configs/mediator.yml + + deliverer: + image: redis-deliverer + build: + context: .. + dockerfile: redis_deliverer/Dockerfile + depends_on: + - redis-cluster + ports: + - 7002:7002 + environment: + - REDIS_SERVER_URL=redis://default:test1234@172.28.0.103:6379 + - TOPIC_PREFIX=acapy + - STATUS_ENDPOINT_HOST=0.0.0.0 + - STATUS_ENDPOINT_PORT=7002 + - STATUS_ENDPOINT_API_KEY=test_api_key_2 + - WAIT_BEFORE_HOSTS=15 + - WAIT_HOSTS=redis-node-3:6379 + - WAIT_HOSTS_TIMEOUT=120 + - WAIT_SLEEP_INTERVAL=1 + - WAIT_HOST_CONNECT_TIMEOUT=60 + networks: + - acapy_default + ``` + +Both relay and mediator [demos](https://github.com/bcgov/aries-acapy-plugin-redis-events/tree/master/demo) are also available. + +# [aries-acapy-cache-redis](https://github.com/Indicio-tech/aries-acapy-cache-redis/blob/main/README.md) [`redis_cache`] + + +ACA-Py uses a modular cache layer to story key-value pairs of data. The purpose +of this plugin is to allow ACA-Py to use Redis as the storage medium for it's +caching needs. + +More details can be found [here](https://github.com/Indicio-tech/aries-acapy-cache-redis/blob/main/README.md). + +### Plugin configuration [`yaml`] +``` +redis_cache: + connection: "redis://default:test1234@172.28.0.103:6379" + max_connection: 50 + credentials: + username: "default" + password: "test1234" + ssl: + cacerts: ./ca.crt +``` +- `redis_cache.connection`: This is required and is expected in `redis://{username}:{password}@{host}:{port}` format. +- `redis_cache.max_connection`: Maximum number of redis pool connections. Default: 50 +- `redis_cache.credentials.username`: Redis instance username +- `redis_cache.credentials.password`: Redis instance password +- `redis_cache.ssl.cacerts` + +### Usage + +#### With Docker +- Running the plugin with docker is simple and straight-forward. There is an +example [docker-compose.yml](https://github.com/Indicio-tech/aries-acapy-cache-redis/blob/main/docker-compose.yml) file in the root of the +project that launches both ACA-Py and an accompanying Redis instance. Running +it is as simple as: + + ```sh + $ docker-compose up --build -d + ``` + +- To launch ACA-Py with an accompanying redis cluster of 6 nodes [3 primaries and 3 replicas], please refer to example [docker-compose.cluster.yml](https://github.com/Indicio-tech/aries-acapy-cache-redis/blob/main/docker-compose.cluster.yml) and run the following: + + Note: Cluster requires external docker network with specified subnet + + ```sh + $ docker network create --subnet=172.28.0.0/24 `network_name` + $ export REDIS_PASSWORD=" ... As specified in redis_cluster.conf ... " + $ export NETWORK_NAME="`network_name`" + $ docker-compose -f docker-compose.cluster.yml up --build -d + ``` +#### Without Docker +Installation +``` +pip install git+https://github.com/Indicio-tech/aries-acapy-cache-redis.git +``` +Startup ACA-Py with `redis_cache` plugin loaded +``` +aca-py start \ + --plugin acapy_cache_redis.v0_1 \ + --plugin-config plugins-config.yaml \ + # ... the remainder of your startup arguments +``` +or +``` +aca-py start \ + --plugin acapy_cache_redis.v0_1 \ + --plugin-config-value "redis_cache.connection=redis://redis-host:6379/0" \ + --plugin-config-value "redis_cache.max_connections=90" \ + --plugin-config-value "redis_cache.credentials.username=username" \ + --plugin-config-value "redis_cache.credentials.password=password" \ + # ... the remainder of your startup arguments +``` +## RedisCluster + +If you startup a redis cluster and an ACA-Py agent loaded with either `redis_queue` or `redis_cache` plugin or both, then during the initialization of the plugin, it will bind an instance of `redis.asyncio.RedisCluster` [onto the `root_profile`]. Other plugin will have access to this redis client for it's functioning. This is done for efficiency and to avoid duplication of resources. diff --git a/SupportedRFCs.md b/SupportedRFCs.md index d54a138269..adaa7ba54f 100644 --- a/SupportedRFCs.md +++ b/SupportedRFCs.md @@ -6,12 +6,12 @@ and an overview of the ACA-Py feature set. This document is manually updated and as such, may not be up to date with the most recent release of ACA-Py or the repository `main` branch. Reminders (and PRs!) to update this page are welcome! If you have any questions, please contact us on the #aries channel on -[Hyperledger Rocketchat](https://chat.hyperledger.org) or through an issue in this repo. +[Hyperledger Discord](https://discord.gg/hyperledger) or through an issue in this repo. **Last Update**: 2021-12-22, Release 0.7.3 > The checklist version of this document was created as a joint effort -> between [Northern Block](https://northernblock.io/) and [Animo Solutions](https://animo.id/). +> between [Northern Block](https://northernblock.io/), [Animo Solutions](https://animo.id/) and the Ontario government, on behalf of the Ontario government. ## AIP Support and Interoperability @@ -45,10 +45,11 @@ A summary of the Aries Interop Profiles and Aries RFCs supported in ACA-Py can b | Issuer | :white_check_mark: | | | Holder | :white_check_mark: | | | Verifier | :white_check_mark: | | -| Mediator Service | :white_check_mark: | Coming Soon: An `aries-mediator-service` repository that is a pre-configured, production ready Aries Mediator Service based on a released version of ACA-Py. | +| Mediator Service | :white_check_mark: | See the [aries-mediator-service](https://github.com/hyperledger/aries-mediator-service), a pre-configured, production ready Aries Mediator Service based on a released version of ACA-Py. | | Mediator Client | :white_check_mark: | | Indy Transaction Author | :white_check_mark: | | | Indy Transaction Endorser | :white_check_mark: | | +| Indy Endorser Service | :construction: | Help Wanted! See the [aries-endorser-service](https://github.com/bcgov/aries-endorser-service), an under-construction, pre-configured, production ready Aries Endorser Service based on a released version of ACA-Py. On completion, we expect this repository to be moved into the Hyperledger GitHub organization. | ## Credential Types @@ -65,13 +66,14 @@ A summary of the Aries Interop Profiles and Aries RFCs supported in ACA-Py can b | `did:web` | :white_check_mark: | Resolution only | | `did:key` | :white_check_mark: | | | `did:peer` | :warning:| AIP 1.0-based `did:peer` DIDs are used, meaning the DIDs are not prefixed with `did:peer` and are not following the conventions of AIP 2.0's [RFC 0627: Static Peer DIDs](https://github.com/hyperledger/aries-rfcs/tree/b3a3942ef052039e73cd23d847f42947f8287da2/features/0627-static-peer-dids) | +| Universal Resolver | :construction: | A [plug in](https://github.com/sicpa-dlab/acapy-resolver-universal) from [SICPA](https://www.sicpa.com/) is available that can be added to an ACA-Py installation to support a [universal resolver](https://dev.uniresolver.io/) capability, providing support for most DID methods in the [W3C DID Method Registry](https://w3c.github.io/did-spec-registries/#did-methods). | ## Secure Storage Types | Secure Storage Types | Supported | Notes | --- | :--: | -- | +| [Aries Askar](https://github.com/hyperledger/aries-askar) | :white_check_mark: | Recommended - Aries Askar provides equivalent/evolved secure storage and cryptography support to the "indy-wallet" part of the Indy SDK. When using Askar (via the `--wallet-type askar` startup parameter), other Indy SDK functionality is handled by [Indy Shared RS](https://github.com/hyperledger/indy-shared-rs) (AnonCreds) and [Indy VDR](https://github.com/hyperledger/indy-vdr) (Indy ledger interactions). | | [Indy SDK "indy-wallet"](https://github.com/hyperledger/indy-sdk/tree/master/docs/design/003-wallet-storage) | :white_check_mark: | Full support for the features of the "indy-wallet" secure storage capabilities found in the Indy SDK. | -| [Aries Askar](https://github.com/hyperledger/aries-askar) | :warning: | Aries Askar provides equivalent/evolved secure storage and cryptography support to the "indy-wallet" part of the Indy SDK. Available in ACA-Py (activated using a startup parameters but not yet widely used. When using Askar, other Indy SDK capabilities are handled by [Indy Shared RS](https://github.com/hyperledger/indy-shared-rs) (AnonCreds) and [Indy VDR](https://github.com/hyperledger/indy-vdr) (Indy ledger interactions). | ## Miscellaneous Features @@ -86,8 +88,8 @@ A summary of the Aries Interop Profiles and Aries RFCs supported in ACA-Py can b | Connection-less (OOB protocol / AIP 2.0) | :white_check_mark: | Only for present proof | | Signed Attachments | :white_check_mark: | Used for OOB | | Multi Indy ledger support (with automatic detection) | :white_check_mark: | Support added in the 0.7.3 Release. | -| Persistence of mediated messages | :construction: | Messages are currently stored in an in-memory and so are subject to loss in the case of a sudden termination of an ACA-Py process. The in-memory queue is properly handled in the case of a graceful shutdown of an ACA-Py process (e.g. processing of the queue completes and no new messages are accepted). Work is underway to add useful external queues handling, including support for multiple external queue implementations (e.g., redis and kafka). | -| Storage Import & Export | :warning: | Supported by directly interacting with the indy-sdk or Aries Askar (e.g., no Admin API endpoint available for wallet import & export). Aries Askar support includes the ability to import storage exported from the Indy SDK's "indy-wallet" component. | +| Persistence of mediated messages | :construction: | Work is mostly complete to add external, persistent queue handling, including support for multiple external queue implementations (notably, plugins for [Redis](https://github.com/bcgov/aries-acapy-plugin-redis-events) and [Kafka](https://github.com/sicpa-dlab/aries-acapy-plugin-kafka-events)). Documentation for that is being worked on. Without persistent queue support, messages are stored in an in-memory queue and so are subject to loss in the case of a sudden termination of an ACA-Py process. The in-memory queue is properly handled in the case of a graceful shutdown of an ACA-Py process (e.g. processing of the queue completes and no new messages are accepted). | +| Storage Import & Export | :warning: | Supported by directly interacting with the indy-sdk or Aries Askar (e.g., no Admin API endpoint available for wallet import & export). Aries Askar support includes the ability to import storage exported from the Indy SDK's "indy-wallet" component. However, a full migration approach from a production ACA-Py using the Indy-SDK storage to use Aries Askar storage has not been implemeted and documented. | ## Supported RFCs @@ -109,12 +111,9 @@ are fully supported in ACA-Py **EXCEPT** as noted in the table below. | RFC | Supported | Notes | --- | :--: | -- | | [0023-did-exchange](https://github.com/hyperledger/aries-rfcs/tree/b3a3942ef052039e73cd23d847f42947f8287da2/features/0023-did-exchange) | :warning: | Not using DIDDoc conventions yet, still using DID format of 0160-connections (which is incorrect and outdated). Also using incorrect format for `did:peer` (or not using a `did:` prefix at all) | -| [0183-revocation-notification](https://github.com/hyperledger/aries-rfcs/tree/b3a3942ef052039e73cd23d847f42947f8287da2/features/0183-revocation-notification) | :white_check_mark: | :new: This was added in release 0.7.3 and will be removed from this list with the next update. | | [0211-route-coordination](https://github.com/hyperledger/aries-rfcs/tree/b3a3942ef052039e73cd23d847f42947f8287da2/features/0211-route-coordination) | :warning: | Only pre-AIP 2.0 version. Must be updated to use `did:key` for full AIP 2.0 support | | [0317-please-ack](https://github.com/hyperledger/aries-rfcs/tree/main/features/0317-please-ack) | :x: | | | [0360-use-did-key](https://github.com/hyperledger/aries-rfcs/tree/b3a3942ef052039e73cd23d847f42947f8287da2/features/0360-use-did-key) | :warning: | Creating and resolving `did:key` DIDs is supported, but not all protocols are updated yet to use `did:key`. This is a breaking change for AIP 1.0 -> AIP 2.0. | -| [0519-goal-codes](https://github.com/hyperledger/aries-rfcs/tree/b3a3942ef052039e73cd23d847f42947f8287da2/concepts/0519-goal-codes) | :white_check_mark: | :new: This was added in release 0.7.3 and will be removed from this list with the next update. | -| [0557-discover-features-v2](https://github.com/hyperledger/aries-rfcs/tree/b3a3942ef052039e73cd23d847f42947f8287da2/features/0557-discover-features-v2) | :white_check_mark: | :new: This was added in release 0.7.3 and will be removed from this list with the next update. | | [0587-encryption-envelope-v2](https://github.com/hyperledger/aries-rfcs/tree/b3a3942ef052039e73cd23d847f42947f8287da2/features/0587-encryption-envelope-v2) | :construction: | Support for the DIDComm V2 envelope format is a work in progress, including the PRs ([AIP-2 base64url consistency](https://github.com/hyperledger/aries-cloudagent-python/pull/1188) and [Small AIP-2 updates](https://github.com/hyperledger/aries-cloudagent-python/pull/1056)) | | [0627-static-peer-dids](https://github.com/hyperledger/aries-rfcs/tree/b3a3942ef052039e73cd23d847f42947f8287da2/features/0627-static-peer-dids) | :x: | | diff --git a/Troubleshooting.md b/Troubleshooting.md new file mode 100644 index 0000000000..15660e6b45 --- /dev/null +++ b/Troubleshooting.md @@ -0,0 +1,109 @@ +# Troubleshooting Aries Cloud Agent Python + +This document contains some troubleshooting information that contributors to the +community think may be helpful. Most of the content here assumes the reader has +gotten started with ACA-Py and has arrived here because of an issue that came up +in their use of ACA-Py. + +Contributions (via pull request) to this document are welcome. Topics added here +will mostly come from reported issues that contributors think would be helpful +to the larger community. + +## Table of Contents + +- [Unable to Connect to Ledger](#unable-to-connect-to-ledger) + - [Local ledger running?](#local-ledger-running) + - [Any Firewalls](#any-firewalls) +- [Damaged, Unpublishable Revocation Registry](#damaged-unpublishable-revocation-registry) + +## Unable to Connect to Ledger + +The most common issue hit by first time users is getting an error on startup "unable to connect to ledger". Here are a list of things to check when you see that error. + +### Local ledger running? + +Unless you specify via startup parameters or environment variables that you are using a public Hyperledger Indy ledger, ACA-Py assumes that you are running a local ledger -- an instance of [von-network](https://github.com/bcgov/von-network). +If that is the cause -- have you started your local ledger, and did it startup properly. Things to check: + +- Any errors in the startup of von-network? +- Is the von-network webserver (usually at `https:/localhost:9000`) accessible? If so, can you click on and see the Genesis File? +- Do you even need a local ledger? If not, you can use a public sandbox ledger, + such as the [Dev Greenlight ledger](), likely by just prefacing your ACA-Py + command with `LEDGER_URL=http://dev.greenlight.bcovrin.vonx.io`. For example, + when running the Alice-Faber demo in the [demo](demo) folder, you can run (for + example), the Faber agent using the command: + `LEDGER_URL=http://dev.greenlight.bcovrin.vonx.io ./run_demo faber` + +### Any Firewalls + +Do you have any firewalls in play that might be blocking the ports that are used by the ledger, notably 9701-9708? To access a ledger +the ACA-Py instance must be able to get to those ports of the ledger, regardless if the ledger is local or remote. + +## Damaged, Unpublishable Revocation Registry + +We have discovered that in the ACA-Py AnonCreds implementation, it is possible +to get into a state where the publishing of updates to a Revocation Registry +(RevReg) is impossible. This can happen where ACA-Py starts to publish an update +to the RevReg, but the write transaction to the Hyperledger Indy ledger fails +for some reason. When a credential revocation is published, aca-py (via indy-sdk +or askar/credx) updates the revocation state in the wallet as well as on the +ledger. The revocation state is dependant on whatever the previous revocation +state is/was, so if the ledger and wallet are mis-matched the publish will fail. +(Andrew/s PR # 1804 (merged) should mitigate but probably won't completely +eliminate this from happening). + +For example, in case we've seen, the write RevRegEntry transaction failed at the +ledger because there was a problem with accepting the TAA (Transaction Author +Agreement). Once the error occurred, the RevReg state held by the ACA-Py agent, +and the RevReg state on the ledger were different. Even after the ability to +write to the ledger was restored, the RevReg could still not be published +because of the differences in the RevReg state. Such a situation can now be +corrected, as follows: + +To address this issue, some new endpoints were added to ACA-Py in Release 0.7.4, +as follows: + +- GET `/revocation/registry//issued` - counts of the number of issued/revoked + within a registry +- GET `/revocation/registry//issued/details` - details of all credentials + issued/revoked within a registry +- GET `/revocation/registry//issued/indy_recs` - calculated rev_reg_delta from + the ledger + - This is used to compare ledger revoked vs wallet revoked credentials, which + is essentially the state of the RevReg on the ledger and in ACA-Py. Where + there is a difference, we have an error. +- PUT `/revocation/registry//fix-revocation-entry-state` - publish an update + to the RevReg state on the ledger to bring it into alignment with what is in + the ACA-Py instance. + - There is a boolean parameter (`apply_ledger_update`) to control whether the + ledger entry actually gets published so, if you are so inclined, you can + call the endpoint to see what the transaction would be, before you actually + try to do a ledger update. This will return: + - `rev_reg_delta` - same as the ".../indy_recs" endpoint + - `accum_calculated` - transaction to write to ledger + - `accum_fixed` - If `apply_ledger_update`, the transaction actually written + to the ledger + +Note that there is (currently) a backlog item to prevent the wallet and ledger +from getting out of sync (e.g. don't update the ACA-Py RevReg state if the +ledger write fails), but even after that change is made, having this ability +will be retained for use if needed. + +We originally ran into this due to the TAA acceptance getting lost when +switching to multi-ledger (as described +[here](https://github.com/hyperledger/aries-cloudagent-python/blob/main/Multiledger.md#a-special-warning-for-taa-acceptance). +Note that this is one reason how this "out of sync" scenario can occur, but +there may be others. + +We add an integration test that demonstrates/tests this issue [here](https://github.com/hyperledger/aries-cloudagent-python/blob/main/demo/features/taa-txn-author-acceptance.feature#L67). + +To run the scenario either manually or using the integration tests, you can do the following: + +- Start von-network in TAA mode: + - `./manage start --taa-sample --logs` +- Start the tails server as usual: + - `./manage start --logs` +- To run the scenario manually, start faber and let the agent know it needs to TAA-accept before doing any ledger writes: + - `./run_demo faber --revocation --taa-accept`, and then you can run through all the transactions using the Swagger page. +- To run the scenario via an integration test, run: + - `./run_bdd -t @taa_required` diff --git a/UnitTests.md b/UnitTests.md new file mode 100644 index 0000000000..bbaaa104fe --- /dev/null +++ b/UnitTests.md @@ -0,0 +1,257 @@ +# ACA-Py Unit Tests + +The following covers the Unit Testing framework in ACA-Py, how to run the tests, and how to add unit tests. + +This [video](https://youtu.be/yJ6LpAiVNFM) is a presentation of the material covered in this document by +developer @shaangill025. + +## Running unit tests in ACA-Py + +- `./scripts/run_tests` +- `./scripts/run_tests aries_clouadagent/protocols/out_of_band/v1_0/tests` +- `./scripts/run_tests_indy` includes Indy specific tests + +## Pytest + +Example: aries_cloudagent/core/tests/test_event_bus.py + +```python +@pytest.fixture +def event_bus(): + yield EventBus() + + +@pytest.fixture +def profile(): + yield async_mock.MagicMock() + + +@pytest.fixture +def event(): + event = Event(topic="anything", payload="payload") + yield event + +class MockProcessor: + def __init__(self): + self.profile = None + self.event = None + + async def __call__(self, profile, event): + self.profile = profile + self.event = event + + +@pytest.fixture +def processor(): + yield MockProcessor() +``` + +--- + +```python +def test_sub_unsub(event_bus: EventBus, processor): + """Test subscribe and unsubscribe.""" + event_bus.subscribe(re.compile(".*"), processor) + assert event_bus.topic_patterns_to_subscribers + assert event_bus.topic_patterns_to_subscribers[re.compile(".*")] == [processor] + event_bus.unsubscribe(re.compile(".*"), processor) + assert not event_bus.topic_patterns_to_subscribers +``` + +From aries_cloudagent/core/event_bus.py + +```python +class EventBus: + def __init__(self): + self.topic_patterns_to_subscribers: Dict[Pattern, List[Callable]] = {} + +def subscribe(self, pattern: Pattern, processor: Callable): + if pattern not in self.topic_patterns_to_subscribers: + self.topic_patterns_to_subscribers[pattern] = [] + self.topic_patterns_to_subscribers[pattern].append(processor) + +def unsubscribe(self, pattern: Pattern, processor: Callable): + if pattern in self.topic_patterns_to_subscribers: + try: + index = self.topic_patterns_to_subscribers[pattern].index(processor) + except ValueError: + return + del self.topic_patterns_to_subscribers[pattern][index] + if not self.topic_patterns_to_subscribers[pattern]: + del self.topic_patterns_to_subscribers[pattern] +``` + +--- + +```python +@pytest.mark.asyncio +async def test_sub_notify(event_bus: EventBus, profile, event, processor): + """Test subscriber receives event.""" + event_bus.subscribe(re.compile(".*"), processor) + await event_bus.notify(profile, event) + assert processor.profile == profile + assert processor.event == event +``` + +```python +async def notify(self, profile: "Profile", event: Event): + partials = [] + for pattern, subscribers in self.topic_patterns_to_subscribers.items(): + match = pattern.match(event.topic) + + if not match: + continue + + for subscriber in subscribers: + partials.append( + partial( + subscriber, + profile, + event.with_metadata(EventMetadata(pattern, match)), + ) + ) + + for processor in partials: + try: + await processor() + except Exception: + LOGGER.exception("Error occurred while processing event") +``` + +--- + +## asynctest + +From: aries_cloudagent/protocols/didexchange/v1_0/tests/test.manager.py + +```python +class TestDidExchangeManager(AsyncTestCase, TestConfig): + async def setUp(self): + self.responder = MockResponder() + + self.oob_mock = async_mock.MagicMock( + clean_finished_oob_record=async_mock.CoroutineMock(return_value=None) + ) + + self.route_manager = async_mock.MagicMock(RouteManager) + ... + self.profile = InMemoryProfile.test_profile( + { + "default_endpoint": "http://aries.ca/endpoint", + "default_label": "This guy", + "additional_endpoints": ["http://aries.ca/another-endpoint"], + "debug.auto_accept_invites": True, + "debug.auto_accept_requests": True, + "multitenant.enabled": True, + "wallet.id": True, + }, + bind={ + BaseResponder: self.responder, + OobMessageProcessor: self.oob_mock, + RouteManager: self.route_manager, + ... + }, + ) + ... + + async def test_receive_invitation_no_auto_accept(self): + async with self.profile.session() as session: + mediation_record = MediationRecord( + role=MediationRecord.ROLE_CLIENT, + state=MediationRecord.STATE_GRANTED, + connection_id=self.test_mediator_conn_id, + routing_keys=self.test_mediator_routing_keys, + endpoint=self.test_mediator_endpoint, + ) + await mediation_record.save(session) + with async_mock.patch.object( + self.multitenant_mgr, "get_default_mediator" + ) as mock_get_default_mediator: + mock_get_default_mediator.return_value = mediation_record + invi_rec = await self.oob_manager.create_invitation( + my_endpoint="testendpoint", + hs_protos=[HSProto.RFC23], + ) + + invitee_record = await self.manager.receive_invitation( + invi_rec.invitation, + auto_accept=False, + ) + assert invitee_record.state == ConnRecord.State.INVITATION.rfc23 +``` + +--- + +```python +async def receive_invitation( + self, + invitation: OOBInvitationMessage, + their_public_did: Optional[str] = None, + auto_accept: Optional[bool] = None, + alias: Optional[str] = None, + mediation_id: Optional[str] = None, +) -> ConnRecord: + ... + accept = ( + ConnRecord.ACCEPT_AUTO + if ( + auto_accept + or ( + auto_accept is None + and self.profile.settings.get("debug.auto_accept_invites") + ) + ) + else ConnRecord.ACCEPT_MANUAL + ) + service_item = invitation.services[0] + # Create connection record + conn_rec = ConnRecord( + invitation_key=( + DIDKey.from_did(service_item.recipient_keys[0]).public_key_b58 + if isinstance(service_item, OOBService) + else None + ), + invitation_msg_id=invitation._id, + their_label=invitation.label, + their_role=ConnRecord.Role.RESPONDER.rfc23, + state=ConnRecord.State.INVITATION.rfc23, + accept=accept, + alias=alias, + their_public_did=their_public_did, + connection_protocol=DIDX_PROTO, + ) + + async with self.profile.session() as session: + await conn_rec.save( + session, + reason="Created new connection record from invitation", + log_params={ + "invitation": invitation, + "their_role": ConnRecord.Role.RESPONDER.rfc23, + }, + ) + + # Save the invitation for later processing + ... + + return conn_rec +``` + +## Other details + +- Error catching + +```python + with self.assertRaises(DIDXManagerError) as ctx: + ... + assert " ... error ..." in str(ctx.exception) +``` + +- function.`assert_called_once_with(parameters)` + function.`assert_called_once()` + +- pytest.mark setup in `setup.cfg` + can be attributed at function or class level. Example, `@pytest.mark.indy` + +- Code coverage + ![Code coverage screenshot](https://i.imgur.com/VhNYcje.png) diff --git a/UpgradingACA-Py.md b/UpgradingACA-Py.md new file mode 100644 index 0000000000..0ad8708564 --- /dev/null +++ b/UpgradingACA-Py.md @@ -0,0 +1,98 @@ +# Upgrading ACA-Py Data + +Some releases of ACA-Py may be improved by, or even require, an upgrade when +moving to a new version. Such changes are documented in the [CHANGELOG.md], +and those with ACA-Py deployments should take note of those upgrades. This +document summarizes the upgrade system in ACA-Py. + +## Version Information and Automatic Upgrades + +The file [version.py] contains the current version of a running instance of +ACA-Py. In addition, a record is made in the ACA-Py secure storage (database) +about the "most recently upgraded" version. When deploying a new version of +ACA-Py, the [version.py] value will be higher than the version in +secure storage. When that happens, an upgrade is executed, and on successful +completion, the version is updated in secure storage to match what is +in [version.py]. + +Upgrades are defined in the [Upgrade Definition YML file]. For a given +version listed in the follow, the corresponding entry is what actions are +required when upgrading from a previous version. If a version is not listed +in the file, there is no upgrade defined for that version from its immediate +predecessor version. + +Once an upgrade is identified as needed, the process is: + +- Collect (if any) the actions to be taken to get from the version recorded in +secure storage to the current [version.py] +- Execute the actions from oldest to newest. + - If the same action is collected more than once (e.g., "Resave the +Connection Records" is defined for two different versions), perform the action +only once. +- Store the current ACA-Py version (from [version.py]) in the secure storage + database. + +## Forced Offline Upgrades + +In some cases, it may be necessary to do an offline upgrade, where ACA-Py is +taken off line temporarily, the database upgraded explicitly, and then +ACA-Py re-deployed as normal. As yet, we do not have any use cases for this, but +those deploying ACA-Py should be aware of this possibility. For example, +we may at some point need an upgrade that **MUST NOT** be executed by more +than one ACA-Py instance. In that case, a "normal" upgrade could be dangerous +for deployments on container orchestration platforms like Kubernetes. + +If the Maintainers of ACA-Py recognize a case where ACA-Py must be upgraded +while offline, a new Upgrade feature will be added that will prevent the "auto +upgrade" process from executing. See [Issue 2201] and [Pull Request 2204] for +the status of that feature. + +[Issue 2201]: https://github.com/hyperledger/aries-cloudagent-python/issues/2201 +[Pull Request 2204]: https://github.com/hyperledger/aries-cloudagent-python/pull/2204 + +Those deploying ACA-Py upgrades for production installations (forced offline or +not) should check in each [CHANGELOG.md] release entry about what upgrades (if +any) will be run when upgrading to that version, and consider how they want +those upgrades to run in their ACA-Py installation. In most cases, simply +deploying the new version should be OK. If the number of records to be upgraded +is high (such as a "resave connections" upgrade to a deployment with many, many +connections), you may want to do a test upgrade offline first, to see if there +is likely to be a service disruption during the upgrade. Plan accordingly! + +## Exceptions + +There are a couple of upgrade exception conditions to consider, as outlined +in the following sections. + +### No version in secure storage + +Versions prior to ACA-Py 0.8.1 did not automatically populate the secure storage +"version" record. That only occurred if an upgrade was explicitly executed. As +of ACA-Py 0.8.1, the version record is added immediately after the secure +storage database is created. If you are upgrading to ACA-Py 0.8.1 or later, and +there is no version record in the secure storage, ACA-Py will assume you are +running version 0.7.5, and execute the upgrades from version 0.7.5 to the +current version. The choice of 0.7.5 as the default is safe because the same +upgrades will be run on any version of ACA-Py up to and including 0.7.5, as can +be seen in the [Upgrade Definition YML file]. Thus, even if you are really +upgrading from (for example) 0.6.2, the same upgrades are needed as from 0.7.5 +to a post-0.8.1 version. + +### Forcing an upgrade + +If you need to force an upgrade from a given version of ACA-Py, a pair of +configuration options can be used together. If you specify "`--from-version +`" and "`--force-upgrade`", the `--from-version` version will override what +is found (or not) in secure storage, and the upgrade will be from that version +to the current one. For example, if you have "0.8.1" in your "secure storage" +version, and you know that the upgrade for version 0.8.1 has not been executed, +you can use the parameters `--from-version v0.7.5 --force-upgrade` to force the +upgrade on next starting an ACA-Py instance. However, given the few upgrades +defined prior to version 0.8.1, and the "[no version in secure +storage](#no-version-in-secure-storage)" handling, it is unlikely this +capability will ever be needed. We expect to deprecate and remove these +options in future (post-0.8.1) ACA-Py versions. + +[CHANGELOG.md]: https://github.com/hyperledger/aries-cloudagent-python/blob/main/CHANGELOG.md +[version.py]: https://github.com/hyperledger/aries-cloudagent-python/blob/main/aries_cloudagent/version.py +[Upgrade Definition YML file]: https://github.com/hyperledger/aries-cloudagent-python/blob/main/aries_cloudagent/commands/default_version_upgrade_config.yml \ No newline at end of file diff --git a/UsingOpenAPI.md b/UsingOpenAPI.md index f0a367d596..51621a885c 100644 --- a/UsingOpenAPI.md +++ b/UsingOpenAPI.md @@ -2,31 +2,43 @@ ACA-Py provides an OpenAPI-documented REST interface for administering the agent's internal state and initiating communication with connected agents. -The running agent provides a `Swagger User Interface` that can be browsed and used to test various scenarios manually (see the [Admin API Readme](AdminApi.md) for details). However it is often desirable to produce native language interfaces rather than coding `Controllers` using HTTP primitives. This is possible using several public code generation (codegen) tools. This page provides some suggestions based on experience with these tools when trying to generate `Typescript` wrappers. The information should be useful to those trying to generate other langauages. Updates to this page based on experience are encouraged. +The running agent provides a `Swagger User Interface` that can be browsed and used to test various scenarios manually (see the [Admin API Readme](AdminAPI.md) for details). However it is often desirable to produce native language interfaces rather than coding `Controllers` using HTTP primitives. This is possible using several public code generation (codegen) tools. This page provides some suggestions based on experience with these tools when trying to generate `Typescript` wrappers. The information should be useful to those trying to generate other langauages. Updates to this page based on experience are encouraged. -## ACA-py, OpenAPI Raw output characteristics +## ACA-Py, OpenAPI Raw Output Characteristics -ACA-Py uses [aiohttp_apispec](https://github.com/maximdanilchenko/aiohttp-apispec) tags in code to produce the OpenAPI spec file at runtime dependent on what features have been loaded. How these tags are created is documented in the [API Standard Behaviour](https://github.com/hyperledger/aries-cloudagent-python/blob/main/AdminAPI.md#api-standard-behaviour) section of the [Admin API Readme](AdminApi.md). The OpenAPI spec is available in raw, unformated form from a running ACA-py instance using a route of `http:///api/docs/swagger.json` or from the browser `Swagger User Interface` directly. +ACA-Py uses [aiohttp_apispec](https://github.com/maximdanilchenko/aiohttp-apispec) tags in code to produce the OpenAPI spec file at runtime dependent on what features have been loaded. How these tags are created is documented in the [API Standard Behaviour](https://github.com/hyperledger/aries-cloudagent-python/blob/main/AdminAPI.md#api-standard-behaviour) section of the [Admin API Readme](AdminAPI.md). The OpenAPI spec is available in raw, unformated form from a running ACA-py instance using a route of `http:///api/docs/swagger.json` or from the browser `Swagger User Interface` directly. -To help identify changes in the ACA-py Admin API over releases there is a tool that can be run located at `scripts/generate-open-api-spec`. This tool will start ACA-py, pull the `swagger.json` file, run a codegen tool and specify a language output of `json`. Apart from providing a better format to compare changes (i.e. by comparing this output to the checked in `open-api/openapi.json` version), the tool can be used to identify any non-conformance to the OpenAPI specification. At the moment `validation` is turned off via the `open-api/openAPIJSON.config` file so that warning messages are printed for non-conformance but the `json` is still output. Most of the warnings reported by `generate-open-api-spec` relate to missing `operationId` fields which results in manufactured method names being created by codgen tools. At the moment [aiohttp_apispec](https://github.com/maximdanilchenko/aiohttp-apispec) does not support adding `operationId` anotations via tags. +The ACA-py Admin API evolves across releases. To track these changes and ensure conformance with the OpenAPI specification, we provide a tool located at [`scripts/generate-open-api-spec`](scripts/generate-open-api-spec). This tool starts ACA-py, retrieves the `swagger.json` file, and runs codegen tools to generate specifications in both Swagger and OpenAPI formats with `json` language output. The output of this tool enables comparison with the checked-in `open-api/swagger.json` and `open-api/openapi.json`, and also serves as a useful resource for identifying any non-conformance to the OpenAPI specification. At the moment `validation` is turned off via the `open-api/openAPIJSON.config` file so that warning messages are printed for non-conformance but the `json` is still output. Most of the warnings reported by `generate-open-api-spec` relate to missing `operationId` fields which results in manufactured method names being created by codgen tools. At the moment [aiohttp_apispec](https://github.com/maximdanilchenko/aiohttp-apispec) does not support adding `operationId` anotations via tags. -The `generate-open-api-spec` tool was initially created to help identify issues with method parameters not being sorted, resulting in somewhat random ordering each time a codegen operation was performed. This is relevent for languages which do not have support for [named parameters](https://en.wikipedia.org/wiki/Named_parameter) such as `Javascript`. It is recomended that the `generate-open-api-spec` is run prior to each release and the resulting `open-api/openapi.json` file checked in to allow tracking of API changes over time. At the moment this process is not automated as part of the release pipeline. +The `generate-open-api-spec` tool was initially created to help identify issues with method parameters not being sorted, resulting in somewhat random ordering each time a codegen operation was performed. This is relevent for languages which do not have support for [named parameters](https://en.wikipedia.org/wiki/Named_parameter) such as `Javascript`. It is recomended that the `generate-open-api-spec` is run prior to each release and the resulting `open-api/openapi.json` file checked in to allow tracking of API changes over time. At the moment this process is not automated as part of the release pipeline. -## Generating Language Wrappers for ACA-py +## Generating Language Wrappers for ACA-Py -There are inevitably differences around `best practice` for method naming based on coding language, and indeed organisation standards. +There are inevitably differences around `best practice` for method naming based on coding language, and indeed organisation standards. -Best practice for generating ACA-Py language wrappers is to obtain the raw OpenAPI file from a configured/running ACA-Py instance and then post-process it with a merge utility to match routes and insert desired `operationId` fields. This allows greatest flexibility in conforming to external naming requirements. +Best practice for generating ACA-Py language wrappers is to obtain the raw OpenAPI file from a configured/running ACA-Py instance and then post-process it with a merge utility to match routes and insert desired `operationId` fields. This allows greatest flexibility in conforming to external naming requirements. -Two major open source code generation tools are [Swagger](https://github.com/swagger-api/swagger-codegen) and [OpenAPI Tools](https://github.com/OpenAPITools/openapi-generator). Which of these to use can be very dependent on language support required and preference for the style of code generated. +Two major open source code generation tools are [Swagger](https://github.com/swagger-api/swagger-codegen) and [OpenAPI Tools](https://github.com/OpenAPITools/openapi-generator). Which of these to use can be very dependent on language support required and preference for the style of code generated. - The [OpenAPI Tools](https://github.com/OpenAPITools/openapi-generator) was found to offer some nice features when generating `Typescript`. It creates seperate files for each class and allows use of a `.openapi-generator-ignore` file to override generation if there is a spec file issue that needs to be maintained manually. +The [OpenAPI Tools](https://github.com/OpenAPITools/openapi-generator) was found to offer some nice features when generating `Typescript`. It creates seperate files for each class and allows use of a `.openapi-generator-ignore` file to override generation if there is a spec file issue that needs to be maintained manually. - If generating code for languages that do not support [named parameters](https://en.wikipedia.org/wiki/Named_parameter) it is recommended to specify the `useSingleRequestParameter` or equivalent in your code generator of choice. The reason is that as mentioned previously, there have been instances where parameters were not sorted when output into the raw ACA-Py API spec file and this approach helps remove that risk. +If generating code for languages that do not support [named parameters](https://en.wikipedia.org/wiki/Named_parameter) it is recommended to specify the `useSingleRequestParameter` or equivalent in your code generator of choice. The reason is that as mentioned previously, there have been instances where parameters were not sorted when output into the raw ACA-Py API spec file and this approach helps remove that risk. - Another suggestion for code generation is to keep the `modelPropertyNaming` set to `original` when generating code. Although it is tempting to try and enable marshalling into standard naming formats such as `camelCase`, the reality is that the models represent what is sent on the wire and documented in the [Aries Protocol RFCS](https://github.com/hyperledger/aries-rfcs/tree/master/features). It has proven handy to be able to see code references correspond directly with protocol RFCs when debugging. It will also correspond directly with what the `model` shows when looking at the ACA-py `Swagger UI` in a browser if you need to try something out manually before coding. One final point is that on occasions it has been discovered that the code generation tools don't always get the marshalling correct in all circumstances when changing model name format. +Another suggestion for code generation is to keep the `modelPropertyNaming` set to `original` when generating code. Although it is tempting to try and enable marshalling into standard naming formats such as `camelCase`, the reality is that the models represent what is sent on the wire and documented in the [Aries Protocol RFCS](https://github.com/hyperledger/aries-rfcs/tree/master/features). It has proven handy to be able to see code references correspond directly with protocol RFCs when debugging. It will also correspond directly with what the `model` shows when looking at the ACA-py `Swagger UI` in a browser if you need to try something out manually before coding. One final point is that on occasions it has been discovered that the code generation tools don't always get the marshalling correct in all circumstances when changing model name format. +## Existing Language Wrappers for ACA-Py +### Python +- [Aries Cloud Controller (PyPi)](https://pypi.org/project/aries-cloudcontroller/) + - [Aries Cloud Controller Python (GitHub / didx-xyz)](https://github.com/didx-xyz/aries-cloudcontroller-python) +- [Traction (GitHub / bcgov)](https://github.com/bcgov/traction) +- [acapy-client (GitHub / Indicio-tech)](https://github.com/Indicio-tech/acapy-client) +### Go +- [go-acapy-client (GitHub / Idej)](https://github.com/ldej/go-acapy-client) + +### Java + +- [ACA-PY Java Client Library (GitHub / hyperledger-labs)](https://github.com/hyperledger-labs/acapy-java-client) diff --git a/aries_cloudagent/admin/request_context.py b/aries_cloudagent/admin/request_context.py index 1fe7f79076..c7a64a11b0 100644 --- a/aries_cloudagent/admin/request_context.py +++ b/aries_cloudagent/admin/request_context.py @@ -23,11 +23,15 @@ def __init__( profile: Profile, *, context: InjectionContext = None, - settings: Mapping[str, object] = None + settings: Mapping[str, object] = None, + root_profile: Profile = None, + metadata: dict = None ): """Initialize an instance of AdminRequestContext.""" self._context = (context or profile.context).start_scope("admin", settings) self._profile = profile + self._root_profile = root_profile + self._metadata = metadata @property def injector(self) -> Injector: @@ -39,6 +43,16 @@ def profile(self) -> Profile: """Accessor for the associated `Profile` instance.""" return self._profile + @property + def root_profile(self) -> Optional[Profile]: + """Accessor for the associated root_profile instance.""" + return self._root_profile + + @property + def metadata(self) -> dict: + """Accessor for the associated metadata.""" + return self._metadata + @property def settings(self) -> Settings: """Accessor for the context settings.""" diff --git a/aries_cloudagent/admin/server.py b/aries_cloudagent/admin/server.py index 01b3b2196f..91dcaf94a7 100644 --- a/aries_cloudagent/admin/server.py +++ b/aries_cloudagent/admin/server.py @@ -4,9 +4,10 @@ from hmac import compare_digest import logging import re -from typing import Callable, Coroutine +from typing import Callable, Coroutine, Optional, Pattern, Sequence, cast import uuid import warnings +import weakref from aiohttp import web from aiohttp_apispec import ( @@ -52,6 +53,7 @@ "acapy::actionmenu::received": "actionmenu", "acapy::actionmenu::get-active-menu": "get-active-menu", "acapy::actionmenu::perform-menu-action": "perform-menu-action", + "acapy::keylist::updated": "keylist", } @@ -115,17 +117,26 @@ def __init__( """ super().__init__(**kwargs) - self._profile = profile + # Weakly hold the profile so this reference doesn't prevent profiles + # from being cleaned up when appropriate. + # Binding this AdminResponder to the profile's context creates a circular + # reference. + self._profile = weakref.ref(profile) self._send = send - async def send_outbound(self, message: OutboundMessage) -> OutboundSendStatus: + async def send_outbound( + self, message: OutboundMessage, **kwargs + ) -> OutboundSendStatus: """ Send outbound message. Args: message: The `OutboundMessage` to be sent """ - return await self._send(self._profile, message) + profile = self._profile() + if not profile: + raise RuntimeError("weakref to profile has expired") + return await self._send(profile, message) async def send_webhook(self, topic: str, payload: dict): """ @@ -139,7 +150,10 @@ async def send_webhook(self, topic: str, payload: dict): "responder.send_webhook is deprecated; please use the event bus instead.", DeprecationWarning, ) - await self._profile.notify("acapy::webhook::" + topic, payload) + profile = self._profile() + if not profile: + raise RuntimeError("weakref to profile has expired") + await profile.notify("acapy::webhook::" + topic, payload) @property def send_fn(self) -> Coroutine: @@ -151,14 +165,10 @@ def send_fn(self) -> Coroutine: async def ready_middleware(request: web.BaseRequest, handler: Coroutine): """Only continue if application is ready to take work.""" - if ( - str(request.rel_url).rstrip("/") - in ( - "/status/live", - "/status/ready", - ) - or request.app._state.get("ready") - ): + if str(request.rel_url).rstrip("/") in ( + "/status/live", + "/status/ready", + ) or request.app._state.get("ready"): try: return await handler(request) except (LedgerConfigError, LedgerTransactionError) as e: @@ -253,9 +263,32 @@ def __init__( self.websocket_queues = {} self.site = None self.multitenant_manager = context.inject_or(BaseMultitenantManager) + self._additional_route_pattern: Optional[Pattern] = None self.server_paths = [] + @property + def additional_routes_pattern(self) -> Optional[Pattern]: + """Pattern for configured addtional routes to permit base wallet to access.""" + if self._additional_route_pattern: + return self._additional_route_pattern + + base_wallet_routes = self.context.settings.get("multitenant.base_wallet_routes") + base_wallet_routes = cast(Sequence[str], base_wallet_routes) + if base_wallet_routes: + self._additional_route_pattern = re.compile( + "^(?:" + "|".join(base_wallet_routes) + ")" + ) + return None + + def _matches_additional_routes(self, path: str) -> bool: + """Path matches additional_routes_pattern.""" + pattern = self.additional_routes_pattern + if pattern: + return bool(pattern.match(path)) + + return False + async def make_application(self) -> web.Application: """Get the aiohttp application instance.""" @@ -267,18 +300,14 @@ async def make_application(self) -> web.Application: assert self.admin_insecure_mode ^ bool(self.admin_api_key) def is_unprotected_path(path: str): - return ( - path - in [ - "/api/doc", - "/api/docs/swagger.json", - "/favicon.ico", - "/ws", # ws handler checks authentication - "/status/live", - "/status/ready", - ] - or path.startswith("/static/swagger/") - ) + return path in [ + "/api/doc", + "/api/docs/swagger.json", + "/favicon.ico", + "/ws", # ws handler checks authentication + "/status/live", + "/status/ready", + ] or path.startswith("/static/swagger/") # If admin_api_key is None, then admin_insecure_mode must be set so # we can safely enable the admin server with no security @@ -331,6 +360,8 @@ async def check_multitenant_authorization(request: web.Request, handler): f"{UUIDFour.PATTERN}/default-mediator)", path, ) + or path.startswith("/mediation/default-mediator") + or self._matches_additional_routes(path) ) # base wallet is not allowed to perform ssi related actions. @@ -341,6 +372,7 @@ async def check_multitenant_authorization(request: web.Request, handler): and not is_server_path and not is_unprotected_path(path) and not base_limited_access_path + and not (request.method == "OPTIONS") # CORS fix ): raise web.HTTPUnauthorized() @@ -352,7 +384,7 @@ async def check_multitenant_authorization(request: web.Request, handler): async def setup_context(request: web.Request, handler): authorization_header = request.headers.get("Authorization") profile = self.root_profile - + meta_data = {} # Multitenancy context setup if self.multitenant_manager and authorization_header: try: @@ -365,6 +397,16 @@ async def setup_context(request: web.Request, handler): profile = await self.multitenant_manager.get_profile_for_token( self.context, token ) + ( + walletid, + walletkey, + ) = self.multitenant_manager.get_wallet_details_from_token( + token=token + ) + meta_data = { + "wallet_id": walletid, + "wallet_key": walletkey, + } except MultitenantManagerError as err: raise web.HTTPUnauthorized(reason=err.roll_up) except (jwt.InvalidTokenError, StorageNotFoundError): @@ -379,7 +421,16 @@ async def setup_context(request: web.Request, handler): # TODO may dynamically adjust the profile used here according to # headers or other parameters - admin_context = AdminRequestContext(profile) + if self.multitenant_manager and authorization_header: + admin_context = AdminRequestContext( + profile=profile, + root_profile=self.root_profile, + metadata=meta_data, + ) + else: + admin_context = AdminRequestContext( + profile=profile, + ) request["context"] = admin_context request["outbound_message_router"] = responder.send @@ -403,7 +454,7 @@ async def setup_context(request: web.Request, handler): ) server_routes = [ - web.get("/", self.redirect_handler, allow_head=False), + web.get("/", self.redirect_handler, allow_head=True), web.get("/plugins", self.plugins_handler, allow_head=False), web.get("/status", self.status_handler, allow_head=False), web.get("/status/config", self.config_handler, allow_head=False), @@ -461,7 +512,7 @@ async def start(self) -> None: def sort_dict(raw: dict) -> dict: """Order (JSON, string keys) dict asciibetically by key, recursively.""" - for (k, v) in raw.items(): + for k, v in raw.items(): if isinstance(v, dict): raw[k] = sort_dict(v) return dict(sorted([item for item in raw.items()], key=lambda x: x[0])) diff --git a/aries_cloudagent/admin/tests/test_admin_server.py b/aries_cloudagent/admin/tests/test_admin_server.py index 7707315e12..049d70c42d 100644 --- a/aries_cloudagent/admin/tests/test_admin_server.py +++ b/aries_cloudagent/admin/tests/test_admin_server.py @@ -1,19 +1,18 @@ import json + import pytest +import mock as async_mock +from async_case import IsolatedAsyncioTestCase from aiohttp import ClientSession, DummyCookieJar, TCPConnector, web from aiohttp.test_utils import unused_port -from asynctest import TestCase as AsyncTestCase -from asynctest import mock as async_mock - from ...config.default_context import DefaultContextBuilder from ...config.injection_context import InjectionContext from ...core.event_bus import Event from ...core.in_memory import InMemoryProfile from ...core.protocol_registry import ProtocolRegistry from ...core.goal_code_registry import GoalCodeRegistry -from ...transport.outbound.message import OutboundMessage from ...utils.stats import Collector from ...utils.task_queue import TaskQueue @@ -21,19 +20,18 @@ from ..server import AdminServer, AdminSetupError -class TestAdminServer(AsyncTestCase): - async def setUp(self): +class TestAdminServer(IsolatedAsyncioTestCase): + async def asyncSetUp(self): self.message_results = [] self.webhook_results = [] self.port = 0 self.connector = TCPConnector(limit=16, limit_per_host=4) - session_args = {"cookie_jar": DummyCookieJar(), "connector": self.connector} self.client_session = ClientSession( cookie_jar=DummyCookieJar(), connector=self.connector ) - async def tearDown(self): + async def asyncTearDown(self): if self.client_session: await self.client_session.close() self.client_session = None @@ -49,9 +47,9 @@ async def test_debug_middleware(self): method="GET", path_qs="/hello/world?a=1&b=2", match_info={"match": "info"}, - text=async_mock.CoroutineMock(return_value="abc123"), + text=async_mock.AsyncMock(return_value="abc123"), ) - handler = async_mock.CoroutineMock() + handler = async_mock.AsyncMock() await test_module.debug_middleware(request, handler) mock_logger.isEnabledFor.assert_called_once() @@ -69,7 +67,7 @@ async def test_ready_middleware(self): request = async_mock.MagicMock( rel_url="/", app=async_mock.MagicMock(_state={"ready": False}) ) - handler = async_mock.CoroutineMock(return_value="OK") + handler = async_mock.AsyncMock(return_value="OK") with self.assertRaises(test_module.web.HTTPServiceUnavailable): await test_module.ready_middleware(request, handler) @@ -77,28 +75,28 @@ async def test_ready_middleware(self): assert await test_module.ready_middleware(request, handler) == "OK" request.app._state["ready"] = True - handler = async_mock.CoroutineMock( + handler = async_mock.AsyncMock( side_effect=test_module.LedgerConfigError("Bad config") ) with self.assertRaises(test_module.LedgerConfigError): await test_module.ready_middleware(request, handler) request.app._state["ready"] = True - handler = async_mock.CoroutineMock( + handler = async_mock.AsyncMock( side_effect=test_module.web.HTTPFound(location="/api/doc") ) with self.assertRaises(test_module.web.HTTPFound): await test_module.ready_middleware(request, handler) request.app._state["ready"] = True - handler = async_mock.CoroutineMock( + handler = async_mock.AsyncMock( side_effect=test_module.asyncio.CancelledError("Cancelled") ) with self.assertRaises(test_module.asyncio.CancelledError): await test_module.ready_middleware(request, handler) request.app._state["ready"] = True - handler = async_mock.CoroutineMock(side_effect=KeyError("No such thing")) + handler = async_mock.AsyncMock(side_effect=KeyError("No such thing")) with self.assertRaises(KeyError): await test_module.ready_middleware(request, handler) @@ -132,10 +130,10 @@ def get_admin_server( profile, self.outbound_message_router, self.webhook_router, - conductor_stop=async_mock.CoroutineMock(), + conductor_stop=async_mock.AsyncMock(), task_queue=TaskQueue(max_active=4) if task_queue else None, conductor_stats=( - None if task_queue else async_mock.CoroutineMock(return_value={"a": 1}) + None if task_queue else async_mock.AsyncMock(return_value={"a": 1}) ), ) @@ -177,7 +175,7 @@ async def test_start_stop(self): await server.stop() with async_mock.patch.object( - web.TCPSite, "start", async_mock.CoroutineMock() + web.TCPSite, "start", async_mock.AsyncMock() ) as mock_start: mock_start.side_effect = OSError("Failure to launch") with self.assertRaises(AdminSetupError): @@ -195,13 +193,14 @@ async def test_import_routes(self): async def test_import_routes_multitenant_middleware(self): # imports all default admin routes - context = InjectionContext() + context = InjectionContext( + settings={"multitenant.base_wallet_routes": ["/test"]} + ) context.injector.bind_instance(ProtocolRegistry, ProtocolRegistry()) context.injector.bind_instance(GoalCodeRegistry, GoalCodeRegistry()) - profile = InMemoryProfile.test_profile() context.injector.bind_instance( test_module.BaseMultitenantManager, - test_module.BaseMultitenantManager(profile), + async_mock.MagicMock(spec=test_module.BaseMultitenantManager), ) await DefaultContextBuilder().load_plugins(context) server = self.get_admin_server( @@ -226,7 +225,7 @@ async def test_import_routes_multitenant_middleware(self): method="GET", headers={"Authorization": "Bearer ..."}, path="/multitenancy/etc", - text=async_mock.CoroutineMock(return_value="abc123"), + text=async_mock.AsyncMock(return_value="abc123"), ) with self.assertRaises(test_module.web.HTTPUnauthorized): await mt_authz_middle(mock_request, None) @@ -235,7 +234,7 @@ async def test_import_routes_multitenant_middleware(self): method="GET", headers={}, path="/protected/non-multitenancy/non-server", - text=async_mock.CoroutineMock(return_value="abc123"), + text=async_mock.AsyncMock(return_value="abc123"), ) with self.assertRaises(test_module.web.HTTPUnauthorized): await mt_authz_middle(mock_request, None) @@ -244,9 +243,19 @@ async def test_import_routes_multitenant_middleware(self): method="GET", headers={"Authorization": "Bearer ..."}, path="/protected/non-multitenancy/non-server", - text=async_mock.CoroutineMock(return_value="abc123"), + text=async_mock.AsyncMock(return_value="abc123"), + ) + mock_handler = async_mock.AsyncMock() + await mt_authz_middle(mock_request, mock_handler) + assert mock_handler.called_once_with(mock_request) + + mock_request = async_mock.MagicMock( + method="GET", + headers={"Authorization": "Non-bearer ..."}, + path="/test", + text=async_mock.AsyncMock(return_value="abc123"), ) - mock_handler = async_mock.CoroutineMock() + mock_handler = async_mock.AsyncMock() await mt_authz_middle(mock_request, mock_handler) assert mock_handler.called_once_with(mock_request) @@ -257,7 +266,7 @@ async def test_import_routes_multitenant_middleware(self): method="GET", headers={"Authorization": "Non-bearer ..."}, path="/protected/non-multitenancy/non-server", - text=async_mock.CoroutineMock(return_value="abc123"), + text=async_mock.AsyncMock(return_value="abc123"), ) with self.assertRaises(test_module.web.HTTPUnauthorized): await setup_ctx_middle(mock_request, None) @@ -266,12 +275,12 @@ async def test_import_routes_multitenant_middleware(self): method="GET", headers={"Authorization": "Bearer ..."}, path="/protected/non-multitenancy/non-server", - text=async_mock.CoroutineMock(return_value="abc123"), + text=async_mock.AsyncMock(return_value="abc123"), ) with async_mock.patch.object( server.multitenant_manager, "get_profile_for_token", - async_mock.CoroutineMock(), + async_mock.AsyncMock(), ) as mock_get_profile: mock_get_profile.side_effect = [ test_module.MultitenantManagerError("corrupt token"), @@ -284,11 +293,15 @@ async def test_import_routes_multitenant_middleware(self): async def test_register_external_plugin_x(self): context = InjectionContext() context.injector.bind_instance(ProtocolRegistry, ProtocolRegistry()) - with self.assertRaises(ValueError): + context.injector.bind_instance(GoalCodeRegistry, GoalCodeRegistry()) + with self.assertLogs(level="ERROR") as logs: builder = DefaultContextBuilder( - settings={"external_plugins": "aries_cloudagent.nosuchmodule"} + settings={"external_plugins": ["aries_cloudagent.nosuchmodule"]} ) await builder.load_plugins(context) + assert "Module doesn't exist: aries_cloudagent.nosuchmodule" in "\n".join( + logs.output + ) async def test_visit_insecure_mode(self): settings = {"admin.admin_insecure_mode": True, "task_queue": True} @@ -321,6 +334,7 @@ async def test_visit_insecure_mode(self): await server.stop() + @pytest.mark.skip(reason="async_case library not compatible with python 3.10") async def test_visit_secure_mode(self): settings = { "admin.admin_insecure_mode": False, @@ -373,6 +387,7 @@ async def test_visit_secure_mode(self): await server.stop() + @pytest.mark.skip(reason="async_case library not compatible with python 3.10") async def test_query_config(self): settings = { "admin.admin_insecure_mode": False, @@ -469,9 +484,9 @@ async def test_server_health_state(self): @pytest.fixture async def server(): test_class = TestAdminServer() - await test_class.setUp() + await test_class.asyncSetUp() yield test_class.get_admin_server() - await test_class.tearDown() + await test_class.asyncTearDown() @pytest.mark.asyncio @@ -482,7 +497,21 @@ async def server(): async def test_on_record_event(server, event_topic, webhook_topic): profile = InMemoryProfile.test_profile() with async_mock.patch.object( - server, "send_webhook", async_mock.CoroutineMock() + server, "send_webhook", async_mock.AsyncMock() ) as mock_send_webhook: await server._on_record_event(profile, Event(event_topic, None)) mock_send_webhook.assert_called_once_with(profile, webhook_topic, None) + + +@pytest.mark.asyncio +async def test_admin_responder_profile_expired_x(): + def _smaller_scope(): + profile = InMemoryProfile.test_profile() + return test_module.AdminResponder(profile, None) + + responder = _smaller_scope() + with pytest.raises(RuntimeError): + await responder.send_outbound(None) + + with pytest.raises(RuntimeError): + await responder.send_webhook("test", {}) diff --git a/aries_cloudagent/admin/tests/test_request_context.py b/aries_cloudagent/admin/tests/test_request_context.py index 7775262638..e74763004e 100644 --- a/aries_cloudagent/admin/tests/test_request_context.py +++ b/aries_cloudagent/admin/tests/test_request_context.py @@ -12,12 +12,26 @@ def setUp(self): self.ctx = test_module.AdminRequestContext(InMemoryProfile.test_profile()) assert self.ctx.__class__.__name__ in str(self.ctx) + self.ctx_with_added_attrs = test_module.AdminRequestContext( + profile=InMemoryProfile.test_profile(), + root_profile=InMemoryProfile.test_profile(), + metadata={"test_attrib_key": "test_attrib_value"}, + ) + assert self.ctx_with_added_attrs.__class__.__name__ in str( + self.ctx_with_added_attrs + ) + def test_session_transaction(self): sesn = self.ctx.session() assert isinstance(sesn, ProfileSession) txn = self.ctx.transaction() assert isinstance(txn, ProfileSession) + sesn = self.ctx_with_added_attrs.session() + assert isinstance(sesn, ProfileSession) + txn = self.ctx_with_added_attrs.transaction() + assert isinstance(txn, ProfileSession) + async def test_session_inject_x(self): test_ctx = test_module.AdminRequestContext.test_context({Collector: None}) async with test_ctx.session() as test_sesn: diff --git a/aries_cloudagent/askar/didcomm/v2.py b/aries_cloudagent/askar/didcomm/v2.py index 514590f626..7d89a3ec9c 100644 --- a/aries_cloudagent/askar/didcomm/v2.py +++ b/aries_cloudagent/askar/didcomm/v2.py @@ -33,7 +33,7 @@ def ecdh_es_encrypt(to_verkeys: Mapping[str, Key], message: bytes) -> bytes: except AskarError: raise DidcommEnvelopeError("Error creating content encryption key") - for (kid, recip_key) in to_verkeys.items(): + for kid, recip_key in to_verkeys.items(): try: epk = Key.generate(recip_key.algorithm, ephemeral=True) except AskarError: @@ -145,7 +145,7 @@ def ecdh_1pu_encrypt( apu = b64url(sender_kid) apv = [] - for (kid, recip_key) in to_verkeys.items(): + for kid, recip_key in to_verkeys.items(): if agree_alg: if agree_alg != recip_key.algorithm: raise DidcommEnvelopeError("Recipient key types must be consistent") @@ -173,7 +173,7 @@ def ecdh_1pu_encrypt( raise DidcommEnvelopeError("Error encrypting message payload") wrapper.set_payload(payload.ciphertext, payload.nonce, payload.tag) - for (kid, recip_key) in to_verkeys.items(): + for kid, recip_key in to_verkeys.items(): enc_key = ecdh.Ecdh1PU(alg_id, apu, apv).sender_wrap_key( wrap_alg, epk, sender_key, recip_key, cek, cc_tag=payload.tag ) @@ -262,7 +262,7 @@ async def unpack_message( recip_kid = None for kid in wrapper.recipient_key_ids: recip_key_entry = next( - await session.fetch_all_keys(tag_filter={"kid": kid}), None + iter(await session.fetch_all_keys(tag_filter={"kid": kid})), None ) if recip_key_entry: recip_kid = kid @@ -289,7 +289,7 @@ async def unpack_message( # FIXME - will need to insert proper sender key resolution method here # instead of looking in the wallet sender_key_entry = next( - await session.fetch_all_keys(tag_filter={"kid": sender_kid}), None + iter(await session.fetch_all_keys(tag_filter={"kid": sender_kid})), None ) if not sender_key_entry: raise DidcommEnvelopeError("Sender public key not found") diff --git a/aries_cloudagent/askar/profile.py b/aries_cloudagent/askar/profile.py index 4b72d20a57..a8cb10df71 100644 --- a/aries_cloudagent/askar/profile.py +++ b/aries_cloudagent/askar/profile.py @@ -36,11 +36,18 @@ class AskarProfile(Profile): BACKEND_NAME = "askar" - def __init__(self, opened: AskarOpenStore, context: InjectionContext = None): + def __init__( + self, + opened: AskarOpenStore, + context: InjectionContext = None, + *, + profile_id: str = None + ): """Create a new AskarProfile instance.""" super().__init__(context=context, name=opened.name, created=opened.created) self.opened = opened self.ledger_pool: IndyVdrLedgerPool = None + self.profile_id = profile_id self.init_ledger_pool() self.bind_providers() @@ -56,8 +63,8 @@ def store(self) -> Store: async def remove(self): """Remove the profile.""" - if self.settings.get("multitenant.wallet_type") == "askar-profile": - await self.store.remove_profile(self.settings.get("wallet.askar_profile")) + if self.profile_id: + await self.store.remove_profile(self.profile_id) def init_ledger_pool(self): """Initialize the ledger pool.""" @@ -160,11 +167,10 @@ def __init__( ): """Create a new IndySdkProfileSession instance.""" super().__init__(profile=profile, context=context, settings=settings) - profile_id = profile.context.settings.get("wallet.askar_profile") if is_txn: - self._opener = self.profile.store.transaction(profile_id) + self._opener = self.profile.store.transaction(profile.profile_id) else: - self._opener = self.profile.store.session(profile_id) + self._opener = self.profile.store.session(profile.profile_id) self._handle: Session = None self._acquire_start: float = None self._acquire_end: float = None @@ -213,6 +219,8 @@ async def _teardown(self, commit: bool = None): await self._handle.commit() except AskarError as err: raise ProfileError("Error committing transaction") from err + if self._handle: + await self._handle.close() self._handle = None self._check_duration() diff --git a/aries_cloudagent/askar/store.py b/aries_cloudagent/askar/store.py index f96112e4a2..6c81d6696d 100644 --- a/aries_cloudagent/askar/store.py +++ b/aries_cloudagent/askar/store.py @@ -146,7 +146,7 @@ async def open_store(self, provision: bool = False) -> "AskarOpenStore": """ try: - if provision: + if provision or self.in_memory: store = await Store.provision( self.get_uri(create=True), self.key_derivation_method, diff --git a/aries_cloudagent/askar/tests/test_profile.py b/aries_cloudagent/askar/tests/test_profile.py index f01da0d2fe..aef94fa481 100644 --- a/aries_cloudagent/askar/tests/test_profile.py +++ b/aries_cloudagent/askar/tests/test_profile.py @@ -1,7 +1,8 @@ import asyncio +import logging import pytest -from asynctest import TestCase as AsyncTestCase, mock +from asynctest import mock from ...askar.profile import AskarProfile from ...config.injection_context import InjectionContext @@ -9,79 +10,79 @@ from .. import profile as test_module -class TestProfile(AsyncTestCase): - @mock.patch("aries_cloudagent.askar.store.AskarOpenStore") - async def test_init_success(self, AskarOpenStore): - askar_profile = AskarProfile( - AskarOpenStore, - ) - - assert askar_profile.opened == AskarOpenStore - - @mock.patch("aries_cloudagent.askar.store.AskarOpenStore") - async def test_remove_success(self, AskarOpenStore): - openStore = AskarOpenStore - context = InjectionContext() - profile_id = "profile_id" - context.settings = { - "multitenant.wallet_type": "askar-profile", - "wallet.askar_profile": profile_id, - "ledger.genesis_transactions": mock.MagicMock(), - } - askar_profile = AskarProfile(openStore, context) - remove_profile_stub = asyncio.Future() - remove_profile_stub.set_result(True) - openStore.store.remove_profile.return_value = remove_profile_stub - - await askar_profile.remove() - - openStore.store.remove_profile.assert_called_once_with(profile_id) - - @mock.patch("aries_cloudagent.askar.store.AskarOpenStore") - async def test_remove_profile_not_removed_if_wallet_type_not_askar_profile( - self, AskarOpenStore - ): - openStore = AskarOpenStore - context = InjectionContext() - context.settings = {"multitenant.wallet_type": "basic"} - askar_profile = AskarProfile(openStore, context) - - await askar_profile.remove() - - openStore.store.remove_profile.assert_not_called() - - @pytest.mark.asyncio - async def test_profile_manager_transaction(self): - profile = "profileId" - - with mock.patch("aries_cloudagent.askar.profile.AskarProfile") as AskarProfile: - askar_profile = AskarProfile(None, True) - askar_profile_transaction = mock.MagicMock() - askar_profile.store.transaction.return_value = askar_profile_transaction - askar_profile.context.settings.get.return_value = profile - - transactionProfile = test_module.AskarProfileSession(askar_profile, True) - - assert transactionProfile._opener == askar_profile_transaction - askar_profile.context.settings.get.assert_called_once_with( - "wallet.askar_profile" - ) - askar_profile.store.transaction.assert_called_once_with(profile) - - @pytest.mark.asyncio - async def test_profile_manager_store(self): - profile = "profileId" - - with mock.patch("aries_cloudagent.askar.profile.AskarProfile") as AskarProfile: - askar_profile = AskarProfile(None, False) - askar_profile_session = mock.MagicMock() - askar_profile.store.session.return_value = askar_profile_session - askar_profile.context.settings.get.return_value = profile - - sessionProfile = test_module.AskarProfileSession(askar_profile, False) - - assert sessionProfile._opener == askar_profile_session - askar_profile.context.settings.get.assert_called_once_with( - "wallet.askar_profile" - ) - askar_profile.store.session.assert_called_once_with(profile) +@pytest.fixture +def open_store(): + yield mock.MagicMock() + + +@pytest.mark.asyncio +async def test_init_success(open_store): + askar_profile = AskarProfile( + open_store, + ) + + assert askar_profile.opened == open_store + + +@pytest.mark.asyncio +async def test_remove_success(open_store): + openStore = open_store + context = InjectionContext() + profile_id = "profile_id" + context.settings = { + "multitenant.wallet_type": "askar-profile", + "wallet.askar_profile": profile_id, + "ledger.genesis_transactions": mock.MagicMock(), + } + askar_profile = AskarProfile(openStore, context, profile_id=profile_id) + remove_profile_stub = asyncio.Future() + remove_profile_stub.set_result(True) + openStore.store.remove_profile.return_value = remove_profile_stub + + await askar_profile.remove() + + openStore.store.remove_profile.assert_called_once_with(profile_id) + + +@pytest.mark.asyncio +async def test_remove_profile_not_removed_if_wallet_type_not_askar_profile(open_store): + openStore = open_store + context = InjectionContext() + context.settings = {"multitenant.wallet_type": "basic"} + askar_profile = AskarProfile(openStore, context) + + await askar_profile.remove() + + openStore.store.remove_profile.assert_not_called() + + +@pytest.mark.asyncio +async def test_profile_manager_transaction(): + profile = "profileId" + + with mock.patch("aries_cloudagent.askar.profile.AskarProfile") as AskarProfile: + askar_profile = AskarProfile(None, True, profile_id=profile) + askar_profile.profile_id = profile + askar_profile_transaction = mock.MagicMock() + askar_profile.store.transaction.return_value = askar_profile_transaction + + transactionProfile = test_module.AskarProfileSession(askar_profile, True) + + assert transactionProfile._opener == askar_profile_transaction + askar_profile.store.transaction.assert_called_once_with(profile) + + +@pytest.mark.asyncio +async def test_profile_manager_store(): + profile = "profileId" + + with mock.patch("aries_cloudagent.askar.profile.AskarProfile") as AskarProfile: + askar_profile = AskarProfile(None, False, profile_id=profile) + askar_profile.profile_id = profile + askar_profile_session = mock.MagicMock() + askar_profile.store.session.return_value = askar_profile_session + + sessionProfile = test_module.AskarProfileSession(askar_profile, False) + + assert sessionProfile._opener == askar_profile_session + askar_profile.store.session.assert_called_once_with(profile) diff --git a/aries_cloudagent/commands/default_version_upgrade_config.yml b/aries_cloudagent/commands/default_version_upgrade_config.yml index 666f57ee37..8c04b9e48f 100644 --- a/aries_cloudagent/commands/default_version_upgrade_config.yml +++ b/aries_cloudagent/commands/default_version_upgrade_config.yml @@ -1,3 +1,8 @@ +v0.8.1: + resave_records: + base_record_path: + - "aries_cloudagent.connections.models.conn_record.ConnRecord" + update_existing_records: false v0.7.2: resave_records: base_record_path: diff --git a/aries_cloudagent/commands/tests/test_upgrade.py b/aries_cloudagent/commands/tests/test_upgrade.py index a44f020b90..e7ad5a827b 100644 --- a/aries_cloudagent/commands/tests/test_upgrade.py +++ b/aries_cloudagent/commands/tests/test_upgrade.py @@ -17,10 +17,7 @@ class TestUpgrade(AsyncTestCase): async def setUp(self): self.session = InMemoryProfile.test_session() self.profile = self.session.profile - - self.session_storage = InMemoryProfile.test_session() - self.profile_storage = self.session_storage.profile - self.storage = self.session_storage.inject(BaseStorage) + self.storage = self.session.inject(BaseStorage) record = StorageRecord( "acapy_version", "v0.7.2", @@ -37,7 +34,7 @@ async def test_upgrade_storage_from_version_included(self): "wallet_config", async_mock.CoroutineMock( return_value=( - self.profile_storage, + self.profile, async_mock.CoroutineMock(did="public DID", verkey="verkey"), ) ), @@ -49,7 +46,7 @@ async def test_upgrade_storage_from_version_included(self): ConnRecord, "save", async_mock.CoroutineMock() ): await test_module.upgrade( - { + settings={ "upgrade.config_path": "./aries_cloudagent/commands/default_version_upgrade_config.yml", "upgrade.from_version": "v0.7.2", } @@ -61,7 +58,7 @@ async def test_upgrade_storage_missing_from_version(self): "wallet_config", async_mock.CoroutineMock( return_value=( - self.profile_storage, + self.profile, async_mock.CoroutineMock(did="public DID", verkey="verkey"), ) ), @@ -72,32 +69,28 @@ async def test_upgrade_storage_missing_from_version(self): ), async_mock.patch.object( ConnRecord, "save", async_mock.CoroutineMock() ): - await test_module.upgrade({}) + await test_module.upgrade(settings={}) async def test_upgrade_from_version(self): + self.profile.settings.extend( + { + "upgrade.from_version": "v0.7.2", + } + ) with async_mock.patch.object( - test_module, - "wallet_config", - async_mock.CoroutineMock( - return_value=( - self.profile, - async_mock.CoroutineMock(did="public DID", verkey="verkey"), - ) - ), - ), async_mock.patch.object( ConnRecord, "query", async_mock.CoroutineMock(return_value=[ConnRecord()]), - ), async_mock.patch.object( - ConnRecord, "save", async_mock.CoroutineMock() - ): + ), async_mock.patch.object(ConnRecord, "save", async_mock.CoroutineMock()): await test_module.upgrade( - { - "upgrade.from_version": "v0.7.2", - } + profile=self.profile, ) async def test_upgrade_callable(self): + version_storage_record = await self.storage.find_record( + type_filter="acapy_version", tag_query={} + ) + await self.storage.delete_record(version_storage_record) with async_mock.patch.object( test_module, "wallet_config", @@ -124,7 +117,7 @@ async def test_upgrade_callable(self): ), ): await test_module.upgrade( - { + settings={ "upgrade.from_version": "v0.7.2", } ) @@ -139,19 +132,22 @@ async def test_upgrade_x_same_version(self): "wallet_config", async_mock.CoroutineMock( return_value=( - self.profile_storage, + self.profile, async_mock.CoroutineMock(did="public DID", verkey="verkey"), ) ), ): - with self.assertRaises(UpgradeError): - await test_module.upgrade( - { - "upgrade.config_path": "./aries_cloudagent/commands/default_version_upgrade_config.yml", - } - ) + await test_module.upgrade( + settings={ + "upgrade.config_path": "./aries_cloudagent/commands/default_version_upgrade_config.yml", + } + ) async def test_upgrade_missing_from_version(self): + version_storage_record = await self.storage.find_record( + type_filter="acapy_version", tag_query={} + ) + await self.storage.delete_record(version_storage_record) with async_mock.patch.object( test_module, "wallet_config", @@ -168,13 +164,19 @@ async def test_upgrade_missing_from_version(self): ), async_mock.patch.object( ConnRecord, "save", async_mock.CoroutineMock() ): - await test_module.upgrade( - { - "upgrade.config_path": "./aries_cloudagent/commands/default_version_upgrade_config.yml", - } - ) + with self.assertRaises(UpgradeError) as ctx: + await test_module.upgrade( + settings={ + "upgrade.config_path": "./aries_cloudagent/commands/default_version_upgrade_config.yml", + } + ) + assert "No upgrade from version found in wallet or" in str(ctx.exception) async def test_upgrade_x_callable_not_set(self): + version_storage_record = await self.storage.find_record( + type_filter="acapy_version", tag_query={} + ) + await self.storage.delete_record(version_storage_record) with async_mock.patch.object( test_module, "wallet_config", @@ -197,17 +199,17 @@ async def test_upgrade_x_callable_not_set(self): }, "update_existing_records": True, }, - "v0.6.0": {"update_existing_records": True}, + "v0.6.0": {"update_existing_records_b": True}, } ), ): with self.assertRaises(UpgradeError) as ctx: await test_module.upgrade( - { + settings={ "upgrade.from_version": "v0.6.0", } ) - assert "No update_existing_records function specified" in str(ctx.exception) + assert "No function specified for" in str(ctx.exception) async def test_upgrade_x_class_not_found(self): with async_mock.patch.object( @@ -236,7 +238,7 @@ async def test_upgrade_x_class_not_found(self): ): with self.assertRaises(UpgradeError) as ctx: await test_module.upgrade( - { + settings={ "upgrade.from_version": "v0.7.2", } ) @@ -269,7 +271,8 @@ async def test_execute(self): "--upgrade-config", "./aries_cloudagent/config/tests/test-acapy-upgrade-config.yaml", "--from-version", - "v0.7.2", + "v0.7.0", + "--force-upgrade", ] ) @@ -300,23 +303,13 @@ async def test_upgrade_x_invalid_record_type(self): ): with self.assertRaises(UpgradeError) as ctx: await test_module.upgrade( - { + settings={ "upgrade.from_version": "v0.7.2", } ) assert "Only BaseRecord can be resaved" in str(ctx.exception) - async def test_upgrade_x_invalid_config(self): - with async_mock.patch.object( - test_module.yaml, - "safe_load", - async_mock.MagicMock(return_value={}), - ): - with self.assertRaises(UpgradeError) as ctx: - await test_module.upgrade({}) - assert "No version configs found in" in str(ctx.exception) - - async def test_upgrade_x_from_version_not_in_config(self): + async def test_upgrade_force(self): with async_mock.patch.object( test_module, "wallet_config", @@ -326,14 +319,70 @@ async def test_upgrade_x_from_version_not_in_config(self): async_mock.CoroutineMock(did="public DID", verkey="verkey"), ) ), + ), async_mock.patch.object( + test_module.yaml, + "safe_load", + async_mock.MagicMock( + return_value={ + "v0.7.2": { + "resave_records": { + "base_record_path": [ + "aries_cloudagent.connections.models.conn_record.ConnRecord" + ], + }, + "update_existing_records": True, + }, + "v0.7.3": { + "update_existing_records": True, + }, + "v0.7.1": { + "update_existing_records": False, + }, + } + ), + ): + await test_module.upgrade( + settings={ + "upgrade.from_version": "v0.7.0", + "upgrade.force_upgrade": True, + } + ) + + async def test_get_upgrade_version_list(self): + assert len(test_module.get_upgrade_version_list(from_version="v0.7.2")) >= 1 + + async def test_add_version_record(self): + await test_module.add_version_record(self.profile, "v0.7.4") + version_storage_record = await self.storage.find_record( + type_filter="acapy_version", tag_query={} + ) + assert version_storage_record.value == "v0.7.4" + await self.storage.delete_record(version_storage_record) + with self.assertRaises(test_module.StorageNotFoundError): + await self.storage.find_record(type_filter="acapy_version", tag_query={}) + await test_module.add_version_record(self.profile, "v0.7.5") + version_storage_record = await self.storage.find_record( + type_filter="acapy_version", tag_query={} + ) + assert version_storage_record.value == "v0.7.5" + + async def test_upgrade_x_invalid_config(self): + with async_mock.patch.object( + test_module.yaml, + "safe_load", + async_mock.MagicMock(return_value={}), ): with self.assertRaises(UpgradeError) as ctx: - await test_module.upgrade( - { - "upgrade.from_version": "v1.2.3", - } - ) - assert "No upgrade configuration found for" in str(ctx.exception) + await test_module.upgrade(settings={}) + assert "No version configs found in" in str(ctx.exception) + + async def test_upgrade_x_params(self): + with self.assertRaises(UpgradeError) as ctx: + await test_module.upgrade(profile=self.profile, settings={}) + assert "upgrade requires either profile or settings" in str(ctx.exception) + with self.assertRaises(UpgradeError) as ctx: + await test_module.upgrade(profile=self.profile, settings={"...": "..."}) + assert "upgrade requires either profile or settings" in str(ctx.exception) def test_main(self): with async_mock.patch.object( diff --git a/aries_cloudagent/commands/upgrade.py b/aries_cloudagent/commands/upgrade.py index 4b2409e20e..e40a438409 100644 --- a/aries_cloudagent/commands/upgrade.py +++ b/aries_cloudagent/commands/upgrade.py @@ -1,16 +1,26 @@ """Upgrade command for handling breaking changes when updating ACA-PY versions.""" import asyncio +import logging +import os import yaml from configargparse import ArgumentParser from packaging import version as package_version -from typing import Callable, Sequence, Optional +from typing import ( + Callable, + Sequence, + Optional, + List, + Union, + Mapping, + Any, +) from ..core.profile import Profile from ..config import argparse as arg from ..config.default_context import DefaultContextBuilder -from ..config.base import BaseError +from ..config.base import BaseError, BaseSettings from ..config.util import common_config from ..config.wallet import wallet_config from ..messaging.models.base_record import BaseRecord @@ -22,9 +32,8 @@ from . import PROG -DEFAULT_UPGRADE_CONFIG_PATH = ( - "./aries_cloudagent/commands/default_version_upgrade_config.yml" -) +DEFAULT_UPGRADE_CONFIG_FILE_NAME = "default_version_upgrade_config.yml" +LOGGER = logging.getLogger(__name__) class UpgradeError(BaseError): @@ -36,22 +45,17 @@ class VersionUpgradeConfig: def __init__(self, config_path: str = None): """Initialize config for use during upgrade process.""" - self.function_map_config = {} + self.function_map_config = UPGRADE_EXISTING_RECORDS_FUNCTION_MAPPING self.upgrade_configs = {} - self.setup_executable_map_config(CONFIG_v7_3) if config_path: self.setup_version_upgrade_config(config_path) else: - self.setup_version_upgrade_config(DEFAULT_UPGRADE_CONFIG_PATH) - - def setup_executable_map_config(self, config_dict: dict): - """Set ups config with reference to functions mapped to versions.""" - for version, config in config_dict.items(): - self.function_map_config[version] = {} - if "update_existing_function_inst" in config: - self.function_map_config[version][ - "update_existing_function_inst" - ] = config.get("update_existing_function_inst") + self.setup_version_upgrade_config( + os.path.join( + os.path.dirname(os.path.realpath(__file__)), + DEFAULT_UPGRADE_CONFIG_FILE_NAME, + ) + ) def setup_version_upgrade_config(self, path: str): """Set ups config dict from the provided YML file.""" @@ -73,19 +77,23 @@ def setup_version_upgrade_config(self, path: str): "resave_records" ).get("base_exch_record_path") version_config_dict[version]["resave_records"] = recs_list - version_config_dict[version]["update_existing_records"] = ( - provided_config.get("update_existing_records") or False - ) + config_key_set = set(provided_config.keys()) + try: + config_key_set.remove("resave_records") + except KeyError: + pass + for executable in config_key_set: + version_config_dict[version][executable] = ( + provided_config.get(executable) or False + ) if version_config_dict == {}: raise UpgradeError(f"No version configs found in {path}") self.upgrade_configs = version_config_dict - def get_update_existing_func(self, ver: str) -> Optional[Callable]: - """Return callable update_existing_records function for specific version.""" - if ver in self.function_map_config: - return self.function_map_config.get(ver).get( - "update_existing_function_inst" - ) + def get_callable(self, executable: str) -> Optional[Callable]: + """Return callable function for executable name.""" + if executable in self.function_map_config: + return self.function_map_config.get(executable) else: return None @@ -95,123 +103,211 @@ def init_argument_parser(parser: ArgumentParser): return arg.load_argument_groups(parser, *arg.group.get_registered(arg.CAT_UPGRADE)) -async def upgrade(settings: dict): +def get_upgrade_version_list( + from_version: str, + sorted_version_list: Optional[List] = None, + config_path: Optional[str] = None, +) -> List: + """Get available versions from the upgrade config.""" + if not sorted_version_list: + version_upgrade_config_inst = VersionUpgradeConfig(config_path) + upgrade_configs = version_upgrade_config_inst.upgrade_configs + versions_found_in_config = upgrade_configs.keys() + sorted_version_list = sorted( + versions_found_in_config, key=lambda x: package_version.parse(x) + ) + + version_list = [] + for version in sorted_version_list: + if package_version.parse(version) >= package_version.parse(from_version): + version_list.append(version) + return version_list + + +async def add_version_record(profile: Profile, version: str): + """Add an acapy_version storage record for provided version.""" + async with profile.session() as session: + storage = session.inject(BaseStorage) + try: + version_storage_record = await storage.find_record( + type_filter=RECORD_TYPE_ACAPY_VERSION, tag_query={} + ) + except StorageNotFoundError: + version_storage_record = None + if not version_storage_record: + await storage.add_record( + StorageRecord( + RECORD_TYPE_ACAPY_VERSION, + version, + ) + ) + else: + await storage.update_record(version_storage_record, version, {}) + LOGGER.info(f"{RECORD_TYPE_ACAPY_VERSION} storage record set to {version}") + + +async def upgrade( + settings: Optional[Union[Mapping[str, Any], BaseSettings]] = None, + profile: Optional[Profile] = None, +): """Perform upgradation steps.""" - context_builder = DefaultContextBuilder(settings) - context = await context_builder.build_context() try: + if profile and (settings or settings == {}): + raise UpgradeError("upgrade requires either profile or settings, not both.") + if profile: + root_profile = profile + settings = profile.settings + else: + context_builder = DefaultContextBuilder(settings) + context = await context_builder.build_context() + root_profile, _ = await wallet_config(context) version_upgrade_config_inst = VersionUpgradeConfig( settings.get("upgrade.config_path") ) upgrade_configs = version_upgrade_config_inst.upgrade_configs - root_profile, public_did = await wallet_config(context) - version_storage_record = None upgrade_to_version = f"v{__version__}" versions_found_in_config = upgrade_configs.keys() sorted_versions_found_in_config = sorted( versions_found_in_config, key=lambda x: package_version.parse(x) ) + upgrade_from_version_storage = None + upgrade_from_version_config = None + upgrade_from_version = None async with root_profile.session() as session: storage = session.inject(BaseStorage) try: version_storage_record = await storage.find_record( type_filter=RECORD_TYPE_ACAPY_VERSION, tag_query={} ) - upgrade_from_version = version_storage_record.value - if "upgrade.from_version" in settings: - print( - ( - f"version {upgrade_from_version} found in storage" - ", --from-version will be ignored." - ) - ) + upgrade_from_version_storage = version_storage_record.value except StorageNotFoundError: - if "upgrade.from_version" in settings: - upgrade_from_version = settings.get("upgrade.from_version") - else: - upgrade_from_version = sorted_versions_found_in_config[-1] - print( - "No ACA-Py version found in wallet storage and " - "no --from-version specified. Selecting " - f"{upgrade_from_version} as --from-version from " - "the config." + LOGGER.info("No ACA-Py version found in wallet storage.") + version_storage_record = None + + if "upgrade.from_version" in settings: + upgrade_from_version_config = settings.get("upgrade.from_version") + LOGGER.info( + ( + f"Selecting {upgrade_from_version_config} as " + "--from-version from the config." ) - if upgrade_from_version == upgrade_to_version: - raise UpgradeError( - f"Version {upgrade_from_version} to upgrade from and " - f"current version to upgrade to {upgrade_to_version} " - "are same." - ) - if upgrade_from_version not in sorted_versions_found_in_config: + ) + + force_upgrade_flag = settings.get("upgrade.force_upgrade") or False + if upgrade_from_version_storage and upgrade_from_version_config: + if ( + package_version.parse(upgrade_from_version_storage) + > package_version.parse(upgrade_from_version_config) + ) and force_upgrade_flag: + upgrade_from_version = upgrade_from_version_config + else: + upgrade_from_version = upgrade_from_version_storage + if ( + not upgrade_from_version + and not upgrade_from_version_storage + and upgrade_from_version_config + ): + upgrade_from_version = upgrade_from_version_config + if ( + not upgrade_from_version + and upgrade_from_version_storage + and not upgrade_from_version_config + ): + upgrade_from_version = upgrade_from_version_storage + if not upgrade_from_version: raise UpgradeError( - f"No upgrade configuration found for {upgrade_from_version}" + "No upgrade from version found in wallet or settings [--from-version]" ) - upgrade_from_version_index = sorted_versions_found_in_config.index( - upgrade_from_version + upgrade_version_in_config = get_upgrade_version_list( + sorted_version_list=sorted_versions_found_in_config, + from_version=upgrade_from_version, ) - for config_from_version in sorted_versions_found_in_config[ - upgrade_from_version_index: - ]: - print(f"Running upgrade process for {config_from_version}") - upgrade_config = upgrade_configs.get(config_from_version) - # Step 1 re-saving all BaseRecord and BaseExchangeRecord - if "resave_records" in upgrade_config: - resave_record_paths = upgrade_config.get("resave_records") - for record_path in resave_record_paths: - try: - record_type = ClassLoader.load_class(record_path) - except ClassNotFoundError as err: - raise UpgradeError( - f"Unknown Record type {record_path}" - ) from err - if not issubclass(record_type, BaseRecord): - raise UpgradeError( - f"Only BaseRecord can be resaved, found: {str(record_type)}" - ) - async with root_profile.session() as session: - all_records = await record_type.query(session) - for record in all_records: - await record.save( - session, - reason="re-saving record during ACA-Py upgrade process", - ) - if len(all_records) == 0: - print(f"No records of {str(record_type)} found") - else: - print( - f"All records of {str(record_type)} successfully re-saved" - ) - # Step 2 Update existing records, if required - if ( - "update_existing_records" in upgrade_config - and upgrade_config.get("update_existing_records") is True - ): - update_existing_recs_callable = ( - version_upgrade_config_inst.get_update_existing_func( - config_from_version - ) + to_update_flag = False + if upgrade_from_version == upgrade_to_version: + LOGGER.info( + ( + f"Version {upgrade_from_version} to upgrade from and " + f"current version to upgrade to {upgrade_to_version} " + "are same. You can apply upgrade from a lower " + "version by running the upgrade command with " + f"--from-version [< {upgrade_to_version}] and " + "--force-upgrade" ) - if not update_existing_recs_callable: + ) + else: + resave_record_path_sets = set() + executables_call_set = set() + for config_from_version in upgrade_version_in_config: + LOGGER.info(f"Running upgrade process for {config_from_version}") + upgrade_config = upgrade_configs.get(config_from_version) + # Step 1 re-saving all BaseRecord and BaseExchangeRecord + if "resave_records" in upgrade_config: + resave_record_paths = upgrade_config.get("resave_records") + for record_path in resave_record_paths: + resave_record_path_sets.add(record_path) + + # Step 2 Update existing records, if required + config_key_set = set(upgrade_config.keys()) + try: + config_key_set.remove("resave_records") + except KeyError: + pass + for callable_name in list(config_key_set): + if upgrade_config.get(callable_name) is False: + continue + executables_call_set.add(callable_name) + + if len(resave_record_path_sets) >= 1 or len(executables_call_set) >= 1: + to_update_flag = True + for record_path in resave_record_path_sets: + try: + rec_type = ClassLoader.load_class(record_path) + except ClassNotFoundError as err: + raise UpgradeError(f"Unknown Record type {record_path}") from err + if not issubclass(rec_type, BaseRecord): raise UpgradeError( - "No update_existing_records function " - f"specified for {config_from_version}" + f"Only BaseRecord can be resaved, found: {str(rec_type)}" ) - await update_existing_recs_callable(root_profile) + async with root_profile.session() as session: + all_records = await rec_type.query(session) + for record in all_records: + await record.save( + session, + reason="re-saving record during the upgrade process", + ) + if len(all_records) == 0: + LOGGER.info(f"No records of {str(rec_type)} found") + else: + LOGGER.info( + f"All recs of {str(rec_type)} successfully re-saved" + ) + for callable_name in executables_call_set: + _callable = version_upgrade_config_inst.get_callable(callable_name) + if not _callable: + raise UpgradeError(f"No function specified for {callable_name}") + await _callable(root_profile) + # Update storage version - async with root_profile.session() as session: - storage = session.inject(BaseStorage) - if not version_storage_record: - await storage.add_record( - StorageRecord( - RECORD_TYPE_ACAPY_VERSION, - upgrade_to_version, + if to_update_flag: + async with root_profile.session() as session: + storage = session.inject(BaseStorage) + if not version_storage_record: + await storage.add_record( + StorageRecord( + RECORD_TYPE_ACAPY_VERSION, + upgrade_to_version, + ) ) + else: + await storage.update_record( + version_storage_record, upgrade_to_version, {} + ) + LOGGER.info( + f"{RECORD_TYPE_ACAPY_VERSION} storage record " + f"set to {upgrade_to_version}" ) - else: - await storage.update_record( - version_storage_record, upgrade_to_version, {} - ) - await root_profile.close() + if not profile: + await root_profile.close() except BaseError as e: raise UpgradeError(f"Error during upgrade: {e}") @@ -236,7 +332,7 @@ def execute(argv: Sequence[str] = None): settings = get_settings(args) common_config(settings) loop = asyncio.get_event_loop() - loop.run_until_complete(upgrade(settings)) + loop.run_until_complete(upgrade(settings=settings)) def main(): @@ -245,12 +341,8 @@ def main(): execute() -# Update every release -CONFIG_v7_3 = { - "v0.7.2": { - "update_existing_function_inst": update_existing_records, - }, +UPGRADE_EXISTING_RECORDS_FUNCTION_MAPPING = { + "update_existing_records": update_existing_records } - main() diff --git a/aries_cloudagent/config/argparse.py b/aries_cloudagent/config/argparse.py index 0f20901662..af00b4220d 100644 --- a/aries_cloudagent/config/argparse.py +++ b/aries_cloudagent/config/argparse.py @@ -18,6 +18,8 @@ from .error import ArgsParseError from .util import BoundedInt, ByteSize +from .plugin_settings import PLUGIN_CONFIG_KEY + CAT_PROVISION = "general" CAT_START = "start" CAT_UPGRADE = "upgrade" @@ -72,7 +74,8 @@ def create_argument_parser(*, prog: str = None): def load_argument_groups(parser: ArgumentParser, *groups: Type[ArgumentGroup]): - """Log a set of argument groups into a parser. + """ + Log a set of argument groups into a parser. Returns: A callable to convert loaded arguments into a settings dictionary @@ -268,6 +271,12 @@ def add_arguments(self, parser: ArgumentParser): "Default: false." ), ) + parser.add_argument( + "--debug-webhooks", + action="store_true", + env_var="ACAPY_DEBUG_WEBHOOKS", + help=("Emit protocol state object as webhook. " "Default: false."), + ) parser.add_argument( "--invite", action="store_true", @@ -421,6 +430,8 @@ def get_settings(self, args: Namespace) -> dict: settings["debug.credentials"] = True if args.debug_presentations: settings["debug.presentations"] = True + if args.debug_webhooks: + settings["debug.webhooks"] = True if args.debug_seed: settings["debug.seed"] = args.debug_seed if args.invite: @@ -534,6 +545,20 @@ def add_arguments(self, parser: ArgumentParser): ), ) + parser.add_argument( + "--block-plugin", + dest="blocked_plugins", + type=str, + action="append", + required=False, + metavar="", + env_var="ACAPY_BLOCKED_PLUGIN", + help=( + "Block plugin module from loading. Multiple " + "instances of this parameter can be specified." + ), + ) + parser.add_argument( "--plugin-config", dest="plugin_config", @@ -604,6 +629,36 @@ def add_arguments(self, parser: ArgumentParser): env_var="ACAPY_READ_ONLY_LEDGER", help="Sets ledger to read-only to prevent updates. Default: false.", ) + parser.add_argument( + "--universal-resolver", + type=str, + nargs="?", + metavar="", + env_var="ACAPY_UNIVERSAL_RESOLVER", + const="DEFAULT", + help="Enable resolution from a universal resolver.", + ) + parser.add_argument( + "--universal-resolver-regex", + type=str, + nargs="+", + metavar="", + env_var="ACAPY_UNIVERSAL_RESOLVER_REGEX", + help=( + "Regex matching DIDs to resolve using the unversal resolver. " + "Multiple can be specified. " + "Defaults to a regex matching all DIDs resolvable by universal " + "resolver instance." + ), + ) + parser.add_argument( + "--universal-resolver-bearer-token", + type=str, + nargs="?", + metavar="", + env_var="ACAPY_UNIVERSAL_RESOLVER_BEARER_TOKEN", + help="Bearer token if universal resolver instance requires authentication.", + ), def get_settings(self, args: Namespace) -> dict: """Extract general settings.""" @@ -611,19 +666,22 @@ def get_settings(self, args: Namespace) -> dict: if args.external_plugins: settings["external_plugins"] = args.external_plugins + if args.blocked_plugins: + settings["blocked_plugins"] = args.blocked_plugins + if args.plugin_config: with open(args.plugin_config, "r") as stream: - settings["plugin_config"] = yaml.safe_load(stream) + settings[PLUGIN_CONFIG_KEY] = yaml.safe_load(stream) if args.plugin_config_values: - if "plugin_config" not in settings: - settings["plugin_config"] = {} + if PLUGIN_CONFIG_KEY not in settings: + settings[PLUGIN_CONFIG_KEY] = {} for value_str in chain(*args.plugin_config_values): key, value = value_str.split("=", maxsplit=1) value = yaml.safe_load(value) deepmerge.always_merger.merge( - settings["plugin_config"], + settings[PLUGIN_CONFIG_KEY], reduce(lambda v, k: {k: v}, key.split(".")[::-1], value), ) @@ -640,6 +698,27 @@ def get_settings(self, args: Namespace) -> dict: if args.read_only_ledger: settings["read_only_ledger"] = True + + if args.universal_resolver_regex and not args.universal_resolver: + raise ArgsParseError( + "--universal-resolver-regex cannot be used without --universal-resolver" + ) + + if args.universal_resolver_bearer_token and not args.universal_resolver: + raise ArgsParseError( + "--universal-resolver-bearer-token " + + "cannot be used without --universal-resolver" + ) + + if args.universal_resolver: + settings["resolver.universal"] = args.universal_resolver + + if args.universal_resolver_regex: + settings["resolver.universal.supported"] = args.universal_resolver_regex + + if args.universal_resolver_bearer_token: + settings["resolver.universal.token"] = args.universal_resolver_bearer_token + return settings @@ -680,7 +759,7 @@ def add_arguments(self, parser: ArgumentParser): parser.add_argument( "--monitor-revocation-notification", action="store_true", - env_var="ACAPY_NOTIFY_REVOCATION", + env_var="ACAPY_MONITOR_REVOCATION_NOTIFICATION", help=( "Specifies that aca-py will emit webhooks on notification of " "revocation received." @@ -800,6 +879,18 @@ def add_arguments(self, parser: ArgumentParser): " HyperLedger Indy ledgers." ), ) + parser.add_argument( + "--accept-taa", + type=str, + nargs=2, + metavar=("", ""), + env_var="ACAPY_ACCEPT_TAA", + help=( + "Specify the acceptance mechanism and taa version for which to accept" + " the transaction author agreement. If not provided, the TAA must" + " be accepted through the TTY or the admin API." + ), + ) def get_settings(self, args: Namespace) -> dict: """Extract ledger settings.""" @@ -807,37 +898,64 @@ def get_settings(self, args: Namespace) -> dict: if args.no_ledger: settings["ledger.disabled"] = True else: - configured = False + single_configured = False + multi_configured = False + update_pool_name = False if args.genesis_url: settings["ledger.genesis_url"] = args.genesis_url - configured = True + single_configured = True elif args.genesis_file: settings["ledger.genesis_file"] = args.genesis_file - configured = True + single_configured = True elif args.genesis_transactions: settings["ledger.genesis_transactions"] = args.genesis_transactions - configured = True + single_configured = True if args.genesis_transactions_list: with open(args.genesis_transactions_list, "r") as stream: txn_config_list = yaml.safe_load(stream) ledger_config_list = [] for txn_config in txn_config_list: ledger_config_list.append(txn_config) + if "is_write" in txn_config and txn_config["is_write"]: + if "genesis_url" in txn_config: + settings["ledger.genesis_url"] = txn_config[ + "genesis_url" + ] + elif "genesis_file" in txn_config: + settings["ledger.genesis_file"] = txn_config[ + "genesis_file" + ] + elif "genesis_transactions" in txn_config: + settings["ledger.genesis_transactions"] = txn_config[ + "genesis_transactions" + ] + else: + raise ArgsParseError( + "No genesis information provided for write ledger" + ) + if "id" in txn_config: + settings["ledger.pool_name"] = txn_config["id"] + update_pool_name = True settings["ledger.ledger_config_list"] = ledger_config_list - configured = True - if not configured: + multi_configured = True + if not (single_configured or multi_configured): raise ArgsParseError( "One of --genesis-url --genesis-file, --genesis-transactions " "or --genesis-transactions-list must be specified (unless " "--no-ledger is specified to explicitly configure aca-py to" " run with no ledger)." ) - if args.ledger_pool_name: + if single_configured and multi_configured: + raise ArgsParseError("Cannot configure both single- and multi-ledger.") + if args.ledger_pool_name and not update_pool_name: settings["ledger.pool_name"] = args.ledger_pool_name if args.ledger_keepalive: settings["ledger.keepalive"] = args.ledger_keepalive if args.ledger_socks_proxy: settings["ledger.socks_proxy"] = args.ledger_socks_proxy + if args.accept_taa: + settings["ledger.taa_acceptance_mechanism"] = args.accept_taa[0] + settings["ledger.taa_acceptance_version"] = args.accept_taa[1] return settings @@ -883,6 +1001,46 @@ def add_arguments(self, parser: ArgumentParser): "('debug', 'info', 'warning', 'error', 'critical')" ), ) + parser.add_argument( + "--log-handler-config", + dest="log_handler_config", + type=str, + metavar="", + default=None, + env_var="ACAPY_LOG_HANDLER_CONFIG", + help=( + "Specifies when, interval, backupCount for the " + "TimedRotatingFileHandler. These attributes are " + "passed as a ; seperated string. For example, " + "when of D (days), interval of 7 and backupCount " + "of 1 will be passed as 'D;7;1'. Note: " + "backupCount of 0 will mean all backup log files " + "will be retained and not deleted at all." + ), + ) + parser.add_argument( + "--log-fmt-pattern", + dest="log_fmt_pattern", + type=str, + metavar="", + default=None, + env_var="ACAPY_LOG_FMT_PATTERN", + help=( + "Specifies logging formatter pattern as string. Examples are included " + "in 'Logging.md'. For information regarding different attributes " + "supported in the pattern, please look at " + "https://docs.python.org/3/library/logging.html#logrecord-attributes." + ), + ) + parser.add_argument( + "--log-json-fmt", + action="store_true", + env_var="ACAPY_LOG_JSON_FMT", + help=( + "Specifies whether to use JSON logging formatter or " + "text logging formatter." + ), + ) def get_settings(self, args: Namespace) -> dict: """Extract logging settings.""" @@ -893,6 +1051,29 @@ def get_settings(self, args: Namespace) -> dict: settings["log.file"] = args.log_file if args.log_level: settings["log.level"] = args.log_level + if args.log_handler_config: + try: + handler_config_attribs = (args.log_handler_config).split(";") + settings["log.handler_when"] = handler_config_attribs[0] + settings["log.handler_interval"] = int(handler_config_attribs[1]) + settings["log.handler_bakcount"] = int(handler_config_attribs[2]) + except IndexError: + raise ArgsParseError( + "With --log-handler-config, the provided argument must be " + "in 'when;interval;backupCount' format. Each of the 3 " + "attributes for TimedRotatingFileHandler must be specified." + ) + except ValueError: + raise ArgsParseError( + "With --log-handler-config, 'interval' and 'backupCount' " + "should be a number [int]" + ) + if args.log_fmt_pattern: + settings["log.fmt_pattern"] = args.log_fmt_pattern + if args.log_json_fmt: + settings["log.json_fmt"] = True + else: + settings["log.json_fmt"] = False return settings @@ -946,7 +1127,17 @@ def add_arguments(self, parser: ArgumentParser): action="store_true", env_var="ACAPY_PUBLIC_INVITES", help=( - "Send invitations out, and receive connection requests, " + "Send invitations out using the public DID for the agent, " + "and receive connection requests solicited by invitations " + "which use the public DID. Default: false." + ), + ) + parser.add_argument( + "--requests-through-public-did", + action="store_true", + env_var="ACAPY_REQUESTS_THROUGH_PUBLIC_DID", + help=( + "Allow agent to receive unsolicited connection requests, " "using the public DID for the agent. Default: false." ), ) @@ -1041,6 +1232,13 @@ def get_settings(self, args: Namespace) -> dict: settings["monitor_forward"] = args.monitor_forward if args.public_invites: settings["public_invites"] = True + if args.requests_through_public_did: + if not args.public_invites: + raise ArgsParseError( + "--public-invites is required to use " + "--requests-through-public-did" + ) + settings["requests_through_public_did"] = True if args.timing: settings["timing.enabled"] = True if args.timing_log: @@ -1154,22 +1352,6 @@ def add_arguments(self, parser: ArgumentParser): "Supported outbound transport types are 'http' and 'ws'." ), ) - parser.add_argument( - "-oq", - "--outbound-queue", - dest="outbound_queue", - type=str, - env_var="ACAPY_OUTBOUND_TRANSPORT_QUEUE", - help=( - "Defines the location of the Outbound Queue Engine. This must be " - "a 'dotpath' to a Python module on the PYTHONPATH, followed by a " - "colon, followed by the name of a Python class that implements " - "BaseOutboundQueue. This commandline option is the official entry " - "point of ACA-py's pluggable queue interface. The default value is: " - "'aries_cloudagent.transport.outbound.queue.redis:RedisOutboundQueue'." - "" - ), - ) parser.add_argument( "-l", "--label", @@ -1249,20 +1431,10 @@ def get_settings(self, args: Namespace): settings["transport.inbound_configs"] = args.inbound_transports else: raise ArgsParseError("-it/--inbound-transport is required") - if not args.outbound_transports and not args.outbound_queue: - raise ArgsParseError( - "-ot/--outbound-transport or -oq/--outbound-queue is required" - ) - if args.outbound_transports and args.outbound_queue: - raise ArgsParseError( - "-ot/--outbound-transport and -oq/--outbound-queue are " - "not allowed together" - ) if args.outbound_transports: settings["transport.outbound_configs"] = args.outbound_transports - if args.outbound_queue: - settings["transport.outbound_queue"] = args.outbound_queue - + else: + raise ArgsParseError("-ot/--outbound-transport is required") settings["transport.enable_undelivered_queue"] = args.enable_undelivered_queue if args.label: @@ -1408,6 +1580,15 @@ def add_arguments(self, parser: ArgumentParser): "to use with a Hyperledger Indy ledger." ), ) + parser.add_argument( + "--wallet-allow-insecure-seed", + action="store_true", + env_var="ACAPY_WALLET_ALLOW_INSECURE_SEED", + help=( + "If this parameter is set, allows to use a custom seed " + "to create a local DID" + ), + ) parser.add_argument( "--wallet-key", type=str, @@ -1528,6 +1709,8 @@ def get_settings(self, args: Namespace) -> dict: settings["wallet.seed"] = args.seed if args.wallet_local_did: settings["wallet.local_did"] = True + if args.wallet_allow_insecure_seed: + settings["wallet.allow_insecure_seed"] = True if args.wallet_key: settings["wallet.key"] = args.wallet_key if args.wallet_rekey: @@ -1603,13 +1786,27 @@ def add_arguments(self, parser: ArgumentParser): parser.add_argument( "--multitenancy-config", type=str, - metavar="", + nargs="+", + metavar="key=value", env_var="ACAPY_MULTITENANCY_CONFIGURATION", help=( - 'Specify multitenancy configuration ("wallet_type" and "wallet_name"). ' - 'For example: "{"wallet_type":"askar-profile","wallet_name":' - '"askar-profile-name"}"' - '"wallet_name" is only used when "wallet_type" is "askar-profile"' + "Specify multitenancy configuration in key=value pairs. " + 'For example: "wallet_type=askar-profile wallet_name=askar-profile-name" ' + "Possible values: wallet_name, wallet_key, cache_size, " + 'key_derivation_method. "wallet_name" is only used when ' + '"wallet_type" is "askar-profile"' + ), + ) + parser.add_argument( + "--base-wallet-routes", + type=str, + nargs="+", + required=False, + metavar="", + help=( + "Patterns matching admin routes that should be permitted for " + "base wallet. The base wallet is preconfigured to have access to " + "essential endpoints. This argument should be used sparingly." ), ) @@ -1630,17 +1827,40 @@ def get_settings(self, args: Namespace): settings["multitenant.admin_enabled"] = True if args.multitenancy_config: - multitenancyConfig = json.loads(args.multitenancy_config) - - if multitenancyConfig.get("wallet_type"): - settings["multitenant.wallet_type"] = multitenancyConfig.get( - "wallet_type" - ) - - if multitenancyConfig.get("wallet_name"): - settings["multitenant.wallet_name"] = multitenancyConfig.get( - "wallet_name" - ) + # Legacy support + if ( + len(args.multitenancy_config) == 1 + and args.multitenancy_config[0][0] == "{" + ): + multitenancy_config = json.loads(args.multitenancy_config[0]) + if multitenancy_config.get("wallet_type"): + settings["multitenant.wallet_type"] = multitenancy_config.get( + "wallet_type" + ) + + if multitenancy_config.get("wallet_name"): + settings["multitenant.wallet_name"] = multitenancy_config.get( + "wallet_name" + ) + + if multitenancy_config.get("cache_size"): + settings["multitenant.cache_size"] = multitenancy_config.get( + "cache_size" + ) + + if multitenancy_config.get("key_derivation_method"): + settings[ + "multitenant.key_derivation_method" + ] = multitenancy_config.get("key_derivation_method") + + else: + for value_str in args.multitenancy_config: + key, value = value_str.split("=", maxsplit=1) + value = yaml.safe_load(value) + settings[f"multitenant.{key}"] = value + + if args.base_wallet_routes: + settings["multitenant.base_wallet_routes"] = args.base_wallet_routes return settings @@ -1690,6 +1910,17 @@ def add_arguments(self, parser: ArgumentParser): "agent who will be endorsing transactions." ), ) + parser.add_argument( + "--endorser-endorse-with-did", + type=str, + metavar="", + env_var="ACAPY_ENDORSER_ENDORSE_WITH_DID", + help=( + "For transaction Endorsers, specify the DID to use to endorse " + "transactions. The default (if not specified) is to use the " + "Endorser's Public DID." + ), + ) parser.add_argument( "--endorser-alias", type=str, @@ -1733,6 +1964,13 @@ def add_arguments(self, parser: ArgumentParser): " the controller must invoke the endpoints required to create the" " revocation registry and assign to the cred def.)", ) + parser.add_argument( + "--auto-promote-author-did", + action="store_true", + env_var="ACAPY_AUTO_PROMOTE_AUTHOR_DID", + help="For Authors, specify whether to automatically promote" + " a DID to the wallet public DID after writing to the ledger.", + ) def get_settings(self, args: Namespace): """Extract endorser settings.""" @@ -1742,6 +1980,7 @@ def get_settings(self, args: Namespace): settings["endorser.auto_endorse"] = False settings["endorser.auto_write"] = False settings["endorser.auto_create_rev_reg"] = False + settings["endorser.auto_promote_author_did"] = False if args.endorser_protocol_role: if args.endorser_protocol_role == ENDORSER_AUTHOR: @@ -1758,6 +1997,17 @@ def get_settings(self, args: Namespace): "Authors" ) + if args.endorser_endorse_with_did: + if settings["endorser.endorser"]: + settings[ + "endorser.endorser_endorse_with_did" + ] = args.endorser_endorse_with_did + else: + raise ArgsParseError( + "Parameter --endorser-endorse-with-did should only be set for " + "transaction Endorsers" + ) + if args.endorser_alias: if settings["endorser.author"]: settings["endorser.endorser_alias"] = args.endorser_alias @@ -1790,7 +2040,6 @@ def get_settings(self, args: Namespace): if settings["endorser.author"]: settings["endorser.auto_request"] = True else: - pass raise ArgsParseError( "Parameter --auto-request-endorsement should only be set for " "transaction Authors" @@ -1800,7 +2049,6 @@ def get_settings(self, args: Namespace): if settings["endorser.endorser"]: settings["endorser.auto_endorse"] = True else: - pass raise ArgsParseError( "Parameter --auto-endorser-transactions should only be set for " "transaction Endorsers" @@ -1810,7 +2058,6 @@ def get_settings(self, args: Namespace): if settings["endorser.author"]: settings["endorser.auto_write"] = True else: - pass raise ArgsParseError( "Parameter --auto-write-transactions should only be set for " "transaction Authors" @@ -1820,16 +2067,24 @@ def get_settings(self, args: Namespace): if settings["endorser.author"]: settings["endorser.auto_create_rev_reg"] = True else: - pass raise ArgsParseError( "Parameter --auto-create-revocation-transactions should only be set " "for transaction Authors" ) + if args.auto_promote_author_did: + if settings["endorser.author"]: + settings["endorser.auto_promote_author_did"] = True + else: + raise ArgsParseError( + "Parameter --auto-promote-author-did should only be set " + "for transaction Authors" + ) + return settings -@group(CAT_UPGRADE) +@group(CAT_START, CAT_UPGRADE) class UpgradeGroup(ArgumentGroup): """ACA-Py Upgrade process settings.""" @@ -1861,6 +2116,17 @@ def add_arguments(self, parser: ArgumentParser): ), ) + parser.add_argument( + "--force-upgrade", + action="store_true", + env_var="ACAPY_UPGRADE_FORCE_UPGRADE", + help=( + "Forces the '—from-version' argument to override the version " + "retrieved from secure storage when calculating upgrades to " + "be run." + ), + ) + def get_settings(self, args: Namespace) -> dict: """Extract ACA-Py upgrade process settings.""" settings = {} @@ -1868,4 +2134,6 @@ def get_settings(self, args: Namespace) -> dict: settings["upgrade.config_path"] = args.upgrade_config_path if args.from_version: settings["upgrade.from_version"] = args.from_version + if args.force_upgrade: + settings["upgrade.force_upgrade"] = args.force_upgrade return settings diff --git a/aries_cloudagent/config/base.py b/aries_cloudagent/config/base.py index 92c48f4756..00d0c4a31c 100644 --- a/aries_cloudagent/config/base.py +++ b/aries_cloudagent/config/base.py @@ -1,7 +1,7 @@ """Configuration base classes.""" from abc import ABC, abstractmethod -from typing import Mapping, Optional, Type, TypeVar +from typing import Any, Iterator, Mapping, Optional, Type, TypeVar from ..core.error import BaseError @@ -16,11 +16,11 @@ class SettingsError(ConfigError): """The base exception raised by `BaseSettings` implementations.""" -class BaseSettings(Mapping[str, object]): +class BaseSettings(Mapping[str, Any]): """Base settings class.""" @abstractmethod - def get_value(self, *var_names, default=None): + def get_value(self, *var_names, default: Optional[Any] = None) -> Any: """Fetch a setting. Args: @@ -32,7 +32,7 @@ def get_value(self, *var_names, default=None): """ - def get_bool(self, *var_names, default=None) -> bool: + def get_bool(self, *var_names, default: Optional[bool] = None) -> Optional[bool]: """Fetch a setting as a boolean value. Args: @@ -42,9 +42,10 @@ def get_bool(self, *var_names, default=None) -> bool: value = self.get_value(*var_names, default) if value is not None: value = bool(value and value not in ("false", "False", "0")) + return value - def get_int(self, *var_names, default=None) -> int: + def get_int(self, *var_names, default: Optional[int] = None) -> Optional[int]: """Fetch a setting as an integer value. Args: @@ -54,9 +55,10 @@ def get_int(self, *var_names, default=None) -> int: value = self.get_value(*var_names, default) if value is not None: value = int(value) + return value - def get_str(self, *var_names, default=None) -> str: + def get_str(self, *var_names, default: Optional[str] = None) -> Optional[str]: """Fetch a setting as a string value. Args: @@ -66,10 +68,11 @@ def get_str(self, *var_names, default=None) -> str: value = self.get_value(*var_names, default=default) if value is not None: value = str(value) + return value @abstractmethod - def __iter__(self): + def __iter__(self) -> Iterator: """Iterate settings keys.""" def __getitem__(self, index): @@ -91,9 +94,13 @@ def copy(self) -> "BaseSettings": """Produce a copy of the settings instance.""" @abstractmethod - def extend(self, other: Mapping[str, object]) -> "BaseSettings": + def extend(self, other: Mapping[str, Any]) -> "BaseSettings": """Merge another mapping to produce a new settings instance.""" + @abstractmethod + def to_dict(self) -> dict: + """Return a dict of the settings instance.""" + def __repr__(self) -> str: """Provide a human readable representation of this object.""" items = ("{}={}".format(k, self[k]) for k in self) @@ -111,7 +118,7 @@ class BaseInjector(ABC): def inject( self, base_cls: Type[InjectType], - settings: Mapping[str, object] = None, + settings: Optional[Mapping[str, Any]] = None, ) -> InjectType: """ Get the provided instance of a given class identifier. @@ -129,7 +136,7 @@ def inject( def inject_or( self, base_cls: Type[InjectType], - settings: Mapping[str, object] = None, + settings: Optional[Mapping[str, Any]] = None, default: Optional[InjectType] = None, ) -> Optional[InjectType]: """ diff --git a/aries_cloudagent/config/base_context.py b/aries_cloudagent/config/base_context.py index 4fde34759d..788a91940f 100644 --- a/aries_cloudagent/config/base_context.py +++ b/aries_cloudagent/config/base_context.py @@ -1,7 +1,7 @@ """Base injection context builder classes.""" from abc import ABC, abstractmethod -from typing import Mapping +from typing import Any, Mapping, Optional from .injection_context import InjectionContext from .settings import Settings @@ -10,7 +10,7 @@ class ContextBuilder(ABC): """Base injection context builder class.""" - def __init__(self, settings: Mapping[str, object] = None): + def __init__(self, settings: Optional[Mapping[str, Any]] = None): """ Initialize an instance of the context builder. diff --git a/aries_cloudagent/config/default_context.py b/aries_cloudagent/config/default_context.py index 4e9b0eddef..a99c3b44a9 100644 --- a/aries_cloudagent/config/default_context.py +++ b/aries_cloudagent/config/default_context.py @@ -1,28 +1,31 @@ """Classes for configuring the default injection context.""" -from .base_context import ContextBuilder -from .injection_context import InjectionContext -from .provider import CachedProvider, ClassProvider - from ..cache.base import BaseCache from ..cache.in_memory import InMemoryCache from ..core.event_bus import EventBus +from ..core.goal_code_registry import GoalCodeRegistry from ..core.plugin_registry import PluginRegistry from ..core.profile import ProfileManager, ProfileManagerProvider from ..core.protocol_registry import ProtocolRegistry -from ..core.goal_code_registry import GoalCodeRegistry -from ..resolver.did_resolver import DIDResolver -from ..resolver.did_resolver_registry import DIDResolverRegistry -from ..tails.base import BaseTailsServer - from ..protocols.actionmenu.v1_0.base_service import BaseMenuService from ..protocols.actionmenu.v1_0.driver_service import DriverMenuService from ..protocols.didcomm_prefix import DIDCommPrefix from ..protocols.introduction.v0_1.base_service import BaseIntroductionService from ..protocols.introduction.v0_1.demo_service import DemoIntroductionService +from ..resolver.did_resolver import DIDResolver +from ..tails.base import BaseTailsServer from ..transport.wire_format import BaseWireFormat -from ..utils.stats import Collector from ..utils.dependencies import is_indy_sdk_module_installed +from ..utils.stats import Collector +from ..wallet.default_verification_key_strategy import ( + DefaultVerificationKeyStrategy, + BaseVerificationKeyStrategy, +) +from ..wallet.did_method import DIDMethods +from ..wallet.key_type import KeyTypes +from .base_context import ContextBuilder +from .injection_context import InjectionContext +from .provider import CachedProvider, ClassProvider class DefaultContextBuilder(ContextBuilder): @@ -50,12 +53,13 @@ async def build_context(self) -> InjectionContext: # Global event bus context.injector.bind_instance(EventBus, EventBus()) - # Global did resolver registry - did_resolver_registry = DIDResolverRegistry() - context.injector.bind_instance(DIDResolverRegistry, did_resolver_registry) - # Global did resolver - context.injector.bind_instance(DIDResolver, DIDResolver(did_resolver_registry)) + context.injector.bind_instance(DIDResolver, DIDResolver([])) + context.injector.bind_instance(DIDMethods, DIDMethods()) + context.injector.bind_instance(KeyTypes, KeyTypes()) + context.injector.bind_instance( + BaseVerificationKeyStrategy, DefaultVerificationKeyStrategy() + ) await self.bind_providers(context) await self.load_plugins(context) @@ -111,7 +115,9 @@ async def bind_providers(self, context: InjectionContext): async def load_plugins(self, context: InjectionContext): """Set up plugin registry and load plugins.""" - plugin_registry = PluginRegistry() + plugin_registry = PluginRegistry( + blocklist=self.settings.get("blocked_plugins", []) + ) context.injector.bind_instance(PluginRegistry, plugin_registry) # Register standard protocol plugins @@ -127,6 +133,7 @@ async def load_plugins(self, context: InjectionContext): plugin_registry.register_plugin("aries_cloudagent.messaging.jsonld") plugin_registry.register_plugin("aries_cloudagent.revocation") plugin_registry.register_plugin("aries_cloudagent.resolver") + plugin_registry.register_plugin("aries_cloudagent.settings") plugin_registry.register_plugin("aries_cloudagent.wallet") if context.settings.get("multitenant.admin_enabled"): diff --git a/aries_cloudagent/config/ledger.py b/aries_cloudagent/config/ledger.py index cdd2d9efc5..8ff0f66640 100644 --- a/aries_cloudagent/config/ledger.py +++ b/aries_cloudagent/config/ledger.py @@ -4,6 +4,7 @@ import logging import re import sys +from typing import Optional import uuid import markdown @@ -30,7 +31,9 @@ async def fetch_genesis_transactions(genesis_url: str) -> str: headers["Content-Type"] = "application/json" LOGGER.info("Fetching genesis transactions from: %s", genesis_url) try: - return await fetch(genesis_url, headers=headers) + # Fetch from --genesis-url likely to fail in composed container setup + # https://github.com/hyperledger/aries-cloudagent-python/issues/1745 + return await fetch(genesis_url, headers=headers, max_attempts=20) except FetchError as e: raise ConfigError("Error retrieving ledger genesis transactions") from e @@ -140,7 +143,7 @@ async def ledger_config( not taa_accepted or taa_info["taa_record"]["digest"] != taa_accepted["digest"] ): - if not await accept_taa(ledger, taa_info, provision): + if not await accept_taa(ledger, profile, taa_info, provision): return False # Publish endpoints if necessary - skipped if TAA is required but not accepted @@ -162,13 +165,8 @@ async def ledger_config( return True -async def accept_taa(ledger: BaseLedger, taa_info, provision: bool = False) -> bool: - """Perform TAA acceptance.""" - - if not sys.stdout.isatty(): - LOGGER.warning("Cannot accept TAA without interactive terminal") - return False - +async def select_aml_tty(taa_info, provision: bool = False) -> Optional[str]: + """Select acceptance mechanism from AML.""" mechanisms = taa_info["aml_record"]["aml"] allow_opts = OrderedDict( [ @@ -230,16 +228,62 @@ async def accept_taa(ledger: BaseLedger, taa_info, provision: bool = False) -> b try: opt = await prompt_toolkit.prompt(opts_text, async_=True) except EOFError: - return False + return None if not opt: opt = "1" opt = opt.strip() if opt in ("x", "X"): - return False + return None if opt in num_mechanisms: mechanism = num_mechanisms[opt] break - await ledger.accept_txn_author_agreement(taa_info["taa_record"], mechanism) + return mechanism + +async def accept_taa( + ledger: BaseLedger, + profile: Profile, + taa_info, + provision: bool = False, +) -> bool: + """Perform TAA acceptance.""" + + mechanisms = taa_info["aml_record"]["aml"] + mechanism = None + + taa_acceptance_mechanism = profile.settings.get("ledger.taa_acceptance_mechanism") + taa_acceptance_version = profile.settings.get("ledger.taa_acceptance_version") + + # If configured, accept the TAA automatically + if taa_acceptance_mechanism: + taa_record_version = taa_info["taa_record"]["version"] + if taa_acceptance_version != taa_record_version: + raise LedgerError( + f"TAA version ({taa_record_version}) is different from TAA accept " + f"version ({taa_acceptance_version}) from configuration. Update the " + "TAA version in the config to accept the TAA." + ) + + if taa_acceptance_mechanism not in mechanisms: + raise LedgerError( + f"TAA acceptance mechanism '{taa_acceptance_mechanism}' is not a " + "valid acceptance mechanism. Valid mechanisms are: " + + str(list(mechanisms.keys())) + ) + + mechanism = taa_acceptance_mechanism + # If tty is available use it (allows to accept newer TAA than configured) + elif sys.stdout.isatty(): + mechanism = await select_aml_tty(taa_info, provision) + else: + LOGGER.warning( + "Cannot accept TAA without interactive terminal or taa accept config" + ) + + if not mechanism: + return False + + LOGGER.debug(f"Accepting the TAA using mechanism '{mechanism}'") + await ledger.accept_txn_author_agreement(taa_info["taa_record"], mechanism) return True diff --git a/aries_cloudagent/config/logging.py b/aries_cloudagent/config/logging.py index 2ab1d52206..bbd91ef9d2 100644 --- a/aries_cloudagent/config/logging.py +++ b/aries_cloudagent/config/logging.py @@ -1,14 +1,27 @@ """Utilities related to logging.""" - +import asyncio import logging +import os +import pkg_resources +import sys +from random import randint +import re +import time as mod_time + +from datetime import datetime, timedelta from io import TextIOWrapper +from logging.handlers import BaseRotatingHandler from logging.config import fileConfig -from typing import TextIO - -import pkg_resources +from portalocker import lock, unlock, LOCK_EX +from pythonjsonlogger import jsonlogger +from typing import Optional, TextIO +from ..core.profile import Profile from ..version import __version__ +from ..wallet.base import BaseWallet, DIDInfo + from .banner import Banner +from .base import BaseSettings DEFAULT_LOGGING_CONFIG_PATH = "aries_cloudagent.config:default_logging_config.ini" @@ -83,7 +96,6 @@ def print_banner( agent_label, inbound_transports, outbound_transports, - outbound_queue, public_did, admin_server=None, banner_length=40, @@ -96,7 +108,6 @@ def print_banner( agent_label: Agent Label inbound_transports: Configured inbound transports outbound_transports: Configured outbound transports - outbound_queue: The outbound queue engine instance admin_server: Admin server info public_did: Public DID banner_length: (Default value = 40) Length of the banner @@ -115,34 +126,53 @@ def print_banner( # Inbound transports banner.print_subtitle("Inbound Transports") - banner.print_spacer() - banner.print_list( - [ - f"{transport.scheme}://{transport.host}:{transport.port}" - for transport in inbound_transports.values() - ] - ) - banner.print_spacer() + internal_in_transports = [ + f"{transport.scheme}://{transport.host}:{transport.port}" + for transport in inbound_transports.values() + if not transport.is_external + ] + if internal_in_transports: + banner.print_spacer() + banner.print_list(internal_in_transports) + banner.print_spacer() + external_in_transports = [ + f"{transport.scheme}://{transport.host}:{transport.port}" + for transport in inbound_transports.values() + if transport.is_external + ] + if external_in_transports: + banner.print_spacer() + banner.print_subtitle(" External Plugin") + banner.print_spacer() + banner.print_list(external_in_transports) + banner.print_spacer() # Outbound transports - schemes = set().union( - *(transport.schemes for transport in outbound_transports.values()) + banner.print_subtitle("Outbound Transports") + internal_schemes = set().union( + *( + transport.schemes + for transport in outbound_transports.values() + if not transport.is_external + ) ) - if schemes: - banner.print_subtitle("Outbound Transports") + if internal_schemes: banner.print_spacer() - banner.print_list([f"{scheme}" for scheme in sorted(schemes)]) + banner.print_list([f"{scheme}" for scheme in sorted(internal_schemes)]) banner.print_spacer() - # Outbound queue - if outbound_queue: - banner.print_subtitle("Outbound Queue") - banner.print_spacer() - banner.print_list( - [ - f"{outbound_queue}", - ] + external_schemes = set().union( + *( + transport.schemes + for transport in outbound_transports.values() + if transport.is_external ) + ) + if external_schemes: + banner.print_spacer() + banner.print_subtitle(" External Plugin") + banner.print_spacer() + banner.print_list([f"{scheme}" for scheme in sorted(external_schemes)]) banner.print_spacer() # DID info @@ -168,3 +198,447 @@ def print_banner( print() print("Listening...") print() + + +###################################################################### +# Derived from +# https://github.com/python/cpython/blob/main/Lib/logging/handlers.py +# and https://github.com/yorks/mpfhandler/blob/master/src/mpfhandler.py +# +# interval and backupCount are not working as intended in mpfhandler +# library. Also the old backup files were not being deleted on rotation. +# This required the following custom implementation. +###################################################################### +class TimedRotatingFileMultiProcessHandler(BaseRotatingHandler): + """ + Handler for logging to a file. + + Rotating the log file at certain timed with file lock unlock + mechanism to support multi-process writing to log file. + """ + + def __init__( + self, + filename, + when="h", + interval=1, + backupCount=1, + encoding=None, + delay=False, + utc=False, + atTime=None, + ): + """ + Initialize an instance of `TimedRotatingFileMultiProcessHandler`. + + Args: + filename: log file name with path + when: specify when to rotate log file + interval: interval when to rotate + backupCount: count of backup file, backupCount of 0 will mean + no limit on count of backup file [no backup will be deleted] + + """ + BaseRotatingHandler.__init__( + self, + filename, + "a", + encoding=encoding, + delay=delay, + ) + self.when = when.upper() + self.backupCount = backupCount + self.utc = utc + self.atTime = atTime + self.mylogfile = "%s.%08d" % ("/tmp/trfmphanldler", randint(0, 99999999)) + self.interval = interval + + if self.when == "S": + self.interval = 1 + self.suffix = "%Y-%m-%d_%H-%M-%S" + self.extMatch = r"^\d{4}-\d{2}-\d{2}_\d{2}-\d{2}-\d{2}(\.\w+)?$" + elif self.when == "M": + self.interval = 60 + self.suffix = "%Y-%m-%d_%H-%M" + self.extMatch = r"^\d{4}-\d{2}-\d{2}_\d{2}-\d{2}(\.\w+)?$" + elif self.when == "H": + self.interval = 60 * 60 + self.suffix = "%Y-%m-%d_%H" + self.extMatch = r"^\d{4}-\d{2}-\d{2}_\d{2}(\.\w+)?$" + elif self.when == "D" or self.when == "MIDNIGHT": + self.interval = 60 * 60 * 24 + self.suffix = "%Y-%m-%d" + self.extMatch = r"^\d{4}-\d{2}-\d{2}(\.\w+)?$" + elif self.when.startswith("W"): + self.interval = 60 * 60 * 24 * 7 + if len(self.when) != 2: + raise ValueError( + "You must specify a day for weekly rollover from 0 " + "to 6 (0 is Monday): %s" % self.when + ) + if self.when[1] < "0" or self.when[1] > "6": + raise ValueError( + "Invalid day specified for weekly rollover: %s" % self.when + ) + self.dayOfWeek = int(self.when[1]) + self.suffix = "%Y-%m-%d" + self.extMatch = r"^\d{4}-\d{2}-\d{2}(\.\w+)?$" + else: + raise ValueError("Invalid rollover interval specified: %s" % self.when) + + self.extMatch = re.compile(self.extMatch, re.ASCII) + self.interval = self.interval * interval + self.stream_lock = None + self.lock_file = self._getLockFile() + self.next_rollover_time = self.get_next_rollover_time() + if not self.next_rollover_time: + self.next_rollover_time = self.compute_next_rollover_time() + self.save_next_rollover_time() + + def _log2mylog(self, msg): + """Write to external log file.""" + time_str = mod_time.strftime( + "%Y-%m-%d %H:%M:%S", mod_time.localtime(mod_time.time()) + ) + msg = str(msg) + content = "%s [%s]\n" % (time_str, msg) + fa = open(self.mylogfile, "a") + fa.write(content) + fa.close() + + def _getLockFile(self): + """Return log lock file.""" + if self.baseFilename.endswith(".log"): + lock_file = self.baseFilename[:-4] + else: + lock_file = self.baseFilename + lock_file += ".lock" + return lock_file + + def _openLockFile(self): + """Open log lock file.""" + lock_file = self._getLockFile() + self.stream_lock = open(lock_file, "w") + + def compute_next_rollover_time(self): + """Return next rollover time.""" + next_time = None + current_datetime = datetime.now() + if self.when == "D": + next_datetime = current_datetime + timedelta(days=self.interval) + next_date = next_datetime.date() + next_time = int(mod_time.mktime(next_date.timetuple())) + elif self.when.startswith("W"): + days = 0 + current_weekday = current_datetime.weekday() + if current_weekday == self.dayOfWeek: + days = self.interval + 7 + elif current_weekday < self.dayOfWeek: + days = self.dayOfWeek - current_weekday + else: + days = 6 - current_weekday + self.dayOfWeek + 1 + next_datetime = current_datetime + timedelta(days=days) + next_date = next_datetime.date() + next_time = int(mod_time.mktime(next_date.timetuple())) + else: + tmp_next_datetime = current_datetime + timedelta(seconds=self.interval) + next_datetime = tmp_next_datetime.replace(microsecond=0) + if self.when == "H": + next_datetime = tmp_next_datetime.replace( + minute=0, second=0, microsecond=0 + ) + elif self.when == "M": + next_datetime = tmp_next_datetime.replace(second=0, microsecond=0) + next_time = int(mod_time.mktime(next_datetime.timetuple())) + return next_time + + def get_next_rollover_time(self): + """Get next rollover time stamp from lock file.""" + try: + fp = open(self.lock_file, "r") + c = fp.read() + fp.close() + return int(c) + except Exception: + return False + + def save_next_rollover_time(self): + """Save the nextRolloverTimestamp to lock file.""" + if not self.next_rollover_time: + return 0 + content = "%d" % self.next_rollover_time + if not self.stream_lock: + self._openLockFile() + lock(self.stream_lock, LOCK_EX) + try: + self.stream_lock.seek(0) + self.stream_lock.write(content) + self.stream_lock.flush() + except Exception: + pass + finally: + unlock(self.stream_lock) + + def acquire(self): + """Acquire thread and file locks.""" + BaseRotatingHandler.acquire(self) + if self.stream_lock: + if self.stream_lock.closed: + try: + self._openLockFile() + except Exception: + self.stream_lock = None + return + lock(self.stream_lock, LOCK_EX) + + def release(self): + """Release file and thread locks.""" + try: + if self.stream_lock and not self.stream_lock.closed: + unlock(self.stream_lock) + except Exception: + pass + finally: + BaseRotatingHandler.release(self) + + def _close_stream(self): + """Close the log file stream.""" + if self.stream: + try: + if not self.stream.closed: + self.stream.flush() + self.stream.close() + finally: + self.stream = None + + def _close_stream_lock(self): + """Close the lock file stream.""" + if self.stream_lock: + try: + if not self.stream_lock.closed: + self.stream_lock.flush() + self.stream_lock.close() + finally: + self.stream_lock = None + + def close(self): + """Close log stream and stream_lock.""" + try: + self._close_stream() + self._close_stream_lock() + finally: + self.stream = None + self.stream_lock = None + + def get_log_files_to_delete(self): + """Delete backup files on rotation based on backupCount.""" + dir_name, base_name = os.path.split(self.baseFilename) + file_names = os.listdir(dir_name) + result = [] + n, e = os.path.splitext(base_name) + prefix = n + "." + plen = len(prefix) + for file_name in file_names: + if self.namer is None: + if not file_name.startswith(base_name): + continue + else: + if ( + not file_name.startswith(base_name) + and file_name.endswith(e) + and len(file_name) > (plen + 1) + and not file_name[plen + 1].isdigit() + ): + continue + if file_name[:plen] == prefix: + suffix = file_name[plen:] + parts = suffix.split(".") + for part in parts: + if self.extMatch.match(part): + result.append(os.path.join(dir_name, file_name)) + break + if len(result) < self.backupCount: + result = [] + else: + result.sort() + result = result[: len(result) - self.backupCount] + return result + + def shouldRollover(self, record): + """Determine if rollover should occur.""" + t = int(mod_time.time()) + if t >= self.next_rollover_time: + return 1 + return 0 + + def doRollover(self): + """Perform rollover.""" + self._close_stream() + self.acquire() + try: + file_next_rollover_time = self.get_next_rollover_time() + if not file_next_rollover_time: + self.release() + return 0 + if self.next_rollover_time < file_next_rollover_time: + self.next_rollover_time = file_next_rollover_time + self.release() + return 0 + except Exception: + pass + time_tuple = mod_time.localtime(self.next_rollover_time - 1) + dfn = self.baseFilename + "." + mod_time.strftime(self.suffix, time_tuple) + if os.path.exists(dfn): + bakname = dfn + ".bak" + while os.path.exists(bakname): + bakname = "%s.%08d" % (bakname, randint(0, 99999999)) + try: + os.rename(dfn, bakname) + except Exception: + pass + if os.path.exists(self.baseFilename): + try: + os.rename(self.baseFilename, dfn) + except Exception: + pass + self.next_rollover_time = self.compute_next_rollover_time() + self.save_next_rollover_time() + if self.backupCount > 0: + for s in self.get_log_files_to_delete(): + os.remove(s) + if not self.delay: + self.stream = self._open() + self.release() + + +LOG_FORMAT_FILE_ALIAS_PATTERN = ( + "%(asctime)s [%(did)s] %(levelname)s %(filename)s %(lineno)d %(message)s" +) + +LOG_FORMAT_FILE_NO_ALIAS_PATTERN = ( + "%(asctime)s %(levelname)s %(filename)s %(lineno)d %(message)s" +) + +LOG_FORMAT_STREAM_PATTERN = ( + "%(asctime)s %(levelname)s %(filename)s %(lineno)d %(message)s" +) + + +def clear_prev_handlers(logger: logging.Logger) -> logging.Logger: + """Remove all handler classes associated with logger instance.""" + iter_count = 0 + num_handlers = len(logger.handlers) + while iter_count < num_handlers: + logger.removeHandler(logger.handlers[0]) + iter_count = iter_count + 1 + return logger + + +def get_logger_inst(profile: Profile, logger_name) -> logging.Logger: + """Return a logger instance with provided name and handlers.""" + logger = None + loop = asyncio.get_event_loop() + did_ident = loop.run_until_complete(get_did_ident(profile)) + if did_ident: + logger = get_logger_with_handlers( + settings=profile.settings, + logger=logging.getLogger(f"{logger_name}_{did_ident}"), + did_ident=did_ident, + interval=profile.settings.get("log.handler_interval") or 7, + backup_count=profile.settings.get("log.handler_bakcount") or 1, + at_when=profile.settings.get("log.handler_when") or "d", + ) + else: + logger = get_logger_with_handlers( + settings=profile.settings, + logger=logging.getLogger(logger_name), + interval=profile.settings.get("log.handler_interval") or 7, + backup_count=profile.settings.get("log.handler_bakcount") or 1, + at_when=profile.settings.get("log.handler_when") or "d", + ) + return logger + + +async def get_did_ident(profile: Profile) -> Optional[str]: + """Get public did identifier for logging, if applicable.""" + did_ident = None + if profile.settings.get("log.file"): + async with profile.session() as session: + wallet = session.inject(BaseWallet) + req_did_info: DIDInfo = await wallet.get_public_did() + if not req_did_info: + req_did_info: DIDInfo = (await wallet.get_local_dids())[0] + if req_did_info: + did_ident = req_did_info.did + return did_ident + else: + return did_ident + + +def get_logger_with_handlers( + settings: BaseSettings, + logger: logging.Logger, + at_when: str = None, + interval: int = None, + backup_count: int = None, + did_ident: str = None, +) -> logging.Logger: + """Return logger instance with necessary handlers if required.""" + if settings.get("log.file"): + # Clear handlers set previously for this logger instance + logger = clear_prev_handlers(logger) + # log file handler + file_path = settings.get("log.file") + file_handler = TimedRotatingFileMultiProcessHandler( + filename=file_path, + interval=interval, + when=at_when, + backupCount=backup_count, + ) + if did_ident: + if settings.get("log.json_fmt"): + file_handler.setFormatter( + jsonlogger.JsonFormatter( + settings.get("log.fmt_pattern") or LOG_FORMAT_FILE_ALIAS_PATTERN + ) + ) + else: + file_handler.setFormatter( + logging.Formatter( + settings.get("log.fmt_pattern") or LOG_FORMAT_FILE_ALIAS_PATTERN + ) + ) + else: + if settings.get("log.json_fmt"): + file_handler.setFormatter( + jsonlogger.JsonFormatter( + settings.get("log.fmt_pattern") + or LOG_FORMAT_FILE_NO_ALIAS_PATTERN + ) + ) + else: + file_handler.setFormatter( + logging.Formatter( + settings.get("log.fmt_pattern") + or LOG_FORMAT_FILE_NO_ALIAS_PATTERN + ) + ) + logger.addHandler(file_handler) + # stream console handler + std_out_handler = logging.StreamHandler(sys.stdout) + std_out_handler.setFormatter( + logging.Formatter( + settings.get("log.fmt_pattern") or LOG_FORMAT_STREAM_PATTERN + ) + ) + logger.addHandler(std_out_handler) + if did_ident: + logger = logging.LoggerAdapter(logger, {"did": did_ident}) + # set log level + logger_level = ( + (settings.get("log.level")).upper() + if settings.get("log.level") + else logging.INFO + ) + logger.setLevel(logger_level) + return logger diff --git a/aries_cloudagent/config/plugin_settings.py b/aries_cloudagent/config/plugin_settings.py new file mode 100644 index 0000000000..1da1392ef0 --- /dev/null +++ b/aries_cloudagent/config/plugin_settings.py @@ -0,0 +1,88 @@ +"""Settings implementation for plugins.""" + +from typing import Any, Mapping, Optional + +from .base import BaseSettings + + +PLUGIN_CONFIG_KEY = "plugin_config" + + +class PluginSettings(BaseSettings): + """Retrieve immutable settings for plugins. + + Plugin settings should be retrieved by calling: + + PluginSettings.for_plugin(settings, "my_plugin", {"default": "values"}) + + This will extract the PLUGIN_CONFIG_KEY in "settings" and return a new + PluginSettings instance. + """ + + def __init__(self, values: Optional[Mapping[str, Any]] = None): + """Initialize a Settings object. + + Args: + values: An optional dictionary of settings + """ + self._values = {} + if values: + self._values.update(values) + + def __contains__(self, index): + """Define 'in' operator.""" + return index in self._values + + def __iter__(self): + """Iterate settings keys.""" + return iter(self._values) + + def __len__(self): + """Fetch the length of the mapping.""" + return len(self._values) + + def __bool__(self): + """Convert settings to a boolean.""" + return True + + def copy(self) -> BaseSettings: + """Produce a copy of the settings instance.""" + return PluginSettings(self._values) + + def extend(self, other: Mapping[str, Any]) -> BaseSettings: + """Merge another settings instance to produce a new instance.""" + vals = self._values.copy() + vals.update(other) + return PluginSettings(vals) + + def to_dict(self) -> dict: + """Return a dict of the settings instance.""" + setting_dict = {} + for k in self: + setting_dict[k] = self[k] + return setting_dict + + def get_value(self, *var_names: str, default: Any = None): + """Fetch a setting. + + Args: + var_names: A list of variable name alternatives + default: The default value to return if none are defined + """ + for k in var_names: + if k in self._values: + return self._values[k] + return default + + @classmethod + def for_plugin( + cls, + settings: BaseSettings, + plugin: str, + default: Optional[Mapping[str, Any]] = None, + ) -> "PluginSettings": + """Construct a PluginSettings object from another settings object. + + PLUGIN_CONFIG_KEY is read from settings. + """ + return cls(settings.get(PLUGIN_CONFIG_KEY, {}).get(plugin, default)) diff --git a/aries_cloudagent/config/settings.py b/aries_cloudagent/config/settings.py index cc20aeb6f1..386e434a49 100644 --- a/aries_cloudagent/config/settings.py +++ b/aries_cloudagent/config/settings.py @@ -1,14 +1,15 @@ """Settings implementation.""" -from typing import Mapping +from typing import Any, Mapping, MutableMapping, Optional from .base import BaseSettings +from .plugin_settings import PluginSettings -class Settings(BaseSettings): +class Settings(BaseSettings, MutableMapping[str, Any]): """Mutable settings implementation.""" - def __init__(self, values: Mapping[str, object] = None): + def __init__(self, values: Optional[Mapping[str, Any]] = None): """Initialize a Settings object. Args: @@ -90,12 +91,23 @@ def copy(self) -> BaseSettings: """Produce a copy of the settings instance.""" return Settings(self._values) - def extend(self, other: Mapping[str, object]) -> BaseSettings: + def extend(self, other: Mapping[str, Any]) -> BaseSettings: """Merge another settings instance to produce a new instance.""" vals = self._values.copy() vals.update(other) return Settings(vals) - def update(self, other: Mapping[str, object]): + def to_dict(self) -> dict: + """Return a dict of the settings instance.""" + setting_dict = {} + for k in self: + setting_dict[k] = self[k] + return setting_dict + + def update(self, other: Mapping[str, Any]): """Update the settings in place.""" self._values.update(other) + + def for_plugin(self, plugin: str, default: Optional[Mapping[str, Any]] = None): + """Retrieve settings for plugin.""" + return PluginSettings.for_plugin(self, plugin, default) diff --git a/aries_cloudagent/config/tests/test_argparse.py b/aries_cloudagent/config/tests/test_argparse.py index 4e76a681d2..a439fd0220 100644 --- a/aries_cloudagent/config/tests/test_argparse.py +++ b/aries_cloudagent/config/tests/test_argparse.py @@ -111,6 +111,7 @@ async def test_upgrade_config(self): "./aries_cloudagent/config/tests/test-acapy-upgrade-config.yml", "--from-version", "v0.7.2", + "--force-upgrade", ] ) @@ -118,6 +119,7 @@ async def test_upgrade_config(self): result.upgrade_config_path == "./aries_cloudagent/config/tests/test-acapy-upgrade-config.yml" ) + assert result.force_upgrade is True settings = group.get_settings(result) @@ -144,27 +146,6 @@ async def test_outbound_is_required(self): with self.assertRaises(argparse.ArgsParseError): settings = group.get_settings(result) - async def test_outbound_queue(self): - """Test outbound queue class path string.""" - parser = argparse.create_argument_parser() - group = argparse.TransportGroup() - group.add_arguments(parser) - - result = parser.parse_args( - [ - "--inbound-transport", - "http", - "0.0.0.0", - "80", - "--outbound-queue", - "my_queue.mod.path", - ] - ) - - settings = group.get_settings(result) - - assert settings.get("transport.outbound_queue") == "my_queue.mod.path" - async def test_general_settings_file(self): """Test file argument parsing.""" @@ -252,7 +233,31 @@ async def test_multitenancy_settings(self): "--jwt-secret", "secret", "--multitenancy-config", - '{"wallet_type":"askar","wallet_name":"test"}', + '{"wallet_type":"askar","wallet_name":"test", "cache_size": 10}', + "--base-wallet-routes", + "/my_route", + ] + ) + + settings = group.get_settings(result) + + assert settings.get("multitenant.enabled") == True + assert settings.get("multitenant.jwt_secret") == "secret" + assert settings.get("multitenant.wallet_type") == "askar" + assert settings.get("multitenant.wallet_name") == "test" + assert settings.get("multitenant.base_wallet_routes") == ["/my_route"] + + result = parser.parse_args( + [ + "--multitenant", + "--jwt-secret", + "secret", + "--multitenancy-config", + "wallet_type=askar", + "wallet_name=test", + "cache_size=10", + "--base-wallet-routes", + "/my_route", ] ) @@ -262,6 +267,7 @@ async def test_multitenancy_settings(self): assert settings.get("multitenant.jwt_secret") == "secret" assert settings.get("multitenant.wallet_type") == "askar" assert settings.get("multitenant.wallet_name") == "test" + assert settings.get("multitenant.base_wallet_routes") == ["/my_route"] async def test_endorser_settings(self): """Test required argument parsing.""" @@ -286,6 +292,60 @@ async def test_endorser_settings(self): assert settings.get("endorser.endorser_public_did") == "did:sov:12345" assert settings.get("endorser.auto_endorse") == False + async def test_logging(self): + """Test logging.""" + + parser = argparse.create_argument_parser() + group = argparse.LoggingGroup() + group.add_arguments(parser) + + result = parser.parse_args( + [ + "--log-file", + "test_file.log", + "--log-level", + "INFO", + "--log-handler-config", + "d;7;1", + "--log-fmt-pattern", + "%(asctime)s %(levelname)s %(filename)s %(lineno)d %(message)s", + ] + ) + + settings = group.get_settings(result) + + assert settings.get("log.file") == "test_file.log" + assert settings.get("log.level") == "INFO" + assert settings.get("log.handler_when") == "d" + assert settings.get("log.handler_interval") == 7 + assert settings.get("log.handler_bakcount") == 1 + assert ( + settings.get("log.fmt_pattern") + == "%(asctime)s %(levelname)s %(filename)s %(lineno)d %(message)s" + ) + assert not settings.get("log.json_fmt") + + result = parser.parse_args( + [ + "--log-file", + "test_file.log", + "--log-level", + "INFO", + "--log-handler-config", + "d;7;1", + "--log-json-fmt", + ] + ) + + settings = group.get_settings(result) + + assert settings.get("log.file") == "test_file.log" + assert settings.get("log.level") == "INFO" + assert settings.get("log.handler_when") == "d" + assert settings.get("log.handler_interval") == 7 + assert settings.get("log.handler_bakcount") == 1 + assert settings.get("log.json_fmt") + async def test_error_raised_when_multitenancy_used_and_no_jwt_provided(self): """Test that error is raised if no jwt_secret is provided with multitenancy.""" @@ -465,3 +525,47 @@ async def test_discover_features_args(self): assert (["test_goal_code_1", "test_goal_code_2"]) == settings.get( "disclose_goal_code_list" ) + + def test_universal_resolver(self): + """Test universal resolver flags.""" + parser = argparse.create_argument_parser() + group = argparse.GeneralGroup() + group.add_arguments(parser) + + result = parser.parse_args(["-e", "test", "--universal-resolver"]) + settings = group.get_settings(result) + endpoint = settings.get("resolver.universal") + assert endpoint + assert endpoint == "DEFAULT" + + result = parser.parse_args( + ["-e", "test", "--universal-resolver", "https://example.com"] + ) + settings = group.get_settings(result) + endpoint = settings.get("resolver.universal") + assert endpoint + assert endpoint == "https://example.com" + + result = parser.parse_args( + [ + "-e", + "test", + "--universal-resolver", + "https://example.com", + "--universal-resolver-regex", + "regex", + ] + ) + settings = group.get_settings(result) + endpoint = settings.get("resolver.universal") + assert endpoint + assert endpoint == "https://example.com" + supported_regex = settings.get("resolver.universal.supported") + assert supported_regex + assert supported_regex == ["regex"] + + result = parser.parse_args( + ["-e", "test", "--universal-resolver-regex", "regex"] + ) + with self.assertRaises(argparse.ArgsParseError): + group.get_settings(result) diff --git a/aries_cloudagent/config/tests/test_ledger.py b/aries_cloudagent/config/tests/test_ledger.py index bb6d2ecaa6..f9c4175f03 100644 --- a/aries_cloudagent/config/tests/test_ledger.py +++ b/aries_cloudagent/config/tests/test_ledger.py @@ -1,6 +1,6 @@ from os import remove from tempfile import NamedTemporaryFile - +import pytest from asynctest import TestCase as AsyncTestCase, mock as async_mock from .. import argparse @@ -643,14 +643,23 @@ async def test_load_multiple_genesis_transactions_from_config_io_x(self): ) @async_mock.patch("sys.stdout") - async def test_ledger_accept_taa_not_tty(self, mock_stdout): + async def test_ledger_accept_taa_not_tty_not_accept_config(self, mock_stdout): mock_stdout.isatty = async_mock.MagicMock(return_value=False) + mock_profile = InMemoryProfile.test_profile() + + taa_info = { + "taa_record": {"version": "1.0", "text": "Agreement"}, + "aml_record": {"aml": ["wallet_agreement", "on_file"]}, + } - assert not await test_module.accept_taa(None, None, provision=False) + assert not await test_module.accept_taa( + None, mock_profile, taa_info, provision=False + ) @async_mock.patch("sys.stdout") - async def test_ledger_accept_taa(self, mock_stdout): + async def test_ledger_accept_taa_tty(self, mock_stdout): mock_stdout.isatty = async_mock.MagicMock(return_value=True) + mock_profile = InMemoryProfile.test_profile() taa_info = { "taa_record": {"version": "1.0", "text": "Agreement"}, @@ -663,7 +672,9 @@ async def test_ledger_accept_taa(self, mock_stdout): test_module.prompt_toolkit, "prompt", async_mock.CoroutineMock() ) as mock_prompt: mock_prompt.side_effect = EOFError() - assert not await test_module.accept_taa(None, taa_info, provision=False) + assert not await test_module.accept_taa( + None, mock_profile, taa_info, provision=False + ) with async_mock.patch.object( test_module, "use_asyncio_event_loop", async_mock.MagicMock() @@ -671,7 +682,9 @@ async def test_ledger_accept_taa(self, mock_stdout): test_module.prompt_toolkit, "prompt", async_mock.CoroutineMock() ) as mock_prompt: mock_prompt.return_value = "x" - assert not await test_module.accept_taa(None, taa_info, provision=False) + assert not await test_module.accept_taa( + None, mock_profile, taa_info, provision=False + ) with async_mock.patch.object( test_module, "use_asyncio_event_loop", async_mock.MagicMock() @@ -682,7 +695,53 @@ async def test_ledger_accept_taa(self, mock_stdout): accept_txn_author_agreement=async_mock.CoroutineMock() ) mock_prompt.return_value = "" - assert await test_module.accept_taa(mock_ledger, taa_info, provision=False) + assert await test_module.accept_taa( + mock_ledger, mock_profile, taa_info, provision=False + ) + + async def test_ledger_accept_taa_tty(self): + taa_info = { + "taa_record": {"version": "1.0", "text": "Agreement"}, + "aml_record": {"aml": {"wallet_agreement": "", "on_file": ""}}, + } + + # Incorrect version + with pytest.raises(LedgerError): + mock_profile = InMemoryProfile.test_profile( + { + "ledger.taa_acceptance_mechanism": "wallet_agreement", + "ledger.taa_acceptance_version": "1.5", + } + ) + assert not await test_module.accept_taa( + None, mock_profile, taa_info, provision=False + ) + + # Incorrect mechanism + with pytest.raises(LedgerError): + mock_profile = InMemoryProfile.test_profile( + { + "ledger.taa_acceptance_mechanism": "not_exist", + "ledger.taa_acceptance_version": "1.0", + } + ) + assert not await test_module.accept_taa( + None, mock_profile, taa_info, provision=False + ) + + # Valid + mock_profile = InMemoryProfile.test_profile( + { + "ledger.taa_acceptance_mechanism": "on_file", + "ledger.taa_acceptance_version": "1.0", + } + ) + mock_ledger = async_mock.MagicMock( + accept_txn_author_agreement=async_mock.CoroutineMock() + ) + assert await test_module.accept_taa( + mock_ledger, mock_profile, taa_info, provision=False + ) async def test_ledger_config(self): """Test required argument parsing.""" diff --git a/aries_cloudagent/config/tests/test_logging.py b/aries_cloudagent/config/tests/test_logging.py index bd74b8471e..853b723692 100644 --- a/aries_cloudagent/config/tests/test_logging.py +++ b/aries_cloudagent/config/tests/test_logging.py @@ -1,14 +1,20 @@ import contextlib +import logging from io import StringIO -from asynctest import mock as async_mock +from asynctest import mock as async_mock, TestCase as AsyncTestCase from tempfile import NamedTemporaryFile from .. import logging as test_module +from ...core.in_memory import InMemoryProfile +from ...wallet.base import BaseWallet +from ...wallet.did_method import SOV, DIDMethods +from ...wallet.key_type import ED25519 -class TestLoggingConfigurator: + +class TestLoggingConfigurator(AsyncTestCase): agent_label_arg_value = "Aries Cloud Agent" transport_arg_value = "transport" host_arg_value = "host" @@ -66,38 +72,15 @@ def test_banner_did(self): test_label, {"in": mock_http}, {"out": mock_https}, - None, test_did, mock_admin_server, ) test_module.LoggingConfigurator.print_banner( - test_label, {"in": mock_http}, {"out": mock_https}, None, test_did + test_label, {"in": mock_http}, {"out": mock_https}, test_did ) output = stdout.getvalue() assert test_did in output - def test_banner_outbound_queue(self): - stdout = StringIO() - mock_http = async_mock.MagicMock(scheme="http", host="1.2.3.4", port=8081) - mock_queue = "mocked queue text" - mock_admin_server = async_mock.MagicMock(host="1.2.3.4", port=8091) - with contextlib.redirect_stdout(stdout): - test_label = "Aries Cloud Agent" - test_did = "55GkHamhTU1ZbTbV2ab9DE" - test_module.LoggingConfigurator.print_banner( - test_label, - {"in": mock_http}, - {}, - mock_queue, - test_did, - mock_admin_server, - ) - test_module.LoggingConfigurator.print_banner( - test_label, {"in": mock_http}, {}, mock_queue, test_did - ) - output = stdout.getvalue() - assert "mocked queue text" in output - def test_load_resource(self): with async_mock.patch("builtins.open", async_mock.MagicMock()) as mock_open: test_module.load_resource("abc", encoding="utf-8") @@ -115,3 +98,85 @@ def test_load_resource(self): test_module.pkg_resources, "resource_stream", async_mock.MagicMock() ) as mock_res_stream: test_module.load_resource("abc:def", encoding=None) + + def test_get_logger_with_handlers(self): + profile = InMemoryProfile.test_profile() + profile.settings["log.file"] = "test_file.log" + logger = logging.getLogger(__name__) + logger = test_module.get_logger_with_handlers( + settings=profile.settings, + logger=logger, + at_when="m", + interval=1, + backup_count=1, + ) + assert logger + logger = test_module.get_logger_with_handlers( + settings=profile.settings, + logger=logger, + did_ident="tenant_did_123", + at_when="m", + interval=1, + backup_count=1, + ) + assert logger + + async def test_get_logger_inst(self): + profile = InMemoryProfile.test_profile() + logger = test_module.get_logger_inst( + profile=profile, + logger_name=__name__, + ) + assert logger + # public did + profile.settings["log.file"] = "test_file.log" + profile.context.injector.bind_instance(DIDMethods, DIDMethods()) + async with profile.session() as session: + wallet: BaseWallet = session.inject_or(BaseWallet) + await wallet.create_local_did( + SOV, + ED25519, + did="DJGEjaMunDtFtBVrn1qJMT", + ) + await wallet.set_public_did("DJGEjaMunDtFtBVrn1qJMT") + logger = test_module.get_logger_inst( + profile=profile, + logger_name=__name__, + ) + # public did, json_fmt, pattern + profile.settings["log.file"] = "test_file.log" + profile.settings["log.json_fmt"] = True + profile.settings[ + "log.fmt_pattern" + ] = "%(asctime)s [%(did)s] %(lineno)d %(message)s" + logger = test_module.get_logger_inst( + profile=profile, + logger_name=__name__, + ) + assert logger + # not public did + profile = InMemoryProfile.test_profile() + profile.settings["log.file"] = "test_file.log" + profile.settings["log.json_fmt"] = False + profile.context.injector.bind_instance(DIDMethods, DIDMethods()) + async with profile.session() as session: + wallet: BaseWallet = session.inject_or(BaseWallet) + await wallet.create_local_did( + SOV, + ED25519, + did="DJGEjaMunDtFtBVrn1qJMT", + ) + logger = test_module.get_logger_inst( + profile=profile, + logger_name=__name__, + ) + assert logger + # not public did, json_fmt, pattern + profile.settings["log.file"] = "test_file.log" + profile.settings["log.json_fmt"] = True + profile.settings["log.fmt_pattern"] = "%(asctime)s %(lineno)d %(message)s" + logger = test_module.get_logger_inst( + profile=profile, + logger_name=__name__, + ) + assert logger diff --git a/aries_cloudagent/config/tests/test_settings.py b/aries_cloudagent/config/tests/test_settings.py index 583d8a42ef..3415d4c260 100644 --- a/aries_cloudagent/config/tests/test_settings.py +++ b/aries_cloudagent/config/tests/test_settings.py @@ -2,8 +2,11 @@ from unittest import TestCase +from aries_cloudagent.config.plugin_settings import PluginSettings + from ..base import SettingsError from ..settings import Settings +from ..plugin_settings import PLUGIN_CONFIG_KEY class TestSettings(TestCase): @@ -59,3 +62,25 @@ def test_set_default(self): assert self.test_instance[self.test_key] == self.test_value self.test_instance.set_default("BOOL", "True") assert self.test_instance["BOOL"] == "True" + + def test_plugin_setting_retrieval(self): + plugin_setting_values = { + "value0": 0, + "value1": 1, + "value2": 2, + "value3": 3, + "value4": 4, + } + self.test_instance[PLUGIN_CONFIG_KEY] = {"my_plugin": plugin_setting_values} + + plugin_settings = self.test_instance.for_plugin("my_plugin") + assert isinstance(plugin_settings, PluginSettings) + assert plugin_settings._values == plugin_setting_values + for key in plugin_setting_values: + assert key in plugin_settings + assert plugin_settings[key] == plugin_setting_values[key] + assert plugin_settings.get_value(key) == plugin_setting_values[key] + with self.assertRaises(KeyError): + plugin_settings["MISSING"] + assert len(plugin_settings) == 5 + assert len(plugin_settings) == 5 diff --git a/aries_cloudagent/config/tests/test_wallet.py b/aries_cloudagent/config/tests/test_wallet.py index 9d514bc735..3ff2e5a8b8 100644 --- a/aries_cloudagent/config/tests/test_wallet.py +++ b/aries_cloudagent/config/tests/test_wallet.py @@ -187,7 +187,6 @@ async def test_wallet_config_bad_seed_x(self): ) as mock_seed_to_did, async_mock.patch.object( test_module, "add_or_update_version_to_storage", async_mock.CoroutineMock() ): - with self.assertRaises(test_module.ConfigError): await test_module.wallet_config(self.context, provision=True) diff --git a/aries_cloudagent/config/wallet.py b/aries_cloudagent/config/wallet.py index 9ade368c52..a49aae304f 100644 --- a/aries_cloudagent/config/wallet.py +++ b/aries_cloudagent/config/wallet.py @@ -7,13 +7,12 @@ from ..core.profile import Profile, ProfileManager, ProfileSession from ..storage.base import BaseStorage from ..storage.error import StorageNotFoundError -from ..version import __version__, RECORD_TYPE_ACAPY_VERSION +from ..version import RECORD_TYPE_ACAPY_VERSION, __version__ from ..wallet.base import BaseWallet -from ..wallet.did_info import DIDInfo from ..wallet.crypto import seed_to_did -from ..wallet.key_type import KeyType -from ..wallet.did_method import DIDMethod - +from ..wallet.did_info import DIDInfo +from ..wallet.did_method import SOV +from ..wallet.key_type import ED25519 from .base import ConfigError from .injection_context import InjectionContext @@ -80,7 +79,7 @@ async def wallet_config( if wallet_seed and seed_to_did(wallet_seed) != public_did: if context.settings.get("wallet.replace_public_did"): replace_did_info = await wallet.create_local_did( - method=DIDMethod.SOV, key_type=KeyType.ED25519, seed=wallet_seed + method=SOV, key_type=ED25519, seed=wallet_seed ) public_did = replace_did_info.did await wallet.set_public_did(public_did) @@ -100,8 +99,8 @@ async def wallet_config( metadata = {"endpoint": endpoint} if endpoint else None local_did_info = await wallet.create_local_did( - method=DIDMethod.SOV, - key_type=KeyType.ED25519, + method=SOV, + key_type=ED25519, seed=wallet_seed, metadata=metadata, ) @@ -111,7 +110,7 @@ async def wallet_config( print(f"Verkey: {local_did_info.verkey}") else: public_did_info = await wallet.create_public_did( - method=DIDMethod.SOV, key_type=KeyType.ED25519, seed=wallet_seed + method=SOV, key_type=ED25519, seed=wallet_seed ) public_did = public_did_info.did if provision: @@ -129,8 +128,8 @@ async def wallet_config( test_seed = "testseed000000000000000000000001" if test_seed: await wallet.create_local_did( - method=DIDMethod.SOV, - key_type=KeyType.ED25519, + method=SOV, + key_type=ED25519, seed=test_seed, metadata={"endpoint": "1.2.3.4:8021"}, ) diff --git a/aries_cloudagent/connections/base_manager.py b/aries_cloudagent/connections/base_manager.py index bd5e281694..400a494ca9 100644 --- a/aries_cloudagent/connections/base_manager.py +++ b/aries_cloudagent/connections/base_manager.py @@ -5,7 +5,7 @@ """ import logging -from typing import List, Sequence, Tuple +from typing import Optional, List, Sequence, Tuple, Text from pydid import ( BaseDIDDocument as ResolvedDocument, @@ -13,8 +13,9 @@ VerificationMethod, ) import pydid -from pydid.verification_method import Ed25519VerificationKey2018 +from pydid.verification_method import Ed25519VerificationKey2018, JsonWebKey2020 +from ..config.logging import get_logger_inst from ..core.error import BaseError from ..core.profile import Profile from ..did.did_key import DIDKey @@ -24,6 +25,9 @@ from ..protocols.coordinate_mediation.v1_0.models.mediation_record import ( MediationRecord, ) +from ..protocols.coordinate_mediation.v1_0.route_manager import ( + RouteManager, +) from ..resolver.base import ResolverError from ..resolver.did_resolver import DIDResolver from ..storage.base import BaseStorage @@ -34,6 +38,7 @@ from .models.conn_record import ConnRecord from .models.connection_target import ConnectionTarget from .models.diddoc import DIDDoc, PublicKey, PublicKeyType, Service +from ..wallet.util import bytes_to_b58, b64_to_bytes class BaseConnectionManagerError(BaseError): @@ -45,7 +50,6 @@ class BaseConnectionManager: RECORD_TYPE_DID_DOC = "did_doc" RECORD_TYPE_DID_KEY = "did_key" - SUPPORTED_KEY_TYPES = (Ed25519VerificationKey2018,) def __init__(self, profile: Profile): """ @@ -54,8 +58,12 @@ def __init__(self, profile: Profile): Args: session: The profile session for this presentation """ - self._logger = logging.getLogger(__name__) self._profile = profile + self._route_manager = profile.inject(RouteManager) + self._logger: logging.Logger = get_logger_inst( + profile=self._profile, + logger_name=__name__, + ) async def create_did_document( self, @@ -146,7 +154,7 @@ async def create_did_document( routing_keys = [*routing_keys, *mediator_routing_keys] svc_endpoints = [mediation_record.endpoint] - for (endpoint_index, svc_endpoint) in enumerate(svc_endpoints or []): + for endpoint_index, svc_endpoint in enumerate(svc_endpoints or []): endpoint_ident = "indy" if endpoint_index == 0 else f"indy{endpoint_index}" service = Service( did_info.did, @@ -223,7 +231,9 @@ async def remove_keys_for_did(self, did: str): storage: BaseStorage = session.inject(BaseStorage) await storage.delete_all_records(self.RECORD_TYPE_DID_KEY, {"did": did}) - async def resolve_invitation(self, did: str): + async def resolve_invitation( + self, did: str, service_accept: Optional[Sequence[Text]] = None + ): """ Resolve invitation with the DID Resolver. @@ -237,7 +247,7 @@ async def resolve_invitation(self, did: str): resolver = self._profile.inject(DIDResolver) try: - doc_dict: dict = await resolver.resolve(self._profile, did) + doc_dict: dict = await resolver.resolve(self._profile, did, service_accept) doc: ResolvedDocument = pydid.deserialize_document(doc_dict, strict=True) except ResolverError as error: raise BaseConnectionManagerError( @@ -263,24 +273,40 @@ async def resolve_invitation(self, did: str): endpoint = first_didcomm_service.service_endpoint recipient_keys: List[VerificationMethod] = [ - doc.dereference(url) for url in first_didcomm_service.recipient_keys + await resolver.dereference(self._profile, url, document=doc) + for url in first_didcomm_service.recipient_keys ] routing_keys: List[VerificationMethod] = [ - doc.dereference(url) for url in first_didcomm_service.routing_keys + await resolver.dereference(self._profile, url, document=doc) + for url in first_didcomm_service.routing_keys ] - for key in [*recipient_keys, *routing_keys]: - if not isinstance(key, self.SUPPORTED_KEY_TYPES): - raise BaseConnectionManagerError( - f"Key type {key.type} is not supported" - ) - return ( endpoint, - [key.material for key in recipient_keys], - [key.material for key in routing_keys], + [ + self._extract_key_material_in_base58_format(key) + for key in recipient_keys + ], + [self._extract_key_material_in_base58_format(key) for key in routing_keys], ) + @staticmethod + def _extract_key_material_in_base58_format(method: VerificationMethod) -> str: + if isinstance(method, Ed25519VerificationKey2018): + return method.material + elif isinstance(method, JsonWebKey2020): + if method.public_key_jwk.get("kty") == "OKP": + return bytes_to_b58(b64_to_bytes(method.public_key_jwk.get("x"), True)) + else: + raise BaseConnectionManagerError( + f"Key type {type(method).__name__}" + f"with kty {method.public_key_jwk.get('kty')} is not supported" + ) + else: + raise BaseConnectionManagerError( + f"Key type {type(method).__name__} is not supported" + ) + async def fetch_connection_targets( self, connection: ConnRecord ) -> Sequence[ConnectionTarget]: diff --git a/aries_cloudagent/connections/models/conn_record.py b/aries_cloudagent/connections/models/conn_record.py index 672d9359e0..5084f01039 100644 --- a/aries_cloudagent/connections/models/conn_record.py +++ b/aries_cloudagent/connections/models/conn_record.py @@ -174,6 +174,8 @@ def __eq__(self, other: Union[str, "ConnRecord.State"]) -> bool: "invitation_key", "their_public_did", "invitation_msg_id", + "state", + "their_role", } RECORD_TYPE = "connection" @@ -321,11 +323,15 @@ async def retrieve_by_invitation_key( invitation_key: The key on the originating invitation initiator: Filter by the initiator value """ - tag_filter = {"invitation_key": invitation_key} + tag_filter = { + "invitation_key": invitation_key, + "state": cls.State.INVITATION.rfc160, + } post_filter = {"state": cls.State.INVITATION.rfc160} if their_role: post_filter["their_role"] = cls.Role.get(their_role).rfc160 + tag_filter["their_role"] = cls.Role.get(their_role).rfc160 return await cls.retrieve_by_tag_filter(session, tag_filter, post_filter) @@ -373,7 +379,7 @@ async def find_existing_connection( @classmethod async def retrieve_by_request_id( - cls, session: ProfileSession, request_id: str + cls, session: ProfileSession, request_id: str, their_role: str = None ) -> "ConnRecord": """Retrieve a connection record from our previous request ID. @@ -382,6 +388,8 @@ async def retrieve_by_request_id( request_id: The ID of the originating connection request """ tag_filter = {"request_id": request_id} + if their_role: + tag_filter["their_role"] = their_role return await cls.retrieve_by_tag_filter(session, tag_filter) @classmethod @@ -674,11 +682,7 @@ class Meta: required=False, description="Routing state of connection", validate=validate.OneOf( - [ - getattr(ConnRecord, m) - for m in vars(ConnRecord) - if m.startswith("ROUTING_STATE_") - ] + ConnRecord.get_attributes_by_prefix("ROUTING_STATE_", walk_mro=False) ), example=ConnRecord.ROUTING_STATE_ACTIVE, ) @@ -687,11 +691,7 @@ class Meta: description="Connection acceptance: manual or auto", example=ConnRecord.ACCEPT_AUTO, validate=validate.OneOf( - [ - getattr(ConnRecord, a) - for a in vars(ConnRecord) - if a.startswith("ACCEPT_") - ] + ConnRecord.get_attributes_by_prefix("ACCEPT_", walk_mro=False) ), ) error_msg = fields.Str( @@ -704,11 +704,7 @@ class Meta: description="Invitation mode", example=ConnRecord.INVITATION_MODE_ONCE, validate=validate.OneOf( - [ - getattr(ConnRecord, i) - for i in vars(ConnRecord) - if i.startswith("INVITATION_MODE_") - ] + ConnRecord.get_attributes_by_prefix("INVITATION_MODE_", walk_mro=False) ), ) alias = fields.Str( diff --git a/aries_cloudagent/connections/models/connection_target.py b/aries_cloudagent/connections/models/connection_target.py index ade3f47392..a62b939af4 100644 --- a/aries_cloudagent/connections/models/connection_target.py +++ b/aries_cloudagent/connections/models/connection_target.py @@ -5,7 +5,7 @@ from marshmallow import EXCLUDE, fields from ...messaging.models.base import BaseModel, BaseModelSchema -from ...messaging.valid import INDY_DID, INDY_RAW_PUBLIC_KEY +from ...messaging.valid import INDY_RAW_PUBLIC_KEY, GENERIC_DID class ConnectionTarget(BaseModel): @@ -53,7 +53,7 @@ class Meta: model_class = ConnectionTarget unknown = EXCLUDE - did = fields.Str(required=False, description="", **INDY_DID) + did = fields.Str(required=False, description="", **GENERIC_DID) endpoint = fields.Str( required=False, description="Connection endpoint", diff --git a/aries_cloudagent/connections/models/diddoc/diddoc.py b/aries_cloudagent/connections/models/diddoc/diddoc.py index d1b8bcb0cc..ea47866487 100644 --- a/aries_cloudagent/connections/models/diddoc/diddoc.py +++ b/aries_cloudagent/connections/models/diddoc/diddoc.py @@ -176,7 +176,6 @@ def add_service_pubkeys( rv = [] for tag in [tags] if isinstance(tags, str) else list(tags): - for svc_key in service.get(tag, {}): canon_key = canon_ref(self.did, svc_key) pubkey = None diff --git a/aries_cloudagent/connections/models/diddoc/tests/test_diddoc.py b/aries_cloudagent/connections/models/diddoc/tests/test_diddoc.py index be002ddc27..141a0e6051 100644 --- a/aries_cloudagent/connections/models/diddoc/tests/test_diddoc.py +++ b/aries_cloudagent/connections/models/diddoc/tests/test_diddoc.py @@ -26,7 +26,6 @@ class TestDIDDoc(AsyncTestCase): async def test_basic(self): - # One authn key by reference dd_in = { "@context": "https://w3id.org/did/v1", diff --git a/aries_cloudagent/connections/models/tests/test_connection_target.py b/aries_cloudagent/connections/models/tests/test_connection_target.py index 093313924f..c60635e736 100644 --- a/aries_cloudagent/connections/models/tests/test_connection_target.py +++ b/aries_cloudagent/connections/models/tests/test_connection_target.py @@ -1,16 +1,22 @@ -from asynctest import TestCase as AsyncTestCase +import pytest from ..connection_target import ConnectionTarget -TEST_DID = "55GkHamhTU1ZbTbV2ab9DE" +TEST_DID_UNQUALIFIED = "55GkHamhTU1ZbTbV2ab9DE" +TEST_DID_SOV = "did:sov:55GkHamhTU1ZbTbV2ab9DE" +TEST_DID_PEER = "did:peer:WgWxqztrNooG92RXvxSTWv" +TEST_DID_WEB = "did:web:example" TEST_VERKEY = "3Dn1SJNPaCXcvvJvSbsFWP2xaCjMom3can8CQNhWrTRx" TEST_ENDPOINT = "http://localhost" -class TestConnectionTarget(AsyncTestCase): - def test_deser(self): +class TestConnectionTarget: + @pytest.mark.parametrize( + "did", [TEST_DID_UNQUALIFIED, TEST_DID_SOV, TEST_DID_PEER, TEST_DID_WEB] + ) + def test_deser(self, did): target = ConnectionTarget( - did=TEST_DID, + did=did, endpoint=TEST_ENDPOINT, label="a label", recipient_keys=[TEST_VERKEY], diff --git a/aries_cloudagent/connections/util.py b/aries_cloudagent/connections/util.py deleted file mode 100644 index e688643f9d..0000000000 --- a/aries_cloudagent/connections/util.py +++ /dev/null @@ -1,36 +0,0 @@ -"""Class for providing base utilities for Mediator support.""" - -from ..protocols.coordinate_mediation.v1_0.manager import MediationManager -from ..protocols.coordinate_mediation.v1_0.models.mediation_record import ( - MediationRecord, -) -from ..core.profile import Profile - -from .base_manager import BaseConnectionManagerError - - -async def mediation_record_if_id( - profile: Profile, mediation_id: str = None, or_default: bool = False -): - """Validate mediation and return record. - - If mediation_id is not None, - validate mediation record state and return record - else, return None - """ - mediation_record = None - if mediation_id: - async with profile.session() as session: - mediation_record = await MediationRecord.retrieve_by_id( - session, mediation_id - ) - elif or_default: - mediation_record = await MediationManager(profile).get_default_mediator() - - if mediation_record: - if mediation_record.state != MediationRecord.STATE_GRANTED: - raise BaseConnectionManagerError( - "Mediation is not granted for mediation identified by " - f"{mediation_record.mediation_id}" - ) - return mediation_record diff --git a/aries_cloudagent/core/conductor.py b/aries_cloudagent/core/conductor.py index 6ed174545f..8952ccb986 100644 --- a/aries_cloudagent/core/conductor.py +++ b/aries_cloudagent/core/conductor.py @@ -11,30 +11,37 @@ import hashlib import json import logging + +from packaging import version as package_version from qrcode import QRCode from ..admin.base_server import BaseAdminServer from ..admin.server import AdminResponder, AdminServer from ..config.default_context import ContextBuilder from ..config.injection_context import InjectionContext -from ..config.provider import ClassProvider from ..config.ledger import ( get_genesis_transactions, ledger_config, load_multiple_genesis_transactions_from_config, ) from ..config.logging import LoggingConfigurator +from ..config.provider import ClassProvider from ..config.wallet import wallet_config +from ..commands.upgrade import ( + get_upgrade_version_list, + add_version_record, + upgrade, +) from ..core.profile import Profile from ..indy.verifier import IndyVerifier -from ..ledger.base import BaseLedger + from ..ledger.error import LedgerConfigError, LedgerTransactionError from ..ledger.multiple_ledger.base_manager import ( BaseMultipleLedgerManager, MultipleLedgerManagerError, ) -from ..ledger.multiple_ledger.manager_provider import MultiIndyLedgerManagerProvider from ..ledger.multiple_ledger.ledger_requests_executor import IndyLedgerRequestsExecutor +from ..ledger.multiple_ledger.manager_provider import MultiIndyLedgerManagerProvider from ..messaging.responder import BaseResponder from ..multitenant.base import BaseMultitenantManager from ..multitenant.manager_provider import MultitenantManagerProvider @@ -45,8 +52,12 @@ from ..protocols.connections.v1_0.messages.connection_invitation import ( ConnectionInvitation, ) -from ..protocols.coordinate_mediation.v1_0.manager import MediationManager from ..protocols.coordinate_mediation.mediation_invite_store import MediationInviteStore +from ..protocols.coordinate_mediation.v1_0.manager import MediationManager +from ..protocols.coordinate_mediation.v1_0.route_manager import RouteManager +from ..protocols.coordinate_mediation.v1_0.route_manager_provider import ( + RouteManagerProvider, +) from ..protocols.out_of_band.v1_0.manager import OutOfBandManager from ..protocols.out_of_band.v1_0.messages.invitation import HSProto, InvitationMessage from ..storage.base import BaseStorage @@ -56,20 +67,21 @@ from ..transport.outbound.base import OutboundDeliveryError from ..transport.outbound.manager import OutboundTransportManager, QueuedOutboundMessage from ..transport.outbound.message import OutboundMessage -from ..transport.outbound.queue.base import BaseOutboundQueue -from ..transport.outbound.queue.loader import get_outbound_queue from ..transport.outbound.status import OutboundSendStatus from ..transport.wire_format import BaseWireFormat from ..utils.stats import Collector from ..utils.task_queue import CompletedTask, TaskQueue from ..vc.ld_proofs.document_loader import DocumentLoader -from ..version import __version__, RECORD_TYPE_ACAPY_VERSION +from ..version import RECORD_TYPE_ACAPY_VERSION, __version__ from ..wallet.did_info import DIDInfo - from .dispatcher import Dispatcher -from .util import STARTUP_EVENT_TOPIC, SHUTDOWN_EVENT_TOPIC +from .oob_processor import OobMessageProcessor +from .util import SHUTDOWN_EVENT_TOPIC, STARTUP_EVENT_TOPIC LOGGER = logging.getLogger(__name__) +# Refer ACA-Py issue #2197 +# When the from version is not found +DEFAULT_ACAPY_VERSION = "v0.7.5" class Conductor: @@ -97,7 +109,6 @@ def __init__(self, context_builder: ContextBuilder) -> None: self.outbound_transport_manager: OutboundTransportManager = None self.root_profile: Profile = None self.setup_public_did: DIDInfo = None - self.outbound_queue: BaseOutboundQueue = None @property def context(self) -> InjectionContext: @@ -142,7 +153,6 @@ async def setup(self): self.root_profile.BACKEND_NAME == "askar" and ledger.BACKEND_NAME == "indy-vdr" ): - context.injector.bind_instance(BaseLedger, ledger) context.injector.bind_provider( IndyVerifier, ClassProvider( @@ -154,7 +164,6 @@ async def setup(self): self.root_profile.BACKEND_NAME == "indy" and ledger.BACKEND_NAME == "indy" ): - context.injector.bind_instance(BaseLedger, ledger) context.injector.bind_provider( IndyVerifier, ClassProvider( @@ -206,13 +215,23 @@ async def setup(self): BaseMultitenantManager, MultitenantManagerProvider(self.root_profile) ) + # Bind route manager provider + context.injector.bind_provider( + RouteManager, RouteManagerProvider(self.root_profile) + ) + + # Bind oob message processor to be able to receive and process un-encrypted + # messages + context.injector.bind_instance( + OobMessageProcessor, + OobMessageProcessor(inbound_message_router=self.inbound_message_router), + ) + # Bind default PyLD document loader context.injector.bind_instance( DocumentLoader, DocumentLoader(self.root_profile) ) - self.outbound_queue = get_outbound_queue(self.root_profile) - # Admin API if context.settings.get("admin.enabled"): try: @@ -273,13 +292,6 @@ async def start(self) -> None: LOGGER.exception("Unable to start outbound transports") raise - if self.outbound_queue: - try: - await self.outbound_queue.start() - except Exception: - LOGGER.exception("Unable to start outbound queue") - raise - # Start up Admin server if self.admin_server: try: @@ -303,32 +315,63 @@ async def start(self) -> None: default_label, self.inbound_transport_manager.registered_transports, self.outbound_transport_manager.registered_transports, - self.outbound_queue, self.setup_public_did and self.setup_public_did.did, self.admin_server, ) # record ACA-Py version in Wallet, if needed + from_version_storage = None + from_version = None + agent_version = f"v{__version__}" async with self.root_profile.session() as session: storage: BaseStorage = session.context.inject(BaseStorage) - agent_version = f"v{__version__}" try: record = await storage.find_record( type_filter=RECORD_TYPE_ACAPY_VERSION, tag_query={}, ) - if record.value != agent_version: - LOGGER.exception( - ( - f"Wallet storage version {record.value} " - "does not match this ACA-Py agent " - f"version {agent_version}. Run aca-py " - "upgrade command to fix this." - ) - ) - raise + from_version_storage = record.value + LOGGER.info( + "Existing acapy_version storage record found, " + f"version set to {from_version_storage}" + ) except StorageNotFoundError: - pass + LOGGER.warning("Wallet version storage record not found.") + from_version_config = self.root_profile.settings.get("upgrade.from_version") + force_upgrade_flag = ( + self.root_profile.settings.get("upgrade.force_upgrade") or False + ) + + if force_upgrade_flag and from_version_config: + if from_version_storage: + if package_version.parse(from_version_storage) > package_version.parse( + from_version_config + ): + from_version = from_version_config + else: + from_version = from_version_storage + else: + from_version = from_version_config + else: + from_version = from_version_storage or from_version_config + if not from_version: + LOGGER.warning( + ( + "No upgrade from version was found from wallet or via" + " --from-version startup argument. Defaulting to " + f"{DEFAULT_ACAPY_VERSION}." + ) + ) + from_version = DEFAULT_ACAPY_VERSION + self.root_profile.settings.set_value("upgrade.from_version", from_version) + config_available_list = get_upgrade_version_list( + config_path=self.root_profile.settings.get("upgrade.config_path"), + from_version=from_version, + ) + if len(config_available_list) >= 1: + await upgrade(profile=self.root_profile) + elif not (from_version_storage and from_version_storage == agent_version): + await add_version_record(profile=self.root_profile, version=agent_version) # Create a static connection for use by the test-suite if context.settings.get("debug.test_suite_endpoint"): @@ -446,24 +489,21 @@ async def start(self) -> None: if mediation_connections_invite else OutOfBandManager(self.root_profile) ) - - conn_record = await mgr.receive_invitation( + record = await mgr.receive_invitation( invitation=invitation_handler.from_url( mediation_invite_record.invite ), auto_accept=True, ) async with self.root_profile.session() as session: - await ( - MediationInviteStore( - session.context.inject(BaseStorage) - ).mark_default_invite_as_used() - ) + await MediationInviteStore( + session.context.inject(BaseStorage) + ).mark_default_invite_as_used() - await conn_record.metadata_set( + await record.metadata_set( session, MediationManager.SEND_REQ_AFTER_CONNECTION, True ) - await conn_record.metadata_set( + await record.metadata_set( session, MediationManager.SET_TO_DEFAULT_ON_GRANTED, True ) @@ -478,7 +518,8 @@ async def start(self) -> None: async def stop(self, timeout=1.0): """Stop the agent.""" # notify protcols that we are shutting down - await self.root_profile.notify(SHUTDOWN_EVENT_TOPIC, {}) + if self.root_profile: + await self.root_profile.notify(SHUTDOWN_EVENT_TOPIC, {}) shutdown = TaskQueue() if self.dispatcher: @@ -489,16 +530,14 @@ async def stop(self, timeout=1.0): shutdown.run(self.inbound_transport_manager.stop()) if self.outbound_transport_manager: shutdown.run(self.outbound_transport_manager.stop()) - if self.outbound_queue: - shutdown.run(self.outbound_queue.stop()) - - # close multitenant profiles - multitenant_mgr = self.context.inject_or(BaseMultitenantManager) - if multitenant_mgr: - for profile in multitenant_mgr._instances.values(): - shutdown.run(profile.close()) if self.root_profile: + # close multitenant profiles + multitenant_mgr = self.context.inject_or(BaseMultitenantManager) + if multitenant_mgr: + for profile in multitenant_mgr.open_profiles: + shutdown.run(profile.close()) + shutdown.run(self.root_profile.close()) await shutdown.complete(timeout) @@ -592,6 +631,26 @@ async def outbound_message_router( """ Route an outbound message. + Args: + profile: The active profile for the request + message: An outbound message to be sent + inbound: The inbound message that produced this response, if available + """ + status: OutboundSendStatus = await self._outbound_message_router( + profile=profile, outbound=outbound, inbound=inbound + ) + await profile.notify(status.topic, outbound) + return status + + async def _outbound_message_router( + self, + profile: Profile, + outbound: OutboundMessage, + inbound: InboundMessage = None, + ) -> OutboundSendStatus: + """ + Route an outbound message. + Args: profile: The active profile for the request message: An outbound message to be sent @@ -631,8 +690,10 @@ async def queue_outbound( message: An outbound message to be sent inbound: The inbound message that produced this response, if available """ + has_target = outbound.target or outbound.target_list + # populate connection target(s) - if not outbound.target and not outbound.target_list and outbound.connection_id: + if not has_target and outbound.connection_id: conn_mgr = ConnectionManager(profile) try: outbound.target_list = await self.dispatcher.run_task( @@ -649,44 +710,23 @@ async def queue_outbound( self.admin_server.notify_fatal_error() raise del conn_mgr - # If ``self.outbound_queue`` is specified (usually set via - # outbound queue `-oq` commandline option), use that external - # queue. Else save the message to an internal queue. This - # internal queue usually results in the message to be sent over - # ACA-py `-ot` transport. - if self.outbound_queue: - return await self._queue_external(profile, outbound) - else: - return self._queue_internal(profile, outbound) - - async def _queue_external( - self, - profile: Profile, - outbound: OutboundMessage, - ) -> OutboundSendStatus: - """Save the message to an external outbound queue.""" - async with self.outbound_queue: - targets = ( - [outbound.target] if outbound.target else (outbound.target_list or []) - ) - for target in targets: - encoded_outbound_message = ( - await self.outbound_transport_manager.encode_outbound_message( - profile, outbound, target - ) - ) - await self.outbound_queue.enqueue_message( - encoded_outbound_message.payload, target.endpoint + # Find oob/connectionless target we can send the message to + elif not has_target and outbound.reply_thread_id: + message_processor = profile.inject(OobMessageProcessor) + outbound.target = await self.dispatcher.run_task( + message_processor.find_oob_target_for_outbound_message( + profile, outbound ) + ) - return OutboundSendStatus.SENT_TO_EXTERNAL_QUEUE + return await self._queue_message(profile, outbound) - def _queue_internal( + async def _queue_message( self, profile: Profile, outbound: OutboundMessage ) -> OutboundSendStatus: """Save the message to an internal outbound queue.""" try: - self.outbound_transport_manager.enqueue_message(profile, outbound) + await self.outbound_transport_manager.enqueue_message(profile, outbound) return OutboundSendStatus.QUEUED_FOR_DELIVERY except OutboundDeliveryError: LOGGER.warning("Cannot queue message for delivery, no supported transport") @@ -697,7 +737,6 @@ def handle_not_delivered( ) -> OutboundSendStatus: """Handle a message that failed delivery via outbound transports.""" queued_for_inbound = self.inbound_transport_manager.return_undelivered(outbound) - return ( OutboundSendStatus.WAITING_FOR_PICKUP if queued_for_inbound diff --git a/aries_cloudagent/core/dispatcher.py b/aries_cloudagent/core/dispatcher.py index 98df420472..c1a176dd4f 100644 --- a/aries_cloudagent/core/dispatcher.py +++ b/aries_cloudagent/core/dispatcher.py @@ -10,17 +10,20 @@ import os import warnings -from typing import Callable, Coroutine, Union +from typing import Callable, Coroutine, Optional, Union, Tuple +import weakref from aiohttp.web import HTTPException +from ..config.logging import get_logger_inst +from ..connections.models.conn_record import ConnRecord from ..core.profile import Profile from ..messaging.agent_message import AgentMessage from ..messaging.base_message import BaseMessage from ..messaging.error import MessageParseError from ..messaging.models.base import BaseModelError from ..messaging.request_context import RequestContext -from ..messaging.responder import BaseResponder +from ..messaging.responder import BaseResponder, SKIP_ACTIVE_CONN_CHECK_MSG_TYPES from ..messaging.util import datetime_now from ..protocols.connections.v1_0.manager import ConnectionManager from ..protocols.problem_report.v1_0.message import ProblemReport @@ -33,8 +36,13 @@ from .error import ProtocolMinorVersionNotSupported from .protocol_registry import ProtocolRegistry - -LOGGER = logging.getLogger(__name__) +from .util import ( + get_version_from_message_type, + validate_get_response_version, + # WARNING_DEGRADED_FEATURES, + # WARNING_VERSION_MISMATCH, + # WARNING_VERSION_NOT_SUPPORTED, +) class ProblemReportParseError(MessageParseError): @@ -54,6 +62,10 @@ def __init__(self, profile: Profile): self.collector: Collector = None self.profile = profile self.task_queue: TaskQueue = None + self.logger: logging.Logger = get_logger_inst( + profile=self.profile, + logger_name=__name__, + ) async def setup(self): """Perform async instance setup.""" @@ -79,7 +91,7 @@ def log_task(self, task: CompletedTask): """Log a completed task using the stats collector.""" if task.exc_info and not issubclass(task.exc_info[0], HTTPException): # skip errors intentionally returned to HTTP clients - LOGGER.exception( + self.logger.exception( "Handler error: %s", task.ident or "", exc_info=task.exc_info ) if self.collector: @@ -130,6 +142,9 @@ async def handle_message( inbound_message: The inbound message instance send_outbound: Async function to send outbound messages + # Raises: + # MessageParseError: If the message type version is not supported + Returns: The response from the handler @@ -137,13 +152,18 @@ async def handle_message( r_time = get_timer() error_result = None + version_warning = None message = None try: - message = await self.make_message(inbound_message.payload) + (message, warning) = await self.make_message( + profile, inbound_message.payload + ) except ProblemReportParseError: pass # avoid problem report recursion except MessageParseError as e: - LOGGER.error(f"Message parsing failed: {str(e)}, sending problem report") + self.logger.error( + f"Message parsing failed: {str(e)}, sending problem report" + ) error_result = ProblemReport( description={ "en": str(e), @@ -152,6 +172,47 @@ async def handle_message( ) if inbound_message.receipt.thread_id: error_result.assign_thread_id(inbound_message.receipt.thread_id) + # if warning: + # warning_message_type = inbound_message.payload.get("@type") + # if warning == WARNING_DEGRADED_FEATURES: + # self.logger.error( + # f"Sending {WARNING_DEGRADED_FEATURES} problem report, " + # "message type received with a minor version at or higher" + # " than protocol minimum supported and current minor version " + # f"for message_type {warning_message_type}" + # ) + # version_warning = ProblemReport( + # description={ + # "en": ( + # "message type received with a minor version at or " + # "higher than protocol minimum supported and current" + # f" minor version for message_type {warning_message_type}" + # ), + # "code": WARNING_DEGRADED_FEATURES, + # } + # ) + # elif warning == WARNING_VERSION_MISMATCH: + # self.logger.error( + # f"Sending {WARNING_VERSION_MISMATCH} problem report, message " + # "type received with a minor version higher than current minor " + # f"version for message_type {warning_message_type}" + # ) + # version_warning = ProblemReport( + # description={ + # "en": ( + # "message type received with a minor version higher" + # " than current minor version for message_type" + # f" {warning_message_type}" + # ), + # "code": WARNING_VERSION_MISMATCH, + # } + # ) + # elif warning == WARNING_VERSION_NOT_SUPPORTED: + # raise MessageParseError( + # f"Message type version not supported for {warning_message_type}" + # ) + # if version_warning and inbound_message.receipt.thread_id: + # version_warning.assign_thread_id(inbound_message.receipt.thread_id) trace_event( self.profile.settings, @@ -173,11 +234,20 @@ async def handle_message( context.injector.bind_instance(BaseResponder, responder) - connection_mgr = ConnectionManager(profile) - connection = await connection_mgr.find_inbound_connection( - inbound_message.receipt - ) - del connection_mgr + # When processing oob attach message we supply the connection id + # associated with the inbound message + if inbound_message.connection_id: + async with self.profile.session() as session: + connection = await ConnRecord.retrieve_by_id( + session, inbound_message.connection_id + ) + else: + connection_mgr = ConnectionManager(profile) + connection = await connection_mgr.find_inbound_connection( + inbound_message.receipt + ) + del connection_mgr + if connection: inbound_message.connection_id = connection.connection_id @@ -187,6 +257,8 @@ async def handle_message( if error_result: await responder.send_reply(error_result) + elif version_warning: + await responder.send_reply(version_warning) elif context.message: context.injector.bind_instance(BaseResponder, responder) @@ -203,7 +275,9 @@ async def handle_message( perf_counter=r_time, ) - async def make_message(self, parsed_msg: dict) -> BaseMessage: + async def make_message( + self, profile: Profile, parsed_msg: dict + ) -> Tuple[BaseMessage, Optional[str]]: """ Deserialize a message dict into the appropriate message instance. @@ -212,6 +286,7 @@ async def make_message(self, parsed_msg: dict) -> BaseMessage: Args: parsed_msg: The parsed message + profile: Profile Returns: An instance of the corresponding message class for this message @@ -228,6 +303,7 @@ async def make_message(self, parsed_msg: dict) -> BaseMessage: if not message_type: raise MessageParseError("Message does not contain '@type' parameter") + message_type_rec_version = get_version_from_message_type(message_type) registry: ProtocolRegistry = self.profile.inject(ProtocolRegistry) try: @@ -244,8 +320,10 @@ async def make_message(self, parsed_msg: dict) -> BaseMessage: if "/problem-report" in message_type: raise ProblemReportParseError("Error parsing problem report message") raise MessageParseError(f"Error deserializing message: {e}") from e - - return instance + _, warning = await validate_get_response_version( + profile, message_type_rec_version, message_cls + ) + return (instance, warning) async def complete(self, timeout: float = 0.1): """Wait for pending tasks to complete.""" @@ -272,7 +350,10 @@ def __init__( """ super().__init__(**kwargs) - self._context = context + # Weakly hold the context so it can be properly garbage collected. + # Binding this DispatcherResponder into the context creates a circular + # reference. + self._context = weakref.ref(context) self._inbound_message = inbound_message self._send = send_outbound @@ -285,13 +366,13 @@ async def create_outbound( Args: message: The message payload """ - if isinstance(message, AgentMessage) and self._context.settings.get( - "timing.enabled" - ): + context = self._context() + if not context: + raise RuntimeError("weakref to context has expired") + + if isinstance(message, AgentMessage) and context.settings.get("timing.enabled"): # Inject the timing decorator - in_time = ( - self._context.message_receipt and self._context.message_receipt.in_time - ) + in_time = context.message_receipt and context.message_receipt.in_time if not message._decorators.get("timing"): message._decorators["timing"] = { "in_time": in_time, @@ -300,14 +381,37 @@ async def create_outbound( return await super().create_outbound(message, **kwargs) - async def send_outbound(self, message: OutboundMessage) -> OutboundSendStatus: + async def send_outbound( + self, message: OutboundMessage, **kwargs + ) -> OutboundSendStatus: """ Send outbound message. Args: message: The `OutboundMessage` to be sent """ - return await self._send(self._context.profile, message, self._inbound_message) + context = self._context() + if not context: + raise RuntimeError("weakref to context has expired") + + msg_type = kwargs.get("message_type") + msg_id = kwargs.get("message_id") + + if ( + message.connection_id + and msg_type + and msg_type not in SKIP_ACTIVE_CONN_CHECK_MSG_TYPES + and not await super().conn_rec_active_state_check( + profile=context.profile, + connection_id=message.connection_id, + ) + ): + raise RuntimeError( + f"Connection {message.connection_id} is not ready" + " which is required for sending outbound" + f" message {msg_id} of type {msg_type}." + ) + return await self._send(context.profile, message, self._inbound_message) async def send_webhook(self, topic: str, payload: dict): """ @@ -321,4 +425,8 @@ async def send_webhook(self, topic: str, payload: dict): "responder.send_webhook is deprecated; please use the event bus instead.", DeprecationWarning, ) - await self._context.profile.notify("acapy::webhook::" + topic, payload) + context = self._context() + if not context: + raise RuntimeError("weakref to context has expired") + + await context.profile.notify("acapy::webhook::" + topic, payload) diff --git a/aries_cloudagent/core/event_bus.py b/aries_cloudagent/core/event_bus.py index 180f4130dc..22d7c8f922 100644 --- a/aries_cloudagent/core/event_bus.py +++ b/aries_cloudagent/core/event_bus.py @@ -15,6 +15,7 @@ Optional, Pattern, TYPE_CHECKING, + Tuple, ) from functools import partial @@ -193,7 +194,7 @@ class MockEventBus(EventBus): def __init__(self): """Initialize MockEventBus.""" super().__init__() - self.events = [] + self.events: List[Tuple[Profile, Event]] = [] async def notify(self, profile: "Profile", event: Event): """Append the event to MockEventBus.events.""" diff --git a/aries_cloudagent/core/in_memory/didcomm/tests/test_1pu.py b/aries_cloudagent/core/in_memory/didcomm/tests/test_1pu.py index f12bdc25d0..e57b7bddda 100644 --- a/aries_cloudagent/core/in_memory/didcomm/tests/test_1pu.py +++ b/aries_cloudagent/core/in_memory/didcomm/tests/test_1pu.py @@ -6,7 +6,6 @@ def test_1pu_hex_example(): - # Previously randomly generated 3 sets of keys aliceSecretKey = "23832cbef38641b8754a35f1f79bbcbc248e09ac93b01c2eaf12474f2ac406b6" alicePublicKey = "04fd4ca9eb7954a03517ac8249e6070aa3112e582f596b10f0d45d757b56d5dc0395a7d207d06503a4d6ad6e2ad3a1fd8cc233c072c0dc0f32213deb712c32cbdf" @@ -41,7 +40,6 @@ def test_1pu_hex_example(): # Example key exchange in https://tools.ietf.org/id/draft-madden-jose-ecdh-1pu-03.html#rfc.appendix.A def test_1pu_appendix_example(): - # Convert the three JWK keys into hex encoded byte format # Alice Key @@ -106,7 +104,6 @@ def test_1pu_appendix_example(): def main(): - test_1pu_hex_example() test_1pu_appendix_example() diff --git a/aries_cloudagent/core/in_memory/didcomm/tests/test_ecdh.py b/aries_cloudagent/core/in_memory/didcomm/tests/test_ecdh.py index 16f15754d9..943456c7b8 100644 --- a/aries_cloudagent/core/in_memory/didcomm/tests/test_ecdh.py +++ b/aries_cloudagent/core/in_memory/didcomm/tests/test_ecdh.py @@ -2,9 +2,9 @@ from ..derive_ecdh import * + # Generate the same shared secret from imported generated keys def test_ecdh_derive_shared_secret(): - # Import keys for two participating users aliceSecretKey = "23832cbef38641b8754a35f1f79bbcbc248e09ac93b01c2eaf12474f2ac406b6" alicePublicKey = "04fd4ca9eb7954a03517ac8249e6070aa3112e582f596b10f0d45d757b56d5dc0395a7d207d06503a4d6ad6e2ad3a1fd8cc233c072c0dc0f32213deb712c32cbdf" @@ -23,7 +23,6 @@ def test_ecdh_derive_shared_secret(): # Generate the same shared secret from random keys def test_ecdh_derive_shared_secret_random(): - # Generate random keys for the two participating users aliceSecretKey = SigningKey.generate(curve=NIST256p) alice = ECDH(curve=NIST256p) @@ -46,7 +45,6 @@ def test_ecdh_derive_shared_secret_random(): # Test the entire key generation flow, DeriveECDHSecret() into ConcatKDF() def test_ecdh_generate_key(): - aliceSecretKey = "23832cbef38641b8754a35f1f79bbcbc248e09ac93b01c2eaf12474f2ac406b6" alicePublicKey = "04fd4ca9eb7954a03517ac8249e6070aa3112e582f596b10f0d45d757b56d5dc0395a7d207d06503a4d6ad6e2ad3a1fd8cc233c072c0dc0f32213deb712c32cbdf" @@ -78,7 +76,6 @@ def test_ecdh_generate_key(): # Test the entire key generation flow, derive_shared_secret() into concat_kdf() def test_ecdh_generate_key_random(): - aliceSecretKey = SigningKey.generate(curve=NIST256p) alice = ECDH(curve=NIST256p) alice.load_private_key(aliceSecretKey) @@ -113,7 +110,6 @@ def test_ecdh_generate_key_random(): def main(): - test_ecdh_derive_shared_secret() test_ecdh_derive_shared_secret_random() test_ecdh_generate_key() diff --git a/aries_cloudagent/core/oob_processor.py b/aries_cloudagent/core/oob_processor.py new file mode 100644 index 0000000000..ccecc1a606 --- /dev/null +++ b/aries_cloudagent/core/oob_processor.py @@ -0,0 +1,371 @@ +"""Oob message processor and functions.""" + +import json +import logging +from typing import Any, Callable, Dict, List, Optional, cast + +from ..messaging.agent_message import AgentMessage +from ..config.logging import get_logger_inst +from ..connections.models.conn_record import ConnRecord +from ..connections.models.connection_target import ConnectionTarget +from ..messaging.decorators.service_decorator import ServiceDecorator +from ..messaging.request_context import RequestContext +from ..protocols.didcomm_prefix import DIDCommPrefix +from ..protocols.issue_credential.v1_0.message_types import CREDENTIAL_OFFER +from ..protocols.issue_credential.v2_0.message_types import CRED_20_OFFER +from ..protocols.present_proof.v1_0.message_types import PRESENTATION_REQUEST +from ..protocols.present_proof.v2_0.message_types import PRES_20_REQUEST +from ..protocols.out_of_band.v1_0.models.oob_record import OobRecord +from ..storage.error import StorageNotFoundError +from ..transport.inbound.message import InboundMessage +from ..transport.outbound.message import OutboundMessage +from ..transport.wire_format import JsonWireFormat +from .error import BaseError +from .profile import Profile + + +class OobMessageProcessorError(BaseError): + """Base error for OobMessageProcessor.""" + + +class OobMessageProcessor: + """Out of band message processor.""" + + def __init__( + self, + inbound_message_router: Callable[ + [Profile, InboundMessage, Optional[bool]], None + ], + ) -> None: + """Initialize an inbound OOB message processor. + + Args: + inbound_message_router: Method to create a new inbound session + + """ + self._inbound_message_router = inbound_message_router + self.wire_format = JsonWireFormat() + + async def clean_finished_oob_record(self, profile: Profile, message: AgentMessage): + """Clean up oob record associated with agent message, if applicable.""" + try: + async with profile.session() as session: + oob_record = await OobRecord.retrieve_by_tag_filter( + session, + {"invi_msg_id": message._thread.pthid}, + {"role": OobRecord.ROLE_SENDER}, + ) + + # If the oob record is not multi use and it doesn't contain any attachments + # We can now safely remove the oob record + if not oob_record.multi_use and not oob_record.invitation.requests_attach: + oob_record.state = OobRecord.STATE_DONE + await oob_record.emit_event(session) + await oob_record.delete_record(session) + except Exception: + # It is fine if no oob record is found, Only retrieved for cleanup + pass + + async def find_oob_target_for_outbound_message( + self, profile: Profile, outbound_message: OutboundMessage + ) -> Optional[ConnectionTarget]: + """Find connection target for the outbound message.""" + logger: logging.Logger = get_logger_inst( + profile=profile, + logger_name=__name__, + ) + try: + async with profile.session() as session: + # Try to find the oob record for the outbound message: + oob_record = await OobRecord.retrieve_by_tag_filter( + session, {"attach_thread_id": outbound_message.reply_thread_id} + ) + + logger.debug( + "extracting their service from oob record %s", + oob_record.their_service, + ) + + their_service = ServiceDecorator.deserialize(oob_record.their_service) + + # Attach ~service decorator so other message can respond + message = json.loads(outbound_message.payload) + if not message.get("~service"): + logger.debug( + "Setting our service on the message ~service %s", + oob_record.our_service, + ) + message["~service"] = oob_record.our_service + + message["~thread"] = { + **message.get("~thread", {}), + "pthid": oob_record.invi_msg_id, + } + + outbound_message.payload = json.dumps(message) + + logger.debug("Sending oob message payload %s", outbound_message.payload) + + return ConnectionTarget( + endpoint=their_service.endpoint, + recipient_keys=their_service.recipient_keys, + routing_keys=their_service.routing_keys, + sender_key=oob_record.our_recipient_key, + ) + except StorageNotFoundError: + return None + + async def find_oob_record_for_inbound_message( + self, context: RequestContext + ) -> Optional[OobRecord]: + """Find oob record for inbound message.""" + message_type = context.message._type + oob_record = None + logger: logging.Logger = get_logger_inst( + profile=context.profile, + logger_name=__name__, + ) + async with context.profile.session() as session: + # First try to find the oob record based on the associated pthid + if context.message_receipt.parent_thread_id: + try: + logger.debug( + "Retrieving OOB record using pthid " + f"{context.message_receipt.parent_thread_id} " + f"for message type {message_type}" + ) + oob_record = await OobRecord.retrieve_by_tag_filter( + session, + {"invi_msg_id": context.message_receipt.parent_thread_id}, + ) + except StorageNotFoundError: + # Fine if record is not found + pass + # Otherwise try to find it using the attach thread id. This is only needed + # for connectionless exchanges where every handler needs the context of the + # oob record for verification. We could attach the oob_record to all messages, + # even if we have a connection, but it would add another query to all inbound + # messages. + if ( + not oob_record + and not context.connection_record + and context.message_receipt.thread_id + and context.message_receipt.recipient_verkey + ): + try: + logger.debug( + "Retrieving OOB record using thid " + f"{context.message_receipt.thread_id} and recipient verkey" + f" {context.message_receipt.recipient_verkey} for " + f"message type {message_type}" + ) + oob_record = await OobRecord.retrieve_by_tag_filter( + session, + { + "attach_thread_id": context.message_receipt.thread_id, + "our_recipient_key": context.message_receipt.recipient_verkey, + }, + ) + except StorageNotFoundError: + # Fine if record is not found + pass + + # If not oob record was found we can return early without oob record + if not oob_record: + return None + + logger.debug( + f"Found out of band record for inbound message with type {message_type}" + f": {oob_record.oob_id}" + ) + + # If the connection does not match with the connection id associated with the + # oob record we don't want to associate the oob record to the current context + # This is not the case if the state is await response, in this case we might want + # to update the connection id on the oob record + if ( + # Only if we created the invitation + oob_record.role == OobRecord.ROLE_SENDER + # If connection is present and not same as oob_record conn id + and context.connection_record + and context.connection_record.connection_id != oob_record.connection_id + ): + logger.debug( + f"Oob record connection id {oob_record.connection_id} is different from" + f" inbound message connection {context.connection_record.connection_id}", + ) + # Mismatch in connection id's in only allowed in state await response + # (connection id can change bc of reuse) + if oob_record.state != OobRecord.STATE_AWAIT_RESPONSE: + logger.debug( + "Inbound message has incorrect connection_id " + f"{context.connection_record.connection_id}. Oob record " + f"{oob_record.oob_id} associated with connection id " + f"{oob_record.connection_id}" + ) + return None + + # If the state is await response, and there are attachments we want to update + # the connection id on the oob record. In case no request_attach is present, + # this is handled by the reuse handlers + if ( + oob_record.invitation.requests_attach + and oob_record.state == OobRecord.STATE_AWAIT_RESPONSE + ): + logger.debug( + f"Removing stale connection {oob_record.connection_id} due " + "to connection reuse" + ) + # Remove stale connection due to connection reuse + if oob_record.connection_id: + async with context.profile.session() as session: + old_conn_record = await ConnRecord.retrieve_by_id( + session, oob_record.connection_id + ) + await old_conn_record.delete_record(session) + + oob_record.connection_id = context.connection_record.connection_id + + # If no attach_thread_id is stored yet we need to match the current message + # thread_id against the attached messages in the oob invitation + if not oob_record.attach_thread_id and oob_record.invitation.requests_attach: + # Check if the current message thread_id corresponds to one of the invitation + # ~thread.thid + allowed_thread_ids = [ + self.get_thread_id(attachment.content) + for attachment in oob_record.invitation.requests_attach + ] + + if context.message_receipt.thread_id not in allowed_thread_ids: + logger.debug( + "Inbound message is for not allowed thread " + f"{context.message_receipt.thread_id}. Allowed " + f"threads are {allowed_thread_ids}" + ) + return None + + oob_record.attach_thread_id = context.message_receipt.thread_id + elif ( + oob_record.attach_thread_id + and context.message_receipt.thread_id != oob_record.attach_thread_id + ): + logger.debug( + f"Inbound message thread id {context.message_receipt.thread_id} does not" + f" match oob record thread id {oob_record.attach_thread_id}" + ) + return None + + their_service = ( + cast( + ServiceDecorator, + ServiceDecorator.deserialize(oob_record.their_service), + ) + if oob_record.their_service + else None + ) + + # Verify the sender key is present in their service in our record + # If we don't have the sender verkey stored yet we can allow any key + if their_service and ( + ( + context.message_receipt.recipient_verkey + and ( + not context.message_receipt.sender_verkey + or context.message_receipt.sender_verkey + not in their_service.recipient_keys + ) + ) + ): + logger.debug( + "Inbound message sender verkey does not match stored service on oob" + " record" + ) + return None + + # If the message has a ~service decorator we save it in the oob record so we + # can reply to this message + if context._message._service: + logger.debug( + "Storing service decorator in oob record %s", + context.message._service.serialize(), + ) + oob_record.their_service = context.message._service.serialize() + + async with context.profile.session() as session: + # We can now remove the oob record as the connection should now be stored in + # the exchange record itself. + if oob_record.connection_id: + oob_record.state = OobRecord.STATE_DONE + await oob_record.emit_event(session) + await oob_record.delete_record(session) + else: + await oob_record.save( + session, reason="Update their service in oob record" + ) + + return oob_record + + async def handle_message( + self, + profile: Profile, + messages: List[Dict[str, Any]], + oob_record: OobRecord, + their_service: Optional[ServiceDecorator] = None, + ): + """Message handler for inbound messages.""" + logger: logging.Logger = get_logger_inst( + profile=profile, + logger_name=__name__, + ) + + supported_types = [ + CREDENTIAL_OFFER, + CRED_20_OFFER, + PRESENTATION_REQUEST, + PRES_20_REQUEST, + ] + + supported_messages = [ + message + for message in messages + if DIDCommPrefix.unqualify(message["@type"]) in supported_types + ] + + if not supported_messages: + message_str = ", ".join(supported_types) + raise OobMessageProcessorError( + f"None of the oob attached messages supported. Supported message types " + f"are {message_str}" + ) + + message = supported_messages[0] + message_str = json.dumps(message) + + async with profile.session() as session: + message_dict, receipt = await self.wire_format.parse_message( + session, message_str + ) + + inbound_message = InboundMessage( + payload=message_dict, + connection_id=oob_record.connection_id, + receipt=receipt, + ) + + # We only need to store this data for connectionless + # (it could be the oob record is already deleted) + if not oob_record.connection_id: + oob_record.attach_thread_id = self.get_thread_id(message) + if their_service: + logger.debug( + "Storing their service in oob record %s", their_service + ) + oob_record.their_service = their_service.serialize() + + await oob_record.save(session) + + self._inbound_message_router(profile, inbound_message, False) + + def get_thread_id(self, message: Dict[str, Any]) -> str: + """Extract thread id from agent message dict.""" + return message.get("~thread", {}).get("thid") or message.get("@id") diff --git a/aries_cloudagent/core/plugin_registry.py b/aries_cloudagent/core/plugin_registry.py index 193c695241..40ea4fb83f 100644 --- a/aries_cloudagent/core/plugin_registry.py +++ b/aries_cloudagent/core/plugin_registry.py @@ -3,7 +3,7 @@ import logging from collections import OrderedDict from types import ModuleType -from typing import Sequence +from typing import Sequence, Iterable from ..config.injection_context import InjectionContext from ..core.event_bus import EventBus @@ -19,9 +19,10 @@ class PluginRegistry: """Plugin registry for indexing application plugins.""" - def __init__(self): + def __init__(self, blocklist: Iterable[str] = []): """Initialize a `PluginRegistry` instance.""" self._plugins = OrderedDict() + self._blocklist = set(blocklist) @property def plugin_names(self) -> Sequence[str]: @@ -119,6 +120,9 @@ def register_plugin(self, module_name: str) -> ModuleType: """Register a plugin module.""" if module_name in self._plugins: mod = self._plugins[module_name] + elif module_name in self._blocklist: + LOGGER.debug(f"Blocked {module_name} from loading due to blocklist") + return None else: try: mod = ClassLoader.load_module(module_name) @@ -214,7 +218,7 @@ async def load_protocol_version( ): """Load a particular protocol version.""" protocol_registry = context.inject(ProtocolRegistry) - goal_code_resgistry = context.inject(GoalCodeRegistry) + goal_code_registry = context.inject(GoalCodeRegistry) if hasattr(mod, "MESSAGE_TYPES"): protocol_registry.register_message_types( mod.MESSAGE_TYPES, version_definition=version_definition @@ -223,7 +227,7 @@ async def load_protocol_version( protocol_registry.register_controllers( mod.CONTROLLERS, version_definition=version_definition ) - goal_code_resgistry.register_controllers(mod.CONTROLLERS) + goal_code_registry.register_controllers(mod.CONTROLLERS) async def load_protocols(self, context: InjectionContext, plugin: ModuleType): """For modules that don't implement setup, register protocols manually.""" diff --git a/aries_cloudagent/core/profile.py b/aries_cloudagent/core/profile.py index e5de864d6e..c78ed8d94f 100644 --- a/aries_cloudagent/core/profile.py +++ b/aries_cloudagent/core/profile.py @@ -315,14 +315,14 @@ def __init__(self): def provide(self, settings: BaseSettings, injector: BaseInjector): """Create the profile manager instance.""" + mgr_type = settings.get_value("wallet.type", default="in_memory") - mgr_type = settings.get_value("wallet.type", default="in_memory").lower() - if mgr_type == "basic": + if mgr_type.lower() == "basic": # map previous value mgr_type = "in_memory" # mgr_type may be a fully qualified class name - mgr_class = self.MANAGER_TYPES.get(mgr_type, mgr_type) + mgr_class = self.MANAGER_TYPES.get(mgr_type.lower(), mgr_type) if mgr_class not in self._inst: LOGGER.info("Create profile manager: %s", mgr_type) diff --git a/aries_cloudagent/core/protocol_registry.py b/aries_cloudagent/core/protocol_registry.py index 805c35efa7..90175f9109 100644 --- a/aries_cloudagent/core/protocol_registry.py +++ b/aries_cloudagent/core/protocol_registry.py @@ -1,13 +1,14 @@ """Handle registration and publication of supported protocols.""" import logging +import re from typing import Mapping, Sequence from ..config.injection_context import InjectionContext from ..utils.classloader import ClassLoader -from .error import ProtocolMinorVersionNotSupported +from .error import ProtocolMinorVersionNotSupported, ProtocolDefinitionValidationError LOGGER = logging.getLogger(__name__) @@ -74,6 +75,79 @@ def parse_type_string(self, message_type): "minor_version": int(version_string_tokens[1]), } + def create_msg_types_for_minor_version(self, typesets, version_definition): + """ + Return mapping of message type to module path for minor versions. + + Args: + typesets: Mappings of message types to register + version_definition: Optional version definition dict + + Returns: + Typesets mapping + + """ + updated_typeset = {} + curr_minor_version = version_definition["current_minor_version"] + min_minor_version = version_definition["minimum_minor_version"] + major_version = version_definition["major_version"] + if curr_minor_version >= min_minor_version: + for version_index in range(min_minor_version, curr_minor_version + 1): + to_check = f"{str(major_version)}.{str(version_index)}" + updated_typeset.update( + self._get_updated_typeset_dict(typesets, to_check, updated_typeset) + ) + else: + raise ProtocolDefinitionValidationError( + "min_minor_version is greater than curr_minor_version for the" + f" following typeset: {str(typesets)}" + ) + return (updated_typeset,) + + def _get_updated_typeset_dict(self, typesets, to_check, updated_typeset) -> dict: + for typeset in typesets: + for msg_type_string, module_path in typeset.items(): + updated_msg_type_string = re.sub( + r"(\d+\.)?(\*|\d+)", to_check, msg_type_string + ) + updated_typeset[updated_msg_type_string] = module_path + return updated_typeset + + def _message_type_check_for_minor_verssion(self, version_definition) -> bool: + if not version_definition: + return False + curr_minor_version = version_definition["current_minor_version"] + min_minor_version = version_definition["minimum_minor_version"] + return bool(curr_minor_version >= 1 and curr_minor_version >= min_minor_version) + + def _create_and_register_updated_typesets(self, typesets, version_definition): + updated_typesets = self.create_msg_types_for_minor_version( + typesets, version_definition + ) + update_flag = False + for typeset in updated_typesets: + if typeset: + self._typemap.update(typeset) + update_flag = True + if update_flag: + return updated_typesets + else: + return None + + def _update_version_map(self, message_type_string, module_path, version_definition): + parsed_type_string = self.parse_type_string(message_type_string) + + if version_definition["major_version"] not in self._versionmap: + self._versionmap[version_definition["major_version"]] = [] + + self._versionmap[version_definition["major_version"]].append( + { + "parsed_type_string": parsed_type_string, + "version_definition": version_definition, + "message_module": module_path, + } + ) + def register_message_types(self, *typesets, version_definition=None): """ Add new supported message types. @@ -85,24 +159,27 @@ def register_message_types(self, *typesets, version_definition=None): """ # Maintain support for versionless protocol modules - for typeset in typesets: - self._typemap.update(typeset) + updated_typesets = None + minor_versions_supported = self._message_type_check_for_minor_verssion( + version_definition + ) + if not minor_versions_supported: + for typeset in typesets: + self._typemap.update(typeset) # Track versioned modules for version routing if version_definition: + # create updated typesets for minor versions and register them + if minor_versions_supported: + updated_typesets = self._create_and_register_updated_typesets( + typesets, version_definition + ) + if updated_typesets: + typesets = updated_typesets for typeset in typesets: for message_type_string, module_path in typeset.items(): - parsed_type_string = self.parse_type_string(message_type_string) - - if version_definition["major_version"] not in self._versionmap: - self._versionmap[version_definition["major_version"]] = [] - - self._versionmap[version_definition["major_version"]].append( - { - "parsed_type_string": parsed_type_string, - "version_definition": version_definition, - "message_module": module_path, - } + self._update_version_map( + message_type_string, module_path, version_definition ) def register_controllers(self, *controller_sets, version_definition=None): diff --git a/aries_cloudagent/core/tests/test_conductor.py b/aries_cloudagent/core/tests/test_conductor.py index d3f17d499b..0ed44f8b53 100644 --- a/aries_cloudagent/core/tests/test_conductor.py +++ b/aries_cloudagent/core/tests/test_conductor.py @@ -1,32 +1,28 @@ from io import StringIO -import asynctest -from asynctest import TestCase as AsyncTestCase -from asynctest import mock as async_mock +import mock as async_mock +from async_case import IsolatedAsyncioTestCase from ...admin.base_server import BaseAdminServer from ...config.base_context import ContextBuilder from ...config.injection_context import InjectionContext from ...connections.models.conn_record import ConnRecord from ...connections.models.connection_target import ConnectionTarget -from ...connections.models.diddoc import ( - DIDDoc, - PublicKey, - PublicKeyType, - Service, -) +from ...connections.models.diddoc import DIDDoc, PublicKey, PublicKeyType, Service +from ...core.event_bus import EventBus, MockEventBus from ...core.in_memory import InMemoryProfileManager from ...core.profile import ProfileManager from ...core.protocol_registry import ProtocolRegistry +from ...multitenant.base import BaseMultitenantManager +from ...multitenant.manager import MultitenantManager from ...protocols.coordinate_mediation.mediation_invite_store import ( MediationInviteRecord, - MediationInviteStore, ) from ...protocols.coordinate_mediation.v1_0.models.mediation_record import ( MediationRecord, ) -from ...resolver.did_resolver import DIDResolver, DIDResolverRegistry -from ...multitenant.base import BaseMultitenantManager +from ...protocols.out_of_band.v1_0.models.oob_record import OobRecord +from ...resolver.did_resolver import DIDResolver from ...storage.base import BaseStorage from ...storage.error import StorageNotFoundError from ...transport.inbound.message import InboundMessage @@ -34,14 +30,14 @@ from ...transport.outbound.base import OutboundDeliveryError from ...transport.outbound.manager import QueuedOutboundMessage from ...transport.outbound.message import OutboundMessage -from ...transport.wire_format import BaseWireFormat +from ...transport.outbound.status import OutboundSendStatus from ...transport.pack_format import PackWireFormat +from ...transport.wire_format import BaseWireFormat from ...utils.stats import Collector from ...version import __version__ from ...wallet.base import BaseWallet -from ...wallet.key_type import KeyType -from ...wallet.did_method import DIDMethod - +from ...wallet.did_method import SOV, DIDMethods +from ...wallet.key_type import ED25519 from .. import conductor as test_module @@ -91,7 +87,9 @@ async def build_context(self) -> InjectionContext: context.injector.bind_instance(ProfileManager, InMemoryProfileManager()) context.injector.bind_instance(ProtocolRegistry, ProtocolRegistry()) context.injector.bind_instance(BaseWireFormat, self.wire_format) - context.injector.bind_instance(DIDResolver, DIDResolver(DIDResolverRegistry())) + context.injector.bind_instance(DIDMethods, DIDMethods()) + context.injector.bind_instance(DIDResolver, DIDResolver([])) + context.injector.bind_instance(EventBus, MockEventBus()) return context @@ -102,8 +100,8 @@ async def build_context(self) -> InjectionContext: return context -class TestConductor(AsyncTestCase, Config, TestDIDs): - async def test_startup(self): +class TestConductor(IsolatedAsyncioTestCase, Config, TestDIDs): + async def test_startup_version_record_exists(self): builder: ContextBuilder = StubContextBuilder(self.test_settings) conductor = test_module.Conductor(builder) @@ -116,19 +114,257 @@ async def test_startup(self): ) as mock_logger, async_mock.patch.object( BaseStorage, "find_record", - async_mock.CoroutineMock( + async_mock.AsyncMock(return_value=async_mock.MagicMock(value="v0.7.3")), + ), async_mock.patch.object( + test_module, + "get_upgrade_version_list", + async_mock.MagicMock( + return_value=["v0.7.4", "0.7.5", "v0.8.0-rc1", "v8.0.0", "v0.8.1-rc2"] + ), + ), async_mock.patch.object( + test_module, + "upgrade", + async_mock.AsyncMock(), + ): + mock_outbound_mgr.return_value.registered_transports = { + "test": async_mock.MagicMock(schemes=["http"]) + } + await conductor.setup() + + session = await conductor.root_profile.session() + + wallet = session.inject(BaseWallet) + await wallet.create_public_did( + SOV, + ED25519, + ) + + mock_inbound_mgr.return_value.setup.assert_awaited_once() + mock_outbound_mgr.return_value.setup.assert_awaited_once() + + mock_inbound_mgr.return_value.registered_transports = {} + mock_outbound_mgr.return_value.registered_transports = {} + + await conductor.start() + + mock_inbound_mgr.return_value.start.assert_awaited_once_with() + mock_outbound_mgr.return_value.start.assert_awaited_once_with() + + mock_logger.print_banner.assert_called_once() + + await conductor.stop() + + mock_inbound_mgr.return_value.stop.assert_awaited_once_with() + mock_outbound_mgr.return_value.stop.assert_awaited_once_with() + + async def test_startup_version_no_upgrade_add_record(self): + builder: ContextBuilder = StubContextBuilder(self.test_settings) + conductor = test_module.Conductor(builder) + + with async_mock.patch.object( + test_module, "InboundTransportManager", autospec=True + ) as mock_inbound_mgr, async_mock.patch.object( + test_module, "OutboundTransportManager", autospec=True + ) as mock_outbound_mgr, async_mock.patch.object( + BaseStorage, + "find_record", + async_mock.AsyncMock(return_value=async_mock.MagicMock(value="v0.8.1")), + ), async_mock.patch.object( + test_module, + "get_upgrade_version_list", + async_mock.MagicMock(return_value=[]), + ), async_mock.patch.object( + test_module, + "add_version_record", + async_mock.AsyncMock(), + ): + mock_outbound_mgr.return_value.registered_transports = { + "test": async_mock.MagicMock(schemes=["http"]) + } + await conductor.setup() + session = await conductor.root_profile.session() + wallet = session.inject(BaseWallet) + await wallet.create_public_did( + SOV, + ED25519, + ) + mock_inbound_mgr.return_value.registered_transports = {} + mock_outbound_mgr.return_value.registered_transports = {} + await conductor.start() + await conductor.stop() + + with async_mock.patch.object( + test_module, "InboundTransportManager", autospec=True + ) as mock_inbound_mgr, async_mock.patch.object( + test_module, "OutboundTransportManager", autospec=True + ) as mock_outbound_mgr, async_mock.patch.object( + BaseStorage, + "find_record", + async_mock.AsyncMock( return_value=async_mock.MagicMock(value=f"v{__version__}") ), + ), async_mock.patch.object( + test_module, + "get_upgrade_version_list", + async_mock.MagicMock(return_value=[]), + ): + mock_outbound_mgr.return_value.registered_transports = { + "test": async_mock.MagicMock(schemes=["http"]) + } + await conductor.setup() + session = await conductor.root_profile.session() + wallet = session.inject(BaseWallet) + await wallet.create_public_did( + SOV, + ED25519, + ) + mock_inbound_mgr.return_value.registered_transports = {} + mock_outbound_mgr.return_value.registered_transports = {} + await conductor.start() + await conductor.stop() + + async def test_startup_version_force_upgrade(self): + test_settings = { + "admin.webhook_urls": ["http://sample.webhook.ca"], + "upgrade.from_version": "v0.7.5", + "upgrade.force_upgrade": True, + } + builder: ContextBuilder = StubContextBuilder(test_settings) + conductor = test_module.Conductor(builder) + with async_mock.patch.object( + test_module, "InboundTransportManager", autospec=True + ) as mock_inbound_mgr, async_mock.patch.object( + test_module, "OutboundTransportManager", autospec=True + ) as mock_outbound_mgr, async_mock.patch.object( + test_module, "LoggingConfigurator", autospec=True + ) as mock_logger, async_mock.patch.object( + BaseStorage, + "find_record", + async_mock.AsyncMock(return_value=async_mock.MagicMock(value="v0.8.0")), + ), async_mock.patch.object( + test_module, + "get_upgrade_version_list", + async_mock.MagicMock(return_value=["v0.8.0-rc1", "v8.0.0", "v0.8.1-rc1"]), + ), async_mock.patch.object( + test_module, + "upgrade", + async_mock.AsyncMock(), ): + mock_outbound_mgr.return_value.registered_transports = { + "test": async_mock.MagicMock(schemes=["http"]) + } + await conductor.setup() + session = await conductor.root_profile.session() + wallet = session.inject(BaseWallet) + await wallet.create_public_did( + SOV, + ED25519, + ) + mock_inbound_mgr.return_value.registered_transports = {} + mock_outbound_mgr.return_value.registered_transports = {} + await conductor.start() + await conductor.stop() + with async_mock.patch.object( + test_module, "InboundTransportManager", autospec=True + ) as mock_inbound_mgr, async_mock.patch.object( + test_module, "OutboundTransportManager", autospec=True + ) as mock_outbound_mgr, async_mock.patch.object( + test_module, "LoggingConfigurator", autospec=True + ) as mock_logger, async_mock.patch.object( + BaseStorage, + "find_record", + async_mock.AsyncMock(return_value=async_mock.MagicMock(value="v0.7.0")), + ), async_mock.patch.object( + test_module, + "get_upgrade_version_list", + async_mock.MagicMock(return_value=["v0.7.2", "v0.7.3", "v0.7.4"]), + ), async_mock.patch.object( + test_module, + "upgrade", + async_mock.AsyncMock(), + ): + mock_outbound_mgr.return_value.registered_transports = { + "test": async_mock.MagicMock(schemes=["http"]) + } + await conductor.setup() + session = await conductor.root_profile.session() + wallet = session.inject(BaseWallet) + await wallet.create_public_did( + SOV, + ED25519, + ) + mock_inbound_mgr.return_value.registered_transports = {} + mock_outbound_mgr.return_value.registered_transports = {} + await conductor.start() + await conductor.stop() + + with async_mock.patch.object( + test_module, "InboundTransportManager", autospec=True + ) as mock_inbound_mgr, async_mock.patch.object( + test_module, "OutboundTransportManager", autospec=True + ) as mock_outbound_mgr, async_mock.patch.object( + test_module, "LoggingConfigurator", autospec=True + ) as mock_logger, async_mock.patch.object( + BaseStorage, + "find_record", + async_mock.AsyncMock(side_effect=StorageNotFoundError()), + ), async_mock.patch.object( + test_module, + "get_upgrade_version_list", + async_mock.MagicMock(return_value=["v0.8.0-rc1", "v8.0.0", "v0.8.1-rc1"]), + ), async_mock.patch.object( + test_module, + "upgrade", + async_mock.AsyncMock(), + ): + await conductor.setup() + session = await conductor.root_profile.session() + wallet = session.inject(BaseWallet) + await wallet.create_public_did( + SOV, + ED25519, + ) + mock_inbound_mgr.return_value.registered_transports = {} + mock_outbound_mgr.return_value.registered_transports = {} + await conductor.start() + mock_logger.print_banner.assert_called_once() + await conductor.stop() + + async def test_startup_version_record_not_exists(self): + builder: ContextBuilder = StubContextBuilder(self.test_settings) + conductor = test_module.Conductor(builder) + + with async_mock.patch.object( + test_module, "InboundTransportManager", autospec=True + ) as mock_inbound_mgr, async_mock.patch.object( + test_module, "OutboundTransportManager", autospec=True + ) as mock_outbound_mgr, async_mock.patch.object( + test_module, "LoggingConfigurator", autospec=True + ) as mock_logger, async_mock.patch.object( + BaseStorage, + "find_record", + async_mock.AsyncMock(side_effect=StorageNotFoundError()), + ), async_mock.patch.object( + test_module, + "get_upgrade_version_list", + async_mock.MagicMock(return_value=["v0.8.0-rc1", "v8.0.0", "v0.8.1-rc1"]), + ), async_mock.patch.object( + test_module, + "upgrade", + async_mock.AsyncMock(), + ): + mock_outbound_mgr.return_value.registered_transports = { + "test": async_mock.MagicMock(schemes=["http"]) + } await conductor.setup() session = await conductor.root_profile.session() wallet = session.inject(BaseWallet) await wallet.create_public_did( - DIDMethod.SOV, - KeyType.ED25519, + SOV, + ED25519, ) mock_inbound_mgr.return_value.setup.assert_awaited_once() @@ -162,7 +398,9 @@ async def test_startup_admin_server_x(self): ) as mock_logger, async_mock.patch.object( test_module, "AdminServer", async_mock.MagicMock() ) as mock_admin_server: - + mock_outbound_mgr.return_value.registered_transports = { + "test": async_mock.MagicMock(schemes=["http"]) + } mock_admin_server.side_effect = ValueError() with self.assertRaises(ValueError): await conductor.setup() @@ -180,11 +418,14 @@ async def test_startup_no_public_did(self): ) as mock_logger, async_mock.patch.object( BaseStorage, "find_record", - async_mock.CoroutineMock( + async_mock.AsyncMock( return_value=async_mock.MagicMock(value=f"v{__version__}") ), ): - + mock_outbound_mgr.return_value.registered_transports = {} + mock_outbound_mgr.return_value.enqueue_message = async_mock.AsyncMock() + mock_outbound_mgr.return_value.start = async_mock.AsyncMock() + mock_outbound_mgr.return_value.stop = async_mock.AsyncMock() await conductor.setup() mock_inbound_mgr.return_value.setup.assert_awaited_once() @@ -217,12 +458,14 @@ async def test_stats(self): ) as mock_outbound_mgr, async_mock.patch.object( test_module, "LoggingConfigurator", autospec=True ) as mock_logger: - mock_inbound_mgr.return_value.sessions = ["dummy"] mock_outbound_mgr.return_value.outbound_buffer = [ async_mock.MagicMock(state=QueuedOutboundMessage.STATE_ENCODE), async_mock.MagicMock(state=QueuedOutboundMessage.STATE_DELIVER), ] + mock_outbound_mgr.return_value.registered_transports = { + "test": async_mock.MagicMock(schemes=["http"]) + } await conductor.setup() @@ -244,12 +487,17 @@ async def test_inbound_message_handler(self): builder: ContextBuilder = StubContextBuilder(self.test_settings) conductor = test_module.Conductor(builder) - await conductor.setup() + with async_mock.patch.object( + test_module, "OutboundTransportManager", autospec=True + ) as mock_outbound_mgr: + mock_outbound_mgr.return_value.registered_transports = { + "test": async_mock.MagicMock(schemes=["http"]) + } + await conductor.setup() with async_mock.patch.object( conductor.dispatcher, "queue_message", autospec=True ) as mock_dispatch_q: - message_body = "{}" receipt = MessageReceipt(direct_response_mode="snail mail") message = InboundMessage(message_body, receipt) @@ -268,7 +516,13 @@ async def test_inbound_message_handler_ledger_x(self): builder: ContextBuilder = StubContextBuilder(self.test_settings_admin) conductor = test_module.Conductor(builder) - await conductor.setup() + with async_mock.patch.object( + test_module, "OutboundTransportManager", autospec=True + ) as mock_outbound_mgr: + mock_outbound_mgr.return_value.registered_transports = { + "test": async_mock.MagicMock(schemes=["http"]) + } + await conductor.setup() with async_mock.patch.object( conductor.dispatcher, "queue_message", autospec=True @@ -294,9 +548,10 @@ async def test_outbound_message_handler_return_route(self): conductor = test_module.Conductor(builder) test_to_verkey = "test-to-verkey" test_from_verkey = "test-from-verkey" - await conductor.setup() + bus = conductor.root_profile.inject(EventBus) + payload = "{}" message = OutboundMessage(payload=payload) message.reply_to_verkey = test_to_verkey @@ -306,13 +561,17 @@ async def test_outbound_message_handler_return_route(self): with async_mock.patch.object( conductor.inbound_transport_manager, "return_to_session" - ) as mock_return, async_mock.patch.object( - conductor, "queue_outbound", async_mock.CoroutineMock() - ) as mock_queue: + ) as mock_return: mock_return.return_value = True - await conductor.outbound_message_router(conductor.context, message) + + status = await conductor.outbound_message_router( + conductor.root_profile, message + ) + assert status == OutboundSendStatus.SENT_TO_SESSION + assert bus.events + assert bus.events[0][1].topic == status.topic + assert bus.events[0][1].payload == message mock_return.assert_called_once_with(message) - mock_queue.assert_not_awaited() async def test_outbound_message_handler_with_target(self): builder: ContextBuilder = StubContextBuilder(self.test_settings) @@ -321,19 +580,29 @@ async def test_outbound_message_handler_with_target(self): with async_mock.patch.object( test_module, "OutboundTransportManager", autospec=True ) as mock_outbound_mgr: - + mock_outbound_mgr.return_value.registered_transports = { + "test": async_mock.MagicMock(schemes=["http"]) + } await conductor.setup() + bus = conductor.root_profile.inject(EventBus) + payload = "{}" target = ConnectionTarget( endpoint="endpoint", recipient_keys=(), routing_keys=(), sender_key="" ) message = OutboundMessage(payload=payload, target=target) - await conductor.outbound_message_router(conductor.context, message) - + status = await conductor.outbound_message_router( + conductor.root_profile, message + ) + assert status == OutboundSendStatus.QUEUED_FOR_DELIVERY + assert bus.events + print(bus.events) + assert bus.events[0][1].topic == status.topic + assert bus.events[0][1].payload == message mock_outbound_mgr.return_value.enqueue_message.assert_called_once_with( - conductor.context, message + conductor.root_profile, message ) async def test_outbound_message_handler_with_connection(self): @@ -345,14 +614,26 @@ async def test_outbound_message_handler_with_connection(self): ) as mock_outbound_mgr, async_mock.patch.object( test_module, "ConnectionManager", autospec=True ) as conn_mgr: - + mock_outbound_mgr.return_value.registered_transports = { + "test": async_mock.MagicMock(schemes=["http"]) + } await conductor.setup() + bus = conductor.root_profile.inject(EventBus) + payload = "{}" connection_id = "connection_id" message = OutboundMessage(payload=payload, connection_id=connection_id) - await conductor.outbound_message_router(conductor.root_profile, message) + status = await conductor.outbound_message_router( + conductor.root_profile, message + ) + + assert status == OutboundSendStatus.QUEUED_FOR_DELIVERY + assert bus.events + print(bus.events) + assert bus.events[0][1].topic == status.topic + assert bus.events[0][1].payload == message conn_mgr.return_value.get_connection_targets.assert_awaited_once_with( connection_id=connection_id @@ -373,24 +654,34 @@ async def test_outbound_message_handler_with_verkey_no_target(self): with async_mock.patch.object( test_module, "OutboundTransportManager", autospec=True ) as mock_outbound_mgr: - + mock_outbound_mgr.return_value.registered_transports = { + "test": async_mock.MagicMock(schemes=["http"]) + } await conductor.setup() + bus = conductor.root_profile.inject(EventBus) + payload = "{}" message = OutboundMessage( payload=payload, reply_to_verkey=TestDIDs.test_verkey ) - await conductor.outbound_message_router( - conductor.context, + status = await conductor.outbound_message_router( + conductor.root_profile, message, inbound=async_mock.MagicMock( receipt=async_mock.MagicMock(recipient_verkey=TestDIDs.test_verkey) ), ) + assert status == OutboundSendStatus.QUEUED_FOR_DELIVERY + assert bus.events + print(bus.events) + assert bus.events[0][1].topic == status.topic + assert bus.events[0][1].payload == message + mock_outbound_mgr.return_value.enqueue_message.assert_called_once_with( - conductor.context, message + conductor.root_profile, message ) async def test_handle_nots(self): @@ -401,8 +692,8 @@ async def test_handle_nots(self): test_module, "OutboundTransportManager", async_mock.MagicMock() ) as mock_outbound_mgr: mock_outbound_mgr.return_value = async_mock.MagicMock( - setup=async_mock.CoroutineMock(), - enqueue_message=async_mock.MagicMock(), + setup=async_mock.AsyncMock(), + enqueue_message=async_mock.AsyncMock(), ) payload = "{}" @@ -422,7 +713,7 @@ async def test_handle_nots(self): conductor.dispatcher, "run_task", async_mock.MagicMock() ) as mock_run_task: mock_conn_mgr.return_value.get_connection_targets = ( - async_mock.CoroutineMock() + async_mock.AsyncMock() ) mock_run_task.side_effect = test_module.ConnectionManagerError() await conductor.queue_outbound(conductor.root_profile, message) @@ -447,36 +738,24 @@ async def test_handle_outbound_queue(self): target=async_mock.MagicMock(endpoint="endpoint"), reply_to_verkey=TestDIDs.test_verkey, ) - await conductor.setup() - conductor.outbound_queue = async_mock.MagicMock( - enqueue_message=async_mock.CoroutineMock() - ) - conductor.outbound_transport_manager = async_mock.MagicMock( - encode_outbound_message=async_mock.CoroutineMock( - return_value=encoded_outbound_message_mock - ) - ) - await conductor.queue_outbound(conductor.root_profile, message) - conductor.outbound_transport_manager.encode_outbound_message.assert_called_once_with( - conductor.root_profile, message, message.target - ) - conductor.outbound_queue.enqueue_message.assert_called_once_with( - encoded_outbound_message_mock.payload, message.target.endpoint - ) - async def test_handle_not_returned_ledger_x(self): builder: ContextBuilder = StubContextBuilder(self.test_settings_admin) conductor = test_module.Conductor(builder) - await conductor.setup() - + with async_mock.patch.object( + test_module, "OutboundTransportManager", autospec=True + ) as mock_outbound_mgr: + mock_outbound_mgr.return_value.registered_transports = { + "test": async_mock.MagicMock(schemes=["http"]) + } + await conductor.setup() with async_mock.patch.object( conductor.dispatcher, "run_task", async_mock.MagicMock() ) as mock_dispatch_run, async_mock.patch.object( - conductor, "queue_outbound", async_mock.CoroutineMock() + conductor, "queue_outbound", async_mock.AsyncMock() ) as mock_queue, async_mock.patch.object( conductor.admin_server, "notify_fatal_error", async_mock.MagicMock() ) as mock_notify: @@ -501,7 +780,13 @@ async def test_queue_outbound_ledger_x(self): builder: ContextBuilder = StubContextBuilder(self.test_settings_admin) conductor = test_module.Conductor(builder) - await conductor.setup() + with async_mock.patch.object( + test_module, "OutboundTransportManager", autospec=True + ) as mock_outbound_mgr: + mock_outbound_mgr.return_value.registered_transports = { + "test": async_mock.MagicMock(schemes=["http"]) + } + await conductor.setup() with async_mock.patch.object( test_module, "ConnectionManager", autospec=True @@ -510,7 +795,7 @@ async def test_queue_outbound_ledger_x(self): ) as mock_dispatch_run, async_mock.patch.object( conductor.admin_server, "notify_fatal_error", async_mock.MagicMock() ) as mock_notify: - conn_mgr.return_value.get_connection_targets = async_mock.CoroutineMock() + conn_mgr.return_value.get_connection_targets = async_mock.AsyncMock() mock_dispatch_run.side_effect = test_module.LedgerConfigError( "No such ledger" ) @@ -532,16 +817,21 @@ async def test_admin(self): builder: ContextBuilder = StubContextBuilder(self.test_settings) builder.update_settings({"admin.enabled": "1"}) conductor = test_module.Conductor(builder) - - await conductor.setup() + with async_mock.patch.object( + test_module, "OutboundTransportManager", autospec=True + ) as mock_outbound_mgr: + mock_outbound_mgr.return_value.registered_transports = { + "test": async_mock.MagicMock(schemes=["http"]) + } + await conductor.setup() admin = conductor.context.inject(BaseAdminServer) assert admin is conductor.admin_server session = await conductor.root_profile.session() wallet = session.inject(BaseWallet) await wallet.create_public_did( - DIDMethod.SOV, - KeyType.ED25519, + SOV, + ED25519, ) with async_mock.patch.object( @@ -551,7 +841,7 @@ async def test_admin(self): ) as admin_stop, async_mock.patch.object( BaseStorage, "find_record", - async_mock.CoroutineMock( + async_mock.AsyncMock( return_value=async_mock.MagicMock(value=f"v{__version__}") ), ): @@ -571,16 +861,21 @@ async def test_admin_startx(self): } ) conductor = test_module.Conductor(builder) - - await conductor.setup() + with async_mock.patch.object( + test_module, "OutboundTransportManager", autospec=True + ) as mock_outbound_mgr: + mock_outbound_mgr.return_value.registered_transports = { + "test": async_mock.MagicMock(schemes=["http"]) + } + await conductor.setup() admin = conductor.context.inject(BaseAdminServer) assert admin is conductor.admin_server session = await conductor.root_profile.session() wallet = session.inject(BaseWallet) await wallet.create_public_did( - DIDMethod.SOV, - KeyType.ED25519, + SOV, + ED25519, ) with async_mock.patch.object( @@ -594,15 +889,15 @@ async def test_admin_startx(self): ) as conn_mgr, async_mock.patch.object( BaseStorage, "find_record", - async_mock.CoroutineMock( + async_mock.AsyncMock( return_value=async_mock.MagicMock(value=f"v{__version__}") ), ): admin_start.side_effect = KeyError("trouble") - oob_mgr.return_value.create_invitation = async_mock.CoroutineMock( + oob_mgr.return_value.create_invitation = async_mock.AsyncMock( side_effect=KeyError("double trouble") ) - conn_mgr.return_value.create_invitation = async_mock.CoroutineMock( + conn_mgr.return_value.create_invitation = async_mock.AsyncMock( side_effect=KeyError("triple trouble") ) await conductor.start() @@ -622,7 +917,9 @@ async def test_setup_collector(self): ) as mock_outbound_mgr, async_mock.patch.object( test_module, "LoggingConfigurator", autospec=True ) as mock_logger: - + mock_outbound_mgr.return_value.registered_transports = { + "test": async_mock.MagicMock(schemes=["http"]) + } await conductor.setup() async def test_start_static(self): @@ -635,20 +932,25 @@ async def test_start_static(self): ) as mock_mgr, async_mock.patch.object( BaseStorage, "find_record", - async_mock.CoroutineMock( + async_mock.AsyncMock( return_value=async_mock.MagicMock(value=f"v{__version__}") ), - ): + ), async_mock.patch.object( + test_module, "OutboundTransportManager", autospec=True + ) as mock_outbound_mgr: + mock_outbound_mgr.return_value.registered_transports = { + "test": async_mock.MagicMock(schemes=["http"]) + } await conductor.setup() session = await conductor.root_profile.session() wallet = session.inject(BaseWallet) await wallet.create_public_did( - DIDMethod.SOV, - KeyType.ED25519, + SOV, + ED25519, ) - mock_mgr.return_value.create_static_connection = async_mock.CoroutineMock() + mock_mgr.return_value.create_static_connection = async_mock.AsyncMock() await conductor.start() mock_mgr.return_value.create_static_connection.assert_awaited_once() @@ -661,17 +963,42 @@ async def test_start_x_in(self): test_module, "ConnectionManager" ) as mock_mgr, async_mock.patch.object( test_module, "InboundTransportManager" - ) as mock_intx_mgr: + ) as mock_intx_mgr, async_mock.patch.object( + test_module, "OutboundTransportManager", autospec=True + ) as mock_outbound_mgr: mock_intx_mgr.return_value = async_mock.MagicMock( - setup=async_mock.CoroutineMock(), - start=async_mock.CoroutineMock(side_effect=KeyError("trouble")), + setup=async_mock.AsyncMock(), + start=async_mock.AsyncMock(side_effect=KeyError("trouble")), + ) + mock_outbound_mgr.return_value.registered_transports = { + "test": async_mock.MagicMock(schemes=["http"]) + } + await conductor.setup() + mock_mgr.return_value.create_static_connection = async_mock.AsyncMock() + with self.assertRaises(KeyError): + await conductor.start() + + async def test_start_x_out_a(self): + builder: ContextBuilder = StubContextBuilder(self.test_settings) + builder.update_settings({"debug.test_suite_endpoint": True}) + conductor = test_module.Conductor(builder) + + with async_mock.patch.object( + test_module, "ConnectionManager" + ) as mock_mgr, async_mock.patch.object( + test_module, "OutboundTransportManager" + ) as mock_outx_mgr: + mock_outx_mgr.return_value = async_mock.MagicMock( + setup=async_mock.AsyncMock(), + start=async_mock.AsyncMock(side_effect=KeyError("trouble")), + registered_transports={"test": async_mock.MagicMock(schemes=["http"])}, ) await conductor.setup() - mock_mgr.return_value.create_static_connection = async_mock.CoroutineMock() + mock_mgr.return_value.create_static_connection = async_mock.AsyncMock() with self.assertRaises(KeyError): await conductor.start() - async def test_start_x_out(self): + async def test_start_x_out_b(self): builder: ContextBuilder = StubContextBuilder(self.test_settings) builder.update_settings({"debug.test_suite_endpoint": True}) conductor = test_module.Conductor(builder) @@ -682,11 +1009,14 @@ async def test_start_x_out(self): test_module, "OutboundTransportManager" ) as mock_outx_mgr: mock_outx_mgr.return_value = async_mock.MagicMock( - setup=async_mock.CoroutineMock(), - start=async_mock.CoroutineMock(side_effect=KeyError("trouble")), + setup=async_mock.AsyncMock(), + start=async_mock.AsyncMock(side_effect=KeyError("trouble")), + stop=async_mock.AsyncMock(), + registered_transports={}, + enqueue_message=async_mock.AsyncMock(), ) await conductor.setup() - mock_mgr.return_value.create_static_connection = async_mock.CoroutineMock() + mock_mgr.return_value.create_static_connection = async_mock.AsyncMock() with self.assertRaises(KeyError): await conductor.start() @@ -697,8 +1027,9 @@ async def test_dispatch_complete_non_fatal_x(self): message_body = "{}" receipt = MessageReceipt(direct_response_mode="snail mail") message = InboundMessage(message_body, receipt) + exc = KeyError("sample exception") mock_task = async_mock.MagicMock( - exc_info=(KeyError, KeyError("sample exception"), "..."), + exc_info=(type(exc), exc, exc.__traceback__), ident="abc", timing={ "queued": 1234567890, @@ -708,7 +1039,13 @@ async def test_dispatch_complete_non_fatal_x(self): }, ) - await conductor.setup() + with async_mock.patch.object( + test_module, "OutboundTransportManager", autospec=True + ) as mock_outbound_mgr: + mock_outbound_mgr.return_value.registered_transports = { + "test": async_mock.MagicMock(schemes=["http"]) + } + await conductor.setup() with async_mock.patch.object( conductor.admin_server, "notify_fatal_error", async_mock.MagicMock() @@ -723,12 +1060,9 @@ async def test_dispatch_complete_fatal_x(self): message_body = "{}" receipt = MessageReceipt(direct_response_mode="snail mail") message = InboundMessage(message_body, receipt) + exc = test_module.LedgerTransactionError("Ledger is wobbly") mock_task = async_mock.MagicMock( - exc_info=( - test_module.LedgerTransactionError, - test_module.LedgerTransactionError("Ledger is wobbly"), - "...", - ), + exc_info=(type(exc), exc, exc.__traceback__), ident="abc", timing={ "queued": 1234567890, @@ -738,7 +1072,13 @@ async def test_dispatch_complete_fatal_x(self): }, ) - await conductor.setup() + with async_mock.patch.object( + test_module, "OutboundTransportManager", autospec=True + ) as mock_outbound_mgr: + mock_outbound_mgr.return_value.registered_transports = { + "test": async_mock.MagicMock(schemes=["http"]) + } + await conductor.setup() with async_mock.patch.object( conductor.admin_server, "notify_fatal_error", async_mock.MagicMock() @@ -757,24 +1097,27 @@ async def test_print_invite_connection(self): ) conductor = test_module.Conductor(builder) - await conductor.setup() - with async_mock.patch( "sys.stdout", new=StringIO() ) as captured, async_mock.patch.object( BaseStorage, "find_record", - async_mock.CoroutineMock( + async_mock.AsyncMock( return_value=async_mock.MagicMock(value=f"v{__version__}") ), - ): + ), async_mock.patch.object( + test_module, "OutboundTransportManager", autospec=True + ) as mock_outbound_mgr: + mock_outbound_mgr.return_value.registered_transports = { + "test": async_mock.MagicMock(schemes=["http"]) + } await conductor.setup() session = await conductor.root_profile.session() wallet = session.inject(BaseWallet) await wallet.create_public_did( - DIDMethod.SOV, - KeyType.ED25519, + SOV, + ED25519, ) await conductor.start() @@ -788,18 +1131,24 @@ async def test_clear_default_mediator(self): builder.update_settings({"mediation.clear": True}) conductor = test_module.Conductor(builder) - await conductor.setup() + with async_mock.patch.object( + test_module, "OutboundTransportManager", autospec=True + ) as mock_outbound_mgr: + mock_outbound_mgr.return_value.registered_transports = { + "test": async_mock.MagicMock(schemes=["http"]) + } + await conductor.setup() with async_mock.patch.object( test_module, "MediationManager", return_value=async_mock.MagicMock( - clear_default_mediator=async_mock.CoroutineMock() + clear_default_mediator=async_mock.AsyncMock() ), ) as mock_mgr, async_mock.patch.object( BaseStorage, "find_record", - async_mock.CoroutineMock( + async_mock.AsyncMock( return_value=async_mock.MagicMock(value=f"v{__version__}") ), ): @@ -812,16 +1161,22 @@ async def test_set_default_mediator(self): builder.update_settings({"mediation.default_id": "test-id"}) conductor = test_module.Conductor(builder) - await conductor.setup() + with async_mock.patch.object( + test_module, "OutboundTransportManager", autospec=True + ) as mock_outbound_mgr: + mock_outbound_mgr.return_value.registered_transports = { + "test": async_mock.MagicMock(schemes=["http"]) + } + await conductor.setup() with async_mock.patch.object( test_module, "MediationManager", return_value=async_mock.MagicMock( - set_default_mediator_by_id=async_mock.CoroutineMock() + set_default_mediator_by_id=async_mock.AsyncMock() ), ) as mock_mgr, async_mock.patch.object( - MediationRecord, "retrieve_by_id", async_mock.CoroutineMock() + MediationRecord, "retrieve_by_id", async_mock.AsyncMock() ), async_mock.patch.object( test_module, "LOGGER", @@ -833,7 +1188,7 @@ async def test_set_default_mediator(self): ), async_mock.patch.object( BaseStorage, "find_record", - async_mock.CoroutineMock( + async_mock.AsyncMock( return_value=async_mock.MagicMock(value=f"v{__version__}") ), ): @@ -846,18 +1201,24 @@ async def test_set_default_mediator_x(self): builder.update_settings({"mediation.default_id": "test-id"}) conductor = test_module.Conductor(builder) - await conductor.setup() + with async_mock.patch.object( + test_module, "OutboundTransportManager", autospec=True + ) as mock_outbound_mgr: + mock_outbound_mgr.return_value.registered_transports = { + "test": async_mock.MagicMock(schemes=["http"]) + } + await conductor.setup() with async_mock.patch.object( MediationRecord, "retrieve_by_id", - async_mock.CoroutineMock(side_effect=Exception()), + async_mock.AsyncMock(side_effect=Exception()), ), async_mock.patch.object( test_module, "LOGGER" ) as mock_logger, async_mock.patch.object( BaseStorage, "find_record", - async_mock.CoroutineMock( + async_mock.AsyncMock( return_value=async_mock.MagicMock(value=f"v{__version__}") ), ): @@ -877,7 +1238,13 @@ async def test_webhook_router(self): test_endpoint = "http://example" test_attempts = 2 - await conductor.setup() + with async_mock.patch.object( + test_module, "OutboundTransportManager", autospec=True + ) as mock_outbound_mgr: + mock_outbound_mgr.return_value.registered_transports = { + "test": async_mock.MagicMock(schemes=["http"]) + } + await conductor.setup() with async_mock.patch.object( conductor.outbound_transport_manager, "enqueue_webhook" ) as mock_enqueue: @@ -914,19 +1281,26 @@ async def test_shutdown_multitenant_profiles(self): ) as mock_outbound_mgr, async_mock.patch.object( test_module, "LoggingConfigurator", autospec=True ) as mock_logger: - + mock_outbound_mgr.return_value.registered_transports = { + "test": async_mock.MagicMock(schemes=["http"]) + } await conductor.setup() multitenant_mgr = conductor.context.inject(BaseMultitenantManager) + assert isinstance(multitenant_mgr, MultitenantManager) - multitenant_mgr._instances = { - "test1": async_mock.MagicMock(close=async_mock.CoroutineMock()), - "test2": async_mock.MagicMock(close=async_mock.CoroutineMock()), - } + multitenant_mgr._profiles.put( + "test1", + async_mock.MagicMock(close=async_mock.AsyncMock()), + ) + multitenant_mgr._profiles.put( + "test2", + async_mock.MagicMock(close=async_mock.AsyncMock()), + ) await conductor.stop() - multitenant_mgr._instances["test1"].close.assert_called_once_with() - multitenant_mgr._instances["test2"].close.assert_called_once_with() + multitenant_mgr._profiles.profiles["test1"].close.assert_called_once_with() + multitenant_mgr._profiles.profiles["test2"].close.assert_called_once_with() def get_invite_store_mock( @@ -936,14 +1310,12 @@ def get_invite_store_mock( used_invite = MediationInviteRecord(invite_string, used=True) return async_mock.MagicMock( - get_mediation_invite_record=async_mock.CoroutineMock( - return_value=unused_invite - ), - mark_default_invite_as_used=async_mock.CoroutineMock(return_value=used_invite), + get_mediation_invite_record=async_mock.AsyncMock(return_value=unused_invite), + mark_default_invite_as_used=async_mock.AsyncMock(return_value=used_invite), ) -class TestConductorMediationSetup(AsyncTestCase, Config): +class TestConductorMediationSetup(IsolatedAsyncioTestCase, Config): """ Test related with setting up mediation from given arguments or stored invitation. """ @@ -958,17 +1330,23 @@ def __get_mediator_config( return builder - @asynctest.patch.object( + @async_mock.patch.object( test_module, "MediationInviteStore", return_value=get_invite_store_mock("test-invite"), ) - @asynctest.patch.object(test_module.ConnectionInvitation, "from_url") + @async_mock.patch.object(test_module.ConnectionInvitation, "from_url") async def test_mediator_invitation_0160(self, mock_from_url, _): conductor = test_module.Conductor( self.__get_mediator_config("test-invite", True) ) - await conductor.setup() + with async_mock.patch.object( + test_module, "OutboundTransportManager", autospec=True + ) as mock_outbound_mgr: + mock_outbound_mgr.return_value.registered_transports = { + "test": async_mock.MagicMock(schemes=["http"]) + } + await conductor.setup() mock_conn_record = async_mock.MagicMock() @@ -977,17 +1355,17 @@ async def test_mediator_invitation_0160(self, mock_from_url, _): "ConnectionManager", async_mock.MagicMock( return_value=async_mock.MagicMock( - receive_invitation=async_mock.CoroutineMock( + receive_invitation=async_mock.AsyncMock( return_value=mock_conn_record ) ) ), ) as mock_mgr, async_mock.patch.object( - mock_conn_record, "metadata_set", async_mock.CoroutineMock() + mock_conn_record, "metadata_set", async_mock.AsyncMock() ), async_mock.patch.object( BaseStorage, "find_record", - async_mock.CoroutineMock( + async_mock.AsyncMock( return_value=async_mock.MagicMock(value=f"v{__version__}") ), ): @@ -996,18 +1374,26 @@ async def test_mediator_invitation_0160(self, mock_from_url, _): mock_from_url.assert_called_once_with("test-invite") mock_mgr.return_value.receive_invitation.assert_called_once() - @asynctest.patch.object( + @async_mock.patch.object( test_module, "MediationInviteStore", return_value=get_invite_store_mock("test-invite"), ) - @asynctest.patch.object(test_module.InvitationMessage, "from_url") + @async_mock.patch.object(test_module.InvitationMessage, "from_url") async def test_mediator_invitation_0434(self, mock_from_url, _): conductor = test_module.Conductor( self.__get_mediator_config("test-invite", False) ) - await conductor.setup() - + with async_mock.patch.object( + test_module, "OutboundTransportManager", autospec=True + ) as mock_outbound_mgr: + mock_outbound_mgr.return_value.registered_transports = { + "test": async_mock.MagicMock(schemes=["http"]) + } + await conductor.setup() + conductor.root_profile.context.update_settings( + {"mediation.connections_invite": False} + ) conn_record = ConnRecord( invitation_key="3Dn1SJNPaCXcvvJvSbsFWP2xaCjMom3can8CQNhWrTRx", their_label="Hello", @@ -1016,30 +1402,37 @@ async def test_mediator_invitation_0434(self, mock_from_url, _): ) conn_record.accept = ConnRecord.ACCEPT_MANUAL await conn_record.save(await conductor.root_profile.session()) + invitation = test_module.InvitationMessage() + oob_record = OobRecord( + invitation=invitation, + invi_msg_id=invitation._id, + role=OobRecord.ROLE_RECEIVER, + connection_id=conn_record.connection_id, + state=OobRecord.STATE_INITIAL, + ) with async_mock.patch.object( test_module, "OutOfBandManager", async_mock.MagicMock( return_value=async_mock.MagicMock( - receive_invitation=async_mock.CoroutineMock( - return_value=conn_record - ) + receive_invitation=async_mock.AsyncMock(return_value=oob_record) ) ), ) as mock_mgr, async_mock.patch.object( BaseStorage, "find_record", - async_mock.CoroutineMock( + async_mock.AsyncMock( return_value=async_mock.MagicMock(value=f"v{__version__}") ), ): + assert not conductor.root_profile.settings["mediation.connections_invite"] await conductor.start() await conductor.stop() mock_from_url.assert_called_once_with("test-invite") mock_mgr.return_value.receive_invitation.assert_called_once() - @asynctest.patch.object(test_module, "MediationInviteStore") - @asynctest.patch.object(test_module.ConnectionInvitation, "from_url") + @async_mock.patch.object(test_module, "MediationInviteStore") + @async_mock.patch.object(test_module.ConnectionInvitation, "from_url") async def test_mediation_invitation_should_use_stored_invitation( self, patched_from_url, patched_invite_store ): @@ -1055,29 +1448,35 @@ async def test_mediation_invitation_should_use_stored_invitation( conductor = test_module.Conductor( self.__get_mediator_config(invite_string, True) ) - await conductor.setup() + with async_mock.patch.object( + test_module, "OutboundTransportManager", autospec=True + ) as mock_outbound_mgr: + mock_outbound_mgr.return_value.registered_transports = { + "test": async_mock.MagicMock(schemes=["http"]) + } + await conductor.setup() mock_conn_record = async_mock.MagicMock() mocked_store = get_invite_store_mock(invite_string) patched_invite_store.return_value = mocked_store connection_manager_mock = async_mock.MagicMock( - receive_invitation=async_mock.CoroutineMock(return_value=mock_conn_record) + receive_invitation=async_mock.AsyncMock(return_value=mock_conn_record) ) mock_mediation_manager = async_mock.MagicMock( - clear_default_mediator=async_mock.CoroutineMock() + clear_default_mediator=async_mock.AsyncMock() ) # when with async_mock.patch.object( test_module, "ConnectionManager", return_value=connection_manager_mock ), async_mock.patch.object( - mock_conn_record, "metadata_set", async_mock.CoroutineMock() + mock_conn_record, "metadata_set", async_mock.AsyncMock() ), async_mock.patch.object( test_module, "MediationManager", return_value=mock_mediation_manager ), async_mock.patch.object( BaseStorage, "find_record", - async_mock.CoroutineMock( + async_mock.AsyncMock( return_value=async_mock.MagicMock(value=f"v{__version__}") ), ): @@ -1091,8 +1490,8 @@ async def test_mediation_invitation_should_use_stored_invitation( patched_from_url.assert_called_with(invite_string) mock_mediation_manager.clear_default_mediator.assert_called_once() - @asynctest.patch.object(test_module, "MediationInviteStore") - @asynctest.patch.object(test_module, "ConnectionManager") + @async_mock.patch.object(test_module, "MediationInviteStore") + @async_mock.patch.object(test_module, "ConnectionManager") async def test_mediation_invitation_should_not_create_connection_for_old_invitation( self, patched_connection_manager, patched_invite_store ): @@ -1102,19 +1501,25 @@ async def test_mediation_invitation_should_not_create_connection_for_old_invitat conductor = test_module.Conductor( self.__get_mediator_config(invite_string, True) ) - await conductor.setup() + with async_mock.patch.object( + test_module, "OutboundTransportManager", autospec=True + ) as mock_outbound_mgr: + mock_outbound_mgr.return_value.registered_transports = { + "test": async_mock.MagicMock(schemes=["http"]) + } + await conductor.setup() invite_store_mock = get_invite_store_mock(invite_string, True) patched_invite_store.return_value = invite_store_mock connection_manager_mock = async_mock.MagicMock( - receive_invitation=async_mock.CoroutineMock() + receive_invitation=async_mock.AsyncMock() ) patched_connection_manager.return_value = connection_manager_mock with async_mock.patch.object( BaseStorage, "find_record", - async_mock.CoroutineMock( + async_mock.AsyncMock( return_value=async_mock.MagicMock(value=f"v{__version__}") ), ): @@ -1128,7 +1533,7 @@ async def test_mediation_invitation_should_not_create_connection_for_old_invitat ) connection_manager_mock.receive_invitation.assert_not_called() - @asynctest.patch.object( + @async_mock.patch.object( test_module, "MediationInviteStore", return_value=get_invite_store_mock("test-invite"), @@ -1137,7 +1542,13 @@ async def test_mediator_invitation_x(self, _): conductor = test_module.Conductor( self.__get_mediator_config("test-invite", True) ) - await conductor.setup() + with async_mock.patch.object( + test_module, "OutboundTransportManager", autospec=True + ) as mock_outbound_mgr: + mock_outbound_mgr.return_value.registered_transports = { + "test": async_mock.MagicMock(schemes=["http"]) + } + await conductor.setup() with async_mock.patch.object( test_module.ConnectionInvitation, @@ -1148,7 +1559,7 @@ async def test_mediator_invitation_x(self, _): ) as mock_logger, async_mock.patch.object( BaseStorage, "find_record", - async_mock.CoroutineMock( + async_mock.AsyncMock( return_value=async_mock.MagicMock(value=f"v{__version__}") ), ): @@ -1166,10 +1577,15 @@ async def test_setup_ledger_both_multiple_and_base(self): with async_mock.patch.object( test_module, "load_multiple_genesis_transactions_from_config", - async_mock.CoroutineMock(), + async_mock.AsyncMock(), ) as mock_multiple_genesis_load, async_mock.patch.object( - test_module, "get_genesis_transactions", async_mock.CoroutineMock() - ) as mock_genesis_load: + test_module, "get_genesis_transactions", async_mock.AsyncMock() + ) as mock_genesis_load, async_mock.patch.object( + test_module, "OutboundTransportManager", autospec=True + ) as mock_outbound_mgr: + mock_outbound_mgr.return_value.registered_transports = { + "test": async_mock.MagicMock(schemes=["http"]) + } await conductor.setup() mock_multiple_genesis_load.assert_called_once() mock_genesis_load.assert_called_once() @@ -1180,47 +1596,15 @@ async def test_setup_ledger_only_base(self): conductor = test_module.Conductor(builder) with async_mock.patch.object( - test_module, "get_genesis_transactions", async_mock.CoroutineMock() - ) as mock_genesis_load: - await conductor.setup() - mock_genesis_load.assert_called_once() - - async def test_startup_x_version_mismatch(self): - builder: ContextBuilder = StubContextBuilder(self.test_settings) - conductor = test_module.Conductor(builder) - - with async_mock.patch.object( - test_module, "InboundTransportManager", autospec=True - ) as mock_inbound_mgr, async_mock.patch.object( + test_module, "get_genesis_transactions", async_mock.AsyncMock() + ) as mock_genesis_load, async_mock.patch.object( test_module, "OutboundTransportManager", autospec=True - ) as mock_outbound_mgr, async_mock.patch.object( - test_module, "LOGGER" - ) as mock_logger, async_mock.patch.object( - BaseStorage, - "find_record", - async_mock.CoroutineMock( - return_value=async_mock.MagicMock(value=f"v0.6.0") - ), - ): - + ) as mock_outbound_mgr: + mock_outbound_mgr.return_value.registered_transports = { + "test": async_mock.MagicMock(schemes=["http"]) + } await conductor.setup() - - session = await conductor.root_profile.session() - - wallet = session.inject(BaseWallet) - await wallet.create_public_did( - DIDMethod.SOV, - KeyType.ED25519, - ) - - mock_inbound_mgr.return_value.setup.assert_awaited_once() - mock_outbound_mgr.return_value.setup.assert_awaited_once() - - mock_inbound_mgr.return_value.registered_transports = {} - mock_outbound_mgr.return_value.registered_transports = {} - with self.assertRaises(RuntimeError): - await conductor.start() - mock_logger.exception.assert_called_once() + mock_genesis_load.assert_called_once() async def test_startup_x_no_storage_version(self): builder: ContextBuilder = StubContextBuilder(self.test_settings) @@ -1235,17 +1619,23 @@ async def test_startup_x_no_storage_version(self): ) as mock_logger, async_mock.patch.object( BaseStorage, "find_record", - async_mock.CoroutineMock(side_effect=StorageNotFoundError()), + async_mock.AsyncMock(side_effect=StorageNotFoundError()), + ), async_mock.patch.object( + test_module, + "upgrade", + async_mock.AsyncMock(), ): - + mock_outbound_mgr.return_value.registered_transports = { + "test": async_mock.MagicMock(schemes=["http"]) + } await conductor.setup() session = await conductor.root_profile.session() wallet = session.inject(BaseWallet) await wallet.create_public_did( - DIDMethod.SOV, - KeyType.ED25519, + SOV, + ED25519, ) mock_inbound_mgr.return_value.setup.assert_awaited_once() diff --git a/aries_cloudagent/core/tests/test_dispatcher.py b/aries_cloudagent/core/tests/test_dispatcher.py index 3b5e245367..450bfb4767 100644 --- a/aries_cloudagent/core/tests/test_dispatcher.py +++ b/aries_cloudagent/core/tests/test_dispatcher.py @@ -1,10 +1,13 @@ import json +from async_case import IsolatedAsyncioTestCase +import mock as async_mock import pytest -from asynctest import TestCase as AsyncTestCase, mock as async_mock from marshmallow import EXCLUDE +from ...cache.base import BaseCache +from ...cache.in_memory import InMemoryCache from ...config.injection_context import InjectionContext from ...core.event_bus import EventBus from ...core.in_memory import InMemoryProfile @@ -18,6 +21,7 @@ V20CredProblemReport, ) from ...protocols.problem_report.v1_0.message import ProblemReport +from ...protocols.coordinate_mediation.v1_0.route_manager import RouteManager from ...transport.inbound.message import InboundMessage from ...transport.inbound.receipt import MessageReceipt from ...transport.outbound.message import OutboundMessage @@ -31,6 +35,7 @@ def make_profile() -> Profile: profile.context.injector.bind_instance(ProtocolRegistry, ProtocolRegistry()) profile.context.injector.bind_instance(Collector, Collector()) profile.context.injector.bind_instance(EventBus, EventBus()) + profile.context.injector.bind_instance(RouteManager, async_mock.MagicMock()) return profile @@ -87,7 +92,7 @@ async def handle(self, context, responder): pass -class TestDispatcher(AsyncTestCase): +class TestDispatcher(IsolatedAsyncioTestCase): async def test_dispatch(self): profile = make_profile() registry = profile.inject(ProtocolRegistry) @@ -108,9 +113,17 @@ async def test_dispatch(self): StubAgentMessageHandler, "handle", autospec=True ) as handler_mock, async_mock.patch.object( test_module, "ConnectionManager", autospec=True - ) as conn_mgr_mock: + ) as conn_mgr_mock, async_mock.patch.object( + test_module, + "get_version_from_message_type", + async_mock.AsyncMock(return_value="1.1"), + ), async_mock.patch.object( + test_module, + "validate_get_response_version", + async_mock.AsyncMock(return_value=("1.1", None)), + ): conn_mgr_mock.return_value = async_mock.MagicMock( - find_inbound_connection=async_mock.CoroutineMock( + find_inbound_connection=async_mock.AsyncMock( return_value=async_mock.MagicMock(connection_id="dummy") ) ) @@ -149,7 +162,15 @@ async def test_dispatch_versioned_message(self): with async_mock.patch.object( StubAgentMessageHandler, "handle", autospec=True - ) as handler_mock: + ) as handler_mock, async_mock.patch.object( + test_module, + "get_version_from_message_type", + async_mock.AsyncMock(return_value="1.1"), + ), async_mock.patch.object( + test_module, + "validate_get_response_version", + async_mock.AsyncMock(return_value=("1.1", None)), + ): await dispatcher.queue_message( dispatcher.profile, make_inbound(message), rcv.send ) @@ -262,7 +283,15 @@ async def test_dispatch_versioned_message_handle_greater_succeeds(self): with async_mock.patch.object( StubAgentMessageHandler, "handle", autospec=True - ) as handler_mock: + ) as handler_mock, async_mock.patch.object( + test_module, + "get_version_from_message_type", + async_mock.AsyncMock(return_value="1.1"), + ), async_mock.patch.object( + test_module, + "validate_get_response_version", + async_mock.AsyncMock(return_value=("1.1", None)), + ): await dispatcher.queue_message( dispatcher.profile, make_inbound(message), rcv.send ) @@ -314,17 +343,22 @@ async def test_bad_message_dispatch_parse_x(self): await dispatcher.setup() rcv = Receiver() bad_messages = ["not even a dict", {"bad": "message"}] - for bad in bad_messages: - await dispatcher.queue_message( - dispatcher.profile, make_inbound(bad), rcv.send - ) - await dispatcher.task_queue - assert rcv.messages and isinstance(rcv.messages[0][1], OutboundMessage) - payload = json.loads(rcv.messages[0][1].payload) - assert payload["@type"] == DIDCommPrefix.qualify_current( - ProblemReport.Meta.message_type - ) - rcv.messages.clear() + with async_mock.patch.object( + test_module, "get_version_from_message_type", async_mock.AsyncMock() + ), async_mock.patch.object( + test_module, "validate_get_response_version", async_mock.AsyncMock() + ): + for bad in bad_messages: + await dispatcher.queue_message( + dispatcher.profile, make_inbound(bad), rcv.send + ) + await dispatcher.task_queue + assert rcv.messages and isinstance(rcv.messages[0][1], OutboundMessage) + payload = json.loads(rcv.messages[0][1].payload) + assert payload["@type"] == DIDCommPrefix.qualify_current( + ProblemReport.Meta.message_type + ) + rcv.messages.clear() async def test_bad_message_dispatch_problem_report_x(self): profile = make_profile() @@ -362,8 +396,9 @@ async def test_dispatch_log(self): dispatcher = test_module.Dispatcher(profile) await dispatcher.setup() + exc = KeyError("sample exception") mock_task = async_mock.MagicMock( - exc_info=(KeyError, KeyError("sample exception"), "..."), + exc_info=(type(exc), exc, exc.__traceback__), ident="abc", timing={ "queued": 1234567890, @@ -380,12 +415,85 @@ async def test_create_send_outbound(self): profile, settings={"timing.enabled": True}, ) + registry = profile.inject(ProtocolRegistry) + registry.register_message_types( + { + pfx.qualify(StubAgentMessage.Meta.message_type): StubAgentMessage + for pfx in DIDCommPrefix + } + ) message = StubAgentMessage() responder = test_module.DispatcherResponder(context, message, None) - outbound_message = await responder.create_outbound(message) - with async_mock.patch.object(responder, "_send", async_mock.CoroutineMock()): + outbound_message = await responder.create_outbound( + json.dumps(message.serialize()) + ) + with async_mock.patch.object( + responder, "_send", async_mock.AsyncMock() + ), async_mock.patch.object( + test_module.BaseResponder, + "conn_rec_active_state_check", + async_mock.AsyncMock(return_value=True), + ): await responder.send_outbound(outbound_message) + async def test_create_send_outbound_with_msg_attrs(self): + profile = make_profile() + context = RequestContext( + profile, + settings={"timing.enabled": True}, + ) + registry = profile.inject(ProtocolRegistry) + registry.register_message_types( + { + pfx.qualify(StubAgentMessage.Meta.message_type): StubAgentMessage + for pfx in DIDCommPrefix + } + ) + message = StubAgentMessage() + responder = test_module.DispatcherResponder(context, message, None) + outbound_message = await responder.create_outbound(message) + with async_mock.patch.object( + responder, "_send", async_mock.AsyncMock() + ), async_mock.patch.object( + test_module.BaseResponder, + "conn_rec_active_state_check", + async_mock.AsyncMock(return_value=True), + ): + await responder.send_outbound( + message=outbound_message, + message_type=message._message_type, + message_id=message._id, + ) + + async def test_create_send_outbound_with_msg_attrs_x(self): + profile = make_profile() + context = RequestContext( + profile, + settings={"timing.enabled": True}, + ) + registry = profile.inject(ProtocolRegistry) + registry.register_message_types( + { + pfx.qualify(StubAgentMessage.Meta.message_type): StubAgentMessage + for pfx in DIDCommPrefix + } + ) + message = StubAgentMessage() + responder = test_module.DispatcherResponder(context, message, None) + outbound_message = await responder.create_outbound(message) + outbound_message.connection_id = "123" + with async_mock.patch.object( + test_module.BaseResponder, + "conn_rec_active_state_check", + async_mock.AsyncMock(return_value=False), + ): + with self.assertRaises(RuntimeError): + await responder.send_outbound( + message=outbound_message, + message_type=message._message_type, + message_id=message._id, + ) + async def test_create_send_webhook(self): profile = make_profile() context = RequestContext(profile) @@ -394,13 +502,183 @@ async def test_create_send_webhook(self): with pytest.deprecated_call(): await responder.send_webhook("topic", {"pay": "load"}) + async def test_conn_rec_active_state_check_a(self): + profile = make_profile() + profile.context.injector.bind_instance(BaseCache, InMemoryCache()) + context = RequestContext(profile) + message = StubAgentMessage() + responder = test_module.DispatcherResponder(context, message, None) + with async_mock.patch.object( + test_module.ConnRecord, "retrieve_by_id", async_mock.AsyncMock() + ) as mock_conn_ret_by_id: + conn_rec = test_module.ConnRecord() + conn_rec.state = test_module.ConnRecord.State.COMPLETED + mock_conn_ret_by_id.return_value = conn_rec + check_flag = await responder.conn_rec_active_state_check( + profile, + "conn-id", + ) + assert check_flag + check_flag = await responder.conn_rec_active_state_check( + profile, + "conn-id", + ) + assert check_flag + + async def test_conn_rec_active_state_check_b(self): + profile = make_profile() + profile.context.injector.bind_instance(BaseCache, InMemoryCache()) + profile.context.injector.bind_instance( + EventBus, async_mock.MagicMock(notify=async_mock.AsyncMock()) + ) + context = RequestContext(profile) + message = StubAgentMessage() + responder = test_module.DispatcherResponder(context, message, None) + with async_mock.patch.object( + test_module.ConnRecord, "retrieve_by_id", async_mock.AsyncMock() + ) as mock_conn_ret_by_id: + conn_rec_a = test_module.ConnRecord() + conn_rec_a.state = test_module.ConnRecord.State.REQUEST + conn_rec_b = test_module.ConnRecord() + conn_rec_b.state = test_module.ConnRecord.State.COMPLETED + mock_conn_ret_by_id.side_effect = [conn_rec_a, conn_rec_b] + check_flag = await responder.conn_rec_active_state_check( + profile, + "conn-id", + ) + assert check_flag + async def test_create_enc_outbound(self): profile = make_profile() context = RequestContext(profile) - message = b"abc123xyz7890000" + message = StubAgentMessage() responder = test_module.DispatcherResponder(context, message, None) with async_mock.patch.object( - responder, "send_outbound", async_mock.CoroutineMock() + responder, "send_outbound", async_mock.AsyncMock() ) as mock_send_outbound: await responder.send(message) assert mock_send_outbound.called_once() + msg_json = json.dumps(StubAgentMessage().serialize()) + message = msg_json.encode("utf-8") + with async_mock.patch.object( + responder, "send_outbound", async_mock.AsyncMock() + ) as mock_send_outbound: + await responder.send(message) + + message = StubAgentMessage() + with async_mock.patch.object( + responder, "send_outbound", async_mock.AsyncMock() + ) as mock_send_outbound: + await responder.send_reply(message) + assert mock_send_outbound.called_once() + + message = json.dumps(StubAgentMessage().serialize()) + with async_mock.patch.object( + responder, "send_outbound", async_mock.AsyncMock() + ) as mock_send_outbound: + await responder.send_reply(message) + + async def test_expired_context_x(self): + def _smaller_scope(): + profile = make_profile() + context = RequestContext(profile) + message = b"abc123xyz7890000" + return test_module.DispatcherResponder(context, message, None) + + responder = _smaller_scope() + with self.assertRaises(RuntimeError): + await responder.create_outbound(b"test") + + with self.assertRaises(RuntimeError): + await responder.send_outbound(None) + + with self.assertRaises(RuntimeError): + await responder.send_webhook("test", {}) + + # async def test_dispatch_version_with_degraded_features(self): + # profile = make_profile() + # registry = profile.inject(ProtocolRegistry) + # registry.register_message_types( + # { + # pfx.qualify(StubAgentMessage.Meta.message_type): StubAgentMessage + # for pfx in DIDCommPrefix + # } + # ) + # dispatcher = test_module.Dispatcher(profile) + # await dispatcher.setup() + # rcv = Receiver() + # message = { + # "@type": DIDCommPrefix.qualify_current(StubAgentMessage.Meta.message_type) + # } + + # with async_mock.patch.object( + # test_module, + # "get_version_from_message_type", + # async_mock.AsyncMock(return_value="1.1"), + # ), async_mock.patch.object( + # test_module, + # "validate_get_response_version", + # async_mock.AsyncMock(return_value=("1.1", "fields-ignored-due-to-version-mismatch")), + # ): + # await dispatcher.queue_message( + # dispatcher.profile, make_inbound(message), rcv.send + # ) + + # async def test_dispatch_fields_ignored_due_to_version_mismatch(self): + # profile = make_profile() + # registry = profile.inject(ProtocolRegistry) + # registry.register_message_types( + # { + # pfx.qualify(StubAgentMessage.Meta.message_type): StubAgentMessage + # for pfx in DIDCommPrefix + # } + # ) + # dispatcher = test_module.Dispatcher(profile) + # await dispatcher.setup() + # rcv = Receiver() + # message = { + # "@type": DIDCommPrefix.qualify_current(StubAgentMessage.Meta.message_type) + # } + + # with async_mock.patch.object( + # test_module, + # "get_version_from_message_type", + # async_mock.AsyncMock(return_value="1.1"), + # ), async_mock.patch.object( + # test_module, + # "validate_get_response_version", + # async_mock.AsyncMock(return_value=("1.1", "version-with-degraded-features")), + # ): + # await dispatcher.queue_message( + # dispatcher.profile, make_inbound(message), rcv.send + # ) + + # async def test_dispatch_version_not_supported(self): + # profile = make_profile() + # registry = profile.inject(ProtocolRegistry) + # registry.register_message_types( + # { + # pfx.qualify(StubAgentMessage.Meta.message_type): StubAgentMessage + # for pfx in DIDCommPrefix + # } + # ) + # dispatcher = test_module.Dispatcher(profile) + # await dispatcher.setup() + # rcv = Receiver() + # message = { + # "@type": DIDCommPrefix.qualify_current(StubAgentMessage.Meta.message_type) + # } + + # with async_mock.patch.object( + # test_module, + # "get_version_from_message_type", + # async_mock.AsyncMock(return_value="1.1"), + # ), async_mock.patch.object( + # test_module, + # "validate_get_response_version", + # async_mock.AsyncMock(return_value=("1.1", "version-not-supported")), + # ): + # with self.assertRaises(test_module.MessageParseError): + # await dispatcher.queue_message( + # dispatcher.profile, make_inbound(message), rcv.send + # ) diff --git a/aries_cloudagent/core/tests/test_oob_processor.py b/aries_cloudagent/core/tests/test_oob_processor.py new file mode 100644 index 0000000000..7c73a946ad --- /dev/null +++ b/aries_cloudagent/core/tests/test_oob_processor.py @@ -0,0 +1,763 @@ +import json + +from asynctest import ANY +from asynctest import TestCase as AsyncTestCase +from asynctest import mock as async_mock + +from ...connections.models.conn_record import ConnRecord +from ...messaging.decorators.attach_decorator import AttachDecorator +from ...messaging.decorators.service_decorator import ServiceDecorator +from ...messaging.request_context import RequestContext +from ...protocols.connections.v1_0.messages.connection_invitation import ( + ConnectionInvitation, +) +from ...protocols.out_of_band.v1_0.messages.invitation import InvitationMessage +from ...protocols.out_of_band.v1_0.models.oob_record import OobRecord +from ...storage.error import StorageNotFoundError +from ...transport.inbound.receipt import MessageReceipt +from ...transport.outbound.message import OutboundMessage +from ..in_memory.profile import InMemoryProfile +from ..oob_processor import OobMessageProcessor, OobMessageProcessorError + + +class TestOobProcessor(AsyncTestCase): + async def setUp(self): + self.profile = InMemoryProfile.test_profile() + self.inbound_message_router = async_mock.CoroutineMock() + self.oob_processor = OobMessageProcessor( + inbound_message_router=self.inbound_message_router + ) + + self.oob_record = async_mock.MagicMock( + connection_id="a-connection-id", + attach_thread_id="the-thid", + their_service={ + "recipientKeys": ["9WCgWKUaAJj3VWxxtzvvMQN3AoFxoBtBDo9ntwJnVVCC"], + "routingKeys": ["6QSduYdf8Bi6t8PfNm5vNomGWDtXhmMmTRzaciudBXYJ"], + "serviceEndpoint": "http://their-service-endpoint.com", + }, + emit_event=async_mock.CoroutineMock(), + delete_record=async_mock.CoroutineMock(), + save=async_mock.CoroutineMock(), + ) + self.context = RequestContext.test_context() + self.context.message = ConnectionInvitation() + + async def test_clean_finished_oob_record_no_multi_use_no_request_attach(self): + test_message = InvitationMessage() + test_message.assign_thread_id("the-thid", "the-pthid") + + mock_oob = async_mock.MagicMock( + emit_event=async_mock.CoroutineMock(), + delete_record=async_mock.CoroutineMock(), + multi_use=False, + invitation=async_mock.MagicMock(requests_attach=[]), + ) + + with async_mock.patch.object( + OobRecord, + "retrieve_by_tag_filter", + async_mock.CoroutineMock(return_value=mock_oob), + ) as mock_retrieve_oob: + await self.oob_processor.clean_finished_oob_record( + self.profile, test_message + ) + + assert mock_oob.state == OobRecord.STATE_DONE + mock_oob.emit_event.assert_called_once() + mock_oob.delete_record.assert_called_once() + + mock_retrieve_oob.assert_called_once_with( + ANY, {"invi_msg_id": "the-pthid"}, {"role": OobRecord.ROLE_SENDER} + ) + + async def test_clean_finished_oob_record_multi_use(self): + test_message = InvitationMessage() + test_message.assign_thread_id("the-thid", "the-pthid") + + mock_oob = async_mock.MagicMock( + emit_event=async_mock.CoroutineMock(), + delete_record=async_mock.CoroutineMock(), + multi_use=True, + invitation=async_mock.MagicMock(requests_attach=[]), + ) + + with async_mock.patch.object( + OobRecord, + "retrieve_by_tag_filter", + async_mock.CoroutineMock(return_value=mock_oob), + ) as mock_retrieve_oob: + await self.oob_processor.clean_finished_oob_record( + self.profile, test_message + ) + + mock_oob.emit_event.assert_not_called() + mock_oob.delete_record.assert_not_called() + + mock_retrieve_oob.assert_called_once_with( + ANY, {"invi_msg_id": "the-pthid"}, {"role": OobRecord.ROLE_SENDER} + ) + + async def test_clean_finished_oob_record_x(self): + test_message = InvitationMessage() + test_message.assign_thread_id("the-thid", "the-pthid") + + with async_mock.patch.object( + OobRecord, + "retrieve_by_tag_filter", + async_mock.CoroutineMock(), + ) as mock_retrieve_oob: + mock_retrieve_oob.side_effect = (StorageNotFoundError(),) + + await self.oob_processor.clean_finished_oob_record( + self.profile, test_message + ) + + async def test_find_oob_target_for_outbound_message(self): + mock_oob = async_mock.MagicMock( + emit_event=async_mock.CoroutineMock(), + delete_record=async_mock.CoroutineMock(), + multi_use=True, + invitation=async_mock.MagicMock(requests_attach=[]), + invi_msg_id="the-pthid", + our_recipient_key="3Dn1SJNPaCXcvvJvSbsFWP2xaCjMom3can8CQNhWrTRx", + their_service={ + "recipientKeys": ["9WCgWKUaAJj3VWxxtzvvMQN3AoFxoBtBDo9ntwJnVVCC"], + "serviceEndpoint": "http://their-service-endpoint.com", + "routingKeys": ["6QSduYdf8Bi6t8PfNm5vNomGWDtXhmMmTRzaciudBXYJ"], + }, + our_service={ + "recipientKeys": ["3Dn1SJNPaCXcvvJvSbsFWP2xaCjMom3can8CQNhWrTRx"], + "serviceEndpoint": "http://our-service-endpoint.com", + "routingKeys": [], + }, + ) + + message = json.dumps({"~thread": {"thid": "the-thid"}}) + outbound = OutboundMessage(reply_thread_id="the-thid", payload=message) + + with async_mock.patch.object( + OobRecord, + "retrieve_by_tag_filter", + async_mock.CoroutineMock(return_value=mock_oob), + ) as mock_retrieve_oob: + target = await self.oob_processor.find_oob_target_for_outbound_message( + self.profile, outbound + ) + + assert target + assert target.endpoint == "http://their-service-endpoint.com" + assert target.recipient_keys == [ + "9WCgWKUaAJj3VWxxtzvvMQN3AoFxoBtBDo9ntwJnVVCC" + ] + assert target.routing_keys == [ + "6QSduYdf8Bi6t8PfNm5vNomGWDtXhmMmTRzaciudBXYJ" + ] + assert target.sender_key == "3Dn1SJNPaCXcvvJvSbsFWP2xaCjMom3can8CQNhWrTRx" + + payload = json.loads(outbound.payload) + + assert payload == { + "~thread": {"thid": "the-thid", "pthid": "the-pthid"}, + "~service": { + "recipientKeys": ["3Dn1SJNPaCXcvvJvSbsFWP2xaCjMom3can8CQNhWrTRx"], + "serviceEndpoint": "http://our-service-endpoint.com", + "routingKeys": [], + }, + } + + mock_retrieve_oob.assert_called_once_with( + ANY, {"attach_thread_id": "the-thid"} + ) + + async def test_find_oob_target_for_outbound_message_oob_not_found(self): + message = json.dumps({}) + outbound = OutboundMessage(reply_thread_id="the-thid", payload=message) + + with async_mock.patch.object( + OobRecord, + "retrieve_by_tag_filter", + async_mock.CoroutineMock(side_effect=(StorageNotFoundError(),)), + ) as mock_retrieve_oob: + target = await self.oob_processor.find_oob_target_for_outbound_message( + self.profile, outbound + ) + + assert not target + mock_retrieve_oob.assert_called_once_with( + ANY, {"attach_thread_id": "the-thid"} + ) + + async def test_find_oob_target_for_outbound_message_update_service_thread(self): + mock_oob = async_mock.MagicMock( + emit_event=async_mock.CoroutineMock(), + delete_record=async_mock.CoroutineMock(), + multi_use=True, + invitation=async_mock.MagicMock(requests_attach=[]), + invi_msg_id="the-pthid", + our_recipient_key="3Dn1SJNPaCXcvvJvSbsFWP2xaCjMom3can8CQNhWrTRx", + their_service={ + "recipientKeys": ["9WCgWKUaAJj3VWxxtzvvMQN3AoFxoBtBDo9ntwJnVVCC"], + "serviceEndpoint": "http://their-service-endpoint.com", + "routingKeys": ["6QSduYdf8Bi6t8PfNm5vNomGWDtXhmMmTRzaciudBXYJ"], + }, + our_service={ + "recipientKeys": ["3Dn1SJNPaCXcvvJvSbsFWP2xaCjMom3can8CQNhWrTRx"], + "serviceEndpoint": "http://our-service-endpoint.com", + "routingKeys": [], + }, + ) + + with async_mock.patch.object( + OobRecord, + "retrieve_by_tag_filter", + async_mock.CoroutineMock(return_value=mock_oob), + ): + message = json.dumps({}) + outbound = OutboundMessage(reply_thread_id="the-thid", payload=message) + await self.oob_processor.find_oob_target_for_outbound_message( + self.profile, outbound + ) + payload = json.loads(outbound.payload) + + assert payload == { + "~thread": {"pthid": "the-pthid"}, + "~service": { + "recipientKeys": ["3Dn1SJNPaCXcvvJvSbsFWP2xaCjMom3can8CQNhWrTRx"], + "serviceEndpoint": "http://our-service-endpoint.com", + "routingKeys": [], + }, + } + + message = json.dumps( + { + "~service": {"already": "present"}, + } + ) + outbound = OutboundMessage(reply_thread_id="the-thid", payload=message) + await self.oob_processor.find_oob_target_for_outbound_message( + self.profile, outbound + ) + payload = json.loads(outbound.payload) + + assert payload == { + "~thread": {"pthid": "the-pthid"}, + "~service": {"already": "present"}, + } + + async def test_find_oob_record_for_inbound_message_parent_thread_id(self): + # With pthid + with async_mock.patch.object( + OobRecord, + "retrieve_by_tag_filter", + async_mock.CoroutineMock(return_value=self.oob_record), + ) as mock_retrieve: + self.context.message_receipt = MessageReceipt( + thread_id="the-thid", parent_thread_id="the-pthid" + ) + + assert await self.oob_processor.find_oob_record_for_inbound_message( + self.context + ) + mock_retrieve.assert_called_once() + mock_retrieve.assert_called_once_with(ANY, {"invi_msg_id": "the-pthid"}) + + # With pthid, throws error + with async_mock.patch.object( + OobRecord, + "retrieve_by_tag_filter", + async_mock.CoroutineMock(side_effect=(StorageNotFoundError(),)), + ) as mock_retrieve: + self.context.message_receipt = MessageReceipt( + thread_id="the-thid", parent_thread_id="the-pthid" + ) + + assert not await self.oob_processor.find_oob_record_for_inbound_message( + self.context + ) + mock_retrieve.assert_called_once() + mock_retrieve.assert_called_once_with(ANY, {"invi_msg_id": "the-pthid"}) + + # Without pthid + with async_mock.patch.object( + OobRecord, + "retrieve_by_tag_filter", + async_mock.CoroutineMock(), + ) as mock_retrieve: + self.context.message_receipt = MessageReceipt() + + assert not await self.oob_processor.find_oob_record_for_inbound_message( + self.context + ) + mock_retrieve.assert_not_called() + + async def test_find_oob_record_for_inbound_message_connectionless_retrieve_oob( + self, + ): + # With thread_id and recipient_verkey + with async_mock.patch.object( + OobRecord, + "retrieve_by_tag_filter", + async_mock.CoroutineMock(return_value=self.oob_record), + ) as mock_retrieve: + self.context.message_receipt = MessageReceipt( + thread_id="the-thid", + recipient_verkey="our-recipient-key", + sender_verkey=self.oob_record.their_service["recipientKeys"][0], + ) + + assert await self.oob_processor.find_oob_record_for_inbound_message( + self.context + ) + mock_retrieve.assert_called_once() + mock_retrieve.assert_called_once_with( + ANY, + { + "attach_thread_id": "the-thid", + "our_recipient_key": "our-recipient-key", + }, + ) + + # With thread_id and recipient_verkey, throws error + with async_mock.patch.object( + OobRecord, + "retrieve_by_tag_filter", + async_mock.CoroutineMock(side_effect=(StorageNotFoundError(),)), + ) as mock_retrieve: + self.context.message_receipt = MessageReceipt( + thread_id="the-thid", recipient_verkey="our-recipient-key" + ) + + assert not await self.oob_processor.find_oob_record_for_inbound_message( + self.context + ) + mock_retrieve.assert_called_once() + mock_retrieve.assert_called_once_with( + ANY, + { + "attach_thread_id": "the-thid", + "our_recipient_key": "our-recipient-key", + }, + ) + + # With connection + with async_mock.patch.object( + OobRecord, + "retrieve_by_tag_filter", + async_mock.CoroutineMock(return_value=None), + ) as mock_retrieve: + self.context.message_receipt = MessageReceipt( + thread_id="the-thid", recipient_verkey="our-recipient-key" + ) + self.context.connection_record = async_mock.MagicMock() + + assert not await self.oob_processor.find_oob_record_for_inbound_message( + self.context + ) + mock_retrieve.assert_not_called() + + # Without thread_id and recipient_verkey + with async_mock.patch.object( + OobRecord, + "retrieve_by_tag_filter", + async_mock.CoroutineMock(return_value=None), + ) as mock_retrieve: + self.context.message_receipt = MessageReceipt() + + assert not await self.oob_processor.find_oob_record_for_inbound_message( + self.context + ) + mock_retrieve.assert_not_called() + + async def test_find_oob_record_for_inbound_message_sender_connection_id_no_match( + self, + ): + with async_mock.patch.object( + OobRecord, + "retrieve_by_tag_filter", + async_mock.CoroutineMock(return_value=self.oob_record), + ) as mock_retrieve: + self.oob_record.role = OobRecord.ROLE_SENDER + self.oob_record.state = OobRecord.STATE_AWAIT_RESPONSE + self.context.connection_record = async_mock.MagicMock( + connection_id="a-connection-id" + ) + self.context.message_receipt = MessageReceipt( + thread_id="the-thid", parent_thread_id="the-pthid" + ) + + assert await self.oob_processor.find_oob_record_for_inbound_message( + self.context + ) + mock_retrieve.assert_called_once() + mock_retrieve.assert_called_once_with(ANY, {"invi_msg_id": "the-pthid"}) + + # Connection id is different + with async_mock.patch.object( + OobRecord, + "retrieve_by_tag_filter", + async_mock.CoroutineMock(return_value=self.oob_record), + ) as mock_retrieve: + self.oob_record.role = OobRecord.ROLE_SENDER + self.oob_record.state = OobRecord.STATE_ACCEPTED + self.context.connection_record = async_mock.MagicMock( + connection_id="another-connection-id" + ) + self.context.message_receipt = MessageReceipt( + thread_id="the-thid", parent_thread_id="the-pthid" + ) + + assert not await self.oob_processor.find_oob_record_for_inbound_message( + self.context + ) + mock_retrieve.assert_called_once() + mock_retrieve.assert_called_once_with(ANY, {"invi_msg_id": "the-pthid"}) + + # Connection id is not the same, state is not await response + with async_mock.patch.object( + OobRecord, + "retrieve_by_tag_filter", + async_mock.CoroutineMock(return_value=self.oob_record), + ) as mock_retrieve: + self.oob_record.role = OobRecord.ROLE_SENDER + self.oob_record.state = OobRecord.STATE_ACCEPTED + self.context.connection_record = async_mock.MagicMock( + connection_id="another-connection-id" + ) + self.context.message_receipt = MessageReceipt( + thread_id="the-thid", parent_thread_id="the-pthid" + ) + + assert not await self.oob_processor.find_oob_record_for_inbound_message( + self.context + ) + mock_retrieve.assert_called_once() + mock_retrieve.assert_called_once_with(ANY, {"invi_msg_id": "the-pthid"}) + + # Connection id is not the same, state is AWAIT_RESPONSE. oob has connection_id + with async_mock.patch.object( + OobRecord, + "retrieve_by_tag_filter", + async_mock.CoroutineMock(return_value=self.oob_record), + ) as mock_retrieve, async_mock.patch.object( + ConnRecord, + "retrieve_by_id", + async_mock.CoroutineMock( + return_value=async_mock.MagicMock( + delete_record=async_mock.CoroutineMock() + ) + ), + ) as mock_retrieve_conn: + self.oob_record.role = OobRecord.ROLE_SENDER + self.oob_record.state = OobRecord.STATE_AWAIT_RESPONSE + self.context.connection_record = async_mock.MagicMock( + connection_id="another-connection-id" + ) + self.context.message_receipt = MessageReceipt( + thread_id="the-thid", parent_thread_id="the-pthid" + ) + + assert await self.oob_processor.find_oob_record_for_inbound_message( + self.context + ) + mock_retrieve.assert_called_once() + mock_retrieve.assert_called_once_with(ANY, {"invi_msg_id": "the-pthid"}) + mock_retrieve_conn.assert_called_once_with(ANY, "a-connection-id") + mock_retrieve_conn.return_value.delete_record.assert_called_once() + + assert self.oob_record.connection_id == "another-connection-id" + + async def test_find_oob_record_for_inbound_message_attach_thread_id_set(self): + # No attach thread_id + self.oob_record.attach_thread_id = None + + self.oob_record.invitation.requests_attach = [ + AttachDecorator.data_json({"@id": "the-thid"}) + ] + + with async_mock.patch.object( + OobRecord, + "retrieve_by_tag_filter", + async_mock.CoroutineMock(return_value=self.oob_record), + ) as mock_retrieve: + self.context.message_receipt = MessageReceipt( + thread_id="the-thid", parent_thread_id="the-pthid" + ) + + assert await self.oob_processor.find_oob_record_for_inbound_message( + self.context + ) + mock_retrieve.assert_called_once() + mock_retrieve.assert_called_once_with(ANY, {"invi_msg_id": "the-pthid"}) + + assert self.oob_record.attach_thread_id == "the-thid" + + async def test_find_oob_record_for_inbound_message_attach_thread_id_not_in_list( + self, + ): + # No attach thread_id + self.oob_record.attach_thread_id = None + + self.oob_record.invitation.requests_attach = [ + AttachDecorator.data_json({"@id": "another-thid"}) + ] + + with async_mock.patch.object( + OobRecord, + "retrieve_by_tag_filter", + async_mock.CoroutineMock(return_value=self.oob_record), + ) as mock_retrieve: + self.context.message_receipt = MessageReceipt( + thread_id="the-thid", parent_thread_id="the-pthid" + ) + + assert not await self.oob_processor.find_oob_record_for_inbound_message( + self.context + ) + mock_retrieve.assert_called_once() + mock_retrieve.assert_called_once_with(ANY, {"invi_msg_id": "the-pthid"}) + + async def test_find_oob_record_for_inbound_message_not_attach_thread_id_matching( + self, + ): + with async_mock.patch.object( + OobRecord, + "retrieve_by_tag_filter", + async_mock.CoroutineMock(return_value=self.oob_record), + ) as mock_retrieve: + self.context.message_receipt = MessageReceipt( + thread_id="the-thid", parent_thread_id="the-pthid" + ) + + assert await self.oob_processor.find_oob_record_for_inbound_message( + self.context + ) + mock_retrieve.assert_called_once() + mock_retrieve.assert_called_once_with(ANY, {"invi_msg_id": "the-pthid"}) + + async def test_find_oob_record_for_inbound_message_not_attach_thread_id_not_matching( + self, + ): + self.oob_record.attach_thread_id = "another-thid" + + with async_mock.patch.object( + OobRecord, + "retrieve_by_tag_filter", + async_mock.CoroutineMock(return_value=self.oob_record), + ) as mock_retrieve: + self.context.message_receipt = MessageReceipt( + thread_id="the-thid", parent_thread_id="the-pthid" + ) + + assert not await self.oob_processor.find_oob_record_for_inbound_message( + self.context + ) + mock_retrieve.assert_called_once() + mock_retrieve.assert_called_once_with(ANY, {"invi_msg_id": "the-pthid"}) + + async def test_find_oob_record_for_inbound_message_recipient_verkey_not_in_their_service( + self, + ): + with async_mock.patch.object( + OobRecord, + "retrieve_by_tag_filter", + async_mock.CoroutineMock(return_value=self.oob_record), + ) as mock_retrieve: + self.context.message_receipt = MessageReceipt( + thread_id="the-thid", + parent_thread_id="the-pthid", + recipient_verkey="recipient-verkey", + sender_verkey="a-sender-verkey", + ) + + assert not await self.oob_processor.find_oob_record_for_inbound_message( + self.context + ) + mock_retrieve.assert_called_once() + mock_retrieve.assert_called_once_with(ANY, {"invi_msg_id": "the-pthid"}) + + async def test_find_oob_record_for_inbound_message_their_service_matching_with_message_receipt( + self, + ): + with async_mock.patch.object( + OobRecord, + "retrieve_by_tag_filter", + async_mock.CoroutineMock(return_value=self.oob_record), + ) as mock_retrieve: + self.context.message_receipt = MessageReceipt( + thread_id="the-thid", + parent_thread_id="the-pthid", + recipient_verkey="recipient-verkey", + sender_verkey="9WCgWKUaAJj3VWxxtzvvMQN3AoFxoBtBDo9ntwJnVVCC", + ) + + assert await self.oob_processor.find_oob_record_for_inbound_message( + self.context + ) + mock_retrieve.assert_called_once() + mock_retrieve.assert_called_once_with(ANY, {"invi_msg_id": "the-pthid"}) + + async def test_find_oob_record_for_inbound_message_their_service_set_on_oob_record( + self, + ): + self.context._message._service = ServiceDecorator( + endpoint="http://example.com/endpoint", + recipient_keys=["9WCgWKUaAJj3VWxxtzvvMQN3AoFxoBtBDo9ntwJnVVCC"], + routing_keys=["9WCgWKUaAJj3VWxxtzvvMQN3AoFxoBtBDo9ntwJnVVCC"], + ) + + self.oob_record.their_service = None + + with async_mock.patch.object( + OobRecord, + "retrieve_by_tag_filter", + async_mock.CoroutineMock(return_value=self.oob_record), + ) as mock_retrieve: + self.context.message_receipt = MessageReceipt( + thread_id="the-thid", + parent_thread_id="the-pthid", + ) + + assert await self.oob_processor.find_oob_record_for_inbound_message( + self.context + ) + mock_retrieve.assert_called_once() + mock_retrieve.assert_called_once_with(ANY, {"invi_msg_id": "the-pthid"}) + + assert self.oob_record.their_service == { + "serviceEndpoint": "http://example.com/endpoint", + "recipientKeys": ["9WCgWKUaAJj3VWxxtzvvMQN3AoFxoBtBDo9ntwJnVVCC"], + "routingKeys": ["9WCgWKUaAJj3VWxxtzvvMQN3AoFxoBtBDo9ntwJnVVCC"], + } + + async def test_find_oob_record_for_inbound_message_session_emit_delete( + self, + ): + with async_mock.patch.object( + OobRecord, + "retrieve_by_tag_filter", + async_mock.CoroutineMock(return_value=self.oob_record), + ) as mock_retrieve: + self.context.message_receipt = MessageReceipt( + thread_id="the-thid", + parent_thread_id="the-pthid", + ) + + assert await self.oob_processor.find_oob_record_for_inbound_message( + self.context + ) + mock_retrieve.assert_called_once() + mock_retrieve.assert_called_once_with(ANY, {"invi_msg_id": "the-pthid"}) + + assert self.oob_record.state == OobRecord.STATE_DONE + self.oob_record.emit_event.assert_called_once() + self.oob_record.delete_record.assert_called_once() + self.oob_record.save.assert_not_called() + + async def test_find_oob_record_for_inbound_message_session_connectionless_save( + self, + ): + self.oob_record.connection_id = None + + with async_mock.patch.object( + OobRecord, + "retrieve_by_tag_filter", + async_mock.CoroutineMock(return_value=self.oob_record), + ) as mock_retrieve: + self.context.message_receipt = MessageReceipt( + thread_id="the-thid", + parent_thread_id="the-pthid", + ) + + assert await self.oob_processor.find_oob_record_for_inbound_message( + self.context + ) + mock_retrieve.assert_called_once() + mock_retrieve.assert_called_once_with(ANY, {"invi_msg_id": "the-pthid"}) + + self.oob_record.emit_event.assert_not_called() + self.oob_record.delete_record.assert_not_called() + self.oob_record.save.assert_called_once() + + async def test_handle_message_connection(self): + oob_record = async_mock.MagicMock( + connection_id="the-conn-id", + save=async_mock.CoroutineMock(), + attach_thread_id=None, + their_service=None, + ) + + await self.oob_processor.handle_message( + self.profile, + [ + { + "@type": "issue-credential/1.0/offer-credential", + "@id": "4a580490-a9d8-44f5-a3f6-14e0b8a219b0", + } + ], + oob_record, + their_service=ServiceDecorator( + endpoint="http://their-service-endpoint.com", + recipient_keys=["9WCgWKUaAJj3VWxxtzvvMQN3AoFxoBtBDo9ntwJnVVCC"], + routing_keys=["6QSduYdf8Bi6t8PfNm5vNomGWDtXhmMmTRzaciudBXYJ"], + ), + ) + + assert oob_record.attach_thread_id == None + assert oob_record.their_service == None + + oob_record.save.assert_not_called() + + self.inbound_message_router.assert_called_once_with(self.profile, ANY, False) + + async def test_handle_message_connectionless(self): + oob_record = async_mock.MagicMock( + save=async_mock.CoroutineMock(), connection_id=None + ) + + await self.oob_processor.handle_message( + self.profile, + [ + { + "@type": "issue-credential/1.0/offer-credential", + "@id": "4a580490-a9d8-44f5-a3f6-14e0b8a219b0", + } + ], + oob_record, + their_service=ServiceDecorator( + endpoint="http://their-service-endpoint.com", + recipient_keys=["9WCgWKUaAJj3VWxxtzvvMQN3AoFxoBtBDo9ntwJnVVCC"], + routing_keys=["6QSduYdf8Bi6t8PfNm5vNomGWDtXhmMmTRzaciudBXYJ"], + ), + ) + + assert oob_record.attach_thread_id == "4a580490-a9d8-44f5-a3f6-14e0b8a219b0" + assert oob_record.their_service == { + "serviceEndpoint": "http://their-service-endpoint.com", + "recipientKeys": ["9WCgWKUaAJj3VWxxtzvvMQN3AoFxoBtBDo9ntwJnVVCC"], + "routingKeys": ["6QSduYdf8Bi6t8PfNm5vNomGWDtXhmMmTRzaciudBXYJ"], + } + + oob_record.save.assert_called_once() + + self.inbound_message_router.assert_called_once_with(self.profile, ANY, False) + + async def test_handle_message_unsupported_message_type(self): + with self.assertRaises(OobMessageProcessorError) as err: + await self.oob_processor.handle_message( + self.profile, [{"@type": "unsupported"}], async_mock.MagicMock() + ) + assert ( + "None of the oob attached messages supported. Supported message types are issue-credential/1.0/offer-credential, issue-credential/2.0/offer-credential, present-proof/1.0/request-presentation, present-proof/2.0/request-presentation" + in err.exception.message + ) + + async def test_get_thread_id(self): + message_w_thread = { + "@id": "the-message-id", + "~thread": {"thid": "the-thread-id"}, + } + message_wo_thread = {"@id": "the-message-id"} + + assert self.oob_processor.get_thread_id(message_w_thread) == "the-thread-id" + assert self.oob_processor.get_thread_id(message_wo_thread) == "the-message-id" diff --git a/aries_cloudagent/core/tests/test_plugin_registry.py b/aries_cloudagent/core/tests/test_plugin_registry.py index 7213e96b7e..7d4d86a9e4 100644 --- a/aries_cloudagent/core/tests/test_plugin_registry.py +++ b/aries_cloudagent/core/tests/test_plugin_registry.py @@ -15,7 +15,8 @@ class TestPluginRegistry(AsyncTestCase): def setUp(self): - self.registry = PluginRegistry() + self.blocked_module = "blocked_module" + self.registry = PluginRegistry(blocklist=[self.blocked_module]) self.context = InjectionContext(enforce_typing=False) self.proto_registry = async_mock.MagicMock( @@ -478,6 +479,23 @@ class MODULE: ] assert self.registry.register_plugin("dummy") == obj + async def test_unregister_plugin_has_setup(self): + class MODULE: + setup = "present" + + obj = MODULE() + with async_mock.patch.object( + ClassLoader, "load_module", async_mock.MagicMock() + ) as load_module: + load_module.side_effect = [ + obj, # module + None, # routes + None, # message types + None, # definition without versions attr + ] + assert self.registry.register_plugin(self.blocked_module) == None + assert self.blocked_module not in self.registry._plugins.keys() + async def test_register_definitions_malformed(self): class MODULE: no_setup = "no setup attr" diff --git a/aries_cloudagent/core/tests/test_protocol_registry.py b/aries_cloudagent/core/tests/test_protocol_registry.py index 5c43668d8b..623ad1d808 100644 --- a/aries_cloudagent/core/tests/test_protocol_registry.py +++ b/aries_cloudagent/core/tests/test_protocol_registry.py @@ -44,6 +44,162 @@ def test_message_type_query(self): matches = self.registry.protocols_matching_query(q) assert matches == () + def test_create_msg_types_for_minor_version(self): + MSG_PATH = "aries_cloudagent.protocols.introduction.v0_1.messages" + test_typesets = ( + { + "did:sov:BzCbsNYhMrjHiqZDTUASHg;spec/introduction-service/1.0/fake-forward-invitation": f"{MSG_PATH}.forward_invitation.ForwardInvitation", + "did:sov:BzCbsNYhMrjHiqZDTUASHg;spec/introduction-service/1.0/fake-invitation": f"{MSG_PATH}.invitation.Invitation", + "did:sov:BzCbsNYhMrjHiqZDTUASHg;spec/introduction-service/1.0/fake-invitation-request": f"{MSG_PATH}.invitation_request.InvitationRequest", + "https://didcom.org/introduction-service/1.0/fake-forward-invitation": f"{MSG_PATH}.forward_invitation.ForwardInvitation", + "https://didcom.org/introduction-service/1.0/fake-invitation": f"{MSG_PATH}.invitation.Invitation", + "https://didcom.org/introduction-service/1.0/fake-invitation-request": f"{MSG_PATH}.invitation_request.InvitationRequest", + }, + ) + test_version_def = { + "current_minor_version": 0, + "major_version": 1, + "minimum_minor_version": 0, + "path": "v0_1", + } + updated_typesets = self.registry.create_msg_types_for_minor_version( + test_typesets, test_version_def + ) + updated_typeset = updated_typesets[0] + assert ( + "https://didcom.org/introduction-service/1.0/fake-forward-invitation" + in updated_typeset + ) + assert ( + "https://didcom.org/introduction-service/1.0/fake-invitation" + in updated_typeset + ) + assert ( + "https://didcom.org/introduction-service/1.0/fake-invitation-request" + in updated_typeset + ) + assert ( + "did:sov:BzCbsNYhMrjHiqZDTUASHg;spec/introduction-service/1.0/fake-forward-invitation" + in updated_typeset + ) + + def test_introduction_create_msg_types_for_minor_version(self): + MSG_PATH = "aries_cloudagent.protocols.introduction.v0_1.messages" + test_typesets = ( + { + "did:sov:BzCbsNYhMrjHiqZDTUASHg;spec/introduction-service/0.1/invitation-request": f"{MSG_PATH}.invitation_request.InvitationRequest", + "did:sov:BzCbsNYhMrjHiqZDTUASHg;spec/introduction-service/0.1/invitation": f"{MSG_PATH}.invitation.Invitation", + "did:sov:BzCbsNYhMrjHiqZDTUASHg;spec/introduction-service/0.1/forward-invitation": f"{MSG_PATH}.invitation_messages.forward_invitation.ForwardInvitation", + "https://didcom.org/introduction-service/0.1/invitation-request": f"{MSG_PATH}.invitation_request.InvitationRequest", + "https://didcom.org/introduction-service/0.1/invitation": f"{MSG_PATH}.invitation.Invitation", + "https://didcom.org/introduction-service/0.1/forward-invitation": f"{MSG_PATH}.forward_invitation.ForwardInvitation", + }, + ) + test_version_def = { + "current_minor_version": 1, + "major_version": 0, + "minimum_minor_version": 1, + "path": "v0_1", + } + updated_typesets = self.registry.create_msg_types_for_minor_version( + test_typesets, test_version_def + ) + updated_typeset = updated_typesets[0] + assert ( + "did:sov:BzCbsNYhMrjHiqZDTUASHg;spec/introduction-service/0.1/invitation-request" + in updated_typeset + ) + assert ( + "did:sov:BzCbsNYhMrjHiqZDTUASHg;spec/introduction-service/0.1/invitation" + in updated_typeset + ) + assert ( + "did:sov:BzCbsNYhMrjHiqZDTUASHg;spec/introduction-service/0.1/forward-invitation" + in updated_typeset + ) + assert ( + "https://didcom.org/introduction-service/0.1/invitation-request" + in updated_typeset + ) + assert ( + "https://didcom.org/introduction-service/0.1/invitation" in updated_typeset + ) + assert ( + "https://didcom.org/introduction-service/0.1/forward-invitation" + in updated_typeset + ) + + def test_oob_create_msg_types_for_minor_version(self): + MSG_PATH = "aries_cloudagent.protocols.out_of_band.v1_0.messages" + test_typesets = ( + { + "did:sov:BzCbsNYhMrjHiqZDTUASHg;spec/out-of-band/1.1/invitation": f"{MSG_PATH}.invitation.Invitation", + "did:sov:BzCbsNYhMrjHiqZDTUASHg;spec/out-of-band/1.1/handshake-reuse": f"{MSG_PATH}.reuse.HandshakeReuse", + "did:sov:BzCbsNYhMrjHiqZDTUASHg;spec/out-of-band/1.1/handshake-reuse-accepted": f"{MSG_PATH}.reuse_accept.HandshakeReuseAccept", + "did:sov:BzCbsNYhMrjHiqZDTUASHg;spec/out-of-band/1.1/problem_report": f"{MSG_PATH}.problem_report.OOBProblemReport", + "https://didcom.org/out-of-band/1.1/invitation": f"{MSG_PATH}.invitation.Invitation", + "https://didcom.org/out-of-band/1.1/handshake-reuse": f"{MSG_PATH}.reuse.HandshakeReuse", + "https://didcom.org/out-of-band/1.1/handshake-reuse-accepted": f"{MSG_PATH}.reuse_accept.HandshakeReuseAccept", + "https://didcom.org/out-of-band/1.1/problem_report": f"{MSG_PATH}.problem_report.OOBProblemReport", + }, + ) + test_version_def = { + "current_minor_version": 1, + "major_version": 1, + "minimum_minor_version": 0, + "path": "v0_1", + } + updated_typesets = self.registry.create_msg_types_for_minor_version( + test_typesets, test_version_def + ) + updated_typeset = updated_typesets[0] + assert "https://didcom.org/out-of-band/1.0/invitation" in updated_typeset + assert "https://didcom.org/out-of-band/1.0/handshake-reuse" in updated_typeset + assert ( + "https://didcom.org/out-of-band/1.0/handshake-reuse-accepted" + in updated_typeset + ) + assert "https://didcom.org/out-of-band/1.0/problem_report" in updated_typeset + assert "https://didcom.org/out-of-band/1.1/invitation" in updated_typeset + assert "https://didcom.org/out-of-band/1.1/handshake-reuse" in updated_typeset + assert ( + "https://didcom.org/out-of-band/1.1/handshake-reuse-accepted" + in updated_typeset + ) + assert "https://didcom.org/out-of-band/1.1/problem_report" in updated_typeset + assert ( + "did:sov:BzCbsNYhMrjHiqZDTUASHg;spec/out-of-band/1.0/invitation" + in updated_typeset + ) + assert ( + "did:sov:BzCbsNYhMrjHiqZDTUASHg;spec/out-of-band/1.0/handshake-reuse" + in updated_typeset + ) + assert ( + "did:sov:BzCbsNYhMrjHiqZDTUASHg;spec/out-of-band/1.0/handshake-reuse-accepted" + in updated_typeset + ) + assert ( + "did:sov:BzCbsNYhMrjHiqZDTUASHg;spec/out-of-band/1.0/problem_report" + in updated_typeset + ) + assert ( + "did:sov:BzCbsNYhMrjHiqZDTUASHg;spec/out-of-band/1.1/invitation" + in updated_typeset + ) + assert ( + "did:sov:BzCbsNYhMrjHiqZDTUASHg;spec/out-of-band/1.1/handshake-reuse" + in updated_typeset + ) + assert ( + "did:sov:BzCbsNYhMrjHiqZDTUASHg;spec/out-of-band/1.1/handshake-reuse-accepted" + in updated_typeset + ) + assert ( + "did:sov:BzCbsNYhMrjHiqZDTUASHg;spec/out-of-band/1.1/problem_report" + in updated_typeset + ) + async def test_disclosed(self): self.registry.register_message_types( {self.test_message_type: self.test_message_handler} diff --git a/aries_cloudagent/core/tests/test_util.py b/aries_cloudagent/core/tests/test_util.py new file mode 100644 index 0000000000..c25f944d74 --- /dev/null +++ b/aries_cloudagent/core/tests/test_util.py @@ -0,0 +1,82 @@ +from async_case import IsolatedAsyncioTestCase + +from ...cache.base import BaseCache +from ...cache.in_memory import InMemoryCache +from ...core.in_memory import InMemoryProfile +from ...core.profile import Profile +from ...protocols.didcomm_prefix import DIDCommPrefix +from ...protocols.introduction.v0_1.messages.invitation import Invitation +from ...protocols.out_of_band.v1_0.messages.reuse import HandshakeReuse + +from .. import util as test_module + + +def make_profile() -> Profile: + profile = InMemoryProfile.test_profile() + profile.context.injector.bind_instance(BaseCache, InMemoryCache()) + return profile + + +class TestUtils(IsolatedAsyncioTestCase): + async def test_validate_get_response_version(self): + profile = make_profile() + (resp_version, warning) = await test_module.validate_get_response_version( + profile, "1.1", HandshakeReuse + ) + assert resp_version == "1.1" + assert not warning + + # cached + (resp_version, warning) = await test_module.validate_get_response_version( + profile, "1.1", HandshakeReuse + ) + assert resp_version == "1.1" + assert not warning + + (resp_version, warning) = await test_module.validate_get_response_version( + profile, "1.0", HandshakeReuse + ) + assert resp_version == "1.0" + assert warning == test_module.WARNING_DEGRADED_FEATURES + + (resp_version, warning) = await test_module.validate_get_response_version( + profile, "1.2", HandshakeReuse + ) + assert resp_version == "1.1" + assert warning == test_module.WARNING_VERSION_MISMATCH + + with self.assertRaises(test_module.ProtocolMinorVersionNotSupported): + (resp_version, warning) = await test_module.validate_get_response_version( + profile, "0.0", Invitation + ) + + with self.assertRaises(Exception): + (resp_version, warning) = await test_module.validate_get_response_version( + profile, "1.0", Invitation + ) + + def test_get_version_from_message_type(self): + assert ( + test_module.get_version_from_message_type( + DIDCommPrefix.qualify_current("out-of-band/1.1/handshake-reuse") + ) + == "1.1" + ) + + def test_get_version_from_message(self): + assert test_module.get_version_from_message(HandshakeReuse()) == "1.1" + + async def test_get_proto_default_version_from_msg_class(self): + profile = make_profile() + assert ( + await test_module.get_proto_default_version_from_msg_class( + profile, HandshakeReuse + ) + ) == "1.1" + + def test_get_proto_default_version(self): + assert ( + test_module.get_proto_default_version( + "aries_cloudagent.protocols.out_of_band.definition" + ) + ) == "1.1" diff --git a/aries_cloudagent/core/util.py b/aries_cloudagent/core/util.py index 791f80c95d..dd28347e4e 100644 --- a/aries_cloudagent/core/util.py +++ b/aries_cloudagent/core/util.py @@ -1,10 +1,180 @@ """Core utilities and constants.""" +import inspect +import os import re +from typing import Optional, Tuple + +from ..cache.base import BaseCache +from ..core.profile import Profile +from ..messaging.agent_message import AgentMessage +from ..utils.classloader import ClassLoader + +from .error import ProtocolMinorVersionNotSupported, ProtocolDefinitionValidationError CORE_EVENT_PREFIX = "acapy::core::" STARTUP_EVENT_TOPIC = CORE_EVENT_PREFIX + "startup" STARTUP_EVENT_PATTERN = re.compile(f"^{STARTUP_EVENT_TOPIC}?$") SHUTDOWN_EVENT_TOPIC = CORE_EVENT_PREFIX + "shutdown" SHUTDOWN_EVENT_PATTERN = re.compile(f"^{SHUTDOWN_EVENT_TOPIC}?$") +WARNING_DEGRADED_FEATURES = "version-with-degraded-features" +WARNING_VERSION_MISMATCH = "fields-ignored-due-to-version-mismatch" +WARNING_VERSION_NOT_SUPPORTED = "version-not-supported" + + +async def validate_get_response_version( + profile: Profile, rec_version: str, msg_class: type +) -> Tuple[str, Optional[str]]: + """ + Return a tuple with version to respond with and warnings. + + Process received version and protocol version definition, + returns the tuple. + + Args: + profile: Profile + rec_version: received version from message + msg_class: type + + Returns: + Tuple with response version and any warnings + + """ + resp_version = rec_version + warning = None + version_string_tokens = rec_version.split(".") + rec_major_version = int(version_string_tokens[0]) + rec_minor_version = int(version_string_tokens[1]) + version_definition = await get_version_def_from_msg_class( + profile, msg_class, rec_major_version + ) + proto_major_version = int(version_definition["major_version"]) + proto_curr_minor_version = int(version_definition["current_minor_version"]) + proto_min_minor_version = int(version_definition["minimum_minor_version"]) + if rec_minor_version < proto_min_minor_version: + warning = WARNING_VERSION_NOT_SUPPORTED + elif ( + rec_minor_version >= proto_min_minor_version + and rec_minor_version < proto_curr_minor_version + ): + warning = WARNING_DEGRADED_FEATURES + elif rec_minor_version > proto_curr_minor_version: + warning = WARNING_VERSION_MISMATCH + if proto_major_version == rec_major_version: + if ( + proto_min_minor_version <= rec_minor_version + and proto_curr_minor_version >= rec_minor_version + ): + resp_version = f"{str(proto_major_version)}.{str(rec_minor_version)}" + elif rec_minor_version > proto_curr_minor_version: + resp_version = f"{str(proto_major_version)}.{str(proto_curr_minor_version)}" + elif rec_minor_version < proto_min_minor_version: + raise ProtocolMinorVersionNotSupported( + "Minimum supported minor version is " + + f"{proto_min_minor_version}." + + f" Received {rec_minor_version}." + ) + else: + raise ProtocolMinorVersionNotSupported( + f"Supported major version {proto_major_version}" + " is not same as received major version" + f" {rec_major_version}." + ) + return (resp_version, warning) + + +def get_version_from_message_type(msg_type: str) -> str: + """Return version from provided message_type.""" + return (re.search(r"(\d+\.)?(\*|\d+)", msg_type)).group() + + +def get_version_from_message(msg: AgentMessage) -> str: + """Return version from provided AgentMessage.""" + msg_type = msg._type + return get_version_from_message_type(msg_type) + + +async def get_proto_default_version_from_msg_class( + profile: Profile, msg_class: type, major_version: int = 1 +) -> str: + """Return default protocol version from version_definition.""" + version_definition = await get_version_def_from_msg_class( + profile, msg_class, major_version + ) + return _get_default_version_from_version_def(version_definition) + + +def get_proto_default_version(def_path: str, major_version: int = 1) -> str: + """Return default protocol version from version_definition.""" + version_definition = _get_version_def_from_path(def_path, major_version) + return _get_default_version_from_version_def(version_definition) + + +def _resolve_definition(search_path: str, msg_class: type) -> str: + try: + path = os.path.normpath(inspect.getfile(msg_class)) + path = search_path + path.rsplit(search_path, 1)[1] + version = (re.search(r"v(\d+\_)?(\*|\d+)", path)).group() + path = path.split(version, 1)[0] + definition_path = (path.replace("/", ".")) + "definition" + if ClassLoader.load_module(definition_path): + return definition_path + except Exception: + # we expect some exceptions resolving paths + pass + + +def _get_path_from_msg_class(msg_class: type) -> str: + search_paths = ["aries_cloudagent", msg_class.__module__.split(".", 1)[0]] + if os.getenv("ACAPY_HOME"): + search_paths.insert(os.getenv("ACAPY_HOME"), 0) + + definition_path = None + searches = 0 + while not definition_path and searches < len(search_paths): + definition_path = _resolve_definition(search_paths[searches], msg_class) + searches = searches + 1 + # we could throw an exception here, + return definition_path + + +def _get_version_def_from_path(definition_path: str, major_version: int = 1): + version_definition = None + definition = ClassLoader.load_module(definition_path) + for protocol_version in definition.versions: + if major_version == protocol_version["major_version"]: + version_definition = protocol_version + break + return version_definition + + +def _get_default_version_from_version_def(version_definition) -> str: + default_major_version = version_definition["major_version"] + default_minor_version = version_definition["current_minor_version"] + return f"{default_major_version}.{default_minor_version}" + + +async def get_version_def_from_msg_class( + profile: Profile, msg_class: type, major_version: int = 1 +): + """Return version_definition of a protocol from msg_class.""" + cache = profile.inject_or(BaseCache) + version_definition = None + if cache: + version_definition = await cache.get( + f"version_definition::{str(msg_class).lower()}" + ) + if version_definition: + return version_definition + definition_path = _get_path_from_msg_class(msg_class) + version_definition = _get_version_def_from_path(definition_path, major_version) + if not version_definition: + raise ProtocolDefinitionValidationError( + f"Unable to load protocol version_definition for {str(msg_class)}" + ) + if cache: + await cache.set( + f"version_definition::{str(msg_class).lower()}", version_definition + ) + return version_definition diff --git a/aries_cloudagent/did/did_key.py b/aries_cloudagent/did/did_key.py index 70101910fb..3a74971612 100644 --- a/aries_cloudagent/did/did_key.py +++ b/aries_cloudagent/did/did_key.py @@ -1,7 +1,15 @@ """DID Key class and resolver methods.""" from ..wallet.crypto import ed25519_pk_to_curve25519 -from ..wallet.key_type import KeyType +from ..wallet.key_type import ( + BLS12381G1G2, + ED25519, + KeyType, + BLS12381G1, + X25519, + BLS12381G2, + KeyTypes, +) from ..wallet.util import b58_to_bytes, bytes_to_b58 from ..vc.ld_proofs.constants import DID_V1_CONTEXT_URL @@ -31,7 +39,7 @@ def from_public_key_b58(cls, public_key: str, key_type: KeyType) -> "DIDKey": return cls.from_public_key(public_key_bytes, key_type) @classmethod - def from_fingerprint(cls, fingerprint: str) -> "DIDKey": + def from_fingerprint(cls, fingerprint: str, key_types=None) -> "DIDKey": """Initialize new DIDKey instance from multibase encoded fingerprint. The fingerprint contains both the public key and key type. @@ -43,7 +51,9 @@ def from_fingerprint(cls, fingerprint: str) -> "DIDKey": key_bytes_with_prefix = b58_to_bytes(fingerprint[1:]) # Get associated key type with prefixed bytes - key_type = KeyType.from_prefixed_bytes(key_bytes_with_prefix) + if not key_types: + key_types = KeyTypes() + key_type = key_types.from_prefixed_bytes(key_bytes_with_prefix) if not key_type: raise Exception( @@ -169,8 +179,8 @@ def construct_did_key_bls12381g1g2(did_key: "DIDKey") -> dict: g1_public_key = did_key.public_key[:48] g2_public_key = did_key.public_key[48:] - bls12381g1_key = DIDKey.from_public_key(g1_public_key, KeyType.BLS12381G1) - bls12381g2_key = DIDKey.from_public_key(g2_public_key, KeyType.BLS12381G2) + bls12381g1_key = DIDKey.from_public_key(g1_public_key, BLS12381G1) + bls12381g2_key = DIDKey.from_public_key(g2_public_key, BLS12381G2) bls12381g1_key_id = f"{did_key.did}#{bls12381g1_key.fingerprint}" bls12381g2_key_id = f"{did_key.did}#{bls12381g2_key.fingerprint}" @@ -241,7 +251,7 @@ def construct_did_key_ed25519(did_key: "DIDKey") -> dict: """ curve25519 = ed25519_pk_to_curve25519(did_key.public_key) - x25519 = DIDKey.from_public_key(curve25519, KeyType.X25519) + x25519 = DIDKey.from_public_key(curve25519, X25519) did_doc = construct_did_signature_key_base( id=did_key.did, @@ -289,9 +299,9 @@ def construct_did_signature_key_base( DID_KEY_RESOLVERS = { - KeyType.ED25519: construct_did_key_ed25519, - KeyType.X25519: construct_did_key_x25519, - KeyType.BLS12381G2: construct_did_key_bls12381g2, - KeyType.BLS12381G1: construct_did_key_bls12381g1, - KeyType.BLS12381G1G2: construct_did_key_bls12381g1g2, + ED25519: construct_did_key_ed25519, + X25519: construct_did_key_x25519, + BLS12381G2: construct_did_key_bls12381g2, + BLS12381G1: construct_did_key_bls12381g1, + BLS12381G1G2: construct_did_key_bls12381g1g2, } diff --git a/aries_cloudagent/did/tests/test_did_key_bls12381g1.py b/aries_cloudagent/did/tests/test_did_key_bls12381g1.py index 9b95465df4..f5b84b3bd0 100644 --- a/aries_cloudagent/did/tests/test_did_key_bls12381g1.py +++ b/aries_cloudagent/did/tests/test_did_key_bls12381g1.py @@ -1,7 +1,7 @@ from unittest import TestCase -from ...wallet.key_type import KeyType +from ...wallet.key_type import BLS12381G1 from ...wallet.util import b58_to_bytes from ..did_key import DIDKey, DID_KEY_RESOLVERS from .test_dids import ( @@ -24,14 +24,12 @@ class TestDIDKey(TestCase): def test_bls12381g1_from_public_key(self): key_bytes = b58_to_bytes(TEST_BLS12381G1_BASE58_KEY) - did_key = DIDKey.from_public_key(key_bytes, KeyType.BLS12381G1) + did_key = DIDKey.from_public_key(key_bytes, BLS12381G1) assert did_key.did == TEST_BLS12381G1_DID def test_bls12381g1_from_public_key_b58(self): - did_key = DIDKey.from_public_key_b58( - TEST_BLS12381G1_BASE58_KEY, KeyType.BLS12381G1 - ) + did_key = DIDKey.from_public_key_b58(TEST_BLS12381G1_BASE58_KEY, BLS12381G1) assert did_key.did == TEST_BLS12381G1_DID @@ -53,20 +51,20 @@ def test_bls12381g1_properties(self): assert did_key.did == TEST_BLS12381G1_DID assert did_key.public_key_b58 == TEST_BLS12381G1_BASE58_KEY assert did_key.public_key == b58_to_bytes(TEST_BLS12381G1_BASE58_KEY) - assert did_key.key_type == KeyType.BLS12381G1 + assert did_key.key_type == BLS12381G1 assert did_key.key_id == TEST_BLS12381G1_KEY_ID assert did_key.prefixed_public_key == TEST_BLS12381G1_PREFIX_BYTES def test_bls12381g1_diddoc(self): did_key = DIDKey.from_did(TEST_BLS12381G1_DID) - resolver = DID_KEY_RESOLVERS[KeyType.BLS12381G1] + resolver = DID_KEY_RESOLVERS[BLS12381G1] assert resolver(did_key) == did_key.did_doc def test_bls12381g1_resolver(self): did_key = DIDKey.from_did(TEST_BLS12381G1_DID) - resolver = DID_KEY_RESOLVERS[KeyType.BLS12381G1] + resolver = DID_KEY_RESOLVERS[BLS12381G1] did_doc = resolver(did_key) assert ( diff --git a/aries_cloudagent/did/tests/test_did_key_bls12381g1g2.py b/aries_cloudagent/did/tests/test_did_key_bls12381g1g2.py index 59e2a05907..5877bed1c6 100644 --- a/aries_cloudagent/did/tests/test_did_key_bls12381g1g2.py +++ b/aries_cloudagent/did/tests/test_did_key_bls12381g1g2.py @@ -1,6 +1,6 @@ from unittest import TestCase -from ...wallet.key_type import KeyType +from ...wallet.key_type import BLS12381G1, BLS12381G1G2, BLS12381G2 from ...wallet.util import b58_to_bytes from ..did_key import DIDKey, DID_KEY_RESOLVERS from .test_dids import ( @@ -26,19 +26,18 @@ [b"\xee\x01", b58_to_bytes(TEST_BLS12381G1G2_BASE58_KEY)] ) + # The tests here are a bit quirky because g1g2 is a concatenation of g1 and g2 public key bytes # but it works with the already existing did key implementation. class TestDIDKey(TestCase): def test_bls12381g1g2_from_public_key(self): key_bytes = b58_to_bytes(TEST_BLS12381G1G2_BASE58_KEY) - did_key = DIDKey.from_public_key(key_bytes, KeyType.BLS12381G1G2) + did_key = DIDKey.from_public_key(key_bytes, BLS12381G1G2) assert did_key.did == TEST_BLS12381G1G2_DID def test_bls12381g1g2_from_public_key_b58(self): - did_key = DIDKey.from_public_key_b58( - TEST_BLS12381G1G2_BASE58_KEY, KeyType.BLS12381G1G2 - ) + did_key = DIDKey.from_public_key_b58(TEST_BLS12381G1G2_BASE58_KEY, BLS12381G1G2) assert did_key.did == TEST_BLS12381G1G2_DID @@ -60,13 +59,13 @@ def test_bls12381g1g2_properties(self): assert did_key.did == TEST_BLS12381G1G2_DID assert did_key.public_key_b58 == TEST_BLS12381G1G2_BASE58_KEY assert did_key.public_key == b58_to_bytes(TEST_BLS12381G1G2_BASE58_KEY) - assert did_key.key_type == KeyType.BLS12381G1G2 + assert did_key.key_type == BLS12381G1G2 assert did_key.prefixed_public_key == TEST_BLS12381G1G2_PREFIX_BYTES def test_bls12381g1g2_diddoc(self): did_key = DIDKey.from_did(TEST_BLS12381G1G2_DID) - resolver = DID_KEY_RESOLVERS[KeyType.BLS12381G1G2] + resolver = DID_KEY_RESOLVERS[BLS12381G1G2] assert resolver(did_key) == did_key.did_doc @@ -74,7 +73,7 @@ def test_bls12381g1g2_resolver(self): did_key = DIDKey.from_did( "did:key:z5TcESXuYUE9aZWYwSdrUEGK1HNQFHyTt4aVpaCTVZcDXQmUheFwfNZmRksaAbBneNm5KyE52SdJeRCN1g6PJmF31GsHWwFiqUDujvasK3wTiDr3vvkYwEJHt7H5RGEKYEp1ErtQtcEBgsgY2DA9JZkHj1J9HZ8MRDTguAhoFtR4aTBQhgnkP4SwVbxDYMEZoF2TMYn3s" ) - resolver = DID_KEY_RESOLVERS[KeyType.BLS12381G1G2] + resolver = DID_KEY_RESOLVERS[BLS12381G1G2] did_doc = resolver(did_key) assert ( @@ -88,13 +87,13 @@ def test_bls12381g1g1_to_g1(self): # TODO: add easier method to go form g1 <- g1g2 -> g2 # First 48 bytes is g1 key g1_public_key = g1g2_did.public_key[:48] - g1_did = DIDKey.from_public_key(g1_public_key, KeyType.BLS12381G1) + g1_did = DIDKey.from_public_key(g1_public_key, BLS12381G1) assert g1_did.fingerprint == TEST_BLS12381G1_FINGERPRINT assert g1_did.did == TEST_BLS12381G1_DID assert g1_did.public_key_b58 == TEST_BLS12381G1_BASE58_KEY assert g1_did.public_key == b58_to_bytes(TEST_BLS12381G1_BASE58_KEY) - assert g1_did.key_type == KeyType.BLS12381G1 + assert g1_did.key_type == BLS12381G1 def test_bls12381g1g1_to_g2(self): g1g2_did = DIDKey.from_did(TEST_BLS12381G1G2_DID) @@ -102,10 +101,10 @@ def test_bls12381g1g1_to_g2(self): # TODO: add easier method to go form g1 <- g1g2 -> g2 # From 48 bytes is g2 key g2_public_key = g1g2_did.public_key[48:] - g2_did = DIDKey.from_public_key(g2_public_key, KeyType.BLS12381G2) + g2_did = DIDKey.from_public_key(g2_public_key, BLS12381G2) assert g2_did.fingerprint == TEST_BLS12381G2_FINGERPRINT assert g2_did.did == TEST_BLS12381G2_DID assert g2_did.public_key_b58 == TEST_BLS12381G2_BASE58_KEY assert g2_did.public_key == b58_to_bytes(TEST_BLS12381G2_BASE58_KEY) - assert g2_did.key_type == KeyType.BLS12381G2 + assert g2_did.key_type == BLS12381G2 diff --git a/aries_cloudagent/did/tests/test_did_key_bls12381g2.py b/aries_cloudagent/did/tests/test_did_key_bls12381g2.py index 0f2cb67fc1..0eb4b4c8f4 100644 --- a/aries_cloudagent/did/tests/test_did_key_bls12381g2.py +++ b/aries_cloudagent/did/tests/test_did_key_bls12381g2.py @@ -1,6 +1,6 @@ from unittest import TestCase -from ...wallet.key_type import KeyType +from ...wallet.key_type import BLS12381G2 from ...wallet.util import b58_to_bytes from ..did_key import DIDKey, DID_KEY_RESOLVERS from .test_dids import ( @@ -19,14 +19,12 @@ class TestDIDKey(TestCase): def test_bls12381g2_from_public_key(self): key_bytes = b58_to_bytes(TEST_BLS12381G2_BASE58_KEY) - did_key = DIDKey.from_public_key(key_bytes, KeyType.BLS12381G2) + did_key = DIDKey.from_public_key(key_bytes, BLS12381G2) assert did_key.did == TEST_BLS12381G2_DID def test_bls12381g2_from_public_key_b58(self): - did_key = DIDKey.from_public_key_b58( - TEST_BLS12381G2_BASE58_KEY, KeyType.BLS12381G2 - ) + did_key = DIDKey.from_public_key_b58(TEST_BLS12381G2_BASE58_KEY, BLS12381G2) assert did_key.did == TEST_BLS12381G2_DID @@ -48,20 +46,20 @@ def test_bls12381g2_properties(self): assert did_key.did == TEST_BLS12381G2_DID assert did_key.public_key_b58 == TEST_BLS12381G2_BASE58_KEY assert did_key.public_key == b58_to_bytes(TEST_BLS12381G2_BASE58_KEY) - assert did_key.key_type == KeyType.BLS12381G2 + assert did_key.key_type == BLS12381G2 assert did_key.key_id == TEST_BLS12381G2_KEY_ID assert did_key.prefixed_public_key == TEST_BLS12381G2_PREFIX_BYTES def test_bls12381g2_diddoc(self): did_key = DIDKey.from_did(TEST_BLS12381G2_DID) - resolver = DID_KEY_RESOLVERS[KeyType.BLS12381G2] + resolver = DID_KEY_RESOLVERS[BLS12381G2] assert resolver(did_key) == did_key.did_doc def test_bls12381g2_resolver(self): did_key = DIDKey.from_did(TEST_BLS12381G2_DID) - resolver = DID_KEY_RESOLVERS[KeyType.BLS12381G2] + resolver = DID_KEY_RESOLVERS[BLS12381G2] did_doc = resolver(did_key) assert ( diff --git a/aries_cloudagent/did/tests/test_did_key_ed25519.py b/aries_cloudagent/did/tests/test_did_key_ed25519.py index 3911fc3e36..53c2eb8bf2 100644 --- a/aries_cloudagent/did/tests/test_did_key_ed25519.py +++ b/aries_cloudagent/did/tests/test_did_key_ed25519.py @@ -1,6 +1,6 @@ from unittest import TestCase -from ...wallet.key_type import KeyType +from ...wallet.key_type import ED25519 from ...wallet.util import b58_to_bytes from ..did_key import DIDKey, DID_KEY_RESOLVERS from .test_dids import DID_ED25519_z6MkmjY8GnV5i9YTDtPETC2uUAW6ejw3nk5mXF5yci5ab7th @@ -17,12 +17,12 @@ class TestDIDKey(TestCase): def test_ed25519_from_public_key(self): key_bytes = b58_to_bytes(TEST_ED25519_BASE58_KEY) - did_key = DIDKey.from_public_key(key_bytes, KeyType.ED25519) + did_key = DIDKey.from_public_key(key_bytes, ED25519) assert did_key.did == TEST_ED25519_DID def test_ed25519_from_public_key_b58(self): - did_key = DIDKey.from_public_key_b58(TEST_ED25519_BASE58_KEY, KeyType.ED25519) + did_key = DIDKey.from_public_key_b58(TEST_ED25519_BASE58_KEY, ED25519) assert did_key.did == TEST_ED25519_DID @@ -44,20 +44,20 @@ def test_ed25519_properties(self): assert did_key.did == TEST_ED25519_DID assert did_key.public_key_b58 == TEST_ED25519_BASE58_KEY assert did_key.public_key == b58_to_bytes(TEST_ED25519_BASE58_KEY) - assert did_key.key_type == KeyType.ED25519 + assert did_key.key_type == ED25519 assert did_key.key_id == TEST_ED25519_KEY_ID assert did_key.prefixed_public_key == TEST_ED25519_PREFIX_BYTES def test_ed25519_diddoc(self): did_key = DIDKey.from_did(TEST_ED25519_DID) - resolver = DID_KEY_RESOLVERS[KeyType.ED25519] + resolver = DID_KEY_RESOLVERS[ED25519] assert resolver(did_key) == did_key.did_doc def test_ed25519_resolver(self): did_key = DIDKey.from_did(TEST_ED25519_DID) - resolver = DID_KEY_RESOLVERS[KeyType.ED25519] + resolver = DID_KEY_RESOLVERS[ED25519] did_doc = resolver(did_key) # resolved using uniresolver, updated to did v1 diff --git a/aries_cloudagent/did/tests/test_did_key_x25519.py b/aries_cloudagent/did/tests/test_did_key_x25519.py index 924011f396..84513da814 100644 --- a/aries_cloudagent/did/tests/test_did_key_x25519.py +++ b/aries_cloudagent/did/tests/test_did_key_x25519.py @@ -1,6 +1,6 @@ from unittest import TestCase -from ...wallet.key_type import KeyType +from ...wallet.key_type import X25519 from ...wallet.util import b58_to_bytes from ..did_key import DIDKey, DID_KEY_RESOLVERS from .test_dids import DID_X25519_z6LShLeXRTzevtwcfehaGEzCMyL3bNsAeKCwcqwJxyCo63yE @@ -15,12 +15,12 @@ class TestDIDKey(TestCase): def test_x25519_from_public_key(self): key_bytes = b58_to_bytes(TEST_X25519_BASE58_KEY) - did_key = DIDKey.from_public_key(key_bytes, KeyType.X25519) + did_key = DIDKey.from_public_key(key_bytes, X25519) assert did_key.did == TEST_X25519_DID def test_x25519_from_public_key_b58(self): - did_key = DIDKey.from_public_key_b58(TEST_X25519_BASE58_KEY, KeyType.X25519) + did_key = DIDKey.from_public_key_b58(TEST_X25519_BASE58_KEY, X25519) assert did_key.did == TEST_X25519_DID @@ -42,20 +42,20 @@ def test_x25519_properties(self): assert did_key.did == TEST_X25519_DID assert did_key.public_key_b58 == TEST_X25519_BASE58_KEY assert did_key.public_key == b58_to_bytes(TEST_X25519_BASE58_KEY) - assert did_key.key_type == KeyType.X25519 + assert did_key.key_type == X25519 assert did_key.key_id == TEST_X25519_KEY_ID assert did_key.prefixed_public_key == TEST_X25519_PREFIX_BYTES def test_x25519_diddoc(self): did_key = DIDKey.from_did(TEST_X25519_DID) - resolver = DID_KEY_RESOLVERS[KeyType.X25519] + resolver = DID_KEY_RESOLVERS[X25519] assert resolver(did_key) == did_key.did_doc def test_x25519_resolver(self): did_key = DIDKey.from_did(TEST_X25519_DID) - resolver = DID_KEY_RESOLVERS[KeyType.X25519] + resolver = DID_KEY_RESOLVERS[X25519] did_doc = resolver(did_key) # resolved using uniresolver, updated to did v1 diff --git a/aries_cloudagent/holder/routes.py b/aries_cloudagent/holder/routes.py index 7d52bdfc7d..26032000b5 100644 --- a/aries_cloudagent/holder/routes.py +++ b/aries_cloudagent/holder/routes.py @@ -274,6 +274,8 @@ async def credentials_remove(request: web.BaseRequest): async with context.profile.session() as session: holder = session.inject(IndyHolder) await holder.delete_credential(credential_id) + topic = "acapy::record::credential::delete" + await context.profile.notify(topic, {"id": credential_id, "state": "deleted"}) except WalletNotFoundError as err: raise web.HTTPNotFound(reason=err.roll_up) from err @@ -376,6 +378,10 @@ async def w3c_cred_remove(request: web.BaseRequest): try: vc_record = await holder.retrieve_credential_by_id(credential_id) await holder.delete_credential(vc_record) + topic = "acapy::record::w3c_credential::delete" + await session.profile.notify( + topic, {"id": credential_id, "state": "deleted"} + ) except StorageNotFoundError as err: raise web.HTTPNotFound(reason=err.roll_up) from err except StorageError as err: diff --git a/aries_cloudagent/indy/credx/holder.py b/aries_cloudagent/indy/credx/holder.py index d0ad6d5110..a212661cbc 100644 --- a/aries_cloudagent/indy/credx/holder.py +++ b/aries_cloudagent/indy/credx/holder.py @@ -6,7 +6,7 @@ import re import uuid -from typing import Sequence, Tuple, Union +from typing import Dict, Sequence, Tuple, Union from aries_askar import AskarError, AskarErrorCode from indy_credx import ( @@ -471,27 +471,26 @@ async def create_presentation( """ - creds = {} - - def get_rev_state(cred_id, timestamp): - reg_id = creds[cred_id].rev_reg_id - if not reg_id: - raise IndyHolderError( - f"Cannot prove credential '{cred_id}' for " - "specific timestamp, credential has no rev_reg_id" - ) - if not rev_states or reg_id not in rev_states: - raise IndyHolderError( - f"No revocation states provided for credential '{cred_id}'" - f"with rev_reg_id '{reg_id}'" - ) - state = rev_states[reg_id].get(timestamp) - if not state: - raise IndyHolderError( - f"No revocation states provided for credential '{cred_id}'" - f"with rev_reg_id '{reg_id}' at timestamp {timestamp}" - ) - return state + creds: Dict[str, Credential] = {} + + def get_rev_state(cred_id: str, detail: dict): + cred = creds[cred_id] + rev_reg_id = cred.rev_reg_id + timestamp = detail.get("timestamp") if rev_reg_id else None + rev_state = None + if timestamp: + if not rev_states or rev_reg_id not in rev_states: + raise IndyHolderError( + f"No revocation states provided for credential '{cred_id}' " + f"with rev_reg_id '{rev_reg_id}'" + ) + rev_state = rev_states[rev_reg_id].get(timestamp) + if not rev_state: + raise IndyHolderError( + f"No revocation states provided for credential '{cred_id}' " + f"with rev_reg_id '{rev_reg_id}' at timestamp {timestamp}" + ) + return timestamp, rev_state self_attest = requested_credentials.get("self_attested_attributes") or {} present_creds = PresentCredentials() @@ -501,25 +500,26 @@ def get_rev_state(cred_id, timestamp): if cred_id not in creds: # NOTE: could be optimized if multiple creds are requested creds[cred_id] = await self._get_credential(cred_id) - timestamp = detail.get("timestamp") + timestamp, rev_state = get_rev_state(cred_id, detail) present_creds.add_attributes( creds[cred_id], reft, reveal=detail["revealed"], timestamp=timestamp, - rev_state=get_rev_state(cred_id, timestamp) if timestamp else None, + rev_state=rev_state, ) req_preds = requested_credentials.get("requested_predicates") or {} for reft, detail in req_preds.items(): + cred_id = detail["cred_id"] if cred_id not in creds: # NOTE: could be optimized if multiple creds are requested creds[cred_id] = await self._get_credential(cred_id) - timestamp = detail.get("timestamp") + timestamp, rev_state = get_rev_state(cred_id, detail) present_creds.add_predicates( creds[cred_id], reft, timestamp=timestamp, - rev_state=get_rev_state(cred_id, timestamp) if timestamp else None, + rev_state=rev_state, ) try: diff --git a/aries_cloudagent/indy/credx/issuer.py b/aries_cloudagent/indy/credx/issuer.py index 9db5b61b48..b4a4524948 100644 --- a/aries_cloudagent/indy/credx/issuer.py +++ b/aries_cloudagent/indy/credx/issuer.py @@ -20,7 +20,6 @@ ) from ...askar.profile import AskarProfile -from ...core.profile import ProfileSession from ..issuer import ( IndyIssuer, @@ -29,8 +28,6 @@ DEFAULT_CRED_DEF_TAG, DEFAULT_SIGNATURE_TYPE, ) -from ...revocation.models.issuer_cred_rev_record import IssuerCredRevRecord - LOGGER = logging.getLogger(__name__) @@ -110,7 +107,7 @@ async def credential_definition_in_wallet( async with self._profile.session() as session: return ( await session.handle.fetch( - CATEGORY_CRED_DEF_KEY_PROOF, credential_definition_id + CATEGORY_CRED_DEF_PRIVATE, credential_definition_id ) ) is not None except AskarError as err: @@ -226,7 +223,6 @@ async def create_credential( credential_offer: dict, credential_request: dict, credential_values: dict, - cred_ex_id: str, revoc_reg_id: str = None, tails_file_path: str = None, ) -> Tuple[str, str]: @@ -238,7 +234,6 @@ async def create_credential( credential_offer: Credential Offer to create credential for credential_request: Credential request to create credential for credential_values: Values to go in credential - cred_ex_id: credential exchange identifier to use in issuer cred rev rec revoc_reg_id: ID of the revocation registry tails_file_path: The location of the tails file @@ -325,20 +320,6 @@ async def create_credential( await txn.handle.replace( CATEGORY_REV_REG_INFO, revoc_reg_id, value_json=rev_info ) - - issuer_cr_rec = IssuerCredRevRecord( - state=IssuerCredRevRecord.STATE_ISSUED, - cred_ex_id=cred_ex_id, - rev_reg_id=revoc_reg_id, - cred_rev_id=str(rev_reg_index), - ) - await issuer_cr_rec.save( - txn, - reason=( - "Created issuer cred rev record for " - f"rev reg id {revoc_reg_id}, {rev_reg_index}" - ), - ) await txn.commit() except AskarError as err: raise IndyIssuerError( @@ -384,7 +365,6 @@ async def revoke_credentials( revoc_reg_id: str, tails_file_path: str, cred_revoc_ids: Sequence[str], - transaction: ProfileSession = None, ) -> Tuple[str, Sequence[str]]: """ Revoke a set of credentials in a revocation registry. @@ -399,66 +379,84 @@ async def revoke_credentials( """ - txn = transaction if transaction else await self._profile.transaction() - try: - rev_reg_def = await txn.handle.fetch(CATEGORY_REV_REG_DEF, revoc_reg_id) - rev_reg = await txn.handle.fetch( - CATEGORY_REV_REG, revoc_reg_id, for_update=True - ) - rev_reg_info = await txn.handle.fetch( - CATEGORY_REV_REG_INFO, revoc_reg_id, for_update=True - ) - if not rev_reg_def: - raise IndyIssuerError("Revocation registry definition not found") - if not rev_reg: - raise IndyIssuerError("Revocation registry not found") - if not rev_reg_info: - raise IndyIssuerError("Revocation registry metadata not found") - except AskarError as err: - raise IndyIssuerError("Error retrieving revocation registry") from err + delta = None + failed_crids = set() + max_attempt = 5 + attempt = 0 - try: - rev_reg_def = RevocationRegistryDefinition.load(rev_reg_def.raw_value) - except CredxError as err: - raise IndyIssuerError( - "Error loading revocation registry definition" - ) from err - - rev_crids = [] - failed_crids = [] - max_cred_num = rev_reg_def.max_cred_num - rev_info = rev_reg_info.value_json - used_ids = set(rev_info.get("used_ids") or []) - - for rev_id in cred_revoc_ids: - rev_id = int(rev_id) - if rev_id < 1 or rev_id > max_cred_num: - LOGGER.error( - "Skipping requested credential revocation" - "on rev reg id %s, cred rev id=%s not in range", - revoc_reg_id, - rev_id, - ) - elif rev_id > rev_info["curr_id"]: - LOGGER.warn( - "Skipping requested credential revocation" - "on rev reg id %s, cred rev id=%s not yet issued", - revoc_reg_id, - rev_id, - ) - elif rev_id in used_ids: - LOGGER.warn( - "Skipping requested credential revocation" - "on rev reg id %s, cred rev id=%s already revoked", - revoc_reg_id, - rev_id, - ) - else: - rev_crids.append(rev_id) + while True: + attempt += 1 + if attempt >= max_attempt: + raise IndyIssuerError("Repeated conflict attempting to update registry") + try: + async with self._profile.session() as session: + rev_reg_def = await session.handle.fetch( + CATEGORY_REV_REG_DEF, revoc_reg_id + ) + rev_reg = await session.handle.fetch(CATEGORY_REV_REG, revoc_reg_id) + rev_reg_info = await session.handle.fetch( + CATEGORY_REV_REG_INFO, revoc_reg_id + ) + if not rev_reg_def: + raise IndyIssuerError("Revocation registry definition not found") + if not rev_reg: + raise IndyIssuerError("Revocation registry not found") + if not rev_reg_info: + raise IndyIssuerError("Revocation registry metadata not found") + except AskarError as err: + raise IndyIssuerError("Error retrieving revocation registry") from err + + try: + rev_reg_def = RevocationRegistryDefinition.load(rev_reg_def.raw_value) + except CredxError as err: + raise IndyIssuerError( + "Error loading revocation registry definition" + ) from err + + rev_crids = set() + failed_crids = set() + max_cred_num = rev_reg_def.max_cred_num + rev_info = rev_reg_info.value_json + used_ids = set(rev_info.get("used_ids") or []) + + for rev_id in cred_revoc_ids: + rev_id = int(rev_id) + if rev_id < 1 or rev_id > max_cred_num: + LOGGER.error( + "Skipping requested credential revocation" + "on rev reg id %s, cred rev id=%s not in range", + revoc_reg_id, + rev_id, + ) + failed_crids.add(rev_id) + elif rev_id > rev_info["curr_id"]: + LOGGER.warn( + "Skipping requested credential revocation" + "on rev reg id %s, cred rev id=%s not yet issued", + revoc_reg_id, + rev_id, + ) + failed_crids.add(rev_id) + elif rev_id in used_ids: + LOGGER.warn( + "Skipping requested credential revocation" + "on rev reg id %s, cred rev id=%s already revoked", + revoc_reg_id, + rev_id, + ) + failed_crids.add(rev_id) + else: + rev_crids.add(rev_id) + + if not rev_crids: + break - if rev_crids: try: rev_reg = RevocationRegistry.load(rev_reg.raw_value) + except CredxError as err: + raise IndyIssuerError("Error loading revocation registry") from err + + try: delta = await asyncio.get_event_loop().run_in_executor( None, lambda: rev_reg.update( @@ -472,22 +470,41 @@ async def revoke_credentials( raise IndyIssuerError("Error updating revocation registry") from err try: - await txn.handle.replace( - CATEGORY_REV_REG, revoc_reg_id, rev_reg.to_json_buffer() - ) - used_ids.update(rev_crids) - rev_info["used_ids"] = list(used_ids) - await txn.handle.replace( - CATEGORY_REV_REG_INFO, revoc_reg_id, value_json=rev_info - ) - if not transaction: + async with self._profile.transaction() as txn: + rev_reg_upd = await txn.handle.fetch( + CATEGORY_REV_REG, revoc_reg_id, for_update=True + ) + rev_info_upd = await txn.handle.fetch( + CATEGORY_REV_REG_INFO, revoc_reg_id, for_update=True + ) + if not rev_reg_upd or not rev_reg_info: + LOGGER.warn( + "Revocation registry missing, skipping update: {}", + revoc_reg_id, + ) + delta = None + break + rev_info_upd = rev_info_upd.value_json + if rev_info_upd != rev_info: + # handle concurrent update to the registry by retrying + continue + await txn.handle.replace( + CATEGORY_REV_REG, revoc_reg_id, rev_reg.to_json_buffer() + ) + used_ids.update(rev_crids) + rev_info_upd["used_ids"] = sorted(used_ids) + await txn.handle.replace( + CATEGORY_REV_REG_INFO, revoc_reg_id, value_json=rev_info_upd + ) await txn.commit() except AskarError as err: raise IndyIssuerError("Error saving revocation registry") from err - else: - delta = None + break - return (delta and delta.to_json(), failed_crids) + return ( + delta and delta.to_json(), + [str(rev_id) for rev_id in sorted(failed_crids)], + ) async def merge_revocation_registry_deltas( self, fro_delta: str, to_delta: str @@ -558,7 +575,7 @@ async def create_and_store_revocation_registry( rev_reg_def, rev_reg_def_private, rev_reg, - rev_reg_delta, + _rev_reg_delta, ) = await asyncio.get_event_loop().run_in_executor( None, lambda: RevocationRegistryDefinition.create( diff --git a/aries_cloudagent/indy/credx/tests/test_cred_issuance.py b/aries_cloudagent/indy/credx/tests/test_cred_issuance.py index 0c23eda904..027c16b94d 100644 --- a/aries_cloudagent/indy/credx/tests/test_cred_issuance.py +++ b/aries_cloudagent/indy/credx/tests/test_cred_issuance.py @@ -137,7 +137,6 @@ async def test_issue_store_non_rev(self): cred_offer, cred_req, {"name": "NAME", "moniker": "MONIKER"}, - cred_ex_id="cred_ex_id", revoc_reg_id=None, tails_file_path=None, ) @@ -255,7 +254,6 @@ async def test_issue_store_rev(self): cred_offer, cred_req, {"name": "NAME", "moniker": "MONIKER"}, - cred_ex_id="cred_ex_id", revoc_reg_id=reg_id, tails_file_path=tails_path, ) diff --git a/aries_cloudagent/indy/credx/verifier.py b/aries_cloudagent/indy/credx/verifier.py index c6677cfa7b..e625076ecd 100644 --- a/aries_cloudagent/indy/credx/verifier.py +++ b/aries_cloudagent/indy/credx/verifier.py @@ -7,7 +7,7 @@ from ...core.profile import Profile -from ..verifier import IndyVerifier +from ..verifier import IndyVerifier, PresVerifyMsg LOGGER = logging.getLogger(__name__) @@ -33,7 +33,7 @@ async def verify_presentation( credential_definitions, rev_reg_defs, rev_reg_entries, - ) -> bool: + ) -> (bool, list): """ Verify a presentation. @@ -46,16 +46,21 @@ async def verify_presentation( rev_reg_entries: revocation registry entries """ + msgs = [] try: - self.non_revoc_intervals(pres_req, pres, credential_definitions) - await self.check_timestamps(self.profile, pres_req, pres, rev_reg_defs) - await self.pre_verify(pres_req, pres) + msgs += self.non_revoc_intervals(pres_req, pres, credential_definitions) + msgs += await self.check_timestamps( + self.profile, pres_req, pres, rev_reg_defs + ) + msgs += await self.pre_verify(pres_req, pres) except ValueError as err: + s = str(err) + msgs.append(f"{PresVerifyMsg.PRES_VALUE_ERROR.value}::{s}") LOGGER.error( f"Presentation on nonce={pres_req['nonce']} " f"cannot be validated: {str(err)}" ) - return False + return (False, msgs) try: presentation = Presentation.load(pres) @@ -68,11 +73,13 @@ async def verify_presentation( rev_reg_defs.values(), rev_reg_entries, ) - except CredxError: + except CredxError as err: + s = str(err) + msgs.append(f"{PresVerifyMsg.PRES_VERIFY_ERROR.value}::{s}") LOGGER.exception( f"Validation of presentation on nonce={pres_req['nonce']} " "failed with error" ) verified = False - return verified + return (verified, msgs) diff --git a/aries_cloudagent/indy/issuer.py b/aries_cloudagent/indy/issuer.py index 05c538cb43..d889746d41 100644 --- a/aries_cloudagent/indy/issuer.py +++ b/aries_cloudagent/indy/issuer.py @@ -4,7 +4,6 @@ from typing import Sequence, Tuple from ..core.error import BaseError -from ..core.profile import ProfileSession DEFAULT_CRED_DEF_TAG = "default" @@ -123,7 +122,6 @@ async def create_credential( credential_offer: dict, credential_request: dict, credential_values: dict, - cred_ex_id: str, revoc_reg_id: str = None, tails_file_path: str = None, ) -> Tuple[str, str]: @@ -135,7 +133,6 @@ async def create_credential( credential_offer: Credential Offer to create credential for credential_request: Credential request to create credential for credential_values: Values to go in credential - cred_ex_id: credential exchange identifier to use in issuer cred rev rec revoc_reg_id: ID of the revocation registry tails_file_path: The location of the tails file @@ -150,7 +147,6 @@ async def revoke_credentials( revoc_reg_id: str, tails_file_path: str, cred_rev_ids: Sequence[str], - transaction: ProfileSession = None, ) -> Tuple[str, Sequence[str]]: """ Revoke a set of credentials in a revocation registry. diff --git a/aries_cloudagent/indy/models/cred_request.py b/aries_cloudagent/indy/models/cred_request.py index 5a022bebc0..92a80e14d6 100644 --- a/aries_cloudagent/indy/models/cred_request.py +++ b/aries_cloudagent/indy/models/cred_request.py @@ -44,7 +44,7 @@ class Meta: unknown = EXCLUDE prover_did = fields.Str( - requred=True, + required=True, description="Prover DID", **INDY_DID, ) diff --git a/aries_cloudagent/indy/models/pres_preview.py b/aries_cloudagent/indy/models/pres_preview.py index 7ab771309f..0edb1cdb06 100644 --- a/aries_cloudagent/indy/models/pres_preview.py +++ b/aries_cloudagent/indy/models/pres_preview.py @@ -14,6 +14,7 @@ from ...messaging.models.base import BaseModel, BaseModelSchema from ...messaging.util import canon from ...messaging.valid import INDY_CRED_DEF_ID, INDY_PREDICATE +from ...multitenant.base import BaseMultitenantManager from ...protocols.didcomm_prefix import DIDCommPrefix from ...wallet.util import b64_to_str @@ -351,7 +352,11 @@ def non_revoc(cred_def_id: str) -> IndyNonRevocationInterval: revoc_support = False if cd_id: if profile: - ledger_exec_inst = profile.inject(IndyLedgerRequestsExecutor) + multitenant_mgr = profile.inject_or(BaseMultitenantManager) + if multitenant_mgr: + ledger_exec_inst = IndyLedgerRequestsExecutor(profile) + else: + ledger_exec_inst = profile.inject(IndyLedgerRequestsExecutor) ledger = ( await ledger_exec_inst.get_ledger_for_identifier( cd_id, @@ -398,7 +403,7 @@ def non_revoc(cred_def_id: str) -> IndyNonRevocationInterval: }, } - for (reft, attr_spec) in attr_specs_names.items(): + for reft, attr_spec in attr_specs_names.items(): proof_req["requested_attributes"][ "{}_{}_uuid".format( len(proof_req["requested_attributes"]), canon(attr_spec["names"][0]) @@ -410,7 +415,11 @@ def non_revoc(cred_def_id: str) -> IndyNonRevocationInterval: revoc_support = False if cd_id: if profile: - ledger_exec_inst = profile.inject(IndyLedgerRequestsExecutor) + multitenant_mgr = profile.inject_or(BaseMultitenantManager) + if multitenant_mgr: + ledger_exec_inst = IndyLedgerRequestsExecutor(profile) + else: + ledger_exec_inst = profile.inject(IndyLedgerRequestsExecutor) ledger = ( await ledger_exec_inst.get_ledger_for_identifier( cd_id, diff --git a/aries_cloudagent/indy/models/tests/test_pres_preview.py b/aries_cloudagent/indy/models/tests/test_pres_preview.py index eee3071e49..5f5809b889 100644 --- a/aries_cloudagent/indy/models/tests/test_pres_preview.py +++ b/aries_cloudagent/indy/models/tests/test_pres_preview.py @@ -13,6 +13,8 @@ IndyLedgerRequestsExecutor, ) from ....messaging.util import canon +from ....multitenant.base import BaseMultitenantManager +from ....multitenant.manager import MultitenantManager from ....protocols.didcomm_prefix import DIDCommPrefix @@ -443,6 +445,10 @@ async def test_to_indy_proof_request_revo(self): context.injector.bind_instance( IndyLedgerRequestsExecutor, IndyLedgerRequestsExecutor(mock_profile) ) + context.injector.bind_instance( + BaseMultitenantManager, + async_mock.MagicMock(MultitenantManager, autospec=True), + ) with async_mock.patch.object( IndyLedgerRequestsExecutor, "get_ledger_for_identifier" ) as mock_get_ledger: diff --git a/aries_cloudagent/indy/models/xform.py b/aries_cloudagent/indy/models/xform.py index 9e7d5e7601..afcb635fe2 100644 --- a/aries_cloudagent/indy/models/xform.py +++ b/aries_cloudagent/indy/models/xform.py @@ -30,7 +30,7 @@ async def indy_proof_req_preview2indy_requested_creds( "requested_predicates": {}, } - for (referent, req_item) in indy_proof_req["requested_attributes"].items(): + for referent, req_item in indy_proof_req["requested_attributes"].items(): credentials = await holder.get_credentials_for_presentation_request_by_referent( presentation_request=indy_proof_req, referents=(referent,), @@ -116,7 +116,7 @@ def indy_proof_req2non_revoc_intervals(indy_proof_req: dict): """Return non-revocation intervals by requested item referent in proof request.""" non_revoc_intervals = {} for req_item_type in ("requested_attributes", "requested_predicates"): - for (reft, req_item) in indy_proof_req[req_item_type].items(): + for reft, req_item in indy_proof_req[req_item_type].items(): interval = req_item.get( "non_revoked", indy_proof_req.get("non_revoked"), diff --git a/aries_cloudagent/indy/sdk/holder.py b/aries_cloudagent/indy/sdk/holder.py index 48cb2abac0..efb1a4f3ba 100644 --- a/aries_cloudagent/indy/sdk/holder.py +++ b/aries_cloudagent/indy/sdk/holder.py @@ -226,11 +226,13 @@ async def fetch(reft, limit): with IndyErrorHandler( "Error when constructing wallet credential query", IndyHolderError ): - search_handle = await ( - indy.anoncreds.prover_search_credentials_for_proof_req( - self.wallet.handle, - json.dumps(presentation_request), - json.dumps(extra_query), + search_handle = ( + await ( + indy.anoncreds.prover_search_credentials_for_proof_req( + self.wallet.handle, + json.dumps(presentation_request), + json.dumps(extra_query), + ) ) ) diff --git a/aries_cloudagent/indy/sdk/issuer.py b/aries_cloudagent/indy/sdk/issuer.py index 2298143e61..3d84473c93 100644 --- a/aries_cloudagent/indy/sdk/issuer.py +++ b/aries_cloudagent/indy/sdk/issuer.py @@ -8,10 +8,8 @@ import indy.blob_storage from indy.error import AnoncredsRevocationRegistryFullError, IndyError, ErrorCode -from ...core.profile import ProfileSession from ...indy.sdk.profile import IndySdkProfile from ...messaging.util import encode -from ...revocation.models.issuer_cred_rev_record import IssuerCredRevRecord from ...storage.error import StorageError from ..issuer import ( @@ -163,7 +161,6 @@ async def create_credential( credential_offer: dict, credential_request: dict, credential_values: dict, - cred_ex_id: str, rev_reg_id: str = None, tails_file_path: str = None, ) -> Tuple[str, str]: @@ -175,7 +172,6 @@ async def create_credential( credential_offer: Credential Offer to create credential for credential_request: Credential request to create credential for credential_values: Values to go in credential - cred_ex_id: credential exchange identifier to use in issuer cred rev rec rev_reg_id: ID of the revocation registry tails_file_path: Path to the local tails file @@ -220,22 +216,6 @@ async def create_credential( rev_reg_id, tails_reader_handle, ) - - if cred_rev_id: - issuer_cr_rec = IssuerCredRevRecord( - state=IssuerCredRevRecord.STATE_ISSUED, - cred_ex_id=cred_ex_id, - rev_reg_id=rev_reg_id, - cred_rev_id=cred_rev_id, - ) - async with self.profile.session() as session: - await issuer_cr_rec.save( - session, - reason=( - "Created issuer cred rev record for " - f"rev reg id {rev_reg_id}, {cred_rev_id}" - ), - ) except AnoncredsRevocationRegistryFullError: LOGGER.warning( "Revocation registry %s is full: cannot create credential", @@ -267,7 +247,6 @@ async def revoke_credentials( rev_reg_id: str, tails_file_path: str, cred_rev_ids: Sequence[str], - transaction: ProfileSession = None, ) -> Tuple[str, Sequence[str]]: """ Revoke a set of credentials in a revocation registry. @@ -281,31 +260,21 @@ async def revoke_credentials( Tuple with the combined revocation delta, list of cred rev ids not revoked """ - failed_crids = [] + failed_crids = set() tails_reader_handle = await create_tails_reader(tails_file_path) result_json = None - for cred_rev_id in cred_rev_ids: + for cred_rev_id in set(cred_rev_ids): with IndyErrorHandler( "Exception when revoking credential", IndyIssuerError ): try: - session = await self.profile.session() delta_json = await indy.anoncreds.issuer_revoke_credential( self.profile.wallet.handle, tails_reader_handle, rev_reg_id, cred_rev_id, ) - issuer_cr_rec = await IssuerCredRevRecord.retrieve_by_ids( - session, - rev_reg_id, - cred_rev_id, - ) - await issuer_cr_rec.set_state( - session, IssuerCredRevRecord.STATE_REVOKED - ) - except IndyError as err: if err.error_code == ErrorCode.AnoncredsInvalidUserRevocId: LOGGER.error( @@ -323,7 +292,7 @@ async def revoke_credentials( err, "Revocation error", IndyIssuerError ).roll_up ) - failed_crids.append(cred_rev_id) + failed_crids.add(int(cred_rev_id)) continue except StorageError as err: LOGGER.warning( @@ -344,7 +313,7 @@ async def revoke_credentials( else: result_json = delta_json - return (result_json, failed_crids) + return (result_json, [str(rev_id) for rev_id in sorted(failed_crids)]) async def merge_revocation_registry_deltas( self, fro_delta: str, to_delta: str diff --git a/aries_cloudagent/indy/sdk/profile.py b/aries_cloudagent/indy/sdk/profile.py index 24badc748a..525e52917b 100644 --- a/aries_cloudagent/indy/sdk/profile.py +++ b/aries_cloudagent/indy/sdk/profile.py @@ -1,9 +1,10 @@ """Manage Indy-SDK profile interaction.""" +import asyncio import logging from typing import Any, Mapping -from weakref import ref +from weakref import finalize, ref from ...config.injection_context import InjectionContext from ...config.provider import ClassProvider @@ -30,13 +31,18 @@ class IndySdkProfile(Profile): BACKEND_NAME = "indy" - def __init__(self, opened: IndyOpenWallet, context: InjectionContext = None): + def __init__( + self, + opened: IndyOpenWallet, + context: InjectionContext = None, + ): """Create a new IndyProfile instance.""" super().__init__(context=context, name=opened.name, created=opened.created) self.opened = opened self.ledger_pool: IndySdkLedgerPool = None self.init_ledger_pool() self.bind_providers() + self._finalizer = self._make_finalizer(opened) @property def name(self) -> str: @@ -116,6 +122,24 @@ async def close(self): await self.opened.close() self.opened = None + def _make_finalizer(self, opened: IndyOpenWallet) -> finalize: + """Return a finalizer for this profile. + + See docs for weakref.finalize for more details on behavior of finalizers. + """ + + async def _closer(opened: IndyOpenWallet): + try: + await opened.close() + except Exception: + LOGGER.exception("Failed to close wallet from finalizer") + + def _finalize(opened: IndyOpenWallet): + LOGGER.debug("Profile finalizer called; closing wallet") + asyncio.get_event_loop().create_task(_closer(opened)) + + return finalize(self, _finalize, opened) + async def remove(self): """Remove the profile associated with this instance.""" if not self.opened: diff --git a/aries_cloudagent/indy/sdk/tests/test_issuer.py b/aries_cloudagent/indy/sdk/tests/test_issuer.py index 5981233abc..3b007e788e 100644 --- a/aries_cloudagent/indy/sdk/tests/test_issuer.py +++ b/aries_cloudagent/indy/sdk/tests/test_issuer.py @@ -51,7 +51,8 @@ async def setUp(self): "name": "test-wallet", } ).create_wallet() - self.profile = IndySdkProfile(self.wallet, self.context) + with async_mock.patch.object(IndySdkProfile, "_make_finalizer"): + self.profile = IndySdkProfile(self.wallet, self.context) self.issuer = test_module.IndySdkIssuer(self.profile) async def tearDown(self): @@ -166,58 +167,46 @@ async def test_create_revoke_credentials( for cr_id in test_cred_rev_ids ] - with async_mock.patch.object( - test_module, "IssuerCredRevRecord", async_mock.MagicMock() - ) as mock_issuer_cr_rec: - mock_issuer_cr_rec.return_value.save = async_mock.CoroutineMock() - mock_issuer_cr_rec.retrieve_by_ids = async_mock.CoroutineMock( - return_value=async_mock.MagicMock( - set_state=async_mock.CoroutineMock(), - ) - ) - - with self.assertRaises(test_module.IndyIssuerError): # missing attribute - cred_json, revoc_id = await self.issuer.create_credential( - test_schema, - test_offer, - test_request, - {}, - "dummy-cxid", - ) - - (cred_json, cred_rev_id) = await self.issuer.create_credential( # main line + with self.assertRaises(test_module.IndyIssuerError): # missing attribute + cred_json, revoc_id = await self.issuer.create_credential( test_schema, test_offer, test_request, - test_values, - "dummy-cxid", - REV_REG_ID, - "/tmp/tails/path/dummy", - ) - mock_indy_create_credential.assert_called_once() - ( - call_wallet, - call_offer, - call_request, - call_values, - call_etc1, - call_etc2, - ) = mock_indy_create_credential.call_args[0] - assert call_wallet is self.wallet.handle - assert json.loads(call_offer) == test_offer - assert json.loads(call_request) == test_request - values = json.loads(call_values) - assert "attr1" in values - - mock_indy_revoke_credential.return_value = json.dumps(TEST_RR_DELTA) - mock_indy_merge_rr_deltas.return_value = json.dumps(TEST_RR_DELTA) - (result, failed) = await self.issuer.revoke_credentials( - REV_REG_ID, tails_file_path="dummy", cred_rev_ids=test_cred_rev_ids + {}, ) - assert json.loads(result) == TEST_RR_DELTA - assert not failed - assert mock_indy_revoke_credential.call_count == 2 - mock_indy_merge_rr_deltas.assert_called_once() + + (cred_json, cred_rev_id) = await self.issuer.create_credential( # main line + test_schema, + test_offer, + test_request, + test_values, + REV_REG_ID, + "/tmp/tails/path/dummy", + ) + mock_indy_create_credential.assert_called_once() + ( + call_wallet, + call_offer, + call_request, + call_values, + call_etc1, + call_etc2, + ) = mock_indy_create_credential.call_args[0] + assert call_wallet is self.wallet.handle + assert json.loads(call_offer) == test_offer + assert json.loads(call_request) == test_request + values = json.loads(call_values) + assert "attr1" in values + + mock_indy_revoke_credential.return_value = json.dumps(TEST_RR_DELTA) + mock_indy_merge_rr_deltas.return_value = json.dumps(TEST_RR_DELTA) + (result, failed) = await self.issuer.revoke_credentials( + REV_REG_ID, tails_file_path="dummy", cred_rev_ids=test_cred_rev_ids + ) + assert json.loads(result) == TEST_RR_DELTA + assert not failed + assert mock_indy_revoke_credential.call_count == 2 + mock_indy_merge_rr_deltas.assert_called_once() @async_mock.patch("indy.anoncreds.issuer_create_credential") @async_mock.patch.object(test_module, "create_tails_reader", autospec=True) @@ -266,70 +255,53 @@ async def test_create_revoke_credentials_x( test_offer, test_request, {}, - "dummy-cxid", ) - with async_mock.patch.object( - test_module, "IssuerCredRevRecord", async_mock.MagicMock() - ) as mock_issuer_cr_rec: - mock_issuer_cr_rec.return_value.save = async_mock.CoroutineMock( - side_effect=test_module.StorageError( - "could not store" # not fatal; maximize coverage - ) - ) - mock_issuer_cr_rec.retrieve_by_ids = async_mock.CoroutineMock( - return_value=async_mock.MagicMock( - set_state=async_mock.CoroutineMock( - side_effect=test_module.StorageError( - "could not store" # not fatal; maximize coverage - ) - ), - ) - ) - - (cred_json, cred_rev_id) = await self.issuer.create_credential( # main line - test_schema, - test_offer, - test_request, - test_values, - "dummy-cxid", - REV_REG_ID, - "/tmp/tails/path/dummy", - ) - mock_indy_create_credential.assert_called_once() - ( - call_wallet, - call_offer, - call_request, - call_values, - call_etc1, - call_etc2, - ) = mock_indy_create_credential.call_args[0] - assert call_wallet is self.wallet.handle - assert json.loads(call_offer) == test_offer - assert json.loads(call_request) == test_request - values = json.loads(call_values) - assert "attr1" in values - - mock_indy_revoke_credential.side_effect = [ - json.dumps(TEST_RR_DELTA), - IndyError( + (cred_json, cred_rev_id) = await self.issuer.create_credential( # main line + test_schema, + test_offer, + test_request, + test_values, + REV_REG_ID, + "/tmp/tails/path/dummy", + ) + mock_indy_create_credential.assert_called_once() + ( + call_wallet, + call_offer, + call_request, + call_values, + call_etc1, + call_etc2, + ) = mock_indy_create_credential.call_args[0] + assert call_wallet is self.wallet.handle + assert json.loads(call_offer) == test_offer + assert json.loads(call_request) == test_request + values = json.loads(call_values) + assert "attr1" in values + + def mock_revoke(_h, _t, _r, cred_rev_id): + if cred_rev_id == "42": + return json.dumps(TEST_RR_DELTA) + if cred_rev_id == "54": + raise IndyError( error_code=ErrorCode.AnoncredsInvalidUserRevocId, error_details={"message": "already revoked"}, - ), - IndyError( - error_code=ErrorCode.UnknownCryptoTypeError, - error_details={"message": "truly an outlier"}, - ), - ] - mock_indy_merge_rr_deltas.return_value = json.dumps(TEST_RR_DELTA) - (result, failed) = await self.issuer.revoke_credentials( - REV_REG_ID, tails_file_path="dummy", cred_rev_ids=test_cred_rev_ids + ) + raise IndyError( + error_code=ErrorCode.UnknownCryptoTypeError, + error_details={"message": "truly an outlier"}, ) - assert json.loads(result) == TEST_RR_DELTA - assert failed == ["54", "103"] - assert mock_indy_revoke_credential.call_count == 3 - mock_indy_merge_rr_deltas.assert_not_called() + + mock_indy_revoke_credential.side_effect = mock_revoke + mock_indy_merge_rr_deltas.return_value = json.dumps(TEST_RR_DELTA) + (result, failed) = await self.issuer.revoke_credentials( + REV_REG_ID, tails_file_path="dummy", cred_rev_ids=test_cred_rev_ids + ) + assert json.loads(result) == TEST_RR_DELTA + assert failed == ["54", "103"] + assert mock_indy_revoke_credential.call_count == 3 + mock_indy_merge_rr_deltas.assert_not_called() @async_mock.patch("indy.anoncreds.issuer_create_credential") @async_mock.patch.object(test_module, "create_tails_reader", autospec=True) @@ -354,25 +326,14 @@ async def test_create_credential_rr_full( error_code=ErrorCode.AnoncredsRevocationRegistryFullError ) - with async_mock.patch.object( - test_module, "IssuerCredRevRecord", async_mock.MagicMock() - ) as mock_issuer_cr_rec: - mock_issuer_cr_rec.return_value.save = async_mock.CoroutineMock() - mock_issuer_cr_rec.retrieve_by_ids = async_mock.CoroutineMock( - return_value=async_mock.MagicMock( - set_state=async_mock.CoroutineMock(), - ) + with self.assertRaises(IndyIssuerRevocationRegistryFullError): + await self.issuer.create_credential( + test_schema, + test_offer, + test_request, + test_values, ) - with self.assertRaises(IndyIssuerRevocationRegistryFullError): - await self.issuer.create_credential( - test_schema, - test_offer, - test_request, - test_values, - "dummy-cxid", - ) - @async_mock.patch("indy.anoncreds.issuer_create_credential") @async_mock.patch.object(test_module, "create_tails_reader", autospec=True) async def test_create_credential_x_indy( @@ -397,25 +358,14 @@ async def test_create_credential_x_indy( error_code=ErrorCode.WalletInvalidHandle ) - with async_mock.patch.object( - test_module, "IssuerCredRevRecord", async_mock.MagicMock() - ) as mock_issuer_cr_rec: - mock_issuer_cr_rec.return_value.save = async_mock.CoroutineMock() - mock_issuer_cr_rec.retrieve_by_ids = async_mock.CoroutineMock( - return_value=async_mock.MagicMock( - set_state=async_mock.CoroutineMock(), - ) + with self.assertRaises(test_module.IndyIssuerError): + await self.issuer.create_credential( + test_schema, + test_offer, + test_request, + test_values, ) - with self.assertRaises(test_module.IndyIssuerError): - await self.issuer.create_credential( - test_schema, - test_offer, - test_request, - test_values, - "dummy-cxid", - ) - @async_mock.patch("indy.anoncreds.issuer_create_and_store_revoc_reg") @async_mock.patch.object(test_module, "create_tails_writer", autospec=True) async def test_create_and_store_revocation_registry( diff --git a/aries_cloudagent/indy/sdk/tests/test_profile.py b/aries_cloudagent/indy/sdk/tests/test_profile.py index 6db4425574..8047d4cea6 100644 --- a/aries_cloudagent/indy/sdk/tests/test_profile.py +++ b/aries_cloudagent/indy/sdk/tests/test_profile.py @@ -1,89 +1,86 @@ -import pytest +import asyncio +import logging from asynctest import mock as async_mock +import pytest from ....config.injection_context import InjectionContext from ....core.error import ProfileError from ....ledger.indy import IndySdkLedgerPool - from ..profile import IndySdkProfile -from ..wallet_setup import IndyWalletConfig, IndyOpenWallet +from ..wallet_setup import IndyOpenWallet, IndyWalletConfig + + +@pytest.fixture +async def open_wallet(): + opened = IndyOpenWallet( + config=IndyWalletConfig({"name": "test-profile"}), + created=True, + handle=1, + master_secret_id="master-secret", + ) + with async_mock.patch.object(opened, "close", async_mock.CoroutineMock()): + yield opened @pytest.fixture() -async def profile(): +async def profile(open_wallet): context = InjectionContext() context.injector.bind_instance(IndySdkLedgerPool, IndySdkLedgerPool("name")) - yield IndySdkProfile( - IndyOpenWallet( - config=IndyWalletConfig({"name": "test-profile"}), - created=True, - handle=1, - master_secret_id="master-secret", - ), - context, - ) + profile = IndySdkProfile(open_wallet, context) + + yield profile + # Trigger finalizer before event loop fixture is closed + profile._finalizer() -class TestIndySdkProfile: - @pytest.mark.asyncio - async def test_properties(self, profile): - assert profile.name == "test-profile" - assert profile.backend == "indy" - assert profile.wallet and profile.wallet.handle == 1 - assert "IndySdkProfile" in str(profile) - assert profile.created - assert profile.wallet.created - assert profile.wallet.master_secret_id == "master-secret" +@pytest.mark.asyncio +async def test_properties(profile: IndySdkProfile): + assert profile.name == "test-profile" + assert profile.backend == "indy" + assert profile.wallet and profile.wallet.handle == 1 - with async_mock.patch.object(profile, "opened", False): - with pytest.raises(ProfileError): - await profile.remove() + assert "IndySdkProfile" in str(profile) + assert profile.created + assert profile.wallet.created + assert profile.wallet.master_secret_id == "master-secret" - with async_mock.patch.object( - profile.opened, "close", async_mock.CoroutineMock() - ): + with async_mock.patch.object(profile, "opened", False): + with pytest.raises(ProfileError): await profile.remove() - assert profile.opened is None - - def test_settings_genesis_transactions(self): - context = InjectionContext( - settings={"ledger.genesis_transactions": async_mock.MagicMock()} - ) - context.injector.bind_instance(IndySdkLedgerPool, IndySdkLedgerPool("name")) - profile = IndySdkProfile( - IndyOpenWallet( - config=IndyWalletConfig({"name": "test-profile"}), - created=True, - handle=1, - master_secret_id="master-secret", - ), - context, - ) - - def test_settings_ledger_config(self): - context = InjectionContext(settings={"ledger.ledger_config_list": True}) - context.injector.bind_instance(IndySdkLedgerPool, IndySdkLedgerPool("name")) - profile = IndySdkProfile( - IndyOpenWallet( - config=IndyWalletConfig({"name": "test-profile"}), - created=True, - handle=1, - master_secret_id="master-secret", - ), - context, - ) - - def test_read_only(self): - context = InjectionContext(settings={"ledger.read_only": True}) - context.injector.bind_instance(IndySdkLedgerPool, IndySdkLedgerPool("name")) - ro_profile = IndySdkProfile( - IndyOpenWallet( - config=IndyWalletConfig({"name": "test-profile"}), - created=True, - handle=1, - master_secret_id="master-secret", - ), - context, - ) + + with async_mock.patch.object(profile.opened, "close", async_mock.CoroutineMock()): + await profile.remove() + assert profile.opened is None + + +def test_settings_genesis_transactions(open_wallet): + context = InjectionContext( + settings={"ledger.genesis_transactions": async_mock.MagicMock()} + ) + context.injector.bind_instance(IndySdkLedgerPool, IndySdkLedgerPool("name")) + profile = IndySdkProfile(open_wallet, context) + + +def test_settings_ledger_config(open_wallet): + context = InjectionContext(settings={"ledger.ledger_config_list": True}) + context.injector.bind_instance(IndySdkLedgerPool, IndySdkLedgerPool("name")) + profile = IndySdkProfile(open_wallet, context) + + +def test_read_only(open_wallet): + context = InjectionContext(settings={"ledger.read_only": True}) + context.injector.bind_instance(IndySdkLedgerPool, IndySdkLedgerPool("name")) + ro_profile = IndySdkProfile(open_wallet, context) + + +def test_finalizer(open_wallet, caplog): + def _smaller_scope(): + profile = IndySdkProfile(open_wallet) + assert profile + + with caplog.at_level(logging.DEBUG): + _smaller_scope() + + assert "finalizer called" in caplog.text diff --git a/aries_cloudagent/indy/sdk/tests/test_util.py b/aries_cloudagent/indy/sdk/tests/test_util.py index cb6a55ea50..be3f5ee36b 100644 --- a/aries_cloudagent/indy/sdk/tests/test_util.py +++ b/aries_cloudagent/indy/sdk/tests/test_util.py @@ -1,20 +1,15 @@ import pytest -from os import makedirs -from os.path import join -from pathlib import Path from shutil import rmtree import indy.blob_storage from asynctest import mock as async_mock, TestCase as AsyncTestCase -from ...util import indy_client_dir, generate_pr_nonce, tails_path +from ...util import indy_client_dir, generate_pr_nonce from ..util import create_tails_reader, create_tails_writer -from .. import util as test_module - @pytest.mark.indy class TestIndyUtils(AsyncTestCase): @@ -49,19 +44,3 @@ async def test_tails_writer(self): async def test_nonce(self): assert await generate_pr_nonce() - - async def test_tails_path(self): - tails_dir = indy_client_dir("tails", create=False) - rmtree(tails_dir, ignore_errors=True) - - tails_local_path = tails_path("rev-reg-id") - assert tails_local_path is None - - tails_rr_dir = indy_client_dir(join("tails", "rev-reg-id"), create=True) - tails_local_path = tails_path("rev-reg-id") - assert tails_local_path is None - - with open(join(tails_rr_dir, "tails-hash"), "w") as f: - f.write("content") - tails_local_path = tails_path("rev-reg-id") - assert tails_local_path diff --git a/aries_cloudagent/indy/sdk/tests/test_verifier.py b/aries_cloudagent/indy/sdk/tests/test_verifier.py index 44784b1ad5..d4abc1bdd1 100644 --- a/aries_cloudagent/indy/sdk/tests/test_verifier.py +++ b/aries_cloudagent/indy/sdk/tests/test_verifier.py @@ -336,7 +336,7 @@ async def test_verify_presentation(self, mock_verify): ) as mock_get_ledger: mock_get_ledger.return_value = (None, self.ledger) INDY_PROOF_REQ_X = deepcopy(INDY_PROOF_REQ_PRED_NAMES) - verified = await self.verifier.verify_presentation( + (verified, msgs) = await self.verifier.verify_presentation( INDY_PROOF_REQ_X, INDY_PROOF_PRED_NAMES, "schemas", @@ -370,7 +370,7 @@ async def test_verify_presentation_x_indy(self, mock_verify): IndyLedgerRequestsExecutor, "get_ledger_for_identifier" ) as mock_get_ledger: mock_get_ledger.return_value = ("test", self.ledger) - verified = await self.verifier.verify_presentation( + (verified, msgs) = await self.verifier.verify_presentation( INDY_PROOF_REQ_NAME, INDY_PROOF_NAME, "schemas", @@ -397,7 +397,7 @@ async def test_check_encoding_attr(self, mock_verify): ) as mock_get_ledger: mock_get_ledger.return_value = (None, self.ledger) mock_verify.return_value = True - verified = await self.verifier.verify_presentation( + (verified, msgs) = await self.verifier.verify_presentation( INDY_PROOF_REQ_NAME, INDY_PROOF_NAME, "schemas", @@ -415,6 +415,8 @@ async def test_check_encoding_attr(self, mock_verify): json.dumps("rev_reg_entries"), ) assert verified is True + assert len(msgs) == 1 + assert "TS_OUT_NRI::19_uuid" in msgs @async_mock.patch("indy.anoncreds.verifier_verify_proof") async def test_check_encoding_attr_tamper_raw(self, mock_verify): @@ -426,7 +428,7 @@ async def test_check_encoding_attr_tamper_raw(self, mock_verify): IndyLedgerRequestsExecutor, "get_ledger_for_identifier" ) as mock_get_ledger: mock_get_ledger.return_value = ("test", self.ledger) - verified = await self.verifier.verify_presentation( + (verified, msgs) = await self.verifier.verify_presentation( INDY_PROOF_REQ_NAME, INDY_PROOF_X, "schemas", @@ -438,6 +440,11 @@ async def test_check_encoding_attr_tamper_raw(self, mock_verify): mock_verify.assert_not_called() assert verified is False + assert len(msgs) == 2 + assert "TS_OUT_NRI::19_uuid" in msgs + assert ( + "VALUE_ERROR::Encoded representation mismatch for 'Preferred Name'" in msgs + ) @async_mock.patch("indy.anoncreds.verifier_verify_proof") async def test_check_encoding_attr_tamper_encoded(self, mock_verify): @@ -449,7 +456,7 @@ async def test_check_encoding_attr_tamper_encoded(self, mock_verify): IndyLedgerRequestsExecutor, "get_ledger_for_identifier" ) as mock_get_ledger: mock_get_ledger.return_value = (None, self.ledger) - verified = await self.verifier.verify_presentation( + (verified, msgs) = await self.verifier.verify_presentation( INDY_PROOF_REQ_NAME, INDY_PROOF_X, "schemas", @@ -461,6 +468,11 @@ async def test_check_encoding_attr_tamper_encoded(self, mock_verify): mock_verify.assert_not_called() assert verified is False + assert len(msgs) == 2 + assert "TS_OUT_NRI::19_uuid" in msgs + assert ( + "VALUE_ERROR::Encoded representation mismatch for 'Preferred Name'" in msgs + ) @async_mock.patch("indy.anoncreds.verifier_verify_proof") async def test_check_pred_names(self, mock_verify): @@ -470,7 +482,7 @@ async def test_check_pred_names(self, mock_verify): mock_get_ledger.return_value = ("test", self.ledger) mock_verify.return_value = True INDY_PROOF_REQ_X = deepcopy(INDY_PROOF_REQ_PRED_NAMES) - verified = await self.verifier.verify_presentation( + (verified, msgs) = await self.verifier.verify_presentation( INDY_PROOF_REQ_X, INDY_PROOF_PRED_NAMES, "schemas", @@ -491,6 +503,10 @@ async def test_check_pred_names(self, mock_verify): ) assert verified is True + assert len(msgs) == 3 + assert "TS_OUT_NRI::18_uuid" in msgs + assert "TS_OUT_NRI::18_id_GE_uuid" in msgs + assert "TS_OUT_NRI::18_busid_GE_uuid" in msgs @async_mock.patch("indy.anoncreds.verifier_verify_proof") async def test_check_pred_names_tamper_pred_value(self, mock_verify): @@ -502,7 +518,7 @@ async def test_check_pred_names_tamper_pred_value(self, mock_verify): IndyLedgerRequestsExecutor, "get_ledger_for_identifier" ) as mock_get_ledger: mock_get_ledger.return_value = (None, self.ledger) - verified = await self.verifier.verify_presentation( + (verified, msgs) = await self.verifier.verify_presentation( deepcopy(INDY_PROOF_REQ_PRED_NAMES), INDY_PROOF_X, "schemas", @@ -514,6 +530,14 @@ async def test_check_pred_names_tamper_pred_value(self, mock_verify): mock_verify.assert_not_called() assert verified is False + assert len(msgs) == 4 + assert "RMV_RFNT_NRI::18_uuid" in msgs + assert "RMV_RFNT_NRI::18_busid_GE_uuid" in msgs + assert "RMV_RFNT_NRI::18_id_GE_uuid" in msgs + assert ( + "VALUE_ERROR::Timestamp on sub-proof #0 is superfluous vs. requested attribute group 18_uuid" + in msgs + ) @async_mock.patch("indy.anoncreds.verifier_verify_proof") async def test_check_pred_names_tamper_pred_req_attr(self, mock_verify): @@ -523,7 +547,7 @@ async def test_check_pred_names_tamper_pred_req_attr(self, mock_verify): IndyLedgerRequestsExecutor, "get_ledger_for_identifier" ) as mock_get_ledger: mock_get_ledger.return_value = (None, self.ledger) - verified = await self.verifier.verify_presentation( + (verified, msgs) = await self.verifier.verify_presentation( INDY_PROOF_REQ_X, INDY_PROOF_PRED_NAMES, "schemas", @@ -535,6 +559,14 @@ async def test_check_pred_names_tamper_pred_req_attr(self, mock_verify): mock_verify.assert_not_called() assert verified is False + assert len(msgs) == 4 + assert "RMV_RFNT_NRI::18_uuid" in msgs + assert "RMV_RFNT_NRI::18_busid_GE_uuid" in msgs + assert "RMV_RFNT_NRI::18_id_GE_uuid" in msgs + assert ( + "VALUE_ERROR::Timestamp on sub-proof #0 is superfluous vs. requested attribute group 18_uuid" + in msgs + ) @async_mock.patch("indy.anoncreds.verifier_verify_proof") async def test_check_pred_names_tamper_attr_groups(self, mock_verify): @@ -546,7 +578,7 @@ async def test_check_pred_names_tamper_attr_groups(self, mock_verify): IndyLedgerRequestsExecutor, "get_ledger_for_identifier" ) as mock_get_ledger: mock_get_ledger.return_value = ("test", self.ledger) - verified = await self.verifier.verify_presentation( + (verified, msgs) = await self.verifier.verify_presentation( deepcopy(INDY_PROOF_REQ_PRED_NAMES), INDY_PROOF_X, "schemas", @@ -558,3 +590,7 @@ async def test_check_pred_names_tamper_attr_groups(self, mock_verify): mock_verify.assert_not_called() assert verified is False + assert len(msgs) == 3 + assert "RMV_RFNT_NRI::18_busid_GE_uuid" in msgs + assert "RMV_RFNT_NRI::18_id_GE_uuid" in msgs + assert "VALUE_ERROR::Missing requested attribute group 18_uuid" in msgs diff --git a/aries_cloudagent/indy/sdk/verifier.py b/aries_cloudagent/indy/sdk/verifier.py index f7695a9e0e..5c67463eed 100644 --- a/aries_cloudagent/indy/sdk/verifier.py +++ b/aries_cloudagent/indy/sdk/verifier.py @@ -8,7 +8,7 @@ from ...core.profile import Profile -from ..verifier import IndyVerifier +from ..verifier import IndyVerifier, PresVerifyMsg LOGGER = logging.getLogger(__name__) @@ -34,7 +34,7 @@ async def verify_presentation( credential_definitions, rev_reg_defs, rev_reg_entries, - ) -> bool: + ) -> (bool, list): """ Verify a presentation. @@ -47,17 +47,26 @@ async def verify_presentation( rev_reg_entries: revocation registry entries """ + LOGGER.debug(f">>> received presentation: {pres}") + LOGGER.debug(f">>> for pres_req: {pres_req}") + msgs = [] try: - self.non_revoc_intervals(pres_req, pres, credential_definitions) - await self.check_timestamps(self.profile, pres_req, pres, rev_reg_defs) - await self.pre_verify(pres_req, pres) + msgs += self.non_revoc_intervals(pres_req, pres, credential_definitions) + msgs += await self.check_timestamps( + self.profile, pres_req, pres, rev_reg_defs + ) + msgs += await self.pre_verify(pres_req, pres) except ValueError as err: + s = str(err) + msgs.append(f"{PresVerifyMsg.PRES_VALUE_ERROR.value}::{s}") LOGGER.error( f"Presentation on nonce={pres_req['nonce']} " f"cannot be validated: {str(err)}" ) - return False + return (False, msgs) + LOGGER.debug(f">>> verifying presentation: {pres}") + LOGGER.debug(f">>> for pres_req: {pres_req}") try: verified = await indy.anoncreds.verifier_verify_proof( json.dumps(pres_req), @@ -67,11 +76,13 @@ async def verify_presentation( json.dumps(rev_reg_defs), json.dumps(rev_reg_entries), ) - except IndyError: + except IndyError as err: + s = str(err) + msgs.append(f"{PresVerifyMsg.PRES_VERIFY_ERROR.value}::{s}") LOGGER.exception( f"Validation of presentation on nonce={pres_req['nonce']} " "failed with error" ) verified = False - return verified + return (verified, msgs) diff --git a/aries_cloudagent/indy/tests/test_verifier.py b/aries_cloudagent/indy/tests/test_verifier.py index 7a00c7276d..90b75b92a6 100644 --- a/aries_cloudagent/indy/tests/test_verifier.py +++ b/aries_cloudagent/indy/tests/test_verifier.py @@ -10,6 +10,8 @@ from ...ledger.multiple_ledger.ledger_requests_executor import ( IndyLedgerRequestsExecutor, ) +from ...multitenant.base import BaseMultitenantManager +from ...multitenant.manager import MultitenantManager from .. import verifier as test_module from ..verifier import IndyVerifier @@ -332,6 +334,28 @@ def setUp(self): self.verifier = MockVerifier() async def test_check_timestamps(self): + # multitenant + mock_profile = InMemoryProfile.test_profile() + context = mock_profile.context + context.injector.bind_instance( + IndyLedgerRequestsExecutor, + IndyLedgerRequestsExecutor(mock_profile), + ) + context.injector.bind_instance( + BaseMultitenantManager, + async_mock.MagicMock(MultitenantManager, autospec=True), + ) + with async_mock.patch.object( + IndyLedgerRequestsExecutor, "get_ledger_for_identifier" + ) as mock_get_ledger: + mock_get_ledger.return_value = (None, self.ledger) + await self.verifier.check_timestamps( + mock_profile, + INDY_PROOF_REQ_NAME, + INDY_PROOF_NAME, + REV_REG_DEFS, + ) + # all clear, with timestamps mock_profile = InMemoryProfile.test_profile() context = mock_profile.context diff --git a/aries_cloudagent/indy/util.py b/aries_cloudagent/indy/util.py index bb44f446b0..2c9a126c46 100644 --- a/aries_cloudagent/indy/util.py +++ b/aries_cloudagent/indy/util.py @@ -1,6 +1,6 @@ """Utilities for dealing with Indy conventions.""" -from os import getenv, listdir, makedirs, urandom +from os import getenv, makedirs, urandom from os.path import isdir, join from pathlib import Path from platform import system @@ -37,17 +37,3 @@ def indy_client_dir(subpath: str = None, create: bool = False) -> str: makedirs(target_dir, exist_ok=True) return target_dir - - -def tails_path(rev_reg_id: str) -> str: - """Return path to indy tails file for input rev reg id.""" - - tails_dir = indy_client_dir(join("tails", rev_reg_id), create=False) - if not isdir(tails_dir): - return None - - content = listdir(tails_dir) - if len(content) != 1: - return None - - return join(tails_dir, content[0]) diff --git a/aries_cloudagent/indy/verifier.py b/aries_cloudagent/indy/verifier.py index 7c219ec136..9a83ce5b8f 100644 --- a/aries_cloudagent/indy/verifier.py +++ b/aries_cloudagent/indy/verifier.py @@ -3,6 +3,7 @@ import logging from abc import ABC, ABCMeta, abstractmethod +from enum import Enum from time import time from typing import Mapping @@ -12,12 +13,25 @@ IndyLedgerRequestsExecutor, ) from ..messaging.util import canon, encode +from ..multitenant.base import BaseMultitenantManager from .models.xform import indy_proof_req2non_revoc_intervals + LOGGER = logging.getLogger(__name__) +class PresVerifyMsg(str, Enum): + """Credential verification codes.""" + + RMV_REFERENT_NON_REVOC_INTERVAL = "RMV_RFNT_NRI" + RMV_GLOBAL_NON_REVOC_INTERVAL = "RMV_GLB_NRI" + TSTMP_OUT_NON_REVOC_INTRVAL = "TS_OUT_NRI" + CT_UNREVEALED_ATTRIBUTES = "UNRVL_ATTR" + PRES_VALUE_ERROR = "VALUE_ERROR" + PRES_VERIFY_ERROR = "VERIFY_ERROR" + + class IndyVerifier(ABC, metaclass=ABCMeta): """Base class for Indy Verifier.""" @@ -31,7 +45,7 @@ def __repr__(self) -> str: """ return "<{}>".format(self.__class__.__name__) - def non_revoc_intervals(self, pres_req: dict, pres: dict, cred_defs: dict): + def non_revoc_intervals(self, pres_req: dict, pres: dict, cred_defs: dict) -> list: """ Remove superfluous non-revocation intervals in presentation request. @@ -44,12 +58,13 @@ def non_revoc_intervals(self, pres_req: dict, pres: dict, cred_defs: dict): pres: corresponding presentation """ - for (req_proof_key, pres_key) in { + msgs = [] + for req_proof_key, pres_key in { "revealed_attrs": "requested_attributes", "revealed_attr_groups": "requested_attributes", "predicates": "requested_predicates", }.items(): - for (uuid, spec) in pres["requested_proof"].get(req_proof_key, {}).items(): + for uuid, spec in pres["requested_proof"].get(req_proof_key, {}).items(): if ( "revocation" not in cred_defs[ @@ -59,6 +74,10 @@ def non_revoc_intervals(self, pres_req: dict, pres: dict, cred_defs: dict): if uuid in pres_req[pres_key] and pres_req[pres_key][uuid].pop( "non_revoked", None ): + msgs.append( + f"{PresVerifyMsg.RMV_REFERENT_NON_REVOC_INTERVAL.value}::" + f"{uuid}" + ) LOGGER.info( ( "Amended presentation request (nonce=%s): removed " @@ -70,8 +89,15 @@ def non_revoc_intervals(self, pres_req: dict, pres: dict, cred_defs: dict): uuid, ) - if all(spec.get("timestamp") is None for spec in pres["identifiers"]): + if all( + ( + spec.get("timestamp") is None + and "revocation" not in cred_defs[spec["cred_def_id"]]["value"] + ) + for spec in pres["identifiers"] + ): pres_req.pop("non_revoked", None) + msgs.append(PresVerifyMsg.RMV_GLOBAL_NON_REVOC_INTERVAL.value) LOGGER.warning( ( "Amended presentation request (nonce=%s); removed global " @@ -79,6 +105,7 @@ def non_revoc_intervals(self, pres_req: dict, pres: dict, cred_defs: dict): ), pres_req["nonce"], ) + return msgs async def check_timestamps( self, @@ -86,7 +113,7 @@ async def check_timestamps( pres_req: Mapping, pres: Mapping, rev_reg_defs: Mapping, - ): + ) -> list: """ Check for suspicious, missing, and superfluous timestamps. @@ -99,26 +126,35 @@ async def check_timestamps( pres: indy proof request rev_reg_defs: rev reg defs by rev reg id, augmented with transaction times """ + msgs = [] now = int(time()) non_revoc_intervals = indy_proof_req2non_revoc_intervals(pres_req) + LOGGER.debug(f">>> got non-revoc intervals: {non_revoc_intervals}") # timestamp for irrevocable credential - for (index, ident) in enumerate(pres["identifiers"]): - if ident.get("timestamp"): - cred_def_id = ident["cred_def_id"] + cred_defs = [] + for index, ident in enumerate(pres["identifiers"]): + LOGGER.debug(f">>> got (index, ident): ({index},{ident})") + cred_def_id = ident["cred_def_id"] + multitenant_mgr = profile.inject_or(BaseMultitenantManager) + if multitenant_mgr: + ledger_exec_inst = IndyLedgerRequestsExecutor(profile) + else: ledger_exec_inst = profile.inject(IndyLedgerRequestsExecutor) - ledger = ( - await ledger_exec_inst.get_ledger_for_identifier( - cred_def_id, - txn_record_type=GET_CRED_DEF, + ledger = ( + await ledger_exec_inst.get_ledger_for_identifier( + cred_def_id, + txn_record_type=GET_CRED_DEF, + ) + )[1] + async with ledger: + cred_def = await ledger.get_credential_definition(cred_def_id) + cred_defs.append(cred_def) + if ident.get("timestamp"): + if not cred_def["value"].get("revocation"): + raise ValueError( + f"Timestamp in presentation identifier #{index} " + f"for irrevocable cred def id {cred_def_id}" ) - )[1] - async with ledger: - cred_def = await ledger.get_credential_definition(cred_def_id) - if not cred_def["value"].get("revocation"): - raise ValueError( - f"Timestamp in presentation identifier #{index} " - f"for irrevocable cred def id {cred_def_id}" - ) # timestamp in the future too far in the past for ident in pres["identifiers"]: @@ -144,88 +180,115 @@ async def check_timestamps( # timestamp superfluous, missing, or outside non-revocation interval revealed_attrs = pres["requested_proof"].get("revealed_attrs", {}) + unrevealed_attrs = pres["requested_proof"].get("unrevealed_attrs", {}) revealed_groups = pres["requested_proof"].get("revealed_attr_groups", {}) self_attested = pres["requested_proof"].get("self_attested_attrs", {}) preds = pres["requested_proof"].get("predicates", {}) - for (uuid, req_attr) in pres_req["requested_attributes"].items(): + for uuid, req_attr in pres_req["requested_attributes"].items(): if "name" in req_attr: if uuid in revealed_attrs: index = revealed_attrs[uuid]["sub_proof_index"] + if cred_defs[index]["value"].get("revocation"): + timestamp = pres["identifiers"][index].get("timestamp") + if (timestamp is not None) ^ bool( + non_revoc_intervals.get(uuid) + ): + LOGGER.debug(f">>> uuid: {uuid}") + LOGGER.debug( + f">>> revealed_attrs[uuid]: {revealed_attrs[uuid]}" + ) + raise ValueError( + f"Timestamp on sub-proof #{index} " + f"is {'superfluous' if timestamp else 'missing'} " + f"vs. requested attribute {uuid}" + ) + if non_revoc_intervals.get(uuid) and not ( + non_revoc_intervals[uuid].get("from", 0) + < timestamp + < non_revoc_intervals[uuid].get("to", now) + ): + msgs.append( + f"{PresVerifyMsg.TSTMP_OUT_NON_REVOC_INTRVAL.value}::" + f"{uuid}" + ) + LOGGER.info( + f"Timestamp {timestamp} from ledger for item" + f"{uuid} falls outside non-revocation interval " + f"{non_revoc_intervals[uuid]}" + ) + elif uuid in unrevealed_attrs: + # nothing to do, attribute value is not revealed + msgs.append( + f"{PresVerifyMsg.CT_UNREVEALED_ATTRIBUTES.value}::" f"{uuid}" + ) + elif uuid not in self_attested: + raise ValueError( + f"Presentation attributes mismatch requested attribute {uuid}" + ) + + elif "names" in req_attr: + group_spec = revealed_groups.get(uuid) + if ( + group_spec is None + or "sub_proof_index" not in group_spec + or "values" not in group_spec + ): + raise ValueError(f"Missing requested attribute group {uuid}") + index = group_spec["sub_proof_index"] + if cred_defs[index]["value"].get("revocation"): timestamp = pres["identifiers"][index].get("timestamp") if (timestamp is not None) ^ bool(non_revoc_intervals.get(uuid)): raise ValueError( f"Timestamp on sub-proof #{index} " f"is {'superfluous' if timestamp else 'missing'} " - f"vs. requested attribute {uuid}" + f"vs. requested attribute group {uuid}" ) if non_revoc_intervals.get(uuid) and not ( non_revoc_intervals[uuid].get("from", 0) < timestamp < non_revoc_intervals[uuid].get("to", now) ): - LOGGER.info( + msgs.append( + f"{PresVerifyMsg.TSTMP_OUT_NON_REVOC_INTRVAL.value}::" + f"{uuid}" + ) + LOGGER.warning( f"Timestamp {timestamp} from ledger for item" f"{uuid} falls outside non-revocation interval " f"{non_revoc_intervals[uuid]}" ) - elif uuid not in self_attested: - raise ValueError( - f"Presentation attributes mismatch requested attribute {uuid}" - ) - elif "names" in req_attr: - group_spec = revealed_groups.get(uuid) - if ( - group_spec is None - or "sub_proof_index" not in group_spec - or "values" not in group_spec - ): - raise ValueError(f"Missing requested attribute group {uuid}") - index = group_spec["sub_proof_index"] + for uuid, req_pred in pres_req["requested_predicates"].items(): + pred_spec = preds.get(uuid) + if pred_spec is None or "sub_proof_index" not in pred_spec: + raise ValueError( + f"Presentation predicates mismatch requested predicate {uuid}" + ) + index = pred_spec["sub_proof_index"] + if cred_defs[index]["value"].get("revocation"): timestamp = pres["identifiers"][index].get("timestamp") if (timestamp is not None) ^ bool(non_revoc_intervals.get(uuid)): raise ValueError( f"Timestamp on sub-proof #{index} " f"is {'superfluous' if timestamp else 'missing'} " - f"vs. requested attribute group {uuid}" + f"vs. requested predicate {uuid}" ) if non_revoc_intervals.get(uuid) and not ( non_revoc_intervals[uuid].get("from", 0) < timestamp < non_revoc_intervals[uuid].get("to", now) ): + msgs.append( + f"{PresVerifyMsg.TSTMP_OUT_NON_REVOC_INTRVAL.value}::" f"{uuid}" + ) LOGGER.warning( - f"Timestamp {timestamp} from ledger for item" - f"{uuid} falls outside non-revocation interval " + f"Best-effort timestamp {timestamp} " + "from ledger falls outside non-revocation interval " f"{non_revoc_intervals[uuid]}" ) + return msgs - for (uuid, req_pred) in pres_req["requested_predicates"].items(): - pred_spec = preds.get(uuid) - if pred_spec is None or "sub_proof_index" not in pred_spec: - raise ValueError( - f"Presentation predicates mismatch requested predicate {uuid}" - ) - index = pred_spec["sub_proof_index"] - timestamp = pres["identifiers"][index].get("timestamp") - if (timestamp is not None) ^ bool(non_revoc_intervals.get(uuid)): - raise ValueError( - f"Timestamp on sub-proof #{index} " - f"is {'superfluous' if timestamp else 'missing'} " - f"vs. requested predicate {uuid}" - ) - if non_revoc_intervals.get(uuid) and not ( - non_revoc_intervals[uuid].get("from", 0) - < timestamp - < non_revoc_intervals[uuid].get("to", now) - ): - LOGGER.warning( - f"Best-effort timestamp {timestamp} " - "from ledger falls outside non-revocation interval " - f"{non_revoc_intervals[uuid]}" - ) - - async def pre_verify(self, pres_req: dict, pres: dict): + async def pre_verify(self, pres_req: dict, pres: dict) -> list: """ Check for essential components and tampering in presentation. @@ -237,6 +300,7 @@ async def pre_verify(self, pres_req: dict, pres: dict): pres: corresponding presentation """ + msgs = [] if not ( pres_req and "requested_predicates" in pres_req @@ -250,7 +314,7 @@ async def pre_verify(self, pres_req: dict, pres: dict): if "proof" not in pres: raise ValueError("Presentation missing 'proof'") - for (uuid, req_pred) in pres_req["requested_predicates"].items(): + for uuid, req_pred in pres_req["requested_predicates"].items(): try: canon_attr = canon(req_pred["name"]) matched = False @@ -273,12 +337,19 @@ async def pre_verify(self, pres_req: dict, pres: dict): raise ValueError(f"Missing requested predicate '{uuid}'") revealed_attrs = pres["requested_proof"].get("revealed_attrs", {}) + unrevealed_attrs = pres["requested_proof"].get("unrevealed_attrs", {}) revealed_groups = pres["requested_proof"].get("revealed_attr_groups", {}) self_attested = pres["requested_proof"].get("self_attested_attrs", {}) - for (uuid, req_attr) in pres_req["requested_attributes"].items(): + for uuid, req_attr in pres_req["requested_attributes"].items(): if "name" in req_attr: if uuid in revealed_attrs: pres_req_attr_spec = {req_attr["name"]: revealed_attrs[uuid]} + elif uuid in unrevealed_attrs: + # unrevealed attribute, nothing to do + pres_req_attr_spec = {} + msgs.append( + f"{PresVerifyMsg.CT_UNREVEALED_ATTRIBUTES.value}::" f"{uuid}" + ) elif uuid in self_attested: if not req_attr.get("restrictions"): continue @@ -304,7 +375,7 @@ async def pre_verify(self, pres_req: dict, pres: dict): f"Request attribute missing 'name' and 'names': '{uuid}'" ) - for (attr, spec) in pres_req_attr_spec.items(): + for attr, spec in pres_req_attr_spec.items(): try: primary_enco = pres["proof"]["proofs"][spec["sub_proof_index"]][ "primary_proof" @@ -315,6 +386,7 @@ async def pre_verify(self, pres_req: dict, pres: dict): raise ValueError(f"Encoded representation mismatch for '{attr}'") if primary_enco != encode(spec["raw"]): raise ValueError(f"Encoded representation mismatch for '{attr}'") + return msgs @abstractmethod def verify_presentation( @@ -325,7 +397,7 @@ def verify_presentation( credential_definitions, rev_reg_defs, rev_reg_entries, - ): + ) -> (bool, list): """ Verify a presentation. diff --git a/aries_cloudagent/ledger/base.py b/aries_cloudagent/ledger/base.py index 3ceaa7e1f3..06bd362d95 100644 --- a/aries_cloudagent/ledger/base.py +++ b/aries_cloudagent/ledger/base.py @@ -1,18 +1,24 @@ """Ledger base class.""" +import json +import logging import re from abc import ABC, abstractmethod, ABCMeta from enum import Enum from hashlib import sha256 -from typing import Sequence, Tuple, Union +from typing import List, Sequence, Tuple, Union -from ..indy.issuer import IndyIssuer +from ..indy.issuer import DEFAULT_CRED_DEF_TAG, IndyIssuer, IndyIssuerError from ..utils import sentinel from ..wallet.did_info import DIDInfo +from .error import BadLedgerRequestError, LedgerError, LedgerTransactionError + from .endpoint_type import EndpointType +LOGGER = logging.getLogger(__name__) + class BaseLedger(ABC, metaclass=ABCMeta): """Base class for ledger.""" @@ -42,6 +48,10 @@ def backend(self) -> str: def read_only(self) -> bool: """Accessor for the ledger read-only flag.""" + @abstractmethod + async def is_ledger_read_only(self) -> bool: + """Check if ledger is read-only including TAA.""" + @abstractmethod async def get_key_for_did(self, did: str) -> str: """Fetch the verkey for a ledger DID. @@ -69,12 +79,46 @@ async def get_all_endpoints_for_did(self, did: str) -> dict: did: The DID to look up on the ledger or in the cache """ + async def _construct_attr_json( + self, + endpoint: str, + endpoint_type: EndpointType = None, + all_exist_endpoints: dict = None, + routing_keys: List[str] = None, + ) -> str: + """Create attr_json string. + + Args: + all_exist_endpoings: Dictionary of all existing endpoints + endpoint: The endpoint address + endpoint_type: The type of the endpoint + routing_keys: List of routing_keys if mediator is present + """ + + if not routing_keys: + routing_keys = [] + + if all_exist_endpoints: + all_exist_endpoints[endpoint_type.indy] = endpoint + all_exist_endpoints["routingKeys"] = routing_keys + attr_json = json.dumps({"endpoint": all_exist_endpoints}) + + else: + endpoint_dict = {endpoint_type.indy: endpoint} + endpoint_dict["routingKeys"] = routing_keys + attr_json = json.dumps({"endpoint": endpoint_dict}) + + return attr_json + @abstractmethod async def update_endpoint_for_did( self, did: str, endpoint: str, endpoint_type: EndpointType = EndpointType.ENDPOINT, + write_ledger: bool = True, + endorser_did: str = None, + routing_keys: List[str] = None, ) -> bool: """Check and update the endpoint on the ledger. @@ -86,8 +130,14 @@ async def update_endpoint_for_did( @abstractmethod async def register_nym( - self, did: str, verkey: str, alias: str = None, role: str = None - ): + self, + did: str, + verkey: str, + alias: str = None, + role: str = None, + write_ledger: bool = True, + endorser_did: str = None, + ) -> Tuple[bool, dict]: """ Register a nym on the ledger. @@ -125,6 +175,10 @@ def did_to_nym(self, did: str) -> str: if did: return re.sub(r"^did:\w+:", "", did) + @abstractmethod + async def get_wallet_public_did(self) -> DIDInfo: + """Fetch the public DID from the wallet.""" + @abstractmethod async def get_txn_author_agreement(self, reload: bool = False): """Get the current transaction author agreement, fetching it if necessary.""" @@ -154,6 +208,7 @@ def taa_digest(self, version: str, text: str): async def txn_endorse( self, request_json: str, + endorse_did: DIDInfo = None, ) -> str: """Endorse (sign) the provided transaction.""" @@ -162,12 +217,60 @@ async def txn_submit( self, request_json: str, sign: bool, - taa_accept: bool, + taa_accept: bool = None, sign_did: DIDInfo = sentinel, + write_ledger: bool = True, ) -> str: """Write the provided (signed and possibly endorsed) transaction to the ledger.""" @abstractmethod + async def fetch_schema_by_id(self, schema_id: str) -> dict: + """ + Get schema from ledger. + + Args: + schema_id: The schema id (or stringified sequence number) to retrieve + + Returns: + Indy schema dict + + """ + + @abstractmethod + async def fetch_schema_by_seq_no(self, seq_no: int) -> dict: + """ + Fetch a schema by its sequence number. + + Args: + seq_no: schema ledger sequence number + + Returns: + Indy schema dict + + """ + + async def check_existing_schema( + self, + public_did: str, + schema_name: str, + schema_version: str, + attribute_names: Sequence[str], + ) -> Tuple[str, dict]: + """Check if a schema has already been published.""" + fetch_schema_id = f"{public_did}:2:{schema_name}:{schema_version}" + schema = await self.fetch_schema_by_id(fetch_schema_id) + if schema: + fetched_attrs = schema["attrNames"].copy() + fetched_attrs.sort() + cmp_attrs = list(attribute_names) + cmp_attrs.sort() + if fetched_attrs != cmp_attrs: + raise LedgerTransactionError( + "Schema already exists on ledger, but attributes do not match: " + + f"{schema_name}:{schema_version} {fetched_attrs} != {cmp_attrs}" + ) + return fetch_schema_id, schema + async def create_and_send_schema( self, issuer: IndyIssuer, @@ -188,6 +291,92 @@ async def create_and_send_schema( """ + public_info = await self.get_wallet_public_did() + if not public_info: + raise BadLedgerRequestError("Cannot publish schema without a public DID") + + schema_info = await self.check_existing_schema( + public_info.did, schema_name, schema_version, attribute_names + ) + if schema_info: + LOGGER.warning("Schema already exists on ledger. Returning details.") + schema_id, schema_def = schema_info + else: + if await self.is_ledger_read_only(): + raise LedgerError( + "Error cannot write schema when ledger is in read only mode" + ) + + try: + schema_id, schema_json = await issuer.create_schema( + public_info.did, + schema_name, + schema_version, + attribute_names, + ) + except IndyIssuerError as err: + raise LedgerError(err.message) from err + schema_def = json.loads(schema_json) + + schema_req = await self._create_schema_request( + public_info, + schema_json, + write_ledger=write_ledger, + endorser_did=endorser_did, + ) + + try: + resp = await self.txn_submit( + schema_req, + sign=True, + sign_did=public_info, + write_ledger=write_ledger, + ) + + if not write_ledger: + return schema_id, {"signed_txn": resp} + + try: + # parse sequence number out of response + seq_no = json.loads(resp)["result"]["txnMetadata"]["seqNo"] + schema_def["seqNo"] = seq_no + except KeyError as err: + raise LedgerError( + "Failed to parse schema sequence number from ledger response" + ) from err + except LedgerTransactionError as e: + # Identify possible duplicate schema errors on indy-node < 1.9 and > 1.9 + if ( + "can have one and only one SCHEMA with name" in e.message + or "UnauthorizedClientRequest" in e.message + ): + # handle potential race condition if multiple agents are publishing + # the same schema simultaneously + schema_info = await self.check_existing_schema( + public_info.did, schema_name, schema_version, attribute_names + ) + if schema_info: + LOGGER.warning( + "Schema already exists on ledger. Returning details." + " Error: %s", + e, + ) + schema_id, schema_def = schema_info + else: + raise + + return schema_id, schema_def + + @abstractmethod + async def _create_schema_request( + self, + public_info: DIDInfo, + schema_json: str, + write_ledger: bool = True, + endorser_did: str = None, + ): + """Create the ledger request for publishing a schema.""" + @abstractmethod async def get_revoc_reg_def(self, revoc_reg_id: str) -> dict: """Look up a revocation registry definition by ID.""" @@ -199,7 +388,7 @@ async def send_revoc_reg_def( issuer_did: str = None, write_ledger: bool = True, endorser_did: str = None, - ): + ) -> dict: """Publish a revocation registry definition to the ledger.""" @abstractmethod @@ -211,10 +400,9 @@ async def send_revoc_reg_entry( issuer_did: str = None, write_ledger: bool = True, endorser_did: str = None, - ): + ) -> dict: """Publish a revocation registry entry to the ledger.""" - @abstractmethod async def create_and_send_credential_definition( self, issuer: IndyIssuer, @@ -239,6 +427,105 @@ async def create_and_send_credential_definition( Tuple with cred def id, cred def structure, and whether it's novel """ + public_info = await self.get_wallet_public_did() + if not public_info: + raise BadLedgerRequestError( + "Cannot publish credential definition without a public DID" + ) + + schema = await self.get_schema(schema_id) + if not schema: + raise LedgerError(f"Ledger {self.pool_name} has no schema {schema_id}") + + novel = False + + # check if cred def is on ledger already + for test_tag in [tag] if tag else ["tag", DEFAULT_CRED_DEF_TAG]: + credential_definition_id = issuer.make_credential_definition_id( + public_info.did, schema, signature_type, test_tag + ) + ledger_cred_def = await self.fetch_credential_definition( + credential_definition_id + ) + if ledger_cred_def: + LOGGER.warning( + "Credential definition %s already exists on ledger %s", + credential_definition_id, + self.pool_name, + ) + + try: + if not await issuer.credential_definition_in_wallet( + credential_definition_id + ): + raise LedgerError( + f"Credential definition {credential_definition_id} is on " + f"ledger {self.pool_name} but not in wallet " + f"{self.profile.name}" + ) + except IndyIssuerError as err: + raise LedgerError(err.message) from err + + credential_definition_json = json.dumps(ledger_cred_def) + break + else: # no such cred def on ledger + try: + if await issuer.credential_definition_in_wallet( + credential_definition_id + ): + raise LedgerError( + f"Credential definition {credential_definition_id} is in " + f"wallet {self.profile.name} but not on ledger " + f"{self.pool.name}" + ) + except IndyIssuerError as err: + raise LedgerError(err.message) from err + + # Cred def is neither on ledger nor in wallet: create and send it + novel = True + try: + ( + credential_definition_id, + credential_definition_json, + ) = await issuer.create_and_store_credential_definition( + public_info.did, + schema, + signature_type, + tag, + support_revocation, + ) + except IndyIssuerError as err: + raise LedgerError(err.message) from err + + if await self.is_ledger_read_only(): + raise LedgerError( + "Error cannot write cred def when ledger is in read only mode" + ) + + cred_def_req = await self._create_credential_definition_request( + public_info, + credential_definition_json, + write_ledger=write_ledger, + endorser_did=endorser_did, + ) + + resp = await self.txn_submit( + cred_def_req, True, sign_did=public_info, write_ledger=write_ledger + ) + if not write_ledger: + return (credential_definition_id, {"signed_txn": resp}, novel) + + return (credential_definition_id, json.loads(credential_definition_json), novel) + + @abstractmethod + async def _create_credential_definition_request( + self, + public_info: DIDInfo, + credential_definition_json: str, + write_ledger: bool = True, + endorser_did: str = None, + ): + """Create the ledger request for publishing a credential definition.""" @abstractmethod async def get_credential_definition(self, credential_definition_id: str) -> dict: diff --git a/aries_cloudagent/ledger/indy.py b/aries_cloudagent/ledger/indy.py index 21d334a64f..a127f8c028 100644 --- a/aries_cloudagent/ledger/indy.py +++ b/aries_cloudagent/ledger/indy.py @@ -5,9 +5,10 @@ import logging import tempfile from datetime import date, datetime +from io import StringIO from os import path from time import time -from typing import Sequence, Tuple, Optional +from typing import TYPE_CHECKING, List, Tuple, Optional import indy.ledger import indy.pool @@ -15,8 +16,6 @@ from ..cache.base import BaseCache from ..config.base import BaseInjector, BaseProvider, BaseSettings -from ..core.profile import Profile -from ..indy.issuer import DEFAULT_CRED_DEF_TAG, IndyIssuer, IndyIssuerError from ..indy.sdk.error import IndyErrorHandler from ..storage.base import StorageRecord from ..storage.indy import IndySdkStorage @@ -37,11 +36,25 @@ ) from .util import TAA_ACCEPTED_RECORD_TYPE +if TYPE_CHECKING: + from ..indy.sdk.profile import IndySdkProfile + LOGGER = logging.getLogger(__name__) GENESIS_TRANSACTION_FILE = "indy_genesis_transactions.txt" +def _normalize_txns(txns: str) -> str: + """Normalize a set of genesis transactions.""" + lines = StringIO() + for line in txns.splitlines(): + line = line.strip() + if line: + lines.write(line) + lines.write("\n") + return lines.getvalue() + + class IndySdkLedgerPoolProvider(BaseProvider): """Indy ledger pool provider which keys off the selected pool name.""" @@ -107,12 +120,28 @@ def __init__( self.cache = cache self.cache_duration = cache_duration self.genesis_transactions = genesis_transactions + self.genesis_txns_cache = genesis_transactions self.handle = None self.name = name self.taa_cache = None self.read_only = read_only self.socks_proxy = socks_proxy + @property + def genesis_txns(self) -> str: + """Get the configured genesis transactions.""" + if not self.genesis_txns_cache: + try: + txn_path = path.join( + tempfile.gettempdir(), f"{self.name}_{GENESIS_TRANSACTION_FILE}" + ) + self.genesis_txns_cache = _normalize_txns(open(txn_path).read()) + except FileNotFoundError: + raise LedgerConfigError( + "Pool config '%s' not found", self.name + ) from None + return self.genesis_txns_cache + async def create_pool_config( self, genesis_transactions: str, recreate: bool = False ): @@ -179,7 +208,7 @@ async def close(self): """Close the pool ledger.""" if self.opened: exc = None - for attempt in range(3): + for _attempt in range(3): try: await indy.pool.close_pool_ledger(self.handle) except IndyError as err: @@ -238,14 +267,14 @@ class IndySdkLedger(BaseLedger): def __init__( self, pool: IndySdkLedgerPool, - profile: Profile, + profile: "IndySdkProfile", ): """ Initialize an IndySdkLedger instance. Args: pool: The pool instance handling the raw ledger connection - wallet: The IndySdkWallet instance + profile: The IndySdkProfile instance """ self.pool = pool self.profile = profile @@ -265,6 +294,18 @@ def read_only(self) -> bool: """Accessor for the ledger read-only flag.""" return self.pool.read_only + async def is_ledger_read_only(self) -> bool: + """Check if ledger is read-only including TAA.""" + if self.read_only: + return self.read_only + # if TAA is required and not accepted we should be in read-only mode + taa = await self.get_txn_author_agreement() + if taa["taa_required"]: + taa_acceptance = await self.get_latest_txn_author_acceptance() + if "mechanism" not in taa_acceptance: + return True + return self.read_only + async def __aenter__(self) -> "IndySdkLedger": """ Context manager entry. @@ -291,22 +332,21 @@ async def get_wallet_public_did(self) -> DIDInfo: async def _endorse( self, request_json: str, + endorse_did: DIDInfo = None, ) -> str: if not self.pool.handle: raise ClosedPoolError( f"Cannot endorse request with closed pool '{self.pool.name}'" ) - public_info = await self.get_wallet_public_did() + public_info = endorse_did if endorse_did else await self.get_wallet_public_did() if not public_info: raise BadLedgerRequestError( "Cannot endorse transaction without a public DID" ) - async with self.profile.session() as session: - wallet = session.inject(BaseWallet) - endorsed_request_json = await indy.ledger.multi_sign_request( - wallet.opened.handle, public_info.did, request_json - ) + endorsed_request_json = await indy.ledger.multi_sign_request( + self.profile.wallet.handle, public_info.did, request_json + ) return endorsed_request_json async def _submit( @@ -325,6 +365,7 @@ async def _submit( sign: whether or not to sign the request taa_accept: whether to apply TAA acceptance to the (signed, write) request sign_did: override the signing DID + write_ledger: skip the request submission """ @@ -348,6 +389,8 @@ async def _submit( if taa_accept: acceptance = await self.get_latest_txn_author_acceptance() if acceptance: + # flake8 and black 23.1.0 check collision fix + # fmt: off request_json = await ( indy.ledger.append_txn_author_agreement_acceptance_to_request( request_json, @@ -358,20 +401,19 @@ async def _submit( acceptance["time"], ) ) - async with self.profile.session() as session: - wallet = session.inject(BaseWallet) - if write_ledger: - submit_op = indy.ledger.sign_and_submit_request( - self.pool.handle, - wallet.opened.handle, - sign_did.did, - request_json, - ) - else: - # multi-sign, since we expect this to get endorsed later - submit_op = indy.ledger.multi_sign_request( - wallet.opened.handle, sign_did.did, request_json - ) + # fmt: on + if write_ledger: + submit_op = indy.ledger.sign_and_submit_request( + self.pool.handle, + self.profile.wallet.handle, + sign_did.did, + request_json, + ) + else: + # multi-sign, since we expect this to get endorsed later + submit_op = indy.ledger.multi_sign_request( + self.profile.wallet.handle, sign_did.did, request_json + ) else: submit_op = indy.ledger.submit_request(self.pool.handle, request_json) @@ -403,9 +445,10 @@ async def _submit( async def txn_endorse( self, request_json: str, + endorse_did: DIDInfo = None, ) -> str: """Endorse a (signed) ledger transaction.""" - return await self._endorse(request_json) + return await self._endorse(request_json, endorse_did=endorse_did) async def txn_submit( self, @@ -413,125 +456,36 @@ async def txn_submit( sign: bool = None, taa_accept: bool = None, sign_did: DIDInfo = sentinel, + write_ledger: bool = True, ) -> str: """Submit a signed (and endorsed) transaction to the ledger.""" return await self._submit( - request_json, sign=sign, taa_accept=taa_accept, sign_did=sign_did + request_json, + sign=sign, + taa_accept=taa_accept, + sign_did=sign_did, + write_ledger=write_ledger, ) - async def create_and_send_schema( + async def _create_schema_request( self, - issuer: IndyIssuer, - schema_name: str, - schema_version: str, - attribute_names: Sequence[str], + public_info: DIDInfo, + schema_json: str, write_ledger: bool = True, endorser_did: str = None, - ) -> Tuple[str, dict]: - """ - Send schema to ledger. - - Args: - issuer: The issuer instance creating the schema - schema_name: The schema name - schema_version: The schema version - attribute_names: A list of schema attributes - - """ - - public_info = await self.get_wallet_public_did() - if not public_info: - raise BadLedgerRequestError("Cannot publish schema without a public DID") - - schema_info = await self.check_existing_schema( - public_info.did, schema_name, schema_version, attribute_names - ) - if schema_info: - LOGGER.warning("Schema already exists on ledger. Returning details.") - schema_id, schema_def = schema_info - else: - if self.pool.read_only: - raise LedgerError( - "Error cannot write schema when ledger is in read only mode" - ) - - try: - schema_id, schema_json = await issuer.create_schema( - public_info.did, - schema_name, - schema_version, - attribute_names, - ) - except IndyIssuerError as err: - raise LedgerError(err.message) from err - schema_def = json.loads(schema_json) - - with IndyErrorHandler("Exception building schema request", LedgerError): - request_json = await indy.ledger.build_schema_request( - public_info.did, schema_json - ) - - try: - if endorser_did and not write_ledger: - request_json = await indy.ledger.append_request_endorser( - request_json, endorser_did - ) - resp = await self._submit( - request_json, True, sign_did=public_info, write_ledger=write_ledger - ) - if not write_ledger: - return schema_id, {"signed_txn": resp} - try: - # parse sequence number out of response - seq_no = json.loads(resp)["result"]["txnMetadata"]["seqNo"] - schema_def["seqNo"] = seq_no - except KeyError as err: - raise LedgerError( - "Failed to parse schema sequence number from ledger response" - ) from err - except LedgerTransactionError as e: - # Identify possible duplicate schema errors on indy-node < 1.9 and > 1.9 - if "can have one and only one SCHEMA with name" in getattr( - e, "message", "" - ) or "UnauthorizedClientRequest" in getattr(e, "message", ""): - # handle potential race condition if multiple agents are publishing - # the same schema simultaneously - schema_info = await self.check_existing_schema( - public_info.did, schema_name, schema_version, attribute_names - ) - if schema_info: - LOGGER.warning( - "Schema already exists on ledger. Returning details." - " Error: %s", - e, - ) - schema_id, schema_def = schema_info - else: - raise + ): + """Create the ledger request for publishing a schema.""" + with IndyErrorHandler("Exception building schema request", LedgerError): + request_json = await indy.ledger.build_schema_request( + public_info.did, schema_json + ) - return schema_id, schema_def + if endorser_did and not write_ledger: + request_json = await indy.ledger.append_request_endorser( + request_json, endorser_did + ) - async def check_existing_schema( - self, - public_did: str, - schema_name: str, - schema_version: str, - attribute_names: Sequence[str], - ) -> Tuple[str, dict]: - """Check if a schema has already been published.""" - fetch_schema_id = f"{public_did}:2:{schema_name}:{schema_version}" - schema = await self.fetch_schema_by_id(fetch_schema_id) - if schema: - fetched_attrs = schema["attrNames"].copy() - fetched_attrs.sort() - cmp_attrs = list(attribute_names) - cmp_attrs.sort() - if fetched_attrs != cmp_attrs: - raise LedgerTransactionError( - "Schema already exists on ledger, but attributes do not match: " - + f"{schema_name}:{schema_version} {fetched_attrs} != {cmp_attrs}" - ) - return fetch_schema_id, schema + return request_json async def get_schema(self, schema_id: str) -> dict: """ @@ -592,7 +546,7 @@ async def fetch_schema_by_id(self, schema_id: str) -> dict: return parsed_response - async def fetch_schema_by_seq_no(self, seq_no: int): + async def fetch_schema_by_seq_no(self, seq_no: int) -> dict: """ Fetch a schema by its sequence number. @@ -624,123 +578,25 @@ async def fetch_schema_by_seq_no(self, seq_no: int): f"Could not get schema from ledger for seq no {seq_no}" ) - async def create_and_send_credential_definition( + async def _create_credential_definition_request( self, - issuer: IndyIssuer, - schema_id: str, - signature_type: str = None, - tag: str = None, - support_revocation: bool = False, + public_info: DIDInfo, + credential_definition_json: str, write_ledger: bool = True, endorser_did: str = None, - ) -> Tuple[str, dict, bool]: - """ - Send credential definition to ledger and store relevant key matter in wallet. - - Args: - issuer: The issuer instance to use for credential definition creation - schema_id: The schema id of the schema to create cred def for - signature_type: The signature type to use on the credential definition - tag: Optional tag to distinguish multiple credential definitions - support_revocation: Optional flag to enable revocation for this cred def - - Returns: - Tuple with cred def id, cred def structure, and whether it's novel - - """ - public_info = await self.get_wallet_public_did() - if not public_info: - raise BadLedgerRequestError( - "Cannot publish credential definition without a public DID" - ) - - schema = await self.get_schema(schema_id) - if not schema: - raise LedgerError(f"Ledger {self.pool.name} has no schema {schema_id}") - - novel = False - - # check if cred def is on ledger already - for test_tag in [tag] if tag else ["tag", DEFAULT_CRED_DEF_TAG]: - credential_definition_id = issuer.make_credential_definition_id( - public_info.did, schema, signature_type, test_tag - ) - ledger_cred_def = await self.fetch_credential_definition( - credential_definition_id + ): + """Create the ledger request for publishing a credential definition.""" + with IndyErrorHandler("Exception building cred def request", LedgerError): + request_json = await indy.ledger.build_cred_def_request( + public_info.did, credential_definition_json ) - if ledger_cred_def: - LOGGER.warning( - "Credential definition %s already exists on ledger %s", - credential_definition_id, - self.pool.name, - ) - try: - async with self.profile.session() as session: - wallet = session.inject(BaseWallet) - if not await issuer.credential_definition_in_wallet( - credential_definition_id - ): - raise LedgerError( - f"Credential definition {credential_definition_id} is on " - f"ledger {self.pool.name} but not in wallet " - f"{wallet.opened.name}" - ) - except IndyIssuerError as err: - raise LedgerError(err.message) from err - credential_definition_json = json.dumps(ledger_cred_def) - break - else: # no such cred def on ledger - try: - async with self.profile.session() as session: - wallet = session.inject(BaseWallet) - if await issuer.credential_definition_in_wallet( - credential_definition_id - ): - raise LedgerError( - f"Credential definition {credential_definition_id} is in " - f"wallet {wallet.opened.name} but not on ledger " - f"{self.pool.name}" - ) - except IndyIssuerError as err: - raise LedgerError(err.message) from err - - # Cred def is neither on ledger nor in wallet: create and send it - novel = True - try: - ( - credential_definition_id, - credential_definition_json, - ) = await issuer.create_and_store_credential_definition( - public_info.did, - schema, - signature_type, - tag, - support_revocation, - ) - except IndyIssuerError as err: - raise LedgerError(err.message) from err - - if self.pool.read_only: - raise LedgerError( - "Error cannot write cred def when ledger is in read only mode" - ) - - with IndyErrorHandler("Exception building cred def request", LedgerError): - request_json = await indy.ledger.build_cred_def_request( - public_info.did, credential_definition_json - ) - if endorser_did and not write_ledger: - request_json = await indy.ledger.append_request_endorser( - request_json, endorser_did - ) - resp = await self._submit( - request_json, True, sign_did=public_info, write_ledger=write_ledger + if endorser_did and not write_ledger: + request_json = await indy.ledger.append_request_endorser( + request_json, endorser_did ) - if not write_ledger: - return (credential_definition_id, {"signed_txn": resp}, novel) - return (credential_definition_id, json.loads(credential_definition_json), novel) + return request_json async def get_credential_definition(self, credential_definition_id: str) -> dict: """ @@ -886,7 +742,13 @@ async def get_endpoint_for_did( return address async def update_endpoint_for_did( - self, did: str, endpoint: str, endpoint_type: EndpointType = None + self, + did: str, + endpoint: str, + endpoint_type: EndpointType = None, + write_ledger: bool = True, + endorser_did: str = None, + routing_keys: List[str] = None, ) -> bool: """Check and update the endpoint on the ledger. @@ -895,6 +757,12 @@ async def update_endpoint_for_did( endpoint: The endpoint address endpoint_type: The type of the endpoint """ + public_info = await self.get_wallet_public_did() + if not public_info: + raise BadLedgerRequestError( + "Cannot update endpoint at ledger without a public DID" + ) + if not endpoint_type: endpoint_type = EndpointType.ENDPOINT @@ -906,30 +774,49 @@ async def update_endpoint_for_did( ) if exist_endpoint_of_type != endpoint: - if self.pool.read_only: + if await self.is_ledger_read_only(): raise LedgerError( "Error cannot update endpoint when ledger is in read only mode" ) nym = self.did_to_nym(did) - if all_exist_endpoints: - all_exist_endpoints[endpoint_type.indy] = endpoint - attr_json = json.dumps({"endpoint": all_exist_endpoints}) - else: - attr_json = json.dumps({"endpoint": {endpoint_type.indy: endpoint}}) + attr_json = await self._construct_attr_json( + endpoint, endpoint_type, all_exist_endpoints, routing_keys + ) with IndyErrorHandler("Exception building attribute request", LedgerError): request_json = await indy.ledger.build_attrib_request( nym, nym, None, attr_json, None ) + + if endorser_did and not write_ledger: + request_json = await indy.ledger.append_request_endorser( + request_json, endorser_did + ) + resp = await self._submit( + request_json, + sign=True, + sign_did=public_info, + write_ledger=write_ledger, + ) + if not write_ledger: + return {"signed_txn": resp} + await self._submit(request_json, True, True) return True + return False async def register_nym( - self, did: str, verkey: str, alias: str = None, role: str = None - ): + self, + did: str, + verkey: str, + alias: str = None, + role: str = None, + write_ledger: bool = True, + endorser_did: str = None, + ) -> Tuple[bool, dict]: """ Register a nym on the ledger. @@ -939,28 +826,32 @@ async def register_nym( alias: Human-friendly alias to assign to the DID. role: For permissioned ledgers, what role should the new DID have. """ - if self.pool.read_only: + if await self.is_ledger_read_only(): raise LedgerError( "Error cannot register nym when ledger is in read only mode" ) public_info = await self.get_wallet_public_did() + if not public_info: + raise WalletNotFoundError( + f"Cannot register NYM to ledger: wallet {self.profile.name} " + "has no public DID" + ) + with IndyErrorHandler("Exception building nym request", LedgerError): + request_json = await indy.ledger.build_nym_request( + public_info.did, did, verkey, alias, role + ) + if endorser_did and not write_ledger: + request_json = await indy.ledger.append_request_endorser( + request_json, endorser_did + ) + resp = await self._submit( + request_json, sign=True, sign_did=public_info, write_ledger=write_ledger + ) # let ledger raise on insufficient privilege + if not write_ledger: + return True, {"signed_txn": resp} async with self.profile.session() as session: wallet = session.inject(BaseWallet) - if not public_info: - raise WalletNotFoundError( - f"Cannot register NYM to ledger: wallet {wallet.opened.name} " - "has no public DID" - ) - - with IndyErrorHandler("Exception building nym request", LedgerError): - request_json = await indy.ledger.build_nym_request( - public_info.did, did, verkey, alias, role - ) - await self._submit( - request_json - ) # let ledger raise on insufficient privilege - try: did_info = await wallet.get_local_did(did) except WalletNotFoundError: @@ -968,6 +859,7 @@ async def register_nym( else: metadata = {**did_info.metadata, **DIDPosture.POSTED.metadata} await wallet.replace_local_did_metadata(did, metadata) + return True, None async def get_nym_role(self, did: str) -> Role: """ @@ -1026,37 +918,37 @@ async def rotate_public_did_keypair(self, next_seed: str = None) -> None: wallet = session.inject(BaseWallet) verkey = await wallet.rotate_did_keypair_start(public_did, next_seed) - # submit to ledger (retain role and alias) - nym = self.did_to_nym(public_did) - with IndyErrorHandler("Exception building nym request", LedgerError): - request_json = await indy.ledger.build_get_nym_request(public_did, nym) + # submit to ledger (retain role and alias) + nym = self.did_to_nym(public_did) + with IndyErrorHandler("Exception building nym request", LedgerError): + request_json = await indy.ledger.build_get_nym_request(public_did, nym) - response_json = await self._submit(request_json) - data = json.loads((json.loads(response_json))["result"]["data"]) - if not data: - raise BadLedgerRequestError( - f"Ledger has no public DID for wallet {wallet.opened.name}" - ) - seq_no = data["seqNo"] + response_json = await self._submit(request_json) + data = json.loads((json.loads(response_json))["result"]["data"]) + if not data: + raise BadLedgerRequestError( + f"Ledger has no public DID for wallet {self.profile.name}" + ) + seq_no = data["seqNo"] - with IndyErrorHandler("Exception building get-txn request", LedgerError): - txn_req_json = await indy.ledger.build_get_txn_request( - None, None, seq_no - ) + with IndyErrorHandler("Exception building get-txn request", LedgerError): + txn_req_json = await indy.ledger.build_get_txn_request(None, None, seq_no) - txn_resp_json = await self._submit(txn_req_json) - txn_resp = json.loads(txn_resp_json) - txn_resp_data = txn_resp["result"]["data"] - if not txn_resp_data: - raise BadLedgerRequestError( - f"Bad or missing ledger NYM transaction for DID {public_did}" - ) - txn_data_data = txn_resp_data["txn"]["data"] - role_token = Role.get(txn_data_data.get("role")).token() - alias = txn_data_data.get("alias") - await self.register_nym(public_did, verkey, role_token, alias) + txn_resp_json = await self._submit(txn_req_json) + txn_resp = json.loads(txn_resp_json) + txn_resp_data = txn_resp["result"]["data"] + if not txn_resp_data: + raise BadLedgerRequestError( + f"Bad or missing ledger NYM transaction for DID {public_did}" + ) + txn_data_data = txn_resp_data["txn"]["data"] + role_token = Role.get(txn_data_data.get("role")).token() + alias = txn_data_data.get("alias") + await self.register_nym(public_did, verkey, role_token, alias) - # update wallet + # update wallet + async with self.profile.session() as session: + wallet = session.inject(BaseWallet) await wallet.rotate_did_keypair_apply(public_did) async def get_txn_author_agreement(self, reload: bool = False) -> dict: @@ -1095,9 +987,7 @@ async def fetch_txn_author_agreement(self) -> dict: async def get_indy_storage(self) -> IndySdkStorage: """Get an IndySdkStorage instance for the current wallet.""" - async with self.profile.session() as session: - wallet = session.inject(BaseWallet) - return IndySdkStorage(wallet.opened) + return IndySdkStorage(self.profile.wallet) def taa_rough_timestamp(self) -> int: """Get a timestamp accurate to the day. @@ -1126,33 +1016,27 @@ async def accept_txn_author_agreement( ) storage = await self.get_indy_storage() await storage.add_record(record) - async with self.profile.session() as session: - wallet = session.inject(BaseWallet) - if self.pool.cache: - cache_key = ( - TAA_ACCEPTED_RECORD_TYPE - + "::" - + wallet.opened.name - + "::" - + self.pool.name - + "::" - ) - await self.pool.cache.set( - cache_key, acceptance, self.pool.cache_duration - ) - - async def get_latest_txn_author_acceptance(self) -> dict: - """Look up the latest TAA acceptance.""" - async with self.profile.session() as session: - wallet = session.inject(BaseWallet) + if self.pool.cache: cache_key = ( TAA_ACCEPTED_RECORD_TYPE + "::" - + wallet.opened.name + + self.profile.name + "::" + self.pool.name + "::" ) + await self.pool.cache.set(cache_key, acceptance, self.pool.cache_duration) + + async def get_latest_txn_author_acceptance(self) -> dict: + """Look up the latest TAA acceptance.""" + cache_key = ( + TAA_ACCEPTED_RECORD_TYPE + + "::" + + self.profile.name + + "::" + + self.pool.name + + "::" + ) acceptance = self.pool.cache and await self.pool.cache.get(cache_key) if not acceptance: storage = await self.get_indy_storage() @@ -1262,7 +1146,7 @@ async def send_revoc_reg_def( issuer_did: str = None, write_ledger: bool = True, endorser_did: str = None, - ): + ) -> dict: """Publish a revocation registry definition to the ledger.""" # NOTE - issuer DID could be extracted from the revoc_reg_def ID if issuer_did: @@ -1298,7 +1182,7 @@ async def send_revoc_reg_entry( issuer_did: str = None, write_ledger: bool = True, endorser_did: str = None, - ): + ) -> dict: """Publish a revocation registry entry to the ledger.""" if issuer_did: async with self.profile.session() as session: diff --git a/aries_cloudagent/ledger/indy_vdr.py b/aries_cloudagent/ledger/indy_vdr.py index d8e8910bc1..abd2e399b1 100644 --- a/aries_cloudagent/ledger/indy_vdr.py +++ b/aries_cloudagent/ledger/indy_vdr.py @@ -12,15 +12,12 @@ from io import StringIO from pathlib import Path from time import time -from typing import Sequence, Tuple, Union, Optional +from typing import List, Tuple, Union, Optional from indy_vdr import ledger, open_pool, Pool, Request, VdrError from ..cache.base import BaseCache from ..core.profile import Profile -from ..indy.issuer import IndyIssuer, IndyIssuerError, DEFAULT_CRED_DEF_TAG -from ..messaging.credential_definitions.util import CRED_DEF_SENT_RECORD_TYPE -from ..messaging.schemas.util import SCHEMA_SENT_RECORD_TYPE from ..storage.base import BaseStorage, StorageRecord from ..utils import sentinel from ..utils.env import storage_path @@ -286,6 +283,18 @@ def read_only(self) -> bool: """Accessor for the ledger read-only flag.""" return self.pool.read_only + async def is_ledger_read_only(self) -> bool: + """Check if ledger is read-only including TAA.""" + if self.read_only: + return self.read_only + # if TAA is required and not accepted we should be in read-only mode + taa = await self.get_txn_author_agreement() + if taa["taa_required"]: + taa_acceptance = await self.get_latest_txn_author_acceptance() + if "mechanism" not in taa_acceptance: + return True + return self.read_only + async def __aenter__(self) -> "IndyVdrLedger": """ Context manager entry. @@ -369,137 +378,23 @@ async def _submit( return request_result - async def create_and_send_schema( + async def _create_schema_request( self, - issuer: IndyIssuer, - schema_name: str, - schema_version: str, - attribute_names: Sequence[str], + public_info: DIDInfo, + schema_json: str, write_ledger: bool = True, endorser_did: str = None, - ) -> Tuple[str, dict]: - """ - Send schema to ledger. - - Args: - issuer: The issuer instance creating the schema - schema_name: The schema name - schema_version: The schema version - attribute_names: A list of schema attributes - - """ - - public_info = await self.get_wallet_public_did() - if not public_info: - raise BadLedgerRequestError("Cannot publish schema without a public DID") - - schema_info = await self.check_existing_schema( - public_info.did, schema_name, schema_version, attribute_names - ) - if schema_info: - LOGGER.warning("Schema already exists on ledger. Returning details.") - schema_id, schema_def = schema_info - else: - if self.read_only: - raise LedgerError( - "Error cannot write schema when ledger is in read only mode" - ) - - try: - schema_id, schema_json = await issuer.create_schema( - public_info.did, - schema_name, - schema_version, - attribute_names, - ) - except IndyIssuerError as err: - raise LedgerError(err.message) from err - schema_def = json.loads(schema_json) - - try: - schema_req = ledger.build_schema_request(public_info.did, schema_json) - except VdrError as err: - raise LedgerError("Exception when building schema request") from err - - if endorser_did and not write_ledger: - schema_req.set_endorser(endorser_did) - - try: - resp = await self._submit( - schema_req, - sign=True, - sign_did=public_info, - write_ledger=write_ledger, - ) - - if not write_ledger: - return schema_id, {"signed_txn": resp} - - try: - # parse sequence number out of response - seq_no = resp["txnMetadata"]["seqNo"] - schema_def["seqNo"] = seq_no - except KeyError as err: - raise LedgerError( - "Failed to parse schema sequence number from ledger response" - ) from err - except LedgerTransactionError as e: - # Identify possible duplicate schema errors on indy-node < 1.9 and > 1.9 - if ( - "can have one and only one SCHEMA with name" in e.message - or "UnauthorizedClientRequest" in e.message - ): - # handle potential race condition if multiple agents are publishing - # the same schema simultaneously - schema_info = await self.check_existing_schema( - public_info.did, schema_name, schema_version, attribute_names - ) - if schema_info: - LOGGER.warning( - "Schema already exists on ledger. Returning details." - " Error: %s", - e, - ) - schema_id, schema_def = schema_info - else: - raise - - schema_id_parts = schema_id.split(":") - schema_tags = { - "schema_id": schema_id, - "schema_issuer_did": public_info.did, - "schema_name": schema_id_parts[-2], - "schema_version": schema_id_parts[-1], - "epoch": str(int(time())), - } - record = StorageRecord(SCHEMA_SENT_RECORD_TYPE, schema_id, schema_tags) - async with self.profile.session() as session: - storage = session.inject(BaseStorage) - await storage.add_record(record) + ): + """Create the ledger request for publishing a schema.""" + try: + schema_req = ledger.build_schema_request(public_info.did, schema_json) + except VdrError as err: + raise LedgerError("Exception when building schema request") from err - return schema_id, schema_def + if endorser_did and not write_ledger: + schema_req.set_endorser(endorser_did) - async def check_existing_schema( - self, - public_did: str, - schema_name: str, - schema_version: str, - attribute_names: Sequence[str], - ) -> Tuple[str, dict]: - """Check if a schema has already been published.""" - fetch_schema_id = f"{public_did}:2:{schema_name}:{schema_version}" - schema = await self.fetch_schema_by_id(fetch_schema_id) - if schema: - fetched_attrs = schema["attrNames"].copy() - fetched_attrs.sort() - cmp_attrs = list(attribute_names) - cmp_attrs.sort() - if fetched_attrs != cmp_attrs: - raise LedgerTransactionError( - "Schema already exists on ledger, but attributes do not match: " - + f"{schema_name}:{schema_version} {fetched_attrs} != {cmp_attrs}" - ) - return fetch_schema_id, schema + return schema_req async def get_schema(self, schema_id: str) -> dict: """ @@ -566,7 +461,7 @@ async def fetch_schema_by_id(self, schema_id: str) -> dict: return schema_data - async def fetch_schema_by_seq_no(self, seq_no: int): + async def fetch_schema_by_seq_no(self, seq_no: int) -> dict: """ Fetch a schema by its sequence number. @@ -600,140 +495,25 @@ async def fetch_schema_by_seq_no(self, seq_no: int): f"Could not get schema from ledger for seq no {seq_no}" ) - async def create_and_send_credential_definition( + async def _create_credential_definition_request( self, - issuer: IndyIssuer, - schema_id: str, - signature_type: str = None, - tag: str = None, - support_revocation: bool = False, + public_info: DIDInfo, + credential_definition_json: str, write_ledger: bool = True, endorser_did: str = None, - ) -> Tuple[str, dict, bool]: - """ - Send credential definition to ledger and store relevant key matter in wallet. - - Args: - issuer: The issuer instance to use for credential definition creation - schema_id: The schema id of the schema to create cred def for - signature_type: The signature type to use on the credential definition - tag: Optional tag to distinguish multiple credential definitions - support_revocation: Optional flag to enable revocation for this cred def - - Returns: - Tuple with cred def id, cred def structure, and whether it's novel - - """ - - public_info = await self.get_wallet_public_did() - if not public_info: - raise BadLedgerRequestError( - "Cannot publish credential definition without a public DID" - ) - - schema = await self.get_schema(schema_id) - if not schema: - raise LedgerError(f"Ledger {self.pool_name} has no schema {schema_id}") - - novel = False - - # check if cred def is on ledger already - for test_tag in [tag] if tag else ["tag", DEFAULT_CRED_DEF_TAG]: - credential_definition_id = issuer.make_credential_definition_id( - public_info.did, schema, signature_type, test_tag - ) - ledger_cred_def = await self.fetch_credential_definition( - credential_definition_id + ): + """Create the ledger request for publishing a credential definition.""" + try: + cred_def_req = ledger.build_cred_def_request( + public_info.did, credential_definition_json ) - if ledger_cred_def: - LOGGER.warning( - "Credential definition %s already exists on ledger %s", - credential_definition_id, - self.pool_name, - ) - - try: - if not await issuer.credential_definition_in_wallet( - credential_definition_id - ): - raise LedgerError( - f"Credential definition {credential_definition_id} is on " - f"ledger {self.pool_name} but not in wallet " - f"{self.profile.name}" - ) - except IndyIssuerError as err: - raise LedgerError(err.message) from err - - credential_definition_json = json.dumps(ledger_cred_def) - break - else: # no such cred def on ledger - try: - if await issuer.credential_definition_in_wallet( - credential_definition_id - ): - raise LedgerError( - f"Credential definition {credential_definition_id} is in " - f"wallet {self.profile.name} but not on ledger {self.pool_name}" - ) - except IndyIssuerError as err: - raise LedgerError(err.message) from err - - # Cred def is neither on ledger nor in wallet: create and send it - novel = True - try: - ( - credential_definition_id, - credential_definition_json, - ) = await issuer.create_and_store_credential_definition( - public_info.did, - schema, - signature_type, - tag, - support_revocation, - ) - except IndyIssuerError as err: - raise LedgerError(err.message) from err - - if self.read_only: - raise LedgerError( - "Error cannot write cred def when ledger is in read only mode" - ) - - try: - cred_def_req = ledger.build_cred_def_request( - public_info.did, credential_definition_json - ) - except VdrError as err: - raise LedgerError("Exception when building cred def request") from err - - if endorser_did and not write_ledger: - cred_def_req.set_endorser(endorser_did) + except VdrError as err: + raise LedgerError("Exception when building cred def request") from err - resp = await self._submit( - cred_def_req, True, sign_did=public_info, write_ledger=write_ledger - ) - if not write_ledger: - return (credential_definition_id, {"signed_txn": resp}, novel) - - # Add non-secrets record - schema_id_parts = schema_id.split(":") - cred_def_tags = { - "schema_id": schema_id, - "schema_issuer_did": schema_id_parts[0], - "schema_name": schema_id_parts[-2], - "schema_version": schema_id_parts[-1], - "issuer_did": public_info.did, - "cred_def_id": credential_definition_id, - "epoch": str(int(time())), - } - record = StorageRecord( - CRED_DEF_SENT_RECORD_TYPE, credential_definition_id, cred_def_tags - ) - async with self.profile.session() as session: - storage = session.inject(BaseStorage) - await storage.add_record(record) + if endorser_did and not write_ledger: + cred_def_req.set_endorser(endorser_did) - return (credential_definition_id, json.loads(credential_definition_json), novel) + return cred_def_req async def get_credential_definition(self, credential_definition_id: str) -> dict: """ @@ -893,7 +673,13 @@ async def get_endpoint_for_did( return address async def update_endpoint_for_did( - self, did: str, endpoint: str, endpoint_type: EndpointType = None + self, + did: str, + endpoint: str, + endpoint_type: EndpointType = None, + write_ledger: bool = True, + endorser_did: str = None, + routing_keys: List[str] = None, ) -> bool: """Check and update the endpoint on the ledger. @@ -902,6 +688,12 @@ async def update_endpoint_for_did( endpoint: The endpoint address endpoint_type: The type of the endpoint """ + public_info = await self.get_wallet_public_did() + if not public_info: + raise BadLedgerRequestError( + "Cannot update endpoint at ledger without a public DID" + ) + if not endpoint_type: endpoint_type = EndpointType.ENDPOINT @@ -920,16 +712,26 @@ async def update_endpoint_for_did( nym = self.did_to_nym(did) - if all_exist_endpoints: - all_exist_endpoints[endpoint_type.indy] = endpoint - attr_json = json.dumps({"endpoint": all_exist_endpoints}) - else: - attr_json = json.dumps({"endpoint": {endpoint_type.indy: endpoint}}) + attr_json = await self._construct_attr_json( + endpoint, endpoint_type, all_exist_endpoints, routing_keys + ) try: attrib_req = ledger.build_attrib_request( nym, nym, None, attr_json, None ) + + if endorser_did and not write_ledger: + attrib_req.set_endorser(endorser_did) + resp = await self._submit( + attrib_req, + True, + sign_did=public_info, + write_ledger=write_ledger, + ) + if not write_ledger: + return {"signed_txn": resp} + except VdrError as err: raise LedgerError("Exception when building attribute request") from err @@ -938,8 +740,14 @@ async def update_endpoint_for_did( return False async def register_nym( - self, did: str, verkey: str, alias: str = None, role: str = None - ): + self, + did: str, + verkey: str, + alias: str = None, + role: str = None, + write_ledger: bool = True, + endorser_did: str = None, + ) -> Tuple[bool, dict]: """ Register a nym on the ledger. @@ -965,8 +773,14 @@ async def register_nym( except VdrError as err: raise LedgerError("Exception when building nym request") from err - await self._submit(nym_req, sign=True, sign_did=public_info) + if endorser_did and not write_ledger: + nym_req.set_endorser(endorser_did) + resp = await self._submit( + nym_req, sign=True, sign_did=public_info, write_ledger=write_ledger + ) + if not write_ledger: + return True, {"signed_txn": resp} async with self.profile.session() as session: wallet = session.inject(BaseWallet) try: @@ -976,6 +790,7 @@ async def register_nym( else: metadata = {**did_info.metadata, **DIDPosture.POSTED.metadata} await wallet.replace_local_did_metadata(did, metadata) + return True, None async def get_nym_role(self, did: str) -> Role: """ @@ -1264,7 +1079,7 @@ async def send_revoc_reg_def( issuer_did: str = None, write_ledger: bool = True, endorser_did: str = None, - ): + ) -> dict: """Publish a revocation registry definition to the ledger.""" # NOTE - issuer DID could be extracted from the revoc_reg_def ID async with self.profile.session() as session: @@ -1301,7 +1116,7 @@ async def send_revoc_reg_entry( issuer_did: str = None, write_ledger: bool = True, endorser_did: str = None, - ): + ) -> dict: """Publish a revocation registry entry to the ledger.""" async with self.profile.session() as session: wallet = session.inject(BaseWallet) @@ -1338,6 +1153,7 @@ async def get_wallet_public_did(self) -> DIDInfo: async def txn_endorse( self, request_json: str, + endorse_did: DIDInfo = None, ) -> str: """Endorse (sign) the provided transaction.""" try: @@ -1347,7 +1163,7 @@ async def txn_endorse( async with self.profile.session() as session: wallet = session.inject(BaseWallet) - sign_did = await wallet.get_public_did() + sign_did = endorse_did if endorse_did else await wallet.get_public_did() if not sign_did: raise BadLedgerRequestError( "Cannot endorse transaction without a public DID" @@ -1364,13 +1180,19 @@ async def txn_submit( self, request_json: str, sign: bool, - taa_accept: bool, + taa_accept: bool = None, sign_did: DIDInfo = sentinel, + write_ledger: bool = True, ) -> str: """Write the provided (signed and possibly endorsed) transaction to the ledger.""" resp = await self._submit( - request_json, sign=sign, taa_accept=taa_accept, sign_did=sign_did + request_json, + sign=sign, + taa_accept=taa_accept, + sign_did=sign_did, + write_ledger=write_ledger, ) - # match the format returned by indy sdk - sdk_resp = {"op": "REPLY", "result": resp} - return json.dumps(sdk_resp) + if write_ledger: + # match the format returned by indy sdk + resp = {"op": "REPLY", "result": resp} + return json.dumps(resp) diff --git a/aries_cloudagent/ledger/merkel_validation/trie.py b/aries_cloudagent/ledger/merkel_validation/trie.py index 2c7acedf98..08eb958140 100644 --- a/aries_cloudagent/ledger/merkel_validation/trie.py +++ b/aries_cloudagent/ledger/merkel_validation/trie.py @@ -79,7 +79,7 @@ async def verify_spv_proof(expected_value, proof_nodes, serialized=True): json.loads(rlp_decode(decoded_node[1])[0].decode("utf-8")) ) == expected_value: return True - except (DecodingError): + except DecodingError: continue return False except Exception: diff --git a/aries_cloudagent/ledger/multiple_ledger/base_manager.py b/aries_cloudagent/ledger/multiple_ledger/base_manager.py index b421119d22..346f36af77 100644 --- a/aries_cloudagent/ledger/multiple_ledger/base_manager.py +++ b/aries_cloudagent/ledger/multiple_ledger/base_manager.py @@ -37,6 +37,10 @@ async def _get_ledger_by_did( ) -> Optional[Tuple[str, BaseLedger, bool]]: """Build and submit GET_NYM request and process response.""" + @abstractmethod + async def get_ledger_inst_by_id(self, ledger_id: str) -> Optional[BaseLedger]: + """Return ledger instance by identifier.""" + @abstractmethod async def lookup_did_in_configured_ledgers( self, did: str, cache_did: bool diff --git a/aries_cloudagent/ledger/multiple_ledger/indy_manager.py b/aries_cloudagent/ledger/multiple_ledger/indy_manager.py index 121c745cb5..9ba534b19f 100644 --- a/aries_cloudagent/ledger/multiple_ledger/indy_manager.py +++ b/aries_cloudagent/ledger/multiple_ledger/indy_manager.py @@ -9,6 +9,7 @@ from ...cache.base import BaseCache from ...core.profile import Profile +from ...ledger.base import BaseLedger from ...ledger.error import LedgerError from ...wallet.crypto import did_is_self_certified @@ -53,7 +54,17 @@ def __init__( async def get_write_ledger(self) -> Optional[Tuple[str, IndySdkLedger]]: """Return the write IndySdkLedger instance.""" - return self.write_ledger_info + # return self.write_ledger_info + if self.write_ledger_info: + return (self.write_ledger_info[0], self.profile.inject_or(BaseLedger)) + else: + return None + + async def get_ledger_inst_by_id(self, ledger_id: str) -> Optional[BaseLedger]: + """Return BaseLedger instance.""" + return self.production_ledgers.get( + ledger_id + ) or self.non_production_ledgers.get(ledger_id) async def get_prod_ledgers(self) -> Mapping: """Return production ledgers mapping.""" @@ -83,7 +94,11 @@ async def _get_ledger_by_did( """ try: indy_sdk_ledger = None - if ledger_id in self.production_ledgers: + if self.write_ledger_info and ledger_id == self.write_ledger_info[0]: + indy_sdk_ledger = await self.get_write_ledger() + if indy_sdk_ledger: + indy_sdk_ledger = indy_sdk_ledger[1] + elif ledger_id in self.production_ledgers: indy_sdk_ledger = self.production_ledgers.get(ledger_id) else: indy_sdk_ledger = self.non_production_ledgers.get(ledger_id) @@ -134,7 +149,9 @@ async def lookup_did_in_configured_ledgers( cache_key = f"did_ledger_id_resolver::{did}" if bool(cache_did and self.cache and await self.cache.get(cache_key)): cached_ledger_id = await self.cache.get(cache_key) - if cached_ledger_id in self.production_ledgers: + if self.write_ledger_info and cached_ledger_id == self.write_ledger_info[0]: + return self.get_write_ledger() + elif cached_ledger_id in self.production_ledgers: return (cached_ledger_id, self.production_ledgers.get(cached_ledger_id)) elif cached_ledger_id in self.non_production_ledgers: return ( diff --git a/aries_cloudagent/ledger/multiple_ledger/indy_vdr_manager.py b/aries_cloudagent/ledger/multiple_ledger/indy_vdr_manager.py index 8cb16f6b5a..0c4d09aa26 100644 --- a/aries_cloudagent/ledger/multiple_ledger/indy_vdr_manager.py +++ b/aries_cloudagent/ledger/multiple_ledger/indy_vdr_manager.py @@ -9,6 +9,7 @@ from ...cache.base import BaseCache from ...core.profile import Profile +from ...ledger.base import BaseLedger from ...ledger.error import LedgerError from ...wallet.crypto import did_is_self_certified @@ -63,6 +64,12 @@ async def get_nonprod_ledgers(self) -> Mapping: """Return non_production ledgers mapping.""" return self.non_production_ledgers + async def get_ledger_inst_by_id(self, ledger_id: str) -> Optional[BaseLedger]: + """Return BaseLedger instance.""" + return self.production_ledgers.get( + ledger_id + ) or self.non_production_ledgers.get(ledger_id) + async def _get_ledger_by_did( self, ledger_id: str, diff --git a/aries_cloudagent/ledger/multiple_ledger/ledger_requests_executor.py b/aries_cloudagent/ledger/multiple_ledger/ledger_requests_executor.py index a56d93dfef..0cf0b4dcb8 100644 --- a/aries_cloudagent/ledger/multiple_ledger/ledger_requests_executor.py +++ b/aries_cloudagent/ledger/multiple_ledger/ledger_requests_executor.py @@ -61,3 +61,8 @@ async def get_ledger_for_identifier( except (MultipleLedgerManagerError, InjectionError): pass return (None, self.profile.inject_or(BaseLedger)) + + async def get_ledger_inst(self, ledger_id: str) -> Optional[BaseLedger]: + """Return ledger instance from ledger_id set in config.""" + multiledger_mgr = self.profile.inject(BaseMultipleLedgerManager) + return await multiledger_mgr.get_ledger_inst_by_id(ledger_id=ledger_id) diff --git a/aries_cloudagent/ledger/multiple_ledger/manager_provider.py b/aries_cloudagent/ledger/multiple_ledger/manager_provider.py index cfe6cc0a17..7a40c2530f 100644 --- a/aries_cloudagent/ledger/multiple_ledger/manager_provider.py +++ b/aries_cloudagent/ledger/multiple_ledger/manager_provider.py @@ -84,24 +84,27 @@ def provide(self, settings: BaseSettings, injector: BaseInjector): pool_name = config.get("pool_name") ledger_is_production = config.get("is_production") ledger_is_write = config.get("is_write") - ledger_pool = pool_class( - pool_name, - keepalive=keepalive, - cache=cache, - genesis_transactions=genesis_transactions, - read_only=read_only, - socks_proxy=socks_proxy, - ) - ledger_instance = ledger_class( - pool=ledger_pool, - profile=self.root_profile, - ) if ledger_is_write: - write_ledger_info = (ledger_id, ledger_instance) - if ledger_is_production: - indy_sdk_production_ledgers[ledger_id] = ledger_instance + write_ledger_info = (ledger_id, None) else: - indy_sdk_non_production_ledgers[ledger_id] = ledger_instance + ledger_pool = pool_class( + pool_name, + keepalive=keepalive, + cache=cache, + genesis_transactions=genesis_transactions, + read_only=read_only, + socks_proxy=socks_proxy, + ) + ledger_instance = ledger_class( + pool=ledger_pool, + profile=self.root_profile, + ) + if ledger_is_production: + indy_sdk_production_ledgers[ledger_id] = ledger_instance + else: + indy_sdk_non_production_ledgers[ + ledger_id + ] = ledger_instance if settings.get_value("ledger.genesis_transactions"): ledger_instance = self.root_profile.inject_or(BaseLedger) ledger_id = "startup::" + ledger_instance.pool.name diff --git a/aries_cloudagent/ledger/multiple_ledger/tests/test_indy_ledger_requests.py b/aries_cloudagent/ledger/multiple_ledger/tests/test_indy_ledger_requests.py index 52e6d52f41..f087660fa5 100644 --- a/aries_cloudagent/ledger/multiple_ledger/tests/test_indy_ledger_requests.py +++ b/aries_cloudagent/ledger/multiple_ledger/tests/test_indy_ledger_requests.py @@ -38,6 +38,9 @@ async def setUp(self): lookup_did_in_configured_ledgers=async_mock.CoroutineMock( return_value=("test_prod_1", self.ledger) ), + get_ledger_inst_by_id=async_mock.CoroutineMock( + return_value=self.ledger + ), ), ) self.profile.context.injector.bind_instance(BaseLedger, self.ledger) @@ -53,6 +56,10 @@ async def test_get_ledger_for_identifier(self): assert ledger_id == "test_prod_1" assert ledger_inst.pool.name == "test_prod_1" + async def test_get_ledger_inst(self): + ledger_inst = await self.indy_ledger_requestor.get_ledger_inst("test_prod_1") + assert ledger_inst + async def test_get_ledger_for_identifier_is_digit(self): ledger_id, ledger = await self.indy_ledger_requestor.get_ledger_for_identifier( "123", 0 diff --git a/aries_cloudagent/ledger/multiple_ledger/tests/test_indy_manager.py b/aries_cloudagent/ledger/multiple_ledger/tests/test_indy_manager.py index 5dac56e7ed..fc3f972999 100644 --- a/aries_cloudagent/ledger/multiple_ledger/tests/test_indy_manager.py +++ b/aries_cloudagent/ledger/multiple_ledger/tests/test_indy_manager.py @@ -11,6 +11,7 @@ from ....cache.base import BaseCache from ....cache.in_memory import InMemoryCache from ....core.in_memory import InMemoryProfile +from ....ledger.base import BaseLedger from ....messaging.responder import BaseResponder from ...error import LedgerError @@ -36,6 +37,7 @@ async def setUp(self): IndySdkLedgerPool("test_prod_1", checked=True), self.profile ) test_write_ledger = ("test_prod_1", test_prod_ledger) + self.context.injector.bind_instance(BaseLedger, test_prod_ledger) self.production_ledger["test_prod_1"] = test_prod_ledger self.production_ledger["test_prod_2"] = IndySdkLedger( IndySdkLedgerPool("test_prod_2", checked=True), self.profile @@ -58,6 +60,14 @@ async def test_get_write_ledger(self): assert ledger_id == "test_prod_1" assert ledger_inst.pool.name == "test_prod_1" + async def test_get_ledger_inst_by_id(self): + ledger_inst = await self.manager.get_ledger_inst_by_id("test_prod_2") + assert ledger_inst + ledger_inst = await self.manager.get_ledger_inst_by_id("test_non_prod_2") + assert ledger_inst + ledger_inst = await self.manager.get_ledger_inst_by_id("test_invalid") + assert not ledger_inst + @async_mock.patch("aries_cloudagent.ledger.indy.IndySdkLedgerPool.context_open") @async_mock.patch("aries_cloudagent.ledger.indy.IndySdkLedgerPool.context_close") @async_mock.patch("indy.ledger.build_get_nym_request") @@ -385,13 +395,16 @@ async def test_lookup_did_in_configured_ledgers_prod_not_cached( async def test_lookup_did_in_configured_ledgers_cached_prod_ledger(self): cache = InMemoryCache() - await cache.set("did_ledger_id_resolver::Av63wJYM7xYR4AiygYq4c3", "test_prod_1") + await cache.set("did_ledger_id_resolver::Av63wJYM7xYR4AiygYq4c3", "test_prod_2") self.profile.context.injector.bind_instance(BaseCache, cache) - (ledger_id, ledger_inst,) = await self.manager.lookup_did_in_configured_ledgers( + ( + ledger_id, + ledger_inst, + ) = await self.manager.lookup_did_in_configured_ledgers( "Av63wJYM7xYR4AiygYq4c3", cache_did=True ) - assert ledger_id == "test_prod_1" - assert ledger_inst.pool.name == "test_prod_1" + assert ledger_id == "test_prod_2" + assert ledger_inst.pool.name == "test_prod_2" async def test_lookup_did_in_configured_ledgers_cached_non_prod_ledger(self): cache = InMemoryCache() @@ -399,7 +412,10 @@ async def test_lookup_did_in_configured_ledgers_cached_non_prod_ledger(self): "did_ledger_id_resolver::Av63wJYM7xYR4AiygYq4c3", "test_non_prod_2", None ) self.profile.context.injector.bind_instance(BaseCache, cache) - (ledger_id, ledger_inst,) = await self.manager.lookup_did_in_configured_ledgers( + ( + ledger_id, + ledger_inst, + ) = await self.manager.lookup_did_in_configured_ledgers( "Av63wJYM7xYR4AiygYq4c3", cache_did=True ) assert ledger_id == "test_non_prod_2" diff --git a/aries_cloudagent/ledger/multiple_ledger/tests/test_indy_vdr_manager.py b/aries_cloudagent/ledger/multiple_ledger/tests/test_indy_vdr_manager.py index a09fb13983..4c4798750d 100644 --- a/aries_cloudagent/ledger/multiple_ledger/tests/test_indy_vdr_manager.py +++ b/aries_cloudagent/ledger/multiple_ledger/tests/test_indy_vdr_manager.py @@ -88,6 +88,14 @@ async def test_get_write_ledger(self): assert ledger_id == "test_prod_1" assert ledger_inst.pool.name == "test_prod_1" + async def test_get_ledger_inst_by_id(self): + ledger_inst = await self.manager.get_ledger_inst_by_id("test_prod_2") + assert ledger_inst + ledger_inst = await self.manager.get_ledger_inst_by_id("test_non_prod_2") + assert ledger_inst + ledger_inst = await self.manager.get_ledger_inst_by_id("test_invalid") + assert not ledger_inst + @async_mock.patch("aries_cloudagent.ledger.indy_vdr.IndyVdrLedgerPool.context_open") @async_mock.patch( "aries_cloudagent.ledger.indy_vdr.IndyVdrLedgerPool.context_close" @@ -443,7 +451,10 @@ async def test_lookup_did_in_configured_ledgers_cached_prod_ledger(self): cache = InMemoryCache() await cache.set("did_ledger_id_resolver::Av63wJYM7xYR4AiygYq4c3", "test_prod_1") self.profile.context.injector.bind_instance(BaseCache, cache) - (ledger_id, ledger_inst,) = await self.manager.lookup_did_in_configured_ledgers( + ( + ledger_id, + ledger_inst, + ) = await self.manager.lookup_did_in_configured_ledgers( "Av63wJYM7xYR4AiygYq4c3", cache_did=True ) assert ledger_id == "test_prod_1" @@ -455,7 +466,10 @@ async def test_lookup_did_in_configured_ledgers_cached_non_prod_ledger(self): "did_ledger_id_resolver::Av63wJYM7xYR4AiygYq4c3", "test_non_prod_2", None ) self.profile.context.injector.bind_instance(BaseCache, cache) - (ledger_id, ledger_inst,) = await self.manager.lookup_did_in_configured_ledgers( + ( + ledger_id, + ledger_inst, + ) = await self.manager.lookup_did_in_configured_ledgers( "Av63wJYM7xYR4AiygYq4c3", cache_did=True ) assert ledger_id == "test_non_prod_2" diff --git a/aries_cloudagent/ledger/multiple_ledger/tests/test_manager_provider.py b/aries_cloudagent/ledger/multiple_ledger/tests/test_manager_provider.py index bf876a7a51..14c75c514b 100644 --- a/aries_cloudagent/ledger/multiple_ledger/tests/test_manager_provider.py +++ b/aries_cloudagent/ledger/multiple_ledger/tests/test_manager_provider.py @@ -66,15 +66,16 @@ async def test_provide_invalid_manager(self): @pytest.mark.indy async def test_provide_indy_manager(self): context = InjectionContext() - profile = IndySdkProfile( - IndyOpenWallet( - config=IndyWalletConfig({"name": "test-profile"}), - created=True, - handle=1, - master_secret_id="master-secret", - ), - context, - ) + with async_mock.patch.object(IndySdkProfile, "_make_finalizer"): + profile = IndySdkProfile( + IndyOpenWallet( + config=IndyWalletConfig({"name": "test-profile"}), + created=True, + handle=1, + master_secret_id="master-secret", + ), + context, + ) context.injector.bind_instance( BaseLedger, IndySdkLedger(IndySdkLedgerPool("name"), profile) ) diff --git a/aries_cloudagent/ledger/routes.py b/aries_cloudagent/ledger/routes.py index dda91013fd..0c8505d49c 100644 --- a/aries_cloudagent/ledger/routes.py +++ b/aries_cloudagent/ledger/routes.py @@ -1,10 +1,15 @@ """Ledger admin routes.""" +import json +import logging + from aiohttp import web from aiohttp_apispec import docs, querystring_schema, request_schema, response_schema from marshmallow import fields, validate from ..admin.request_context import AdminRequestContext +from ..connections.models.conn_record import ConnRecord +from ..messaging.models.base import BaseModelError from ..messaging.models.openapi import OpenAPISchema from ..messaging.valid import ( ENDPOINT, @@ -12,8 +17,23 @@ INDY_DID, INDY_RAW_PUBLIC_KEY, INT_EPOCH, + UUIDFour, +) +from ..multitenant.base import BaseMultitenantManager + +from ..protocols.endorse_transaction.v1_0.manager import ( + TransactionManager, + TransactionManagerError, +) +from ..protocols.endorse_transaction.v1_0.models.transaction_record import ( + TransactionRecord, + TransactionRecordSchema, +) +from ..protocols.endorse_transaction.v1_0.util import ( + is_author_role, + get_endorser_connection_id, ) -from ..storage.error import StorageError +from ..storage.error import StorageError, StorageNotFoundError from ..wallet.error import WalletError, WalletNotFoundError from .base import BaseLedger, Role as LedgerRole @@ -32,6 +52,10 @@ ) from .endpoint_type import EndpointType from .error import BadLedgerRequestError, LedgerError, LedgerTransactionError +from .util import notify_register_did_event + + +LOGGER = logging.getLogger(__name__) class LedgerModulesResultSchema(OpenAPISchema): @@ -109,6 +133,23 @@ class RegisterLedgerNymQueryStringSchema(OpenAPISchema): ) +class CreateDidTxnForEndorserOptionSchema(OpenAPISchema): + """Class for user to input whether to create a transaction for endorser or not.""" + + create_transaction_for_endorser = fields.Boolean( + description="Create Transaction For Endorser's signature", + required=False, + ) + + +class SchemaConnIdMatchInfoSchema(OpenAPISchema): + """Path parameters and validators for request taking connection id.""" + + conn_id = fields.Str( + description="Connection identifier", required=False, example=UUIDFour.EXAMPLE + ) + + class QueryStringDIDSchema(OpenAPISchema): """Parameters and validators for query string with DID only.""" @@ -128,7 +169,7 @@ class QueryStringEndpointSchema(OpenAPISchema): ) -class RegisterLedgerNymResponseSchema(OpenAPISchema): +class TxnOrRegisterLedgerNymResponseSchema(OpenAPISchema): """Response schema for ledger nym registration.""" success = fields.Bool( @@ -136,6 +177,12 @@ class RegisterLedgerNymResponseSchema(OpenAPISchema): example=True, ) + txn = fields.Nested( + TransactionRecordSchema(), + required=False, + description="DID transaction to endorse", + ) + class GetNymRoleResponseSchema(OpenAPISchema): """Response schema to get nym role operation.""" @@ -172,7 +219,9 @@ class GetDIDEndpointResponseSchema(OpenAPISchema): summary="Send a NYM registration to the ledger.", ) @querystring_schema(RegisterLedgerNymQueryStringSchema()) -@response_schema(RegisterLedgerNymResponseSchema(), 200, description="") +@querystring_schema(CreateDidTxnForEndorserOptionSchema()) +@querystring_schema(SchemaConnIdMatchInfoSchema()) +@response_schema(TxnOrRegisterLedgerNymResponseSchema(), 200, description="") async def register_ledger_nym(request: web.BaseRequest): """ Request handler for registering a NYM with the ledger. @@ -181,6 +230,7 @@ async def register_ledger_nym(request: web.BaseRequest): request: aiohttp request object """ context: AdminRequestContext = request["context"] + outbound_handler = request["outbound_message_router"] async with context.profile.session() as session: ledger = session.inject_or(BaseLedger) if not ledger: @@ -201,11 +251,74 @@ async def register_ledger_nym(request: web.BaseRequest): if role == "reset": # indy: empty to reset, null for regular user role = "" # visually: confusing - correct 'reset' to empty string here + create_transaction_for_endorser = json.loads( + request.query.get("create_transaction_for_endorser", "false") + ) + write_ledger = not create_transaction_for_endorser + endorser_did = None + connection_id = request.query.get("conn_id") + + # check if we need to endorse + if is_author_role(context.profile): + # authors cannot write to the ledger + write_ledger = False + create_transaction_for_endorser = True + if not connection_id: + # author has not provided a connection id, so determine which to use + connection_id = await get_endorser_connection_id(context.profile) + if not connection_id: + raise web.HTTPBadRequest(reason="No endorser connection found") + + if not write_ledger: + try: + async with context.profile.session() as session: + connection_record = await ConnRecord.retrieve_by_id( + session, connection_id + ) + except StorageNotFoundError as err: + raise web.HTTPNotFound(reason=err.roll_up) from err + except BaseModelError as err: + raise web.HTTPBadRequest(reason=err.roll_up) from err + + async with context.profile.session() as session: + endorser_info = await connection_record.metadata_get( + session, "endorser_info" + ) + if not endorser_info: + raise web.HTTPForbidden( + reason="Endorser Info is not set up in " + "connection metadata for this connection record" + ) + if "endorser_did" not in endorser_info.keys(): + raise web.HTTPForbidden( + reason=' "endorser_did" is not set in "endorser_info"' + " in connection metadata for this connection record" + ) + endorser_did = endorser_info["endorser_did"] + + meta_data = {"did": did, "verkey": verkey, "alias": alias, "role": role} success = False + txn = None async with ledger: try: - await ledger.register_nym(did, verkey, alias, role) - success = True + # if we are an author check if we have a public DID or not + write_ledger_nym_transaction = True + # special case - if we are an author with no public DID + if create_transaction_for_endorser: + public_info = await ledger.get_wallet_public_did() + if not public_info: + write_ledger_nym_transaction = False + success = False + txn = {"signed_txn": json.dumps(meta_data)} + if write_ledger_nym_transaction: + (success, txn) = await ledger.register_nym( + did, + verkey, + alias, + role, + write_ledger=write_ledger, + endorser_did=endorser_did, + ) except LedgerTransactionError as err: raise web.HTTPForbidden(reason=err.roll_up) except LedgerError as err: @@ -220,7 +333,45 @@ async def register_ledger_nym(request: web.BaseRequest): ) ) - return web.json_response({"success": success}) + if not create_transaction_for_endorser: + # Notify event + await notify_register_did_event(context.profile, did, meta_data) + return web.json_response({"success": success}) + else: + transaction_mgr = TransactionManager(context.profile) + try: + transaction = await transaction_mgr.create_record( + messages_attach=txn["signed_txn"], + connection_id=connection_id, + meta_data=meta_data, + ) + except StorageError as err: + raise web.HTTPBadRequest(reason=err.roll_up) from err + + # if auto-request, send the request to the endorser + if context.settings.get_value("endorser.auto_request"): + try: + endorser_write_txn = not write_ledger_nym_transaction + transaction, transaction_request = await transaction_mgr.create_request( + transaction=transaction, + author_goal_code=TransactionRecord.REGISTER_PUBLIC_DID + if endorser_write_txn + else None, + signer_goal_code=TransactionRecord.WRITE_DID_TRANSACTION + if endorser_write_txn + else None, + endorser_write_txn=endorser_write_txn, + # TODO see if we need to parameterize these params + # expires_time=expires_time, + # endorser_write_txn=endorser_write_txn, + ) + txn = transaction.serialize() + except (StorageError, TransactionManagerError) as err: + raise web.HTTPBadRequest(reason=err.roll_up) from err + + await outbound_handler(transaction_request, connection_id=connection_id) + + return web.json_response({"success": success, "txn": txn}) @docs( @@ -243,7 +394,11 @@ async def get_nym_role(request: web.BaseRequest): raise web.HTTPBadRequest(reason="Request query must include DID") async with context.profile.session() as session: - ledger_exec_inst = session.inject(IndyLedgerRequestsExecutor) + multitenant_mgr = session.inject_or(BaseMultitenantManager) + if multitenant_mgr: + ledger_exec_inst = IndyLedgerRequestsExecutor(context.profile) + else: + ledger_exec_inst = session.inject(IndyLedgerRequestsExecutor) ledger_id, ledger = await ledger_exec_inst.get_ledger_for_identifier( did, txn_record_type=GET_NYM_ROLE, @@ -316,7 +471,11 @@ async def get_did_verkey(request: web.BaseRequest): raise web.HTTPBadRequest(reason="Request query must include DID") async with context.profile.session() as session: - ledger_exec_inst = session.inject(IndyLedgerRequestsExecutor) + multitenant_mgr = session.inject_or(BaseMultitenantManager) + if multitenant_mgr: + ledger_exec_inst = IndyLedgerRequestsExecutor(context.profile) + else: + ledger_exec_inst = session.inject(IndyLedgerRequestsExecutor) ledger_id, ledger = await ledger_exec_inst.get_ledger_for_identifier( did, txn_record_type=GET_KEY_FOR_DID, @@ -361,7 +520,11 @@ async def get_did_endpoint(request: web.BaseRequest): raise web.HTTPBadRequest(reason="Request query must include DID") async with context.profile.session() as session: - ledger_exec_inst = session.inject(IndyLedgerRequestsExecutor) + multitenant_mgr = session.inject_or(BaseMultitenantManager) + if multitenant_mgr: + ledger_exec_inst = IndyLedgerRequestsExecutor(context.profile) + else: + ledger_exec_inst = session.inject(IndyLedgerRequestsExecutor) ledger_id, ledger = await ledger_exec_inst.get_ledger_for_identifier( did, txn_record_type=GET_ENDPOINT_FOR_DID, @@ -451,6 +614,7 @@ async def ledger_accept_taa(request: web.BaseRequest): raise web.HTTPForbidden(reason=reason) accept_input = await request.json() + LOGGER.info(">>> accepting TAA with: %s", accept_input) async with ledger: try: taa_info = await ledger.get_txn_author_agreement() @@ -458,13 +622,27 @@ async def ledger_accept_taa(request: web.BaseRequest): raise web.HTTPBadRequest( reason=f"Ledger {ledger.pool_name} TAA not available" ) + LOGGER.info("TAA on ledger: ", taa_info) + # this is a bit of a hack, but the "\ufeff" code is included in the + # ledger TAA and digest calculation, so it needs to be included in the + # TAA text that the user is accepting + # (if you copy the TAA text using swagger it won't include this character) + if taa_info["taa_record"]["text"].startswith("\ufeff"): + if not accept_input["text"].startswith("\ufeff"): + LOGGER.info( + ">>> pre-pending -endian character to TAA acceptance text" + ) + accept_input["text"] = "\ufeff" + accept_input["text"] taa_record = { "version": accept_input["version"], "text": accept_input["text"], "digest": ledger.taa_digest( - accept_input["version"], accept_input["text"] + accept_input["version"], + accept_input["text"], ), } + taa_record_digest = taa_record["digest"] + LOGGER.info(">>> accepting with digest: %s", taa_record_digest) await ledger.accept_txn_author_agreement( taa_record, accept_input["mechanism"] ) diff --git a/aries_cloudagent/ledger/tests/test_indy.py b/aries_cloudagent/ledger/tests/test_indy.py index c558e1c332..fe9c8216ee 100644 --- a/aries_cloudagent/ledger/tests/test_indy.py +++ b/aries_cloudagent/ledger/tests/test_indy.py @@ -8,19 +8,17 @@ from asynctest import mock as async_mock, TestCase as AsyncTestCase from ...config.injection_context import InjectionContext -from ...core.in_memory import InMemoryProfile from ...cache.in_memory import InMemoryCache from ...indy.issuer import IndyIssuer, IndyIssuerError from ...indy.sdk.profile import IndySdkProfile -from ...indy.sdk.wallet_setup import IndyWalletConfig from ...storage.record import StorageRecord from ...wallet.base import BaseWallet from ...wallet.did_info import DIDInfo from ...wallet.did_posture import DIDPosture from ...wallet.error import WalletNotFoundError -from ...wallet.indy import IndyOpenWallet, IndySdkWallet -from ...wallet.key_type import KeyType -from ...wallet.did_method import DIDMethod +from ...wallet.indy import IndySdkWallet +from ...wallet.key_type import ED25519 +from ...wallet.did_method import SOV from ..endpoint_type import EndpointType from ..indy import ( @@ -70,16 +68,17 @@ async def setUp(self): did=self.test_did, verkey="3Dn1SJNPaCXcvvJvSbsFWP2xaCjMom3can8CQNhWrTRx", metadata={"test": "test"}, - method=DIDMethod.SOV, - key_type=KeyType.ED25519, + method=SOV, + key_type=ED25519, ) self.test_verkey = "3Dn1SJNPaCXcvvJvSbsFWP2xaCjMom3can8CQNhWrTRx" context = InjectionContext() context.injector.bind_instance(IndySdkLedgerPool, IndySdkLedgerPool("name")) - self.profile = IndySdkProfile( - async_mock.CoroutineMock(), - context, - ) + with async_mock.patch.object(IndySdkProfile, "_make_finalizer"): + self.profile = IndySdkProfile( + async_mock.CoroutineMock(), + context, + ) self.session = await self.profile.session() @async_mock.patch("indy.pool.create_pool_ledger_config") @@ -338,7 +337,6 @@ async def test_submit_signed_taa_accept( mock_create_config, mock_set_proto, ): - mock_append_taa.return_value = "{}" mock_sign_submit.return_value = '{"op": "REPLY"}' @@ -388,7 +386,6 @@ async def test_submit_unsigned( mock_create_config, mock_set_proto, ): - mock_did = async_mock.MagicMock() future = asyncio.Future() @@ -420,7 +417,6 @@ async def test_submit_unsigned_ledger_transaction_error( mock_create_config, mock_set_proto, ): - mock_did = async_mock.MagicMock() future = asyncio.Future() @@ -455,7 +451,6 @@ async def test_submit_rejected( mock_create_config, mock_set_proto, ): - mock_did = async_mock.MagicMock() future = asyncio.Future() @@ -536,8 +531,10 @@ async def test_txn_endorse( @async_mock.patch("aries_cloudagent.storage.indy.IndySdkStorage.add_record") @async_mock.patch("indy.ledger.build_schema_request") @async_mock.patch("indy.ledger.append_request_endorser") + @async_mock.patch("aries_cloudagent.ledger.indy.IndySdkLedger.is_ledger_read_only") async def test_send_schema( self, + mock_is_ledger_read_only, mock_append_request_endorser, mock_build_schema_req, mock_add_record, @@ -549,6 +546,7 @@ async def test_send_schema( ): mock_wallet = async_mock.MagicMock() self.session.context.injector.bind_provider(BaseWallet, mock_wallet) + mock_is_ledger_read_only.return_value = False issuer = async_mock.MagicMock(IndyIssuer) ledger = IndySdkLedger(IndySdkLedgerPool("name", checked=True), self.profile) @@ -592,8 +590,9 @@ async def test_send_schema( mock_submit.assert_called_once_with( mock_build_schema_req.return_value, - True, + sign=True, sign_did=mock_wallet_get_public_did.return_value, + taa_accept=None, write_ledger=True, ) @@ -675,8 +674,10 @@ async def test_send_schema_already_exists( ) @async_mock.patch("aries_cloudagent.storage.indy.IndySdkStorage.add_record") @async_mock.patch("indy.ledger.build_schema_request") + @async_mock.patch("aries_cloudagent.ledger.indy.IndySdkLedger.is_ledger_read_only") async def test_send_schema_ledger_transaction_error_already_exists( self, + mock_is_ledger_read_only, mock_build_schema_req, mock_add_record, mock_check_existing, @@ -685,9 +686,9 @@ async def test_send_schema_ledger_transaction_error_already_exists( mock_create_config, mock_set_proto, ): - mock_wallet = async_mock.MagicMock() self.session.context.injector.bind_provider(BaseWallet, mock_wallet) + mock_is_ledger_read_only.return_value = False issuer = async_mock.MagicMock(IndyIssuer) issuer.create_schema.return_value = ("1", "{}") @@ -730,7 +731,6 @@ async def test_send_schema_ledger_read_only( mock_create_config, mock_set_proto, ): - mock_wallet = async_mock.MagicMock() self.session.context.injector.bind_provider(BaseWallet, mock_wallet) @@ -762,17 +762,19 @@ async def test_send_schema_ledger_read_only( @async_mock.patch( "aries_cloudagent.ledger.indy.IndySdkLedger.check_existing_schema" ) + @async_mock.patch("aries_cloudagent.ledger.indy.IndySdkLedger.is_ledger_read_only") async def test_send_schema_issuer_error( self, + mock_is_ledger_read_only, mock_check_existing, mock_close_pool, mock_open_ledger, mock_create_config, mock_set_proto, ): - mock_wallet = async_mock.MagicMock() self.session.context.injector.bind_provider(BaseWallet, mock_wallet) + mock_is_ledger_read_only.return_value = False issuer = async_mock.MagicMock(IndyIssuer) issuer.create_schema = async_mock.CoroutineMock( @@ -814,7 +816,6 @@ async def test_send_schema_ledger_transaction_error( mock_create_config, mock_set_proto, ): - mock_wallet = async_mock.MagicMock() self.session.context.injector.bind_provider(BaseWallet, mock_wallet) @@ -848,8 +849,10 @@ async def test_send_schema_ledger_transaction_error( ) @async_mock.patch("aries_cloudagent.storage.indy.IndySdkStorage.add_record") @async_mock.patch("indy.ledger.build_schema_request") + @async_mock.patch("aries_cloudagent.ledger.indy.IndySdkLedger.is_ledger_read_only") async def test_send_schema_no_seq_no( self, + mock_is_ledger_read_only, mock_build_schema_req, mock_add_record, mock_fetch_schema_by_seq_no, @@ -861,6 +864,7 @@ async def test_send_schema_no_seq_no( mock_wallet = async_mock.MagicMock() self.session.context.injector.bind_provider(BaseWallet, mock_wallet) issuer = async_mock.MagicMock(IndyIssuer) + mock_is_ledger_read_only.return_value = False ledger = IndySdkLedger(IndySdkLedgerPool("name", checked=True), self.profile) issuer.create_schema.return_value = ("schema_issuer_did:name:1.0", "{}") mock_fetch_schema_by_id.return_value = None @@ -1141,8 +1145,10 @@ async def test_get_schema_by_wrong_seq_no( @async_mock.patch("aries_cloudagent.storage.indy.IndySdkStorage.find_all_records") @async_mock.patch("aries_cloudagent.storage.indy.IndySdkStorage.add_record") @async_mock.patch("indy.ledger.build_cred_def_request") + @async_mock.patch("aries_cloudagent.ledger.indy.IndySdkLedger.is_ledger_read_only") async def test_send_credential_definition( self, + mock_is_ledger_read_only, mock_build_cred_def, mock_add_record, mock_find_all_records, @@ -1155,6 +1161,7 @@ async def test_send_credential_definition( mock_wallet = async_mock.MagicMock() self.session.context.injector.bind_provider(BaseWallet, mock_wallet) mock_find_all_records.return_value = [] + mock_is_ledger_read_only.return_value = False mock_get_schema.return_value = {"seqNo": 999} cred_def_id = f"{self.test_did}:3:CL:999:default" @@ -1202,8 +1209,8 @@ async def test_send_credential_definition( did=self.test_did, verkey=self.test_verkey, metadata=None, - method=DIDMethod.SOV, - key_type=KeyType.ED25519, + method=SOV, + key_type=ED25519, ) mock_did = mock_wallet_get_public_did.return_value ( @@ -1229,8 +1236,10 @@ async def test_send_credential_definition( @async_mock.patch("aries_cloudagent.storage.indy.IndySdkStorage.add_record") @async_mock.patch("indy.ledger.build_cred_def_request") @async_mock.patch("indy.ledger.append_request_endorser") + @async_mock.patch("aries_cloudagent.ledger.indy.IndySdkLedger.is_ledger_read_only") async def test_send_credential_definition_endorse_only( self, + mock_is_ledger_read_only, mock_append_request_endorser, mock_build_cred_def, mock_add_record, @@ -1244,6 +1253,7 @@ async def test_send_credential_definition_endorse_only( mock_wallet = async_mock.MagicMock() self.session.context.injector.bind_provider(BaseWallet, mock_wallet) mock_find_all_records.return_value = [] + mock_is_ledger_read_only.return_value = False mock_get_schema.return_value = {"seqNo": 999} cred_def_id = f"{self.test_did}:3:CL:999:default" @@ -1279,8 +1289,8 @@ async def test_send_credential_definition_endorse_only( self.test_did, self.test_verkey, None, - DIDMethod.SOV, - KeyType.ED25519, + SOV, + ED25519, ) async with ledger: ( @@ -1362,8 +1372,8 @@ async def test_send_credential_definition_exists_in_ledger_and_wallet( did=self.test_did, verkey=self.test_verkey, metadata=None, - method=DIDMethod.SOV, - key_type=KeyType.ED25519, + method=SOV, + key_type=ED25519, ) async with ledger: @@ -1774,8 +1784,8 @@ async def test_send_credential_definition_on_ledger_in_wallet( did=self.test_did, verkey=self.test_verkey, metadata=None, - method=DIDMethod.SOV, - key_type=KeyType.ED25519, + method=SOV, + key_type=ED25519, ) mock_did = mock_wallet_get_public_did.return_value @@ -1848,8 +1858,8 @@ async def test_send_credential_definition_create_cred_def_exception( did=self.test_did, verkey=self.test_verkey, metadata=None, - method=DIDMethod.SOV, - key_type=KeyType.ED25519, + method=SOV, + key_type=ED25519, ) async with ledger: with self.assertRaises(LedgerError): @@ -2227,8 +2237,10 @@ async def test_get_endpoint_for_did_no_endpoint( @async_mock.patch("indy.ledger.build_get_attrib_request") @async_mock.patch("indy.ledger.build_attrib_request") @async_mock.patch("aries_cloudagent.ledger.indy.IndySdkLedger._submit") + @async_mock.patch("aries_cloudagent.ledger.indy.IndySdkLedger.is_ledger_read_only") async def test_update_endpoint_for_did( self, + mock_is_ledger_read_only, mock_submit, mock_build_attrib_req, mock_build_get_attrib_req, @@ -2238,6 +2250,7 @@ async def test_update_endpoint_for_did( mock_wallet = async_mock.MagicMock() self.session.context.injector.bind_provider(BaseWallet, mock_wallet) endpoint = ["http://old.aries.ca", "http://new.aries.ca"] + mock_is_ledger_read_only.return_value = False mock_submit.side_effect = [ json.dumps( { @@ -2276,13 +2289,116 @@ async def test_update_endpoint_for_did( ) assert response + @async_mock.patch("aries_cloudagent.ledger.indy.IndySdkLedgerPool.context_open") + @async_mock.patch("aries_cloudagent.ledger.indy.IndySdkLedgerPool.context_close") + @pytest.mark.asyncio + async def test_construct_attr_json_with_routing_keys(self, mock_close, mock_open): + ledger = IndySdkLedger(IndySdkLedgerPool("name", checked=True), self.profile) + async with ledger: + attr_json = await ledger._construct_attr_json( + "https://url", + EndpointType.ENDPOINT, + routing_keys=["3YJCx3TqotDWFGv7JMR5erEvrmgu5y4FDqjR7sKWxgXn"], + ) + assert attr_json == json.dumps( + { + "endpoint": { + "endpoint": "https://url", + "routingKeys": ["3YJCx3TqotDWFGv7JMR5erEvrmgu5y4FDqjR7sKWxgXn"], + } + } + ) + + @async_mock.patch("aries_cloudagent.ledger.indy.IndySdkLedgerPool.context_open") + @async_mock.patch("aries_cloudagent.ledger.indy.IndySdkLedgerPool.context_close") + @pytest.mark.asyncio + async def test_construct_attr_json_with_routing_keys_all_exist_endpoints( + self, mock_close, mock_open + ): + ledger = IndySdkLedger(IndySdkLedgerPool("name", checked=True), self.profile) + async with ledger: + attr_json = await ledger._construct_attr_json( + "https://url", + EndpointType.ENDPOINT, + all_exist_endpoints={"profile": "https://endpoint/profile"}, + routing_keys=["3YJCx3TqotDWFGv7JMR5erEvrmgu5y4FDqjR7sKWxgXn"], + ) + assert attr_json == json.dumps( + { + "endpoint": { + "profile": "https://endpoint/profile", + "endpoint": "https://url", + "routingKeys": ["3YJCx3TqotDWFGv7JMR5erEvrmgu5y4FDqjR7sKWxgXn"], + } + } + ) + + @async_mock.patch("aries_cloudagent.ledger.indy.IndySdkLedgerPool.context_open") + @async_mock.patch("aries_cloudagent.ledger.indy.IndySdkLedgerPool.context_close") + @async_mock.patch("indy.ledger.build_get_attrib_request") + @async_mock.patch("indy.ledger.build_attrib_request") + @async_mock.patch("aries_cloudagent.ledger.indy.IndySdkLedger._submit") + @async_mock.patch("aries_cloudagent.ledger.indy.IndySdkLedger.is_ledger_read_only") + @pytest.mark.asyncio + async def test_update_endpoint_for_did_calls_attr_json( + self, + mock_is_ledger_read_only, + mock_submit, + mock_build_attrib_req, + mock_build_get_attrib_req, + mock_close, + mock_open, + ): + routing_keys = ["3YJCx3TqotDWFGv7JMR5erEvrmgu5y4FDqjR7sKWxgXn"] + mock_wallet = async_mock.MagicMock() + self.session.context.injector.bind_provider(BaseWallet, mock_wallet) + ledger = IndySdkLedger(IndySdkLedgerPool("name", checked=True), self.profile) + mock_is_ledger_read_only.return_value = False + async with ledger: + with async_mock.patch.object( + IndySdkWallet, "get_public_did" + ) as mock_wallet_get_public_did, async_mock.patch.object( + ledger, + "_construct_attr_json", + async_mock.CoroutineMock( + return_value=json.dumps( + { + "endpoint": { + "endpoint": { + "endpoint": "https://url", + "routingKeys": [], + } + } + } + ) + ), + ) as mock_construct_attr_json, async_mock.patch.object( + ledger, + "get_all_endpoints_for_did", + async_mock.CoroutineMock(return_value={}), + ), async_mock.patch.object( + ledger, "did_to_nym" + ): + mock_wallet_get_public_did.return_value = self.test_did_info + await ledger.update_endpoint_for_did( + mock_wallet_get_public_did, + "https://url", + EndpointType.ENDPOINT, + routing_keys=routing_keys, + ) + mock_construct_attr_json.assert_called_once_with( + "https://url", EndpointType.ENDPOINT, {}, routing_keys + ) + @async_mock.patch("aries_cloudagent.ledger.indy.IndySdkLedgerPool.context_open") @async_mock.patch("aries_cloudagent.ledger.indy.IndySdkLedgerPool.context_close") @async_mock.patch("indy.ledger.build_get_attrib_request") @async_mock.patch("indy.ledger.build_attrib_request") @async_mock.patch("aries_cloudagent.ledger.indy.IndySdkLedger._submit") + @async_mock.patch("aries_cloudagent.ledger.indy.IndySdkLedger.is_ledger_read_only") async def test_update_endpoint_for_did_no_prior_endpoints( self, + mock_is_ledger_read_only, mock_submit, mock_build_attrib_req, mock_build_get_attrib_req, @@ -2292,6 +2408,7 @@ async def test_update_endpoint_for_did_no_prior_endpoints( mock_wallet = async_mock.MagicMock() self.session.context.injector.bind_provider(BaseWallet, mock_wallet) endpoint = "http://new.aries.ca" + mock_is_ledger_read_only.return_value = False ledger = IndySdkLedger(IndySdkLedgerPool("name", checked=True), self.profile) with async_mock.patch.object( IndySdkWallet, "get_public_did" @@ -2327,8 +2444,10 @@ async def test_update_endpoint_for_did_no_prior_endpoints( @async_mock.patch("indy.ledger.build_get_attrib_request") @async_mock.patch("indy.ledger.build_attrib_request") @async_mock.patch("aries_cloudagent.ledger.indy.IndySdkLedger._submit") + @async_mock.patch("aries_cloudagent.ledger.indy.IndySdkLedger.is_ledger_read_only") async def test_update_endpoint_of_type_profile_for_did( self, + mock_is_ledger_read_only, mock_submit, mock_build_attrib_req, mock_build_get_attrib_req, @@ -2339,6 +2458,7 @@ async def test_update_endpoint_of_type_profile_for_did( self.session.context.injector.bind_provider(BaseWallet, mock_wallet) endpoint = ["http://company.com/oldProfile", "http://company.com/newProfile"] endpoint_type = EndpointType.PROFILE + mock_is_ledger_read_only.return_value = False mock_submit.side_effect = [ json.dumps( { @@ -2352,6 +2472,11 @@ async def test_update_endpoint_of_type_profile_for_did( for i in range(len(endpoint)) ] ledger = IndySdkLedger(IndySdkLedgerPool("name", checked=True), self.profile) + # ledger = async_mock.patch.object( + # ledger, + # "is_ledger_read_only", + # async_mock.CoroutineMock(return_value=False), + # ) with async_mock.patch.object( IndySdkWallet, "get_public_did" ) as mock_wallet_get_public_did: @@ -2444,11 +2569,18 @@ async def test_update_endpoint_for_did_read_only( @async_mock.patch("aries_cloudagent.ledger.indy.IndySdkLedgerPool.context_close") @async_mock.patch("indy.ledger.build_nym_request") @async_mock.patch("aries_cloudagent.ledger.indy.IndySdkLedger._submit") + @async_mock.patch("aries_cloudagent.ledger.indy.IndySdkLedger.is_ledger_read_only") async def test_register_nym( - self, mock_submit, mock_build_nym_req, mock_close, mock_open + self, + mock_is_ledger_read_only, + mock_submit, + mock_build_nym_req, + mock_close, + mock_open, ): mock_wallet = async_mock.MagicMock() self.session.context.injector.bind_provider(BaseWallet, mock_wallet) + mock_is_ledger_read_only.return_value = False with async_mock.patch.object( IndySdkWallet, "get_public_did" ) as mock_wallet_get_public_did, async_mock.patch.object( @@ -2516,12 +2648,19 @@ async def test_register_nym_read_only(self, mock_close, mock_open): @async_mock.patch("aries_cloudagent.ledger.indy.IndySdkLedgerPool.context_open") @async_mock.patch("aries_cloudagent.ledger.indy.IndySdkLedgerPool.context_close") - async def test_register_nym_no_public_did(self, mock_close, mock_open): + @async_mock.patch("aries_cloudagent.ledger.indy.IndySdkLedger.is_ledger_read_only") + async def test_register_nym_no_public_did( + self, + mock_is_ledger_read_only, + mock_close, + mock_open, + ): mock_wallet = async_mock.MagicMock( type="indy", get_local_did=async_mock.CoroutineMock(), replace_local_did_metadata=async_mock.CoroutineMock(), ) + mock_is_ledger_read_only.return_value = False self.session.context.injector.bind_provider(BaseWallet, mock_wallet) ledger = IndySdkLedger(IndySdkLedgerPool("name", checked=True), self.profile) with async_mock.patch.object( @@ -2541,14 +2680,21 @@ async def test_register_nym_no_public_did(self, mock_close, mock_open): @async_mock.patch("aries_cloudagent.ledger.indy.IndySdkLedgerPool.context_close") @async_mock.patch("indy.ledger.build_nym_request") @async_mock.patch("aries_cloudagent.ledger.indy.IndySdkLedger._submit") + @async_mock.patch("aries_cloudagent.ledger.indy.IndySdkLedger.is_ledger_read_only") async def test_register_nym_ledger_x( - self, mock_submit, mock_build_nym_req, mock_close, mock_open + self, + mock_is_ledger_read_only, + mock_submit, + mock_build_nym_req, + mock_close, + mock_open, ): mock_wallet = async_mock.MagicMock() mock_build_nym_req.side_effect = IndyError( error_code=ErrorCode.CommonInvalidParam1, error_details={"message": "not today"}, ) + mock_is_ledger_read_only.return_value = False self.session.context.injector.bind_provider(BaseWallet, mock_wallet) ledger = IndySdkLedger(IndySdkLedgerPool("name", checked=True), self.profile) with async_mock.patch.object( @@ -2568,11 +2714,18 @@ async def test_register_nym_ledger_x( @async_mock.patch("aries_cloudagent.ledger.indy.IndySdkLedgerPool.context_close") @async_mock.patch("indy.ledger.build_nym_request") @async_mock.patch("aries_cloudagent.ledger.indy.IndySdkLedger._submit") + @async_mock.patch("aries_cloudagent.ledger.indy.IndySdkLedger.is_ledger_read_only") async def test_register_nym_steward_register_others_did( - self, mock_submit, mock_build_nym_req, mock_close, mock_open + self, + mock_is_ledger_read_only, + mock_submit, + mock_build_nym_req, + mock_close, + mock_open, ): mock_wallet = async_mock.MagicMock() self.session.context.injector.bind_provider(BaseWallet, mock_wallet) + mock_is_ledger_read_only.return_value = False ledger = IndySdkLedger(IndySdkLedgerPool("name", checked=True), self.profile) with async_mock.patch.object( IndySdkWallet, "get_public_did" diff --git a/aries_cloudagent/ledger/tests/test_indy_vdr.py b/aries_cloudagent/ledger/tests/test_indy_vdr.py index bf8375f1d6..6a575a5737 100644 --- a/aries_cloudagent/ledger/tests/test_indy_vdr.py +++ b/aries_cloudagent/ledger/tests/test_indy_vdr.py @@ -1,4 +1,5 @@ import json +from aries_cloudagent.messaging.valid import ENDPOINT_TYPE import pytest from asynctest import mock as async_mock @@ -8,8 +9,8 @@ from ...core.in_memory import InMemoryProfile from ...indy.issuer import IndyIssuer from ...wallet.base import BaseWallet -from ...wallet.key_type import KeyType -from ...wallet.did_method import DIDMethod +from ...wallet.key_type import KeyType, ED25519 +from ...wallet.did_method import SOV, DIDMethods from ...wallet.did_info import DIDInfo from ..endpoint_type import EndpointType @@ -27,7 +28,7 @@ @pytest.fixture() def ledger(): - profile = InMemoryProfile.test_profile() + profile = InMemoryProfile.test_profile(bind={DIDMethods: DIDMethods()}) ledger = IndyVdrLedger(IndyVdrLedgerPool("test-ledger"), profile) async def open(): @@ -38,6 +39,8 @@ async def close(): with async_mock.patch.object(ledger.pool, "open", open), async_mock.patch.object( ledger.pool, "close", close + ), async_mock.patch.object( + ledger, "is_ledger_read_only", async_mock.CoroutineMock(return_value=False) ): yield ledger @@ -64,7 +67,7 @@ async def test_submit_signed( ledger: IndyVdrLedger, ): wallet = (await ledger.profile.session()).wallet - test_did = await wallet.create_public_did(DIDMethod.SOV, KeyType.ED25519) + test_did = await wallet.create_public_did(SOV, ED25519) test_msg = indy_vdr.ledger.build_get_txn_request(test_did.did, 1, 1) async with ledger: @@ -117,7 +120,7 @@ async def test_submit_signed_taa_accept( ledger: IndyVdrLedger, ): wallet = (await ledger.profile.session()).wallet - test_did = await wallet.create_public_did(DIDMethod.SOV, KeyType.ED25519) + test_did = await wallet.create_public_did(SOV, ED25519) async with ledger: test_msg = indy_vdr.ledger.build_get_txn_request(test_did.did, 1, 1) @@ -185,7 +188,7 @@ async def test_txn_endorse( with pytest.raises(BadLedgerRequestError): await ledger.txn_endorse(request_json=test_msg.body) - test_did = await wallet.create_public_did(DIDMethod.SOV, KeyType.ED25519) + test_did = await wallet.create_public_did(SOV, ED25519) test_msg.set_endorser(test_did.did) endorsed_json = await ledger.txn_endorse(request_json=test_msg.body) @@ -198,7 +201,7 @@ async def test_send_schema( ledger: IndyVdrLedger, ): wallet = (await ledger.profile.session()).wallet - test_did = await wallet.create_public_did(DIDMethod.SOV, KeyType.ED25519) + test_did = await wallet.create_public_did(SOV, ED25519) issuer = async_mock.MagicMock(IndyIssuer) issuer.create_schema.return_value = ( "schema_issuer_did:schema_name:9.1", @@ -237,8 +240,9 @@ async def test_send_schema( endorser_did=test_did.did, ) assert schema_id == issuer.create_schema.return_value[0] - assert signed_txn["signed_txn"].get("endorser") == test_did.did - assert signed_txn["signed_txn"].get("signature") + txn = json.loads(signed_txn["signed_txn"]) + assert txn.get("endorser") == test_did.did + assert txn.get("signature") @pytest.mark.asyncio async def test_send_schema_no_public_did( @@ -258,7 +262,7 @@ async def test_send_schema_already_exists( ledger: IndyVdrLedger, ): wallet = (await ledger.profile.session()).wallet - test_did = await wallet.create_public_did(DIDMethod.SOV, KeyType.ED25519) + test_did = await wallet.create_public_did(SOV, ED25519) issuer = async_mock.MagicMock(IndyIssuer) issuer.create_schema.return_value = ( "schema_issuer_did:schema_name:9.1", @@ -288,7 +292,7 @@ async def test_send_schema_ledger_read_only( ledger: IndyVdrLedger, ): wallet = (await ledger.profile.session()).wallet - test_did = await wallet.create_public_did(DIDMethod.SOV, KeyType.ED25519) + test_did = await wallet.create_public_did(SOV, ED25519) issuer = async_mock.MagicMock(IndyIssuer) issuer.create_schema.return_value = ( "schema_issuer_did:schema_name:9.1", @@ -301,6 +305,10 @@ async def test_send_schema_ledger_read_only( ledger, "check_existing_schema", async_mock.CoroutineMock(return_value=False), + ), async_mock.patch.object( + ledger, + "is_ledger_read_only", + async_mock.CoroutineMock(return_value=True), ): with pytest.raises(LedgerError): schema_id, schema_def = await ledger.create_and_send_schema( @@ -313,7 +321,7 @@ async def test_send_schema_ledger_transaction_error( ledger: IndyVdrLedger, ): wallet = (await ledger.profile.session()).wallet - test_did = await wallet.create_public_did(DIDMethod.SOV, KeyType.ED25519) + test_did = await wallet.create_public_did(SOV, ED25519) issuer = async_mock.MagicMock(IndyIssuer) issuer.create_schema.return_value = ( "schema_issuer_did:schema_name:9.1", @@ -375,7 +383,7 @@ async def test_send_credential_definition( ledger: IndyVdrLedger, ): wallet = (await ledger.profile.session()).wallet - test_did = await wallet.create_public_did(DIDMethod.SOV, KeyType.ED25519) + test_did = await wallet.create_public_did(SOV, ED25519) schema_id = "55GkHamhTU1ZbTbV2ab9DE:2:schema_name:9.1" cred_def_id = "55GkHamhTU1ZbTbV2ab9DE:3:CL:99:tag" cred_def = { @@ -590,7 +598,7 @@ async def test_update_endpoint_for_did( ledger: IndyVdrLedger, ): wallet = (await ledger.profile.session()).wallet - test_did = await wallet.create_public_did(DIDMethod.SOV, KeyType.ED25519) + test_did = await wallet.create_public_did(SOV, ED25519) async with ledger: ledger.pool_handle.submit_request.side_effect = ( {"data": None}, @@ -600,6 +608,109 @@ async def test_update_endpoint_for_did( "55GkHamhTU1ZbTbV2ab9DE", "https://url", EndpointType.ENDPOINT ) + @pytest.mark.parametrize( + "all_exist_endpoints, routing_keys, result", + [ + ( + {"profile": "https://endpoint/profile"}, + ["3YJCx3TqotDWFGv7JMR5erEvrmgu5y4FDqjR7sKWxgXn"], + { + "endpoint": { + "profile": "https://endpoint/profile", + "endpoint": "https://url", + "routingKeys": ["3YJCx3TqotDWFGv7JMR5erEvrmgu5y4FDqjR7sKWxgXn"], + } + }, + ), + ( + {"profile": "https://endpoint/profile"}, + None, + { + "endpoint": { + "profile": "https://endpoint/profile", + "endpoint": "https://url", + "routingKeys": [], + } + }, + ), + ( + None, + ["3YJCx3TqotDWFGv7JMR5erEvrmgu5y4FDqjR7sKWxgXn"], + { + "endpoint": { + "endpoint": "https://url", + "routingKeys": ["3YJCx3TqotDWFGv7JMR5erEvrmgu5y4FDqjR7sKWxgXn"], + } + }, + ), + (None, None, {"endpoint": {"endpoint": "https://url", "routingKeys": []}}), + ( + { + "profile": "https://endpoint/profile", + "spec_divergent_endpoint": "https://endpoint", + }, + ["3YJCx3TqotDWFGv7JMR5erEvrmgu5y4FDqjR7sKWxgXn"], + { + "endpoint": { + "profile": "https://endpoint/profile", + "spec_divergent_endpoint": "https://endpoint", + "endpoint": "https://url", + "routingKeys": ["3YJCx3TqotDWFGv7JMR5erEvrmgu5y4FDqjR7sKWxgXn"], + } + }, + ), + ], + ) + @pytest.mark.asyncio + async def test_construct_attr_json( + self, ledger: IndyVdrLedger, all_exist_endpoints, routing_keys, result + ): + async with ledger: + attr_json = await ledger._construct_attr_json( + "https://url", EndpointType.ENDPOINT, all_exist_endpoints, routing_keys + ) + assert attr_json == json.dumps(result) + + @pytest.mark.asyncio + async def test_update_endpoint_for_did_calls_attr_json(self, ledger: IndyVdrLedger): + routing_keys = ["3YJCx3TqotDWFGv7JMR5erEvrmgu5y4FDqjR7sKWxgXn"] + wallet = (await ledger.profile.session()).wallet + test_did = await wallet.create_public_did(SOV, ED25519) + + async with ledger: + with async_mock.patch.object( + ledger, + "_construct_attr_json", + async_mock.CoroutineMock( + return_value=json.dumps( + { + "endpoint": { + "endpoint": { + "endpoint": "https://url", + "routingKeys": [], + } + } + } + ) + ), + ) as mock_construct_attr_json, async_mock.patch.object( + ledger, + "get_all_endpoints_for_did", + async_mock.CoroutineMock(return_value={}), + ): + await ledger.update_endpoint_for_did( + test_did.did, + "https://url", + EndpointType.ENDPOINT, + routing_keys=routing_keys, + ) + mock_construct_attr_json.assert_called_once_with( + "https://url", + EndpointType.ENDPOINT, + {}, + routing_keys, + ) + @pytest.mark.asyncio async def test_update_endpoint_for_did_no_public( self, @@ -631,8 +742,8 @@ async def test_register_nym_local( ledger: IndyVdrLedger, ): wallet: BaseWallet = (await ledger.profile.session()).wallet - public_did = await wallet.create_public_did(DIDMethod.SOV, KeyType.ED25519) - post_did = await wallet.create_local_did(DIDMethod.SOV, KeyType.ED25519) + public_did = await wallet.create_public_did(SOV, ED25519) + post_did = await wallet.create_local_did(SOV, ED25519) async with ledger: await ledger.register_nym(post_did.did, post_did.verkey) did = await wallet.get_local_did(post_did.did) @@ -644,7 +755,7 @@ async def test_register_nym_non_local( ledger: IndyVdrLedger, ): wallet: BaseWallet = (await ledger.profile.session()).wallet - public_did = await wallet.create_public_did(DIDMethod.SOV, KeyType.ED25519) + public_did = await wallet.create_public_did(SOV, ED25519) async with ledger: await ledger.register_nym("55GkHamhTU1ZbTbV2ab9DE", "verkey") @@ -787,7 +898,7 @@ async def test_send_revoc_reg_def( ledger: IndyVdrLedger, ): wallet: BaseWallet = (await ledger.profile.session()).wallet - public_did = await wallet.create_public_did(DIDMethod.SOV, KeyType.ED25519) + public_did = await wallet.create_public_did(SOV, ED25519) async with ledger: reg_id = ( "55GkHamhTU1ZbTbV2ab9DE:4:55GkHamhTU1ZbTbV2ab9DE:3:CL:99:tag:CL_ACCUM:0" @@ -816,7 +927,7 @@ async def test_send_revoc_reg_entry( ledger: IndyVdrLedger, ): wallet: BaseWallet = (await ledger.profile.session()).wallet - public_did = await wallet.create_public_did(DIDMethod.SOV, KeyType.ED25519) + public_did = await wallet.create_public_did(SOV, ED25519) async with ledger: reg_id = ( "55GkHamhTU1ZbTbV2ab9DE:4:55GkHamhTU1ZbTbV2ab9DE:3:CL:99:tag:CL_ACCUM:0" @@ -855,7 +966,7 @@ async def test_credential_definition_id2schema_id(self, ledger: IndyVdrLedger): @pytest.mark.asyncio async def test_rotate_did_keypair(self, ledger: IndyVdrLedger): wallet = (await ledger.profile.session()).wallet - public_did = await wallet.create_public_did(DIDMethod.SOV, KeyType.ED25519) + public_did = await wallet.create_public_did(SOV, ED25519) async with ledger: with async_mock.patch.object( @@ -869,4 +980,5 @@ async def test_rotate_did_keypair(self, ledger: IndyVdrLedger): ] ), ): + ledger.profile.context.injector.bind_instance(DIDMethods, DIDMethods()) await ledger.rotate_public_did_keypair() diff --git a/aries_cloudagent/ledger/tests/test_routes.py b/aries_cloudagent/ledger/tests/test_routes.py index 8b2ed92a1a..14853fc6bb 100644 --- a/aries_cloudagent/ledger/tests/test_routes.py +++ b/aries_cloudagent/ledger/tests/test_routes.py @@ -1,6 +1,8 @@ -from asynctest import mock as async_mock, TestCase as AsyncTestCase +from typing import Tuple + +from async_case import IsolatedAsyncioTestCase +import mock as async_mock -from ...admin.request_context import AdminRequestContext from ...core.in_memory import InMemoryProfile from ...ledger.base import BaseLedger from ...ledger.endpoint_type import EndpointType @@ -9,14 +11,16 @@ ) from ...ledger.multiple_ledger.base_manager import ( BaseMultipleLedgerManager, - MultipleLedgerManagerError, ) +from ...multitenant.base import BaseMultitenantManager +from ...multitenant.manager import MultitenantManager from .. import routes as test_module from ..indy import Role +from ...connections.models.conn_record import ConnRecord -class TestLedgerRoutes(AsyncTestCase): +class TestLedgerRoutes(IsolatedAsyncioTestCase): def setUp(self): self.ledger = async_mock.create_autospec(BaseLedger) self.ledger.pool_name = "pool.0" @@ -26,7 +30,7 @@ def setUp(self): self.profile.context.injector.bind_instance(BaseLedger, self.ledger) self.request_dict = { "context": self.context, - "outbound_message_router": async_mock.CoroutineMock(), + "outbound_message_router": async_mock.AsyncMock(), } self.request = async_mock.MagicMock( app={}, @@ -45,7 +49,7 @@ async def test_missing_ledger(self): self.profile.context.injector.bind_instance( IndyLedgerRequestsExecutor, async_mock.MagicMock( - get_ledger_for_identifier=async_mock.CoroutineMock( + get_ledger_for_identifier=async_mock.AsyncMock( return_value=(None, None) ) ), @@ -80,7 +84,7 @@ async def test_get_verkey_a(self): self.profile.context.injector.bind_instance( IndyLedgerRequestsExecutor, async_mock.MagicMock( - get_ledger_for_identifier=async_mock.CoroutineMock( + get_ledger_for_identifier=async_mock.AsyncMock( return_value=(None, self.ledger) ) ), @@ -100,7 +104,7 @@ async def test_get_verkey_b(self): self.profile.context.injector.bind_instance( IndyLedgerRequestsExecutor, async_mock.MagicMock( - get_ledger_for_identifier=async_mock.CoroutineMock( + get_ledger_for_identifier=async_mock.AsyncMock( return_value=("test_ledger_id", self.ledger) ) ), @@ -119,6 +123,29 @@ async def test_get_verkey_b(self): ) assert result is json_response.return_value + async def test_get_verkey_multitenant(self): + self.context.injector.bind_instance( + BaseMultitenantManager, + async_mock.MagicMock(MultitenantManager, autospec=True), + ) + self.request.query = {"did": self.test_did} + with async_mock.patch.object( + IndyLedgerRequestsExecutor, + "get_ledger_for_identifier", + async_mock.AsyncMock(return_value=("test_ledger_id", self.ledger)), + ), async_mock.patch.object( + test_module.web, "json_response", async_mock.Mock() + ) as json_response: + self.ledger.get_key_for_did.return_value = self.test_verkey + result = await test_module.get_did_verkey(self.request) + json_response.assert_called_once_with( + { + "ledger_id": "test_ledger_id", + "verkey": self.ledger.get_key_for_did.return_value, + } + ) + assert result is json_response.return_value + async def test_get_verkey_no_did(self): self.request.query = {"no": "did"} with self.assertRaises(test_module.web.HTTPBadRequest): @@ -128,7 +155,7 @@ async def test_get_verkey_did_not_public(self): self.profile.context.injector.bind_instance( IndyLedgerRequestsExecutor, async_mock.MagicMock( - get_ledger_for_identifier=async_mock.CoroutineMock( + get_ledger_for_identifier=async_mock.AsyncMock( return_value=("test_ledger_id", self.ledger) ) ), @@ -142,7 +169,7 @@ async def test_get_verkey_x(self): self.profile.context.injector.bind_instance( IndyLedgerRequestsExecutor, async_mock.MagicMock( - get_ledger_for_identifier=async_mock.CoroutineMock( + get_ledger_for_identifier=async_mock.AsyncMock( return_value=(None, self.ledger) ) ), @@ -156,7 +183,7 @@ async def test_get_endpoint(self): self.profile.context.injector.bind_instance( IndyLedgerRequestsExecutor, async_mock.MagicMock( - get_ledger_for_identifier=async_mock.CoroutineMock( + get_ledger_for_identifier=async_mock.AsyncMock( return_value=(None, self.ledger) ) ), @@ -172,11 +199,34 @@ async def test_get_endpoint(self): ) assert result is json_response.return_value + async def test_get_endpoint_multitenant(self): + self.context.injector.bind_instance( + BaseMultitenantManager, + async_mock.MagicMock(MultitenantManager, autospec=True), + ) + self.request.query = {"did": self.test_did} + with async_mock.patch.object( + IndyLedgerRequestsExecutor, + "get_ledger_for_identifier", + async_mock.AsyncMock(return_value=("test_ledger_id", self.ledger)), + ), async_mock.patch.object( + test_module.web, "json_response", async_mock.Mock() + ) as json_response: + self.ledger.get_endpoint_for_did.return_value = self.test_endpoint + result = await test_module.get_did_endpoint(self.request) + json_response.assert_called_once_with( + { + "ledger_id": "test_ledger_id", + "endpoint": self.ledger.get_endpoint_for_did.return_value, + } + ) + assert result is json_response.return_value + async def test_get_endpoint_of_type_profile(self): self.profile.context.injector.bind_instance( IndyLedgerRequestsExecutor, async_mock.MagicMock( - get_ledger_for_identifier=async_mock.CoroutineMock( + get_ledger_for_identifier=async_mock.AsyncMock( return_value=("test_ledger_id", self.ledger) ) ), @@ -209,7 +259,7 @@ async def test_get_endpoint_x(self): self.profile.context.injector.bind_instance( IndyLedgerRequestsExecutor, async_mock.MagicMock( - get_ledger_for_identifier=async_mock.CoroutineMock( + get_ledger_for_identifier=async_mock.AsyncMock( return_value=("test_ledger_id", self.ledger) ) ), @@ -228,11 +278,11 @@ async def test_register_nym(self): with async_mock.patch.object( test_module.web, "json_response", async_mock.Mock() ) as json_response: - self.ledger.register_nym.return_value = True + success: bool = True + txn: dict = None + self.ledger.register_nym.return_value: Tuple[bool, dict] = (success, txn) result = await test_module.register_ledger_nym(self.request) - json_response.assert_called_once_with( - {"success": self.ledger.register_nym.return_value} - ) + json_response.assert_called_once_with({"success": success}) assert result is json_response.return_value async def test_register_nym_bad_request(self): @@ -266,11 +316,232 @@ async def test_register_nym_wallet_error(self): with self.assertRaises(test_module.web.HTTPBadRequest): await test_module.register_ledger_nym(self.request) + async def test_register_nym_create_transaction_for_endorser(self): + self.request.query = { + "did": "a_test_did", + "verkey": "a_test_verkey", + "alias": "did_alias", + "role": "ENDORSER", + "create_transaction_for_endorser": "true", + "conn_id": "dummy", + } + + with async_mock.patch.object( + ConnRecord, "retrieve_by_id", async_mock.AsyncMock() + ) as mock_conn_rec_retrieve, async_mock.patch.object( + test_module, "TransactionManager", async_mock.MagicMock() + ) as mock_txn_mgr, async_mock.patch.object( + test_module.web, "json_response", async_mock.MagicMock() + ) as mock_response: + mock_txn_mgr.return_value = async_mock.MagicMock( + create_record=async_mock.AsyncMock( + return_value=async_mock.MagicMock( + serialize=async_mock.MagicMock(return_value={"...": "..."}) + ) + ) + ) + mock_conn_rec_retrieve.return_value = async_mock.MagicMock( + metadata_get=async_mock.AsyncMock( + return_value={ + "endorser_did": ("did"), + "endorser_name": ("name"), + } + ) + ) + self.ledger.register_nym.return_value: Tuple[bool, dict] = ( + True, + {"signed_txn": {"...": "..."}}, + ) + + result = await test_module.register_ledger_nym(self.request) + assert result == mock_response.return_value + mock_response.assert_called_once_with( + {"success": True, "txn": {"signed_txn": {"...": "..."}}} + ) + + async def test_register_nym_create_transaction_for_endorser_no_public_did(self): + self.request.query = { + "did": "a_test_did", + "verkey": "a_test_verkey", + "alias": "did_alias", + "role": "reset", + "create_transaction_for_endorser": "true", + "conn_id": "dummy", + } + self.profile.context.settings["endorser.author"] = True + + with async_mock.patch.object( + ConnRecord, "retrieve_by_id", async_mock.AsyncMock() + ) as mock_conn_rec_retrieve, async_mock.patch.object( + test_module, "TransactionManager", async_mock.MagicMock() + ) as mock_txn_mgr, async_mock.patch.object( + test_module.web, "json_response", async_mock.MagicMock() + ) as mock_response: + mock_txn_mgr.return_value = async_mock.MagicMock( + create_record=async_mock.AsyncMock( + return_value=async_mock.MagicMock( + serialize=async_mock.MagicMock(return_value={"...": "..."}) + ) + ) + ) + mock_conn_rec_retrieve.return_value = async_mock.MagicMock( + metadata_get=async_mock.AsyncMock( + return_value={ + "endorser_did": ("did"), + "endorser_name": ("name"), + } + ) + ) + self.ledger.register_nym.return_value: Tuple[bool, dict] = ( + True, + {"signed_txn": {"...": "..."}}, + ) + + result = await test_module.register_ledger_nym(self.request) + assert result == mock_response.return_value + mock_response.assert_called_once_with( + {"success": True, "txn": {"signed_txn": {"...": "..."}}} + ) + + async def test_register_nym_create_transaction_for_endorser_storage_x(self): + self.request.query = { + "did": "a_test_did", + "verkey": "a_test_verkey", + "alias": "did_alias", + "role": "ENDORSER", + "create_transaction_for_endorser": "true", + "conn_id": "dummy", + } + + with async_mock.patch.object( + ConnRecord, "retrieve_by_id", async_mock.AsyncMock() + ) as mock_conn_rec_retrieve, async_mock.patch.object( + test_module, "TransactionManager", async_mock.MagicMock() + ) as mock_txn_mgr: + mock_txn_mgr.return_value = async_mock.MagicMock( + create_record=async_mock.AsyncMock( + side_effect=test_module.StorageError() + ) + ) + mock_conn_rec_retrieve.return_value = async_mock.MagicMock( + metadata_get=async_mock.AsyncMock( + return_value={ + "endorser_did": ("did"), + "endorser_name": ("name"), + } + ) + ) + self.ledger.register_nym.return_value: Tuple[bool, dict] = ( + True, + {"signed_txn": {"...": "..."}}, + ) + + with self.assertRaises(test_module.web.HTTPBadRequest): + await test_module.register_ledger_nym(self.request) + + async def test_register_nym_create_transaction_for_endorser_not_found_x(self): + self.request.query = { + "did": "a_test_did", + "verkey": "a_test_verkey", + "alias": "did_alias", + "role": "ENDORSER", + "create_transaction_for_endorser": "true", + "conn_id": "dummy", + } + + with async_mock.patch.object( + ConnRecord, "retrieve_by_id", async_mock.AsyncMock() + ) as mock_conn_rec_retrieve: + mock_conn_rec_retrieve.side_effect = test_module.StorageNotFoundError() + self.ledger.register_nym.return_value: Tuple[bool, dict] = ( + True, + {"signed_txn": {"...": "..."}}, + ) + + with self.assertRaises(test_module.web.HTTPNotFound): + await test_module.register_ledger_nym(self.request) + + async def test_register_nym_create_transaction_for_endorser_base_model_x(self): + self.request.query = { + "did": "a_test_did", + "verkey": "a_test_verkey", + "alias": "did_alias", + "role": "ENDORSER", + "create_transaction_for_endorser": "true", + "conn_id": "dummy", + } + + with async_mock.patch.object( + ConnRecord, "retrieve_by_id", async_mock.AsyncMock() + ) as mock_conn_rec_retrieve: + mock_conn_rec_retrieve.side_effect = test_module.BaseModelError() + self.ledger.register_nym.return_value: Tuple[bool, dict] = ( + True, + {"signed_txn": {"...": "..."}}, + ) + + with self.assertRaises(test_module.web.HTTPBadRequest): + await test_module.register_ledger_nym(self.request) + + async def test_register_nym_create_transaction_for_endorser_no_endorser_info_x( + self, + ): + self.request.query = { + "did": "a_test_did", + "verkey": "a_test_verkey", + "alias": "did_alias", + "role": "ENDORSER", + "create_transaction_for_endorser": "true", + "conn_id": "dummy", + } + + with async_mock.patch.object( + ConnRecord, "retrieve_by_id", async_mock.AsyncMock() + ) as mock_conn_rec_retrieve: + mock_conn_rec_retrieve.return_value = async_mock.MagicMock( + metadata_get=async_mock.AsyncMock(return_value=None) + ) + self.ledger.register_nym.return_value: Tuple[bool, dict] = ( + True, + {"signed_txn": {"...": "..."}}, + ) + + with self.assertRaises(test_module.web.HTTPForbidden): + await test_module.register_ledger_nym(self.request) + + async def test_register_nym_create_transaction_for_endorser_no_endorser_did_x(self): + self.request.query = { + "did": "a_test_did", + "verkey": "a_test_verkey", + "alias": "did_alias", + "role": "ENDORSER", + "create_transaction_for_endorser": "true", + "conn_id": "dummy", + } + + with async_mock.patch.object( + ConnRecord, "retrieve_by_id", async_mock.AsyncMock() + ) as mock_conn_rec_retrieve: + mock_conn_rec_retrieve.return_value = async_mock.MagicMock( + metadata_get=async_mock.AsyncMock( + return_value={ + "endorser_name": ("name"), + } + ) + ) + self.ledger.register_nym.return_value: Tuple[bool, dict] = ( + True, + {"signed_txn": {"...": "..."}}, + ) + + with self.assertRaises(test_module.web.HTTPForbidden): + await test_module.register_ledger_nym(self.request) + async def test_get_nym_role_a(self): self.profile.context.injector.bind_instance( IndyLedgerRequestsExecutor, async_mock.MagicMock( - get_ledger_for_identifier=async_mock.CoroutineMock( + get_ledger_for_identifier=async_mock.AsyncMock( return_value=(None, self.ledger) ) ), @@ -289,7 +560,7 @@ async def test_get_nym_role_b(self): self.profile.context.injector.bind_instance( IndyLedgerRequestsExecutor, async_mock.MagicMock( - get_ledger_for_identifier=async_mock.CoroutineMock( + get_ledger_for_identifier=async_mock.AsyncMock( return_value=("test_ledger_id", self.ledger) ) ), @@ -306,6 +577,27 @@ async def test_get_nym_role_b(self): ) assert result is json_response.return_value + async def test_get_nym_role_multitenant(self): + self.context.injector.bind_instance( + BaseMultitenantManager, + async_mock.MagicMock(MultitenantManager, autospec=True), + ) + self.request.query = {"did": self.test_did} + + with async_mock.patch.object( + IndyLedgerRequestsExecutor, + "get_ledger_for_identifier", + async_mock.AsyncMock(return_value=("test_ledger_id", self.ledger)), + ), async_mock.patch.object( + test_module.web, "json_response", async_mock.Mock() + ) as json_response: + self.ledger.get_nym_role.return_value = Role.USER + result = await test_module.get_nym_role(self.request) + json_response.assert_called_once_with( + {"ledger_id": "test_ledger_id", "role": "USER"} + ) + assert result is json_response.return_value + async def test_get_nym_role_bad_request(self): self.request.query = {"no": "did"} with self.assertRaises(test_module.web.HTTPBadRequest): @@ -315,7 +607,7 @@ async def test_get_nym_role_ledger_txn_error(self): self.profile.context.injector.bind_instance( IndyLedgerRequestsExecutor, async_mock.MagicMock( - get_ledger_for_identifier=async_mock.CoroutineMock( + get_ledger_for_identifier=async_mock.AsyncMock( return_value=("test_ledger_id", self.ledger) ) ), @@ -331,7 +623,7 @@ async def test_get_nym_role_bad_ledger_req(self): self.profile.context.injector.bind_instance( IndyLedgerRequestsExecutor, async_mock.MagicMock( - get_ledger_for_identifier=async_mock.CoroutineMock( + get_ledger_for_identifier=async_mock.AsyncMock( return_value=("test_ledger_id", self.ledger) ) ), @@ -347,7 +639,7 @@ async def test_get_nym_role_ledger_error(self): self.profile.context.injector.bind_instance( IndyLedgerRequestsExecutor, async_mock.MagicMock( - get_ledger_for_identifier=async_mock.CoroutineMock( + get_ledger_for_identifier=async_mock.AsyncMock( return_value=(None, self.ledger) ) ), @@ -361,7 +653,7 @@ async def test_rotate_public_did_keypair(self): with async_mock.patch.object( test_module.web, "json_response", async_mock.Mock() ) as json_response: - self.ledger.rotate_public_did_keypair = async_mock.CoroutineMock() + self.ledger.rotate_public_did_keypair = async_mock.AsyncMock() await test_module.rotate_public_did_keypair(self.request) json_response.assert_called_once_with({}) @@ -370,7 +662,7 @@ async def test_rotate_public_did_keypair_public_wallet_x(self): with async_mock.patch.object( test_module.web, "json_response", async_mock.Mock() ) as json_response: - self.ledger.rotate_public_did_keypair = async_mock.CoroutineMock( + self.ledger.rotate_public_did_keypair = async_mock.AsyncMock( side_effect=test_module.WalletError("Exception") ) @@ -413,7 +705,7 @@ async def test_get_taa_x(self): await test_module.ledger_get_taa(self.request) async def test_taa_accept_not_required(self): - self.request.json = async_mock.CoroutineMock( + self.request.json = async_mock.AsyncMock( return_value={ "version": "version", "text": "text", @@ -426,7 +718,7 @@ async def test_taa_accept_not_required(self): await test_module.ledger_accept_taa(self.request) async def test_accept_taa(self): - self.request.json = async_mock.CoroutineMock( + self.request.json = async_mock.AsyncMock( return_value={ "version": "version", "text": "text", @@ -437,7 +729,10 @@ async def test_accept_taa(self): with async_mock.patch.object( test_module.web, "json_response", async_mock.Mock() ) as json_response: - self.ledger.get_txn_author_agreement.return_value = {"taa_required": True} + self.ledger.get_txn_author_agreement.return_value = { + "taa_record": {"text": "text"}, + "taa_required": True, + } result = await test_module.ledger_accept_taa(self.request) json_response.assert_called_once_with({}) self.ledger.accept_txn_author_agreement.assert_awaited_once_with( @@ -451,14 +746,17 @@ async def test_accept_taa(self): assert result is json_response.return_value async def test_accept_taa_x(self): - self.request.json = async_mock.CoroutineMock( + self.request.json = async_mock.AsyncMock( return_value={ "version": "version", "text": "text", "mechanism": "mechanism", } ) - self.ledger.get_txn_author_agreement.return_value = {"taa_required": True} + self.ledger.get_txn_author_agreement.return_value = { + "taa_record": {"text": "text"}, + "taa_required": True, + } self.ledger.accept_txn_author_agreement.side_effect = test_module.StorageError() with self.assertRaises(test_module.web.HTTPBadRequest): await test_module.ledger_accept_taa(self.request) @@ -479,7 +777,7 @@ async def test_get_write_ledger(self): self.profile.context.injector.bind_instance( BaseMultipleLedgerManager, async_mock.MagicMock( - get_write_ledger=async_mock.CoroutineMock( + get_write_ledger=async_mock.AsyncMock( return_value=("test_ledger_id", self.ledger) ) ), @@ -504,14 +802,14 @@ async def test_get_ledger_config(self): self.profile.context.injector.bind_instance( BaseMultipleLedgerManager, async_mock.MagicMock( - get_prod_ledgers=async_mock.CoroutineMock( + get_prod_ledgers=async_mock.AsyncMock( return_value={ "test_1": async_mock.MagicMock(), "test_2": async_mock.MagicMock(), "test_5": async_mock.MagicMock(), } ), - get_nonprod_ledgers=async_mock.CoroutineMock( + get_nonprod_ledgers=async_mock.AsyncMock( return_value={ "test_3": async_mock.MagicMock(), "test_4": async_mock.MagicMock(), diff --git a/aries_cloudagent/ledger/util.py b/aries_cloudagent/ledger/util.py index 558ea411a9..165d64a8b4 100644 --- a/aries_cloudagent/ledger/util.py +++ b/aries_cloudagent/ledger/util.py @@ -1,3 +1,19 @@ """Ledger utilities.""" +import re + +from ..core.profile import Profile + + TAA_ACCEPTED_RECORD_TYPE = "taa_accepted" + +DID_EVENT_PREFIX = "acapy::REGISTER_DID::" +EVENT_LISTENER_PATTERN = re.compile(f"^{DID_EVENT_PREFIX}(.*)?$") + + +async def notify_register_did_event(profile: Profile, did: str, meta_data: dict): + """Send notification for a DID post-process event.""" + await profile.notify( + DID_EVENT_PREFIX + did, + meta_data, + ) diff --git a/aries_cloudagent/messaging/agent_message.py b/aries_cloudagent/messaging/agent_message.py index 60b0534433..2d17f1d35a 100644 --- a/aries_cloudagent/messaging/agent_message.py +++ b/aries_cloudagent/messaging/agent_message.py @@ -1,9 +1,11 @@ """Agent message base class and schema.""" -from collections import OrderedDict -from typing import Mapping, Union import uuid +from collections import OrderedDict +from re import sub +from typing import Mapping, Optional, Union, Text + from marshmallow import ( EXCLUDE, fields, @@ -21,6 +23,7 @@ from .decorators.default import DecoratorSet from .decorators.signature_decorator import SignatureDecorator # TODO deprecated from .decorators.thread_decorator import ThreadDecorator +from .decorators.service_decorator import ServiceDecorator from .decorators.trace_decorator import ( TraceDecorator, TraceReport, @@ -52,7 +55,13 @@ class Meta: schema_class = None message_type = None - def __init__(self, _id: str = None, _decorators: BaseDecoratorSet = None): + def __init__( + self, + _id: str = None, + _type: Optional[Text] = None, + _version: Optional[Text] = None, + _decorators: BaseDecoratorSet = None, + ): """ Initialize base agent message object. @@ -80,6 +89,12 @@ def __init__(self, _id: str = None, _decorators: BaseDecoratorSet = None): self.__class__.__name__ ) ) + if _type: + self._message_type = _type + elif _version: + self._message_type = self.get_updated_msg_type(_version) + else: + self._message_type = self.Meta.message_type # Not required for now # if not self.Meta.handler_class: # raise TypeError( @@ -117,7 +132,12 @@ def _type(self) -> str: Current DIDComm prefix, slash, message type defined on `Meta.message_type` """ - return DIDCommPrefix.qualify_current(self.Meta.message_type) + return DIDCommPrefix.qualify_current(self._message_type) + + @_type.setter + def _type(self, msg_type: str): + """Set the message type identifier.""" + self._message_type = msg_type @property def _id(self) -> str: @@ -145,6 +165,10 @@ def _decorators(self, value: BaseDecoratorSet): """Fetch the message's decorator set.""" self._message_decorators = value + def get_updated_msg_type(self, version: str) -> str: + """Update version to Meta.message_type.""" + return sub(r"(\d+\.)?(\*|\d+)", version, self.Meta.message_type) + def get_signature(self, field_name: str) -> SignatureDecorator: """ Get the signature for a named field. @@ -249,6 +273,30 @@ async def verify_signatures(self, wallet: BaseWallet) -> bool: return False return True + @property + def _service(self) -> ServiceDecorator: + """ + Accessor for the message's service decorator. + + Returns: + The ServiceDecorator for this message + + """ + return self._decorators.get("service") + + @_service.setter + def _service(self, val: Union[ServiceDecorator, dict]): + """ + Setter for the message's service decorator. + + Args: + val: ServiceDecorator or dict to set as the service + """ + if val is None: + self._decorators.pop("service", None) + else: + self._decorators["service"] = val + @property def _thread(self) -> ThreadDecorator: """ diff --git a/aries_cloudagent/messaging/credential_definitions/routes.py b/aries_cloudagent/messaging/credential_definitions/routes.py index d547027d6d..5724b8a979 100644 --- a/aries_cloudagent/messaging/credential_definitions/routes.py +++ b/aries_cloudagent/messaging/credential_definitions/routes.py @@ -28,6 +28,7 @@ GET_CRED_DEF, IndyLedgerRequestsExecutor, ) +from ...multitenant.base import BaseMultitenantManager from ...protocols.endorse_transaction.v1_0.manager import ( TransactionManager, TransactionManagerError, @@ -40,7 +41,7 @@ get_endorser_connection_id, ) -from ...revocation.util import notify_revocation_reg_event +from ...revocation.indy import IndyRevocation from ...storage.base import BaseStorage, StorageRecord from ...storage.error import StorageError @@ -185,6 +186,23 @@ async def credential_definitions_send_credential_definition(request: web.BaseReq tag = body.get("tag") rev_reg_size = body.get("revocation_registry_size") + tag_query = {"schema_id": schema_id} + async with profile.session() as session: + storage = session.inject(BaseStorage) + found = await storage.find_all_records( + type_filter=CRED_DEF_SENT_RECORD_TYPE, + tag_query=tag_query, + ) + if 0 < len(found): + # need to check the 'tag' value + for record in found: + cred_def_id = record.value + cred_def_id_parts = cred_def_id.split(":") + if tag == cred_def_id_parts[4]: + raise web.HTTPBadRequest( + reason=f"Cred def for {schema_id} {tag} already exists" + ) + # check if we need to endorse if is_author_role(context.profile): # authors cannot write to the ledger @@ -248,9 +266,12 @@ async def credential_definitions_send_credential_definition(request: web.BaseReq except (IndyIssuerError, LedgerError) as e: raise web.HTTPBadRequest(reason=e.message) from e + issuer_did = cred_def_id.split(":")[0] meta_data = { "context": { "schema_id": schema_id, + "cred_def_id": cred_def_id, + "issuer_did": issuer_did, "support_revocation": support_revocation, "novel": novel, "tag": tag, @@ -263,15 +284,20 @@ async def credential_definitions_send_credential_definition(request: web.BaseReq if not create_transaction_for_endorser: # Notify event - issuer_did = cred_def_id.split(":")[0] - meta_data["context"]["schema_id"] = schema_id - meta_data["context"]["cred_def_id"] = cred_def_id - meta_data["context"]["issuer_did"] = issuer_did meta_data["processing"]["auto_create_rev_reg"] = True await notify_cred_def_event(context.profile, cred_def_id, meta_data) - return web.json_response({"credential_definition_id": cred_def_id}) + return web.json_response( + { + "sent": {"credential_definition_id": cred_def_id}, + "credential_definition_id": cred_def_id, + } + ) + # If the transaction is for the endorser, but the schema has already been created, + # then we send back the schema since the transaction will fail to be created. + elif "signed_txn" not in cred_def: + return web.json_response({"sent": {"credential_definition_id": cred_def_id}}) else: meta_data["processing"]["auto_create_rev_reg"] = context.settings.get_value( "endorser.auto_create_rev_reg" @@ -301,7 +327,12 @@ async def credential_definitions_send_credential_definition(request: web.BaseReq await outbound_handler(transaction_request, connection_id=connection_id) - return web.json_response({"txn": transaction.serialize()}) + return web.json_response( + { + "sent": {"credential_definition_id": cred_def_id}, + "txn": transaction.serialize(), + } + ) @docs( @@ -358,7 +389,11 @@ async def credential_definitions_get_credential_definition(request: web.BaseRequ cred_def_id = request.match_info["cred_def_id"] async with context.profile.session() as session: - ledger_exec_inst = session.inject(IndyLedgerRequestsExecutor) + multitenant_mgr = session.inject_or(BaseMultitenantManager) + if multitenant_mgr: + ledger_exec_inst = IndyLedgerRequestsExecutor(context.profile) + else: + ledger_exec_inst = session.inject(IndyLedgerRequestsExecutor) ledger_id, ledger = await ledger_exec_inst.get_ledger_for_identifier( cred_def_id, txn_record_type=GET_CRED_DEF, @@ -403,7 +438,11 @@ async def credential_definitions_fix_cred_def_wallet_record(request: web.BaseReq async with context.profile.session() as session: storage = session.inject(BaseStorage) - ledger_exec_inst = session.inject(IndyLedgerRequestsExecutor) + multitenant_mgr = session.inject_or(BaseMultitenantManager) + if multitenant_mgr: + ledger_exec_inst = IndyLedgerRequestsExecutor(context.profile) + else: + ledger_exec_inst = session.inject(IndyLedgerRequestsExecutor) ledger_id, ledger = await ledger_exec_inst.get_ledger_for_identifier( cred_def_id, txn_record_type=GET_CRED_DEF, @@ -475,16 +514,15 @@ async def on_cred_def_event(profile: Profile, event: Event): if support_revocation and novel and auto_create_rev_reg: # this kicks off the revocation registry creation process, which is 3 steps: # 1 - create revocation registry (ledger transaction may require endorsement) - # 2 - create revocation entry (ledger transaction may require endorsement) - # 3 - upload tails file + # 2 - upload tails file + # 3 - create revocation entry (ledger transaction may require endorsement) # For a cred def we also automatically create a second "pending" revocation # registry, so when the first one fills up we can continue to issue credentials # without a delay - await notify_revocation_reg_event( - profile, + revoc = IndyRevocation(profile) + await revoc.init_issuer_registry( cred_def_id, rev_reg_size, - auto_create_rev_reg=auto_create_rev_reg, create_pending_rev_reg=create_pending_rev_reg, endorser_connection_id=endorser_connection_id, ) diff --git a/aries_cloudagent/messaging/credential_definitions/tests/test_routes.py b/aries_cloudagent/messaging/credential_definitions/tests/test_routes.py index a1e72ddf6e..3a0cccb1b2 100644 --- a/aries_cloudagent/messaging/credential_definitions/tests/test_routes.py +++ b/aries_cloudagent/messaging/credential_definitions/tests/test_routes.py @@ -8,8 +8,9 @@ from ....ledger.multiple_ledger.ledger_requests_executor import ( IndyLedgerRequestsExecutor, ) +from ....multitenant.base import BaseMultitenantManager +from ....multitenant.manager import MultitenantManager from ....storage.base import BaseStorage -from ....tails.base import BaseTailsServer from .. import routes as test_module from ....connections.models.conn_record import ConnRecord @@ -81,7 +82,10 @@ async def test_send_credential_definition(self): ) assert result == mock_response.return_value mock_response.assert_called_once_with( - {"credential_definition_id": CRED_DEF_ID} + { + "sent": {"credential_definition_id": CRED_DEF_ID}, + "credential_definition_id": CRED_DEF_ID, + } ) async def test_send_credential_definition_create_transaction_for_endorser(self): @@ -126,7 +130,12 @@ async def test_send_credential_definition_create_transaction_for_endorser(self): ) ) assert result == mock_response.return_value - mock_response.assert_called_once_with({"txn": {"...": "..."}}) + mock_response.assert_called_once_with( + { + "sent": {"credential_definition_id": CRED_DEF_ID}, + "txn": {"...": "..."}, + } + ) async def test_send_credential_definition_create_transaction_for_endorser_storage_x( self, @@ -149,7 +158,6 @@ async def test_send_credential_definition_create_transaction_for_endorser_storag ) as mock_conn_rec_retrieve, async_mock.patch.object( test_module, "TransactionManager", async_mock.MagicMock() ) as mock_txn_mgr: - mock_conn_rec_retrieve.return_value = async_mock.MagicMock( metadata_get=async_mock.CoroutineMock( return_value={ @@ -346,6 +354,28 @@ async def test_get_credential_definition(self): } ) + async def test_get_credential_definition_multitenant(self): + self.profile_injector.bind_instance( + BaseMultitenantManager, + async_mock.MagicMock(MultitenantManager, autospec=True), + ) + self.request.match_info = {"cred_def_id": CRED_DEF_ID} + with async_mock.patch.object( + IndyLedgerRequestsExecutor, + "get_ledger_for_identifier", + async_mock.CoroutineMock(return_value=("test_ledger_id", self.ledger)), + ), async_mock.patch.object(test_module.web, "json_response") as mock_response: + result = await test_module.credential_definitions_get_credential_definition( + self.request + ) + assert result == mock_response.return_value + mock_response.assert_called_once_with( + { + "ledger_id": "test_ledger_id", + "credential_definition": {"cred": "def", "signed_txn": "..."}, + } + ) + async def test_get_credential_definition_no_ledger(self): self.profile_injector.bind_instance( IndyLedgerRequestsExecutor, diff --git a/aries_cloudagent/messaging/decorators/attach_decorator.py b/aries_cloudagent/messaging/decorators/attach_decorator.py index e86f17734d..eadf77952b 100644 --- a/aries_cloudagent/messaging/decorators/attach_decorator.py +++ b/aries_cloudagent/messaging/decorators/attach_decorator.py @@ -24,12 +24,13 @@ str_to_b64, unpad, ) -from ...wallet.key_type import KeyType +from ...wallet.key_type import ED25519 from ...did.did_key import DIDKey from ..models.base import BaseModel, BaseModelError, BaseModelSchema from ..valid import ( BASE64, BASE64URL_NO_PAD, + DictOrDictListField, INDY_ISO8601_DATETIME, JWS_HEADER_KID, SHA256, @@ -201,7 +202,7 @@ def did_key(verkey: str) -> str: if verkey.startswith("did:key:"): return verkey - return DIDKey.from_public_key_b58(verkey, KeyType.ED25519).did + return DIDKey.from_public_key_b58(verkey, ED25519).did def raw_key(verkey: str) -> str: @@ -228,7 +229,7 @@ def __init__( sha256_: str = None, links_: Union[Sequence[str], str] = None, base64_: str = None, - json_: dict = None, + json_: Union[Sequence[dict], dict] = None, ): """ Initialize decorator data. @@ -414,7 +415,7 @@ def build_protected(verkey: str): ) self.jws_ = AttachDecoratorDataJWS.deserialize(jws) - async def verify(self, wallet: BaseWallet) -> bool: + async def verify(self, wallet: BaseWallet, signer_verkey: str = None) -> bool: """ Verify the signature(s). @@ -428,7 +429,7 @@ async def verify(self, wallet: BaseWallet) -> bool: assert self.jws b64_payload = unpad(set_urlsafe_b64(self.base64, True)) - + verkey_to_check = [] for sig in [self.jws] if self.signatures == 1 else self.jws.signatures: b64_protected = sig.protected b64_sig = sig.signature @@ -438,10 +439,14 @@ async def verify(self, wallet: BaseWallet) -> bool: sign_input = (b64_protected + "." + b64_payload).encode("ascii") b_sig = b64_to_bytes(b64_sig, urlsafe=True) verkey = bytes_to_b58(b64_to_bytes(protected["jwk"]["x"], urlsafe=True)) - if not await wallet.verify_message( - sign_input, b_sig, verkey, KeyType.ED25519 - ): + encoded_pk = DIDKey.from_did(protected["jwk"]["kid"]).public_key_b58 + verkey_to_check.append(encoded_pk) + if not await wallet.verify_message(sign_input, b_sig, verkey, ED25519): + return False + if not await wallet.verify_message(sign_input, b_sig, encoded_pk, ED25519): return False + if signer_verkey and signer_verkey not in verkey_to_check: + return False return True def __eq__(self, other): @@ -484,7 +489,7 @@ def validate_data_spec(self, data: Mapping, **kwargs): required=False, data_key="jws", ) - json_ = fields.Dict( + json_ = DictOrDictListField( description="JSON-serialized data", required=False, example='{"sample": "content"}', @@ -611,7 +616,7 @@ def data_base64( @classmethod def data_json( cls, - mapping: dict, + mapping: Union[Sequence[dict], dict], *, ident: str = None, description: str = None, diff --git a/aries_cloudagent/messaging/decorators/default.py b/aries_cloudagent/messaging/decorators/default.py index 0d6d9f27c1..bc8c314466 100644 --- a/aries_cloudagent/messaging/decorators/default.py +++ b/aries_cloudagent/messaging/decorators/default.py @@ -8,6 +8,7 @@ from .trace_decorator import TraceDecorator from .timing_decorator import TimingDecorator from .transport_decorator import TransportDecorator +from .service_decorator import ServiceDecorator DEFAULT_MODELS = { "l10n": LocalizationDecorator, @@ -16,6 +17,7 @@ "trace": TraceDecorator, "timing": TimingDecorator, "transport": TransportDecorator, + "service": ServiceDecorator, } diff --git a/aries_cloudagent/messaging/decorators/service_decorator.py b/aries_cloudagent/messaging/decorators/service_decorator.py new file mode 100644 index 0000000000..b59de93565 --- /dev/null +++ b/aries_cloudagent/messaging/decorators/service_decorator.py @@ -0,0 +1,105 @@ +""" +A message decorator for services. + +A service decorator adds routing information to a message so agent can respond without +needing to perform a handshake. +""" + +from typing import List, Optional + +from marshmallow import EXCLUDE, fields + +from ..models.base import BaseModel, BaseModelSchema +from ..valid import INDY_RAW_PUBLIC_KEY + + +class ServiceDecorator(BaseModel): + """Class representing service decorator.""" + + class Meta: + """ServiceDecorator metadata.""" + + schema_class = "ServiceDecoratorSchema" + + def __init__( + self, + *, + endpoint: str, + recipient_keys: List[str], + routing_keys: Optional[List[str]] = None, + ): + """ + Initialize a ServiceDecorator instance. + + Args: + endpoint: Endpoint which this agent can be reached at + recipient_keys: List of recipient keys + routing_keys: List of routing keys + + """ + super().__init__() + self._endpoint = endpoint + self._recipient_keys = recipient_keys + self._routing_keys = routing_keys + + @property + def endpoint(self): + """ + Accessor for service endpoint. + + Returns: + This service's `serviceEndpoint` + + """ + return self._endpoint + + @property + def recipient_keys(self): + """ + Accessor for recipient keys. + + Returns: + This service's `recipientKeys` + + """ + return self._recipient_keys + + @property + def routing_keys(self): + """ + Accessor for routing keys. + + Returns: + This service's `routingKeys` + + """ + return self._routing_keys + + +class ServiceDecoratorSchema(BaseModelSchema): + """Thread decorator schema used in serialization/deserialization.""" + + class Meta: + """ServiceDecoratorSchema metadata.""" + + model_class = ServiceDecorator + unknown = EXCLUDE + + recipient_keys = fields.List( + fields.Str(description="Recipient public key", **INDY_RAW_PUBLIC_KEY), + data_key="recipientKeys", + required=True, + description="List of recipient keys", + ) + endpoint = fields.Str( + data_key="serviceEndpoint", + required=True, + description="Service endpoint at which to reach this agent", + example="http://192.168.56.101:8020", + ) + routing_keys = fields.List( + fields.Str(description="Routing key", **INDY_RAW_PUBLIC_KEY), + data_key="routingKeys", + required=False, + description="List of routing keys", + ) diff --git a/aries_cloudagent/messaging/decorators/signature_decorator.py b/aries_cloudagent/messaging/decorators/signature_decorator.py index 71bf1c42ea..a8b67cce29 100644 --- a/aries_cloudagent/messaging/decorators/signature_decorator.py +++ b/aries_cloudagent/messaging/decorators/signature_decorator.py @@ -9,7 +9,7 @@ from ...protocols.didcomm_prefix import DIDCommPrefix from ...wallet.base import BaseWallet from ...wallet.util import b64_to_bytes, bytes_to_b64 -from ...wallet.key_type import KeyType +from ...wallet.key_type import ED25519 from ..models.base import BaseModel, BaseModelSchema from ..valid import Base64URL, BASE64URL, INDY_RAW_PUBLIC_KEY @@ -111,9 +111,7 @@ async def verify(self, wallet: BaseWallet) -> bool: return False msg_bin = b64_to_bytes(self.sig_data, urlsafe=True) sig_bin = b64_to_bytes(self.signature, urlsafe=True) - return await wallet.verify_message( - msg_bin, sig_bin, self.signer, KeyType.ED25519 - ) + return await wallet.verify_message(msg_bin, sig_bin, self.signer, ED25519) def __str__(self): """Get a string representation of this class.""" diff --git a/aries_cloudagent/messaging/decorators/tests/test_attach_decorator.py b/aries_cloudagent/messaging/decorators/tests/test_attach_decorator.py index 6cd9e1d67a..c0f1f26169 100644 --- a/aries_cloudagent/messaging/decorators/tests/test_attach_decorator.py +++ b/aries_cloudagent/messaging/decorators/tests/test_attach_decorator.py @@ -1,34 +1,27 @@ import json -import pytest import uuid - from copy import deepcopy from datetime import datetime, timezone from unittest import TestCase +import pytest + from ....indy.sdk.wallet_setup import IndyWalletConfig from ....messaging.models.base import BaseModelError +from ....wallet.did_method import SOV from ....wallet.indy import IndySdkWallet +from ....wallet.key_type import ED25519 from ....wallet.util import b64_to_bytes, bytes_to_b64 -from ....wallet.key_type import KeyType -from ....wallet.did_method import DIDMethod - from ..attach_decorator import ( AttachDecorator, - AttachDecoratorSchema, AttachDecoratorData, - AttachDecoratorDataSchema, AttachDecoratorData1JWS, - AttachDecoratorData1JWSSchema, AttachDecoratorDataJWS, - AttachDecoratorDataJWSSchema, AttachDecoratorDataJWSHeader, - AttachDecoratorDataJWSHeaderSchema, did_key, raw_key, ) - KID = "did:sov:LjgpST2rjsoxYegQDRm7EL#keys-4" INDY_CRED = { "schema_id": "LjgpST2rjsoxYegQDRm7EL:2:icon:1.0", @@ -433,9 +426,7 @@ def test_data_json_external_mutation(self): class TestAttachDecoratorSignature: @pytest.mark.asyncio async def test_did_raw_key(self, wallet, seed): - did_info = await wallet.create_local_did( - DIDMethod.SOV, KeyType.ED25519, seed[0] - ) + did_info = await wallet.create_local_did(SOV, ED25519, seed[0]) did_key0 = did_key(did_info.verkey) raw_key0 = raw_key(did_key0) assert raw_key0 != did_key0 @@ -457,8 +448,7 @@ async def test_indy_sign(self, wallet, seed): ) deco_indy_master = deepcopy(deco_indy) did_info = [ - await wallet.create_local_did(DIDMethod.SOV, KeyType.ED25519, seed[i]) - for i in [0, 1] + await wallet.create_local_did(SOV, ED25519, seed[i]) for i in [0, 1] ] assert deco_indy.data.signatures == 0 assert deco_indy.data.header_map() is None @@ -475,6 +465,7 @@ async def test_indy_sign(self, wallet, seed): assert deco_indy.data.header_map()["kid"] == did_key(did_info[0].verkey) assert deco_indy.data.header_map()["jwk"]["kid"] == did_key(did_info[0].verkey) assert await deco_indy.data.verify(wallet) + assert await deco_indy.data.verify(wallet, did_info[0].verkey) indy_cred = json.loads(deco_indy.data.signed.decode()) assert indy_cred == INDY_CRED diff --git a/aries_cloudagent/messaging/decorators/tests/test_decorator_set.py b/aries_cloudagent/messaging/decorators/tests/test_decorator_set.py index bed13f62c7..96e2c034af 100644 --- a/aries_cloudagent/messaging/decorators/tests/test_decorator_set.py +++ b/aries_cloudagent/messaging/decorators/tests/test_decorator_set.py @@ -33,7 +33,6 @@ def test_deco_set(self): assert all(k in deco_set.models for k in DEFAULT_MODELS) def test_extract(self): - decor_value = {} message = {"~decorator": decor_value, "one": "TWO"} @@ -47,7 +46,6 @@ def test_extract(self): assert remain == {"one": "TWO"} def test_dict(self): - decors = BaseDecoratorSet() decors["test"] = "TEST" assert decors["test"] == "TEST" @@ -55,7 +53,6 @@ def test_dict(self): assert result == {"~test": "TEST"} def test_decorator_model(self): - decor_value = {} message = {"~test": {"value": "TEST"}} @@ -70,7 +67,6 @@ def test_decorator_model(self): assert result == message def test_field_decorator(self): - decor_value = {} message = {"test~decorator": decor_value, "one": "TWO"} @@ -86,7 +82,6 @@ def test_field_decorator(self): assert "test~decorator" in decors.to_dict() def test_skip_decorator(self): - decor_value = {} message = {"handled~decorator": decor_value, "one": "TWO"} diff --git a/aries_cloudagent/messaging/decorators/tests/test_localization_decorator.py b/aries_cloudagent/messaging/decorators/tests/test_localization_decorator.py index 93e7e32d4b..6140f4e1f4 100644 --- a/aries_cloudagent/messaging/decorators/tests/test_localization_decorator.py +++ b/aries_cloudagent/messaging/decorators/tests/test_localization_decorator.py @@ -4,7 +4,6 @@ class TestThreadDecorator(TestCase): - LOCALE = "en-ca" LOCALIZABLE = ["a", "b"] CATALOGS = ["http://192.168.56.111/my-project/catalog.json"] diff --git a/aries_cloudagent/messaging/decorators/tests/test_signature_decorator.py b/aries_cloudagent/messaging/decorators/tests/test_signature_decorator.py index c7d23a25e1..de863ca4b9 100644 --- a/aries_cloudagent/messaging/decorators/tests/test_signature_decorator.py +++ b/aries_cloudagent/messaging/decorators/tests/test_signature_decorator.py @@ -1,6 +1,6 @@ from asynctest import TestCase as AsyncTestCase -from ....wallet.key_type import KeyType +from ....wallet.key_type import ED25519 from ....core.in_memory import InMemoryProfile from ....protocols.trustping.v1_0.messages.ping import Ping from ....wallet.in_memory import InMemoryWallet @@ -43,7 +43,7 @@ async def test_create_decode_verify(self): profile = InMemoryProfile.test_profile() wallet = InMemoryWallet(profile) - key_info = await wallet.create_signing_key(KeyType.ED25519) + key_info = await wallet.create_signing_key(ED25519) deco = await SignatureDecorator.create( Ping(), key_info.verkey, wallet, timestamp=None diff --git a/aries_cloudagent/messaging/decorators/tests/test_thread_decorator.py b/aries_cloudagent/messaging/decorators/tests/test_thread_decorator.py index 898930233f..8ad7be4878 100644 --- a/aries_cloudagent/messaging/decorators/tests/test_thread_decorator.py +++ b/aries_cloudagent/messaging/decorators/tests/test_thread_decorator.py @@ -4,14 +4,12 @@ class TestThreadDecorator(TestCase): - thread_id = "tid-001" parent_id = "tid-000" sender_order = 1 received_orders = {"did": 2} def test_init(self): - decorator = ThreadDecorator( thid=self.thread_id, pthid=self.parent_id, @@ -24,7 +22,6 @@ def test_init(self): assert decorator.received_orders == self.received_orders def test_serialize_load(self): - decorator = ThreadDecorator( thid=self.thread_id, pthid=self.parent_id, diff --git a/aries_cloudagent/messaging/decorators/tests/test_trace_decorator.py b/aries_cloudagent/messaging/decorators/tests/test_trace_decorator.py index 2d2613fe96..b67da74477 100644 --- a/aries_cloudagent/messaging/decorators/tests/test_trace_decorator.py +++ b/aries_cloudagent/messaging/decorators/tests/test_trace_decorator.py @@ -4,7 +4,6 @@ class TestTraceDecorator(TestCase): - target_api = "http://example.com/api/trace/" full_thread_api = False target_msg = TRACE_MESSAGE_TARGET @@ -20,7 +19,6 @@ class TestTraceDecorator(TestCase): outcome = "OK ..." def test_init_api(self): - decorator = TraceDecorator( target=self.target_api, full_thread=self.full_thread_api, @@ -29,7 +27,6 @@ def test_init_api(self): assert decorator.full_thread == self.full_thread_api def test_init_message(self): - x_msg_id = self.msg_id x_thread_id = self.thread_id x_trace_report = TraceReport( @@ -64,7 +61,6 @@ def test_init_message(self): assert trace_report.outcome == self.outcome def test_serialize_load(self): - x_msg_id = self.msg_id x_thread_id = self.thread_id x_trace_report = TraceReport( diff --git a/aries_cloudagent/messaging/jsonld/credential.py b/aries_cloudagent/messaging/jsonld/credential.py index 23813276b0..5c693a0b63 100644 --- a/aries_cloudagent/messaging/jsonld/credential.py +++ b/aries_cloudagent/messaging/jsonld/credential.py @@ -5,7 +5,7 @@ from ...did.did_key import DIDKey from ...vc.ld_proofs import DocumentLoader from ...wallet.base import BaseWallet -from ...wallet.key_type import KeyType +from ...wallet.key_type import ED25519 from ...wallet.util import b64_to_bytes, b64_to_str, bytes_to_b64, str_to_b64 from .create_verify_data import create_verify_data @@ -18,7 +18,7 @@ def did_key(verkey: str) -> str: if verkey.startswith("did:key:"): return verkey - return DIDKey.from_public_key_b58(verkey, KeyType.ED25519).did + return DIDKey.from_public_key_b58(verkey, ED25519).did def b64encode(str): @@ -76,7 +76,7 @@ async def jws_verify(session, verify_data, signature, public_key): wallet = session.inject(BaseWallet) verified = await wallet.verify_message( - jws_to_verify, decoded_signature, public_key, KeyType.ED25519 + jws_to_verify, decoded_signature, public_key, ED25519 ) return verified diff --git a/aries_cloudagent/messaging/jsonld/routes.py b/aries_cloudagent/messaging/jsonld/routes.py index f0ff1b8c29..f89db9752a 100644 --- a/aries_cloudagent/messaging/jsonld/routes.py +++ b/aries_cloudagent/messaging/jsonld/routes.py @@ -5,7 +5,6 @@ from marshmallow import INCLUDE, Schema, fields from pydid.verification_method import ( Ed25519VerificationKey2018, - KnownVerificationMethods, ) from ...admin.request_context import AdminRequestContext @@ -85,7 +84,7 @@ async def sign(request: web.BaseRequest): session, doc.get("credential"), doc.get("options"), body.get("verkey") ) response["signed_doc"] = doc_with_proof - except (BaseJSONLDMessagingError) as err: + except BaseJSONLDMessagingError as err: response["error"] = str(err) except (WalletError, InjectionError): raise web.HTTPForbidden(reason="No wallet available") @@ -148,7 +147,6 @@ async def verify(request: web.BaseRequest): vmethod = await resolver.dereference( profile, doc["proof"]["verificationMethod"], - cls=KnownVerificationMethods, ) if not isinstance(vmethod, SUPPORTED_VERIFICATION_METHOD_TYPES): diff --git a/aries_cloudagent/messaging/jsonld/tests/test_credential.py b/aries_cloudagent/messaging/jsonld/tests/test_credential.py index 297d5a3b0e..3046388533 100644 --- a/aries_cloudagent/messaging/jsonld/tests/test_credential.py +++ b/aries_cloudagent/messaging/jsonld/tests/test_credential.py @@ -13,7 +13,7 @@ from ....vc.ld_proofs import DocumentLoader from ....wallet.base import BaseWallet from ....wallet.in_memory import InMemoryWallet -from ....wallet.key_type import KeyType +from ....wallet.key_type import ED25519 from .. import credential as test_module from ..create_verify_data import DroppedAttributeError @@ -60,7 +60,7 @@ async def test_verify_jws_header(self): class TestOps(AsyncTestCase): async def setUp(self): self.wallet = InMemoryWallet(InMemoryProfile.test_profile()) - await self.wallet.create_signing_key(KeyType.ED25519, TEST_SEED) + await self.wallet.create_signing_key(ED25519, TEST_SEED) self.session = InMemoryProfile.test_session(bind={BaseWallet: self.wallet}) self.profile = self.session.profile diff --git a/aries_cloudagent/messaging/jsonld/tests/test_routes.py b/aries_cloudagent/messaging/jsonld/tests/test_routes.py index cf2fbc35e9..ec41daf578 100644 --- a/aries_cloudagent/messaging/jsonld/tests/test_routes.py +++ b/aries_cloudagent/messaging/jsonld/tests/test_routes.py @@ -14,9 +14,9 @@ from ....resolver.did_resolver import DIDResolver from ....vc.ld_proofs.document_loader import DocumentLoader from ....wallet.base import BaseWallet -from ....wallet.did_method import DIDMethod +from ....wallet.did_method import SOV, DIDMethods from ....wallet.error import WalletError -from ....wallet.key_type import KeyType +from ....wallet.key_type import ED25519 from ..error import ( BadJWSHeaderError, DroppedAttributeError, @@ -234,22 +234,22 @@ async def test_verify_bad_ver_meth_deref_req_error( assert "error" in mock_response.call_args[0][0] -@pytest.mark.asyncio -async def test_verify_bad_ver_meth_not_ver_meth( - mock_resolver, mock_verify_request, mock_response, request_body -): - request_body["doc"]["proof"][ - "verificationMethod" - ] = "did:example:1234abcd#did-communication" - await test_module.verify(mock_verify_request(request_body)) - assert "error" in mock_response.call_args[0][0] - - +@pytest.mark.parametrize( + "vmethod", + [ + "did:example:1234abcd#key-2", + "did:example:1234abcd#did-communication", + ], +) @pytest.mark.asyncio async def test_verify_bad_vmethod_unsupported( - mock_resolver, mock_verify_request, mock_response, request_body + mock_resolver, + mock_verify_request, + mock_response, + request_body, + vmethod, ): - request_body["doc"]["proof"]["verificationMethod"] = "did:example:1234abcd#key-2" + request_body["doc"]["proof"]["verificationMethod"] = vmethod with pytest.raises(web.HTTPBadRequest): await test_module.verify(mock_verify_request(request_body)) @@ -274,8 +274,9 @@ async def setUp(self): self.context.profile.context.injector.bind_instance( DocumentLoader, custom_document_loader ) + self.context.profile.context.injector.bind_instance(DIDMethods, DIDMethods()) self.did_info = await (await self.context.session()).wallet.create_local_did( - DIDMethod.SOV, KeyType.ED25519 + SOV, ED25519 ) self.request_dict = { "context": self.context, @@ -446,7 +447,7 @@ async def test_sign_credential(self): ), "degree": { "type": "BachelorDegree", - "name": u"Bachelor of Encyclopædic Arts", + "name": "Bachelor of Encyclopædic Arts", }, }, }, diff --git a/aries_cloudagent/messaging/models/base.py b/aries_cloudagent/messaging/models/base.py index fd00c7d68d..79b94016bf 100644 --- a/aries_cloudagent/messaging/models/base.py +++ b/aries_cloudagent/messaging/models/base.py @@ -5,7 +5,8 @@ from abc import ABC from collections import namedtuple -from typing import Mapping, Union +from typing import Mapping, Optional, Type, TypeVar, Union, cast, overload +from typing_extensions import Literal from marshmallow import Schema, post_dump, pre_load, post_load, ValidationError, EXCLUDE @@ -17,7 +18,7 @@ SerDe = namedtuple("SerDe", "ser de") -def resolve_class(the_cls, relative_cls: type = None): +def resolve_class(the_cls, relative_cls: Optional[type] = None) -> type: """ Resolve a class. @@ -38,6 +39,10 @@ def resolve_class(the_cls, relative_cls: type = None): elif isinstance(the_cls, str): default_module = relative_cls and relative_cls.__module__ resolved = ClassLoader.load_class(the_cls, default_module) + else: + raise TypeError( + f"Could not resolve class from {the_cls}; incorrect type {type(the_cls)}" + ) return resolved @@ -53,7 +58,10 @@ def resolve_meta_property(obj, prop_name: str, defval=None): The meta property """ - cls = obj.__class__ + if isinstance(obj, type): + cls = obj + else: + cls = obj.__class__ found = defval while cls: Meta = getattr(cls, "Meta", None) @@ -70,6 +78,9 @@ class BaseModelError(BaseError): """Base exception class for base model errors.""" +ModelType = TypeVar("ModelType", bound="BaseModel") + + class BaseModel(ABC): """Base model that provides convenience methods.""" @@ -94,7 +105,7 @@ def __init__(self): ) @classmethod - def _get_schema_class(cls): + def _get_schema_class(cls) -> Type["BaseModelSchema"]: """ Get the schema class. @@ -102,10 +113,16 @@ def _get_schema_class(cls): The resolved schema class """ - return resolve_class(cls.Meta.schema_class, cls) + resolved = resolve_class(cls.Meta.schema_class, cls) + if issubclass(resolved, BaseModelSchema): + return resolved + + raise TypeError( + f"Resolved class is not a subclass of BaseModelSchema: {resolved}" + ) @property - def Schema(self) -> type: + def Schema(self) -> Type["BaseModelSchema"]: """ Accessor for the model's schema class. @@ -115,8 +132,49 @@ def Schema(self) -> type: """ return self._get_schema_class() + @overload + @classmethod + def deserialize( + cls: Type[ModelType], + obj, + *, + unknown: Optional[str] = None, + ) -> ModelType: + """Convert from JSON representation to a model instance.""" + ... + + @overload @classmethod - def deserialize(cls, obj, unknown: str = None, none2none: str = False): + def deserialize( + cls: Type[ModelType], + obj, + *, + none2none: Literal[False], + unknown: Optional[str] = None, + ) -> ModelType: + """Convert from JSON representation to a model instance.""" + ... + + @overload + @classmethod + def deserialize( + cls: Type[ModelType], + obj, + *, + none2none: Literal[True], + unknown: Optional[str] = None, + ) -> Optional[ModelType]: + """Convert from JSON representation to a model instance.""" + ... + + @classmethod + def deserialize( + cls: Type[ModelType], + obj, + *, + unknown: Optional[str] = None, + none2none: bool = False, + ) -> Optional[ModelType]: """ Convert from JSON representation to a model instance. @@ -132,18 +190,45 @@ def deserialize(cls, obj, unknown: str = None, none2none: str = False): if obj is None and none2none: return None - schema = cls._get_schema_class()(unknown=unknown or EXCLUDE) + schema_cls = cls._get_schema_class() + schema = schema_cls( + unknown=unknown or resolve_meta_property(schema_cls, "unknown", EXCLUDE) + ) + try: - return schema.loads(obj) if isinstance(obj, str) else schema.load(obj) + return cast( + ModelType, + schema.loads(obj) if isinstance(obj, str) else schema.load(obj), + ) except (AttributeError, ValidationError) as err: LOGGER.exception(f"{cls.__name__} message validation error:") raise BaseModelError(f"{cls.__name__} schema validation failed") from err + @overload + def serialize( + self, + *, + as_string: Literal[True], + unknown: Optional[str] = None, + ) -> str: + """Create a JSON-compatible dict representation of the model instance.""" + ... + + @overload def serialize( self, - as_string=False, - unknown: str = None, + *, + unknown: Optional[str] = None, ) -> dict: + """Create a JSON-compatible dict representation of the model instance.""" + ... + + def serialize( + self, + *, + as_string: bool = False, + unknown: Optional[str] = None, + ) -> Union[str, dict]: """ Create a JSON-compatible dict representation of the model instance. @@ -154,7 +239,10 @@ def serialize( A dict representation of this model, or a JSON string if as_string is True """ - schema = self.Schema(unknown=unknown or EXCLUDE) + schema_cls = self._get_schema_class() + schema = schema_cls( + unknown=unknown or resolve_meta_property(schema_cls, "unknown", EXCLUDE) + ) try: return ( schema.dumps(self, separators=(",", ":")) @@ -168,18 +256,17 @@ def serialize( ) from err @classmethod - def serde(cls, obj: Union["BaseModel", Mapping]) -> SerDe: + def serde(cls, obj: Union["BaseModel", Mapping]) -> Optional[SerDe]: """Return serialized, deserialized representations of input object.""" + if obj is None: + return None - return ( - SerDe(obj.serialize(), obj) - if isinstance(obj, BaseModel) - else None - if obj is None - else SerDe(obj, cls.deserialize(obj)) - ) + if isinstance(obj, BaseModel): + return SerDe(obj.serialize(), obj) - def validate(self, unknown: str = None): + return SerDe(obj, cls.deserialize(obj)) + + def validate(self, unknown: Optional[str] = None): """Validate a constructed model.""" schema = self.Schema(unknown=unknown) errors = schema.validate(self.serialize()) @@ -191,7 +278,7 @@ def validate(self, unknown: str = None): def from_json( cls, json_repr: Union[str, bytes], - unknown: str = None, + unknown: Optional[str] = None, ): """ Parse a JSON string into a model instance. @@ -218,7 +305,7 @@ def to_json(self, unknown: str = None) -> str: A JSON representation of this message """ - return json.dumps(self.serialize(unknown=unknown or EXCLUDE)) + return json.dumps(self.serialize(unknown=unknown)) def __repr__(self) -> str: """ @@ -319,7 +406,14 @@ def make_model(self, data: dict, **kwargs): A model instance """ - return self.Model(**data) + try: + cls_inst = self.Model(**data) + except TypeError as err: + if "_type" in str(err) and "_type" in data: + data["msg_type"] = data["_type"] + del data["_type"] + cls_inst = self.Model(**data) + return cls_inst @post_dump def remove_skipped_values(self, data, **kwargs): diff --git a/aries_cloudagent/messaging/models/base_record.py b/aries_cloudagent/messaging/models/base_record.py index 07c56b2008..c696cf6771 100644 --- a/aries_cloudagent/messaging/models/base_record.py +++ b/aries_cloudagent/messaging/models/base_record.py @@ -19,7 +19,7 @@ from ..util import datetime_to_str, time_now from ..valid import INDY_ISO8601_DATETIME -from .base import BaseModel, BaseModelSchema +from .base import BaseModel, BaseModelSchema, BaseModelError LOGGER = logging.getLogger(__name__) @@ -81,6 +81,7 @@ class Meta: EVENT_NAMESPACE: str = "acapy::record" LOG_STATE_FLAG = None TAG_NAMES = {"state"} + STATE_DELETED = "deleted" def __init__( self, @@ -89,6 +90,7 @@ def __init__( *, created_at: Union[str, datetime] = None, updated_at: Union[str, datetime] = None, + new_with_id: bool = False, ): """Initialize a new BaseRecord.""" if not self.RECORD_TYPE: @@ -99,6 +101,7 @@ def __init__( ) self._id = id self._last_state = state + self._new_with_id = new_with_id self.state = state self.created_at = datetime_to_str(created_at) self.updated_at = datetime_to_str(updated_at) @@ -218,7 +221,11 @@ async def clear_cached_key(cls, session: ProfileSession, cache_key: str): @classmethod async def retrieve_by_id( - cls: Type[RecordType], session: ProfileSession, record_id: str + cls: Type[RecordType], + session: ProfileSession, + record_id: str, + *, + for_update=False, ) -> RecordType: """ Retrieve a stored record by ID. @@ -230,7 +237,7 @@ async def retrieve_by_id( storage = session.inject(BaseStorage) result = await storage.get_record( - cls.RECORD_TYPE, record_id, {"retrieveTags": False} + cls.RECORD_TYPE, record_id, {"forUpdate": for_update, "retrieveTags": False} ) vals = json.loads(result.value) return cls.from_storage(record_id, vals) @@ -241,6 +248,8 @@ async def retrieve_by_tag_filter( session: ProfileSession, tag_filter: dict, post_filter: dict = None, + *, + for_update=False, ) -> RecordType: """ Retrieve a record by tag filter. @@ -256,7 +265,7 @@ async def retrieve_by_tag_filter( rows = await storage.find_all_records( cls.RECORD_TYPE, cls.prefix_tag_filter(tag_filter), - options={"retrieveTags": False}, + options={"forUpdate": for_update, "retrieveTags": False}, ) found = None for record in rows: @@ -321,7 +330,10 @@ async def query( positive=False, alt=alt, ): - result.append(cls.from_storage(record.id, vals)) + try: + result.append(cls.from_storage(record.id, vals)) + except BaseModelError as err: + raise BaseModelError(f"{err}, for record id {record.id}") return result async def save( @@ -349,15 +361,17 @@ async def save( try: self.updated_at = time_now() storage = session.inject(BaseStorage) - if self._id: + if self._id and not self._new_with_id: record = self.storage_record await storage.update_record(record, record.value, record.tags) new_record = False else: - self._id = str(uuid.uuid4()) + if not self._id: + self._id = str(uuid.uuid4()) self.created_at = self.updated_at await storage.add_record(self.storage_record) new_record = True + self._new_with_id = False finally: params = {self.RECORD_TYPE: self.serialize()} if log_params: @@ -405,8 +419,11 @@ async def delete_record(self, session: ProfileSession): if self._id: storage = session.inject(BaseStorage) + if self.state: + self._previous_state = self.state + self.state = BaseRecord.STATE_DELETED + await self.emit_event(session, self.serialize()) await storage.delete_record(self.storage_record) - # FIXME - update state and send webhook? async def emit_event(self, session: ProfileSession, payload: Any = None): """ @@ -481,6 +498,24 @@ def __eq__(self, other: Any) -> bool: return self.value == other.value and self.tags == other.tags return False + @classmethod + def get_attributes_by_prefix(cls, prefix: str, walk_mro: bool = True): + """ + List all values for attributes with common prefix. + + Args: + prefix: Common prefix to look for + walk_mro: Walk MRO to find attributes inherited from superclasses + """ + + bases = cls.__mro__ if walk_mro else [cls] + return [ + vars(base)[name] + for base in bases + for name in vars(base) + if name.startswith(prefix) + ] + class BaseExchangeRecord(BaseRecord): """Represents a base record with event tracing capability.""" diff --git a/aries_cloudagent/messaging/models/tests/test_base.py b/aries_cloudagent/messaging/models/tests/test_base.py index 62c327b7a1..9cc6eaf868 100644 --- a/aries_cloudagent/messaging/models/tests/test_base.py +++ b/aries_cloudagent/messaging/models/tests/test_base.py @@ -1,15 +1,6 @@ -import json - from asynctest import TestCase as AsyncTestCase, mock as async_mock -from marshmallow import EXCLUDE, fields, validates_schema, ValidationError - -from ....cache.base import BaseCache -from ....config.injection_context import InjectionContext -from ....storage.base import BaseStorage, StorageRecord - -from ...responder import BaseResponder, MockResponder -from ...util import time_now +from marshmallow import EXCLUDE, INCLUDE, fields, validates_schema, ValidationError from ..base import BaseModel, BaseModelError, BaseModelSchema @@ -35,6 +26,48 @@ def validate_fields(self, data, **kwargs): raise ValidationError("") +class ModelImplWithUnknown(BaseModel): + class Meta: + schema_class = "SchemaImplWithUnknown" + + def __init__(self, *, attr=None, **kwargs): + self.attr = attr + self.extra = kwargs + + +class SchemaImplWithUnknown(BaseModelSchema): + class Meta: + model_class = ModelImplWithUnknown + unknown = INCLUDE + + attr = fields.String(required=True) + + @validates_schema + def validate_fields(self, data, **kwargs): + if data["attr"] != "succeeds": + raise ValidationError("") + + +class ModelImplWithoutUnknown(BaseModel): + class Meta: + schema_class = "SchemaImplWithoutUnknown" + + def __init__(self, *, attr=None): + self.attr = attr + + +class SchemaImplWithoutUnknown(BaseModelSchema): + class Meta: + model_class = ModelImplWithoutUnknown + + attr = fields.String(required=True) + + @validates_schema + def validate_fields(self, data, **kwargs): + if data["attr"] != "succeeds": + raise ValidationError("") + + class TestBase(AsyncTestCase): def test_model_validate_fails(self): model = ModelImpl(attr="string") @@ -63,3 +96,24 @@ def test_from_json_x(self): data = "{}{}" with self.assertRaises(BaseModelError): ModelImpl.from_json(data) + + def test_model_with_unknown(self): + model = ModelImplWithUnknown(attr="succeeds") + model = model.validate() + assert model.attr == "succeeds" + + model = ModelImplWithUnknown.deserialize( + {"attr": "succeeds", "another": "value"} + ) + assert model.extra + assert model.extra["another"] == "value" + assert model.attr == "succeeds" + + def test_model_without_unknown_default_exclude(self): + model = ModelImplWithoutUnknown(attr="succeeds") + model = model.validate() + assert model.attr == "succeeds" + + assert ModelImplWithoutUnknown.deserialize( + {"attr": "succeeds", "another": "value"} + ) diff --git a/aries_cloudagent/messaging/models/tests/test_base_record.py b/aries_cloudagent/messaging/models/tests/test_base_record.py index 4caf5dc3d6..87749e2df8 100644 --- a/aries_cloudagent/messaging/models/tests/test_base_record.py +++ b/aries_cloudagent/messaging/models/tests/test_base_record.py @@ -12,6 +12,7 @@ StorageError, StorageRecord, ) +from ....messaging.models.base import BaseModelError from ...util import time_now @@ -181,6 +182,26 @@ async def test_query(self): assert result[0]._id == record_id assert result[0].value == record_value + async def test_query_x(self): + session = InMemoryProfile.test_session() + mock_storage = async_mock.MagicMock(BaseStorage, autospec=True) + session.context.injector.bind_instance(BaseStorage, mock_storage) + record_id = "record_id" + record_value = {"created_at": time_now(), "updated_at": time_now()} + tag_filter = {"tag": "filter"} + stored = StorageRecord( + BaseRecordImpl.RECORD_TYPE, json.dumps(record_value), {}, record_id + ) + + mock_storage.find_all_records.return_value = [stored] + with async_mock.patch.object( + BaseRecordImpl, + "from_storage", + async_mock.MagicMock(side_effect=BaseModelError), + ): + with self.assertRaises(BaseModelError): + await BaseRecordImpl.query(session, tag_filter) + async def test_query_post_filter(self): session = InMemoryProfile.test_session() mock_storage = async_mock.MagicMock(BaseStorage, autospec=True) diff --git a/aries_cloudagent/messaging/request_context.py b/aries_cloudagent/messaging/request_context.py index 97e0d1f9ef..715591cb2d 100644 --- a/aries_cloudagent/messaging/request_context.py +++ b/aries_cloudagent/messaging/request_context.py @@ -61,7 +61,7 @@ def connection_ready(self, active: bool): self._connection_ready = active @property - def connection_record(self) -> ConnRecord: + def connection_record(self) -> Optional[ConnRecord]: """Accessor for the related connection record.""" return self._connection_record diff --git a/aries_cloudagent/messaging/responder.py b/aries_cloudagent/messaging/responder.py index 32c5af641a..75913a0645 100644 --- a/aries_cloudagent/messaging/responder.py +++ b/aries_cloudagent/messaging/responder.py @@ -4,18 +4,30 @@ The responder is provided to message handlers to enable them to send a new message in response to the message being handled. """ +import asyncio +import json from abc import ABC, abstractmethod -import json -from typing import Sequence, Union +from typing import Sequence, Union, Optional, Tuple +from ..cache.base import BaseCache from ..connections.models.connection_target import ConnectionTarget +from ..connections.models.conn_record import ConnRecord from ..core.error import BaseError +from ..core.profile import Profile from ..transport.outbound.message import OutboundMessage from .base_message import BaseMessage from ..transport.outbound.status import OutboundSendStatus +SKIP_ACTIVE_CONN_CHECK_MSG_TYPES = [ + "didexchange/1.0/request", + "didexchange/1.0/response", + "connections/1.0/invitation", + "connections/1.0/request", + "connections/1.0/response", +] + class ResponderError(BaseError): """Responder error.""" @@ -79,7 +91,18 @@ async def send( ) -> OutboundSendStatus: """Convert a message to an OutboundMessage and send it.""" outbound = await self.create_outbound(message, **kwargs) - return await self.send_outbound(outbound) + if isinstance(message, BaseMessage): + msg_type = message._message_type + msg_id = message._id + else: + msg_dict = json.loads(message) + msg_type = msg_dict.get("@type") + msg_id = msg_dict.get("@id") + return await self.send_outbound( + message=outbound, + message_type=msg_type, + message_id=msg_id, + ) async def send_reply( self, @@ -109,10 +132,59 @@ async def send_reply( target=target, target_list=target_list, ) - return await self.send_outbound(outbound) + if isinstance(message, BaseMessage): + msg_type = message._message_type + msg_id = message._id + else: + msg_dict = json.loads(message) + msg_type = msg_dict.get("@type") + msg_id = msg_dict.get("@id") + return await self.send_outbound( + message=outbound, message_type=msg_type, message_id=msg_id + ) + + async def conn_rec_active_state_check( + self, profile: Profile, connection_id: str, timeout: int = 7 + ) -> bool: + """Check if the connection record is ready for sending outbound message.""" + + async def _wait_for_state() -> Tuple[bool, Optional[str]]: + while True: + async with profile.session() as session: + conn_record = await ConnRecord.retrieve_by_id( + session, connection_id + ) + if conn_record.is_ready: + # if ConnRecord.State.get(conn_record.state) in ( + # ConnRecord.State.COMPLETED, + # ): + return (True, conn_record.state) + await asyncio.sleep(1) + + try: + cache_key = f"conn_rec_state::{connection_id}" + connection_state = None + cache = profile.inject_or(BaseCache) + if cache: + connection_state = await cache.get(cache_key) + if connection_state and ConnRecord.State.get(connection_state) in ( + ConnRecord.State.COMPLETED, + ConnRecord.State.RESPONSE, + ): + return True + check_flag, connection_state = await asyncio.wait_for( + _wait_for_state(), timeout + ) + if cache and connection_state: + await cache.set(cache_key, connection_state) + return check_flag + except asyncio.TimeoutError: + return False @abstractmethod - async def send_outbound(self, message: OutboundMessage) -> OutboundSendStatus: + async def send_outbound( + self, message: OutboundMessage, **kwargs + ) -> OutboundSendStatus: """ Send an outbound message. @@ -152,7 +224,9 @@ async def send_reply( self.messages.append((message, kwargs)) return OutboundSendStatus.QUEUED_FOR_DELIVERY - async def send_outbound(self, message: OutboundMessage) -> OutboundSendStatus: + async def send_outbound( + self, message: OutboundMessage, **kwargs + ) -> OutboundSendStatus: """Send an outbound message.""" self.messages.append((message, None)) return OutboundSendStatus.QUEUED_FOR_DELIVERY diff --git a/aries_cloudagent/messaging/schemas/routes.py b/aries_cloudagent/messaging/schemas/routes.py index 5c7c6eb7fb..aa3f48b0fa 100644 --- a/aries_cloudagent/messaging/schemas/routes.py +++ b/aries_cloudagent/messaging/schemas/routes.py @@ -28,6 +28,7 @@ GET_SCHEMA, IndyLedgerRequestsExecutor, ) +from ...multitenant.base import BaseMultitenantManager from ...protocols.endorse_transaction.v1_0.manager import ( TransactionManager, TransactionManagerError, @@ -183,6 +184,18 @@ async def schemas_send_schema(request: web.BaseRequest): schema_version = body.get("schema_version") attributes = body.get("attributes") + tag_query = {"schema_name": schema_name, "schema_version": schema_version} + async with profile.session() as session: + storage = session.inject(BaseStorage) + found = await storage.find_all_records( + type_filter=SCHEMA_SENT_RECORD_TYPE, + tag_query=tag_query, + ) + if 0 < len(found): + raise web.HTTPBadRequest( + reason=f"Schema {schema_name} {schema_version} already exists" + ) + # check if we need to endorse if is_author_role(context.profile): # authors cannot write to the ledger @@ -259,8 +272,20 @@ async def schemas_send_schema(request: web.BaseRequest): if not create_transaction_for_endorser: # Notify event await notify_schema_event(context.profile, schema_id, meta_data) - return web.json_response({"schema_id": schema_id, "schema": schema_def}) - + return web.json_response( + { + "sent": {"schema_id": schema_id, "schema": schema_def}, + "schema_id": schema_id, + "schema": schema_def, + } + ) + + # If the transaction is for the endorser, but the schema has already been created, + # then we send back the schema since the transaction will fail to be created. + elif "signed_txn" not in schema_def: + return web.json_response( + {"sent": {"schema_id": schema_id, "schema": schema_def}} + ) else: transaction_mgr = TransactionManager(context.profile) try: @@ -286,7 +311,12 @@ async def schemas_send_schema(request: web.BaseRequest): await outbound_handler(transaction_request, connection_id=connection_id) - return web.json_response({"txn": transaction.serialize()}) + return web.json_response( + { + "sent": {"schema_id": schema_id, "schema": schema_def}, + "txn": transaction.serialize(), + } + ) @docs( @@ -338,7 +368,11 @@ async def schemas_get_schema(request: web.BaseRequest): schema_id = request.match_info["schema_id"] async with context.profile.session() as session: - ledger_exec_inst = session.inject(IndyLedgerRequestsExecutor) + multitenant_mgr = session.inject_or(BaseMultitenantManager) + if multitenant_mgr: + ledger_exec_inst = IndyLedgerRequestsExecutor(context.profile) + else: + ledger_exec_inst = session.inject(IndyLedgerRequestsExecutor) ledger_id, ledger = await ledger_exec_inst.get_ledger_for_identifier( schema_id, txn_record_type=GET_SCHEMA, @@ -383,7 +417,11 @@ async def schemas_fix_schema_wallet_record(request: web.BaseRequest): async with profile.session() as session: storage = session.inject(BaseStorage) - ledger_exec_inst = session.inject(IndyLedgerRequestsExecutor) + multitenant_mgr = session.inject_or(BaseMultitenantManager) + if multitenant_mgr: + ledger_exec_inst = IndyLedgerRequestsExecutor(context.profile) + else: + ledger_exec_inst = session.inject(IndyLedgerRequestsExecutor) ledger_id, ledger = await ledger_exec_inst.get_ledger_for_identifier( schema_id, txn_record_type=GET_SCHEMA, diff --git a/aries_cloudagent/messaging/schemas/tests/test_routes.py b/aries_cloudagent/messaging/schemas/tests/test_routes.py index ff445535ff..f5347a6b2b 100644 --- a/aries_cloudagent/messaging/schemas/tests/test_routes.py +++ b/aries_cloudagent/messaging/schemas/tests/test_routes.py @@ -8,6 +8,8 @@ from ....ledger.multiple_ledger.ledger_requests_executor import ( IndyLedgerRequestsExecutor, ) +from ....multitenant.base import BaseMultitenantManager +from ....multitenant.manager import MultitenantManager from ....storage.base import BaseStorage from .. import routes as test_module @@ -70,6 +72,13 @@ async def test_send_schema(self): assert result == mock_response.return_value mock_response.assert_called_once_with( { + "sent": { + "schema_id": SCHEMA_ID, + "schema": { + "schema": "def", + "signed_txn": "...", + }, + }, "schema_id": SCHEMA_ID, "schema": { "schema": "def", @@ -116,7 +125,18 @@ async def test_send_schema_create_transaction_for_endorser(self): ) result = await test_module.schemas_send_schema(self.request) assert result == mock_response.return_value - mock_response.assert_called_once_with({"txn": {"...": "..."}}) + mock_response.assert_called_once_with( + { + "sent": { + "schema_id": SCHEMA_ID, + "schema": { + "schema": "def", + "signed_txn": "...", + }, + }, + "txn": {"...": "..."}, + } + ) async def test_send_schema_create_transaction_for_endorser_storage_x(self): self.request.json = async_mock.CoroutineMock( @@ -137,7 +157,6 @@ async def test_send_schema_create_transaction_for_endorser_storage_x(self): ) as mock_conn_rec_retrieve, async_mock.patch.object( test_module, "TransactionManager", async_mock.MagicMock() ) as mock_txn_mgr: - mock_txn_mgr.return_value = async_mock.MagicMock( create_record=async_mock.CoroutineMock( side_effect=test_module.StorageError() @@ -306,6 +325,26 @@ async def test_get_schema(self): } ) + async def test_get_schema_multitenant(self): + self.profile_injector.bind_instance( + BaseMultitenantManager, + async_mock.MagicMock(MultitenantManager, autospec=True), + ) + self.request.match_info = {"schema_id": SCHEMA_ID} + with async_mock.patch.object( + IndyLedgerRequestsExecutor, + "get_ledger_for_identifier", + async_mock.CoroutineMock(return_value=("test_ledger_id", self.ledger)), + ), async_mock.patch.object(test_module.web, "json_response") as mock_response: + result = await test_module.schemas_get_schema(self.request) + assert result == mock_response.return_value + mock_response.assert_called_once_with( + { + "ledger_id": "test_ledger_id", + "schema": {"schema": "def", "signed_txn": "..."}, + } + ) + async def test_get_schema_on_seq_no(self): self.profile_injector.bind_instance( IndyLedgerRequestsExecutor, diff --git a/aries_cloudagent/messaging/tests/test_agent_message.py b/aries_cloudagent/messaging/tests/test_agent_message.py index 4e1b87e28e..947328da63 100644 --- a/aries_cloudagent/messaging/tests/test_agent_message.py +++ b/aries_cloudagent/messaging/tests/test_agent_message.py @@ -4,7 +4,7 @@ from ...core.in_memory import InMemoryProfile from ...protocols.didcomm_prefix import DIDCommPrefix -from ...wallet.key_type import KeyType +from ...wallet.key_type import ED25519 from ..agent_message import AgentMessage, AgentMessageSchema from ..decorators.signature_decorator import SignatureDecorator @@ -72,7 +72,7 @@ class BadImplementationClass(AgentMessage): async def test_field_signature(self): session = InMemoryProfile.test_session() wallet = session.wallet - key_info = await wallet.create_signing_key(KeyType.ED25519) + key_info = await wallet.create_signing_key(ED25519) msg = SignedAgentMessage() msg.value = None diff --git a/aries_cloudagent/messaging/util.py b/aries_cloudagent/messaging/util.py index a5e28b15a1..2c23795a36 100644 --- a/aries_cloudagent/messaging/util.py +++ b/aries_cloudagent/messaging/util.py @@ -11,7 +11,7 @@ LOGGER = logging.getLogger(__name__) -I32_BOUND = 2 ** 31 +I32_BOUND = 2**31 def datetime_to_str(dt: Union[str, datetime]) -> str: diff --git a/aries_cloudagent/messaging/valid.py b/aries_cloudagent/messaging/valid.py index acaf9a27f8..5af2a2d7be 100644 --- a/aries_cloudagent/messaging/valid.py +++ b/aries_cloudagent/messaging/valid.py @@ -23,60 +23,41 @@ class StrOrDictField(Field): """URI or Dict field for Marshmallow.""" - def _serialize(self, value, attr, obj, **kwargs): - return value - def _deserialize(self, value, attr, data, **kwargs): - if isinstance(value, (str, dict)): - return value - else: + if not isinstance(value, (str, dict)): raise ValidationError("Field should be str or dict") + return super()._deserialize(value, attr, data, **kwargs) class StrOrNumberField(Field): """String or Number field for Marshmallow.""" - def _serialize(self, value, attr, obj, **kwargs): - return value - def _deserialize(self, value, attr, data, **kwargs): - if isinstance(value, (str, float, int)): - return value - else: + if not isinstance(value, (str, float, int)): raise ValidationError("Field should be str or int or float") + return super()._deserialize(value, attr, data, **kwargs) class DictOrDictListField(Field): """Dict or Dict List field for Marshmallow.""" - def _serialize(self, value, attr, obj, **kwargs): - return value - def _deserialize(self, value, attr, data, **kwargs): - # dict - if isinstance(value, dict): - return value - # list of dicts - elif isinstance(value, list) and all(isinstance(item, dict) for item in value): - return value - else: - raise ValidationError("Field should be dict or list of dicts") + if not isinstance(value, dict): + if not isinstance(value, list) or not all( + isinstance(item, dict) for item in value + ): + raise ValidationError("Field should be dict or list of dicts") + return super()._deserialize(value, attr, data, **kwargs) class UriOrDictField(StrOrDictField): """URI or Dict field for Marshmallow.""" - def __init__(self, *args, **kwargs): - """Initialize new UriOrDictField instance.""" - super().__init__(*args, **kwargs) - - # Insert validation into self.validators so that multiple errors can be stored. - self.validators.insert(0, self._uri_validator) - - def _uri_validator(self, value): - # Check if URI when + def _deserialize(self, value, attr, data, **kwargs): if isinstance(value, str): - return Uri()(value) + # Check regex + Uri()(value) + return super()._deserialize(value, attr, data, **kwargs) class IntEpoch(Range): @@ -349,6 +330,29 @@ def __init__(self): ) +class RoutingKey(Regexp): + """ + Validate between indy or did key. + + Validate value against indy (Ed25519VerificationKey2018) + raw public key or DID key specification. + """ + + EXAMPLE = DIDKey.EXAMPLE + PATTERN = re.compile(DIDKey.PATTERN.pattern + "|" + IndyRawPublicKey.PATTERN) + + def __init__(self): + """Initializer.""" + + super().__init__( + RoutingKey.PATTERN, + error=( + "Value {input} is not in W3C did:key" + " or Ed25519VerificationKey2018 key format" + ), + ) + + class IndyCredDefId(Regexp): """Validate value against indy credential definition identifier specification.""" @@ -752,7 +756,7 @@ def __call__(self, value): except ValidationError: raise ValidationError( f"credential subject id {value[0]} must be URI" - ) + ) from None return value @@ -788,6 +792,7 @@ def __init__( JWT = {"validate": JSONWebToken(), "example": JSONWebToken.EXAMPLE} DID_KEY = {"validate": DIDKey(), "example": DIDKey.EXAMPLE} DID_POSTURE = {"validate": DIDPosture(), "example": DIDPosture.EXAMPLE} +ROUTING_KEY = {"validate": RoutingKey(), "example": RoutingKey.EXAMPLE} INDY_DID = {"validate": IndyDID(), "example": IndyDID.EXAMPLE} GENERIC_DID = {"validate": MaybeIndyDID(), "example": MaybeIndyDID.EXAMPLE} INDY_RAW_PUBLIC_KEY = { diff --git a/aries_cloudagent/multitenant/admin/routes.py b/aries_cloudagent/multitenant/admin/routes.py index fd0a8a6b3e..b9c78f9e20 100644 --- a/aries_cloudagent/multitenant/admin/routes.py +++ b/aries_cloudagent/multitenant/admin/routes.py @@ -3,26 +3,72 @@ from aiohttp import web from aiohttp_apispec import ( docs, - request_schema, match_info_schema, - response_schema, querystring_schema, + request_schema, + response_schema, ) -from marshmallow import fields, validate, validates_schema, ValidationError +from marshmallow import ValidationError, fields, validate, validates_schema from ...admin.request_context import AdminRequestContext -from ...messaging.valid import JSONWebToken, UUIDFour +from ...core.error import BaseError +from ...core.profile import ProfileManagerProvider from ...messaging.models.base import BaseModelError from ...messaging.models.openapi import OpenAPISchema +from ...messaging.valid import JSONWebToken, UUIDFour from ...multitenant.base import BaseMultitenantManager from ...storage.error import StorageError, StorageNotFoundError -from ...wallet.models.wallet_record import WalletRecord, WalletRecordSchema from ...wallet.error import WalletSettingsError +from ...wallet.models.wallet_record import WalletRecord, WalletRecordSchema +from ..error import WalletKeyMissingError -from ...core.error import BaseError -from ...core.profile import ProfileManagerProvider -from ..error import WalletKeyMissingError +ACAPY_LIFECYCLE_CONFIG_FLAG_MAP = { + "ACAPY_LOG_LEVEL": "log.level", + "ACAPY_INVITE_PUBLIC": "debug.invite_public", + "ACAPY_PUBLIC_INVITES": "public_invites", + "ACAPY_AUTO_ACCEPT_INVITES": "debug.auto_accept_invites", + "ACAPY_AUTO_ACCEPT_REQUESTS": "debug.auto_accept_requests", + "ACAPY_AUTO_PING_CONNECTION": "auto_ping_connection", + "ACAPY_MONITOR_PING": "debug.monitor_ping", + "ACAPY_AUTO_RESPOND_MESSAGES": "debug.auto_respond_messages", + "ACAPY_AUTO_RESPOND_CREDENTIAL_OFFER": "debug.auto_respond_credential_offer", + "ACAPY_AUTO_RESPOND_CREDENTIAL_REQUEST": "debug.auto_respond_credential_request", + "ACAPY_AUTO_VERIFY_PRESENTATION": "debug.auto_verify_presentation", + "ACAPY_NOTIFY_REVOCATION": "revocation.notify", + "ACAPY_AUTO_REQUEST_ENDORSEMENT": "endorser.auto_request", + "ACAPY_AUTO_WRITE_TRANSACTIONS": "endorser.auto_write", + "ACAPY_CREATE_REVOCATION_TRANSACTIONS": "endorser.auto_create_rev_reg", + "ACAPY_ENDORSER_ROLE": "endorser.protocol_role", +} + +ACAPY_LIFECYCLE_CONFIG_FLAG_ARGS_MAP = { + "log-level": "log.level", + "invite-public": "debug.invite_public", + "public-invites": "public_invites", + "auto-accept-invites": "debug.auto_accept_invites", + "auto-accept-requests": "debug.auto_accept_requests", + "auto-ping-connection": "auto_ping_connection", + "monitor-ping": "debug.monitor_ping", + "auto-respond-messages": "debug.auto_respond_messages", + "auto-respond-credential-offer": "debug.auto_respond_credential_offer", + "auto-respond-credential-request": "debug.auto_respond_credential_request", + "auto-verify-presentation": "debug.auto_verify_presentation", + "notify-revocation": "revocation.notify", + "auto-request-endorsement": "endorser.auto_request", + "auto-write-transactions": "endorser.auto_write", + "auto-create-revocation-transactions": "endorser.auto_create_rev_reg", + "endorser-protocol-role": "endorser.protocol_role", +} + +ACAPY_ENDORSER_FLAGS_DEPENDENT_ON_AUTHOR_ROLE = [ + "ACAPY_AUTO_REQUEST_ENDORSEMENT", + "ACAPY_AUTO_WRITE_TRANSACTIONS", + "ACAPY_CREATE_REVOCATION_TRANSACTIONS", + "auto-request-endorsement", + "auto-write-transactions", + "auto-create-revocation-transactions", +] def format_wallet_record(wallet_record: WalletRecord): @@ -37,6 +83,35 @@ def format_wallet_record(wallet_record: WalletRecord): return wallet_info +def get_extra_settings_dict_per_tenant(tenant_settings: dict) -> dict: + """Get per tenant settings to be applied when creating wallet.""" + + endorser_role_flag = tenant_settings.get( + "ACAPY_ENDORSER_ROLE" + ) or tenant_settings.get("endorser_protocol_role") + extra_settings = {} + if endorser_role_flag and endorser_role_flag == "author": + extra_settings["endorser.author"] = True + elif endorser_role_flag and endorser_role_flag == "endorser": + extra_settings["endorser.endorser"] = True + for flag in tenant_settings.keys(): + if ( + flag in ACAPY_ENDORSER_FLAGS_DEPENDENT_ON_AUTHOR_ROLE + and endorser_role_flag != "author" + ): + # These flags require endorser role as author, if not set as author then + # this setting will be ignored. + continue + if flag != "ACAPY_ENDORSER_ROLE": + map_flag = ACAPY_LIFECYCLE_CONFIG_FLAG_MAP.get( + flag + ) or ACAPY_LIFECYCLE_CONFIG_FLAG_ARGS_MAP.get(flag) + if not map_flag: + continue + extra_settings[map_flag] = tenant_settings[flag] + return extra_settings + + class MultitenantModuleResponseSchema(OpenAPISchema): """Response schema for multitenant module.""" @@ -58,6 +133,18 @@ class CreateWalletRequestSchema(OpenAPISchema): description="Master key used for key derivation.", example="MySecretKey123" ) + extra_settings = fields.Dict( + description="Agent config key-value pairs", + required=False, + ) + + wallet_key_derivation = fields.Str( + description="Key derivation", + required=False, + example="RAW", + validate=validate.OneOf(["ARGON2I_MOD", "ARGON2I_INT", "RAW"]), + ) + wallet_type = fields.Str( description="Type of the wallet to create", example="indy", @@ -137,6 +224,10 @@ class UpdateWalletRequestSchema(OpenAPISchema): default="default", validate=validate.OneOf(["default", "both", "base"]), ) + extra_settings = fields.Dict( + description="Agent config key-value pairs", + required=False, + ) wallet_webhook_urls = fields.List( fields.Str( description="Optional webhook URL to receive webhook messages", @@ -289,6 +380,7 @@ async def wallet_create(request: web.BaseRequest): wallet_key = body.get("wallet_key") wallet_webhook_urls = body.get("wallet_webhook_urls") or [] wallet_dispatch_type = body.get("wallet_dispatch_type") or "default" + extra_settings = body.get("extra_settings") or {} # If no webhooks specified, then dispatch only to base webhook targets if wallet_webhook_urls == []: wallet_dispatch_type = "base" @@ -300,13 +392,18 @@ async def wallet_create(request: web.BaseRequest): "wallet.webhook_urls": wallet_webhook_urls, "wallet.dispatch_type": wallet_dispatch_type, } + extra_subwallet_setting = get_extra_settings_dict_per_tenant(extra_settings) + settings.update(extra_subwallet_setting) label = body.get("label") image_url = body.get("image_url") + key_derivation = body.get("wallet_key_derivation") if label: settings["default_label"] = label if image_url: settings["image_url"] = image_url + if key_derivation: # allow lower levels to handle default + settings["wallet.key_derivation_method"] = key_derivation try: multitenant_mgr = context.profile.inject(BaseMultitenantManager) @@ -315,7 +412,7 @@ async def wallet_create(request: web.BaseRequest): settings, key_management_mode ) - token = multitenant_mgr.create_auth_token(wallet_record, wallet_key) + token = await multitenant_mgr.create_auth_token(wallet_record, wallet_key) except BaseError as err: raise web.HTTPBadRequest(reason=err.roll_up) from err @@ -346,6 +443,7 @@ async def wallet_update(request: web.BaseRequest): wallet_dispatch_type = body.get("wallet_dispatch_type") label = body.get("label") image_url = body.get("image_url") + extra_settings = body.get("extra_settings") or {} if all( v is None for v in (wallet_webhook_urls, wallet_dispatch_type, label, image_url) @@ -368,6 +466,8 @@ async def wallet_update(request: web.BaseRequest): settings["default_label"] = label if image_url is not None: settings["image_url"] = image_url + extra_subwallet_setting = get_extra_settings_dict_per_tenant(extra_settings) + settings.update(extra_subwallet_setting) try: multitenant_mgr = context.profile.inject(BaseMultitenantManager) @@ -413,7 +513,7 @@ async def wallet_create_token(request: web.BaseRequest): " the wallet key to be provided" ) - token = multitenant_mgr.create_auth_token(wallet_record, wallet_key) + token = await multitenant_mgr.create_auth_token(wallet_record, wallet_key) except StorageNotFoundError as err: raise web.HTTPNotFound(reason=err.roll_up) from err except WalletKeyMissingError as err: diff --git a/aries_cloudagent/multitenant/admin/tests/test_routes.py b/aries_cloudagent/multitenant/admin/tests/test_routes.py index c1f50f49c3..312997c4b6 100644 --- a/aries_cloudagent/multitenant/admin/tests/test_routes.py +++ b/aries_cloudagent/multitenant/admin/tests/test_routes.py @@ -139,9 +139,67 @@ async def test_wallets_list_query(self): } ) + async def test_wallet_create_tenant_settings(self): + body = { + "wallet_name": "test", + "default_label": "test_label", + "wallet_type": "indy", + "wallet_key": "test", + "key_management_mode": "managed", + "wallet_webhook_urls": [], + "wallet_dispatch_type": "base", + "extra_settings": { + "ACAPY_LOG_LEVEL": "INFO", + "ACAPY_INVITE_PUBLIC": True, + "ACAPY_PUBLIC_INVITES": True, + }, + } + self.request.json = async_mock.CoroutineMock(return_value=body) + + with async_mock.patch.object(test_module.web, "json_response") as mock_response: + wallet_mock = async_mock.MagicMock( + serialize=async_mock.MagicMock( + return_value={ + "wallet_id": "test", + "settings": {}, + "key_management_mode": body["key_management_mode"], + } + ) + ) # wallet_record + self.mock_multitenant_mgr.create_wallet = async_mock.CoroutineMock( + return_value=wallet_mock + ) + + self.mock_multitenant_mgr.create_auth_token = async_mock.CoroutineMock( + return_value="test_token" + ) + print(self.request["context"]) + await test_module.wallet_create(self.request) + + self.mock_multitenant_mgr.create_wallet.assert_called_once_with( + { + "wallet.name": body["wallet_name"], + "wallet.type": body["wallet_type"], + "wallet.key": body["wallet_key"], + "wallet.webhook_urls": body["wallet_webhook_urls"], + "wallet.dispatch_type": body["wallet_dispatch_type"], + "log.level": "INFO", + "debug.invite_public": True, + "public_invites": True, + }, + body["key_management_mode"], + ) + self.mock_multitenant_mgr.create_auth_token.assert_called_once_with( + wallet_mock, body["wallet_key"] + ) + mock_response.assert_called_once_with( + {**test_module.format_wallet_record(wallet_mock), "token": "test_token"} + ) + async def test_wallet_create(self): body = { "wallet_name": "test", + "default_label": "test_label", "wallet_type": "indy", "wallet_key": "test", "key_management_mode": "managed", @@ -164,7 +222,7 @@ async def test_wallet_create(self): return_value=wallet_mock ) - self.mock_multitenant_mgr.create_auth_token = async_mock.Mock( + self.mock_multitenant_mgr.create_auth_token = async_mock.CoroutineMock( return_value="test_token" ) print(self.request["context"]) @@ -206,6 +264,7 @@ async def test_wallet_create_optional_default_fields(self): body = { "wallet_name": "test", "wallet_key": "test", + "wallet_key_derivation": "ARGON2I_MOD", "wallet_webhook_urls": [], "wallet_dispatch_type": "base", "label": "my_test_label", @@ -215,7 +274,7 @@ async def test_wallet_create_optional_default_fields(self): with async_mock.patch.object(test_module.web, "json_response") as mock_response: self.mock_multitenant_mgr.create_wallet = async_mock.CoroutineMock() - self.mock_multitenant_mgr.create_auth_token = async_mock.Mock() + self.mock_multitenant_mgr.create_auth_token = async_mock.CoroutineMock() await test_module.wallet_create(self.request) self.mock_multitenant_mgr.create_wallet.assert_called_once_with( @@ -227,10 +286,83 @@ async def test_wallet_create_optional_default_fields(self): "image_url": body["image_url"], "wallet.webhook_urls": body["wallet_webhook_urls"], "wallet.dispatch_type": body["wallet_dispatch_type"], + "wallet.key_derivation_method": body["wallet_key_derivation"], }, WalletRecord.MODE_MANAGED, ) + async def test_wallet_create_raw_key_derivation(self): + body = { + "wallet_name": "test", + "wallet_key": "test", + "wallet_key_derivation": "RAW", + } + self.request.json = async_mock.CoroutineMock(return_value=body) + + with async_mock.patch.object(test_module.web, "json_response") as mock_response: + self.mock_multitenant_mgr.create_wallet = async_mock.CoroutineMock() + self.mock_multitenant_mgr.create_auth_token = async_mock.CoroutineMock() + + await test_module.wallet_create(self.request) + self.mock_multitenant_mgr.create_wallet.assert_called_once_with( + { + "wallet.type": "in_memory", + "wallet.name": body["wallet_name"], + "wallet.key": body["wallet_key"], + "wallet.key_derivation_method": body["wallet_key_derivation"], + "wallet.webhook_urls": [], + "wallet.dispatch_type": "base", + }, + WalletRecord.MODE_MANAGED, + ) + + async def test_wallet_update_tenant_settings(self): + self.request.match_info = {"wallet_id": "test-wallet-id"} + body = { + "wallet_webhook_urls": ["test-webhook-url"], + "wallet_dispatch_type": "default", + "label": "test-label", + "image_url": "test-image-url", + "extra_settings": { + "ACAPY_LOG_LEVEL": "INFO", + "ACAPY_INVITE_PUBLIC": True, + "ACAPY_PUBLIC_INVITES": True, + }, + } + self.request.json = async_mock.CoroutineMock(return_value=body) + + with async_mock.patch.object(test_module.web, "json_response") as mock_response: + settings = { + "wallet.webhook_urls": body["wallet_webhook_urls"], + "wallet.dispatch_type": body["wallet_dispatch_type"], + "default_label": body["label"], + "image_url": body["image_url"], + "log.level": "INFO", + "debug.invite_public": True, + "public_invites": True, + } + wallet_mock = async_mock.MagicMock( + serialize=async_mock.MagicMock( + return_value={ + "wallet_id": "test-wallet-id", + "settings": settings, + } + ) + ) + self.mock_multitenant_mgr.update_wallet = async_mock.CoroutineMock( + return_value=wallet_mock + ) + + await test_module.wallet_update(self.request) + + self.mock_multitenant_mgr.update_wallet.assert_called_once_with( + "test-wallet-id", + settings, + ) + mock_response.assert_called_once_with( + {"wallet_id": "test-wallet-id", "settings": settings} + ) + async def test_wallet_update(self): self.request.match_info = {"wallet_id": "test-wallet-id"} body = { @@ -440,7 +572,7 @@ async def test_wallet_create_token_managed(self): ) as mock_response: mock_wallet_record_retrieve_by_id.return_value = mock_wallet_record - self.mock_multitenant_mgr.create_auth_token = async_mock.Mock( + self.mock_multitenant_mgr.create_auth_token = async_mock.CoroutineMock( return_value="test_token" ) @@ -468,7 +600,7 @@ async def test_wallet_create_token_unmanaged(self): ) as mock_response: mock_wallet_record_retrieve_by_id.return_value = mock_wallet_record - self.mock_multitenant_mgr.create_auth_token = async_mock.Mock( + self.mock_multitenant_mgr.create_auth_token = async_mock.CoroutineMock( return_value="test_token" ) diff --git a/aries_cloudagent/multitenant/askar_profile_manager.py b/aries_cloudagent/multitenant/askar_profile_manager.py index c692b04ea8..83135cfe8e 100644 --- a/aries_cloudagent/multitenant/askar_profile_manager.py +++ b/aries_cloudagent/multitenant/askar_profile_manager.py @@ -1,5 +1,6 @@ """Manager for askar profile multitenancy mode.""" +from typing import Iterable, Optional, cast from ..core.profile import ( Profile, ) @@ -13,15 +14,25 @@ class AskarProfileMultitenantManager(BaseMultitenantManager): """Class for handling askar profile multitenancy.""" - DEFAULT_MULTIENANT_WALLET_NAME = "multitenant_sub_wallet" + DEFAULT_MULTITENANT_WALLET_NAME = "multitenant_sub_wallet" - def __init__(self, profile: Profile): + def __init__(self, profile: Profile, multitenant_profile: AskarProfile = None): """Initialize askar profile multitenant Manager. Args: profile: The base profile for this manager """ super().__init__(profile) + self._multitenant_profile: Optional[AskarProfile] = multitenant_profile + + @property + def open_profiles(self) -> Iterable[Profile]: + """Return iterator over open profiles. + + Only the core multitenant profile is considered open. + """ + if self._multitenant_profile: + yield self._multitenant_profile async def get_wallet_profile( self, @@ -33,6 +44,13 @@ async def get_wallet_profile( ) -> Profile: """Get Askar profile for a wallet record. + An object of type AskarProfile is returned but this should not be + confused with the underlying profile mechanism provided by Askar that + enables multiple "profiles" to share a wallet. Usage of this mechanism + is what causes this implementation of BaseMultitenantManager.get_wallet_profile + to look different from others, especially since no explicit clean up is + required for profiles that are no longer in use. + Args: base_context: Base context to extend from wallet_record: Wallet record to get the context for @@ -42,12 +60,10 @@ async def get_wallet_profile( Profile: Profile for the wallet record """ - multitenant_wallet_name = ( - base_context.settings.get("multitenant.wallet_name") - or self.DEFAULT_MULTIENANT_WALLET_NAME - ) - - if multitenant_wallet_name not in self._instances: + if not self._multitenant_profile: + multitenant_wallet_name = base_context.settings.get( + "multitenant.wallet_name", self.DEFAULT_MULTITENANT_WALLET_NAME + ) context = base_context.copy() sub_wallet_settings = { "wallet.recreate": False, @@ -65,13 +81,14 @@ async def get_wallet_profile( context.settings = context.settings.extend(sub_wallet_settings) profile, _ = await wallet_config(context, provision=False) - self._instances[multitenant_wallet_name] = profile + self._multitenant_profile = cast(AskarProfile, profile) - multitenant_wallet = self._instances[multitenant_wallet_name] - profile_context = multitenant_wallet.context.copy() + profile_context = self._multitenant_profile.context.copy() if provision: - await multitenant_wallet.store.create_profile(wallet_record.wallet_id) + await self._multitenant_profile.store.create_profile( + wallet_record.wallet_id + ) extra_settings = { "admin.webhook_urls": self.get_webhook_urls(base_context, wallet_record), @@ -82,7 +99,13 @@ async def get_wallet_profile( wallet_record.settings ).extend(extra_settings) - return AskarProfile(multitenant_wallet.opened, profile_context) + assert self._multitenant_profile.opened + + return AskarProfile( + self._multitenant_profile.opened, + profile_context, + profile_id=wallet_record.wallet_id, + ) async def remove_wallet_profile(self, profile: Profile): """Remove the wallet profile instance. diff --git a/aries_cloudagent/multitenant/base.py b/aries_cloudagent/multitenant/base.py index 5bdd9a9787..03fbb7a515 100644 --- a/aries_cloudagent/multitenant/base.py +++ b/aries_cloudagent/multitenant/base.py @@ -1,30 +1,26 @@ """Manager for multitenancy.""" +from abc import ABC, abstractmethod +from datetime import datetime import logging -from abc import abstractmethod +from typing import Iterable, List, Optional, cast, Tuple import jwt -from typing import List, Optional, cast -from ..core.profile import ( - Profile, - ProfileSession, -) -from ..messaging.responder import BaseResponder from ..config.injection_context import InjectionContext -from ..wallet.models.wallet_record import WalletRecord -from ..wallet.base import BaseWallet from ..core.error import BaseError -from ..protocols.routing.v1_0.manager import RouteNotFoundError, RoutingManager -from ..protocols.routing.v1_0.models.route_record import RouteRecord -from ..transport.wire_format import BaseWireFormat -from ..storage.base import BaseStorage -from ..storage.error import StorageNotFoundError +from ..core.profile import Profile, ProfileSession from ..protocols.coordinate_mediation.v1_0.manager import ( MediationManager, MediationRecord, ) - +from ..protocols.coordinate_mediation.v1_0.route_manager import RouteManager +from ..protocols.routing.v1_0.manager import RouteNotFoundError, RoutingManager +from ..protocols.routing.v1_0.models.route_record import RouteRecord +from ..storage.base import BaseStorage +from ..transport.wire_format import BaseWireFormat +from ..wallet.base import BaseWallet +from ..wallet.models.wallet_record import WalletRecord from .error import WalletKeyMissingError LOGGER = logging.getLogger(__name__) @@ -34,7 +30,7 @@ class MultitenantManagerError(BaseError): """Generic multitenant error.""" -class BaseMultitenantManager: +class BaseMultitenantManager(ABC): """Base class for handling multitenancy.""" def __init__(self, profile: Profile): @@ -47,7 +43,10 @@ def __init__(self, profile: Profile): if not profile: raise MultitenantManagerError("Missing profile") - self._instances: dict[str, Profile] = {} + @property + @abstractmethod + def open_profiles(self) -> Iterable[Profile]: + """Return iterator over open profiles.""" async def get_default_mediator(self) -> Optional[MediationRecord]: """Retrieve the default mediator used for subwallet routing. @@ -183,26 +182,29 @@ async def create_wallet( ) await wallet_record.save(session) + try: + # provision wallet + profile = await self.get_wallet_profile( + self._profile.context, + wallet_record, + { + "wallet.key": wallet_key, + }, + provision=True, + ) - # provision wallet - profile = await self.get_wallet_profile( - self._profile.context, - wallet_record, - { - "wallet.key": wallet_key, - }, - provision=True, - ) - - # subwallet context - async with profile.session() as session: - wallet = session.inject(BaseWallet) - public_did_info = await wallet.get_public_did() + # subwallet context + async with profile.session() as session: + wallet = session.inject(BaseWallet) + public_did_info = await wallet.get_public_did() - if public_did_info: - await self.add_key( - wallet_record.wallet_id, public_did_info.verkey, skip_if_exists=True - ) + if public_did_info: + await profile.inject(RouteManager).route_verkey( + profile, public_did_info.verkey + ) + except Exception: + await wallet_record.delete_record(session) + raise return wallet_record @@ -211,7 +213,7 @@ async def update_wallet( wallet_id: str, new_settings: dict, ) -> WalletRecord: - """Update a existing wallet and wallet record. + """Update an existing wallet record. Args: wallet_id: The wallet id of the wallet record @@ -227,18 +229,6 @@ async def update_wallet( wallet_record.update_settings(new_settings) await wallet_record.save(session) - # update profile only if loaded - if wallet_id in self._instances: - profile = self._instances[wallet_id] - profile.settings.update(wallet_record.settings) - - extra_settings = { - "admin.webhook_urls": self.get_webhook_urls( - self._profile.context, wallet_record - ), - } - profile.settings.update(extra_settings) - return wallet_record async def remove_wallet(self, wallet_id: str, wallet_key: str = None): @@ -290,50 +280,7 @@ async def remove_wallet_profile(self, profile: Profile): """ - async def add_key( - self, wallet_id: str, recipient_key: str, *, skip_if_exists: bool = False - ): - """ - Add a wallet key to map incoming messages to specific subwallets. - - Args: - wallet_id: The wallet id the key corresponds to - recipient_key: The recipient key belonging to the wallet - skip_if_exists: Whether to skip the action if the key is already registered - for relaying / mediation - """ - - LOGGER.info( - f"Add route record for recipient {recipient_key} to wallet {wallet_id}" - ) - routing_mgr = RoutingManager(self._profile) - mediation_mgr = MediationManager(self._profile) - mediation_record = await mediation_mgr.get_default_mediator() - - if skip_if_exists: - try: - async with self._profile.session() as session: - await RouteRecord.retrieve_by_recipient_key(session, recipient_key) - - # If no error is thrown, it means there is already a record - return - except (StorageNotFoundError): - pass - - await routing_mgr.create_route_record( - recipient_key=recipient_key, internal_wallet_id=wallet_id - ) - - # External mediation - if mediation_record: - keylist_updates = await mediation_mgr.add_key(recipient_key) - - responder = self._profile.inject(BaseResponder) - await responder.send( - keylist_updates, connection_id=mediation_record.connection_id - ) - - def create_auth_token( + async def create_auth_token( self, wallet_record: WalletRecord, wallet_key: str = None ) -> str: """Create JWT auth token for specified wallet record. @@ -351,8 +298,9 @@ def create_auth_token( str: JWT auth token """ + iat = int(round(datetime.utcnow().timestamp())) - jwt_payload = {"wallet_id": wallet_record.wallet_id} + jwt_payload = {"wallet_id": wallet_record.wallet_id, "iat": iat} jwt_secret = self._profile.settings.get("multitenant.jwt_secret") if wallet_record.requires_external_key: @@ -361,10 +309,37 @@ def create_auth_token( jwt_payload["wallet_key"] = wallet_key - token = jwt.encode(jwt_payload, jwt_secret, algorithm="HS256").decode() + token = jwt.encode(jwt_payload, jwt_secret, algorithm="HS256") + + # Store iat for verification later on + wallet_record.jwt_iat = iat + async with self._profile.session() as session: + await wallet_record.save(session) return token + def get_wallet_details_from_token(self, token: str) -> Tuple[str, str]: + """Get the wallet_id and wallet_key from provided token.""" + jwt_secret = self._profile.context.settings.get("multitenant.jwt_secret") + token_body = jwt.decode(token, jwt_secret, algorithms=["HS256"]) + wallet_id = token_body.get("wallet_id") + wallet_key = token_body.get("wallet_key") + return wallet_id, wallet_key + + async def get_wallet_and_profile( + self, context: InjectionContext, wallet_id: str, wallet_key: str + ) -> Tuple[WalletRecord, Profile]: + """Get the wallet_record and profile associated with wallet id and key.""" + extra_settings = {} + async with self._profile.session() as session: + wallet = await WalletRecord.retrieve_by_id(session, wallet_id) + if wallet.requires_external_key: + if not wallet_key: + raise WalletKeyMissingError() + extra_settings["wallet.key"] = wallet_key + profile = await self.get_wallet_profile(context, wallet, extra_settings) + return (wallet, profile) + async def get_profile_for_token( self, context: InjectionContext, token: str ) -> Profile: @@ -389,6 +364,7 @@ async def get_profile_for_token( wallet_id = token_body.get("wallet_id") wallet_key = token_body.get("wallet_key") + iat = token_body.get("iat") async with self._profile.session() as session: wallet = await WalletRecord.retrieve_by_id(session, wallet_id) @@ -399,6 +375,9 @@ async def get_profile_for_token( extra_settings["wallet.key"] = wallet_key + if wallet.jwt_iat and wallet.jwt_iat != iat: + raise MultitenantManagerError("Token not valid") + profile = await self.get_wallet_profile(context, wallet, extra_settings) return profile @@ -421,9 +400,22 @@ async def _get_wallet_by_key(self, recipient_key: str) -> Optional[WalletRecord] ) return wallet - except (RouteNotFoundError): + except RouteNotFoundError: pass + async def get_profile_for_key( + self, context: InjectionContext, recipient_key: str + ) -> Optional[Profile]: + """Retrieve a wallet profile by recipient key.""" + wallet = await self._get_wallet_by_key(recipient_key) + if not wallet: + return None + + if wallet.requires_external_key: + raise WalletKeyMissingError() + + return await self.get_wallet_profile(context, wallet) + async def get_wallets_by_message( self, message_body, wire_format: BaseWireFormat = None ) -> List[WalletRecord]: diff --git a/aries_cloudagent/multitenant/cache.py b/aries_cloudagent/multitenant/cache.py new file mode 100644 index 0000000000..1fb3f37e3c --- /dev/null +++ b/aries_cloudagent/multitenant/cache.py @@ -0,0 +1,113 @@ +"""Cache for multitenancy profiles.""" + +import logging +from collections import OrderedDict +from typing import Optional +from weakref import WeakValueDictionary + +from ..core.profile import Profile + +LOGGER = logging.getLogger(__name__) + + +class ProfileCache: + """Profile cache that caches based on LRU strategy.""" + + def __init__(self, capacity: int): + """Initialize ProfileCache. + + Args: + capacity: The capacity of the cache. If capacity is exceeded + profiles are closed. + """ + + LOGGER.debug(f"Profile cache initialized with capacity {capacity}") + + self._cache: OrderedDict[str, Profile] = OrderedDict() + self.profiles: WeakValueDictionary[str, Profile] = WeakValueDictionary() + self.capacity = capacity + + def _cleanup(self): + """Prune cache until size matches defined capacity.""" + if len(self._cache) > self.capacity: + LOGGER.debug( + f"Profile limit of {self.capacity} reached." + " Evicting least recently used profiles..." + ) + while len(self._cache) > self.capacity: + key, _ = self._cache.popitem(last=False) + LOGGER.debug(f"Evicted profile with key {key}") + + def get(self, key: str) -> Optional[Profile]: + """Get profile with associated key from cache. + + If a profile is open but has been evicted from the cache, this will + reinsert the profile back into the cache. This prevents attempting to + open a profile that is already open. Triggers clean up. + + Args: + key (str): the key to get the profile for. + + Returns: + Optional[Profile]: Profile if found in cache. + + """ + value = self.profiles.get(key) + if value: + if key not in self._cache: + LOGGER.debug( + f"Rescuing profile {key} from eviction from cache; profile " + "will be reinserted into cache" + ) + self._cache[key] = value + self._cache.move_to_end(key) + self._cleanup() + + return value + + def has(self, key: str) -> bool: + """Check whether there is a profile with associated key in the cache. + + Args: + key (str): the key to check for a profile + + Returns: + bool: Whether the key exists in the cache + + """ + return key in self.profiles + + def put(self, key: str, value: Profile) -> None: + """Add profile with associated key to the cache. + + If new profile exceeds the cache capacity least recently used profiles + that are not used will be removed from the cache. + + Args: + key (str): the key to set + value (Profile): the profile to set + """ + + # Profiles are responsible for cleaning up after themselves when they + # fall out of scope. Previously the cache needed to create a finalizer. + # value.finalzer() + + # Keep track of currently opened profiles using weak references + self.profiles[key] = value + + # Strong reference to profile to hold open until evicted + LOGGER.debug(f"Setting profile with id {key} in profile cache") + self._cache[key] = value + + # Refresh profile livliness + self._cache.move_to_end(key) + self._cleanup() + + def remove(self, key: str): + """Remove profile with associated key from the cache. + + Args: + key (str): The key to remove from the cache. + """ + del self.profiles[key] + del self._cache[key] diff --git a/aries_cloudagent/multitenant/manager.py b/aries_cloudagent/multitenant/manager.py index 5bbbcc6632..e7bf2d9447 100644 --- a/aries_cloudagent/multitenant/manager.py +++ b/aries_cloudagent/multitenant/manager.py @@ -1,12 +1,16 @@ """Manager for multitenancy.""" -from ..core.profile import ( - Profile, -) -from ..config.wallet import wallet_config +import logging +from typing import Iterable + from ..config.injection_context import InjectionContext -from ..wallet.models.wallet_record import WalletRecord +from ..config.wallet import wallet_config +from ..core.profile import Profile from ..multitenant.base import BaseMultitenantManager +from ..wallet.models.wallet_record import WalletRecord +from .cache import ProfileCache + +LOGGER = logging.getLogger(__name__) class MultitenantManager(BaseMultitenantManager): @@ -19,6 +23,14 @@ def __init__(self, profile: Profile): profile: The profile for this manager """ super().__init__(profile) + self._profiles = ProfileCache( + profile.settings.get_int("multitenant.cache_size") or 100 + ) + + @property + def open_profiles(self) -> Iterable[Profile]: + """Return iterator over open profiles.""" + yield from self._profiles.profiles.values() async def get_wallet_profile( self, @@ -40,7 +52,8 @@ async def get_wallet_profile( """ wallet_id = wallet_record.wallet_id - if wallet_id not in self._instances: + profile = self._profiles.get(wallet_id) + if not profile: # Extend base context context = base_context.copy() @@ -68,9 +81,37 @@ async def get_wallet_profile( # MTODO: add ledger config profile, _ = await wallet_config(context, provision=provision) - self._instances[wallet_id] = profile + self._profiles.put(wallet_id, profile) + + return profile + + async def update_wallet(self, wallet_id: str, new_settings: dict) -> WalletRecord: + """Update an existing wallet and wallet record. + + Args: + wallet_id: The wallet id of the wallet record + new_settings: The context settings to be updated for this wallet + + Returns: + WalletRecord: The updated wallet record + + """ + wallet_record = await super().update_wallet(wallet_id, new_settings) + + # Wallet record has been updated but profile settings in memory must + # also be refreshed; update profile only if loaded + profile = self._profiles.get(wallet_id) + if profile: + profile.settings.update(wallet_record.settings) + + extra_settings = { + "admin.webhook_urls": self.get_webhook_urls( + self._profile.context, wallet_record + ), + } + profile.settings.update(extra_settings) - return self._instances[wallet_id] + return wallet_record async def remove_wallet_profile(self, profile: Profile): """Remove the wallet profile instance. @@ -79,6 +120,6 @@ async def remove_wallet_profile(self, profile: Profile): profile: The wallet profile instance """ - wallet_id = profile.settings.get("wallet.id") - del self._instances[wallet_id] + wallet_id = profile.settings.get_str("wallet.id") + self._profiles.remove(wallet_id) await profile.remove() diff --git a/aries_cloudagent/multitenant/route_manager.py b/aries_cloudagent/multitenant/route_manager.py new file mode 100644 index 0000000000..954b3c98f9 --- /dev/null +++ b/aries_cloudagent/multitenant/route_manager.py @@ -0,0 +1,148 @@ +"""Multitenancy route manager.""" + + +import logging +from typing import List, Optional, Tuple + +from ..connections.models.conn_record import ConnRecord +from ..core.profile import Profile +from ..messaging.responder import BaseResponder +from ..protocols.coordinate_mediation.v1_0.manager import MediationManager +from ..protocols.coordinate_mediation.v1_0.models.mediation_record import ( + MediationRecord, +) +from ..protocols.coordinate_mediation.v1_0.normalization import normalize_from_did_key +from ..protocols.coordinate_mediation.v1_0.route_manager import ( + CoordinateMediationV1RouteManager, + RouteManager, +) +from ..protocols.routing.v1_0.manager import RoutingManager +from ..protocols.routing.v1_0.models.route_record import RouteRecord +from ..storage.error import StorageNotFoundError +from .base import BaseMultitenantManager + + +LOGGER = logging.getLogger(__name__) + + +class MultitenantRouteManager(RouteManager): + """Multitenancy route manager.""" + + def __init__( + self, + root_profile: Profile, + ): + """Initialize multitenant route manager.""" + self.root_profile = root_profile + + async def get_base_wallet_mediator(self) -> Optional[MediationRecord]: + """Get base wallet's default mediator.""" + return await MediationManager(self.root_profile).get_default_mediator() + + async def _route_for_key( + self, + profile: Profile, + recipient_key: str, + mediation_record: Optional[MediationRecord] = None, + *, + skip_if_exists: bool = False, + replace_key: Optional[str] = None, + ): + wallet_id = profile.settings["wallet.id"] + LOGGER.info( + f"Add route record for recipient {recipient_key} to wallet {wallet_id}" + ) + routing_mgr = RoutingManager(self.root_profile) + mediation_mgr = MediationManager(self.root_profile) + # If base wallet had mediator, only notify that mediator. + # Else, if subwallet has mediator, notify that mediator. + base_mediation_record = await self.get_base_wallet_mediator() + mediation_record = base_mediation_record or mediation_record + + if skip_if_exists: + try: + async with self.root_profile.session() as session: + await RouteRecord.retrieve_by_recipient_key(session, recipient_key) + + # If no error is thrown, it means there is already a record + return None + except StorageNotFoundError: + pass + + await routing_mgr.create_route_record( + recipient_key=recipient_key, internal_wallet_id=wallet_id + ) + + # External mediation + keylist_updates = None + if mediation_record: + keylist_updates = await mediation_mgr.add_key(recipient_key) + if replace_key: + keylist_updates = await mediation_mgr.remove_key( + replace_key, keylist_updates + ) + # in order to locate the correct verkey for message packing we need + # to use the correct profile. + # if we are using default/base mediation then we need + # the root_profile to create the responder. + # if sub-wallets are configuring their own mediation, then + # we need the sub-wallet (profile) to create the responder. + responder = ( + self.root_profile.inject(BaseResponder) + if base_mediation_record + else profile.inject(BaseResponder) + ) + await responder.send( + keylist_updates, connection_id=mediation_record.connection_id + ) + + return keylist_updates + + async def routing_info( + self, + profile: Profile, + my_endpoint: str, + mediation_record: Optional[MediationRecord] = None, + ) -> Tuple[List[str], str]: + """Return routing info.""" + routing_keys = [] + + base_mediation_record = await self.get_base_wallet_mediator() + + if base_mediation_record: + routing_keys = base_mediation_record.routing_keys + my_endpoint = base_mediation_record.endpoint + + if mediation_record: + routing_keys = [*routing_keys, *mediation_record.routing_keys] + my_endpoint = mediation_record.endpoint + + return routing_keys, my_endpoint + + +class BaseWalletRouteManager(CoordinateMediationV1RouteManager): + """Route manager for operations specific to the base wallet.""" + + async def connection_from_recipient_key( + self, profile: Profile, recipient_key: str + ) -> ConnRecord: + """Retrieve a connection by recipient key. + + The recipient key is expected to be a local key owned by this agent. + + Since the multi-tenant base wallet can receive and send keylist updates + for sub wallets, we check the sub wallet's connections before the base + wallet. + """ + LOGGER.debug("Retrieving connection for recipient key for multitenant wallet") + manager = profile.inject(BaseMultitenantManager) + profile_to_search = ( + await manager.get_profile_for_key( + profile.context, normalize_from_did_key(recipient_key) + ) + or profile + ) + + return await super().connection_from_recipient_key( + profile_to_search, recipient_key + ) diff --git a/aries_cloudagent/multitenant/tests/test_askar_profile_manager.py b/aries_cloudagent/multitenant/tests/test_askar_profile_manager.py index 6bad949cb4..5bbc1d926c 100644 --- a/aries_cloudagent/multitenant/tests/test_askar_profile_manager.py +++ b/aries_cloudagent/multitenant/tests/test_askar_profile_manager.py @@ -43,79 +43,61 @@ async def test_get_wallet_profile_should_open_store_and_return_profile_with_wall with async_mock.patch( "aries_cloudagent.multitenant.askar_profile_manager.wallet_config" - ) as wallet_config: - with async_mock.patch( - "aries_cloudagent.multitenant.askar_profile_manager.AskarProfile" - ) as AskarProfile: - sub_wallet_profile_context = InjectionContext() - sub_wallet_profile = AskarProfile(None, None) - sub_wallet_profile.context.copy.return_value = ( - sub_wallet_profile_context - ) + ) as wallet_config, async_mock.patch( + "aries_cloudagent.multitenant.askar_profile_manager.AskarProfile", + ) as AskarProfile: + sub_wallet_profile_context = InjectionContext() + sub_wallet_profile = AskarProfile(None, None) + sub_wallet_profile.context.copy.return_value = sub_wallet_profile_context - def side_effect(context, provision): - sub_wallet_profile.name = askar_profile_mock_name - return sub_wallet_profile, None + def side_effect(context, provision): + sub_wallet_profile.name = askar_profile_mock_name + return sub_wallet_profile, None - wallet_config.side_effect = side_effect + wallet_config.side_effect = side_effect - profile = await self.manager.get_wallet_profile( - self.profile.context, wallet_record - ) + profile = await self.manager.get_wallet_profile( + self.profile.context, wallet_record + ) - assert profile.name == askar_profile_mock_name - wallet_config.assert_called_once() - wallet_config_settings_argument = wallet_config.call_args[0][0].settings - assert ( - wallet_config_settings_argument.get("wallet.name") - == self.DEFAULT_MULTIENANT_WALLET_NAME - ) - assert wallet_config_settings_argument.get("wallet.id") == None - assert wallet_config_settings_argument.get("auto_provision") == True - assert wallet_config_settings_argument.get("wallet.type") == "askar" - AskarProfile.assert_called_with( - sub_wallet_profile.opened, sub_wallet_profile_context - ) - assert ( - sub_wallet_profile_context.settings.get("wallet.seed") - == "test_seed" - ) - assert ( - sub_wallet_profile_context.settings.get("wallet.rekey") - == "test_rekey" - ) - assert ( - sub_wallet_profile_context.settings.get("wallet.name") - == "test_name" - ) - assert ( - sub_wallet_profile_context.settings.get("wallet.type") - == "test_type" - ) - assert sub_wallet_profile_context.settings.get("mediation.open") == True - assert ( - sub_wallet_profile_context.settings.get("mediation.invite") - == "http://invite.com" - ) - assert ( - sub_wallet_profile_context.settings.get("mediation.default_id") - == "24a96ef5" - ) - assert ( - sub_wallet_profile_context.settings.get("mediation.clear") == True - ) - assert ( - sub_wallet_profile_context.settings.get("wallet.id") - == wallet_record.wallet_id - ) - assert ( - sub_wallet_profile_context.settings.get("wallet.name") - == "test_name" - ) - assert ( - sub_wallet_profile_context.settings.get("wallet.askar_profile") - == wallet_record.wallet_id - ) + assert profile.name == askar_profile_mock_name + wallet_config.assert_called_once() + wallet_config_settings_argument = wallet_config.call_args[0][0].settings + assert ( + wallet_config_settings_argument.get("wallet.name") + == self.DEFAULT_MULTIENANT_WALLET_NAME + ) + assert wallet_config_settings_argument.get("wallet.id") == None + assert wallet_config_settings_argument.get("auto_provision") == True + assert wallet_config_settings_argument.get("wallet.type") == "askar" + AskarProfile.assert_called_with( + sub_wallet_profile.opened, sub_wallet_profile_context, profile_id="test" + ) + assert sub_wallet_profile_context.settings.get("wallet.seed") == "test_seed" + assert ( + sub_wallet_profile_context.settings.get("wallet.rekey") == "test_rekey" + ) + assert sub_wallet_profile_context.settings.get("wallet.name") == "test_name" + assert sub_wallet_profile_context.settings.get("wallet.type") == "test_type" + assert sub_wallet_profile_context.settings.get("mediation.open") == True + assert ( + sub_wallet_profile_context.settings.get("mediation.invite") + == "http://invite.com" + ) + assert ( + sub_wallet_profile_context.settings.get("mediation.default_id") + == "24a96ef5" + ) + assert sub_wallet_profile_context.settings.get("mediation.clear") == True + assert ( + sub_wallet_profile_context.settings.get("wallet.id") + == wallet_record.wallet_id + ) + assert sub_wallet_profile_context.settings.get("wallet.name") == "test_name" + assert ( + sub_wallet_profile_context.settings.get("wallet.askar_profile") + == wallet_record.wallet_id + ) async def test_get_wallet_profile_should_create_profile(self): wallet_record = WalletRecord(wallet_id="test", settings={}) @@ -128,9 +110,7 @@ async def test_get_wallet_profile_should_create_profile(self): sub_wallet_profile = AskarProfile(None, None) sub_wallet_profile.context.copy.return_value = InjectionContext() sub_wallet_profile.store.create_profile.return_value = create_profile_stub - self.manager._instances[ - self.DEFAULT_MULTIENANT_WALLET_NAME - ] = sub_wallet_profile + self.manager._multitenant_profile = sub_wallet_profile await self.manager.get_wallet_profile( self.profile.context, wallet_record, provision=True @@ -172,8 +152,23 @@ def side_effect(context, provision): ) async def test_remove_wallet_profile(self): - test_profile = InMemoryProfile.test_profile() + test_profile = InMemoryProfile.test_profile({"wallet.id": "test"}) with async_mock.patch.object(InMemoryProfile, "remove") as profile_remove: await self.manager.remove_wallet_profile(test_profile) profile_remove.assert_called_once_with() + + async def test_open_profiles(self): + assert len(list(self.manager.open_profiles)) == 0 + + create_profile_stub = asyncio.Future() + create_profile_stub.set_result("") + with async_mock.patch( + "aries_cloudagent.multitenant.askar_profile_manager.AskarProfile" + ) as AskarProfile: + sub_wallet_profile = AskarProfile(None, None) + sub_wallet_profile.context.copy.return_value = InjectionContext() + sub_wallet_profile.store.create_profile.return_value = create_profile_stub + self.manager._multitenant_profile = sub_wallet_profile + + assert len(list(self.manager.open_profiles)) == 1 diff --git a/aries_cloudagent/multitenant/tests/test_base.py b/aries_cloudagent/multitenant/tests/test_base.py index 4f6cb8dabf..1e28b90b18 100644 --- a/aries_cloudagent/multitenant/tests/test_base.py +++ b/aries_cloudagent/multitenant/tests/test_base.py @@ -1,28 +1,50 @@ +from datetime import datetime + from asynctest import TestCase as AsyncTestCase from asynctest import mock as async_mock - import jwt -from ...core.in_memory import InMemoryProfile +from .. import base as test_module from ...config.base import InjectionError +from ...core.in_memory import InMemoryProfile from ...messaging.responder import BaseResponder -from ...wallet.models.wallet_record import WalletRecord -from ...wallet.in_memory import InMemoryWallet -from ...wallet.did_info import DIDInfo -from ...storage.error import StorageNotFoundError -from ...storage.in_memory import InMemoryStorage -from ...protocols.routing.v1_0.manager import RoutingManager -from ...protocols.routing.v1_0.models.route_record import RouteRecord from ...protocols.coordinate_mediation.v1_0.manager import ( - MediationRecord, MediationManager, + MediationRecord, ) -from ...wallet.key_type import KeyType -from ...wallet.did_method import DIDMethod +from ...protocols.coordinate_mediation.v1_0.route_manager import RouteManager +from ...protocols.routing.v1_0.manager import RoutingManager +from ...protocols.routing.v1_0.models.route_record import RouteRecord +from ...storage.error import StorageNotFoundError +from ...storage.in_memory import InMemoryStorage +from ...wallet.did_info import DIDInfo +from ...wallet.did_method import SOV +from ...wallet.in_memory import InMemoryWallet +from ...wallet.key_type import ED25519 +from ...wallet.models.wallet_record import WalletRecord from ..base import BaseMultitenantManager, MultitenantManagerError from ..error import WalletKeyMissingError +class MockMultitenantManager(BaseMultitenantManager): + async def get_wallet_profile( + self, + base_context, + wallet_record: WalletRecord, + extra_settings: dict = ..., + *, + provision=False + ): + """Do nothing.""" + + async def remove_wallet_profile(self, profile): + """Do nothing.""" + + @property + def open_profiles(self): + """Do nothing.""" + + class TestBaseMultitenantManager(AsyncTestCase): async def setUp(self): self.profile = InMemoryProfile.test_profile() @@ -31,11 +53,11 @@ async def setUp(self): self.responder = async_mock.CoroutineMock(send=async_mock.CoroutineMock()) self.context.injector.bind_instance(BaseResponder, self.responder) - self.manager = BaseMultitenantManager(self.profile) + self.manager = MockMultitenantManager(self.profile) async def test_init_throws_no_profile(self): with self.assertRaises(MultitenantManagerError): - BaseMultitenantManager(None) + MockMultitenantManager(None) async def test_get_default_mediator(self): with async_mock.patch.object( @@ -158,7 +180,7 @@ async def test_get_wallet_by_key(self): async def test_create_wallet_removes_key_only_unmanaged_mode(self): with async_mock.patch.object( - BaseMultitenantManager, "get_wallet_profile" + self.manager, "get_wallet_profile" ) as get_wallet_profile: get_wallet_profile.return_value = InMemoryProfile.test_profile() @@ -174,7 +196,7 @@ async def test_create_wallet_removes_key_only_unmanaged_mode(self): async def test_create_wallet_fails_if_wallet_name_exists(self): with async_mock.patch.object( - BaseMultitenantManager, "_wallet_name_exists" + self.manager, "_wallet_name_exists" ) as _wallet_name_exists: _wallet_name_exists.return_value = True @@ -187,14 +209,15 @@ async def test_create_wallet_fails_if_wallet_name_exists(self): ) async def test_create_wallet_saves_wallet_record_creates_profile(self): + mock_route_manager = async_mock.MagicMock() + mock_route_manager.route_verkey = async_mock.CoroutineMock() + self.context.injector.bind_instance(RouteManager, mock_route_manager) with async_mock.patch.object( WalletRecord, "save" ) as wallet_record_save, async_mock.patch.object( - BaseMultitenantManager, "get_wallet_profile" - ) as get_wallet_profile, async_mock.patch.object( - BaseMultitenantManager, "add_key" - ) as add_key: + self.manager, "get_wallet_profile" + ) as get_wallet_profile: get_wallet_profile.return_value = InMemoryProfile.test_profile() wallet_record = await self.manager.create_wallet( @@ -209,7 +232,7 @@ async def test_create_wallet_saves_wallet_record_creates_profile(self): {"wallet.key": "test_key"}, provision=True, ) - add_key.assert_not_called() + mock_route_manager.route_verkey.assert_not_called() assert isinstance(wallet_record, WalletRecord) assert wallet_record.wallet_name == "test_wallet" assert wallet_record.key_management_mode == WalletRecord.MODE_MANAGED @@ -220,20 +243,23 @@ async def test_create_wallet_adds_wallet_route(self): did="public-did", verkey="test_verkey", metadata={"meta": "data"}, - method=DIDMethod.SOV, - key_type=KeyType.ED25519, + method=SOV, + key_type=ED25519, ) + mock_route_manager = async_mock.MagicMock() + mock_route_manager.route_verkey = async_mock.CoroutineMock() + with async_mock.patch.object( WalletRecord, "save" ) as wallet_record_save, async_mock.patch.object( - BaseMultitenantManager, "get_wallet_profile" + self.manager, "get_wallet_profile" ) as get_wallet_profile, async_mock.patch.object( - BaseMultitenantManager, "add_key" - ) as add_key, async_mock.patch.object( InMemoryWallet, "get_public_did" ) as get_public_did: - get_wallet_profile.return_value = InMemoryProfile.test_profile() + get_wallet_profile.return_value = InMemoryProfile.test_profile( + bind={RouteManager: mock_route_manager} + ) get_public_did.return_value = did_info wallet_record = await self.manager.create_wallet( @@ -241,8 +267,8 @@ async def test_create_wallet_adds_wallet_route(self): WalletRecord.MODE_MANAGED, ) - add_key.assert_called_once_with( - wallet_record.wallet_id, did_info.verkey, skip_if_exists=True + mock_route_manager.route_verkey.assert_called_once_with( + get_wallet_profile.return_value, did_info.verkey ) wallet_record_save.assert_called_once() @@ -257,15 +283,13 @@ async def test_create_wallet_adds_wallet_route(self): assert wallet_record.key_management_mode == WalletRecord.MODE_MANAGED assert wallet_record.wallet_key == "test_key" - async def test_update_wallet_update_wallet_profile(self): + async def test_update_wallet(self): with async_mock.patch.object( WalletRecord, "retrieve_by_id" ) as retrieve_by_id, async_mock.patch.object( WalletRecord, "save" ) as wallet_record_save: wallet_id = "test-wallet-id" - wallet_profile = InMemoryProfile.test_profile() - self.manager._instances["test-wallet-id"] = wallet_profile retrieve_by_id.return_value = WalletRecord( wallet_id=wallet_id, settings={ @@ -285,10 +309,6 @@ async def test_update_wallet_update_wallet_profile(self): assert isinstance(wallet_record, WalletRecord) assert wallet_record.wallet_webhook_urls == ["new-webhook-url"] assert wallet_record.wallet_dispatch_type == "default" - assert wallet_profile.settings.get("wallet.webhook_urls") == [ - "new-webhook-url" - ] - assert wallet_profile.settings.get("wallet.dispatch_type") == "default" async def test_remove_wallet_fails_no_wallet_key_but_required(self): with async_mock.patch.object(WalletRecord, "retrieve_by_id") as retrieve_by_id: @@ -305,9 +325,9 @@ async def test_remove_wallet_removes_profile_wallet_storage_records(self): with async_mock.patch.object( WalletRecord, "retrieve_by_id" ) as retrieve_by_id, async_mock.patch.object( - BaseMultitenantManager, "get_wallet_profile" + self.manager, "get_wallet_profile" ) as get_wallet_profile, async_mock.patch.object( - BaseMultitenantManager, "remove_wallet_profile" + self.manager, "remove_wallet_profile" ) as remove_wallet_profile, async_mock.patch.object( WalletRecord, "delete_record" ) as wallet_delete_record, async_mock.patch.object( @@ -334,73 +354,6 @@ async def test_remove_wallet_removes_profile_wallet_storage_records(self): RouteRecord.RECORD_TYPE, {"wallet_id": "test"} ) - async def test_add_key_no_mediation(self): - with async_mock.patch.object( - RoutingManager, "create_route_record" - ) as create_route_record, async_mock.patch.object( - MediationManager, "add_key" - ) as mediation_add_key: - await self.manager.add_key("wallet_id", "recipient_key") - - create_route_record.assert_called_once_with( - recipient_key="recipient_key", internal_wallet_id="wallet_id" - ) - mediation_add_key.assert_not_called() - - async def test_add_key_skip_if_exists_does_not_exist(self): - with async_mock.patch.object( - RoutingManager, "create_route_record" - ) as create_route_record, async_mock.patch.object( - RouteRecord, "retrieve_by_recipient_key" - ) as retrieve_by_recipient_key: - retrieve_by_recipient_key.side_effect = StorageNotFoundError() - - await self.manager.add_key( - "wallet_id", "recipient_key", skip_if_exists=True - ) - - create_route_record.assert_called_once_with( - recipient_key="recipient_key", internal_wallet_id="wallet_id" - ) - - async def test_add_key_skip_if_exists_does_exist(self): - with async_mock.patch.object( - RoutingManager, "create_route_record" - ) as create_route_record, async_mock.patch.object( - RouteRecord, "retrieve_by_recipient_key" - ) as retrieve_by_recipient_key: - await self.manager.add_key( - "wallet_id", "recipient_key", skip_if_exists=True - ) - - create_route_record.assert_not_called() - - async def test_add_key_mediation(self): - with async_mock.patch.object( - RoutingManager, "create_route_record" - ) as create_route_record, async_mock.patch.object( - MediationManager, "get_default_mediator" - ) as get_default_mediator, async_mock.patch.object( - MediationManager, "add_key" - ) as mediation_add_key: - default_mediator = async_mock.CoroutineMock() - keylist_updates = async_mock.CoroutineMock() - - get_default_mediator.return_value = default_mediator - mediation_add_key.return_value = keylist_updates - - await self.manager.add_key("wallet_id", "recipient_key") - - create_route_record.assert_called_once_with( - recipient_key="recipient_key", internal_wallet_id="wallet_id" - ) - - get_default_mediator.assert_called_once() - mediation_add_key.assert_called_once_with("recipient_key") - self.responder.send.assert_called_once_with( - keylist_updates, connection_id=default_mediator.connection_id - ) - async def test_create_auth_token_fails_no_wallet_key_but_required(self): self.profile.settings["multitenant.jwt_secret"] = "very_secret_jwt" wallet_record = WalletRecord( @@ -414,41 +367,119 @@ async def test_create_auth_token_fails_no_wallet_key_but_required(self): async def test_create_auth_token_managed(self): self.profile.settings["multitenant.jwt_secret"] = "very_secret_jwt" - wallet_record = WalletRecord( + wallet_record = async_mock.MagicMock( wallet_id="test_wallet", key_management_mode=WalletRecord.MODE_MANAGED, + requires_external_key=False, settings={}, + save=async_mock.CoroutineMock(), ) + utc_now = datetime(2020, 1, 1, 0, 0, 0) + iat = int(round(utc_now.timestamp())) + expected_token = jwt.encode( - {"wallet_id": wallet_record.wallet_id}, "very_secret_jwt" - ).decode() + {"wallet_id": wallet_record.wallet_id, "iat": iat}, "very_secret_jwt" + ) - token = self.manager.create_auth_token(wallet_record) + with async_mock.patch.object(test_module, "datetime") as mock_datetime: + mock_datetime.utcnow.return_value = utc_now + token = await self.manager.create_auth_token(wallet_record) + assert wallet_record.jwt_iat == iat assert expected_token == token async def test_create_auth_token_unmanaged(self): self.profile.settings["multitenant.jwt_secret"] = "very_secret_jwt" - wallet_record = WalletRecord( + wallet_record = async_mock.MagicMock( wallet_id="test_wallet", key_management_mode=WalletRecord.MODE_UNMANAGED, + requires_external_key=True, settings={"wallet.type": "indy"}, + save=async_mock.CoroutineMock(), ) + utc_now = datetime(2020, 1, 1, 0, 0, 0) + iat = int(round(utc_now.timestamp())) + expected_token = jwt.encode( - {"wallet_id": wallet_record.wallet_id, "wallet_key": "test_key"}, + { + "wallet_id": wallet_record.wallet_id, + "iat": iat, + "wallet_key": "test_key", + }, "very_secret_jwt", - ).decode() + ) - token = self.manager.create_auth_token(wallet_record, "test_key") + with async_mock.patch.object(test_module, "datetime") as mock_datetime: + mock_datetime.utcnow.return_value = utc_now + token = await self.manager.create_auth_token(wallet_record, "test_key") + assert wallet_record.jwt_iat == iat assert expected_token == token + async def test_get_wallet_details_from_token(self): + self.profile.settings["multitenant.jwt_secret"] = "very_secret_jwt" + wallet_record = WalletRecord( + key_management_mode=WalletRecord.MODE_MANAGED, + settings={"wallet.type": "indy", "wallet.key": "wallet_key"}, + jwt_iat=100, + ) + session = await self.profile.session() + await wallet_record.save(session) + token = jwt.encode( + {"wallet_id": wallet_record.wallet_id, "iat": 100}, + "very_secret_jwt", + algorithm="HS256", + ) + ret_wallet_id, ret_wallet_key = self.manager.get_wallet_details_from_token( + token + ) + assert ret_wallet_id == wallet_record.wallet_id + assert not ret_wallet_key + + token = jwt.encode( + { + "wallet_id": wallet_record.wallet_id, + "iat": 100, + "wallet_key": "wallet_key", + }, + "very_secret_jwt", + algorithm="HS256", + ) + ret_wallet_id, ret_wallet_key = self.manager.get_wallet_details_from_token( + token + ) + assert ret_wallet_id == wallet_record.wallet_id + assert ret_wallet_key == "wallet_key" + + async def test_get_wallet_and_profile(self): + self.profile.settings["multitenant.jwt_secret"] = "very_secret_jwt" + wallet_record = WalletRecord( + key_management_mode=WalletRecord.MODE_MANAGED, + settings={"wallet.type": "indy", "wallet.key": "wallet_key"}, + jwt_iat=100, + ) + + session = await self.profile.session() + await wallet_record.save(session) + + with async_mock.patch.object( + self.manager, "get_wallet_profile" + ) as get_wallet_profile: + mock_profile = InMemoryProfile.test_profile() + get_wallet_profile.return_value = mock_profile + + wallet, profile = await self.manager.get_wallet_and_profile( + self.profile.context, wallet_record.wallet_id, "wallet_key" + ) + assert wallet == wallet_record + assert profile == mock_profile + async def test_get_profile_for_token_invalid_token_raises(self): self.profile.settings["multitenant.jwt_secret"] = "very_secret_jwt" - token = jwt.encode({"wallet_id": "test"}, "some_random_key").decode() + token = jwt.encode({"wallet_id": "test"}, "some_random_key") with self.assertRaises(jwt.InvalidTokenError): await self.manager.get_profile_for_token(self.profile.context, token) @@ -463,12 +494,12 @@ async def test_get_profile_for_token_wallet_key_missing_raises(self): await wallet_record.save(session) token = jwt.encode( {"wallet_id": wallet_record.wallet_id}, "very_secret_jwt", algorithm="HS256" - ).decode() + ) with self.assertRaises(WalletKeyMissingError): await self.manager.get_profile_for_token(self.profile.context, token) - async def test_get_profile_for_token_managed_wallet(self): + async def test_get_profile_for_token_managed_wallet_no_iat(self): self.profile.settings["multitenant.jwt_secret"] = "very_secret_jwt" wallet_record = WalletRecord( key_management_mode=WalletRecord.MODE_MANAGED, @@ -480,10 +511,47 @@ async def test_get_profile_for_token_managed_wallet(self): token = jwt.encode( {"wallet_id": wallet_record.wallet_id}, "very_secret_jwt", algorithm="HS256" - ).decode() + ) + + with async_mock.patch.object( + self.manager, "get_wallet_profile" + ) as get_wallet_profile: + mock_profile = InMemoryProfile.test_profile() + get_wallet_profile.return_value = mock_profile + + profile = await self.manager.get_profile_for_token( + self.profile.context, token + ) + + get_wallet_profile.assert_called_once_with( + self.profile.context, + wallet_record, + {}, + ) + + assert profile == mock_profile + + async def test_get_profile_for_token_managed_wallet_iat(self): + iat = 100 + + self.profile.settings["multitenant.jwt_secret"] = "very_secret_jwt" + wallet_record = WalletRecord( + key_management_mode=WalletRecord.MODE_MANAGED, + settings={"wallet.type": "indy", "wallet.key": "wallet_key"}, + jwt_iat=iat, + ) + + session = await self.profile.session() + await wallet_record.save(session) + + token = jwt.encode( + {"wallet_id": wallet_record.wallet_id, "iat": iat}, + "very_secret_jwt", + algorithm="HS256", + ) with async_mock.patch.object( - BaseMultitenantManager, "get_wallet_profile" + self.manager, "get_wallet_profile" ) as get_wallet_profile: mock_profile = InMemoryProfile.test_profile() get_wallet_profile.return_value = mock_profile @@ -500,6 +568,46 @@ async def test_get_profile_for_token_managed_wallet(self): assert profile == mock_profile + async def test_get_profile_for_token_managed_wallet_x_iat_no_match(self): + iat = 100 + + self.profile.settings["multitenant.jwt_secret"] = "very_secret_jwt" + wallet_record = WalletRecord( + key_management_mode=WalletRecord.MODE_MANAGED, + settings={"wallet.type": "indy", "wallet.key": "wallet_key"}, + jwt_iat=iat, + ) + + session = await self.profile.session() + await wallet_record.save(session) + + token = jwt.encode( + # Change iat from record value + {"wallet_id": wallet_record.wallet_id, "iat": 200}, + "very_secret_jwt", + algorithm="HS256", + ) + + with async_mock.patch.object( + self.manager, "get_wallet_profile" + ) as get_wallet_profile, self.assertRaises( + MultitenantManagerError, msg="Token not valid" + ): + mock_profile = InMemoryProfile.test_profile() + get_wallet_profile.return_value = mock_profile + + profile = await self.manager.get_profile_for_token( + self.profile.context, token + ) + + get_wallet_profile.assert_called_once_with( + self.profile.context, + wallet_record, + {}, + ) + + assert profile == mock_profile + async def test_get_profile_for_token_unmanaged_wallet(self): self.profile.settings["multitenant.jwt_secret"] = "very_secret_jwt" wallet_record = WalletRecord( @@ -514,10 +622,10 @@ async def test_get_profile_for_token_unmanaged_wallet(self): {"wallet_id": wallet_record.wallet_id, "wallet_key": "wallet_key"}, "very_secret_jwt", algorithm="HS256", - ).decode() + ) with async_mock.patch.object( - BaseMultitenantManager, "get_wallet_profile" + self.manager, "get_wallet_profile" ) as get_wallet_profile: mock_profile = InMemoryProfile.test_profile() get_wallet_profile.return_value = mock_profile @@ -557,7 +665,7 @@ async def test_get_wallets_by_message(self): ] with async_mock.patch.object( - BaseMultitenantManager, "_get_wallet_by_key" + self.manager, "_get_wallet_by_key" ) as get_wallet_by_key: get_wallet_by_key.side_effect = return_wallets @@ -569,3 +677,18 @@ async def test_get_wallets_by_message(self): assert wallets[0] == return_wallets[0] assert wallets[1] == return_wallets[3] assert get_wallet_by_key.call_count == 4 + + async def test_get_profile_for_key(self): + mock_wallet = async_mock.MagicMock() + mock_wallet.requires_external_key = False + with async_mock.patch.object( + self.manager, + "_get_wallet_by_key", + async_mock.CoroutineMock(return_value=mock_wallet), + ), async_mock.patch.object( + self.manager, "get_wallet_profile", async_mock.CoroutineMock() + ) as mock_get_wallet_profile: + profile = await self.manager.get_profile_for_key( + self.context, "test-verkey" + ) + assert profile == mock_get_wallet_profile.return_value diff --git a/aries_cloudagent/multitenant/tests/test_cache.py b/aries_cloudagent/multitenant/tests/test_cache.py new file mode 100644 index 0000000000..ae7dbcc303 --- /dev/null +++ b/aries_cloudagent/multitenant/tests/test_cache.py @@ -0,0 +1,107 @@ +from ...core.profile import Profile + +from ..cache import ProfileCache + + +class MockProfile(Profile): + def session(self, context=None): + ... + + def transaction(self, context=None): + ... + + +def test_get_not_in_cache(): + cache = ProfileCache(1) + + assert cache.get("1") is None + + +def test_put_get_in_cache(): + cache = ProfileCache(1) + + profile = MockProfile() + cache.put("1", profile) + + assert cache.get("1") is profile + + +def test_remove(): + cache = ProfileCache(1) + + profile = MockProfile() + cache.put("1", profile) + + assert cache.get("1") is profile + + cache.remove("1") + + assert cache.get("1") is None + + +def test_has_true(): + cache = ProfileCache(1) + + profile = MockProfile() + + assert cache.has("1") is False + cache.put("1", profile) + assert cache.has("1") is True + + +def test_cleanup(): + cache = ProfileCache(1) + + cache.put("1", MockProfile()) + + assert len(cache.profiles) == 1 + + cache.put("2", MockProfile()) + + assert len(cache.profiles) == 1 + assert cache.get("1") == None + + +def test_cleanup_lru(): + cache = ProfileCache(3) + + cache.put("1", MockProfile()) + cache.put("2", MockProfile()) + cache.put("3", MockProfile()) + + assert len(cache.profiles) == 3 + + cache.get("1") + + cache.put("4", MockProfile()) + + assert len(cache._cache) == 3 + assert cache.get("1") + assert cache.get("2") is None + assert cache.get("3") + assert cache.get("4") + + +def test_rescue_open_profile(): + cache = ProfileCache(3) + + cache.put("1", MockProfile()) + cache.put("2", MockProfile()) + cache.put("3", MockProfile()) + + assert len(cache.profiles) == 3 + + held = cache.profiles["1"] + cache.put("4", MockProfile()) + + assert len(cache.profiles) == 4 + assert len(cache._cache) == 3 + + cache.get("1") + + assert len(cache.profiles) == 3 + assert len(cache._cache) == 3 + assert cache.get("1") + assert cache.get("2") is None + assert cache.get("3") + assert cache.get("4") diff --git a/aries_cloudagent/multitenant/tests/test_manager.py b/aries_cloudagent/multitenant/tests/test_manager.py index 7e01959f95..c851369c11 100644 --- a/aries_cloudagent/multitenant/tests/test_manager.py +++ b/aries_cloudagent/multitenant/tests/test_manager.py @@ -19,7 +19,7 @@ async def setUp(self): async def test_get_wallet_profile_returns_from_cache(self): wallet_record = WalletRecord(wallet_id="test") - self.manager._instances["test"] = InMemoryProfile.test_profile() + self.manager._profiles.put("test", InMemoryProfile.test_profile()) with async_mock.patch( "aries_cloudagent.config.wallet.wallet_config" @@ -27,12 +27,12 @@ async def test_get_wallet_profile_returns_from_cache(self): profile = await self.manager.get_wallet_profile( self.profile.context, wallet_record ) - assert profile is self.manager._instances["test"] + assert profile is self.manager._profiles.get("test") wallet_config.assert_not_called() async def test_get_wallet_profile_not_in_cache(self): wallet_record = WalletRecord(wallet_id="test", settings={}) - self.manager._instances["test"] = InMemoryProfile.test_profile() + self.manager._profiles.put("test", InMemoryProfile.test_profile()) self.profile.context.update_settings( {"admin.webhook_urls": ["http://localhost:8020"]} ) @@ -43,7 +43,7 @@ async def test_get_wallet_profile_not_in_cache(self): profile = await self.manager.get_wallet_profile( self.profile.context, wallet_record ) - assert profile is self.manager._instances["test"] + assert profile is self.manager._profiles.get("test") wallet_config.assert_not_called() async def test_get_wallet_profile_settings(self): @@ -174,13 +174,46 @@ def side_effect(context, provision): assert profile.settings.get("mediation.default_id") == "24a96ef5" assert profile.settings.get("mediation.clear") == True + async def test_update_wallet_update_wallet_profile(self): + with async_mock.patch.object( + WalletRecord, "retrieve_by_id" + ) as retrieve_by_id, async_mock.patch.object( + WalletRecord, "save" + ) as wallet_record_save: + wallet_id = "test-wallet-id" + wallet_profile = InMemoryProfile.test_profile() + self.manager._profiles.put("test-wallet-id", wallet_profile) + retrieve_by_id.return_value = WalletRecord( + wallet_id=wallet_id, + settings={ + "wallet.webhook_urls": ["test-webhook-url"], + "wallet.dispatch_type": "both", + }, + ) + + new_settings = { + "wallet.webhook_urls": ["new-webhook-url"], + "wallet.dispatch_type": "default", + } + wallet_record = await self.manager.update_wallet(wallet_id, new_settings) + + wallet_record_save.assert_called_once() + + assert isinstance(wallet_record, WalletRecord) + assert wallet_record.wallet_webhook_urls == ["new-webhook-url"] + assert wallet_record.wallet_dispatch_type == "default" + assert wallet_profile.settings.get("wallet.webhook_urls") == [ + "new-webhook-url" + ] + assert wallet_profile.settings.get("wallet.dispatch_type") == "default" + async def test_remove_wallet_profile(self): test_profile = InMemoryProfile.test_profile( settings={"wallet.id": "test"}, ) - self.manager._instances["test"] = test_profile + self.manager._profiles.put("test", test_profile) with async_mock.patch.object(InMemoryProfile, "remove") as profile_remove: await self.manager.remove_wallet_profile(test_profile) - assert "test" not in self.manager._instances + assert not self.manager._profiles.has("test") profile_remove.assert_called_once_with() diff --git a/aries_cloudagent/multitenant/tests/test_route_manager.py b/aries_cloudagent/multitenant/tests/test_route_manager.py new file mode 100644 index 0000000000..200bc62a7b --- /dev/null +++ b/aries_cloudagent/multitenant/tests/test_route_manager.py @@ -0,0 +1,385 @@ +from asynctest import mock +import pytest + +from ...core.in_memory import InMemoryProfile +from ...core.profile import Profile +from ...messaging.responder import BaseResponder, MockResponder +from ...messaging.responder import BaseResponder, MockResponder +from ...protocols.coordinate_mediation.v1_0.models.mediation_record import ( + MediationRecord, +) +from ...protocols.coordinate_mediation.v1_0.route_manager import RouteManager +from ...protocols.routing.v1_0.manager import RoutingManager +from ...protocols.routing.v1_0.models.route_record import RouteRecord +from ...storage.error import StorageNotFoundError +from ..base import BaseMultitenantManager +from ..route_manager import BaseWalletRouteManager, MultitenantRouteManager + +TEST_RECORD_VERKEY = "3Dn1SJNPaCXcvvJvSbsFWP2xaCjMom3can8CQNhWrTRx" +TEST_VERKEY = "did:key:z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL" +TEST_ROUTE_RECORD_VERKEY = "9WCgWKUaAJj3VWxxtzvvMQN3AoFxoBtBDo9ntwJnVVCC" +TEST_ROUTE_VERKEY = "did:key:z6MknxTj6Zj1VrDWc1ofaZtmCVv2zNXpD58Xup4ijDGoQhya" + + +@pytest.fixture +def wallet_id(): + yield "test-wallet-id" + + +@pytest.fixture +def mock_responder(): + yield MockResponder() + + +@pytest.fixture +def root_profile(mock_responder: MockResponder): + yield InMemoryProfile.test_profile( + bind={ + BaseResponder: mock_responder, + } + ) + + +@pytest.fixture +def sub_profile(mock_responder: MockResponder, wallet_id: str): + yield InMemoryProfile.test_profile( + settings={ + "wallet.id": wallet_id, + }, + bind={ + BaseResponder: mock_responder, + }, + ) + + +@pytest.fixture +def route_manager(root_profile: Profile, sub_profile: Profile, wallet_id: str): + yield MultitenantRouteManager(root_profile) + + +@pytest.fixture +def base_route_manager(): + yield BaseWalletRouteManager() + + +@pytest.mark.asyncio +async def test_route_for_key_sub_mediator_no_base_mediator( + route_manager: MultitenantRouteManager, + mock_responder: MockResponder, + wallet_id: str, + sub_profile: Profile, +): + mediation_record = MediationRecord( + mediation_id="test-mediation-id", connection_id="test-mediator-conn-id" + ) + + with mock.patch.object( + route_manager, "get_base_wallet_mediator", mock.CoroutineMock(return_value=None) + ), mock.patch.object( + RoutingManager, "create_route_record", mock.CoroutineMock() + ) as mock_create_route_record: + keylist_update = await route_manager._route_for_key( + sub_profile, + TEST_VERKEY, + mediation_record, + skip_if_exists=False, + replace_key=None, + ) + + mock_create_route_record.assert_called_once_with( + recipient_key=TEST_VERKEY, internal_wallet_id=wallet_id + ) + assert keylist_update + assert keylist_update.serialize()["updates"] == [ + {"action": "add", "recipient_key": TEST_VERKEY} + ] + assert mock_responder.messages + assert ( + keylist_update, + {"connection_id": "test-mediator-conn-id"}, + ) == mock_responder.messages[0] + + +@pytest.mark.asyncio +async def test_route_for_key_sub_mediator_and_base_mediator( + sub_profile: Profile, + route_manager: MultitenantRouteManager, + mock_responder: MockResponder, + wallet_id: str, +): + mediation_record = MediationRecord( + mediation_id="test-mediation-id", connection_id="test-mediator-conn-id" + ) + base_mediation_record = MediationRecord( + mediation_id="test-base-mediation-id", + connection_id="test-base-mediator-conn-id", + ) + + with mock.patch.object( + route_manager, + "get_base_wallet_mediator", + mock.CoroutineMock(return_value=base_mediation_record), + ), mock.patch.object( + RoutingManager, "create_route_record", mock.CoroutineMock() + ) as mock_create_route_record: + keylist_update = await route_manager._route_for_key( + sub_profile, + TEST_VERKEY, + mediation_record, + skip_if_exists=False, + replace_key=None, + ) + + mock_create_route_record.assert_called_once_with( + recipient_key=TEST_VERKEY, internal_wallet_id=wallet_id + ) + assert keylist_update + assert keylist_update.serialize()["updates"] == [ + {"action": "add", "recipient_key": TEST_VERKEY} + ] + assert mock_responder.messages + assert ( + keylist_update, + {"connection_id": "test-base-mediator-conn-id"}, + ) == mock_responder.messages[0] + + +@pytest.mark.asyncio +async def test_route_for_key_base_mediator_no_sub_mediator( + sub_profile: Profile, + route_manager: MultitenantRouteManager, + mock_responder: MockResponder, + wallet_id: str, +): + base_mediation_record = MediationRecord( + mediation_id="test-base-mediation-id", + connection_id="test-base-mediator-conn-id", + ) + + with mock.patch.object( + route_manager, + "get_base_wallet_mediator", + mock.CoroutineMock(return_value=base_mediation_record), + ), mock.patch.object( + RoutingManager, "create_route_record", mock.CoroutineMock() + ) as mock_create_route_record: + keylist_update = await route_manager._route_for_key( + sub_profile, + TEST_VERKEY, + None, + skip_if_exists=False, + replace_key=None, + ) + + mock_create_route_record.assert_called_once_with( + recipient_key=TEST_VERKEY, internal_wallet_id=wallet_id + ) + assert keylist_update + assert keylist_update.serialize()["updates"] == [ + {"action": "add", "recipient_key": TEST_VERKEY} + ] + assert mock_responder.messages + assert ( + keylist_update, + {"connection_id": "test-base-mediator-conn-id"}, + ) == mock_responder.messages[0] + + +@pytest.mark.asyncio +async def test_route_for_key_skip_if_exists_and_exists( + sub_profile: Profile, + route_manager: MultitenantRouteManager, + mock_responder: MockResponder, +): + mediation_record = MediationRecord( + mediation_id="test-mediation-id", connection_id="test-mediator-conn-id" + ) + with mock.patch.object( + RouteRecord, "retrieve_by_recipient_key", mock.CoroutineMock() + ): + keylist_update = await route_manager._route_for_key( + sub_profile, + TEST_VERKEY, + mediation_record, + skip_if_exists=True, + replace_key=None, + ) + assert keylist_update is None + assert not mock_responder.messages + + +@pytest.mark.asyncio +async def test_route_for_key_skip_if_exists_and_absent( + sub_profile: Profile, + route_manager: MultitenantRouteManager, + mock_responder: MockResponder, +): + mediation_record = MediationRecord( + mediation_id="test-mediation-id", connection_id="test-mediator-conn-id" + ) + with mock.patch.object( + RouteRecord, + "retrieve_by_recipient_key", + mock.CoroutineMock(side_effect=StorageNotFoundError), + ): + keylist_update = await route_manager._route_for_key( + sub_profile, + TEST_VERKEY, + mediation_record, + skip_if_exists=True, + replace_key=None, + ) + assert keylist_update + assert keylist_update.serialize()["updates"] == [ + {"action": "add", "recipient_key": TEST_VERKEY} + ] + assert mock_responder.messages + assert ( + keylist_update, + {"connection_id": "test-mediator-conn-id"}, + ) == mock_responder.messages[0] + + +@pytest.mark.asyncio +async def test_route_for_key_replace_key( + sub_profile: Profile, + route_manager: MultitenantRouteManager, + mock_responder: MockResponder, +): + mediation_record = MediationRecord( + mediation_id="test-mediation-id", connection_id="test-mediator-conn-id" + ) + keylist_update = await route_manager._route_for_key( + sub_profile, + TEST_VERKEY, + mediation_record, + skip_if_exists=False, + replace_key=TEST_ROUTE_VERKEY, + ) + assert keylist_update + assert keylist_update.serialize()["updates"] == [ + {"action": "add", "recipient_key": TEST_VERKEY}, + {"action": "remove", "recipient_key": TEST_ROUTE_VERKEY}, + ] + assert mock_responder.messages + assert ( + keylist_update, + {"connection_id": "test-mediator-conn-id"}, + ) == mock_responder.messages[0] + + +@pytest.mark.asyncio +async def test_route_for_key_no_mediator( + sub_profile: Profile, + route_manager: MultitenantRouteManager, +): + assert ( + await route_manager._route_for_key( + sub_profile, + TEST_VERKEY, + None, + skip_if_exists=True, + replace_key=TEST_ROUTE_VERKEY, + ) + is None + ) + + +@pytest.mark.asyncio +async def test_routing_info_with_mediator( + sub_profile: Profile, + route_manager: MultitenantRouteManager, +): + mediation_record = MediationRecord( + mediation_id="test-mediation-id", + connection_id="test-mediator-conn-id", + routing_keys=["test-key-0", "test-key-1"], + endpoint="http://mediator.example.com", + ) + keys, endpoint = await route_manager.routing_info( + sub_profile, "http://example.com", mediation_record + ) + assert keys == mediation_record.routing_keys + assert endpoint == mediation_record.endpoint + + +@pytest.mark.asyncio +async def test_routing_info_no_mediator( + sub_profile: Profile, + route_manager: MultitenantRouteManager, +): + keys, endpoint = await route_manager.routing_info( + sub_profile, "http://example.com", None + ) + assert keys == [] + assert endpoint == "http://example.com" + + +@pytest.mark.asyncio +async def test_routing_info_with_base_mediator( + sub_profile: Profile, + route_manager: MultitenantRouteManager, +): + base_mediation_record = MediationRecord( + mediation_id="test-base-mediation-id", + connection_id="test-base-mediator-conn-id", + routing_keys=["test-key-0", "test-key-1"], + endpoint="http://base.mediator.example.com", + ) + + with mock.patch.object( + route_manager, + "get_base_wallet_mediator", + mock.CoroutineMock(return_value=base_mediation_record), + ): + keys, endpoint = await route_manager.routing_info( + sub_profile, "http://example.com", None + ) + assert keys == base_mediation_record.routing_keys + assert endpoint == base_mediation_record.endpoint + + +@pytest.mark.asyncio +async def test_routing_info_with_base_mediator_and_sub_mediator( + sub_profile: Profile, + route_manager: MultitenantRouteManager, +): + mediation_record = MediationRecord( + mediation_id="test-mediation-id", + connection_id="test-mediator-conn-id", + routing_keys=["test-key-0", "test-key-1"], + endpoint="http://mediator.example.com", + ) + base_mediation_record = MediationRecord( + mediation_id="test-base-mediation-id", + connection_id="test-base-mediator-conn-id", + routing_keys=["test-base-key-0", "test-base-key-1"], + endpoint="http://base.mediator.example.com", + ) + + with mock.patch.object( + route_manager, + "get_base_wallet_mediator", + mock.CoroutineMock(return_value=base_mediation_record), + ): + keys, endpoint = await route_manager.routing_info( + sub_profile, "http://example.com", mediation_record + ) + assert keys == [*base_mediation_record.routing_keys, *mediation_record.routing_keys] + assert endpoint == mediation_record.endpoint + + +@pytest.mark.asyncio +async def test_connection_from_recipient_key( + sub_profile: Profile, base_route_manager: BaseWalletRouteManager +): + manager = mock.MagicMock() + manager.get_profile_for_key = mock.CoroutineMock(return_value=sub_profile) + sub_profile.context.injector.bind_instance(BaseMultitenantManager, manager) + with mock.patch.object( + RouteManager, "connection_from_recipient_key", mock.CoroutineMock() + ) as mock_conn_for_recip: + result = await base_route_manager.connection_from_recipient_key( + sub_profile, TEST_VERKEY + ) + assert result == mock_conn_for_recip.return_value diff --git a/aries_cloudagent/protocols/actionmenu/v1_0/tests/test_routes.py b/aries_cloudagent/protocols/actionmenu/v1_0/tests/test_routes.py index 48148e5ff9..f766d9a77f 100644 --- a/aries_cloudagent/protocols/actionmenu/v1_0/tests/test_routes.py +++ b/aries_cloudagent/protocols/actionmenu/v1_0/tests/test_routes.py @@ -78,7 +78,6 @@ async def test_actionmenu_perform(self): ) as mock_perform, async_mock.patch.object( test_module.web, "json_response" ) as mock_response: - mock_conn_record.retrieve_by_id = async_mock.CoroutineMock() res = await test_module.actionmenu_perform(self.request) @@ -97,7 +96,6 @@ async def test_actionmenu_perform_no_conn_record(self): ) as mock_conn_record, async_mock.patch.object( test_module, "Perform", autospec=True ) as mock_perform: - # Emulate storage not found (bad connection id) mock_conn_record.retrieve_by_id = async_mock.CoroutineMock( side_effect=StorageNotFoundError @@ -115,7 +113,6 @@ async def test_actionmenu_perform_conn_not_ready(self): ) as mock_conn_record, async_mock.patch.object( test_module, "Perform", autospec=True ) as mock_perform: - # Emulate connection not ready mock_conn_record.retrieve_by_id = async_mock.CoroutineMock() mock_conn_record.retrieve_by_id.return_value.is_ready = False @@ -134,7 +131,6 @@ async def test_actionmenu_request(self): ) as menu_request, async_mock.patch.object( test_module.web, "json_response" ) as mock_response: - mock_conn_record.retrieve_by_id = async_mock.CoroutineMock() res = await test_module.actionmenu_request(self.request) @@ -153,7 +149,6 @@ async def test_actionmenu_request_no_conn_record(self): ) as mock_conn_record, async_mock.patch.object( test_module, "Perform", autospec=True ) as mock_perform: - # Emulate storage not found (bad connection id) mock_conn_record.retrieve_by_id = async_mock.CoroutineMock( side_effect=StorageNotFoundError @@ -171,7 +166,6 @@ async def test_actionmenu_request_conn_not_ready(self): ) as mock_conn_record, async_mock.patch.object( test_module, "Perform", autospec=True ) as mock_perform: - # Emulate connection not ready mock_conn_record.retrieve_by_id = async_mock.CoroutineMock() mock_conn_record.retrieve_by_id.return_value.is_ready = False @@ -190,7 +184,6 @@ async def test_actionmenu_send(self): ) as mock_menu, async_mock.patch.object( test_module.web, "json_response" ) as mock_response: - mock_conn_record.retrieve_by_id = async_mock.CoroutineMock() mock_menu.deserialize = async_mock.MagicMock() @@ -210,7 +203,6 @@ async def test_actionmenu_send_deserialize_x(self): ) as mock_conn_record, async_mock.patch.object( test_module, "Menu", autospec=True ) as mock_menu: - mock_conn_record.retrieve_by_id = async_mock.CoroutineMock() mock_menu.deserialize = async_mock.MagicMock( side_effect=test_module.BaseModelError("cannot deserialize") @@ -228,7 +220,6 @@ async def test_actionmenu_send_no_conn_record(self): ) as mock_conn_record, async_mock.patch.object( test_module, "Menu", autospec=True ) as mock_menu: - mock_menu.deserialize = async_mock.MagicMock() # Emulate storage not found (bad connection id) @@ -248,7 +239,6 @@ async def test_actionmenu_send_conn_not_ready(self): ) as mock_conn_record, async_mock.patch.object( test_module, "Menu", autospec=True ) as mock_menu: - mock_menu.deserialize = async_mock.MagicMock() # Emulate connection not ready diff --git a/aries_cloudagent/protocols/actionmenu/v1_0/tests/test_service.py b/aries_cloudagent/protocols/actionmenu/v1_0/tests/test_service.py index 8a94c08361..4573997115 100644 --- a/aries_cloudagent/protocols/actionmenu/v1_0/tests/test_service.py +++ b/aries_cloudagent/protocols/actionmenu/v1_0/tests/test_service.py @@ -17,8 +17,8 @@ async def test_get_active_menu(self): mock_event_bus = MockEventBus() self.context.profile.context.injector.bind_instance(EventBus, mock_event_bus) - self.menu_service = await ( - test_module.DriverMenuService.service_handler()(self.context) + self.menu_service = await test_module.DriverMenuService.service_handler()( + self.context ) connection = async_mock.MagicMock() @@ -41,8 +41,8 @@ async def test_perform_menu_action(self): mock_event_bus = MockEventBus() self.context.profile.context.injector.bind_instance(EventBus, mock_event_bus) - self.menu_service = await ( - test_module.DriverMenuService.service_handler()(self.context) + self.menu_service = await test_module.DriverMenuService.service_handler()( + self.context ) action_name = "action" diff --git a/aries_cloudagent/protocols/basicmessage/v1_0/tests/test_routes.py b/aries_cloudagent/protocols/basicmessage/v1_0/tests/test_routes.py index 6265cbd762..ca730021b0 100644 --- a/aries_cloudagent/protocols/basicmessage/v1_0/tests/test_routes.py +++ b/aries_cloudagent/protocols/basicmessage/v1_0/tests/test_routes.py @@ -34,7 +34,6 @@ async def test_connections_send_message(self): ) as mock_basic_message, async_mock.patch.object( test_module.web, "json_response" ) as mock_response: - mock_connection_record.retrieve_by_id = async_mock.CoroutineMock() res = await test_module.connections_send_message(self.request) @@ -50,7 +49,6 @@ async def test_connections_send_message_no_conn_record(self): ) as mock_connection_record, async_mock.patch.object( test_module, "BasicMessage", autospec=True ) as mock_basic_message: - # Emulate storage not found (bad connection id) mock_connection_record.retrieve_by_id = async_mock.CoroutineMock( side_effect=StorageNotFoundError @@ -68,7 +66,6 @@ async def test_connections_send_message_not_ready(self): ) as mock_connection_record, async_mock.patch.object( test_module, "BasicMessage", autospec=True ) as mock_basic_message: - # Emulate connection not ready mock_connection_record.retrieve_by_id = async_mock.CoroutineMock() mock_connection_record.retrieve_by_id.return_value.is_ready = False diff --git a/aries_cloudagent/protocols/connections/v1_0/handlers/connection_request_handler.py b/aries_cloudagent/protocols/connections/v1_0/handlers/connection_request_handler.py index b1f26a85ae..82eb79cf81 100644 --- a/aries_cloudagent/protocols/connections/v1_0/handlers/connection_request_handler.py +++ b/aries_cloudagent/protocols/connections/v1_0/handlers/connection_request_handler.py @@ -1,12 +1,8 @@ """Connection request handler.""" -from .....messaging.base_handler import ( - BaseHandler, - BaseResponder, - RequestContext, -) from .....connections.models.conn_record import ConnRecord - +from .....messaging.base_handler import BaseHandler, BaseResponder, RequestContext +from ....coordinate_mediation.v1_0.manager import MediationManager from ..manager import ConnectionManager, ConnectionManagerError from ..messages.connection_request import ConnectionRequest from ..messages.problem_report import ConnectionProblemReport @@ -30,23 +26,24 @@ async def handle(self, context: RequestContext, responder: BaseResponder): profile = context.profile mgr = ConnectionManager(profile) + mediation_id = None if context.connection_record: async with profile.session() as session: mediation_metadata = await context.connection_record.metadata_get( - session, "mediation", {} + session, MediationManager.METADATA_KEY, {} ) - else: - mediation_metadata = {} + mediation_id = mediation_metadata.get(MediationManager.METADATA_ID) try: connection = await mgr.receive_request( context.message, context.message_receipt, - mediation_id=mediation_metadata.get("id"), ) if connection.accept == ConnRecord.ACCEPT_AUTO: - response = await mgr.create_response(connection) + response = await mgr.create_response( + connection, mediation_id=mediation_id + ) await responder.send_reply( response, connection_id=connection.connection_id ) diff --git a/aries_cloudagent/protocols/connections/v1_0/handlers/tests/test_request_handler.py b/aries_cloudagent/protocols/connections/v1_0/handlers/tests/test_request_handler.py index ec16a72cef..7fa1d3ab31 100644 --- a/aries_cloudagent/protocols/connections/v1_0/handlers/tests/test_request_handler.py +++ b/aries_cloudagent/protocols/connections/v1_0/handlers/tests/test_request_handler.py @@ -83,7 +83,7 @@ async def test_called(self, mock_conn_mgr, request_context): responder = MockResponder() await handler_inst.handle(request_context, responder) mock_conn_mgr.return_value.receive_request.assert_called_once_with( - request_context.message, request_context.message_receipt, mediation_id=None + request_context.message, request_context.message_receipt ) assert not responder.messages @@ -101,31 +101,38 @@ async def test_called_with_auto_response(self, mock_conn_mgr, request_context): responder = MockResponder() await handler_inst.handle(request_context, responder) mock_conn_mgr.return_value.receive_request.assert_called_once_with( - request_context.message, request_context.message_receipt, mediation_id=None + request_context.message, request_context.message_receipt + ) + mock_conn_mgr.return_value.create_response.assert_called_once_with( + mock_conn_rec, mediation_id=None ) assert responder.messages @pytest.mark.asyncio @async_mock.patch.object(handler, "ConnectionManager") - async def test_connection_record_with_mediation_metadata( + async def test_connection_record_with_mediation_metadata_auto_response( self, mock_conn_mgr, request_context, connection_record ): - mock_conn_mgr.return_value.receive_request = async_mock.CoroutineMock() + mock_conn_rec = async_mock.MagicMock() + mock_conn_rec.accept = ConnRecord.ACCEPT_AUTO + mock_conn_mgr.return_value.receive_request = async_mock.CoroutineMock( + return_value=mock_conn_rec + ) + mock_conn_mgr.return_value.create_response = async_mock.CoroutineMock() request_context.message = ConnectionRequest() with async_mock.patch.object( connection_record, "metadata_get", async_mock.CoroutineMock(return_value={"id": "test-mediation-id"}), - ) as mock_metadata_get: + ): handler_inst = handler.ConnectionRequestHandler() responder = MockResponder() await handler_inst.handle(request_context, responder) - mock_conn_mgr.return_value.receive_request.assert_called_once_with( - request_context.message, - request_context.message_receipt, - mediation_id="test-mediation-id", + mock_conn_mgr.return_value.receive_request.assert_called_once() + mock_conn_mgr.return_value.create_response.assert_called_once_with( + mock_conn_rec, mediation_id="test-mediation-id" ) - assert not responder.messages + assert responder.messages @pytest.mark.asyncio @async_mock.patch.object(handler, "ConnectionManager") @@ -146,7 +153,6 @@ async def test_connection_record_without_mediation_metadata( mock_conn_mgr.return_value.receive_request.assert_called_once_with( request_context.message, request_context.message_receipt, - mediation_id=None, ) assert not responder.messages diff --git a/aries_cloudagent/protocols/connections/v1_0/manager.py b/aries_cloudagent/protocols/connections/v1_0/manager.py index 96e72b8b37..a1514388f3 100644 --- a/aries_cloudagent/protocols/connections/v1_0/manager.py +++ b/aries_cloudagent/protocols/connections/v1_0/manager.py @@ -1,15 +1,15 @@ """Classes to manage connections.""" import logging +from typing import Coroutine, Optional, Sequence, Tuple, cast -from typing import Coroutine, Sequence, Tuple +from ....core.oob_processor import OobMessageProcessor from ....cache.base import BaseCache from ....config.base import InjectionError from ....connections.base_manager import BaseConnectionManager from ....connections.models.conn_record import ConnRecord from ....connections.models.connection_target import ConnectionTarget -from ....connections.util import mediation_record_if_id from ....core.error import BaseError from ....core.profile import Profile from ....messaging.responder import BaseResponder @@ -17,18 +17,15 @@ from ....storage.error import StorageError, StorageNotFoundError from ....transport.inbound.receipt import MessageReceipt from ....wallet.base import BaseWallet -from ....wallet.did_info import DIDInfo from ....wallet.crypto import create_keypair, seed_to_did -from ....wallet.key_type import KeyType -from ....wallet.did_method import DIDMethod +from ....wallet.did_info import DIDInfo +from ....wallet.did_method import SOV from ....wallet.error import WalletNotFoundError +from ....wallet.key_type import ED25519 from ....wallet.util import bytes_to_b58 -from ...routing.v1_0.manager import RoutingManager from ...coordinate_mediation.v1_0.manager import MediationManager - -from ...coordinate_mediation.v1_0.models.mediation_record import MediationRecord from ...discovery.v2_0.manager import V20DiscoveryMgr - +from ...routing.v1_0.manager import RoutingManager from .message_types import ARIES_PROTOCOL as CONN_PROTO from .messages.connection_invitation import ConnectionInvitation from .messages.connection_request import ConnectionRequest @@ -124,60 +121,33 @@ async def create_invitation( """ # Mediation Record can still be None after this operation if no # mediation id passed and no default - mediation_record = await mediation_record_if_id( + mediation_record = await self._route_manager.mediation_record_if_id( self.profile, mediation_id, or_default=True, ) - keylist_updates = None image_url = self.profile.context.settings.get("image_url") + invitation = None + connection = None - # Multitenancy setup - multitenant_mgr = self.profile.inject_or(BaseMultitenantManager) - wallet_id = self.profile.settings.get("wallet.id") + invitation_mode = ConnRecord.INVITATION_MODE_ONCE + if multi_use: + invitation_mode = ConnRecord.INVITATION_MODE_MULTI if not my_label: my_label = self.profile.settings.get("default_label") - if public: - if not self.profile.settings.get("public_invites"): - raise ConnectionManagerError("Public invitations are not enabled") - - async with self.profile.session() as session: - wallet = session.inject(BaseWallet) - public_did = await wallet.get_public_did() - if not public_did: - raise ConnectionManagerError( - "Cannot create public invitation with no public DID" - ) - - if multi_use: - raise ConnectionManagerError( - "Cannot use public and multi_use at the same time" - ) - - if metadata: - raise ConnectionManagerError( - "Cannot use public and set metadata at the same time" + accept = ( + ConnRecord.ACCEPT_AUTO + if ( + auto_accept + or ( + auto_accept is None + and self.profile.settings.get("debug.auto_accept_requests") ) - - # FIXME - allow ledger instance to format public DID with prefix? - invitation = ConnectionInvitation( - label=my_label, did=f"did:sov:{public_did.did}", image_url=image_url ) - - # Add mapping for multitenant relaying. - # Mediation of public keys is not supported yet - if multitenant_mgr and wallet_id: - await multitenant_mgr.add_key( - wallet_id, public_did.verkey, skip_if_exists=True - ) - - return None, invitation - - invitation_mode = ConnRecord.INVITATION_MODE_ONCE - if multi_use: - invitation_mode = ConnRecord.INVITATION_MODE_MULTI + else ConnRecord.ACCEPT_MANUAL + ) if recipient_keys: # TODO: register recipient keys for relay @@ -188,85 +158,81 @@ async def create_invitation( async with self.profile.session() as session: wallet = session.inject(BaseWallet) invitation_signing_key = await wallet.create_signing_key( - key_type=KeyType.ED25519 + key_type=ED25519 ) invitation_key = invitation_signing_key.verkey recipient_keys = [invitation_key] - mediation_mgr = MediationManager(self.profile) - keylist_updates = await mediation_mgr.add_key( - invitation_key, keylist_updates - ) - if multitenant_mgr and wallet_id: - await multitenant_mgr.add_key(wallet_id, invitation_key) + if public: + if not self.profile.settings.get("public_invites"): + raise ConnectionManagerError("Public invitations are not enabled") - accept = ( - ConnRecord.ACCEPT_AUTO - if ( - auto_accept - or ( - auto_accept is None - and self.profile.settings.get("debug.auto_accept_requests") + async with self.profile.session() as session: + wallet = session.inject(BaseWallet) + public_did = await wallet.get_public_did() + if not public_did: + raise ConnectionManagerError( + "Cannot create public invitation with no public DID" ) - ) - else ConnRecord.ACCEPT_MANUAL - ) - # Create connection record - connection = ConnRecord( - invitation_key=invitation_key, # TODO: determine correct key to use - their_role=ConnRecord.Role.REQUESTER.rfc160, - state=ConnRecord.State.INVITATION.rfc160, - accept=accept, - invitation_mode=invitation_mode, - alias=alias, - connection_protocol=CONN_PROTO, - ) - async with self.profile.session() as session: - await connection.save(session, reason="Created new invitation") - - routing_keys = [] - my_endpoint = my_endpoint or self.profile.settings.get("default_endpoint") + # FIXME - allow ledger instance to format public DID with prefix? + invitation = ConnectionInvitation( + label=my_label, did=f"did:sov:{public_did.did}", image_url=image_url + ) - # The base wallet can act as a mediator for all tenants - if multitenant_mgr and wallet_id: - base_mediation_record = await multitenant_mgr.get_default_mediator() + connection = ConnRecord( # create connection record + invitation_key=public_did.verkey, + invitation_msg_id=invitation._id, + invitation_mode=invitation_mode, + their_role=ConnRecord.Role.REQUESTER.rfc23, + state=ConnRecord.State.INVITATION.rfc23, + accept=accept, + alias=alias, + connection_protocol=CONN_PROTO, + ) - if base_mediation_record: - routing_keys = base_mediation_record.routing_keys - my_endpoint = base_mediation_record.endpoint + async with self.profile.session() as session: + await connection.save(session, reason="Created new invitation") - # If we use a mediator for the base wallet we don't - # need to register the key at the subwallet mediator - # because it only needs to know the key of the base mediator - # sub wallet mediator -> base wallet mediator -> agent - keylist_updates = None - if mediation_record: - routing_keys = [*routing_keys, *mediation_record.routing_keys] - my_endpoint = mediation_record.endpoint + # Add mapping for multitenant relaying. + # Mediation of public keys is not supported yet + await self._route_manager.route_verkey(self.profile, public_did.verkey) - # Save that this invitation was created with mediation + else: + # Create connection record + connection = ConnRecord( + invitation_key=invitation_key, # TODO: determine correct key to use + their_role=ConnRecord.Role.REQUESTER.rfc160, + state=ConnRecord.State.INVITATION.rfc160, + accept=accept, + invitation_mode=invitation_mode, + alias=alias, + connection_protocol=CONN_PROTO, + ) async with self.profile.session() as session: - await connection.metadata_set( - session, "mediation", {"id": mediation_record.mediation_id} - ) + await connection.save(session, reason="Created new invitation") - if keylist_updates: - responder = self.profile.inject_or(BaseResponder) - await responder.send( - keylist_updates, connection_id=mediation_record.connection_id - ) + await self._route_manager.route_invitation( + self.profile, connection, mediation_record + ) + routing_keys, my_endpoint = await self._route_manager.routing_info( + self.profile, + my_endpoint or cast(str, self.profile.settings.get("default_endpoint")), + mediation_record, + ) + + # Create connection invitation message + # Note: Need to split this into two stages + # to support inbound routing of invites + # Would want to reuse create_did_document and convert the result + invitation = ConnectionInvitation( + label=my_label, + recipient_keys=recipient_keys, + routing_keys=routing_keys, + endpoint=my_endpoint, + image_url=image_url, + ) - # Create connection invitation message - # Note: Need to split this into two stages to support inbound routing of invites - # Would want to reuse create_did_document and convert the result - invitation = ConnectionInvitation( - label=my_label, - recipient_keys=recipient_keys, - routing_keys=routing_keys, - endpoint=my_endpoint, - image_url=image_url, - ) async with self.profile.session() as session: await connection.attach_invitation(session, invitation) @@ -279,11 +245,10 @@ async def create_invitation( async def receive_invitation( self, invitation: ConnectionInvitation, - their_public_did: str = None, - auto_accept: bool = None, - alias: str = None, - mediation_id: str = None, - mediation_record: MediationRecord = None, + their_public_did: Optional[str] = None, + auto_accept: Optional[bool] = None, + alias: Optional[str] = None, + mediation_id: Optional[str] = None, ) -> ConnRecord: """ Create a new connection record to track a received invitation. @@ -342,6 +307,10 @@ async def receive_invitation( # Save the invitation for later processing await connection.attach_invitation(session, invitation) + await self._route_manager.save_mediator_for_connection( + self.profile, connection, mediation_id=mediation_id + ) + if connection.accept == ConnRecord.ACCEPT_AUTO: request = await self.create_request(connection, mediation_id=mediation_id) responder = self.profile.inject_or(BaseResponder) @@ -376,23 +345,19 @@ async def create_request( """ - keylist_updates = None - - # Mediation Record can still be None after this operation if no - # mediation id passed and no default - mediation_record = await mediation_record_if_id( + mediation_record = await self._route_manager.mediation_record_for_connection( self.profile, + connection, mediation_id, or_default=True, ) multitenant_mgr = self.profile.inject_or(BaseMultitenantManager) wallet_id = self.profile.settings.get("wallet.id") - base_mediation_record = None + base_mediation_record = None if multitenant_mgr and wallet_id: base_mediation_record = await multitenant_mgr.get_default_mediator() - my_info = None if connection.my_did: async with self.profile.session() as session: @@ -402,16 +367,13 @@ async def create_request( async with self.profile.session() as session: wallet = session.inject(BaseWallet) # Create new DID for connection - my_info = await wallet.create_local_did(DIDMethod.SOV, KeyType.ED25519) + my_info = await wallet.create_local_did(SOV, ED25519) connection.my_did = my_info.did - mediation_mgr = MediationManager(self.profile) - keylist_updates = await mediation_mgr.add_key( - my_info.verkey, keylist_updates - ) - # Add mapping for multitenant relay - if multitenant_mgr and wallet_id: - await multitenant_mgr.add_key(wallet_id, my_info.verkey) + # Idempotent; if routing has already been set up, no action taken + await self._route_manager.route_connection_as_invitee( + self.profile, connection, mediation_record + ) # Create connection request message if my_endpoint: @@ -448,21 +410,12 @@ async def create_request( async with self.profile.session() as session: await connection.save(session, reason="Created connection request") - # Notify mediator of keylist changes - if keylist_updates and mediation_record: - # send a update keylist message with new recipient keys. - responder = self.profile.inject_or(BaseResponder) - await responder.send( - keylist_updates, connection_id=mediation_record.connection_id - ) - return request async def receive_request( self, request: ConnectionRequest, receipt: MessageReceipt, - mediation_id: str = None, ) -> ConnRecord: """ Receive and store a connection request. @@ -481,16 +434,10 @@ async def receive_request( settings=self.profile.settings, ) - mediation_mgr = MediationManager(self.profile) - keylist_updates = None connection = None connection_key = None my_info = None - # Multitenancy setup - multitenant_mgr = self.profile.inject_or(BaseMultitenantManager) - wallet_id = self.profile.settings.get("wallet.id") - # Determine what key will need to sign the response if receipt.recipient_did_public: async with self.profile.session() as session: @@ -527,17 +474,12 @@ async def receive_request( if connection.is_multiuse_invitation: async with self.profile.session() as session: wallet = session.inject(BaseWallet) - my_info = await wallet.create_local_did( - DIDMethod.SOV, KeyType.ED25519 - ) - keylist_updates = await mediation_mgr.add_key( - my_info.verkey, keylist_updates - ) + my_info = await wallet.create_local_did(SOV, ED25519) new_connection = ConnRecord( invitation_key=connection_key, my_did=my_info.did, - state=ConnRecord.State.INVITATION.rfc160, + state=ConnRecord.State.REQUEST.rfc160, accept=connection.accept, their_role=connection.their_role, connection_protocol=CONN_PROTO, @@ -548,6 +490,7 @@ async def receive_request( reason=( "Received connection request from multi-use invitation DID" ), + event=False, ) # Transfer metadata from multi-use to new connection @@ -560,14 +503,6 @@ async def receive_request( connection = new_connection - # Add mapping for multitenant relay - if multitenant_mgr and wallet_id: - await multitenant_mgr.add_key(wallet_id, my_info.verkey) - else: - # remove key from mediator keylist - keylist_updates = await mediation_mgr.remove_key( - connection_key, keylist_updates - ) conn_did_doc = request.connection.did_doc if not conn_did_doc: raise ConnectionManagerError( @@ -593,15 +528,8 @@ async def receive_request( else: # request from public did async with self.profile.session() as session: wallet = session.inject(BaseWallet) - my_info = await wallet.create_local_did(DIDMethod.SOV, KeyType.ED25519) - # send update-keylist message with new recipient keys - keylist_updates = await mediation_mgr.add_key( - my_info.verkey, keylist_updates - ) + my_info = await wallet.create_local_did(SOV, ED25519) - # Add mapping for multitenant relay - if multitenant_mgr and wallet_id: - await multitenant_mgr.add_key(wallet_id, my_info.verkey) async with self.profile.session() as session: connection = await ConnRecord.retrieve_by_invitation_msg_id( session=session, @@ -609,6 +537,11 @@ async def receive_request( their_role=ConnRecord.Role.REQUESTER.rfc160, ) if not connection: + if not self.profile.settings.get("requests_through_public_did"): + raise ConnectionManagerError( + "Unsolicited connection requests to " + "public DID is not enabled" + ) connection = ConnRecord() connection.invitation_key = connection_key connection.my_did = my_info.did @@ -631,13 +564,9 @@ async def receive_request( # Attach the connection request so it can be found and responded to await connection.attach_request(session, request) - # Send keylist updates to mediator - mediation_record = await mediation_record_if_id(self.profile, mediation_id) - if keylist_updates and mediation_record: - responder = self.profile.inject_or(BaseResponder) - await responder.send( - keylist_updates, connection_id=mediation_record.connection_id - ) + # Clean associated oob record if not needed anymore + oob_processor = self.profile.inject(OobMessageProcessor) + await oob_processor.clean_finished_oob_record(self.profile, request) return connection @@ -665,14 +594,15 @@ async def create_response( settings=self.profile.settings, ) - keylist_updates = None - mediation_record = await mediation_record_if_id(self.profile, mediation_id) + mediation_record = await self._route_manager.mediation_record_for_connection( + self.profile, connection, mediation_id + ) # Multitenancy setup multitenant_mgr = self.profile.inject_or(BaseMultitenantManager) wallet_id = self.profile.settings.get("wallet.id") - base_mediation_record = None + base_mediation_record = None if multitenant_mgr and wallet_id: base_mediation_record = await multitenant_mgr.get_default_mediator() @@ -686,6 +616,7 @@ async def create_response( async with self.profile.session() as session: request = await connection.retrieve_request(session) + if connection.my_did: async with self.profile.session() as session: wallet = session.inject(BaseWallet) @@ -693,15 +624,13 @@ async def create_response( else: async with self.profile.session() as session: wallet = session.inject(BaseWallet) - my_info = await wallet.create_local_did(DIDMethod.SOV, KeyType.ED25519) + my_info = await wallet.create_local_did(SOV, ED25519) connection.my_did = my_info.did - mediation_mgr = MediationManager(self.profile) - keylist_updates = await mediation_mgr.add_key( - my_info.verkey, keylist_updates - ) - # Add mapping for multitenant relay - if multitenant_mgr and wallet_id: - await multitenant_mgr.add_key(wallet_id, my_info.verkey) + + # Idempotent; if routing has already been set up, no action taken + await self._route_manager.route_connection_as_inviter( + self.profile, connection, mediation_record + ) # Create connection response message if my_endpoint: @@ -743,13 +672,6 @@ async def create_response( log_params={"response": response}, ) - # Update mediator if necessary - if keylist_updates and mediation_record: - responder = self.profile.inject_or(BaseResponder) - await responder.send( - keylist_updates, connection_id=mediation_record.connection_id - ) - # TODO It's possible the mediation request sent here might arrive # before the connection response. This would result in an error condition # difficult to accomodate for without modifying handlers for trust ping @@ -833,6 +755,17 @@ async def accept_response( ) if their_did != conn_did_doc.did: raise ConnectionManagerError("Connection DID does not match DIDDoc id") + # Verify connection response using connection field + async with self.profile.session() as session: + wallet = session.inject(BaseWallet) + try: + await response.verify_signed_field( + "connection", wallet, connection.invitation_key + ) + except ValueError: + raise ConnectionManagerError( + "connection field verification using invitation_key failed" + ) await self.store_did_document(conn_did_doc) connection.their_did = their_did @@ -887,6 +820,7 @@ async def create_static_connection( their_endpoint: str = None, their_label: str = None, alias: str = None, + mediation_id: str = None, ) -> Tuple[DIDInfo, DIDInfo, ConnRecord]: """ Register a new static connection (for use by the test suite). @@ -904,17 +838,10 @@ async def create_static_connection( Tuple: my DIDInfo, their DIDInfo, new `ConnRecord` instance """ - # Multitenancy setup - multitenant_mgr = self.profile.inject_or(BaseMultitenantManager) - wallet_id = self.profile.settings.get("wallet.id") - base_mediation_record = None - async with self.profile.session() as session: wallet = session.inject(BaseWallet) # seed and DID optional - my_info = await wallet.create_local_did( - DIDMethod.SOV, KeyType.ED25519, my_seed, my_did - ) + my_info = await wallet.create_local_did(SOV, ED25519, my_seed, my_did) # must provide their DID and verkey if the seed is not known if (not their_did or not their_verkey) and not their_seed: @@ -924,11 +851,9 @@ async def create_static_connection( if not their_did: their_did = seed_to_did(their_seed) if not their_verkey: - their_verkey_bin, _ = create_keypair(KeyType.ED25519, their_seed.encode()) + their_verkey_bin, _ = create_keypair(ED25519, their_seed.encode()) their_verkey = bytes_to_b58(their_verkey_bin) - their_info = DIDInfo( - their_did, their_verkey, {}, method=DIDMethod.SOV, key_type=KeyType.ED25519 - ) + their_info = DIDInfo(their_did, their_verkey, {}, method=SOV, key_type=ED25519) # Create connection record connection = ConnRecord( @@ -948,20 +873,32 @@ async def create_static_connection( connection_id=connection.connection_id ) - # Add mapping for multitenant relaying / mediation + # Routing + mediation_record = await self._route_manager.mediation_record_if_id( + self.profile, mediation_id, or_default=True + ) + + multitenant_mgr = self.profile.inject_or(BaseMultitenantManager) + wallet_id = self.profile.settings.get("wallet.id") + + base_mediation_record = None if multitenant_mgr and wallet_id: base_mediation_record = await multitenant_mgr.get_default_mediator() - await multitenant_mgr.add_key(wallet_id, my_info.verkey) + + await self._route_manager.route_static( + self.profile, connection, mediation_record + ) # Synthesize their DID doc did_doc = await self.create_did_document( their_info, None, [their_endpoint or ""], - mediation_records=[base_mediation_record] - if base_mediation_record - else None, + mediation_records=list( + filter(None, [base_mediation_record, mediation_record]) + ), ) + await self.store_did_document(did_doc) return my_info, their_info, connection @@ -1151,7 +1088,13 @@ async def get_connection_targets( targets = await self.fetch_connection_targets(connection) - await entry.set_result([row.serialize() for row in targets], 3600) + if connection.state == ConnRecord.State.COMPLETED.rfc160: + # Only set cache if connection has reached completed state + # Otherwise, a replica that participated early in exchange + # may have bad data set in cache. + await entry.set_result( + [row.serialize() for row in targets], 3600 + ) else: targets = await self.fetch_connection_targets(connection) return targets @@ -1176,7 +1119,7 @@ async def establish_inbound( my_info = await wallet.get_local_did(connection.my_did) else: # Create new DID for connection - my_info = await wallet.create_local_did(DIDMethod.SOV, KeyType.ED25519) + my_info = await wallet.create_local_did(SOV, ED25519) connection.my_did = my_info.did try: diff --git a/aries_cloudagent/protocols/connections/v1_0/messages/tests/test_connection_invitation.py b/aries_cloudagent/protocols/connections/v1_0/messages/tests/test_connection_invitation.py index 58d38b5917..b35bf95e1a 100644 --- a/aries_cloudagent/protocols/connections/v1_0/messages/tests/test_connection_invitation.py +++ b/aries_cloudagent/protocols/connections/v1_0/messages/tests/test_connection_invitation.py @@ -84,7 +84,6 @@ def test_from_no_url(self): class TestConnectionInvitationSchema(TestCase): - connection_invitation = ConnectionInvitation( label="label", did="did:sov:QmWbsNYhMrjHiqZDTUTEJs" ) diff --git a/aries_cloudagent/protocols/connections/v1_0/messages/tests/test_connection_response.py b/aries_cloudagent/protocols/connections/v1_0/messages/tests/test_connection_response.py index 98beb344fd..0fadf94ff4 100644 --- a/aries_cloudagent/protocols/connections/v1_0/messages/tests/test_connection_response.py +++ b/aries_cloudagent/protocols/connections/v1_0/messages/tests/test_connection_response.py @@ -2,7 +2,7 @@ from asynctest import TestCase as AsyncTestCase -from ......wallet.key_type import KeyType +from ......wallet.key_type import ED25519 from ......connections.models.diddoc import ( DIDDoc, PublicKey, @@ -19,7 +19,6 @@ class TestConfig: - test_seed = "testseed000000000000000000000001" test_did = "55GkHamhTU1ZbTbV2ab9DE" test_verkey = "3Dn1SJNPaCXcvvJvSbsFWP2xaCjMom3can8CQNhWrTRx" @@ -108,7 +107,7 @@ async def test_make_model(self): ) session = InMemoryProfile.test_session() wallet = session.wallet - key_info = await wallet.create_signing_key(KeyType.ED25519) + key_info = await wallet.create_signing_key(ED25519) await connection_response.sign_field("connection", key_info.verkey, wallet) data = connection_response.serialize() model_instance = ConnectionResponse.deserialize(data) diff --git a/aries_cloudagent/protocols/connections/v1_0/models/connection_detail.py b/aries_cloudagent/protocols/connections/v1_0/models/connection_detail.py index cdbac51cd7..96a64abed1 100644 --- a/aries_cloudagent/protocols/connections/v1_0/models/connection_detail.py +++ b/aries_cloudagent/protocols/connections/v1_0/models/connection_detail.py @@ -23,7 +23,7 @@ def _serialize(self, value, attr, obj, **kwargs): """ return value.serialize() - def _deserialize(self, value, attr, data, **kwargs): + def _deserialize(self, value, attr=None, data=None, **kwargs): """ Deserialize a value into a DIDDoc. diff --git a/aries_cloudagent/protocols/connections/v1_0/routes.py b/aries_cloudagent/protocols/connections/v1_0/routes.py index ae65d52f99..11d8ba5651 100644 --- a/aries_cloudagent/protocols/connections/v1_0/routes.py +++ b/aries_cloudagent/protocols/connections/v1_0/routes.py @@ -10,11 +10,12 @@ request_schema, response_schema, ) - +from typing import cast from marshmallow import fields, validate, validates_schema from ....admin.request_context import AdminRequestContext from ....connections.models.conn_record import ConnRecord, ConnRecordSchema +from ....cache.base import BaseCache from ....messaging.models.base import BaseModelError from ....messaging.models.openapi import OpenAPISchema from ....messaging.valid import ( @@ -115,7 +116,7 @@ class CreateInvitationRequestSchema(OpenAPISchema): mediation_id = fields.Str( required=False, description="Identifier for active mediation record to be used", - **MEDIATION_ID_SCHEMA + **MEDIATION_ID_SCHEMA, ) @@ -186,6 +187,9 @@ class ConnectionsListQueryStringSchema(OpenAPISchema): ), ) their_did = fields.Str(description="Their DID", required=False, **INDY_DID) + their_public_did = fields.Str( + description="Their Public DID", required=False, **INDY_DID + ) their_role = fields.Str( description="Their role in the connection protocol", required=False, @@ -202,6 +206,11 @@ class ConnectionsListQueryStringSchema(OpenAPISchema): ), example=ConnRecord.Protocol.RFC_0160.aries_protocol, ) + invitation_msg_id = fields.UUID( + description="Identifier of the associated Invitation Mesage", + required=False, + example=UUIDFour.EXAMPLE, + ) class CreateInvitationQueryStringSchema(OpenAPISchema): @@ -239,7 +248,7 @@ class ReceiveInvitationQueryStringSchema(OpenAPISchema): mediation_id = fields.Str( required=False, description="Identifier for active mediation record to be used", - **MEDIATION_ID_SCHEMA + **MEDIATION_ID_SCHEMA, ) @@ -253,7 +262,7 @@ class AcceptInvitationQueryStringSchema(OpenAPISchema): mediation_id = fields.Str( required=False, description="Identifier for active mediation record to be used", - **MEDIATION_ID_SCHEMA + **MEDIATION_ID_SCHEMA, ) @@ -332,6 +341,8 @@ async def connections_list(request: web.BaseRequest): "their_did", "request_id", "invitation_key", + "their_public_did", + "invitation_msg_id", ): if param_name in request.query and request.query[param_name] != "": tag_filter[param_name] = request.query[param_name] @@ -526,11 +537,16 @@ async def connections_create_invitation(request: web.BaseRequest): metadata=metadata, mediation_id=mediation_id, ) - + invitation_url = invitation.to_url(base_url) + base_endpoint = service_endpoint or cast( + str, profile.settings.get("default_endpoint") + ) result = { "connection_id": connection and connection.connection_id, "invitation": invitation.serialize(), - "invitation_url": invitation.to_url(base_url), + "invitation_url": f"{base_endpoint}{invitation_url}" + if invitation_url.startswith("?") + else invitation_url, } except (ConnectionManagerError, StorageError, BaseModelError) as err: raise web.HTTPBadRequest(reason=err.roll_up) from err @@ -724,6 +740,9 @@ async def connections_remove(request: web.BaseRequest): async with profile.session() as session: connection = await ConnRecord.retrieve_by_id(session, connection_id) await connection.delete_record(session) + cache = session.inject_or(BaseCache) + if cache: + await cache.clear(f"conn_rec_state::{connection_id}") except StorageNotFoundError as err: raise web.HTTPNotFound(reason=err.roll_up) from err except StorageError as err: diff --git a/aries_cloudagent/protocols/connections/v1_0/tests/test_manager.py b/aries_cloudagent/protocols/connections/v1_0/tests/test_manager.py index c986cb88cf..c54c75203e 100644 --- a/aries_cloudagent/protocols/connections/v1_0/tests/test_manager.py +++ b/aries_cloudagent/protocols/connections/v1_0/tests/test_manager.py @@ -11,6 +11,7 @@ from .....connections.models.conn_record import ConnRecord from .....connections.models.connection_target import ConnectionTarget from .....connections.models.diddoc import DIDDoc, PublicKey, PublicKeyType, Service +from .....core.oob_processor import OobMessageProcessor from .....core.in_memory import InMemoryProfile from .....core.profile import ProfileSession from .....did.did_key import DIDKey @@ -19,29 +20,26 @@ from .....multitenant.manager import MultitenantManager from .....protocols.routing.v1_0.manager import RoutingManager from .....resolver.did_resolver import DIDResolver -from .....resolver.did_resolver_registry import DIDResolverRegistry from .....storage.error import StorageNotFoundError from .....transport.inbound.receipt import MessageReceipt from .....wallet.base import DIDInfo -from .....wallet.did_info import KeyInfo -from .....wallet.did_method import DIDMethod +from .....wallet.did_method import SOV, DIDMethods from .....wallet.error import WalletNotFoundError from .....wallet.in_memory import InMemoryWallet -from .....wallet.key_type import KeyType +from .....wallet.key_type import ED25519 from ....coordinate_mediation.v1_0.manager import MediationManager -from ....coordinate_mediation.v1_0.messages.inner.keylist_update_rule import ( - KeylistUpdateRule, -) -from ....coordinate_mediation.v1_0.messages.keylist_update import KeylistUpdate +from ....coordinate_mediation.v1_0.route_manager import RouteManager from ....coordinate_mediation.v1_0.messages.mediate_request import MediationRequest from ....coordinate_mediation.v1_0.models.mediation_record import MediationRecord from ....discovery.v2_0.manager import V20DiscoveryMgr from ..manager import ConnectionManager, ConnectionManagerError +from .. import manager as test_module from ..messages.connection_invitation import ConnectionInvitation from ..messages.connection_request import ConnectionRequest from ..messages.connection_response import ConnectionResponse from ..models.connection_detail import ConnectionDetail +from .....wallet.util import bytes_to_b64, b58_to_bytes class TestConnectionManager(AsyncTestCase): @@ -73,6 +71,17 @@ async def setUp(self): self.responder = MockResponder() + self.oob_mock = async_mock.MagicMock( + clean_finished_oob_record=async_mock.CoroutineMock(return_value=None) + ) + self.route_manager = async_mock.MagicMock(RouteManager) + self.route_manager.routing_info = async_mock.CoroutineMock( + return_value=([], self.test_endpoint) + ) + self.route_manager.mediation_record_if_id = async_mock.CoroutineMock( + return_value=None + ) + self.profile = InMemoryProfile.test_profile( { "default_endpoint": "http://aries.ca/endpoint", @@ -81,7 +90,13 @@ async def setUp(self): "debug.auto_accept_invites": True, "debug.auto_accept_requests": True, }, - bind={BaseResponder: self.responder, BaseCache: InMemoryCache()}, + bind={ + BaseResponder: self.responder, + BaseCache: InMemoryCache(), + OobMessageProcessor: self.oob_mock, + RouteManager: self.route_manager, + DIDMethods: DIDMethods(), + }, ) self.context = self.profile.context @@ -99,21 +114,6 @@ async def setUp(self): self.manager = ConnectionManager(self.profile) assert self.manager.profile - async def test_create_invitation_public_and_multi_use_fails(self): - self.context.update_settings({"public_invites": True}) - with async_mock.patch.object( - InMemoryWallet, "get_public_did", autospec=True - ) as mock_wallet_get_public_did: - mock_wallet_get_public_did.return_value = DIDInfo( - self.test_did, - self.test_verkey, - None, - method=DIDMethod.SOV, - key_type=KeyType.ED25519, - ) - with self.assertRaises(ConnectionManagerError): - await self.manager.create_invitation(public=True, multi_use=True) - async def test_create_invitation_non_multi_use_invitation_fails_on_reuse(self): connect_record, connect_invite = await self.manager.create_invitation() @@ -146,6 +146,7 @@ async def test_create_invitation_non_multi_use_invitation_fails_on_reuse(self): async def test_create_invitation_public(self): self.context.update_settings({"public_invites": True}) + self.route_manager.route_verkey = async_mock.CoroutineMock() with async_mock.patch.object( InMemoryWallet, "get_public_did", autospec=True ) as mock_wallet_get_public_did: @@ -153,54 +154,17 @@ async def test_create_invitation_public(self): self.test_did, self.test_verkey, None, - method=DIDMethod.SOV, - key_type=KeyType.ED25519, + method=SOV, + key_type=ED25519, ) connect_record, connect_invite = await self.manager.create_invitation( public=True, my_endpoint="testendpoint" ) - assert connect_record is None + assert connect_record assert connect_invite.did.endswith(self.test_did) - - async def test_create_invitation_multitenant(self): - self.context.update_settings( - {"wallet.id": "test_wallet", "multitenant.enabled": True} - ) - - with async_mock.patch.object( - InMemoryWallet, "create_signing_key", autospec=True - ) as mock_wallet_create_signing_key: - mock_wallet_create_signing_key.return_value = KeyInfo( - self.test_verkey, None, KeyType.ED25519 - ) - await self.manager.create_invitation() - self.multitenant_mgr.add_key.assert_called_once_with( - "test_wallet", self.test_verkey - ) - - async def test_create_invitation_public_multitenant(self): - self.context.update_settings( - { - "public_invites": True, - "wallet.id": "test_wallet", - "multitenant.enabled": True, - } - ) - - with async_mock.patch.object( - InMemoryWallet, "get_public_did", autospec=True - ) as mock_wallet_get_public_did: - mock_wallet_get_public_did.return_value = DIDInfo( - self.test_did, - self.test_verkey, - None, - method=DIDMethod.SOV, - key_type=KeyType.ED25519, - ) - await self.manager.create_invitation(public=True) - self.multitenant_mgr.add_key.assert_called_once_with( - "test_wallet", self.test_verkey, skip_if_exists=True + self.route_manager.route_verkey.assert_called_once_with( + self.profile, self.test_verkey ) async def test_create_invitation_public_no_public_invites(self): @@ -255,8 +219,8 @@ async def test_create_invitation_multi_use(self): async def test_create_invitation_recipient_routing_endpoint(self): async with self.profile.session() as session: await session.wallet.create_local_did( - method=DIDMethod.SOV, - key_type=KeyType.ED25519, + method=SOV, + key_type=ED25519, seed=self.test_seed, did=self.test_did, metadata=None, @@ -289,23 +253,6 @@ async def test_create_invitation_metadata_assigned(self): assert await record.metadata_get_all(session) == {"hello": "world"} - async def test_create_invitation_public_and_metadata_fails(self): - self.context.update_settings({"public_invites": True}) - with async_mock.patch.object( - InMemoryWallet, "get_public_did", autospec=True - ) as mock_wallet_get_public_did: - mock_wallet_get_public_did.return_value = DIDInfo( - self.test_did, - self.test_verkey, - None, - method=DIDMethod.SOV, - key_type=KeyType.ED25519, - ) - with self.assertRaises(ConnectionManagerError): - await self.manager.create_invitation( - public=True, metadata={"hello": "world"} - ) - async def test_create_invitation_multi_use_metadata_transfers_to_connection(self): async with self.profile.session() as session: connect_record, _ = await self.manager.create_invitation( @@ -329,6 +276,9 @@ async def test_create_invitation_multi_use_metadata_transfers_to_connection(self assert await new_conn_rec.metadata_get_all(session) == {"test": "value"} async def test_create_invitation_mediation_overwrites_routing_and_endpoint(self): + self.route_manager.routing_info = async_mock.CoroutineMock( + return_value=(self.test_mediator_routing_keys, self.test_mediator_endpoint) + ) async with self.profile.session() as session: mediation_record = MediationRecord( role=MediationRecord.ROLE_CLIENT, @@ -352,6 +302,9 @@ async def test_create_invitation_mediation_overwrites_routing_and_endpoint(self) mock_get_default_mediator.assert_not_called() async def test_create_invitation_mediation_using_default(self): + self.route_manager.routing_info = async_mock.CoroutineMock( + return_value=(self.test_mediator_routing_keys, self.test_mediator_endpoint) + ) async with self.profile.session() as session: mediation_record = MediationRecord( role=MediationRecord.ROLE_CLIENT, @@ -362,17 +315,19 @@ async def test_create_invitation_mediation_using_default(self): ) await mediation_record.save(session) with async_mock.patch.object( - MediationManager, - "get_default_mediator", + self.route_manager, + "mediation_record_if_id", async_mock.CoroutineMock(return_value=mediation_record), - ) as mock_get_default_mediator: + ): _, invite = await self.manager.create_invitation( routing_keys=[self.test_verkey], my_endpoint=self.test_endpoint, ) assert invite.routing_keys == self.test_mediator_routing_keys assert invite.endpoint == self.test_mediator_endpoint - mock_get_default_mediator.assert_called_once() + self.route_manager.routing_info.assert_awaited_once_with( + self.profile, self.test_endpoint, mediation_record + ) async def test_receive_invitation(self): (_, connect_invite) = await self.manager.create_invitation( @@ -425,41 +380,6 @@ async def test_receive_invitation_mediation_passes_id_when_auto_accept(self): invitee_record, mediation_id="test-mediation-id" ) - async def test_receive_invitation_bad_mediation(self): - _, connect_invite = await self.manager.create_invitation( - my_endpoint="testendpoint" - ) - with self.assertRaises(StorageNotFoundError): - await self.manager.receive_invitation( - connect_invite, mediation_id="not-a-mediation-id" - ) - - async def test_receive_invitation_mediation_not_granted(self): - async with self.profile.session() as session: - _, connect_invite = await self.manager.create_invitation( - my_endpoint="testendpoint" - ) - - mediation_record = MediationRecord( - role=MediationRecord.ROLE_CLIENT, - state=MediationRecord.STATE_DENIED, - connection_id=self.test_mediator_conn_id, - routing_keys=self.test_mediator_routing_keys, - endpoint=self.test_mediator_endpoint, - ) - await mediation_record.save(session) - with self.assertRaises(BaseConnectionManagerError): - await self.manager.receive_invitation( - connect_invite, mediation_id=mediation_record.mediation_id - ) - - mediation_record.state = MediationRecord.STATE_REQUEST - await mediation_record.save(session) - with self.assertRaises(BaseConnectionManagerError): - await self.manager.receive_invitation( - connect_invite, mediation_id=mediation_record.mediation_id - ) - async def test_create_request(self): conn_req = await self.manager.create_request( ConnRecord( @@ -486,8 +406,8 @@ async def test_create_request_my_endpoint(self): async def test_create_request_my_did(self): async with self.profile.session() as session: await session.wallet.create_local_did( - method=DIDMethod.SOV, - key_type=KeyType.ED25519, + method=SOV, + key_type=ED25519, seed=None, did=self.test_did, ) @@ -506,16 +426,33 @@ async def test_create_request_multitenant(self): self.context.update_settings( {"wallet.id": "test_wallet", "multitenant.enabled": True} ) + mediation_record = MediationRecord( + role=MediationRecord.ROLE_CLIENT, + state=MediationRecord.STATE_GRANTED, + connection_id=self.test_mediator_conn_id, + routing_keys=self.test_mediator_routing_keys, + endpoint=self.test_mediator_endpoint, + ) with async_mock.patch.object( InMemoryWallet, "create_local_did", autospec=True - ) as mock_wallet_create_local_did: + ) as mock_wallet_create_local_did, async_mock.patch.object( + self.multitenant_mgr, + "get_default_mediator", + async_mock.CoroutineMock(return_value=mediation_record), + ), async_mock.patch.object( + ConnectionManager, "create_did_document", autospec=True + ) as create_did_document, async_mock.patch.object( + self.route_manager, + "mediation_record_for_connection", + async_mock.CoroutineMock(return_value=None), + ): mock_wallet_create_local_did.return_value = DIDInfo( self.test_did, self.test_verkey, None, - method=DIDMethod.SOV, - key_type=KeyType.ED25519, + method=SOV, + key_type=ED25519, ) await self.manager.create_request( ConnRecord( @@ -523,69 +460,66 @@ async def test_create_request_multitenant(self): their_label="Hello", their_role=ConnRecord.Role.RESPONDER.rfc160, alias="Bob", - ) + ), + my_endpoint=self.test_endpoint, ) - self.multitenant_mgr.add_key.assert_called_once_with( - "test_wallet", self.test_verkey + create_did_document.assert_called_once_with( + self.manager, + mock_wallet_create_local_did.return_value, + None, + [self.test_endpoint], + mediation_records=[mediation_record], ) + self.route_manager.route_connection_as_invitee.assert_called_once() async def test_create_request_mediation_id(self): - async with self.profile.session() as session: - mediation_record = MediationRecord( - role=MediationRecord.ROLE_CLIENT, - state=MediationRecord.STATE_GRANTED, - connection_id=self.test_mediator_conn_id, - routing_keys=self.test_mediator_routing_keys, - endpoint=self.test_mediator_endpoint, - ) - await mediation_record.save(session) - - record = ConnRecord( - invitation_key=self.test_verkey, - their_label="Hello", - their_role=ConnRecord.Role.RESPONDER.rfc160, - alias="Bob", - ) + mediation_record = MediationRecord( + role=MediationRecord.ROLE_CLIENT, + state=MediationRecord.STATE_GRANTED, + connection_id=self.test_mediator_conn_id, + routing_keys=self.test_mediator_routing_keys, + endpoint=self.test_mediator_endpoint, + ) - # Ensure the path with new did creation is hit - record.my_did = None + record = ConnRecord( + invitation_key=self.test_verkey, + their_label="Hello", + their_role=ConnRecord.Role.RESPONDER.rfc160, + alias="Bob", + ) - with async_mock.patch.object( - ConnectionManager, "create_did_document", autospec=True - ) as create_did_document, async_mock.patch.object( - InMemoryWallet, "create_local_did" - ) as create_local_did, async_mock.patch.object( - MediationManager, "get_default_mediator" - ) as mock_get_default_mediator: - did_info = DIDInfo( - did=self.test_did, - verkey=self.test_verkey, - metadata={}, - method=DIDMethod.SOV, - key_type=KeyType.ED25519, - ) - create_local_did.return_value = did_info - await self.manager.create_request( - record, - mediation_id=mediation_record.mediation_id, - my_endpoint=self.test_endpoint, - ) - create_local_did.assert_called_once_with(DIDMethod.SOV, KeyType.ED25519) - create_did_document.assert_called_once_with( - self.manager, - did_info, - None, - [self.test_endpoint], - mediation_records=[mediation_record], - ) - mock_get_default_mediator.assert_not_called() + # Ensure the path with new did creation is hit + record.my_did = None - assert len(self.responder.messages) == 1 - message, used_kwargs = self.responder.messages[0] - assert isinstance(message, KeylistUpdate) - assert ( - "connection_id" in used_kwargs - and used_kwargs["connection_id"] == self.test_mediator_conn_id + with async_mock.patch.object( + ConnectionManager, "create_did_document", autospec=True + ) as create_did_document, async_mock.patch.object( + InMemoryWallet, "create_local_did" + ) as create_local_did, async_mock.patch.object( + self.route_manager, + "mediation_record_for_connection", + async_mock.CoroutineMock(return_value=mediation_record), + ): + did_info = DIDInfo( + did=self.test_did, + verkey=self.test_verkey, + metadata={}, + method=SOV, + key_type=ED25519, + ) + create_local_did.return_value = did_info + await self.manager.create_request( + record, + mediation_id=mediation_record.mediation_id, + my_endpoint=self.test_endpoint, + ) + create_local_did.assert_called_once_with(SOV, ED25519) + create_did_document.assert_called_once_with( + self.manager, + did_info, + None, + [self.test_endpoint], + mediation_records=[mediation_record], ) async def test_create_request_default_mediator(self): @@ -614,23 +548,23 @@ async def test_create_request_default_mediator(self): ) as create_did_document, async_mock.patch.object( InMemoryWallet, "create_local_did" ) as create_local_did, async_mock.patch.object( - MediationManager, - "get_default_mediator", + self.route_manager, + "mediation_record_for_connection", async_mock.CoroutineMock(return_value=mediation_record), - ) as mock_get_default_mediator: + ): did_info = DIDInfo( did=self.test_did, verkey=self.test_verkey, metadata={}, - method=DIDMethod.SOV, - key_type=KeyType.ED25519, + method=SOV, + key_type=ED25519, ) create_local_did.return_value = did_info await self.manager.create_request( record, my_endpoint=self.test_endpoint, ) - create_local_did.assert_called_once_with(DIDMethod.SOV, KeyType.ED25519) + create_local_did.assert_called_once_with(SOV, ED25519) create_did_document.assert_called_once_with( self.manager, did_info, @@ -638,44 +572,6 @@ async def test_create_request_default_mediator(self): [self.test_endpoint], mediation_records=[mediation_record], ) - mock_get_default_mediator.assert_called_once() - - assert len(self.responder.messages) == 1 - message, used_kwargs = self.responder.messages[0] - assert isinstance(message, KeylistUpdate) - assert ( - "connection_id" in used_kwargs - and used_kwargs["connection_id"] == self.test_mediator_conn_id - ) - - async def test_create_request_bad_mediation(self): - record, _ = await self.manager.create_invitation(my_endpoint="testendpoint") - with self.assertRaises(StorageNotFoundError): - await self.manager.create_request(record, mediation_id="not-a-mediation-id") - - async def test_create_request_mediation_not_granted(self): - async with self.profile.session() as session: - record, _ = await self.manager.create_invitation(my_endpoint="testendpoint") - - mediation_record = MediationRecord( - role=MediationRecord.ROLE_CLIENT, - state=MediationRecord.STATE_DENIED, - connection_id=self.test_mediator_conn_id, - routing_keys=self.test_mediator_routing_keys, - endpoint=self.test_mediator_endpoint, - ) - await mediation_record.save(session) - with self.assertRaises(BaseConnectionManagerError): - await self.manager.create_request( - record, mediation_id=mediation_record.mediation_id - ) - - mediation_record.state = MediationRecord.STATE_REQUEST - await mediation_record.save(session) - with self.assertRaises(BaseConnectionManagerError): - await self.manager.create_request( - record, mediation_id=mediation_record.mediation_id - ) async def test_receive_request_public_did_oob_invite(self): async with self.profile.session() as session: @@ -689,8 +585,8 @@ async def test_receive_request_public_did_oob_invite(self): recipient_did=self.test_did, recipient_did_public=True ) await session.wallet.create_local_did( - method=DIDMethod.SOV, - key_type=KeyType.ED25519, + method=SOV, + key_type=ED25519, seed=None, did=self.test_did, ) @@ -713,7 +609,11 @@ async def test_receive_request_public_did_oob_invite(self): conn_rec = await self.manager.receive_request(mock_request, receipt) assert conn_rec - async def test_receive_request_public_did_conn_invite(self): + self.oob_mock.clean_finished_oob_record.assert_called_once_with( + self.profile, mock_request + ) + + async def test_receive_request_public_did_unsolicited_fails(self): async with self.profile.session() as session: mock_request = async_mock.MagicMock() mock_request.connection = async_mock.MagicMock() @@ -725,14 +625,14 @@ async def test_receive_request_public_did_conn_invite(self): recipient_did=self.test_did, recipient_did_public=True ) await session.wallet.create_local_did( - method=DIDMethod.SOV, - key_type=KeyType.ED25519, + method=SOV, + key_type=ED25519, seed=None, did=self.test_did, ) self.context.update_settings({"public_invites": True}) - with async_mock.patch.object( + with self.assertRaises(ConnectionManagerError), async_mock.patch.object( ConnRecord, "connection_id", autospec=True ), async_mock.patch.object( ConnRecord, "save", autospec=True @@ -747,115 +647,84 @@ async def test_receive_request_public_did_conn_invite(self): ) as mock_conn_retrieve_by_invitation_msg_id: mock_conn_retrieve_by_invitation_msg_id.return_value = None conn_rec = await self.manager.receive_request(mock_request, receipt) - assert conn_rec - async def test_receive_request_multi_use_multitenant(self): + async def test_receive_request_public_did_conn_invite(self): async with self.profile.session() as session: - multiuse_info = await session.wallet.create_local_did( - DIDMethod.SOV, KeyType.ED25519 - ) - new_info = await session.wallet.create_local_did( - DIDMethod.SOV, KeyType.ED25519 - ) - mock_request = async_mock.MagicMock() - mock_request.connection = async_mock.MagicMock( - is_multiuse_invitation=True, invitation_key=multiuse_info.verkey - ) + mock_request.connection = async_mock.MagicMock() mock_request.connection.did = self.test_did mock_request.connection.did_doc = async_mock.MagicMock() mock_request.connection.did_doc.did = self.test_did - receipt = MessageReceipt(recipient_verkey=multiuse_info.verkey) - self.context.update_settings( - {"wallet.id": "test_wallet", "multitenant.enabled": True} + receipt = MessageReceipt( + recipient_did=self.test_did, recipient_did_public=True ) + await session.wallet.create_local_did( + method=SOV, + key_type=ED25519, + seed=None, + did=self.test_did, + ) + + mock_connection_record = async_mock.MagicMock() + mock_connection_record.save = async_mock.CoroutineMock() + mock_connection_record.attach_request = async_mock.CoroutineMock() + + self.context.update_settings({"public_invites": True}) with async_mock.patch.object( - ConnRecord, "attach_request", autospec=True + ConnRecord, "connection_id", autospec=True ), async_mock.patch.object( ConnRecord, "save", autospec=True + ) as mock_conn_rec_save, async_mock.patch.object( + ConnRecord, "attach_request", autospec=True + ) as mock_conn_attach_request, async_mock.patch.object( + ConnRecord, "retrieve_by_id", autospec=True + ) as mock_conn_retrieve_by_id, async_mock.patch.object( + ConnRecord, "retrieve_request", autospec=True ), async_mock.patch.object( - ConnRecord, "retrieve_by_invitation_key" - ) as mock_conn_retrieve_by_invitation_key, async_mock.patch.object( - InMemoryWallet, "create_local_did", autospec=True - ) as mock_wallet_create_local_did: - mock_wallet_create_local_did.return_value = DIDInfo( - new_info.did, - new_info.verkey, - None, - method=DIDMethod.SOV, - key_type=KeyType.ED25519, - ) - mock_conn_retrieve_by_invitation_key.return_value = ( - async_mock.MagicMock( - connection_id="dummy", - retrieve_invitation=async_mock.CoroutineMock(return_value={}), - metadata_get_all=async_mock.CoroutineMock(return_value={}), - ) - ) - await self.manager.receive_request(mock_request, receipt) - - self.multitenant_mgr.add_key.assert_called_once_with( - "test_wallet", new_info.verkey - ) + ConnRecord, + "retrieve_by_invitation_msg_id", + async_mock.CoroutineMock(return_value=mock_connection_record), + ) as mock_conn_retrieve_by_invitation_msg_id: + conn_rec = await self.manager.receive_request(mock_request, receipt) + assert conn_rec - async def test_receive_request_public_multitenant(self): + async def test_receive_request_public_did_unsolicited(self): async with self.profile.session() as session: - new_info = await session.wallet.create_local_did( - DIDMethod.SOV, KeyType.ED25519 - ) - mock_request = async_mock.MagicMock() - mock_request.connection = async_mock.MagicMock( - accept=ConnRecord.ACCEPT_MANUAL - ) + mock_request.connection = async_mock.MagicMock() mock_request.connection.did = self.test_did mock_request.connection.did_doc = async_mock.MagicMock() mock_request.connection.did_doc.did = self.test_did - receipt = MessageReceipt(recipient_did_public=True) - self.context.update_settings( - { - "wallet.id": "test_wallet", - "multitenant.enabled": True, - "public_invites": True, - "debug.auto_accept_requests": False, - } + receipt = MessageReceipt( + recipient_did=self.test_did, recipient_did_public=True + ) + await session.wallet.create_local_did( + method=SOV, + key_type=ED25519, + seed=None, + did=self.test_did, ) + self.context.update_settings({"public_invites": True}) + self.context.update_settings({"requests_through_public_did": True}) with async_mock.patch.object( - ConnRecord, "retrieve_request", autospec=True - ), async_mock.patch.object( - ConnRecord, "attach_request", autospec=True + ConnRecord, "connection_id", autospec=True ), async_mock.patch.object( ConnRecord, "save", autospec=True + ) as mock_conn_rec_save, async_mock.patch.object( + ConnRecord, "attach_request", autospec=True + ) as mock_conn_attach_request, async_mock.patch.object( + ConnRecord, "retrieve_by_id", autospec=True + ) as mock_conn_retrieve_by_id, async_mock.patch.object( + ConnRecord, "retrieve_request", autospec=True ), async_mock.patch.object( - InMemoryWallet, "create_local_did", autospec=True - ) as mock_wallet_create_local_did, async_mock.patch.object( - InMemoryWallet, "get_local_did", autospec=True - ) as mock_wallet_get_local_did, async_mock.patch.object( ConnRecord, "retrieve_by_invitation_msg_id", async_mock.CoroutineMock() ) as mock_conn_retrieve_by_invitation_msg_id: - mock_conn_retrieve_by_invitation_msg_id.return_value = ConnRecord() - mock_wallet_create_local_did.return_value = DIDInfo( - new_info.did, - new_info.verkey, - None, - method=DIDMethod.SOV, - key_type=KeyType.ED25519, - ) - mock_wallet_get_local_did.return_value = DIDInfo( - self.test_did, - self.test_verkey, - None, - method=DIDMethod.SOV, - key_type=KeyType.ED25519, - ) - await self.manager.receive_request(mock_request, receipt) - - self.multitenant_mgr.add_key.assert_called_once_with( - "test_wallet", new_info.verkey - ) + mock_conn_retrieve_by_invitation_msg_id.return_value = None + conn_rec = await self.manager.receive_request(mock_request, receipt) + assert conn_rec async def test_receive_request_public_did_no_did_doc(self): async with self.profile.session() as session: @@ -868,8 +737,8 @@ async def test_receive_request_public_did_no_did_doc(self): recipient_did=self.test_did, recipient_did_public=True ) await session.wallet.create_local_did( - method=DIDMethod.SOV, - key_type=KeyType.ED25519, + method=SOV, + key_type=ED25519, seed=None, did=self.test_did, ) @@ -899,8 +768,8 @@ async def test_receive_request_public_did_wrong_did(self): recipient_did=self.test_did, recipient_did_public=True ) await session.wallet.create_local_did( - method=DIDMethod.SOV, - key_type=KeyType.ED25519, + method=SOV, + key_type=ED25519, seed=None, did=self.test_did, ) @@ -928,8 +797,8 @@ async def test_receive_request_public_did_no_public_invites(self): receipt = MessageReceipt(recipient_did=self.test_did, recipient_did_public=True) async with self.profile.session() as session: await session.wallet.create_local_did( - method=DIDMethod.SOV, - key_type=KeyType.ED25519, + method=SOV, + key_type=ED25519, seed=None, did=self.test_did, ) @@ -959,8 +828,8 @@ async def test_receive_request_public_did_no_auto_accept(self): recipient_did=self.test_did, recipient_did_public=True ) await session.wallet.create_local_did( - method=DIDMethod.SOV, - key_type=KeyType.ED25519, + method=SOV, + key_type=ED25519, seed=None, did=self.test_did, ) @@ -986,136 +855,6 @@ async def test_receive_request_public_did_no_auto_accept(self): messages = self.responder.messages assert not messages - async def test_receive_request_mediation_id(self): - async with self.profile.session() as session: - - mock_request = async_mock.MagicMock() - mock_request.connection = async_mock.MagicMock() - mock_request.connection.did = self.test_did - mock_request.connection.did_doc = async_mock.MagicMock() - mock_request.connection.did_doc.did = self.test_did - - receipt = MessageReceipt( - recipient_did=self.test_did, recipient_did_public=False - ) - await session.wallet.create_local_did( - method=DIDMethod.SOV, - key_type=KeyType.ED25519, - seed=None, - did=self.test_did, - ) - - mediation_record = MediationRecord( - role=MediationRecord.ROLE_CLIENT, - state=MediationRecord.STATE_GRANTED, - connection_id=self.test_mediator_conn_id, - routing_keys=self.test_mediator_routing_keys, - endpoint=self.test_mediator_endpoint, - ) - await mediation_record.save(session) - - record, invite = await self.manager.create_invitation() - record.accept = ConnRecord.ACCEPT_MANUAL - - await record.save(session) - - with async_mock.patch.object( - ConnRecord, "save", autospec=True - ) as mock_conn_rec_save, async_mock.patch.object( - ConnRecord, "attach_request", autospec=True - ) as mock_conn_attach_request, async_mock.patch.object( - ConnRecord, "retrieve_by_invitation_key" - ) as mock_conn_retrieve_by_invitation_key, async_mock.patch.object( - ConnRecord, "retrieve_request", autospec=True - ): - mock_conn_retrieve_by_invitation_key.return_value = record - conn_rec = await self.manager.receive_request( - mock_request, receipt, mediation_id=mediation_record.mediation_id - ) - - assert len(self.responder.messages) == 1 - message, target = self.responder.messages[0] - assert isinstance(message, KeylistUpdate) - assert len(message.updates) == 1 - (remove,) = message.updates - assert remove.action == KeylistUpdateRule.RULE_REMOVE - assert remove.recipient_key == record.invitation_key - - async def test_receive_request_bad_mediation(self): - mock_request = async_mock.MagicMock() - mock_request.connection = async_mock.MagicMock() - mock_request.connection.did = self.test_did - mock_request.connection.did_doc = async_mock.MagicMock() - mock_request.connection.did_doc.did = self.test_did - receipt = MessageReceipt( - recipient_did=self.test_did, recipient_did_public=False - ) - record, invite = await self.manager.create_invitation() - with async_mock.patch.object( - ConnRecord, "save", autospec=True - ) as mock_conn_rec_save, async_mock.patch.object( - ConnRecord, "attach_request", autospec=True - ) as mock_conn_attach_request, async_mock.patch.object( - ConnRecord, "retrieve_by_invitation_key" - ) as mock_conn_retrieve_by_invitation_key, async_mock.patch.object( - ConnRecord, "retrieve_request", autospec=True - ): - mock_conn_retrieve_by_invitation_key.return_value = record - with self.assertRaises(StorageNotFoundError): - await self.manager.receive_request( - mock_request, receipt, mediation_id="not-a-mediation-id" - ) - - async def test_receive_request_mediation_not_granted(self): - async with self.profile.session() as session: - mock_request = async_mock.MagicMock() - mock_request.connection = async_mock.MagicMock() - mock_request.connection.did = self.test_did - mock_request.connection.did_doc = self.make_did_doc( - self.test_target_did, self.test_target_verkey - ) - mock_request.connection.did_doc.did = self.test_did - receipt = MessageReceipt( - recipient_did=self.test_did, recipient_did_public=False - ) - record, invite = await self.manager.create_invitation() - - mediation_record = MediationRecord( - role=MediationRecord.ROLE_CLIENT, - state=MediationRecord.STATE_DENIED, - connection_id=self.test_mediator_conn_id, - routing_keys=self.test_mediator_routing_keys, - endpoint=self.test_mediator_endpoint, - ) - - await mediation_record.save(session) - - with async_mock.patch.object( - ConnRecord, "save", autospec=True - ) as mock_conn_rec_save, async_mock.patch.object( - ConnRecord, "attach_request", autospec=True - ) as mock_conn_attach_request, async_mock.patch.object( - ConnRecord, "retrieve_by_invitation_key" - ) as mock_conn_retrieve_by_invitation_key, async_mock.patch.object( - ConnRecord, "retrieve_request", autospec=True - ): - mock_conn_retrieve_by_invitation_key.return_value = record - with self.assertRaises(BaseConnectionManagerError): - await self.manager.receive_request( - mock_request, - receipt, - mediation_id=mediation_record.mediation_id, - ) - - mediation_record.state = MediationRecord.STATE_REQUEST - await mediation_record.save(session) - with self.assertRaises(BaseConnectionManagerError): - await self.manager.receive_request( - mock_request, - receipt, - mediation_id=mediation_record.mediation_id, - ) - async def test_create_response(self): conn_rec = ConnRecord(state=ConnRecord.State.REQUEST.rfc160) @@ -1137,28 +876,62 @@ async def test_create_response_multitenant(self): {"wallet.id": "test_wallet", "multitenant.enabled": True} ) + mediation_record = MediationRecord( + role=MediationRecord.ROLE_CLIENT, + state=MediationRecord.STATE_GRANTED, + connection_id=self.test_mediator_conn_id, + routing_keys=self.test_mediator_routing_keys, + endpoint=self.test_mediator_endpoint, + ) + with async_mock.patch.object( - ConnectionResponse, "sign_field", autospec=True + ConnRecord, "log_state", autospec=True + ), async_mock.patch.object( + ConnRecord, "save", autospec=True + ), async_mock.patch.object( + ConnRecord, "metadata_get", async_mock.CoroutineMock(return_value=False) + ), async_mock.patch.object( + self.route_manager, + "mediation_record_for_connection", + async_mock.CoroutineMock(return_value=mediation_record), ), async_mock.patch.object( ConnRecord, "retrieve_request", autospec=True + ), async_mock.patch.object( + ConnectionResponse, "sign_field", autospec=True ), async_mock.patch.object( InMemoryWallet, "create_local_did", autospec=True - ) as mock_wallet_create_local_did: + ) as mock_wallet_create_local_did, async_mock.patch.object( + self.multitenant_mgr, + "get_default_mediator", + async_mock.CoroutineMock(return_value=mediation_record), + ), async_mock.patch.object( + ConnectionManager, "create_did_document", autospec=True + ) as create_did_document, async_mock.patch.object( + self.route_manager, + "mediation_record_for_connection", + async_mock.CoroutineMock(return_value=None), + ): mock_wallet_create_local_did.return_value = DIDInfo( self.test_did, self.test_verkey, None, - method=DIDMethod.SOV, - key_type=KeyType.ED25519, + method=SOV, + key_type=ED25519, ) await self.manager.create_response( ConnRecord( state=ConnRecord.State.REQUEST, - ) + ), + my_endpoint=self.test_endpoint, ) - self.multitenant_mgr.add_key.assert_called_once_with( - "test_wallet", self.test_verkey + create_did_document.assert_called_once_with( + self.manager, + mock_wallet_create_local_did.return_value, + None, + [self.test_endpoint], + mediation_records=[mediation_record], ) + self.route_manager.route_connection_as_inviter.assert_called_once() async def test_create_response_bad_state(self): with self.assertRaises(ConnectionManagerError): @@ -1173,78 +946,67 @@ async def test_create_response_bad_state(self): ) async def test_create_response_mediation(self): - async with self.profile.session() as session: - mediation_record = MediationRecord( - role=MediationRecord.ROLE_CLIENT, - state=MediationRecord.STATE_GRANTED, - connection_id=self.test_mediator_conn_id, - routing_keys=self.test_mediator_routing_keys, - endpoint=self.test_mediator_endpoint, - ) - await mediation_record.save(session) + mediation_record = MediationRecord( + role=MediationRecord.ROLE_CLIENT, + state=MediationRecord.STATE_GRANTED, + connection_id=self.test_mediator_conn_id, + routing_keys=self.test_mediator_routing_keys, + endpoint=self.test_mediator_endpoint, + ) - conn_rec = ConnRecord( - state=ConnRecord.State.REQUEST.rfc160, - ) - conn_rec.my_did = None + record = ConnRecord( + connection_id="test-conn-id", + invitation_key=self.test_verkey, + their_label="Hello", + their_role=ConnRecord.Role.RESPONDER.rfc160, + alias="Bob", + state=ConnRecord.State.REQUEST.rfc160, + ) - with async_mock.patch.object( - ConnRecord, "log_state", autospec=True - ) as mock_conn_log_state, async_mock.patch.object( - ConnRecord, "retrieve_request", autospec=True - ) as mock_conn_retrieve_request, async_mock.patch.object( - ConnRecord, "save", autospec=True - ) as mock_conn_save, async_mock.patch.object( - ConnectionResponse, "sign_field", autospec=True - ) as mock_sign, async_mock.patch.object( - conn_rec, "metadata_get", async_mock.CoroutineMock(return_value=False) - ): - await self.manager.create_response( - conn_rec, mediation_id=mediation_record.mediation_id - ) + # Ensure the path with new did creation is hit + record.my_did = None - assert len(self.responder.messages) == 1 - message, target = self.responder.messages[0] - assert isinstance(message, KeylistUpdate) - assert len(message.updates) == 1 - (add,) = message.updates - assert add.action == KeylistUpdateRule.RULE_ADD - assert add.recipient_key - - async def test_create_response_bad_mediation(self): - record = async_mock.MagicMock() - with self.assertRaises(StorageNotFoundError): + with async_mock.patch.object( + ConnRecord, "log_state", autospec=True + ), async_mock.patch.object( + ConnRecord, "save", autospec=True + ), async_mock.patch.object( + record, "metadata_get", async_mock.CoroutineMock(return_value=False) + ), async_mock.patch.object( + ConnectionManager, "create_did_document", autospec=True + ) as create_did_document, async_mock.patch.object( + InMemoryWallet, "create_local_did" + ) as create_local_did, async_mock.patch.object( + self.route_manager, + "mediation_record_for_connection", + async_mock.CoroutineMock(return_value=mediation_record), + ), async_mock.patch.object( + record, "retrieve_request", autospec=True + ), async_mock.patch.object( + ConnectionResponse, "sign_field", autospec=True + ): + did_info = DIDInfo( + did=self.test_did, + verkey=self.test_verkey, + metadata={}, + method=SOV, + key_type=ED25519, + ) + create_local_did.return_value = did_info await self.manager.create_response( - record, mediation_id="not-a-mediation-id" + record, + mediation_id=mediation_record.mediation_id, + my_endpoint=self.test_endpoint, ) - - async def test_create_response_mediation_not_granted(self): - async with self.profile.session() as session: - record = ConnRecord(state=ConnRecord.State.REQUEST) - with async_mock.patch.object( - ConnRecord, "retrieve_request" - ) as retrieve_request, async_mock.patch.object( - ConnectionResponse, "sign_field", autospec=True - ) as mock_sign: - mediation_record = MediationRecord( - role=MediationRecord.ROLE_CLIENT, - state=MediationRecord.STATE_DENIED, - connection_id=self.test_mediator_conn_id, - routing_keys=self.test_mediator_routing_keys, - endpoint=self.test_mediator_endpoint, - ) - await mediation_record.save(session) - with self.assertRaises(BaseConnectionManagerError): - await self.manager.create_response( - record, mediation_id=mediation_record.mediation_id - ) - - mediation_record.state = MediationRecord.STATE_REQUEST - await mediation_record.save(session) - with self.assertRaises(BaseConnectionManagerError): - await self.manager.create_response( - record, mediation_id=mediation_record.mediation_id - ) + create_local_did.assert_called_once_with(SOV, ED25519) + create_did_document.assert_called_once_with( + self.manager, + did_info, + None, + [self.test_endpoint], + mediation_records=[mediation_record], + ) + self.route_manager.route_connection_as_inviter.assert_called_once() async def test_create_response_auto_send_mediation_request(self): conn_rec = ConnRecord( @@ -1277,7 +1039,9 @@ async def test_accept_response_find_by_thread_id(self): mock_response.connection.did = self.test_target_did mock_response.connection.did_doc = async_mock.MagicMock() mock_response.connection.did_doc.did = self.test_target_did - + mock_response.verify_signed_field = async_mock.CoroutineMock( + return_value="sig_verkey" + ) receipt = MessageReceipt(recipient_did=self.test_did, recipient_did_public=True) with async_mock.patch.object( @@ -1294,6 +1058,7 @@ async def test_accept_response_find_by_thread_id(self): save=async_mock.CoroutineMock(), metadata_get=async_mock.CoroutineMock(), connection_id="test-conn-id", + invitation_key="test-invitation-key", ) conn_rec = await self.manager.accept_response(mock_response, receipt) assert conn_rec.their_did == self.test_target_did @@ -1306,6 +1071,9 @@ async def test_accept_response_not_found_by_thread_id_receipt_has_sender_did(sel mock_response.connection.did = self.test_target_did mock_response.connection.did_doc = async_mock.MagicMock() mock_response.connection.did_doc.did = self.test_target_did + mock_response.verify_signed_field = async_mock.CoroutineMock( + return_value="sig_verkey" + ) receipt = MessageReceipt(sender_did=self.test_target_did) @@ -1326,6 +1094,7 @@ async def test_accept_response_not_found_by_thread_id_receipt_has_sender_did(sel save=async_mock.CoroutineMock(), metadata_get=async_mock.CoroutineMock(return_value=False), connection_id="test-conn-id", + invitation_key="test-invitation-id", ) conn_rec = await self.manager.accept_response(mock_response, receipt) @@ -1426,14 +1195,47 @@ async def test_accept_response_find_by_thread_id_did_mismatch(self): with self.assertRaises(ConnectionManagerError): await self.manager.accept_response(mock_response, receipt) - async def test_accept_response_auto_send_mediation_request(self): + async def test_accept_response_verify_invitation_key_sign_failure(self): mock_response = async_mock.MagicMock() mock_response._thread = async_mock.MagicMock() mock_response.connection = async_mock.MagicMock() mock_response.connection.did = self.test_target_did mock_response.connection.did_doc = async_mock.MagicMock() mock_response.connection.did_doc.did = self.test_target_did + mock_response.verify_signed_field = async_mock.CoroutineMock( + side_effect=ValueError + ) + receipt = MessageReceipt(recipient_did=self.test_did, recipient_did_public=True) + with async_mock.patch.object( + ConnRecord, "save", autospec=True + ) as mock_conn_rec_save, async_mock.patch.object( + ConnRecord, "retrieve_by_request_id", async_mock.CoroutineMock() + ) as mock_conn_retrieve_by_req_id, async_mock.patch.object( + MediationManager, "get_default_mediator", async_mock.CoroutineMock() + ): + mock_conn_retrieve_by_req_id.return_value = async_mock.MagicMock( + did=self.test_target_did, + did_doc=async_mock.MagicMock(did=self.test_target_did), + state=ConnRecord.State.RESPONSE.rfc23, + save=async_mock.CoroutineMock(), + metadata_get=async_mock.CoroutineMock(), + connection_id="test-conn-id", + invitation_key="test-invitation-key", + ) + with self.assertRaises(ConnectionManagerError): + await self.manager.accept_response(mock_response, receipt) + + async def test_accept_response_auto_send_mediation_request(self): + mock_response = async_mock.MagicMock() + mock_response._thread = async_mock.MagicMock() + mock_response.connection = async_mock.MagicMock() + mock_response.connection.did = self.test_target_did + mock_response.connection.did_doc = async_mock.MagicMock() + mock_response.connection.did_doc.did = self.test_target_did + mock_response.verify_signed_field = async_mock.CoroutineMock( + return_value="sig_verkey" + ) receipt = MessageReceipt(recipient_did=self.test_did, recipient_did_public=True) with async_mock.patch.object( @@ -1450,6 +1252,7 @@ async def test_accept_response_auto_send_mediation_request(self): save=async_mock.CoroutineMock(), metadata_get=async_mock.CoroutineMock(return_value=True), connection_id="test-conn-id", + invitation_key="test-invitation-key", ) conn_rec = await self.manager.accept_response(mock_response, receipt) assert conn_rec.their_did == self.test_target_did @@ -1511,8 +1314,8 @@ async def test_create_static_connection_multitenant(self): self.test_did, self.test_verkey, None, - method=DIDMethod.SOV, - key_type=KeyType.ED25519, + method=SOV, + key_type=ED25519, ) await self.manager.create_static_connection( @@ -1522,9 +1325,7 @@ async def test_create_static_connection_multitenant(self): their_endpoint=self.test_endpoint, ) - self.multitenant_mgr.add_key.assert_called_once_with( - "test_wallet", self.test_verkey - ) + self.route_manager.route_static.assert_called_once() async def test_create_static_connection_multitenant_auto_disclose_features(self): self.context.update_settings( @@ -1546,8 +1347,8 @@ async def test_create_static_connection_multitenant_auto_disclose_features(self) self.test_did, self.test_verkey, None, - method=DIDMethod.SOV, - key_type=KeyType.ED25519, + method=SOV, + key_type=ED25519, ) await self.manager.create_static_connection( my_did=self.test_did, @@ -1555,9 +1356,7 @@ async def test_create_static_connection_multitenant_auto_disclose_features(self) their_verkey=self.test_target_verkey, their_endpoint=self.test_endpoint, ) - self.multitenant_mgr.add_key.assert_called_once_with( - "test_wallet", self.test_verkey - ) + self.route_manager.route_static.assert_called_once() mock_proactive_disclose_features.assert_called_once() async def test_create_static_connection_multitenant_mediator(self): @@ -1580,8 +1379,8 @@ async def test_create_static_connection_multitenant_mediator(self): self.test_did, self.test_verkey, None, - method=DIDMethod.SOV, - key_type=KeyType.ED25519, + method=SOV, + key_type=ED25519, ) # With default mediator @@ -1602,14 +1401,14 @@ async def test_create_static_connection_multitenant_mediator(self): their_endpoint=self.test_endpoint, ) - assert self.multitenant_mgr.add_key.call_count is 2 + assert self.route_manager.route_static.call_count == 2 their_info = DIDInfo( self.test_target_did, self.test_target_verkey, {}, - method=DIDMethod.SOV, - key_type=KeyType.ED25519, + method=SOV, + key_type=ED25519, ) create_did_document.assert_has_calls( [ @@ -1619,9 +1418,7 @@ async def test_create_static_connection_multitenant_mediator(self): [self.test_endpoint], mediation_records=[default_mediator], ), - call( - their_info, None, [self.test_endpoint], mediation_records=None - ), + call(their_info, None, [self.test_endpoint], mediation_records=[]), ] ) @@ -1788,8 +1585,8 @@ async def test_resolve_inbound_connection(self): self.test_did, self.test_verkey, {"posted": True}, - method=DIDMethod.SOV, - key_type=KeyType.ED25519, + method=SOV, + key_type=ED25519, ) mock_mgr_find_conn.return_value = mock_conn @@ -1840,8 +1637,8 @@ async def test_create_did_document(self): self.test_did, self.test_verkey, None, - method=DIDMethod.SOV, - key_type=KeyType.ED25519, + method=SOV, + key_type=ED25519, ) mock_conn = async_mock.MagicMock( @@ -1873,8 +1670,8 @@ async def test_create_did_document_not_active(self): self.test_did, self.test_verkey, None, - method=DIDMethod.SOV, - key_type=KeyType.ED25519, + method=SOV, + key_type=ED25519, ) mock_conn = async_mock.MagicMock( @@ -1901,8 +1698,8 @@ async def test_create_did_document_no_services(self): self.test_did, self.test_verkey, None, - method=DIDMethod.SOV, - key_type=KeyType.ED25519, + method=SOV, + key_type=ED25519, ) mock_conn = async_mock.MagicMock( @@ -1936,8 +1733,8 @@ async def test_create_did_document_no_service_endpoint(self): self.test_did, self.test_verkey, None, - method=DIDMethod.SOV, - key_type=KeyType.ED25519, + method=SOV, + key_type=ED25519, ) mock_conn = async_mock.MagicMock( @@ -1974,8 +1771,8 @@ async def test_create_did_document_no_service_recip_keys(self): self.test_did, self.test_verkey, None, - method=DIDMethod.SOV, - key_type=KeyType.ED25519, + method=SOV, + key_type=ED25519, ) mock_conn = async_mock.MagicMock( @@ -2020,8 +1817,8 @@ async def test_create_did_document_mediation(self): self.test_did, self.test_verkey, None, - method=DIDMethod.SOV, - key_type=KeyType.ED25519, + method=SOV, + key_type=ED25519, ) mediation_record = MediationRecord( role=MediationRecord.ROLE_CLIENT, @@ -2046,8 +1843,8 @@ async def test_create_did_document_multiple_mediators(self): self.test_did, self.test_verkey, None, - method=DIDMethod.SOV, - key_type=KeyType.ED25519, + method=SOV, + key_type=ED25519, ) mediation_record1 = MediationRecord( role=MediationRecord.ROLE_CLIENT, @@ -2079,8 +1876,8 @@ async def test_create_did_document_mediation_svc_endpoints_overwritten(self): self.test_did, self.test_verkey, None, - method=DIDMethod.SOV, - key_type=KeyType.ED25519, + method=SOV, + key_type=ED25519, ) mediation_record = MediationRecord( role=MediationRecord.ROLE_CLIENT, @@ -2114,8 +1911,8 @@ async def test_did_key_storage(self): async def test_get_connection_targets_conn_invitation_no_did(self): async with self.profile.session() as session: local_did = await session.wallet.create_local_did( - method=DIDMethod.SOV, - key_type=KeyType.ED25519, + method=SOV, + key_type=ED25519, seed=self.test_seed, did=self.test_did, metadata=None, @@ -2173,8 +1970,8 @@ async def test_get_connection_targets_conn_invitation_no_did(self): async def test_get_connection_targets_retrieve_connection(self): async with self.profile.session() as session: local_did = await session.wallet.create_local_did( - method=DIDMethod.SOV, - key_type=KeyType.ED25519, + method=SOV, + key_type=ED25519, seed=self.test_seed, did=self.test_did, metadata=None, @@ -2226,8 +2023,8 @@ async def test_get_conn_targets_conn_invitation_no_cache(self): async with self.profile.session() as session: self.context.injector.clear_binding(BaseCache) local_did = await session.wallet.create_local_did( - method=DIDMethod.SOV, - key_type=KeyType.ED25519, + method=SOV, + key_type=ED25519, seed=self.test_seed, did=self.test_did, metadata=None, @@ -2274,12 +2071,10 @@ async def test_fetch_connection_targets_no_my_did(self): async def test_fetch_connection_targets_conn_invitation_did_no_resolver(self): async with self.profile.session() as session: - self.context.injector.bind_instance( - DIDResolver, DIDResolver(DIDResolverRegistry()) - ) + self.context.injector.bind_instance(DIDResolver, DIDResolver([])) await session.wallet.create_local_did( - method=DIDMethod.SOV, - key_type=KeyType.ED25519, + method=SOV, + key_type=ED25519, seed=self.test_seed, did=self.test_did, metadata=None, @@ -2321,11 +2116,14 @@ async def test_fetch_connection_targets_conn_invitation_did_resolver(self): return_value=self.test_endpoint ) self.resolver.resolve = async_mock.CoroutineMock(return_value=did_doc) + self.resolver.dereference = async_mock.CoroutineMock( + return_value=did_doc.verification_method[0] + ) self.context.injector.bind_instance(DIDResolver, self.resolver) local_did = await session.wallet.create_local_did( - method=DIDMethod.SOV, - key_type=KeyType.ED25519, + method=SOV, + key_type=ED25519, seed=self.test_seed, did=self.test_did, metadata=None, @@ -2390,10 +2188,13 @@ async def test_fetch_connection_targets_conn_invitation_btcr_resolver(self): return_value=self.test_endpoint ) self.resolver.resolve = async_mock.CoroutineMock(return_value=did_doc) + self.resolver.dereference = async_mock.CoroutineMock( + return_value=did_doc.verification_method[0] + ) self.context.injector.bind_instance(DIDResolver, self.resolver) local_did = await session.wallet.create_local_did( - method=DIDMethod.SOV, - key_type=KeyType.ED25519, + method=SOV, + key_type=ED25519, seed=self.test_seed, did=did_doc.id, metadata=None, @@ -2457,8 +2258,8 @@ async def test_fetch_connection_targets_conn_invitation_btcr_without_services(se self.context.injector.bind_instance(DIDResolver, self.resolver) local_did = await session.wallet.create_local_did( - method=DIDMethod.SOV, - key_type=KeyType.ED25519, + method=SOV, + key_type=ED25519, seed=self.test_seed, did=did_doc.id, metadata=None, @@ -2497,8 +2298,8 @@ async def test_fetch_connection_targets_conn_invitation_no_didcomm_services(self self.resolver.resolve = async_mock.CoroutineMock(return_value=did_doc) self.context.injector.bind_instance(DIDResolver, self.resolver) await session.wallet.create_local_did( - method=DIDMethod.SOV, - key_type=KeyType.ED25519, + method=SOV, + key_type=ED25519, seed=self.test_seed, did=did_doc.id, metadata=None, @@ -2522,13 +2323,81 @@ async def test_fetch_connection_targets_conn_invitation_no_didcomm_services(self with self.assertRaises(BaseConnectionManagerError): await self.manager.fetch_connection_targets(mock_conn) + async def test_fetch_connection_targets_conn_invitation_supported_JsonWebKey2020_key_type( + self, + ): + async with self.profile.session() as session: + builder = DIDDocumentBuilder("did:btcr:x705-jznz-q3nl-srs") + vmethod = builder.verification_method.add( + JsonWebKey2020, + ident="1", + public_key_jwk={ + "kty": "OKP", + "crv": "Ed25519", + "x": bytes_to_b64(b58_to_bytes(self.test_target_verkey), True), + }, + ) + builder.service.add_didcomm( + type_="IndyAgent", + service_endpoint=self.test_endpoint, + recipient_keys=[vmethod], + ) + did_doc = builder.build() + self.resolver = async_mock.MagicMock() + self.resolver.get_endpoint_for_did = async_mock.CoroutineMock( + return_value=self.test_endpoint + ) + self.resolver.resolve = async_mock.CoroutineMock(return_value=did_doc) + self.resolver.dereference = async_mock.CoroutineMock( + return_value=did_doc.verification_method[0] + ) + self.context.injector.bind_instance(DIDResolver, self.resolver) + local_did = await session.wallet.create_local_did( + method=SOV, + key_type=ED25519, + seed=self.test_seed, + did=did_doc.id, + metadata=None, + ) + + conn_invite = ConnectionInvitation( + did=did_doc.id, + endpoint=self.test_endpoint, + recipient_keys=[vmethod.public_key_jwk], + routing_keys=[self.test_verkey], + label="label", + ) + mock_conn = async_mock.MagicMock( + my_did=did_doc.id, + their_did=self.test_target_did, + connection_id="dummy", + their_role=ConnRecord.Role.RESPONDER.rfc23, + state=ConnRecord.State.INVITATION.rfc23, + retrieve_invitation=async_mock.CoroutineMock(return_value=conn_invite), + ) + + targets = await self.manager.fetch_connection_targets(mock_conn) + assert len(targets) == 1 + target = targets[0] + assert target.did == mock_conn.their_did + assert target.endpoint == self.test_endpoint + assert target.label == conn_invite.label + assert target.recipient_keys == [self.test_target_verkey] + assert target.routing_keys == [] + assert target.sender_key == local_did.verkey + async def test_fetch_connection_targets_conn_invitation_unsupported_key_type(self): async with self.profile.session() as session: builder = DIDDocumentBuilder("did:btcr:x705-jznz-q3nl-srs") vmethod = builder.verification_method.add( JsonWebKey2020, ident="1", - public_key_jwk={"jwk": "stuff"}, + public_key_jwk={ + "kty": "EC", + "crv": "P-256", + "x": "2syLh57B-dGpa0F8p1JrO6JU7UUSF6j7qL-vfk1eOoY", + "y": "BgsGtI7UPsObMRjdElxLOrgAO9JggNMjOcfzEPox18w", + }, ) builder.service.add_didcomm( type_="IndyAgent", @@ -2541,10 +2410,13 @@ async def test_fetch_connection_targets_conn_invitation_unsupported_key_type(sel return_value=self.test_endpoint ) self.resolver.resolve = async_mock.CoroutineMock(return_value=did_doc) + self.resolver.dereference = async_mock.CoroutineMock( + return_value=did_doc.verification_method[0] + ) self.context.injector.bind_instance(DIDResolver, self.resolver) local_did = await session.wallet.create_local_did( - method=DIDMethod.SOV, - key_type=KeyType.ED25519, + method=SOV, + key_type=ED25519, seed=self.test_seed, did=did_doc.id, metadata=None, @@ -2570,12 +2442,10 @@ async def test_fetch_connection_targets_conn_invitation_unsupported_key_type(sel async def test_fetch_connection_targets_oob_invitation_svc_did_no_resolver(self): async with self.profile.session() as session: - self.context.injector.bind_instance( - DIDResolver, DIDResolver(DIDResolverRegistry()) - ) + self.context.injector.bind_instance(DIDResolver, DIDResolver([])) await session.wallet.create_local_did( - method=DIDMethod.SOV, - key_type=KeyType.ED25519, + method=SOV, + key_type=ED25519, seed=self.test_seed, did=self.test_did, metadata=None, @@ -2612,11 +2482,14 @@ async def test_fetch_connection_targets_oob_invitation_svc_did_resolver(self): self.resolver = async_mock.MagicMock() self.resolver.resolve = async_mock.CoroutineMock(return_value=did_doc) + self.resolver.dereference = async_mock.CoroutineMock( + return_value=did_doc.verification_method[0] + ) self.context.injector.bind_instance(DIDResolver, self.resolver) local_did = await session.wallet.create_local_did( - method=DIDMethod.SOV, - key_type=KeyType.ED25519, + method=SOV, + key_type=ED25519, seed=self.test_seed, did=self.test_did, metadata=None, @@ -2660,8 +2533,8 @@ async def test_fetch_connection_targets_oob_invitation_svc_block_resolver(self): self.context.injector.bind_instance(DIDResolver, self.resolver) local_did = await session.wallet.create_local_did( - method=DIDMethod.SOV, - key_type=KeyType.ED25519, + method=SOV, + key_type=ED25519, seed=self.test_seed, did=self.test_did, metadata=None, @@ -2675,7 +2548,7 @@ async def test_fetch_connection_targets_oob_invitation_svc_block_resolver(self): service_endpoint=self.test_endpoint, recipient_keys=[ DIDKey.from_public_key_b58( - self.test_target_verkey, KeyType.ED25519 + self.test_target_verkey, ED25519 ).did ], routing_keys=[], @@ -2706,8 +2579,8 @@ async def test_fetch_connection_targets_oob_invitation_svc_block_resolver(self): async def test_fetch_connection_targets_conn_initiator_completed_no_their_did(self): async with self.profile.session() as session: await session.wallet.create_local_did( - method=DIDMethod.SOV, - key_type=KeyType.ED25519, + method=SOV, + key_type=ED25519, seed=self.test_seed, did=self.test_did, metadata=None, @@ -2723,8 +2596,8 @@ async def test_fetch_connection_targets_conn_initiator_completed_no_their_did(se async def test_fetch_connection_targets_conn_completed_their_did(self): async with self.profile.session() as session: local_did = await session.wallet.create_local_did( - method=DIDMethod.SOV, - key_type=KeyType.ED25519, + method=SOV, + key_type=ED25519, seed=self.test_seed, did=self.test_did, metadata=None, @@ -2754,8 +2627,8 @@ async def test_fetch_connection_targets_conn_completed_their_did(self): async def test_fetch_connection_targets_conn_no_invi_with_their_did(self): async with self.profile.session() as session: local_did = await session.wallet.create_local_did( - method=DIDMethod.SOV, - key_type=KeyType.ED25519, + method=SOV, + key_type=ED25519, seed=self.test_seed, did=self.test_did, metadata=None, @@ -2809,8 +2682,8 @@ async def test_diddoc_connection_targets_diddoc_underspecified(self): async def test_establish_inbound(self): async with self.profile.session() as session: await session.wallet.create_local_did( - method=DIDMethod.SOV, - key_type=KeyType.ED25519, + method=SOV, + key_type=ED25519, seed=self.test_seed, did=self.test_did, metadata=None, @@ -2839,8 +2712,8 @@ async def test_establish_inbound(self): async def test_establish_inbound_conn_rec_no_my_did(self): async with self.profile.session() as session: await session.wallet.create_local_did( - method=DIDMethod.SOV, - key_type=KeyType.ED25519, + method=SOV, + key_type=ED25519, seed=self.test_seed, did=self.test_did, metadata=None, @@ -2868,8 +2741,8 @@ async def test_establish_inbound_conn_rec_no_my_did(self): async def test_establish_inbound_no_conn_record(self): async with self.profile.session() as session: await session.wallet.create_local_did( - method=DIDMethod.SOV, - key_type=KeyType.ED25519, + method=SOV, + key_type=ED25519, seed=self.test_seed, did=self.test_did, metadata=None, @@ -2897,8 +2770,8 @@ async def test_establish_inbound_no_conn_record(self): async def test_establish_inbound_router_not_ready(self): async with self.profile.session() as session: await session.wallet.create_local_did( - method=DIDMethod.SOV, - key_type=KeyType.ED25519, + method=SOV, + key_type=ED25519, seed=self.test_seed, did=self.test_did, metadata=None, diff --git a/aries_cloudagent/protocols/connections/v1_0/tests/test_routes.py b/aries_cloudagent/protocols/connections/v1_0/tests/test_routes.py index be40c47e1d..adc23cf242 100644 --- a/aries_cloudagent/protocols/connections/v1_0/tests/test_routes.py +++ b/aries_cloudagent/protocols/connections/v1_0/tests/test_routes.py @@ -5,6 +5,8 @@ from asynctest import mock as async_mock from .....admin.request_context import AdminRequestContext +from .....cache.base import BaseCache +from .....cache.in_memory import InMemoryCache from .....connections.models.conn_record import ConnRecord from .....storage.error import StorageNotFoundError @@ -32,6 +34,8 @@ async def test_connections_list(self): "their_role": ConnRecord.Role.REQUESTER.rfc160, "connection_protocol": ConnRecord.Protocol.RFC_0160.aries_protocol, "invitation_key": "some-invitation-key", + "their_public_did": "a_public_did", + "invitation_msg_id": "dummy_msg", } STATE_COMPLETED = ConnRecord.State.COMPLETED @@ -89,7 +93,12 @@ async def test_connections_list(self): await test_module.connections_list(self.request) mock_conn_rec.query.assert_called_once_with( ANY, - {"invitation_id": "dummy", "invitation_key": "some-invitation-key"}, + { + "invitation_id": "dummy", + "invitation_key": "some-invitation-key", + "their_public_did": "a_public_did", + "invitation_msg_id": "dummy_msg", + }, post_filter_positive={ "their_role": [v for v in ConnRecord.Role.REQUESTER.value], "connection_protocol": ConnRecord.Protocol.RFC_0160.aries_protocol, @@ -353,7 +362,6 @@ async def test_connections_create_invitation(self): ) as mock_conn_mgr, async_mock.patch.object( test_module.web, "json_response" ) as mock_response: - mock_conn_mgr.return_value.create_invitation = async_mock.CoroutineMock( return_value=( async_mock.MagicMock( # connection record @@ -536,7 +544,6 @@ async def test_connections_accept_invitation(self): ) as mock_conn_mgr, async_mock.patch.object( test_module.web, "json_response" ) as mock_response: - mock_conn_rec_retrieve_by_id.return_value = mock_conn_rec mock_conn_mgr.return_value.create_request = async_mock.CoroutineMock() @@ -700,6 +707,26 @@ async def test_connections_remove(self): await test_module.connections_remove(self.request) mock_response.assert_called_once_with({}) + async def test_connections_remove_cache_key(self): + cache = InMemoryCache() + profile = self.context.profile + await cache.set("conn_rec_state::dummy", "active") + profile.context.injector.bind_instance(BaseCache, cache) + self.request.match_info = {"conn_id": "dummy"} + mock_conn_rec = async_mock.MagicMock() + mock_conn_rec.delete_record = async_mock.CoroutineMock() + assert (await cache.get("conn_rec_state::dummy")) == "active" + with async_mock.patch.object( + test_module.ConnRecord, "retrieve_by_id", async_mock.CoroutineMock() + ) as mock_conn_rec_retrieve_by_id, async_mock.patch.object( + test_module.web, "json_response" + ) as mock_response: + mock_conn_rec_retrieve_by_id.return_value = mock_conn_rec + + await test_module.connections_remove(self.request) + mock_response.assert_called_once_with({}) + assert not (await cache.get("conn_rec_state::dummy")) + async def test_connections_remove_not_found(self): self.request.match_info = {"conn_id": "dummy"} diff --git a/aries_cloudagent/protocols/coordinate_mediation/v1_0/handlers/keylist_update_handler.py b/aries_cloudagent/protocols/coordinate_mediation/v1_0/handlers/keylist_update_handler.py index 8e8f8922a5..20c63a5e15 100644 --- a/aries_cloudagent/protocols/coordinate_mediation/v1_0/handlers/keylist_update_handler.py +++ b/aries_cloudagent/protocols/coordinate_mediation/v1_0/handlers/keylist_update_handler.py @@ -32,6 +32,7 @@ async def handle(self, context: RequestContext, responder: BaseResponder): session, context.connection_record.connection_id ) response = await mgr.update_keylist(record, updates=context.message.updates) + response.assign_thread_from(context.message) await responder.send_reply(response) except (StorageNotFoundError, MediationNotGrantedError): reply = CMProblemReport( diff --git a/aries_cloudagent/protocols/coordinate_mediation/v1_0/handlers/keylist_update_response_handler.py b/aries_cloudagent/protocols/coordinate_mediation/v1_0/handlers/keylist_update_response_handler.py index bcca040072..d2c33704d8 100644 --- a/aries_cloudagent/protocols/coordinate_mediation/v1_0/handlers/keylist_update_response_handler.py +++ b/aries_cloudagent/protocols/coordinate_mediation/v1_0/handlers/keylist_update_response_handler.py @@ -1,11 +1,14 @@ """Handler for keylist-update-response message.""" +from .....core.profile import Profile from .....messaging.base_handler import BaseHandler, HandlerException from .....messaging.request_context import RequestContext from .....messaging.responder import BaseResponder - -from ..messages.keylist_update_response import KeylistUpdateResponse +from .....storage.error import StorageNotFoundError +from .....wallet.error import WalletNotFoundError from ..manager import MediationManager +from ..messages.keylist_update_response import KeylistUpdateResponse +from ..route_manager import RouteManager class KeylistUpdateResponseHandler(BaseHandler): @@ -25,3 +28,39 @@ async def handle(self, context: RequestContext, responder: BaseResponder): await mgr.store_update_results( context.connection_record.connection_id, context.message.updated ) + await self.notify_keylist_updated( + context.profile, context.connection_record.connection_id, context.message + ) + + async def notify_keylist_updated( + self, profile: Profile, connection_id: str, response: KeylistUpdateResponse + ): + """Notify of keylist update response received.""" + route_manager = profile.inject(RouteManager) + self._logger.debug( + "Retrieving connection ID from route manager of type %s", + type(route_manager).__name__, + ) + try: + key_to_connection = { + updated.recipient_key: await route_manager.connection_from_recipient_key( + profile, updated.recipient_key + ) + for updated in response.updated + } + except (StorageNotFoundError, WalletNotFoundError) as err: + raise HandlerException( + "Unknown recipient key received in keylist update response" + ) from err + + await profile.notify( + MediationManager.KEYLIST_UPDATED_EVENT, + { + "connection_id": connection_id, + "thread_id": response._thread_id, + "updated": [update.serialize() for update in response.updated], + "mediated_connections": { + key: conn.connection_id for key, conn in key_to_connection.items() + }, + }, + ) diff --git a/aries_cloudagent/protocols/coordinate_mediation/v1_0/handlers/tests/test_keylist_query_handler.py b/aries_cloudagent/protocols/coordinate_mediation/v1_0/handlers/tests/test_keylist_query_handler.py index 6d6d7d9152..fd216be4a7 100644 --- a/aries_cloudagent/protocols/coordinate_mediation/v1_0/handlers/tests/test_keylist_query_handler.py +++ b/aries_cloudagent/protocols/coordinate_mediation/v1_0/handlers/tests/test_keylist_query_handler.py @@ -18,6 +18,7 @@ TEST_CONN_ID = "conn-id" TEST_VERKEY = "3Dn1SJNPaCXcvvJvSbsFWP2xaCjMom3can8CQNhWrTRx" +TEST_VERKEY_DIDKEY = "did:key:z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL" class TestKeylistQueryHandler(AsyncTestCase): @@ -77,4 +78,4 @@ async def test_handler(self): result, _target = responder.messages[0] assert isinstance(result, Keylist) assert len(result.keys) == 1 - assert result.keys[0].recipient_key == TEST_VERKEY + assert result.keys[0].recipient_key == TEST_VERKEY_DIDKEY diff --git a/aries_cloudagent/protocols/coordinate_mediation/v1_0/handlers/tests/test_keylist_update_response_handler.py b/aries_cloudagent/protocols/coordinate_mediation/v1_0/handlers/tests/test_keylist_update_response_handler.py index c9e3bb868f..b5774c5723 100644 --- a/aries_cloudagent/protocols/coordinate_mediation/v1_0/handlers/tests/test_keylist_update_response_handler.py +++ b/aries_cloudagent/protocols/coordinate_mediation/v1_0/handlers/tests/test_keylist_update_response_handler.py @@ -1,10 +1,14 @@ """Test handler for keylist-update-response message.""" +from functools import partial +from typing import AsyncGenerator import pytest from asynctest import TestCase as AsyncTestCase from asynctest import mock as async_mock + from ......connections.models.conn_record import ConnRecord +from ......core.event_bus import EventBus, MockEventBus from ......messaging.base_handler import HandlerException from ......messaging.request_context import RequestContext from ......messaging.responder import MockResponder @@ -12,10 +16,14 @@ from ...messages.inner.keylist_updated import KeylistUpdated from ...messages.keylist_update_response import KeylistUpdateResponse from ...manager import MediationManager +from ...route_manager import RouteManager +from ...tests.test_route_manager import MockRouteManager from ..keylist_update_response_handler import KeylistUpdateResponseHandler TEST_CONN_ID = "conn-id" -TEST_VERKEY = "3Dn1SJNPaCXcvvJvSbsFWP2xaCjMom3can8CQNhWrTRx" +TEST_THREAD_ID = "thread-id" +TEST_VERKEY = "did:key:z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL" +TEST_ROUTE_VERKEY = "did:key:z6MknxTj6Zj1VrDWc1ofaZtmCVv2zNXpD58Xup4ijDGoQhya" class TestKeylistUpdateResponseHandler(AsyncTestCase): @@ -34,6 +42,14 @@ async def setUp(self): self.context.message = KeylistUpdateResponse(updated=self.updated) self.context.connection_ready = True self.context.connection_record = ConnRecord(connection_id=TEST_CONN_ID) + self.mock_event_bus = MockEventBus() + self.context.profile.context.injector.bind_instance( + EventBus, self.mock_event_bus + ) + self.route_manager = MockRouteManager() + self.context.profile.context.injector.bind_instance( + RouteManager, self.route_manager + ) async def test_handler_no_active_connection(self): handler, responder = KeylistUpdateResponseHandler(), MockResponder() @@ -46,6 +62,87 @@ async def test_handler(self): handler, responder = KeylistUpdateResponseHandler(), MockResponder() with async_mock.patch.object( MediationManager, "store_update_results" - ) as mock_method: + ) as mock_store, async_mock.patch.object( + handler, "notify_keylist_updated" + ) as mock_notify: await handler.handle(self.context, responder) - mock_method.assert_called_once_with(TEST_CONN_ID, self.updated) + mock_store.assert_called_once_with(TEST_CONN_ID, self.updated) + mock_notify.assert_called_once_with( + self.context.profile, TEST_CONN_ID, self.context.message + ) + + async def test_notify_keylist_updated(self): + """test notify_keylist_updated.""" + handler = KeylistUpdateResponseHandler() + + async def _result_generator(): + yield ConnRecord(connection_id="conn_id_1") + yield ConnRecord(connection_id="conn_id_2") + + async def _retrieve_by_invitation_key( + generator: AsyncGenerator, *args, **kwargs + ): + return await generator.__anext__() + + with async_mock.patch.object( + self.route_manager, + "connection_from_recipient_key", + partial(_retrieve_by_invitation_key, _result_generator()), + ): + response = KeylistUpdateResponse( + updated=[ + KeylistUpdated( + recipient_key=TEST_ROUTE_VERKEY, + action=KeylistUpdateRule.RULE_ADD, + result=KeylistUpdated.RESULT_SUCCESS, + ), + KeylistUpdated( + recipient_key=TEST_VERKEY, + action=KeylistUpdateRule.RULE_REMOVE, + result=KeylistUpdated.RESULT_SUCCESS, + ), + ], + ) + + response.assign_thread_id(TEST_THREAD_ID) + await handler.notify_keylist_updated( + self.context.profile, TEST_CONN_ID, response + ) + assert self.mock_event_bus.events + assert ( + self.mock_event_bus.events[0][1].topic + == MediationManager.KEYLIST_UPDATED_EVENT + ) + assert self.mock_event_bus.events[0][1].payload == { + "connection_id": TEST_CONN_ID, + "thread_id": TEST_THREAD_ID, + "updated": [result.serialize() for result in response.updated], + "mediated_connections": { + TEST_ROUTE_VERKEY: "conn_id_1", + TEST_VERKEY: "conn_id_2", + }, + } + + async def test_notify_keylist_updated_x_unknown_recip_key(self): + """test notify_keylist_updated.""" + handler = KeylistUpdateResponseHandler() + response = KeylistUpdateResponse( + updated=[ + KeylistUpdated( + recipient_key=TEST_ROUTE_VERKEY, + action=KeylistUpdateRule.RULE_ADD, + result=KeylistUpdated.RESULT_SUCCESS, + ), + KeylistUpdated( + recipient_key=TEST_VERKEY, + action=KeylistUpdateRule.RULE_REMOVE, + result=KeylistUpdated.RESULT_SUCCESS, + ), + ], + ) + + response.assign_thread_id(TEST_THREAD_ID) + with pytest.raises(HandlerException): + await handler.notify_keylist_updated( + self.context.profile, TEST_CONN_ID, response + ) diff --git a/aries_cloudagent/protocols/coordinate_mediation/v1_0/handlers/tests/test_mediation_grant_handler.py b/aries_cloudagent/protocols/coordinate_mediation/v1_0/handlers/tests/test_mediation_grant_handler.py index c4c5d1a074..e8924dbb83 100644 --- a/aries_cloudagent/protocols/coordinate_mediation/v1_0/handlers/tests/test_mediation_grant_handler.py +++ b/aries_cloudagent/protocols/coordinate_mediation/v1_0/handlers/tests/test_mediation_grant_handler.py @@ -18,7 +18,8 @@ from .. import mediation_grant_handler as test_module TEST_CONN_ID = "conn-id" -TEST_VERKEY = "3Dn1SJNPaCXcvvJvSbsFWP2xaCjMom3can8CQNhWrTRx" +TEST_RECORD_VERKEY = "3Dn1SJNPaCXcvvJvSbsFWP2xaCjMom3can8CQNhWrTRx" +TEST_VERKEY = "did:key:z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL" TEST_ENDPOINT = "https://example.com" @@ -58,7 +59,7 @@ async def test_handler(self): assert record assert record.state == MediationRecord.STATE_GRANTED assert record.endpoint == TEST_ENDPOINT - assert record.routing_keys == [TEST_VERKEY] + assert record.routing_keys == [TEST_RECORD_VERKEY] async def test_handler_connection_has_set_to_default_meta(self): handler, responder = MediationGrantHandler(), MockResponder() diff --git a/aries_cloudagent/protocols/coordinate_mediation/v1_0/handlers/tests/test_mediation_request_handler.py b/aries_cloudagent/protocols/coordinate_mediation/v1_0/handlers/tests/test_mediation_request_handler.py index 61d2b4d449..d3c342ec07 100644 --- a/aries_cloudagent/protocols/coordinate_mediation/v1_0/handlers/tests/test_mediation_request_handler.py +++ b/aries_cloudagent/protocols/coordinate_mediation/v1_0/handlers/tests/test_mediation_request_handler.py @@ -13,6 +13,7 @@ from ...models.mediation_record import MediationRecord from ..mediation_request_handler import MediationRequestHandler +from ......wallet.did_method import DIDMethods TEST_CONN_ID = "conn-id" TEST_VERKEY = "3Dn1SJNPaCXcvvJvSbsFWP2xaCjMom3can8CQNhWrTRx" @@ -24,6 +25,7 @@ class TestMediationRequestHandler(AsyncTestCase): async def setUp(self): """setup dependencies of messaging""" self.context = RequestContext.test_context() + self.context.profile.context.injector.bind_instance(DIDMethods, DIDMethods()) self.session = await self.context.session() self.context.message = MediationRequest() self.context.connection_ready = True diff --git a/aries_cloudagent/protocols/coordinate_mediation/v1_0/manager.py b/aries_cloudagent/protocols/coordinate_mediation/v1_0/manager.py index bd86337700..fc76b311a0 100644 --- a/aries_cloudagent/protocols/coordinate_mediation/v1_0/manager.py +++ b/aries_cloudagent/protocols/coordinate_mediation/v1_0/manager.py @@ -1,7 +1,6 @@ """Manager for Mediation coordination.""" import json import logging - from typing import Optional, Sequence, Tuple from ....core.error import BaseError @@ -9,16 +8,14 @@ from ....storage.base import BaseStorage from ....storage.error import StorageNotFoundError from ....storage.record import StorageRecord -from ....wallet.key_type import KeyType -from ....wallet.did_method import DIDMethod from ....wallet.base import BaseWallet from ....wallet.did_info import DIDInfo - +from ....wallet.did_method import SOV +from ....wallet.key_type import ED25519 from ...routing.v1_0.manager import RoutingManager from ...routing.v1_0.models.route_record import RouteRecord from ...routing.v1_0.models.route_update import RouteUpdate from ...routing.v1_0.models.route_updated import RouteUpdated - from .messages.inner.keylist_key import KeylistKey from .messages.inner.keylist_query_paginate import KeylistQueryPaginate from .messages.inner.keylist_update_rule import KeylistUpdateRule @@ -31,6 +28,7 @@ from .messages.mediate_grant import MediationGrant from .messages.mediate_request import MediationRequest from .models.mediation_record import MediationRecord +from .normalization import normalize_from_did_key LOGGER = logging.getLogger(__name__) @@ -58,6 +56,9 @@ class MediationManager: DEFAULT_MEDIATOR_RECORD_TYPE = "default_mediator" SEND_REQ_AFTER_CONNECTION = "send_mediation_request_on_connection" SET_TO_DEFAULT_ON_GRANTED = "set_to_default_on_granted" + METADATA_KEY = "mediation" + METADATA_ID = "id" + KEYLIST_UPDATED_EVENT = "acapy::keylist::updated" def __init__(self, profile: Profile): """Initialize Mediation Manager. @@ -109,8 +110,8 @@ async def _create_routing_did(self, session: ProfileSession) -> DIDInfo: wallet = session.inject(BaseWallet) storage = session.inject(BaseStorage) info = await wallet.create_local_did( - method=DIDMethod.SOV, - key_type=KeyType.ED25519, + method=SOV, + key_type=ED25519, metadata={"type": "routing_did"}, ) record = StorageRecord( @@ -248,8 +249,9 @@ async def update_keylist( } def rule_to_update(rule: KeylistUpdateRule): + recipient_key = normalize_from_did_key(rule.recipient_key) return RouteUpdate( - recipient_key=rule.recipient_key, action=action_map[rule.action] + recipient_key=recipient_key, action=action_map[rule.action] ) def updated_to_keylist_updated(updated: RouteUpdated): @@ -444,7 +446,11 @@ async def request_granted(self, record: MediationRecord, grant: MediationGrant): """ record.state = MediationRecord.STATE_GRANTED record.endpoint = grant.endpoint - record.routing_keys = grant.routing_keys + # record.routing_keys = grant.routing_keys + routing_keys = [] + for key in grant.routing_keys: + routing_keys.append(normalize_from_did_key(key)) + record.routing_keys = routing_keys async with self._profile.session() as session: await record.save(session, reason="Mediation request granted.") @@ -532,63 +538,68 @@ async def store_update_results( session: An active profile session """ - session = await self._profile.session() + # TODO The stored recipient keys are did:key! + to_save: Sequence[RouteRecord] = [] to_remove: Sequence[RouteRecord] = [] - for updated in results: - if updated.result != KeylistUpdated.RESULT_SUCCESS: - # TODO better handle different results? - LOGGER.warning( - "Keylist update failure: %s(%s): %s", - updated.action, - updated.recipient_key, - updated.result, - ) - continue - if updated.action == KeylistUpdateRule.RULE_ADD: - # Multi-tenancy uses route record for internal relaying of wallets - # So the record could already exist. We update in that case - try: - record = await RouteRecord.retrieve_by_recipient_key( - session, updated.recipient_key - ) - record.connection_id = connection_id - record.role = RouteRecord.ROLE_CLIENT - except StorageNotFoundError: - record = RouteRecord( - role=RouteRecord.ROLE_CLIENT, - recipient_key=updated.recipient_key, - connection_id=connection_id, - ) - to_save.append(record) - elif updated.action == KeylistUpdateRule.RULE_REMOVE: - try: - records = await RouteRecord.query( - session, - { - "role": RouteRecord.ROLE_CLIENT, - "connection_id": connection_id, - "recipient_key": updated.recipient_key, - }, - ) - except StorageNotFoundError as err: - LOGGER.error( - "No route found while processing keylist update response: %s", - err, + + async with self._profile.session() as session: + for updated in results: + if updated.result != KeylistUpdated.RESULT_SUCCESS: + # TODO better handle different results? + LOGGER.warning( + "Keylist update failure: %s(%s): %s", + updated.action, + updated.recipient_key, + updated.result, ) - else: - if len(records) > 1: + continue + if updated.action == KeylistUpdateRule.RULE_ADD: + # Multi-tenancy uses route record for internal relaying of wallets + # So the record could already exist. We update in that case + try: + record = await RouteRecord.retrieve_by_recipient_key( + session, updated.recipient_key + ) + record.connection_id = connection_id + record.role = RouteRecord.ROLE_CLIENT + except StorageNotFoundError: + record = RouteRecord( + role=RouteRecord.ROLE_CLIENT, + recipient_key=updated.recipient_key, + connection_id=connection_id, + ) + to_save.append(record) + elif updated.action == KeylistUpdateRule.RULE_REMOVE: + try: + records = await RouteRecord.query( + session, + { + "role": RouteRecord.ROLE_CLIENT, + "connection_id": connection_id, + "recipient_key": updated.recipient_key, + }, + ) + except StorageNotFoundError as err: LOGGER.error( - f"Too many ({len(records)}) routes found " - "while processing keylist update response" + "No route found while processing keylist update response: %s", + err, ) - record = records[0] - to_remove.append(record) - - for record_for_saving in to_save: - await record_for_saving.save(session, reason="Route successfully added.") - for record_for_removal in to_remove: - await record_for_removal.delete_record(session) + else: + if len(records) > 1: + LOGGER.error( + f"Too many ({len(records)}) routes found " + "while processing keylist update response" + ) + record = records[0] + to_remove.append(record) + + for record_for_saving in to_save: + await record_for_saving.save( + session, reason="Route successfully added." + ) + for record_for_removal in to_remove: + await record_for_removal.delete_record(session) async def get_my_keylist( self, connection_id: Optional[str] = None diff --git a/aries_cloudagent/protocols/coordinate_mediation/v1_0/messages/inner/keylist_key.py b/aries_cloudagent/protocols/coordinate_mediation/v1_0/messages/inner/keylist_key.py index aa7701ff5a..90982731ba 100644 --- a/aries_cloudagent/protocols/coordinate_mediation/v1_0/messages/inner/keylist_key.py +++ b/aries_cloudagent/protocols/coordinate_mediation/v1_0/messages/inner/keylist_key.py @@ -3,7 +3,8 @@ from marshmallow import EXCLUDE, fields from ......messaging.models.base import BaseModel, BaseModelSchema -from ......messaging.valid import INDY_RAW_PUBLIC_KEY +from ......messaging.valid import DID_KEY +from ...normalization import normalize_from_public_key class KeylistKey(BaseModel): @@ -32,7 +33,7 @@ def __init__( """ super().__init__(**kwargs) - self.recipient_key = recipient_key + self.recipient_key = normalize_from_public_key(recipient_key) class KeylistKeySchema(BaseModelSchema): @@ -44,4 +45,4 @@ class Meta: model_class = KeylistKey unknown = EXCLUDE - recipient_key = fields.Str(required=True, **INDY_RAW_PUBLIC_KEY) + recipient_key = fields.Str(required=True, **DID_KEY) diff --git a/aries_cloudagent/protocols/coordinate_mediation/v1_0/messages/inner/keylist_update_rule.py b/aries_cloudagent/protocols/coordinate_mediation/v1_0/messages/inner/keylist_update_rule.py index 6ece696537..de3524d4f9 100644 --- a/aries_cloudagent/protocols/coordinate_mediation/v1_0/messages/inner/keylist_update_rule.py +++ b/aries_cloudagent/protocols/coordinate_mediation/v1_0/messages/inner/keylist_update_rule.py @@ -8,7 +8,8 @@ from marshmallow.validate import OneOf from ......messaging.models.base import BaseModel, BaseModelSchema -from ......messaging.valid import INDY_RAW_PUBLIC_KEY +from ......messaging.valid import ROUTING_KEY +from ...normalization import normalize_from_public_key class KeylistUpdateRule(BaseModel): @@ -32,7 +33,7 @@ def __init__(self, recipient_key: str, action: str, **kwargs): """ super().__init__(**kwargs) - self.recipient_key = recipient_key + self.recipient_key = normalize_from_public_key(recipient_key) self.action = action @@ -45,7 +46,7 @@ class Meta: model_class = KeylistUpdateRule recipient_key = fields.Str( - description="Key to remove or add", required=True, **INDY_RAW_PUBLIC_KEY + description="Key to remove or add", required=True, **ROUTING_KEY ) action = fields.Str( required=True, diff --git a/aries_cloudagent/protocols/coordinate_mediation/v1_0/messages/inner/keylist_updated.py b/aries_cloudagent/protocols/coordinate_mediation/v1_0/messages/inner/keylist_updated.py index ac354e45d1..388e166451 100644 --- a/aries_cloudagent/protocols/coordinate_mediation/v1_0/messages/inner/keylist_updated.py +++ b/aries_cloudagent/protocols/coordinate_mediation/v1_0/messages/inner/keylist_updated.py @@ -6,7 +6,8 @@ from marshmallow import EXCLUDE, fields from ......messaging.models.base import BaseModel, BaseModelSchema -from ......messaging.valid import INDY_RAW_PUBLIC_KEY +from ......messaging.valid import DID_KEY +from ...normalization import normalize_from_public_key class KeylistUpdated(BaseModel): @@ -40,7 +41,7 @@ def __init__( """ super().__init__(**kwargs) - self.recipient_key = recipient_key + self.recipient_key = normalize_from_public_key(recipient_key) self.action = action self.result = result @@ -54,6 +55,6 @@ class Meta: model_class = KeylistUpdated unknown = EXCLUDE - recipient_key = fields.Str(required=True, **INDY_RAW_PUBLIC_KEY) + recipient_key = fields.Str(required=True, **DID_KEY) action = fields.Str(required=True) result = fields.Str(required=True) diff --git a/aries_cloudagent/protocols/coordinate_mediation/v1_0/messages/mediate_grant.py b/aries_cloudagent/protocols/coordinate_mediation/v1_0/messages/mediate_grant.py index b8616090dc..6f82d8be48 100644 --- a/aries_cloudagent/protocols/coordinate_mediation/v1_0/messages/mediate_grant.py +++ b/aries_cloudagent/protocols/coordinate_mediation/v1_0/messages/mediate_grant.py @@ -9,6 +9,7 @@ from .....messaging.agent_message import AgentMessage, AgentMessageSchema from ..message_types import MEDIATE_GRANT, PROTOCOL_PACKAGE +from ..normalization import normalize_from_public_key HANDLER_CLASS = ( f"{PROTOCOL_PACKAGE}.handlers.mediation_grant_handler.MediationGrantHandler" @@ -41,7 +42,11 @@ def __init__( """ super(MediationGrant, self).__init__(**kwargs) self.endpoint = endpoint - self.routing_keys = list(routing_keys) if routing_keys else [] + self.routing_keys = ( + list(normalize_from_public_key(key) for key in routing_keys) + if routing_keys + else [] + ) class MediationGrantSchema(AgentMessageSchema): diff --git a/aries_cloudagent/protocols/coordinate_mediation/v1_0/messages/tests/test_keylist.py b/aries_cloudagent/protocols/coordinate_mediation/v1_0/messages/tests/test_keylist.py index a65512d026..a955580966 100644 --- a/aries_cloudagent/protocols/coordinate_mediation/v1_0/messages/tests/test_keylist.py +++ b/aries_cloudagent/protocols/coordinate_mediation/v1_0/messages/tests/test_keylist.py @@ -18,7 +18,7 @@ class TestKeylist(MessageTest, TestCase): "pagination": KeylistQueryPaginate(10, 10), "keys": [ KeylistKey( - recipient_key="3Dn1SJNPaCXcvvJvSbsFWP2xaCjMom3can8CQNhWrTRx", + recipient_key="did:key:z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL", action="added", result="success", ) diff --git a/aries_cloudagent/protocols/coordinate_mediation/v1_0/messages/tests/test_keylist_update.py b/aries_cloudagent/protocols/coordinate_mediation/v1_0/messages/tests/test_keylist_update.py index 650105425e..caa0b3c976 100644 --- a/aries_cloudagent/protocols/coordinate_mediation/v1_0/messages/tests/test_keylist_update.py +++ b/aries_cloudagent/protocols/coordinate_mediation/v1_0/messages/tests/test_keylist_update.py @@ -15,6 +15,8 @@ class TestKeylistUpdate(MessageTest, TestCase): SCHEMA = KeylistUpdateSchema VALUES = { "updates": [ - KeylistUpdateRule("3Dn1SJNPaCXcvvJvSbsFWP2xaCjMom3can8CQNhWrTRx", "add") + KeylistUpdateRule( + "did:key:z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL", "add" + ) ] } diff --git a/aries_cloudagent/protocols/coordinate_mediation/v1_0/messages/tests/test_keylist_update_response.py b/aries_cloudagent/protocols/coordinate_mediation/v1_0/messages/tests/test_keylist_update_response.py index b7f70e25af..e59ea63511 100644 --- a/aries_cloudagent/protocols/coordinate_mediation/v1_0/messages/tests/test_keylist_update_response.py +++ b/aries_cloudagent/protocols/coordinate_mediation/v1_0/messages/tests/test_keylist_update_response.py @@ -16,7 +16,7 @@ class TestKeylistUpdateResponse(MessageTest, TestCase): VALUES = { "updated": [ KeylistUpdated( - recipient_key="3Dn1SJNPaCXcvvJvSbsFWP2xaCjMom3can8CQNhWrTRx", + recipient_key="did:key:z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL", action="added", result="success", ) diff --git a/aries_cloudagent/protocols/coordinate_mediation/v1_0/messages/tests/test_mediate_grant.py b/aries_cloudagent/protocols/coordinate_mediation/v1_0/messages/tests/test_mediate_grant.py index 2f73b32afe..9c992ff883 100644 --- a/aries_cloudagent/protocols/coordinate_mediation/v1_0/messages/tests/test_mediate_grant.py +++ b/aries_cloudagent/protocols/coordinate_mediation/v1_0/messages/tests/test_mediate_grant.py @@ -12,4 +12,7 @@ class TestMediateGrant(MessageTest, TestCase): TYPE = MEDIATE_GRANT CLASS = MediationGrant SCHEMA = MediationGrantSchema - VALUES = {"endpoint": "http://localhost:3000", "routing_keys": ["test_routing_key"]} + VALUES = { + "endpoint": "http://localhost:3000", + "routing_keys": ["did:key:z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL"], + } diff --git a/aries_cloudagent/protocols/coordinate_mediation/v1_0/models/mediation_record.py b/aries_cloudagent/protocols/coordinate_mediation/v1_0/models/mediation_record.py index 9841a70265..f7801a45c5 100644 --- a/aries_cloudagent/protocols/coordinate_mediation/v1_0/models/mediation_record.py +++ b/aries_cloudagent/protocols/coordinate_mediation/v1_0/models/mediation_record.py @@ -6,7 +6,7 @@ from .....core.profile import ProfileSession from .....messaging.models.base_record import BaseRecord, BaseRecordSchema -from .....messaging.valid import INDY_RAW_PUBLIC_KEY +from .....messaging.valid import DID_KEY from .....storage.base import StorageDuplicateError, StorageNotFoundError @@ -19,6 +19,7 @@ class Meta: schema_class = "MediationRecordSchema" RECORD_TYPE = "mediation_requests" + RECORD_TOPIC = "mediation" RECORD_ID_NAME = "mediation_id" TAG_NAMES = {"state", "role", "connection_id"} @@ -171,5 +172,5 @@ class Meta: connection_id = fields.Str(required=True) mediator_terms = fields.List(fields.Str(), required=False) recipient_terms = fields.List(fields.Str(), required=False) - routing_keys = fields.List(fields.Str(**INDY_RAW_PUBLIC_KEY), required=False) + routing_keys = fields.List(fields.Str(**DID_KEY), required=False) endpoint = fields.Str(required=False) diff --git a/aries_cloudagent/protocols/coordinate_mediation/v1_0/normalization.py b/aries_cloudagent/protocols/coordinate_mediation/v1_0/normalization.py new file mode 100644 index 0000000000..d699565367 --- /dev/null +++ b/aries_cloudagent/protocols/coordinate_mediation/v1_0/normalization.py @@ -0,0 +1,19 @@ +"""Normalization methods used while transitioning to DID:Key method.""" +from ....did.did_key import DIDKey +from ....wallet.key_type import ED25519 + + +def normalize_from_did_key(key: str): + """Normalize Recipient/Routing keys from DID:Key to public keys.""" + if key.startswith("did:key:"): + return DIDKey.from_did(key).public_key_b58 + + return key + + +def normalize_from_public_key(key: str): + """Normalize Recipient/Routing keys from public keys to DID:Key.""" + if key.startswith("did:key:"): + return key + + return DIDKey.from_public_key_b58(key, ED25519).did diff --git a/aries_cloudagent/protocols/coordinate_mediation/v1_0/route_manager.py b/aries_cloudagent/protocols/coordinate_mediation/v1_0/route_manager.py new file mode 100644 index 0000000000..990252cd5a --- /dev/null +++ b/aries_cloudagent/protocols/coordinate_mediation/v1_0/route_manager.py @@ -0,0 +1,331 @@ +"""Route manager. + +Set up routing for newly formed connections. +""" + + +from abc import ABC, abstractmethod +import logging +from typing import List, Optional, Tuple + +from ....connections.models.conn_record import ConnRecord +from ....core.profile import Profile +from ....messaging.responder import BaseResponder +from ....storage.error import StorageNotFoundError +from ....wallet.base import BaseWallet +from ....wallet.did_info import DIDInfo +from ....wallet.did_method import SOV +from ....wallet.key_type import ED25519 +from ...routing.v1_0.models.route_record import RouteRecord +from .manager import MediationManager +from .messages.keylist_update import KeylistUpdate +from .models.mediation_record import MediationRecord +from .normalization import normalize_from_did_key + + +LOGGER = logging.getLogger(__name__) + + +class RouteManagerError(Exception): + """Raised on error from route manager.""" + + +class RouteManager(ABC): + """Base Route Manager.""" + + async def get_or_create_my_did( + self, profile: Profile, conn_record: ConnRecord + ) -> DIDInfo: + """Create or retrieve DID info for a conneciton.""" + if not conn_record.my_did: + async with profile.session() as session: + wallet = session.inject(BaseWallet) + # Create new DID for connection + my_info = await wallet.create_local_did(SOV, ED25519) + conn_record.my_did = my_info.did + await conn_record.save(session, reason="Connection my did created") + else: + async with profile.session() as session: + wallet = session.inject(BaseWallet) + my_info = await wallet.get_local_did(conn_record.my_did) + + return my_info + + def _validate_mediation_state(self, mediation_record: MediationRecord): + """Perform mediation state validation.""" + if mediation_record.state != MediationRecord.STATE_GRANTED: + raise RouteManagerError( + "Mediation is not granted for mediation identified by " + f"{mediation_record.mediation_id}" + ) + + async def mediation_record_for_connection( + self, + profile: Profile, + conn_record: ConnRecord, + mediation_id: Optional[str] = None, + or_default: bool = False, + ): + """Return relevant mediator for connection.""" + if conn_record.connection_id: + async with profile.session() as session: + mediation_metadata = await conn_record.metadata_get( + session, MediationManager.METADATA_KEY, {} + ) + mediation_id = ( + mediation_metadata.get(MediationManager.METADATA_ID) or mediation_id + ) + + mediation_record = await self.mediation_record_if_id( + profile, mediation_id, or_default + ) + if mediation_record: + await self.save_mediator_for_connection( + profile, conn_record, mediation_record + ) + return mediation_record + + async def mediation_record_if_id( + self, + profile: Profile, + mediation_id: Optional[str] = None, + or_default: bool = False, + ): + """Validate mediation and return record. + + If mediation_id is not None, + validate mediation record state and return record + else, return None + """ + mediation_record = None + if mediation_id: + async with profile.session() as session: + mediation_record = await MediationRecord.retrieve_by_id( + session, mediation_id + ) + elif or_default: + mediation_record = await MediationManager(profile).get_default_mediator() + + if mediation_record: + self._validate_mediation_state(mediation_record) + return mediation_record + + @abstractmethod + async def _route_for_key( + self, + profile: Profile, + recipient_key: str, + mediation_record: Optional[MediationRecord] = None, + *, + skip_if_exists: bool = False, + replace_key: Optional[str] = None, + ) -> Optional[KeylistUpdate]: + """Route a key.""" + + async def route_connection_as_invitee( + self, + profile: Profile, + conn_record: ConnRecord, + mediation_record: Optional[MediationRecord] = None, + ) -> Optional[KeylistUpdate]: + """Set up routing for a new connection when we are the invitee.""" + LOGGER.debug("Routing connection as invitee") + my_info = await self.get_or_create_my_did(profile, conn_record) + return await self._route_for_key( + profile, my_info.verkey, mediation_record, skip_if_exists=True + ) + + async def route_connection_as_inviter( + self, + profile: Profile, + conn_record: ConnRecord, + mediation_record: Optional[MediationRecord] = None, + ) -> Optional[KeylistUpdate]: + """Set up routing for a new connection when we are the inviter.""" + LOGGER.debug("Routing connection as inviter") + my_info = await self.get_or_create_my_did(profile, conn_record) + + replace_key = conn_record.invitation_key + async with profile.session() as session: + wallet = session.inject(BaseWallet) + public_did = await wallet.get_public_did() + + # Do not replace key, if it is public + if public_did and public_did.verkey == conn_record.invitation_key: + replace_key = None + + return await self._route_for_key( + profile, + my_info.verkey, + mediation_record, + replace_key=replace_key, + skip_if_exists=True, + ) + + async def route_connection( + self, + profile: Profile, + conn_record: ConnRecord, + mediation_record: Optional[MediationRecord] = None, + ) -> Optional[KeylistUpdate]: + """Set up routing for a connection. + + This method will evaluate connection state and call the appropriate methods. + """ + if conn_record.rfc23_state == ConnRecord.State.INVITATION.rfc23strict( + ConnRecord.Role.RESPONDER + ): + return await self.route_connection_as_invitee( + profile, conn_record, mediation_record + ) + + if conn_record.rfc23_state == ConnRecord.State.REQUEST.rfc23strict( + ConnRecord.Role.REQUESTER + ): + return await self.route_connection_as_inviter( + profile, conn_record, mediation_record + ) + + return None + + async def route_invitation( + self, + profile: Profile, + conn_record: ConnRecord, + mediation_record: Optional[MediationRecord] = None, + ) -> Optional[KeylistUpdate]: + """Set up routing for receiving a response to an invitation.""" + await self.save_mediator_for_connection(profile, conn_record, mediation_record) + + if conn_record.invitation_key: + return await self._route_for_key( + profile, + conn_record.invitation_key, + mediation_record, + skip_if_exists=True, + ) + + raise ValueError("Expected connection to have invitation_key") + + async def route_verkey(self, profile: Profile, verkey: str): + """Establish routing for a public DID.""" + return await self._route_for_key(profile, verkey, skip_if_exists=True) + + async def route_public_did(self, profile: Profile, verkey: str): + """Establish routing for a public DID. + + [DEPRECATED] Establish routing for a public DID. Use route_verkey() instead. + """ + return await self._route_for_key(profile, verkey, skip_if_exists=True) + + async def route_static( + self, + profile: Profile, + conn_record: ConnRecord, + mediation_record: Optional[MediationRecord] = None, + ) -> Optional[KeylistUpdate]: + """Establish routing for a static connection.""" + my_info = await self.get_or_create_my_did(profile, conn_record) + return await self._route_for_key( + profile, my_info.verkey, mediation_record, skip_if_exists=True + ) + + async def save_mediator_for_connection( + self, + profile: Profile, + conn_record: ConnRecord, + mediation_record: Optional[MediationRecord] = None, + mediation_id: Optional[str] = None, + ): + """Save mediator info to connection metadata.""" + async with profile.session() as session: + if mediation_id: + mediation_record = await MediationRecord.retrieve_by_id( + session, mediation_id + ) + + if mediation_record: + await conn_record.metadata_set( + session, + MediationManager.METADATA_KEY, + {MediationManager.METADATA_ID: mediation_record.mediation_id}, + ) + + @abstractmethod + async def routing_info( + self, + profile: Profile, + my_endpoint: str, + mediation_record: Optional[MediationRecord] = None, + ) -> Tuple[List[str], str]: + """Retrieve routing keys.""" + + async def connection_from_recipient_key( + self, profile: Profile, recipient_key: str + ) -> ConnRecord: + """Retrieve connection for a recipient_key. + + The recipient key is expected to be a local key owned by this agent. + """ + async with profile.session() as session: + wallet = session.inject(BaseWallet) + try: + conn = await ConnRecord.retrieve_by_tag_filter( + session, {"invitation_key": normalize_from_did_key(recipient_key)} + ) + except StorageNotFoundError: + did_info = await wallet.get_local_did_for_verkey( + normalize_from_did_key(recipient_key) + ) + conn = await ConnRecord.retrieve_by_did(session, my_did=did_info.did) + + return conn + + +class CoordinateMediationV1RouteManager(RouteManager): + """Manage routes using Coordinate Mediation protocol.""" + + async def _route_for_key( + self, + profile: Profile, + recipient_key: str, + mediation_record: Optional[MediationRecord] = None, + *, + skip_if_exists: bool = False, + replace_key: Optional[str] = None, + ) -> Optional[KeylistUpdate]: + if not mediation_record: + return None + + if skip_if_exists: + try: + async with profile.session() as session: + await RouteRecord.retrieve_by_recipient_key(session, recipient_key) + + return None + except StorageNotFoundError: + pass + + # Keylist update is idempotent, skip_if_exists ignored + mediation_mgr = MediationManager(profile) + keylist_update = await mediation_mgr.add_key(recipient_key) + if replace_key: + keylist_update = await mediation_mgr.remove_key(replace_key, keylist_update) + + responder = profile.inject(BaseResponder) + await responder.send( + keylist_update, connection_id=mediation_record.connection_id + ) + return keylist_update + + async def routing_info( + self, + profile: Profile, + my_endpoint: str, + mediation_record: Optional[MediationRecord] = None, + ) -> Tuple[List[str], str]: + """Return routing info for mediator.""" + if mediation_record: + return mediation_record.routing_keys, mediation_record.endpoint + + return [], my_endpoint diff --git a/aries_cloudagent/protocols/coordinate_mediation/v1_0/route_manager_provider.py b/aries_cloudagent/protocols/coordinate_mediation/v1_0/route_manager_provider.py new file mode 100644 index 0000000000..a48dddaec8 --- /dev/null +++ b/aries_cloudagent/protocols/coordinate_mediation/v1_0/route_manager_provider.py @@ -0,0 +1,32 @@ +"""RouteManager provider.""" +from ....config.base import BaseInjector, BaseProvider, BaseSettings +from ....core.profile import Profile +from ....multitenant.base import BaseMultitenantManager +from ....multitenant.route_manager import ( + MultitenantRouteManager, + BaseWalletRouteManager, +) +from .route_manager import CoordinateMediationV1RouteManager + + +class RouteManagerProvider(BaseProvider): + """Route manager provider. + + Decides whcih route manager to use based on settings. + """ + + def __init__(self, root_profile: Profile): + """Initialize route manager provider.""" + self.root_profile = root_profile + + def provide(self, settings: BaseSettings, injector: BaseInjector): + """Create the appropriate route manager instance.""" + wallet_id = settings.get("wallet.id") + multitenant_mgr = self.root_profile.inject_or(BaseMultitenantManager) + if multitenant_mgr: + if wallet_id: + return MultitenantRouteManager(self.root_profile) + else: + return BaseWalletRouteManager() + + return CoordinateMediationV1RouteManager() diff --git a/aries_cloudagent/protocols/coordinate_mediation/v1_0/routes.py b/aries_cloudagent/protocols/coordinate_mediation/v1_0/routes.py index 6740dce76b..74b1c69291 100644 --- a/aries_cloudagent/protocols/coordinate_mediation/v1_0/routes.py +++ b/aries_cloudagent/protocols/coordinate_mediation/v1_0/routes.py @@ -16,10 +16,8 @@ from ....messaging.models.openapi import OpenAPISchema from ....messaging.valid import UUIDFour from ....storage.error import StorageError, StorageNotFoundError - from ...connections.v1_0.routes import ConnectionsConnIdMatchInfoSchema from ...routing.v1_0.models.route_record import RouteRecord, RouteRecordSchema - from .manager import MediationManager, MediationManagerError from .message_types import SPEC_URI from .messages.inner.keylist_update_rule import ( @@ -31,6 +29,7 @@ from .messages.mediate_deny import MediationDenySchema from .messages.mediate_grant import MediationGrantSchema from .models.mediation_record import MediationRecord, MediationRecordSchema +from .route_manager import RouteManager CONNECTION_ID_SCHEMA = fields.UUID( @@ -246,7 +245,8 @@ async def delete_mediation_request(request: web.BaseRequest): session, mediation_id ) result = mediation_record.serialize() - await mediation_record.delete_record(session) + async with context.profile.session() as session: + await mediation_record.delete_record(session) except StorageNotFoundError as err: raise web.HTTPNotFound(reason=err.roll_up) from err except (BaseModelError, StorageError) as err: @@ -494,7 +494,7 @@ async def set_default_mediator(request: web.BaseRequest): mediator_mgr = MediationManager(context.profile) await mediator_mgr.set_default_mediator_by_id(mediation_id=mediation_id) default_mediator = await mediator_mgr.get_default_mediator() - results = default_mediator.serialize() + results = default_mediator.serialize() if default_mediator else {} except (StorageError, BaseModelError) as err: raise web.HTTPBadRequest(reason=err.roll_up) from err return web.json_response(results, status=201) @@ -509,12 +509,47 @@ async def clear_default_mediator(request: web.BaseRequest): mediator_mgr = MediationManager(context.profile) default_mediator = await mediator_mgr.get_default_mediator() await mediator_mgr.clear_default_mediator() - results = default_mediator.serialize() + results = default_mediator.serialize() if default_mediator else {} except (StorageError, BaseModelError) as err: raise web.HTTPBadRequest(reason=err.roll_up) from err return web.json_response(results, status=201) +@docs(tags=["mediation"], summary="Update keylist for a connection") +@match_info_schema(ConnectionsConnIdMatchInfoSchema()) +@request_schema(MediationIdMatchInfoSchema()) +# TODO Fix this response so that it adequately represents Optionals +@response_schema(KeylistUpdateSchema(), 200) +async def update_keylist_for_connection(request: web.BaseRequest): + """Update keylist for a connection.""" + context: AdminRequestContext = request["context"] + body = await request.json() + mediation_id = body.get("mediation_id") + connection_id = request.match_info["conn_id"] + try: + route_manager = context.inject(RouteManager) + + async with context.session() as session: + connection_record = await ConnRecord.retrieve_by_id(session, connection_id) + mediation_record = await route_manager.mediation_record_for_connection( + context.profile, connection_record, mediation_id, or_default=True + ) + + # MediationRecord is permitted to be None; route manager will + # ensure the correct mediator is notified. + keylist_update = await route_manager.route_connection( + context.profile, connection_record, mediation_record + ) + + results = keylist_update.serialize() if keylist_update else {} + except StorageNotFoundError as err: + raise web.HTTPNotFound(reason=err.roll_up) from err + except (StorageError, BaseModelError) as err: + raise web.HTTPBadRequest(reason=err.roll_up) from err + + return web.json_response(results, status=200) + + async def register(app: web.Application): """Register routes.""" @@ -547,6 +582,9 @@ async def register(app: web.Application): ), web.put("/mediation/{mediation_id}/default-mediator", set_default_mediator), web.delete("/mediation/default-mediator", clear_default_mediator), + web.post( + "/mediation/update-keylist/{conn_id}", update_keylist_for_connection + ), ] ) diff --git a/aries_cloudagent/protocols/coordinate_mediation/v1_0/tests/test_mediation_manager.py b/aries_cloudagent/protocols/coordinate_mediation/v1_0/tests/test_mediation_manager.py index d2505ccfb9..7e93eaa841 100644 --- a/aries_cloudagent/protocols/coordinate_mediation/v1_0/tests/test_mediation_manager.py +++ b/aries_cloudagent/protocols/coordinate_mediation/v1_0/tests/test_mediation_manager.py @@ -1,19 +1,18 @@ """Test MediationManager.""" import logging +from typing import AsyncGenerator, AsyncIterable, Iterable -import pytest - +from functools import partial from asynctest import mock as async_mock +import pytest +from .. import manager as test_module +from .....core.event_bus import EventBus, MockEventBus +from .....core.in_memory import InMemoryProfile from .....core.profile import Profile, ProfileSession -from .....connections.models.conn_record import ConnRecord -from .....messaging.request_context import RequestContext +from .....did.did_key import DIDKey from .....storage.error import StorageNotFoundError -from .....transport.inbound.receipt import MessageReceipt - from ....routing.v1_0.models.route_record import RouteRecord - -from .. import manager as test_module from ..manager import ( MediationAlreadyExists, MediationManager, @@ -22,43 +21,53 @@ ) from ..messages.inner.keylist_update_rule import KeylistUpdateRule from ..messages.inner.keylist_updated import KeylistUpdated +from ..messages.keylist_update_response import KeylistUpdateResponse from ..messages.mediate_deny import MediationDeny from ..messages.mediate_grant import MediationGrant from ..messages.mediate_request import MediationRequest from ..models.mediation_record import MediationRecord +from .....wallet.did_method import DIDMethods TEST_CONN_ID = "conn-id" +TEST_THREAD_ID = "thread-id" TEST_ENDPOINT = "https://example.com" -TEST_VERKEY = "3Dn1SJNPaCXcvvJvSbsFWP2xaCjMom3can8CQNhWrTRx" -TEST_ROUTE_VERKEY = "9WCgWKUaAJj3VWxxtzvvMQN3AoFxoBtBDo9ntwJnVVCC" +TEST_RECORD_VERKEY = "3Dn1SJNPaCXcvvJvSbsFWP2xaCjMom3can8CQNhWrTRx" +TEST_VERKEY = "did:key:z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL" +TEST_ROUTE_RECORD_VERKEY = "9WCgWKUaAJj3VWxxtzvvMQN3AoFxoBtBDo9ntwJnVVCC" +TEST_ROUTE_VERKEY = "did:key:z6MknxTj6Zj1VrDWc1ofaZtmCVv2zNXpD58Xup4ijDGoQhya" pytestmark = pytest.mark.asyncio @pytest.fixture -async def profile() -> Profile: +def profile() -> Iterable[Profile]: """Fixture for profile used in tests.""" # pylint: disable=W0621 - context = RequestContext.test_context() - context.message_receipt = MessageReceipt(sender_verkey=TEST_VERKEY) - context.connection_record = ConnRecord(connection_id=TEST_CONN_ID) - yield context.profile + yield InMemoryProfile.test_profile( + bind={EventBus: MockEventBus(), DIDMethods: DIDMethods()} + ) + + +@pytest.fixture +def mock_event_bus(profile: Profile): + yield profile.inject(EventBus) @pytest.fixture -async def session(profile) -> ProfileSession: # pylint: disable=W0621 +async def session(profile) -> AsyncIterable[ProfileSession]: # pylint: disable=W0621 """Fixture for profile session used in tests.""" - yield await profile.session() + async with profile.session() as session: + yield session @pytest.fixture -async def manager(profile) -> MediationManager: # pylint: disable=W0621 +def manager(profile) -> Iterable[MediationManager]: # pylint: disable=W0621 """Fixture for manager used in tests.""" yield MediationManager(profile) @pytest.fixture -def record() -> MediationRecord: +def record() -> Iterable[MediationRecord]: """Fixture for record used in tests.""" yield MediationRecord( state=MediationRecord.STATE_GRANTED, connection_id=TEST_CONN_ID @@ -71,7 +80,7 @@ class TestMediationManager: # pylint: disable=R0904,W0621 async def test_create_manager_no_profile(self): """test_create_manager_no_profile.""" with pytest.raises(MediationManagerError): - await MediationManager(None) + MediationManager(None) async def test_create_did(self, manager, session): """test_create_did.""" @@ -111,9 +120,11 @@ async def test_grant_request(self, session, manager): assert record.connection_id == TEST_CONN_ID record, grant = await manager.grant_request(record.mediation_id) assert grant.endpoint == session.settings.get("default_endpoint") - assert grant.routing_keys == [ - (await manager._retrieve_routing_did(session)).verkey - ] + routing_key = await manager._retrieve_routing_did(session) + routing_key = DIDKey.from_public_key_b58( + routing_key.verkey, routing_key.key_type + ).did + assert grant.routing_keys == [routing_key] async def test_deny_request(self, manager): """test_deny_request.""" @@ -126,9 +137,9 @@ async def test_deny_request(self, manager): async def test_update_keylist_delete(self, session, manager, record): """test_update_keylist_delete.""" - await RouteRecord(connection_id=TEST_CONN_ID, recipient_key=TEST_VERKEY).save( - session - ) + await RouteRecord( + connection_id=TEST_CONN_ID, recipient_key=TEST_RECORD_VERKEY + ).save(session) response = await manager.update_keylist( record=record, updates=[ @@ -161,9 +172,9 @@ async def test_update_keylist_create(self, manager, record): async def test_update_keylist_create_existing(self, session, manager, record): """test_update_keylist_create_existing.""" - await RouteRecord(connection_id=TEST_CONN_ID, recipient_key=TEST_VERKEY).save( - session - ) + await RouteRecord( + connection_id=TEST_CONN_ID, recipient_key=TEST_RECORD_VERKEY + ).save(session) response = await manager.update_keylist( record=record, updates=[ @@ -287,7 +298,7 @@ async def test_request_granted(self, manager): await manager.request_granted(record, grant) assert record.state == MediationRecord.STATE_GRANTED assert record.endpoint == TEST_ENDPOINT - assert record.routing_keys == [TEST_ROUTE_VERKEY] + assert record.routing_keys == [TEST_ROUTE_RECORD_VERKEY] async def test_request_denied(self, manager): """test_request_denied.""" @@ -363,7 +374,11 @@ async def test_add_remove_key_mix(self, manager): assert update.updates[0].recipient_key == TEST_VERKEY assert update.updates[1].recipient_key == TEST_ROUTE_VERKEY - async def test_store_update_results(self, session, manager): + async def test_store_update_results( + self, + session: ProfileSession, + manager: MediationManager, + ): """test_store_update_results.""" await RouteRecord( role=RouteRecord.ROLE_CLIENT, diff --git a/aries_cloudagent/protocols/coordinate_mediation/v1_0/tests/test_route_manager.py b/aries_cloudagent/protocols/coordinate_mediation/v1_0/tests/test_route_manager.py new file mode 100644 index 0000000000..4d9efceb72 --- /dev/null +++ b/aries_cloudagent/protocols/coordinate_mediation/v1_0/tests/test_route_manager.py @@ -0,0 +1,718 @@ +from asynctest import mock +import pytest + +from .....connections.models.conn_record import ConnRecord +from .....core.in_memory import InMemoryProfile +from .....wallet.base import BaseWallet +from .....core.profile import Profile +from .....messaging.responder import BaseResponder, MockResponder +from .....storage.error import StorageNotFoundError +from .....wallet.did_info import DIDInfo +from .....wallet.did_method import SOV +from .....wallet.in_memory import InMemoryWallet +from .....wallet.key_type import ED25519 +from ....routing.v1_0.models.route_record import RouteRecord +from ..manager import MediationManager +from ..messages.keylist_update import KeylistUpdate +from ..models.mediation_record import MediationRecord +from ..route_manager import ( + CoordinateMediationV1RouteManager, + RouteManager, + RouteManagerError, +) + +TEST_RECORD_DID = "55GkHamhTU1ZbTbV2ab9DE" +TEST_RECORD_VERKEY = "3Dn1SJNPaCXcvvJvSbsFWP2xaCjMom3can8CQNhWrTRx" +TEST_VERKEY = "did:key:z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL" +TEST_ROUTE_RECORD_VERKEY = "9WCgWKUaAJj3VWxxtzvvMQN3AoFxoBtBDo9ntwJnVVCC" +TEST_ROUTE_VERKEY = "did:key:z6MknxTj6Zj1VrDWc1ofaZtmCVv2zNXpD58Xup4ijDGoQhya" + + +class MockRouteManager(RouteManager): + """Concretion of RouteManager for testing.""" + + _route_for_key = mock.CoroutineMock() + routing_info = mock.CoroutineMock() + + +@pytest.fixture +def mock_responder(): + yield MockResponder() + + +@pytest.fixture +def profile(mock_responder: MockResponder): + yield InMemoryProfile.test_profile(bind={BaseResponder: mock_responder}) + + +@pytest.fixture +def route_manager(): + manager = MockRouteManager() + manager._route_for_key = mock.CoroutineMock( + return_value=mock.MagicMock(KeylistUpdate) + ) + manager.routing_info = mock.CoroutineMock(return_value=([], "http://example.com")) + yield manager + + +@pytest.fixture +def mediation_route_manager(): + yield CoordinateMediationV1RouteManager() + + +@pytest.fixture +def conn_record(): + record = ConnRecord(connection_id="12345") + record.metadata_get = mock.CoroutineMock(return_value={}) + record.metadata_set = mock.CoroutineMock() + yield record + + +@pytest.mark.asyncio +async def test_get_or_create_my_did_no_did( + profile: Profile, route_manager: RouteManager, conn_record: ConnRecord +): + conn_record.my_did = None + mock_did_info = mock.MagicMock() + with mock.patch.object( + InMemoryWallet, + "create_local_did", + mock.CoroutineMock(return_value=mock_did_info), + ) as mock_create_local_did, mock.patch.object( + conn_record, "save", mock.CoroutineMock() + ) as mock_save: + info = await route_manager.get_or_create_my_did(profile, conn_record) + assert mock_did_info == info + mock_create_local_did.assert_called_once() + mock_save.assert_called_once() + + +@pytest.mark.asyncio +async def test_get_or_create_my_did_existing_did( + profile: Profile, route_manager: RouteManager, conn_record: ConnRecord +): + conn_record.my_did = "test-did" + mock_did_info = mock.MagicMock(DIDInfo) + with mock.patch.object( + InMemoryWallet, "get_local_did", mock.CoroutineMock(return_value=mock_did_info) + ) as mock_get_local_did: + info = await route_manager.get_or_create_my_did(profile, conn_record) + assert mock_did_info == info + mock_get_local_did.assert_called_once() + + +@pytest.mark.asyncio +async def test_mediation_record_for_connection_mediation_id( + profile: Profile, route_manager: RouteManager, conn_record: ConnRecord +): + mediation_record = MediationRecord(mediation_id="test-mediation-id") + with mock.patch.object( + route_manager, + "mediation_record_if_id", + mock.CoroutineMock(return_value=mediation_record), + ) as mock_mediation_record_if_id, mock.patch.object( + route_manager, "save_mediator_for_connection", mock.CoroutineMock() + ): + assert ( + await route_manager.mediation_record_for_connection( + profile, conn_record, mediation_record.mediation_id + ) + == mediation_record + ) + mock_mediation_record_if_id.assert_called_once_with( + profile, mediation_record.mediation_id, False + ) + + +@pytest.mark.asyncio +async def test_mediation_record_for_connection_mediation_metadata( + profile: Profile, route_manager: RouteManager, conn_record: ConnRecord +): + mediation_record = MediationRecord(mediation_id="test-mediation-id") + conn_record.metadata_get.return_value = { + MediationManager.METADATA_ID: mediation_record.mediation_id + } + with mock.patch.object( + route_manager, + "mediation_record_if_id", + mock.CoroutineMock(return_value=mediation_record), + ) as mock_mediation_record_if_id, mock.patch.object( + route_manager, "save_mediator_for_connection", mock.CoroutineMock() + ): + assert ( + await route_manager.mediation_record_for_connection( + profile, conn_record, "another-mediation-id" + ) + == mediation_record + ) + mock_mediation_record_if_id.assert_called_once_with( + profile, mediation_record.mediation_id, False + ) + + +@pytest.mark.asyncio +async def test_mediation_record_for_connection_default( + profile: Profile, route_manager: RouteManager, conn_record: ConnRecord +): + mediation_record = MediationRecord(mediation_id="test-mediation-id") + with mock.patch.object( + route_manager, + "mediation_record_if_id", + mock.CoroutineMock(return_value=mediation_record), + ) as mock_mediation_record_if_id, mock.patch.object( + route_manager, "save_mediator_for_connection", mock.CoroutineMock() + ): + assert ( + await route_manager.mediation_record_for_connection( + profile, conn_record, None, or_default=True + ) + == mediation_record + ) + mock_mediation_record_if_id.assert_called_once_with(profile, None, True) + + +@pytest.mark.asyncio +async def test_mediation_record_if_id_with_id( + profile: Profile, route_manager: RouteManager +): + mediation_record = MediationRecord( + mediation_id="test-mediation-id", state=MediationRecord.STATE_GRANTED + ) + with mock.patch.object( + MediationRecord, + "retrieve_by_id", + mock.CoroutineMock(return_value=mediation_record), + ) as mock_retrieve_by_id: + actual = await route_manager.mediation_record_if_id( + profile, mediation_id=mediation_record.mediation_id + ) + assert mediation_record == actual + mock_retrieve_by_id.assert_called_once() + + +@pytest.mark.asyncio +async def test_mediation_record_if_id_with_id_bad_state( + profile: Profile, route_manager: RouteManager +): + mediation_record = MediationRecord( + mediation_id="test-mediation-id", state=MediationRecord.STATE_DENIED + ) + with mock.patch.object( + MediationRecord, + "retrieve_by_id", + mock.CoroutineMock(return_value=mediation_record), + ): + with pytest.raises(RouteManagerError): + await route_manager.mediation_record_if_id( + profile, mediation_id=mediation_record.mediation_id + ) + + +@pytest.mark.asyncio +async def test_mediation_record_if_id_with_id_and_default( + profile: Profile, route_manager: RouteManager +): + mediation_record = MediationRecord( + mediation_id="test-mediation-id", state=MediationRecord.STATE_GRANTED + ) + with mock.patch.object( + MediationRecord, + "retrieve_by_id", + mock.CoroutineMock(return_value=mediation_record), + ) as mock_retrieve_by_id, mock.patch.object( + MediationManager, "get_default_mediator", mock.CoroutineMock() + ) as mock_get_default_mediator: + actual = await route_manager.mediation_record_if_id( + profile, mediation_id=mediation_record.mediation_id, or_default=True + ) + assert mediation_record == actual + mock_retrieve_by_id.assert_called_once() + mock_get_default_mediator.assert_not_called() + + +@pytest.mark.asyncio +async def test_mediation_record_if_id_without_id_and_default( + profile: Profile, + route_manager: RouteManager, +): + mediation_record = MediationRecord( + mediation_id="test-mediation-id", state=MediationRecord.STATE_GRANTED + ) + with mock.patch.object( + MediationRecord, "retrieve_by_id", mock.CoroutineMock() + ) as mock_retrieve_by_id, mock.patch.object( + MediationManager, + "get_default_mediator", + mock.CoroutineMock(return_value=mediation_record), + ) as mock_get_default_mediator: + actual = await route_manager.mediation_record_if_id( + profile, mediation_id=None, or_default=True + ) + assert mediation_record == actual + mock_retrieve_by_id.assert_not_called() + mock_get_default_mediator.assert_called_once() + + +@pytest.mark.asyncio +async def test_mediation_record_if_id_without_id_and_no_default( + profile: Profile, + route_manager: RouteManager, +): + with mock.patch.object( + MediationRecord, "retrieve_by_id", mock.CoroutineMock(return_value=None) + ) as mock_retrieve_by_id, mock.patch.object( + MediationManager, "get_default_mediator", mock.CoroutineMock(return_value=None) + ) as mock_get_default_mediator: + assert ( + await route_manager.mediation_record_if_id( + profile, mediation_id=None, or_default=True + ) + is None + ) + mock_retrieve_by_id.assert_not_called() + mock_get_default_mediator.assert_called_once() + + +@pytest.mark.asyncio +async def test_route_connection_as_invitee( + profile: Profile, route_manager: RouteManager, conn_record: ConnRecord +): + mediation_record = MediationRecord(mediation_id="test-mediation-id") + mock_did_info = mock.MagicMock(DIDInfo) + with mock.patch.object( + route_manager, + "get_or_create_my_did", + mock.CoroutineMock(return_value=mock_did_info), + ): + await route_manager.route_connection_as_invitee( + profile, conn_record, mediation_record + ) + route_manager._route_for_key.assert_called_once_with( + profile, mock_did_info.verkey, mediation_record, skip_if_exists=True + ) + + +@pytest.mark.asyncio +async def test_route_connection_as_inviter( + profile: Profile, route_manager: RouteManager, conn_record: ConnRecord +): + mediation_record = MediationRecord(mediation_id="test-mediation-id") + mock_did_info = mock.MagicMock(DIDInfo) + conn_record.invitation_key = "test-invitation-key" + with mock.patch.object( + route_manager, + "get_or_create_my_did", + mock.CoroutineMock(return_value=mock_did_info), + ): + await route_manager.route_connection_as_inviter( + profile, conn_record, mediation_record + ) + route_manager._route_for_key.assert_called_once_with( + profile, + mock_did_info.verkey, + mediation_record, + replace_key="test-invitation-key", + skip_if_exists=True, + ) + + +@pytest.mark.asyncio +async def test_route_connection_state_inviter_replace_key_none( + profile: Profile, route_manager: RouteManager, conn_record: ConnRecord +): + mediation_record = MediationRecord(mediation_id="test-mediation-id") + mock_did_info = mock.MagicMock(DIDInfo) + conn_record.invitation_key = TEST_RECORD_VERKEY + + with mock.patch.object( + route_manager, + "get_or_create_my_did", + mock.CoroutineMock(return_value=mock_did_info), + ), mock.patch.object( + InMemoryWallet, + "get_public_did", + mock.CoroutineMock( + return_value=DIDInfo( + TEST_RECORD_DID, + TEST_RECORD_VERKEY, + None, + method=SOV, + key_type=ED25519, + ) + ), + ): + await route_manager.route_connection_as_inviter( + profile, conn_record, mediation_record + ) + route_manager._route_for_key.assert_called_once_with( + profile, + mock_did_info.verkey, + mediation_record, + replace_key=None, + skip_if_exists=True, + ) + + +@pytest.mark.asyncio +async def test_route_connection_state_invitee( + profile: Profile, route_manager: RouteManager, conn_record: ConnRecord +): + mediation_record = MediationRecord(mediation_id="test-mediation-id") + conn_record.state = "invitation" + conn_record.their_role = "responder" + with mock.patch.object( + route_manager, "route_connection_as_invitee", mock.CoroutineMock() + ) as mock_route_connection_as_invitee, mock.patch.object( + route_manager, "route_connection_as_inviter", mock.CoroutineMock() + ) as mock_route_connection_as_inviter: + await route_manager.route_connection(profile, conn_record, mediation_record) + mock_route_connection_as_invitee.assert_called_once() + mock_route_connection_as_inviter.assert_not_called() + + +@pytest.mark.asyncio +async def test_route_connection_state_inviter( + profile: Profile, route_manager: RouteManager, conn_record: ConnRecord +): + mediation_record = MediationRecord(mediation_id="test-mediation-id") + conn_record.state = "request" + conn_record.their_role = "requester" + with mock.patch.object( + route_manager, "route_connection_as_invitee", mock.CoroutineMock() + ) as mock_route_connection_as_invitee, mock.patch.object( + route_manager, "route_connection_as_inviter", mock.CoroutineMock() + ) as mock_route_connection_as_inviter: + await route_manager.route_connection(profile, conn_record, mediation_record) + mock_route_connection_as_inviter.assert_called_once() + mock_route_connection_as_invitee.assert_not_called() + + +@pytest.mark.asyncio +async def test_route_connection_state_other( + profile: Profile, route_manager: RouteManager, conn_record: ConnRecord +): + mediation_record = MediationRecord(mediation_id="test-mediation-id") + conn_record.state = "response" + conn_record.their_role = "requester" + assert ( + await route_manager.route_connection(profile, conn_record, mediation_record) + is None + ) + + +@pytest.mark.asyncio +async def test_route_invitation_with_key( + profile: Profile, route_manager: RouteManager, conn_record: ConnRecord +): + mediation_record = MediationRecord(mediation_id="test-mediation-id") + conn_record.invitation_key = "test-invitation-key" + with mock.patch.object( + route_manager, "save_mediator_for_connection", mock.CoroutineMock() + ): + await route_manager.route_invitation(profile, conn_record, mediation_record) + route_manager._route_for_key.assert_called_once() + + +@pytest.mark.asyncio +async def test_route_invitation_without_key( + profile: Profile, route_manager: RouteManager, conn_record: ConnRecord +): + mediation_record = MediationRecord(mediation_id="test-mediation-id") + with mock.patch.object( + route_manager, "save_mediator_for_connection", mock.CoroutineMock() + ): + with pytest.raises(ValueError): + await route_manager.route_invitation(profile, conn_record, mediation_record) + route_manager._route_for_key.assert_not_called() + + +@pytest.mark.asyncio +async def test_route_public_did(profile: Profile, route_manager: RouteManager): + await route_manager.route_public_did(profile, "test-verkey") + route_manager._route_for_key.assert_called_once_with( + profile, "test-verkey", skip_if_exists=True + ) + + +@pytest.mark.asyncio +async def test_route_verkey(profile: Profile, route_manager: RouteManager): + await route_manager.route_verkey(profile, "test-verkey") + route_manager._route_for_key.assert_called_once_with( + profile, "test-verkey", skip_if_exists=True + ) + + +@pytest.mark.asyncio +async def test_route_static( + profile: Profile, route_manager: RouteManager, conn_record: ConnRecord +): + mediation_record = MediationRecord(mediation_id="test-mediation-id") + mock_did_info = mock.MagicMock(DIDInfo) + conn_record.invitation_key = "test-invitation-key" + with mock.patch.object( + route_manager, + "get_or_create_my_did", + mock.CoroutineMock(return_value=mock_did_info), + ): + await route_manager.route_static(profile, conn_record, mediation_record) + route_manager._route_for_key.assert_called_once_with( + profile, + mock_did_info.verkey, + mediation_record, + skip_if_exists=True, + ) + + +@pytest.mark.asyncio +async def test_save_mediator_for_connection_record( + profile: Profile, + route_manager: RouteManager, + conn_record: ConnRecord, +): + mediation_record = MediationRecord(mediation_id="test-mediation-id") + session = mock.MagicMock() + profile.session = mock.MagicMock(return_value=session) + session.__aenter__ = mock.CoroutineMock(return_value=session) + session.__aexit__ = mock.CoroutineMock() + with mock.patch.object( + MediationRecord, "retrieve_by_id", mock.CoroutineMock() + ) as mock_retrieve_by_id: + await route_manager.save_mediator_for_connection( + profile, conn_record, mediation_record + ) + mock_retrieve_by_id.assert_not_called() + conn_record.metadata_set.assert_called_once_with( + session, + MediationManager.METADATA_KEY, + {MediationManager.METADATA_ID: mediation_record.mediation_id}, + ) + + +@pytest.mark.asyncio +async def test_save_mediator_for_connection_id( + profile: Profile, + route_manager: RouteManager, + conn_record: ConnRecord, +): + mediation_record = MediationRecord(mediation_id="test-mediation-id") + session = mock.MagicMock() + profile.session = mock.MagicMock(return_value=session) + session.__aenter__ = mock.CoroutineMock(return_value=session) + session.__aexit__ = mock.CoroutineMock() + with mock.patch.object( + MediationRecord, + "retrieve_by_id", + mock.CoroutineMock(return_value=mediation_record), + ) as mock_retrieve_by_id: + await route_manager.save_mediator_for_connection( + profile, conn_record, mediation_id=mediation_record.mediation_id + ) + mock_retrieve_by_id.assert_called_once() + conn_record.metadata_set.assert_called_once_with( + session, + MediationManager.METADATA_KEY, + {MediationManager.METADATA_ID: mediation_record.mediation_id}, + ) + + +@pytest.mark.asyncio +async def test_save_mediator_for_connection_no_mediator( + profile: Profile, + route_manager: RouteManager, + conn_record: ConnRecord, +): + with mock.patch.object( + MediationRecord, "retrieve_by_id", mock.CoroutineMock() + ) as mock_retrieve_by_id: + await route_manager.save_mediator_for_connection(profile, conn_record) + mock_retrieve_by_id.assert_not_called() + conn_record.metadata_set.assert_not_called() + + +@pytest.mark.asyncio +async def test_connection_from_recipient_key_invite( + profile: Profile, route_manager: RouteManager, conn_record: ConnRecord +): + with mock.patch.object( + ConnRecord, + "retrieve_by_tag_filter", + mock.CoroutineMock(return_value=conn_record), + ): + result = await route_manager.connection_from_recipient_key(profile, TEST_VERKEY) + assert conn_record == result + + +@pytest.mark.asyncio +async def test_connection_from_recipient_key_local_did( + profile: Profile, route_manager: RouteManager, conn_record: ConnRecord +): + mock_provider = mock.MagicMock() + mock_wallet = mock.MagicMock() + mock_wallet.get_local_did_for_verkey = mock.CoroutineMock() + mock_provider.provide = mock.MagicMock(return_value=mock_wallet) + session = await profile.session() + session.context.injector.bind_provider(BaseWallet, mock_provider) + with mock.patch.object( + profile, "session", mock.MagicMock(return_value=session) + ), mock.patch.object( + ConnRecord, "retrieve_by_did", mock.CoroutineMock(return_value=conn_record) + ): + result = await route_manager.connection_from_recipient_key(profile, TEST_VERKEY) + assert conn_record == result + + +@pytest.mark.asyncio +async def test_mediation_route_for_key( + profile: Profile, + mediation_route_manager: CoordinateMediationV1RouteManager, + mock_responder: MockResponder, +): + mediation_record = MediationRecord( + mediation_id="test-mediation-id", connection_id="test-mediator-conn-id" + ) + keylist_update = await mediation_route_manager._route_for_key( + profile, + TEST_VERKEY, + mediation_record, + skip_if_exists=False, + replace_key=None, + ) + assert keylist_update + assert keylist_update.serialize()["updates"] == [ + {"action": "add", "recipient_key": TEST_VERKEY} + ] + assert mock_responder.messages + assert ( + keylist_update, + {"connection_id": "test-mediator-conn-id"}, + ) == mock_responder.messages[0] + + +@pytest.mark.asyncio +async def test_mediation_route_for_key_skip_if_exists_and_exists( + profile: Profile, + mediation_route_manager: CoordinateMediationV1RouteManager, + mock_responder: MockResponder, +): + mediation_record = MediationRecord( + mediation_id="test-mediation-id", connection_id="test-mediator-conn-id" + ) + with mock.patch.object( + RouteRecord, "retrieve_by_recipient_key", mock.CoroutineMock() + ): + keylist_update = await mediation_route_manager._route_for_key( + profile, + TEST_VERKEY, + mediation_record, + skip_if_exists=True, + replace_key=None, + ) + assert keylist_update is None + assert not mock_responder.messages + + +@pytest.mark.asyncio +async def test_mediation_route_for_key_skip_if_exists_and_absent( + profile: Profile, + mediation_route_manager: CoordinateMediationV1RouteManager, + mock_responder: MockResponder, +): + mediation_record = MediationRecord( + mediation_id="test-mediation-id", connection_id="test-mediator-conn-id" + ) + with mock.patch.object( + RouteRecord, + "retrieve_by_recipient_key", + mock.CoroutineMock(side_effect=StorageNotFoundError), + ): + keylist_update = await mediation_route_manager._route_for_key( + profile, + TEST_VERKEY, + mediation_record, + skip_if_exists=True, + replace_key=None, + ) + assert keylist_update + assert keylist_update.serialize()["updates"] == [ + {"action": "add", "recipient_key": TEST_VERKEY} + ] + assert mock_responder.messages + assert ( + keylist_update, + {"connection_id": "test-mediator-conn-id"}, + ) == mock_responder.messages[0] + + +@pytest.mark.asyncio +async def test_mediation_route_for_key_replace_key( + profile: Profile, + mediation_route_manager: CoordinateMediationV1RouteManager, + mock_responder: MockResponder, +): + mediation_record = MediationRecord( + mediation_id="test-mediation-id", connection_id="test-mediator-conn-id" + ) + keylist_update = await mediation_route_manager._route_for_key( + profile, + TEST_VERKEY, + mediation_record, + skip_if_exists=False, + replace_key=TEST_ROUTE_VERKEY, + ) + assert keylist_update + assert keylist_update.serialize()["updates"] == [ + {"action": "add", "recipient_key": TEST_VERKEY}, + {"action": "remove", "recipient_key": TEST_ROUTE_VERKEY}, + ] + assert mock_responder.messages + assert ( + keylist_update, + {"connection_id": "test-mediator-conn-id"}, + ) == mock_responder.messages[0] + + +@pytest.mark.asyncio +async def test_mediation_route_for_key_no_mediator( + profile: Profile, + mediation_route_manager: CoordinateMediationV1RouteManager, +): + assert ( + await mediation_route_manager._route_for_key( + profile, + TEST_VERKEY, + None, + skip_if_exists=True, + replace_key=TEST_ROUTE_VERKEY, + ) + is None + ) + + +@pytest.mark.asyncio +async def test_mediation_routing_info_with_mediator( + profile: Profile, + mediation_route_manager: CoordinateMediationV1RouteManager, +): + mediation_record = MediationRecord( + mediation_id="test-mediation-id", + connection_id="test-mediator-conn-id", + routing_keys=["test-key-0", "test-key-1"], + endpoint="http://mediator.example.com", + ) + keys, endpoint = await mediation_route_manager.routing_info( + profile, "http://example.com", mediation_record + ) + assert keys == mediation_record.routing_keys + assert endpoint == mediation_record.endpoint + + +@pytest.mark.asyncio +async def test_mediation_routing_info_no_mediator( + profile: Profile, + mediation_route_manager: CoordinateMediationV1RouteManager, +): + keys, endpoint = await mediation_route_manager.routing_info( + profile, "http://example.com", None + ) + assert keys == [] + assert endpoint == "http://example.com" diff --git a/aries_cloudagent/protocols/coordinate_mediation/v1_0/tests/test_routes.py b/aries_cloudagent/protocols/coordinate_mediation/v1_0/tests/test_routes.py index a19070a6e6..ababf0ea16 100644 --- a/aries_cloudagent/protocols/coordinate_mediation/v1_0/tests/test_routes.py +++ b/aries_cloudagent/protocols/coordinate_mediation/v1_0/tests/test_routes.py @@ -1,22 +1,19 @@ -import json - -from asynctest import mock as async_mock, TestCase as AsyncTestCase +from asynctest import TestCase as AsyncTestCase, mock as async_mock +from .. import routes as test_module from .....admin.request_context import AdminRequestContext from .....core.in_memory import InMemoryProfile -from .....config.injection_context import InjectionContext -from .....messaging.request_context import RequestContext - -from .. import routes as test_module -from ..manager import MediationManager +from .....storage.error import StorageError, StorageNotFoundError from ..models.mediation_record import MediationRecord +from ..route_manager import RouteManager +from .....wallet.did_method import DIDMethods class TestCoordinateMediationRoutes(AsyncTestCase): def setUp(self): self.profile = InMemoryProfile.test_profile() - self.context = self.profile.context - setattr(self.context, "profile", self.profile) + self.profile.context.injector.bind_instance(DIDMethods, DIDMethods()) + self.context = AdminRequestContext.test_context(profile=self.profile) self.outbound_message_router = async_mock.CoroutineMock() self.request_dict = { "context": self.context, @@ -77,7 +74,7 @@ async def test_list_mediation_requests(self): ) as mock_query, async_mock.patch.object( test_module.web, "json_response" ) as json_response, async_mock.patch.object( - self.context.profile, + self.profile, "session", async_mock.MagicMock(return_value=InMemoryProfile.test_session()), ) as session: @@ -99,7 +96,7 @@ async def test_list_mediation_requests_filters(self): ) as mock_query, async_mock.patch.object( test_module.web, "json_response" ) as json_response, async_mock.patch.object( - self.context.profile, + self.profile, "session", async_mock.MagicMock(return_value=InMemoryProfile.test_session()), ) as session: @@ -394,7 +391,7 @@ async def test_mediation_request_deny_x_storage_error(self): await test_module.mediation_request_deny(self.request) async def test_get_keylist(self): - session = await self.context.profile.session() + session = await self.profile.session() self.request.query["role"] = MediationRecord.ROLE_SERVER self.request.query["conn_id"] = "test-id" @@ -411,7 +408,7 @@ async def test_get_keylist(self): "query", async_mock.CoroutineMock(return_value=query_results), ) as mock_query, async_mock.patch.object( - self.context.profile, + self.profile, "session", async_mock.MagicMock(return_value=session), ) as mock_session, async_mock.patch.object( @@ -427,13 +424,13 @@ async def test_get_keylist(self): ) async def test_get_keylist_no_matching_records(self): - session = await self.context.profile.session() + session = await self.profile.session() with async_mock.patch.object( test_module.RouteRecord, "query", async_mock.CoroutineMock(return_value=[]), ) as mock_query, async_mock.patch.object( - self.context.profile, + self.profile, "session", async_mock.MagicMock(return_value=session), ) as mock_session, async_mock.patch.object( @@ -454,8 +451,26 @@ async def test_get_keylist_storage_error(self): async def test_send_keylist_update(self): body = { "updates": [ - {"recipient_key": "test-key0", "action": "add"}, - {"recipient_key": "test-key1", "action": "remove"}, + { + "recipient_key": "EwUKjVLboiLSuoWSEtDvrgrd41EUxG5bLecQrkHB63Up", + "action": "add", + }, + { + "recipient_key": "3Dn1SJNPaCXcvvJvSbsFWP2xaCjMom3can8CQNhWrTRx", + "action": "remove", + }, + ] + } + body_with_didkey = { + "updates": [ + { + "recipient_key": "did:key:z6MktPjNKjb39Fpv2JM8vTBmhnQcsaWLN9Kx2fXLh2FC1GGC", + "action": "add", + }, + { + "recipient_key": "did:key:z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL", + "action": "remove", + }, ] } @@ -477,13 +492,16 @@ async def test_send_keylist_update(self): ), ) as mock_response: results, status = await test_module.send_keylist_update(self.request) - assert results["updates"] == body["updates"] + assert results["updates"] == body_with_didkey["updates"] assert status == 201 async def test_send_keylist_update_bad_action(self): self.request.json.return_value = { "updates": [ - {"recipient_key": "test-key0", "action": "wrong"}, + { + "recipient_key": "3Dn1SJNPaCXcvvJvSbsFWP2xaCjMom3can8CQNhWrTRx", + "action": "wrong", + }, ] } @@ -493,7 +511,10 @@ async def test_send_keylist_update_bad_action(self): async def test_send_keylist_update_bad_mediation_state(self): self.request.json.return_value = { "updates": [ - {"recipient_key": "test-key0", "action": "add"}, + { + "recipient_key": "EwUKjVLboiLSuoWSEtDvrgrd41EUxG5bLecQrkHB63Up", + "action": "add", + }, ] } @@ -516,7 +537,10 @@ async def test_send_keylist_update_bad_updates(self): async def test_send_keylist_update_x_no_mediation_rec(self): self.request.json.return_value = { "updates": [ - {"recipient_key": "test-key0", "action": "add"}, + { + "recipient_key": "EwUKjVLboiLSuoWSEtDvrgrd41EUxG5bLecQrkHB63Up", + "action": "add", + }, ] } with async_mock.patch.object( @@ -529,7 +553,10 @@ async def test_send_keylist_update_x_no_mediation_rec(self): async def test_send_keylist_update_x_storage_error(self): self.request.json.return_value = { "updates": [ - {"recipient_key": "test-key0", "action": "add"}, + { + "recipient_key": "EwUKjVLboiLSuoWSEtDvrgrd41EUxG5bLecQrkHB63Up", + "action": "add", + }, ] } @@ -583,7 +610,6 @@ async def test_send_keylist_query_x_storage_error(self): async def test_get_default_mediator(self): self.request.query = {} - self.context.session = async_mock.CoroutineMock() with async_mock.patch.object( test_module.web, "json_response" ) as json_response, async_mock.patch.object( @@ -599,7 +625,6 @@ async def test_get_default_mediator(self): async def test_get_empty_default_mediator(self): self.request.query = {} - self.context.session = async_mock.CoroutineMock() with async_mock.patch.object( test_module.web, "json_response" ) as json_response, async_mock.patch.object( @@ -615,7 +640,6 @@ async def test_get_empty_default_mediator(self): async def test_get_default_mediator_storage_error(self): self.request.query = {} - self.context.session = async_mock.CoroutineMock() with async_mock.patch.object( test_module.web, "json_response" ) as json_response, async_mock.patch.object( @@ -631,7 +655,6 @@ async def test_set_default_mediator(self): "mediation_id": "fake_id", } self.request.query = {} - self.context.session = async_mock.CoroutineMock() with async_mock.patch.object( test_module.MediationManager, "get_default_mediator", @@ -654,7 +677,6 @@ async def test_set_default_mediator_storage_error(self): "mediation_id": "bad_id", } self.request.query = {} - self.context.session = async_mock.CoroutineMock() with async_mock.patch.object( test_module.MediationManager, "get_default_mediator", @@ -671,7 +693,6 @@ async def test_set_default_mediator_storage_error(self): async def test_clear_default_mediator(self): self.request.query = {} - self.context.session = async_mock.CoroutineMock() with async_mock.patch.object( test_module.MediationManager, "get_default_mediator", @@ -691,7 +712,6 @@ async def test_clear_default_mediator(self): async def test_clear_default_mediator_storage_error(self): self.request.query = {} - self.context.session = async_mock.CoroutineMock() with async_mock.patch.object( test_module.MediationManager, "get_default_mediator", @@ -706,6 +726,72 @@ async def test_clear_default_mediator_storage_error(self): with self.assertRaises(test_module.web.HTTPBadRequest): await test_module.clear_default_mediator(self.request) + async def test_update_keylist_for_connection(self): + self.request.query = {} + self.request.json.return_value = {"mediation_id": "test-mediation-id"} + self.request.match_info = { + "conn_id": "test-conn-id", + } + mock_route_manager = async_mock.MagicMock(RouteManager) + mock_keylist_update = async_mock.MagicMock() + mock_keylist_update.serialize.return_value = {"mock": "serialized"} + mock_route_manager.route_connection = async_mock.CoroutineMock( + return_value=mock_keylist_update + ) + mock_route_manager.mediation_record_for_connection = async_mock.CoroutineMock() + self.context.injector.bind_instance(RouteManager, mock_route_manager) + with async_mock.patch.object( + test_module.ConnRecord, "retrieve_by_id", async_mock.CoroutineMock() + ) as mock_conn_rec_retrieve_by_id, async_mock.patch.object( + test_module.web, "json_response" + ) as json_response: + await test_module.update_keylist_for_connection(self.request) + json_response.assert_called_once_with({"mock": "serialized"}, status=200) + + async def test_update_keylist_for_connection_not_found(self): + self.request.query = {} + self.request.json.return_value = {"mediation_id": "test-mediation-id"} + self.request.match_info = { + "conn_id": "test-conn-id", + } + mock_route_manager = async_mock.MagicMock(RouteManager) + mock_keylist_update = async_mock.MagicMock() + mock_keylist_update.serialize.return_value = {"mock": "serialized"} + mock_route_manager.route_connection = async_mock.CoroutineMock( + return_value=mock_keylist_update + ) + mock_route_manager.mediation_record_for_connection = async_mock.CoroutineMock() + self.context.injector.bind_instance(RouteManager, mock_route_manager) + with async_mock.patch.object( + test_module.ConnRecord, + "retrieve_by_id", + async_mock.CoroutineMock(side_effect=StorageNotFoundError), + ) as mock_conn_rec_retrieve_by_id: + with self.assertRaises(test_module.web.HTTPNotFound): + await test_module.update_keylist_for_connection(self.request) + + async def test_update_keylist_for_connection_storage_error(self): + self.request.query = {} + self.request.json.return_value = {"mediation_id": "test-mediation-id"} + self.request.match_info = { + "conn_id": "test-conn-id", + } + mock_route_manager = async_mock.MagicMock(RouteManager) + mock_keylist_update = async_mock.MagicMock() + mock_keylist_update.serialize.return_value = {"mock": "serialized"} + mock_route_manager.route_connection = async_mock.CoroutineMock( + return_value=mock_keylist_update + ) + mock_route_manager.mediation_record_for_connection = async_mock.CoroutineMock() + self.context.injector.bind_instance(RouteManager, mock_route_manager) + with async_mock.patch.object( + test_module.ConnRecord, + "retrieve_by_id", + async_mock.CoroutineMock(side_effect=StorageError), + ) as mock_conn_rec_retrieve_by_id: + with self.assertRaises(test_module.web.HTTPBadRequest): + await test_module.update_keylist_for_connection(self.request) + async def test_register(self): mock_app = async_mock.MagicMock() mock_app.add_routes = async_mock.MagicMock() diff --git a/aries_cloudagent/protocols/didexchange/v1_0/handlers/request_handler.py b/aries_cloudagent/protocols/didexchange/v1_0/handlers/request_handler.py index b7f036e11f..766ecb8571 100644 --- a/aries_cloudagent/protocols/didexchange/v1_0/handlers/request_handler.py +++ b/aries_cloudagent/protocols/didexchange/v1_0/handlers/request_handler.py @@ -2,9 +2,8 @@ from .....connections.models.conn_record import ConnRecord from .....messaging.base_handler import BaseHandler, BaseResponder, RequestContext - +from ....coordinate_mediation.v1_0.manager import MediationManager from ....problem_report.v1_0.message import ProblemReport - from ..manager import DIDXManager, DIDXManagerError from ..messages.request import DIDXRequest @@ -27,15 +26,14 @@ async def handle(self, context: RequestContext, responder: BaseResponder): profile = context.profile mgr = DIDXManager(profile) + mediation_id = None if context.connection_record: async with profile.session() as session: mediation_metadata = await context.connection_record.metadata_get( - session, "mediation", {} + session, MediationManager.METADATA_KEY, {} ) - else: - mediation_metadata = {} + mediation_id = mediation_metadata.get(MediationManager.METADATA_ID) - mediation_id = mediation_metadata.get("id") try: conn_rec = await mgr.receive_request( request=context.message, @@ -45,7 +43,6 @@ async def handle(self, context: RequestContext, responder: BaseResponder): if context.message_receipt.recipient_did_public else context.message_receipt.recipient_verkey ), - mediation_id=mediation_id, ) # Auto respond diff --git a/aries_cloudagent/protocols/didexchange/v1_0/handlers/tests/test_complete_handler.py b/aries_cloudagent/protocols/didexchange/v1_0/handlers/tests/test_complete_handler.py index bbeba7c27f..19372e515c 100644 --- a/aries_cloudagent/protocols/didexchange/v1_0/handlers/tests/test_complete_handler.py +++ b/aries_cloudagent/protocols/didexchange/v1_0/handlers/tests/test_complete_handler.py @@ -10,11 +10,13 @@ from ...messages.problem_report_reason import ProblemReportReason from .. import complete_handler as test_module +from ......wallet.did_method import DIDMethods @pytest.fixture() def request_context() -> RequestContext: ctx = RequestContext.test_context() + ctx.injector.bind_instance(DIDMethods, DIDMethods()) ctx.message_receipt = MessageReceipt() yield ctx diff --git a/aries_cloudagent/protocols/didexchange/v1_0/handlers/tests/test_invitation_handler.py b/aries_cloudagent/protocols/didexchange/v1_0/handlers/tests/test_invitation_handler.py index 95a728310c..279d905863 100644 --- a/aries_cloudagent/protocols/didexchange/v1_0/handlers/tests/test_invitation_handler.py +++ b/aries_cloudagent/protocols/didexchange/v1_0/handlers/tests/test_invitation_handler.py @@ -9,11 +9,13 @@ from ...handlers.invitation_handler import InvitationHandler from ...messages.problem_report_reason import ProblemReportReason +from ......wallet.did_method import DIDMethods @pytest.fixture() def request_context() -> RequestContext: ctx = RequestContext.test_context() + ctx.injector.bind_instance(DIDMethods, DIDMethods()) ctx.message_receipt = MessageReceipt() yield ctx diff --git a/aries_cloudagent/protocols/didexchange/v1_0/handlers/tests/test_request_handler.py b/aries_cloudagent/protocols/didexchange/v1_0/handlers/tests/test_request_handler.py index 21eb4c2688..6c7a5892b8 100644 --- a/aries_cloudagent/protocols/didexchange/v1_0/handlers/tests/test_request_handler.py +++ b/aries_cloudagent/protocols/didexchange/v1_0/handlers/tests/test_request_handler.py @@ -8,15 +8,12 @@ PublicKeyType, Service, ) -from ......core.profile import ProfileSession from ......core.in_memory import InMemoryProfile -from ......wallet.key_type import KeyType -from ......wallet.did_method import DIDMethod +from ......wallet.did_method import SOV, DIDMethods +from ......wallet.key_type import ED25519 from ......messaging.decorators.attach_decorator import AttachDecorator from ......messaging.request_context import RequestContext from ......messaging.responder import MockResponder -from ......storage.base import BaseStorage -from ......storage.error import StorageNotFoundError from ......transport.inbound.receipt import MessageReceipt from .....problem_report.v1_0.message import ProblemReport @@ -79,6 +76,7 @@ async def setUp(self): "debug.auto_accept_requests_public": True, } ) + self.session.profile.context.injector.bind_instance(DIDMethods, DIDMethods()) self.conn_rec = conn_record.ConnRecord( my_did="55GkHamhTU1ZbTbV2ab9DE", @@ -90,9 +88,7 @@ async def setUp(self): await self.conn_rec.save(self.session) wallet = self.session.wallet - self.did_info = await wallet.create_local_did( - method=DIDMethod.SOV, key_type=KeyType.ED25519 - ) + self.did_info = await wallet.create_local_did(method=SOV, key_type=ED25519) self.did_doc_attach = AttachDecorator.data_base64(self.did_doc().serialize()) await self.did_doc_attach.data.sign(self.did_info.verkey, wallet) @@ -103,47 +99,6 @@ async def setUp(self): did_doc_attach=self.did_doc_attach, ) - async def test_connection_record_with_mediation_metadata(self): - test_exist_conn = conn_record.ConnRecord( - my_did="did:sov:LjgpST2rjsoxYegQDRm7EL", - their_did="did:sov:LjgpST2rjsoxYegQDRm7EL", - their_public_did="did:sov:LjgpST2rjsoxYegQDRm7EL", - invitation_msg_id="12345678-1234-5678-1234-567812345678", - their_role=conn_record.ConnRecord.Role.REQUESTER, - ) - await test_exist_conn.save(self.session) - await test_exist_conn.metadata_set( - self.session, "mediation", {"id": "mediation-test-id"} - ) - test_ctx = RequestContext.test_context() - test_ctx.message = DIDXRequest() - test_ctx.message_receipt = MessageReceipt() - test_ctx.connection_record = test_exist_conn - responder = MockResponder() - handler_inst = test_module.DIDXRequestHandler() - await handler_inst.handle(test_ctx, responder) - mediation_metadata = await test_ctx.connection_record.metadata_get( - self.session, "mediation", {} - ) - assert mediation_metadata.get("id") == "mediation-test-id" - assert not responder.messages - - @async_mock.patch.object(test_module, "DIDXManager") - async def test_connection_record_without_mediation_metadata(self, mock_didx_mgr): - mock_didx_mgr.return_value.receive_request = async_mock.CoroutineMock() - self.ctx.message = DIDXRequest() - self.ctx.connection_record = None - handler_inst = test_module.DIDXRequestHandler() - responder = MockResponder() - await handler_inst.handle(self.ctx, responder) - mock_didx_mgr.return_value.receive_request.assert_called_once_with( - request=self.ctx.message, - recipient_did=self.ctx.message_receipt.recipient_did, - recipient_verkey=None, - mediation_id=None, - ) - assert not responder.messages - @async_mock.patch.object(test_module, "DIDXManager") async def test_called(self, mock_didx_mgr): mock_didx_mgr.return_value.receive_request = async_mock.CoroutineMock() @@ -156,7 +111,6 @@ async def test_called(self, mock_didx_mgr): request=self.ctx.message, recipient_did=self.ctx.message_receipt.recipient_did, recipient_verkey=None, - mediation_id=None, ) assert not responder.messages @@ -178,7 +132,41 @@ async def test_called_with_auto_response(self, mock_didx_mgr): request=self.ctx.message, recipient_did=self.ctx.message_receipt.recipient_did, recipient_verkey=None, - mediation_id=None, + ) + mock_didx_mgr.return_value.create_response.assert_called_once_with( + mock_conn_rec, mediation_id=None + ) + assert responder.messages + + @async_mock.patch.object(test_module, "DIDXManager") + async def test_connection_record_with_mediation_metadata_auto_response( + self, mock_didx_mgr + ): + test_exist_conn = conn_record.ConnRecord( + my_did="did:sov:LjgpST2rjsoxYegQDRm7EL", + their_did="did:sov:LjgpST2rjsoxYegQDRm7EL", + their_public_did="did:sov:LjgpST2rjsoxYegQDRm7EL", + invitation_msg_id="12345678-1234-5678-1234-567812345678", + their_role=conn_record.ConnRecord.Role.REQUESTER, + ) + test_exist_conn.metadata_get = async_mock.CoroutineMock( + return_value={"id": "mediation-test-id"} + ) + test_exist_conn.accept = conn_record.ConnRecord.ACCEPT_AUTO + test_exist_conn.save = async_mock.CoroutineMock() + mock_didx_mgr.return_value.receive_request = async_mock.CoroutineMock( + return_value=test_exist_conn + ) + mock_didx_mgr.return_value.create_response = async_mock.CoroutineMock() + test_ctx = RequestContext.test_context() + test_ctx.message = DIDXRequest() + test_ctx.message_receipt = MessageReceipt() + test_ctx.connection_record = test_exist_conn + responder = MockResponder() + handler_inst = test_module.DIDXRequestHandler() + await handler_inst.handle(test_ctx, responder) + mock_didx_mgr.return_value.create_response.assert_called_once_with( + test_exist_conn, mediation_id="mediation-test-id" ) assert responder.messages diff --git a/aries_cloudagent/protocols/didexchange/v1_0/handlers/tests/test_response_handler.py b/aries_cloudagent/protocols/didexchange/v1_0/handlers/tests/test_response_handler.py index a161b5f325..ba81955e36 100644 --- a/aries_cloudagent/protocols/didexchange/v1_0/handlers/tests/test_response_handler.py +++ b/aries_cloudagent/protocols/didexchange/v1_0/handlers/tests/test_response_handler.py @@ -13,8 +13,8 @@ from ......messaging.request_context import RequestContext from ......messaging.responder import MockResponder from ......transport.inbound.receipt import MessageReceipt -from ......wallet.key_type import KeyType -from ......wallet.did_method import DIDMethod +from ......wallet.did_method import SOV, DIDMethods +from ......wallet.key_type import ED25519 from .....problem_report.v1_0.message import ProblemReport from .....trustping.v1_0.messages.ping import Ping @@ -63,10 +63,11 @@ async def setUp(self): self.ctx = RequestContext.test_context() self.ctx.message_receipt = MessageReceipt() + self.ctx.profile.context.injector.bind_instance(DIDMethods, DIDMethods()) wallet = (await self.ctx.session()).wallet self.did_info = await wallet.create_local_did( - method=DIDMethod.SOV, - key_type=KeyType.ED25519, + method=SOV, + key_type=ED25519, ) self.did_doc_attach = AttachDecorator.data_base64(self.did_doc().serialize()) diff --git a/aries_cloudagent/protocols/didexchange/v1_0/manager.py b/aries_cloudagent/protocols/didexchange/v1_0/manager.py index 282bbb8335..b209114b01 100644 --- a/aries_cloudagent/protocols/didexchange/v1_0/manager.py +++ b/aries_cloudagent/protocols/didexchange/v1_0/manager.py @@ -2,37 +2,42 @@ import json import logging +from typing import Optional +import pydid +from pydid import BaseDIDDocument as ResolvedDocument +from pydid import DIDCommService + +from ....connections.base_manager import BaseConnectionManager from ....connections.models.conn_record import ConnRecord from ....connections.models.diddoc import DIDDoc -from ....connections.base_manager import BaseConnectionManager -from ....connections.util import mediation_record_if_id from ....core.error import BaseError +from ....core.oob_processor import OobMessageProcessor from ....core.profile import Profile +from ....did.did_key import DIDKey from ....messaging.decorators.attach_decorator import AttachDecorator from ....messaging.responder import BaseResponder from ....multitenant.base import BaseMultitenantManager +from ....resolver.base import ResolverError +from ....resolver.did_resolver import DIDResolver from ....storage.error import StorageNotFoundError from ....transport.inbound.receipt import MessageReceipt from ....wallet.base import BaseWallet -from ....wallet.error import WalletError -from ....wallet.key_type import KeyType -from ....wallet.did_method import DIDMethod +from ....wallet.did_method import SOV from ....wallet.did_posture import DIDPosture -from ....did.did_key import DIDKey - +from ....wallet.error import WalletError +from ....wallet.key_type import ED25519 from ...coordinate_mediation.v1_0.manager import MediationManager from ...discovery.v2_0.manager import V20DiscoveryMgr from ...out_of_band.v1_0.messages.invitation import ( InvitationMessage as OOBInvitationMessage, ) from ...out_of_band.v1_0.messages.service import Service as OOBService - from .message_types import ARIES_PROTOCOL as DIDX_PROTO from .messages.complete import DIDXComplete +from .messages.problem_report_reason import ProblemReportReason from .messages.request import DIDXRequest from .messages.response import DIDXResponse -from .messages.problem_report_reason import ProblemReportReason class DIDXManagerError(BaseError): @@ -67,10 +72,10 @@ def profile(self) -> Profile: async def receive_invitation( self, invitation: OOBInvitationMessage, - their_public_did: str = None, - auto_accept: bool = None, - alias: str = None, - mediation_id: str = None, + their_public_did: Optional[str] = None, + auto_accept: Optional[bool] = None, + alias: Optional[str] = None, + mediation_id: Optional[str] = None, ) -> ConnRecord: # leave in didexchange as it uses a responder: not out-of-band """ Create a new connection record to track a received invitation. @@ -141,6 +146,17 @@ async def receive_invitation( # Save the invitation for later processing await conn_rec.attach_invitation(session, invitation) + if not conn_rec.invitation_key and conn_rec.their_public_did: + did_document = await self.get_resolved_did_document( + conn_rec.their_public_did + ) + conn_rec.invitation_key = did_document.verification_method[ + 0 + ].public_key_base58 + + await self._route_manager.save_mediator_for_connection( + self.profile, conn_rec, mediation_id=mediation_id + ) if conn_rec.accept == ConnRecord.ACCEPT_AUTO: request = await self.create_request(conn_rec, mediation_id=mediation_id) @@ -242,18 +258,18 @@ async def create_request( """ # Mediation Support - mediation_mgr = MediationManager(self.profile) - keylist_updates = None - mediation_record = await mediation_record_if_id( + mediation_record = await self._route_manager.mediation_record_for_connection( self.profile, + conn_rec, mediation_id, or_default=True, ) - base_mediation_record = None # Multitenancy setup multitenant_mgr = self.profile.inject_or(BaseMultitenantManager) wallet_id = self.profile.settings.get("wallet.id") + + base_mediation_record = None if multitenant_mgr and wallet_id: base_mediation_record = await multitenant_mgr.get_default_mediator() @@ -268,16 +284,15 @@ async def create_request( async with self.profile.session() as session: wallet = session.inject(BaseWallet) my_info = await wallet.create_local_did( - method=DIDMethod.SOV, - key_type=KeyType.ED25519, + method=SOV, + key_type=ED25519, ) conn_rec.my_did = my_info.did - keylist_updates = await mediation_mgr.add_key( - my_info.verkey, keylist_updates - ) - # Add mapping for multitenant relay - if multitenant_mgr and wallet_id: - await multitenant_mgr.add_key(wallet_id, my_info.verkey) + + # Idempotent; if routing has already been set up, no action taken + await self._route_manager.route_connection_as_invitee( + self.profile, conn_rec, mediation_record + ) # Create connection request message if my_endpoint: @@ -288,6 +303,7 @@ async def create_request( if default_endpoint: my_endpoints.append(default_endpoint) my_endpoints.extend(self.profile.settings.get("additional_endpoints", [])) + did_doc = await self.create_did_document( my_info, conn_rec.inbound_connection_id, @@ -296,14 +312,11 @@ async def create_request( filter(None, [base_mediation_record, mediation_record]) ), ) - if ( - conn_rec.their_public_did is not None - and conn_rec.their_public_did.startswith("did:") - ): + if conn_rec.their_public_did is not None: qualified_did = conn_rec.their_public_did - else: - qualified_did = f"did:sov:{conn_rec.their_public_did}" - pthid = conn_rec.invitation_msg_id or qualified_did + did_document = await self.get_resolved_did_document(qualified_did) + did_url = await self.get_first_applicable_didcomm_service(did_document) + pthid = conn_rec.invitation_msg_id or did_url attach = AttachDecorator.data_base64(did_doc.serialize()) async with self.profile.session() as session: wallet = session.inject(BaseWallet) @@ -323,12 +336,6 @@ async def create_request( async with self.profile.session() as session: await conn_rec.save(session, reason="Created connection request") - # Notify Mediator - if keylist_updates and mediation_record: - responder = self.profile.inject_or(BaseResponder) - await responder.send( - keylist_updates, connection_id=mediation_record.connection_id - ) return request async def receive_request( @@ -339,7 +346,6 @@ async def receive_request( my_endpoint: str = None, alias: str = None, auto_accept_implicit: bool = None, - mediation_id: str = None, ) -> ConnRecord: """ Receive and store a connection request. @@ -351,8 +357,6 @@ async def receive_request( my_endpoint: My endpoint alias: Alias for the connection auto_accept: Auto-accept request against implicit invitation - mediation_id: The record id for mediation that contains routing_keys and - service endpoint Returns: The new or updated `ConnRecord` instance @@ -363,16 +367,10 @@ async def receive_request( settings=self.profile.settings, ) - mediation_mgr = MediationManager(self.profile) - keylist_updates = None conn_rec = None connection_key = None my_info = None - # Multitenancy setup - multitenant_mgr = self.profile.inject_or(BaseMultitenantManager) - wallet_id = self.profile.settings.get("wallet.id") - # Determine what key will need to sign the response if recipient_verkey: # peer DID connection_key = recipient_verkey @@ -419,12 +417,9 @@ async def receive_request( async with self.profile.session() as session: wallet = session.inject(BaseWallet) my_info = await wallet.create_local_did( - method=DIDMethod.SOV, - key_type=KeyType.ED25519, + method=SOV, + key_type=ED25519, ) - keylist_updates = await mediation_mgr.add_key( - my_info.verkey, keylist_updates - ) new_conn_rec = ConnRecord( invitation_key=connection_key, @@ -452,14 +447,6 @@ async def receive_request( conn_rec = new_conn_rec - # Add mapping for multitenant relay - if multitenant_mgr and wallet_id: - await multitenant_mgr.add_key(wallet_id, my_info.verkey) - else: - keylist_updates = await mediation_mgr.remove_key( - connection_key, keylist_updates - ) - # request DID doc describes requester DID if not (request.did_doc_attach and request.did_doc_attach.data): raise DIDXManagerError( @@ -468,9 +455,7 @@ async def receive_request( ) async with self.profile.session() as session: wallet = session.inject(BaseWallet) - if not await request.did_doc_attach.data.verify(wallet): - raise DIDXManagerError("DID Doc signature failed verification") - conn_did_doc = DIDDoc.from_json(request.did_doc_attach.data.signed.decode()) + conn_did_doc = await self.verify_diddoc(wallet, request.did_doc_attach) if request.did != conn_did_doc.did: raise DIDXManagerError( ( @@ -498,21 +483,17 @@ async def receive_request( ) else: # request is against implicit invitation on public DID + if not self.profile.settings.get("requests_through_public_did"): + raise DIDXManagerError( + "Unsolicited connection requests to " "public DID is not enabled" + ) async with self.profile.session() as session: wallet = session.inject(BaseWallet) my_info = await wallet.create_local_did( - method=DIDMethod.SOV, - key_type=KeyType.ED25519, + method=SOV, + key_type=ED25519, ) - keylist_updates = await mediation_mgr.add_key( - my_info.verkey, keylist_updates - ) - - # Add mapping for multitenant relay - if multitenant_mgr and wallet_id: - await multitenant_mgr.add_key(wallet_id, my_info.verkey) - auto_accept = bool( auto_accept_implicit or ( @@ -545,13 +526,9 @@ async def receive_request( # Attach the connection request so it can be found and responded to await conn_rec.attach_request(session, request) - # Send keylist updates to mediator - mediation_record = await mediation_record_if_id(self.profile, mediation_id) - if keylist_updates and mediation_record: - responder = self.profile.inject(BaseResponder) - await responder.send( - keylist_updates, connection_id=mediation_record.connection_id - ) + # Clean associated oob record if not needed anymore + oob_processor = self.profile.inject(OobMessageProcessor) + await oob_processor.clean_finished_oob_record(self.profile, request) return conn_rec @@ -580,14 +557,15 @@ async def create_response( settings=self.profile.settings, ) - mediation_mgr = MediationManager(self.profile) - keylist_updates = None - mediation_record = await mediation_record_if_id(self.profile, mediation_id) - base_mediation_record = None + mediation_record = await self._route_manager.mediation_record_for_connection( + self.profile, conn_rec, mediation_id + ) # Multitenancy setup multitenant_mgr = self.profile.inject_or(BaseMultitenantManager) wallet_id = self.profile.settings.get("wallet.id") + + base_mediation_record = None if multitenant_mgr and wallet_id: base_mediation_record = await multitenant_mgr.get_default_mediator() @@ -606,16 +584,15 @@ async def create_response( async with self.profile.session() as session: wallet = session.inject(BaseWallet) my_info = await wallet.create_local_did( - method=DIDMethod.SOV, - key_type=KeyType.ED25519, + method=SOV, + key_type=ED25519, ) conn_rec.my_did = my_info.did - keylist_updates = await mediation_mgr.add_key( - my_info.verkey, keylist_updates - ) - # Add mapping for multitenant relay - if multitenant_mgr and wallet_id: - await multitenant_mgr.add_key(wallet_id, my_info.verkey) + + # Idempotent; if routing has already been set up, no action taken + await self._route_manager.route_connection_as_inviter( + self.profile, conn_rec, mediation_record + ) # Create connection response message if my_endpoint: @@ -626,6 +603,7 @@ async def create_response( if default_endpoint: my_endpoints.append(default_endpoint) my_endpoints.extend(self.profile.settings.get("additional_endpoints", [])) + did_doc = await self.create_did_document( my_info, conn_rec.inbound_connection_id, @@ -652,13 +630,6 @@ async def create_response( log_params={"response": response}, ) - # Update Mediator if necessary - if keylist_updates and mediation_record: - responder = self.profile.inject_or(BaseResponder) - await responder.send( - keylist_updates, connection_id=mediation_record.connection_id - ) - async with self.profile.session() as session: send_mediation_request = await conn_rec.metadata_get( session, MediationManager.SEND_REQ_AFTER_CONNECTION @@ -702,13 +673,24 @@ async def accept_response( conn_rec = None if response._thread: # identify the request by the thread ID - try: - async with self.profile.session() as session: + async with self.profile.session() as session: + try: conn_rec = await ConnRecord.retrieve_by_request_id( - session, response._thread_id + session, + response._thread_id, + their_role=ConnRecord.Role.RESPONDER.rfc23, ) - except StorageNotFoundError: - pass + except StorageNotFoundError: + pass + if not conn_rec: + try: + conn_rec = await ConnRecord.retrieve_by_request_id( + session, + response._thread_id, + their_role=ConnRecord.Role.RESPONDER.rfc160, + ) + except StorageNotFoundError: + pass if not conn_rec and receipt.sender_did: # identify connection by the DID they used for us @@ -740,7 +722,9 @@ async def accept_response( raise DIDXManagerError("No DIDDoc attached; cannot connect to public DID") async with self.profile.session() as session: wallet = session.inject(BaseWallet) - conn_did_doc = await self.verify_diddoc(wallet, response.did_doc_attach) + conn_did_doc = await self.verify_diddoc( + wallet, response.did_doc_attach, conn_rec.invitation_key + ) if their_did != conn_did_doc.did: raise DIDXManagerError( f"Connection DID {their_did} " @@ -809,12 +793,27 @@ async def accept_complete( conn_rec = None # identify the request by the thread ID - try: - async with self.profile.session() as session: + async with self.profile.session() as session: + try: conn_rec = await ConnRecord.retrieve_by_request_id( - session, complete._thread_id + session, + complete._thread_id, + their_role=ConnRecord.Role.REQUESTER.rfc23, ) - except StorageNotFoundError: + except StorageNotFoundError: + pass + + if not conn_rec: + try: + conn_rec = await ConnRecord.retrieve_by_request_id( + session, + complete._thread_id, + their_role=ConnRecord.Role.REQUESTER.rfc160, + ) + except StorageNotFoundError: + pass + + if not conn_rec: raise DIDXManagerError( "No corresponding connection request found", error_code=ProblemReportReason.COMPLETE_NOT_ACCEPTED.value, @@ -835,12 +834,53 @@ async def verify_diddoc( self, wallet: BaseWallet, attached: AttachDecorator, + invi_key: str = None, ) -> DIDDoc: """Verify DIDDoc attachment and return signed data.""" signed_diddoc_bytes = attached.data.signed if not signed_diddoc_bytes: raise DIDXManagerError("DID doc attachment is not signed.") - if not await attached.data.verify(wallet): + if not await attached.data.verify(wallet, invi_key): raise DIDXManagerError("DID doc attachment signature failed verification") return DIDDoc.deserialize(json.loads(signed_diddoc_bytes.decode())) + + async def get_resolved_did_document(self, qualified_did: str) -> ResolvedDocument: + """Return resolved DID document.""" + resolver = self._profile.inject(DIDResolver) + if not qualified_did.startswith("did:"): + qualified_did = f"did:sov:{qualified_did}" + try: + doc_dict: dict = await resolver.resolve(self._profile, qualified_did) + doc = pydid.deserialize_document(doc_dict, strict=True) + return doc + except ResolverError as error: + raise DIDXManagerError( + "Failed to resolve public DID in invitation" + ) from error + + async def get_first_applicable_didcomm_service( + self, did_doc: ResolvedDocument + ) -> str: + """Return first applicable DIDComm service url with highest priority.""" + if not did_doc.service: + raise DIDXManagerError( + "Cannot connect via public DID that has no associated services" + ) + + didcomm_services = sorted( + [ + service + for service in did_doc.service + if isinstance(service, DIDCommService) + ], + key=lambda service: service.priority, + ) + + if not didcomm_services: + raise DIDXManagerError( + "Cannot connect via public DID that has no associated DIDComm services" + ) + + first_didcomm_service, *_ = didcomm_services + return first_didcomm_service.id diff --git a/aries_cloudagent/protocols/didexchange/v1_0/messages/tests/test_request.py b/aries_cloudagent/protocols/didexchange/v1_0/messages/tests/test_request.py index a7ad4f405f..dc4c8b189e 100644 --- a/aries_cloudagent/protocols/didexchange/v1_0/messages/tests/test_request.py +++ b/aries_cloudagent/protocols/didexchange/v1_0/messages/tests/test_request.py @@ -2,21 +2,13 @@ from asynctest import TestCase as AsyncTestCase -from ......connections.models.diddoc import ( - DIDDoc, - PublicKey, - PublicKeyType, - Service, -) -from ......wallet.did_method import DIDMethod -from ......wallet.key_type import KeyType +from ......connections.models.diddoc import DIDDoc, PublicKey, PublicKeyType, Service from ......core.in_memory import InMemoryProfile from ......messaging.decorators.attach_decorator import AttachDecorator - +from ......wallet.did_method import SOV, DIDMethods +from ......wallet.key_type import ED25519 from .....didcomm_prefix import DIDCommPrefix - from ...message_types import DIDX_REQUEST - from ..request import DIDXRequest @@ -57,10 +49,12 @@ def make_did_doc(self): class TestDIDXRequest(AsyncTestCase, TestConfig): async def setUp(self): - self.wallet = InMemoryProfile.test_session().wallet + self.session = InMemoryProfile.test_session() + self.session.profile.context.injector.bind_instance(DIDMethods, DIDMethods()) + self.wallet = self.session.wallet self.did_info = await self.wallet.create_local_did( - method=DIDMethod.SOV, - key_type=KeyType.ED25519, + method=SOV, + key_type=ED25519, ) did_doc_attach = AttachDecorator.data_base64(self.make_did_doc().serialize()) @@ -114,10 +108,12 @@ class TestDIDXRequestSchema(AsyncTestCase, TestConfig): """Test request schema.""" async def setUp(self): - self.wallet = InMemoryProfile.test_session().wallet + self.session = InMemoryProfile.test_session() + self.session.profile.context.injector.bind_instance(DIDMethods, DIDMethods()) + self.wallet = self.session.wallet self.did_info = await self.wallet.create_local_did( - method=DIDMethod.SOV, - key_type=KeyType.ED25519, + method=SOV, + key_type=ED25519, ) did_doc_attach = AttachDecorator.data_base64(self.make_did_doc().serialize()) diff --git a/aries_cloudagent/protocols/didexchange/v1_0/messages/tests/test_response.py b/aries_cloudagent/protocols/didexchange/v1_0/messages/tests/test_response.py index 9cf32b8359..3be60ca51e 100644 --- a/aries_cloudagent/protocols/didexchange/v1_0/messages/tests/test_response.py +++ b/aries_cloudagent/protocols/didexchange/v1_0/messages/tests/test_response.py @@ -2,21 +2,13 @@ from asynctest import TestCase as AsyncTestCase -from ......connections.models.diddoc import ( - DIDDoc, - PublicKey, - PublicKeyType, - Service, -) -from ......wallet.did_method import DIDMethod -from ......wallet.key_type import KeyType +from ......connections.models.diddoc import DIDDoc, PublicKey, PublicKeyType, Service from ......core.in_memory import InMemoryProfile from ......messaging.decorators.attach_decorator import AttachDecorator - +from ......wallet.did_method import SOV, DIDMethods +from ......wallet.key_type import ED25519 from .....didcomm_prefix import DIDCommPrefix - from ...message_types import DIDX_RESPONSE - from ..response import DIDXResponse @@ -56,10 +48,13 @@ def make_did_doc(self): class TestDIDXResponse(AsyncTestCase, TestConfig): async def setUp(self): - self.wallet = InMemoryProfile.test_session().wallet + self.session = InMemoryProfile.test_session() + self.session.profile.context.injector.bind_instance(DIDMethods, DIDMethods()) + self.wallet = self.session.wallet + self.did_info = await self.wallet.create_local_did( - method=DIDMethod.SOV, - key_type=KeyType.ED25519, + method=SOV, + key_type=ED25519, ) did_doc_attach = AttachDecorator.data_base64(self.make_did_doc().serialize()) @@ -110,10 +105,13 @@ class TestDIDXResponseSchema(AsyncTestCase, TestConfig): """Test response schema.""" async def setUp(self): - self.wallet = InMemoryProfile.test_session().wallet + self.session = InMemoryProfile.test_session() + self.session.profile.context.injector.bind_instance(DIDMethods, DIDMethods()) + self.wallet = self.session.wallet + self.did_info = await self.wallet.create_local_did( - method=DIDMethod.SOV, - key_type=KeyType.ED25519, + method=SOV, + key_type=ED25519, ) did_doc_attach = AttachDecorator.data_base64(self.make_did_doc().serialize()) diff --git a/aries_cloudagent/protocols/didexchange/v1_0/tests/test_manager.py b/aries_cloudagent/protocols/didexchange/v1_0/tests/test_manager.py index aee31fa30b..f5ddf31b4d 100644 --- a/aries_cloudagent/protocols/didexchange/v1_0/tests/test_manager.py +++ b/aries_cloudagent/protocols/didexchange/v1_0/tests/test_manager.py @@ -1,52 +1,46 @@ import json -from asynctest import mock as async_mock, TestCase as AsyncTestCase +from asynctest import TestCase as AsyncTestCase +from asynctest import mock as async_mock +from pydid import DIDDocument from .....cache.base import BaseCache from .....cache.in_memory import InMemoryCache +from .....connections.base_manager import BaseConnectionManagerError from .....connections.models.conn_record import ConnRecord from .....connections.models.connection_target import ConnectionTarget -from .....connections.models.diddoc import ( - DIDDoc, - PublicKey, - PublicKeyType, - Service, -) +from .....connections.models.diddoc import DIDDoc, PublicKey, PublicKeyType, Service from .....core.in_memory import InMemoryProfile +from .....core.oob_processor import OobMessageProcessor +from .....did.did_key import DIDKey from .....ledger.base import BaseLedger -from .....messaging.responder import BaseResponder, MockResponder from .....messaging.decorators.attach_decorator import AttachDecorator +from .....messaging.responder import BaseResponder, MockResponder from .....multitenant.base import BaseMultitenantManager from .....multitenant.manager import MultitenantManager +from .....resolver.base import ResolverError +from .....resolver.did_resolver import DIDResolver +from .....resolver.tests import DOC from .....storage.error import StorageNotFoundError from .....transport.inbound.receipt import MessageReceipt from .....wallet.did_info import DIDInfo +from .....wallet.did_method import SOV, DIDMethods from .....wallet.error import WalletError from .....wallet.in_memory import InMemoryWallet -from .....wallet.did_method import DIDMethod -from .....wallet.key_type import KeyType -from .....did.did_key import DIDKey - -from .....connections.base_manager import BaseConnectionManagerError - +from .....wallet.key_type import ED25519 from ....coordinate_mediation.v1_0.manager import MediationManager -from ....coordinate_mediation.v1_0.messages.keylist_update import ( - KeylistUpdate, - KeylistUpdateRule, -) from ....coordinate_mediation.v1_0.models.mediation_record import MediationRecord -from ....discovery.v2_0.manager import V20DiscoveryMgr +from ....coordinate_mediation.v1_0.route_manager import RouteManager from ....didcomm_prefix import DIDCommPrefix +from ....discovery.v2_0.manager import V20DiscoveryMgr from ....out_of_band.v1_0.manager import OutOfBandManager from ....out_of_band.v1_0.messages.invitation import HSProto, InvitationMessage from ....out_of_band.v1_0.messages.service import Service as OOBService - from .. import manager as test_module from ..manager import DIDXManager, DIDXManagerError class TestConfig: - test_seed = "testseed000000000000000000000001" test_did = "55GkHamhTU1ZbTbV2ab9DE" test_verkey = "3Dn1SJNPaCXcvvJvSbsFWP2xaCjMom3can8CQNhWrTRx" @@ -77,6 +71,21 @@ class TestDidExchangeManager(AsyncTestCase, TestConfig): async def setUp(self): self.responder = MockResponder() + self.oob_mock = async_mock.MagicMock( + clean_finished_oob_record=async_mock.CoroutineMock(return_value=None) + ) + + self.route_manager = async_mock.MagicMock(RouteManager) + self.route_manager.routing_info = async_mock.CoroutineMock( + return_value=([], self.test_endpoint) + ) + self.route_manager.mediation_record_if_id = async_mock.CoroutineMock( + return_value=None + ) + self.route_manager.mediation_record_for_connection = async_mock.CoroutineMock( + return_value=None + ) + self.profile = InMemoryProfile.test_profile( { "default_endpoint": "http://aries.ca/endpoint", @@ -87,13 +96,19 @@ async def setUp(self): "multitenant.enabled": True, "wallet.id": True, }, - bind={BaseResponder: self.responder, BaseCache: InMemoryCache()}, + bind={ + BaseResponder: self.responder, + BaseCache: InMemoryCache(), + OobMessageProcessor: self.oob_mock, + RouteManager: self.route_manager, + DIDMethods: DIDMethods(), + }, ) self.context = self.profile.context async with self.profile.session() as session: self.did_info = await session.wallet.create_local_did( - method=DIDMethod.SOV, - key_type=KeyType.ED25519, + method=SOV, + key_type=ED25519, ) self.ledger = async_mock.create_autospec(BaseLedger) @@ -102,18 +117,25 @@ async def setUp(self): return_value=TestConfig.test_endpoint ) self.context.injector.bind_instance(BaseLedger, self.ledger) + self.resolver = async_mock.MagicMock() + did_doc = DIDDocument.deserialize(DOC) + self.resolver.resolve = async_mock.CoroutineMock(return_value=did_doc) + self.context.injector.bind_instance(DIDResolver, self.resolver) self.multitenant_mgr = async_mock.MagicMock(MultitenantManager, autospec=True) self.context.injector.bind_instance( BaseMultitenantManager, self.multitenant_mgr ) + self.multitenant_mgr.get_default_mediator = async_mock.CoroutineMock( + return_value=None + ) self.manager = DIDXManager(self.profile) assert self.manager.profile self.oob_manager = OutOfBandManager(self.profile) self.test_mediator_routing_keys = [ DIDKey.from_public_key_b58( - "3Dn1SJNPaCXcvvJvSbsFWP2xaCjMom3can8CQNhWrTRR", KeyType.ED25519 + "3Dn1SJNPaCXcvvJvSbsFWP2xaCjMom3can8CQNhWrTRR", ED25519 ).did ] self.test_mediator_conn_id = "mediator-conn-id" @@ -168,6 +190,37 @@ async def test_receive_invitation(self): invitee_record = await self.manager.receive_invitation(invi_msg) assert invitee_record.state == ConnRecord.State.REQUEST.rfc23 + async def test_receive_invitation_oob_public_did(self): + async with self.profile.session() as session: + self.profile.context.update_settings({"public_invites": True}) + public_did_info = None + await session.wallet.create_public_did( + SOV, + ED25519, + ) + public_did_info = await session.wallet.get_public_did() + with async_mock.patch.object( + test_module, "AttachDecorator", autospec=True + ) as mock_attach_deco, async_mock.patch.object( + self.multitenant_mgr, "get_default_mediator" + ) as mock_get_default_mediator: + mock_get_default_mediator.return_value = None + invi_rec = await self.oob_manager.create_invitation( + my_endpoint="testendpoint", + hs_protos=[HSProto.RFC23], + ) + invi_msg = invi_rec.invitation + invi_msg.services = [public_did_info.did] + mock_attach_deco.data_base64 = async_mock.MagicMock( + return_value=async_mock.MagicMock( + data=async_mock.MagicMock(sign=async_mock.CoroutineMock()) + ) + ) + invitee_record = await self.manager.receive_invitation( + invi_msg, their_public_did=public_did_info.did + ) + assert invitee_record.state == ConnRecord.State.REQUEST.rfc23 + async def test_receive_invitation_no_auto_accept(self): async with self.profile.session() as session: mediation_record = MediationRecord( @@ -242,24 +295,15 @@ async def test_create_request_implicit(self): async def test_create_request_implicit_use_public_did(self): async with self.profile.session() as session: - mediation_record = MediationRecord( - role=MediationRecord.ROLE_CLIENT, - state=MediationRecord.STATE_GRANTED, - connection_id=self.test_mediator_conn_id, - routing_keys=self.test_mediator_routing_keys, - endpoint=self.test_mediator_endpoint, - ) - await mediation_record.save(session) - info_public = await session.wallet.create_public_did( - DIDMethod.SOV, - KeyType.ED25519, + SOV, + ED25519, ) conn_rec = await self.manager.create_request_implicit( their_public_did=TestConfig.test_target_did, my_label=None, my_endpoint=None, - mediation_id=mediation_record._id, + mediation_id=None, use_public_did=True, alias="Tester", ) @@ -323,8 +367,8 @@ async def test_create_request_multitenant(self): TestConfig.test_did, TestConfig.test_verkey, None, - method=DIDMethod.SOV, - key_type=KeyType.ED25519, + method=SOV, + key_type=ED25519, ) mock_attach_deco.data_base64 = async_mock.MagicMock( return_value=async_mock.MagicMock( @@ -348,9 +392,7 @@ async def test_create_request_multitenant(self): ) ) - self.multitenant_mgr.add_key.assert_called_once_with( - "test_wallet", TestConfig.test_verkey - ) + self.route_manager.route_connection_as_invitee.assert_called_once() async def test_create_request_mediation_id(self): async with self.profile.session() as session: @@ -395,13 +437,8 @@ async def test_create_request_mediation_id(self): mediation_id=mediation_record._id, ) assert didx_req - assert len(self.responder.messages) == 1 - message, used_kwargs = self.responder.messages[0] - assert isinstance(message, KeylistUpdate) - assert ( - "connection_id" in used_kwargs - and used_kwargs["connection_id"] == self.test_mediator_conn_id - ) + + self.route_manager.route_connection_as_invitee.assert_called_once() async def test_create_request_my_endpoint(self): mock_conn_rec = async_mock.MagicMock( @@ -440,7 +477,6 @@ async def test_receive_request_explicit_public_did(self): did=TestConfig.test_did, did_doc_attach=async_mock.MagicMock( data=async_mock.MagicMock( - verify=async_mock.CoroutineMock(return_value=True), signed=async_mock.MagicMock( decode=async_mock.MagicMock(return_value="dummy-did-doc") ), @@ -459,8 +495,8 @@ async def test_receive_request_explicit_public_did(self): await mediation_record.save(session) await session.wallet.create_local_did( - method=DIDMethod.SOV, - key_type=KeyType.ED25519, + method=SOV, + key_type=ED25519, seed=None, did=TestConfig.test_did, ) @@ -479,6 +515,10 @@ async def test_receive_request_explicit_public_did(self): ) as mock_attach_deco, async_mock.patch.object( test_module, "DIDXResponse", autospec=True ) as mock_response, async_mock.patch.object( + self.manager, + "verify_diddoc", + async_mock.CoroutineMock(return_value=DIDDoc(TestConfig.test_did)), + ), async_mock.patch.object( self.manager, "create_did_document", async_mock.CoroutineMock() ) as mock_create_did_doc, async_mock.patch.object( MediationManager, "prepare_request", autospec=True @@ -539,9 +579,11 @@ async def test_receive_request_explicit_public_did(self): my_endpoint=None, alias=None, auto_accept_implicit=None, - mediation_id=None, ) assert conn_rec + self.oob_mock.clean_finished_oob_record.assert_called_once_with( + self.profile, mock_request + ) async def test_receive_request_invi_not_found(self): async with self.profile.session() as session: @@ -552,8 +594,8 @@ async def test_receive_request_invi_not_found(self): ) await session.wallet.create_local_did( - method=DIDMethod.SOV, - key_type=KeyType.ED25519, + method=SOV, + key_type=ED25519, seed=None, did=TestConfig.test_did, ) @@ -572,183 +614,9 @@ async def test_receive_request_invi_not_found(self): my_endpoint=None, alias=None, auto_accept_implicit=None, - mediation_id=None, ) assert "No explicit invitation found" in str(context.exception) - async def test_receive_request_with_mediator_without_multi_use_multitenant(self): - async with self.profile.session() as session: - multiuse_info = await session.wallet.create_local_did( - method=DIDMethod.SOV, - key_type=KeyType.ED25519, - ) - did_doc_dict = self.make_did_doc( - did=TestConfig.test_target_did, - verkey=TestConfig.test_target_verkey, - ).serialize() - del did_doc_dict["authentication"] - del did_doc_dict["service"] - new_info = await session.wallet.create_local_did( - method=DIDMethod.SOV, - key_type=KeyType.ED25519, - ) - - mock_request = async_mock.MagicMock() - mock_request.connection = async_mock.MagicMock( - is_multiuse_invitation=False, invitation_key=multiuse_info.verkey - ) - mock_request.connection.did = TestConfig.test_did - mock_request.connection.did_doc = async_mock.MagicMock() - mock_request.connection.did_doc.did = TestConfig.test_did - mock_request.did = self.test_target_did - mock_request.did_doc_attach = async_mock.MagicMock( - data=async_mock.MagicMock( - verify=async_mock.CoroutineMock(return_value=True), - signed=async_mock.MagicMock( - decode=async_mock.MagicMock( - return_value=json.dumps(did_doc_dict) - ) - ), - ) - ) - - await session.wallet.create_local_did( - method=DIDMethod.SOV, - key_type=KeyType.ED25519, - seed=None, - did=TestConfig.test_did, - ) - - mediation_record = MediationRecord( - role=MediationRecord.ROLE_CLIENT, - state=MediationRecord.STATE_GRANTED, - connection_id=self.test_mediator_conn_id, - routing_keys=self.test_mediator_routing_keys, - endpoint=self.test_mediator_endpoint, - ) - await mediation_record.save(session) - - record = ConnRecord( - invitation_key=TestConfig.test_verkey, - their_label="Hello", - their_role=ConnRecord.Role.RESPONDER.rfc160, - alias="Bob", - ) - record.accept = ConnRecord.ACCEPT_MANUAL - await record.save(session) - - with async_mock.patch.object( - ConnRecord, "save", autospec=True - ) as mock_conn_rec_save, async_mock.patch.object( - ConnRecord, "attach_request", autospec=True - ) as mock_conn_attach_request, async_mock.patch.object( - ConnRecord, "retrieve_by_invitation_key" - ) as mock_conn_retrieve_by_invitation_key, async_mock.patch.object( - ConnRecord, "retrieve_request", autospec=True - ): - mock_conn_retrieve_by_invitation_key.return_value = record - conn_rec = await self.manager.receive_request( - request=mock_request, - recipient_did=TestConfig.test_did, - recipient_verkey=TestConfig.test_verkey, - my_endpoint=None, - alias=None, - auto_accept_implicit=None, - mediation_id=mediation_record.mediation_id, - ) - - assert len(self.responder.messages) == 1 - message, target = self.responder.messages[0] - assert isinstance(message, KeylistUpdate) - assert len(message.updates) == 1 - (remove,) = message.updates - assert remove.action == KeylistUpdateRule.RULE_REMOVE - assert remove.recipient_key == record.invitation_key - - async def test_receive_request_with_mediator_without_multi_use_multitenant_mismatch( - self, - ): - async with self.profile.session() as session: - multiuse_info = await session.wallet.create_local_did( - method=DIDMethod.SOV, - key_type=KeyType.ED25519, - ) - did_doc_dict = self.make_did_doc( - did=TestConfig.test_target_did, - verkey=TestConfig.test_target_verkey, - ).serialize() - del did_doc_dict["authentication"] - del did_doc_dict["service"] - new_info = await session.wallet.create_local_did( - method=DIDMethod.SOV, - key_type=KeyType.ED25519, - ) - - mock_request = async_mock.MagicMock() - mock_request.connection = async_mock.MagicMock( - is_multiuse_invitation=False, invitation_key=multiuse_info.verkey - ) - mock_request.connection.did = TestConfig.test_did - mock_request.connection.did_doc = async_mock.MagicMock() - mock_request.connection.did_doc.did = TestConfig.test_did - mock_request.did_doc_attach = async_mock.MagicMock( - data=async_mock.MagicMock( - verify=async_mock.CoroutineMock(return_value=True), - signed=async_mock.MagicMock( - decode=async_mock.MagicMock( - return_value=json.dumps(did_doc_dict) - ) - ), - ) - ) - - await session.wallet.create_local_did( - method=DIDMethod.SOV, - key_type=KeyType.ED25519, - seed=None, - did=TestConfig.test_did, - ) - - mediation_record = MediationRecord( - role=MediationRecord.ROLE_CLIENT, - state=MediationRecord.STATE_GRANTED, - connection_id=self.test_mediator_conn_id, - routing_keys=self.test_mediator_routing_keys, - endpoint=self.test_mediator_endpoint, - ) - await mediation_record.save(session) - - record = ConnRecord( - invitation_key=TestConfig.test_verkey, - their_label="Hello", - their_role=ConnRecord.Role.RESPONDER.rfc160, - alias="Bob", - ) - record.accept = ConnRecord.ACCEPT_MANUAL - await record.save(session) - - with async_mock.patch.object( - ConnRecord, "save", autospec=True - ) as mock_conn_rec_save, async_mock.patch.object( - ConnRecord, "attach_request", autospec=True - ) as mock_conn_attach_request, async_mock.patch.object( - ConnRecord, "retrieve_by_invitation_key" - ) as mock_conn_retrieve_by_invitation_key, async_mock.patch.object( - ConnRecord, "retrieve_request", autospec=True - ): - mock_conn_retrieve_by_invitation_key.return_value = record - with self.assertRaises(DIDXManagerError) as context: - conn_rec = await self.manager.receive_request( - request=mock_request, - recipient_did=TestConfig.test_did, - recipient_verkey=TestConfig.test_verkey, - my_endpoint=None, - alias=None, - auto_accept_implicit=None, - mediation_id=mediation_record.mediation_id, - ) - assert "does not match" in str(context.exception) - async def test_receive_request_public_did_no_did_doc_attachment(self): async with self.profile.session() as session: mock_request = async_mock.MagicMock( @@ -758,8 +626,8 @@ async def test_receive_request_public_did_no_did_doc_attachment(self): ) await session.wallet.create_local_did( - method=DIDMethod.SOV, - key_type=KeyType.ED25519, + method=SOV, + key_type=ED25519, seed=None, did=TestConfig.test_did, ) @@ -797,7 +665,6 @@ async def test_receive_request_public_did_no_did_doc_attachment(self): my_endpoint=TestConfig.test_endpoint, alias="Alias", auto_accept_implicit=None, - mediation_id=None, ) assert "DID Doc attachment missing or has no data" in str( context.exception @@ -819,8 +686,8 @@ async def test_receive_request_public_did_x_not_public(self): ) await session.wallet.create_local_did( - method=DIDMethod.SOV, - key_type=KeyType.ED25519, + method=SOV, + key_type=ED25519, seed=None, did=TestConfig.test_did, ) @@ -842,7 +709,6 @@ async def test_receive_request_public_did_x_not_public(self): my_endpoint=TestConfig.test_endpoint, alias="Alias", auto_accept_implicit=False, - mediation_id=None, ) assert "is not public" in str(context.exception) @@ -853,7 +719,6 @@ async def test_receive_request_public_did_x_wrong_did(self): did=TestConfig.test_did, did_doc_attach=async_mock.MagicMock( data=async_mock.MagicMock( - verify=async_mock.CoroutineMock(return_value=True), signed=async_mock.MagicMock( decode=async_mock.MagicMock(return_value="dummy-did-doc") ), @@ -863,8 +728,8 @@ async def test_receive_request_public_did_x_wrong_did(self): ) await session.wallet.create_local_did( - method=DIDMethod.SOV, - key_type=KeyType.ED25519, + method=SOV, + key_type=ED25519, seed=None, did=TestConfig.test_did, ) @@ -877,7 +742,11 @@ async def test_receive_request_public_did_x_wrong_did(self): test_module, "DIDPosture", autospec=True ) as mock_did_posture, async_mock.patch.object( test_module.DIDDoc, "from_json", async_mock.MagicMock() - ) as mock_did_doc_from_json: + ) as mock_did_doc_from_json, async_mock.patch.object( + self.manager, + "verify_diddoc", + async_mock.CoroutineMock(return_value=DIDDoc("LjgpST2rjsoxYegQDRm7EL")), + ): mock_conn_record = async_mock.MagicMock( accept=ConnRecord.ACCEPT_MANUAL, my_did=None, @@ -907,7 +776,6 @@ async def test_receive_request_public_did_x_wrong_did(self): my_endpoint=TestConfig.test_endpoint, alias="Alias", auto_accept_implicit=False, - mediation_id=None, ) assert "does not match" in str(context.exception) @@ -918,15 +786,17 @@ async def test_receive_request_public_did_x_did_doc_attach_bad_sig(self): did=TestConfig.test_did, did_doc_attach=async_mock.MagicMock( data=async_mock.MagicMock( - verify=async_mock.CoroutineMock(return_value=False) + signed=async_mock.MagicMock( + decode=async_mock.MagicMock(return_value="dummy-did-doc") + ), ) ), _thread=async_mock.MagicMock(pthid="did:sov:publicdid0000000000000"), ) await session.wallet.create_local_did( - method=DIDMethod.SOV, - key_type=KeyType.ED25519, + method=SOV, + key_type=ED25519, seed=None, did=TestConfig.test_did, ) @@ -937,7 +807,11 @@ async def test_receive_request_public_did_x_did_doc_attach_bad_sig(self): test_module, "ConnRecord", async_mock.MagicMock() ) as mock_conn_rec_cls, async_mock.patch.object( test_module, "DIDPosture", autospec=True - ) as mock_did_posture: + ) as mock_did_posture, async_mock.patch.object( + self.manager, + "verify_diddoc", + async_mock.CoroutineMock(side_effect=DIDXManagerError), + ): mock_conn_record = async_mock.MagicMock( accept=ConnRecord.ACCEPT_MANUAL, my_did=None, @@ -956,7 +830,7 @@ async def test_receive_request_public_did_x_did_doc_attach_bad_sig(self): return_value=test_module.DIDPosture.PUBLIC ) - with self.assertRaises(DIDXManagerError) as context: + with self.assertRaises(DIDXManagerError): await self.manager.receive_request( request=mock_request, recipient_did=TestConfig.test_did, @@ -964,9 +838,7 @@ async def test_receive_request_public_did_x_did_doc_attach_bad_sig(self): my_endpoint=TestConfig.test_endpoint, alias="Alias", auto_accept_implicit=False, - mediation_id=None, ) - assert "DID Doc signature failed" in str(context.exception) async def test_receive_request_public_did_no_public_invites(self): async with self.profile.session() as session: @@ -984,8 +856,8 @@ async def test_receive_request_public_did_no_public_invites(self): ) await session.wallet.create_local_did( - method=DIDMethod.SOV, - key_type=KeyType.ED25519, + method=SOV, + key_type=ED25519, seed=None, did=TestConfig.test_did, ) @@ -1013,7 +885,6 @@ async def test_receive_request_public_did_no_public_invites(self): my_endpoint=TestConfig.test_endpoint, alias="Alias", auto_accept_implicit=False, - mediation_id=None, ) assert "Public invitations are not enabled" in str(context.exception) @@ -1023,7 +894,6 @@ async def test_receive_request_public_did_no_auto_accept(self): did=TestConfig.test_did, did_doc_attach=async_mock.MagicMock( data=async_mock.MagicMock( - verify=async_mock.CoroutineMock(return_value=True), signed=async_mock.MagicMock( decode=async_mock.MagicMock(return_value="dummy-did-doc") ), @@ -1033,8 +903,8 @@ async def test_receive_request_public_did_no_auto_accept(self): ) await session.wallet.create_local_did( - method=DIDMethod.SOV, - key_type=KeyType.ED25519, + method=SOV, + key_type=ED25519, seed=None, did=TestConfig.test_did, ) @@ -1055,7 +925,11 @@ async def test_receive_request_public_did_no_auto_accept(self): test_module, "DIDXResponse", autospec=True ) as mock_response, async_mock.patch.object( self.manager, "create_did_document", async_mock.CoroutineMock() - ) as mock_create_did_doc: + ) as mock_create_did_doc, async_mock.patch.object( + self.manager, + "verify_diddoc", + async_mock.CoroutineMock(return_value=DIDDoc(TestConfig.test_did)), + ): mock_conn_record = async_mock.MagicMock( accept=ConnRecord.ACCEPT_MANUAL, my_did=None, @@ -1084,14 +958,13 @@ async def test_receive_request_public_did_no_auto_accept(self): my_endpoint=TestConfig.test_endpoint, alias="Alias", auto_accept_implicit=False, - mediation_id=None, ) assert conn_rec messages = self.responder.messages assert not messages - async def test_receive_request_peer_did(self): + async def test_receive_request_implicit_public_did_not_enabled(self): async with self.profile.session() as session: mock_request = async_mock.MagicMock( did=TestConfig.test_did, @@ -1103,93 +976,59 @@ async def test_receive_request_peer_did(self): ), ) ), - _thread=async_mock.MagicMock(pthid="dummy-pthid"), + _thread=async_mock.MagicMock(pthid="did:sov:publicdid0000000000000"), ) - - mock_conn = async_mock.MagicMock( - my_did=TestConfig.test_did, - their_did=TestConfig.test_target_did, - invitation_key=TestConfig.test_verkey, - connection_id="dummy", - is_multiuse_invitation=True, - state=ConnRecord.State.INVITATION.rfc23, - their_role=ConnRecord.Role.REQUESTER.rfc23, - save=async_mock.CoroutineMock(), - attach_request=async_mock.CoroutineMock(), - accept=ConnRecord.ACCEPT_MANUAL, - metadata_get_all=async_mock.CoroutineMock( - return_value={"test": "value"} - ), + mediation_record = MediationRecord( + role=MediationRecord.ROLE_CLIENT, + state=MediationRecord.STATE_GRANTED, + connection_id=self.test_mediator_conn_id, + routing_keys=self.test_mediator_routing_keys, + endpoint=self.test_mediator_endpoint, ) - mock_conn_rec_state_request = ConnRecord.State.REQUEST + await mediation_record.save(session) await session.wallet.create_local_did( - method=DIDMethod.SOV, - key_type=KeyType.ED25519, + method=SOV, + key_type=ED25519, seed=None, did=TestConfig.test_did, ) self.profile.context.update_settings({"public_invites": True}) + with async_mock.patch.object( test_module, "ConnRecord", async_mock.MagicMock() ) as mock_conn_rec_cls, async_mock.patch.object( test_module, "DIDDoc", autospec=True ) as mock_did_doc, async_mock.patch.object( - test_module, "AttachDecorator", autospec=True - ) as mock_attach_deco, async_mock.patch.object( - test_module, "DIDXResponse", autospec=True - ) as mock_response: - mock_conn_rec_cls.retrieve_by_invitation_key = async_mock.CoroutineMock( - return_value=mock_conn - ) - mock_conn_rec_cls.return_value = async_mock.MagicMock( - accept=ConnRecord.ACCEPT_AUTO, - my_did=None, - state=mock_conn_rec_state_request.rfc23, - attach_request=async_mock.CoroutineMock(), - retrieve_request=async_mock.CoroutineMock(), - save=async_mock.CoroutineMock(), - metadata_set=async_mock.CoroutineMock(), - ) - mock_did_doc.from_json = async_mock.MagicMock( - return_value=async_mock.MagicMock(did=TestConfig.test_did) - ) - mock_attach_deco.data_base64 = async_mock.MagicMock( - return_value=async_mock.MagicMock( - data=async_mock.MagicMock(sign=async_mock.CoroutineMock()) - ) + test_module, "DIDPosture", autospec=True + ) as mock_did_posture, async_mock.patch.object( + self.manager, + "verify_diddoc", + async_mock.CoroutineMock(return_value=DIDDoc(TestConfig.test_did)), + ): + mock_did_posture.get = async_mock.MagicMock( + return_value=test_module.DIDPosture.PUBLIC ) - mock_response.return_value = async_mock.MagicMock( - assign_thread_from=async_mock.MagicMock(), - assign_trace_from=async_mock.MagicMock(), + mock_conn_rec_cls.retrieve_by_invitation_key = async_mock.CoroutineMock( + side_effect=StorageNotFoundError() ) - - conn_rec = await self.manager.receive_request( - request=mock_request, - recipient_did=TestConfig.test_did, - recipient_verkey=TestConfig.test_verkey, - my_endpoint=TestConfig.test_endpoint, - alias="Alias", - auto_accept_implicit=False, - mediation_id=None, + mock_conn_rec_cls.retrieve_by_invitation_msg_id = ( + async_mock.CoroutineMock(return_value=None) ) - assert conn_rec - mock_conn_rec_cls.return_value.metadata_set.assert_called() - assert not self.responder.messages + with self.assertRaises(DIDXManagerError) as context: + await self.manager.receive_request( + request=mock_request, + recipient_did=TestConfig.test_did, + my_endpoint=None, + alias=None, + auto_accept_implicit=None, + ) + assert "Unsolicited connection requests" in str(context.exception) - async def test_receive_request_multiuse_multitenant(self): + async def test_receive_request_implicit_public_did(self): async with self.profile.session() as session: - multiuse_info = await session.wallet.create_local_did( - method=DIDMethod.SOV, - key_type=KeyType.ED25519, - ) - new_info = await session.wallet.create_local_did( - method=DIDMethod.SOV, - key_type=KeyType.ED25519, - ) - mock_request = async_mock.MagicMock( did=TestConfig.test_did, did_doc_attach=async_mock.MagicMock( @@ -1200,148 +1039,165 @@ async def test_receive_request_multiuse_multitenant(self): ), ) ), - _thread=async_mock.MagicMock(pthid="dummy-pthid"), + _thread=async_mock.MagicMock(pthid="did:sov:publicdid0000000000000"), + ) + mediation_record = MediationRecord( + role=MediationRecord.ROLE_CLIENT, + state=MediationRecord.STATE_GRANTED, + connection_id=self.test_mediator_conn_id, + routing_keys=self.test_mediator_routing_keys, + endpoint=self.test_mediator_endpoint, ) + await mediation_record.save(session) - self.context.update_settings( - {"wallet.id": "test_wallet", "multitenant.enabled": True} + await session.wallet.create_local_did( + method=SOV, + key_type=ED25519, + seed=None, + did=TestConfig.test_did, ) - ACCEPT_MANUAL = ConnRecord.ACCEPT_MANUAL + + self.profile.context.update_settings({"public_invites": True}) + self.profile.context.update_settings({"requests_through_public_did": True}) + ACCEPT_AUTO = ConnRecord.ACCEPT_AUTO + STATE_REQUEST = ConnRecord.State.REQUEST + with async_mock.patch.object( - test_module, "ConnRecord", autospec=True + test_module, "ConnRecord", async_mock.MagicMock() ) as mock_conn_rec_cls, async_mock.patch.object( - InMemoryWallet, "create_local_did", autospec=True - ) as mock_wallet_create_local_did, async_mock.patch.object( test_module, "DIDDoc", autospec=True - ) as mock_did_doc: - mock_conn_rec = async_mock.CoroutineMock( - connection_id="dummy", - accept=ACCEPT_MANUAL, - is_multiuse_invitation=True, - attach_request=async_mock.CoroutineMock(), - save=async_mock.CoroutineMock(), - retrieve_invitation=async_mock.CoroutineMock(return_value={}), - metadata_get_all=async_mock.CoroutineMock(return_value={}), - retrieve_request=async_mock.CoroutineMock(), + ) as mock_did_doc, async_mock.patch.object( + test_module, "DIDPosture", autospec=True + ) as mock_did_posture, async_mock.patch.object( + self.manager, + "verify_diddoc", + async_mock.CoroutineMock(return_value=DIDDoc(TestConfig.test_did)), + ): + mock_did_posture.get = async_mock.MagicMock( + return_value=test_module.DIDPosture.PUBLIC ) - mock_conn_rec_cls.return_value = mock_conn_rec mock_conn_rec_cls.retrieve_by_invitation_key = async_mock.CoroutineMock( - return_value=mock_conn_rec + side_effect=StorageNotFoundError() ) - mock_wallet_create_local_did.return_value = DIDInfo( - new_info.did, - new_info.verkey, - None, - method=DIDMethod.SOV, - key_type=KeyType.ED25519, + mock_conn_rec_cls.retrieve_by_invitation_msg_id = ( + async_mock.CoroutineMock(return_value=None) ) - mock_did_doc.from_json = async_mock.MagicMock( - return_value=async_mock.MagicMock(did=TestConfig.test_did) + + mock_conn_record = async_mock.MagicMock( + accept=ACCEPT_AUTO, + my_did=None, + state=STATE_REQUEST.rfc23, + attach_request=async_mock.CoroutineMock(), + retrieve_request=async_mock.CoroutineMock(), + metadata_get_all=async_mock.CoroutineMock(return_value={}), + metadata_get=async_mock.CoroutineMock(return_value=True), + save=async_mock.CoroutineMock(), ) - await self.manager.receive_request( + + mock_conn_rec_cls.return_value = mock_conn_record + + conn_rec = await self.manager.receive_request( request=mock_request, recipient_did=TestConfig.test_did, - recipient_verkey=TestConfig.test_verkey, - my_endpoint=TestConfig.test_endpoint, - alias="Alias", - auto_accept_implicit=False, - mediation_id=None, + recipient_verkey=None, + my_endpoint=None, + alias=None, + auto_accept_implicit=None, ) - - self.multitenant_mgr.add_key.assert_called_once_with( - "test_wallet", new_info.verkey + assert conn_rec + self.oob_mock.clean_finished_oob_record.assert_called_once_with( + self.profile, mock_request ) - async def test_receive_request_implicit_multitenant(self): + async def test_receive_request_peer_did(self): async with self.profile.session() as session: - new_info = await session.wallet.create_local_did( - method=DIDMethod.SOV, - key_type=KeyType.ED25519, - ) - mock_request = async_mock.MagicMock( did=TestConfig.test_did, did_doc_attach=async_mock.MagicMock( data=async_mock.MagicMock( - verify=async_mock.CoroutineMock(return_value=True), signed=async_mock.MagicMock( decode=async_mock.MagicMock(return_value="dummy-did-doc") ), ) ), - _thread=async_mock.MagicMock(pthid="did:sov:publicdid0000000000000"), + _thread=async_mock.MagicMock(pthid="dummy-pthid"), ) - self.context.update_settings( - { - "wallet.id": "test_wallet", - "multitenant.enabled": True, - "public_invites": True, - "debug.auto_accept_requests": False, - } + mock_conn = async_mock.MagicMock( + my_did=TestConfig.test_did, + their_did=TestConfig.test_target_did, + invitation_key=TestConfig.test_verkey, + connection_id="dummy", + is_multiuse_invitation=True, + state=ConnRecord.State.INVITATION.rfc23, + their_role=ConnRecord.Role.REQUESTER.rfc23, + save=async_mock.CoroutineMock(), + attach_request=async_mock.CoroutineMock(), + accept=ConnRecord.ACCEPT_MANUAL, + metadata_get_all=async_mock.CoroutineMock( + return_value={"test": "value"} + ), ) + mock_conn_rec_state_request = ConnRecord.State.REQUEST - ACCEPT_MANUAL = ConnRecord.ACCEPT_MANUAL + await session.wallet.create_local_did( + method=SOV, + key_type=ED25519, + seed=None, + did=TestConfig.test_did, + ) + + self.profile.context.update_settings({"public_invites": True}) with async_mock.patch.object( - test_module, "ConnRecord", autospec=True + test_module, "ConnRecord", async_mock.MagicMock() ) as mock_conn_rec_cls, async_mock.patch.object( - InMemoryWallet, "create_local_did", autospec=True - ) as mock_wallet_create_local_did, async_mock.patch.object( - InMemoryWallet, "get_local_did", autospec=True - ) as mock_wallet_get_local_did, async_mock.patch.object( - test_module, "DIDPosture", autospec=True - ) as mock_did_posture, async_mock.patch.object( test_module, "DIDDoc", autospec=True - ) as mock_did_doc: - mock_conn_rec = async_mock.CoroutineMock( - connection_id="dummy", - accept=ACCEPT_MANUAL, - is_multiuse_invitation=False, + ) as mock_did_doc, async_mock.patch.object( + test_module, "AttachDecorator", autospec=True + ) as mock_attach_deco, async_mock.patch.object( + test_module, "DIDXResponse", autospec=True + ) as mock_response, async_mock.patch.object( + self.manager, + "verify_diddoc", + async_mock.CoroutineMock(return_value=DIDDoc(TestConfig.test_did)), + ): + mock_conn_rec_cls.retrieve_by_invitation_key = async_mock.CoroutineMock( + return_value=mock_conn + ) + mock_conn_rec_cls.return_value = async_mock.MagicMock( + accept=ConnRecord.ACCEPT_AUTO, + my_did=None, + state=mock_conn_rec_state_request.rfc23, attach_request=async_mock.CoroutineMock(), - save=async_mock.CoroutineMock(), - retrieve_invitation=async_mock.CoroutineMock(return_value={}), - metadata_get_all=async_mock.CoroutineMock(return_value={}), retrieve_request=async_mock.CoroutineMock(), - ) - mock_conn_rec_cls.return_value = mock_conn_rec - mock_conn_rec_cls.retrieve_by_invitation_msg_id = ( - async_mock.CoroutineMock(return_value=[]) - ) - - mock_did_posture.get = async_mock.MagicMock( - return_value=test_module.DIDPosture.PUBLIC - ) - - mock_wallet_create_local_did.return_value = DIDInfo( - new_info.did, - new_info.verkey, - None, - method=DIDMethod.SOV, - key_type=KeyType.ED25519, + save=async_mock.CoroutineMock(), + metadata_set=async_mock.CoroutineMock(), ) mock_did_doc.from_json = async_mock.MagicMock( return_value=async_mock.MagicMock(did=TestConfig.test_did) ) - mock_wallet_get_local_did.return_value = DIDInfo( - TestConfig.test_did, - TestConfig.test_verkey, - None, - method=DIDMethod.SOV, - key_type=KeyType.ED25519, + mock_attach_deco.data_base64 = async_mock.MagicMock( + return_value=async_mock.MagicMock( + data=async_mock.MagicMock(sign=async_mock.CoroutineMock()) + ) + ) + mock_response.return_value = async_mock.MagicMock( + assign_thread_from=async_mock.MagicMock(), + assign_trace_from=async_mock.MagicMock(), ) - await self.manager.receive_request( + + conn_rec = await self.manager.receive_request( request=mock_request, recipient_did=TestConfig.test_did, - recipient_verkey=None, + recipient_verkey=TestConfig.test_verkey, my_endpoint=TestConfig.test_endpoint, alias="Alias", auto_accept_implicit=False, - mediation_id=None, ) + assert conn_rec + mock_conn_rec_cls.return_value.metadata_set.assert_called() - self.multitenant_mgr.add_key.assert_called_once_with( - "test_wallet", new_info.verkey - ) + assert not self.responder.messages async def test_receive_request_peer_did_not_found_x(self): async with self.profile.session() as session: @@ -1359,8 +1215,8 @@ async def test_receive_request_peer_did_not_found_x(self): ) await session.wallet.create_local_did( - method=DIDMethod.SOV, - key_type=KeyType.ED25519, + method=SOV, + key_type=ED25519, seed=None, did=TestConfig.test_did, ) @@ -1379,7 +1235,6 @@ async def test_receive_request_peer_did_not_found_x(self): my_endpoint=TestConfig.test_endpoint, alias="Alias", auto_accept_implicit=False, - mediation_id=None, ) async def test_create_response(self): @@ -1462,13 +1317,7 @@ async def test_create_response_mediation_id(self): record, mediation_id=mediation_record.mediation_id ) - assert len(self.responder.messages) == 1 - message, target = self.responder.messages[0] - assert isinstance(message, KeylistUpdate) - assert len(message.updates) == 1 - (add,) = message.updates - assert add.action == KeylistUpdateRule.RULE_ADD - assert add.recipient_key + self.route_manager.route_connection_as_inviter.assert_called_once() async def test_create_response_mediation_id_invalid_conn_state(self): async with self.profile.session() as session: @@ -1542,8 +1391,8 @@ async def test_create_response_multitenant(self): TestConfig.test_did, TestConfig.test_verkey, None, - method=DIDMethod.SOV, - key_type=KeyType.ED25519, + method=SOV, + key_type=ED25519, ) mock_create_did_doc.return_value = async_mock.MagicMock( serialize=async_mock.MagicMock() @@ -1555,9 +1404,7 @@ async def test_create_response_multitenant(self): ) await self.manager.create_response(conn_rec) - self.multitenant_mgr.add_key.assert_called_once_with( - "test_wallet", TestConfig.test_verkey - ) + self.route_manager.route_connection_as_inviter.assert_called_once() async def test_create_response_conn_rec_my_did(self): conn_rec = ConnRecord( @@ -1961,8 +1808,8 @@ async def test_create_did_document(self): TestConfig.test_did, TestConfig.test_verkey, None, - method=DIDMethod.SOV, - key_type=KeyType.ED25519, + method=SOV, + key_type=ED25519, ) mock_conn = async_mock.MagicMock( @@ -1995,8 +1842,8 @@ async def test_create_did_document_not_completed(self): TestConfig.test_did, TestConfig.test_verkey, None, - method=DIDMethod.SOV, - key_type=KeyType.ED25519, + method=SOV, + key_type=ED25519, ) mock_conn = async_mock.MagicMock( @@ -2023,8 +1870,8 @@ async def test_create_did_document_no_services(self): TestConfig.test_did, TestConfig.test_verkey, None, - method=DIDMethod.SOV, - key_type=KeyType.ED25519, + method=SOV, + key_type=ED25519, ) mock_conn = async_mock.MagicMock( @@ -2058,8 +1905,8 @@ async def test_create_did_document_no_service_endpoint(self): TestConfig.test_did, TestConfig.test_verkey, None, - method=DIDMethod.SOV, - key_type=KeyType.ED25519, + method=SOV, + key_type=ED25519, ) mock_conn = async_mock.MagicMock( @@ -2096,8 +1943,8 @@ async def test_create_did_document_no_service_recip_keys(self): TestConfig.test_did, TestConfig.test_verkey, None, - method=DIDMethod.SOV, - key_type=KeyType.ED25519, + method=SOV, + key_type=ED25519, ) mock_conn = async_mock.MagicMock( @@ -2142,8 +1989,8 @@ async def test_did_key_storage(self): TestConfig.test_did, TestConfig.test_verkey, None, - method=DIDMethod.SOV, - key_type=KeyType.ED25519, + method=SOV, + key_type=ED25519, ) did_doc = self.make_did_doc( @@ -2183,3 +2030,20 @@ async def test_diddoc_connection_targets_diddoc_underspecified(self): x_did_doc._service = {} with self.assertRaises(BaseConnectionManagerError): self.manager.diddoc_connection_targets(x_did_doc, TestConfig.test_verkey) + + async def test_resolve_did_document_error(self): + public_did_info = None + async with self.profile.session() as session: + await session.wallet.create_public_did( + SOV, + ED25519, + ) + public_did_info = await session.wallet.get_public_did() + with async_mock.patch.object( + self.resolver, + "resolve", + async_mock.CoroutineMock(side_effect=ResolverError()), + ): + with self.assertRaises(DIDXManagerError) as ctx: + await self.manager.get_resolved_did_document(public_did_info.did) + assert "Failed to resolve public DID in invitation" in str(ctx.exception) diff --git a/aries_cloudagent/protocols/didexchange/v1_0/tests/test_routes.py b/aries_cloudagent/protocols/didexchange/v1_0/tests/test_routes.py index 2f7bef8d74..8de2345b31 100644 --- a/aries_cloudagent/protocols/didexchange/v1_0/tests/test_routes.py +++ b/aries_cloudagent/protocols/didexchange/v1_0/tests/test_routes.py @@ -1,16 +1,17 @@ from asynctest import TestCase as AsyncTestCase from asynctest import mock as async_mock +from .. import routes as test_module from .....admin.request_context import AdminRequestContext from .....storage.error import StorageNotFoundError - -from .. import routes as test_module +from ....coordinate_mediation.v1_0.route_manager import RouteManager class TestDIDExchangeConnRoutes(AsyncTestCase): async def setUp(self): self.session_inject = {} self.context = AdminRequestContext.test_context(self.session_inject) + self.profile = self.context.profile self.request_dict = { "context": self.context, "outbound_message_router": async_mock.CoroutineMock(), @@ -21,6 +22,9 @@ async def setUp(self): query={}, __getitem__=lambda _, k: self.request_dict[k], ) + self.profile.context.injector.bind_instance( + RouteManager, async_mock.MagicMock() + ) async def test_didx_accept_invitation(self): self.request.match_info = {"conn_id": "dummy"} @@ -39,7 +43,6 @@ async def test_didx_accept_invitation(self): ) as mock_didx_mgr, async_mock.patch.object( test_module.web, "json_response" ) as mock_response: - mock_conn_rec_class.retrieve_by_id.return_value = mock_conn_rec mock_didx_mgr.return_value.create_request = async_mock.CoroutineMock() diff --git a/aries_cloudagent/protocols/discovery/v1_0/messages/tests/test_disclose.py b/aries_cloudagent/protocols/discovery/v1_0/messages/tests/test_disclose.py index 658f25bc67..84e02cf12e 100644 --- a/aries_cloudagent/protocols/discovery/v1_0/messages/tests/test_disclose.py +++ b/aries_cloudagent/protocols/discovery/v1_0/messages/tests/test_disclose.py @@ -42,7 +42,6 @@ def test_serialize(self, mock_disclose_schema_dump): class TestDiscloseSchema(TestCase): - disclose = Disclose(protocols=[]) def test_make_model(self): diff --git a/aries_cloudagent/protocols/discovery/v1_0/messages/tests/test_query.py b/aries_cloudagent/protocols/discovery/v1_0/messages/tests/test_query.py index 114c02f80c..18af3b6ba7 100644 --- a/aries_cloudagent/protocols/discovery/v1_0/messages/tests/test_query.py +++ b/aries_cloudagent/protocols/discovery/v1_0/messages/tests/test_query.py @@ -39,7 +39,6 @@ def test_serialize(self, mock_query_schema_dump): class TestQuerySchema(TestCase): - query = Query(query="*", comment="comment") def test_make_model(self): diff --git a/aries_cloudagent/protocols/discovery/v1_0/routes.py b/aries_cloudagent/protocols/discovery/v1_0/routes.py index a39b8c2159..66bd9dedd0 100644 --- a/aries_cloudagent/protocols/discovery/v1_0/routes.py +++ b/aries_cloudagent/protocols/discovery/v1_0/routes.py @@ -18,15 +18,6 @@ ) -class V10DiscoveryExchangeResultSchema(OpenAPISchema): - """Result schema for Discover Features v1.0 exchange record.""" - - results = fields.Nested( - V10DiscoveryRecordSchema, - description="Discover Features v1.0 exchange record", - ) - - class V10DiscoveryExchangeListResultSchema(OpenAPISchema): """Result schema for Discover Features v1.0 exchange records.""" @@ -70,7 +61,7 @@ class QueryDiscoveryExchRecordsSchema(OpenAPISchema): summary="Query supported features", ) @querystring_schema(QueryFeaturesQueryStringSchema()) -@response_schema(V10DiscoveryExchangeResultSchema(), 200, description="") +@response_schema(V10DiscoveryRecordSchema(), 200, description="") async def query_features(request: web.BaseRequest): """ Request handler for creating and sending feature query. diff --git a/aries_cloudagent/protocols/discovery/v2_0/messages/tests/test_queries.py b/aries_cloudagent/protocols/discovery/v2_0/messages/tests/test_queries.py index e8957dc912..01dfa793f3 100644 --- a/aries_cloudagent/protocols/discovery/v2_0/messages/tests/test_queries.py +++ b/aries_cloudagent/protocols/discovery/v2_0/messages/tests/test_queries.py @@ -60,7 +60,6 @@ def test_serialize(self, mock_queries_schema_dump): class TestQuerySchema(TestCase): - test_queries = [ QueryItem(feature_type="protocol", match=TEST_QUERY_PROTOCOL), QueryItem(feature_type="goal-code", match=TEST_QUERY_GOAL_CODE), diff --git a/aries_cloudagent/protocols/endorse_transaction/v1_0/controller.py b/aries_cloudagent/protocols/endorse_transaction/v1_0/controller.py index a4ddfe7e6e..b9ea0d92cb 100644 --- a/aries_cloudagent/protocols/endorse_transaction/v1_0/controller.py +++ b/aries_cloudagent/protocols/endorse_transaction/v1_0/controller.py @@ -5,6 +5,9 @@ ENDORSE_TRANSACTION = "aries.transaction.endorse" REFUSE_TRANSACTION = "aries.transaction.refuse" WRITE_TRANSACTION = "aries.transaction.ledger.write" +WRITE_TRANSACTION = "aries.transaction.ledger.write" +WRITE_DID_TRANSACTION = "aries.transaction.ledger.write_did" +REGISTER_PUBLIC_DID = "aries.transaction.register_public_did" class Controller: @@ -15,4 +18,10 @@ def __init__(self, protocol: str): def determine_goal_codes(self) -> Sequence[str]: """Return defined goal_codes.""" - return [ENDORSE_TRANSACTION, REFUSE_TRANSACTION, WRITE_TRANSACTION] + return [ + ENDORSE_TRANSACTION, + REFUSE_TRANSACTION, + WRITE_TRANSACTION, + WRITE_DID_TRANSACTION, + REGISTER_PUBLIC_DID, + ] diff --git a/aries_cloudagent/protocols/endorse_transaction/v1_0/handlers/endorsed_transaction_response_handler.py b/aries_cloudagent/protocols/endorse_transaction/v1_0/handlers/endorsed_transaction_response_handler.py index a8cff20d62..5cdb726d7f 100644 --- a/aries_cloudagent/protocols/endorse_transaction/v1_0/handlers/endorsed_transaction_response_handler.py +++ b/aries_cloudagent/protocols/endorse_transaction/v1_0/handlers/endorsed_transaction_response_handler.py @@ -45,7 +45,7 @@ async def handle(self, context: RequestContext, responder: BaseResponder): ( transaction, transaction_acknowledgement_message, - ) = await mgr.complete_transaction(transaction=transaction) + ) = await mgr.complete_transaction(transaction, False) await responder.send_reply( transaction_acknowledgement_message, diff --git a/aries_cloudagent/protocols/endorse_transaction/v1_0/handlers/tests/test_transaction_job_to_send_handler.py b/aries_cloudagent/protocols/endorse_transaction/v1_0/handlers/tests/test_transaction_job_to_send_handler.py index 9a1546fefa..41c66d4a78 100644 --- a/aries_cloudagent/protocols/endorse_transaction/v1_0/handlers/tests/test_transaction_job_to_send_handler.py +++ b/aries_cloudagent/protocols/endorse_transaction/v1_0/handlers/tests/test_transaction_job_to_send_handler.py @@ -34,26 +34,6 @@ async def test_called(self): ) assert not responder.messages - async def test_called_not_ready(self): - request_context = RequestContext.test_context() - request_context.message_receipt = MessageReceipt() - request_context.connection_record = async_mock.MagicMock() - - with async_mock.patch.object( - test_module, "TransactionManager", autospec=True - ) as mock_tran_mgr: - mock_tran_mgr.return_value.set_transaction_their_job = ( - async_mock.CoroutineMock() - ) - request_context.message = TransactionJobToSend() - request_context.connection_ready = False - handler = test_module.TransactionJobToSendHandler() - responder = MockResponder() - with self.assertRaises(test_module.HandlerException): - await handler.handle(request_context, responder) - - assert not responder.messages - async def test_called_x(self): request_context = RequestContext.test_context() request_context.message_receipt = MessageReceipt() diff --git a/aries_cloudagent/protocols/endorse_transaction/v1_0/handlers/transaction_job_to_send_handler.py b/aries_cloudagent/protocols/endorse_transaction/v1_0/handlers/transaction_job_to_send_handler.py index 4e42cb63a8..68a8ce8ab5 100644 --- a/aries_cloudagent/protocols/endorse_transaction/v1_0/handlers/transaction_job_to_send_handler.py +++ b/aries_cloudagent/protocols/endorse_transaction/v1_0/handlers/transaction_job_to_send_handler.py @@ -3,7 +3,6 @@ from .....messaging.base_handler import ( BaseHandler, BaseResponder, - HandlerException, RequestContext, ) @@ -26,9 +25,6 @@ async def handle(self, context: RequestContext, responder: BaseResponder): self._logger.debug(f"TransactionJobToSendHandler called with context {context}") assert isinstance(context.message, TransactionJobToSend) - if not context.connection_ready: - raise HandlerException("No connection established") - mgr = TransactionManager(context.profile) try: await mgr.set_transaction_their_job( diff --git a/aries_cloudagent/protocols/endorse_transaction/v1_0/manager.py b/aries_cloudagent/protocols/endorse_transaction/v1_0/manager.py index dca138e444..c41508bd20 100644 --- a/aries_cloudagent/protocols/endorse_transaction/v1_0/manager.py +++ b/aries_cloudagent/protocols/endorse_transaction/v1_0/manager.py @@ -15,12 +15,16 @@ from ....messaging.credential_definitions.util import notify_cred_def_event from ....messaging.schemas.util import notify_schema_event from ....revocation.util import ( - notify_revocation_entry_event, - notify_revocation_tails_file_event, + notify_revocation_reg_endorsed_event, + notify_revocation_entry_endorsed_event, ) from ....storage.error import StorageError, StorageNotFoundError from ....transport.inbound.receipt import MessageReceipt from ....wallet.base import BaseWallet +from ....wallet.util import ( + notify_endorse_did_event, + notify_endorse_did_attrib_event, +) from .messages.cancel_transaction import CancelTransaction from .messages.endorsed_transaction_response import EndorsedTransactionResponse @@ -114,6 +118,8 @@ async def create_request( signed_request: dict = None, expires_time: str = None, endorser_write_txn: bool = None, + author_goal_code: str = None, + signer_goal_code: str = None, ): """ Create a new Transaction Request. @@ -138,8 +144,12 @@ async def create_request( "context": TransactionRecord.SIGNATURE_CONTEXT, "method": TransactionRecord.ADD_SIGNATURE, "signature_type": TransactionRecord.SIGNATURE_TYPE, - "signer_goal_code": TransactionRecord.ENDORSE_TRANSACTION, - "author_goal_code": TransactionRecord.WRITE_TRANSACTION, + "signer_goal_code": signer_goal_code + if signer_goal_code + else TransactionRecord.ENDORSE_TRANSACTION, + "author_goal_code": author_goal_code + if author_goal_code + else TransactionRecord.WRITE_TRANSACTION, } transaction.signature_request.clear() transaction.signature_request.append(signature_request) @@ -204,6 +214,7 @@ async def create_endorse_response( self, transaction: TransactionRecord, state: str, + use_endorser_did: str = None, ): """ Create a response to endorse a transaction. @@ -228,29 +239,74 @@ async def create_endorse_response( transaction._type = TransactionRecord.SIGNATURE_RESPONSE transaction_json = transaction.messages_attach[0]["data"]["json"] + ledger_response = {} async with self._profile.session() as session: wallet: BaseWallet = session.inject_or(BaseWallet) if not wallet: raise StorageError("No wallet available") - endorser_did_info = await wallet.get_public_did() + endorser_did_info = None + override_did = ( + use_endorser_did + if use_endorser_did + else session.context.settings.get_value( + "endorser.endorser_endorse_with_did" + ) + ) + if override_did: + endorser_did_info = await wallet.get_local_did(override_did) + else: + endorser_did_info = await wallet.get_public_did() if not endorser_did_info: raise StorageError( - "Transaction cannot be endorsed as there is no Public DID in wallet" + "Transaction cannot be endorsed as there is no Public DID in wallet " + "or Endorser DID specified" ) endorser_did = endorser_did_info.did endorser_verkey = endorser_did_info.verkey - async with self._profile.session() as session: - ledger = session.context.inject_or(BaseLedger) - if not ledger: - reason = "No ledger available" - if not session.context.settings.get_value("wallet.type"): - reason += ": missing wallet-type?" - raise LedgerError(reason=reason) + ledger = self._profile.context.inject_or(BaseLedger) + if not ledger: + reason = "No ledger available" + if not self._profile.context.settings.get_value("wallet.type"): + reason += ": missing wallet-type?" + raise LedgerError(reason=reason) async with ledger: - endorsed_msg = await shield(ledger.txn_endorse(transaction_json)) + # check our goal code! + txn_goal_code = ( + transaction.signature_request[0]["signer_goal_code"] + if transaction.signature_request + and "signer_goal_code" in transaction.signature_request[0] + else TransactionRecord.ENDORSE_TRANSACTION + ) + if txn_goal_code == TransactionRecord.ENDORSE_TRANSACTION: + endorsed_msg = await shield( + ledger.txn_endorse(transaction_json, endorse_did=endorser_did_info) + ) + elif txn_goal_code == TransactionRecord.WRITE_DID_TRANSACTION: + # get DID info from transaction.meta_data + meta_data = json.loads(transaction_json) + (success, txn) = await shield( + ledger.register_nym( + meta_data["did"], + meta_data["verkey"], + meta_data["alias"], + meta_data["role"], + ) + ) + # we don't have an endorsed transaction so just return did meta-data + ledger_response = { + "result": { + "txn": {"type": "1", "data": {"dest": meta_data["did"]}} + }, + "meta_data": meta_data, + } + endorsed_msg = json.dumps(ledger_response) + else: + raise TransactionManagerError( + f"Invalid goal code for transaction record:" f" {txn_goal_code}" + ) # need to return the endorsed msg or else the ledger will reject the # eventual transaction write @@ -260,7 +316,7 @@ async def create_endorse_response( "message_id": transaction.messages_attach[0]["@id"], "context": TransactionRecord.SIGNATURE_CONTEXT, "method": TransactionRecord.ADD_SIGNATURE, - "signer_goal_code": TransactionRecord.ENDORSE_TRANSACTION, + "signer_goal_code": txn_goal_code, "signature_type": TransactionRecord.SIGNATURE_TYPE, "signature": {endorser_did: endorsed_msg or endorser_verkey}, } @@ -273,8 +329,12 @@ async def create_endorse_response( async with self._profile.session() as session: await transaction.save(session, reason="Created an endorsed response") - if transaction.endorser_write_txn: - ledger_response = await self.complete_transaction(transaction) + if ( + transaction.endorser_write_txn + and txn_goal_code == TransactionRecord.ENDORSE_TRANSACTION + ): + # running as the endorser, we've been asked to write the transaction + ledger_response = await self.complete_transaction(transaction, True) endorsed_transaction_response = EndorsedTransactionResponse( transaction_id=transaction.thread_id, thread_id=transaction._id, @@ -292,6 +352,7 @@ async def create_endorse_response( signature_response=signature_response, state=state, endorser_did=endorser_did, + ledger_response=ledger_response, ) return transaction, endorsed_transaction_response @@ -339,7 +400,9 @@ async def receive_endorse_response(self, response: EndorsedTransactionResponse): return transaction - async def complete_transaction(self, transaction: TransactionRecord): + async def complete_transaction( + self, transaction: TransactionRecord, endorser: bool = False + ): """ Complete a transaction. @@ -353,13 +416,17 @@ async def complete_transaction(self, transaction: TransactionRecord): The updated transaction """ + ledger_transaction = transaction.messages_attach[0]["data"]["json"] - async with self._profile.session() as session: + # check if we (author) have requested the endorser to write the transaction + if (endorser and transaction.endorser_write_txn) or ( + (not endorser) and (not transaction.endorser_write_txn) + ): ledger = self._profile.inject(BaseLedger) if not ledger: reason = "No ledger available" - if not session.context.settings.get_value("wallet.type"): + if not self._profile.context.settings.get_value("wallet.type"): reason += ": missing wallet-type?" raise TransactionManagerError(reason) @@ -373,7 +440,10 @@ async def complete_transaction(self, transaction: TransactionRecord): except (IndyIssuerError, LedgerError) as err: raise TransactionManagerError(err.roll_up) from err - ledger_response = json.loads(ledger_response_json) + ledger_response = json.loads(ledger_response_json) + + else: + ledger_response = ledger_transaction transaction.state = TransactionRecord.STATE_TRANSACTION_ACKED @@ -382,7 +452,7 @@ async def complete_transaction(self, transaction: TransactionRecord): # this scenario is where the endorser is writing the transaction # (called from self.create_endorse_response()) - if transaction.endorser_write_txn: + if endorser and transaction.endorser_write_txn: return ledger_response connection_id = transaction.connection_id @@ -718,13 +788,15 @@ async def endorsed_txn_post_processing( would be stored in wallet. """ - async with self._profile.session() as session: - ledger = self._profile.inject(BaseLedger) - if not ledger: - reason = "No ledger available" - if not session.context.settings.get_value("wallet.type"): - reason += ": missing wallet-type?" - raise TransactionManagerError(reason) + if isinstance(ledger_response, str): + ledger_response = json.loads(ledger_response) + + ledger = self._profile.inject(BaseLedger) + if not ledger: + reason = "No ledger available" + if not self._profile.context.settings.get_value("wallet.type"): + reason += ": missing wallet-type?" + raise TransactionManagerError(reason) # setup meta_data to pass to future events, if necessary meta_data = transaction.meta_data @@ -766,29 +838,27 @@ async def endorsed_txn_post_processing( # revocation registry transaction rev_reg_id = ledger_response["result"]["txnMetadata"]["txnId"] meta_data["context"]["rev_reg_id"] = rev_reg_id - auto_create_rev_reg = meta_data["processing"].get( - "auto_create_rev_reg", False + await notify_revocation_reg_endorsed_event( + self._profile, rev_reg_id, meta_data ) - # If "auto_processing" is enabled, also create the revocation entry record - if auto_create_rev_reg: - await notify_revocation_entry_event( - self._profile, rev_reg_id, meta_data - ) - elif ledger_response["result"]["txn"]["type"] == "114": # revocation entry transaction rev_reg_id = ledger_response["result"]["txn"]["data"]["revocRegDefId"] meta_data["context"]["rev_reg_id"] = rev_reg_id - auto_create_rev_reg = meta_data["processing"].get( - "auto_create_rev_reg", False + await notify_revocation_entry_endorsed_event( + self._profile, rev_reg_id, meta_data ) - # If "auto_processing" is enabled, also upload tails file for this registry - if auto_create_rev_reg: - await notify_revocation_tails_file_event( - self._profile, rev_reg_id, meta_data - ) + elif ledger_response["result"]["txn"]["type"] == "1": + # write DID to ledger + did = ledger_response["result"]["txn"]["data"]["dest"] + await notify_endorse_did_event(self._profile, did, meta_data) + + elif ledger_response["result"]["txn"]["type"] == "100": + # write DID ATTRIB to ledger + did = ledger_response["result"]["txn"]["data"]["dest"] + await notify_endorse_did_attrib_event(self._profile, did, meta_data) else: # TODO unknown ledger transaction type, just ignore for now ... diff --git a/aries_cloudagent/protocols/endorse_transaction/v1_0/models/transaction_record.py b/aries_cloudagent/protocols/endorse_transaction/v1_0/models/transaction_record.py index 33e734e5ca..3b863c6837 100644 --- a/aries_cloudagent/protocols/endorse_transaction/v1_0/models/transaction_record.py +++ b/aries_cloudagent/protocols/endorse_transaction/v1_0/models/transaction_record.py @@ -9,7 +9,13 @@ ) from .....messaging.valid import UUIDFour -from ..controller import ENDORSE_TRANSACTION, REFUSE_TRANSACTION, WRITE_TRANSACTION +from ..controller import ( + ENDORSE_TRANSACTION, + REFUSE_TRANSACTION, + WRITE_TRANSACTION, + WRITE_DID_TRANSACTION, + REGISTER_PUBLIC_DID, +) class TransactionRecord(BaseExchangeRecord): @@ -40,6 +46,8 @@ class Meta: ENDORSE_TRANSACTION = ENDORSE_TRANSACTION REFUSE_TRANSACTION = REFUSE_TRANSACTION WRITE_TRANSACTION = WRITE_TRANSACTION + WRITE_DID_TRANSACTION = WRITE_DID_TRANSACTION + REGISTER_PUBLIC_DID = REGISTER_PUBLIC_DID FORMAT_VERSION = "dif/endorse-transaction/request@v1.0" diff --git a/aries_cloudagent/protocols/endorse_transaction/v1_0/routes.py b/aries_cloudagent/protocols/endorse_transaction/v1_0/routes.py index 4a31f089e2..b2f14ae7f0 100644 --- a/aries_cloudagent/protocols/endorse_transaction/v1_0/routes.py +++ b/aries_cloudagent/protocols/endorse_transaction/v1_0/routes.py @@ -60,6 +60,15 @@ class TranIdMatchInfoSchema(OpenAPISchema): ) +class EndorserDIDInfoSchema(OpenAPISchema): + """Path parameters and validators for request Endorser DID.""" + + endorser_did = fields.Str( + description="Endorser DID", + required=False, + ) + + class AssignTransactionJobsSchema(OpenAPISchema): """Assign transaction related jobs to connection record.""" @@ -289,6 +298,7 @@ async def transaction_create_request(request: web.BaseRequest): tags=["endorse-transaction"], summary="For Endorser to endorse a particular transaction record", ) +@querystring_schema(EndorserDIDInfoSchema()) @match_info_schema(TranIdMatchInfoSchema()) @response_schema(TransactionRecordSchema(), 200) async def endorse_transaction_response(request: web.BaseRequest): @@ -305,6 +315,7 @@ async def endorse_transaction_response(request: web.BaseRequest): outbound_handler = request["outbound_message_router"] transaction_id = request.match_info["tran_id"] + endorser_did = request.query.get("endorser_did") try: async with context.profile.session() as session: transaction = await TransactionRecord.retrieve_by_id( @@ -313,6 +324,7 @@ async def endorse_transaction_response(request: web.BaseRequest): connection_record = await ConnRecord.retrieve_by_id( session, transaction.connection_id ) + # provided DID is validated in the TransactionManager except StorageNotFoundError as err: raise web.HTTPNotFound(reason=err.roll_up) from err @@ -341,6 +353,7 @@ async def endorse_transaction_response(request: web.BaseRequest): ) = await transaction_mgr.create_endorse_response( transaction=transaction, state=TransactionRecord.STATE_TRANSACTION_ENDORSED, + use_endorser_did=endorser_did, ) except (IndyIssuerError, LedgerError) as err: raise web.HTTPBadRequest(reason=err.roll_up) from err @@ -697,7 +710,7 @@ async def transaction_write(request: web.BaseRequest): ( tx_completed, transaction_acknowledgement_message, - ) = await transaction_mgr.complete_transaction(transaction=transaction) + ) = await transaction_mgr.complete_transaction(transaction, False) except StorageError as err: raise web.HTTPBadRequest(reason=err.roll_up) from err @@ -751,11 +764,15 @@ async def on_startup_event(profile: Profile, event: Event): invite = InvitationMessage.from_url(endorser_invitation) if invite: oob_mgr = OutOfBandManager(profile) - conn_record = await oob_mgr.receive_invitation( + oob_record = await oob_mgr.receive_invitation( invitation=invite, auto_accept=True, alias=endorser_alias, ) + async with profile.session() as session: + conn_record = await ConnRecord.retrieve_by_id( + session, oob_record.connection_id + ) else: invite = ConnectionInvitation.from_url(endorser_invitation) if invite: @@ -787,11 +804,10 @@ async def on_startup_event(profile: Profile, event: Event): value = {"endorser_did": endorser_did, "endorser_name": endorser_alias} await conn_record.metadata_set(session, key="endorser_info", value=value) - except Exception as e: + except Exception: # log the error, but continue LOGGER.exception( "Error accepting endorser invitation/configuring endorser connection: %s", - str(e), ) diff --git a/aries_cloudagent/protocols/endorse_transaction/v1_0/tests/test_manager.py b/aries_cloudagent/protocols/endorse_transaction/v1_0/tests/test_manager.py index edad031889..df10ffe4ee 100644 --- a/aries_cloudagent/protocols/endorse_transaction/v1_0/tests/test_manager.py +++ b/aries_cloudagent/protocols/endorse_transaction/v1_0/tests/test_manager.py @@ -2,31 +2,22 @@ import json import uuid -from aiohttp import web -from asynctest import mock as async_mock from asynctest import TestCase as AsyncTestCase +from asynctest import mock as async_mock from .....admin.request_context import AdminRequestContext from .....cache.base import BaseCache from .....cache.in_memory import InMemoryCache from .....connections.models.conn_record import ConnRecord -from .....core.in_memory import InMemoryProfile -from .....core.profile import Profile from .....ledger.base import BaseLedger from .....storage.error import StorageNotFoundError from .....wallet.base import BaseWallet -from .....wallet.did_info import DIDInfo -from .....wallet.did_method import DIDMethod -from .....wallet.key_type import KeyType - +from .....wallet.did_method import SOV, DIDMethods +from .....wallet.key_type import ED25519 from ..manager import TransactionManager, TransactionManagerError -from ..messages.messages_attach import MessagesAttach -from ..messages.transaction_acknowledgement import TransactionAcknowledgement -from ..messages.transaction_request import TransactionRequest from ..models.transaction_record import TransactionRecord from ..transaction_jobs import TransactionJob - TEST_DID = "LjgpST2rjsoxYegQDRm7EL" SCHEMA_NAME = "bc-reg" SCHEMA_TXN = 12 @@ -115,17 +106,19 @@ async def setUp(self): self.ledger.txn_endorse = async_mock.CoroutineMock( return_value=self.test_endorsed_message ) + self.ledger.register_nym = async_mock.CoroutineMock(return_value=(True, {})) self.context = AdminRequestContext.test_context() self.profile = self.context.profile injector = self.profile.context.injector injector.bind_instance(BaseLedger, self.ledger) + injector.bind_instance(DIDMethods, DIDMethods()) async with self.profile.session() as session: self.wallet: BaseWallet = session.inject_or(BaseWallet) await self.wallet.create_local_did( - DIDMethod.SOV, - KeyType.ED25519, + SOV, + ED25519, did="DJGEjaMunDtFtBVrn1qJMT", metadata={"meta": "data"}, ) @@ -235,6 +228,48 @@ async def test_create_request(self): transaction_request.messages_attach == transaction_record.messages_attach[0] ) + async def test_create_request_author_did(self): + transaction_record = await self.manager.create_record( + messages_attach=self.test_messages_attach, + connection_id=self.test_connection_id, + ) + + with async_mock.patch.object( + TransactionRecord, "save", autospec=True + ) as save_record: + ( + transaction_record, + transaction_request, + ) = await self.manager.create_request( + transaction_record, + expires_time=self.test_expires_time, + author_goal_code=TransactionRecord.REGISTER_PUBLIC_DID, + signer_goal_code=TransactionRecord.WRITE_DID_TRANSACTION, + ) + save_record.assert_called_once() + + assert transaction_record._type == TransactionRecord.SIGNATURE_REQUEST + assert transaction_record.signature_request[0] == { + "context": TransactionRecord.SIGNATURE_CONTEXT, + "method": TransactionRecord.ADD_SIGNATURE, + "signature_type": TransactionRecord.SIGNATURE_TYPE, + "signer_goal_code": TransactionRecord.WRITE_DID_TRANSACTION, + "author_goal_code": TransactionRecord.REGISTER_PUBLIC_DID, + } + assert transaction_record.state == TransactionRecord.STATE_REQUEST_SENT + assert transaction_record.connection_id == self.test_connection_id + assert transaction_record.timing["expires_time"] == self.test_expires_time + + assert transaction_request.transaction_id == transaction_record._id + assert ( + transaction_request.signature_request + == transaction_record.signature_request[0] + ) + assert transaction_request.timing == transaction_record.timing + assert ( + transaction_request.messages_attach == transaction_record.messages_attach[0] + ) + async def test_recieve_request(self): mock_request = async_mock.MagicMock() mock_request.transaction_id = self.test_author_transaction_id @@ -340,6 +375,55 @@ async def test_create_endorse_response(self): ) assert endorsed_transaction_response.endorser_did == self.test_endorser_did + async def test_create_endorse_response_author_did(self): + transaction_record = await self.manager.create_record( + messages_attach=self.test_messages_attach, + connection_id=self.test_connection_id, + ) + + with async_mock.patch.object( + TransactionRecord, "save", autospec=True + ) as save_record: + ( + transaction_record, + transaction_request, + ) = await self.manager.create_request( + transaction_record, + expires_time=self.test_expires_time, + author_goal_code=TransactionRecord.REGISTER_PUBLIC_DID, + signer_goal_code=TransactionRecord.WRITE_DID_TRANSACTION, + ) + save_record.assert_called_once() + + transaction_record.state = TransactionRecord.STATE_REQUEST_RECEIVED + transaction_record.thread_id = self.test_author_transaction_id + transaction_record.messages_attach[0]["data"]["json"] = json.dumps( + { + "did": "test", + "verkey": "test", + "alias": "test", + "role": "", + } + ) + + with async_mock.patch.object( + TransactionRecord, "save", autospec=True + ) as save_record: + ( + transaction_record, + endorsed_transaction_response, + ) = await self.manager.create_endorse_response( + transaction_record, + state=TransactionRecord.STATE_TRANSACTION_ENDORSED, + ) + save_record.assert_called_once() + + assert transaction_record._type == TransactionRecord.SIGNATURE_RESPONSE + assert ( + transaction_record.messages_attach[0]["data"]["json"] + == '{"result": {"txn": {"type": "1", "data": {"dest": "test"}}}, "meta_data": {"did": "test", "verkey": "test", "alias": "test", "role": ""}}' + ) + async def test_receive_endorse_response(self): transaction_record = await self.manager.create_record( messages_attach=self.test_messages_attach, @@ -412,7 +496,6 @@ async def test_complete_transaction(self): ) as save_record, async_mock.patch.object( ConnRecord, "retrieve_by_id" ) as mock_conn_rec_retrieve: - mock_conn_rec_retrieve.return_value = async_mock.MagicMock( metadata_get=async_mock.CoroutineMock( return_value={ @@ -427,7 +510,7 @@ async def test_complete_transaction(self): ( transaction_record, transaction_acknowledgement_message, - ) = await self.manager.complete_transaction(transaction_record) + ) = await self.manager.complete_transaction(transaction_record, False) save_record.assert_called_once() assert transaction_record.state == TransactionRecord.STATE_TRANSACTION_ACKED diff --git a/aries_cloudagent/protocols/endorse_transaction/v1_0/tests/test_routes.py b/aries_cloudagent/protocols/endorse_transaction/v1_0/tests/test_routes.py index d91b1ff8b8..4d6e3b0897 100644 --- a/aries_cloudagent/protocols/endorse_transaction/v1_0/tests/test_routes.py +++ b/aries_cloudagent/protocols/endorse_transaction/v1_0/tests/test_routes.py @@ -1,20 +1,18 @@ import asyncio import json -from asynctest import mock as async_mock, TestCase as AsyncTestCase +from asynctest import TestCase as AsyncTestCase +from asynctest import mock as async_mock -from .....admin.request_context import AdminRequestContext from .....connections.models.conn_record import ConnRecord from .....core.in_memory import InMemoryProfile from .....ledger.base import BaseLedger from .....wallet.base import BaseWallet from .....wallet.did_info import DIDInfo -from .....wallet.did_method import DIDMethod -from .....wallet.key_type import KeyType - -from ..models.transaction_record import TransactionRecord +from .....wallet.did_method import SOV +from .....wallet.key_type import ED25519 from .. import routes as test_module - +from ..models.transaction_record import TransactionRecord TEST_DID = "LjgpST2rjsoxYegQDRm7EL" SCHEMA_NAME = "bc-reg" @@ -436,8 +434,8 @@ async def test_endorse_transaction_response(self): "did", "verkey", {"meta": "data"}, - method=DIDMethod.SOV, - key_type=KeyType.ED25519, + method=SOV, + key_type=ED25519, ) ) ), @@ -515,8 +513,8 @@ async def test_endorse_transaction_response_not_found_x(self): "did", "verkey", {"meta": "data"}, - method=DIDMethod.SOV, - key_type=KeyType.ED25519, + method=SOV, + key_type=ED25519, ) ) ), @@ -544,8 +542,8 @@ async def test_endorse_transaction_response_base_model_x(self): "did", "verkey", {"meta": "data"}, - method=DIDMethod.SOV, - key_type=KeyType.ED25519, + method=SOV, + key_type=ED25519, ) ) ), @@ -579,8 +577,8 @@ async def test_endorse_transaction_response_no_jobs_x(self): "did", "verkey", {"meta": "data"}, - method=DIDMethod.SOV, - key_type=KeyType.ED25519, + method=SOV, + key_type=ED25519, ) ) ), @@ -616,8 +614,8 @@ async def skip_test_endorse_transaction_response_no_ledger_x(self): "did", "verkey", {"meta": "data"}, - method=DIDMethod.SOV, - key_type=KeyType.ED25519, + method=SOV, + key_type=ED25519, ) ) ), @@ -671,8 +669,8 @@ async def test_endorse_transaction_response_wrong_my_job_x(self): "did", "verkey", {"meta": "data"}, - method=DIDMethod.SOV, - key_type=KeyType.ED25519, + method=SOV, + key_type=ED25519, ) ) ), @@ -714,8 +712,8 @@ async def skip_test_endorse_transaction_response_ledger_x(self): "did", "verkey", {"meta": "data"}, - method=DIDMethod.SOV, - key_type=KeyType.ED25519, + method=SOV, + key_type=ED25519, ) ) ), @@ -772,8 +770,8 @@ async def test_endorse_transaction_response_txn_mgr_x(self): "did", "verkey", {"meta": "data"}, - method=DIDMethod.SOV, - key_type=KeyType.ED25519, + method=SOV, + key_type=ED25519, ) ) ), @@ -824,8 +822,8 @@ async def test_refuse_transaction_response(self): "did", "verkey", {"meta": "data"}, - method=DIDMethod.SOV, - key_type=KeyType.ED25519, + method=SOV, + key_type=ED25519, ) ) ), @@ -882,8 +880,8 @@ async def test_refuse_transaction_response_not_found_x(self): "did", "verkey", {"meta": "data"}, - method=DIDMethod.SOV, - key_type=KeyType.ED25519, + method=SOV, + key_type=ED25519, ) ) ), @@ -912,8 +910,8 @@ async def test_refuse_transaction_response_conn_base_model_x(self): "did", "verkey", {"meta": "data"}, - method=DIDMethod.SOV, - key_type=KeyType.ED25519, + method=SOV, + key_type=ED25519, ) ) ), @@ -947,8 +945,8 @@ async def test_refuse_transaction_response_no_jobs_x(self): "did", "verkey", {"meta": "data"}, - method=DIDMethod.SOV, - key_type=KeyType.ED25519, + method=SOV, + key_type=ED25519, ) ) ), @@ -984,8 +982,8 @@ async def test_refuse_transaction_response_wrong_my_job_x(self): "did", "verkey", {"meta": "data"}, - method=DIDMethod.SOV, - key_type=KeyType.ED25519, + method=SOV, + key_type=ED25519, ) ) ), @@ -1027,8 +1025,8 @@ async def test_refuse_transaction_response_txn_mgr_x(self): "did", "verkey", {"meta": "data"}, - method=DIDMethod.SOV, - key_type=KeyType.ED25519, + method=SOV, + key_type=ED25519, ) ) ), @@ -1555,7 +1553,6 @@ async def test_transaction_write_schema_txn(self): ) as mock_txn_mgr, async_mock.patch.object( test_module.web, "json_response" ) as mock_response: - mock_txn_mgr.return_value.complete_transaction = async_mock.CoroutineMock() mock_txn_mgr.return_value.complete_transaction.return_value = ( @@ -1602,7 +1599,6 @@ async def test_transaction_write_wrong_state_x(self): with async_mock.patch.object( TransactionRecord, "retrieve_by_id", async_mock.CoroutineMock() ) as mock_txn_rec_retrieve: - mock_txn_rec_retrieve.return_value = async_mock.MagicMock( serialize=async_mock.MagicMock(return_value={"...": "..."}), state=TransactionRecord.STATE_TRANSACTION_CREATED, diff --git a/aries_cloudagent/protocols/introduction/v0_1/tests/test_service.py b/aries_cloudagent/protocols/introduction/v0_1/tests/test_service.py index a2bff3f785..ea758821a8 100644 --- a/aries_cloudagent/protocols/introduction/v0_1/tests/test_service.py +++ b/aries_cloudagent/protocols/introduction/v0_1/tests/test_service.py @@ -5,7 +5,7 @@ from .....messaging.request_context import RequestContext from .....messaging.responder import MockResponder from .....did.did_key import DIDKey -from .....wallet.key_type import KeyType +from .....wallet.key_type import ED25519 from ....didcomm_prefix import DIDCommPrefix from ....out_of_band.v1_0.message_types import INVITATION as OOB_INVITATION @@ -36,12 +36,10 @@ def setUp(self): _type="did-communication", did=TEST_DID, recipient_keys=[ - DIDKey.from_public_key_b58(TEST_VERKEY, KeyType.ED25519).did + DIDKey.from_public_key_b58(TEST_VERKEY, ED25519).did ], routing_keys=[ - DIDKey.from_public_key_b58( - TEST_ROUTE_VERKEY, KeyType.ED25519 - ).did + DIDKey.from_public_key_b58(TEST_ROUTE_VERKEY, ED25519).did ], service_endpoint=TEST_ENDPOINT, ) diff --git a/aries_cloudagent/protocols/issue_credential/v1_0/handlers/credential_ack_handler.py b/aries_cloudagent/protocols/issue_credential/v1_0/handlers/credential_ack_handler.py index 26faeb8915..5229bfef50 100644 --- a/aries_cloudagent/protocols/issue_credential/v1_0/handlers/credential_ack_handler.py +++ b/aries_cloudagent/protocols/issue_credential/v1_0/handlers/credential_ack_handler.py @@ -1,5 +1,6 @@ """Credential ack message handler.""" +from .....core.oob_processor import OobMessageProcessor from .....messaging.base_handler import BaseHandler, HandlerException from .....messaging.request_context import RequestContext from .....messaging.responder import BaseResponder @@ -29,12 +30,27 @@ async def handle(self, context: RequestContext, responder: BaseResponder): context.message.serialize(as_string=True), ) - if not context.connection_ready: - raise HandlerException("No connection established for credential ack") + # If connection is present it must be ready for use + if context.connection_record and not context.connection_ready: + raise HandlerException("Connection used for credential ack not ready") + + # Find associated oob record + oob_processor = context.inject(OobMessageProcessor) + oob_record = await oob_processor.find_oob_record_for_inbound_message(context) + + # Either connection or oob context must be present + if not context.connection_record and not oob_record: + raise HandlerException( + "No connection or associated connectionless exchange found for credential" + " ack" + ) credential_manager = CredentialManager(context.profile) await credential_manager.receive_credential_ack( - context.message, context.connection_record.connection_id + context.message, + context.connection_record.connection_id + if context.connection_record + else None, ) trace_event( diff --git a/aries_cloudagent/protocols/issue_credential/v1_0/handlers/credential_issue_handler.py b/aries_cloudagent/protocols/issue_credential/v1_0/handlers/credential_issue_handler.py index 1e6404e521..7fe0bd7bf6 100644 --- a/aries_cloudagent/protocols/issue_credential/v1_0/handlers/credential_issue_handler.py +++ b/aries_cloudagent/protocols/issue_credential/v1_0/handlers/credential_issue_handler.py @@ -1,5 +1,6 @@ """Credential issue message handler.""" +from .....core.oob_processor import OobMessageProcessor from .....indy.holder import IndyHolderError from .....messaging.base_handler import BaseHandler, HandlerException from .....messaging.models.base import BaseModelError @@ -34,12 +35,26 @@ async def handle(self, context: RequestContext, responder: BaseResponder): "Received credential message: %s", context.message.serialize(as_string=True) ) - if not context.connection_ready: - raise HandlerException("No connection established for credential issue") + # If connection is present it must be ready for use + if context.connection_record and not context.connection_ready: + raise HandlerException("Connection used for credential not ready") + + # Find associated oob record + oob_processor = context.inject(OobMessageProcessor) + oob_record = await oob_processor.find_oob_record_for_inbound_message(context) + + # Either connection or oob context must be present + if not context.connection_record and not oob_record: + raise HandlerException( + "No connection or associated connectionless exchange found for credential" + ) credential_manager = CredentialManager(profile) cred_ex_record = await credential_manager.receive_credential( - context.message, context.connection_record.connection_id + context.message, + context.connection_record.connection_id + if context.connection_record + else None, ) # mgr only finds, saves record: on exception, saving state null is hopeless r_time = trace_event( @@ -50,7 +65,7 @@ async def handle(self, context: RequestContext, responder: BaseResponder): ) # Automatically move to next state if flag is set - if context.settings.get("debug.auto_store_credential"): + if cred_ex_record and context.settings.get("debug.auto_store_credential"): try: cred_ex_record = await credential_manager.store_credential( cred_ex_record @@ -62,7 +77,7 @@ async def handle(self, context: RequestContext, responder: BaseResponder): StorageError, ) as err: # treat failure to store as mangled on receipt hence protocol error - self._logger.exception(err) + self._logger.exception("Error storing issued credential") if cred_ex_record: async with profile.session() as session: await cred_ex_record.save_error_state( @@ -76,7 +91,7 @@ async def handle(self, context: RequestContext, responder: BaseResponder): ) ) - credential_ack_message = await credential_manager.send_credential_ack( + (_, credential_ack_message) = await credential_manager.send_credential_ack( cred_ex_record ) diff --git a/aries_cloudagent/protocols/issue_credential/v1_0/handlers/credential_offer_handler.py b/aries_cloudagent/protocols/issue_credential/v1_0/handlers/credential_offer_handler.py index cddc2e4010..a9e114ee40 100644 --- a/aries_cloudagent/protocols/issue_credential/v1_0/handlers/credential_offer_handler.py +++ b/aries_cloudagent/protocols/issue_credential/v1_0/handlers/credential_offer_handler.py @@ -1,5 +1,8 @@ """Credential offer message handler.""" + +from .....wallet.util import default_did_from_verkey +from .....core.oob_processor import OobMessageProcessor from .....indy.holder import IndyHolderError from .....ledger.error import LedgerError from .....messaging.base_handler import BaseHandler, HandlerException @@ -36,12 +39,30 @@ async def handle(self, context: RequestContext, responder: BaseResponder): context.message.serialize(as_string=True), ) - if not context.connection_ready: - raise HandlerException("No connection established for credential offer") + # If connection is present it must be ready for use + if context.connection_record and not context.connection_ready: + raise HandlerException("Connection used for credential offer not ready") + + # Find associated oob record + oob_processor = context.inject(OobMessageProcessor) + oob_record = await oob_processor.find_oob_record_for_inbound_message(context) + + # Either connection or oob context must be present + if not context.connection_record and not oob_record: + raise HandlerException( + "No connection or associated connectionless exchange found for credential" + " offer" + ) + + connection_id = ( + context.connection_record.connection_id + if context.connection_record + else None + ) credential_manager = CredentialManager(profile) cred_ex_record = await credential_manager.receive_offer( - context.message, context.connection_record.connection_id + context.message, connection_id ) # mgr only finds, saves record: on exception, saving state null is hopeless r_time = trace_event( @@ -51,8 +72,16 @@ async def handle(self, context: RequestContext, responder: BaseResponder): perf_counter=r_time, ) + if context.connection_record: + holder_did = context.connection_record.my_did + else: + # Transform recipient key into did + holder_did = default_did_from_verkey(oob_record.our_recipient_key) + # If auto respond is turned on, automatically reply with credential request - if context.settings.get("debug.auto_respond_credential_offer"): + if cred_ex_record and context.settings.get( + "debug.auto_respond_credential_offer" + ): credential_request_message = None try: ( @@ -60,7 +89,7 @@ async def handle(self, context: RequestContext, responder: BaseResponder): credential_request_message, ) = await credential_manager.create_request( cred_ex_record=cred_ex_record, - holder_did=context.connection_record.my_did, + holder_did=holder_did, ) await responder.send_reply(credential_request_message) except ( @@ -70,7 +99,7 @@ async def handle(self, context: RequestContext, responder: BaseResponder): LedgerError, StorageError, ) as err: - self._logger.exception(err) + self._logger.exception("Error responding to credential offer") if cred_ex_record: async with profile.session() as session: await cred_ex_record.save_error_state( diff --git a/aries_cloudagent/protocols/issue_credential/v1_0/handlers/credential_problem_report_handler.py b/aries_cloudagent/protocols/issue_credential/v1_0/handlers/credential_problem_report_handler.py index bb88d6614f..24d2b25079 100644 --- a/aries_cloudagent/protocols/issue_credential/v1_0/handlers/credential_problem_report_handler.py +++ b/aries_cloudagent/protocols/issue_credential/v1_0/handlers/credential_problem_report_handler.py @@ -1,6 +1,6 @@ """Credential problem report message handler.""" -from .....messaging.base_handler import BaseHandler +from .....messaging.base_handler import BaseHandler, HandlerException from .....messaging.request_context import RequestContext from .....messaging.responder import BaseResponder from .....storage.error import StorageError, StorageNotFoundError @@ -26,6 +26,16 @@ async def handle(self, context: RequestContext, responder: BaseResponder): ) assert isinstance(context.message, CredentialProblemReport) + # If connection is present it must be ready for use + if context.connection_record and not context.connection_ready: + raise HandlerException( + "Connection used for credential problem report not ready" + ) + elif not context.connection_record: + raise HandlerException( + "Connectionless not supported for credential problem report" + ) + credential_manager = CredentialManager(context.profile) try: await credential_manager.receive_problem_report( diff --git a/aries_cloudagent/protocols/issue_credential/v1_0/handlers/credential_proposal_handler.py b/aries_cloudagent/protocols/issue_credential/v1_0/handlers/credential_proposal_handler.py index fe2a94bf72..b42338aa10 100644 --- a/aries_cloudagent/protocols/issue_credential/v1_0/handlers/credential_proposal_handler.py +++ b/aries_cloudagent/protocols/issue_credential/v1_0/handlers/credential_proposal_handler.py @@ -37,8 +37,13 @@ async def handle(self, context: RequestContext, responder: BaseResponder): context.message.serialize(as_string=True), ) - if not context.connection_ready: - raise HandlerException("No connection established for credential proposal") + # If connection is present it must be ready for use + if context.connection_record and not context.connection_ready: + raise HandlerException("Connection used for credential proposal not ready") + elif not context.connection_record: + raise HandlerException( + "Connectionless not supported for credential proposal" + ) credential_manager = CredentialManager(profile) cred_ex_record = await credential_manager.receive_proposal( @@ -72,7 +77,7 @@ async def handle(self, context: RequestContext, responder: BaseResponder): LedgerError, StorageError, ) as err: - self._logger.exception(err) + self._logger.exception("Error responding to credential proposal") if cred_ex_record: async with profile.session() as session: await cred_ex_record.save_error_state( diff --git a/aries_cloudagent/protocols/issue_credential/v1_0/handlers/credential_request_handler.py b/aries_cloudagent/protocols/issue_credential/v1_0/handlers/credential_request_handler.py index 39ff2e73b5..f9bc43e70e 100644 --- a/aries_cloudagent/protocols/issue_credential/v1_0/handlers/credential_request_handler.py +++ b/aries_cloudagent/protocols/issue_credential/v1_0/handlers/credential_request_handler.py @@ -1,5 +1,6 @@ """Credential request message handler.""" +from .....core.oob_processor import OobMessageProcessor from .....indy.issuer import IndyIssuerError from .....ledger.error import LedgerError from .....messaging.base_handler import BaseHandler, HandlerException @@ -36,12 +37,25 @@ async def handle(self, context: RequestContext, responder: BaseResponder): context.message.serialize(as_string=True), ) - if not context.connection_ready: - raise HandlerException("No connection established for credential request") + # If connection is present it must be ready for use + if context.connection_record and not context.connection_ready: + raise HandlerException("Connection used for credential request not ready") + + # Find associated oob record. If the credential offer was created as an oob + # attachment the presentation exchange record won't have a connection id (yet) + oob_processor = context.inject(OobMessageProcessor) + oob_record = await oob_processor.find_oob_record_for_inbound_message(context) + + # Either connection or oob context must be present + if not context.connection_record and not oob_record: + raise HandlerException( + "No connection or associated connectionless exchange found for credential" + " request" + ) credential_manager = CredentialManager(profile) cred_ex_record = await credential_manager.receive_request( - context.message, context.connection_record.connection_id + context.message, context.connection_record, oob_record ) # mgr only finds, saves record: on exception, saving state null is hopeless r_time = trace_event( @@ -52,7 +66,7 @@ async def handle(self, context: RequestContext, responder: BaseResponder): ) # If auto_issue is enabled, respond immediately - if cred_ex_record.auto_issue: + if cred_ex_record and cred_ex_record.auto_issue: if ( cred_ex_record.credential_proposal_dict and cred_ex_record.credential_proposal_dict.credential_proposal @@ -74,7 +88,7 @@ async def handle(self, context: RequestContext, responder: BaseResponder): LedgerError, StorageError, ) as err: - self._logger.exception(err) + self._logger.exception("Error responding to credential request") if cred_ex_record: async with profile.session() as session: await cred_ex_record.save_error_state( diff --git a/aries_cloudagent/protocols/issue_credential/v1_0/handlers/tests/test_credential_ack_handler.py b/aries_cloudagent/protocols/issue_credential/v1_0/handlers/tests/test_credential_ack_handler.py index fa0fad4c10..ae0c31f0e6 100644 --- a/aries_cloudagent/protocols/issue_credential/v1_0/handlers/tests/test_credential_ack_handler.py +++ b/aries_cloudagent/protocols/issue_credential/v1_0/handlers/tests/test_credential_ack_handler.py @@ -1,5 +1,7 @@ from asynctest import mock as async_mock, TestCase as AsyncTestCase + +from ......core.oob_processor import OobMessageProcessor from ......messaging.request_context import RequestContext from ......messaging.responder import MockResponder from ......transport.inbound.receipt import MessageReceipt @@ -12,6 +14,14 @@ class TestCredentialAckHandler(AsyncTestCase): async def test_called(self): request_context = RequestContext.test_context() + + mock_oob_processor = async_mock.MagicMock( + find_oob_record_for_inbound_message=async_mock.CoroutineMock( + return_value=async_mock.MagicMock() + ) + ) + request_context.injector.bind_instance(OobMessageProcessor, mock_oob_processor) + request_context.message_receipt = MessageReceipt() request_context.connection_record = async_mock.MagicMock() @@ -31,6 +41,9 @@ async def test_called(self): mock_cred_mgr.return_value.receive_credential_ack.assert_called_once_with( request_context.message, request_context.connection_record.connection_id ) + mock_oob_processor.find_oob_record_for_inbound_message.assert_called_once_with( + request_context + ) assert not responder.messages async def test_called_not_ready(self): @@ -48,7 +61,39 @@ async def test_called_not_ready(self): request_context.connection_ready = False handler = test_module.CredentialAckHandler() responder = MockResponder() - with self.assertRaises(test_module.HandlerException): + with self.assertRaises(test_module.HandlerException) as err: + await handler.handle(request_context, responder) + assert ( + err.exception.message == "Connection used for credential ack not ready" + ) + + async def test_called_no_connection_no_oob(self): + request_context = RequestContext.test_context() + request_context.message_receipt = MessageReceipt() + + mock_oob_processor = async_mock.MagicMock( + find_oob_record_for_inbound_message=async_mock.CoroutineMock( + # No oob record found + return_value=None + ) + ) + request_context.injector.bind_instance(OobMessageProcessor, mock_oob_processor) + + with async_mock.patch.object( + test_module, "CredentialManager", autospec=True + ) as mock_cred_mgr: + mock_cred_mgr.return_value.receive_credential_ack = ( + async_mock.CoroutineMock() + ) + request_context.message = CredentialAck() + request_context.connection_ready = False + handler = test_module.CredentialAckHandler() + responder = MockResponder() + with self.assertRaises(test_module.HandlerException) as err: await handler.handle(request_context, responder) + assert ( + err.exception.message + == "No connection or associated connectionless exchange found for credential ack" + ) assert not responder.messages diff --git a/aries_cloudagent/protocols/issue_credential/v1_0/handlers/tests/test_credential_issue_handler.py b/aries_cloudagent/protocols/issue_credential/v1_0/handlers/tests/test_credential_issue_handler.py index 8e07df1ce7..0f831927a1 100644 --- a/aries_cloudagent/protocols/issue_credential/v1_0/handlers/tests/test_credential_issue_handler.py +++ b/aries_cloudagent/protocols/issue_credential/v1_0/handlers/tests/test_credential_issue_handler.py @@ -1,5 +1,6 @@ from asynctest import mock as async_mock, TestCase as AsyncTestCase +from ......core.oob_processor import OobMessageProcessor from ......messaging.request_context import RequestContext from ......messaging.responder import MockResponder from ......transport.inbound.receipt import MessageReceipt @@ -16,6 +17,13 @@ async def test_called(self): request_context.settings["debug.auto_store_credential"] = False request_context.connection_record = async_mock.MagicMock() + mock_oob_processor = async_mock.MagicMock( + find_oob_record_for_inbound_message=async_mock.CoroutineMock( + return_value=async_mock.MagicMock() + ) + ) + request_context.injector.bind_instance(OobMessageProcessor, mock_oob_processor) + with async_mock.patch.object( test_module, "CredentialManager", autospec=True ) as mock_cred_mgr: @@ -30,6 +38,9 @@ async def test_called(self): mock_cred_mgr.return_value.receive_credential.assert_called_once_with( request_context.message, request_context.connection_record.connection_id ) + mock_oob_processor.find_oob_record_for_inbound_message.assert_called_once_with( + request_context + ) assert not responder.messages async def test_called_auto_store(self): @@ -38,6 +49,13 @@ async def test_called_auto_store(self): request_context.settings["debug.auto_store_credential"] = True request_context.connection_record = async_mock.MagicMock() + mock_oob_processor = async_mock.MagicMock( + find_oob_record_for_inbound_message=async_mock.CoroutineMock( + return_value=async_mock.MagicMock() + ) + ) + request_context.injector.bind_instance(OobMessageProcessor, mock_oob_processor) + with async_mock.patch.object( test_module, "CredentialManager", autospec=True ) as mock_cred_mgr: @@ -45,7 +63,10 @@ async def test_called_auto_store(self): receive_credential=async_mock.CoroutineMock(), store_credential=async_mock.CoroutineMock(), send_credential_ack=async_mock.CoroutineMock( - return_value="credential_ack_message" + return_value=( + async_mock.CoroutineMock(), + async_mock.CoroutineMock(), + ) ), ) request_context.message = CredentialIssue() @@ -58,6 +79,9 @@ async def test_called_auto_store(self): mock_cred_mgr.return_value.receive_credential.assert_called_once_with( request_context.message, request_context.connection_record.connection_id ) + mock_oob_processor.find_oob_record_for_inbound_message.assert_called_once_with( + request_context + ) assert mock_cred_mgr.return_value.send_credential_ack.call_count == 1 async def test_called_auto_store_x(self): @@ -66,6 +90,13 @@ async def test_called_auto_store_x(self): request_context.settings["debug.auto_store_credential"] = True request_context.connection_record = async_mock.MagicMock() + mock_oob_processor = async_mock.MagicMock( + find_oob_record_for_inbound_message=async_mock.CoroutineMock( + return_value=async_mock.MagicMock() + ) + ) + request_context.injector.bind_instance(OobMessageProcessor, mock_oob_processor) + with async_mock.patch.object( test_module, "CredentialManager", autospec=True ) as mock_cred_mgr: @@ -78,7 +109,12 @@ async def test_called_auto_store_x(self): store_credential=async_mock.CoroutineMock( side_effect=test_module.IndyHolderError() ), - send_credential_ack=async_mock.CoroutineMock(), + send_credential_ack=async_mock.CoroutineMock( + return_value=( + async_mock.CoroutineMock(), + async_mock.CoroutineMock(), + ) + ), ) request_context.message = CredentialIssue() @@ -97,6 +133,7 @@ async def test_called_auto_store_x(self): async def test_called_not_ready(self): request_context = RequestContext.test_context() request_context.message_receipt = MessageReceipt() + request_context.connection_record = async_mock.MagicMock() with async_mock.patch.object( test_module, "CredentialManager", autospec=True @@ -106,7 +143,36 @@ async def test_called_not_ready(self): request_context.connection_ready = False handler = test_module.CredentialIssueHandler() responder = MockResponder() - with self.assertRaises(test_module.HandlerException): + with self.assertRaises(test_module.HandlerException) as err: + await handler.handle(request_context, responder) + assert err.exception.message == "Connection used for credential not ready" + + assert not responder.messages + + async def test_called_no_connection_no_oob(self): + request_context = RequestContext.test_context() + request_context.message_receipt = MessageReceipt() + + mock_oob_processor = async_mock.MagicMock( + find_oob_record_for_inbound_message=async_mock.CoroutineMock( + # No oob record found + return_value=None + ) + ) + request_context.injector.bind_instance(OobMessageProcessor, mock_oob_processor) + + with async_mock.patch.object( + test_module, "CredentialManager", autospec=True + ) as mock_cred_mgr: + mock_cred_mgr.return_value.receive_credential = async_mock.CoroutineMock() + request_context.message = CredentialIssue() + handler = test_module.CredentialIssueHandler() + responder = MockResponder() + with self.assertRaises(test_module.HandlerException) as err: await handler.handle(request_context, responder) + assert ( + err.exception.message + == "No connection or associated connectionless exchange found for credential" + ) assert not responder.messages diff --git a/aries_cloudagent/protocols/issue_credential/v1_0/handlers/tests/test_credential_offer_handler.py b/aries_cloudagent/protocols/issue_credential/v1_0/handlers/tests/test_credential_offer_handler.py index 1716b67210..a9aedbb0a9 100644 --- a/aries_cloudagent/protocols/issue_credential/v1_0/handlers/tests/test_credential_offer_handler.py +++ b/aries_cloudagent/protocols/issue_credential/v1_0/handlers/tests/test_credential_offer_handler.py @@ -1,5 +1,6 @@ from asynctest import mock as async_mock, TestCase as AsyncTestCase +from ......core.oob_processor import OobMessageProcessor from ......messaging.request_context import RequestContext from ......messaging.responder import MockResponder from ......transport.inbound.receipt import MessageReceipt @@ -16,6 +17,13 @@ async def test_called(self): request_context.settings["debug.auto_respond_credential_offer"] = False request_context.connection_record = async_mock.MagicMock() + mock_oob_processor = async_mock.MagicMock( + find_oob_record_for_inbound_message=async_mock.CoroutineMock( + return_value=async_mock.MagicMock() + ) + ) + request_context.injector.bind_instance(OobMessageProcessor, mock_oob_processor) + with async_mock.patch.object( test_module, "CredentialManager", autospec=True ) as mock_cred_mgr: @@ -30,6 +38,9 @@ async def test_called(self): mock_cred_mgr.return_value.receive_offer.assert_called_once_with( request_context.message, request_context.connection_record.connection_id ) + mock_oob_processor.find_oob_record_for_inbound_message.assert_called_once_with( + request_context + ) assert not responder.messages async def test_called_auto_request(self): @@ -39,6 +50,13 @@ async def test_called_auto_request(self): request_context.connection_record = async_mock.MagicMock() request_context.connection_record.my_did = "dummy" + mock_oob_processor = async_mock.MagicMock( + find_oob_record_for_inbound_message=async_mock.CoroutineMock( + return_value=async_mock.MagicMock() + ) + ) + request_context.injector.bind_instance(OobMessageProcessor, mock_oob_processor) + with async_mock.patch.object( test_module, "CredentialManager", autospec=True ) as mock_cred_mgr: @@ -56,6 +74,9 @@ async def test_called_auto_request(self): mock_cred_mgr.return_value.receive_offer.assert_called_once_with( request_context.message, request_context.connection_record.connection_id ) + mock_oob_processor.find_oob_record_for_inbound_message.assert_called_once_with( + request_context + ) messages = responder.messages assert len(messages) == 1 (result, target) = messages[0] @@ -69,6 +90,13 @@ async def test_called_auto_request_x(self): request_context.connection_record = async_mock.MagicMock() request_context.connection_record.my_did = "dummy" + mock_oob_processor = async_mock.MagicMock( + find_oob_record_for_inbound_message=async_mock.CoroutineMock( + return_value=async_mock.MagicMock() + ) + ) + request_context.injector.bind_instance(OobMessageProcessor, mock_oob_processor) + with async_mock.patch.object( test_module, "CredentialManager", autospec=True ) as mock_cred_mgr: @@ -107,7 +135,40 @@ async def test_called_not_ready(self): request_context.connection_ready = False handler = test_module.CredentialOfferHandler() responder = MockResponder() - with self.assertRaises(test_module.HandlerException): + with self.assertRaises(test_module.HandlerException) as err: await handler.handle(request_context, responder) + assert ( + err.exception.message + == "Connection used for credential offer not ready" + ) + + assert not responder.messages + + async def test_no_conn_no_oob(self): + request_context = RequestContext.test_context() + request_context.message_receipt = MessageReceipt() + + mock_oob_processor = async_mock.MagicMock( + find_oob_record_for_inbound_message=async_mock.CoroutineMock( + # No oob record found + return_value=None + ) + ) + request_context.injector.bind_instance(OobMessageProcessor, mock_oob_processor) + + with async_mock.patch.object( + test_module, "CredentialManager", autospec=True + ) as mock_cred_mgr: + mock_cred_mgr.return_value.receive_offer = async_mock.CoroutineMock() + request_context.message = CredentialOffer() + request_context.connection_ready = False + handler = test_module.CredentialOfferHandler() + responder = MockResponder() + with self.assertRaises(test_module.HandlerException) as err: + await handler.handle(request_context, responder) + assert ( + err.exception.message + == "No connection or associated connectionless exchange found for credential offer" + ) assert not responder.messages diff --git a/aries_cloudagent/protocols/issue_credential/v1_0/handlers/tests/test_credential_problem_report_handler.py b/aries_cloudagent/protocols/issue_credential/v1_0/handlers/tests/test_credential_problem_report_handler.py index ebac8cc496..2187e6258c 100644 --- a/aries_cloudagent/protocols/issue_credential/v1_0/handlers/tests/test_credential_problem_report_handler.py +++ b/aries_cloudagent/protocols/issue_credential/v1_0/handlers/tests/test_credential_problem_report_handler.py @@ -21,6 +21,7 @@ async def test_called(self): with async_mock.patch.object( test_module, "CredentialManager", autospec=True ) as mock_cred_mgr: + request_context.connection_ready = True mock_cred_mgr.return_value.receive_problem_report = ( async_mock.CoroutineMock() ) @@ -48,6 +49,7 @@ async def test_called_x(self): with async_mock.patch.object( test_module, "CredentialManager", autospec=True ) as mock_cred_mgr: + request_context.connection_ready = True mock_cred_mgr.return_value.receive_problem_report = ( async_mock.CoroutineMock( side_effect=test_module.StorageError("Disk full") @@ -68,3 +70,48 @@ async def test_called_x(self): request_context.message, request_context.connection_record.connection_id ) assert not responder.messages + + async def test_called_not_ready(self): + request_context = RequestContext.test_context() + request_context.message_receipt = MessageReceipt() + request_context.connection_record = async_mock.MagicMock() + request_context.connection_ready = False + + request_context.message = CredentialProblemReport( + description={ + "en": "Change of plans", + "code": ProblemReportReason.ISSUANCE_ABANDONED.value, + } + ) + handler = test_module.CredentialProblemReportHandler() + responder = MockResponder() + + with self.assertRaises(test_module.HandlerException) as err: + await handler.handle(request_context, responder) + assert ( + err.exception.message + == "Connection used for credential problem report not ready" + ) + + async def test_called_no_connection(self): + request_context = RequestContext.test_context() + request_context.message_receipt = MessageReceipt() + request_context.connection_record = None + + request_context.message = CredentialProblemReport( + description={ + "en": "Change of plans", + "code": ProblemReportReason.ISSUANCE_ABANDONED.value, + } + ) + handler = test_module.CredentialProblemReportHandler() + responder = MockResponder() + + with self.assertRaises(test_module.HandlerException) as err: + await handler.handle(request_context, responder) + assert ( + err.exception.message + == "Connectionless not supported for credential problem report" + ) + + assert not responder.messages diff --git a/aries_cloudagent/protocols/issue_credential/v1_0/handlers/tests/test_credential_proposal_handler.py b/aries_cloudagent/protocols/issue_credential/v1_0/handlers/tests/test_credential_proposal_handler.py index 57949df47f..911f056d43 100644 --- a/aries_cloudagent/protocols/issue_credential/v1_0/handlers/tests/test_credential_proposal_handler.py +++ b/aries_cloudagent/protocols/issue_credential/v1_0/handlers/tests/test_credential_proposal_handler.py @@ -109,7 +109,27 @@ async def test_called_not_ready(self): request_context.connection_ready = False handler = test_module.CredentialProposalHandler() responder = MockResponder() - with self.assertRaises(test_module.HandlerException): + with self.assertRaises(test_module.HandlerException) as err: await handler.handle(request_context, responder) + assert ( + err.exception.message + == "Connection used for credential proposal not ready" + ) + + assert not responder.messages + + async def test_called_no_connection(self): + request_context = RequestContext.test_context() + request_context.message_receipt = MessageReceipt() + + request_context.message = CredentialProposal() + handler = test_module.CredentialProposalHandler() + responder = MockResponder() + with self.assertRaises(test_module.HandlerException) as err: + await handler.handle(request_context, responder) + assert ( + err.exception.message + == "Connectionless not supported for credential proposal" + ) assert not responder.messages diff --git a/aries_cloudagent/protocols/issue_credential/v1_0/handlers/tests/test_credential_request_handler.py b/aries_cloudagent/protocols/issue_credential/v1_0/handlers/tests/test_credential_request_handler.py index bc7b2b8e43..b74bf445ef 100644 --- a/aries_cloudagent/protocols/issue_credential/v1_0/handlers/tests/test_credential_request_handler.py +++ b/aries_cloudagent/protocols/issue_credential/v1_0/handlers/tests/test_credential_request_handler.py @@ -1,5 +1,6 @@ from asynctest import mock as async_mock, TestCase as AsyncTestCase +from ......core.oob_processor import OobMessageProcessor from ......messaging.request_context import RequestContext from ......messaging.responder import MockResponder from ......transport.inbound.receipt import MessageReceipt @@ -19,6 +20,14 @@ async def test_called(self): request_context.message_receipt = MessageReceipt() request_context.connection_record = async_mock.MagicMock() + oob_record = async_mock.MagicMock() + mock_oob_processor = async_mock.MagicMock( + find_oob_record_for_inbound_message=async_mock.CoroutineMock( + return_value=oob_record + ) + ) + request_context.injector.bind_instance(OobMessageProcessor, mock_oob_processor) + with async_mock.patch.object( test_module, "CredentialManager", autospec=True ) as mock_cred_mgr: @@ -34,7 +43,10 @@ async def test_called(self): mock_cred_mgr.assert_called_once_with(request_context.profile) mock_cred_mgr.return_value.receive_request.assert_called_once_with( - request_context.message, request_context.connection_record.connection_id + request_context.message, request_context.connection_record, oob_record + ) + mock_oob_processor.find_oob_record_for_inbound_message.assert_called_once_with( + request_context ) assert not responder.messages @@ -43,6 +55,14 @@ async def test_called_auto_issue(self): request_context.message_receipt = MessageReceipt() request_context.connection_record = async_mock.MagicMock() + oob_record = async_mock.MagicMock() + mock_oob_processor = async_mock.MagicMock( + find_oob_record_for_inbound_message=async_mock.CoroutineMock( + return_value=oob_record + ) + ) + request_context.injector.bind_instance(OobMessageProcessor, mock_oob_processor) + ATTR_DICT = {"test": "123", "hello": "world"} cred_ex_rec = V10CredentialExchange( credential_proposal_dict={ @@ -74,7 +94,10 @@ async def test_called_auto_issue(self): mock_cred_mgr.assert_called_once_with(request_context.profile) mock_cred_mgr.return_value.receive_request.assert_called_once_with( - request_context.message, request_context.connection_record.connection_id + request_context.message, request_context.connection_record, oob_record + ) + mock_oob_processor.find_oob_record_for_inbound_message.assert_called_once_with( + request_context ) messages = responder.messages assert len(messages) == 1 @@ -87,6 +110,14 @@ async def test_called_auto_issue_x(self): request_context.message_receipt = MessageReceipt() request_context.connection_record = async_mock.MagicMock() + oob_record = async_mock.MagicMock() + mock_oob_processor = async_mock.MagicMock( + find_oob_record_for_inbound_message=async_mock.CoroutineMock( + return_value=oob_record + ) + ) + request_context.injector.bind_instance(OobMessageProcessor, mock_oob_processor) + ATTR_DICT = {"test": "123", "hello": "world"} cred_ex_rec = V10CredentialExchange( credential_proposal_dict={ @@ -128,6 +159,14 @@ async def test_called_auto_issue_no_preview(self): request_context.message_receipt = MessageReceipt() request_context.connection_record = async_mock.MagicMock() + oob_record = async_mock.MagicMock() + mock_oob_processor = async_mock.MagicMock( + find_oob_record_for_inbound_message=async_mock.CoroutineMock( + return_value=oob_record + ) + ) + request_context.injector.bind_instance(OobMessageProcessor, mock_oob_processor) + cred_ex_rec = V10CredentialExchange( credential_proposal_dict={"cred_def_id": CD_ID} ) @@ -152,7 +191,10 @@ async def test_called_auto_issue_no_preview(self): mock_cred_mgr.assert_called_once_with(request_context.profile) mock_cred_mgr.return_value.receive_request.assert_called_once_with( - request_context.message, request_context.connection_record.connection_id + request_context.message, request_context.connection_record, oob_record + ) + mock_oob_processor.find_oob_record_for_inbound_message.assert_called_once_with( + request_context ) assert not responder.messages @@ -169,7 +211,39 @@ async def test_called_not_ready(self): request_context.connection_ready = False handler = test_module.CredentialRequestHandler() responder = MockResponder() - with self.assertRaises(test_module.HandlerException): + with self.assertRaises(test_module.HandlerException) as err: + await handler.handle(request_context, responder) + assert ( + err.exception.message + == "Connection used for credential request not ready" + ) + + assert not responder.messages + + async def test_called_no_connection_no_oob(self): + request_context = RequestContext.test_context() + request_context.message_receipt = MessageReceipt() + + mock_oob_processor = async_mock.MagicMock( + find_oob_record_for_inbound_message=async_mock.CoroutineMock( + # No oob record found + return_value=None + ) + ) + request_context.injector.bind_instance(OobMessageProcessor, mock_oob_processor) + + with async_mock.patch.object( + test_module, "CredentialManager", autospec=True + ) as mock_cred_mgr: + mock_cred_mgr.return_value.receive_request = async_mock.CoroutineMock() + request_context.message = CredentialRequest() + handler = test_module.CredentialRequestHandler() + responder = MockResponder() + with self.assertRaises(test_module.HandlerException) as err: await handler.handle(request_context, responder) + assert ( + err.exception.message + == "No connection or associated connectionless exchange found for credential request" + ) assert not responder.messages diff --git a/aries_cloudagent/protocols/issue_credential/v1_0/manager.py b/aries_cloudagent/protocols/issue_credential/v1_0/manager.py index 27c391222f..b2fcc41d2a 100644 --- a/aries_cloudagent/protocols/issue_credential/v1_0/manager.py +++ b/aries_cloudagent/protocols/issue_credential/v1_0/manager.py @@ -4,9 +4,10 @@ import json import logging -from typing import Mapping, Tuple +from typing import Mapping, Optional, Tuple from ....cache.base import BaseCache +from ....connections.models.conn_record import ConnRecord from ....core.error import BaseError from ....core.profile import Profile from ....indy.holder import IndyHolder, IndyHolderError @@ -21,13 +22,14 @@ CRED_DEF_SENT_RECORD_TYPE, ) from ....messaging.responder import BaseResponder +from ....multitenant.base import BaseMultitenantManager from ....revocation.indy import IndyRevocation +from ....revocation.models.issuer_cred_rev_record import IssuerCredRevRecord from ....revocation.models.revocation_registry import RevocationRegistry -from ....revocation.models.issuer_rev_reg_record import IssuerRevRegRecord -from ....revocation.util import notify_revocation_reg_event from ....storage.base import BaseStorage from ....storage.error import StorageError, StorageNotFoundError +from ...out_of_band.v1_0.models.oob_record import OobRecord from .messages.credential_ack import CredentialAck from .messages.credential_issue import CredentialIssue from .messages.credential_offer import CredentialOffer @@ -266,7 +268,11 @@ async def _create(cred_def_id): credential_preview = credential_proposal_message.credential_proposal # vet attributes - ledger_exec_inst = self._profile.inject(IndyLedgerRequestsExecutor) + multitenant_mgr = self.profile.inject_or(BaseMultitenantManager) + if multitenant_mgr: + ledger_exec_inst = IndyLedgerRequestsExecutor(self.profile) + else: + ledger_exec_inst = self.profile.inject(IndyLedgerRequestsExecutor) ledger = ( await ledger_exec_inst.get_ledger_for_identifier( cred_def_id, @@ -303,7 +309,7 @@ async def _create(cred_def_id): offers_attach=[CredentialOffer.wrap_indy_offer(credential_offer)], ) - credential_offer_message._thread = {"thid": cred_ex_record.thread_id} + credential_offer_message._thread = {"thid": credential_offer_message._thread_id} credential_offer_message.assign_trace_decorator( self._profile.settings, cred_ex_record.trace ) @@ -325,7 +331,7 @@ async def _create(cred_def_id): return (cred_ex_record, credential_offer_message) async def receive_offer( - self, message: CredentialOffer, connection_id: str + self, message: CredentialOffer, connection_id: Optional[str] ) -> V10CredentialExchange: """ Receive a credential offer. @@ -346,35 +352,49 @@ async def receive_offer( cred_def_id=cred_def_id, ) - async with self._profile.session() as session: + async with self._profile.transaction() as txn: # Get credential exchange record (holder sent proposal first) # or create it (issuer sent offer first) try: - cred_ex_record = await ( - V10CredentialExchange.retrieve_by_connection_and_thread( - session, connection_id, message._thread_id + cred_ex_record = ( + await ( + V10CredentialExchange.retrieve_by_connection_and_thread( + txn, + connection_id, + message._thread_id, + role=V10CredentialExchange.ROLE_HOLDER, + for_update=True, + ) ) ) - cred_ex_record.credential_proposal_dict = credential_proposal_dict except StorageNotFoundError: # issuer sent this offer free of any proposal cred_ex_record = V10CredentialExchange( connection_id=connection_id, thread_id=message._thread_id, initiator=V10CredentialExchange.INITIATOR_EXTERNAL, role=V10CredentialExchange.ROLE_HOLDER, - credential_proposal_dict=credential_proposal_dict, auto_remove=not self._profile.settings.get( "preserve_exchange_records" ), trace=(message._trace is not None), ) + else: + if cred_ex_record.state != V10CredentialExchange.STATE_PROPOSAL_SENT: + raise CredentialManagerError( + f"Credential exchange {cred_ex_record.credential_exchange_id} " + f"in {cred_ex_record.state} state " + f"(must be {V10CredentialExchange.STATE_PROPOSAL_SENT})" + ) + cred_ex_record.credential_proposal_dict = credential_proposal_dict + cred_ex_record.credential_offer_dict = message cred_ex_record.credential_offer = indy_offer cred_ex_record.state = V10CredentialExchange.STATE_OFFER_RECEIVED cred_ex_record.schema_id = schema_id cred_ex_record.credential_definition_id = cred_def_id - await cred_ex_record.save(session, reason="receive credential offer") + await cred_ex_record.save(txn, reason="receive credential offer") + await txn.commit() return cred_ex_record @@ -393,18 +413,17 @@ async def create_request( A tuple (credential exchange record, credential request message) """ - if cred_ex_record.state != V10CredentialExchange.STATE_OFFER_RECEIVED: - raise CredentialManagerError( - f"Credential exchange {cred_ex_record.credential_exchange_id} " - f"in {cred_ex_record.state} state " - f"(must be {V10CredentialExchange.STATE_OFFER_RECEIVED})" - ) - credential_definition_id = cred_ex_record.credential_definition_id cred_offer_ser = cred_ex_record._credential_offer.ser + cred_req_ser = None + cred_req_meta = None async def _create(): - ledger_exec_inst = self._profile.inject(IndyLedgerRequestsExecutor) + multitenant_mgr = self.profile.inject_or(BaseMultitenantManager) + if multitenant_mgr: + ledger_exec_inst = IndyLedgerRequestsExecutor(self.profile) + else: + ledger_exec_inst = self.profile.inject(IndyLedgerRequestsExecutor) ledger = ( await ledger_exec_inst.get_ledger_for_identifier( credential_definition_id, @@ -427,12 +446,14 @@ async def _create(): "metadata": json.loads(metadata_json), } - if cred_ex_record.credential_request: + if cred_ex_record.state == V10CredentialExchange.STATE_REQUEST_SENT: LOGGER.warning( "create_request called multiple times for v1.0 credential exchange: %s", cred_ex_record.credential_exchange_id, ) - else: + cred_req_ser = cred_ex_record._credential_request.ser + cred_req_meta = cred_ex_record.credential_request_metadata + elif cred_ex_record.state == V10CredentialExchange.STATE_OFFER_RECEIVED: nonce = cred_offer_ser["nonce"] cache_key = ( f"credential_request::{credential_definition_id}::{holder_did}::{nonce}" @@ -448,31 +469,51 @@ async def _create(): await entry.set_result(cred_req_result, 3600) if not cred_req_result: cred_req_result = await _create() + cred_req_ser = cred_req_result["request"] + cred_req_meta = cred_req_result["metadata"] - ( - cred_ex_record.credential_request, - cred_ex_record.credential_request_metadata, - ) = (cred_req_result["request"], cred_req_result["metadata"]) + async with self._profile.transaction() as txn: + cred_ex_record = await V10CredentialExchange.retrieve_by_id( + txn, cred_ex_record.credential_exchange_id, for_update=True + ) + if cred_ex_record.state != V10CredentialExchange.STATE_OFFER_RECEIVED: + raise CredentialManagerError( + f"Credential exchange {cred_ex_record.credential_exchange_id} " + f"in {cred_ex_record.state} state " + f"(must be {V10CredentialExchange.STATE_OFFER_RECEIVED})" + ) + + cred_ex_record.credential_request = cred_req_ser + cred_ex_record.credential_request_metadata = cred_req_meta + cred_ex_record.state = V10CredentialExchange.STATE_REQUEST_SENT + await cred_ex_record.save(txn, reason="create credential request") + await txn.commit() + else: + raise CredentialManagerError( + f"Credential exchange {cred_ex_record.credential_exchange_id} " + f"in {cred_ex_record.state} state " + f"(must be {V10CredentialExchange.STATE_OFFER_RECEIVED})" + ) credential_request_message = CredentialRequest( - requests_attach=[ - CredentialRequest.wrap_indy_cred_req( - cred_ex_record._credential_request.ser - ) - ] + requests_attach=[CredentialRequest.wrap_indy_cred_req(cred_req_ser)] + ) + # Assign thid (and optionally pthid) to message + credential_request_message.assign_thread_from( + cred_ex_record.credential_offer_dict ) - credential_request_message._thread = {"thid": cred_ex_record.thread_id} credential_request_message.assign_trace_decorator( self._profile.settings, cred_ex_record.trace ) - cred_ex_record.state = V10CredentialExchange.STATE_REQUEST_SENT - async with self._profile.session() as session: - await cred_ex_record.save(session, reason="create credential request") - return (cred_ex_record, credential_request_message) - async def receive_request(self, message: CredentialRequest, connection_id: str): + async def receive_request( + self, + message: CredentialRequest, + connection_record: Optional[ConnRecord], + oob_record: Optional[OobRecord], + ): """ Receive a credential request. @@ -486,28 +527,43 @@ async def receive_request(self, message: CredentialRequest, connection_id: str): assert len(message.requests_attach or []) == 1 credential_request = message.indy_cred_req(0) - async with self._profile.session() as session: + # connection_id is None in the record if this is in response to + # an request~attach from an OOB message. If so, we do not want to filter + # the record by connection_id. + connection_id = None if oob_record else connection_record.connection_id + + async with self._profile.transaction() as txn: try: - cred_ex_record = await ( - V10CredentialExchange.retrieve_by_connection_and_thread( - session, connection_id, message._thread_id + cred_ex_record = ( + await ( + V10CredentialExchange.retrieve_by_connection_and_thread( + txn, + connection_id, + message._thread_id, + role=V10CredentialExchange.ROLE_ISSUER, + for_update=True, + ) ) ) except StorageNotFoundError: - try: - cred_ex_record = await V10CredentialExchange.retrieve_by_tag_filter( - session, - {"thread_id": message._thread_id}, - {"connection_id": None}, - ) - cred_ex_record.connection_id = connection_id - except StorageNotFoundError: - raise CredentialManagerError( - "Indy issue credential format can't start from credential request" - ) + raise CredentialManagerError( + "Indy issue credential format can't start from credential request" + ) from None + if cred_ex_record.state != V10CredentialExchange.STATE_OFFER_SENT: + LOGGER.error( + "Skipping credential request; exchange state is %s (id=%s)", + cred_ex_record.state, + cred_ex_record.credential_exchange_id, + ) + return None + + if connection_record: + cred_ex_record.connection_id = connection_record.connection_id + cred_ex_record.credential_request = credential_request cred_ex_record.state = V10CredentialExchange.STATE_REQUEST_RECEIVED - await cred_ex_record.save(session, reason="receive credential request") + await cred_ex_record.save(txn, reason="receive credential request") + await txn.commit() return cred_ex_record @@ -531,26 +587,39 @@ async def issue_credential( """ - if cred_ex_record.state != V10CredentialExchange.STATE_REQUEST_RECEIVED: + credential_ser = None + + if cred_ex_record.credential: + LOGGER.warning( + "issue_credential called multiple times for v1.0 credential exchange %s", + cred_ex_record.credential_exchange_id, + ) + credential_ser = cred_ex_record._credential.ser + + elif cred_ex_record.state != V10CredentialExchange.STATE_REQUEST_RECEIVED: raise CredentialManagerError( f"Credential exchange {cred_ex_record.credential_exchange_id} " f"in {cred_ex_record.state} state " f"(must be {V10CredentialExchange.STATE_REQUEST_RECEIVED})" ) - schema_id = cred_ex_record.schema_id - rev_reg = None - - if cred_ex_record.credential: - LOGGER.warning( - "issue_credential called multiple times for " - + "credential exchange record %s - abstaining", - cred_ex_record.credential_exchange_id, - ) else: cred_offer_ser = cred_ex_record._credential_offer.ser cred_req_ser = cred_ex_record._credential_request.ser - ledger_exec_inst = self._profile.inject(IndyLedgerRequestsExecutor) + cred_values = ( + cred_ex_record.credential_proposal_dict.credential_proposal.attr_dict( + decode=False + ) + ) + schema_id = cred_ex_record.schema_id + cred_def_id = cred_ex_record.credential_definition_id + + issuer = self.profile.inject(IndyIssuer) + multitenant_mgr = self.profile.inject_or(BaseMultitenantManager) + if multitenant_mgr: + ledger_exec_inst = IndyLedgerRequestsExecutor(self.profile) + else: + ledger_exec_inst = self.profile.inject(IndyLedgerRequestsExecutor) ledger = ( await ledger_exec_inst.get_ledger_for_identifier( schema_id, @@ -562,143 +631,95 @@ async def issue_credential( credential_definition = await ledger.get_credential_definition( cred_ex_record.credential_definition_id ) + revocable = credential_definition["value"].get("revocation") - tails_path = None - if credential_definition["value"].get("revocation"): - revoc = IndyRevocation(self._profile) - try: - active_rev_reg_rec = await revoc.get_active_issuer_rev_reg_record( - cred_ex_record.credential_definition_id + for attempt in range(max(retries, 1)): + if attempt > 0: + LOGGER.info( + "Waiting 2s before retrying credential issuance " + "for cred def '%s'", + cred_def_id, ) - rev_reg = await active_rev_reg_rec.get_registry() - cred_ex_record.revoc_reg_id = active_rev_reg_rec.revoc_reg_id + await asyncio.sleep(2) + if revocable: + revoc = IndyRevocation(self._profile) + registry_info = await revoc.get_or_create_active_registry( + cred_def_id + ) + if not registry_info: + continue + del revoc + issuer_rev_reg, rev_reg = registry_info + rev_reg_id = issuer_rev_reg.revoc_reg_id tails_path = rev_reg.tails_local_path - await rev_reg.get_or_fetch_local_tails_path() - - except StorageNotFoundError: - async with self._profile.session() as session: - posted_rev_reg_recs = ( - await IssuerRevRegRecord.query_by_cred_def_id( - session, - cred_ex_record.credential_definition_id, - state=IssuerRevRegRecord.STATE_POSTED, - ) - ) - if not posted_rev_reg_recs: - # Send next 2 rev regs, publish tails files in background - async with self._profile.session() as session: - old_rev_reg_recs = sorted( - await IssuerRevRegRecord.query_by_cred_def_id( - session, - cred_ex_record.credential_definition_id, - ) - ) # prefer to reuse prior rev reg size - cred_def_id = cred_ex_record.credential_definition_id - rev_reg_size = ( - old_rev_reg_recs[0].max_cred_num - if old_rev_reg_recs - else None - ) - for _ in range(2): - await notify_revocation_reg_event( - self.profile, - cred_def_id, - rev_reg_size, - auto_create_rev_reg=True, - ) - - if retries > 0: - LOGGER.info( - "Waiting 2s on posted rev reg for cred def %s, retrying", - cred_ex_record.credential_definition_id, - ) - await asyncio.sleep(2) - return await self.issue_credential( - cred_ex_record=cred_ex_record, - comment=comment, - retries=retries - 1, - ) + else: + rev_reg_id = None + tails_path = None - raise CredentialManagerError( - f"Cred def id {cred_ex_record.credential_definition_id} " - "has no active revocation registry" + try: + (credential_json, cred_rev_id) = await issuer.create_credential( + schema, + cred_offer_ser, + cred_req_ser, + cred_values, + rev_reg_id, + tails_path, ) - del revoc - - credential_values = ( - cred_ex_record.credential_proposal_dict.credential_proposal.attr_dict( - decode=False - ) - ) - issuer = self._profile.inject(IndyIssuer) - try: - ( - credential_json, - cred_ex_record.revocation_id, - ) = await issuer.create_credential( - schema, - cred_offer_ser, - cred_req_ser, - credential_values, - cred_ex_record.credential_exchange_id, - cred_ex_record.revoc_reg_id, - tails_path, - ) - - # If the rev reg is now full - if rev_reg and rev_reg.max_creds == int(cred_ex_record.revocation_id): - async with self._profile.session() as session: - await active_rev_reg_rec.set_state( - session, - IssuerRevRegRecord.STATE_FULL, - ) - - # Send next 1 rev reg, publish tails file in background - cred_def_id = cred_ex_record.credential_definition_id - rev_reg_size = active_rev_reg_rec.max_cred_num - await notify_revocation_reg_event( - self.profile, - cred_def_id, - rev_reg_size, - auto_create_rev_reg=True, + except IndyIssuerRevocationRegistryFullError: + # unlucky, another instance filled the registry first + continue + + if revocable and rev_reg.max_creds <= int(cred_rev_id): + revoc = IndyRevocation(self._profile) + await revoc.handle_full_registry(rev_reg_id) + del revoc + + credential_ser = json.loads(credential_json) + break + + if not credential_ser: + raise CredentialManagerError( + f"Cred def id {cred_ex_record.credential_definition_id} " + "has no active revocation registry" + ) from None + + async with self._profile.transaction() as txn: + if revocable and cred_rev_id: + issuer_cr_rec = IssuerCredRevRecord( + state=IssuerCredRevRecord.STATE_ISSUED, + cred_ex_id=cred_ex_record.credential_exchange_id, + cred_ex_version=IssuerCredRevRecord.VERSION_1, + rev_reg_id=rev_reg_id, + cred_rev_id=cred_rev_id, ) - - except IndyIssuerRevocationRegistryFullError: - # unlucky: duelling instance issued last cred near same time as us - async with self._profile.session() as session: - await active_rev_reg_rec.set_state( - session, - IssuerRevRegRecord.STATE_FULL, + await issuer_cr_rec.save( + txn, + reason=( + "Created issuer cred rev record for " + f"rev reg id {rev_reg_id}, index {cred_rev_id}" + ), ) - if retries > 0: - # use next rev reg; at worst, lucky instance is putting one up - LOGGER.info( - "Waiting 1s and retrying: revocation registry %s is full", - active_rev_reg_rec.revoc_reg_id, - ) - await asyncio.sleep(1) - return await self.issue_credential( - cred_ex_record=cred_ex_record, - comment=comment, - retries=retries - 1, + cred_ex_record = await V10CredentialExchange.retrieve_by_id( + txn, cred_ex_record.credential_exchange_id, for_update=True + ) + if cred_ex_record.state != V10CredentialExchange.STATE_REQUEST_RECEIVED: + raise CredentialManagerError( + f"Credential exchange {cred_ex_record.credential_exchange_id} " + f"in {cred_ex_record.state} state " + f"(must be {V10CredentialExchange.STATE_REQUEST_RECEIVED})" ) - - raise - - cred_ex_record.credential = json.loads(credential_json) - - cred_ex_record.state = V10CredentialExchange.STATE_ISSUED - async with self._profile.session() as session: - # FIXME - re-fetch record to check state, apply transactional update - await cred_ex_record.save(session, reason="issue credential") + cred_ex_record.state = V10CredentialExchange.STATE_ISSUED + cred_ex_record.credential = credential_ser + cred_ex_record.revoc_reg_id = rev_reg_id + cred_ex_record.revocation_id = cred_rev_id + await cred_ex_record.save(txn, reason="issue credential") + await txn.commit() credential_message = CredentialIssue( comment=comment, - credentials_attach=[ - CredentialIssue.wrap_indy_credential(cred_ex_record._credential.ser) - ], + credentials_attach=[CredentialIssue.wrap_indy_credential(credential_ser)], ) credential_message._thread = {"thid": cred_ex_record.thread_id} credential_message.assign_trace_decorator( @@ -708,7 +729,7 @@ async def issue_credential( return (cred_ex_record, credential_message) async def receive_credential( - self, message: CredentialIssue, connection_id: str + self, message: CredentialIssue, connection_id: Optional[str] ) -> V10CredentialExchange: """ Receive a credential from an issuer. @@ -722,20 +743,35 @@ async def receive_credential( assert len(message.credentials_attach or []) == 1 raw_credential = message.indy_credential(0) - # FIXME use transaction, fetch for_update - async with self._profile.session() as session: - cred_ex_record = await ( - V10CredentialExchange.retrieve_by_connection_and_thread( - session, - connection_id, - message._thread_id, + async with self._profile.transaction() as txn: + try: + cred_ex_record = ( + await ( + V10CredentialExchange.retrieve_by_connection_and_thread( + txn, + connection_id, + message._thread_id, + role=V10CredentialExchange.ROLE_HOLDER, + for_update=True, + ) + ) + ) + except StorageNotFoundError: + raise CredentialManagerError( + "No credential exchange record found for received credential" + ) from None + if cred_ex_record.state != V10CredentialExchange.STATE_REQUEST_SENT: + raise CredentialManagerError( + f"Credential exchange {cred_ex_record.credential_exchange_id} " + f"in {cred_ex_record.state} state " + f"(must be {V10CredentialExchange.STATE_REQUEST_SENT})" ) - ) - cred_ex_record.raw_credential = raw_credential cred_ex_record.state = V10CredentialExchange.STATE_CREDENTIAL_RECEIVED - await cred_ex_record.save(session, reason="receive credential") + await cred_ex_record.save(txn, reason="receive credential") + await txn.commit() + return cred_ex_record async def store_credential( @@ -753,7 +789,7 @@ async def store_credential( Updated credential exchange record """ - if cred_ex_record.state != (V10CredentialExchange.STATE_CREDENTIAL_RECEIVED): + if cred_ex_record.state != V10CredentialExchange.STATE_CREDENTIAL_RECEIVED: raise CredentialManagerError( f"Credential exchange {cred_ex_record.credential_exchange_id} " f"in {cred_ex_record.state} state " @@ -762,7 +798,11 @@ async def store_credential( raw_cred_serde = cred_ex_record._raw_credential revoc_reg_def = None - ledger_exec_inst = self._profile.inject(IndyLedgerRequestsExecutor) + multitenant_mgr = self.profile.inject_or(BaseMultitenantManager) + if multitenant_mgr: + ledger_exec_inst = IndyLedgerRequestsExecutor(self.profile) + else: + ledger_exec_inst = self.profile.inject(IndyLedgerRequestsExecutor) ledger = ( await ledger_exec_inst.get_ledger_for_identifier( raw_cred_serde.de.cred_def_id, @@ -802,34 +842,44 @@ async def store_credential( rev_reg_def=revoc_reg_def, ) except IndyHolderError as e: - LOGGER.error(f"Error storing credential. {e.error_code}: {e.message}") + LOGGER.error("Error storing credential: %s: %s", e.error_code, e.message) raise e credential_json = await holder.get_credential(credential_id) credential = json.loads(credential_json) - cred_ex_record.credential_id = credential_id - cred_ex_record.credential = credential - cred_ex_record.revoc_reg_id = credential.get("rev_reg_id", None) - cred_ex_record.revocation_id = credential.get("cred_rev_id", None) + async with self._profile.transaction() as txn: + cred_ex_record = await V10CredentialExchange.retrieve_by_id( + txn, cred_ex_record.credential_exchange_id, for_update=True + ) + if cred_ex_record.state != V10CredentialExchange.STATE_CREDENTIAL_RECEIVED: + raise CredentialManagerError( + f"Credential exchange {cred_ex_record.credential_exchange_id} " + f"in {cred_ex_record.state} state " + f"(must be {V10CredentialExchange.STATE_CREDENTIAL_RECEIVED})" + ) - async with self._profile.session() as session: - # FIXME - re-fetch record to check state, apply transactional update - await cred_ex_record.save(session, reason="store credential") + cred_ex_record.credential_id = credential_id + cred_ex_record.credential = credential + cred_ex_record.revoc_reg_id = credential.get("rev_reg_id", None) + cred_ex_record.revocation_id = credential.get("cred_rev_id", None) + await cred_ex_record.save(txn, reason="store credential") + await txn.commit() return cred_ex_record async def send_credential_ack( self, cred_ex_record: V10CredentialExchange, - ): + ) -> Tuple[V10CredentialExchange, CredentialAck]: """ Create, send, and return ack message for input credential exchange record. Delete credential exchange record if set to auto-remove. Returns: - Tuple: cred ex record, credential ack message for tracing. + a tuple of the updated credential exchange record + and the credential ack message for tracing """ credential_ack_message = CredentialAck() @@ -840,17 +890,42 @@ async def send_credential_ack( self._profile.settings, cred_ex_record.trace ) - cred_ex_record.state = V10CredentialExchange.STATE_ACKED try: - async with self._profile.session() as session: - # FIXME - re-fetch record to check state, apply transactional update - await cred_ex_record.save(session, reason="ack credential") + async with self._profile.transaction() as txn: + try: + cred_ex_record = await V10CredentialExchange.retrieve_by_id( + txn, cred_ex_record.credential_exchange_id, for_update=True + ) + except StorageNotFoundError: + LOGGER.warning( + "Skipping credential exchange ack, record not found: '%s'", + cred_ex_record.credential_exchange_id, + ) + return (cred_ex_record, None) + + if ( + cred_ex_record.state + != V10CredentialExchange.STATE_CREDENTIAL_RECEIVED + ): + LOGGER.warning( + "Skipping credential exchange ack, state is '%s' for record '%s'", + cred_ex_record.state, + cred_ex_record.credential_exchange_id, + ) + return (cred_ex_record, None) + + cred_ex_record.state = V10CredentialExchange.STATE_ACKED + await cred_ex_record.save(txn, reason="ack credential") + await txn.commit() - if cred_ex_record.auto_remove: + if cred_ex_record.auto_remove: + async with self._profile.session() as session: await cred_ex_record.delete_record(session) # all done: delete - except StorageError as err: - LOGGER.exception(err) # holder still owes an ack: carry on + except StorageError: + LOGGER.exception( + "Error updating credential exchange" + ) # holder still owes an ack: carry on responder = self._profile.inject_or(BaseResponder) if responder: @@ -864,11 +939,11 @@ async def send_credential_ack( cred_ex_record.thread_id, ) - return cred_ex_record, credential_ack_message + return (cred_ex_record, credential_ack_message) async def receive_credential_ack( - self, message: CredentialAck, connection_id: str - ) -> V10CredentialExchange: + self, message: CredentialAck, connection_id: Optional[str] + ) -> Optional[V10CredentialExchange]: """ Receive credential ack from holder. @@ -876,18 +951,31 @@ async def receive_credential_ack( credential exchange record, retrieved and updated """ - # FIXME use transaction, fetch for_update - async with self._profile.session() as session: - cred_ex_record = await ( - V10CredentialExchange.retrieve_by_connection_and_thread( - session, - connection_id, + async with self._profile.transaction() as txn: + try: + cred_ex_record = ( + await ( + V10CredentialExchange.retrieve_by_connection_and_thread( + txn, + connection_id, + message._thread_id, + role=V10CredentialExchange.ROLE_ISSUER, + for_update=True, + ) + ) + ) + except StorageNotFoundError: + LOGGER.warning( + "Skip ack message on credential exchange, record not found %s", message._thread_id, ) - ) + return None + if cred_ex_record.state == V10CredentialExchange.STATE_ACKED: + return None cred_ex_record.state = V10CredentialExchange.STATE_ACKED - await cred_ex_record.save(session, reason="credential acked") + await cred_ex_record.save(txn, reason="credential acked") + await txn.commit() if cred_ex_record.auto_remove: async with self._profile.session() as session: @@ -905,22 +993,29 @@ async def receive_problem_report( credential exchange record, retrieved and updated """ - # FIXME use transaction, fetch for_update - async with self._profile.session() as session: - cred_ex_record = await ( - V10CredentialExchange.retrieve_by_connection_and_thread( - session, - connection_id, + async with self._profile.transaction() as txn: + try: + cred_ex_record = ( + await ( + V10CredentialExchange.retrieve_by_connection_and_thread( + txn, connection_id, message._thread_id, for_update=True + ) + ) + ) + except StorageNotFoundError: + LOGGER.warning( + "Skip problem report on credential exchange, record not found %s", message._thread_id, ) - ) + return None - cred_ex_record.state = None + cred_ex_record.state = V10CredentialExchange.STATE_ABANDONED code = message.description.get( "code", ProblemReportReason.ISSUANCE_ABANDONED.value, ) cred_ex_record.error_msg = f"{code}: {message.description.get('en', code)}" - await cred_ex_record.save(session, reason="received problem report") + await cred_ex_record.save(txn, reason="received problem report") + await txn.commit() return cred_ex_record diff --git a/aries_cloudagent/protocols/issue_credential/v1_0/messages/credential_exchange_webhook.py b/aries_cloudagent/protocols/issue_credential/v1_0/messages/credential_exchange_webhook.py new file mode 100644 index 0000000000..52641ff8c7 --- /dev/null +++ b/aries_cloudagent/protocols/issue_credential/v1_0/messages/credential_exchange_webhook.py @@ -0,0 +1,49 @@ +"""v1.0 credential exchange webhook.""" + + +class V10CredentialExchangeWebhook: + """Class representing a state only credential exchange webhook.""" + + __acceptable_keys_list = [ + "connection_id", + "credential_exchange_id", + "cred_ex_id", + "cred_def_id", + "role", + "initiator", + "revoc_reg_id", + "revocation_id", + "auto_offer", + "auto_issue", + "auto_remove", + "error_msg", + "thread_id", + "parent_thread_id", + "state", + "credential_definition_id", + "schema_id", + "credential_id", + "trace", + "public_did", + "cred_id_stored", + "conn_id", + "created_at", + "updated_at", + ] + + def __init__( + self, + **kwargs, + ): + """ + Initialize webhook object from V10CredentialExchange. + + from a list of accepted attributes. + """ + [ + self.__setattr__(key, kwargs.get(key)) + for key in self.__acceptable_keys_list + if kwargs.get(key) is not None + ] + if kwargs.get("_id") is not None: + self.credential_exchange_id = kwargs.get("_id") diff --git a/aries_cloudagent/protocols/issue_credential/v1_0/models/credential_exchange.py b/aries_cloudagent/protocols/issue_credential/v1_0/models/credential_exchange.py index 129cfeb745..f12a82f258 100644 --- a/aries_cloudagent/protocols/issue_credential/v1_0/models/credential_exchange.py +++ b/aries_cloudagent/protocols/issue_credential/v1_0/models/credential_exchange.py @@ -2,7 +2,7 @@ import logging -from typing import Any, Mapping, Union +from typing import Any, Mapping, Optional, Union from marshmallow import fields, validate @@ -17,6 +17,9 @@ from ..messages.credential_proposal import CredentialProposal, CredentialProposalSchema from ..messages.credential_offer import CredentialOffer, CredentialOfferSchema +from ..messages.credential_exchange_webhook import ( + V10CredentialExchangeWebhook, +) from . import UNENCRYPTED_TAGS @@ -51,12 +54,13 @@ class Meta: STATE_CREDENTIAL_RECEIVED = "credential_received" STATE_ACKED = "credential_acked" STATE_CREDENTIAL_REVOKED = "credential_revoked" + STATE_ABANDONED = "abandoned" def __init__( self, *, credential_exchange_id: str = None, - connection_id: str = None, + connection_id: Optional[str] = None, thread_id: str = None, parent_thread_id: str = None, initiator: str = None, @@ -188,6 +192,7 @@ async def save_error_state( self, session: ProfileSession, *, + state: str = None, reason: str = None, log_params: Mapping[str, Any] = None, log_override: bool = False, @@ -202,10 +207,10 @@ async def save_error_state( override: Override configured logging regimen, print to stderr instead """ - if self._last_state is None: # already done + if self._last_state == state: # already done return - self.state = None + self.state = state or V10CredentialExchange.STATE_ABANDONED if reason: self.error_msg = reason @@ -216,8 +221,35 @@ async def save_error_state( log_params=log_params, log_override=log_override, ) - except StorageError as err: - LOGGER.exception(err) + except StorageError: + LOGGER.exception("Error saving credential exchange error state") + + # Override + async def emit_event(self, session: ProfileSession, payload: Any = None): + """ + Emit an event. + + Args: + session: The profile session to use + payload: The event payload + """ + + if not self.RECORD_TOPIC: + return + + if self.state: + topic = f"{self.EVENT_NAMESPACE}::{self.RECORD_TOPIC}::{self.state}" + else: + topic = f"{self.EVENT_NAMESPACE}::{self.RECORD_TOPIC}" + + if session.profile.settings.get("debug.webhooks"): + if not payload: + payload = self.serialize() + else: + payload = V10CredentialExchangeWebhook(**self.__dict__) + payload = payload.__dict__ + + await session.profile.notify(topic, payload) @property def record_value(self) -> dict: @@ -260,18 +292,30 @@ def record_value(self) -> dict: @classmethod async def retrieve_by_connection_and_thread( - cls, session: ProfileSession, connection_id: str, thread_id: str + cls, + session: ProfileSession, + connection_id: Optional[str], + thread_id: str, + role: Optional[str] = None, + *, + for_update=False, ) -> "V10CredentialExchange": """Retrieve a credential exchange record by connection and thread ID.""" - cache_key = f"credential_exchange_ctidx::{connection_id}::{thread_id}" + cache_key = f"credential_exchange_ctidx::{connection_id}::{thread_id}::{role}" record_id = await cls.get_cached_key(session, cache_key) if record_id: - record = await cls.retrieve_by_id(session, record_id) + record = await cls.retrieve_by_id(session, record_id, for_update=for_update) else: + post_filter = {} + if role: + post_filter["role"] = role + if connection_id: + post_filter["connection_id"] = connection_id record = await cls.retrieve_by_tag_filter( session, {"thread_id": thread_id}, - {"connection_id": connection_id} if connection_id else None, + post_filter, + for_update=for_update, ) await cls.set_cached_key(session, cache_key, record.credential_exchange_id) return record diff --git a/aries_cloudagent/protocols/issue_credential/v1_0/routes.py b/aries_cloudagent/protocols/issue_credential/v1_0/routes.py index fd8c7b3f59..3cba79cb88 100644 --- a/aries_cloudagent/protocols/issue_credential/v1_0/routes.py +++ b/aries_cloudagent/protocols/issue_credential/v1_0/routes.py @@ -11,6 +11,8 @@ from json.decoder import JSONDecodeError from marshmallow import fields, validate +from ...out_of_band.v1_0.models.oob_record import OobRecord +from ....wallet.util import default_did_from_verkey from ....admin.request_context import AdminRequestContext from ....connections.models.conn_record import ConnRecord from ....core.profile import Profile @@ -380,7 +382,10 @@ async def credential_exchange_retrieve(request: web.BaseRequest): @docs( tags=["issue-credential v1.0"], - summary="Send holder a credential, automating entire flow", + summary=( + "Create a credential record without " + "sending (generally for use with Out-Of-Band)" + ), ) @request_schema(V10CredentialCreateSchema()) @response_schema(V10CredentialExchangeSchema(), 200, description="") @@ -972,30 +977,48 @@ async def credential_exchange_send_request(request: web.BaseRequest): cred_ex_record = None connection_record = None - try: - async with profile.session() as session: + + async with profile.session() as session: + try: + cred_ex_record = await V10CredentialExchange.retrieve_by_id( + session, credential_exchange_id + ) + except StorageNotFoundError as err: + raise web.HTTPNotFound(reason=err.roll_up) from err + + # Fetch connection if exchange has record + connection_record = None + if cred_ex_record.connection_id: try: - cred_ex_record = await V10CredentialExchange.retrieve_by_id( - session, credential_exchange_id + connection_record = await ConnRecord.retrieve_by_id( + session, cred_ex_record.connection_id ) - connection_id = cred_ex_record.connection_id except StorageNotFoundError as err: - raise web.HTTPNotFound(reason=err.roll_up) from err + raise web.HTTPBadRequest(reason=err.roll_up) from err - connection_record = await ConnRecord.retrieve_by_id( + if connection_record and not connection_record.is_ready: + raise web.HTTPForbidden( + reason=f"Connection {connection_record.connection_id} not ready" + ) + + if connection_record: + holder_did = connection_record.my_did + else: + # Need to get the holder DID from the out of band record + async with profile.session() as session: + oob_record = await OobRecord.retrieve_by_tag_filter( session, - connection_id, + {"invi_msg_id": cred_ex_record.credential_offer_dict._thread.pthid}, ) - if not connection_record.is_ready: - raise web.HTTPForbidden(reason=f"Connection {connection_id} not ready") + # Transform recipient key into did + holder_did = default_did_from_verkey(oob_record.our_recipient_key) + try: credential_manager = CredentialManager(profile) ( cred_ex_record, credential_request_message, - ) = await credential_manager.create_request( - cred_ex_record, connection_record.my_did - ) + ) = await credential_manager.create_request(cred_ex_record, holder_did) result = cred_ex_record.serialize() @@ -1017,7 +1040,9 @@ async def credential_exchange_send_request(request: web.BaseRequest): outbound_handler, ) - await outbound_handler(credential_request_message, connection_id=connection_id) + await outbound_handler( + credential_request_message, connection_id=cred_ex_record.connection_id + ) trace_event( context.settings, @@ -1060,20 +1085,31 @@ async def credential_exchange_issue(request: web.BaseRequest): cred_ex_record = None connection_record = None - try: - async with profile.session() as session: + + async with profile.session() as session: + try: + cred_ex_record = await V10CredentialExchange.retrieve_by_id( + session, credential_exchange_id + ) + except StorageNotFoundError as err: + raise web.HTTPNotFound(reason=err.roll_up) from err + + # Fetch connection if exchange has record + connection_record = None + if cred_ex_record.connection_id: try: - cred_ex_record = await V10CredentialExchange.retrieve_by_id( - session, credential_exchange_id + connection_record = await ConnRecord.retrieve_by_id( + session, cred_ex_record.connection_id ) except StorageNotFoundError as err: - raise web.HTTPNotFound(reason=err.roll_up) from err - connection_id = cred_ex_record.connection_id + raise web.HTTPBadRequest(reason=err.roll_up) from err - connection_record = await ConnRecord.retrieve_by_id(session, connection_id) - if not connection_record.is_ready: - raise web.HTTPForbidden(reason=f"Connection {connection_id} not ready") + if connection_record and not connection_record.is_ready: + raise web.HTTPForbidden( + reason=f"Connection {connection_record.connection_id} not ready" + ) + try: credential_manager = CredentialManager(profile) ( cred_ex_record, @@ -1100,7 +1136,9 @@ async def credential_exchange_issue(request: web.BaseRequest): outbound_handler, ) - await outbound_handler(credential_issue_message, connection_id=connection_id) + await outbound_handler( + credential_issue_message, connection_id=cred_ex_record.connection_id + ) trace_event( context.settings, @@ -1146,20 +1184,30 @@ async def credential_exchange_store(request: web.BaseRequest): cred_ex_record = None connection_record = None - try: - async with profile.session() as session: + + async with profile.session() as session: + try: + cred_ex_record = await V10CredentialExchange.retrieve_by_id( + session, credential_exchange_id + ) + except StorageNotFoundError as err: + raise web.HTTPNotFound(reason=err.roll_up) from err + + # Fetch connection if exchange has record + if cred_ex_record.connection_id: try: - cred_ex_record = await V10CredentialExchange.retrieve_by_id( - session, credential_exchange_id + connection_record = await ConnRecord.retrieve_by_id( + session, cred_ex_record.connection_id ) except StorageNotFoundError as err: - raise web.HTTPNotFound(reason=err.roll_up) from err + raise web.HTTPBadRequest(reason=err.roll_up) from err - connection_id = cred_ex_record.connection_id - connection_record = await ConnRecord.retrieve_by_id(session, connection_id) - if not connection_record.is_ready: - raise web.HTTPForbidden(reason=f"Connection {connection_id} not ready") + if connection_record and not connection_record.is_ready: + raise web.HTTPForbidden( + reason=f"Connection {connection_record.connection_id} not ready" + ) + try: credential_manager = CredentialManager(profile) cred_ex_record = await credential_manager.store_credential( cred_ex_record, @@ -1234,6 +1282,11 @@ async def credential_exchange_problem_report(request: web.BaseRequest): cred_ex_record = await V10CredentialExchange.retrieve_by_id( session, credential_exchange_id ) + + if not cred_ex_record.connection_id: + raise web.HTTPBadRequest( + reason="No connection associated with credential exchange." + ) report = problem_report_for_record(cred_ex_record, description) await cred_ex_record.save_error_state( session, diff --git a/aries_cloudagent/protocols/issue_credential/v1_0/tests/test_manager.py b/aries_cloudagent/protocols/issue_credential/v1_0/tests/test_manager.py index 5084eebcca..6053bd81f0 100644 --- a/aries_cloudagent/protocols/issue_credential/v1_0/tests/test_manager.py +++ b/aries_cloudagent/protocols/issue_credential/v1_0/tests/test_manager.py @@ -11,12 +11,15 @@ from .....cache.in_memory import InMemoryCache from .....indy.holder import IndyHolder from .....indy.issuer import IndyIssuer +from .....messaging.decorators.thread_decorator import ThreadDecorator from .....messaging.credential_definitions.util import CRED_DEF_SENT_RECORD_TYPE from .....messaging.responder import BaseResponder, MockResponder from .....ledger.base import BaseLedger from .....ledger.multiple_ledger.ledger_requests_executor import ( IndyLedgerRequestsExecutor, ) +from .....multitenant.base import BaseMultitenantManager +from .....multitenant.manager import MultitenantManager from .....storage.base import StorageRecord from .....storage.error import StorageNotFoundError @@ -55,6 +58,9 @@ async def setUp(self): setattr( self.profile, "session", async_mock.MagicMock(return_value=self.session) ) + setattr( + self.profile, "transaction", async_mock.MagicMock(return_value=self.session) + ) Ledger = async_mock.MagicMock() self.ledger = Ledger() @@ -278,12 +284,14 @@ async def test_create_free_offer(self): credential_proposal=preview, cred_def_id=CRED_DEF_ID, schema_id=None ) - exchange = V10CredentialExchange( + stored_exchange = V10CredentialExchange( credential_exchange_id="dummy-cxid", credential_definition_id=CRED_DEF_ID, role=V10CredentialExchange.ROLE_ISSUER, credential_proposal_dict=proposal.serialize(), + new_with_id=True, ) + await stored_exchange.save(self.session) with async_mock.patch.object( V10CredentialExchange, "save", autospec=True @@ -313,25 +321,27 @@ async def test_create_free_offer(self): await self.session.storage.add_record(cred_def_record) (ret_exchange, ret_offer) = await self.manager.create_offer( - cred_ex_record=exchange, + cred_ex_record=stored_exchange, counter_proposal=None, comment=comment, ) - assert ret_exchange is exchange + assert ret_exchange is stored_exchange save_ex.assert_called_once() issuer.create_credential_offer.assert_called_once_with(CRED_DEF_ID) - assert exchange.credential_exchange_id == ret_exchange._id # cover property - assert exchange.thread_id == ret_offer._thread_id - assert exchange.credential_definition_id == CRED_DEF_ID - assert exchange.role == V10CredentialExchange.ROLE_ISSUER - assert exchange.schema_id == SCHEMA_ID - assert exchange.state == V10CredentialExchange.STATE_OFFER_SENT - assert exchange._credential_offer.ser == INDY_OFFER + assert ( + stored_exchange.credential_exchange_id == ret_exchange._id + ) # cover property + assert stored_exchange.thread_id == ret_offer._thread_id + assert stored_exchange.credential_definition_id == CRED_DEF_ID + assert stored_exchange.role == V10CredentialExchange.ROLE_ISSUER + assert stored_exchange.schema_id == SCHEMA_ID + assert stored_exchange.state == V10CredentialExchange.STATE_OFFER_SENT + assert stored_exchange._credential_offer.ser == INDY_OFFER (ret_exchange, ret_offer) = await self.manager.create_offer( - cred_ex_record=exchange, + cred_ex_record=stored_exchange, counter_proposal=None, comment=comment, ) # once more to cover case where offer is available in cache @@ -352,12 +362,18 @@ async def test_create_free_offer_attr_mismatch(self): credential_proposal=preview, cred_def_id=CRED_DEF_ID, schema_id=None ) - exchange = V10CredentialExchange( + stored_exchange = V10CredentialExchange( credential_exchange_id="dummy-cxid", credential_definition_id=CRED_DEF_ID, role=V10CredentialExchange.ROLE_ISSUER, credential_proposal_dict=proposal.serialize(), + new_with_id=True, + ) + self.context.injector.bind_instance( + BaseMultitenantManager, + async_mock.MagicMock(MultitenantManager, autospec=True), ) + await stored_exchange.save(self.session) with async_mock.patch.object( V10CredentialExchange, "save", autospec=True @@ -388,7 +404,7 @@ async def test_create_free_offer_attr_mismatch(self): with self.assertRaises(CredentialManagerError): await self.manager.create_offer( - cred_ex_record=exchange, + cred_ex_record=stored_exchange, counter_proposal=None, comment=comment, ) @@ -407,11 +423,13 @@ async def test_create_bound_offer(self): ) ) proposal = CredentialProposal(credential_proposal=preview) - exchange = V10CredentialExchange( + stored_exchange = V10CredentialExchange( credential_exchange_id="dummy-cxid", credential_proposal_dict=proposal.serialize(), role=V10CredentialExchange.ROLE_ISSUER, + new_with_id=True, ) + await stored_exchange.save(self.session) with async_mock.patch.object( V10CredentialExchange, "save", autospec=True @@ -443,21 +461,21 @@ async def test_create_bound_offer(self): await self.session.storage.add_record(cred_def_record) (ret_exchange, ret_offer) = await self.manager.create_offer( - cred_ex_record=exchange, + cred_ex_record=stored_exchange, counter_proposal=None, comment=comment, ) - assert ret_exchange is exchange + assert ret_exchange is stored_exchange save_ex.assert_called_once() issuer.create_credential_offer.assert_called_once_with(CRED_DEF_ID) - assert exchange.thread_id == ret_offer._thread_id - assert exchange.schema_id == SCHEMA_ID - assert exchange.credential_definition_id == CRED_DEF_ID - assert exchange.role == V10CredentialExchange.ROLE_ISSUER - assert exchange.state == V10CredentialExchange.STATE_OFFER_SENT - assert exchange._credential_offer.ser == INDY_OFFER + assert stored_exchange.thread_id == ret_offer._thread_id + assert stored_exchange.schema_id == SCHEMA_ID + assert stored_exchange.credential_definition_id == CRED_DEF_ID + assert stored_exchange.role == V10CredentialExchange.ROLE_ISSUER + assert stored_exchange.state == V10CredentialExchange.STATE_OFFER_SENT + assert stored_exchange._credential_offer.ser == INDY_OFFER # additionally check that credential preview was passed through assert ret_offer.credential_preview.attributes == preview.attributes @@ -476,11 +494,13 @@ async def test_create_bound_offer_no_cred_def(self): ) ) proposal = CredentialProposal(credential_proposal=preview) - exchange = V10CredentialExchange( + stored_exchange = V10CredentialExchange( credential_exchange_id="dummy-cxid", credential_proposal_dict=proposal.serialize(), role=V10CredentialExchange.ROLE_ISSUER, + new_with_id=True, ) + await stored_exchange.save(self.session) with async_mock.patch.object( V10CredentialExchange, "save", autospec=True @@ -498,7 +518,7 @@ async def test_create_bound_offer_no_cred_def(self): with self.assertRaises(CredentialManagerError): await self.manager.create_offer( - cred_ex_record=exchange, + cred_ex_record=stored_exchange, counter_proposal=None, comment=comment, ) @@ -529,9 +549,12 @@ async def test_receive_offer_proposed(self): credential_proposal_dict=proposal.serialize(), initiator=V10CredentialExchange.INITIATOR_EXTERNAL, role=V10CredentialExchange.ROLE_HOLDER, + state=V10CredentialExchange.STATE_PROPOSAL_SENT, schema_id=SCHEMA_ID, thread_id=thread_id, + new_with_id=True, ) + await stored_exchange.save(self.session) with async_mock.patch.object( V10CredentialExchange, "save", autospec=True @@ -549,6 +572,7 @@ async def test_receive_offer_proposed(self): assert exchange.role == V10CredentialExchange.ROLE_HOLDER assert exchange.state == V10CredentialExchange.STATE_OFFER_RECEIVED assert exchange._credential_offer.ser == INDY_OFFER + assert exchange.credential_offer_dict == offer proposal = exchange.credential_proposal_dict assert proposal.credential_proposal.attributes == preview.attributes @@ -588,12 +612,18 @@ async def test_receive_free_offer(self): assert exchange.state == V10CredentialExchange.STATE_OFFER_RECEIVED assert exchange._credential_offer.ser == INDY_OFFER assert exchange.credential_proposal_dict + assert exchange.credential_offer_dict == offer async def test_create_request(self): connection_id = "test_conn_id" thread_id = "thread-id" holder_did = "did" + credential_offer_dict = CredentialOffer( + "thread-id", + ) + credential_offer_dict._thread = ThreadDecorator(pthid="some-pthid") + stored_exchange = V10CredentialExchange( credential_exchange_id="dummy-cxid", connection_id=connection_id, @@ -602,9 +632,12 @@ async def test_create_request(self): initiator=V10CredentialExchange.INITIATOR_SELF, role=V10CredentialExchange.ROLE_HOLDER, state=V10CredentialExchange.STATE_OFFER_RECEIVED, + credential_offer_dict=credential_offer_dict, schema_id=SCHEMA_ID, thread_id=thread_id, + new_with_id=True, ) + await stored_exchange.save(self.session) self.cache = InMemoryCache() self.context.injector.bind_instance(BaseCache, self.cache) @@ -643,31 +676,42 @@ async def test_create_request(self): await self.manager.create_request(stored_exchange, holder_did) # cover case with existing cred req - stored_exchange.state = V10CredentialExchange.STATE_OFFER_RECEIVED - stored_exchange.credential_request = INDY_CRED_REQ ( ret_existing_exchange, ret_existing_request, - ) = await self.manager.create_request(stored_exchange, holder_did) + ) = await self.manager.create_request(ret_exchange, holder_did) assert ret_existing_exchange == ret_exchange assert ret_existing_request._thread_id == thread_id + assert ret_existing_request._thread.pthid == "some-pthid" async def test_create_request_no_cache(self): connection_id = "test_conn_id" thread_id = "thread-id" holder_did = "did" + credential_offer_dict = CredentialOffer( + "thread-id", + ) + credential_offer_dict._thread = ThreadDecorator(pthid="some-pthid") + stored_exchange = V10CredentialExchange( credential_exchange_id="dummy-cxid", connection_id=connection_id, credential_definition_id=CRED_DEF_ID, credential_offer=INDY_OFFER, + credential_offer_dict=credential_offer_dict, initiator=V10CredentialExchange.INITIATOR_SELF, role=V10CredentialExchange.ROLE_HOLDER, state=V10CredentialExchange.STATE_OFFER_RECEIVED, schema_id=SCHEMA_ID, thread_id=thread_id, + new_with_id=True, ) + self.context.injector.bind_instance( + BaseMultitenantManager, + async_mock.MagicMock(MultitenantManager, autospec=True), + ) + await stored_exchange.save(self.session) with async_mock.patch.object( V10CredentialExchange, "save", autospec=True @@ -694,6 +738,7 @@ async def test_create_request_no_cache(self): assert ret_request.indy_cred_req() == INDY_CRED_REQ assert ret_request._thread_id == thread_id + assert ret_request._thread.pthid == "some-pthid" assert ret_exchange.state == V10CredentialExchange.STATE_REQUEST_SENT @@ -712,20 +757,25 @@ async def test_create_request_bad_state(self): state=V10CredentialExchange.STATE_PROPOSAL_SENT, schema_id=SCHEMA_ID, thread_id=thread_id, + new_with_id=True, ) + await stored_exchange.save(self.session) with self.assertRaises(CredentialManagerError): await self.manager.create_request(stored_exchange, holder_did) async def test_receive_request(self): - connection_id = "test_conn_id" + mock_conn = async_mock.MagicMock(connection_id="test_conn_id") stored_exchange = V10CredentialExchange( credential_exchange_id="dummy-cxid", - connection_id=connection_id, + connection_id=mock_conn.connection_id, initiator=V10CredentialExchange.INITIATOR_EXTERNAL, role=V10CredentialExchange.ROLE_ISSUER, + state=V10CredentialExchange.STATE_OFFER_SENT, + new_with_id=True, ) + await stored_exchange.save(self.session) request = CredentialRequest( requests_attach=[CredentialRequest.wrap_indy_cred_req(INDY_CRED_REQ)] @@ -738,10 +788,14 @@ async def test_receive_request(self): "retrieve_by_connection_and_thread", async_mock.CoroutineMock(return_value=stored_exchange), ) as retrieve_ex: - exchange = await self.manager.receive_request(request, connection_id) + exchange = await self.manager.receive_request(request, mock_conn, None) retrieve_ex.assert_called_once_with( - self.session, connection_id, request._thread_id + self.session, + "test_conn_id", + request._thread_id, + role=V10CredentialExchange.ROLE_ISSUER, + for_update=True, ) save_ex.assert_called_once() @@ -753,30 +807,36 @@ async def test_receive_request_no_connection_cred_request(self): credential_exchange_id="dummy-cxid", initiator=V10CredentialExchange.INITIATOR_EXTERNAL, role=V10CredentialExchange.ROLE_ISSUER, + state=V10CredentialExchange.STATE_OFFER_SENT, + new_with_id=True, ) + await stored_exchange.save(self.session) request = CredentialRequest( requests_attach=[CredentialRequest.wrap_indy_cred_req(INDY_CRED_REQ)] ) + mock_conn = async_mock.MagicMock( + connection_id="test_conn_id", + ) + mock_oob = async_mock.MagicMock() + with async_mock.patch.object( V10CredentialExchange, "save", autospec=True ) as mock_save, async_mock.patch.object( V10CredentialExchange, "retrieve_by_connection_and_thread", async_mock.CoroutineMock(), - ) as mock_retrieve, async_mock.patch.object( - V10CredentialExchange, "retrieve_by_tag_filter", async_mock.CoroutineMock() - ) as mock_retrieve_tag_filter: - mock_retrieve.side_effect = (StorageNotFoundError(),) - mock_retrieve_tag_filter.return_value = stored_exchange - cx_rec = await self.manager.receive_request(request, "test_conn_id") + ) as mock_retrieve: + mock_retrieve.return_value = stored_exchange + cx_rec = await self.manager.receive_request(request, mock_conn, mock_oob) mock_retrieve.assert_called_once_with( - self.session, "test_conn_id", request._thread_id - ) - mock_retrieve_tag_filter.assert_called_once_with( - self.session, {"thread_id": request._thread_id}, {"connection_id": None} + self.session, + None, + request._thread_id, + role=V10CredentialExchange.ROLE_ISSUER, + for_update=True, ) mock_save.assert_called_once() assert cx_rec.state == V10CredentialExchange.STATE_REQUEST_RECEIVED @@ -788,36 +848,39 @@ async def test_receive_request_no_cred_ex_with_offer_found(self): credential_exchange_id="dummy-cxid", initiator=V10CredentialExchange.INITIATOR_EXTERNAL, role=V10CredentialExchange.ROLE_ISSUER, + state=V10CredentialExchange.STATE_OFFER_SENT, + new_with_id=True, ) + await stored_exchange.save(self.session) request = CredentialRequest( requests_attach=[CredentialRequest.wrap_indy_cred_req(INDY_CRED_REQ)] ) + mock_conn = async_mock.MagicMock( + connection_id="test_conn_id", + ) + with async_mock.patch.object( V10CredentialExchange, "save", autospec=True ) as mock_save, async_mock.patch.object( V10CredentialExchange, "retrieve_by_connection_and_thread", async_mock.CoroutineMock(), - ) as mock_retrieve, async_mock.patch.object( - V10CredentialExchange, "retrieve_by_tag_filter", async_mock.CoroutineMock() - ) as mock_retrieve_tag_filter: + ) as mock_retrieve: mock_retrieve.side_effect = (StorageNotFoundError(),) - mock_retrieve_tag_filter.side_effect = (StorageNotFoundError(),) with self.assertRaises(CredentialManagerError): - cx_rec = await self.manager.receive_request(request, "test_conn_id") + cx_rec = await self.manager.receive_request(request, mock_conn, None) mock_retrieve.assert_called_once_with( - self.session, "test_conn_id", request._thread_id - ) - mock_retrieve_tag_filter.assert_called_once_with( self.session, - {"thread_id": request._thread_id}, - {"connection_id": None}, + "test_conn_id", + request._thread_id, + role=V10CredentialExchange.ROLE_ISSUER, + for_update=True, ) - async def test_issue_credential(self): + async def test_issue_credential_revocable(self): connection_id = "test_conn_id" comment = "comment" cred_values = {"attr": "value"} @@ -840,7 +903,9 @@ async def test_issue_credential(self): role=V10CredentialExchange.ROLE_ISSUER, state=V10CredentialExchange.STATE_REQUEST_RECEIVED, thread_id=thread_id, + new_with_id=True, ) + await stored_exchange.save(self.session) issuer = async_mock.MagicMock() cred = {"indy": "credential"} @@ -853,18 +918,18 @@ async def test_issue_credential(self): with async_mock.patch.object( test_module, "IndyRevocation", autospec=True ) as revoc, async_mock.patch.object( - asyncio, "ensure_future", autospec=True - ) as asyncio_mock, async_mock.patch.object( V10CredentialExchange, "save", autospec=True ) as save_ex: - revoc.return_value.get_active_issuer_rev_reg_record = async_mock.CoroutineMock( - return_value=async_mock.MagicMock( # active_rev_reg_rec - revoc_reg_id=REV_REG_ID, - get_registry=async_mock.CoroutineMock( - return_value=async_mock.MagicMock( # rev_reg - tails_local_path="dummy-path", - get_or_fetch_local_tails_path=async_mock.CoroutineMock(), - ) + revoc.return_value.get_or_create_active_registry = async_mock.CoroutineMock( + return_value=( + async_mock.MagicMock( # active_rev_reg_rec + revoc_reg_id=REV_REG_ID, + ), + async_mock.MagicMock( # rev_reg + registry_id=REV_REG_ID, + tails_local_path="dummy-path", + get_or_fetch_local_tails_path=async_mock.CoroutineMock(), + max_creds=10, ), ) ) @@ -879,7 +944,6 @@ async def test_issue_credential(self): INDY_OFFER, INDY_CRED_REQ, cred_values, - stored_exchange.credential_exchange_id, REV_REG_ID, "dummy-path", ) @@ -890,13 +954,11 @@ async def test_issue_credential(self): assert ret_cred_issue._thread_id == thread_id # cover case with existing cred - stored_exchange.credential = cred - stored_exchange.state = V10CredentialExchange.STATE_REQUEST_RECEIVED ( ret_existing_exchange, ret_existing_cred, ) = await self.manager.issue_credential( - stored_exchange, comment=comment, retries=0 + ret_exchange, comment=comment, retries=0 ) assert ret_existing_exchange == ret_exchange assert ret_existing_cred._thread_id == thread_id @@ -908,7 +970,10 @@ async def test_issue_credential_non_revocable(self): comment = "comment" cred_values = {"attr": "value"} thread_id = "thread-id" - + self.context.injector.bind_instance( + BaseMultitenantManager, + async_mock.MagicMock(MultitenantManager, autospec=True), + ) stored_exchange = V10CredentialExchange( credential_exchange_id="dummy-cxid", connection_id=connection_id, @@ -926,7 +991,9 @@ async def test_issue_credential_non_revocable(self): role=V10CredentialExchange.ROLE_ISSUER, state=V10CredentialExchange.STATE_REQUEST_RECEIVED, thread_id=thread_id, + new_with_id=True, ) + await stored_exchange.save(self.session) issuer = async_mock.MagicMock() cred = {"indy": "credential"} @@ -944,17 +1011,13 @@ async def test_issue_credential_non_revocable(self): self.ledger.__aenter__ = async_mock.CoroutineMock(return_value=self.ledger) self.context.injector.clear_binding(BaseLedger) self.context.injector.bind_instance(BaseLedger, self.ledger) - self.context.injector.bind_instance( - IndyLedgerRequestsExecutor, - async_mock.MagicMock( - get_ledger_for_identifier=async_mock.CoroutineMock( - return_value=("test_ledger_id", self.ledger) - ) - ), - ) with async_mock.patch.object( V10CredentialExchange, "save", autospec=True - ) as save_ex: + ) as save_ex, async_mock.patch.object( + IndyLedgerRequestsExecutor, + "get_ledger_for_identifier", + async_mock.CoroutineMock(return_value=("test_ledger_id", self.ledger)), + ): (ret_exchange, ret_cred_issue) = await self.manager.issue_credential( stored_exchange, comment=comment, retries=0 ) @@ -966,7 +1029,6 @@ async def test_issue_credential_non_revocable(self): INDY_OFFER, INDY_CRED_REQ, cred_values, - stored_exchange.credential_exchange_id, None, None, ) @@ -1000,7 +1062,9 @@ async def test_issue_credential_fills_rr(self): state=V10CredentialExchange.STATE_REQUEST_RECEIVED, thread_id=thread_id, revocation_id="1000", + new_with_id=True, ) + await stored_exchange.save(self.session) issuer = async_mock.MagicMock() cred = {"indy": "credential"} @@ -1012,33 +1076,28 @@ async def test_issue_credential_fills_rr(self): with async_mock.patch.object( test_module, "IndyRevocation", autospec=True ) as revoc, async_mock.patch.object( - asyncio, "ensure_future", autospec=True - ) as asyncio_mock, async_mock.patch.object( V10CredentialExchange, "save", autospec=True ) as save_ex: revoc.return_value = async_mock.MagicMock( - get_active_issuer_rev_reg_record=( + get_or_create_active_registry=( async_mock.CoroutineMock( - return_value=async_mock.MagicMock( # active_rev_reg_rec - revoc_reg_id=REV_REG_ID, - get_registry=async_mock.CoroutineMock( - return_value=async_mock.MagicMock( # rev_reg - tails_local_path="dummy-path", - max_creds=1000, - get_or_fetch_local_tails_path=( - async_mock.CoroutineMock() - ), - ) + return_value=( + async_mock.MagicMock( # active_rev_reg_rec + revoc_reg_id=REV_REG_ID, + set_state=async_mock.CoroutineMock(), + ), + async_mock.MagicMock( # rev_reg + registry_id=REV_REG_ID, + tails_local_path="dummy-path", + max_creds=1000, + get_or_fetch_local_tails_path=( + async_mock.CoroutineMock() + ), ), - set_state=async_mock.CoroutineMock(), ) ) ), - init_issuer_registry=async_mock.CoroutineMock( - return_value=async_mock.MagicMock( # pending_rev_reg_rec - stage_pending_registry=async_mock.CoroutineMock() - ) - ), + handle_full_registry=async_mock.CoroutineMock(), ) (ret_exchange, ret_cred_issue) = await self.manager.issue_credential( stored_exchange, comment=comment, retries=0 @@ -1051,11 +1110,12 @@ async def test_issue_credential_fills_rr(self): INDY_OFFER, INDY_CRED_REQ, cred_values, - stored_exchange.credential_exchange_id, REV_REG_ID, "dummy-path", ) + revoc.return_value.handle_full_registry.assert_awaited_once_with(REV_REG_ID) + assert ret_exchange._credential.ser == cred assert ret_cred_issue.indy_credential() == cred assert ret_exchange.state == V10CredentialExchange.STATE_ISSUED @@ -1075,7 +1135,9 @@ async def test_issue_credential_request_bad_state(self): state=V10CredentialExchange.STATE_PROPOSAL_SENT, schema_id=SCHEMA_ID, thread_id=thread_id, + new_with_id=True, ) + await stored_exchange.save(self.session) with self.assertRaises(CredentialManagerError): await self.manager.issue_credential(stored_exchange) @@ -1103,7 +1165,9 @@ async def test_issue_credential_no_active_rr_no_retries(self): role=V10CredentialExchange.ROLE_ISSUER, state=V10CredentialExchange.STATE_REQUEST_RECEIVED, thread_id=thread_id, + new_with_id=True, ) + await stored_exchange.save(self.session) issuer = async_mock.MagicMock() cred = {"indy": "credential"} @@ -1121,28 +1185,28 @@ async def test_issue_credential_no_active_rr_no_retries(self): ), ) with async_mock.patch.object( - test_module, "IssuerRevRegRecord", autospec=True - ) as issuer_rr_rec, async_mock.patch.object( test_module, "IndyRevocation", autospec=True - ) as revoc, async_mock.patch.object( - V10CredentialExchange, "save", autospec=True - ) as save_ex: - revoc.return_value.get_active_issuer_rev_reg_record = ( - async_mock.CoroutineMock(side_effect=test_module.StorageNotFoundError()) - ) - revoc.return_value.init_issuer_registry = async_mock.CoroutineMock( - return_value=async_mock.MagicMock( # pending_rev_reg_rec - stage_pending_registry=async_mock.CoroutineMock() - ) - ) - issuer_rr_rec.query_by_cred_def_id = async_mock.CoroutineMock( - return_value=[] + ) as revoc: + revoc.return_value.get_or_create_active_registry = async_mock.CoroutineMock( + side_effect=[ + None, + ( + async_mock.MagicMock( # active_rev_reg_rec + revoc_reg_id=REV_REG_ID, + set_state=async_mock.CoroutineMock(), + ), + async_mock.MagicMock( # rev_reg + tails_local_path="dummy-path", + get_or_fetch_local_tails_path=(async_mock.CoroutineMock()), + ), + ), + ] ) - with self.assertRaises(CredentialManagerError) as x_cred_mgr: + with self.assertRaises(CredentialManagerError) as context: await self.manager.issue_credential( stored_exchange, comment=comment, retries=0 ) - assert "has no active revocation registry" in x_cred_mgr.message + assert "has no active revocation registry" in context.message async def test_issue_credential_no_active_rr_retry(self): connection_id = "test_conn_id" @@ -1167,7 +1231,9 @@ async def test_issue_credential_no_active_rr_retry(self): role=V10CredentialExchange.ROLE_ISSUER, state=V10CredentialExchange.STATE_REQUEST_RECEIVED, thread_id=thread_id, + new_with_id=True, ) + await stored_exchange.save(self.session) issuer = async_mock.MagicMock() cred = {"indy": "credential"} @@ -1184,97 +1250,17 @@ async def test_issue_credential_no_active_rr_retry(self): ) ), ) - with async_mock.patch.object( - test_module, "IssuerRevRegRecord", autospec=True - ) as issuer_rr_rec, async_mock.patch.object( - test_module, "IndyRevocation", autospec=True - ) as revoc, async_mock.patch.object( - V10CredentialExchange, "save", autospec=True - ) as save_ex: - revoc.return_value.get_active_issuer_rev_reg_record = ( - async_mock.CoroutineMock(side_effect=test_module.StorageNotFoundError()) - ) - issuer_rr_rec.query_by_cred_def_id = async_mock.CoroutineMock( - side_effect=[ - [], # posted_rev_reg_recs - [async_mock.MagicMock(max_cred_num=1000)], # old_rev_reg_recs - ] - * 2 - ) - revoc.return_value.init_issuer_registry = async_mock.CoroutineMock( - return_value=async_mock.MagicMock( # pending_rev_reg_rec - stage_pending_registry=async_mock.CoroutineMock() - ) - ) - with self.assertRaises(CredentialManagerError) as x_cred_mgr: - await self.manager.issue_credential( - stored_exchange, comment=comment, retries=1 - ) - assert "has no active revocation registry" in x_cred_mgr.message - - async def test_issue_credential_rr_full(self): - connection_id = "test_conn_id" - comment = "comment" - cred_values = {"attr": "value"} - thread_id = "thread-id" - - stored_exchange = V10CredentialExchange( - credential_exchange_id="dummy-cxid", - connection_id=connection_id, - credential_definition_id=CRED_DEF_ID, - credential_offer=INDY_OFFER, - credential_request=INDY_CRED_REQ, - credential_proposal_dict=CredentialProposal( - credential_proposal=CredentialPreview.deserialize( - {"attributes": [{"name": "attr", "value": "value"}]} - ), - cred_def_id=CRED_DEF_ID, - schema_id=SCHEMA_ID, - ).serialize(), - initiator=V10CredentialExchange.INITIATOR_SELF, - role=V10CredentialExchange.ROLE_ISSUER, - state=V10CredentialExchange.STATE_REQUEST_RECEIVED, - thread_id=thread_id, - ) - - issuer = async_mock.MagicMock() - cred = {"indy": "credential"} - issuer.create_credential = async_mock.CoroutineMock( - side_effect=test_module.IndyIssuerRevocationRegistryFullError("Nope") - ) - self.context.injector.bind_instance(IndyIssuer, issuer) - self.context.injector.bind_instance( - IndyLedgerRequestsExecutor, - async_mock.MagicMock( - get_ledger_for_identifier=async_mock.CoroutineMock( - return_value=("test_ledger_id", self.ledger) - ) - ), - ) with async_mock.patch.object( test_module, "IndyRevocation", autospec=True ) as revoc: - revoc.return_value.get_active_issuer_rev_reg_record = ( - async_mock.CoroutineMock( - return_value=async_mock.MagicMock( # active_rev_reg_rec - revoc_reg_id=REV_REG_ID, - set_state=async_mock.CoroutineMock(), - get_registry=async_mock.CoroutineMock( - return_value=async_mock.MagicMock( # rev_reg - tails_local_path="dummy-path", - get_or_fetch_local_tails_path=( - async_mock.CoroutineMock() - ), - ) - ), - ) - ) + revoc.return_value.get_or_create_active_registry = async_mock.CoroutineMock( + return_value=None ) - - with self.assertRaises(test_module.IndyIssuerRevocationRegistryFullError): + with self.assertRaises(CredentialManagerError) as context: await self.manager.issue_credential( stored_exchange, comment=comment, retries=1 ) + assert "has no active revocation registry" in context.message async def test_receive_credential(self): connection_id = "test_conn_id" @@ -1283,8 +1269,11 @@ async def test_receive_credential(self): credential_exchange_id="dummy-cxid", connection_id=connection_id, initiator=V10CredentialExchange.INITIATOR_EXTERNAL, - role=V10CredentialExchange.ROLE_ISSUER, + role=V10CredentialExchange.ROLE_HOLDER, + state=V10CredentialExchange.STATE_REQUEST_SENT, + new_with_id=True, ) + await stored_exchange.save(self.session) issue = CredentialIssue( credentials_attach=[CredentialIssue.wrap_indy_credential(INDY_CRED)] @@ -1300,7 +1289,11 @@ async def test_receive_credential(self): exchange = await self.manager.receive_credential(issue, connection_id) retrieve_ex.assert_called_once_with( - self.session, connection_id, issue._thread_id + self.session, + connection_id, + issue._thread_id, + role=V10CredentialExchange.ROLE_HOLDER, + for_update=True, ) save_ex.assert_called_once() @@ -1337,7 +1330,9 @@ async def test_store_credential(self): state=V10CredentialExchange.STATE_CREDENTIAL_RECEIVED, thread_id=thread_id, auto_remove=True, + new_with_id=True, ) + await stored_exchange.save(self.session) cred_id = "cred-id" holder = async_mock.MagicMock() @@ -1361,7 +1356,6 @@ async def test_store_credential(self): ) as save_ex, async_mock.patch.object( V10CredentialExchange, "delete_record", autospec=True ) as delete_ex: - mock_rev_reg.from_definition = async_mock.MagicMock( return_value=async_mock.MagicMock( get_or_fetch_local_tails_path=async_mock.CoroutineMock() @@ -1406,7 +1400,9 @@ async def test_store_credential_bad_state(self): role=V10CredentialExchange.ROLE_HOLDER, state=V10CredentialExchange.STATE_OFFER_RECEIVED, thread_id=thread_id, + new_with_id=True, ) + await stored_exchange.save(self.session) cred_id = "cred-id" with self.assertRaises(CredentialManagerError): @@ -1416,7 +1412,10 @@ async def test_store_credential_no_preview(self): connection_id = "test_conn_id" cred_req_meta = {"req": "meta"} thread_id = "thread-id" - + self.context.injector.bind_instance( + BaseMultitenantManager, + async_mock.MagicMock(MultitenantManager, autospec=True), + ) cred_no_rev = {**INDY_CRED} cred_no_rev["rev_reg_id"] = None cred_no_rev["rev_reg"] = None @@ -1435,7 +1434,9 @@ async def test_store_credential_no_preview(self): role=V10CredentialExchange.ROLE_HOLDER, state=V10CredentialExchange.STATE_CREDENTIAL_RECEIVED, thread_id=thread_id, + new_with_id=True, ) + await stored_exchange.save(self.session) cred_def = async_mock.MagicMock() self.ledger.get_credential_definition = async_mock.CoroutineMock( @@ -1503,7 +1504,9 @@ async def test_store_credential_holder_store_indy_error(self): role=V10CredentialExchange.ROLE_HOLDER, state=V10CredentialExchange.STATE_CREDENTIAL_RECEIVED, thread_id=thread_id, + new_with_id=True, ) + await stored_exchange.save(self.session) cred_def = async_mock.MagicMock() self.ledger.get_credential_definition = async_mock.CoroutineMock( @@ -1535,12 +1538,15 @@ async def test_send_credential_ack(self): credential_exchange_id="dummy-cxid", connection_id=connection_id, initiator=V10CredentialExchange.INITIATOR_SELF, + state=V10CredentialExchange.STATE_CREDENTIAL_RECEIVED, thread_id="thid", parent_thread_id="pthid", role=V10CredentialExchange.ROLE_ISSUER, trace=False, auto_remove=True, + new_with_id=True, ) + await stored_exchange.save(self.session) with async_mock.patch.object( V10CredentialExchange, "save", autospec=True @@ -1552,16 +1558,17 @@ async def test_send_credential_ack(self): test_module.LOGGER, "warning", async_mock.MagicMock() ) as mock_log_warning: mock_delete_ex.side_effect = test_module.StorageError() - (_, ack) = await self.manager.send_credential_ack(stored_exchange) + (exch, ack) = await self.manager.send_credential_ack(stored_exchange) assert ack._thread mock_log_exception.assert_called_once() # cover exception log-and-continue mock_log_warning.assert_called_once() # no BaseResponder + assert exch.state == V10CredentialExchange.STATE_ACKED mock_responder = MockResponder() # cover with responder self.context.injector.bind_instance(BaseResponder, mock_responder) - (cx_rec, ack) = await self.manager.send_credential_ack(stored_exchange) + (exch, ack) = await self.manager.send_credential_ack(stored_exchange) assert ack._thread - assert cx_rec.state == V10CredentialExchange.STATE_ACKED + assert exch.state == V10CredentialExchange.STATE_ACKED async def test_receive_credential_ack(self): connection_id = "connection-id" @@ -1570,7 +1577,9 @@ async def test_receive_credential_ack(self): connection_id=connection_id, initiator=V10CredentialExchange.INITIATOR_SELF, role=V10CredentialExchange.ROLE_ISSUER, + new_with_id=True, ) + await stored_exchange.save(self.session) ack = CredentialAck() @@ -1587,7 +1596,11 @@ async def test_receive_credential_ack(self): ret_exchange = await self.manager.receive_credential_ack(ack, connection_id) retrieve_ex.assert_called_once_with( - self.session, connection_id, ack._thread_id + self.session, + connection_id, + ack._thread_id, + role=V10CredentialExchange.ROLE_ISSUER, + for_update=True, ) save_ex.assert_called_once() @@ -1601,7 +1614,9 @@ async def test_receive_problem_report(self): connection_id=connection_id, initiator=V10CredentialExchange.INITIATOR_SELF, role=V10CredentialExchange.ROLE_ISSUER, + new_with_id=True, ) + await stored_exchange.save(self.session) problem = CredentialProblemReport( description={ "code": test_module.ProblemReportReason.ISSUANCE_ABANDONED.value, @@ -1622,20 +1637,14 @@ async def test_receive_problem_report(self): problem, connection_id ) retrieve_ex.assert_called_once_with( - self.session, connection_id, problem._thread_id + self.session, connection_id, problem._thread_id, for_update=True ) save_ex.assert_called_once() - assert ret_exchange.state is None + assert ret_exchange.state == V10CredentialExchange.STATE_ABANDONED async def test_receive_problem_report_x(self): connection_id = "connection-id" - stored_exchange = V10CredentialExchange( - credential_exchange_id="dummy-cxid", - initiator=V10CredentialExchange.INITIATOR_SELF, - role=V10CredentialExchange.ROLE_ISSUER, - state=V10CredentialExchange.STATE_REQUEST_RECEIVED, - ) problem = CredentialProblemReport( description={ "code": test_module.ProblemReportReason.ISSUANCE_ABANDONED.value, @@ -1650,8 +1659,8 @@ async def test_receive_problem_report_x(self): ) as retrieve_ex: retrieve_ex.side_effect = test_module.StorageNotFoundError("No such record") - with self.assertRaises(test_module.StorageNotFoundError): - await self.manager.receive_problem_report(problem, connection_id) + exch = await self.manager.receive_problem_report(problem, connection_id) + assert exch is None async def test_retrieve_records(self): self.cache = InMemoryCache() diff --git a/aries_cloudagent/protocols/issue_credential/v1_0/tests/test_routes.py b/aries_cloudagent/protocols/issue_credential/v1_0/tests/test_routes.py index 8744ffe449..129f7eceb1 100644 --- a/aries_cloudagent/protocols/issue_credential/v1_0/tests/test_routes.py +++ b/aries_cloudagent/protocols/issue_credential/v1_0/tests/test_routes.py @@ -1,10 +1,7 @@ from asynctest import mock as async_mock, TestCase as AsyncTestCase from .....admin.request_context import AdminRequestContext -from .....wallet.key_type import KeyType -from .....wallet.did_method import DIDMethod from .....wallet.base import BaseWallet -from .....wallet.did_info import DIDInfo from .. import routes as test_module @@ -256,7 +253,6 @@ async def test_credential_exchange_send_no_conn_record(self): ) as mock_conn_rec, async_mock.patch.object( test_module, "CredentialManager", autospec=True ) as mock_credential_manager: - # Emulate storage not found (bad connection id) mock_conn_rec.retrieve_by_id = async_mock.CoroutineMock( side_effect=test_module.StorageNotFoundError() @@ -283,7 +279,6 @@ async def test_credential_exchange_send_not_ready(self): ) as mock_conn_rec, async_mock.patch.object( test_module, "CredentialManager", autospec=True ) as mock_credential_manager: - # Emulate connection not ready mock_conn_rec.retrieve_by_id.return_value.is_ready = False @@ -346,7 +341,6 @@ async def test_credential_exchange_send_proposal(self): ) as mock_credential_manager, async_mock.patch.object( test_module.web, "json_response" ) as mock_response: - mock_cred_ex_record = async_mock.MagicMock() mock_credential_manager.return_value.create_proposal.return_value = ( mock_cred_ex_record @@ -370,7 +364,6 @@ async def test_credential_exchange_send_proposal_no_conn_record(self): ) as mock_credential_manager, async_mock.patch.object( test_module.CredentialPreview, "deserialize", autospec=True ) as mock_preview_deserialize: - # Emulate storage not found (bad connection id) mock_conn_rec.retrieve_by_id = async_mock.CoroutineMock( side_effect=test_module.StorageNotFoundError() @@ -408,7 +401,6 @@ async def test_credential_exchange_send_proposal_not_ready(self): ) as mock_credential_manager, async_mock.patch.object( test_module.CredentialPreview, "deserialize", autospec=True ) as mock_preview_deserialize: - # Emulate connection not ready mock_conn_rec.retrieve_by_id = async_mock.CoroutineMock() mock_conn_rec.retrieve_by_id.return_value.is_ready = False @@ -598,7 +590,6 @@ async def test_credential_exchange_send_free_offer(self): ) as mock_credential_manager, async_mock.patch.object( test_module.web, "json_response" ) as mock_response: - mock_credential_manager.return_value.create_offer = ( async_mock.CoroutineMock() ) @@ -650,7 +641,6 @@ async def test_credential_exchange_send_free_offer_no_conn_record(self): ) as mock_conn_rec, async_mock.patch.object( test_module, "CredentialManager", autospec=True ) as mock_credential_manager: - # Emulate storage not found (bad connection id) mock_conn_rec.retrieve_by_id = async_mock.CoroutineMock( side_effect=test_module.StorageNotFoundError() @@ -676,7 +666,6 @@ async def test_credential_exchange_send_free_offer_not_ready(self): ) as mock_conn_rec, async_mock.patch.object( test_module, "CredentialManager", autospec=True ) as mock_credential_manager: - # Emulate connection not ready mock_conn_rec.retrieve_by_id = async_mock.CoroutineMock() mock_conn_rec.retrieve_by_id.return_value.is_ready = False @@ -742,7 +731,6 @@ async def test_credential_exchange_send_bound_offer(self): ) as mock_cred_ex, async_mock.patch.object( test_module.web, "json_response" ) as mock_response: - mock_cred_ex.retrieve_by_id = async_mock.CoroutineMock() mock_cred_ex.retrieve_by_id.return_value.state = ( mock_cred_ex.STATE_PROPOSAL_RECEIVED @@ -881,11 +869,49 @@ async def test_credential_exchange_send_request(self): ) as mock_cred_ex, async_mock.patch.object( test_module.web, "json_response" ) as mock_response: + mock_cred_ex.retrieve_by_id = async_mock.CoroutineMock() + mock_cred_ex.retrieve_by_id.return_value.state = ( + mock_cred_ex.STATE_OFFER_RECEIVED + ) + + mock_cred_ex_record = async_mock.MagicMock() + + mock_credential_manager.return_value.create_request.return_value = ( + mock_cred_ex_record, + async_mock.MagicMock(), + ) + + await test_module.credential_exchange_send_request(self.request) + + mock_response.assert_called_once_with( + mock_cred_ex_record.serialize.return_value + ) + + async def test_credential_exchange_send_request_no_conn(self): + self.request.json = async_mock.CoroutineMock() + self.request.match_info = {"cred_ex_id": "dummy"} + + with async_mock.patch.object( + test_module, "OobRecord", autospec=True + ) as mock_oob_rec, async_mock.patch.object( + test_module, "default_did_from_verkey", autospec=True + ) as mock_default_did_from_verkey, async_mock.patch.object( + test_module, "CredentialManager", autospec=True + ) as mock_credential_manager, async_mock.patch.object( + test_module, "V10CredentialExchange", autospec=True + ) as mock_cred_ex, async_mock.patch.object( + test_module.web, "json_response" + ) as mock_response: + mock_oob_rec.retrieve_by_tag_filter = async_mock.CoroutineMock( + return_value=async_mock.MagicMock(our_recipient_key="our-recipient_key") + ) + mock_default_did_from_verkey.return_value = "holder-did" mock_cred_ex.retrieve_by_id = async_mock.CoroutineMock() mock_cred_ex.retrieve_by_id.return_value.state = ( mock_cred_ex.STATE_OFFER_RECEIVED ) + mock_cred_ex.retrieve_by_id.return_value.connection_id = None mock_cred_ex_record = async_mock.MagicMock() @@ -896,9 +922,13 @@ async def test_credential_exchange_send_request(self): await test_module.credential_exchange_send_request(self.request) + mock_credential_manager.return_value.create_request.assert_called_once_with( + mock_cred_ex.retrieve_by_id.return_value, "holder-did" + ) mock_response.assert_called_once_with( mock_cred_ex_record.serialize.return_value ) + mock_default_did_from_verkey.assert_called_once_with("our-recipient_key") async def test_credential_exchange_send_request_bad_cred_ex_id(self): self.request.json = async_mock.CoroutineMock() @@ -996,7 +1026,6 @@ async def test_credential_exchange_issue(self): ) as mock_cred_ex, async_mock.patch.object( test_module.web, "json_response" ) as mock_response: - mock_cred_ex.retrieve_by_id = async_mock.CoroutineMock() mock_cred_ex.retrieve_by_id.return_value.state = ( mock_cred_ex.STATE_REQUEST_RECEIVED @@ -1046,7 +1075,6 @@ async def test_credential_exchange_issue_no_conn_record(self): ) as mock_credential_manager, async_mock.patch.object( test_module, "V10CredentialExchange", autospec=True ) as mock_cred_ex_cls: - mock_cred_ex_rec.state = mock_cred_ex_cls.STATE_REQUEST_RECEIVED mock_cred_ex_cls.retrieve_by_id = async_mock.CoroutineMock( return_value=mock_cred_ex_rec @@ -1079,7 +1107,6 @@ async def test_credential_exchange_issue_not_ready(self): ) as mock_credential_manager, async_mock.patch.object( test_module, "V10CredentialExchange", autospec=True ) as mock_cred_ex: - mock_cred_ex.retrieve_by_id = async_mock.CoroutineMock() mock_cred_ex.retrieve_by_id.return_value.state = ( mock_cred_ex.STATE_REQUEST_RECEIVED @@ -1175,7 +1202,6 @@ async def test_credential_exchange_store(self): ) as mock_cred_ex, async_mock.patch.object( test_module.web, "json_response" ) as mock_response: - mock_cred_ex.retrieve_by_id = async_mock.CoroutineMock() mock_cred_ex.retrieve_by_id.return_value.state = ( mock_cred_ex.STATE_CREDENTIAL_RECEIVED @@ -1212,7 +1238,6 @@ async def test_credential_exchange_store_bad_cred_id_json(self): ) as mock_cred_ex, async_mock.patch.object( test_module.web, "json_response" ) as mock_response: - mock_cred_ex.retrieve_by_id = async_mock.CoroutineMock() mock_cred_ex.retrieve_by_id.return_value.state = ( mock_cred_ex.STATE_CREDENTIAL_RECEIVED @@ -1339,10 +1364,7 @@ async def test_credential_exchange_store_x(self): return_value=mock_cred_ex_record ), send_credential_ack=async_mock.CoroutineMock( - return_value=( - mock_cred_ex_record, - async_mock.MagicMock(), - ) + return_value=(mock_cred_ex_record, async_mock.MagicMock()) ), ) diff --git a/aries_cloudagent/protocols/issue_credential/v2_0/formats/indy/handler.py b/aries_cloudagent/protocols/issue_credential/v2_0/formats/indy/handler.py index 2257a91cd7..7ef901260f 100644 --- a/aries_cloudagent/protocols/issue_credential/v2_0/formats/indy/handler.py +++ b/aries_cloudagent/protocols/issue_credential/v2_0/formats/indy/handler.py @@ -24,13 +24,11 @@ CredDefQueryStringSchema, ) from ......messaging.decorators.attach_decorator import AttachDecorator -from ......revocation.models.issuer_rev_reg_record import IssuerRevRegRecord -from ......revocation.models.revocation_registry import RevocationRegistry +from ......multitenant.base import BaseMultitenantManager from ......revocation.indy import IndyRevocation -from ......revocation.util import notify_revocation_reg_event +from ......revocation.models.issuer_cred_rev_record import IssuerCredRevRecord +from ......revocation.models.revocation_registry import RevocationRegistry from ......storage.base import BaseStorage -from ......storage.error import StorageNotFoundError - from ...message_types import ( ATTACHMENT_FORMAT, @@ -97,25 +95,26 @@ async def get_detail_record(self, cred_ex_id: str) -> V20CredExRecordIndy: session, cred_ex_id ) - if len(records) > 1: - LOGGER.warning( - "Cred ex id %s has %d %s detail records: should be 1", - cred_ex_id, - len(records), - IndyCredFormatHandler.format.api, - ) - return records[0] if records else None + if len(records) > 1: + LOGGER.warning( + "Cred ex id %s has %d %s detail records: should be 1", + cred_ex_id, + len(records), + IndyCredFormatHandler.format.api, + ) + return records[0] if records else None async def _check_uniqueness(self, cred_ex_id: str): """Raise exception on evidence that cred ex already has cred issued to it.""" async with self.profile.session() as session: - if await IndyCredFormatHandler.format.detail.query_by_cred_ex_id( + exist = await IndyCredFormatHandler.format.detail.query_by_cred_ex_id( session, cred_ex_id - ): - raise V20CredFormatError( - f"{IndyCredFormatHandler.format.api} detail record already " - f"exists for cred ex id {cred_ex_id}" - ) + ) + if exist: + raise V20CredFormatError( + f"{IndyCredFormatHandler.format.api} detail record already " + f"exists for cred ex id {cred_ex_id}" + ) def get_format_identifier(self, message_type: str) -> str: """Get attachment format identifier for format and message combination. @@ -201,7 +200,11 @@ async def _create(): offer_json = await issuer.create_credential_offer(cred_def_id) return json.loads(offer_json) - ledger_exec_inst = self._profile.inject(IndyLedgerRequestsExecutor) + multitenant_mgr = self.profile.inject_or(BaseMultitenantManager) + if multitenant_mgr: + ledger_exec_inst = IndyLedgerRequestsExecutor(self.profile) + else: + ledger_exec_inst = self.profile.inject(IndyLedgerRequestsExecutor) ledger = ( await ledger_exec_inst.get_ledger_for_identifier( cred_def_id, @@ -262,7 +265,11 @@ async def create_request( cred_def_id = cred_offer["cred_def_id"] async def _create(): - ledger_exec_inst = self._profile.inject(IndyLedgerRequestsExecutor) + multitenant_mgr = self.profile.inject_or(BaseMultitenantManager) + if multitenant_mgr: + ledger_exec_inst = IndyLedgerRequestsExecutor(self.profile) + else: + ledger_exec_inst = self.profile.inject(IndyLedgerRequestsExecutor) ledger = ( await ledger_exec_inst.get_ledger_for_identifier( cred_def_id, @@ -324,13 +331,18 @@ async def issue_credential( cred_request = cred_ex_record.cred_request.attachment( IndyCredFormatHandler.format ) - + cred_values = cred_ex_record.cred_offer.credential_preview.attr_dict( + decode=False + ) schema_id = cred_offer["schema_id"] cred_def_id = cred_offer["cred_def_id"] - rev_reg_id = None - rev_reg = None - ledger_exec_inst = self._profile.inject(IndyLedgerRequestsExecutor) + issuer = self.profile.inject(IndyIssuer) + multitenant_mgr = self.profile.inject_or(BaseMultitenantManager) + if multitenant_mgr: + ledger_exec_inst = IndyLedgerRequestsExecutor(self.profile) + else: + ledger_exec_inst = self.profile.inject(IndyLedgerRequestsExecutor) ledger = ( await ledger_exec_inst.get_ledger_for_identifier( schema_id, @@ -340,124 +352,82 @@ async def issue_credential( async with ledger: schema = await ledger.get_schema(schema_id) cred_def = await ledger.get_credential_definition(cred_def_id) + revocable = cred_def["value"].get("revocation") + result = None - tails_path = None - if cred_def["value"].get("revocation"): - revoc = IndyRevocation(self.profile) - try: - active_rev_reg_rec = await revoc.get_active_issuer_rev_reg_record( - cred_def_id + for attempt in range(max(retries, 1)): + if attempt > 0: + LOGGER.info( + "Waiting 2s before retrying credential issuance for cred def '%s'", + cred_def_id, ) - rev_reg = await active_rev_reg_rec.get_registry() - rev_reg_id = active_rev_reg_rec.revoc_reg_id - + await asyncio.sleep(2) + + if revocable: + revoc = IndyRevocation(self.profile) + registry_info = await revoc.get_or_create_active_registry(cred_def_id) + if not registry_info: + continue + del revoc + issuer_rev_reg, rev_reg = registry_info + rev_reg_id = issuer_rev_reg.revoc_reg_id tails_path = rev_reg.tails_local_path - await rev_reg.get_or_fetch_local_tails_path() - - except StorageNotFoundError: - async with self.profile.session() as session: - posted_rev_reg_recs = await IssuerRevRegRecord.query_by_cred_def_id( - session, - cred_def_id, - state=IssuerRevRegRecord.STATE_POSTED, - ) - if not posted_rev_reg_recs: - # Send next 2 rev regs, publish tails files in background - async with self.profile.session() as session: - old_rev_reg_recs = sorted( - await IssuerRevRegRecord.query_by_cred_def_id( - session, - cred_def_id, - ) - ) # prefer to reuse prior rev reg size - rev_reg_size = ( - old_rev_reg_recs[0].max_cred_num if old_rev_reg_recs else None - ) - for _ in range(2): - await notify_revocation_reg_event( - self.profile, - cred_def_id, - rev_reg_size, - auto_create_rev_reg=True, - ) - - if retries > 0: - LOGGER.info( - ("Waiting 2s on posted rev reg " "for cred def %s, retrying"), - cred_def_id, - ) - await asyncio.sleep(2) - return await self.issue_credential( - cred_ex_record, - retries - 1, - ) + else: + rev_reg_id = None + tails_path = None - raise V20CredFormatError( - f"Cred def id {cred_def_id} " "has no active revocation registry" + try: + (cred_json, cred_rev_id) = await issuer.create_credential( + schema, + cred_offer, + cred_request, + cred_values, + rev_reg_id, + tails_path, ) - del revoc + except IndyIssuerRevocationRegistryFullError: + # unlucky, another instance filled the registry first + continue - cred_values = cred_ex_record.cred_offer.credential_preview.attr_dict( - decode=False - ) - issuer = self.profile.inject(IndyIssuer) - try: - (cred_json, cred_rev_id,) = await issuer.create_credential( - schema, - cred_offer, - cred_request, - cred_values, - cred_ex_record.cred_ex_id, - rev_reg_id, - tails_path, + if revocable and rev_reg.max_creds <= int(cred_rev_id): + revoc = IndyRevocation(self.profile) + await revoc.handle_full_registry(rev_reg_id) + del revoc + + result = self.get_format_data(CRED_20_ISSUE, json.loads(cred_json)) + break + + if not result: + raise V20CredFormatError( + f"Cred def '{cred_def_id}' has no active revocation registry" ) + async with self._profile.transaction() as txn: detail_record = V20CredExRecordIndy( cred_ex_id=cred_ex_record.cred_ex_id, rev_reg_id=rev_reg_id, cred_rev_id=cred_rev_id, ) - - # If the rev reg is now full - if rev_reg and rev_reg.max_creds == int(cred_rev_id): - async with self.profile.session() as session: - await active_rev_reg_rec.set_state( - session, - IssuerRevRegRecord.STATE_FULL, - ) - - # Send next 1 rev reg, publish tails file in background - rev_reg_size = active_rev_reg_rec.max_cred_num - await notify_revocation_reg_event( - self.profile, cred_def_id, rev_reg_size, auto_create_rev_reg=True + await detail_record.save(txn, reason="v2.0 issue credential") + + if revocable and cred_rev_id: + issuer_cr_rec = IssuerCredRevRecord( + state=IssuerCredRevRecord.STATE_ISSUED, + cred_ex_id=cred_ex_record.cred_ex_id, + cred_ex_version=IssuerCredRevRecord.VERSION_2, + rev_reg_id=rev_reg_id, + cred_rev_id=cred_rev_id, ) - - async with self.profile.session() as session: - await detail_record.save(session, reason="v2.0 issue credential") - - except IndyIssuerRevocationRegistryFullError: - # unlucky: duelling instance issued last cred near same time as us - async with self.profile.session() as session: - await active_rev_reg_rec.set_state( - session, - IssuerRevRegRecord.STATE_FULL, + await issuer_cr_rec.save( + txn, + reason=( + "Created issuer cred rev record for " + f"rev reg id {rev_reg_id}, index {cred_rev_id}" + ), ) + await txn.commit() - if retries > 0: - # use next rev reg; at worst, lucky instance is putting one up - LOGGER.info( - "Waiting 1s and retrying: revocation registry %s is full", - active_rev_reg_rec.revoc_reg_id, - ) - await asyncio.sleep(1) - return await self.issue_credential( - cred_ex_record, - retries - 1, - ) - - raise - - return self.get_format_data(CRED_20_ISSUE, json.loads(cred_json)) + return result async def receive_credential( self, cred_ex_record: V20CredExRecord, cred_issue_message: V20CredIssue @@ -474,7 +444,11 @@ async def store_credential( cred = cred_ex_record.cred_issue.attachment(IndyCredFormatHandler.format) rev_reg_def = None - ledger_exec_inst = self._profile.inject(IndyLedgerRequestsExecutor) + multitenant_mgr = self.profile.inject_or(BaseMultitenantManager) + if multitenant_mgr: + ledger_exec_inst = IndyLedgerRequestsExecutor(self.profile) + else: + ledger_exec_inst = self.profile.inject(IndyLedgerRequestsExecutor) ledger = ( await ledger_exec_inst.get_ledger_for_identifier( cred["cred_def_id"], diff --git a/aries_cloudagent/protocols/issue_credential/v2_0/formats/indy/tests/test_handler.py b/aries_cloudagent/protocols/issue_credential/v2_0/formats/indy/tests/test_handler.py index 9bffbad2a9..7366437304 100644 --- a/aries_cloudagent/protocols/issue_credential/v2_0/formats/indy/tests/test_handler.py +++ b/aries_cloudagent/protocols/issue_credential/v2_0/formats/indy/tests/test_handler.py @@ -13,6 +13,8 @@ from .......ledger.multiple_ledger.ledger_requests_executor import ( IndyLedgerRequestsExecutor, ) +from .......multitenant.base import BaseMultitenantManager +from .......multitenant.manager import MultitenantManager from .......indy.issuer import IndyIssuer from .......cache.in_memory import InMemoryCache from .......cache.base import BaseCache @@ -497,6 +499,10 @@ async def test_create_offer_attr_mismatch(self): AttachDecorator.data_base64({"cred_def_id": CRED_DEF_ID}, ident="0") ], ) + self.context.injector.bind_instance( + BaseMultitenantManager, + async_mock.MagicMock(MultitenantManager, autospec=True), + ) cred_def_record = StorageRecord( CRED_DEF_SENT_RECORD_TYPE, @@ -516,9 +522,13 @@ async def test_create_offer_attr_mismatch(self): self.issuer.create_credential_offer = async_mock.CoroutineMock( return_value=json.dumps(INDY_OFFER) ) - - with self.assertRaises(V20CredFormatError): - await self.handler.create_offer(cred_proposal) + with async_mock.patch.object( + IndyLedgerRequestsExecutor, + "get_ledger_for_identifier", + async_mock.CoroutineMock(return_value=(None, self.ledger)), + ): + with self.assertRaises(V20CredFormatError): + await self.handler.create_offer(cred_proposal) async def test_create_offer_no_matching_sent_cred_def(self): cred_proposal = V20CredProposal( @@ -602,7 +612,18 @@ async def test_create_request(self): # cover case with no cache in injection context self.context.injector.clear_binding(BaseCache) cred_ex_record._id = "dummy-id3" - await self.handler.create_request(cred_ex_record, {"holder_did": holder_did}) + self.context.injector.bind_instance( + BaseMultitenantManager, + async_mock.MagicMock(MultitenantManager, autospec=True), + ) + with async_mock.patch.object( + IndyLedgerRequestsExecutor, + "get_ledger_for_identifier", + async_mock.CoroutineMock(return_value=(None, self.ledger)), + ): + await self.handler.create_request( + cred_ex_record, {"holder_did": holder_did} + ) async def test_create_request_bad_state(self): cred_ex_record = V20CredExRecord(state=V20CredExRecord.STATE_OFFER_SENT) @@ -657,7 +678,7 @@ async def test_receive_request_no_offer(self): in str(context.exception) ) - async def test_issue_credential(self): + async def test_issue_credential_revocable(self): attr_values = { "legalName": "value", "jurisdictionId": "value", @@ -708,22 +729,17 @@ async def test_issue_credential(self): with async_mock.patch.object( test_module, "IndyRevocation", autospec=True - ) as revoc, async_mock.patch.object( - asyncio, "ensure_future", autospec=True - ) as asyncio_mock: - revoc.return_value.get_active_issuer_rev_reg_record = ( - async_mock.CoroutineMock( - return_value=async_mock.MagicMock( # active_rev_reg_rec + ) as revoc: + revoc.return_value.get_or_create_active_registry = async_mock.CoroutineMock( + return_value=( + async_mock.MagicMock( # active_rev_reg_rec revoc_reg_id=REV_REG_ID, - get_registry=async_mock.CoroutineMock( - return_value=async_mock.MagicMock( # rev_reg - tails_local_path="dummy-path", - get_or_fetch_local_tails_path=( - async_mock.CoroutineMock() - ), - ) - ), - ) + ), + async_mock.MagicMock( # rev_reg + tails_local_path="dummy-path", + get_or_fetch_local_tails_path=(async_mock.CoroutineMock()), + max_creds=10, + ), ) ) @@ -736,7 +752,6 @@ async def test_issue_credential(self): INDY_OFFER, INDY_CRED_REQ, attr_values, - cred_ex_record.cred_ex_id, REV_REG_ID, "dummy-path", ) @@ -802,20 +817,27 @@ async def test_issue_credential_non_revocable(self): self.ledger.get_credential_definition = async_mock.CoroutineMock( return_value=CRED_DEF_NR ) - - (cred_format, attachment) = await self.handler.issue_credential( - cred_ex_record, retries=0 + self.context.injector.bind_instance( + BaseMultitenantManager, + async_mock.MagicMock(MultitenantManager, autospec=True), ) + with async_mock.patch.object( + IndyLedgerRequestsExecutor, + "get_ledger_for_identifier", + async_mock.CoroutineMock(return_value=("test_ledger_id", self.ledger)), + ): + (cred_format, attachment) = await self.handler.issue_credential( + cred_ex_record, retries=0 + ) - self.issuer.create_credential.assert_called_once_with( - SCHEMA, - INDY_OFFER, - INDY_CRED_REQ, - attr_values, - cred_ex_record.cred_ex_id, - None, - None, - ) + self.issuer.create_credential.assert_called_once_with( + SCHEMA, + INDY_OFFER, + INDY_CRED_REQ, + attr_values, + None, + None, + ) # assert identifier match assert cred_format.attach_id == self.handler.format.api == attachment.ident @@ -841,109 +863,6 @@ async def test_issue_credential_not_unique_x(self): assert "indy detail record already exists" in str(context.exception) - async def test_issue_credential_fills_revocation_registry(self): - attr_values = { - "legalName": "value", - "jurisdictionId": "value", - "incorporationDate": "value", - } - cred_rev_id = "1000" - - cred_preview = V20CredPreview( - attributes=[ - V20CredAttrSpec(name=k, value=v) for (k, v) in attr_values.items() - ] - ) - cred_offer = V20CredOffer( - credential_preview=cred_preview, - formats=[ - V20CredFormat( - attach_id="0", - format_=ATTACHMENT_FORMAT[CRED_20_OFFER][ - V20CredFormat.Format.INDY.api - ], - ) - ], - offers_attach=[AttachDecorator.data_base64(INDY_OFFER, ident="0")], - ) - cred_request = V20CredRequest( - formats=[ - V20CredFormat( - attach_id="0", - format_=ATTACHMENT_FORMAT[CRED_20_REQUEST][ - V20CredFormat.Format.INDY.api - ], - ) - ], - requests_attach=[AttachDecorator.data_base64(INDY_CRED_REQ, ident="0")], - ) - - cred_ex_record = V20CredExRecord( - cred_ex_id="dummy-cxid", - cred_offer=cred_offer.serialize(), - cred_request=cred_request.serialize(), - initiator=V20CredExRecord.INITIATOR_SELF, - role=V20CredExRecord.ROLE_ISSUER, - state=V20CredExRecord.STATE_REQUEST_RECEIVED, - ) - - self.issuer.create_credential = async_mock.CoroutineMock( - return_value=(json.dumps(INDY_CRED), cred_rev_id) - ) - - with async_mock.patch.object( - test_module, "IndyRevocation", autospec=True - ) as revoc, async_mock.patch.object( - asyncio, "ensure_future", autospec=True - ) as asyncio_mock: - revoc.return_value = async_mock.MagicMock( - get_active_issuer_rev_reg_record=( - async_mock.CoroutineMock( - return_value=async_mock.MagicMock( # active_rev_reg_rec - revoc_reg_id=REV_REG_ID, - get_registry=async_mock.CoroutineMock( - return_value=async_mock.MagicMock( # rev_reg - tails_local_path="dummy-path", - max_creds=1000, - get_or_fetch_local_tails_path=( - async_mock.CoroutineMock() - ), - ) - ), - set_state=async_mock.CoroutineMock(), - ) - ) - ), - init_issuer_registry=async_mock.CoroutineMock( - return_value=async_mock.MagicMock( # pending_rev_reg_rec - stage_pending_registry=async_mock.CoroutineMock() - ) - ), - ) - - (cred_format, attachment) = await self.handler.issue_credential( - cred_ex_record, retries=0 - ) - - self.issuer.create_credential.assert_called_once_with( - SCHEMA, - INDY_OFFER, - INDY_CRED_REQ, - attr_values, - cred_ex_record.cred_ex_id, - REV_REG_ID, - "dummy-path", - ) - - # assert identifier match - assert cred_format.attach_id == self.handler.format.api == attachment.ident - - # assert content of attachment is proposal data - assert attachment.content == INDY_CRED - - # assert data is encoded as base64 - assert attachment.data.base64 - async def test_issue_credential_no_active_rr_no_retries(self): attr_values = { "legalName": "value", @@ -995,22 +914,11 @@ async def test_issue_credential_no_active_rr_no_retries(self): ) with async_mock.patch.object( - test_module, "IssuerRevRegRecord", autospec=True - ) as issuer_rr_rec, async_mock.patch.object( test_module, "IndyRevocation", autospec=True ) as revoc: - revoc.return_value.get_active_issuer_rev_reg_record = ( - async_mock.CoroutineMock(side_effect=StorageNotFoundError()) - ) - revoc.return_value.init_issuer_registry = async_mock.CoroutineMock( - return_value=async_mock.MagicMock( # pending_rev_reg_rec - stage_pending_registry=async_mock.CoroutineMock() - ) - ) - issuer_rr_rec.query_by_cred_def_id = async_mock.CoroutineMock( - return_value=[] + revoc.return_value.get_or_create_active_registry = async_mock.CoroutineMock( + return_value=() ) - with self.assertRaises(V20CredFormatError) as context: await self.handler.issue_credential(cred_ex_record, retries=0) assert "has no active revocation registry" in str(context.exception) @@ -1066,24 +974,22 @@ async def test_issue_credential_no_active_rr_retry(self): ) with async_mock.patch.object( - test_module, "IssuerRevRegRecord", autospec=True - ) as issuer_rr_rec, async_mock.patch.object( test_module, "IndyRevocation", autospec=True ) as revoc: - revoc.return_value.get_active_issuer_rev_reg_record = ( - async_mock.CoroutineMock(side_effect=StorageNotFoundError()) - ) - issuer_rr_rec.query_by_cred_def_id = async_mock.CoroutineMock( + revoc.return_value.get_or_create_active_registry = async_mock.CoroutineMock( side_effect=[ - [], # posted_rev_reg_recs - [async_mock.MagicMock(max_cred_num=1000)], # old_rev_reg_recs + None, + ( + async_mock.MagicMock( # active_rev_reg_rec + revoc_reg_id=REV_REG_ID, + set_state=async_mock.CoroutineMock(), + ), + async_mock.MagicMock( # rev_reg + tails_local_path="dummy-path", + get_or_fetch_local_tails_path=(async_mock.CoroutineMock()), + ), + ), ] - * 2 - ) - revoc.return_value.init_issuer_registry = async_mock.CoroutineMock( - return_value=async_mock.MagicMock( # pending_rev_reg_rec - stage_pending_registry=async_mock.CoroutineMock() - ) ) with self.assertRaises(V20CredFormatError) as context: @@ -1142,25 +1048,22 @@ async def test_issue_credential_rr_full(self): with async_mock.patch.object( test_module, "IndyRevocation", autospec=True ) as revoc: - revoc.return_value.get_active_issuer_rev_reg_record = ( - async_mock.CoroutineMock( - return_value=async_mock.MagicMock( # active_rev_reg_rec + revoc.return_value.get_or_create_active_registry = async_mock.CoroutineMock( + return_value=( + async_mock.MagicMock( # active_rev_reg_rec revoc_reg_id=REV_REG_ID, set_state=async_mock.CoroutineMock(), - get_registry=async_mock.CoroutineMock( - return_value=async_mock.MagicMock( # rev_reg - tails_local_path="dummy-path", - get_or_fetch_local_tails_path=( - async_mock.CoroutineMock() - ), - ) - ), - ) + ), + async_mock.MagicMock( # rev_reg + tails_local_path="dummy-path", + get_or_fetch_local_tails_path=(async_mock.CoroutineMock()), + ), ) ) - with self.assertRaises(test_module.IndyIssuerRevocationRegistryFullError): + with self.assertRaises(V20CredFormatError) as context: await self.handler.issue_credential(cred_ex_record, retries=1) + assert "has no active revocation registry" in str(context.exception) async def test_receive_credential(self): cred_ex_record = async_mock.MagicMock() @@ -1254,8 +1157,15 @@ async def test_store_credential(self): with self.assertRaises(V20CredFormatError) as context: await self.handler.store_credential(stored_cx_rec, cred_id=cred_id) assert "No credential exchange " in str(context.exception) - + self.context.injector.bind_instance( + BaseMultitenantManager, + async_mock.MagicMock(MultitenantManager, autospec=True), + ) with async_mock.patch.object( + IndyLedgerRequestsExecutor, + "get_ledger_for_identifier", + async_mock.CoroutineMock(return_value=("test_ledger_id", self.ledger)), + ), async_mock.patch.object( test_module, "RevocationRegistry", autospec=True ) as mock_rev_reg, async_mock.patch.object( test_module.IndyCredFormatHandler, "get_detail_record", autospec=True diff --git a/aries_cloudagent/protocols/issue_credential/v2_0/formats/ld_proof/handler.py b/aries_cloudagent/protocols/issue_credential/v2_0/formats/ld_proof/handler.py index e2ede57342..747c112235 100644 --- a/aries_cloudagent/protocols/issue_credential/v2_0/formats/ld_proof/handler.py +++ b/aries_cloudagent/protocols/issue_credential/v2_0/formats/ld_proof/handler.py @@ -1,17 +1,17 @@ """V2.0 issue-credential linked data proof credential format handler.""" + from ......vc.ld_proofs.error import LinkedDataProofException from ......vc.ld_proofs.check import get_properties_without_context import logging -from typing import Mapping +from typing import Mapping, Optional from marshmallow import EXCLUDE, INCLUDE from pyld import jsonld from pyld.jsonld import JsonLdProcessor -from ......did.did_key import DIDKey from ......messaging.decorators.attach_decorator import AttachDecorator from ......storage.vc_holder.base import VCHolder from ......storage.vc_holder.vc_record import VCRecord @@ -34,8 +34,11 @@ ) from ......vc.ld_proofs.constants import SECURITY_CONTEXT_BBS_URL from ......wallet.base import BaseWallet, DIDInfo +from ......wallet.default_verification_key_strategy import ( + BaseVerificationKeyStrategy, +) from ......wallet.error import WalletNotFoundError -from ......wallet.key_type import KeyType +from ......wallet.key_type import BLS12381G2, ED25519 from ...message_types import ( ATTACHMENT_FORMAT, @@ -64,20 +67,20 @@ AuthenticationProofPurpose.term, } SUPPORTED_ISSUANCE_SUITES = {Ed25519Signature2018} -SIGNATURE_SUITE_KEY_TYPE_MAPPING = {Ed25519Signature2018: KeyType.ED25519} +SIGNATURE_SUITE_KEY_TYPE_MAPPING = {Ed25519Signature2018: ED25519} # We only want to add bbs suites to supported if the module is installed if BbsBlsSignature2020.BBS_SUPPORTED: SUPPORTED_ISSUANCE_SUITES.add(BbsBlsSignature2020) - SIGNATURE_SUITE_KEY_TYPE_MAPPING[BbsBlsSignature2020] = KeyType.BLS12381G2 + SIGNATURE_SUITE_KEY_TYPE_MAPPING[BbsBlsSignature2020] = BLS12381G2 PROOF_TYPE_SIGNATURE_SUITE_MAPPING = { - suite.signature_type: suite - for suite, key_type in SIGNATURE_SUITE_KEY_TYPE_MAPPING.items() + suite.signature_type: suite for suite in SIGNATURE_SUITE_KEY_TYPE_MAPPING } + KEY_TYPE_SIGNATURE_SUITE_MAPPING = { key_type: suite for suite, key_type in SIGNATURE_SUITE_KEY_TYPE_MAPPING.items() } @@ -128,14 +131,14 @@ async def get_detail_record(self, cred_ex_id: str) -> V20CredExRecordLDProof: session, cred_ex_id ) - if len(records) > 1: - LOGGER.warning( - "Cred ex id %s has %d %s detail records: should be 1", - cred_ex_id, - len(records), - LDProofCredFormatHandler.format.api, - ) - return records[0] if records else None + if len(records) > 1: + LOGGER.warning( + "Cred ex id %s has %d %s detail records: should be 1", + cred_ex_id, + len(records), + LDProofCredFormatHandler.format.api, + ) + return records[0] if records else None def get_format_identifier(self, message_type: str) -> str: """Get attachment format identifier for format and message combination. @@ -252,7 +255,9 @@ async def _did_info_for_did(self, did: str) -> DIDInfo: # All other methods we can just query return await wallet.get_local_did(did) - async def _get_suite_for_detail(self, detail: LDProofVCDetail) -> LinkedDataProof: + async def _get_suite_for_detail( + self, detail: LDProofVCDetail, verification_method: Optional[str] = None + ) -> LinkedDataProof: issuer_id = detail.credential.issuer_id proof_type = detail.options.proof_type @@ -267,7 +272,18 @@ async def _get_suite_for_detail(self, detail: LDProofVCDetail) -> LinkedDataProo ) did_info = await self._did_info_for_did(issuer_id) - verification_method = self._get_verification_method(issuer_id) + verkey_id_strategy = self.profile.context.inject(BaseVerificationKeyStrategy) + verification_method = ( + verification_method + or await verkey_id_strategy.get_verification_method_id_for_did( + issuer_id, self.profile, proof_purpose="assertionMethod" + ) + ) + + if verification_method is None: + raise V20CredFormatError( + f"Unable to get retrieve verification method for did {issuer_id}" + ) suite = await self._get_suite( proof_type=proof_type, @@ -304,19 +320,6 @@ async def _get_suite( ), ) - def _get_verification_method(self, did: str): - """Get the verification method for a did.""" - - if did.startswith("did:key:"): - return DIDKey.from_did(did).key_id - elif did.startswith("did:sov:"): - # key-1 is what the resolver uses for key id - return did + "#key-1" - else: - raise V20CredFormatError( - f"Unable to get retrieve verification method for did {did}" - ) - def _get_proof_purpose( self, *, proof_purpose: str = None, challenge: str = None, domain: str = None ) -> ProofPurpose: @@ -456,7 +459,9 @@ async def receive_request( """Receive linked data proof request.""" async def issue_credential( - self, cred_ex_record: V20CredExRecord, retries: int = 5 + self, + cred_ex_record: V20CredExRecord, + retries: int = 5, ) -> CredFormatAttachment: """Issue linked data proof credential.""" if not cred_ex_record.cred_request: @@ -471,7 +476,9 @@ async def issue_credential( detail = await self._prepare_detail(detail) # Get signature suite, proof purpose and document loader - suite = await self._get_suite_for_detail(detail) + suite = await self._get_suite_for_detail( + detail, cred_ex_record.verification_method + ) proof_purpose = self._get_proof_purpose( proof_purpose=detail.options.proof_purpose, challenge=detail.options.challenge, @@ -503,7 +510,7 @@ async def receive_credential( # Remove values from cred that are not part of detail cred_dict.pop("proof") - credential_status = cred_dict.pop("credentialStatus", None) + credential_status = cred_dict.get("credentialStatus", None) detail_status = detail.options.credential_status if cred_dict != detail_dict["credential"]: @@ -591,7 +598,7 @@ async def store_credential( raise V20CredFormatError(f"Received invalid credential: {result}") # Saving expanded type as a cred_tag - expanded = jsonld.expand(cred_dict) + expanded = jsonld.expand(cred_dict, options={"documentLoader": document_loader}) types = JsonLdProcessor.get_values( expanded[0], "@type", diff --git a/aries_cloudagent/protocols/issue_credential/v2_0/formats/ld_proof/tests/test_handler.py b/aries_cloudagent/protocols/issue_credential/v2_0/formats/ld_proof/tests/test_handler.py index c4666a679e..4c059e52cd 100644 --- a/aries_cloudagent/protocols/issue_credential/v2_0/formats/ld_proof/tests/test_handler.py +++ b/aries_cloudagent/protocols/issue_credential/v2_0/formats/ld_proof/tests/test_handler.py @@ -26,9 +26,13 @@ ) from .......vc.ld_proofs.constants import SECURITY_CONTEXT_BBS_URL from .......vc.tests.document_loader import custom_document_loader -from .......wallet.key_type import KeyType +from .......wallet.default_verification_key_strategy import ( + DefaultVerificationKeyStrategy, + BaseVerificationKeyStrategy, +) +from .......wallet.key_type import BLS12381G2, ED25519 from .......wallet.error import WalletNotFoundError -from .......wallet.did_method import DIDMethod +from .......wallet.did_method import SOV from .......wallet.base import BaseWallet from ....models.detail.ld_proof import V20CredExRecordLDProof @@ -48,7 +52,7 @@ CRED_20_ISSUE, ) -from ...handler import LOGGER, V20CredFormatError +from ...handler import V20CredFormatError from ..handler import LDProofCredFormatHandler from ..handler import LOGGER as LD_PROOF_LOGGER @@ -124,6 +128,11 @@ async def setUp(self): # Set custom document loader self.context.injector.bind_instance(DocumentLoader, custom_document_loader) + # Set default verkey ID strategy + self.context.injector.bind_instance( + BaseVerificationKeyStrategy, DefaultVerificationKeyStrategy() + ) + self.handler = LDProofCredFormatHandler(self.profile) self.cred_proposal = V20CredProposal( @@ -217,8 +226,8 @@ async def test_assert_can_issue_with_id_and_proof_type(self): did=TEST_DID_SOV, verkey="verkey", metadata={}, - method=DIDMethod.SOV, - key_type=KeyType.ED25519, + method=SOV, + key_type=ED25519, ) mock_did_info.return_value = did_info await self.handler._assert_can_issue_with_id_and_proof_type( @@ -229,8 +238,8 @@ async def test_assert_can_issue_with_id_and_proof_type(self): did=TEST_DID_SOV, verkey="verkey", metadata={}, - method=DIDMethod.SOV, - key_type=KeyType.BLS12381G2, + method=SOV, + key_type=BLS12381G2, ) mock_did_info.return_value = invalid_did_info with self.assertRaises(V20CredFormatError) as context: @@ -274,14 +283,13 @@ async def test_get_suite_for_detail(self): "_did_info_for_did", async_mock.CoroutineMock(), ) as mock_did_info: - suite = await self.handler._get_suite_for_detail(detail) assert suite.signature_type == detail.options.proof_type assert type(suite) == Ed25519Signature2018 assert suite.verification_method == DIDKey.from_did(TEST_DID_KEY).key_id assert suite.proof == {"created": LD_PROOF_VC_DETAIL["options"]["created"]} - assert suite.key_pair.key_type == KeyType.ED25519 + assert suite.key_pair.key_type == ED25519 assert suite.key_pair.public_key_base58 == mock_did_info.return_value.verkey mock_can_issue.assert_called_once_with( @@ -303,7 +311,7 @@ async def test_get_suite(self): assert type(suite) == BbsBlsSignature2020 assert suite.verification_method == "verification_method" assert suite.proof == proof - assert suite.key_pair.key_type == KeyType.BLS12381G2 + assert suite.key_pair.key_type == BLS12381G2 assert suite.key_pair.public_key_base58 == did_info.verkey suite = await self.handler._get_suite( @@ -316,27 +324,9 @@ async def test_get_suite(self): assert type(suite) == Ed25519Signature2018 assert suite.verification_method == "verification_method" assert suite.proof == proof - assert suite.key_pair.key_type == KeyType.ED25519 + assert suite.key_pair.key_type == ED25519 assert suite.key_pair.public_key_base58 == did_info.verkey - async def test_get_verification_method(self): - assert ( - self.handler._get_verification_method(TEST_DID_KEY) - == DIDKey.from_did(TEST_DID_KEY).key_id - ) - - assert ( - self.handler._get_verification_method(TEST_DID_SOV) - == TEST_DID_SOV + "#key-1" - ) - - with self.assertRaises(V20CredFormatError) as context: - self.handler._get_verification_method("did:random:not-supported") - - assert "Unable to get retrieve verification method for did" in str( - context.exception - ) - async def test_get_proof_purpose(self): purpose = self.handler._get_proof_purpose() assert type(purpose) == CredentialIssuancePurpose @@ -589,7 +579,7 @@ async def test_issue_credential(self): detail = LDProofVCDetail.deserialize(LD_PROOF_VC_DETAIL) - mock_get_suite.assert_called_once_with(detail) + mock_get_suite.assert_called_once_with(detail, None) mock_issue.assert_called_once_with( credential=LD_PROOF_VC_DETAIL["credential"], suite=mock_get_suite.return_value, @@ -770,13 +760,14 @@ async def test_receive_credential_x_credential_status_ne(self): async def test_receive_credential_x_credential_status_ne_both_set(self): detail = deepcopy(LD_PROOF_VC_DETAIL) + status_entry = {"type": "SomeRandomType"} - # Set credential status so it's only set on the detail - # not the issued credential + # Set credential status in both request and reference credential detail["options"]["credentialStatus"] = {"type": "CredentialStatusType"} + detail["credential"]["credentialStatus"] = deepcopy(status_entry) vc = deepcopy(LD_PROOF_VC) - vc["credentialStatus"] = {"type": "SomeRandomType"} + vc["credentialStatus"] = deepcopy(status_entry) cred_issue = V20CredIssue( formats=[ diff --git a/aries_cloudagent/protocols/issue_credential/v2_0/handlers/cred_ack_handler.py b/aries_cloudagent/protocols/issue_credential/v2_0/handlers/cred_ack_handler.py index bd7a4404d7..9c580fd01d 100644 --- a/aries_cloudagent/protocols/issue_credential/v2_0/handlers/cred_ack_handler.py +++ b/aries_cloudagent/protocols/issue_credential/v2_0/handlers/cred_ack_handler.py @@ -1,5 +1,6 @@ """Credential ack message handler.""" +from .....core.oob_processor import OobMessageProcessor from .....messaging.base_handler import BaseHandler, HandlerException from .....messaging.request_context import RequestContext from .....messaging.responder import BaseResponder @@ -29,12 +30,27 @@ async def handle(self, context: RequestContext, responder: BaseResponder): context.message.serialize(as_string=True), ) - if not context.connection_ready: - raise HandlerException("No connection established for credential ack") + # If connection is present it must be ready for use + if context.connection_record and not context.connection_ready: + raise HandlerException("Connection used for credential ack not ready") + + # Find associated oob record + oob_processor = context.inject(OobMessageProcessor) + oob_record = await oob_processor.find_oob_record_for_inbound_message(context) + + # Either connection or oob context must be present + if not context.connection_record and not oob_record: + raise HandlerException( + "No connection or associated connectionless exchange found for credential" + " ack" + ) cred_manager = V20CredManager(context.profile) await cred_manager.receive_credential_ack( - context.message, context.connection_record.connection_id + context.message, + context.connection_record.connection_id + if context.connection_record + else None, ) trace_event( diff --git a/aries_cloudagent/protocols/issue_credential/v2_0/handlers/cred_issue_handler.py b/aries_cloudagent/protocols/issue_credential/v2_0/handlers/cred_issue_handler.py index d33c44be5f..ee148857f1 100644 --- a/aries_cloudagent/protocols/issue_credential/v2_0/handlers/cred_issue_handler.py +++ b/aries_cloudagent/protocols/issue_credential/v2_0/handlers/cred_issue_handler.py @@ -1,5 +1,6 @@ """Credential issue message handler.""" +from .....core.oob_processor import OobMessageProcessor from .....indy.holder import IndyHolderError from .....messaging.base_handler import BaseHandler, HandlerException from .....messaging.models.base import BaseModelError @@ -35,12 +36,26 @@ async def handle(self, context: RequestContext, responder: BaseResponder): context.message.serialize(as_string=True), ) - if not context.connection_ready: - raise HandlerException("No connection established for credential issue") + # If connection is present it must be ready for use + if context.connection_record and not context.connection_ready: + raise HandlerException("Connection used for credential not ready") + + # Find associated oob record + oob_processor = context.inject(OobMessageProcessor) + oob_record = await oob_processor.find_oob_record_for_inbound_message(context) + + # Either connection or oob context must be present + if not context.connection_record and not oob_record: + raise HandlerException( + "No connection or associated connectionless exchange found for credential" + ) cred_manager = V20CredManager(context.profile) cred_ex_record = await cred_manager.receive_credential( - context.message, context.connection_record.connection_id + context.message, + context.connection_record.connection_id + if context.connection_record + else None, ) # mgr only finds, saves record: on exception, saving null state is hopeless r_time = trace_event( @@ -61,7 +76,7 @@ async def handle(self, context: RequestContext, responder: BaseResponder): V20CredManagerError, ) as err: # treat failure to store as mangled on receipt hence protocol error - self._logger.exception(err) + self._logger.exception("Error storing issued credential") if cred_ex_record: async with context.profile.session() as session: await cred_ex_record.save_error_state( diff --git a/aries_cloudagent/protocols/issue_credential/v2_0/handlers/cred_offer_handler.py b/aries_cloudagent/protocols/issue_credential/v2_0/handlers/cred_offer_handler.py index aaab690de6..9fc223c4c0 100644 --- a/aries_cloudagent/protocols/issue_credential/v2_0/handlers/cred_offer_handler.py +++ b/aries_cloudagent/protocols/issue_credential/v2_0/handlers/cred_offer_handler.py @@ -1,5 +1,7 @@ """Credential offer message handler.""" +from .....wallet.util import default_did_from_verkey +from .....core.oob_processor import OobMessageProcessor from .....indy.holder import IndyHolderError from .....ledger.error import LedgerError from .....messaging.base_handler import BaseHandler, HandlerException @@ -36,13 +38,31 @@ async def handle(self, context: RequestContext, responder: BaseResponder): context.message.serialize(as_string=True), ) - if not context.connection_ready: - raise HandlerException("No connection established for credential offer") + # If connection is present it must be ready for use + if context.connection_record and not context.connection_ready: + raise HandlerException("Connection used for credential offer not ready") + + # Find associated oob record + oob_processor = context.inject(OobMessageProcessor) + oob_record = await oob_processor.find_oob_record_for_inbound_message(context) + + # Either connection or oob context must be present + if not context.connection_record and not oob_record: + raise HandlerException( + "No connection or associated connectionless exchange found for credential" + " offer" + ) + + connection_id = ( + context.connection_record.connection_id + if context.connection_record + else None + ) profile = context.profile cred_manager = V20CredManager(profile) cred_ex_record = await cred_manager.receive_offer( - context.message, context.connection_record.connection_id + context.message, connection_id ) # mgr only finds, saves record: on exception, saving state null is hopeless r_time = trace_event( @@ -52,13 +72,19 @@ async def handle(self, context: RequestContext, responder: BaseResponder): perf_counter=r_time, ) + if context.connection_record: + holder_did = context.connection_record.my_did + else: + # Transform recipient key into did + holder_did = default_did_from_verkey(oob_record.our_recipient_key) + # If auto respond is turned on, automatically reply with credential request if context.settings.get("debug.auto_respond_credential_offer"): cred_request_message = None try: (_, cred_request_message) = await cred_manager.create_request( cred_ex_record=cred_ex_record, - holder_did=context.connection_record.my_did, + holder_did=holder_did, ) await responder.send_reply(cred_request_message) except ( @@ -68,7 +94,7 @@ async def handle(self, context: RequestContext, responder: BaseResponder): StorageError, V20CredManagerError, ) as err: - self._logger.exception(err) + self._logger.exception("Error responding to credential offer") if cred_ex_record: async with profile.session() as session: await cred_ex_record.save_error_state( diff --git a/aries_cloudagent/protocols/issue_credential/v2_0/handlers/cred_problem_report_handler.py b/aries_cloudagent/protocols/issue_credential/v2_0/handlers/cred_problem_report_handler.py index 18b564dace..32515d1365 100644 --- a/aries_cloudagent/protocols/issue_credential/v2_0/handlers/cred_problem_report_handler.py +++ b/aries_cloudagent/protocols/issue_credential/v2_0/handlers/cred_problem_report_handler.py @@ -1,6 +1,6 @@ """Credential problem report message handler.""" -from .....messaging.base_handler import BaseHandler +from .....messaging.base_handler import BaseHandler, HandlerException from .....messaging.request_context import RequestContext from .....messaging.responder import BaseResponder from .....storage.error import StorageError, StorageNotFoundError @@ -26,6 +26,16 @@ async def handle(self, context: RequestContext, responder: BaseResponder): ) assert isinstance(context.message, V20CredProblemReport) + # If connection is present it must be ready for use + if context.connection_record and not context.connection_ready: + raise HandlerException( + "Connection used for credential problem report not ready" + ) + elif not context.connection_record: + raise HandlerException( + "Connectionless not supported for credential problem report" + ) + cred_manager = V20CredManager(context.profile) try: await cred_manager.receive_problem_report( diff --git a/aries_cloudagent/protocols/issue_credential/v2_0/handlers/cred_proposal_handler.py b/aries_cloudagent/protocols/issue_credential/v2_0/handlers/cred_proposal_handler.py index c9f2be9e8a..e59e9a36d2 100644 --- a/aries_cloudagent/protocols/issue_credential/v2_0/handlers/cred_proposal_handler.py +++ b/aries_cloudagent/protocols/issue_credential/v2_0/handlers/cred_proposal_handler.py @@ -36,8 +36,13 @@ async def handle(self, context: RequestContext, responder: BaseResponder): context.message.serialize(as_string=True), ) - if not context.connection_ready: - raise HandlerException("No connection established for credential proposal") + # If connection is present it must be ready for use + if context.connection_record and not context.connection_ready: + raise HandlerException("Connection used for credential proposal not ready") + elif not context.connection_record: + raise HandlerException( + "Connectionless not supported for credential proposal" + ) profile = context.profile cred_manager = V20CredManager(profile) @@ -69,7 +74,7 @@ async def handle(self, context: RequestContext, responder: BaseResponder): StorageError, V20CredManagerError, ) as err: - self._logger.exception(err) + self._logger.exception("Error responding to credential proposal") async with profile.session() as session: await cred_ex_record.save_error_state( session, diff --git a/aries_cloudagent/protocols/issue_credential/v2_0/handlers/cred_request_handler.py b/aries_cloudagent/protocols/issue_credential/v2_0/handlers/cred_request_handler.py index 789bb00f20..1bdf7671e9 100644 --- a/aries_cloudagent/protocols/issue_credential/v2_0/handlers/cred_request_handler.py +++ b/aries_cloudagent/protocols/issue_credential/v2_0/handlers/cred_request_handler.py @@ -1,5 +1,6 @@ """Credential request message handler.""" +from .....core.oob_processor import OobMessageProcessor from .....indy.issuer import IndyIssuerError from .....ledger.error import LedgerError from .....messaging.base_handler import BaseHandler, HandlerException @@ -36,13 +37,26 @@ async def handle(self, context: RequestContext, responder: BaseResponder): context.message.serialize(as_string=True), ) - if not context.connection_ready: - raise HandlerException("No connection established for credential request") + # If connection is present it must be ready for use + if context.connection_record and not context.connection_ready: + raise HandlerException("Connection used for credential request not ready") + + # Find associated oob record. If the credential offer was created as an oob + # attachment the presentation exchange record won't have a connection id (yet) + oob_processor = context.inject(OobMessageProcessor) + oob_record = await oob_processor.find_oob_record_for_inbound_message(context) + + # Either connection or oob context must be present + if not context.connection_record and not oob_record: + raise HandlerException( + "No connection or associated connectionless exchange found for credential" + " request" + ) profile = context.profile cred_manager = V20CredManager(profile) cred_ex_record = await cred_manager.receive_request( - context.message, context.connection_record.connection_id + context.message, context.connection_record, oob_record ) # mgr only finds, saves record: on exception, saving state null is hopeless r_time = trace_event( diff --git a/aries_cloudagent/protocols/issue_credential/v2_0/handlers/tests/test_cred_ack_handler.py b/aries_cloudagent/protocols/issue_credential/v2_0/handlers/tests/test_cred_ack_handler.py index 8d047435b4..f4283168fa 100644 --- a/aries_cloudagent/protocols/issue_credential/v2_0/handlers/tests/test_cred_ack_handler.py +++ b/aries_cloudagent/protocols/issue_credential/v2_0/handlers/tests/test_cred_ack_handler.py @@ -1,5 +1,6 @@ from asynctest import mock as async_mock, TestCase as AsyncTestCase +from ......core.oob_processor import OobMessageProcessor from ......messaging.request_context import RequestContext from ......messaging.responder import MockResponder from ......transport.inbound.receipt import MessageReceipt @@ -15,6 +16,13 @@ async def test_called(self): request_context.message_receipt = MessageReceipt() request_context.connection_record = async_mock.MagicMock() + mock_oob_processor = async_mock.MagicMock( + find_oob_record_for_inbound_message=async_mock.CoroutineMock( + return_value=async_mock.MagicMock() + ) + ) + request_context.injector.bind_instance(OobMessageProcessor, mock_oob_processor) + with async_mock.patch.object( test_module, "V20CredManager", autospec=True ) as mock_cred_mgr: @@ -32,6 +40,9 @@ async def test_called(self): request_context.message, request_context.connection_record.connection_id, ) + mock_oob_processor.find_oob_record_for_inbound_message.assert_called_once_with( + request_context + ) assert not responder.messages async def test_called_not_ready(self): @@ -47,7 +58,34 @@ async def test_called_not_ready(self): request_context.connection_ready = False handler = test_module.V20CredAckHandler() responder = MockResponder() - with self.assertRaises(test_module.HandlerException): + with self.assertRaises(test_module.HandlerException) as err: await handler.handle(request_context, responder) + assert ( + err.exception.message == "Connection used for credential ack not ready" + ) + + assert not responder.messages + + async def test_called_no_connection_no_oob(self): + request_context = RequestContext.test_context() + request_context.message_receipt = MessageReceipt() + + mock_oob_processor = async_mock.MagicMock( + find_oob_record_for_inbound_message=async_mock.CoroutineMock( + # No oob record found + return_value=None + ) + ) + request_context.injector.bind_instance(OobMessageProcessor, mock_oob_processor) + + request_context.message = V20CredAck() + handler = test_module.V20CredAckHandler() + responder = MockResponder() + with self.assertRaises(test_module.HandlerException) as err: + await handler.handle(request_context, responder) + assert ( + err.exception.message + == "No connection or associated connectionless exchange found for credential ack" + ) assert not responder.messages diff --git a/aries_cloudagent/protocols/issue_credential/v2_0/handlers/tests/test_cred_issue_handler.py b/aries_cloudagent/protocols/issue_credential/v2_0/handlers/tests/test_cred_issue_handler.py index 26a848ba5e..2a47af1d5d 100644 --- a/aries_cloudagent/protocols/issue_credential/v2_0/handlers/tests/test_cred_issue_handler.py +++ b/aries_cloudagent/protocols/issue_credential/v2_0/handlers/tests/test_cred_issue_handler.py @@ -1,5 +1,6 @@ from asynctest import mock as async_mock, TestCase as AsyncTestCase +from ......core.oob_processor import OobMessageProcessor from ......messaging.request_context import RequestContext from ......messaging.responder import MockResponder from ......transport.inbound.receipt import MessageReceipt @@ -16,6 +17,13 @@ async def test_called(self): request_context.settings["debug.auto_store_credential"] = False request_context.connection_record = async_mock.MagicMock() + mock_oob_processor = async_mock.MagicMock( + find_oob_record_for_inbound_message=async_mock.CoroutineMock( + return_value=async_mock.MagicMock() + ) + ) + request_context.injector.bind_instance(OobMessageProcessor, mock_oob_processor) + with async_mock.patch.object( test_module, "V20CredManager", autospec=True ) as mock_cred_mgr: @@ -30,6 +38,9 @@ async def test_called(self): mock_cred_mgr.return_value.receive_credential.assert_called_once_with( request_context.message, request_context.connection_record.connection_id ) + mock_oob_processor.find_oob_record_for_inbound_message.assert_called_once_with( + request_context + ) assert not responder.messages async def test_called_auto_store(self): @@ -38,6 +49,13 @@ async def test_called_auto_store(self): request_context.settings["debug.auto_store_credential"] = True request_context.connection_record = async_mock.MagicMock() + mock_oob_processor = async_mock.MagicMock( + find_oob_record_for_inbound_message=async_mock.CoroutineMock( + return_value=async_mock.MagicMock() + ) + ) + request_context.injector.bind_instance(OobMessageProcessor, mock_oob_processor) + with async_mock.patch.object( test_module, "V20CredManager", autospec=True ) as mock_cred_mgr: @@ -56,6 +74,9 @@ async def test_called_auto_store(self): mock_cred_mgr.return_value.receive_credential.assert_called_once_with( request_context.message, request_context.connection_record.connection_id ) + mock_oob_processor.find_oob_record_for_inbound_message.assert_called_once_with( + request_context + ) assert mock_cred_mgr.return_value.send_cred_ack.call_count == 1 async def test_called_auto_store_x(self): @@ -64,6 +85,13 @@ async def test_called_auto_store_x(self): request_context.settings["debug.auto_store_credential"] = True request_context.connection_record = async_mock.MagicMock() + mock_oob_processor = async_mock.MagicMock( + find_oob_record_for_inbound_message=async_mock.CoroutineMock( + return_value=async_mock.MagicMock() + ) + ) + request_context.injector.bind_instance(OobMessageProcessor, mock_oob_processor) + with async_mock.patch.object( test_module, "V20CredManager", autospec=True ) as mock_cred_mgr: @@ -94,6 +122,7 @@ async def test_called_auto_store_x(self): async def test_called_not_ready(self): request_context = RequestContext.test_context() request_context.message_receipt = MessageReceipt() + request_context.connection_record = async_mock.MagicMock() with async_mock.patch.object( test_module, "V20CredManager", autospec=True @@ -103,7 +132,32 @@ async def test_called_not_ready(self): request_context.connection_ready = False handler_inst = test_module.V20CredIssueHandler() responder = MockResponder() - with self.assertRaises(test_module.HandlerException): + with self.assertRaises(test_module.HandlerException) as err: await handler_inst.handle(request_context, responder) + assert err.exception.message == "Connection used for credential not ready" + + assert not responder.messages + + async def test_called_no_connection_no_oob(self): + request_context = RequestContext.test_context() + request_context.message_receipt = MessageReceipt() + + mock_oob_processor = async_mock.MagicMock( + find_oob_record_for_inbound_message=async_mock.CoroutineMock( + # No oob record found + return_value=None + ) + ) + request_context.injector.bind_instance(OobMessageProcessor, mock_oob_processor) + + request_context.message = V20CredIssue() + handler_inst = test_module.V20CredIssueHandler() + responder = MockResponder() + with self.assertRaises(test_module.HandlerException) as err: + await handler_inst.handle(request_context, responder) + assert ( + err.exception.message + == "No connection or associated connectionless exchange found for credential" + ) assert not responder.messages diff --git a/aries_cloudagent/protocols/issue_credential/v2_0/handlers/tests/test_cred_offer_handler.py b/aries_cloudagent/protocols/issue_credential/v2_0/handlers/tests/test_cred_offer_handler.py index 9bd47ff2e5..66e295ce34 100644 --- a/aries_cloudagent/protocols/issue_credential/v2_0/handlers/tests/test_cred_offer_handler.py +++ b/aries_cloudagent/protocols/issue_credential/v2_0/handlers/tests/test_cred_offer_handler.py @@ -1,5 +1,6 @@ from asynctest import mock as async_mock, TestCase as AsyncTestCase +from ......core.oob_processor import OobMessageProcessor from ......messaging.request_context import RequestContext from ......messaging.responder import MockResponder from ......transport.inbound.receipt import MessageReceipt @@ -16,6 +17,13 @@ async def test_called(self): request_context.settings["debug.auto_respond_credential_offer"] = False request_context.connection_record = async_mock.MagicMock() + mock_oob_processor = async_mock.MagicMock( + find_oob_record_for_inbound_message=async_mock.CoroutineMock( + return_value=async_mock.MagicMock() + ) + ) + request_context.injector.bind_instance(OobMessageProcessor, mock_oob_processor) + with async_mock.patch.object( test_module, "V20CredManager", autospec=True ) as mock_cred_mgr: @@ -30,6 +38,9 @@ async def test_called(self): mock_cred_mgr.return_value.receive_offer.assert_called_once_with( request_context.message, request_context.connection_record.connection_id ) + mock_oob_processor.find_oob_record_for_inbound_message.assert_called_once_with( + request_context + ) assert not responder.messages async def test_called_auto_request(self): @@ -39,6 +50,13 @@ async def test_called_auto_request(self): request_context.connection_record = async_mock.MagicMock() request_context.connection_record.my_did = "dummy" + mock_oob_processor = async_mock.MagicMock( + find_oob_record_for_inbound_message=async_mock.CoroutineMock( + return_value=async_mock.MagicMock() + ) + ) + request_context.injector.bind_instance(OobMessageProcessor, mock_oob_processor) + with async_mock.patch.object( test_module, "V20CredManager", autospec=True ) as mock_cred_mgr: @@ -56,6 +74,9 @@ async def test_called_auto_request(self): mock_cred_mgr.return_value.receive_offer.assert_called_once_with( request_context.message, request_context.connection_record.connection_id ) + mock_oob_processor.find_oob_record_for_inbound_message.assert_called_once_with( + request_context + ) messages = responder.messages assert len(messages) == 1 (result, target) = messages[0] @@ -69,6 +90,13 @@ async def test_called_auto_request_x(self): request_context.connection_record = async_mock.MagicMock() request_context.connection_record.my_did = "dummy" + mock_oob_processor = async_mock.MagicMock( + find_oob_record_for_inbound_message=async_mock.CoroutineMock( + return_value=async_mock.MagicMock() + ) + ) + request_context.injector.bind_instance(OobMessageProcessor, mock_oob_processor) + with async_mock.patch.object( test_module, "V20CredManager", autospec=True ) as mock_cred_mgr: @@ -107,7 +135,36 @@ async def test_called_not_ready(self): request_context.connection_ready = False handler_inst = test_module.V20CredOfferHandler() responder = MockResponder() - with self.assertRaises(test_module.HandlerException): + with self.assertRaises(test_module.HandlerException) as err: await handler_inst.handle(request_context, responder) + assert ( + err.exception.message + == "Connection used for credential offer not ready" + ) + + assert not responder.messages + + async def test_no_conn_no_oob(self): + request_context = RequestContext.test_context() + request_context.message_receipt = MessageReceipt() + + mock_oob_processor = async_mock.MagicMock( + find_oob_record_for_inbound_message=async_mock.CoroutineMock( + # No oob record found + return_value=None + ) + ) + request_context.injector.bind_instance(OobMessageProcessor, mock_oob_processor) + + request_context.message = V20CredOffer() + request_context.connection_ready = False + handler_inst = test_module.V20CredOfferHandler() + responder = MockResponder() + with self.assertRaises(test_module.HandlerException) as err: + await handler_inst.handle(request_context, responder) + assert ( + err.exception.message + == "No connection or associated connectionless exchange found for credential offer" + ) assert not responder.messages diff --git a/aries_cloudagent/protocols/issue_credential/v2_0/handlers/tests/test_cred_problem_report_handler.py b/aries_cloudagent/protocols/issue_credential/v2_0/handlers/tests/test_cred_problem_report_handler.py index 54891e72ac..fa5195fce9 100644 --- a/aries_cloudagent/protocols/issue_credential/v2_0/handlers/tests/test_cred_problem_report_handler.py +++ b/aries_cloudagent/protocols/issue_credential/v2_0/handlers/tests/test_cred_problem_report_handler.py @@ -21,6 +21,7 @@ async def test_called(self): mock_cred_mgr.return_value.receive_problem_report = ( async_mock.CoroutineMock() ) + request_context.connection_ready = True request_context.message = V20CredProblemReport( description={ "en": "oh no", @@ -45,6 +46,7 @@ async def test_called_x(self): with async_mock.patch.object( test_module, "V20CredManager", autospec=True ) as mock_cred_mgr: + request_context.connection_ready = True mock_cred_mgr.return_value.receive_problem_report = ( async_mock.CoroutineMock( side_effect=test_module.StorageError("Disk full") @@ -65,3 +67,48 @@ async def test_called_x(self): request_context.message, request_context.connection_record.connection_id ) assert not responder.messages + + async def test_called_not_ready(self): + request_context = RequestContext.test_context() + request_context.message_receipt = MessageReceipt() + request_context.connection_record = async_mock.MagicMock() + request_context.connection_ready = False + + request_context.message = V20CredProblemReport( + description={ + "en": "Change of plans", + "code": ProblemReportReason.ISSUANCE_ABANDONED.value, + } + ) + handler = test_module.CredProblemReportHandler() + responder = MockResponder() + + with self.assertRaises(test_module.HandlerException) as err: + await handler.handle(request_context, responder) + assert ( + err.exception.message + == "Connection used for credential problem report not ready" + ) + + async def test_called_no_connection(self): + request_context = RequestContext.test_context() + request_context.message_receipt = MessageReceipt() + request_context.connection_record = None + + request_context.message = V20CredProblemReport( + description={ + "en": "Change of plans", + "code": ProblemReportReason.ISSUANCE_ABANDONED.value, + } + ) + handler = test_module.CredProblemReportHandler() + responder = MockResponder() + + with self.assertRaises(test_module.HandlerException) as err: + await handler.handle(request_context, responder) + assert ( + err.exception.message + == "Connectionless not supported for credential problem report" + ) + + assert not responder.messages diff --git a/aries_cloudagent/protocols/issue_credential/v2_0/handlers/tests/test_cred_proposal_handler.py b/aries_cloudagent/protocols/issue_credential/v2_0/handlers/tests/test_cred_proposal_handler.py index bcbe9373d3..daaf4c0d79 100644 --- a/aries_cloudagent/protocols/issue_credential/v2_0/handlers/tests/test_cred_proposal_handler.py +++ b/aries_cloudagent/protocols/issue_credential/v2_0/handlers/tests/test_cred_proposal_handler.py @@ -109,7 +109,27 @@ async def test_called_not_ready(self): request_context.connection_ready = False handler_inst = test_module.V20CredProposalHandler() responder = MockResponder() - with self.assertRaises(test_module.HandlerException): + with self.assertRaises(test_module.HandlerException) as err: await handler_inst.handle(request_context, responder) + assert ( + err.exception.message + == "Connection used for credential proposal not ready" + ) + + assert not responder.messages + + async def test_called_no_connection(self): + request_context = RequestContext.test_context() + request_context.message_receipt = MessageReceipt() + + request_context.message = V20CredProposal() + handler_inst = test_module.V20CredProposalHandler() + responder = MockResponder() + with self.assertRaises(test_module.HandlerException) as err: + await handler_inst.handle(request_context, responder) + assert ( + err.exception.message + == "Connectionless not supported for credential proposal" + ) assert not responder.messages diff --git a/aries_cloudagent/protocols/issue_credential/v2_0/handlers/tests/test_cred_request_handler.py b/aries_cloudagent/protocols/issue_credential/v2_0/handlers/tests/test_cred_request_handler.py index 9df3a5288a..25edba8b36 100644 --- a/aries_cloudagent/protocols/issue_credential/v2_0/handlers/tests/test_cred_request_handler.py +++ b/aries_cloudagent/protocols/issue_credential/v2_0/handlers/tests/test_cred_request_handler.py @@ -1,5 +1,6 @@ from asynctest import mock as async_mock, TestCase as AsyncTestCase +from ......core.oob_processor import OobMessageProcessor from ......messaging.request_context import RequestContext from ......messaging.responder import MockResponder from ......transport.inbound.receipt import MessageReceipt @@ -18,6 +19,14 @@ async def test_called(self): request_context.message_receipt = MessageReceipt() request_context.connection_record = async_mock.MagicMock() + oob_record = async_mock.MagicMock() + mock_oob_processor = async_mock.MagicMock( + find_oob_record_for_inbound_message=async_mock.CoroutineMock( + return_value=oob_record + ) + ) + request_context.injector.bind_instance(OobMessageProcessor, mock_oob_processor) + with async_mock.patch.object( test_module, "V20CredManager", autospec=True ) as mock_cred_mgr: @@ -33,7 +42,7 @@ async def test_called(self): mock_cred_mgr.assert_called_once_with(request_context.profile) mock_cred_mgr.return_value.receive_request.assert_called_once_with( - request_context.message, request_context.connection_record.connection_id + request_context.message, request_context.connection_record, oob_record ) assert not responder.messages @@ -42,6 +51,14 @@ async def test_called_auto_issue(self): request_context.message_receipt = MessageReceipt() request_context.connection_record = async_mock.MagicMock() + oob_record = async_mock.MagicMock() + mock_oob_processor = async_mock.MagicMock( + find_oob_record_for_inbound_message=async_mock.CoroutineMock( + return_value=oob_record + ) + ) + request_context.injector.bind_instance(OobMessageProcessor, mock_oob_processor) + cred_ex_rec = V20CredExRecord() with async_mock.patch.object( @@ -65,7 +82,7 @@ async def test_called_auto_issue(self): mock_cred_mgr.assert_called_once_with(request_context.profile) mock_cred_mgr.return_value.receive_request.assert_called_once_with( - request_context.message, request_context.connection_record.connection_id + request_context.message, request_context.connection_record, oob_record ) messages = responder.messages assert len(messages) == 1 @@ -80,6 +97,14 @@ async def test_called_auto_issue_x(self): cred_ex_rec = V20CredExRecord() + oob_record = async_mock.MagicMock() + mock_oob_processor = async_mock.MagicMock( + find_oob_record_for_inbound_message=async_mock.CoroutineMock( + return_value=oob_record + ) + ) + request_context.injector.bind_instance(OobMessageProcessor, mock_oob_processor) + with async_mock.patch.object( test_module, "V20CredManager", autospec=True ) as mock_cred_mgr, async_mock.patch.object( @@ -119,7 +144,35 @@ async def test_called_not_ready(self): request_context.connection_ready = False handler = test_module.V20CredRequestHandler() responder = MockResponder() - with self.assertRaises(test_module.HandlerException): + with self.assertRaises(test_module.HandlerException) as err: await handler.handle(request_context, responder) + assert ( + err.exception.message + == "Connection used for credential request not ready" + ) + + assert not responder.messages + + async def test_called_no_connection_no_oob(self): + request_context = RequestContext.test_context() + request_context.message_receipt = MessageReceipt() + + mock_oob_processor = async_mock.MagicMock( + find_oob_record_for_inbound_message=async_mock.CoroutineMock( + # No oob record found + return_value=None + ) + ) + request_context.injector.bind_instance(OobMessageProcessor, mock_oob_processor) + + request_context.message = V20CredRequest() + handler = test_module.V20CredRequestHandler() + responder = MockResponder() + with self.assertRaises(test_module.HandlerException) as err: + await handler.handle(request_context, responder) + assert ( + err.exception.message + == "No connection or associated connectionless exchange found for credential request" + ) assert not responder.messages diff --git a/aries_cloudagent/protocols/issue_credential/v2_0/manager.py b/aries_cloudagent/protocols/issue_credential/v2_0/manager.py index 17d709e19e..006217fabf 100644 --- a/aries_cloudagent/protocols/issue_credential/v2_0/manager.py +++ b/aries_cloudagent/protocols/issue_credential/v2_0/manager.py @@ -2,8 +2,10 @@ import logging -from typing import Mapping, Tuple +from typing import Mapping, Optional, Tuple +from ....connections.models.conn_record import ConnRecord +from ....core.oob_processor import OobRecord from ....core.error import BaseError from ....core.profile import Profile from ....messaging.responder import BaseResponder @@ -53,6 +55,7 @@ async def prepare_send( self, connection_id: str, cred_proposal: V20CredProposal, + verification_method: Optional[str] = None, auto_remove: bool = None, ) -> Tuple[V20CredExRecord, V20CredOffer]: """ @@ -61,6 +64,7 @@ async def prepare_send( Args: connection_id: connection for which to create offer cred_proposal: credential proposal with preview + verification_method: an optional verification method to be used when issuing auto_remove: flag to remove the record automatically on completion Returns: @@ -71,6 +75,7 @@ async def prepare_send( auto_remove = not self._profile.settings.get("preserve_exchange_records") cred_ex_record = V20CredExRecord( connection_id=connection_id, + verification_method=verification_method, initiator=V20CredExRecord.INITIATOR_SELF, role=V20CredExRecord.ROLE_ISSUER, cred_proposal=cred_proposal, @@ -78,12 +83,11 @@ async def prepare_send( auto_remove=auto_remove, trace=(cred_proposal._trace is not None), ) - (cred_ex_record, cred_offer) = await self.create_offer( + return await self.create_offer( cred_ex_record=cred_ex_record, counter_proposal=None, comment="create automated v2.0 credential exchange record", ) - return (cred_ex_record, cred_offer) async def create_proposal( self, @@ -264,7 +268,7 @@ async def create_offer( async def receive_offer( self, cred_offer_message: V20CredOffer, - connection_id: str, + connection_id: Optional[str], ) -> V20CredExRecord: """ Receive a credential offer. @@ -282,10 +286,11 @@ async def receive_offer( # or create it (issuer sent offer first) try: async with self._profile.session() as session: - cred_ex_record = await ( - V20CredExRecord.retrieve_by_conn_and_thread( - session, connection_id, cred_offer_message._thread_id - ) + cred_ex_record = await V20CredExRecord.retrieve_by_conn_and_thread( + session, + connection_id, + cred_offer_message._thread_id, + role=V20CredExRecord.ROLE_HOLDER, ) except StorageNotFoundError: # issuer sent this offer free of any proposal cred_ex_record = V20CredExRecord( @@ -376,7 +381,8 @@ async def create_request( requests_attach=[attach for (_, attach) in request_formats], ) - cred_request_message._thread = {"thid": cred_ex_record.thread_id} + # Assign thid (and optionally pthid) to message + cred_request_message.assign_thread_from(cred_ex_record.cred_offer) cred_request_message.assign_trace_decorator( self._profile.settings, cred_ex_record.trace ) @@ -391,7 +397,10 @@ async def create_request( return (cred_ex_record, cred_request_message) async def receive_request( - self, cred_request_message: V20CredRequest, connection_id: str + self, + cred_request_message: V20CredRequest, + connection_record: Optional[ConnRecord], + oob_record: Optional[OobRecord], ) -> V20CredExRecord: """ Receive a credential request. @@ -404,46 +413,47 @@ async def receive_request( credential exchange record, updated """ + # connection_id is None in the record if this is in response to + # an request~attach from an OOB message. If so, we do not want to filter + # the record by connection_id. + connection_id = None if oob_record else connection_record.connection_id + async with self._profile.session() as session: try: - cred_ex_record = await ( - V20CredExRecord.retrieve_by_conn_and_thread( - session, connection_id, cred_request_message._thread_id - ) + cred_ex_record = await V20CredExRecord.retrieve_by_conn_and_thread( + session, + connection_id, + cred_request_message._thread_id, + role=V20CredExRecord.ROLE_ISSUER, ) except StorageNotFoundError: - try: - cred_ex_record = await V20CredExRecord.retrieve_by_tag_filter( - session, - {"thread_id": cred_request_message._thread_id}, - {"connection_id": None}, - ) - cred_ex_record.connection_id = connection_id - except StorageNotFoundError: - # holder sent this request free of any offer - cred_ex_record = V20CredExRecord( - connection_id=connection_id, - thread_id=cred_request_message._thread_id, - initiator=V20CredExRecord.INITIATOR_EXTERNAL, - role=V20CredExRecord.ROLE_ISSUER, - auto_remove=not self._profile.settings.get( - "preserve_exchange_records" - ), - trace=(cred_request_message._trace is not None), - auto_issue=self._profile.settings.get( - "debug.auto_respond_credential_request" - ), - ) + # holder sent this request free of any offer + cred_ex_record = V20CredExRecord( + connection_id=connection_id, + thread_id=cred_request_message._thread_id, + initiator=V20CredExRecord.INITIATOR_EXTERNAL, + role=V20CredExRecord.ROLE_ISSUER, + auto_remove=not self._profile.settings.get( + "preserve_exchange_records" + ), + trace=(cred_request_message._trace is not None), + auto_issue=self._profile.settings.get( + "debug.auto_respond_credential_request" + ), + ) - for format in cred_request_message.formats: - cred_format = V20CredFormat.Format.get(format.format) - if cred_format: - await cred_format.handler(self.profile).receive_request( - cred_ex_record, cred_request_message - ) + if connection_record: + cred_ex_record.connection_id = connection_record.connection_id + + for format in cred_request_message.formats: + cred_format = V20CredFormat.Format.get(format.format) + if cred_format: + await cred_format.handler(self.profile).receive_request( + cred_ex_record, cred_request_message + ) - cred_ex_record.cred_request = cred_request_message - cred_ex_record.state = V20CredExRecord.STATE_REQUEST_RECEIVED + cred_ex_record.cred_request = cred_request_message + cred_ex_record.state = V20CredExRecord.STATE_REQUEST_RECEIVED async with self._profile.session() as session: await cred_ex_record.save(session, reason="receive v2.0 credential request") @@ -526,7 +536,7 @@ async def issue_credential( return (cred_ex_record, cred_issue_message) async def receive_credential( - self, cred_issue_message: V20CredIssue, connection_id: str + self, cred_issue_message: V20CredIssue, connection_id: Optional[str] ) -> V20CredExRecord: """ Receive a credential issue message from an issuer. @@ -541,45 +551,44 @@ async def receive_credential( # FIXME use transaction, fetch for_update async with self._profile.session() as session: - cred_ex_record = await ( - V20CredExRecord.retrieve_by_conn_and_thread( - session, - connection_id, - cred_issue_message._thread_id, - ) + cred_ex_record = await V20CredExRecord.retrieve_by_conn_and_thread( + session, + connection_id, + cred_issue_message._thread_id, + role=V20CredExRecord.ROLE_HOLDER, ) - cred_request_message = cred_ex_record.cred_request - req_formats = [ - V20CredFormat.Format.get(fmt.format) - for fmt in cred_request_message.formats - if V20CredFormat.Format.get(fmt.format) - ] - issue_formats = [ - V20CredFormat.Format.get(fmt.format) - for fmt in cred_issue_message.formats - if V20CredFormat.Format.get(fmt.format) - ] - handled_formats = [] - - # check that we didn't receive any formats not present in the request - if set(issue_formats) - set(req_formats): - raise V20CredManagerError( - "Received issue credential format(s) not present in credential " - f"request: {set(issue_formats) - set(req_formats)}" - ) + cred_request_message = cred_ex_record.cred_request + req_formats = [ + V20CredFormat.Format.get(fmt.format) + for fmt in cred_request_message.formats + if V20CredFormat.Format.get(fmt.format) + ] + issue_formats = [ + V20CredFormat.Format.get(fmt.format) + for fmt in cred_issue_message.formats + if V20CredFormat.Format.get(fmt.format) + ] + handled_formats = [] - for issue_format in issue_formats: - await issue_format.handler(self.profile).receive_credential( - cred_ex_record, cred_issue_message - ) - handled_formats.append(issue_format) + # check that we didn't receive any formats not present in the request + if set(issue_formats) - set(req_formats): + raise V20CredManagerError( + "Received issue credential format(s) not present in credential " + f"request: {set(issue_formats) - set(req_formats)}" + ) + + for issue_format in issue_formats: + await issue_format.handler(self.profile).receive_credential( + cred_ex_record, cred_issue_message + ) + handled_formats.append(issue_format) - if len(handled_formats) == 0: - raise V20CredManagerError("No supported credential formats received.") + if len(handled_formats) == 0: + raise V20CredManagerError("No supported credential formats received.") - cred_ex_record.cred_issue = cred_issue_message - cred_ex_record.state = V20CredExRecord.STATE_CREDENTIAL_RECEIVED + cred_ex_record.cred_issue = cred_issue_message + cred_ex_record.state = V20CredExRecord.STATE_CREDENTIAL_RECEIVED async with self._profile.session() as session: await cred_ex_record.save(session, reason="receive v2.0 credential issue") @@ -646,11 +655,13 @@ async def send_cred_ack( # FIXME - re-fetch record to check state, apply transactional update await cred_ex_record.save(session, reason="store credential v2.0") - if cred_ex_record.auto_remove: - await cred_ex_record.delete_record(session) # all done: delete + if cred_ex_record.auto_remove: + await self.delete_cred_ex_record(cred_ex_record.cred_ex_id) - except StorageError as err: - LOGGER.exception(err) # holder still owes an ack: carry on + except StorageError: + LOGGER.exception( + "Error sending credential ack" + ) # holder still owes an ack: carry on responder = self._profile.inject_or(BaseResponder) if responder: @@ -667,7 +678,7 @@ async def send_cred_ack( return cred_ex_record, cred_ack_message async def receive_credential_ack( - self, cred_ack_message: V20CredAck, connection_id: str + self, cred_ack_message: V20CredAck, connection_id: Optional[str] ) -> V20CredExRecord: """ Receive credential ack from holder. @@ -682,12 +693,11 @@ async def receive_credential_ack( """ # FIXME use transaction, fetch for_update async with self._profile.session() as session: - cred_ex_record = await ( - V20CredExRecord.retrieve_by_conn_and_thread( - session, - connection_id, - cred_ack_message._thread_id, - ) + cred_ex_record = await V20CredExRecord.retrieve_by_conn_and_thread( + session, + connection_id, + cred_ack_message._thread_id, + role=V20CredExRecord.ROLE_ISSUER, ) cred_ex_record.state = V20CredExRecord.STATE_DONE @@ -724,15 +734,13 @@ async def receive_problem_report( """ # FIXME use transaction, fetch for_update async with self._profile.session() as session: - cred_ex_record = await ( - V20CredExRecord.retrieve_by_conn_and_thread( - session, - connection_id, - message._thread_id, - ) + cred_ex_record = await V20CredExRecord.retrieve_by_conn_and_thread( + session, + connection_id, + message._thread_id, ) - cred_ex_record.state = None + cred_ex_record.state = V20CredExRecord.STATE_ABANDONED code = message.description.get( "code", ProblemReportReason.ISSUANCE_ABANDONED.value, diff --git a/aries_cloudagent/protocols/issue_credential/v2_0/messages/cred_ex_record_webhook.py b/aries_cloudagent/protocols/issue_credential/v2_0/messages/cred_ex_record_webhook.py new file mode 100644 index 0000000000..aa566db23d --- /dev/null +++ b/aries_cloudagent/protocols/issue_credential/v2_0/messages/cred_ex_record_webhook.py @@ -0,0 +1,49 @@ +"""v2.0 credential exchange webhook.""" + + +class V20CredExRecordWebhook: + """Class representing a state only credential exchange webhook.""" + + __acceptable_keys_list = [ + "connection_id", + "credential_exchange_id", + "cred_ex_id", + "cred_def_id", + "role", + "initiator", + "revoc_reg_id", + "revocation_id", + "auto_offer", + "auto_issue", + "auto_remove", + "error_msg", + "thread_id", + "parent_thread_id", + "state", + "credential_definition_id", + "schema_id", + "credential_id", + "trace", + "public_did", + "cred_id_stored", + "conn_id", + "created_at", + "updated_at", + ] + + def __init__( + self, + **kwargs, + ): + """ + Initialize webhook object from V20CredExRecord. + + from a list of accepted attributes. + """ + [ + self.__setattr__(key, kwargs.get(key)) + for key in self.__acceptable_keys_list + if kwargs.get(key) is not None + ] + if kwargs.get("_id") is not None: + self.cred_ex_id = kwargs.get("_id") diff --git a/aries_cloudagent/protocols/issue_credential/v2_0/models/cred_ex_record.py b/aries_cloudagent/protocols/issue_credential/v2_0/models/cred_ex_record.py index fc23fb5e5a..339c884c70 100644 --- a/aries_cloudagent/protocols/issue_credential/v2_0/models/cred_ex_record.py +++ b/aries_cloudagent/protocols/issue_credential/v2_0/models/cred_ex_record.py @@ -2,7 +2,7 @@ import logging -from typing import Any, Mapping, Union +from typing import Any, Mapping, Optional, Union from marshmallow import fields, Schema, validate @@ -17,6 +17,7 @@ from ..messages.cred_offer import V20CredOffer, V20CredOfferSchema from ..messages.cred_request import V20CredRequest, V20CredRequestSchema from ..messages.inner.cred_preview import V20CredPreviewSchema +from ..messages.cred_ex_record_webhook import V20CredExRecordWebhook from . import UNENCRYPTED_TAGS @@ -51,12 +52,14 @@ class Meta: STATE_CREDENTIAL_RECEIVED = "credential-received" STATE_DONE = "done" STATE_CREDENTIAL_REVOKED = "credential-revoked" + STATE_ABANDONED = "abandoned" def __init__( self, *, cred_ex_id: str = None, connection_id: str = None, + verification_method: Optional[str] = None, thread_id: str = None, parent_thread_id: str = None, initiator: str = None, @@ -80,6 +83,7 @@ def __init__( super().__init__(cred_ex_id, state, trace=trace, **kwargs) self._id = cred_ex_id self.connection_id = connection_id or conn_id + self.verification_method = verification_method self.thread_id = thread_id self.parent_thread_id = parent_thread_id self.initiator = initiator @@ -148,6 +152,7 @@ async def save_error_state( self, session: ProfileSession, *, + state: str = None, reason: str = None, log_params: Mapping[str, Any] = None, log_override: bool = False, @@ -162,10 +167,10 @@ async def save_error_state( override: Override configured logging regimen, print to stderr instead """ - if self._last_state is None: # already done + if self._last_state == state: # already done return - self.state = None + self.state = state or V20CredExRecord.STATE_ABANDONED if reason: self.error_msg = reason @@ -179,6 +184,33 @@ async def save_error_state( except StorageError as err: LOGGER.exception(err) + # Override + async def emit_event(self, session: ProfileSession, payload: Any = None): + """ + Emit an event. + + Args: + session: The profile session to use + payload: The event payload + """ + + if not self.RECORD_TOPIC: + return + + if self.state: + topic = f"{self.EVENT_NAMESPACE}::{self.RECORD_TOPIC}::{self.state}" + else: + topic = f"{self.EVENT_NAMESPACE}::{self.RECORD_TOPIC}" + + if session.profile.settings.get("debug.webhooks"): + if not payload: + payload = self.serialize() + else: + payload = V20CredExRecordWebhook(**self.__dict__) + payload = payload.__dict__ + + await session.profile.notify(topic, payload) + @property def record_value(self) -> Mapping: """Accessor for the JSON record value generated for this credential exchange.""" @@ -187,6 +219,7 @@ def record_value(self) -> Mapping: prop: getattr(self, prop) for prop in ( "connection_id", + "verification_method", "parent_thread_id", "initiator", "role", @@ -212,7 +245,11 @@ def record_value(self) -> Mapping: @classmethod async def retrieve_by_conn_and_thread( - cls, session: ProfileSession, connection_id: str, thread_id: str + cls, + session: ProfileSession, + connection_id: Optional[str], + thread_id: str, + role: Optional[str] = None, ) -> "V20CredExRecord": """Retrieve a credential exchange record by connection and thread ID.""" cache_key = f"credential_exchange_ctidx::{connection_id}::{thread_id}" @@ -220,10 +257,15 @@ async def retrieve_by_conn_and_thread( if record_id: record = await cls.retrieve_by_id(session, record_id) else: + post_filter = {} + if role: + post_filter["role"] = role + if connection_id: + post_filter["connection_id"] = connection_id record = await cls.retrieve_by_tag_filter( session, {"thread_id": thread_id}, - {"connection_id": connection_id} if connection_id else None, + post_filter, ) await cls.set_cached_key(session, cache_key, record.cred_ex_id) return record @@ -285,11 +327,7 @@ class Meta: description="Issue-credential exchange initiator: self or external", example=V20CredExRecord.INITIATOR_SELF, validate=validate.OneOf( - [ - getattr(V20CredExRecord, m) - for m in vars(V20CredExRecord) - if m.startswith("INITIATOR_") - ] + V20CredExRecord.get_attributes_by_prefix("INITIATOR_", walk_mro=False) ), ) role = fields.Str( @@ -297,11 +335,7 @@ class Meta: description="Issue-credential exchange role: holder or issuer", example=V20CredExRecord.ROLE_ISSUER, validate=validate.OneOf( - [ - getattr(V20CredExRecord, m) - for m in vars(V20CredExRecord) - if m.startswith("ROLE_") - ] + V20CredExRecord.get_attributes_by_prefix("ROLE_", walk_mro=False) ), ) state = fields.Str( @@ -309,11 +343,7 @@ class Meta: description="Issue-credential exchange state", example=V20CredExRecord.STATE_DONE, validate=validate.OneOf( - [ - getattr(V20CredExRecord, m) - for m in vars(V20CredExRecord) - if m.startswith("STATE_") - ] + V20CredExRecord.get_attributes_by_prefix("STATE_", walk_mro=True) ), ) cred_preview = fields.Nested( diff --git a/aries_cloudagent/protocols/issue_credential/v2_0/routes.py b/aries_cloudagent/protocols/issue_credential/v2_0/routes.py index 27bfe4938b..760c602815 100644 --- a/aries_cloudagent/protocols/issue_credential/v2_0/routes.py +++ b/aries_cloudagent/protocols/issue_credential/v2_0/routes.py @@ -1,6 +1,7 @@ """Credential exchange admin routes.""" -from ....vc.ld_proofs.error import LinkedDataProofException +import logging + from json.decoder import JSONDecodeError from typing import Mapping @@ -14,6 +15,8 @@ ) from marshmallow import fields, validate, validates_schema, ValidationError +from ...out_of_band.v1_0.models.oob_record import OobRecord +from ....wallet.util import default_did_from_verkey from ....admin.request_context import AdminRequestContext from ....connections.models.conn_record import ConnRecord from ....core.profile import Profile @@ -33,6 +36,7 @@ ) from ....storage.error import StorageError, StorageNotFoundError from ....utils.tracing import trace_event, get_timer, AdminAPIMessageTracingSchema +from ....vc.ld_proofs.error import LinkedDataProofException from . import problem_report_for_record, report_problem from .manager import V20CredManager, V20CredManagerError @@ -47,6 +51,8 @@ from .formats.handler import V20CredFormatError from .formats.ld_proof.models.cred_detail import LDProofVCDetailSchema +LOGGER = logging.getLogger(__name__) + class V20IssueCredentialModuleResponseSchema(OpenAPISchema): """Response schema for v2.0 Issue Credential Module.""" @@ -271,6 +277,13 @@ class V20CredExFreeSchema(V20IssueCredSchemaCore): example=UUIDFour.EXAMPLE, # typically but not necessarily a UUID4 ) + verification_method = fields.Str( + required=False, + default=None, + allow_none=True, + description="For ld-proofs. Verification method for signing.", + ) + class V20CredBoundOfferRequestSchema(OpenAPISchema): """Request schema for sending bound credential offer admin message.""" @@ -392,19 +405,31 @@ def _formats_filters(filt_spec: Mapping) -> Mapping: ) -async def _get_result_with_details( +async def _get_attached_credentials( profile: Profile, cred_ex_record: V20CredExRecord ) -> Mapping: - """Get credential exchange result with detail records.""" - result = {"cred_ex_record": cred_ex_record.serialize()} + """Fetch the detail records attached to a credential exchange.""" + result = {} for fmt in V20CredFormat.Format: detail_record = await fmt.handler(profile).get_detail_record( cred_ex_record.cred_ex_id ) + if detail_record: + result[fmt.api] = detail_record + + return result - result[fmt.api] = detail_record.serialize() if detail_record else None +def _format_result_with_details( + cred_ex_record: V20CredExRecord, details: Mapping +) -> Mapping: + """Get credential exchange result with detail records.""" + result = {"cred_ex_record": cred_ex_record.serialize()} + for fmt in V20CredFormat.Format: + ident = fmt.api + detail_record = details.get(ident) + result[ident] = detail_record.serialize() if detail_record else None return result @@ -446,7 +471,8 @@ async def credential_exchange_list(request: web.BaseRequest): results = [] for cxr in cred_ex_records: - result = await _get_result_with_details(profile, cxr) + details = await _get_attached_credentials(profile, cxr) + result = _format_result_with_details(cxr, details) results.append(result) except (StorageError, BaseModelError) as err: @@ -482,8 +508,8 @@ async def credential_exchange_retrieve(request: web.BaseRequest): async with profile.session() as session: cred_ex_record = await V20CredExRecord.retrieve_by_id(session, cred_ex_id) - result = await _get_result_with_details(context.profile, cred_ex_record) - + details = await _get_attached_credentials(profile, cred_ex_record) + result = _format_result_with_details(cred_ex_record, details) except StorageNotFoundError as err: # no such cred ex record: not protocol error, user fat-fingered id raise web.HTTPNotFound(reason=err.roll_up) from err @@ -502,7 +528,10 @@ async def credential_exchange_retrieve(request: web.BaseRequest): @docs( tags=["issue-credential v2.0"], - summary="Create credential from attribute values", + summary=( + "Create a credential record without " + "sending (generally for use with Out-Of-Band)" + ), ) @request_schema(V20IssueCredSchemaCore()) @response_schema(V20CredExRecordSchema(), 200, description="") @@ -606,6 +635,8 @@ async def credential_exchange_send(request: web.BaseRequest): comment = body.get("comment") connection_id = body.get("connection_id") + verification_method = body.get("verification_method") + filt_spec = body.get("filter") if not filt_spec: raise web.HTTPBadRequest(reason="Missing filter") @@ -646,6 +677,7 @@ async def credential_exchange_send(request: web.BaseRequest): cred_manager = V20CredManager(profile) (cred_ex_record, cred_offer_message) = await cred_manager.prepare_send( connection_id, + verification_method=verification_method, cred_proposal=cred_proposal, auto_remove=auto_remove, ) @@ -658,6 +690,7 @@ async def credential_exchange_send(request: web.BaseRequest): V20CredManagerError, V20CredFormatError, ) as err: + LOGGER.exception("Error preparing credential offer") if cred_ex_record: async with profile.session() as session: await cred_ex_record.save_error_state(session, reason=err.roll_up) @@ -746,6 +779,7 @@ async def credential_exchange_send_proposal(request: web.BaseRequest): result = cred_ex_record.serialize() except (BaseModelError, StorageError) as err: + LOGGER.exception("Error preparing credential proposal") if cred_ex_record: async with profile.session() as session: await cred_ex_record.save_error_state(session, reason=err.roll_up) @@ -778,6 +812,7 @@ async def _create_free_offer( preview_spec: dict = None, comment: str = None, trace_msg: bool = None, + thread_id: str = None, ): """Create a credential offer and related exchange record.""" @@ -800,6 +835,7 @@ async def _create_free_offer( auto_issue=auto_issue, auto_remove=auto_remove, trace=trace_msg, + thread_id=thread_id ) cred_manager = V20CredManager(profile) @@ -866,6 +902,7 @@ async def credential_exchange_create_free_offer(request: web.BaseRequest): V20CredFormatError, V20CredManagerError, ) as err: + LOGGER.exception("Error creating free credential offer") if cred_ex_record: async with profile.session() as session: await cred_ex_record.save_error_state(session, reason=err.roll_up) @@ -918,6 +955,7 @@ async def credential_exchange_send_free_offer(request: web.BaseRequest): comment = body.get("comment") preview_spec = body.get("credential_preview") trace_msg = body.get("trace") + thread_id = body.get("thread_id") cred_ex_record = None conn_record = None @@ -936,6 +974,7 @@ async def credential_exchange_send_free_offer(request: web.BaseRequest): preview_spec=preview_spec, comment=comment, trace_msg=trace_msg, + thread_id=thread_id ) result = cred_ex_record.serialize() @@ -947,6 +986,7 @@ async def credential_exchange_send_free_offer(request: web.BaseRequest): V20CredFormatError, V20CredManagerError, ) as err: + LOGGER.exception("Error preparing free credential offer") if cred_ex_record: async with profile.session() as session: await cred_ex_record.save_error_state(session, reason=err.roll_up) @@ -1051,6 +1091,7 @@ async def credential_exchange_send_bound_offer(request: web.BaseRequest): V20CredFormatError, V20CredManagerError, ) as err: + LOGGER.exception("Error preparing bound credential offer") if cred_ex_record: async with profile.session() as session: await cred_ex_record.save_error_state(session, reason=err.roll_up) @@ -1155,6 +1196,7 @@ async def credential_exchange_send_free_request(request: web.BaseRequest): StorageError, V20CredManagerError, ) as err: + LOGGER.exception("Error preparing free credential request") if cred_ex_record: async with profile.session() as session: await cred_ex_record.save_error_state(session, reason=err.roll_up) @@ -1222,16 +1264,36 @@ async def credential_exchange_send_bound_request(request: web.BaseRequest): except StorageNotFoundError as err: raise web.HTTPNotFound(reason=err.roll_up) from err - connection_id = cred_ex_record.connection_id - conn_record = await ConnRecord.retrieve_by_id(session, connection_id) + conn_record = None + if cred_ex_record.connection_id: + try: + conn_record = await ConnRecord.retrieve_by_id( + session, cred_ex_record.connection_id + ) + except StorageNotFoundError as err: + raise web.HTTPBadRequest(reason=err.roll_up) from err + + if conn_record and not conn_record.is_ready: + raise web.HTTPForbidden( + reason=f"Connection {cred_ex_record.connection_id} not ready" + ) - if not conn_record.is_ready: - raise web.HTTPForbidden(reason=f"Connection {connection_id} not ready") + if conn_record or holder_did: + holder_did = holder_did or conn_record.my_did + else: + # Need to get the holder DID from the out of band record + async with profile.session() as session: + oob_record = await OobRecord.retrieve_by_tag_filter( + session, + {"invi_msg_id": cred_ex_record.cred_offer._thread.pthid}, + ) + # Transform recipient key into did + holder_did = default_did_from_verkey(oob_record.our_recipient_key) cred_manager = V20CredManager(profile) cred_ex_record, cred_request_message = await cred_manager.create_request( cred_ex_record, - holder_did if holder_did else conn_record.my_did, + holder_did, ) result = cred_ex_record.serialize() @@ -1244,6 +1306,7 @@ async def credential_exchange_send_bound_request(request: web.BaseRequest): V20CredFormatError, V20CredManagerError, ) as err: + LOGGER.exception("Error preparing bound credential request") if cred_ex_record: async with profile.session() as session: await cred_ex_record.save_error_state(session, reason=err.roll_up) @@ -1255,7 +1318,9 @@ async def credential_exchange_send_bound_request(request: web.BaseRequest): outbound_handler, ) - await outbound_handler(cred_request_message, connection_id=connection_id) + await outbound_handler( + cred_request_message, connection_id=cred_ex_record.connection_id + ) trace_event( context.settings, @@ -1307,11 +1372,16 @@ async def credential_exchange_issue(request: web.BaseRequest): ) except StorageNotFoundError as err: raise web.HTTPNotFound(reason=err.roll_up) from err - connection_id = cred_ex_record.connection_id - conn_record = await ConnRecord.retrieve_by_id(session, connection_id) - if not conn_record.is_ready: - raise web.HTTPForbidden(reason=f"Connection {connection_id} not ready") + conn_record = None + if cred_ex_record.connection_id: + conn_record = await ConnRecord.retrieve_by_id( + session, cred_ex_record.connection_id + ) + if conn_record and not conn_record.is_ready: + raise web.HTTPForbidden( + reason=f"Connection {cred_ex_record.connection_id} not ready" + ) cred_manager = V20CredManager(profile) (cred_ex_record, cred_issue_message) = await cred_manager.issue_credential( @@ -1319,7 +1389,8 @@ async def credential_exchange_issue(request: web.BaseRequest): comment=comment, ) - result = await _get_result_with_details(profile, cred_ex_record) + details = await _get_attached_credentials(profile, cred_ex_record) + result = _format_result_with_details(cred_ex_record, details) except ( BaseModelError, @@ -1329,6 +1400,7 @@ async def credential_exchange_issue(request: web.BaseRequest): V20CredFormatError, V20CredManagerError, ) as err: + LOGGER.exception("Error preparing issued credential") if cred_ex_record: async with profile.session() as session: await cred_ex_record.save_error_state(session, reason=err.roll_up) @@ -1340,7 +1412,9 @@ async def credential_exchange_issue(request: web.BaseRequest): outbound_handler, ) - await outbound_handler(cred_issue_message, connection_id=connection_id) + await outbound_handler( + cred_issue_message, connection_id=cred_ex_record.connection_id + ) trace_event( context.settings, @@ -1395,10 +1469,15 @@ async def credential_exchange_store(request: web.BaseRequest): except StorageNotFoundError as err: raise web.HTTPNotFound(reason=err.roll_up) from err - connection_id = cred_ex_record.connection_id - conn_record = await ConnRecord.retrieve_by_id(session, connection_id) - if not conn_record.is_ready: - raise web.HTTPForbidden(reason=f"Connection {connection_id} not ready") + conn_record = None + if cred_ex_record.connection_id: + conn_record = await ConnRecord.retrieve_by_id( + session, cred_ex_record.connection_id + ) + if conn_record and not conn_record.is_ready: + raise web.HTTPForbidden( + reason=f"Connection {cred_ex_record.connection_id} not ready" + ) cred_manager = V20CredManager(profile) cred_ex_record = await cred_manager.store_credential(cred_ex_record, cred_id) @@ -1408,6 +1487,7 @@ async def credential_exchange_store(request: web.BaseRequest): StorageError, V20CredManagerError, ) as err: # treat failure to store as mangled on receipt hence protocol error + LOGGER.exception("Error storing issued credential") if cred_ex_record: async with profile.session() as session: await cred_ex_record.save_error_state(session, reason=err.roll_up) @@ -1420,14 +1500,16 @@ async def credential_exchange_store(request: web.BaseRequest): ) try: + # fetch these early, before potential removal + details = await _get_attached_credentials(profile, cred_ex_record) + + # the record may be auto-removed here ( cred_ex_record, cred_ack_message, ) = await cred_manager.send_cred_ack(cred_ex_record) - # We first need to retrieve the the cred_ex_record with detail record - # as the record may be auto removed - result = await _get_result_with_details(profile, cred_ex_record) + result = _format_result_with_details(cred_ex_record, details) except ( BaseModelError, diff --git a/aries_cloudagent/protocols/issue_credential/v2_0/tests/test_manager.py b/aries_cloudagent/protocols/issue_credential/v2_0/tests/test_manager.py index 534aa1360e..55d654c5f1 100644 --- a/aries_cloudagent/protocols/issue_credential/v2_0/tests/test_manager.py +++ b/aries_cloudagent/protocols/issue_credential/v2_0/tests/test_manager.py @@ -10,6 +10,7 @@ from .....cache.in_memory import InMemoryCache from .....core.in_memory import InMemoryProfile from .....indy.issuer import IndyIssuer +from .....messaging.decorators.thread_decorator import ThreadDecorator from .....messaging.decorators.attach_decorator import AttachDecorator from .....messaging.responder import BaseResponder, MockResponder from .....ledger.base import BaseLedger @@ -164,7 +165,6 @@ async def test_create_proposal(self): ) as mock_save, async_mock.patch.object( V20CredFormat.Format, "handler" ) as mock_handler: - mock_handler.return_value.create_proposal = async_mock.CoroutineMock( return_value=( V20CredFormat( @@ -208,7 +208,6 @@ async def test_create_proposal_no_preview(self): ) as mock_save, async_mock.patch.object( V20CredFormat.Format, "handler" ) as mock_handler: - mock_handler.return_value.create_proposal = async_mock.CoroutineMock( return_value=( V20CredFormat( @@ -260,7 +259,6 @@ async def test_receive_proposal(self): ) as mock_save, async_mock.patch.object( V20CredFormat.Format, "handler" ) as mock_handler: - mock_handler.return_value.receive_proposal = async_mock.CoroutineMock() cred_proposal = V20CredProposal( @@ -334,7 +332,6 @@ async def test_create_free_offer(self): ) as mock_save, async_mock.patch.object( V20CredFormat.Format, "handler" ) as mock_handler: - mock_handler.return_value.create_offer = async_mock.CoroutineMock( return_value=( V20CredFormat( @@ -626,7 +623,6 @@ async def test_create_bound_request(self): ) as mock_save, async_mock.patch.object( V20CredFormat.Format, "handler" ) as mock_handler: - mock_handler.return_value.create_request = async_mock.CoroutineMock( return_value=( V20CredFormat( @@ -693,9 +689,13 @@ async def test_create_free_request(self): filters_attach=[AttachDecorator.data_base64(LD_PROOF_VC_DETAIL, ident="0")], ) + cred_offer = V20CredOffer(thread_id) + cred_offer._thread = ThreadDecorator(pthid="some-pthid") + stored_cx_rec = V20CredExRecord( cred_ex_id="dummy-cxid", connection_id=connection_id, + cred_offer=cred_offer, initiator=V20CredExRecord.INITIATOR_SELF, role=V20CredExRecord.ROLE_HOLDER, thread_id=thread_id, @@ -710,7 +710,6 @@ async def test_create_free_request(self): ) as mock_save, async_mock.patch.object( V20CredFormat.Format, "handler" ) as mock_handler: - mock_handler.return_value.create_request = async_mock.CoroutineMock( return_value=( V20CredFormat( @@ -735,6 +734,7 @@ async def test_create_free_request(self): assert ret_cred_req.attachment() == LD_PROOF_VC_DETAIL assert ret_cred_req._thread_id == thread_id + assert ret_cred_req._thread.pthid == "some-pthid" assert ret_cx_rec.state == V20CredExRecord.STATE_REQUEST_SENT @@ -757,10 +757,10 @@ async def test_create_request_bad_state(self): assert " state " in str(context.exception) async def test_receive_request(self): - connection_id = "test_conn_id" + mock_conn = async_mock.MagicMock(connection_id="test_conn_id") stored_cx_rec = V20CredExRecord( cred_ex_id="dummy-cxid", - connection_id=connection_id, + connection_id=mock_conn.connection_id, initiator=V20CredExRecord.INITIATOR_EXTERNAL, role=V20CredExRecord.ROLE_ISSUER, state=V20CredExRecord.STATE_OFFER_SENT, @@ -788,10 +788,13 @@ async def test_receive_request(self): mock_handler.return_value.receive_request = async_mock.CoroutineMock() # mock_retrieve.return_value = stored_cx_rec - cx_rec = await self.manager.receive_request(cred_request, connection_id) + cx_rec = await self.manager.receive_request(cred_request, mock_conn, None) mock_retrieve.assert_called_once_with( - self.session, connection_id, cred_request._thread_id + self.session, + "test_conn_id", + cred_request._thread_id, + role=V20CredExRecord.ROLE_ISSUER, ) mock_handler.return_value.receive_request.assert_called_once_with( cx_rec, cred_request @@ -821,28 +824,28 @@ async def test_receive_request_no_connection_cred_request(self): requests_attach=[AttachDecorator.data_base64(INDY_CRED_REQ, ident="0")], ) + mock_conn = async_mock.MagicMock(connection_id="test_conn_id") + mock_oob = async_mock.MagicMock() + with async_mock.patch.object( V20CredExRecord, "save", autospec=True ) as mock_save, async_mock.patch.object( V20CredExRecord, "retrieve_by_conn_and_thread", async_mock.CoroutineMock() ) as mock_retrieve, async_mock.patch.object( - V20CredExRecord, "retrieve_by_tag_filter", async_mock.CoroutineMock() - ) as mock_retrieve_tag_filter, async_mock.patch.object( V20CredFormat.Format, "handler" ) as mock_handler: - mock_retrieve.side_effect = (StorageNotFoundError(),) - mock_retrieve_tag_filter.return_value = stored_cx_rec + mock_retrieve.return_value = stored_cx_rec mock_handler.return_value.receive_request = async_mock.CoroutineMock() - cx_rec = await self.manager.receive_request(cred_request, "test_conn_id") + cx_rec = await self.manager.receive_request( + cred_request, mock_conn, mock_oob + ) mock_retrieve.assert_called_once_with( - self.session, "test_conn_id", cred_request._thread_id - ) - mock_retrieve_tag_filter.assert_called_once_with( self.session, - {"thread_id": cred_request._thread_id}, - {"connection_id": None}, + None, + cred_request._thread_id, + role=V20CredExRecord.ROLE_ISSUER, ) mock_handler.return_value.receive_request.assert_called_once_with( cx_rec, cred_request @@ -853,6 +856,7 @@ async def test_receive_request_no_connection_cred_request(self): assert cx_rec.connection_id == "test_conn_id" async def test_receive_request_no_cred_ex_with_offer_found(self): + mock_conn = async_mock.MagicMock(connection_id="test_conn_id") stored_cx_rec = V20CredExRecord( cred_ex_id="dummy-cxid", initiator=V20CredExRecord.INITIATOR_EXTERNAL, @@ -877,23 +881,18 @@ async def test_receive_request_no_cred_ex_with_offer_found(self): ) as mock_save, async_mock.patch.object( V20CredExRecord, "retrieve_by_conn_and_thread", async_mock.CoroutineMock() ) as mock_retrieve, async_mock.patch.object( - V20CredExRecord, "retrieve_by_tag_filter", async_mock.CoroutineMock() - ) as mock_retrieve_tag_filter, async_mock.patch.object( V20CredFormat.Format, "handler" ) as mock_handler: mock_retrieve.side_effect = (StorageNotFoundError(),) - mock_retrieve_tag_filter.side_effect = (StorageNotFoundError(),) mock_handler.return_value.receive_request = async_mock.CoroutineMock() - cx_rec = await self.manager.receive_request(cred_request, "test_conn_id") + cx_rec = await self.manager.receive_request(cred_request, mock_conn, None) mock_retrieve.assert_called_once_with( - self.session, "test_conn_id", cred_request._thread_id - ) - mock_retrieve_tag_filter.assert_called_once_with( self.session, - {"thread_id": cred_request._thread_id}, - {"connection_id": None}, + "test_conn_id", + cred_request._thread_id, + role=V20CredExRecord.ROLE_ISSUER, ) mock_handler.return_value.receive_request.assert_called_once_with( cx_rec, cred_request @@ -981,7 +980,6 @@ async def test_issue_credential(self): ) as mock_save, async_mock.patch.object( V20CredFormat.Format, "handler" ) as mock_handler: - mock_handler.return_value.issue_credential = async_mock.CoroutineMock( return_value=( V20CredFormat( @@ -1094,7 +1092,6 @@ async def test_receive_cred(self): ) as mock_retrieve, async_mock.patch.object( V20CredFormat.Format, "handler" ) as mock_handler: - mock_handler.return_value.receive_credential = async_mock.CoroutineMock() mock_retrieve.return_value = stored_cx_rec ret_cx_rec = await self.manager.receive_credential( @@ -1103,7 +1100,10 @@ async def test_receive_cred(self): ) mock_retrieve.assert_called_once_with( - self.session, connection_id, cred_issue._thread_id + self.session, + connection_id, + cred_issue._thread_id, + role=V20CredExRecord.ROLE_HOLDER, ) mock_save.assert_called_once() mock_handler.return_value.receive_credential.assert_called_once_with( @@ -1239,7 +1239,6 @@ async def test_store_credential(self): ) as mock_delete, async_mock.patch.object( V20CredFormat.Format, "handler" ) as mock_handler: - mock_handler.return_value.store_credential = async_mock.CoroutineMock() ret_cx_rec = await self.manager.store_credential( @@ -1331,6 +1330,7 @@ async def test_receive_cred_ack(self): self.session, connection_id, ack._thread_id, + role=V20CredExRecord.ROLE_ISSUER, ) mock_save.assert_called_once() @@ -1400,7 +1400,7 @@ async def test_receive_problem_report(self): ) save_ex.assert_called_once() - assert ret_exchange.state is None + assert ret_exchange.state == V20CredExRecord.STATE_ABANDONED async def test_receive_problem_report_x(self): connection_id = "connection-id" diff --git a/aries_cloudagent/protocols/issue_credential/v2_0/tests/test_routes.py b/aries_cloudagent/protocols/issue_credential/v2_0/tests/test_routes.py index 23c00f4b3f..845f21555c 100644 --- a/aries_cloudagent/protocols/issue_credential/v2_0/tests/test_routes.py +++ b/aries_cloudagent/protocols/issue_credential/v2_0/tests/test_routes.py @@ -2,10 +2,6 @@ from asynctest import mock as async_mock, TestCase as AsyncTestCase from .....admin.request_context import AdminRequestContext -from .....wallet.key_type import KeyType -from .....wallet.did_method import DIDMethod -from .....wallet.base import BaseWallet -from .....wallet.did_info import DIDInfo from .. import routes as test_module from ..formats.indy.handler import IndyCredFormatHandler @@ -351,6 +347,49 @@ async def test_credential_exchange_send(self): mock_response.assert_called_once_with(mock_cx_rec.serialize.return_value) + async def test_credential_exchange_send_request_no_conn_no_holder_did(self): + self.request.json = async_mock.CoroutineMock(return_value={}) + self.request.match_info = {"cred_ex_id": "dummy"} + + with async_mock.patch.object( + test_module, "OobRecord", autospec=True + ) as mock_oob_rec, async_mock.patch.object( + test_module, "default_did_from_verkey", autospec=True + ) as mock_default_did_from_verkey, async_mock.patch.object( + test_module, "V20CredManager", autospec=True + ) as mock_cred_mgr, async_mock.patch.object( + test_module, "V20CredExRecord", autospec=True + ) as mock_cred_ex, async_mock.patch.object( + test_module.web, "json_response" + ) as mock_response: + mock_oob_rec.retrieve_by_tag_filter = async_mock.CoroutineMock( + return_value=async_mock.MagicMock(our_recipient_key="our-recipient_key") + ) + mock_default_did_from_verkey.return_value = "holder-did" + + mock_cred_ex.retrieve_by_id = async_mock.CoroutineMock() + mock_cred_ex.retrieve_by_id.return_value.state = ( + mock_cred_ex.STATE_OFFER_RECEIVED + ) + mock_cred_ex.retrieve_by_id.return_value.connection_id = None + + mock_cred_ex_record = async_mock.MagicMock() + + mock_cred_mgr.return_value.create_request.return_value = ( + mock_cred_ex_record, + async_mock.MagicMock(), + ) + + await test_module.credential_exchange_send_bound_request(self.request) + + mock_cred_mgr.return_value.create_request.assert_called_once_with( + mock_cred_ex.retrieve_by_id.return_value, "holder-did" + ) + mock_response.assert_called_once_with( + mock_cred_ex_record.serialize.return_value + ) + mock_default_did_from_verkey.assert_called_once_with("our-recipient_key") + async def test_credential_exchange_send_no_conn_record(self): connection_id = "connection-id" preview_spec = {"attributes": [{"name": "attr", "value": "value"}]} @@ -367,7 +406,6 @@ async def test_credential_exchange_send_no_conn_record(self): ) as mock_conn_rec, async_mock.patch.object( test_module, "V20CredManager", autospec=True ) as mock_cred_mgr: - # Emulate storage not found (bad connection id) mock_conn_rec.retrieve_by_id = async_mock.CoroutineMock( side_effect=test_module.StorageNotFoundError() @@ -544,7 +582,6 @@ async def test_credential_exchange_send_proposal_not_ready(self): ) as mock_cred_mgr, async_mock.patch.object( test_module.V20CredPreview, "deserialize", autospec=True ) as mock_preview_deser: - # Emulate connection not ready mock_conn_rec.retrieve_by_id = async_mock.CoroutineMock() mock_conn_rec.retrieve_by_id.return_value.is_ready = False @@ -691,7 +728,6 @@ async def test_credential_exchange_send_free_offer_no_conn_record(self): ) as mock_conn_rec, async_mock.patch.object( test_module, "V20CredManager", autospec=True ) as mock_cred_mgr: - # Emulate storage not found (bad connection id) mock_conn_rec.retrieve_by_id = async_mock.CoroutineMock( side_effect=test_module.StorageNotFoundError() @@ -722,7 +758,6 @@ async def test_credential_exchange_send_free_offer_not_ready(self): ) as mock_conn_rec, async_mock.patch.object( test_module, "V20CredManager", autospec=True ) as mock_cred_mgr: - # Emulate connection not ready mock_conn_rec.retrieve_by_id = async_mock.CoroutineMock() mock_conn_rec.retrieve_by_id.return_value.is_ready = False @@ -946,7 +981,6 @@ async def test_credential_exchange_send_request(self): ) as mock_cx_rec_cls, async_mock.patch.object( test_module.web, "json_response" ) as mock_response: - mock_cx_rec_cls.retrieve_by_id = async_mock.CoroutineMock() mock_cx_rec_cls.retrieve_by_id.return_value.state = ( test_module.V20CredExRecord.STATE_OFFER_RECEIVED @@ -1056,7 +1090,6 @@ async def test_credential_exchange_send_free_request(self): ) as mock_cred_mgr, async_mock.patch.object( test_module.web, "json_response" ) as mock_response: - mock_cred_mgr.return_value.create_request = async_mock.CoroutineMock() mock_cx_rec = async_mock.MagicMock() @@ -1091,7 +1124,6 @@ async def test_credential_exchange_send_free_request_no_conn_record(self): ) as mock_conn_rec, async_mock.patch.object( test_module, "V20CredManager", autospec=True ) as mock_cred_mgr: - # Emulate storage not found (bad connection id) mock_conn_rec.retrieve_by_id = async_mock.CoroutineMock( side_effect=test_module.StorageNotFoundError() @@ -1118,7 +1150,6 @@ async def test_credential_exchange_send_free_request_not_ready(self): ) as mock_conn_rec, async_mock.patch.object( test_module, "V20CredManager", autospec=True ) as mock_cred_mgr: - # Emulate connection not ready mock_conn_rec.retrieve_by_id = async_mock.CoroutineMock() mock_conn_rec.retrieve_by_id.return_value.is_ready = False @@ -1146,7 +1177,6 @@ async def test_credential_exchange_send_free_request_x(self): ) as mock_cred_mgr, async_mock.patch.object( test_module.web, "json_response" ) as mock_response: - mock_cred_mgr.return_value.create_request = async_mock.CoroutineMock( side_effect=[ test_module.LedgerError(), @@ -1237,7 +1267,6 @@ async def test_credential_exchange_issue_no_conn_record(self): ) as mock_cred_mgr, async_mock.patch.object( test_module, "V20CredExRecord", autospec=True ) as mock_cx_rec_cls: - mock_cx_rec.state = mock_cx_rec_cls.STATE_REQUEST_RECEIVED mock_cx_rec_cls.retrieve_by_id = async_mock.CoroutineMock( return_value=mock_cx_rec @@ -1268,7 +1297,6 @@ async def test_credential_exchange_issue_not_ready(self): ) as mock_cred_mgr, async_mock.patch.object( test_module, "V20CredExRecord", autospec=True ) as mock_cx_rec: - mock_cx_rec.retrieve_by_id = async_mock.CoroutineMock() mock_cx_rec.retrieve_by_id.return_value.state = ( test_module.V20CredExRecord.STATE_REQUEST_RECEIVED @@ -1303,7 +1331,6 @@ async def test_credential_exchange_issue_rev_reg_full(self): ) as mock_cred_mgr, async_mock.patch.object( test_module, "V20CredExRecord", autospec=True ) as mock_cx_rec_cls: - mock_cx_rec.state = mock_cx_rec_cls.STATE_REQUEST_RECEIVED mock_cx_rec_cls.retrieve_by_id = async_mock.CoroutineMock( return_value=mock_cx_rec @@ -1424,7 +1451,6 @@ async def test_credential_exchange_store_bad_cred_id_json(self): ) as mock_ld_proof_get_detail_record, async_mock.patch.object( IndyCredFormatHandler, "get_detail_record", autospec=True ) as mock_indy_get_detail_record: - mock_cx_rec_cls.retrieve_by_id = async_mock.CoroutineMock() mock_cx_rec_cls.retrieve_by_id.return_value.state = ( test_module.V20CredExRecord.STATE_CREDENTIAL_RECEIVED diff --git a/aries_cloudagent/protocols/out_of_band/definition.py b/aries_cloudagent/protocols/out_of_band/definition.py index 62bddef6f5..13c1f8a8ef 100644 --- a/aries_cloudagent/protocols/out_of_band/definition.py +++ b/aries_cloudagent/protocols/out_of_band/definition.py @@ -4,7 +4,7 @@ { "major_version": 1, "minimum_minor_version": 0, - "current_minor_version": 0, + "current_minor_version": 1, "path": "v1_0", } ] diff --git a/aries_cloudagent/protocols/out_of_band/v1_0/manager.py b/aries_cloudagent/protocols/out_of_band/v1_0/manager.py index 7b8478e258..4cf530a0ec 100644 --- a/aries_cloudagent/protocols/out_of_band/v1_0/manager.py +++ b/aries_cloudagent/protocols/out_of_band/v1_0/manager.py @@ -1,56 +1,43 @@ """Classes to manage connections.""" import asyncio -import json import logging +import re +from typing import Mapping, Optional, Sequence, Union, Text +import uuid -from typing import Mapping, Sequence +from ....messaging.decorators.service_decorator import ServiceDecorator +from ....core.event_bus import EventBus +from ....core.util import get_version_from_message from ....connections.base_manager import BaseConnectionManager from ....connections.models.conn_record import ConnRecord -from ....connections.util import mediation_record_if_id from ....core.error import BaseError +from ....core.oob_processor import OobMessageProcessor from ....core.profile import Profile from ....did.did_key import DIDKey -from ....indy.holder import IndyHolder -from ....indy.models.xform import indy_proof_req_preview2indy_requested_creds -from ....messaging.decorators.attach_decorator import AttachDecorator from ....messaging.responder import BaseResponder -from ....multitenant.base import BaseMultitenantManager from ....storage.error import StorageNotFoundError from ....transport.inbound.receipt import MessageReceipt from ....wallet.base import BaseWallet -from ....wallet.util import b64_to_bytes -from ....wallet.key_type import KeyType - -from ...coordinate_mediation.v1_0.manager import MediationManager +from ....wallet.key_type import ED25519 from ...connections.v1_0.manager import ConnectionManager from ...connections.v1_0.messages.connection_invitation import ConnectionInvitation from ...didcomm_prefix import DIDCommPrefix from ...didexchange.v1_0.manager import DIDXManager -from ...issue_credential.v1_0.manager import CredentialManager as V10CredManager -from ...issue_credential.v1_0.messages.credential_offer import ( - CredentialOffer as V10CredOffer, -) -from ...issue_credential.v1_0.message_types import CREDENTIAL_OFFER from ...issue_credential.v1_0.models.credential_exchange import V10CredentialExchange -from ...issue_credential.v2_0.manager import V20CredManager -from ...issue_credential.v2_0.messages.cred_offer import V20CredOffer -from ...issue_credential.v2_0.message_types import CRED_20_OFFER from ...issue_credential.v2_0.models.cred_ex_record import V20CredExRecord -from ...present_proof.v1_0.manager import PresentationManager -from ...present_proof.v1_0.message_types import PRESENTATION_REQUEST from ...present_proof.v1_0.models.presentation_exchange import V10PresentationExchange -from ...present_proof.v2_0.manager import V20PresManager -from ...present_proof.v2_0.message_types import PRES_20_REQUEST from ...present_proof.v2_0.models.pres_exchange import V20PresExRecord - from .messages.invitation import HSProto, InvitationMessage from .messages.problem_report import OOBProblemReport from .messages.reuse import HandshakeReuse from .messages.reuse_accept import HandshakeReuseAccept from .messages.service import Service as ServiceMessage from .models.invitation import InvitationRecord +from .models.oob_record import OobRecord +from .messages.service import Service +from .message_types import DEFAULT_VERSION LOGGER = logging.getLogger(__name__) REUSE_WEBHOOK_TOPIC = "acapy::webhook::connection_reuse" @@ -101,6 +88,8 @@ async def create_invitation( attachments: Sequence[Mapping] = None, metadata: dict = None, mediation_id: str = None, + service_accept: Optional[Sequence[Text]] = None, + protocol_version: Optional[Text] = None, ) -> InvitationRecord: """ Generate new connection invitation. @@ -119,18 +108,20 @@ async def create_invitation( multi_use: set to True to create an invitation for multiple-use connection alias: optional alias to apply to connection for later use attachments: list of dicts in form of {"id": ..., "type": ...} + service_accept: Optional list of mime types in the order of preference of + the sender that the receiver can use in responding to the message + protocol_version: OOB protocol version [1.0, 1.1] Returns: Invitation record """ - mediation_mgr = MediationManager(self.profile) - mediation_record = await mediation_record_if_id( + mediation_record = await self._route_manager.mediation_record_if_id( self.profile, mediation_id, or_default=True, ) - keylist_updates = None + image_url = self.profile.context.settings.get("image_url") if not (hs_protos or attachments): raise OutOfBandManagerError( @@ -138,32 +129,32 @@ async def create_invitation( "request attachments, or both" ) - # Multitenancy setup - multitenant_mgr = self.profile.inject_or(BaseMultitenantManager) - wallet_id = self.profile.settings.get("wallet.id") - - accept = bool( + auto_accept = bool( auto_accept or ( auto_accept is None and self.profile.settings.get("debug.auto_accept_requests") ) ) - if public: - if multi_use: - raise OutOfBandManagerError( - "Cannot create public invitation with multi_use" - ) - if metadata: - raise OutOfBandManagerError( - "Cannot store metadata on public invitations" - ) + if not hs_protos and metadata: + raise OutOfBandManagerError( + "Cannot store metadata without handshake protocols" + ) + + if attachments and multi_use: + raise OutOfBandManagerError( + "Cannot create multi use invitation with attachments" + ) + + invitation_message_id = str(uuid.uuid4()) message_attachments = [] for atch in attachments or []: a_type = atch.get("type") a_id = atch.get("id") + message = None + if a_type == "credential-offer": try: async with self.profile.session() as session: @@ -171,22 +162,15 @@ async def create_invitation( session, a_id, ) - message_attachments.append( - InvitationMessage.wrap_message( - cred_ex_rec.credential_offer_dict.serialize() - ) - ) + message = cred_ex_rec.credential_offer_dict.serialize() + except StorageNotFoundError: async with self.profile.session() as session: cred_ex_rec = await V20CredExRecord.retrieve_by_id( session, a_id, ) - message_attachments.append( - InvitationMessage.wrap_message( - cred_ex_rec.cred_offer.serialize() - ) - ) + message = cred_ex_rec.cred_offer.serialize() elif a_type == "present-proof": try: async with self.profile.session() as session: @@ -194,25 +178,24 @@ async def create_invitation( session, a_id, ) - message_attachments.append( - InvitationMessage.wrap_message( - pres_ex_rec.presentation_request_dict.serialize() - ) - ) + message = pres_ex_rec.presentation_request_dict.serialize() except StorageNotFoundError: async with self.profile.session() as session: pres_ex_rec = await V20PresExRecord.retrieve_by_id( session, a_id, ) - message_attachments.append( - InvitationMessage.wrap_message( - pres_ex_rec.pres_request.serialize() - ) - ) + message = pres_ex_rec.pres_request.serialize() else: raise OutOfBandManagerError(f"Unknown attachment type: {a_type}") + # Assign pthid to the attached message + message["~thread"] = { + **message.get("~thread", {}), + "pthid": invitation_message_id, + } + message_attachments.append(InvitationMessage.wrap_message(message)) + handshake_protocols = [ DIDCommPrefix.qualify_current(hsp.name) for hsp in hs_protos or [] ] or None @@ -220,6 +203,10 @@ async def create_invitation( hs_protos[0].name if hs_protos and len(hs_protos) >= 1 else None ) + our_recipient_key = None + our_service = None + conn_rec = None + if public: if not self.profile.settings.get("public_invites"): raise OutOfBandManagerError("Public invitations are not enabled") @@ -227,118 +214,126 @@ async def create_invitation( async with self.profile.session() as session: wallet = session.inject(BaseWallet) public_did = await wallet.get_public_did() + if not public_did: raise OutOfBandManagerError( "Cannot create public invitation with no public DID" ) invi_msg = InvitationMessage( # create invitation message + _id=invitation_message_id, label=my_label or self.profile.settings.get("default_label"), handshake_protocols=handshake_protocols, requests_attach=message_attachments, services=[f"did:sov:{public_did.did}"], + accept=service_accept if protocol_version != "1.0" else None, + version=protocol_version or DEFAULT_VERSION, + image_url=image_url, ) - keylist_updates = await mediation_mgr.add_key( - public_did.verkey, keylist_updates - ) + + our_recipient_key = public_did.verkey endpoint, *_ = await self.resolve_invitation(public_did.did) invi_url = invi_msg.to_url(endpoint) - conn_rec = ConnRecord( # create connection record - invitation_key=public_did.verkey, - invitation_msg_id=invi_msg._id, - their_role=ConnRecord.Role.REQUESTER.rfc23, - state=ConnRecord.State.INVITATION.rfc23, - accept=ConnRecord.ACCEPT_AUTO if accept else ConnRecord.ACCEPT_MANUAL, - alias=alias, - connection_protocol=connection_protocol, - ) + # Only create connection record if hanshake_protocols is defined + if handshake_protocols: + invitation_mode = ( + ConnRecord.INVITATION_MODE_MULTI + if multi_use + else ConnRecord.INVITATION_MODE_ONCE + ) + conn_rec = ConnRecord( # create connection record + invitation_key=public_did.verkey, + invitation_msg_id=invi_msg._id, + invitation_mode=invitation_mode, + their_role=ConnRecord.Role.REQUESTER.rfc23, + state=ConnRecord.State.INVITATION.rfc23, + accept=ConnRecord.ACCEPT_AUTO + if auto_accept + else ConnRecord.ACCEPT_MANUAL, + alias=alias, + connection_protocol=connection_protocol, + ) - async with self.profile.session() as session: - await conn_rec.save(session, reason="Created new invitation") - await conn_rec.attach_invitation(session, invi_msg) + async with self.profile.session() as session: + await conn_rec.save(session, reason="Created new invitation") + await conn_rec.attach_invitation(session, invi_msg) - if multitenant_mgr and wallet_id: # add mapping for multitenant relay - await multitenant_mgr.add_key( - wallet_id, public_did.verkey, skip_if_exists=True - ) + await conn_rec.attach_invitation(session, invi_msg) - else: - invitation_mode = ( - ConnRecord.INVITATION_MODE_MULTI - if multi_use - else ConnRecord.INVITATION_MODE_ONCE - ) + if metadata: + for key, value in metadata.items(): + await conn_rec.metadata_set(session, key, value) + else: + our_service = ServiceDecorator( + recipient_keys=[our_recipient_key], + endpoint=endpoint, + routing_keys=[], + ).serialize() + else: if not my_endpoint: my_endpoint = self.profile.settings.get("default_endpoint") - # Create and store new invitation key - + # Create and store new key for exchange async with self.profile.session() as session: wallet = session.inject(BaseWallet) - connection_key = await wallet.create_signing_key(KeyType.ED25519) - keylist_updates = await mediation_mgr.add_key( - connection_key.verkey, keylist_updates - ) - # Add mapping for multitenant relay - if multitenant_mgr and wallet_id: - await multitenant_mgr.add_key(wallet_id, connection_key.verkey) + connection_key = await wallet.create_signing_key(ED25519) + + our_recipient_key = connection_key.verkey + # Initializing InvitationMessage here to include # invitation_msg_id in webhook poyload - invi_msg = InvitationMessage() - # Create connection record - conn_rec = ConnRecord( - invitation_key=connection_key.verkey, - their_role=ConnRecord.Role.REQUESTER.rfc23, - state=ConnRecord.State.INVITATION.rfc23, - accept=ConnRecord.ACCEPT_AUTO if accept else ConnRecord.ACCEPT_MANUAL, - invitation_mode=invitation_mode, - alias=alias, - connection_protocol=connection_protocol, - invitation_msg_id=invi_msg._id, + invi_msg = InvitationMessage( + _id=invitation_message_id, version=protocol_version or DEFAULT_VERSION ) - async with self.profile.session() as session: - await conn_rec.save(session, reason="Created new connection") - - routing_keys = [] - # The base wallet can act as a mediator for all tenants - if multitenant_mgr and wallet_id: - base_mediation_record = await multitenant_mgr.get_default_mediator() - - if base_mediation_record: - routing_keys = base_mediation_record.routing_keys - my_endpoint = base_mediation_record.endpoint + if handshake_protocols: + invitation_mode = ( + ConnRecord.INVITATION_MODE_MULTI + if multi_use + else ConnRecord.INVITATION_MODE_ONCE + ) + # Create connection record + conn_rec = ConnRecord( + invitation_key=connection_key.verkey, + their_role=ConnRecord.Role.REQUESTER.rfc23, + state=ConnRecord.State.INVITATION.rfc23, + accept=ConnRecord.ACCEPT_AUTO + if auto_accept + else ConnRecord.ACCEPT_MANUAL, + invitation_mode=invitation_mode, + alias=alias, + connection_protocol=connection_protocol, + invitation_msg_id=invi_msg._id, + ) - # If we use a mediator for the base wallet we don't - # need to register the key at the subwallet mediator - # because it only needs to know the key of the base mediator - # sub wallet mediator -> base wallet mediator -> agent - keylist_updates = None - if mediation_record: - routing_keys = [*routing_keys, *mediation_record.routing_keys] - my_endpoint = mediation_record.endpoint + async with self.profile.session() as session: + await conn_rec.save(session, reason="Created new connection") - # Save that this invitation was created with mediation + routing_keys, my_endpoint = await self._route_manager.routing_info( + self.profile, my_endpoint, mediation_record + ) - async with self.profile.session() as session: - await conn_rec.metadata_set( - session, "mediation", {"id": mediation_record.mediation_id} - ) + if not conn_rec: + our_service = ServiceDecorator( + recipient_keys=[our_recipient_key], + endpoint=my_endpoint, + routing_keys=routing_keys, + ).serialize() + # Need to make sure the created key is routed by the base wallet + await self._route_manager.route_verkey( + self.profile, connection_key.verkey + ) - if keylist_updates: - responder = self.profile.inject_or(BaseResponder) - await responder.send( - keylist_updates, connection_id=mediation_record.connection_id - ) routing_keys = [ key if len(key.split(":")) == 3 - else DIDKey.from_public_key_b58(key, KeyType.ED25519).did + else DIDKey.from_public_key_b58(key, ED25519).did for key in routing_keys ] + # Create connection invitation message # Note: Need to split this into two stages to support inbound routing # of invitations @@ -346,14 +341,14 @@ async def create_invitation( invi_msg.label = my_label or self.profile.settings.get("default_label") invi_msg.handshake_protocols = handshake_protocols invi_msg.requests_attach = message_attachments + invi_msg.accept = service_accept if protocol_version != "1.0" else None + invi_msg.image_url = image_url invi_msg.services = [ ServiceMessage( _id="#inline", _type="did-communication", recipient_keys=[ - DIDKey.from_public_key_b58( - connection_key.verkey, KeyType.ED25519 - ).did + DIDKey.from_public_key_b58(connection_key.verkey, ED25519).did ], service_endpoint=my_endpoint, routing_keys=routing_keys, @@ -362,16 +357,35 @@ async def create_invitation( invi_url = invi_msg.to_url() # Update connection record + if conn_rec: + async with self.profile.session() as session: + await conn_rec.attach_invitation(session, invi_msg) - async with self.profile.session() as session: - await conn_rec.attach_invitation(session, invi_msg) + if metadata: + for key, value in metadata.items(): + await conn_rec.metadata_set(session, key, value) - if metadata: - async with self.profile.session() as session: - for key, value in metadata.items(): - await conn_rec.metadata_set(session, key, value) + oob_record = OobRecord( + role=OobRecord.ROLE_SENDER, + state=OobRecord.STATE_AWAIT_RESPONSE, + connection_id=conn_rec.connection_id if conn_rec else None, + invi_msg_id=invi_msg._id, + invitation=invi_msg, + our_recipient_key=our_recipient_key, + our_service=our_service, + multi_use=multi_use, + ) + + async with self.profile.session() as session: + await oob_record.save(session, reason="Created new oob invitation") + + if conn_rec: + await self._route_manager.route_invitation( + self.profile, conn_rec, mediation_record + ) return InvitationRecord( # for return via admin API, not storage + oob_id=oob_record.oob_id, state=InvitationRecord.STATE_INITIAL, invi_msg_id=invi_msg._id, invitation=invi_msg, @@ -382,10 +396,10 @@ async def receive_invitation( self, invitation: InvitationMessage, use_existing_connection: bool = True, - auto_accept: bool = None, - alias: str = None, - mediation_id: str = None, - ) -> ConnRecord: + auto_accept: Optional[bool] = None, + alias: Optional[str] = None, + mediation_id: Optional[str] = None, + ) -> OobRecord: """ Receive an out of band invitation message. @@ -402,7 +416,9 @@ async def receive_invitation( """ if mediation_id: try: - await mediation_record_if_id(self.profile, mediation_id) + await self._route_manager.mediation_record_if_id( + self.profile, mediation_id + ) except StorageNotFoundError: mediation_id = None @@ -414,558 +430,442 @@ async def receive_invitation( raise OutOfBandManagerError( "Invitation must specify handshake_protocols, requests_attach, or both" ) + # Get the single service item oob_service_item = invitation.services[0] - if isinstance(oob_service_item, ServiceMessage): - service = oob_service_item - public_did = None - else: - # If it's in the did format, we need to convert to a full service block - # An existing connection can only be reused based on a public DID - # in an out-of-band message (RFC 0434). - - service_did = oob_service_item - # TODO: resolve_invitation should resolve key_info objects - # or something else that includes the key type. We now assume - # ED25519 keys - endpoint, recipient_keys, routing_keys = await self.resolve_invitation( - service_did - ) - public_did = service_did.split(":")[-1] - service = ServiceMessage.deserialize( - { - "id": "#inline", - "type": "did-communication", - "recipientKeys": [ - DIDKey.from_public_key_b58(key, KeyType.ED25519).did - for key in recipient_keys - ], - "routingKeys": [ - DIDKey.from_public_key_b58(key, KeyType.ED25519).did - for key in routing_keys - ], - "serviceEndpoint": endpoint, - } - ) + # service_accept + service_accept = invitation.accept - unq_handshake_protos = [ - HSProto.get(hsp) - for hsp in dict.fromkeys( - [ - DIDCommPrefix.unqualify(proto) - for proto in invitation.handshake_protocols - ] - ) - ] + # Get the DID public did, if any + public_did = None + if isinstance(oob_service_item, str): + public_did = oob_service_item.split(":")[-1] - # Reuse Connection - only if started by an invitation with Public DID conn_rec = None - if public_did is not None: # invite has public DID: seek existing connection + + # Find existing connection - only if started by an invitation with Public DID + # and use_existing_connection is true + if ( + public_did is not None and use_existing_connection + ): # invite has public DID: seek existing connection + LOGGER.debug( + "Trying to find existing connection for oob invitation with " + f"did {public_did}" + ) async with self._profile.session() as session: conn_rec = await ConnRecord.find_existing_connection( session=session, their_public_did=public_did ) - if conn_rec is not None: - num_included_protocols = len(unq_handshake_protos) - num_included_req_attachments = len(invitation.requests_attach) - # With handshake protocol, request attachment; use existing connection - if ( - num_included_protocols >= 1 - and num_included_req_attachments == 0 - and use_existing_connection - ): - await self.create_handshake_reuse_message( - invi_msg=invitation, - conn_record=conn_rec, - ) - try: - await asyncio.wait_for( - self.check_reuse_msg_state( - conn_rec=conn_rec, - ), - 15, - ) - async with self.profile.session() as session: - await conn_rec.metadata_delete( - session=session, key="reuse_msg_id" - ) - msg_state = await conn_rec.metadata_get( - session, "reuse_msg_state" - ) + oob_record = OobRecord( + role=OobRecord.ROLE_RECEIVER, + invi_msg_id=invitation._id, + invitation=invitation, + state=OobRecord.STATE_INITIAL, + connection_id=conn_rec.connection_id if conn_rec else None, + ) - if msg_state == "not_accepted": - conn_rec = None - else: - async with self.profile.session() as session: - await conn_rec.metadata_delete( - session=session, key="reuse_msg_state" - ) - # refetch connection for accurate state after handshake - conn_rec = await ConnRecord.retrieve_by_id( - session=session, record_id=conn_rec.connection_id - ) - except asyncio.TimeoutError: - # If no reuse_accepted or problem_report message was received within - # the 15s timeout then a new connection to be created - async with self.profile.session() as session: - sent_reuse_msg_id = await conn_rec.metadata_get( - session=session, key="reuse_msg_id" - ) - await conn_rec.metadata_delete( - session=session, key="reuse_msg_id" - ) - await conn_rec.metadata_delete( - session=session, key="reuse_msg_state" - ) - conn_rec.state = ConnRecord.State.ABANDONED.rfc160 - await conn_rec.save( - session, reason="No HandshakeReuseAccept message received" - ) - # Emit webhook - await self.profile.notify( - REUSE_ACCEPTED_WEBHOOK_TOPIC, - { - "thread_id": sent_reuse_msg_id, - "connection_id": conn_rec.connection_id, - "state": "rejected", - "comment": ( - "No HandshakeReuseAccept message received, " - f"connection {conn_rec.connection_id} ", - f"and invitation {invitation._id}", - ), - }, - ) - conn_rec = None - # Inverse of the following cases - # Handshake_Protocol not included - # Request_Attachment included - # Use_Existing_Connection Yes - # Handshake_Protocol included - # Request_Attachment included - # Use_Existing_Connection Yes - elif not ( - ( - num_included_protocols == 0 - and num_included_req_attachments >= 1 - and use_existing_connection - ) - or ( - num_included_protocols >= 1 - and num_included_req_attachments >= 1 - and use_existing_connection + # Try to reuse the connection. If not accepted sets the conn_rec to None + if conn_rec and not invitation.requests_attach: + oob_record = await self._handle_hanshake_reuse( + oob_record, conn_rec, get_version_from_message(invitation) + ) + + LOGGER.warning( + f"Connection reuse request finished with state {oob_record.state}" + ) + + if oob_record.state == OobRecord.STATE_ACCEPTED: + return oob_record + else: + # Set connection record to None if not accepted + # Will make new connection + conn_rec = None + + # Try to create a connection. Either if the reuse failed or we didn't have a + # connection yet. Throws an error if connection could not be created + if not conn_rec and invitation.handshake_protocols: + oob_record = await self._perform_handshake( + oob_record=oob_record, + alias=alias, + auto_accept=auto_accept, + mediation_id=mediation_id, + service_accept=service_accept, + ) + LOGGER.debug( + f"Performed handshake with connection {oob_record.connection_id}" + ) + # re-fetch connection record + async with self.profile.session() as session: + conn_rec = await ConnRecord.retrieve_by_id( + session, oob_record.connection_id ) + + # If a connection record is associated with the oob record we can remove it now as + # we can leverage the connection for all exchanges. Otherwise we need to keep it + # around for the connectionless exchange + if conn_rec: + oob_record.state = OobRecord.STATE_DONE + async with self.profile.session() as session: + await oob_record.emit_event(session) + await oob_record.delete_record(session) + else: + oob_record.state = OobRecord.STATE_PREPARE_RESPONSE + async with self.profile.session() as session: + await oob_record.save(session) + + # Handle any attachments + if invitation.requests_attach: + LOGGER.debug( + f"Process attached messages for oob exchange {oob_record.oob_id} " + f"(connection_id {oob_record.connection_id})" + ) + + # FIXME: this should ideally be handled using an event handler. Once the + # connection is ready we start processing the attached messages. + # For now we use the timeout method + if ( + conn_rec + and not conn_rec.is_ready + and not await self._wait_for_conn_rec_active(conn_rec.connection_id) ): - conn_rec = None - if conn_rec is None: - if not unq_handshake_protos: raise OutOfBandManagerError( - "No existing connection exists and handshake_protocol is missing" + "Connection not ready to process attach message " + f"for connection_id: {oob_record.connection_id} and " + f"invitation_msg_id {invitation._id}", ) - # Create a new connection - for proto in unq_handshake_protos: - if proto is HSProto.RFC23: - didx_mgr = DIDXManager(self.profile) - conn_rec = await didx_mgr.receive_invitation( - invitation=invitation, - their_public_did=public_did, - auto_accept=auto_accept, - alias=alias, - mediation_id=mediation_id, - ) - elif proto is HSProto.RFC160: - service.recipient_keys = [ - DIDKey.from_did(key).public_key_b58 - for key in service.recipient_keys or [] - ] - service.routing_keys = [ - DIDKey.from_did(key).public_key_b58 - for key in service.routing_keys - ] or [] - connection_invitation = ConnectionInvitation.deserialize( - { - "@id": invitation._id, - "@type": DIDCommPrefix.qualify_current(proto.name), - "label": invitation.label, - "recipientKeys": service.recipient_keys, - "serviceEndpoint": service.service_endpoint, - "routingKeys": service.routing_keys, - } - ) - conn_mgr = ConnectionManager(self.profile) - conn_rec = await conn_mgr.receive_invitation( - invitation=connection_invitation, - their_public_did=public_did, - auto_accept=auto_accept, - alias=alias, - mediation_id=mediation_id, - ) - if conn_rec is not None: - break - - # Request Attach - if len(invitation.requests_attach) >= 1 and conn_rec is not None: - req_attach = invitation.requests_attach[0] - if isinstance(req_attach, AttachDecorator): - if req_attach.data is not None: - unq_req_attach_type = DIDCommPrefix.unqualify( - req_attach.content["@type"] + + if not conn_rec: + # Create and store new key for connectionless exchange + async with self.profile.session() as session: + wallet = session.inject(BaseWallet) + connection_key = await wallet.create_signing_key(ED25519) + oob_record.our_recipient_key = connection_key.verkey + oob_record.our_service = ServiceDecorator( + recipient_keys=[connection_key.verkey], + endpoint=self.profile.settings.get("default_endpoint"), + routing_keys=[], + ).serialize() + + # Need to make sure the created key is routed by the base wallet + await self._route_manager.route_verkey( + self.profile, connection_key.verkey ) - if unq_req_attach_type == PRESENTATION_REQUEST: - await self._process_pres_request_v1( - req_attach=req_attach, - service=service, - conn_rec=conn_rec, - trace=(invitation._trace is not None), - ) - elif unq_req_attach_type == PRES_20_REQUEST: - await self._process_pres_request_v2( - req_attach=req_attach, - service=service, - conn_rec=conn_rec, - trace=(invitation._trace is not None), - ) - elif unq_req_attach_type == CREDENTIAL_OFFER: - if auto_accept or self.profile.settings.get( - "debug.auto_accept_invites" - ): - try: - conn_rec = await asyncio.wait_for( - self.conn_rec_is_active(conn_rec.connection_id), - 7, - ) - except asyncio.TimeoutError: - LOGGER.warning( - "Connection not ready to receive credential, " - f"For connection_id:{conn_rec.connection_id} and " - f"invitation_msg_id {invitation._id}", - ) - await self._process_cred_offer_v1( - req_attach=req_attach, - conn_rec=conn_rec, - trace=(invitation._trace is not None), - ) - elif unq_req_attach_type == CRED_20_OFFER: - if auto_accept or self.profile.settings.get( - "debug.auto_accept_invites" - ): - try: - conn_rec = await asyncio.wait_for( - self.conn_rec_is_active(conn_rec.connection_id), - 7, - ) - except asyncio.TimeoutError: - LOGGER.warning( - "Connection not ready to receive credential, " - f"For connection_id:{conn_rec.connection_id} and " - f"invitation_msg_id {invitation._id}", - ) - await self._process_cred_offer_v2( - req_attach=req_attach, - conn_rec=conn_rec, - trace=(invitation._trace is not None), - ) - else: - raise OutOfBandManagerError( - ( - "Unsupported requests~attach type " - f"{req_attach.content['@type']}: must unqualify to" - f"{PRESENTATION_REQUEST} or {PRES_20_REQUEST}" - f"{CREDENTIAL_OFFER} or {CRED_20_OFFER}" - ) - ) - else: - raise OutOfBandManagerError("requests~attach is not properly formatted") + await oob_record.save(session) - return conn_rec + await self._process_request_attach(oob_record) - async def _process_pres_request_v1( - self, - req_attach: AttachDecorator, - service: ServiceMessage, - conn_rec: ConnRecord, - trace: bool, - ): - """ - Create exchange for v1 pres request attachment, auto-present if configured. + return oob_record - Args: - req_attach: request attachment on invitation - service: service message from invitation - conn_rec: connection record - trace: trace setting for presentation exchange record - """ - pres_mgr = PresentationManager(self.profile) - pres_request_msg = req_attach.content - indy_proof_request = json.loads( - b64_to_bytes( - pres_request_msg["request_presentations~attach"][0]["data"]["base64"] - ) - ) - oob_invi_service = service.serialize() - pres_request_msg["~service"] = { - "recipientKeys": oob_invi_service.get("recipientKeys"), - "routingKeys": oob_invi_service.get("routingKeys"), - "serviceEndpoint": oob_invi_service.get("serviceEndpoint"), - } - pres_ex_record = V10PresentationExchange( - connection_id=conn_rec.connection_id, - thread_id=pres_request_msg["@id"], - initiator=V10PresentationExchange.INITIATOR_EXTERNAL, - role=V10PresentationExchange.ROLE_PROVER, - presentation_request=indy_proof_request, - presentation_request_dict=pres_request_msg, - auto_present=self.profile.context.settings.get( - "debug.auto_respond_presentation_request" - ), - trace=trace, + async def _process_request_attach(self, oob_record: OobRecord): + invitation = oob_record.invitation + + message_processor = self.profile.inject(OobMessageProcessor) + messages = [attachment.content for attachment in invitation.requests_attach] + + their_service = None + if not oob_record.connection_id: + service = oob_record.invitation.services[0] + their_service = await self._service_decorator_from_service(service) + LOGGER.debug("Found service for oob record %s", their_service) + + await message_processor.handle_message( + self.profile, messages, oob_record=oob_record, their_service=their_service ) - pres_ex_record = await pres_mgr.receive_request(pres_ex_record) - if pres_ex_record.auto_present: - try: - async with self.profile.session() as session: - req_creds = await indy_proof_req_preview2indy_requested_creds( - indy_proof_req=indy_proof_request, - preview=None, - holder=session.inject(IndyHolder), - ) - except ValueError as err: - LOGGER.exception( - "Unable to auto-respond to presentation request " - f"{pres_ex_record.presentation_exchange_id}, prover" - " could still build proof manually" - ) - raise OutOfBandManagerError( - "Cannot auto-respond to presentation request attachment" - ) from err - - (pres_ex_record, presentation_message) = await pres_mgr.create_presentation( - presentation_exchange_record=pres_ex_record, - requested_credentials=req_creds, - comment=( - "auto-presented for proof request nonce={}".format( - indy_proof_request["nonce"] - ) - ), + async def _service_decorator_from_service( + self, service: Union[Service, str] + ) -> ServiceDecorator: + if isinstance(service, str): + ( + endpoint, + recipient_keys, + routing_keys, + ) = await self.resolve_invitation(service) + + return ServiceDecorator( + endpoint=endpoint, + recipient_keys=recipient_keys, + routing_keys=routing_keys, ) - responder = self.profile.inject_or(BaseResponder) - if responder: - await responder.send( - message=presentation_message, - target_list=await self.fetch_connection_targets( - connection=conn_rec - ), - ) else: - raise OutOfBandManagerError( - ( - "Configuration sets auto_present false: cannot " - "respond automatically to presentation requests" - ) + # Create ~service decorator from the oob service + recipient_keys = [ + DIDKey.from_did(did_key).public_key_b58 + for did_key in service.recipient_keys + ] + routing_keys = [ + DIDKey.from_did(did_key).public_key_b58 + for did_key in service.routing_keys + ] + + return ServiceDecorator( + endpoint=service.service_endpoint, + recipient_keys=recipient_keys, + routing_keys=routing_keys, ) - async def _process_pres_request_v2( - self, - req_attach: AttachDecorator, - service: ServiceMessage, - conn_rec: ConnRecord, - trace: bool, - ): - """ - Create exchange for v2 pres request attachment, auto-present if configured. + async def _wait_for_reuse_response( + self, oob_id: str, timeout: int = 15 + ) -> OobRecord: + """Wait for reuse response. + + Wait for reuse response message state. Either by receiving a reuse accepted or + problem report. If no answer is received withing the timeout, the state will be + set to reuse_not_accepted Args: - req_attach: request attachment on invitation - service: service message from invitation - conn_rec: connection record - trace: trace setting for presentation exchange record + oob_id: Identifier of the oob record + timeout: The timeout in seconds to wait for the reuse state [default=15] + + Returns: + """ - pres_mgr = V20PresManager(self.profile) - pres_request_msg = req_attach.content - oob_invi_service = service.serialize() - pres_request_msg["~service"] = { - "recipientKeys": oob_invi_service.get("recipientKeys"), - "routingKeys": oob_invi_service.get("routingKeys"), - "serviceEndpoint": oob_invi_service.get("serviceEndpoint"), - } - pres_ex_record = V20PresExRecord( - connection_id=conn_rec.connection_id, - thread_id=pres_request_msg["@id"], - initiator=V20PresExRecord.INITIATOR_EXTERNAL, - role=V20PresExRecord.ROLE_PROVER, - pres_request=pres_request_msg, - auto_present=self.profile.context.settings.get( - "debug.auto_respond_presentation_request" - ), - trace=trace, + OOB_REUSE_RESPONSE_STATE = re.compile( + "^acapy::record::out_of_band::(reuse-accepted|reuse-not-accepted)$" ) - pres_ex_record = await pres_mgr.receive_pres_request(pres_ex_record) - if pres_ex_record.auto_present: - (pres_ex_record, pres_msg) = await pres_mgr.create_pres( - pres_ex_record=pres_ex_record, - comment=( - f"auto-presented for proof requests" - f", pres_ex_record: {pres_ex_record.pres_ex_id}" - ), - ) - responder = self.profile.inject_or(BaseResponder) - if responder: - await responder.send( - message=pres_msg, - target_list=await self.fetch_connection_targets( - connection=conn_rec - ), - ) - else: - raise OutOfBandManagerError( - ( - "Configuration set auto_present false: cannot " - "respond automatically to presentation requests" - ) - ) + async def _wait_for_state() -> OobRecord: + event = self.profile.inject(EventBus) + with event.wait_for_event( + self.profile, + OOB_REUSE_RESPONSE_STATE, + lambda event: event.payload.get("oob_id") == oob_id, + ) as await_event: + # After starting the listener first retrieve the record from storage. + # This rules out the scenario where the record was in the desired state + # Before starting the event listener + async with self.profile.session() as session: + oob_record = await OobRecord.retrieve_by_id(session, oob_id) - async def _process_cred_offer_v1( - self, - req_attach: AttachDecorator, - conn_rec: ConnRecord, - trace: bool, - ): - """ - Create exchange for v1 cred offer attachment, auto-offer if configured. + if oob_record.state in [ + OobRecord.STATE_ACCEPTED, + OobRecord.STATE_NOT_ACCEPTED, + ]: + return oob_record - Args: - req_attach: request attachment on invitation - service: service message from invitation - conn_rec: connection record - """ - cred_mgr = V10CredManager(self.profile) - cred_offer_msg = req_attach.content - cred_offer = V10CredOffer.deserialize(cred_offer_msg) - cred_offer.assign_trace_decorator(self.profile.settings, trace) - # receive credential offer - cred_ex_record = await cred_mgr.receive_offer( - message=cred_offer, connection_id=conn_rec.connection_id + LOGGER.debug(f"Wait for oob {oob_id} to receive reuse accepted mesage") + event = await await_event + LOGGER.debug("Received reuse response message") + return OobRecord.deserialize(event.payload) + + try: + oob_record = await asyncio.wait_for( + _wait_for_state(), + timeout, + ) + + return oob_record + except asyncio.TimeoutError: + async with self.profile.session() as session: + oob_record = await OobRecord.retrieve_by_id(session, oob_id) + return oob_record + + async def _wait_for_conn_rec_active( + self, connection_id: str, timeout: int = 7 + ) -> Optional[ConnRecord]: + CONNECTION_READY_EVENT = re.compile( + "^acapy::record::connections::(active|completed|response)$" ) - if self.profile.context.settings.get("debug.auto_respond_credential_offer"): - if conn_rec.is_ready: - (_, cred_request_message) = await cred_mgr.create_request( - cred_ex_record=cred_ex_record, - holder_did=conn_rec.my_did, - ) - responder = self.profile.inject_or(BaseResponder) - if responder: - await responder.send( - message=cred_request_message, - target_list=await self.fetch_connection_targets( - connection=conn_rec - ), + + LOGGER.debug(f"Wait for connection {connection_id} to become active") + + async def _wait_for_state() -> ConnRecord: + event = self.profile.inject(EventBus) + with event.wait_for_event( + self.profile, + CONNECTION_READY_EVENT, + lambda event: event.payload.get("connection_id") == connection_id, + ) as await_event: + # After starting the listener first retrieve the record from storage. + # This rules out the scenario where the record was in the desired state + # Before starting the event listener + async with self.profile.session() as session: + conn_record = await ConnRecord.retrieve_by_id( + session, connection_id ) - else: - raise OutOfBandManagerError( - ( - "Configuration sets auto_offer false: cannot " - "respond automatically to credential offers" - ) + if conn_record.is_ready: + return conn_record + + LOGGER.debug(f"Wait for connection {connection_id} to become active") + # Wait for connection record to be in state + event = await await_event + return ConnRecord.deserialize(event.payload) + + try: + return await asyncio.wait_for( + _wait_for_state(), + timeout, ) - async def _process_cred_offer_v2( - self, - req_attach: AttachDecorator, - conn_rec: ConnRecord, - trace: bool, - ): - """ - Create exchange for v1 cred offer attachment, auto-offer if configured. + except asyncio.TimeoutError: + LOGGER.warning(f"Connection for connection_id {connection_id} not ready") + return None - Args: - req_attach: request attachment on invitation - service: service message from invitation - conn_rec: connection record - """ - cred_mgr = V20CredManager(self.profile) - cred_offer_msg = req_attach.content - cred_offer = V20CredOffer.deserialize(cred_offer_msg) - cred_offer.assign_trace_decorator(self.profile.settings, trace) + async def _handle_hanshake_reuse( + self, oob_record: OobRecord, conn_record: ConnRecord, version: str + ) -> OobRecord: + # Send handshake reuse + oob_record = await self._create_handshake_reuse_message( + oob_record, conn_record, version + ) - cred_ex_record = await cred_mgr.receive_offer( - cred_offer_message=cred_offer, connection_id=conn_rec.connection_id + # Wait for the reuse accepted message + oob_record = await self._wait_for_reuse_response(oob_record.oob_id) + LOGGER.debug( + f"Oob reuse for oob id {oob_record.oob_id} with connection " + f"{oob_record.connection_id} finished with state {oob_record.state}" ) - if self.profile.context.settings.get("debug.auto_respond_credential_offer"): - if conn_rec.is_ready: - (_, cred_request_message) = await cred_mgr.create_request( - cred_ex_record=cred_ex_record, - holder_did=conn_rec.my_did, - ) - responder = self.profile.inject_or(BaseResponder) - if responder: - await responder.send( - message=cred_request_message, - target_list=await self.fetch_connection_targets( - connection=conn_rec - ), - ) - else: - raise OutOfBandManagerError( - ( - "Configuration sets auto_offer false: cannot " - "respond automatically to credential offers" - ) + + if oob_record.state != OobRecord.STATE_ACCEPTED: + # Remove associated connection id as reuse has ben denied + oob_record.connection_id = None + oob_record.state = OobRecord.STATE_NOT_ACCEPTED + + # OOB_TODO: replace webhook event with new oob webhook event + # Emit webhook if the reuse was not accepted + await self.profile.notify( + REUSE_ACCEPTED_WEBHOOK_TOPIC, + { + "thread_id": oob_record.reuse_msg_id, + "connection_id": conn_record.connection_id, + "state": "rejected", + "comment": ( + "No HandshakeReuseAccept message received, " + f"connection {conn_record.connection_id} ", + f"and invitation {oob_record.invitation._id}", + ), + }, ) - async def check_reuse_msg_state( + async with self.profile.session() as session: + await oob_record.save(session) + + return oob_record + + async def _perform_handshake( self, - conn_rec: ConnRecord, - ): - """ - Check reuse message state from the ConnRecord Metadata. + *, + oob_record: OobRecord, + alias: Optional[str] = None, + auto_accept: Optional[bool] = None, + mediation_id: Optional[str] = None, + service_accept: Optional[Sequence[Text]] = None, + ) -> OobRecord: + invitation = oob_record.invitation + + supported_handshake_protocols = [ + HSProto.get(hsp) + for hsp in dict.fromkeys( + [ + DIDCommPrefix.unqualify(proto) + for proto in invitation.handshake_protocols + ] + ) + ] - Args: - conn_rec: The required ConnRecord with updated metadata + # Get the single service item + service = invitation.services[0] + public_did = None + if isinstance(service, str): + # If it's in the did format, we need to convert to a full service block + # An existing connection can only be reused based on a public DID + # in an out-of-band message (RFC 0434). - Returns: + public_did = service.split(":")[-1] - """ - received = False - while not received: - msg_state = None - async with self.profile.session() as session: - msg_state = await conn_rec.metadata_get(session, "reuse_msg_state") - if msg_state != "initial": - received = True - return + # TODO: resolve_invitation should resolve key_info objects + # or something else that includes the key type. We now assume + # ED25519 keys + endpoint, recipient_keys, routing_keys = await self.resolve_invitation( + service, + service_accept=service_accept, + ) + service = ServiceMessage.deserialize( + { + "id": "#inline", + "type": "did-communication", + "recipientKeys": [ + DIDKey.from_public_key_b58(key, ED25519).did + for key in recipient_keys + ], + "routingKeys": [ + DIDKey.from_public_key_b58(key, ED25519).did + for key in routing_keys + ], + "serviceEndpoint": endpoint, + } + ) - async def conn_rec_is_active(self, conn_rec_id: str) -> ConnRecord: - """ - Return when ConnRecord state becomes active. + LOGGER.debug(f"Creating connection with public did {public_did}") + + conn_record = None + for protocol in supported_handshake_protocols: + # DIDExchange + if protocol is HSProto.RFC23: + didx_mgr = DIDXManager(self.profile) + conn_record = await didx_mgr.receive_invitation( + invitation=invitation, + their_public_did=public_did, + auto_accept=auto_accept, + alias=alias, + mediation_id=mediation_id, + ) + break + # 0160 Connection + elif protocol is HSProto.RFC160: + service.recipient_keys = [ + DIDKey.from_did(key).public_key_b58 + for key in service.recipient_keys or [] + ] + service.routing_keys = [ + DIDKey.from_did(key).public_key_b58 for key in service.routing_keys + ] or [] + connection_invitation = ConnectionInvitation.deserialize( + { + "@id": invitation._id, + "@type": DIDCommPrefix.qualify_current(protocol.name), + "label": invitation.label, + "recipientKeys": service.recipient_keys, + "serviceEndpoint": service.service_endpoint, + "routingKeys": service.routing_keys, + } + ) + conn_mgr = ConnectionManager(self.profile) + conn_record = await conn_mgr.receive_invitation( + invitation=connection_invitation, + their_public_did=public_did, + auto_accept=auto_accept, + alias=alias, + mediation_id=mediation_id, + ) + break - Args: - conn_rec: ConnRecord + if not conn_record: + raise OutOfBandManagerError( + f"Unable to create connection. Could not perform handshake using any of " + f"the handshake_protocols (supported {supported_handshake_protocols})" + ) - Returns: - ConnRecord + async with self.profile.session() as session: + oob_record.connection_id = conn_record.connection_id + await oob_record.save(session) - """ - while True: - async with self.profile.session() as session: - conn_rec = await ConnRecord.retrieve_by_id(session, conn_rec_id) - if conn_rec.is_ready: - return conn_rec - await asyncio.sleep(0.5) + return oob_record - async def create_handshake_reuse_message( + async def _create_handshake_reuse_message( self, - invi_msg: InvitationMessage, + oob_record: OobRecord, conn_record: ConnRecord, - ) -> None: + version: str, + ) -> OobRecord: """ Create and Send a Handshake Reuse message under RFC 0434. Args: - invi_msg: OOB Invitation Message - service: Service block extracted from the OOB invitation + oob_record: OOB Record + conn_record: Connection record associated with the oob record Returns: @@ -975,26 +875,26 @@ async def create_handshake_reuse_message( """ try: - pthid = invi_msg._id - reuse_msg = HandshakeReuse() - thid = reuse_msg._id - reuse_msg.assign_thread_id(thid=thid, pthid=pthid) + reuse_msg = HandshakeReuse(version=version) + reuse_msg.assign_thread_id(thid=reuse_msg._id, pthid=oob_record.invi_msg_id) + connection_targets = await self.fetch_connection_targets( connection=conn_record ) - responder = self.profile.inject_or(BaseResponder) - if responder: - await responder.send( - message=reuse_msg, - target_list=connection_targets, - ) - async with self.profile.session() as session: - await conn_record.metadata_set( - session=session, key="reuse_msg_id", value=reuse_msg._id - ) - await conn_record.metadata_set( - session=session, key="reuse_msg_state", value="initial" - ) + + responder = self.profile.inject(BaseResponder) + await responder.send( + message=reuse_msg, + target_list=connection_targets, + ) + + async with self.profile.session() as session: + oob_record.reuse_msg_id = reuse_msg._id + oob_record.state = OobRecord.STATE_AWAIT_RESPONSE + await oob_record.save(session, reason="Storing reuse msg data") + + return oob_record + except Exception as err: raise OutOfBandManagerError( f"Error on creating and sending a handshake reuse message: {err}" @@ -1002,11 +902,11 @@ async def create_handshake_reuse_message( async def delete_stale_connection_by_invitation(self, invi_msg_id: str): """Delete unused connections, using existing an active connection instead.""" - tag_filter = {} - post_filter = {} - tag_filter["invitation_msg_id"] = invi_msg_id - post_filter["invitation_mode"] = "once" - post_filter["state"] = "invitation" + tag_filter = { + "invitation_msg_id": invi_msg_id, + } + post_filter = {"invitation_mode": "once", "state": "invitation"} + async with self.profile.session() as session: conn_records = await ConnRecord.query( session, @@ -1040,20 +940,39 @@ async def receive_reuse_message( """ invi_msg_id = reuse_msg._thread.pthid - reuse_msg_id = reuse_msg._thread.thid - responder = self.profile.inject_or(BaseResponder) - reuse_accept_msg = HandshakeReuseAccept() + reuse_msg_id = reuse_msg._thread_id + + reuse_accept_msg = HandshakeReuseAccept( + version=get_version_from_message(reuse_msg) + ) reuse_accept_msg.assign_thread_id(thid=reuse_msg_id, pthid=invi_msg_id) connection_targets = await self.fetch_connection_targets(connection=conn_rec) - if responder: - await responder.send( - message=reuse_accept_msg, - target_list=connection_targets, - ) + + responder = self.profile.inject(BaseResponder) + # Update ConnRecord's invi_msg_id async with self._profile.session() as session: + oob_record = await OobRecord.retrieve_by_tag_filter( + session, + {"invi_msg_id": invi_msg_id}, + {"state": OobRecord.STATE_AWAIT_RESPONSE}, + ) + + oob_record.state = OobRecord.STATE_DONE + oob_record.reuse_msg_id = reuse_msg_id + oob_record.connection_id = conn_rec.connection_id + + # We don't want to store this state. We either remove the record + # (no multi-use) or we can't update the record (multi-use) + await oob_record.emit_event(session) + + # If the oob_record is not multi-use we can now remove it + if not oob_record.multi_use: + await oob_record.delete_record(session) + conn_rec.invitation_msg_id = invi_msg_id await conn_rec.save(session, reason="Assigning new invitation_msg_id") + # Delete the ConnRecord created; re-use existing connection await self.delete_stale_connection_by_invitation(invi_msg_id) # Emit webhook @@ -1063,12 +982,17 @@ async def receive_reuse_message( "thread_id": reuse_msg_id, "connection_id": conn_rec.connection_id, "comment": ( - f"Connection {conn_rec.connection_id} is being reused ", - f"for invitation {invi_msg_id}", + f"Connection {conn_rec.connection_id} is being reused " + f"for invitation {invi_msg_id}" ), }, ) + await responder.send( + message=reuse_accept_msg, + target_list=connection_targets, + ) + async def receive_reuse_accepted_message( self, reuse_accepted_msg: HandshakeReuseAccept, @@ -1078,7 +1002,7 @@ async def receive_reuse_accepted_message( """ Receive and process a HandshakeReuseAccept message under RFC 0434. - Process a `HandshakeReuseAccept` message by updating the ConnRecord metadata + Process a `HandshakeReuseAccept` message by updating the OobRecord state to `accepted`. Args: @@ -1094,15 +1018,21 @@ async def receive_reuse_accepted_message( """ invi_msg_id = reuse_accepted_msg._thread.pthid thread_reuse_msg_id = reuse_accepted_msg._thread.thid + try: async with self.profile.session() as session: - conn_reuse_msg_id = await conn_record.metadata_get( - session=session, key="reuse_msg_id" - ) - assert thread_reuse_msg_id == conn_reuse_msg_id - await conn_record.metadata_set( - session=session, key="reuse_msg_state", value="accepted" + oob_record = await OobRecord.retrieve_by_tag_filter( + session, + {"invi_msg_id": invi_msg_id, "reuse_msg_id": thread_reuse_msg_id}, ) + + oob_record.state = OobRecord.STATE_ACCEPTED + oob_record.connection_id = conn_record.connection_id + + # We can now remove the oob_record + await oob_record.emit_event(session) + await oob_record.delete_record(session) + conn_record.invitation_msg_id = invi_msg_id await conn_record.save( session, reason="Assigning new invitation_msg_id" @@ -1115,8 +1045,8 @@ async def receive_reuse_accepted_message( "connection_id": conn_record.connection_id, "state": "accepted", "comment": ( - f"Connection {conn_record.connection_id} is being reused ", - f"for invitation {invi_msg_id}", + f"Connection {conn_record.connection_id} is being reused " + f"for invitation {invi_msg_id}" ), }, ) @@ -1130,17 +1060,15 @@ async def receive_reuse_accepted_message( "state": "rejected", "comment": ( "Unable to process HandshakeReuseAccept message, " - f"connection {conn_record.connection_id} ", - f"and invitation {invi_msg_id}", + f"connection {conn_record.connection_id} " + f"and invitation {invi_msg_id}" ), }, ) raise OutOfBandManagerError( ( - ( - "Error processing reuse accepted message " - f"for OOB invitation {invi_msg_id}, {e}" - ) + "Error processing reuse accepted message " + f"for OOB invitation {invi_msg_id}, {e}" ) ) @@ -1153,7 +1081,7 @@ async def receive_problem_report( """ Receive and process a ProblemReport message from the inviter to invitee. - Process a `ProblemReport` message by updating the ConnRecord metadata + Process a `ProblemReport` message by updating the OobRecord state to `not_accepted`. Args: @@ -1167,23 +1095,20 @@ async def receive_problem_report( HandshakeReuseAccept message """ + invi_msg_id = problem_report._thread.pthid + thread_reuse_msg_id = problem_report._thread.thid try: - invi_msg_id = problem_report._thread.pthid - thread_reuse_msg_id = problem_report._thread.thid async with self.profile.session() as session: - conn_reuse_msg_id = await conn_record.metadata_get( - session=session, key="reuse_msg_id" - ) - assert thread_reuse_msg_id == conn_reuse_msg_id - await conn_record.metadata_set( - session=session, key="reuse_msg_state", value="not_accepted" + oob_record = await OobRecord.retrieve_by_tag_filter( + session, + {"invi_msg_id": invi_msg_id, "reuse_msg_id": thread_reuse_msg_id}, ) + oob_record.state = OobRecord.STATE_NOT_ACCEPTED + await oob_record.save(session) except Exception as e: raise OutOfBandManagerError( ( - ( - "Error processing problem report message " - f"for OOB invitation {invi_msg_id}, {e}" - ) + "Error processing problem report message " + f"for OOB invitation {invi_msg_id}, {e}" ) ) diff --git a/aries_cloudagent/protocols/out_of_band/v1_0/message_types.py b/aries_cloudagent/protocols/out_of_band/v1_0/message_types.py index d8fb709e09..e8fcd09a94 100644 --- a/aries_cloudagent/protocols/out_of_band/v1_0/message_types.py +++ b/aries_cloudagent/protocols/out_of_band/v1_0/message_types.py @@ -1,5 +1,7 @@ """Message and inner object type identifiers for Out of Band messages.""" +from ....core.util import get_proto_default_version + from ...didcomm_prefix import DIDCommPrefix SPEC_URI = ( @@ -7,11 +9,17 @@ "2da7fc4ee043effa3a9960150e7ba8c9a4628b68/features/0434-outofband" ) +# Default Version +DEFAULT_VERSION = get_proto_default_version( + "aries_cloudagent.protocols.out_of_band.definition", 1 +) + # Message types -INVITATION = "out-of-band/1.0/invitation" -MESSAGE_REUSE = "out-of-band/1.0/handshake-reuse" -MESSAGE_REUSE_ACCEPT = "out-of-band/1.0/handshake-reuse-accepted" -PROBLEM_REPORT = "out-of-band/1.0/problem_report" +INVITATION = f"out-of-band/{DEFAULT_VERSION}/invitation" +MESSAGE_REUSE = f"out-of-band/{DEFAULT_VERSION}/handshake-reuse" +MESSAGE_REUSE_ACCEPT = f"out-of-band/{DEFAULT_VERSION}/handshake-reuse-accepted" +PROBLEM_REPORT = f"out-of-band/{DEFAULT_VERSION}/problem_report" + PROTOCOL_PACKAGE = "aries_cloudagent.protocols.out_of_band.v1_0" diff --git a/aries_cloudagent/protocols/out_of_band/v1_0/messages/invitation.py b/aries_cloudagent/protocols/out_of_band/v1_0/messages/invitation.py index 04a2cfa3fa..be3c0b80a8 100644 --- a/aries_cloudagent/protocols/out_of_band/v1_0/messages/invitation.py +++ b/aries_cloudagent/protocols/out_of_band/v1_0/messages/invitation.py @@ -3,7 +3,7 @@ from collections import namedtuple from enum import Enum from re import sub -from typing import Sequence, Text, Union +from typing import Optional, Sequence, Text, Union from urllib.parse import parse_qs, urljoin, urlparse from marshmallow import ( @@ -26,7 +26,7 @@ from ....didexchange.v1_0.message_types import ARIES_PROTOCOL as DIDX_PROTO from ....connections.v1_0.message_types import ARIES_PROTOCOL as CONN_PROTO -from ..message_types import INVITATION +from ..message_types import INVITATION, DEFAULT_VERSION from .service import Service @@ -96,13 +96,17 @@ def _serialize(self, value, attr, obj, **kwargs): def _deserialize(self, value, attr, data, **kwargs): if isinstance(value, dict): return Service.deserialize(value) + elif isinstance(value, Service): + return value elif isinstance(value, str): - if bool(DIDValidation.PATTERN.match(value)): - return value - else: + if not DIDValidation.PATTERN.match(value): raise ValidationError( "Service item must be a valid decentralized identifier (DID)" ) + return value + raise ValidationError( + "Service item must be a valid decentralized identifier (DID) or object" + ) class InvitationMessage(AgentMessage): @@ -120,9 +124,13 @@ def __init__( *, comment: str = None, label: str = None, + image_url: str = None, handshake_protocols: Sequence[Text] = None, requests_attach: Sequence[AttachDecorator] = None, services: Sequence[Union[Service, Text]] = None, + accept: Optional[Sequence[Text]] = None, + version: str = DEFAULT_VERSION, + msg_type: Optional[Text] = None, **kwargs, ): """ @@ -133,13 +141,15 @@ def __init__( """ # super().__init__(_id=_id, **kwargs) - super().__init__(**kwargs) + super().__init__(_type=msg_type, _version=version, **kwargs) self.label = label + self.image_url = image_url self.handshake_protocols = ( list(handshake_protocols) if handshake_protocols else [] ) self.requests_attach = list(requests_attach) if requests_attach else [] self.services = services + self.accept = accept @classmethod def wrap_message(cls, message: dict) -> AttachDecorator: @@ -197,17 +207,33 @@ class Meta: model_class = InvitationMessage unknown = EXCLUDE + _type = fields.Str( + data_key="@type", + required=False, + description="Message type", + example="https://didcomm.org/my-family/1.0/my-message-type", + ) label = fields.Str(required=False, description="Optional label", example="Bob") + image_url = fields.URL( + data_key="imageUrl", + required=False, + allow_none=True, + description="Optional image URL for out-of-band invitation", + example="http://192.168.56.101/img/logo.jpg", + ) handshake_protocols = fields.List( fields.Str( description="Handshake protocol", example=DIDCommPrefix.qualify_current(HSProto.RFC23.name), - validate=lambda hsp: ( - DIDCommPrefix.unqualify(hsp) in [p.name for p in HSProto] - ), ), required=False, ) + accept = fields.List( + fields.Str(), + example=["didcomm/aip1", "didcomm/aip2;env=rfc19"], + description=("List of mime type in order of preference"), + required=False, + ) requests_attach = fields.Nested( AttachDecoratorSchema, required=False, @@ -251,13 +277,10 @@ def validate_fields(self, data, **kwargs): """ handshake_protocols = data.get("handshake_protocols") requests_attach = data.get("requests_attach") - if not ( - (handshake_protocols and len(handshake_protocols) > 0) - or (requests_attach and len(requests_attach) > 0) - ): + if not handshake_protocols and not requests_attach: raise ValidationError( "Model must include non-empty " - "handshake_protocols or requests_attach or both" + "handshake_protocols or requests~attach or both" ) # services = data.get("services") diff --git a/aries_cloudagent/protocols/out_of_band/v1_0/messages/problem_report.py b/aries_cloudagent/protocols/out_of_band/v1_0/messages/problem_report.py index f6ddb3bf86..fc2e01039e 100644 --- a/aries_cloudagent/protocols/out_of_band/v1_0/messages/problem_report.py +++ b/aries_cloudagent/protocols/out_of_band/v1_0/messages/problem_report.py @@ -3,9 +3,11 @@ import logging from enum import Enum +from typing import Optional, Text from marshmallow import ( EXCLUDE, + fields, pre_dump, validates_schema, ValidationError, @@ -13,7 +15,7 @@ from ....problem_report.v1_0.message import ProblemReport, ProblemReportSchema -from ..message_types import PROBLEM_REPORT, PROTOCOL_PACKAGE +from ..message_types import PROBLEM_REPORT, PROTOCOL_PACKAGE, DEFAULT_VERSION HANDLER_CLASS = ( f"{PROTOCOL_PACKAGE}.handlers" @@ -40,9 +42,15 @@ class Meta: message_type = PROBLEM_REPORT schema_class = "OOBProblemReportSchema" - def __init__(self, *args, **kwargs): + def __init__( + self, + version: str = DEFAULT_VERSION, + msg_type: Optional[Text] = None, + *args, + **kwargs, + ): """Initialize a ProblemReport message instance.""" - super().__init__(*args, **kwargs) + super().__init__(_type=msg_type, _version=version, *args, **kwargs) class OOBProblemReportSchema(ProblemReportSchema): @@ -54,6 +62,13 @@ class Meta: model_class = OOBProblemReport unknown = EXCLUDE + _type = fields.Str( + data_key="@type", + required=False, + description="Message type", + example="https://didcomm.org/my-family/1.0/my-message-type", + ) + @pre_dump def check_thread_deco(self, obj, **kwargs): """Thread decorator, and its thid and pthid, are mandatory.""" diff --git a/aries_cloudagent/protocols/out_of_band/v1_0/messages/reuse.py b/aries_cloudagent/protocols/out_of_band/v1_0/messages/reuse.py index df40511e80..d53f5aeddb 100644 --- a/aries_cloudagent/protocols/out_of_band/v1_0/messages/reuse.py +++ b/aries_cloudagent/protocols/out_of_band/v1_0/messages/reuse.py @@ -1,10 +1,11 @@ """Represents a Handshake Reuse message under RFC 0434.""" -from marshmallow import EXCLUDE, pre_dump, ValidationError +from marshmallow import EXCLUDE, fields, pre_dump, ValidationError +from typing import Optional, Text from .....messaging.agent_message import AgentMessage, AgentMessageSchema -from ..message_types import MESSAGE_REUSE, PROTOCOL_PACKAGE +from ..message_types import MESSAGE_REUSE, PROTOCOL_PACKAGE, DEFAULT_VERSION HANDLER_CLASS = ( f"{PROTOCOL_PACKAGE}.handlers.reuse_handler.HandshakeReuseMessageHandler" @@ -23,10 +24,12 @@ class Meta: def __init__( self, + version: str = DEFAULT_VERSION, + msg_type: Optional[Text] = None, **kwargs, ): """Initialize Handshake Reuse message object.""" - super().__init__(**kwargs) + super().__init__(_type=msg_type, _version=version, **kwargs) class HandshakeReuseSchema(AgentMessageSchema): @@ -38,6 +41,13 @@ class Meta: model_class = HandshakeReuse unknown = EXCLUDE + _type = fields.Str( + data_key="@type", + required=False, + description="Message type", + example="https://didcomm.org/my-family/1.0/my-message-type", + ) + @pre_dump def check_thread_deco(self, obj, **kwargs): """Thread decorator, and its thid and pthid, are mandatory.""" diff --git a/aries_cloudagent/protocols/out_of_band/v1_0/messages/reuse_accept.py b/aries_cloudagent/protocols/out_of_band/v1_0/messages/reuse_accept.py index d519ab0a2b..0b0e21a58e 100644 --- a/aries_cloudagent/protocols/out_of_band/v1_0/messages/reuse_accept.py +++ b/aries_cloudagent/protocols/out_of_band/v1_0/messages/reuse_accept.py @@ -1,10 +1,11 @@ """Represents a Handshake Reuse Accept message under RFC 0434.""" -from marshmallow import EXCLUDE, pre_dump, ValidationError +from marshmallow import EXCLUDE, fields, pre_dump, ValidationError +from typing import Optional, Text from .....messaging.agent_message import AgentMessage, AgentMessageSchema -from ..message_types import MESSAGE_REUSE_ACCEPT, PROTOCOL_PACKAGE +from ..message_types import MESSAGE_REUSE_ACCEPT, PROTOCOL_PACKAGE, DEFAULT_VERSION HANDLER_CLASS = ( f"{PROTOCOL_PACKAGE}.handlers" @@ -24,10 +25,12 @@ class Meta: def __init__( self, + version: str = DEFAULT_VERSION, + msg_type: Optional[Text] = None, **kwargs, ): """Initialize Handshake Reuse Accept object.""" - super().__init__(**kwargs) + super().__init__(_type=msg_type, _version=version, **kwargs) class HandshakeReuseAcceptSchema(AgentMessageSchema): @@ -39,6 +42,13 @@ class Meta: model_class = HandshakeReuseAccept unknown = EXCLUDE + _type = fields.Str( + data_key="@type", + required=False, + description="Message type", + example="https://didcomm.org/my-family/1.0/my-message-type", + ) + @pre_dump def check_thread_deco(self, obj, **kwargs): """Thread decorator, and its thid and pthid, are mandatory.""" diff --git a/aries_cloudagent/protocols/out_of_band/v1_0/messages/tests/test_invitation.py b/aries_cloudagent/protocols/out_of_band/v1_0/messages/tests/test_invitation.py index 5340dd66dc..004c7d5ca5 100644 --- a/aries_cloudagent/protocols/out_of_band/v1_0/messages/tests/test_invitation.py +++ b/aries_cloudagent/protocols/out_of_band/v1_0/messages/tests/test_invitation.py @@ -4,11 +4,12 @@ from ......messaging.models.base import BaseModelError from ......did.did_key import DIDKey -from ......wallet.key_type import KeyType +from ......wallet.key_type import ED25519 from .....connections.v1_0.message_types import ARIES_PROTOCOL as CONN_PROTO from .....didcomm_prefix import DIDCommPrefix from .....didexchange.v1_0.message_types import ARIES_PROTOCOL as DIDX_PROTO +from .....didexchange.v1_0.messages.request import DIDXRequest from ...message_types import INVITATION @@ -44,14 +45,14 @@ def test_properties(self): class TestInvitationMessage(TestCase): def test_init(self): """Test initialization message.""" - invi = InvitationMessage( + invi_msg = InvitationMessage( comment="Hello", label="A label", handshake_protocols=[DIDCommPrefix.qualify_current(DIDX_PROTO)], services=[TEST_DID], ) - assert invi.services == [TEST_DID] - assert invi._type == DIDCommPrefix.qualify_current(INVITATION) + assert invi_msg.services == [TEST_DID] + assert "out-of-band/1.1/invitation" in invi_msg._type service = Service(_id="#inline", _type=DID_COMM, did=TEST_DID) invi_msg = InvitationMessage( @@ -59,9 +60,10 @@ def test_init(self): label="A label", handshake_protocols=[DIDCommPrefix.qualify_current(DIDX_PROTO)], services=[service], + version="1.0", ) assert invi_msg.services == [service] - assert invi_msg._type == DIDCommPrefix.qualify_current(INVITATION) + assert "out-of-band/1.0/invitation" in invi_msg._type def test_wrap_serde(self): """Test conversion of aries message to attachment decorator.""" @@ -80,9 +82,7 @@ def test_wrap_serde(self): service = Service( _id="#inline", _type=DID_COMM, - recipient_keys=[ - DIDKey.from_public_key_b58(TEST_VERKEY, KeyType.ED25519).did - ], + recipient_keys=[DIDKey.from_public_key_b58(TEST_VERKEY, ED25519).did], service_endpoint="http://1.2.3.4:8080/service", ) data_deser = { @@ -110,9 +110,7 @@ def test_url_round_trip(self): service = Service( _id="#inline", _type=DID_COMM, - recipient_keys=[ - DIDKey.from_public_key_b58(TEST_VERKEY, KeyType.ED25519).did - ], + recipient_keys=[DIDKey.from_public_key_b58(TEST_VERKEY, ED25519).did], service_endpoint="http://1.2.3.4:8080/service", ) invi_msg = InvitationMessage( @@ -141,6 +139,17 @@ def test_invalid_invi_wrong_type_services(self): "services": [123], } - invi_schema = InvitationMessageSchema() - with pytest.raises(test_module.ValidationError): - invi_schema.validate_fields(obj_x) + errs = InvitationMessageSchema().validate(obj_x) + assert errs and "services" in errs + + def test_assign_msg_type_version_to_model_inst(self): + test_msg = InvitationMessage() + assert "1.1" in test_msg._type + assert "1.1" in InvitationMessage.Meta.message_type + test_msg = InvitationMessage(version="1.2") + assert "1.2" in test_msg._type + assert "1.1" in InvitationMessage.Meta.message_type + test_req = DIDXRequest() + assert "1.0" in test_req._type + assert "1.2" in test_msg._type + assert "1.1" in InvitationMessage.Meta.message_type diff --git a/aries_cloudagent/protocols/out_of_band/v1_0/messages/tests/test_problem_report.py b/aries_cloudagent/protocols/out_of_band/v1_0/messages/tests/test_problem_report.py index 0b605b179f..c594ad146b 100644 --- a/aries_cloudagent/protocols/out_of_band/v1_0/messages/tests/test_problem_report.py +++ b/aries_cloudagent/protocols/out_of_band/v1_0/messages/tests/test_problem_report.py @@ -73,3 +73,11 @@ def test_validate_and_logger(self): self._caplog.set_level(logging.WARNING) OOBProblemReportSchema().validate_fields(data) assert "Unexpected error code received" in self._caplog.text + + def test_assign_msg_type_version_to_model_inst(self): + test_msg = OOBProblemReport() + assert "1.1" in test_msg._type + assert "1.1" in OOBProblemReport.Meta.message_type + test_msg = OOBProblemReport(version="1.2") + assert "1.2" in test_msg._type + assert "1.1" in OOBProblemReport.Meta.message_type diff --git a/aries_cloudagent/protocols/out_of_band/v1_0/messages/tests/test_reuse.py b/aries_cloudagent/protocols/out_of_band/v1_0/messages/tests/test_reuse.py index 5cd79750d0..837698e718 100644 --- a/aries_cloudagent/protocols/out_of_band/v1_0/messages/tests/test_reuse.py +++ b/aries_cloudagent/protocols/out_of_band/v1_0/messages/tests/test_reuse.py @@ -35,3 +35,11 @@ def test_pre_dump_x(self): """Exercise pre-dump serialization requirements.""" with pytest.raises(BaseModelError): data = self.reuse_msg.serialize() + + def test_assign_msg_type_version_to_model_inst(self): + test_msg = HandshakeReuse() + assert "1.1" in test_msg._type + assert "1.1" in HandshakeReuse.Meta.message_type + test_msg = HandshakeReuse(version="1.2") + assert "1.2" in test_msg._type + assert "1.1" in HandshakeReuse.Meta.message_type diff --git a/aries_cloudagent/protocols/out_of_band/v1_0/messages/tests/test_reuse_accept.py b/aries_cloudagent/protocols/out_of_band/v1_0/messages/tests/test_reuse_accept.py index 3feca5439a..556493c618 100644 --- a/aries_cloudagent/protocols/out_of_band/v1_0/messages/tests/test_reuse_accept.py +++ b/aries_cloudagent/protocols/out_of_band/v1_0/messages/tests/test_reuse_accept.py @@ -6,6 +6,8 @@ from ......messaging.models.base import BaseModelError +from .....didcomm_prefix import DIDCommPrefix + from ..reuse_accept import HandshakeReuseAccept, HandshakeReuseAcceptSchema @@ -31,7 +33,25 @@ def test_make_model(self): model_instance = HandshakeReuseAccept.deserialize(data) assert isinstance(model_instance, HandshakeReuseAccept) + def test_make_model_backward_comp(self): + """Make reuse-accept model.""" + self.reuse_accept_msg.assign_thread_id(thid="test_thid", pthid="test_pthid") + data = self.reuse_accept_msg.serialize() + data["@type"] = DIDCommPrefix.qualify_current( + "out-of-band/1.0/handshake-reuse-accepted" + ) + model_instance = HandshakeReuseAccept.deserialize(data) + assert isinstance(model_instance, HandshakeReuseAccept) + def test_pre_dump_x(self): """Exercise pre-dump serialization requirements.""" with pytest.raises(BaseModelError): data = self.reuse_accept_msg.serialize() + + def test_assign_msg_type_version_to_model_inst(self): + test_msg = HandshakeReuseAccept() + assert "1.1" in test_msg._type + assert "1.1" in HandshakeReuseAccept.Meta.message_type + test_msg = HandshakeReuseAccept(version="1.2") + assert "1.2" in test_msg._type + assert "1.1" in HandshakeReuseAccept.Meta.message_type diff --git a/aries_cloudagent/protocols/out_of_band/v1_0/models/invitation.py b/aries_cloudagent/protocols/out_of_band/v1_0/models/invitation.py index 513fceb9c1..062466f05c 100644 --- a/aries_cloudagent/protocols/out_of_band/v1_0/models/invitation.py +++ b/aries_cloudagent/protocols/out_of_band/v1_0/models/invitation.py @@ -35,6 +35,7 @@ def __init__( invi_msg_id: str = None, invitation: Union[InvitationMessage, Mapping] = None, # invitation message invitation_url: str = None, + oob_id: str = None, public_did: str = None, # backward-compat: BaseRecord.from_storage() trace: bool = False, **kwargs, @@ -46,6 +47,7 @@ def __init__( self.invi_msg_id = invi_msg_id self._invitation = InvitationMessage.serde(invitation) self.invitation_url = invitation_url + self.oob_id = oob_id self.trace = trace @property @@ -69,11 +71,7 @@ def record_value(self) -> dict: return { **{ prop: getattr(self, prop) - for prop in ( - "invitation_url", - "state", - "trace", - ) + for prop in ("invitation_url", "state", "trace", "oob_id") }, **{ prop: getattr(self, f"_{prop}").ser @@ -110,6 +108,11 @@ class Meta: description="Invitation message identifier", example=UUIDFour.EXAMPLE, ) + oob_id = fields.Str( + required=False, + description="Out of band record identifier", + example=UUIDFour.EXAMPLE, + ) invitation = fields.Nested( InvitationMessageSchema(), required=False, diff --git a/aries_cloudagent/protocols/out_of_band/v1_0/models/oob_record.py b/aries_cloudagent/protocols/out_of_band/v1_0/models/oob_record.py new file mode 100644 index 0000000000..e550d9eb54 --- /dev/null +++ b/aries_cloudagent/protocols/out_of_band/v1_0/models/oob_record.py @@ -0,0 +1,296 @@ +"""Record for out of band invitations.""" + +import json +from typing import Any, Mapping, Optional, Union + +from marshmallow import fields, validate + +from .....connections.models.conn_record import ConnRecord +from .....core.profile import ProfileSession +from .....messaging.decorators.service_decorator import ( + ServiceDecorator, + ServiceDecoratorSchema, +) + +from .....messaging.models.base_record import BaseExchangeRecord, BaseExchangeSchema +from .....messaging.valid import UUIDFour + +from ..messages.invitation import InvitationMessage, InvitationMessageSchema + +from .....storage.base import BaseStorage +from .....storage.record import StorageRecord +from .....storage.error import StorageNotFoundError + + +class OobRecord(BaseExchangeRecord): + """Represents an out of band record.""" + + class Meta: + """OobRecord metadata.""" + + schema_class = "OobRecordSchema" + + RECORD_TYPE = "oob_record" + RECORD_TYPE_METADATA = ConnRecord.RECORD_TYPE_METADATA + RECORD_ID_NAME = "oob_id" + RECORD_TOPIC = "out_of_band" + TAG_NAMES = { + "invi_msg_id", + "attach_thread_id", + "our_recipient_key", + "connection_id", + "reuse_msg_id", + } + + STATE_INITIAL = "initial" + STATE_PREPARE_RESPONSE = "prepare-response" + STATE_AWAIT_RESPONSE = "await-response" + STATE_NOT_ACCEPTED = "reuse-not-accepted" + STATE_ACCEPTED = "reuse-accepted" + STATE_DONE = "done" + + ROLE_SENDER = "sender" + ROLE_RECEIVER = "receiver" + + def __init__( + self, + *, + state: str, + invi_msg_id: str, + role: str, + invitation: Union[InvitationMessage, Mapping[str, Any]], + their_service: Optional[ServiceDecorator] = None, + connection_id: Optional[str] = None, + reuse_msg_id: Optional[str] = None, + oob_id: Optional[str] = None, + attach_thread_id: Optional[str] = None, + our_recipient_key: Optional[str] = None, + our_service: Optional[ServiceDecorator] = None, + multi_use: bool = False, + trace: bool = False, + **kwargs, + ): + """Initialize a new OobRecord.""" + super().__init__(oob_id, state, trace=trace, **kwargs) + self._id = oob_id + self.state = state + self.invi_msg_id = invi_msg_id + self.role = role + self._invitation = InvitationMessage.serde(invitation) + self.connection_id = connection_id + self.reuse_msg_id = reuse_msg_id + self.their_service = their_service + self.our_service = our_service + self.attach_thread_id = attach_thread_id + self.our_recipient_key = our_recipient_key + self.multi_use = multi_use + self.trace = trace + + @property + def oob_id(self) -> str: + """Accessor for the ID associated with this exchange.""" + return self._id + + @property + def invitation(self) -> InvitationMessage: + """Accessor; get deserialized view.""" + return None if self._invitation is None else self._invitation.de + + @invitation.setter + def invitation(self, value): + """Setter; store de/serialized views.""" + self._invitation = InvitationMessage.serde(value) + + @property + def record_value(self) -> dict: + """Accessor for the JSON record value generated for this invitation.""" + return { + **{ + prop: getattr(self, prop) + for prop in ( + "state", + "their_service", + "connection_id", + "role", + "our_service", + ) + }, + **{ + prop: getattr(self, f"_{prop}").ser + for prop in ("invitation",) + if getattr(self, prop) is not None + }, + } + + async def delete_record(self, session: ProfileSession): + """Perform connection record deletion actions. + + Args: + session (ProfileSession): session + + """ + await super().delete_record(session) + + # Delete metadata + if self.connection_id: + storage = session.inject(BaseStorage) + await storage.delete_all_records( + self.RECORD_TYPE_METADATA, + {"connection_id": self.connection_id}, + ) + + async def metadata_get( + self, session: ProfileSession, key: str, default: Any = None + ) -> Any: + """Retrieve arbitrary metadata associated with this connection. + + Args: + session (ProfileSession): session used for storage + key (str): key identifying metadata + default (Any): default value to get; type should be a JSON + compatible value. + + Returns: + Any: metadata stored by key + + """ + assert self.connection_id + storage: BaseStorage = session.inject(BaseStorage) + try: + record = await storage.find_record( + self.RECORD_TYPE_METADATA, + {"key": key, "connection_id": self.connection_id}, + ) + return json.loads(record.value) + except StorageNotFoundError: + return default + + async def metadata_set(self, session: ProfileSession, key: str, value: Any): + """Set arbitrary metadata associated with this connection. + + Args: + session (ProfileSession): session used for storage + key (str): key identifying metadata + value (Any): value to set + """ + assert self.connection_id + value = json.dumps(value) + storage: BaseStorage = session.inject(BaseStorage) + try: + record = await storage.find_record( + self.RECORD_TYPE_METADATA, + {"key": key, "connection_id": self.connection_id}, + ) + await storage.update_record(record, value, record.tags) + except StorageNotFoundError: + record = StorageRecord( + self.RECORD_TYPE_METADATA, + value, + {"key": key, "connection_id": self.connection_id}, + ) + await storage.add_record(record) + + async def metadata_delete(self, session: ProfileSession, key: str): + """Delete custom metadata associated with this connection. + + Args: + session (ProfileSession): session used for storage + key (str): key of metadata to delete + """ + assert self.connection_id + storage: BaseStorage = session.inject(BaseStorage) + try: + record = await storage.find_record( + self.RECORD_TYPE_METADATA, + {"key": key, "connection_id": self.connection_id}, + ) + await storage.delete_record(record) + except StorageNotFoundError as err: + raise KeyError(f"{key} not found in connection metadata") from err + + async def metadata_get_all(self, session: ProfileSession) -> dict: + """Return all custom metadata associated with this connection. + + Args: + session (ProfileSession): session used for storage + + Returns: + dict: dictionary representation of all metadata values + + """ + assert self.connection_id + storage: BaseStorage = session.inject(BaseStorage) + records = await storage.find_all_records( + self.RECORD_TYPE_METADATA, + {"connection_id": self.connection_id}, + ) + return {record.tags["key"]: json.loads(record.value) for record in records} + + def __eq__(self, other: Any) -> bool: + """Comparison between records.""" + return super().__eq__(other) + + +class OobRecordSchema(BaseExchangeSchema): + """Schema to allow serialization/deserialization of invitation records.""" + + class Meta: + """OobRecordSchema metadata.""" + + model_class = OobRecord + + oob_id = fields.Str( + required=True, + description="Oob record identifier", + example=UUIDFour.EXAMPLE, + ) + state = fields.Str( + required=True, + description="Out of band message exchange state", + example=OobRecord.STATE_AWAIT_RESPONSE, + validate=validate.OneOf( + OobRecord.get_attributes_by_prefix("STATE_", walk_mro=True) + ), + ) + invi_msg_id = fields.Str( + required=True, + description="Invitation message identifier", + example=UUIDFour.EXAMPLE, + ) + invitation = fields.Nested( + InvitationMessageSchema(), + required=True, + description="Out of band invitation message", + ) + + their_service = fields.Nested( + ServiceDecoratorSchema(), + required=False, + ) + + connection_id = fields.Str( + description="Connection record identifier", + required=False, + example=UUIDFour.EXAMPLE, + ) + + attach_thread_id = fields.Str( + description="Connection record identifier", + required=False, + example=UUIDFour.EXAMPLE, + ) + + our_recipient_key = fields.Str( + description="Recipient key used for oob invitation", + required=False, + example=UUIDFour.EXAMPLE, + ) + + role = fields.Str( + description="OOB Role", + required=False, + example=OobRecord.ROLE_RECEIVER, + validate=validate.OneOf( + OobRecord.get_attributes_by_prefix("ROLE_", walk_mro=False) + ), + ) diff --git a/aries_cloudagent/protocols/out_of_band/v1_0/models/tests/test_invitation.py b/aries_cloudagent/protocols/out_of_band/v1_0/models/tests/test_invitation.py index 3c1bc0fac5..12dae75f89 100644 --- a/aries_cloudagent/protocols/out_of_band/v1_0/models/tests/test_invitation.py +++ b/aries_cloudagent/protocols/out_of_band/v1_0/models/tests/test_invitation.py @@ -23,6 +23,7 @@ def test_invitation_record(self): "invitation_url": None, "state": None, "trace": False, + "oob_id": None, } another = InvitationRecord(invi_msg_id="99999") diff --git a/aries_cloudagent/protocols/out_of_band/v1_0/routes.py b/aries_cloudagent/protocols/out_of_band/v1_0/routes.py index a4cca26875..3b9b489500 100644 --- a/aries_cloudagent/protocols/out_of_band/v1_0/routes.py +++ b/aries_cloudagent/protocols/out_of_band/v1_0/routes.py @@ -9,7 +9,6 @@ from marshmallow.exceptions import ValidationError from ....admin.request_context import AdminRequestContext -from ....connections.models.conn_record import ConnRecordSchema from ....messaging.models.base import BaseModelError from ....messaging.models.openapi import OpenAPISchema from ....messaging.valid import UUID4 @@ -22,6 +21,7 @@ from .messages.invitation import HSProto, InvitationMessage, InvitationMessageSchema from .message_types import SPEC_URI from .models.invitation import InvitationRecordSchema +from .models.oob_record import OobRecordSchema LOGGER = logging.getLogger(__name__) @@ -75,6 +75,15 @@ class AttachmentDefSchema(OpenAPISchema): ), required=False, ) + accept = fields.List( + fields.Str(), + description=( + "List of mime type in order of preference that should be" + " use in responding to the message" + ), + example=["didcomm/aip1", "didcomm/aip2;env=rfc19"], + required=False, + ) use_public_did = fields.Boolean( default=False, description="Whether to use public DID in invitation", @@ -92,6 +101,11 @@ class AttachmentDefSchema(OpenAPISchema): required=False, example="Invitation to Barry", ) + protocol_version = fields.Str( + description="OOB protocol version", + required=False, + example="1.1", + ) alias = fields.Str( description="Alias for connection", required=False, @@ -151,11 +165,13 @@ async def invitation_create(request: web.BaseRequest): body = await request.json() if request.body_exists else {} attachments = body.get("attachments") handshake_protocols = body.get("handshake_protocols", []) + service_accept = body.get("accept") use_public_did = body.get("use_public_did", False) metadata = body.get("metadata") my_label = body.get("my_label") alias = body.get("alias") mediation_id = body.get("mediation_id") + protocol_version = body.get("protocol_version") multi_use = json.loads(request.query.get("multi_use", "false")) auto_accept = json.loads(request.query.get("auto_accept", "null")) @@ -175,9 +191,11 @@ async def invitation_create(request: web.BaseRequest): metadata=metadata, alias=alias, mediation_id=mediation_id, + service_accept=service_accept, + protocol_version=protocol_version, ) except (StorageNotFoundError, ValidationError, OutOfBandManagerError) as e: - raise web.HTTPBadRequest(reason=str(e)) + raise web.HTTPBadRequest(reason=e.roll_up) return web.json_response(invi_rec.serialize()) @@ -188,7 +206,7 @@ async def invitation_create(request: web.BaseRequest): ) @querystring_schema(InvitationReceiveQueryStringSchema()) @request_schema(InvitationMessageSchema()) -@response_schema(ConnRecordSchema(), 200, description="") +@response_schema(OobRecordSchema(), 200, description="") async def invitation_receive(request: web.BaseRequest): """ Request handler for receiving a new connection invitation. diff --git a/aries_cloudagent/protocols/out_of_band/v1_0/tests/test_manager.py b/aries_cloudagent/protocols/out_of_band/v1_0/tests/test_manager.py index 297d10c59b..4cbb29ab65 100644 --- a/aries_cloudagent/protocols/out_of_band/v1_0/tests/test_manager.py +++ b/aries_cloudagent/protocols/out_of_band/v1_0/tests/test_manager.py @@ -1,115 +1,108 @@ """Test OOB Manager.""" -import asyncio import json - -from asynctest import mock as async_mock, TestCase as AsyncTestCase from copy import deepcopy -from datetime import datetime, timezone, timedelta -from uuid import UUID +from datetime import datetime, timedelta, timezone +from typing import List +from unittest.mock import ANY + +from asynctest import TestCase as AsyncTestCase, mock as async_mock from .....connections.models.conn_record import ConnRecord from .....connections.models.connection_target import ConnectionTarget from .....connections.models.diddoc import DIDDoc, PublicKey, PublicKeyType, Service +from .....core.event_bus import EventBus from .....core.in_memory import InMemoryProfile -from .....core.profile import ProfileSession +from .....core.util import get_version_from_message +from .....core.oob_processor import OobMessageProcessor from .....did.did_key import DIDKey -from .....indy.holder import IndyHolder -from .....indy.models.pres_preview import ( - IndyPresAttrSpec, - IndyPresPredSpec, - IndyPresPreview, -) from .....messaging.decorators.attach_decorator import AttachDecorator +from .....messaging.decorators.service_decorator import ServiceDecorator from .....messaging.responder import BaseResponder, MockResponder -from .....messaging.util import str_to_epoch, datetime_now, datetime_to_str +from .....messaging.util import datetime_now, datetime_to_str, str_to_epoch from .....multitenant.base import BaseMultitenantManager from .....multitenant.manager import MultitenantManager +from .....protocols.coordinate_mediation.v1_0.manager import MediationManager +from .....protocols.coordinate_mediation.v1_0.route_manager import RouteManager from .....protocols.coordinate_mediation.v1_0.models.mediation_record import ( MediationRecord, ) -from .....protocols.coordinate_mediation.v1_0.manager import MediationManager from .....protocols.didexchange.v1_0.manager import DIDXManager -from .....protocols.issue_credential.v1_0.manager import ( - CredentialManager as V10CredManager, -) from .....protocols.issue_credential.v1_0.messages.credential_offer import ( CredentialOffer as V10CredOffer, ) from .....protocols.issue_credential.v1_0.messages.inner.credential_preview import ( - CredentialPreview as V10CredentialPreview, CredAttrSpec as V10CredAttrSpec, ) -from .....protocols.issue_credential.v1_0.tests import ( - INDY_OFFER, - INDY_CRED_REQ, +from .....protocols.issue_credential.v1_0.messages.inner.credential_preview import ( + CredentialPreview as V10CredentialPreview, +) +from .....protocols.issue_credential.v1_0.tests import INDY_OFFER +from .....protocols.issue_credential.v2_0.message_types import ( + ATTACHMENT_FORMAT as V20_CRED_ATTACH_FORMAT, ) -from .....protocols.issue_credential.v2_0.manager import V20CredManager +from .....protocols.issue_credential.v2_0.message_types import CRED_20_OFFER from .....protocols.issue_credential.v2_0.messages.cred_format import V20CredFormat +from .....protocols.issue_credential.v2_0.messages.cred_offer import V20CredOffer from .....protocols.issue_credential.v2_0.messages.inner.cred_preview import ( - V20CredPreview, V20CredAttrSpec, + V20CredPreview, ) -from .....protocols.issue_credential.v2_0.messages.cred_offer import V20CredOffer -from .....protocols.issue_credential.v2_0.models.cred_ex_record import V20CredExRecord -from .....protocols.issue_credential.v2_0.message_types import ( - ATTACHMENT_FORMAT as V20_CRED_ATTACH_FORMAT, - CRED_20_OFFER, -) -from .....protocols.present_proof.v1_0.manager import PresentationManager + from .....protocols.present_proof.v1_0.message_types import ( - PRESENTATION_REQUEST, ATTACH_DECO_IDS as V10_PRES_ATTACH_FORMAT, ) -from .....protocols.present_proof.v1_0.messages.presentation import Presentation +from .....protocols.present_proof.v1_0.message_types import PRESENTATION_REQUEST from .....protocols.present_proof.v1_0.messages.presentation_request import ( PresentationRequest, ) -from .....protocols.present_proof.v1_0.models.presentation_exchange import ( - V10PresentationExchange, -) -from .....protocols.present_proof.v2_0.manager import V20PresManager + from .....protocols.present_proof.v2_0.message_types import ( ATTACHMENT_FORMAT as V20_PRES_ATTACH_FORMAT, - PRES_20, - PRES_20_REQUEST, ) -from .....protocols.present_proof.v2_0.messages.pres import V20Pres +from .....protocols.present_proof.v2_0.message_types import PRES_20_REQUEST from .....protocols.present_proof.v2_0.messages.pres_format import V20PresFormat from .....protocols.present_proof.v2_0.messages.pres_request import V20PresRequest from .....storage.error import StorageNotFoundError -from .....storage.vc_holder.base import VCHolder -from .....storage.vc_holder.vc_record import VCRecord from .....transport.inbound.receipt import MessageReceipt from .....wallet.did_info import DIDInfo, KeyInfo -from .....wallet.did_method import DIDMethod +from .....wallet.did_method import SOV from .....wallet.in_memory import InMemoryWallet -from .....wallet.key_type import KeyType - +from .....wallet.key_type import ED25519 +from ....connections.v1_0.messages.connection_invitation import ConnectionInvitation from ....didcomm_prefix import DIDCommPrefix +from ....issue_credential.v1_0.message_types import CREDENTIAL_OFFER from ....issue_credential.v1_0.models.credential_exchange import V10CredentialExchange - from .. import manager as test_module from ..manager import ( + REUSE_ACCEPTED_WEBHOOK_TOPIC, + REUSE_WEBHOOK_TOPIC, OutOfBandManager, OutOfBandManagerError, ) -from ..message_types import INVITATION +from ..message_types import INVITATION, MESSAGE_REUSE from ..messages.invitation import HSProto, InvitationMessage +from ..messages.invitation import Service as OobService +from ..messages.problem_report import ProblemReport, ProblemReportReason from ..messages.reuse import HandshakeReuse from ..messages.reuse_accept import HandshakeReuseAccept -from ..messages.problem_report import ProblemReport, ProblemReportReason from ..models.invitation import InvitationRecord +from ..models.oob_record import OobRecord class TestConfig: - test_did = "55GkHamhTU1ZbTbV2ab9DE" test_verkey = "3Dn1SJNPaCXcvvJvSbsFWP2xaCjMom3can8CQNhWrTRx" test_endpoint = "http://localhost" test_target_did = "GbuDUYXaUZRfHD2jeDuQuP" their_public_did = "55GkHamhTU1ZbTbV2ab9DE" + test_service = OobService( + recipient_keys=[test_verkey], + routing_keys=[], + service_endpoint=test_endpoint, + ) NOW_8601 = datetime.utcnow().replace(tzinfo=timezone.utc).isoformat(" ", "seconds") + TEST_INVI_MESSAGE_TYPE = "out-of-band/1.1/invitation" NOW_EPOCH = str_to_epoch(NOW_8601) CD_ID = "GMm4vMw8LLrLJjp81kRRLp:3:CL:12:tag" INDY_PROOF_REQ = json.loads( @@ -200,23 +193,6 @@ class TestConfig: }, } - PRES_PREVIEW = IndyPresPreview( - attributes=[ - IndyPresAttrSpec(name="player", cred_def_id=CD_ID, value="Richie Knucklez"), - IndyPresAttrSpec( - name="screenCapture", - cred_def_id=CD_ID, - mime_type="image/png", - value="aW1hZ2luZSBhIHNjcmVlbiBjYXB0dXJl", - ), - ], - predicates=[ - IndyPresPredSpec( - name="highScore", cred_def_id=CD_ID, predicate=">=", threshold=1000000 - ) - ], - ) - PRES_REQ_V1 = PresentationRequest( comment="Test", request_presentations_attach=[ @@ -338,6 +314,17 @@ def setUp(self): self.responder = MockResponder() self.responder.send = async_mock.CoroutineMock() + self.test_mediator_routing_keys = [ + "3Dn1SJNPaCXcvvJvSbsFWP2xaCjMom3can8CQNhWrTRR" + ] + self.test_mediator_conn_id = "mediator-conn-id" + self.test_mediator_endpoint = "http://mediator.example.com" + + self.route_manager = async_mock.MagicMock(RouteManager) + self.route_manager.routing_info = async_mock.CoroutineMock( + return_value=(self.test_mediator_routing_keys, self.test_mediator_endpoint) + ) + self.profile = InMemoryProfile.test_profile( { "default_endpoint": TestConfig.test_endpoint, @@ -345,10 +332,16 @@ def setUp(self): "additional_endpoints": ["http://aries.ca/another-endpoint"], "debug.auto_accept_invites": True, "debug.auto_accept_requests": True, - } + }, + bind={ + RouteManager: self.route_manager, + }, ) self.profile.context.injector.bind_instance(BaseResponder, self.responder) + self.profile.context.injector.bind_instance( + EventBus, async_mock.MagicMock(notify=async_mock.CoroutineMock()) + ) self.mt_mgr = async_mock.MagicMock() self.mt_mgr = async_mock.create_autospec(MultitenantManager) self.profile.context.injector.bind_instance(BaseMultitenantManager, self.mt_mgr) @@ -366,20 +359,16 @@ def setUp(self): [], ) - self.test_conn_rec = ConnRecord( + self.test_conn_rec = async_mock.MagicMock( + connection_id="dummy", my_did=TestConfig.test_did, their_did=TestConfig.test_target_did, - their_role=None, + their_role=ConnRecord.Role.REQUESTER, state=ConnRecord.State.COMPLETED, their_public_did=self.their_public_did, + save=async_mock.CoroutineMock(), ) - self.test_mediator_routing_keys = [ - "3Dn1SJNPaCXcvvJvSbsFWP2xaCjMom3can8CQNhWrTRR" - ] - self.test_mediator_conn_id = "mediator-conn-id" - self.test_mediator_endpoint = "http://mediator.example.com" - async def test_create_invitation_handshake_succeeds(self): self.profile.context.update_settings({"public_invites": True}) @@ -390,8 +379,8 @@ async def test_create_invitation_handshake_succeeds(self): TestConfig.test_did, TestConfig.test_verkey, None, - method=DIDMethod.SOV, - key_type=KeyType.ED25519, + method=SOV, + key_type=ED25519, ) invi_rec = await self.manager.create_invitation( my_endpoint=TestConfig.test_endpoint, @@ -399,48 +388,15 @@ async def test_create_invitation_handshake_succeeds(self): hs_protos=[HSProto.RFC23], ) - assert invi_rec._invitation.ser["@type"] == DIDCommPrefix.qualify_current( - INVITATION + assert invi_rec.invitation._type == DIDCommPrefix.qualify_current( + self.TEST_INVI_MESSAGE_TYPE ) - assert not invi_rec._invitation.ser.get("requests~attach") + assert not invi_rec.invitation.requests_attach assert ( DIDCommPrefix.qualify_current(HSProto.RFC23.name) in invi_rec.invitation.handshake_protocols ) - assert invi_rec._invitation.ser["services"] == [ - f"did:sov:{TestConfig.test_did}" - ] - - async def test_create_invitation_mediation_overwrites_routing_and_endpoint(self): - async with self.profile.session() as session: - mock_conn_rec = async_mock.MagicMock() - - mediation_record = MediationRecord( - role=MediationRecord.ROLE_CLIENT, - state=MediationRecord.STATE_GRANTED, - connection_id=self.test_mediator_conn_id, - routing_keys=self.test_mediator_routing_keys, - endpoint=self.test_mediator_endpoint, - ) - await mediation_record.save(session) - with async_mock.patch.object( - MediationManager, - "get_default_mediator_id", - ) as mock_get_default_mediator, async_mock.patch.object( - mock_conn_rec, "metadata_set", async_mock.CoroutineMock() - ) as mock_metadata_set: - invite = await self.manager.create_invitation( - my_endpoint=TestConfig.test_endpoint, - my_label="test123", - hs_protos=[HSProto.RFC23], - mediation_id=mediation_record.mediation_id, - ) - assert isinstance(invite, InvitationRecord) - assert invite._invitation.ser["@type"] == DIDCommPrefix.qualify_current( - INVITATION - ) - assert invite.invitation.label == "test123" - mock_get_default_mediator.assert_not_called() + assert invi_rec.invitation.services == [f"did:sov:{TestConfig.test_did}"] async def test_create_invitation_multitenant_local(self): self.profile.context.update_settings( @@ -450,15 +406,13 @@ async def test_create_invitation_multitenant_local(self): } ) - self.multitenant_mgr.add_key = async_mock.CoroutineMock() - with async_mock.patch.object( InMemoryWallet, "create_signing_key", autospec=True ) as mock_wallet_create_signing_key, async_mock.patch.object( self.multitenant_mgr, "get_default_mediator" ) as mock_get_default_mediator: mock_wallet_create_signing_key.return_value = KeyInfo( - TestConfig.test_verkey, None, KeyType.ED25519 + TestConfig.test_verkey, None, ED25519 ) mock_get_default_mediator.return_value = MediationRecord() await self.manager.create_invitation( @@ -467,9 +421,7 @@ async def test_create_invitation_multitenant_local(self): multi_use=False, ) - self.multitenant_mgr.add_key.assert_called_once_with( - "test_wallet", TestConfig.test_verkey - ) + self.route_manager.route_invitation.assert_called_once() async def test_create_invitation_multitenant_public(self): self.profile.context.update_settings( @@ -480,8 +432,6 @@ async def test_create_invitation_multitenant_public(self): } ) - self.multitenant_mgr.add_key = async_mock.CoroutineMock() - with async_mock.patch.object( InMemoryWallet, "get_public_did", autospec=True ) as mock_wallet_get_public_did: @@ -489,8 +439,8 @@ async def test_create_invitation_multitenant_public(self): self.test_did, self.test_verkey, None, - method=DIDMethod.SOV, - key_type=KeyType.ED25519, + method=SOV, + key_type=ED25519, ) await self.manager.create_invitation( hs_protos=[HSProto.RFC23], @@ -498,9 +448,48 @@ async def test_create_invitation_multitenant_public(self): multi_use=False, ) - self.multitenant_mgr.add_key.assert_called_once_with( - "test_wallet", TestConfig.test_verkey, skip_if_exists=True + self.route_manager.route_invitation.assert_called_once() + + async def test_create_invitation_mediation_overwrites_routing_and_endpoint(self): + async with self.profile.session() as session: + mock_conn_rec = async_mock.MagicMock() + + mediation_record = MediationRecord( + role=MediationRecord.ROLE_CLIENT, + state=MediationRecord.STATE_GRANTED, + connection_id=self.test_mediator_conn_id, + routing_keys=self.test_mediator_routing_keys, + endpoint=self.test_mediator_endpoint, ) + await mediation_record.save(session) + with async_mock.patch.object( + MediationManager, + "get_default_mediator_id", + ) as mock_get_default_mediator, async_mock.patch.object( + mock_conn_rec, "metadata_set", async_mock.CoroutineMock() + ) as mock_metadata_set: + invite = await self.manager.create_invitation( + my_endpoint=TestConfig.test_endpoint, + my_label="test123", + hs_protos=[HSProto.RFC23], + mediation_id=mediation_record.mediation_id, + ) + assert isinstance(invite, InvitationRecord) + assert invite.invitation._type == DIDCommPrefix.qualify_current( + self.TEST_INVI_MESSAGE_TYPE + ) + assert invite.invitation.label == "test123" + assert ( + DIDKey.from_did( + invite.invitation.services[0].routing_keys[0] + ).public_key_b58 + == self.test_mediator_routing_keys[0] + ) + assert ( + invite.invitation.services[0].service_endpoint + == self.test_mediator_endpoint + ) + mock_get_default_mediator.assert_not_called() async def test_create_invitation_no_handshake_no_attachments_x(self): with self.assertRaises(OutOfBandManagerError) as context: @@ -525,8 +514,8 @@ async def test_create_invitation_attachment_v1_0_cred_offer(self): TestConfig.test_did, TestConfig.test_verkey, None, - method=DIDMethod.SOV, - key_type=KeyType.ED25519, + method=SOV, + key_type=ED25519, ) mock_retrieve_cxid.return_value = async_mock.MagicMock( credential_offer_dict=self.CRED_OFFER_V1 @@ -539,7 +528,12 @@ async def test_create_invitation_attachment_v1_0_cred_offer(self): attachments=[{"type": "credential-offer", "id": "dummy-id"}], ) + mock_retrieve_cxid.assert_called_once_with(ANY, "dummy-id") assert isinstance(invi_rec, InvitationRecord) + assert invi_rec.invitation.handshake_protocols + assert invi_rec.invitation.requests_attach[0].content[ + "@type" + ] == DIDCommPrefix.qualify_current(CREDENTIAL_OFFER) async def test_create_invitation_attachment_v1_0_cred_offer_no_handshake(self): self.profile.context.update_settings({"public_invites": True}) @@ -554,8 +548,8 @@ async def test_create_invitation_attachment_v1_0_cred_offer_no_handshake(self): TestConfig.test_did, TestConfig.test_verkey, None, - method=DIDMethod.SOV, - key_type=KeyType.ED25519, + method=SOV, + key_type=ED25519, ) mock_retrieve_cxid.return_value = async_mock.MagicMock( credential_offer_dict=self.CRED_OFFER_V1 @@ -568,8 +562,13 @@ async def test_create_invitation_attachment_v1_0_cred_offer_no_handshake(self): attachments=[{"type": "credential-offer", "id": "dummy-id"}], ) + mock_retrieve_cxid.assert_called_once_with(ANY, "dummy-id") assert isinstance(invi_rec, InvitationRecord) - assert not invi_rec._invitation.ser["handshake_protocols"] + assert not invi_rec.invitation.handshake_protocols + assert invi_rec.invitation.requests_attach[0].content == { + **self.CRED_OFFER_V1.serialize(), + "~thread": {"pthid": invi_rec.invi_msg_id}, + } async def test_create_invitation_attachment_v2_0_cred_offer(self): with async_mock.patch.object( @@ -587,15 +586,13 @@ async def test_create_invitation_attachment_v2_0_cred_offer(self): TestConfig.test_did, TestConfig.test_verkey, None, - method=DIDMethod.SOV, - key_type=KeyType.ED25519, + method=SOV, + key_type=ED25519, ) mock_retrieve_cxid_v1.side_effect = test_module.StorageNotFoundError() mock_retrieve_cxid_v2.return_value = async_mock.MagicMock( cred_offer=async_mock.MagicMock( - serialize=async_mock.MagicMock( - return_value=json.dumps({"cred": "offer"}) - ) + serialize=async_mock.MagicMock(return_value={"cred": "offer"}) ) ) invi_rec = await self.manager.create_invitation( @@ -606,7 +603,13 @@ async def test_create_invitation_attachment_v2_0_cred_offer(self): attachments=[{"type": "credential-offer", "id": "dummy-id"}], ) - assert invi_rec._invitation.ser["requests~attach"] + mock_retrieve_cxid_v2.assert_called_once_with(ANY, "dummy-id") + assert isinstance(invi_rec, InvitationRecord) + assert not invi_rec.invitation.handshake_protocols + assert invi_rec.invitation.requests_attach[0].content == { + "cred": "offer", + "~thread": {"pthid": invi_rec.invi_msg_id}, + } async def test_create_invitation_attachment_present_proof_v1_0(self): self.profile.context.update_settings({"public_invites": True}) @@ -621,8 +624,8 @@ async def test_create_invitation_attachment_present_proof_v1_0(self): TestConfig.test_did, TestConfig.test_verkey, None, - method=DIDMethod.SOV, - key_type=KeyType.ED25519, + method=SOV, + key_type=ED25519, ) mock_retrieve_pxid.return_value = async_mock.MagicMock( presentation_request_dict=self.PRES_REQ_V1 @@ -635,10 +638,13 @@ async def test_create_invitation_attachment_present_proof_v1_0(self): attachments=[{"type": "present-proof", "id": "dummy-id"}], ) - assert invi_rec._invitation.ser["requests~attach"] - mock_retrieve_pxid.assert_called_once() - assert isinstance(mock_retrieve_pxid.call_args[0][0], ProfileSession) - assert mock_retrieve_pxid.call_args[0][1] == "dummy-id" + mock_retrieve_pxid.assert_called_once_with(ANY, "dummy-id") + assert isinstance(invi_rec, InvitationRecord) + assert invi_rec.invitation.handshake_protocols + assert invi_rec.invitation.requests_attach[0].content == { + **self.PRES_REQ_V1.serialize(), + "~thread": {"pthid": invi_rec.invi_msg_id}, + } async def test_create_invitation_attachment_present_proof_v2_0(self): self.profile.context.update_settings({"public_invites": True}) @@ -657,8 +663,8 @@ async def test_create_invitation_attachment_present_proof_v2_0(self): TestConfig.test_did, TestConfig.test_verkey, None, - method=DIDMethod.SOV, - key_type=KeyType.ED25519, + method=SOV, + key_type=ED25519, ) mock_retrieve_pxid_1.side_effect = StorageNotFoundError() mock_retrieve_pxid_2.return_value = async_mock.MagicMock( @@ -672,348 +678,13 @@ async def test_create_invitation_attachment_present_proof_v2_0(self): attachments=[{"type": "present-proof", "id": "dummy-id"}], ) - assert invi_rec._invitation.ser["requests~attach"] - mock_retrieve_pxid_1.assert_called_once() - assert isinstance(mock_retrieve_pxid_1.call_args[0][0], ProfileSession) - assert mock_retrieve_pxid_1.call_args[0][1] == "dummy-id" - mock_retrieve_pxid_2.assert_called_once() - assert isinstance(mock_retrieve_pxid_2.call_args[0][0], ProfileSession) - assert mock_retrieve_pxid_2.call_args[0][1] == "dummy-id" - - async def test_dif_req_v2_attach_pres_existing_conn_auto_present_pres_msg_with_challenge( - self, - ): - async with self.profile.session() as session: - self.profile.context.update_settings({"public_invites": True}) - self.profile.context.update_settings( - {"debug.auto_respond_presentation_request": True} - ) - test_exist_conn = ConnRecord( - my_did=TestConfig.test_did, - their_did=TestConfig.test_target_did, - their_public_did=TestConfig.test_target_did, - invitation_msg_id="12345678-0123-4567-1234-567812345678", - their_role=ConnRecord.Role.REQUESTER, - ) - await test_exist_conn.save(session) - await test_exist_conn.metadata_set(session, "reuse_msg_state", "initial") - await test_exist_conn.metadata_set(session, "reuse_msg_id", "test_123") - receipt = MessageReceipt( - recipient_did=TestConfig.test_did, - recipient_did_public=False, - sender_did=TestConfig.test_target_did, - ) - dif_proof_req = deepcopy(TestConfig.DIF_PROOF_REQ) - dif_proof_req["options"] = {} - dif_proof_req["options"][ - "challenge" - ] = "3fa85f64-5717-4562-b3fc-2c963f66afa7" - dif_pres_req_v2 = V20PresRequest( - comment="some comment", - will_confirm=True, - formats=[ - V20PresFormat( - attach_id="dif", - format_=V20_PRES_ATTACH_FORMAT[PRES_20_REQUEST][ - V20PresFormat.Format.DIF.api - ], - ) - ], - request_presentations_attach=[ - AttachDecorator.data_json(mapping=dif_proof_req, ident="dif") - ], - ) - - px2_rec = test_module.V20PresExRecord( - auto_present=True, - pres_request=dif_pres_req_v2.serialize(), - ) - - dif_req_attach_v2 = AttachDecorator.data_json( - mapping=dif_pres_req_v2.serialize(), - ident="request-0", - ).serialize() - - with async_mock.patch.object( - DIDXManager, - "receive_invitation", - autospec=True, - ) as didx_mgr_receive_invitation, async_mock.patch.object( - V20PresManager, - "receive_pres_request", - autospec=True, - ) as pres_mgr_receive_pres_req, async_mock.patch( - "aries_cloudagent.protocols.out_of_band.v1_0.manager.InvitationMessage", - autospec=True, - ) as inv_message_cls, async_mock.patch.object( - OutOfBandManager, - "fetch_connection_targets", - autospec=True, - ) as oob_mgr_fetch_conn, async_mock.patch.object( - ConnRecord, - "find_existing_connection", - async_mock.CoroutineMock(), - ) as oob_mgr_find_existing_conn, async_mock.patch.object( - OutOfBandManager, - "check_reuse_msg_state", - autospec=True, - ) as oob_mgr_check_reuse_state, async_mock.patch.object( - OutOfBandManager, - "create_handshake_reuse_message", - autospec=True, - ) as oob_mgr_create_reuse_msg, async_mock.patch.object( - OutOfBandManager, - "receive_reuse_message", - autospec=True, - ) as oob_mgr_receive_reuse_msg, async_mock.patch.object( - OutOfBandManager, - "receive_reuse_accepted_message", - autospec=True, - ) as oob_mgr_receive_accept_msg, async_mock.patch.object( - OutOfBandManager, - "receive_problem_report", - autospec=True, - ) as oob_mgr_receive_problem_report, async_mock.patch.object( - V20PresManager, - "create_pres", - autospec=True, - ) as pres_mgr_create_pres: - oob_mgr_find_existing_conn.return_value = test_exist_conn - pres_mgr_receive_pres_req.return_value = px2_rec - pres_mgr_create_pres.return_value = ( - px2_rec, - V20Pres( - formats=[ - V20PresFormat( - attach_id="dif", - format_=V20_PRES_ATTACH_FORMAT[PRES_20][ - V20PresFormat.Format.DIF.api - ], - ) - ], - presentations_attach=[ - AttachDecorator.data_json( - mapping={"bogus": "proof"}, - ident="dif", - ) - ], - ), - ) - self.profile.context.injector.bind_instance( - VCHolder, - async_mock.MagicMock( - search_credentials=async_mock.MagicMock( - return_value=async_mock.MagicMock( - fetch=async_mock.CoroutineMock( - return_value=[ - VCRecord( - contexts=[ - "https://www.w3.org/2018/credentials/v1", - "https://www.w3.org/2018/credentials/examples/v1", - ], - expanded_types=[ - "https://www.w3.org/2018/credentials#VerifiableCredential", - "https://example.org/examples#UniversityDegreeCredential", - ], - issuer_id="https://example.edu/issuers/565049", - subject_ids=[ - "did:example:ebfeb1f712ebc6f1c276e12ec21" - ], - proof_types=["Ed25519Signature2018"], - schema_ids=[ - "https://example.org/examples/degree.json" - ], - cred_value={"...": "..."}, - given_id="http://example.edu/credentials/3732", - cred_tags={"some": "tag"}, - ) - ] - ) - ) - ) - ), - ) - mock_oob_invi = async_mock.MagicMock( - handshake_protocols=[ - pfx.qualify(HSProto.RFC23.name) for pfx in DIDCommPrefix - ], - services=[TestConfig.test_target_did], - requests_attach=[AttachDecorator.deserialize(dif_req_attach_v2)], - ) - - inv_message_cls.deserialize.return_value = mock_oob_invi - - conn_rec = await self.manager.receive_invitation( - mock_oob_invi, use_existing_connection=True - ) - assert conn_rec is not None - - async def test_dif_req_v2_attach_pres_existing_conn_auto_present_pres_msg_with_nonce( - self, - ): - async with self.profile.session() as session: - self.profile.context.update_settings({"public_invites": True}) - self.profile.context.update_settings( - {"debug.auto_respond_presentation_request": True} - ) - test_exist_conn = ConnRecord( - my_did=TestConfig.test_did, - their_did=TestConfig.test_target_did, - their_public_did=TestConfig.test_target_did, - invitation_msg_id="12345678-0123-4567-1234-567812345678", - their_role=ConnRecord.Role.REQUESTER, - ) - await test_exist_conn.save(session) - await test_exist_conn.metadata_set(session, "reuse_msg_state", "initial") - await test_exist_conn.metadata_set(session, "reuse_msg_id", "test_123") - receipt = MessageReceipt( - recipient_did=TestConfig.test_did, - recipient_did_public=False, - sender_did=TestConfig.test_target_did, - ) - - dif_proof_req = deepcopy(TestConfig.DIF_PROOF_REQ) - dif_proof_req["options"] = {} - dif_proof_req["options"]["nonce"] = "12345" - dif_pres_req_v2 = V20PresRequest( - comment="some comment", - will_confirm=True, - formats=[ - V20PresFormat( - attach_id="dif", - format_=V20_PRES_ATTACH_FORMAT[PRES_20_REQUEST][ - V20PresFormat.Format.DIF.api - ], - ) - ], - request_presentations_attach=[ - AttachDecorator.data_json(mapping=dif_proof_req, ident="dif") - ], - ) - - px2_rec = test_module.V20PresExRecord( - auto_present=True, - pres_request=dif_pres_req_v2.serialize(), - ) - - dif_req_attach_v2 = AttachDecorator.data_json( - mapping=dif_pres_req_v2.serialize(), - ident="request-0", - ).serialize() - - with async_mock.patch.object( - DIDXManager, - "receive_invitation", - autospec=True, - ) as didx_mgr_receive_invitation, async_mock.patch.object( - V20PresManager, - "receive_pres_request", - autospec=True, - ) as pres_mgr_receive_pres_req, async_mock.patch( - "aries_cloudagent.protocols.out_of_band.v1_0.manager.InvitationMessage", - autospec=True, - ) as inv_message_cls, async_mock.patch.object( - OutOfBandManager, - "fetch_connection_targets", - autospec=True, - ) as oob_mgr_fetch_conn, async_mock.patch.object( - ConnRecord, - "find_existing_connection", - async_mock.CoroutineMock(), - ) as oob_mgr_find_existing_conn, async_mock.patch.object( - OutOfBandManager, - "check_reuse_msg_state", - autospec=True, - ) as oob_mgr_check_reuse_state, async_mock.patch.object( - OutOfBandManager, - "create_handshake_reuse_message", - autospec=True, - ) as oob_mgr_create_reuse_msg, async_mock.patch.object( - OutOfBandManager, - "receive_reuse_message", - autospec=True, - ) as oob_mgr_receive_reuse_msg, async_mock.patch.object( - OutOfBandManager, - "receive_reuse_accepted_message", - autospec=True, - ) as oob_mgr_receive_accept_msg, async_mock.patch.object( - OutOfBandManager, - "receive_problem_report", - autospec=True, - ) as oob_mgr_receive_problem_report, async_mock.patch.object( - V20PresManager, - "create_pres", - autospec=True, - ) as pres_mgr_create_pres: - oob_mgr_find_existing_conn.return_value = test_exist_conn - pres_mgr_receive_pres_req.return_value = px2_rec - pres_mgr_create_pres.return_value = ( - px2_rec, - V20Pres( - formats=[ - V20PresFormat( - attach_id="dif", - format_=V20_PRES_ATTACH_FORMAT[PRES_20][ - V20PresFormat.Format.DIF.api - ], - ) - ], - presentations_attach=[ - AttachDecorator.data_json( - mapping={"bogus": "proof"}, - ident="dif", - ) - ], - ), - ) - self.profile.context.injector.bind_instance( - VCHolder, - async_mock.MagicMock( - search_credentials=async_mock.MagicMock( - return_value=async_mock.MagicMock( - fetch=async_mock.CoroutineMock( - return_value=[ - VCRecord( - contexts=[ - "https://www.w3.org/2018/credentials/v1", - "https://www.w3.org/2018/credentials/examples/v1", - ], - expanded_types=[ - "https://www.w3.org/2018/credentials#VerifiableCredential", - "https://example.org/examples#UniversityDegreeCredential", - ], - issuer_id="https://example.edu/issuers/565049", - subject_ids=[ - "did:example:ebfeb1f712ebc6f1c276e12ec21" - ], - proof_types=["Ed25519Signature2018"], - schema_ids=[ - "https://example.org/examples/degree.json" - ], - cred_value={"...": "..."}, - given_id="http://example.edu/credentials/3732", - cred_tags={"some": "tag"}, - ) - ] - ) - ) - ) - ), - ) - mock_oob_invi = async_mock.MagicMock( - handshake_protocols=[ - pfx.qualify(HSProto.RFC23.name) for pfx in DIDCommPrefix - ], - services=[TestConfig.test_target_did], - requests_attach=[AttachDecorator.deserialize(dif_req_attach_v2)], - ) - - inv_message_cls.deserialize.return_value = mock_oob_invi - - conn_rec = await self.manager.receive_invitation( - mock_oob_invi, use_existing_connection=True - ) - assert conn_rec is not None + mock_retrieve_pxid_2.assert_called_once_with(ANY, "dummy-id") + assert isinstance(invi_rec, InvitationRecord) + assert invi_rec.invitation.handshake_protocols + assert invi_rec.invitation.requests_attach[0].content == { + **TestConfig.PRES_REQ_V2.serialize(), + "~thread": {"pthid": invi_rec.invi_msg_id}, + } async def test_create_invitation_public_x_no_public_invites(self): self.profile.context.update_settings({"public_invites": False}) @@ -1039,6 +710,19 @@ async def test_create_invitation_public_x_multi_use(self): ) assert "Cannot create public invitation with" in str(context.exception) + async def test_create_invitation_requests_attach_x_multi_use(self): + with self.assertRaises(OutOfBandManagerError) as context: + await self.manager.create_invitation( + public=False, + my_endpoint="testendpoint", + hs_protos=[test_module.HSProto.RFC23], + attachments=[{"some": "attachment"}], + multi_use=True, + ) + assert "Cannot create multi use invitation with attachments" in str( + context.exception + ) + async def test_create_invitation_public_x_no_public_did(self): self.profile.context.update_settings({"public_invites": True}) @@ -1066,15 +750,15 @@ async def test_create_invitation_attachment_x(self): TestConfig.test_did, TestConfig.test_verkey, None, - method=DIDMethod.SOV, - key_type=KeyType.ED25519, + method=SOV, + key_type=ED25519, ) with self.assertRaises(OutOfBandManagerError) as context: await self.manager.create_invitation( my_endpoint=TestConfig.test_endpoint, public=False, hs_protos=[test_module.HSProto.RFC23], - multi_use=True, + multi_use=False, attachments=[{"having": "attachment", "is": "no", "good": "here"}], ) assert "Unknown attachment type" in str(context.exception) @@ -1105,11 +789,12 @@ async def test_create_invitation_peer_did(self): public=False, hs_protos=[test_module.HSProto.RFC23], multi_use=False, + service_accept=["didcomm/aip1", "didcomm/aip2;env=rfc19"], ) assert invi_rec._invitation.ser[ "@type" - ] == DIDCommPrefix.qualify_current(INVITATION) + ] == DIDCommPrefix.qualify_current(self.TEST_INVI_MESSAGE_TYPE) assert not invi_rec._invitation.ser.get("requests~attach") assert invi_rec.invitation.label == "That guy" assert ( @@ -1123,7 +808,7 @@ async def test_create_invitation_peer_did(self): assert ( service["routingKeys"][0] == DIDKey.from_public_key_b58( - self.test_mediator_routing_keys[0], KeyType.ED25519 + self.test_mediator_routing_keys[0], ED25519 ).did ) assert service["serviceEndpoint"] == self.test_mediator_endpoint @@ -1150,64 +835,402 @@ async def test_create_invitation_x_public_metadata(self): TestConfig.test_did, TestConfig.test_verkey, None, - method=DIDMethod.SOV, - key_type=KeyType.ED25519, + method=SOV, + key_type=ED25519, ) with self.assertRaises(OutOfBandManagerError) as context: await self.manager.create_invitation( - public=True, - hs_protos=[test_module.HSProto.RFC23], + public=False, + hs_protos=[], + attachments=[{"an": "attachment"}], metadata={"hello": "world"}, multi_use=False, ) - assert "Cannot store metadata on public" in str(context.exception) - - async def test_receive_invitation_with_valid_mediation(self): - async with self.profile.session() as session: - self.profile.context.update_settings({"public_invites": True}) - mediation_record = MediationRecord( - role=MediationRecord.ROLE_CLIENT, - state=MediationRecord.STATE_GRANTED, - connection_id=self.test_mediator_conn_id, - routing_keys=self.test_mediator_routing_keys, - endpoint=self.test_mediator_endpoint, + assert "Cannot store metadata without handshake protocols" in str( + context.exception ) - await mediation_record.save(session) - with async_mock.patch.object( - DIDXManager, "receive_invitation", async_mock.CoroutineMock() - ) as mock_didx_recv_invi: - invite = await self.manager.create_invitation( - my_endpoint=TestConfig.test_endpoint, - my_label="test123", - hs_protos=[HSProto.RFC23], - ) - invi_msg = invite.invitation - invitee_record = await self.manager.receive_invitation( - invitation=invi_msg, - mediation_id=mediation_record._id, - ) - mock_didx_recv_invi.assert_called_once_with( - invitation=invi_msg, - their_public_did=None, - auto_accept=None, - alias=None, - mediation_id=mediation_record._id, + + async def test_wait_for_conn_rec_active_retrieve_by_id(self): + with async_mock.patch.object( + ConnRecord, + "retrieve_by_id", + async_mock.CoroutineMock( + return_value=async_mock.MagicMock( + connection_id="the-retrieved-connection-id" ) + ), + ): + conn_rec = await self.manager._wait_for_conn_rec_active("a-connection-id") + assert conn_rec.connection_id == "the-retrieved-connection-id" - async def test_receive_invitation_with_invalid_mediation(self): + async def test_create_handshake_reuse_msg(self): self.profile.context.update_settings({"public_invites": True}) + with async_mock.patch.object( - DIDXManager, - "receive_invitation", - async_mock.CoroutineMock(), - ) as mock_didx_recv_invi: + OutOfBandManager, + "fetch_connection_targets", + autospec=True, + ) as oob_mgr_fetch_conn, async_mock.patch.object( + ConnRecord, + "retrieve_by_id", + async_mock.CoroutineMock(return_value=self.test_conn_rec), + ): + oob_mgr_fetch_conn.return_value = ConnectionTarget( + did=TestConfig.test_did, + endpoint=TestConfig.test_endpoint, + recipient_keys=[TestConfig.test_verkey], + sender_key=TestConfig.test_verkey, + ) + + invitation = InvitationMessage() + oob_record = OobRecord( + invitation=invitation, + invi_msg_id=invitation._id, + role=OobRecord.ROLE_RECEIVER, + connection_id=self.test_conn_rec.connection_id, + state=OobRecord.STATE_INITIAL, + ) + + oob_record = await self.manager._create_handshake_reuse_message( + oob_record, self.test_conn_rec, get_version_from_message(invitation) + ) + + _, kwargs = self.responder.send.call_args + reuse_message: HandshakeReuse = kwargs.get("message") + + assert oob_record.state == OobRecord.STATE_AWAIT_RESPONSE + + # Assert responder has been called with the reuse message + assert reuse_message._type == DIDCommPrefix.qualify_current( + "out-of-band/1.1/handshake-reuse" + ) + assert oob_record.reuse_msg_id == reuse_message._id + + async def test_create_handshake_reuse_msg_catch_exception(self): + self.profile.context.update_settings({"public_invites": True}) + with async_mock.patch.object( + OutOfBandManager, + "fetch_connection_targets", + autospec=True, + ) as oob_mgr_fetch_conn: + oob_mgr_fetch_conn.side_effect = StorageNotFoundError() + with self.assertRaises(OutOfBandManagerError) as context: + await self.manager._create_handshake_reuse_message( + async_mock.MagicMock(), self.test_conn_rec, "1.0" + ) + assert "Error on creating and sending a handshake reuse message" in str( + context.exception + ) + + async def test_receive_reuse_message_existing_found(self): + self.profile.context.update_settings({"public_invites": True}) + + receipt = MessageReceipt( + recipient_did=TestConfig.test_did, + recipient_did_public=False, + ) + + reuse_msg = HandshakeReuse() + reuse_msg.assign_thread_id(thid="the-thread-id", pthid="the-pthid") + + self.test_conn_rec.invitation_msg_id = "test_123" + self.test_conn_rec.state = ConnRecord.State.COMPLETED.rfc160 + + with async_mock.patch.object( + OutOfBandManager, + "fetch_connection_targets", + autospec=True, + ) as oob_mgr_fetch_conn, async_mock.patch.object( + OobRecord, + "retrieve_by_tag_filter", + autospec=True, + ) as mock_retrieve_oob, async_mock.patch.object( + self.profile, "notify", autospec=True + ) as mock_notify: + mock_retrieve_oob.return_value = async_mock.MagicMock( + emit_event=async_mock.CoroutineMock(), + delete_record=async_mock.CoroutineMock(), + multi_use=False, + ) + + await self.manager.receive_reuse_message( + reuse_msg, receipt, self.test_conn_rec + ) + mock_notify.assert_called_once_with( + REUSE_WEBHOOK_TOPIC, + { + "thread_id": "the-thread-id", + "connection_id": self.test_conn_rec.connection_id, + "comment": "Connection dummy is being reused for invitation the-pthid", + }, + ) + + # delete should be called if multi_use == False + mock_retrieve_oob.return_value.delete_record.assert_called_once() + mock_retrieve_oob.return_value.emit_event.assert_called_once() + self.responder.send.assert_called_once_with( + message=ANY, target_list=oob_mgr_fetch_conn.return_value + ) + + assert mock_retrieve_oob.return_value.state == OobRecord.STATE_DONE + assert mock_retrieve_oob.return_value.reuse_msg_id == reuse_msg._thread_id + assert ( + mock_retrieve_oob.return_value.connection_id + == self.test_conn_rec.connection_id + ) + + async def test_receive_reuse_message_existing_found_multi_use(self): + self.profile.context.update_settings({"public_invites": True}) + + receipt = MessageReceipt( + recipient_did=TestConfig.test_did, + recipient_did_public=False, + ) + + reuse_msg = HandshakeReuse(version="1.0") + reuse_msg.assign_thread_id(thid="the-thread-id", pthid="the-pthid") + + self.test_conn_rec.invitation_msg_id = "test_123" + self.test_conn_rec.state = ConnRecord.State.COMPLETED.rfc160 + + with async_mock.patch.object( + OutOfBandManager, + "fetch_connection_targets", + autospec=True, + ) as oob_mgr_fetch_conn, async_mock.patch.object( + OobRecord, + "retrieve_by_tag_filter", + autospec=True, + ) as mock_retrieve_oob, async_mock.patch.object( + self.profile, "notify", autospec=True + ) as mock_notify: + mock_retrieve_oob.return_value = async_mock.MagicMock( + emit_event=async_mock.CoroutineMock(), + delete_record=async_mock.CoroutineMock(), + multi_use=True, + ) + + await self.manager.receive_reuse_message( + reuse_msg, receipt, self.test_conn_rec + ) + mock_notify.assert_called_once_with( + REUSE_WEBHOOK_TOPIC, + { + "thread_id": "the-thread-id", + "connection_id": self.test_conn_rec.connection_id, + "comment": "Connection dummy is being reused for invitation the-pthid", + }, + ) + + # delete should be called if multi_use == False + mock_retrieve_oob.return_value.delete_record.assert_not_called() + mock_retrieve_oob.return_value.emit_event.assert_called_once() + self.responder.send.assert_called_once_with( + message=ANY, target_list=oob_mgr_fetch_conn.return_value + ) + + assert mock_retrieve_oob.return_value.state == OobRecord.STATE_DONE + assert mock_retrieve_oob.return_value.reuse_msg_id == reuse_msg._thread_id + assert ( + mock_retrieve_oob.return_value.connection_id + == self.test_conn_rec.connection_id + ) + + async def test_receive_reuse_accepted(self): + self.profile.context.update_settings({"public_invites": True}) + + receipt = MessageReceipt( + recipient_did=TestConfig.test_did, + recipient_did_public=False, + sender_did="test_did", + ) + reuse_msg_accepted = HandshakeReuseAccept() + reuse_msg_accepted.assign_thread_id(thid="the-thread-id", pthid="the-pthid") + + with async_mock.patch.object( + self.profile, "notify", autospec=True + ) as mock_notify, async_mock.patch.object( + OobRecord, "retrieve_by_tag_filter", autospec=True + ) as mock_retrieve_oob: + mock_retrieve_oob.return_value = async_mock.MagicMock( + emit_event=async_mock.CoroutineMock(), + delete_record=async_mock.CoroutineMock(), + ) + + await self.manager.receive_reuse_accepted_message( + reuse_msg_accepted, receipt, self.test_conn_rec + ) + + mock_retrieve_oob.return_value.emit_event.assert_called_once() + mock_retrieve_oob.return_value.delete_record.assert_called_once() + mock_notify.assert_called_once_with( + REUSE_ACCEPTED_WEBHOOK_TOPIC, + { + "thread_id": "the-thread-id", + "connection_id": self.test_conn_rec.connection_id, + "state": "accepted", + "comment": f"Connection {self.test_conn_rec.connection_id} is being reused for invitation the-pthid", + }, + ) + + async def test_receive_reuse_accepted_x(self): + self.profile.context.update_settings({"public_invites": True}) + + receipt = MessageReceipt( + recipient_did=TestConfig.test_did, + recipient_did_public=False, + sender_did="test_did", + ) + reuse_msg_accepted = HandshakeReuseAccept() + reuse_msg_accepted.assign_thread_id(thid="the-thread-id", pthid="the-pthid") + + with async_mock.patch.object( + self.profile, "notify", autospec=True + ) as mock_notify, async_mock.patch.object( + OobRecord, "retrieve_by_tag_filter", autospec=True + ) as mock_retrieve_oob: + mock_retrieve_oob.side_effect = (StorageNotFoundError,) + + with self.assertRaises(test_module.OutOfBandManagerError) as err: + await self.manager.receive_reuse_accepted_message( + reuse_msg_accepted, receipt, self.test_conn_rec + ) + assert "Error processing reuse accepted message " in err.exception.message + + mock_notify.assert_called_once_with( + REUSE_ACCEPTED_WEBHOOK_TOPIC, + { + "thread_id": "the-thread-id", + "state": "rejected", + "connection_id": self.test_conn_rec.connection_id, + "comment": f"Unable to process HandshakeReuseAccept message, connection {self.test_conn_rec.connection_id} and invitation the-pthid", + }, + ) + + async def test_receive_problem_report(self): + self.profile.context.update_settings({"public_invites": True}) + + receipt = MessageReceipt( + recipient_did=TestConfig.test_did, + recipient_did_public=False, + sender_did="test_did", + ) + problem_report = ProblemReport( + description={ + "en": "test", + "code": ProblemReportReason.EXISTING_CONNECTION_NOT_ACTIVE.value, + } + ) + problem_report.assign_thread_id(thid="the-thread-id", pthid="the-pthid") + + with async_mock.patch.object( + OobRecord, "retrieve_by_tag_filter", autospec=True + ) as mock_retrieve_oob: + mock_retrieve_oob.return_value = async_mock.MagicMock( + emit_event=async_mock.CoroutineMock(), + delete_record=async_mock.CoroutineMock(), + save=async_mock.CoroutineMock(), + ) + + await self.manager.receive_problem_report( + problem_report, receipt, self.test_conn_rec + ) + + mock_retrieve_oob.assert_called_once_with( + ANY, {"invi_msg_id": "the-pthid", "reuse_msg_id": "the-thread-id"} + ) + assert mock_retrieve_oob.return_value.state == OobRecord.STATE_NOT_ACCEPTED + + async def test_receive_problem_report_x(self): + self.profile.context.update_settings({"public_invites": True}) + + receipt = MessageReceipt( + recipient_did=TestConfig.test_did, + recipient_did_public=False, + sender_did="test_did", + ) + problem_report = ProblemReport( + description={ + "en": "test", + "code": ProblemReportReason.EXISTING_CONNECTION_NOT_ACTIVE.value, + } + ) + problem_report.assign_thread_id(thid="the-thread-id", pthid="the-pthid") + + with async_mock.patch.object( + OobRecord, "retrieve_by_tag_filter", autospec=True + ) as mock_retrieve_oob: + mock_retrieve_oob.side_effect = (StorageNotFoundError(),) + + with self.assertRaises(OutOfBandManagerError) as err: + await self.manager.receive_problem_report( + problem_report, receipt, self.test_conn_rec + ) + assert "Error processing problem report message " in err.exception.message + + async def test_receive_invitation_with_valid_mediation(self): + mock_conn = async_mock.MagicMock(connection_id="dummy-connection") + + async with self.profile.session() as session: + self.profile.context.update_settings({"public_invites": True}) + mediation_record = MediationRecord( + role=MediationRecord.ROLE_CLIENT, + state=MediationRecord.STATE_GRANTED, + connection_id=self.test_mediator_conn_id, + routing_keys=self.test_mediator_routing_keys, + endpoint=self.test_mediator_endpoint, + ) + await mediation_record.save(session) + with async_mock.patch.object( + DIDXManager, "receive_invitation", async_mock.CoroutineMock() + ) as mock_didx_recv_invi, async_mock.patch.object( + ConnRecord, "retrieve_by_id", async_mock.CoroutineMock() + ) as mock_retrieve_conn_by_id: + invite = await self.manager.create_invitation( + my_endpoint=TestConfig.test_endpoint, + my_label="test123", + hs_protos=[HSProto.RFC23], + ) + + mock_retrieve_conn_by_id.return_value = mock_conn + mock_didx_recv_invi.return_value = mock_conn + invi_msg = invite.invitation + await self.manager.receive_invitation( + invitation=invi_msg, + mediation_id=mediation_record._id, + ) + mock_didx_recv_invi.assert_called_once_with( + invitation=invi_msg, + their_public_did=None, + auto_accept=None, + alias=None, + mediation_id=mediation_record._id, + ) + + async def test_receive_invitation_with_invalid_mediation(self): + mock_conn = async_mock.MagicMock(connection_id="dummy-connection") + + with async_mock.patch.object( + DIDXManager, + "receive_invitation", + async_mock.CoroutineMock(), + ) as mock_didx_recv_invi, async_mock.patch.object( + ConnRecord, + "retrieve_by_id", + async_mock.CoroutineMock(), + ) as mock_retrieve_conn_by_id: invite = await self.manager.create_invitation( my_endpoint=TestConfig.test_endpoint, my_label="test123", hs_protos=[HSProto.RFC23], ) + mock_didx_recv_invi.return_value = mock_conn + mock_retrieve_conn_by_id.return_value = mock_conn invi_msg = invite.invitation - invitee_record = await self.manager.receive_invitation( + self.route_manager.mediation_record_if_id = async_mock.CoroutineMock( + side_effect=StorageNotFoundError + ) + await self.manager.receive_invitation( invi_msg, mediation_id="test-mediation-id", ) @@ -1221,61 +1244,59 @@ async def test_receive_invitation_with_invalid_mediation(self): async def test_receive_invitation_didx_services_with_service_block(self): self.profile.context.update_settings({"public_invites": True}) + + mock_conn = async_mock.MagicMock(connection_id="dummy-connection") + with async_mock.patch.object( test_module, "DIDXManager", autospec=True ) as didx_mgr_cls, async_mock.patch.object( - test_module, - "InvitationMessage", - autospec=True, - ) as invi_msg_cls: + ConnRecord, "retrieve_by_id", async_mock.CoroutineMock() + ) as mock_retrieve_conn_by_id: didx_mgr_cls.return_value = async_mock.MagicMock( - receive_invitation=async_mock.CoroutineMock() + receive_invitation=async_mock.CoroutineMock(return_value=mock_conn) ) - mock_oob_invi = async_mock.MagicMock( + mock_retrieve_conn_by_id.return_value = mock_conn + oob_invitation = InvitationMessage( requests_attach=[], handshake_protocols=[ pfx.qualify(HSProto.RFC23.name) for pfx in DIDCommPrefix ], services=[ - async_mock.MagicMock( + OobService( recipient_keys=["dummy"], routing_keys=[], ) ], ) - invi_msg_cls.deserialize.return_value = mock_oob_invi - await self.manager.receive_invitation(mock_oob_invi) + await self.manager.receive_invitation(oob_invitation) - async def test_receive_invitation_connection_mock(self): + async def test_receive_invitation_connection_protocol(self): self.profile.context.update_settings({"public_invites": True}) + + mock_conn = async_mock.MagicMock(connection_id="dummy-connection") + with async_mock.patch.object( test_module, "ConnectionManager", autospec=True ) as conn_mgr_cls, async_mock.patch.object( - test_module, - "InvitationMessage", - autospec=True, - ) as invi_msg_cls, async_mock.patch.object( - self.manager, - "receive_invitation", - async_mock.CoroutineMock(), - ) as mock_receive_invitation: - mock_receive_invitation.return_value = self.test_conn_rec.serialize() + ConnRecord, "retrieve_by_id", async_mock.CoroutineMock() + ) as mock_conn_retrieve_by_id: conn_mgr_cls.return_value = async_mock.MagicMock( - receive_invitation=async_mock.CoroutineMock() + receive_invitation=async_mock.CoroutineMock(return_value=mock_conn) ) - mock_oob_invi = async_mock.MagicMock( + mock_conn_retrieve_by_id.return_value = mock_conn + oob_invitation = InvitationMessage( handshake_protocols=[ pfx.qualify(HSProto.RFC160.name) for pfx in DIDCommPrefix ], label="test", _id="test123", services=[ - async_mock.MagicMock( + OobService( recipient_keys=[ DIDKey.from_public_key_b58( "9WCgWKUaAJj3VWxxtzvvMQN3AoFxoBtBDo9ntwJnVVCC", - KeyType.ED25519, + ED25519, ).did ], routing_keys=[], @@ -1284,2187 +1305,349 @@ async def test_receive_invitation_connection_mock(self): ], requests_attach=[], ) - invi_msg_cls.deserialize.return_value = mock_oob_invi - result = await self.manager.receive_invitation(mock_oob_invi) - assert result == self.test_conn_rec.serialize() + oob_record = await self.manager.receive_invitation(oob_invitation) + conn_mgr_cls.return_value.receive_invitation.assert_called_once_with( + invitation=ANY, + their_public_did=None, + auto_accept=None, + alias=None, + mediation_id=None, + ) + _, kwargs = conn_mgr_cls.return_value.receive_invitation.call_args + invitation = kwargs["invitation"] + assert isinstance(invitation, ConnectionInvitation) - async def test_receive_invitation_connection(self): - self.profile.context.update_settings({"public_invites": True}) - oob_invi_rec = await self.manager.create_invitation( - auto_accept=True, - public=False, - hs_protos=[test_module.HSProto.RFC160], - multi_use=False, - ) + assert invitation.endpoint == "http://localhost" + assert invitation.recipient_keys == [ + "9WCgWKUaAJj3VWxxtzvvMQN3AoFxoBtBDo9ntwJnVVCC" + ] + assert not invitation.routing_keys - result = await self.manager.receive_invitation( - invitation=oob_invi_rec.invitation, - use_existing_connection=True, - auto_accept=True, - ) - connection_id = UUID(result.connection_id, version=4) - assert ( - connection_id.hex == result.connection_id.replace("-", "") - and len(result.connection_id) > 5 - ) + assert oob_record.state == "deleted" + assert oob_record._previous_state == OobRecord.STATE_DONE async def test_receive_invitation_services_with_neither_service_blocks_nor_dids( self, ): self.profile.context.update_settings({"public_invites": True}) - with async_mock.patch.object( - test_module, "InvitationMessage", async_mock.MagicMock() - ) as invi_msg_cls: - mock_invi_msg = async_mock.MagicMock( - services=[], - ) - invi_msg_cls.deserialize.return_value = mock_invi_msg - with self.assertRaises(OutOfBandManagerError): - await self.manager.receive_invitation(mock_invi_msg) - - async def test_receive_invitation_services_with_service_did(self): - self.profile.context.update_settings({"public_invites": True}) - with async_mock.patch.object( - test_module, "DIDXManager", autospec=True - ) as didx_mgr_cls, async_mock.patch.object( - test_module, "InvitationMessage", autospec=True - ) as invi_msg_cls: - didx_mgr_cls.return_value = async_mock.MagicMock( - receive_invitation=async_mock.CoroutineMock() - ) - mock_oob_invi = async_mock.MagicMock( - handshake_protocols=[ - pfx.qualify(HSProto.RFC23.name) for pfx in DIDCommPrefix - ], - services=[TestConfig.test_did], - requests_attach=[], - ) - invi_msg_cls.deserialize.return_value = mock_oob_invi + oob_invitation = InvitationMessage( + services=[], + ) + with self.assertRaises(OutOfBandManagerError) as err: + await self.manager.receive_invitation(oob_invitation) - invi_rec = await self.manager.receive_invitation(mock_oob_invi) - assert invi_rec._invitation.ser["services"] + assert "service array must have exactly one element" in err.exception.message - async def test_receive_invitation_attachment_x(self): + async def test_receive_invitation_no_hs_protos_no_attach( + self, + ): self.profile.context.update_settings({"public_invites": True}) - with async_mock.patch.object( - DIDXManager, "receive_invitation", autospec=True - ) as didx_mgr_receive_invitation, async_mock.patch( - "aries_cloudagent.protocols.out_of_band.v1_0.manager.InvitationMessage", - autospec=True, - ) as inv_message_cls: - - mock_oob_invi = async_mock.MagicMock( - services=[TestConfig.test_did], - handshake_protocols=[ - pfx.qualify(HSProto.RFC23.name) for pfx in DIDCommPrefix - ], - requests_attach=[{"having": "attachment", "is": "no", "good": "here"}], - ) - inv_message_cls.deserialize.return_value = mock_oob_invi - - with self.assertRaises(OutOfBandManagerError) as context: - await self.manager.receive_invitation(mock_oob_invi) - assert "requests~attach is not properly formatted" in str(context.exception) + oob_invitation = InvitationMessage( + services=["did:sov:something"], + ) + with self.assertRaises(OutOfBandManagerError) as err: + await self.manager.receive_invitation(oob_invitation) - async def test_receive_invitation_req_pres_v1_0_attachment_x(self): - self.profile.context.update_settings({"public_invites": True}) - with async_mock.patch.object( - DIDXManager, "receive_invitation", autospec=True - ) as didx_mgr_receive_invitation, async_mock.patch( - "aries_cloudagent.protocols.out_of_band.v1_0.manager.InvitationMessage", - autospec=True, - ) as inv_message_cls: - mock_oob_invi = async_mock.MagicMock( - handshake_protocols=[ - pfx.qualify(HSProto.RFC23.name) for pfx in DIDCommPrefix - ], - services=[TestConfig.test_did], - requests_attach=[ - async_mock.MagicMock( - data=async_mock.MagicMock( - json={ - "@type": DIDCommPrefix.qualify_current( - PRESENTATION_REQUEST - ) - } - ) - ), - ], - ) - inv_message_cls.deserialize.return_value = mock_oob_invi - - with self.assertRaises(OutOfBandManagerError) as context: - result = await self.manager.receive_invitation(mock_oob_invi) - connection_id = UUID(result.connection_id, version=4) - assert ( - connection_id.hex == result.connection_id - and len(result.connection_id) > 5 - ) - assert "requests~attach is not properly formatted" in str(context.exception) - - async def test_receive_invitation_invalid_request_type_x(self): - self.profile.context.update_settings({"public_invites": True}) - with async_mock.patch.object( - DIDXManager, "receive_invitation", autospec=True - ) as didx_mgr_receive_invitation, async_mock.patch( - "aries_cloudagent.protocols.out_of_band.v1_0.manager.InvitationMessage", - autospec=True, - ) as inv_message_cls: - - mock_oob_invi = async_mock.MagicMock( - services=[TestConfig.test_did], - handshake_protocols=[], - requests_attach=[], - ) - inv_message_cls.deserialize.return_value = mock_oob_invi - - with self.assertRaises(OutOfBandManagerError): - await self.manager.receive_invitation(mock_oob_invi) - - async def test_find_existing_connection(self): - async with self.profile.session() as session: - test_conn_rec = ConnRecord( - my_did=TestConfig.test_did, - their_did=TestConfig.test_target_did, - their_role=None, - state=ConnRecord.State.COMPLETED, - their_public_did=self.their_public_did, - ) - await test_conn_rec.save(session) - conn_record = await ConnRecord.find_existing_connection( - session=session, their_public_did="not_addded" - ) - assert conn_record == None - - conn_record = await ConnRecord.find_existing_connection( - session=session, their_public_did=self.their_public_did - ) - assert conn_record == test_conn_rec - await test_conn_rec.delete_record(session) - - async def test_check_reuse_msg_state(self): - async with self.profile.session() as session: - await self.test_conn_rec.save(session) - await self.test_conn_rec.metadata_set( - session, "reuse_msg_state", "accepted" - ) - assert await self.manager.check_reuse_msg_state(self.test_conn_rec) is None - - async def test_create_handshake_reuse_msg(self): - async with self.profile.session() as session: - self.profile.context.update_settings({"public_invites": True}) - await self.test_conn_rec.save(session) - with async_mock.patch.object( - DIDXManager, "receive_invitation", autospec=True - ) as didx_mgr_receive_invitation, async_mock.patch( - "aries_cloudagent.protocols.out_of_band.v1_0.manager.InvitationMessage", - autospec=True, - ) as inv_message_cls, async_mock.patch.object( - OutOfBandManager, - "fetch_connection_targets", - autospec=True, - ) as oob_mgr_fetch_conn: - oob_mgr_fetch_conn.return_value = ConnectionTarget( - did=TestConfig.test_did, - endpoint=TestConfig.test_endpoint, - recipient_keys=TestConfig.test_verkey, - sender_key=TestConfig.test_verkey, - ) - oob_invi = InvitationMessage() - - await self.manager.create_handshake_reuse_message( - oob_invi, self.test_conn_rec - ) - assert ( - len(await self.test_conn_rec.metadata_get(session, "reuse_msg_id")) - > 6 - ) - assert ( - await self.test_conn_rec.metadata_get(session, "reuse_msg_state") - == "initial" - ) - - async def test_create_handshake_reuse_msg_catch_exception(self): - async with self.profile.session() as session: - self.profile.context.update_settings({"public_invites": True}) - await self.test_conn_rec.save(session) - with async_mock.patch.object( - DIDXManager, "receive_invitation", autospec=True - ) as didx_mgr_receive_invitation, async_mock.patch( - "aries_cloudagent.protocols.out_of_band.v1_0.manager.InvitationMessage", - autospec=True, - ) as inv_message_cls, async_mock.patch.object( - OutOfBandManager, - "fetch_connection_targets", - autospec=True, - ) as oob_mgr_fetch_conn: - oob_mgr_fetch_conn.side_effect = StorageNotFoundError() - oob_invi = InvitationMessage() - with self.assertRaises(OutOfBandManagerError) as context: - await self.manager.create_handshake_reuse_message( - oob_invi, self.test_conn_rec - ) - assert "Error on creating and sending a handshake reuse message" in str( - context.exception - ) - - async def test_receive_reuse_message_existing_found(self): - async with self.profile.session() as session: - self.profile.context.update_settings({"public_invites": True}) - receipt = MessageReceipt( - recipient_did=TestConfig.test_did, - recipient_did_public=False, - ) - reuse_msg = HandshakeReuse() - reuse_msg.assign_thread_id(thid="test_123", pthid="test_123") - self.test_conn_rec.invitation_msg_id = "test_123" - self.test_conn_rec.state = ConnRecord.State.COMPLETED.rfc160 - await self.test_conn_rec.save(session) - with async_mock.patch.object( - DIDXManager, "receive_invitation", autospec=True - ) as didx_mgr_receive_invitation, async_mock.patch( - "aries_cloudagent.protocols.out_of_band.v1_0.manager.InvitationMessage", - autospec=True, - ) as inv_message_cls, async_mock.patch.object( - OutOfBandManager, - "fetch_connection_targets", - autospec=True, - ) as oob_mgr_fetch_conn, async_mock.patch.object( - ConnRecord, - "find_existing_connection", - async_mock.CoroutineMock(), - ) as oob_mgr_find_existing_conn, async_mock.patch.object( - InvitationRecord, - "retrieve_by_tag_filter", - autospec=True, - ) as retrieve_invi_rec, async_mock.patch.object( - self.profile, "notify", autospec=True - ) as mock_notify: - oob_mgr_find_existing_conn.return_value = self.test_conn_rec - oob_mgr_fetch_conn.return_value = ConnectionTarget( - did=TestConfig.test_did, - endpoint=TestConfig.test_endpoint, - recipient_keys=TestConfig.test_verkey, - sender_key=TestConfig.test_verkey, - ) - oob_invi = InvitationMessage() - retrieve_invi_rec.return_value = InvitationRecord( - invi_msg_id="test_123" - ) - await self.manager.receive_reuse_message( - reuse_msg, receipt, self.test_conn_rec - ) - mock_notify.assert_called_once() - assert ( - len( - await ConnRecord.query( - session=session, - tag_filter={"invitation_msg_id": "test_123"}, - post_filter_positive={}, - alt=True, - ) - ) - == 1 - ) - - async def test_receive_reuse_message_existing_not_found(self): - async with self.profile.session() as session: - self.profile.context.update_settings({"public_invites": True}) - receipt = MessageReceipt( - recipient_did=TestConfig.test_did, - recipient_did_public=False, - sender_did="test_did", - ) - reuse_msg = HandshakeReuse() - reuse_msg.assign_thread_id(thid="test_123", pthid="test_123") - self.test_conn_rec.invitation_msg_id = "test_123" - self.test_conn_rec.state = ConnRecord.State.REQUEST.rfc160 - await self.test_conn_rec.save(session) - with async_mock.patch.object( - DIDXManager, "receive_invitation", autospec=True - ) as didx_mgr_receive_invitation, async_mock.patch( - "aries_cloudagent.protocols.out_of_band.v1_0.manager.InvitationMessage", - autospec=True, - ) as inv_message_cls, async_mock.patch.object( - OutOfBandManager, - "fetch_connection_targets", - autospec=True, - ) as oob_mgr_fetch_conn, async_mock.patch.object( - InvitationRecord, - "retrieve_by_tag_filter", - autospec=True, - ) as retrieve_invi_rec, async_mock.patch.object( - ConnRecord, - "find_existing_connection", - async_mock.CoroutineMock(), - ) as oob_mgr_find_existing_conn, async_mock.patch.object( - self.profile, "notify", autospec=True - ) as mock_notify: - oob_mgr_find_existing_conn.return_value = None - oob_mgr_fetch_conn.return_value = ConnectionTarget( - did=TestConfig.test_did, - endpoint=TestConfig.test_endpoint, - recipient_keys=TestConfig.test_verkey, - sender_key=TestConfig.test_verkey, - ) - oob_invi = InvitationMessage() - retrieve_invi_rec.return_value = InvitationRecord( - invi_msg_id="test_123" - ) - await self.manager.receive_reuse_message( - reuse_msg, receipt, self.test_conn_rec - ) - mock_notify.assert_called_once() - assert len(self.responder.messages) == 0 - - async def test_receive_reuse_message_problem_report_logic(self): - async with self.profile.session() as session: - self.profile.context.update_settings({"public_invites": True}) - receipt = MessageReceipt( - recipient_did=TestConfig.test_did, - recipient_did_public=False, - sender_did="test_did", - ) - reuse_msg = HandshakeReuse() - reuse_msg.assign_thread_id(thid="test_123", pthid="test_123") - self.test_conn_rec.invitation_msg_id = "test_456" - self.test_conn_rec.their_did = "test_did" - self.test_conn_rec.state = ConnRecord.State.COMPLETED.rfc160 - await self.test_conn_rec.save(session) - with async_mock.patch.object( - OutOfBandManager, - "fetch_connection_targets", - autospec=True, - ) as oob_mgr_fetch_conn, async_mock.patch.object( - self.profile, "notify", autospec=True - ) as mock_notify: - oob_mgr_fetch_conn.return_value = ConnectionTarget( - did=TestConfig.test_did, - endpoint=TestConfig.test_endpoint, - recipient_keys=TestConfig.test_verkey, - sender_key=TestConfig.test_verkey, - ) - await self.manager.receive_reuse_message( - reuse_msg, receipt, self.test_conn_rec - ) - mock_notify.assert_called_once() - - async def test_receive_reuse_accepted(self): - async with self.profile.session() as session: - self.profile.context.update_settings({"public_invites": True}) - receipt = MessageReceipt( - recipient_did=TestConfig.test_did, - recipient_did_public=False, - sender_did="test_did", - ) - reuse_msg_accepted = HandshakeReuseAccept() - reuse_msg_accepted.assign_thread_id(thid="test_123", pthid="test_123") - self.test_conn_rec.invitation_msg_id = "test_123" - self.test_conn_rec.state = ConnRecord.State.COMPLETED.rfc160 - await self.test_conn_rec.save(session) - await self.test_conn_rec.metadata_set(session, "reuse_msg_id", "test_123") - await self.test_conn_rec.metadata_set(session, "reuse_msg_state", "initial") - with async_mock.patch.object( - DIDXManager, "receive_invitation", autospec=True - ) as didx_mgr_receive_invitation, async_mock.patch( - "aries_cloudagent.protocols.out_of_band.v1_0.manager.InvitationMessage", - autospec=True, - ) as inv_message_cls, async_mock.patch.object( - OutOfBandManager, - "fetch_connection_targets", - autospec=True, - ) as oob_mgr_fetch_conn, async_mock.patch.object( - self.profile, "notify", autospec=True - ) as mock_notify: - - await self.manager.receive_reuse_accepted_message( - reuse_msg_accepted, receipt, self.test_conn_rec - ) - mock_notify.assert_called_once() - assert ( - await self.test_conn_rec.metadata_get(session, "reuse_msg_state") - == "accepted" - ) - - async def test_receive_reuse_accepted(self): - async with self.profile.session() as session: - self.profile.context.update_settings({"public_invites": True}) - receipt = MessageReceipt( - recipient_did=TestConfig.test_did, - recipient_did_public=False, - sender_did="test_did", - ) - reuse_msg_accepted = HandshakeReuseAccept() - reuse_msg_accepted.assign_thread_id(thid="test_123", pthid="test_123") - self.test_conn_rec.invitation_msg_id = "test_123" - self.test_conn_rec.state = ConnRecord.State.COMPLETED.rfc160 - await self.test_conn_rec.save(session) - await self.test_conn_rec.metadata_set(session, "reuse_msg_id", "test_123") - await self.test_conn_rec.metadata_set(session, "reuse_msg_state", "initial") - with async_mock.patch.object( - DIDXManager, "receive_invitation", autospec=True - ) as didx_mgr_receive_invitation, async_mock.patch( - "aries_cloudagent.protocols.out_of_band.v1_0.manager.InvitationMessage", - autospec=True, - ) as inv_message_cls, async_mock.patch.object( - OutOfBandManager, - "fetch_connection_targets", - autospec=True, - ) as oob_mgr_fetch_conn, async_mock.patch.object( - self.profile, "notify", autospec=True - ) as mock_notify: - - await self.manager.receive_reuse_accepted_message( - reuse_msg_accepted, receipt, self.test_conn_rec - ) - mock_notify.assert_called_once() - assert ( - await self.test_conn_rec.metadata_get(session, "reuse_msg_state") - == "accepted" - ) - - async def test_receive_reuse_accepted_invalid_conn(self): - self.profile.context.update_settings({"public_invites": True}) - receipt = MessageReceipt( - recipient_did=TestConfig.test_did, - recipient_did_public=False, - sender_did="test_did", - ) - reuse_msg_accepted = HandshakeReuseAccept() - reuse_msg_accepted.assign_thread_id(thid="test_123", pthid="test_123") - test_invalid_conn = ConnRecord( - my_did="Test", - their_did="Test", - invitation_msg_id="test_456", - connection_id="12345678-0123-4567-1234-567812345678", - ) - with async_mock.patch.object( - DIDXManager, "receive_invitation", autospec=True - ) as didx_mgr_receive_invitation, async_mock.patch( - "aries_cloudagent.protocols.out_of_band.v1_0.manager.InvitationMessage", - autospec=True, - ) as inv_message_cls, async_mock.patch.object( - OutOfBandManager, - "fetch_connection_targets", - autospec=True, - ) as oob_mgr_fetch_conn, async_mock.patch.object( - self.profile, "notify", autospec=True - ) as mock_notify: - with self.assertRaises(OutOfBandManagerError) as context: - await self.manager.receive_reuse_accepted_message( - reuse_msg_accepted, receipt, test_invalid_conn - ) - mock_notify.assert_called_once() - assert "Error processing reuse accepted message" in str(context.exception) - - async def test_receive_reuse_accepted_message_catch_exception(self): - async with self.profile.session() as session: - self.profile.context.update_settings({"public_invites": True}) - receipt = MessageReceipt( - recipient_did=TestConfig.test_did, - recipient_did_public=False, - sender_did="test_did", - ) - reuse_msg_accepted = HandshakeReuseAccept() - reuse_msg_accepted.assign_thread_id(thid="test_123", pthid="test_123") - self.test_conn_rec.invitation_msg_id = "test_123" - self.test_conn_rec.state = ConnRecord.State.COMPLETED.rfc160 - await self.test_conn_rec.save(session) - await self.test_conn_rec.metadata_set(session, "reuse_msg_id", "test_123") - await self.test_conn_rec.metadata_set(session, "reuse_msg_state", "initial") - - with async_mock.patch.object( - self.test_conn_rec, - "metadata_set", - async_mock.CoroutineMock(side_effect=StorageNotFoundError), - ), async_mock.patch.object( - self.profile, "notify", autospec=True - ) as mock_notify: - with self.assertRaises(OutOfBandManagerError) as context: - await self.manager.receive_reuse_accepted_message( - reuse_msg_accepted, receipt, self.test_conn_rec - ) - mock_notify.assert_called_once() - assert "Error processing reuse accepted message" in str( - context.exception - ) - - async def test_problem_report_received_not_active(self): - async with self.profile.session() as session: - self.profile.context.update_settings({"public_invites": True}) - receipt = MessageReceipt( - recipient_did=TestConfig.test_did, - recipient_did_public=False, - sender_did="test_did", - ) - problem_report = ProblemReport( - description={ - "en": "test", - "code": ProblemReportReason.EXISTING_CONNECTION_NOT_ACTIVE.value, - } - ) - problem_report.assign_thread_id(thid="test_123", pthid="test_123") - self.test_conn_rec.invitation_msg_id = "test_123" - self.test_conn_rec.state = ConnRecord.State.COMPLETED.rfc160 - await self.test_conn_rec.save(session) - await self.test_conn_rec.metadata_set(session, "reuse_msg_id", "test_123") - await self.test_conn_rec.metadata_set(session, "reuse_msg_state", "initial") - with async_mock.patch.object( - DIDXManager, "receive_invitation", autospec=True - ) as didx_mgr_receive_invitation, async_mock.patch( - "aries_cloudagent.protocols.out_of_band.v1_0.manager.InvitationMessage", - autospec=True, - ) as inv_message_cls, async_mock.patch.object( - OutOfBandManager, - "fetch_connection_targets", - autospec=True, - ) as oob_mgr_fetch_conn: - - await self.manager.receive_problem_report( - problem_report, receipt, self.test_conn_rec - ) - assert ( - await self.test_conn_rec.metadata_get(session, "reuse_msg_state") - == "not_accepted" - ) - - async def test_problem_report_received_not_exists(self): - async with self.profile.session() as session: - self.profile.context.update_settings({"public_invites": True}) - receipt = MessageReceipt( - recipient_did=TestConfig.test_did, - recipient_did_public=False, - sender_did="test_did", - ) - problem_report = ProblemReport( - description={ - "en": "test", - "code": ProblemReportReason.NO_EXISTING_CONNECTION.value, - } - ) - problem_report.assign_thread_id(thid="test_123", pthid="test_123") - self.test_conn_rec.invitation_msg_id = "test_123" - self.test_conn_rec.state = ConnRecord.State.COMPLETED.rfc160 - await self.test_conn_rec.save(session) - await self.test_conn_rec.metadata_set(session, "reuse_msg_id", "test_123") - await self.test_conn_rec.metadata_set(session, "reuse_msg_state", "initial") - with async_mock.patch.object( - DIDXManager, "receive_invitation", autospec=True - ) as didx_mgr_receive_invitation, async_mock.patch( - "aries_cloudagent.protocols.out_of_band.v1_0.manager.InvitationMessage", - autospec=True, - ) as inv_message_cls, async_mock.patch.object( - OutOfBandManager, - "fetch_connection_targets", - autospec=True, - ) as oob_mgr_fetch_conn: - - await self.manager.receive_problem_report( - problem_report, receipt, self.test_conn_rec - ) - assert ( - await self.test_conn_rec.metadata_get(session, "reuse_msg_state") - == "not_accepted" - ) - - async def test_problem_report_received_invalid_conn(self): - self.profile.context.update_settings({"public_invites": True}) - receipt = MessageReceipt( - recipient_did=TestConfig.test_did, - recipient_did_public=False, - sender_did="test_did", - ) - problem_report = ProblemReport( - description={ - "en": "test", - "code": ProblemReportReason.NO_EXISTING_CONNECTION.value, - } - ) - problem_report.assign_thread_id(thid="test_123", pthid="test_123") - test_invalid_conn = ConnRecord( - my_did="Test", - their_did="Test", - invitation_msg_id="test_456", - connection_id="12345678-0123-4567-1234-567812345678", - ) - with async_mock.patch.object( - DIDXManager, "receive_invitation", autospec=True - ) as didx_mgr_receive_invitation, async_mock.patch( - "aries_cloudagent.protocols.out_of_band.v1_0.manager.InvitationMessage", - autospec=True, - ) as inv_message_cls, async_mock.patch.object( - OutOfBandManager, - "fetch_connection_targets", - autospec=True, - ) as oob_mgr_fetch_conn: - - with self.assertRaises(OutOfBandManagerError) as context: - await self.manager.receive_problem_report( - problem_report, receipt, test_invalid_conn - ) - assert "Error processing problem report message" in str(context.exception) - - async def test_existing_conn_record_public_did(self): - async with self.profile.session() as session: - self.profile.context.update_settings({"public_invites": True}) - test_exist_conn = ConnRecord( - my_did=TestConfig.test_did, - their_did=TestConfig.test_target_did, - their_public_did=TestConfig.test_target_did, - invitation_msg_id="12345678-0123-4567-1234-567812345678", - their_role=ConnRecord.Role.REQUESTER, - ) - await test_exist_conn.save(session) - await test_exist_conn.metadata_set(session, "reuse_msg_state", "initial") - await test_exist_conn.metadata_set(session, "reuse_msg_id", "test_123") - receipt = MessageReceipt( - recipient_did=TestConfig.test_did, - recipient_did_public=False, - sender_did=TestConfig.test_target_did, - ) - - with async_mock.patch.object( - DIDXManager, "receive_invitation", autospec=True - ) as didx_mgr_receive_invitation, async_mock.patch( - "aries_cloudagent.protocols.out_of_band.v1_0.manager.InvitationMessage", - autospec=True, - ) as inv_message_cls, async_mock.patch.object( - OutOfBandManager, - "fetch_connection_targets", - autospec=True, - ) as oob_mgr_fetch_conn, async_mock.patch.object( - ConnRecord, - "find_existing_connection", - async_mock.CoroutineMock(), - ) as oob_mgr_find_existing_conn, async_mock.patch.object( - OutOfBandManager, - "check_reuse_msg_state", - autospec=True, - ) as oob_mgr_check_reuse_state, async_mock.patch.object( - OutOfBandManager, - "create_handshake_reuse_message", - autospec=True, - ) as oob_mgr_create_reuse_msg, async_mock.patch.object( - OutOfBandManager, - "receive_reuse_message", - autospec=True, - ) as oob_mgr_receive_reuse_msg, async_mock.patch.object( - OutOfBandManager, - "receive_reuse_accepted_message", - autospec=True, - ) as oob_mgr_receive_accept_msg, async_mock.patch.object( - OutOfBandManager, - "receive_problem_report", - autospec=True, - ) as oob_mgr_receive_problem_report: - oob_mgr_find_existing_conn.return_value = test_exist_conn - oob_mgr_check_reuse_state.return_value = None - oob_mgr_create_reuse_msg.return_value = None - oob_mgr_receive_reuse_msg.return_value = None - oob_mgr_receive_accept_msg.return_value = None - oob_mgr_receive_problem_report.return_value = None - await test_exist_conn.metadata_set( - session, "reuse_msg_state", "accepted" - ) - mock_oob_invi = async_mock.MagicMock( - handshake_protocols=[ - pfx.qualify(HSProto.RFC23.name) for pfx in DIDCommPrefix - ], - services=[TestConfig.test_target_did], - requests_attach=[], - ) - inv_message_cls.deserialize.return_value = mock_oob_invi - - result = await self.manager.receive_invitation( - mock_oob_invi, use_existing_connection=True - ) - retrieved_conn_records = await ConnRecord.query( - session=session, - tag_filter={ - "invitation_msg_id": "12345678-0123-4567-1234-567812345678" - }, - post_filter_positive={}, - alt=True, - ) - assert ( - await retrieved_conn_records[0].metadata_get( - session, "reuse_msg_id" - ) - is None - ) - assert ( - await retrieved_conn_records[0].metadata_get( - session, "reuse_msg_state" - ) - is None - ) - assert result.connection_id == retrieved_conn_records[0].connection_id - - async def test_existing_conn_record_public_did_not_accepted(self): - async with self.profile.session() as session: - self.profile.context.update_settings({"public_invites": True}) - test_exist_conn = ConnRecord( - my_did=TestConfig.test_did, - their_did="did:sov:LjgpST2rjsoxYegQDRm7EL", - their_public_did="did:sov:LjgpST2rjsoxYegQDRm7EL", - invitation_msg_id="12345678-0123-4567-1234-567812345678", - their_role=ConnRecord.Role.REQUESTER, - ) - await test_exist_conn.save(session) - await test_exist_conn.metadata_set(session, "reuse_msg_id", "test_123") - await test_exist_conn.metadata_set(session, "reuse_msg_state", "initial") - - test_new_conn = ConnRecord( - my_did=TestConfig.test_did, - their_did="did:sov:LjgpST2rjsoxYegQDRm7EL", - their_public_did="did:sov:LjgpST2rjsoxYegQDRm7EL", - invitation_msg_id="12345678-0123-4567-1234-1234545454487", - their_role=ConnRecord.Role.REQUESTER, - ) - - receipt = MessageReceipt( - recipient_did=TestConfig.test_did, - recipient_did_public=False, - sender_did=TestConfig.test_target_did, - ) - - with async_mock.patch.object( - DIDXManager, "receive_invitation", autospec=True - ) as didx_mgr_receive_invitation, async_mock.patch( - "aries_cloudagent.protocols.out_of_band.v1_0.manager.InvitationMessage", - autospec=True, - ) as inv_message_cls, async_mock.patch.object( - OutOfBandManager, - "fetch_connection_targets", - autospec=True, - ) as oob_mgr_fetch_conn, async_mock.patch.object( - ConnRecord, - "find_existing_connection", - async_mock.CoroutineMock(), - ) as oob_mgr_find_existing_conn, async_mock.patch.object( - OutOfBandManager, - "check_reuse_msg_state", - autospec=True, - ) as oob_mgr_check_reuse_state, async_mock.patch.object( - OutOfBandManager, - "create_handshake_reuse_message", - autospec=True, - ) as oob_mgr_create_reuse_msg, async_mock.patch.object( - OutOfBandManager, - "receive_reuse_message", - autospec=True, - ) as oob_mgr_receive_reuse_msg, async_mock.patch.object( - OutOfBandManager, - "receive_reuse_accepted_message", - autospec=True, - ) as oob_mgr_receive_accept_msg, async_mock.patch.object( - OutOfBandManager, - "receive_problem_report", - autospec=True, - ) as oob_mgr_receive_problem_report: - oob_mgr_find_existing_conn.return_value = test_exist_conn - oob_mgr_check_reuse_state.return_value = None - oob_mgr_create_reuse_msg.return_value = None - oob_mgr_receive_reuse_msg.return_value = None - oob_mgr_receive_accept_msg.return_value = None - oob_mgr_receive_problem_report.return_value = None - await test_exist_conn.metadata_set( - session, "reuse_msg_state", "not_accepted" - ) - didx_mgr_receive_invitation.return_value = test_new_conn - mock_oob_invi = async_mock.MagicMock( - handshake_protocols=[ - pfx.qualify(HSProto.RFC23.name) for pfx in DIDCommPrefix - ], - services=[TestConfig.test_target_did], - requests_attach=[], - ) - inv_message_cls.deserialize.return_value = mock_oob_invi - - result = await self.manager.receive_invitation( - mock_oob_invi, use_existing_connection=True - ) - retrieved_conn_records = await ConnRecord.query( - session=session, - tag_filter={ - "invitation_msg_id": "12345678-0123-4567-1234-567812345678" - }, - post_filter_positive={}, - alt=True, - ) - assert ( - await retrieved_conn_records[0].metadata_get( - session, "reuse_msg_state" - ) - == "not_accepted" - ) - assert result.connection_id != retrieved_conn_records[0].connection_id - - async def test_existing_conn_record_public_did_inverse_cases(self): - async with self.profile.session() as session: - self.profile.context.update_settings({"public_invites": True}) - test_exist_conn = ConnRecord( - my_did=TestConfig.test_did, - their_did=TestConfig.test_target_did, - their_public_did=TestConfig.test_target_did, - invitation_msg_id="12345678-0123-4567-1234-567812345678", - their_role=ConnRecord.Role.REQUESTER, - ) - await self.test_conn_rec.save(session) - await test_exist_conn.save(session) - await test_exist_conn.metadata_set(session, "reuse_msg_state", "initial") - await test_exist_conn.metadata_set(session, "reuse_msg_id", "test_123") - - with async_mock.patch.object( - DIDXManager, "receive_invitation", autospec=True - ) as didx_mgr_receive_invitation, async_mock.patch( - "aries_cloudagent.protocols.out_of_band.v1_0.manager.InvitationMessage", - autospec=True, - ) as inv_message_cls, async_mock.patch.object( - OutOfBandManager, - "fetch_connection_targets", - autospec=True, - ) as oob_mgr_fetch_conn, async_mock.patch.object( - ConnRecord, - "find_existing_connection", - async_mock.CoroutineMock(), - ) as oob_mgr_find_existing_conn, async_mock.patch.object( - OutOfBandManager, - "check_reuse_msg_state", - autospec=True, - ) as oob_mgr_check_reuse_state: - oob_mgr_find_existing_conn.return_value = test_exist_conn - didx_mgr_receive_invitation.return_value = self.test_conn_rec - mock_oob_invi = async_mock.MagicMock( - handshake_protocols=[ - pfx.qualify(HSProto.RFC23.name) for pfx in DIDCommPrefix - ], - services=[TestConfig.test_target_did], - requests_attach=[], - ) - inv_message_cls.deserialize.return_value = mock_oob_invi - - result = await self.manager.receive_invitation( - mock_oob_invi, use_existing_connection=False - ) - retrieved_conn_records = await ConnRecord.query( - session=session, - tag_filter={ - "invitation_msg_id": "12345678-0123-4567-1234-567812345678" - }, - post_filter_positive={}, - alt=True, - ) - assert result.connection_id != retrieved_conn_records[0].connection_id - - async def test_existing_conn_record_public_did_timeout(self): - async with self.profile.session() as session: - self.profile.context.update_settings({"public_invites": True}) - test_exist_conn = ConnRecord( - my_did=TestConfig.test_did, - their_did=TestConfig.test_target_did, - their_public_did=TestConfig.test_target_did, - invitation_msg_id="12345678-0123-4567-1234-567812345678", - their_role=ConnRecord.Role.REQUESTER, - ) - await test_exist_conn.save(session) - await test_exist_conn.metadata_set(session, "reuse_msg_state", "initial") - await test_exist_conn.metadata_set(session, "reuse_msg_id", "test_123") - receipt = MessageReceipt( - recipient_did=TestConfig.test_did, - recipient_did_public=False, - sender_did=TestConfig.test_target_did, - ) - - with async_mock.patch.object( - DIDXManager, "receive_invitation", autospec=True - ) as didx_mgr_receive_invitation, async_mock.patch( - "aries_cloudagent.protocols.out_of_band.v1_0.manager.InvitationMessage", - autospec=True, - ) as inv_message_cls, async_mock.patch.object( - OutOfBandManager, - "fetch_connection_targets", - autospec=True, - ) as oob_mgr_fetch_conn, async_mock.patch.object( - ConnRecord, - "find_existing_connection", - async_mock.CoroutineMock(), - ) as oob_mgr_find_existing_conn, async_mock.patch.object( - OutOfBandManager, - "check_reuse_msg_state", - autospec=True, - ) as oob_mgr_check_reuse_state, async_mock.patch.object( - self.profile, "notify", autospec=True - ) as mock_notify: - oob_mgr_find_existing_conn.return_value = test_exist_conn - oob_mgr_check_reuse_state.side_effect = asyncio.TimeoutError - mock_oob_invi = async_mock.MagicMock( - handshake_protocols=[ - pfx.qualify(HSProto.RFC23.name) for pfx in DIDCommPrefix - ], - services=[TestConfig.test_target_did], - requests_attach=[], - ) - inv_message_cls.deserialize.return_value = mock_oob_invi - - result = await self.manager.receive_invitation( - mock_oob_invi, use_existing_connection=True - ) - mock_notify.assert_called() - retrieved_conn_records = await ConnRecord.query( - session=session, - tag_filter={"their_public_did": TestConfig.test_target_did}, - ) - assert ( - retrieved_conn_records[0].state == ConnRecord.State.ABANDONED.rfc160 - ) - - async def test_existing_conn_record_public_did_timeout_no_handshake_protocol(self): - async with self.profile.session() as session: - self.profile.context.update_settings({"public_invites": True}) - test_exist_conn = ConnRecord( - my_did=TestConfig.test_did, - their_did=TestConfig.test_target_did, - their_public_did=TestConfig.test_target_did, - invitation_msg_id="12345678-0123-4567-1234-567812345678", - their_role=ConnRecord.Role.REQUESTER, - ) - await test_exist_conn.save(session) - await test_exist_conn.metadata_set(session, "reuse_msg_state", "initial") - await test_exist_conn.metadata_set(session, "reuse_msg_id", "test_123") - receipt = MessageReceipt( - recipient_did=TestConfig.test_did, - recipient_did_public=False, - sender_did=TestConfig.test_target_did, - ) - - with async_mock.patch.object( - DIDXManager, "receive_invitation", autospec=True - ) as didx_mgr_receive_invitation, async_mock.patch( - "aries_cloudagent.protocols.out_of_band.v1_0.manager.InvitationMessage", - autospec=True, - ) as inv_message_cls, async_mock.patch.object( - OutOfBandManager, - "fetch_connection_targets", - autospec=True, - ) as oob_mgr_fetch_conn, async_mock.patch.object( - ConnRecord, - "find_existing_connection", - async_mock.CoroutineMock(), - ) as oob_mgr_find_existing_conn: - oob_mgr_find_existing_conn.return_value = test_exist_conn - mock_oob_invi = async_mock.MagicMock( - handshake_protocols=[], - services=[TestConfig.test_target_did], - requests_attach=[ - {"having": "attachment", "is": "no", "good": "here"} - ], - ) - inv_message_cls.deserialize.return_value = mock_oob_invi - with self.assertRaises(OutOfBandManagerError) as context: - result = await self.manager.receive_invitation( - mock_oob_invi, use_existing_connection=False - ) - assert "No existing connection exists and " in str(context.exception) - - async def test_req_v1_attach_presentation_existing_conn_no_auto_present(self): - async with self.profile.session() as session: - self.profile.context.update_settings({"public_invites": True}) - test_exist_conn = ConnRecord( - my_did=TestConfig.test_did, - their_did=TestConfig.test_target_did, - their_public_did=TestConfig.test_target_did, - invitation_msg_id="12345678-0123-4567-1234-567812345678", - their_role=ConnRecord.Role.REQUESTER, - ) - await test_exist_conn.save(session) - await test_exist_conn.metadata_set(session, "reuse_msg_state", "initial") - await test_exist_conn.metadata_set(session, "reuse_msg_id", "test_123") - receipt = MessageReceipt( - recipient_did=TestConfig.test_did, - recipient_did_public=False, - sender_did=TestConfig.test_target_did, - ) - - exchange_rec = V10PresentationExchange() - - with async_mock.patch.object( - DIDXManager, "receive_invitation", autospec=True - ) as didx_mgr_receive_invitation, async_mock.patch.object( - PresentationManager, "receive_request", autospec=True - ) as pres_mgr_receive_request, async_mock.patch( - "aries_cloudagent.protocols.out_of_band.v1_0.manager.InvitationMessage", - autospec=True, - ) as inv_message_cls, async_mock.patch.object( - OutOfBandManager, - "fetch_connection_targets", - autospec=True, - ) as oob_mgr_fetch_conn, async_mock.patch.object( - ConnRecord, - "find_existing_connection", - async_mock.CoroutineMock(), - ) as oob_mgr_find_existing_conn, async_mock.patch.object( - OutOfBandManager, - "check_reuse_msg_state", - autospec=True, - ) as oob_mgr_check_reuse_state, async_mock.patch.object( - OutOfBandManager, - "create_handshake_reuse_message", - autospec=True, - ) as oob_mgr_create_reuse_msg, async_mock.patch.object( - OutOfBandManager, - "receive_reuse_message", - autospec=True, - ) as oob_mgr_receive_reuse_msg, async_mock.patch.object( - OutOfBandManager, - "receive_reuse_accepted_message", - autospec=True, - ) as oob_mgr_receive_accept_msg, async_mock.patch.object( - OutOfBandManager, - "receive_problem_report", - autospec=True, - ) as oob_mgr_receive_problem_report: - oob_mgr_find_existing_conn.return_value = test_exist_conn - pres_mgr_receive_request.return_value = exchange_rec - - mock_oob_invi = async_mock.MagicMock( - handshake_protocols=[ - pfx.qualify(HSProto.RFC23.name) for pfx in DIDCommPrefix - ], - services=[TestConfig.test_target_did], - requests_attach=[ - AttachDecorator.deserialize(TestConfig.req_attach_v1) - ], - ) - - inv_message_cls.deserialize.return_value = mock_oob_invi - - with self.assertRaises(OutOfBandManagerError) as context: - result = await self.manager.receive_invitation( - mock_oob_invi, use_existing_connection=True - ) - assert "Configuration sets auto_present false" in str(context.exception) - - async def test_req_v1_attach_presentation_existing_conn_auto_present_pres_msg(self): - async with self.profile.session() as session: - self.profile.context.update_settings({"public_invites": True}) - self.profile.context.update_settings( - {"debug.auto_respond_presentation_request": True} - ) - test_exist_conn = ConnRecord( - my_did=TestConfig.test_did, - their_did=TestConfig.test_target_did, - their_public_did=TestConfig.test_target_did, - invitation_msg_id="12345678-0123-4567-1234-567812345678", - their_role=ConnRecord.Role.REQUESTER, - ) - await test_exist_conn.save(session) - await test_exist_conn.metadata_set(session, "reuse_msg_state", "initial") - await test_exist_conn.metadata_set(session, "reuse_msg_id", "test_123") - receipt = MessageReceipt( - recipient_did=TestConfig.test_did, - recipient_did_public=False, - sender_did=TestConfig.test_target_did, - ) - - exchange_rec = V10PresentationExchange() - exchange_rec.auto_present = True - exchange_rec.presentation_request = TestConfig.INDY_PROOF_REQ - - with async_mock.patch.object( - DIDXManager, - "receive_invitation", - autospec=True, - ) as didx_mgr_receive_invitation, async_mock.patch.object( - PresentationManager, - "receive_request", - autospec=True, - ) as pres_mgr_receive_request, async_mock.patch( - "aries_cloudagent.protocols.out_of_band.v1_0.manager.InvitationMessage", - autospec=True, - ) as inv_message_cls, async_mock.patch.object( - OutOfBandManager, - "fetch_connection_targets", - autospec=True, - ) as oob_mgr_fetch_conn, async_mock.patch.object( - ConnRecord, - "find_existing_connection", - async_mock.CoroutineMock(), - ) as oob_mgr_find_existing_conn, async_mock.patch.object( - OutOfBandManager, - "check_reuse_msg_state", - autospec=True, - ) as oob_mgr_check_reuse_state, async_mock.patch.object( - OutOfBandManager, - "create_handshake_reuse_message", - autospec=True, - ) as oob_mgr_create_reuse_msg, async_mock.patch.object( - OutOfBandManager, - "receive_reuse_message", - autospec=True, - ) as oob_mgr_receive_reuse_msg, async_mock.patch.object( - OutOfBandManager, - "receive_reuse_accepted_message", - autospec=True, - ) as oob_mgr_receive_accept_msg, async_mock.patch.object( - OutOfBandManager, - "receive_problem_report", - autospec=True, - ) as oob_mgr_receive_problem_report, async_mock.patch.object( - PresentationManager, - "create_presentation", - autospec=True, - ) as pres_mgr_create_presentation: - oob_mgr_find_existing_conn.return_value = test_exist_conn - pres_mgr_receive_request.return_value = exchange_rec - pres_mgr_create_presentation.return_value = ( - exchange_rec, - Presentation( - presentations_attach=[ - AttachDecorator.data_base64({"bogus": "proof"}) - ] - ), - ) - holder = async_mock.MagicMock(IndyHolder, autospec=True) - get_creds = async_mock.CoroutineMock( - return_value=( - { - "cred_info": {"referent": "dummy_reft"}, - "attrs": { - "player": "Richie Knucklez", - "screenCapture": "aW1hZ2luZSBhIHNjcmVlbiBjYXB0dXJl", - "highScore": "1234560", - }, - }, - ) - ) - holder.get_credentials_for_presentation_request_by_referent = get_creds - holder.create_credential_request = async_mock.CoroutineMock( - return_value=( - json.dumps(TestConfig.indy_cred_req), - json.dumps(TestConfig.cred_req_meta), - ) - ) - self.profile.context.injector.bind_instance(IndyHolder, holder) - mock_oob_invi = async_mock.MagicMock( - handshake_protocols=[ - pfx.qualify(HSProto.RFC23.name) for pfx in DIDCommPrefix - ], - services=[TestConfig.test_target_did], - requests_attach=[ - AttachDecorator.deserialize(TestConfig.req_attach_v1) - ], - ) - - inv_message_cls.deserialize.return_value = mock_oob_invi - - conn_rec = await self.manager.receive_invitation( - mock_oob_invi, use_existing_connection=True - ) - assert conn_rec is not None - - async def test_req_v1_attach_pres_catch_value_error(self): - async with self.profile.session() as session: - self.profile.context.update_settings({"public_invites": True}) - self.profile.context.update_settings( - {"debug.auto_respond_presentation_request": True} - ) - test_exist_conn = ConnRecord( - my_did=TestConfig.test_did, - their_did=TestConfig.test_target_did, - their_public_did=TestConfig.test_target_did, - invitation_msg_id="12345678-0123-4567-1234-567812345678", - their_role=ConnRecord.Role.REQUESTER, - ) - await test_exist_conn.save(session) - await test_exist_conn.metadata_set(session, "reuse_msg_state", "initial") - await test_exist_conn.metadata_set(session, "reuse_msg_id", "test_123") - receipt = MessageReceipt( - recipient_did=TestConfig.test_did, - recipient_did_public=False, - sender_did=TestConfig.test_target_did, - ) - - exchange_rec = V10PresentationExchange() - exchange_rec.auto_present = True - exchange_rec.presentation_request = TestConfig.INDY_PROOF_REQ - - with async_mock.patch.object( - DIDXManager, - "receive_invitation", - autospec=True, - ) as didx_mgr_receive_invitation, async_mock.patch.object( - PresentationManager, - "receive_request", - autospec=True, - ) as pres_mgr_receive_request, async_mock.patch( - "aries_cloudagent.protocols.out_of_band.v1_0.manager.InvitationMessage", - autospec=True, - ) as inv_message_cls, async_mock.patch.object( - OutOfBandManager, - "fetch_connection_targets", - autospec=True, - ) as oob_mgr_fetch_conn, async_mock.patch.object( - ConnRecord, - "find_existing_connection", - async_mock.CoroutineMock(), - ) as oob_mgr_find_existing_conn, async_mock.patch.object( - OutOfBandManager, - "check_reuse_msg_state", - autospec=True, - ) as oob_mgr_check_reuse_state, async_mock.patch.object( - OutOfBandManager, - "create_handshake_reuse_message", - autospec=True, - ) as oob_mgr_create_reuse_msg, async_mock.patch.object( - OutOfBandManager, - "receive_reuse_message", - autospec=True, - ) as oob_mgr_receive_reuse_msg, async_mock.patch.object( - OutOfBandManager, - "receive_reuse_accepted_message", - autospec=True, - ) as oob_mgr_receive_accept_msg, async_mock.patch.object( - OutOfBandManager, - "receive_problem_report", - autospec=True, - ) as oob_mgr_receive_problem_report, async_mock.patch.object( - PresentationManager, - "create_presentation", - autospec=True, - ) as pres_mgr_create_presentation: - oob_mgr_find_existing_conn.return_value = test_exist_conn - pres_mgr_receive_request.return_value = exchange_rec - pres_mgr_create_presentation.return_value = ( - exchange_rec, - Presentation(comment="this is test"), - ) - holder = async_mock.MagicMock(IndyHolder, autospec=True) - get_creds = async_mock.CoroutineMock(return_value=()) - holder.get_credentials_for_presentation_request_by_referent = get_creds - holder.create_credential_request = async_mock.CoroutineMock( - return_value=( - json.dumps(TestConfig.indy_cred_req), - json.dumps(TestConfig.cred_req_meta), - ) - ) - self.profile.context.injector.bind_instance(IndyHolder, holder) - mock_oob_invi = async_mock.MagicMock( - handshake_protocols=[ - pfx.qualify(HSProto.RFC23.name) for pfx in DIDCommPrefix - ], - services=[TestConfig.test_target_did], - requests_attach=[ - AttachDecorator.deserialize(TestConfig.req_attach_v1) - ], - ) - - inv_message_cls.deserialize.return_value = mock_oob_invi - with self.assertRaises(OutOfBandManagerError) as context: - await self.manager.receive_invitation( - mock_oob_invi, use_existing_connection=True - ) - assert "Cannot auto-respond" in str(context.exception) - - async def test_req_v2_attach_presentation_existing_conn_no_auto_present(self): - async with self.profile.session() as session: - self.profile.context.update_settings({"public_invites": True}) - test_exist_conn = ConnRecord( - my_did=TestConfig.test_did, - their_did=TestConfig.test_target_did, - their_public_did=TestConfig.test_target_did, - invitation_msg_id="12345678-0123-4567-1234-567812345678", - their_role=ConnRecord.Role.REQUESTER, - ) - await test_exist_conn.save(session) - await test_exist_conn.metadata_set(session, "reuse_msg_state", "initial") - await test_exist_conn.metadata_set(session, "reuse_msg_id", "test_123") - receipt = MessageReceipt( - recipient_did=TestConfig.test_did, - recipient_did_public=False, - sender_did=TestConfig.test_target_did, - ) - - px2_rec = test_module.V20PresExRecord() - - with async_mock.patch.object( - DIDXManager, "receive_invitation", autospec=True - ) as didx_mgr_receive_invitation, async_mock.patch.object( - V20PresManager, "receive_pres_request", autospec=True - ) as pres_mgr_receive_pres_req, async_mock.patch( - "aries_cloudagent.protocols.out_of_band.v1_0.manager.InvitationMessage", - autospec=True, - ) as inv_message_cls, async_mock.patch.object( - OutOfBandManager, - "fetch_connection_targets", - autospec=True, - ) as oob_mgr_fetch_conn, async_mock.patch.object( - ConnRecord, - "find_existing_connection", - async_mock.CoroutineMock(), - ) as oob_mgr_find_existing_conn, async_mock.patch.object( - OutOfBandManager, - "check_reuse_msg_state", - autospec=True, - ) as oob_mgr_check_reuse_state, async_mock.patch.object( - OutOfBandManager, - "create_handshake_reuse_message", - autospec=True, - ) as oob_mgr_create_reuse_msg, async_mock.patch.object( - OutOfBandManager, - "receive_reuse_message", - autospec=True, - ) as oob_mgr_receive_reuse_msg, async_mock.patch.object( - OutOfBandManager, - "receive_reuse_accepted_message", - autospec=True, - ) as oob_mgr_receive_accept_msg, async_mock.patch.object( - OutOfBandManager, - "receive_problem_report", - autospec=True, - ) as oob_mgr_receive_problem_report: - oob_mgr_find_existing_conn.return_value = test_exist_conn - pres_mgr_receive_pres_req.return_value = px2_rec - - mock_oob_invi = async_mock.MagicMock( - handshake_protocols=[ - pfx.qualify(HSProto.RFC23.name) for pfx in DIDCommPrefix - ], - services=[TestConfig.test_target_did], - requests_attach=[ - AttachDecorator.deserialize(TestConfig.req_attach_v2) - ], - ) - - inv_message_cls.deserialize.return_value = mock_oob_invi - - with self.assertRaises(OutOfBandManagerError) as context: - await self.manager.receive_invitation( - mock_oob_invi, use_existing_connection=True - ) - assert ( - "Configuration set auto_present false: cannot respond automatically to presentation requests" - == str(context.exception) - ) - - async def test_req_v2_attach_presentation_existing_conn_auto_present_pres_msg(self): - async with self.profile.session() as session: - self.profile.context.update_settings({"public_invites": True}) - self.profile.context.update_settings( - {"debug.auto_respond_presentation_request": True} - ) - test_exist_conn = ConnRecord( - my_did=TestConfig.test_did, - their_did=TestConfig.test_target_did, - their_public_did=TestConfig.test_target_did, - invitation_msg_id="12345678-0123-4567-1234-567812345678", - their_role=ConnRecord.Role.REQUESTER, + assert ( + "Invitation must specify handshake_protocols, requests_attach, or both" + in err.exception.message + ) + + async def test_existing_conn_record_public_did(self): + self.profile.context.update_settings({"public_invites": True}) + + test_exist_conn = ConnRecord( + connection_id="connection_id", + my_did=TestConfig.test_did, + their_did=TestConfig.test_target_did, + their_public_did=TestConfig.test_target_did, + invitation_msg_id="12345678-0123-4567-1234-567812345678", + their_role=ConnRecord.Role.REQUESTER, + ) + + with async_mock.patch.object( + ConnRecord, + "find_existing_connection", + async_mock.CoroutineMock(), + ) as oob_mgr_find_existing_conn, async_mock.patch.object( + OobRecord, "save", async_mock.CoroutineMock() + ) as oob_record_save, async_mock.patch.object( + OobRecord, "retrieve_by_id", async_mock.CoroutineMock() + ) as oob_record_retrieve_by_id, async_mock.patch.object( + OutOfBandManager, "fetch_connection_targets", autospec=True + ) as oob_mgr_fetch_conn: + oob_mgr_find_existing_conn.return_value = test_exist_conn + oob_mgr_fetch_conn.return_value = [] + oob_invitation = InvitationMessage( + handshake_protocols=[ + pfx.qualify(HSProto.RFC23.name) for pfx in DIDCommPrefix + ], + services=[TestConfig.test_target_did], + requests_attach=[], ) - await test_exist_conn.save(session) - await test_exist_conn.metadata_set(session, "reuse_msg_state", "initial") - await test_exist_conn.metadata_set(session, "reuse_msg_id", "test_123") - receipt = MessageReceipt( - recipient_did=TestConfig.test_did, - recipient_did_public=False, - sender_did=TestConfig.test_target_did, + + oob_record_retrieve_by_id.return_value = async_mock.MagicMock( + state=OobRecord.STATE_ACCEPTED ) - px2_rec = test_module.V20PresExRecord( - auto_present=True, - pres_request=TestConfig.PRES_REQ_V2.serialize(), + result = await self.manager.receive_invitation( + oob_invitation, use_existing_connection=True ) - with async_mock.patch.object( - DIDXManager, - "receive_invitation", - autospec=True, - ) as didx_mgr_receive_invitation, async_mock.patch.object( - V20PresManager, - "receive_pres_request", - autospec=True, - ) as pres_mgr_receive_pres_req, async_mock.patch( - "aries_cloudagent.protocols.out_of_band.v1_0.manager.InvitationMessage", - autospec=True, - ) as inv_message_cls, async_mock.patch.object( - OutOfBandManager, - "fetch_connection_targets", - autospec=True, - ) as oob_mgr_fetch_conn, async_mock.patch.object( - ConnRecord, - "find_existing_connection", - async_mock.CoroutineMock(), - ) as oob_mgr_find_existing_conn, async_mock.patch.object( - OutOfBandManager, - "check_reuse_msg_state", - autospec=True, - ) as oob_mgr_check_reuse_state, async_mock.patch.object( - OutOfBandManager, - "create_handshake_reuse_message", - autospec=True, - ) as oob_mgr_create_reuse_msg, async_mock.patch.object( - OutOfBandManager, - "receive_reuse_message", - autospec=True, - ) as oob_mgr_receive_reuse_msg, async_mock.patch.object( - OutOfBandManager, - "receive_reuse_accepted_message", - autospec=True, - ) as oob_mgr_receive_accept_msg, async_mock.patch.object( - OutOfBandManager, - "receive_problem_report", - autospec=True, - ) as oob_mgr_receive_problem_report, async_mock.patch.object( - V20PresManager, - "create_pres", - autospec=True, - ) as pres_mgr_create_pres: - oob_mgr_find_existing_conn.return_value = test_exist_conn - pres_mgr_receive_pres_req.return_value = px2_rec - pres_mgr_create_pres.return_value = ( - px2_rec, - V20Pres( - formats=[ - V20PresFormat( - attach_id="indy", - format_=V20_PRES_ATTACH_FORMAT[PRES_20][ - V20PresFormat.Format.INDY.api - ], - ) - ], - presentations_attach=[ - AttachDecorator.data_base64( - mapping={"bogus": "proof"}, - ident="indy", - ) - ], - ), - ) - holder = async_mock.MagicMock(IndyHolder, autospec=True) - get_creds = async_mock.CoroutineMock( - return_value=( - { - "cred_info": {"referent": "dummy_reft"}, - "attrs": { - "player": "Richie Knucklez", - "screenCapture": "aW1hZ2luZSBhIHNjcmVlbiBjYXB0dXJl", - "highScore": "1234560", - }, - }, - ) - ) - holder.get_credentials_for_presentation_request_by_referent = get_creds - holder.create_credential_request = async_mock.CoroutineMock( - return_value=( - json.dumps(TestConfig.indy_cred_req), - json.dumps(TestConfig.cred_req_meta), - ) - ) - self.profile.context.injector.bind_instance(IndyHolder, holder) - mock_oob_invi = async_mock.MagicMock( - handshake_protocols=[ - pfx.qualify(HSProto.RFC23.name) for pfx in DIDCommPrefix - ], - services=[TestConfig.test_target_did], - requests_attach=[ - AttachDecorator.deserialize(TestConfig.req_attach_v2) - ], - ) + oob_mgr_find_existing_conn.assert_called_once() + assert result.state == OobRecord.STATE_ACCEPTED + oob_record_save.assert_called_once_with( + ANY, reason="Storing reuse msg data" + ) - inv_message_cls.deserialize.return_value = mock_oob_invi + async def test_receive_invitation_handshake_reuse(self): + self.profile.context.update_settings({"public_invites": True}) - conn_rec = await self.manager.receive_invitation( - mock_oob_invi, use_existing_connection=True - ) - assert conn_rec is not None + test_exist_conn = ConnRecord( + connection_id="connection_id", + my_did=TestConfig.test_did, + their_did=TestConfig.test_target_did, + their_public_did=TestConfig.test_target_did, + invitation_msg_id="12345678-0123-4567-1234-567812345678", + their_role=ConnRecord.Role.REQUESTER, + ) - async def test_req_v2_attach_pres_catch_value_error(self): - async with self.profile.session() as session: - self.profile.context.update_settings({"public_invites": True}) - self.profile.context.update_settings( - {"debug.auto_respond_presentation_request": False} + with async_mock.patch.object( + test_module.OutOfBandManager, + "_handle_hanshake_reuse", + async_mock.CoroutineMock(), + ) as handle_handshake_reuse, async_mock.patch.object( + test_module.OutOfBandManager, + "_perform_handshake", + async_mock.CoroutineMock(), + ) as perform_handshake, async_mock.patch.object( + ConnRecord, + "find_existing_connection", + async_mock.CoroutineMock(return_value=test_exist_conn), + ): + oob_invitation = InvitationMessage( + handshake_protocols=[ + pfx.qualify(HSProto.RFC23.name) for pfx in DIDCommPrefix + ], + services=[TestConfig.test_target_did], + requests_attach=[], ) - test_exist_conn = ConnRecord( - my_did=TestConfig.test_did, - their_did=TestConfig.test_target_did, - their_public_did=TestConfig.test_target_did, - invitation_msg_id="12345678-0123-4567-1234-567812345678", - their_role=ConnRecord.Role.REQUESTER, + + handle_handshake_reuse.return_value = async_mock.MagicMock( + state=OobRecord.STATE_ACCEPTED ) - await test_exist_conn.save(session) - await test_exist_conn.metadata_set(session, "reuse_msg_state", "initial") - await test_exist_conn.metadata_set(session, "reuse_msg_id", "test_123") - receipt = MessageReceipt( - recipient_did=TestConfig.test_did, - recipient_did_public=False, - sender_did=TestConfig.test_target_did, + + result = await self.manager.receive_invitation( + oob_invitation, use_existing_connection=True ) - px2_rec = test_module.V20PresExRecord( - auto_present=False, - pres_request=TestConfig.PRES_REQ_V2.serialize(), + perform_handshake.assert_not_called() + handle_handshake_reuse.assert_called_once_with( + ANY, test_exist_conn, get_version_from_message(oob_invitation) ) - with async_mock.patch.object( - DIDXManager, - "receive_invitation", - autospec=True, - ) as didx_mgr_receive_invitation, async_mock.patch.object( - V20PresManager, - "receive_pres_request", - autospec=True, - ) as pres_mgr_receive_pres_req, async_mock.patch( - "aries_cloudagent.protocols.out_of_band.v1_0.manager.InvitationMessage", - autospec=True, - ) as inv_message_cls, async_mock.patch.object( - OutOfBandManager, - "fetch_connection_targets", - autospec=True, - ) as oob_mgr_fetch_conn, async_mock.patch.object( - ConnRecord, - "find_existing_connection", - async_mock.CoroutineMock(), - ) as oob_mgr_find_existing_conn, async_mock.patch.object( - OutOfBandManager, - "check_reuse_msg_state", - autospec=True, - ) as oob_mgr_check_reuse_state, async_mock.patch.object( - OutOfBandManager, - "create_handshake_reuse_message", - autospec=True, - ) as oob_mgr_create_reuse_msg, async_mock.patch.object( - OutOfBandManager, - "receive_reuse_message", - autospec=True, - ) as oob_mgr_receive_reuse_msg, async_mock.patch.object( - OutOfBandManager, - "receive_reuse_accepted_message", - autospec=True, - ) as oob_mgr_receive_accept_msg, async_mock.patch.object( - OutOfBandManager, - "receive_problem_report", - autospec=True, - ) as oob_mgr_receive_problem_report, async_mock.patch.object( - V20PresManager, - "create_pres", - autospec=True, - ) as pres_mgr_create_pres: - oob_mgr_find_existing_conn.return_value = test_exist_conn - pres_mgr_receive_pres_req.return_value = px2_rec - pres_mgr_create_pres.return_value = ( - px2_rec, - V20Pres( - formats=[ - V20PresFormat( - attach_id="indy", - format_=V20_PRES_ATTACH_FORMAT[PRES_20][ - V20PresFormat.Format.INDY.api - ], - ) - ], - presentations_attach=[ - AttachDecorator.data_base64( - mapping={"bogus": "proof"}, - ident="indy", - ) - ], - ), - ) - holder = async_mock.MagicMock(IndyHolder, autospec=True) - get_creds = async_mock.CoroutineMock(return_value=()) - holder.get_credentials_for_presentation_request_by_referent = get_creds - holder.create_credential_request = async_mock.CoroutineMock( - return_value=( - json.dumps(TestConfig.indy_cred_req), - json.dumps(TestConfig.cred_req_meta), - ) - ) - self.profile.context.injector.bind_instance(IndyHolder, holder) - mock_oob_invi = async_mock.MagicMock( - handshake_protocols=[ - pfx.qualify(HSProto.RFC23.name) for pfx in DIDCommPrefix - ], - services=[TestConfig.test_target_did], - requests_attach=[ - AttachDecorator.deserialize(TestConfig.req_attach_v2) - ], - ) + assert result.state == OobRecord.STATE_ACCEPTED - inv_message_cls.deserialize.return_value = mock_oob_invi - with self.assertRaises(OutOfBandManagerError) as context: - await self.manager.receive_invitation( - mock_oob_invi, use_existing_connection=True - ) - assert "cannot respond automatically" in str(context.exception) + async def test_receive_invitation_handshake_reuse_failed(self): + self.profile.context.update_settings({"public_invites": True}) - async def test_req_attach_cred_offer_v1(self): - async with self.profile.session() as session: - self.profile.context.update_settings({"public_invites": True}) - self.profile.context.update_settings( - {"debug.auto_respond_credential_offer": True} + test_exist_conn = ConnRecord( + connection_id="connection_id", + my_did=TestConfig.test_did, + their_did=TestConfig.test_target_did, + their_public_did=TestConfig.test_target_did, + invitation_msg_id="12345678-0123-4567-1234-567812345678", + their_role=ConnRecord.Role.REQUESTER, + ) + + with async_mock.patch.object( + test_module.OutOfBandManager, + "_handle_hanshake_reuse", + async_mock.CoroutineMock(), + ) as handle_handshake_reuse, async_mock.patch.object( + test_module.OutOfBandManager, + "_perform_handshake", + async_mock.CoroutineMock(), + ) as perform_handshake, async_mock.patch.object( + ConnRecord, + "find_existing_connection", + async_mock.CoroutineMock(return_value=test_exist_conn), + ), async_mock.patch.object( + ConnRecord, + "retrieve_by_id", + async_mock.CoroutineMock(return_value=test_exist_conn), + ): + oob_invitation = InvitationMessage( + handshake_protocols=[ + pfx.qualify(HSProto.RFC23.name) for pfx in DIDCommPrefix + ], + services=[TestConfig.test_target_did], + requests_attach=[], ) - test_exist_conn = ConnRecord( - my_did=TestConfig.test_did, - their_did=TestConfig.test_target_did, - their_public_did=TestConfig.test_target_did, - invitation_msg_id="12345678-0123-4567-1234-567812345678", - their_role=ConnRecord.Role.REQUESTER, - state=ConnRecord.State.COMPLETED, + + mock_oob = async_mock.MagicMock( + delete_record=async_mock.CoroutineMock(), + emit_event=async_mock.CoroutineMock(), ) - await test_exist_conn.save(session) - await test_exist_conn.metadata_set(session, "reuse_msg_state", "initial") - await test_exist_conn.metadata_set(session, "reuse_msg_id", "test_123") + perform_handshake.return_value = mock_oob - receipt = MessageReceipt( - recipient_did=TestConfig.test_did, - recipient_did_public=False, - sender_did=TestConfig.test_target_did, + handle_handshake_reuse.return_value = async_mock.MagicMock( + state=OobRecord.STATE_NOT_ACCEPTED ) - req_attach = deepcopy(TestConfig.req_attach_v1) - del req_attach["data"]["json"] - req_attach["data"]["json"] = TestConfig.CRED_OFFER_V1.serialize() - exchange_rec = V10CredentialExchange() - exchange_rec.credential_offer = TestConfig.CRED_OFFER_V1 - with async_mock.patch.object( - DIDXManager, - "receive_invitation", - autospec=True, - ) as didx_mgr_receive_invitation, async_mock.patch.object( - V10CredManager, - "receive_offer", - autospec=True, - ) as cred_mgr_offer_receive, async_mock.patch( - "aries_cloudagent.protocols.out_of_band.v1_0.manager.InvitationMessage", - autospec=True, - ) as inv_message_cls, async_mock.patch.object( - OutOfBandManager, - "fetch_connection_targets", - autospec=True, - ) as oob_mgr_fetch_conn, async_mock.patch.object( - ConnRecord, - "find_existing_connection", - async_mock.CoroutineMock(), - ) as oob_mgr_find_existing_conn, async_mock.patch.object( - OutOfBandManager, - "check_reuse_msg_state", - autospec=True, - ) as oob_mgr_check_reuse_state, async_mock.patch.object( - OutOfBandManager, - "conn_rec_is_active", - autospec=True, - ) as oob_mgr_check_conn_rec_active, async_mock.patch.object( - OutOfBandManager, - "create_handshake_reuse_message", - autospec=True, - ) as oob_mgr_create_reuse_msg, async_mock.patch.object( - OutOfBandManager, - "receive_reuse_message", - autospec=True, - ) as oob_mgr_receive_reuse_msg, async_mock.patch.object( - OutOfBandManager, - "receive_reuse_accepted_message", - autospec=True, - ) as oob_mgr_receive_accept_msg, async_mock.patch.object( - OutOfBandManager, - "receive_problem_report", - autospec=True, - ) as oob_mgr_receive_problem_report, async_mock.patch.object( - V10CredManager, - "create_request", - autospec=True, - ) as cred_mgr_request_receive: - oob_mgr_find_existing_conn.return_value = test_exist_conn - oob_mgr_check_conn_rec_active.return_value = test_exist_conn - cred_mgr_offer_receive.return_value = exchange_rec - cred_mgr_request_receive.return_value = (exchange_rec, INDY_CRED_REQ) - mock_oob_invi = async_mock.MagicMock( - handshake_protocols=[ - pfx.qualify(HSProto.RFC23.name) for pfx in DIDCommPrefix - ], - services=[TestConfig.test_target_did], - requests_attach=[AttachDecorator.deserialize(req_attach)], - ) - inv_message_cls.deserialize.return_value = mock_oob_invi - conn_rec = await self.manager.receive_invitation( - mock_oob_invi, use_existing_connection=True - ) - assert conn_rec is not None + result = await self.manager.receive_invitation( + oob_invitation, + use_existing_connection=True, + alias="alias", + auto_accept=True, + mediation_id="mediation_id", + ) - async def test_req_attach_cred_offer_v1_no_issue(self): - self.profile.context.update_settings({"public_invites": True}) - self.profile.context.update_settings( - {"debug.auto_respond_credential_offer": False} + handle_handshake_reuse.assert_called_once_with( + ANY, test_exist_conn, get_version_from_message(oob_invitation) ) - test_exist_conn = ConnRecord( - my_did=TestConfig.test_did, - their_did=TestConfig.test_target_did, - their_public_did=TestConfig.test_target_did, - invitation_msg_id="12345678-0123-4567-1234-567812345678", - their_role=ConnRecord.Role.REQUESTER, - state=ConnRecord.State.COMPLETED, + perform_handshake.assert_called_once_with( + oob_record=ANY, + alias="alias", + auto_accept=True, + mediation_id="mediation_id", + service_accept=None, ) - await test_exist_conn.save(session) - await test_exist_conn.metadata_set(session, "reuse_msg_state", "initial") - await test_exist_conn.metadata_set(session, "reuse_msg_id", "test_123") - receipt = MessageReceipt( - recipient_did=TestConfig.test_did, - recipient_did_public=False, - sender_did=TestConfig.test_target_did, - ) - req_attach = deepcopy(TestConfig.req_attach_v1) - del req_attach["data"]["json"] - req_attach["data"]["json"] = TestConfig.CRED_OFFER_V1.serialize() + assert mock_oob.state == OobRecord.STATE_DONE + assert result is mock_oob + mock_oob.emit_event.assert_called_once() + mock_oob.delete_record.assert_called_once() - exchange_rec = V10CredentialExchange() - exchange_rec.credential_offer = TestConfig.CRED_OFFER_V1 + async def test_receive_invitation_services_with_service_did(self): + self.profile.context.update_settings({"public_invites": True}) - with async_mock.patch.object( - DIDXManager, - "receive_invitation", - autospec=True, - ) as didx_mgr_receive_invitation, async_mock.patch.object( - V10CredManager, - "receive_offer", - autospec=True, - ) as cred_mgr_offer_receive, async_mock.patch( - "aries_cloudagent.protocols.out_of_band.v1_0.manager.InvitationMessage", - autospec=True, - ) as inv_message_cls, async_mock.patch.object( - OutOfBandManager, - "fetch_connection_targets", - autospec=True, - ) as oob_mgr_fetch_conn, async_mock.patch.object( - ConnRecord, - "find_existing_connection", - async_mock.CoroutineMock(), - ) as oob_mgr_find_existing_conn, async_mock.patch.object( - OutOfBandManager, - "check_reuse_msg_state", - autospec=True, - ) as oob_mgr_check_reuse_state, async_mock.patch.object( - OutOfBandManager, - "conn_rec_is_active", - autospec=True, - ) as oob_mgr_check_conn_rec_active, async_mock.patch.object( - OutOfBandManager, - "create_handshake_reuse_message", - autospec=True, - ) as oob_mgr_create_reuse_msg, async_mock.patch.object( - OutOfBandManager, - "receive_reuse_message", - autospec=True, - ) as oob_mgr_receive_reuse_msg, async_mock.patch.object( - OutOfBandManager, - "receive_reuse_accepted_message", - autospec=True, - ) as oob_mgr_receive_accept_msg, async_mock.patch.object( - OutOfBandManager, - "receive_problem_report", - autospec=True, - ) as oob_mgr_receive_problem_report: - oob_mgr_find_existing_conn.return_value = test_exist_conn - cred_mgr_offer_receive.return_value = exchange_rec - mock_oob_invi = async_mock.MagicMock( - handshake_protocols=[ - pfx.qualify(HSProto.RFC23.name) for pfx in DIDCommPrefix - ], - services=[TestConfig.test_target_did], - requests_attach=[AttachDecorator.deserialize(req_attach)], - ) - inv_message_cls.deserialize.return_value = mock_oob_invi - with self.assertRaises(OutOfBandManagerError) as context: - await self.manager.receive_invitation( - mock_oob_invi, use_existing_connection=True - ) - assert "Configuration sets auto_offer false" in str(context.exception) + mock_conn = async_mock.MagicMock(connection_id="dummy") - async def test_req_attach_cred_offer_v2(self): - async with self.profile.session() as session: - self.profile.context.update_settings({"public_invites": True}) - self.profile.context.update_settings( - {"debug.auto_respond_credential_offer": True} + with async_mock.patch.object( + test_module, "DIDXManager", autospec=True + ) as didx_mgr_cls, async_mock.patch.object( + ConnRecord, + "retrieve_by_id", + async_mock.CoroutineMock(return_value=mock_conn), + ): + didx_mgr_cls.return_value = async_mock.MagicMock( + receive_invitation=async_mock.CoroutineMock(return_value=mock_conn) ) - test_exist_conn = ConnRecord( - my_did=TestConfig.test_did, - their_did=TestConfig.test_target_did, - their_public_did=TestConfig.test_target_did, - invitation_msg_id="12345678-0123-4567-1234-567812345678", - their_role=ConnRecord.Role.REQUESTER, - state=ConnRecord.State.COMPLETED, + oob_invitation = InvitationMessage( + handshake_protocols=[ + pfx.qualify(HSProto.RFC23.name) for pfx in DIDCommPrefix + ], + services=[TestConfig.test_did], + requests_attach=[], ) - await test_exist_conn.save(session) - await test_exist_conn.metadata_set(session, "reuse_msg_state", "initial") - await test_exist_conn.metadata_set(session, "reuse_msg_id", "test_123") - receipt = MessageReceipt( - recipient_did=TestConfig.test_did, - recipient_did_public=False, - sender_did=TestConfig.test_target_did, - ) - req_attach = deepcopy(TestConfig.req_attach_v1) - del req_attach["data"]["json"] - req_attach["data"]["json"] = TestConfig.CRED_OFFER_V2.serialize() + invitation_record = await self.manager.receive_invitation(oob_invitation) + assert invitation_record.invitation.services - exchange_rec = V20CredExRecord() - exchange_rec.cred_offer = TestConfig.CRED_OFFER_V2 + async def test_request_attach_oob_message_processor_connectionless(self): + requests_attach: List[AttachDecorator] = [ + AttachDecorator.deserialize(deepcopy(TestConfig.req_attach_v1)) + ] - with async_mock.patch.object( - DIDXManager, - "receive_invitation", - autospec=True, - ) as didx_mgr_receive_invitation, async_mock.patch.object( - V20CredManager, - "receive_offer", - autospec=True, - ) as cred_mgr_offer_receive, async_mock.patch( - "aries_cloudagent.protocols.out_of_band.v1_0.manager.InvitationMessage", - autospec=True, - ) as inv_message_cls, async_mock.patch.object( - OutOfBandManager, - "fetch_connection_targets", - autospec=True, - ) as oob_mgr_fetch_conn, async_mock.patch.object( - ConnRecord, - "find_existing_connection", - async_mock.CoroutineMock(), - ) as oob_mgr_find_existing_conn, async_mock.patch.object( - OutOfBandManager, - "check_reuse_msg_state", - autospec=True, - ) as oob_mgr_check_reuse_state, async_mock.patch.object( - OutOfBandManager, - "conn_rec_is_active", - autospec=True, - ) as oob_mgr_check_conn_rec_active, async_mock.patch.object( - OutOfBandManager, - "create_handshake_reuse_message", - autospec=True, - ) as oob_mgr_create_reuse_msg, async_mock.patch.object( - OutOfBandManager, - "receive_reuse_message", - autospec=True, - ) as oob_mgr_receive_reuse_msg, async_mock.patch.object( - OutOfBandManager, - "receive_reuse_accepted_message", - autospec=True, - ) as oob_mgr_receive_accept_msg, async_mock.patch.object( - OutOfBandManager, - "receive_problem_report", - autospec=True, - ) as oob_mgr_receive_problem_report, async_mock.patch.object( - V20CredManager, - "create_request", - autospec=True, - ) as cred_mgr_request_receive: - oob_mgr_find_existing_conn.return_value = test_exist_conn - oob_mgr_check_conn_rec_active.return_value = test_exist_conn - cred_mgr_offer_receive.return_value = exchange_rec - cred_mgr_request_receive.return_value = (exchange_rec, INDY_CRED_REQ) - mock_oob_invi = async_mock.MagicMock( - handshake_protocols=[ - pfx.qualify(HSProto.RFC23.name) for pfx in DIDCommPrefix - ], - services=[TestConfig.test_target_did], - requests_attach=[AttachDecorator.deserialize(req_attach)], - ) - inv_message_cls.deserialize.return_value = mock_oob_invi - conn_rec = await self.manager.receive_invitation( - mock_oob_invi, use_existing_connection=True - ) - assert conn_rec is not None + mock_oob_processor = async_mock.MagicMock( + handle_message=async_mock.CoroutineMock() + ) + self.profile.context.injector.bind_instance( + OobMessageProcessor, mock_oob_processor + ) - async def test_req_attach_cred_offer_v2_no_issue(self): - async with self.profile.session() as session: - self.profile.context.update_settings({"public_invites": True}) - self.profile.context.update_settings( - {"debug.auto_respond_credential_offer": False} + mock_service_decorator = ServiceDecorator( + endpoint=self.test_endpoint, recipient_keys=[self.test_verkey] + ) + + with async_mock.patch.object( + InMemoryWallet, + "create_signing_key", + async_mock.CoroutineMock(), + ) as mock_create_signing_key, async_mock.patch.object( + OutOfBandManager, + "_service_decorator_from_service", + async_mock.CoroutineMock(), + ) as mock_service_decorator_from_service: + mock_create_signing_key.return_value = KeyInfo( + verkey="a-verkey", metadata={}, key_type=ED25519 ) - test_exist_conn = ConnRecord( - my_did=TestConfig.test_did, - their_did=TestConfig.test_target_did, - their_public_did=TestConfig.test_target_did, - invitation_msg_id="12345678-0123-4567-1234-567812345678", - their_role=ConnRecord.Role.REQUESTER, - state=ConnRecord.State.COMPLETED, + mock_service_decorator_from_service.return_value = mock_service_decorator + oob_invitation = InvitationMessage( + handshake_protocols=[], + services=[self.test_service], + requests_attach=requests_attach, ) - await test_exist_conn.save(session) - await test_exist_conn.metadata_set(session, "reuse_msg_state", "initial") - await test_exist_conn.metadata_set(session, "reuse_msg_id", "test_123") - receipt = MessageReceipt( - recipient_did=TestConfig.test_did, - recipient_did_public=False, - sender_did=TestConfig.test_target_did, + oob_record = await self.manager.receive_invitation( + oob_invitation, use_existing_connection=True ) - req_attach = deepcopy(TestConfig.req_attach_v1) - del req_attach["data"]["json"] - req_attach["data"]["json"] = TestConfig.CRED_OFFER_V2.serialize() - exchange_rec = V20CredExRecord() - exchange_rec.cred_offer = TestConfig.CRED_OFFER_V2 + assert oob_record.our_recipient_key == "a-verkey" + assert oob_record.our_service + assert oob_record.state == OobRecord.STATE_PREPARE_RESPONSE - with async_mock.patch.object( - DIDXManager, - "receive_invitation", - autospec=True, - ) as didx_mgr_receive_invitation, async_mock.patch.object( - V20CredManager, - "receive_offer", - autospec=True, - ) as cred_mgr_offer_receive, async_mock.patch( - "aries_cloudagent.protocols.out_of_band.v1_0.manager.InvitationMessage", - autospec=True, - ) as inv_message_cls, async_mock.patch.object( - OutOfBandManager, - "fetch_connection_targets", - autospec=True, - ) as oob_mgr_fetch_conn, async_mock.patch.object( - ConnRecord, - "find_existing_connection", - async_mock.CoroutineMock(), - ) as oob_mgr_find_existing_conn, async_mock.patch.object( - OutOfBandManager, - "check_reuse_msg_state", - autospec=True, - ) as oob_mgr_check_reuse_state, async_mock.patch.object( - OutOfBandManager, - "conn_rec_is_active", - autospec=True, - ) as oob_mgr_check_conn_rec_active, async_mock.patch.object( - OutOfBandManager, - "create_handshake_reuse_message", - autospec=True, - ) as oob_mgr_create_reuse_msg, async_mock.patch.object( - OutOfBandManager, - "receive_reuse_message", - autospec=True, - ) as oob_mgr_receive_reuse_msg, async_mock.patch.object( - OutOfBandManager, - "receive_reuse_accepted_message", - autospec=True, - ) as oob_mgr_receive_accept_msg, async_mock.patch.object( - OutOfBandManager, - "receive_problem_report", - autospec=True, - ) as oob_mgr_receive_problem_report: - oob_mgr_find_existing_conn.return_value = test_exist_conn - cred_mgr_offer_receive.return_value = exchange_rec - mock_oob_invi = async_mock.MagicMock( - handshake_protocols=[ - pfx.qualify(HSProto.RFC23.name) for pfx in DIDCommPrefix - ], - services=[TestConfig.test_target_did], - requests_attach=[AttachDecorator.deserialize(req_attach)], - ) - inv_message_cls.deserialize.return_value = mock_oob_invi - with self.assertRaises(OutOfBandManagerError) as context: - await self.manager.receive_invitation( - mock_oob_invi, use_existing_connection=True - ) - assert "Configuration sets auto_offer false" in str(context.exception) + mock_create_signing_key.assert_called_once_with(ED25519) + mock_oob_processor.handle_message.assert_called_once_with( + self.profile, + [attachment.content for attachment in requests_attach], + oob_record=oob_record, + their_service=mock_service_decorator, + ) - async def test_catch_unsupported_request_attach(self): + async def test_request_attach_oob_message_processor_connection(self): async with self.profile.session() as session: self.profile.context.update_settings({"public_invites": True}) - self.profile.context.update_settings( - {"debug.auto_respond_credential_offer": False} - ) test_exist_conn = ConnRecord( + connection_id="a-connection-id", my_did=TestConfig.test_did, their_did=TestConfig.test_target_did, their_public_did=TestConfig.test_target_did, invitation_msg_id="12345678-0123-4567-1234-567812345678", their_role=ConnRecord.Role.REQUESTER, + state=ConnRecord.State.COMPLETED.rfc160, ) - await test_exist_conn.save(session) - await test_exist_conn.metadata_set(session, "reuse_msg_state", "initial") - await test_exist_conn.metadata_set(session, "reuse_msg_id", "test_123") - - receipt = MessageReceipt( - recipient_did=TestConfig.test_did, - recipient_did_public=False, - sender_did=TestConfig.test_target_did, - ) - req_attach = deepcopy(TestConfig.req_attach_v1) - del req_attach["data"]["json"] - req_attach["data"]["json"] = TestConfig.CRED_OFFER_V1.serialize() - req_attach["data"]["json"]["@type"] = "test" - - with async_mock.patch.object( - DIDXManager, - "receive_invitation", - autospec=True, - ) as didx_mgr_receive_invitation, async_mock.patch( - "aries_cloudagent.protocols.out_of_band.v1_0.manager.InvitationMessage", - autospec=True, - ) as inv_message_cls, async_mock.patch.object( - OutOfBandManager, - "fetch_connection_targets", - autospec=True, - ) as oob_mgr_fetch_conn, async_mock.patch.object( - ConnRecord, - "find_existing_connection", - async_mock.CoroutineMock(), - ) as oob_mgr_find_existing_conn, async_mock.patch.object( - OutOfBandManager, - "check_reuse_msg_state", - autospec=True, - ) as oob_mgr_check_reuse_state, async_mock.patch.object( - OutOfBandManager, - "conn_rec_is_active", - autospec=True, - ) as oob_mgr_check_conn_rec_active, async_mock.patch.object( - OutOfBandManager, - "create_handshake_reuse_message", - autospec=True, - ) as oob_mgr_create_reuse_msg, async_mock.patch.object( - OutOfBandManager, - "receive_reuse_message", - autospec=True, - ) as oob_mgr_receive_reuse_msg, async_mock.patch.object( - OutOfBandManager, - "receive_reuse_accepted_message", - autospec=True, - ) as oob_mgr_receive_accept_msg, async_mock.patch.object( - OutOfBandManager, - "receive_problem_report", - autospec=True, - ) as oob_mgr_receive_problem_report: - oob_mgr_find_existing_conn.return_value = test_exist_conn - mock_oob_invi = async_mock.MagicMock( - handshake_protocols=[ - pfx.qualify(HSProto.RFC23.name) for pfx in DIDCommPrefix - ], - services=[TestConfig.test_target_did], - requests_attach=[AttachDecorator.deserialize(req_attach)], - ) - inv_message_cls.deserialize.return_value = mock_oob_invi - with self.assertRaises(OutOfBandManagerError) as context: - await self.manager.receive_invitation( - mock_oob_invi, use_existing_connection=True - ) - assert "Unsupported requests~attach type" in str(context.exception) - async def test_check_conn_rec_active_a(self): - async with self.profile.session() as session: - await self.test_conn_rec.save(session) - conn_rec = await self.manager.conn_rec_is_active( - self.test_conn_rec.connection_id - ) - assert conn_rec.connection_id == self.test_conn_rec.connection_id - - async def test_check_conn_rec_active_b(self): - connection_id = self.test_conn_rec.connection_id - conn_rec_request = deepcopy(self.test_conn_rec) - conn_rec_request.state = "request" - conn_rec_active = deepcopy(self.test_conn_rec) - conn_rec_active.state = "active" - with async_mock.patch.object( - test_module.ConnRecord, - "retrieve_by_id", - autospec=True, - ) as mock_conn_rec_retrieve: - mock_conn_rec_retrieve.side_effect = [conn_rec_request, conn_rec_active] - conn_rec = await self.manager.conn_rec_is_active(connection_id) - assert conn_rec.state == "active" + requests_attach: List[AttachDecorator] = [ + AttachDecorator.deserialize(deepcopy(TestConfig.req_attach_v1)) + ] - async def test_request_attach_cred_offer_v1_check_conn_rec_active_timeout(self): - async with self.profile.session() as session: - self.profile.context.update_settings({"public_invites": True}) - self.profile.context.update_settings( - {"debug.auto_respond_credential_offer": True} + mock_oob_processor = async_mock.MagicMock( + handle_message=async_mock.CoroutineMock() ) - test_exist_conn = ConnRecord( - my_did=TestConfig.test_did, - their_did=TestConfig.test_target_did, - their_public_did=TestConfig.test_target_did, - invitation_msg_id="12345678-0123-4567-1234-567812345678", - their_role=ConnRecord.Role.REQUESTER, + self.profile.context.injector.bind_instance( + OobMessageProcessor, mock_oob_processor ) - await test_exist_conn.save(session) - await test_exist_conn.metadata_set(session, "reuse_msg_state", "initial") - await test_exist_conn.metadata_set(session, "reuse_msg_id", "test_123") - req_attach = deepcopy(TestConfig.req_attach_v1) - del req_attach["data"]["json"] - req_attach["data"]["json"] = TestConfig.CRED_OFFER_V1.serialize() - exchange_rec = V20CredExRecord() - exchange_rec.cred_offer = TestConfig.CRED_OFFER_V1 with async_mock.patch.object( - DIDXManager, - "receive_invitation", - autospec=True, - ), async_mock.patch.object( - V10CredManager, - "receive_offer", - autospec=True, - ) as cred_mgr_offer_receive, async_mock.patch( - "aries_cloudagent.protocols.out_of_band.v1_0.manager.InvitationMessage", - autospec=True, - ) as inv_message_cls, async_mock.patch.object( - OutOfBandManager, - "fetch_connection_targets", - autospec=True, - ), async_mock.patch.object( ConnRecord, "find_existing_connection", async_mock.CoroutineMock(), - ) as oob_mgr_find_existing_conn, async_mock.patch.object( - OutOfBandManager, - "check_reuse_msg_state", - autospec=True, - ), async_mock.patch.object( - OutOfBandManager, - "conn_rec_is_active", - autospec=True, - ) as oob_mgr_check_conn_rec_active, async_mock.patch.object( - OutOfBandManager, - "create_handshake_reuse_message", - autospec=True, - ), async_mock.patch.object( - OutOfBandManager, - "receive_reuse_message", - autospec=True, - ), async_mock.patch.object( - OutOfBandManager, - "receive_reuse_accepted_message", - autospec=True, - ), async_mock.patch.object( - OutOfBandManager, - "receive_problem_report", - autospec=True, - ), async_mock.patch.object( - V10CredManager, - "create_request", - autospec=True, - ) as cred_mgr_request_receive, async_mock.patch.object( - test_module.LOGGER, "warning", async_mock.MagicMock() - ) as mock_logger_warning: + ) as oob_mgr_find_existing_conn: oob_mgr_find_existing_conn.return_value = test_exist_conn - cred_mgr_offer_receive.return_value = exchange_rec - cred_mgr_request_receive.return_value = (exchange_rec, INDY_CRED_REQ) - oob_mgr_check_conn_rec_active.side_effect = asyncio.TimeoutError - mock_oob_invi = async_mock.MagicMock( + oob_invitation = InvitationMessage( handshake_protocols=[ pfx.qualify(HSProto.RFC23.name) for pfx in DIDCommPrefix ], services=[TestConfig.test_target_did], - requests_attach=[AttachDecorator.deserialize(req_attach)], + requests_attach=requests_attach, + ) + + oob_record = await self.manager.receive_invitation( + oob_invitation, use_existing_connection=True ) - inv_message_cls.deserialize.return_value = mock_oob_invi - conn_rec = await self.manager.receive_invitation( - mock_oob_invi, use_existing_connection=True + + mock_oob_processor.handle_message.assert_called_once_with( + self.profile, + [attachment.content for attachment in requests_attach], + oob_record=oob_record, + their_service=None, ) - mock_logger_warning.assert_called_once() - assert conn_rec is not None - async def test_request_attach_cred_offer_v2_check_conn_rec_active_timeout(self): + async def test_request_attach_wait_for_conn_rec_active(self): async with self.profile.session() as session: self.profile.context.update_settings({"public_invites": True}) - self.profile.context.update_settings( - {"debug.auto_respond_credential_offer": True} - ) test_exist_conn = ConnRecord( my_did=TestConfig.test_did, their_did=TestConfig.test_target_did, @@ -3472,87 +1655,67 @@ async def test_request_attach_cred_offer_v2_check_conn_rec_active_timeout(self): invitation_msg_id="12345678-0123-4567-1234-567812345678", their_role=ConnRecord.Role.REQUESTER, ) - await test_exist_conn.save(session) - await test_exist_conn.metadata_set(session, "reuse_msg_state", "initial") - await test_exist_conn.metadata_set(session, "reuse_msg_id", "test_123") - receipt = MessageReceipt( - recipient_did=TestConfig.test_did, - recipient_did_public=False, - sender_did=TestConfig.test_target_did, - ) - req_attach = deepcopy(TestConfig.req_attach_v1) - del req_attach["data"]["json"] - req_attach["data"]["json"] = TestConfig.CRED_OFFER_V2.serialize() - exchange_rec = V20CredExRecord() - exchange_rec.cred_offer = TestConfig.CRED_OFFER_V2 with async_mock.patch.object( - DIDXManager, - "receive_invitation", - autospec=True, - ) as didx_mgr_receive_invitation, async_mock.patch.object( - V20CredManager, - "receive_offer", - autospec=True, - ) as cred_mgr_offer_receive, async_mock.patch( - "aries_cloudagent.protocols.out_of_band.v1_0.manager.InvitationMessage", - autospec=True, - ) as inv_message_cls, async_mock.patch.object( - OutOfBandManager, - "fetch_connection_targets", - autospec=True, - ) as oob_mgr_fetch_conn, async_mock.patch.object( + OutOfBandManager, "_wait_for_conn_rec_active" + ) as mock_wait_for_conn_rec_active, async_mock.patch.object( ConnRecord, "find_existing_connection", async_mock.CoroutineMock(), - ) as oob_mgr_find_existing_conn, async_mock.patch.object( - OutOfBandManager, - "check_reuse_msg_state", - autospec=True, - ) as oob_mgr_check_reuse_state, async_mock.patch.object( - OutOfBandManager, - "conn_rec_is_active", - autospec=True, - ) as oob_mgr_check_conn_rec_active, async_mock.patch.object( - OutOfBandManager, - "create_handshake_reuse_message", - autospec=True, - ) as oob_mgr_create_reuse_msg, async_mock.patch.object( - OutOfBandManager, - "receive_reuse_message", - autospec=True, - ) as oob_mgr_receive_reuse_msg, async_mock.patch.object( - OutOfBandManager, - "receive_reuse_accepted_message", - autospec=True, - ) as oob_mgr_receive_accept_msg, async_mock.patch.object( - OutOfBandManager, - "receive_problem_report", - autospec=True, - ) as oob_mgr_receive_problem_report, async_mock.patch.object( - V20CredManager, - "create_request", - autospec=True, - ) as cred_mgr_request_receive, async_mock.patch.object( - test_module.LOGGER, "warning", async_mock.MagicMock() - ) as mock_logger_warning: + ) as oob_mgr_find_existing_conn: oob_mgr_find_existing_conn.return_value = test_exist_conn - cred_mgr_offer_receive.return_value = exchange_rec - cred_mgr_request_receive.return_value = (exchange_rec, INDY_CRED_REQ) - oob_mgr_check_conn_rec_active.side_effect = asyncio.TimeoutError - mock_oob_invi = async_mock.MagicMock( + mock_wait_for_conn_rec_active.return_value = None + oob_invitation = InvitationMessage( handshake_protocols=[ pfx.qualify(HSProto.RFC23.name) for pfx in DIDCommPrefix ], services=[TestConfig.test_target_did], - requests_attach=[AttachDecorator.deserialize(req_attach)], + requests_attach=[ + AttachDecorator.deserialize(deepcopy(TestConfig.req_attach_v1)) + ], ) - inv_message_cls.deserialize.return_value = mock_oob_invi - conn_rec = await self.manager.receive_invitation( - mock_oob_invi, use_existing_connection=True + + with self.assertRaises(test_module.OutOfBandManagerError) as err: + oob_record = await self.manager.receive_invitation( + oob_invitation, use_existing_connection=True + ) + assert ( + "Connection not ready to process attach message for connection_id:" + in err.exception.message ) - mock_logger_warning.assert_called_once() - assert conn_rec is not None + + async def test_service_decorator_from_service_did(self): + did = "did:sov:something" + + self.manager.resolve_invitation = async_mock.CoroutineMock() + self.manager.resolve_invitation.return_value = ( + TestConfig.test_endpoint, + [TestConfig.test_verkey], + self.test_mediator_routing_keys, + ) + + service = await self.manager._service_decorator_from_service(did) + + assert service.endpoint == TestConfig.test_endpoint + assert service.recipient_keys == [TestConfig.test_verkey] + assert service.routing_keys == self.test_mediator_routing_keys + + async def test_service_decorator_from_service_object(self): + oob_service = OobService( + service_endpoint=TestConfig.test_endpoint, + recipient_keys=[ + DIDKey.from_public_key_b58(TestConfig.test_verkey, ED25519).did + ], + routing_keys=[ + DIDKey.from_public_key_b58(verkey, ED25519).did + for verkey in self.test_mediator_routing_keys + ], + ) + service = await self.manager._service_decorator_from_service(oob_service) + + assert service.endpoint == TestConfig.test_endpoint + assert service.recipient_keys == [TestConfig.test_verkey] + assert service.routing_keys == self.test_mediator_routing_keys async def test_delete_stale_connection_by_invitation(self): current_datetime = datetime_now() diff --git a/aries_cloudagent/protocols/out_of_band/v1_0/tests/test_routes.py b/aries_cloudagent/protocols/out_of_band/v1_0/tests/test_routes.py index d7aaffabf8..cf0349c34a 100644 --- a/aries_cloudagent/protocols/out_of_band/v1_0/tests/test_routes.py +++ b/aries_cloudagent/protocols/out_of_band/v1_0/tests/test_routes.py @@ -57,6 +57,49 @@ async def test_invitation_create(self): metadata=body["metadata"], alias=None, mediation_id=None, + service_accept=None, + protocol_version=None, + ) + mock_json_response.assert_called_once_with({"abc": "123"}) + + async def test_invitation_create_with_accept(self): + self.request.query = { + "multi_use": "true", + "auto_accept": "true", + } + body = { + "attachments": async_mock.MagicMock(), + "handshake_protocols": [test_module.HSProto.RFC23.name], + "accept": ["didcomm/aip1", "didcomm/aip2;env=rfc19"], + "use_public_did": True, + "metadata": {"hello": "world"}, + } + self.request.json = async_mock.CoroutineMock(return_value=body) + + with async_mock.patch.object( + test_module, "OutOfBandManager", autospec=True + ) as mock_oob_mgr, async_mock.patch.object( + test_module.web, "json_response", async_mock.Mock() + ) as mock_json_response: + mock_oob_mgr.return_value.create_invitation = async_mock.CoroutineMock( + return_value=async_mock.MagicMock( + serialize=async_mock.MagicMock(return_value={"abc": "123"}) + ) + ) + + await test_module.invitation_create(self.request) + mock_oob_mgr.return_value.create_invitation.assert_called_once_with( + my_label=None, + auto_accept=True, + public=True, + multi_use=True, + hs_protos=[test_module.HSProto.RFC23], + attachments=body["attachments"], + metadata=body["metadata"], + alias=None, + mediation_id=None, + service_accept=["didcomm/aip1", "didcomm/aip2;env=rfc19"], + protocol_version=None, ) mock_json_response.assert_called_once_with({"abc": "123"}) diff --git a/aries_cloudagent/protocols/present_proof/dif/pres_exch.py b/aries_cloudagent/protocols/present_proof/dif/pres_exch.py index 4a2ef289cf..5b9cdfe1de 100644 --- a/aries_cloudagent/protocols/present_proof/dif/pres_exch.py +++ b/aries_cloudagent/protocols/present_proof/dif/pres_exch.py @@ -237,7 +237,9 @@ def extract_info(self, data, **kwargs): """deserialize.""" new_data = {} if isinstance(data, dict): - if "oneof_filter" in data: + if "uri_groups" in data: + return data + elif "oneof_filter" in data and isinstance(data["oneof_filter"], list): new_data["oneof_filter"] = True uri_group_list_of_list = [] uri_group_list = data.get("oneof_filter") @@ -646,7 +648,7 @@ class Meta: ), example=( { - "oneOf": [ + "oneof_filter": [ [ {"uri": "https://www.w3.org/Test1#Test1"}, {"uri": "https://www.w3.org/Test2#Test2"}, @@ -835,7 +837,7 @@ class Meta: fmt = fields.Str( description="Format", required=False, - default="ldp_vp", + default="ldp_vc", data_key="format", ) path = fields.Str( diff --git a/aries_cloudagent/protocols/present_proof/dif/pres_exch_handler.py b/aries_cloudagent/protocols/present_proof/dif/pres_exch_handler.py index 86c8372206..46962448f0 100644 --- a/aries_cloudagent/protocols/present_proof/dif/pres_exch_handler.py +++ b/aries_cloudagent/protocols/present_proof/dif/pres_exch_handler.py @@ -10,6 +10,7 @@ """ import pytz import re +import logging from datetime import datetime from dateutil.parser import parse as dateutil_parser @@ -23,7 +24,6 @@ from ....core.error import BaseError from ....core.profile import Profile -from ....did.did_key import DIDKey from ....storage.vc_holder.vc_record import VCRecord from ....vc.ld_proofs import ( Ed25519Signature2018, @@ -38,8 +38,11 @@ ) from ....vc.vc_ld.prove import sign_presentation, create_presentation, derive_credential from ....wallet.base import BaseWallet, DIDInfo +from ....wallet.default_verification_key_strategy import ( + BaseVerificationKeyStrategy, +) from ....wallet.error import WalletError, WalletNotFoundError -from ....wallet.key_type import KeyType +from ....wallet.key_type import BLS12381G2, ED25519 from .pres_exch import ( PresentationDefinition, @@ -61,6 +64,7 @@ PRESENTATION_SUBMISSION_JSONLD_TYPE = "PresentationSubmission" PYTZ_TIMEZONE_PATTERN = re.compile(r"(([a-zA-Z]+)(?:\/)([a-zA-Z]+))") LIST_INDEX_PATTERN = re.compile(r"\[(\W+)\]|\[(\d+)\]") +LOGGER = logging.getLogger(__name__) class DIFPresExchError(BaseError): @@ -71,14 +75,14 @@ class DIFPresExchHandler: """Base Presentation Exchange Handler.""" ISSUE_SIGNATURE_SUITE_KEY_TYPE_MAPPING = { - Ed25519Signature2018: KeyType.ED25519, + Ed25519Signature2018: ED25519, } if BbsBlsSignature2020.BBS_SUPPORTED: - ISSUE_SIGNATURE_SUITE_KEY_TYPE_MAPPING[BbsBlsSignature2020] = KeyType.BLS12381G2 + ISSUE_SIGNATURE_SUITE_KEY_TYPE_MAPPING[BbsBlsSignature2020] = BLS12381G2 DERIVE_SIGNATURE_SUITE_KEY_TYPE_MAPPING = { - BbsBlsSignatureProof2020: KeyType.BLS12381G2, + BbsBlsSignatureProof2020: BLS12381G2, } PROOF_TYPE_SIGNATURE_SUITE_MAPPING = { suite.signature_type: suite @@ -115,7 +119,17 @@ async def _get_issue_suite( ): """Get signature suite for signing presentation.""" did_info = await self._did_info_for_did(issuer_id) - verification_method = self._get_verification_method(issuer_id) + verkey_id_strategy = self.profile.context.inject(BaseVerificationKeyStrategy) + verification_method = ( + await verkey_id_strategy.get_verification_method_id_for_did( + issuer_id, self.profile, proof_purpose="assertionMethod" + ) + ) + + if verification_method is None: + raise DIFPresExchError( + f"Unable to get retrieve verification method for did {issuer_id}" + ) # Get signature class based on proof type SignatureClass = self.PROOF_TYPE_SIGNATURE_SUITE_MAPPING[self.proof_type] @@ -149,18 +163,6 @@ async def _get_derive_suite( ), ) - def _get_verification_method(self, did: str): - """Get the verification method for a did.""" - if did.startswith("did:key:"): - return DIDKey.from_did(did).key_id - elif did.startswith("did:sov:"): - # key-1 is what uniresolver uses for key id - return did + "#key-1" - else: - raise DIFPresExchError( - f"Unable to get retrieve verification method for did {did}" - ) - async def _did_info_for_did(self, did: str) -> DIDInfo: """Get the did info for specified did. @@ -194,9 +196,9 @@ async def get_sign_key_credential_subject_id( issuer_id = None filtered_creds_list = [] if self.proof_type == BbsBlsSignature2020.signature_type: - reqd_key_type = KeyType.BLS12381G2 + reqd_key_type = BLS12381G2 else: - reqd_key_type = KeyType.ED25519 + reqd_key_type = ED25519 for cred in applicable_creds: if cred.subject_ids and len(cred.subject_ids) > 0: if not issuer_id: @@ -492,7 +494,8 @@ def create_vcrecord(self, cred_dict: dict) -> VCRecord: if type(schemas) is dict: schemas = [schemas] schema_ids = [schema.get("id") for schema in schemas] - expanded = jsonld.expand(cred_dict) + document_loader = self.profile.inject(DocumentLoader) + expanded = jsonld.expand(cred_dict, options={"documentLoader": document_loader}) types = JsonLdProcessor.get_values( expanded[0], "@type", @@ -789,8 +792,11 @@ def exclusive_minimum_check(self, val: any, _filter: Filter) -> bool: given_date = self.string_to_timezone_aware_datetime(str(val)) return given_date > to_compare_date else: - if self.is_numeric(val): + try: + val = self.is_numeric(val) return val > _filter.exclusive_min + except DIFPresExchError as err: + LOGGER.error(err) return False except (TypeError, ValueError, ParserError): return False @@ -817,8 +823,11 @@ def exclusive_maximum_check(self, val: any, _filter: Filter) -> bool: given_date = self.string_to_timezone_aware_datetime(str(val)) return given_date < to_compare_date else: - if self.is_numeric(val): + try: + val = self.is_numeric(val) return val < _filter.exclusive_max + except DIFPresExchError as err: + LOGGER.error(err) return False except (TypeError, ValueError, ParserError): return False @@ -845,8 +854,11 @@ def maximum_check(self, val: any, _filter: Filter) -> bool: given_date = self.string_to_timezone_aware_datetime(str(val)) return given_date <= to_compare_date else: - if self.is_numeric(val): + try: + val = self.is_numeric(val) return val <= _filter.maximum + except DIFPresExchError as err: + LOGGER.error(err) return False except (TypeError, ValueError, ParserError): return False @@ -873,8 +885,11 @@ def minimum_check(self, val: any, _filter: Filter) -> bool: given_date = self.string_to_timezone_aware_datetime(str(val)) return given_date >= to_compare_date else: - if self.is_numeric(val): + try: + val = self.is_numeric(val) return val >= _filter.minimum + except DIFPresExchError as err: + LOGGER.error(err) return False except (TypeError, ValueError, ParserError): return False @@ -1147,19 +1162,31 @@ async def apply_requirements( nested_result=nested_result, exclude=exclude ) - def is_numeric(self, val: any) -> bool: + def is_numeric(self, val: any): """ Check if val is an int or float. Args: val: to check Return: - bool + numeric value + Raises: + DIFPresExchError: Provided value has invalid/incompatible type + """ if isinstance(val, float) or isinstance(val, int): - return True - else: - return False + return val + elif isinstance(val, str): + if val.isdigit(): + return int(val) + else: + try: + return float(val) + except ValueError: + pass + raise DIFPresExchError( + "Invalid type provided for comparision/numeric operation." + ) async def merge_nested_results( self, nested_result: Sequence[dict], exclude: dict @@ -1204,7 +1231,7 @@ async def create_vp( challenge: str = None, domain: str = None, records_filter: dict = None, - ) -> dict: + ) -> Union[Sequence[dict], dict]: """ Create VerifiablePresentation. @@ -1218,78 +1245,99 @@ async def create_vp( req = await self.make_requirement( srs=pd.submission_requirements, descriptors=pd.input_descriptors ) - result = await self.apply_requirements( - req=req, credentials=credentials, records_filter=records_filter - ) - applicable_creds, descriptor_maps = await self.merge(result) - applicable_creds_list = [] - for credential in applicable_creds: - applicable_creds_list.append(credential.cred_value) - if ( - not self.profile.settings.get("debug.auto_respond_presentation_request") - and not records_filter - and len(applicable_creds_list) > 1 - ): - raise DIFPresExchError( - "Multiple credentials are applicable for presentation_definition " - f"{pd.id} and --auto-respond-presentation-request setting is not " - "enabled. Please specify which credentials should be applied to " - "which input_descriptors using record_ids filter." + result = [] + if req.nested_req: + for nested_req in req.nested_req: + res = await self.apply_requirements( + req=nested_req, + credentials=credentials, + records_filter=records_filter, + ) + result.append(res) + else: + res = await self.apply_requirements( + req=req, credentials=credentials, records_filter=records_filter ) - # submission_property - submission_property = PresentationSubmission( - id=str(uuid4()), definition_id=pd.id, descriptor_maps=descriptor_maps - ) - if self.is_holder: - ( - issuer_id, - filtered_creds_list, - ) = await self.get_sign_key_credential_subject_id( - applicable_creds=applicable_creds + result.append(res) + + result_vp = [] + for res in result: + applicable_creds, descriptor_maps = await self.merge(res) + applicable_creds_list = [] + for credential in applicable_creds: + applicable_creds_list.append(credential.cred_value) + if ( + not self.profile.settings.get("debug.auto_respond_presentation_request") + and not records_filter + and len(applicable_creds_list) > 1 + ): + raise DIFPresExchError( + "Multiple credentials are applicable for presentation_definition " + f"{pd.id} and --auto-respond-presentation-request setting is not " + "enabled. Please specify which credentials should be applied to " + "which input_descriptors using record_ids filter." + ) + # submission_property + submission_property = PresentationSubmission( + id=str(uuid4()), definition_id=pd.id, descriptor_maps=descriptor_maps ) - if not issuer_id and len(filtered_creds_list) == 0: - vp = await create_presentation(credentials=applicable_creds_list) - vp["presentation_submission"] = submission_property.serialize() - if self.proof_type is BbsBlsSignature2020.signature_type: - vp["@context"].append(SECURITY_CONTEXT_BBS_URL) - return vp - else: - vp = await create_presentation(credentials=filtered_creds_list) - else: - if not self.pres_signing_did: + if self.is_holder: ( issuer_id, filtered_creds_list, ) = await self.get_sign_key_credential_subject_id( applicable_creds=applicable_creds ) - if not issuer_id: + if not issuer_id and len(filtered_creds_list) == 0: vp = await create_presentation(credentials=applicable_creds_list) vp["presentation_submission"] = submission_property.serialize() if self.proof_type is BbsBlsSignature2020.signature_type: vp["@context"].append(SECURITY_CONTEXT_BBS_URL) - return vp + result_vp.append(vp) + continue else: vp = await create_presentation(credentials=filtered_creds_list) else: - issuer_id = self.pres_signing_did - vp = await create_presentation(credentials=applicable_creds_list) - vp["presentation_submission"] = submission_property.serialize() - if self.proof_type is BbsBlsSignature2020.signature_type: - vp["@context"].append(SECURITY_CONTEXT_BBS_URL) - async with self.profile.session() as session: - wallet = session.inject(BaseWallet) - issue_suite = await self._get_issue_suite( - wallet=wallet, - issuer_id=issuer_id, - ) - signed_vp = await sign_presentation( - presentation=vp, - suite=issue_suite, - challenge=challenge, - document_loader=document_loader, - ) - return signed_vp + if not self.pres_signing_did: + ( + issuer_id, + filtered_creds_list, + ) = await self.get_sign_key_credential_subject_id( + applicable_creds=applicable_creds + ) + if not issuer_id: + vp = await create_presentation( + credentials=applicable_creds_list + ) + vp["presentation_submission"] = submission_property.serialize() + if self.proof_type is BbsBlsSignature2020.signature_type: + vp["@context"].append(SECURITY_CONTEXT_BBS_URL) + result_vp.append(vp) + continue + else: + vp = await create_presentation(credentials=filtered_creds_list) + else: + issuer_id = self.pres_signing_did + vp = await create_presentation(credentials=applicable_creds_list) + vp["presentation_submission"] = submission_property.serialize() + if self.proof_type is BbsBlsSignature2020.signature_type: + vp["@context"].append(SECURITY_CONTEXT_BBS_URL) + async with self.profile.session() as session: + wallet = session.inject(BaseWallet) + issue_suite = await self._get_issue_suite( + wallet=wallet, + issuer_id=issuer_id, + ) + signed_vp = await sign_presentation( + presentation=vp, + suite=issue_suite, + challenge=challenge, + document_loader=document_loader, + ) + result_vp.append(signed_vp) + if len(result_vp) == 1: + return result_vp[0] + return result_vp def check_if_cred_id_derived(self, id: str) -> bool: """Check if credential or credentialSubjet id is derived.""" @@ -1330,7 +1378,7 @@ async def merge( if f"{cred_id}-{cred_id}" not in dict_of_descriptors: descriptor_map = InputDescriptorMapping( id=desc_id, - fmt="ldp_vp", + fmt="ldp_vc", path=(f"$.verifiableCredential[{dict_of_creds[cred_id]}]"), ) descriptors.append(descriptor_map) @@ -1341,7 +1389,7 @@ async def merge( async def verify_received_pres( self, pd: PresentationDefinition, - pres: dict, + pres: Union[Sequence[dict], dict], ): """ Verify credentials received in presentation. @@ -1350,8 +1398,24 @@ async def verify_received_pres( pres: received VerifiablePresentation pd: PresentationDefinition """ - descriptor_map_list = pres["presentation_submission"].get("descriptor_map") input_descriptors = pd.input_descriptors + if isinstance(pres, Sequence): + for pr in pres: + descriptor_map_list = pr["presentation_submission"].get( + "descriptor_map" + ) + await self.__verify_desc_map_list( + descriptor_map_list, pr, input_descriptors + ) + else: + descriptor_map_list = pres["presentation_submission"].get("descriptor_map") + await self.__verify_desc_map_list( + descriptor_map_list, pres, input_descriptors + ) + + async def __verify_desc_map_list( + self, descriptor_map_list, pres, input_descriptors + ): inp_desc_id_contraint_map = {} inp_desc_id_schema_one_of_filter = set() inp_desc_id_schemas_map = {} diff --git a/aries_cloudagent/protocols/present_proof/dif/pres_proposal_schema.py b/aries_cloudagent/protocols/present_proof/dif/pres_proposal_schema.py index 4f48851001..f8c227e150 100644 --- a/aries_cloudagent/protocols/present_proof/dif/pres_proposal_schema.py +++ b/aries_cloudagent/protocols/present_proof/dif/pres_proposal_schema.py @@ -3,7 +3,7 @@ from ....messaging.models.openapi import OpenAPISchema -from .pres_exch import InputDescriptorsSchema +from .pres_exch import InputDescriptorsSchema, DIFOptionsSchema class DIFProofProposalSchema(OpenAPISchema): @@ -16,3 +16,7 @@ class DIFProofProposalSchema(OpenAPISchema): ), required=False, ) + options = fields.Nested( + DIFOptionsSchema(), + required=False, + ) diff --git a/aries_cloudagent/protocols/present_proof/dif/tests/test_pres_exch.py b/aries_cloudagent/protocols/present_proof/dif/tests/test_pres_exch.py index 34638ef764..b8515188cd 100644 --- a/aries_cloudagent/protocols/present_proof/dif/tests/test_pres_exch.py +++ b/aries_cloudagent/protocols/present_proof/dif/tests/test_pres_exch.py @@ -363,7 +363,7 @@ def test_verifiable_presentation_wrapper(self): "descriptor_map": [ { "id": "citizenship_input_1", - "format": "ldp_vp", + "format": "ldp_vc", "path": "$.verifiableCredential[0]", } ], @@ -395,6 +395,10 @@ def test_schemas_input_desc_filter(self): deser_schema_filter = SchemasInputDescriptorFilter.deserialize( test_schemas_filter ) + ser_schema_filter = deser_schema_filter.serialize() + deser_schema_filter = SchemasInputDescriptorFilter.deserialize( + ser_schema_filter + ) assert deser_schema_filter.oneof_filter assert deser_schema_filter.uri_groups[0][0].uri == test_schema_list[0][0].get( "uri" @@ -418,6 +422,10 @@ def test_schemas_input_desc_filter(self): deser_schema_filter = SchemasInputDescriptorFilter.deserialize( test_schemas_filter ) + ser_schema_filter = deser_schema_filter.serialize() + deser_schema_filter = SchemasInputDescriptorFilter.deserialize( + ser_schema_filter + ) assert deser_schema_filter.oneof_filter assert deser_schema_filter.uri_groups[0][0].uri == test_schema_list[0].get( "uri" @@ -428,6 +436,10 @@ def test_schemas_input_desc_filter(self): assert isinstance(deser_schema_filter, SchemasInputDescriptorFilter) deser_schema_filter = SchemasInputDescriptorFilter.deserialize(test_schema_list) + ser_schema_filter = deser_schema_filter.serialize() + deser_schema_filter = SchemasInputDescriptorFilter.deserialize( + ser_schema_filter + ) assert not deser_schema_filter.oneof_filter assert deser_schema_filter.uri_groups[0][0].uri == test_schema_list[0].get( "uri" diff --git a/aries_cloudagent/protocols/present_proof/dif/tests/test_pres_exch_handler.py b/aries_cloudagent/protocols/present_proof/dif/tests/test_pres_exch_handler.py index 67a56badb0..e487d218cf 100644 --- a/aries_cloudagent/protocols/present_proof/dif/tests/test_pres_exch_handler.py +++ b/aries_cloudagent/protocols/present_proof/dif/tests/test_pres_exch_handler.py @@ -1,22 +1,27 @@ import asyncio +from copy import deepcopy from datetime import datetime +from typing import Sequence +from uuid import uuid4 + +import mock as async_mock import pytest -from asynctest import mock as async_mock -from copy import deepcopy -from uuid import uuid4 +from aries_cloudagent.wallet.key_type import BLS12381G2, ED25519 from .....core.in_memory import InMemoryProfile from .....did.did_key import DIDKey -from .....resolver.did_resolver_registry import DIDResolverRegistry from .....resolver.did_resolver import DIDResolver from .....storage.vc_holder.vc_record import VCRecord from .....wallet.base import BaseWallet, DIDInfo from .....wallet.crypto import KeyType -from .....wallet.did_method import DIDMethod +from .....wallet.default_verification_key_strategy import ( + DefaultVerificationKeyStrategy, + BaseVerificationKeyStrategy, +) +from .....wallet.did_method import SOV, KEY, DIDMethods from .....wallet.error import WalletNotFoundError from .....vc.ld_proofs import ( - BbsBlsSignatureProof2020, BbsBlsSignature2020, ) from .....vc.ld_proofs.document_loader import DocumentLoader @@ -25,7 +30,6 @@ from .....vc.tests.document_loader import custom_document_loader from .....vc.tests.data import ( BBS_SIGNED_VC_MATTR, - BBS_NESTED_VC_REVEAL_DOCUMENT_MATTR, ) from .. import pres_exch_handler as test_module @@ -60,7 +64,7 @@ ) -@pytest.yield_fixture(scope="class") +@pytest.fixture(scope="class") def event_loop(request): loop = asyncio.get_event_loop_policy().new_event_loop() yield loop @@ -69,12 +73,13 @@ def event_loop(request): @pytest.fixture(scope="class") def profile(): - profile = InMemoryProfile.test_profile() + profile = InMemoryProfile.test_profile(bind={DIDMethods: DIDMethods()}) context = profile.context - did_resolver_registry = DIDResolverRegistry() - context.injector.bind_instance(DIDResolverRegistry, did_resolver_registry) - context.injector.bind_instance(DIDResolver, DIDResolver(did_resolver_registry)) + context.injector.bind_instance(DIDResolver, DIDResolver([])) context.injector.bind_instance(DocumentLoader, custom_document_loader) + context.injector.bind_instance( + BaseVerificationKeyStrategy, DefaultVerificationKeyStrategy() + ) context.settings["debug.auto_respond_presentation_request"] = True return profile @@ -84,11 +89,11 @@ async def setup_tuple(profile): async with profile.session() as session: wallet = session.inject_or(BaseWallet) await wallet.create_local_did( - method=DIDMethod.SOV, key_type=KeyType.ED25519, did="WgWxqztrNooG92RXvxSTWv" + method=SOV, key_type=ED25519, did="WgWxqztrNooG92RXvxSTWv" ) await wallet.create_local_did( - method=DIDMethod.KEY, - key_type=KeyType.BLS12381G2, + method=KEY, + key_type=BLS12381G2, ) creds, pds = get_test_data() return creds, pds @@ -108,7 +113,17 @@ async def test_load_cred_json_a(self, setup_tuple, profile): pd=tmp_pd[0], challenge="1f44d55f-f161-4938-a659-f8026467f126", ) - assert len(tmp_vp.get("verifiableCredential")) == tmp_pd[1] + + if isinstance(tmp_vp, Sequence): + cred_count_list = [] + for tmp_vp_single in tmp_vp: + cred_count_list.append( + len(tmp_vp_single.get("verifiableCredential")) + ) + + assert min(cred_count_list) == tmp_pd[1] + else: + assert len(tmp_vp.get("verifiableCredential")) == tmp_pd[1] @pytest.mark.asyncio @pytest.mark.ursa_bbs_signatures @@ -125,7 +140,17 @@ async def test_load_cred_json_b(self, setup_tuple, profile): pd=tmp_pd[0], challenge="1f44d55f-f161-4938-a659-f8026467f126", ) - assert len(tmp_vp.get("verifiableCredential")) == tmp_pd[1] + + if isinstance(tmp_vp, Sequence): + cred_count_list = [] + for tmp_vp_single in tmp_vp: + cred_count_list.append( + len(tmp_vp_single.get("verifiableCredential")) + ) + + assert min(cred_count_list) == tmp_pd[1] + else: + assert len(tmp_vp.get("verifiableCredential")) == tmp_pd[1] @pytest.mark.asyncio async def test_to_requirement_catch_errors(self, profile): @@ -1562,6 +1587,7 @@ async def test_filter_string(self, setup_tuple, profile): assert len(tmp_vp.get("verifiableCredential")) == 1 @pytest.mark.asyncio + @pytest.mark.ursa_bbs_signatures async def test_filter_schema(self, setup_tuple, profile): cred_list, pd_list = setup_tuple dif_pres_exch_handler = DIFPresExchHandler(profile) @@ -1583,6 +1609,7 @@ async def test_filter_schema(self, setup_tuple, profile): == 0 ) + @pytest.mark.ursa_bbs_signatures def test_cred_schema_match_a(self, setup_tuple, profile): cred_list, pd_list = setup_tuple dif_pres_exch_handler = DIFPresExchHandler(profile) @@ -1594,6 +1621,7 @@ def test_cred_schema_match_a(self, setup_tuple, profile): is True ) + @pytest.mark.ursa_bbs_signatures @pytest.mark.asyncio async def test_merge_nested(self, setup_tuple, profile): cred_list, pd_list = setup_tuple @@ -1655,6 +1683,7 @@ async def test_merge_nested_cred_no_id(self, profile): test_nested_result, {} ) + @pytest.mark.ursa_bbs_signatures def test_subject_is_issuer(self, setup_tuple, profile): cred_list, pd_list = setup_tuple dif_pres_exch_handler = DIFPresExchHandler(profile) @@ -1666,14 +1695,17 @@ def test_subject_is_issuer(self, setup_tuple, profile): tmp_cred.issuer_id = "19b823fb-55ef-49f4-8caf-2a26b8b9286f" assert dif_pres_exch_handler.subject_is_issuer(tmp_cred) is False - @pytest.mark.asyncio def test_is_numeric(self, profile): dif_pres_exch_handler = DIFPresExchHandler(profile) - assert dif_pres_exch_handler.is_numeric("test") is False - assert dif_pres_exch_handler.is_numeric(1) is True - assert dif_pres_exch_handler.is_numeric(2 + 3j) is False + with pytest.raises(DIFPresExchError): + dif_pres_exch_handler.is_numeric("test") + assert dif_pres_exch_handler.is_numeric(1) == 1 + assert dif_pres_exch_handler.is_numeric(2.20) == 2.20 + assert dif_pres_exch_handler.is_numeric("2.20") == 2.20 + assert dif_pres_exch_handler.is_numeric("2") == 2 + with pytest.raises(DIFPresExchError): + dif_pres_exch_handler.is_numeric(2 + 3j) - @pytest.mark.asyncio def test_filter_no_match(self, profile): dif_pres_exch_handler = DIFPresExchHandler(profile) tmp_filter_excl_min = Filter(exclusive_min=7) @@ -1691,7 +1723,6 @@ def test_filter_no_match(self, profile): tmp_filter_max = Filter(maximum=10) assert dif_pres_exch_handler.maximum_check("test", tmp_filter_max) is False - @pytest.mark.asyncio def test_filter_valueerror(self, profile): dif_pres_exch_handler = DIFPresExchHandler(profile) tmp_filter_excl_min = Filter(exclusive_min=7, fmt="date") @@ -1709,7 +1740,6 @@ def test_filter_valueerror(self, profile): tmp_filter_max = Filter(maximum=10, fmt="date") assert dif_pres_exch_handler.maximum_check("test", tmp_filter_max) is False - @pytest.mark.asyncio def test_filter_length_check(self, profile): dif_pres_exch_handler = DIFPresExchHandler(profile) tmp_filter_both = Filter(min_length=7, max_length=10) @@ -1720,7 +1750,6 @@ def test_filter_length_check(self, profile): assert dif_pres_exch_handler.length_check("test", tmp_filter_max) is True assert dif_pres_exch_handler.length_check("test12", tmp_filter_min) is False - @pytest.mark.asyncio def test_filter_pattern_check(self, profile): dif_pres_exch_handler = DIFPresExchHandler(profile) tmp_filter = Filter(pattern="test1|test2") @@ -1728,7 +1757,6 @@ def test_filter_pattern_check(self, profile): tmp_filter = Filter(const="test3") assert dif_pres_exch_handler.pattern_check("test3", tmp_filter) is False - @pytest.mark.asyncio def test_is_len_applicable(self, profile): dif_pres_exch_handler = DIFPresExchHandler(profile) tmp_req_a = Requirement(count=1) @@ -1739,7 +1767,6 @@ def test_is_len_applicable(self, profile): assert dif_pres_exch_handler.is_len_applicable(tmp_req_b, 2) is False assert dif_pres_exch_handler.is_len_applicable(tmp_req_c, 6) is False - @pytest.mark.asyncio def test_create_vcrecord(self, profile): dif_pres_exch_handler = DIFPresExchHandler(profile) test_cred_dict = { @@ -1837,6 +1864,7 @@ def test_invalid_string_filter(self, profile): val="test", _filter=Filter() ) + @pytest.mark.ursa_bbs_signatures def test_cred_schema_match_b(self, profile, setup_tuple): dif_pres_exch_handler = DIFPresExchHandler(profile) cred_list, pd_list = setup_tuple @@ -1846,19 +1874,6 @@ def test_cred_schema_match_b(self, profile, setup_tuple): test_cred, "https://example.org/examples/degree.json" ) - def test_verification_method(self, profile): - dif_pres_exch_handler = DIFPresExchHandler(profile) - assert ( - dif_pres_exch_handler._get_verification_method( - "did:key:z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL" - ) - == DIDKey.from_did( - "did:key:z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL" - ).key_id - ) - with pytest.raises(DIFPresExchError): - dif_pres_exch_handler._get_verification_method("did:test:test") - @pytest.mark.asyncio @pytest.mark.ursa_bbs_signatures async def test_sign_pres_no_cred_subject_id(self, profile, setup_tuple): @@ -2027,14 +2042,14 @@ async def test_get_sign_key_credential_subject_id(self, profile): with async_mock.patch.object( DIFPresExchHandler, "_did_info_for_did", - async_mock.CoroutineMock(), + async_mock.AsyncMock(), ) as mock_did_info: did_info = DIDInfo( did="did:sov:LjgpST2rjsoxYegQDRm7EL", verkey="verkey", metadata={}, - method=DIDMethod.SOV, - key_type=KeyType.ED25519, + method=SOV, + key_type=ED25519, ) mock_did_info.return_value = did_info ( @@ -2092,14 +2107,14 @@ async def test_get_sign_key_credential_subject_id_error(self, profile): with async_mock.patch.object( DIFPresExchHandler, "_did_info_for_did", - async_mock.CoroutineMock(), + async_mock.AsyncMock(), ) as mock_did_info: did_info = DIDInfo( did="did:sov:LjgpST2rjsoxYegQDRm7EL", verkey="verkey", metadata={}, - method=DIDMethod.SOV, - key_type=KeyType.ED25519, + method=SOV, + key_type=ED25519, ) mock_did_info.return_value = did_info with pytest.raises(DIFPresExchError): @@ -2160,14 +2175,14 @@ async def test_get_sign_key_credential_subject_id_bbsbls(self, profile): with async_mock.patch.object( DIFPresExchHandler, "_did_info_for_did", - async_mock.CoroutineMock(), + async_mock.AsyncMock(), ) as mock_did_info: did_info = DIDInfo( did="did:key:z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL", verkey="verkey", metadata={}, - method=DIDMethod.KEY, - key_type=KeyType.BLS12381G2, + method=KEY, + key_type=BLS12381G2, ) mock_did_info.return_value = did_info ( @@ -2181,6 +2196,7 @@ async def test_get_sign_key_credential_subject_id_bbsbls(self, profile): ) assert len(filtered_creds) == 2 + @pytest.mark.ursa_bbs_signatures @pytest.mark.asyncio async def test_create_vp_no_issuer(self, profile, setup_tuple): dif_pres_exch_handler = DIFPresExchHandler(profile) @@ -2229,23 +2245,23 @@ async def test_create_vp_no_issuer(self, profile, setup_tuple): with async_mock.patch.object( DIFPresExchHandler, "_did_info_for_did", - async_mock.CoroutineMock(), + async_mock.AsyncMock(), ) as mock_did_info, async_mock.patch.object( DIFPresExchHandler, "make_requirement", - async_mock.CoroutineMock(), + async_mock.AsyncMock(), ) as mock_make_req, async_mock.patch.object( DIFPresExchHandler, "apply_requirements", - async_mock.CoroutineMock(), + async_mock.AsyncMock(), ) as mock_apply_req, async_mock.patch.object( DIFPresExchHandler, "merge", - async_mock.CoroutineMock(), + async_mock.AsyncMock(), ) as mock_merge, async_mock.patch.object( test_module, "create_presentation", - async_mock.CoroutineMock(), + async_mock.AsyncMock(), ) as mock_create_vp: mock_make_req.return_value = async_mock.MagicMock() mock_apply_req.return_value = async_mock.MagicMock() @@ -2256,8 +2272,8 @@ async def test_create_vp_no_issuer(self, profile, setup_tuple): did="did:key:z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL", verkey="verkey", metadata={}, - method=DIDMethod.KEY, - key_type=KeyType.BLS12381G2, + method=KEY, + key_type=BLS12381G2, ) mock_did_info.return_value = did_info vp = await dif_pres_exch_handler.create_vp( @@ -2265,13 +2281,15 @@ async def test_create_vp_no_issuer(self, profile, setup_tuple): pd=pd_list[0][0], challenge="3fa85f64-5717-4562-b3fc-2c963f66afa7", ) - assert vp["test"] == "1" - assert ( - vp["presentation_submission"]["definition_id"] - == "32f54163-7166-48f1-93d8-ff217bdb0653" - ) + for vp_single in vp: + assert vp_single["test"] == "1" + assert ( + vp_single["presentation_submission"]["definition_id"] + == "32f54163-7166-48f1-93d8-ff217bdb0653" + ) @pytest.mark.asyncio + @pytest.mark.ursa_bbs_signatures async def test_create_vp_with_bbs_suite(self, profile, setup_tuple): dif_pres_exch_handler = DIFPresExchHandler( profile, proof_type=BbsBlsSignature2020.signature_type @@ -2280,27 +2298,27 @@ async def test_create_vp_with_bbs_suite(self, profile, setup_tuple): with async_mock.patch.object( DIFPresExchHandler, "_did_info_for_did", - async_mock.CoroutineMock(), + async_mock.AsyncMock(), ) as mock_did_info, async_mock.patch.object( DIFPresExchHandler, "make_requirement", - async_mock.CoroutineMock(), + async_mock.AsyncMock(), ) as mock_make_req, async_mock.patch.object( DIFPresExchHandler, "apply_requirements", - async_mock.CoroutineMock(), + async_mock.AsyncMock(), ) as mock_apply_req, async_mock.patch.object( DIFPresExchHandler, "merge", - async_mock.CoroutineMock(), + async_mock.AsyncMock(), ) as mock_merge, async_mock.patch.object( test_module, "create_presentation", - async_mock.CoroutineMock(), + async_mock.AsyncMock(), ) as mock_create_vp, async_mock.patch.object( test_module, "sign_presentation", - async_mock.CoroutineMock(), + async_mock.AsyncMock(), ) as mock_sign_vp: mock_make_req.return_value = async_mock.MagicMock() mock_apply_req.return_value = async_mock.MagicMock() @@ -2315,8 +2333,8 @@ async def test_create_vp_with_bbs_suite(self, profile, setup_tuple): did="did:key:z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL", verkey="verkey", metadata={}, - method=DIDMethod.KEY, - key_type=KeyType.BLS12381G2, + method=KEY, + key_type=BLS12381G2, ) mock_did_info.return_value = did_info vp = await dif_pres_exch_handler.create_vp( @@ -2324,10 +2342,12 @@ async def test_create_vp_with_bbs_suite(self, profile, setup_tuple): pd=pd_list[0][0], challenge="3fa85f64-5717-4562-b3fc-2c963f66afa7", ) - assert vp["test"] == "1" - assert SECURITY_CONTEXT_BBS_URL in vp["@context"] + for vp_single in vp: + assert vp_single["test"] == "1" + assert SECURITY_CONTEXT_BBS_URL in vp_single["@context"] @pytest.mark.asyncio + @pytest.mark.ursa_bbs_signatures async def test_create_vp_no_issuer_with_bbs_suite(self, profile, setup_tuple): dif_pres_exch_handler = DIFPresExchHandler( profile, proof_type=BbsBlsSignature2020.signature_type @@ -2336,27 +2356,27 @@ async def test_create_vp_no_issuer_with_bbs_suite(self, profile, setup_tuple): with async_mock.patch.object( DIFPresExchHandler, "_did_info_for_did", - async_mock.CoroutineMock(), + async_mock.AsyncMock(), ) as mock_did_info, async_mock.patch.object( DIFPresExchHandler, "make_requirement", - async_mock.CoroutineMock(), + async_mock.AsyncMock(), ) as mock_make_req, async_mock.patch.object( DIFPresExchHandler, "apply_requirements", - async_mock.CoroutineMock(), + async_mock.AsyncMock(), ) as mock_apply_req, async_mock.patch.object( DIFPresExchHandler, "merge", - async_mock.CoroutineMock(), + async_mock.AsyncMock(), ) as mock_merge, async_mock.patch.object( test_module, "create_presentation", - async_mock.CoroutineMock(), + async_mock.AsyncMock(), ) as mock_create_vp, async_mock.patch.object( DIFPresExchHandler, "get_sign_key_credential_subject_id", - async_mock.CoroutineMock(), + async_mock.AsyncMock(), ) as mock_sign_key_cred_subject: mock_make_req.return_value = async_mock.MagicMock() mock_apply_req.return_value = async_mock.MagicMock() @@ -2368,8 +2388,8 @@ async def test_create_vp_no_issuer_with_bbs_suite(self, profile, setup_tuple): did="did:key:z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL", verkey="verkey", metadata={}, - method=DIDMethod.KEY, - key_type=KeyType.BLS12381G2, + method=KEY, + key_type=BLS12381G2, ) mock_did_info.return_value = did_info vp = await dif_pres_exch_handler.create_vp( @@ -2377,8 +2397,10 @@ async def test_create_vp_no_issuer_with_bbs_suite(self, profile, setup_tuple): pd=pd_list[0][0], challenge="3fa85f64-5717-4562-b3fc-2c963f66afa7", ) - assert vp["test"] == "1" - assert SECURITY_CONTEXT_BBS_URL in vp["@context"] + # 2 sub_reqs, vp is a sequence + for vp_single in vp: + assert vp_single["test"] == "1" + assert SECURITY_CONTEXT_BBS_URL in vp_single["@context"] @pytest.mark.asyncio @pytest.mark.ursa_bbs_signatures @@ -2980,6 +3002,7 @@ async def test_filter_creds_record_id(self, profile): assert filtered_cred_list[1].record_id in record_id_list @pytest.mark.asyncio + @pytest.mark.ursa_bbs_signatures async def test_create_vp_record_ids(self, profile): dif_pres_exch_handler = DIFPresExchHandler(profile) test_pd_filter_with_only_num_type = """ @@ -3053,6 +3076,8 @@ async def test_multiple_applicable_creds_with_no_id(self, profile, setup_tuple): pd=tmp_pd[0], challenge="1f44d55f-f161-4938-a659-f8026467f126", ) + # only 1 sub_req + assert isinstance(tmp_vp, dict) assert len(tmp_vp["verifiableCredential"]) == 2 assert ( tmp_vp.get("verifiableCredential")[0] @@ -3073,19 +3098,22 @@ async def test_multiple_applicable_creds_with_no_id(self, profile, setup_tuple): pd=tmp_pd[0], challenge="1f44d55f-f161-4938-a659-f8026467f126", ) - assert len(tmp_vp["verifiableCredential"]) == 2 - assert ( - tmp_vp.get("verifiableCredential")[0] - .get("credentialSubject") - .get("givenName") - == "TEST" - ) - assert ( - tmp_vp.get("verifiableCredential")[1] - .get("credentialSubject") - .get("givenName") - == "TEST" - ) + assert isinstance(tmp_vp, Sequence) + # 1 for each submission requirement group + assert len(tmp_vp) == 3 + for tmp_vp_single in tmp_vp: + assert ( + tmp_vp_single.get("verifiableCredential")[0] + .get("credentialSubject") + .get("givenName") + == "TEST" + ) + assert ( + tmp_vp_single.get("verifiableCredential")[1] + .get("credentialSubject") + .get("givenName") + == "TEST" + ) @pytest.mark.asyncio @pytest.mark.ursa_bbs_signatures diff --git a/aries_cloudagent/protocols/present_proof/indy/pres_exch_handler.py b/aries_cloudagent/protocols/present_proof/indy/pres_exch_handler.py index 56cec44dfd..4bff88cc89 100644 --- a/aries_cloudagent/protocols/present_proof/indy/pres_exch_handler.py +++ b/aries_cloudagent/protocols/present_proof/indy/pres_exch_handler.py @@ -14,6 +14,7 @@ GET_REVOC_REG_DELTA, IndyLedgerRequestsExecutor, ) +from ....multitenant.base import BaseMultitenantManager from ....revocation.models.revocation_registry import RevocationRegistry from ..v1_0.models.presentation_exchange import V10PresentationExchange @@ -93,7 +94,11 @@ async def return_presentation( for credential in credentials.values(): schema_id = credential["schema_id"] - ledger_exec_inst = self._profile.inject(IndyLedgerRequestsExecutor) + multitenant_mgr = self._profile.inject_or(BaseMultitenantManager) + if multitenant_mgr: + ledger_exec_inst = IndyLedgerRequestsExecutor(self._profile) + else: + ledger_exec_inst = self._profile.inject(IndyLedgerRequestsExecutor) ledger = ( await ledger_exec_inst.get_ledger_for_identifier( schema_id, @@ -127,7 +132,11 @@ async def return_presentation( if "timestamp" in precis: continue rev_reg_id = credentials[credential_id]["rev_reg_id"] - ledger_exec_inst = self._profile.inject(IndyLedgerRequestsExecutor) + multitenant_mgr = self._profile.inject_or(BaseMultitenantManager) + if multitenant_mgr: + ledger_exec_inst = IndyLedgerRequestsExecutor(self._profile) + else: + ledger_exec_inst = self._profile.inject(IndyLedgerRequestsExecutor) ledger = ( await ledger_exec_inst.get_ledger_for_identifier( rev_reg_id, @@ -185,7 +194,7 @@ async def return_presentation( f"Failed to create revocation state: {e.error_code}, {e.message}" ) raise e - for (referent, precis) in requested_referents.items(): + for referent, precis in requested_referents.items(): if "timestamp" not in precis: continue if referent in requested_credentials["requested_attributes"]: @@ -222,7 +231,11 @@ async def process_pres_identifiers( for identifier in identifiers: schema_ids.append(identifier["schema_id"]) cred_def_ids.append(identifier["cred_def_id"]) - ledger_exec_inst = self._profile.inject(IndyLedgerRequestsExecutor) + multitenant_mgr = self._profile.inject_or(BaseMultitenantManager) + if multitenant_mgr: + ledger_exec_inst = IndyLedgerRequestsExecutor(self._profile) + else: + ledger_exec_inst = self._profile.inject(IndyLedgerRequestsExecutor) ledger = ( await ledger_exec_inst.get_ledger_for_identifier( identifier["schema_id"], diff --git a/aries_cloudagent/protocols/present_proof/v1_0/handlers/presentation_ack_handler.py b/aries_cloudagent/protocols/present_proof/v1_0/handlers/presentation_ack_handler.py index 0a1d01232c..d66c500fc0 100644 --- a/aries_cloudagent/protocols/present_proof/v1_0/handlers/presentation_ack_handler.py +++ b/aries_cloudagent/protocols/present_proof/v1_0/handlers/presentation_ack_handler.py @@ -1,5 +1,6 @@ """Presentation ack message handler.""" +from .....core.oob_processor import OobMessageProcessor from .....messaging.base_handler import BaseHandler, HandlerException from .....messaging.request_context import RequestContext from .....messaging.responder import BaseResponder @@ -29,8 +30,20 @@ async def handle(self, context: RequestContext, responder: BaseResponder): context.message.serialize(as_string=True), ) - if not context.connection_ready: - raise HandlerException("No connection established for presentation ack") + # If connection is present it must be ready for use + if context.connection_record and not context.connection_ready: + raise HandlerException("Connection used for presentation ack not ready") + + # Find associated oob record + oob_processor = context.inject(OobMessageProcessor) + oob_record = await oob_processor.find_oob_record_for_inbound_message(context) + + # Either connection or oob context must be present + if not context.connection_record and not oob_record: + raise HandlerException( + "No connection or associated connectionless exchange found for" + " presentation ack" + ) presentation_manager = PresentationManager(context.profile) await presentation_manager.receive_presentation_ack( diff --git a/aries_cloudagent/protocols/present_proof/v1_0/handlers/presentation_handler.py b/aries_cloudagent/protocols/present_proof/v1_0/handlers/presentation_handler.py index 40c4d73dc6..e30eb65306 100644 --- a/aries_cloudagent/protocols/present_proof/v1_0/handlers/presentation_handler.py +++ b/aries_cloudagent/protocols/present_proof/v1_0/handlers/presentation_handler.py @@ -1,7 +1,8 @@ """Presentation message handler.""" +from .....core.oob_processor import OobMessageProcessor from .....ledger.error import LedgerError -from .....messaging.base_handler import BaseHandler +from .....messaging.base_handler import BaseHandler, HandlerException from .....messaging.models.base import BaseModelError from .....messaging.request_context import RequestContext from .....messaging.responder import BaseResponder @@ -35,10 +36,24 @@ async def handle(self, context: RequestContext, responder: BaseResponder): context.message.serialize(as_string=True), ) + # If connection is present it must be ready for use + if context.connection_record and not context.connection_ready: + raise HandlerException("Connection used for presentation not ready") + + # Find associated oob record. If the presentation request was created as an oob + # attachment the presentation exchange record won't have a connection id (yet) + oob_processor = context.inject(OobMessageProcessor) + oob_record = await oob_processor.find_oob_record_for_inbound_message(context) + + # Normally we would do a check here that there is either a connection or + # an associated oob record. However as present proof supported receiving + # presentation without oob record or connection record + # (aip-1 style connectionless) we can't perform this check here + presentation_manager = PresentationManager(profile) presentation_exchange_record = await presentation_manager.receive_presentation( - context.message, context.connection_record + context.message, context.connection_record, oob_record ) # mgr saves record state null if need be and possible r_time = trace_event( @@ -49,10 +64,14 @@ async def handle(self, context: RequestContext, responder: BaseResponder): ) # Automatically move to next state if flag is set - if context.settings.get("debug.auto_verify_presentation"): + if ( + presentation_exchange_record + and presentation_exchange_record.auto_verify + or context.settings.get("debug.auto_verify_presentation") + ): try: await presentation_manager.verify_presentation( - presentation_exchange_record + presentation_exchange_record, responder ) except (BaseModelError, LedgerError, StorageError) as err: self._logger.exception(err) diff --git a/aries_cloudagent/protocols/present_proof/v1_0/handlers/presentation_problem_report_handler.py b/aries_cloudagent/protocols/present_proof/v1_0/handlers/presentation_problem_report_handler.py index e20d496c01..fb5874145a 100644 --- a/aries_cloudagent/protocols/present_proof/v1_0/handlers/presentation_problem_report_handler.py +++ b/aries_cloudagent/protocols/present_proof/v1_0/handlers/presentation_problem_report_handler.py @@ -1,6 +1,6 @@ """Presentation problem report message handler.""" -from .....messaging.base_handler import BaseHandler +from .....messaging.base_handler import BaseHandler, HandlerException from .....messaging.request_context import RequestContext from .....messaging.responder import BaseResponder from .....storage.error import StorageError, StorageNotFoundError @@ -26,6 +26,16 @@ async def handle(self, context: RequestContext, responder: BaseResponder): ) assert isinstance(context.message, PresentationProblemReport) + # If connection is present it must be ready for use + if context.connection_record and not context.connection_ready: + raise HandlerException( + "Connection used for presentation problem report not ready" + ) + elif not context.connection_record: + raise HandlerException( + "Connectionless not supported for presentation problem report" + ) + presentation_manager = PresentationManager(context.profile) try: await presentation_manager.receive_problem_report( diff --git a/aries_cloudagent/protocols/present_proof/v1_0/handlers/presentation_proposal_handler.py b/aries_cloudagent/protocols/present_proof/v1_0/handlers/presentation_proposal_handler.py index 89708a4368..7977626453 100644 --- a/aries_cloudagent/protocols/present_proof/v1_0/handlers/presentation_proposal_handler.py +++ b/aries_cloudagent/protocols/present_proof/v1_0/handlers/presentation_proposal_handler.py @@ -37,9 +37,14 @@ async def handle(self, context: RequestContext, responder: BaseResponder): context.message.serialize(as_string=True), ) - if not context.connection_ready: + if not context.connection_record: raise HandlerException( - "No connection established for presentation proposal" + "Connectionless not supported for presentation proposal" + ) + # If connection is present it must be ready for use + elif not context.connection_ready: + raise HandlerException( + "Connection used for presentation proposal not ready" ) presentation_manager = PresentationManager(profile) diff --git a/aries_cloudagent/protocols/present_proof/v1_0/handlers/presentation_request_handler.py b/aries_cloudagent/protocols/present_proof/v1_0/handlers/presentation_request_handler.py index 21979940ee..1736d22843 100644 --- a/aries_cloudagent/protocols/present_proof/v1_0/handlers/presentation_request_handler.py +++ b/aries_cloudagent/protocols/present_proof/v1_0/handlers/presentation_request_handler.py @@ -1,5 +1,6 @@ """Presentation request message handler.""" +from .....core.oob_processor import OobMessageProcessor from .....indy.holder import IndyHolder, IndyHolderError from .....indy.models.xform import indy_proof_req_preview2indy_requested_creds from .....ledger.error import LedgerError @@ -40,8 +41,26 @@ async def handle(self, context: RequestContext, responder: BaseResponder): context.message.serialize(as_string=True), ) - if not context.connection_ready: - raise HandlerException("No connection established for presentation request") + # If connection is present it must be ready for use + if context.connection_record and not context.connection_ready: + raise HandlerException("Connection used for presentation request not ready") + + # Find associated oob record + oob_processor = context.inject(OobMessageProcessor) + oob_record = await oob_processor.find_oob_record_for_inbound_message(context) + + # Either connection or oob context must be present + if not context.connection_record and not oob_record: + raise HandlerException( + "No connection or associated connectionless exchange found for" + " presentation request" + ) + + connection_id = ( + context.connection_record.connection_id + if context.connection_record + else None + ) presentation_manager = PresentationManager(profile) @@ -56,11 +75,18 @@ async def handle(self, context: RequestContext, responder: BaseResponder): ) = await V10PresentationExchange.retrieve_by_tag_filter( session, {"thread_id": context.message._thread_id}, - {"connection_id": context.connection_record.connection_id}, + { + "role": V10PresentationExchange.ROLE_PROVER, + "connection_id": connection_id, + }, ) # holder initiated via proposal + presentation_exchange_record.presentation_request = indy_proof_request + presentation_exchange_record.presentation_request_dict = ( + context.message.serialize() + ) except StorageNotFoundError: # verifier sent this request free of any proposal presentation_exchange_record = V10PresentationExchange( - connection_id=context.connection_record.connection_id, + connection_id=connection_id, thread_id=context.message._thread_id, initiator=V10PresentationExchange.INITIATOR_EXTERNAL, role=V10PresentationExchange.ROLE_PROVER, @@ -72,7 +98,6 @@ async def handle(self, context: RequestContext, responder: BaseResponder): trace=(context.message._trace is not None), ) - presentation_exchange_record.presentation_request = indy_proof_request presentation_exchange_record = await presentation_manager.receive_request( presentation_exchange_record ) # mgr only saves record: on exception, saving state null is hopeless diff --git a/aries_cloudagent/protocols/present_proof/v1_0/handlers/tests/test_presentation_ack_handler.py b/aries_cloudagent/protocols/present_proof/v1_0/handlers/tests/test_presentation_ack_handler.py index 34f1578f03..db233adc17 100644 --- a/aries_cloudagent/protocols/present_proof/v1_0/handlers/tests/test_presentation_ack_handler.py +++ b/aries_cloudagent/protocols/present_proof/v1_0/handlers/tests/test_presentation_ack_handler.py @@ -1,5 +1,6 @@ from asynctest import mock as async_mock, TestCase as AsyncTestCase +from ......core.oob_processor import OobMessageProcessor from ......messaging.request_context import RequestContext from ......messaging.responder import MockResponder from ......transport.inbound.receipt import MessageReceipt @@ -15,6 +16,13 @@ async def test_called(self): request_context.message_receipt = MessageReceipt() session = request_context.session() + mock_oob_processor = async_mock.MagicMock( + find_oob_record_for_inbound_message=async_mock.CoroutineMock( + return_value=async_mock.MagicMock() + ) + ) + request_context.injector.bind_instance(OobMessageProcessor, mock_oob_processor) + with async_mock.patch.object( test_module, "PresentationManager", autospec=True ) as mock_pres_mgr: @@ -37,6 +45,7 @@ async def test_called(self): async def test_called_not_ready(self): request_context = RequestContext.test_context() request_context.message_receipt = MessageReceipt() + request_context.connection_record = async_mock.MagicMock() with async_mock.patch.object( test_module, "PresentationManager", autospec=True @@ -48,7 +57,32 @@ async def test_called_not_ready(self): request_context.connection_ready = False handler = test_module.PresentationAckHandler() responder = MockResponder() - with self.assertRaises(test_module.HandlerException): + with self.assertRaises(test_module.HandlerException) as err: await handler.handle(request_context, responder) + assert err.exception.message == "Connection used for presentation ack not ready" + + assert not responder.messages + + async def test_called_no_connection_no_oob(self): + request_context = RequestContext.test_context() + request_context.message_receipt = MessageReceipt() + + mock_oob_processor = async_mock.MagicMock( + find_oob_record_for_inbound_message=async_mock.CoroutineMock( + # No oob record found + return_value=None + ) + ) + request_context.injector.bind_instance(OobMessageProcessor, mock_oob_processor) + + request_context.message = PresentationAck() + handler = test_module.PresentationAckHandler() + responder = MockResponder() + with self.assertRaises(test_module.HandlerException) as err: + await handler.handle(request_context, responder) + assert ( + err.exception.message + == "No connection or associated connectionless exchange found for presentation ack" + ) assert not responder.messages diff --git a/aries_cloudagent/protocols/present_proof/v1_0/handlers/tests/test_presentation_handler.py b/aries_cloudagent/protocols/present_proof/v1_0/handlers/tests/test_presentation_handler.py index 75fca2e9f7..775bdb068a 100644 --- a/aries_cloudagent/protocols/present_proof/v1_0/handlers/tests/test_presentation_handler.py +++ b/aries_cloudagent/protocols/present_proof/v1_0/handlers/tests/test_presentation_handler.py @@ -2,6 +2,7 @@ from asynctest import mock as async_mock, TestCase as AsyncTestCase +from ......core.oob_processor import OobMessageProcessor from ......messaging.request_context import RequestContext from ......messaging.responder import MockResponder from ......transport.inbound.receipt import MessageReceipt @@ -17,6 +18,14 @@ async def test_called(self): request_context.message_receipt = MessageReceipt() request_context.settings["debug.auto_verify_presentation"] = False + oob_record = async_mock.MagicMock() + mock_oob_processor = async_mock.MagicMock( + find_oob_record_for_inbound_message=async_mock.CoroutineMock( + return_value=oob_record + ) + ) + request_context.injector.bind_instance(OobMessageProcessor, mock_oob_processor) + with async_mock.patch.object( test_module, "PresentationManager", autospec=True ) as mock_pres_mgr: @@ -30,7 +39,7 @@ async def test_called(self): mock_pres_mgr.assert_called_once_with(request_context.profile) mock_pres_mgr.return_value.receive_presentation.assert_called_once_with( - request_context.message, request_context.connection_record + request_context.message, request_context.connection_record, oob_record ) assert not responder.messages @@ -39,6 +48,14 @@ async def test_called_auto_verify(self): request_context.message_receipt = MessageReceipt() request_context.settings["debug.auto_verify_presentation"] = True + oob_record = async_mock.MagicMock() + mock_oob_processor = async_mock.MagicMock( + find_oob_record_for_inbound_message=async_mock.CoroutineMock( + return_value=oob_record + ) + ) + request_context.injector.bind_instance(OobMessageProcessor, mock_oob_processor) + with async_mock.patch.object( test_module, "PresentationManager", autospec=True ) as mock_pres_mgr: @@ -53,7 +70,7 @@ async def test_called_auto_verify(self): mock_pres_mgr.assert_called_once_with(request_context.profile) mock_pres_mgr.return_value.receive_presentation.assert_called_once_with( - request_context.message, request_context.connection_record + request_context.message, request_context.connection_record, oob_record ) assert not responder.messages @@ -62,6 +79,14 @@ async def test_called_auto_verify_x(self): request_context.message_receipt = MessageReceipt() request_context.settings["debug.auto_verify_presentation"] = True + oob_record = async_mock.MagicMock() + mock_oob_processor = async_mock.MagicMock( + find_oob_record_for_inbound_message=async_mock.CoroutineMock( + return_value=oob_record + ) + ) + request_context.injector.bind_instance(OobMessageProcessor, mock_oob_processor) + with async_mock.patch.object( test_module, "PresentationManager", autospec=True ) as mock_pres_mgr: diff --git a/aries_cloudagent/protocols/present_proof/v1_0/handlers/tests/test_presentation_problem_report_handler.py b/aries_cloudagent/protocols/present_proof/v1_0/handlers/tests/test_presentation_problem_report_handler.py index 9d7b7cc925..a726dc349c 100644 --- a/aries_cloudagent/protocols/present_proof/v1_0/handlers/tests/test_presentation_problem_report_handler.py +++ b/aries_cloudagent/protocols/present_proof/v1_0/handlers/tests/test_presentation_problem_report_handler.py @@ -21,6 +21,7 @@ async def test_called(self): with async_mock.patch.object( test_module, "PresentationManager", autospec=True ) as mock_pres_mgr: + request_context.connection_ready = True mock_pres_mgr.return_value.receive_problem_report = ( async_mock.CoroutineMock() ) @@ -48,6 +49,7 @@ async def test_called_x(self): with async_mock.patch.object( test_module, "PresentationManager", autospec=True ) as mock_pres_mgr: + request_context.connection_ready = True mock_pres_mgr.return_value.receive_problem_report = ( async_mock.CoroutineMock( side_effect=test_module.StorageError("Disk full") diff --git a/aries_cloudagent/protocols/present_proof/v1_0/handlers/tests/test_presentation_proposal_handler.py b/aries_cloudagent/protocols/present_proof/v1_0/handlers/tests/test_presentation_proposal_handler.py index 96e7c96e50..a60154533c 100644 --- a/aries_cloudagent/protocols/present_proof/v1_0/handlers/tests/test_presentation_proposal_handler.py +++ b/aries_cloudagent/protocols/present_proof/v1_0/handlers/tests/test_presentation_proposal_handler.py @@ -16,6 +16,7 @@ async def test_called(self): request_context = RequestContext.test_context() request_context.message_receipt = MessageReceipt() request_context.settings["debug.auto_respond_presentation_proposal"] = False + request_context.connection_record = async_mock.MagicMock() with async_mock.patch.object( test_module, "PresentationManager", autospec=True @@ -42,6 +43,7 @@ async def test_called_auto_request(self): request_context.message.comment = "hello world" request_context.message_receipt = MessageReceipt() request_context.settings["debug.auto_respond_presentation_proposal"] = True + request_context.connection_record = async_mock.MagicMock() with async_mock.patch.object( test_module, "PresentationManager", autospec=True @@ -80,6 +82,7 @@ async def test_called_auto_request_x(self): request_context.message.comment = "hello world" request_context.message_receipt = MessageReceipt() request_context.settings["debug.auto_respond_presentation_proposal"] = True + request_context.connection_record = async_mock.MagicMock() with async_mock.patch.object( test_module, "PresentationManager", autospec=True @@ -107,6 +110,7 @@ async def test_called_auto_request_x(self): async def test_called_not_ready(self): request_context = RequestContext.test_context() request_context.message_receipt = MessageReceipt() + request_context.connection_record = async_mock.MagicMock() with async_mock.patch.object( test_module, "PresentationManager", autospec=True @@ -116,7 +120,27 @@ async def test_called_not_ready(self): request_context.connection_ready = False handler = test_module.PresentationProposalHandler() responder = MockResponder() - with self.assertRaises(test_module.HandlerException): + with self.assertRaises(test_module.HandlerException) as err: await handler.handle(request_context, responder) + assert ( + err.exception.message + == "Connection used for presentation proposal not ready" + ) + + assert not responder.messages + + async def test_called_no_connection(self): + request_context = RequestContext.test_context() + request_context.message_receipt = MessageReceipt() + + request_context.message = PresentationProposal() + handler = test_module.PresentationProposalHandler() + responder = MockResponder() + with self.assertRaises(test_module.HandlerException) as err: + await handler.handle(request_context, responder) + assert ( + err.exception.message + == "Connectionless not supported for presentation proposal" + ) assert not responder.messages diff --git a/aries_cloudagent/protocols/present_proof/v1_0/handlers/tests/test_presentation_request_handler.py b/aries_cloudagent/protocols/present_proof/v1_0/handlers/tests/test_presentation_request_handler.py index e8cefb0371..83329461a4 100644 --- a/aries_cloudagent/protocols/present_proof/v1_0/handlers/tests/test_presentation_request_handler.py +++ b/aries_cloudagent/protocols/present_proof/v1_0/handlers/tests/test_presentation_request_handler.py @@ -1,5 +1,8 @@ from asynctest import mock as async_mock, TestCase as AsyncTestCase + +from ......core.oob_processor import OobMessageProcessor +from ......indy.holder import IndyHolder from ......indy.models.pres_preview import ( IndyPresAttrSpec, IndyPresPredSpec, @@ -71,6 +74,13 @@ async def test_called(self): return_value=INDY_PROOF_REQ ) + mock_oob_processor = async_mock.MagicMock( + find_oob_record_for_inbound_message=async_mock.CoroutineMock( + return_value=async_mock.MagicMock() + ) + ) + request_context.injector.bind_instance(OobMessageProcessor, mock_oob_processor) + px_rec_instance = test_module.V10PresentationExchange( presentation_proposal_dict={ "presentation_proposal": { @@ -92,7 +102,6 @@ async def test_called(self): ) as mock_pres_mgr, async_mock.patch.object( test_module, "V10PresentationExchange", autospec=True ) as mock_pres_ex_cls: - mock_pres_ex_cls.retrieve_by_tag_filter = async_mock.CoroutineMock( return_value=px_rec_instance ) @@ -110,6 +119,9 @@ async def test_called(self): mock_pres_mgr.return_value.receive_request.assert_called_once_with( px_rec_instance ) + mock_oob_processor.find_oob_record_for_inbound_message.assert_called_once_with( + request_context + ) assert not responder.messages async def test_called_not_found(self): @@ -122,6 +134,13 @@ async def test_called_not_found(self): return_value=INDY_PROOF_REQ ) + mock_oob_processor = async_mock.MagicMock( + find_oob_record_for_inbound_message=async_mock.CoroutineMock( + return_value=async_mock.MagicMock() + ) + ) + request_context.injector.bind_instance(OobMessageProcessor, mock_oob_processor) + px_rec_instance = test_module.V10PresentationExchange( presentation_proposal_dict={ "presentation_proposal": { @@ -143,7 +162,6 @@ async def test_called_not_found(self): ) as mock_pres_mgr, async_mock.patch.object( test_module, "V10PresentationExchange", autospec=True ) as mock_pres_ex_cls: - mock_pres_ex_cls.retrieve_by_tag_filter = async_mock.CoroutineMock( side_effect=StorageNotFoundError ) @@ -162,6 +180,9 @@ async def test_called_not_found(self): mock_pres_mgr.return_value.receive_request.assert_called_once_with( px_rec_instance ) + mock_oob_processor.find_oob_record_for_inbound_message.assert_called_once_with( + request_context + ) assert not responder.messages async def test_called_auto_present(self): @@ -203,21 +224,25 @@ async def test_called_auto_present(self): presentation_proposal_dict=presentation_proposal, auto_present=True, ) + + mock_oob_processor = async_mock.MagicMock( + find_oob_record_for_inbound_message=async_mock.CoroutineMock( + return_value=async_mock.MagicMock() + ) + ) + mock_holder = async_mock.MagicMock( + get_credentials_for_presentation_request_by_referent=async_mock.CoroutineMock( + return_value=[{"cred_info": {"referent": "dummy"}}] + ) + ) + request_context.injector.bind_instance(OobMessageProcessor, mock_oob_processor) + request_context.injector.bind_instance(IndyHolder, mock_holder) + with async_mock.patch.object( test_module, "PresentationManager", autospec=True ) as mock_pres_mgr, async_mock.patch.object( test_module, "V10PresentationExchange", autospec=True - ) as mock_pres_ex_cls, async_mock.patch.object( - test_module, "IndyHolder", autospec=True - ) as mock_holder: - - mock_holder.get_credentials_for_presentation_request_by_referent = ( - async_mock.CoroutineMock( - return_value=[{"cred_info": {"referent": "dummy"}}] - ) - ) - request_context.inject = async_mock.MagicMock(return_value=mock_holder) - + ) as mock_pres_ex_cls: mock_pres_ex_cls.return_value = px_rec_instance mock_pres_ex_cls.retrieve_by_tag_filter = async_mock.CoroutineMock( return_value=px_rec_instance @@ -238,6 +263,9 @@ async def test_called_auto_present(self): mock_pres_mgr.return_value.receive_request.assert_called_once_with( px_rec_instance ) + mock_oob_processor.find_oob_record_for_inbound_message.assert_called_once_with( + request_context + ) messages = responder.messages assert len(messages) == 1 (result, target) = messages[0] @@ -285,21 +313,26 @@ async def test_called_auto_present_x(self): save_error_state=async_mock.CoroutineMock(), ) - with async_mock.patch.object( - test_module, "PresentationManager", autospec=True - ) as mock_pres_mgr, async_mock.patch.object( - test_module, "V10PresentationExchange", autospec=True - ) as mock_pres_ex_cls, async_mock.patch.object( - test_module, "IndyHolder", autospec=True - ) as mock_holder: - - mock_holder.get_credentials_for_presentation_request_by_referent = ( + mock_oob_processor = async_mock.MagicMock( + find_oob_record_for_inbound_message=async_mock.CoroutineMock( + return_value=async_mock.MagicMock() + ) + ) + mock_holder = async_mock.MagicMock( + get_credentials_for_presentation_request_by_referent=( async_mock.CoroutineMock( return_value=[{"cred_info": {"referent": "dummy"}}] ) ) - request_context.inject = async_mock.MagicMock(return_value=mock_holder) + ) + request_context.injector.bind_instance(OobMessageProcessor, mock_oob_processor) + request_context.injector.bind_instance(IndyHolder, mock_holder) + with async_mock.patch.object( + test_module, "PresentationManager", autospec=True + ) as mock_pres_mgr, async_mock.patch.object( + test_module, "V10PresentationExchange", autospec=True + ) as mock_pres_ex_cls: mock_pres_ex_cls.return_value = mock_px_rec mock_pres_ex_cls.retrieve_by_tag_filter = async_mock.CoroutineMock( return_value=mock_px_rec @@ -356,15 +389,13 @@ async def test_called_auto_present_no_preview(self): request_context.message_receipt = MessageReceipt() px_rec_instance = test_module.V10PresentationExchange(auto_present=True) - with async_mock.patch.object( - test_module, "PresentationManager", autospec=True - ) as mock_pres_mgr, async_mock.patch.object( - test_module, "V10PresentationExchange", autospec=True - ) as mock_pres_ex_cls, async_mock.patch.object( - test_module, "IndyHolder", autospec=True - ) as mock_holder: - - mock_holder.get_credentials_for_presentation_request_by_referent = ( + mock_oob_processor = async_mock.MagicMock( + find_oob_record_for_inbound_message=async_mock.CoroutineMock( + return_value=async_mock.MagicMock() + ) + ) + mock_holder = async_mock.MagicMock( + get_credentials_for_presentation_request_by_referent=( async_mock.CoroutineMock( return_value=[ {"cred_info": {"referent": "dummy-0"}}, @@ -372,8 +403,15 @@ async def test_called_auto_present_no_preview(self): ] ) ) - request_context.inject = async_mock.MagicMock(return_value=mock_holder) + ) + request_context.injector.bind_instance(OobMessageProcessor, mock_oob_processor) + request_context.injector.bind_instance(IndyHolder, mock_holder) + with async_mock.patch.object( + test_module, "PresentationManager", autospec=True + ) as mock_pres_mgr, async_mock.patch.object( + test_module, "V10PresentationExchange", autospec=True + ) as mock_pres_ex_cls: mock_pres_ex_cls.return_value = px_rec_instance mock_pres_ex_cls.retrieve_by_tag_filter = async_mock.CoroutineMock( return_value=px_rec_instance @@ -394,6 +432,9 @@ async def test_called_auto_present_no_preview(self): mock_pres_mgr.return_value.receive_request.assert_called_once_with( px_rec_instance ) + mock_oob_processor.find_oob_record_for_inbound_message.assert_called_once_with( + request_context + ) messages = responder.messages assert len(messages) == 1 (result, target) = messages[0] @@ -428,19 +469,24 @@ async def test_called_auto_present_pred_no_match(self): request_context.message_receipt = MessageReceipt() px_rec_instance = test_module.V10PresentationExchange(auto_present=True) + mock_oob_processor = async_mock.MagicMock( + find_oob_record_for_inbound_message=async_mock.CoroutineMock( + return_value=async_mock.MagicMock() + ) + ) + mock_holder = async_mock.MagicMock( + get_credentials_for_presentation_request_by_referent=( + async_mock.CoroutineMock(return_value=[]) + ) + ) + request_context.injector.bind_instance(OobMessageProcessor, mock_oob_processor) + request_context.injector.bind_instance(IndyHolder, mock_holder) + with async_mock.patch.object( test_module, "PresentationManager", autospec=True ) as mock_pres_mgr, async_mock.patch.object( test_module, "V10PresentationExchange", autospec=True - ) as mock_pres_ex_cls, async_mock.patch.object( - test_module, "IndyHolder", autospec=True - ) as mock_holder: - - mock_holder.get_credentials_for_presentation_request_by_referent = ( - async_mock.CoroutineMock(return_value=[]) - ) - request_context.inject = async_mock.MagicMock(return_value=mock_holder) - + ) as mock_pres_ex_cls: mock_pres_ex_cls.return_value = px_rec_instance mock_pres_ex_cls.retrieve_by_tag_filter = async_mock.CoroutineMock( return_value=px_rec_instance @@ -461,6 +507,9 @@ async def test_called_auto_present_pred_no_match(self): mock_pres_mgr.return_value.receive_request.assert_called_once_with( px_rec_instance ) + mock_oob_processor.find_oob_record_for_inbound_message.assert_called_once_with( + request_context + ) assert not responder.messages async def test_called_auto_present_pred_single_match(self): @@ -491,21 +540,26 @@ async def test_called_auto_present_pred_single_match(self): request_context.message_receipt = MessageReceipt() px_rec_instance = test_module.V10PresentationExchange(auto_present=True) - with async_mock.patch.object( - test_module, "PresentationManager", autospec=True - ) as mock_pres_mgr, async_mock.patch.object( - test_module, "V10PresentationExchange", autospec=True - ) as mock_pres_ex_cls, async_mock.patch.object( - test_module, "IndyHolder", autospec=True - ) as mock_holder: - - mock_holder.get_credentials_for_presentation_request_by_referent = ( + mock_oob_processor = async_mock.MagicMock( + find_oob_record_for_inbound_message=async_mock.CoroutineMock( + return_value=async_mock.MagicMock() + ) + ) + mock_holder = async_mock.MagicMock( + get_credentials_for_presentation_request_by_referent=( async_mock.CoroutineMock( return_value=[{"cred_info": {"referent": "dummy-0"}}] ) ) - request_context.inject = async_mock.MagicMock(return_value=mock_holder) + ) + request_context.injector.bind_instance(OobMessageProcessor, mock_oob_processor) + request_context.injector.bind_instance(IndyHolder, mock_holder) + with async_mock.patch.object( + test_module, "PresentationManager", autospec=True + ) as mock_pres_mgr, async_mock.patch.object( + test_module, "V10PresentationExchange", autospec=True + ) as mock_pres_ex_cls: mock_pres_ex_cls.return_value = px_rec_instance mock_pres_ex_cls.retrieve_by_tag_filter = async_mock.CoroutineMock( return_value=px_rec_instance @@ -526,6 +580,9 @@ async def test_called_auto_present_pred_single_match(self): mock_pres_mgr.return_value.receive_request.assert_called_once_with( px_rec_instance ) + mock_oob_processor.find_oob_record_for_inbound_message.assert_called_once_with( + request_context + ) messages = responder.messages assert len(messages) == 1 (result, target) = messages[0] @@ -560,15 +617,13 @@ async def test_called_auto_present_pred_multi_match(self): request_context.message_receipt = MessageReceipt() px_rec_instance = test_module.V10PresentationExchange(auto_present=True) - with async_mock.patch.object( - test_module, "PresentationManager", autospec=True - ) as mock_pres_mgr, async_mock.patch.object( - test_module, "V10PresentationExchange", autospec=True - ) as mock_pres_ex_cls, async_mock.patch.object( - test_module, "IndyHolder", autospec=True - ) as mock_holder: - - mock_holder.get_credentials_for_presentation_request_by_referent = ( + mock_oob_processor = async_mock.MagicMock( + find_oob_record_for_inbound_message=async_mock.CoroutineMock( + return_value=async_mock.MagicMock() + ) + ) + mock_holder = async_mock.MagicMock( + get_credentials_for_presentation_request_by_referent=( async_mock.CoroutineMock( return_value=[ {"cred_info": {"referent": "dummy-0"}}, @@ -576,8 +631,15 @@ async def test_called_auto_present_pred_multi_match(self): ] ) ) - request_context.inject = async_mock.MagicMock(return_value=mock_holder) + ) + request_context.injector.bind_instance(IndyHolder, mock_holder) + request_context.injector.bind_instance(OobMessageProcessor, mock_oob_processor) + with async_mock.patch.object( + test_module, "PresentationManager", autospec=True + ) as mock_pres_mgr, async_mock.patch.object( + test_module, "V10PresentationExchange", autospec=True + ) as mock_pres_ex_cls: mock_pres_ex_cls.return_value = px_rec_instance mock_pres_ex_cls.retrieve_by_tag_filter = async_mock.CoroutineMock( return_value=px_rec_instance @@ -598,6 +660,9 @@ async def test_called_auto_present_pred_multi_match(self): mock_pres_mgr.return_value.receive_request.assert_called_once_with( px_rec_instance ) + mock_oob_processor.find_oob_record_for_inbound_message.assert_called_once_with( + request_context + ) messages = responder.messages assert len(messages) == 1 (result, target) = messages[0] @@ -652,15 +717,13 @@ async def test_called_auto_present_multi_cred_match_reft(self): auto_present=True, ) - with async_mock.patch.object( - test_module, "PresentationManager", autospec=True - ) as mock_pres_mgr, async_mock.patch.object( - test_module, "V10PresentationExchange", autospec=True - ) as mock_pres_ex_cls, async_mock.patch.object( - test_module, "IndyHolder", autospec=True - ) as mock_holder: - - mock_holder.get_credentials_for_presentation_request_by_referent = ( + mock_oob_processor = async_mock.MagicMock( + find_oob_record_for_inbound_message=async_mock.CoroutineMock( + return_value=async_mock.MagicMock() + ) + ) + mock_holder = async_mock.MagicMock( + get_credentials_for_presentation_request_by_referent=( async_mock.CoroutineMock( return_value=[ { @@ -699,8 +762,15 @@ async def test_called_auto_present_multi_cred_match_reft(self): ] ) ) - request_context.inject = async_mock.MagicMock(return_value=mock_holder) + ) + request_context.injector.bind_instance(OobMessageProcessor, mock_oob_processor) + request_context.injector.bind_instance(IndyHolder, mock_holder) + with async_mock.patch.object( + test_module, "PresentationManager", autospec=True + ) as mock_pres_mgr, async_mock.patch.object( + test_module, "V10PresentationExchange", autospec=True + ) as mock_pres_ex_cls: mock_pres_ex_cls.return_value = px_rec_instance mock_pres_ex_cls.retrieve_by_tag_filter = async_mock.CoroutineMock( return_value=px_rec_instance @@ -721,6 +791,9 @@ async def test_called_auto_present_multi_cred_match_reft(self): mock_pres_mgr.return_value.receive_request.assert_called_once_with( px_rec_instance ) + mock_oob_processor.find_oob_record_for_inbound_message.assert_called_once_with( + request_context + ) messages = responder.messages assert len(messages) == 1 (result, target) = messages[0] @@ -766,45 +839,51 @@ async def test_called_auto_present_bait_and_switch(self): auto_present=True, ) + by_reft = async_mock.CoroutineMock( + return_value=[ + { + "cred_info": { + "referent": "dummy-0", + "cred_def_id": CD_ID, + "attrs": {"ident": "zero", "favourite": "yam"}, + } + }, + { + "cred_info": { + "referent": "dummy-1", + "cred_def_id": CD_ID, + "attrs": {"ident": "one", "favourite": "turnip"}, + } + }, + { + "cred_info": { + "referent": "dummy-2", + "cred_def_id": CD_ID, + "attrs": { + "ident": "two", + "favourite": "the idea of a potato but not a potato", + }, + } + }, + ] + ) + mock_holder = async_mock.MagicMock( + get_credentials_for_presentation_request_by_referent=by_reft + ) + request_context.injector.bind_instance(IndyHolder, mock_holder) + + mock_oob_processor = async_mock.MagicMock( + find_oob_record_for_inbound_message=async_mock.CoroutineMock( + return_value=async_mock.MagicMock() + ) + ) + request_context.injector.bind_instance(OobMessageProcessor, mock_oob_processor) + with async_mock.patch.object( test_module, "PresentationManager", autospec=True ) as mock_pres_mgr, async_mock.patch.object( test_module, "V10PresentationExchange", autospec=True - ) as mock_pres_ex_cls, async_mock.patch.object( - test_module, "IndyHolder", autospec=True - ) as mock_holder: - - by_reft = async_mock.CoroutineMock( - return_value=[ - { - "cred_info": { - "referent": "dummy-0", - "cred_def_id": CD_ID, - "attrs": {"ident": "zero", "favourite": "yam"}, - } - }, - { - "cred_info": { - "referent": "dummy-1", - "cred_def_id": CD_ID, - "attrs": {"ident": "one", "favourite": "turnip"}, - } - }, - { - "cred_info": { - "referent": "dummy-2", - "cred_def_id": CD_ID, - "attrs": { - "ident": "two", - "favourite": "the idea of a potato but not a potato", - }, - } - }, - ] - ) - mock_holder.get_credentials_for_presentation_request_by_referent = by_reft - request_context.inject = async_mock.MagicMock(return_value=mock_holder) - + ) as mock_pres_ex_cls: mock_pres_ex_cls.return_value = px_rec_instance mock_pres_ex_cls.retrieve_by_tag_filter = async_mock.CoroutineMock( return_value=px_rec_instance @@ -826,11 +905,15 @@ async def test_called_auto_present_bait_and_switch(self): mock_pres_mgr.return_value.receive_request.assert_called_once_with( px_rec_instance ) + mock_oob_processor.find_oob_record_for_inbound_message.assert_called_once_with( + request_context + ) assert not responder.messages async def test_called_not_ready(self): request_context = RequestContext.test_context() request_context.message_receipt = MessageReceipt() + request_context.connection_record = async_mock.MagicMock() with async_mock.patch.object( test_module, "PresentationManager", autospec=True @@ -840,7 +923,35 @@ async def test_called_not_ready(self): request_context.connection_ready = False handler = test_module.PresentationRequestHandler() responder = MockResponder() - with self.assertRaises(test_module.HandlerException): + with self.assertRaises(test_module.HandlerException) as err: await handler.handle(request_context, responder) + assert ( + err.exception.message + == "Connection used for presentation request not ready" + ) + + assert not responder.messages + + async def test_no_conn_no_oob(self): + request_context = RequestContext.test_context() + request_context.message_receipt = MessageReceipt() + + mock_oob_processor = async_mock.MagicMock( + find_oob_record_for_inbound_message=async_mock.CoroutineMock( + # No oob record found + return_value=None + ) + ) + request_context.injector.bind_instance(OobMessageProcessor, mock_oob_processor) + + request_context.message = PresentationRequest() + handler = test_module.PresentationRequestHandler() + responder = MockResponder() + with self.assertRaises(test_module.HandlerException) as err: + await handler.handle(request_context, responder) + assert ( + err.exception.message + == "No connection or associated connectionless exchange found for presentation request" + ) assert not responder.messages diff --git a/aries_cloudagent/protocols/present_proof/v1_0/manager.py b/aries_cloudagent/protocols/present_proof/v1_0/manager.py index 50cf219c41..a929949fc6 100644 --- a/aries_cloudagent/protocols/present_proof/v1_0/manager.py +++ b/aries_cloudagent/protocols/present_proof/v1_0/manager.py @@ -2,7 +2,9 @@ import json import logging +from typing import Optional +from ...out_of_band.v1_0.models.oob_record import OobRecord from ....connections.models.conn_record import ConnRecord from ....core.error import BaseError from ....core.profile import Profile @@ -10,7 +12,6 @@ from ....messaging.decorators.attach_decorator import AttachDecorator from ....messaging.responder import BaseResponder from ....storage.error import StorageNotFoundError - from ..indy.pres_exch_handler import IndyPresExchHandler from .messages.presentation_ack import PresentationAck @@ -165,7 +166,10 @@ async def create_bound_request( return presentation_exchange_record, presentation_request_message async def create_exchange_for_request( - self, connection_id: str, presentation_request_message: PresentationRequest + self, + connection_id: str, + presentation_request_message: PresentationRequest, + auto_verify: bool = None, ): """ Create a presentation exchange record for input presentation request. @@ -187,6 +191,7 @@ async def create_exchange_for_request( state=V10PresentationExchange.STATE_REQUEST_SENT, presentation_request=presentation_request_message.indy_proof_request(), presentation_request_dict=presentation_request_message, + auto_verify=auto_verify, trace=(presentation_request_message._trace is not None), ) async with self._profile.session() as session: @@ -276,7 +281,10 @@ async def create_presentation( ], ) - presentation_message._thread = {"thid": presentation_exchange_record.thread_id} + # Assign thid (and optionally pthid) to message + presentation_message.assign_thread_from( + presentation_exchange_record.presentation_request_dict + ) presentation_message.assign_trace_decorator( self._profile.settings, presentation_exchange_record.trace ) @@ -294,7 +302,10 @@ async def create_presentation( return presentation_exchange_record, presentation_message async def receive_presentation( - self, message: Presentation, connection_record: ConnRecord + self, + message: Presentation, + connection_record: Optional[ConnRecord], + oob_record: Optional[OobRecord], ): """ Receive a presentation, from message in context on manager creation. @@ -306,25 +317,33 @@ async def receive_presentation( presentation = message.indy_proof() thread_id = message._thread_id - connection_id_filter = ( - {"connection_id": connection_record.connection_id} - if connection_record is not None + # Normally we only set the connection_id to None if an oob record is present + # But present proof supports the old-style AIP-1 connectionless exchange that + # bypasses the oob record. So we can't verify if an oob record is associated with + # the exchange because it is possible that there is None + connection_id = ( + None + if oob_record + else connection_record.connection_id + if connection_record else None ) + async with self._profile.session() as session: - try: - ( - presentation_exchange_record - ) = await V10PresentationExchange.retrieve_by_tag_filter( - session, {"thread_id": thread_id}, connection_id_filter - ) - except StorageNotFoundError: - # Proof Request not bound to any connection: requests_attach in OOB msg - ( - presentation_exchange_record - ) = await V10PresentationExchange.retrieve_by_tag_filter( - session, {"thread_id": thread_id}, None + # Find by thread_id and role. Verify connection id later + presentation_exchange_record = ( + await V10PresentationExchange.retrieve_by_tag_filter( + session, + {"thread_id": thread_id}, + { + "role": V10PresentationExchange.ROLE_VERIFIER, + }, ) + ) + + # Save connection id (if it wasn't already present) + if connection_record: + presentation_exchange_record.connection_id = connection_record.connection_id # Check for bait-and-switch in presented attribute values vs. proposal if presentation_exchange_record.presentation_proposal_dict: @@ -334,7 +353,7 @@ async def receive_presentation( presentation_preview = exchange_pres_proposal.presentation_proposal proof_req = presentation_exchange_record._presentation_request.ser - for (reft, attr_spec) in presentation["requested_proof"][ + for reft, attr_spec in presentation["requested_proof"][ "revealed_attrs" ].items(): name = proof_req["requested_attributes"][reft]["name"] @@ -346,7 +365,9 @@ async def receive_presentation( name=name, value=value, ): - presentation_exchange_record.state = None + presentation_exchange_record.state = ( + V10PresentationExchange.STATE_ABANDONED + ) async with self._profile.session() as session: await presentation_exchange_record.save( session, @@ -371,7 +392,9 @@ async def receive_presentation( return presentation_exchange_record async def verify_presentation( - self, presentation_exchange_record: V10PresentationExchange + self, + presentation_exchange_record: V10PresentationExchange, + responder: Optional[BaseResponder] = None, ): """ Verify a presentation. @@ -395,18 +418,18 @@ async def verify_presentation( ) = await indy_handler.process_pres_identifiers(indy_proof["identifiers"]) verifier = self._profile.inject(IndyVerifier) - presentation_exchange_record.verified = json.dumps( # tag: needs string value - await verifier.verify_presentation( - dict( - indy_proof_request - ), # copy to avoid changing the proof req in the stored pres exch - indy_proof, - schemas, - cred_defs, - rev_reg_defs, - rev_reg_entries, - ) + (verified_bool, verified_msgs) = await verifier.verify_presentation( + dict( + indy_proof_request + ), # copy to avoid changing the proof req in the stored pres exch + indy_proof, + schemas, + cred_defs, + rev_reg_defs, + rev_reg_entries, ) + presentation_exchange_record.verified = json.dumps(verified_bool) + presentation_exchange_record.verified_msgs = list(set(verified_msgs)) presentation_exchange_record.state = V10PresentationExchange.STATE_VERIFIED async with self._profile.session() as session: @@ -414,11 +437,13 @@ async def verify_presentation( session, reason="verify presentation" ) - await self.send_presentation_ack(presentation_exchange_record) + await self.send_presentation_ack(presentation_exchange_record, responder) return presentation_exchange_record async def send_presentation_ack( - self, presentation_exchange_record: V10PresentationExchange + self, + presentation_exchange_record: V10PresentationExchange, + responder: Optional[BaseResponder] = None, ): """ Send acknowledgement of presentation receipt. @@ -427,10 +452,32 @@ async def send_presentation_ack( presentation_exchange_record: presentation exchange record with thread id """ - responder = self._profile.inject_or(BaseResponder) + responder = responder or self._profile.inject_or(BaseResponder) + + if not presentation_exchange_record.connection_id: + # Find associated oob record. If this presentation exchange is created + # without oob (aip1 style connectionless) we can't send a presentation ack + # because we don't have their service + try: + async with self._profile.session() as session: + await OobRecord.retrieve_by_tag_filter( + session, + {"attach_thread_id": presentation_exchange_record.thread_id}, + ) + except StorageNotFoundError: + # This can happen in AIP1 style connectionless exchange. ACA-PY only + # supported this for receiving a presentation + LOGGER.error( + "Unable to send connectionless presentation ack without associated " + "oob record. This can happen if proof request was sent without " + "wrapping it in an out of band invitation (AIP1-style)." + ) + return if responder: - presentation_ack_message = PresentationAck() + presentation_ack_message = PresentationAck( + verification_result=presentation_exchange_record.verified + ) presentation_ack_message._thread = { "thid": presentation_exchange_record.thread_id } @@ -440,6 +487,7 @@ async def send_presentation_ack( await responder.send_reply( presentation_ack_message, + # connection_id can be none in case of connectionless connection_id=presentation_exchange_record.connection_id, ) else: @@ -449,7 +497,7 @@ async def send_presentation_ack( ) async def receive_presentation_ack( - self, message: PresentationAck, connection_record: ConnRecord + self, message: PresentationAck, connection_record: Optional[ConnRecord] ): """ Receive a presentation ack, from message in context on manager creation. @@ -458,15 +506,21 @@ async def receive_presentation_ack( presentation exchange record, retrieved and updated """ + connection_id = connection_record.connection_id if connection_record else None + async with self._profile.session() as session: ( presentation_exchange_record ) = await V10PresentationExchange.retrieve_by_tag_filter( session, {"thread_id": message._thread_id}, - {"connection_id": connection_record.connection_id}, + { + # connection_id can be null in connectionless + "connection_id": connection_id, + "role": V10PresentationExchange.ROLE_PROVER, + }, ) - + presentation_exchange_record.verified = message._verification_result presentation_exchange_record.state = ( V10PresentationExchange.STATE_PRESENTATION_ACKED ) @@ -489,15 +543,13 @@ async def receive_problem_report( """ # FIXME use transaction, fetch for_update async with self._profile.session() as session: - pres_ex_record = await ( - V10PresentationExchange.retrieve_by_tag_filter( - session, - {"thread_id": message._thread_id}, - {"connection_id": connection_id}, - ) + pres_ex_record = await V10PresentationExchange.retrieve_by_tag_filter( + session, + {"thread_id": message._thread_id}, + {"connection_id": connection_id}, ) - pres_ex_record.state = None + pres_ex_record.state = V10PresentationExchange.STATE_ABANDONED code = message.description.get("code", ProblemReportReason.ABANDONED.value) pres_ex_record.error_msg = f"{code}: {message.description.get('en', code)}" await pres_ex_record.save(session, reason="received problem report") diff --git a/aries_cloudagent/protocols/present_proof/v1_0/messages/presentation_ack.py b/aries_cloudagent/protocols/present_proof/v1_0/messages/presentation_ack.py index 36f181d56e..98ad5d2ef3 100644 --- a/aries_cloudagent/protocols/present_proof/v1_0/messages/presentation_ack.py +++ b/aries_cloudagent/protocols/present_proof/v1_0/messages/presentation_ack.py @@ -1,6 +1,6 @@ """Represents an explicit RFC 15 ack message, adopted into present-proof protocol.""" -from marshmallow import EXCLUDE +from marshmallow import EXCLUDE, fields, validate from ....notification.v1_0.messages.ack import V10Ack, V10AckSchema @@ -21,7 +21,7 @@ class Meta: message_type = PRESENTATION_ACK schema_class = "PresentationAckSchema" - def __init__(self, status: str = None, **kwargs): + def __init__(self, status: str = None, verification_result: str = None, **kwargs): """ Initialize an explicit ack message instance. @@ -30,6 +30,7 @@ def __init__(self, status: str = None, **kwargs): """ super().__init__(status, **kwargs) + self._verification_result = verification_result class PresentationAckSchema(V10AckSchema): @@ -40,3 +41,10 @@ class Meta: model_class = PresentationAck unknown = EXCLUDE + + verification_result = fields.Str( + required=False, + description="Whether presentation is verified: true or false", + example="true", + validate=validate.OneOf(["true", "false"]), + ) diff --git a/aries_cloudagent/protocols/present_proof/v1_0/messages/presentation_webhook.py b/aries_cloudagent/protocols/present_proof/v1_0/messages/presentation_webhook.py new file mode 100644 index 0000000000..0a58f25e71 --- /dev/null +++ b/aries_cloudagent/protocols/present_proof/v1_0/messages/presentation_webhook.py @@ -0,0 +1,39 @@ +"""v1.0 presentation exchange information webhook.""" + + +class V10PresentationExchangeWebhook: + """Class representing a state only presentation exchange webhook.""" + + __acceptable_keys_list = [ + "connection_id", + "presentation_exchange_id", + "role", + "initiator", + "auto_present", + "auto_verify", + "error_msg", + "state", + "thread_id", + "trace", + "verified", + "verified_msgs", + "created_at", + "updated_at", + ] + + def __init__( + self, + **kwargs, + ): + """ + Initialize webhook object from V10PresentationExchange. + + from a list of accepted attributes. + """ + [ + self.__setattr__(key, kwargs.get(key)) + for key in self.__acceptable_keys_list + if kwargs.get(key) is not None + ] + if kwargs.get("_id") is not None: + self.presentation_exchange_id = kwargs.get("_id") diff --git a/aries_cloudagent/protocols/present_proof/v1_0/models/presentation_exchange.py b/aries_cloudagent/protocols/present_proof/v1_0/models/presentation_exchange.py index ef13ad5b89..98a49bed34 100644 --- a/aries_cloudagent/protocols/present_proof/v1_0/models/presentation_exchange.py +++ b/aries_cloudagent/protocols/present_proof/v1_0/models/presentation_exchange.py @@ -2,7 +2,7 @@ import logging -from typing import Any, Mapping, Union +from typing import Any, Mapping, Optional, Union from marshmallow import fields, validate @@ -21,6 +21,7 @@ PresentationRequest, PresentationRequestSchema, ) +from ..messages.presentation_webhook import V10PresentationExchangeWebhook from . import UNENCRYPTED_TAGS @@ -54,12 +55,13 @@ class Meta: STATE_PRESENTATION_RECEIVED = "presentation_received" STATE_VERIFIED = "verified" STATE_PRESENTATION_ACKED = "presentation_acked" + STATE_ABANDONED = "abandoned" def __init__( self, *, presentation_exchange_id: str = None, - connection_id: str = None, + connection_id: Optional[str] = None, thread_id: str = None, initiator: str = None, role: str = None, @@ -73,7 +75,9 @@ def __init__( ] = None, # aries message presentation: Union[IndyProof, Mapping] = None, # indy proof verified: str = None, + verified_msgs: list = None, auto_present: bool = False, + auto_verify: bool = False, error_msg: str = None, trace: bool = False, # backward compat: BaseRecord.from_storage() **kwargs, @@ -94,7 +98,9 @@ def __init__( ) self._presentation = IndyProof.serde(presentation) self.verified = verified + self.verified_msgs = verified_msgs self.auto_present = auto_present + self.auto_verify = auto_verify self.error_msg = error_msg @property @@ -158,6 +164,7 @@ async def save_error_state( self, session: ProfileSession, *, + state: str = None, reason: str = None, log_params: Mapping[str, Any] = None, log_override: bool = False, @@ -172,10 +179,10 @@ async def save_error_state( override: Override configured logging regimen, print to stderr instead """ - if self._last_state is None: # already done + if self._last_state == state: # already done return - self.state = None + self.state = state or V10PresentationExchange.STATE_ABANDONED if reason: self.error_msg = reason @@ -189,6 +196,33 @@ async def save_error_state( except StorageError as err: LOGGER.exception(err) + # Override + async def emit_event(self, session: ProfileSession, payload: Any = None): + """ + Emit an event. + + Args: + session: The profile session to use + payload: The event payload + """ + + if not self.RECORD_TOPIC: + return + + if self.state: + topic = f"{self.EVENT_NAMESPACE}::{self.RECORD_TOPIC}::{self.state}" + else: + topic = f"{self.EVENT_NAMESPACE}::{self.RECORD_TOPIC}" + + if session.profile.settings.get("debug.webhooks"): + if not payload: + payload = self.serialize() + else: + payload = V10PresentationExchangeWebhook(**self.__dict__) + payload = payload.__dict__ + + await session.profile.notify(topic, payload) + @property def record_value(self) -> Mapping: """Accessor for the JSON record value generated for this credential exchange.""" @@ -201,8 +235,10 @@ def record_value(self) -> Mapping: "role", "state", "auto_present", + "auto_verify", "error_msg", "verified", + "verified_msgs", "trace", ) }, @@ -290,11 +326,21 @@ class Meta: example="true", validate=validate.OneOf(["true", "false"]), ) + verified_msgs = fields.List( + fields.Str( + required=False, + description="Proof verification warning or error information", + ), + required=False, + ) auto_present = fields.Bool( required=False, description="Prover choice to auto-present proof as verifier requests", example=False, ) + auto_verify = fields.Bool( + required=False, description="Verifier choice to auto-verify proof presentation" + ) error_msg = fields.Str( required=False, description="Error message", example="Invalid structure" ) diff --git a/aries_cloudagent/protocols/present_proof/v1_0/models/tests/test_record.py b/aries_cloudagent/protocols/present_proof/v1_0/models/tests/test_record.py index 62d5c64884..13d9e5aac3 100644 --- a/aries_cloudagent/protocols/present_proof/v1_0/models/tests/test_record.py +++ b/aries_cloudagent/protocols/present_proof/v1_0/models/tests/test_record.py @@ -110,8 +110,10 @@ async def test_record(self): "role": None, "state": None, "auto_present": True, + "auto_verify": False, "error_msg": None, "verified": None, + "verified_msgs": None, "trace": False, } diff --git a/aries_cloudagent/protocols/present_proof/v1_0/routes.py b/aries_cloudagent/protocols/present_proof/v1_0/routes.py index ddc90d4ef3..997534f678 100644 --- a/aries_cloudagent/protocols/present_proof/v1_0/routes.py +++ b/aries_cloudagent/protocols/present_proof/v1_0/routes.py @@ -36,7 +36,7 @@ from ....wallet.error import WalletNotFoundError from . import problem_report_for_record, report_problem -from .manager import PresentationManager +from .manager import PresentationManager, PresentationManagerError from .message_types import ATTACH_DECO_IDS, PRESENTATION_REQUEST, SPEC_URI from .messages.presentation_problem_report import ProblemReportReason from .messages.presentation_proposal import PresentationProposal @@ -130,6 +130,11 @@ class V10PresentationCreateRequestRequestSchema(AdminAPIMessageTracingSchema): proof_request = fields.Nested(IndyProofRequestSchema(), required=True) comment = fields.Str(required=False, allow_none=True) + auto_verify = fields.Bool( + description="Verifier choice to auto-verify proof presentation", + required=False, + example=False, + ) trace = fields.Bool( description="Whether to trace event (default false)", required=False, @@ -147,6 +152,21 @@ class V10PresentationSendRequestRequestSchema( ) +class V10PresentationSendRequestToProposalSchema(AdminAPIMessageTracingSchema): + """Request schema for sending a proof request bound to a proposal.""" + + auto_verify = fields.Bool( + description="Verifier choice to auto-verify proof presentation", + required=False, + example=False, + ) + trace = fields.Bool( + description="Whether to trace event (default false)", + required=False, + example=False, + ) + + class CredentialsFetchQueryStringSchema(OpenAPISchema): """Parameters and validators for credentials fetch request query string.""" @@ -457,7 +477,6 @@ async def presentation_exchange_create_request(request: web.BaseRequest): context: AdminRequestContext = request["context"] profile = context.profile - outbound_handler = request["outbound_message_router"] body = await request.json() @@ -475,6 +494,10 @@ async def presentation_exchange_create_request(request: web.BaseRequest): ) ], ) + presentation_request_message.assign_thread_id(body.get("thread_id")) + auto_verify = body.get( + "auto_verify", context.settings.get("debug.auto_verify_presentation") + ) trace_msg = body.get("trace") presentation_request_message.assign_trace_decorator( context.settings, @@ -487,6 +510,7 @@ async def presentation_exchange_create_request(request: web.BaseRequest): pres_ex_record = await presentation_manager.create_exchange_for_request( connection_id=None, presentation_request_message=presentation_request_message, + auto_verify=auto_verify, ) result = pres_ex_record.serialize() except (BaseModelError, StorageError) as err: @@ -496,8 +520,6 @@ async def presentation_exchange_create_request(request: web.BaseRequest): # other party does not care about our false protocol start raise web.HTTPBadRequest(reason=err.roll_up) - await outbound_handler(presentation_request_message, connection_id=None) - trace_event( context.settings, presentation_request_message, @@ -557,11 +579,15 @@ async def presentation_exchange_send_free_request(request: web.BaseRequest): ) ], ) + presentation_request_message.assign_thread_id(body.get("thread_id")) trace_msg = body.get("trace") presentation_request_message.assign_trace_decorator( context.settings, trace_msg, ) + auto_verify = body.get( + "auto_verify", context.settings.get("debug.auto_verify_presentation") + ) pres_ex_record = None try: @@ -569,6 +595,7 @@ async def presentation_exchange_send_free_request(request: web.BaseRequest): pres_ex_record = await presentation_manager.create_exchange_for_request( connection_id=connection_id, presentation_request_message=presentation_request_message, + auto_verify=auto_verify, ) result = pres_ex_record.serialize() except (BaseModelError, StorageError) as err: @@ -595,7 +622,7 @@ async def presentation_exchange_send_free_request(request: web.BaseRequest): summary="Sends a presentation request in reference to a proposal", ) @match_info_schema(V10PresExIdMatchInfoSchema()) -@request_schema(AdminAPIMessageTracingSchema()) +@request_schema(V10PresentationSendRequestToProposalSchema()) @response_schema(V10PresentationExchangeSchema(), 200, description="") async def presentation_exchange_send_bound_request(request: web.BaseRequest): """ @@ -644,6 +671,9 @@ async def presentation_exchange_send_bound_request(request: web.BaseRequest): if not connection_record.is_ready: raise web.HTTPForbidden(reason=f"Connection {conn_id} not ready") + pres_ex_record.auto_verify = body.get( + "auto_verify", context.settings.get("debug.auto_verify_presentation") + ) try: presentation_manager = PresentationManager(profile) ( @@ -722,14 +752,20 @@ async def presentation_exchange_send_presentation(request: web.BaseRequest): ) ) - connection_id = pres_ex_record.connection_id - try: - connection_record = await ConnRecord.retrieve_by_id(session, connection_id) - except StorageNotFoundError as err: - raise web.HTTPBadRequest(reason=err.roll_up) from err + # Fetch connection if exchange has record + connection_record = None + if pres_ex_record.connection_id: + try: + connection_record = await ConnRecord.retrieve_by_id( + session, pres_ex_record.connection_id + ) + except StorageNotFoundError as err: + raise web.HTTPBadRequest(reason=err.roll_up) from err - if not connection_record.is_ready: - raise web.HTTPForbidden(reason=f"Connection {connection_id} not ready") + if connection_record and not connection_record.is_ready: + raise web.HTTPForbidden( + reason=f"Connection {connection_record.connection_id} not ready" + ) try: presentation_manager = PresentationManager(profile) @@ -770,7 +806,9 @@ async def presentation_exchange_send_presentation(request: web.BaseRequest): context.settings, trace_msg, ) - await outbound_handler(presentation_message, connection_id=connection_id) + await outbound_handler( + presentation_message, connection_id=pres_ex_record.connection_id + ) trace_event( context.settings, @@ -840,6 +878,8 @@ async def presentation_exchange_verify_presentation(request: web.BaseRequest): pres_ex_record, outbound_handler, ) + except PresentationManagerError as err: + return web.HTTPBadRequest(reason=err.roll_up) trace_event( context.settings, diff --git a/aries_cloudagent/protocols/present_proof/v1_0/tests/test_manager.py b/aries_cloudagent/protocols/present_proof/v1_0/tests/test_manager.py index 4e0d9d3580..044c21d317 100644 --- a/aries_cloudagent/protocols/present_proof/v1_0/tests/test_manager.py +++ b/aries_cloudagent/protocols/present_proof/v1_0/tests/test_manager.py @@ -4,6 +4,10 @@ from asynctest import mock as async_mock, TestCase as AsyncTestCase +from aries_cloudagent.protocols.issue_credential.v1_0.models.credential_exchange import ( + V10CredentialExchange, +) + from .....core.in_memory import InMemoryProfile from .....indy.holder import IndyHolder, IndyHolderError from .....indy.issuer import IndyIssuer @@ -313,7 +317,7 @@ async def setUp(self): Verifier = async_mock.MagicMock(IndyVerifier, autospec=True) self.verifier = Verifier() self.verifier.verify_presentation = async_mock.CoroutineMock( - return_value="true" + return_value=("true", []) ) injector.bind_instance(IndyVerifier, self.verifier) @@ -951,22 +955,24 @@ async def test_receive_presentation(self): "session", async_mock.MagicMock(return_value=self.profile.session()), ) as session: - retrieve_ex.side_effect = [ - StorageNotFoundError("no such record"), - exchange_dummy, - ] + retrieve_ex.side_effect = [exchange_dummy] exchange_out = await self.manager.receive_presentation( - PRES, connection_record + PRES, connection_record, None + ) + retrieve_ex.assert_called_once_with( + session.return_value, + {"thread_id": "dummy"}, + { + "role": V10PresentationExchange.ROLE_VERIFIER, + "connection_id": CONN_ID, + }, ) - assert retrieve_ex.call_count == 2 save_ex.assert_called_once() assert exchange_out.state == ( V10PresentationExchange.STATE_PRESENTATION_RECEIVED ) async def test_receive_presentation_oob(self): - connection_record = async_mock.MagicMock(connection_id=CONN_ID) - exchange_dummy = V10PresentationExchange( presentation_proposal_dict={ "presentation_proposal": { @@ -1058,10 +1064,17 @@ async def test_receive_presentation_oob(self): V10PresentationExchange, "save", autospec=True ) as save_ex, async_mock.patch.object( V10PresentationExchange, "retrieve_by_tag_filter", autospec=True - ) as retrieve_ex: - retrieve_ex.side_effect = [StorageNotFoundError(), exchange_dummy] - exchange_out = await self.manager.receive_presentation( - PRES, connection_record + ) as retrieve_ex, async_mock.patch.object( + self.profile, + "session", + async_mock.MagicMock(return_value=self.profile.session()), + ) as session: + retrieve_ex.side_effect = [exchange_dummy] + exchange_out = await self.manager.receive_presentation(PRES, None, None) + retrieve_ex.assert_called_once_with( + session.return_value, + {"thread_id": "dummy"}, + {"role": V10PresentationExchange.ROLE_VERIFIER, "connection_id": None}, ) assert exchange_out.state == ( V10PresentationExchange.STATE_PRESENTATION_RECEIVED @@ -1164,7 +1177,7 @@ async def test_receive_presentation_bait_and_switch(self): ) as retrieve_ex: retrieve_ex.return_value = exchange_dummy with self.assertRaises(PresentationManagerError): - await self.manager.receive_presentation(PRES, connection_record) + await self.manager.receive_presentation(PRES, connection_record, None) async def test_receive_presentation_connectionless(self): exchange_dummy = V10PresentationExchange() @@ -1179,9 +1192,11 @@ async def test_receive_presentation_connectionless(self): async_mock.MagicMock(return_value=self.profile.session()), ) as session: retrieve_ex.return_value = exchange_dummy - exchange_out = await self.manager.receive_presentation(PRES, None) + exchange_out = await self.manager.receive_presentation(PRES, None, None) retrieve_ex.assert_called_once_with( - session.return_value, {"thread_id": PRES._thread_id}, None + session.return_value, + {"thread_id": PRES._thread_id}, + {"role": V10PresentationExchange.ROLE_VERIFIER, "connection_id": None}, ) save_ex.assert_called_once() @@ -1251,7 +1266,7 @@ async def test_verify_presentation_with_revocation(self): """ async def test_send_presentation_ack(self): - exchange = V10PresentationExchange() + exchange = V10PresentationExchange(connection_id="dummy") responder = MockResponder() self.profile.context.injector.bind_instance(BaseResponder, responder) @@ -1260,13 +1275,33 @@ async def test_send_presentation_ack(self): messages = responder.messages assert len(messages) == 1 + async def test_send_presentation_ack_oob(self): + exchange = V10PresentationExchange(thread_id="some-thread-id") + + responder = MockResponder() + self.profile.context.injector.bind_instance(BaseResponder, responder) + + with async_mock.patch.object( + test_module.OobRecord, "retrieve_by_tag_filter" + ) as mock_retrieve_oob, async_mock.patch.object( + self.profile, + "session", + async_mock.MagicMock(return_value=self.profile.session()), + ) as session: + await self.manager.send_presentation_ack(exchange) + messages = responder.messages + mock_retrieve_oob.assert_called_once_with( + session.return_value, {"attach_thread_id": "some-thread-id"} + ) + assert len(messages) == 1 + async def test_send_presentation_ack_no_responder(self): exchange = V10PresentationExchange() self.profile.context.injector.clear_binding(BaseResponder) await self.manager.send_presentation_ack(exchange) - async def test_receive_presentation_ack(self): + async def test_receive_presentation_ack_a(self): connection_record = async_mock.MagicMock(connection_id=CONN_ID) exchange_dummy = V10PresentationExchange() @@ -1287,6 +1322,28 @@ async def test_receive_presentation_ack(self): V10PresentationExchange.STATE_PRESENTATION_ACKED ) + async def test_receive_presentation_ack_b(self): + connection_record = async_mock.MagicMock(connection_id=CONN_ID) + + exchange_dummy = V10PresentationExchange() + message = async_mock.MagicMock(_verification_result="true") + + with async_mock.patch.object( + V10PresentationExchange, "save", autospec=True + ) as save_ex, async_mock.patch.object( + V10PresentationExchange, "retrieve_by_tag_filter", autospec=True + ) as retrieve_ex: + retrieve_ex.return_value = exchange_dummy + exchange_out = await self.manager.receive_presentation_ack( + message, connection_record + ) + save_ex.assert_called_once() + + assert exchange_out.state == ( + V10PresentationExchange.STATE_PRESENTATION_ACKED + ) + assert exchange_out.verified == "true" + async def test_receive_problem_report(self): connection_id = "connection-id" stored_exchange = V10PresentationExchange( @@ -1327,7 +1384,7 @@ async def test_receive_problem_report(self): ) save_ex.assert_called_once() - assert ret_exchange.state is None + assert ret_exchange.state == V10CredentialExchange.STATE_ABANDONED async def test_receive_problem_report_x(self): connection_id = "connection-id" diff --git a/aries_cloudagent/protocols/present_proof/v1_0/tests/test_routes.py b/aries_cloudagent/protocols/present_proof/v1_0/tests/test_routes.py index 41424eb20e..77e3ea1ca7 100644 --- a/aries_cloudagent/protocols/present_proof/v1_0/tests/test_routes.py +++ b/aries_cloudagent/protocols/present_proof/v1_0/tests/test_routes.py @@ -67,7 +67,6 @@ async def test_presentation_exchange_list(self): ), autospec=True, ) as mock_presentation_exchange: - # Since we are mocking import importlib.reload(test_module) @@ -101,7 +100,6 @@ async def test_presentation_exchange_list_x(self): ), autospec=True, ) as mock_presentation_exchange: - # Since we are mocking import importlib.reload(test_module) @@ -122,7 +120,6 @@ async def test_presentation_exchange_credentials_list_not_found(self): ), autospec=True, ) as mock_presentation_exchange: - # Since we are mocking import importlib.reload(test_module) @@ -158,7 +155,6 @@ async def test_presentation_exchange_credentials_x(self): ), autospec=True, ) as mock_presentation_exchange: - # Since we are mocking import importlib.reload(test_module) @@ -191,7 +187,6 @@ async def test_presentation_exchange_credentials_list_single_referent(self): ), autospec=True, ) as mock_presentation_exchange: - # Since we are mocking import importlib.reload(test_module) @@ -229,7 +224,6 @@ async def test_presentation_exchange_credentials_list_multiple_referents(self): ), autospec=True, ) as mock_presentation_exchange: - # Since we are mocking import importlib.reload(test_module) @@ -253,7 +247,6 @@ async def test_presentation_exchange_retrieve(self): ), autospec=True, ) as mock_pres_ex: - # Since we are mocking import importlib.reload(test_module) @@ -280,7 +273,6 @@ async def test_presentation_exchange_retrieve_not_found(self): ), autospec=True, ) as mock_pres_ex: - # Since we are mocking import importlib.reload(test_module) @@ -307,7 +299,6 @@ async def test_presentation_exchange_retrieve_x(self): ), autospec=True, ) as mock_pres_ex: - # Since we are mocking import importlib.reload(test_module) @@ -334,7 +325,6 @@ async def test_presentation_exchange_send_proposal(self): "aries_cloudagent.indy.models.pres_preview.IndyPresPreview", autospec=True, ) as mock_preview: - # Since we are mocking import importlib.reload(test_module) @@ -360,7 +350,6 @@ async def test_presentation_exchange_send_proposal_no_conn_record(self): "aries_cloudagent.connections.models.conn_record.ConnRecord", autospec=True, ) as mock_connection_record: - # Since we are mocking import importlib.reload(test_module) @@ -388,7 +377,6 @@ async def test_presentation_exchange_send_proposal_not_ready(self): ), autospec=True, ) as mock_proposal: - # Since we are mocking import importlib.reload(test_module) @@ -411,7 +399,6 @@ async def test_presentation_exchange_send_proposal_x(self): "aries_cloudagent.indy.models.pres_preview.IndyPresPreview", autospec=True, ) as mock_preview: - # Since we are mocking import importlib.reload(test_module) @@ -456,7 +443,6 @@ async def test_presentation_exchange_create_request(self): "aries_cloudagent.indy.util.generate_pr_nonce", autospec=True, ) as mock_generate_nonce: - # Since we are mocking import importlib.reload(test_module) @@ -510,7 +496,6 @@ async def test_presentation_exchange_create_request_x(self): "aries_cloudagent.indy.util.generate_pr_nonce", autospec=True, ) as mock_generate_nonce: - # Since we are mocking import importlib.reload(test_module) @@ -562,7 +547,6 @@ async def test_presentation_exchange_send_free_request(self): ), autospec=True, ) as mock_presentation_exchange: - # Since we are mocking import importlib.reload(test_module) @@ -601,7 +585,6 @@ async def test_presentation_exchange_send_free_request_not_found(self): "aries_cloudagent.connections.models.conn_record.ConnRecord", autospec=True, ) as mock_connection_record: - # Since we are mocking import importlib.reload(test_module) @@ -620,7 +603,6 @@ async def test_presentation_exchange_send_free_request_not_ready(self): "aries_cloudagent.connections.models.conn_record.ConnRecord", autospec=True, ) as mock_connection_record: - # Since we are mocking import importlib.reload(test_module) @@ -664,7 +646,6 @@ async def test_presentation_exchange_send_free_request_x(self): ), autospec=True, ) as mock_presentation_exchange: - # Since we are mocking import importlib.reload(test_module) @@ -731,7 +712,6 @@ async def test_presentation_exchange_send_bound_request(self): "models.presentation_exchange.V10PresentationExchange", autospec=True, ) as mock_presentation_exchange: - # Since we are mocking import importlib.reload(test_module) @@ -793,7 +773,6 @@ async def test_presentation_exchange_send_bound_request_not_found(self): ), autospec=True, ) as mock_presentation_exchange: - # Since we are mocking import importlib.reload(test_module) @@ -838,7 +817,6 @@ async def test_presentation_exchange_send_bound_request_not_ready(self): ), autospec=True, ) as mock_presentation_exchange: - # Since we are mocking import importlib.reload(test_module) @@ -883,7 +861,6 @@ async def test_presentation_exchange_send_bound_request_bad_state(self): ), autospec=True, ) as mock_presentation_exchange: - # Since we are mocking import importlib.reload(test_module) @@ -923,7 +900,6 @@ async def test_presentation_exchange_send_bound_request_x(self): ), autospec=True, ) as mock_presentation_exchange: - # Since we are mocking import importlib.reload(test_module) @@ -998,13 +974,13 @@ async def test_presentation_exchange_send_presentation(self): ), autospec=True, ) as mock_presentation_exchange: - # Since we are mocking import importlib.reload(test_module) mock_presentation_exchange.state = ( test_module.V10PresentationExchange.STATE_REQUEST_RECEIVED ) + mock_presentation_exchange.connection_id = "dummy" mock_presentation_exchange.retrieve_by_id = async_mock.CoroutineMock( return_value=async_mock.MagicMock( state=mock_presentation_exchange.STATE_REQUEST_RECEIVED, @@ -1074,7 +1050,6 @@ async def test_presentation_exchange_send_presentation_not_found(self): ), autospec=True, ) as mock_presentation_exchange: - # Since we are mocking import importlib.reload(test_module) @@ -1119,7 +1094,6 @@ async def test_presentation_exchange_send_presentation_not_ready(self): ), autospec=True, ) as mock_presentation_exchange: - # Since we are mocking import importlib.reload(test_module) @@ -1149,7 +1123,6 @@ async def test_presentation_exchange_send_presentation_bad_state(self): ), autospec=True, ) as mock_presentation_exchange: - # Since we are mocking import importlib.reload(test_module) @@ -1195,7 +1168,6 @@ async def test_presentation_exchange_send_presentation_x(self): ), autospec=True, ) as mock_presentation_exchange: - # Since we are mocking import importlib.reload(test_module) @@ -1250,7 +1222,6 @@ async def test_presentation_exchange_verify_presentation(self): ), autospec=True, ) as mock_presentation_exchange: - # Since we are mocking import importlib.reload(test_module) @@ -1310,7 +1281,6 @@ async def test_presentation_exchange_verify_presentation_bad_state(self): ), autospec=True, ) as mock_presentation_exchange: - # Since we are mocking import importlib.reload(test_module) @@ -1353,7 +1323,6 @@ async def test_presentation_exchange_verify_presentation_x(self): ), autospec=True, ) as mock_presentation_exchange: - # Since we are mocking import importlib.reload(test_module) @@ -1411,7 +1380,6 @@ async def test_presentation_exchange_problem_report(self): ) as mock_problem_report, async_mock.patch.object( test_module.web, "json_response" ) as mock_response: - # Since we are mocking import importlib.reload(test_module) @@ -1443,7 +1411,6 @@ async def test_presentation_exchange_problem_report_bad_pres_ex_id(self): ), autospec=True, ) as mock_pres_ex: - # Since we are mocking import importlib.reload(test_module) @@ -1473,7 +1440,6 @@ async def test_presentation_exchange_problem_report_x(self): ) as mock_problem_report, async_mock.patch.object( test_module.web, "json_response" ) as mock_response: - # Since we are mocking import importlib.reload(test_module) mock_pres_ex.retrieve_by_id = async_mock.CoroutineMock( @@ -1493,7 +1459,6 @@ async def test_presentation_exchange_remove(self): ), autospec=True, ) as mock_presentation_exchange: - # Since we are mocking import importlib.reload(test_module) @@ -1522,7 +1487,6 @@ async def test_presentation_exchange_remove_not_found(self): ), autospec=True, ) as mock_presentation_exchange: - # Since we are mocking import importlib.reload(test_module) @@ -1544,7 +1508,6 @@ async def test_presentation_exchange_remove_x(self): ), autospec=True, ) as mock_presentation_exchange: - # Since we are mocking import importlib.reload(test_module) diff --git a/aries_cloudagent/protocols/present_proof/v2_0/formats/dif/handler.py b/aries_cloudagent/protocols/present_proof/v2_0/formats/dif/handler.py index 33f1b653d0..1ad50dbc71 100644 --- a/aries_cloudagent/protocols/present_proof/v2_0/formats/dif/handler.py +++ b/aries_cloudagent/protocols/present_proof/v2_0/formats/dif/handler.py @@ -1,5 +1,6 @@ """V2.0 present-proof dif presentation-exchange format handler.""" +import json import logging from marshmallow import RAISE @@ -20,7 +21,7 @@ ) from ......vc.vc_ld.verify import verify_presentation from ......wallet.base import BaseWallet -from ......wallet.key_type import KeyType +from ......wallet.key_type import ED25519, BLS12381G2 from .....problem_report.v1_0.message import ProblemReport @@ -55,14 +56,12 @@ class DIFPresFormatHandler(V20PresFormatHandler): format = V20PresFormat.Format.DIF ISSUE_SIGNATURE_SUITE_KEY_TYPE_MAPPING = { - Ed25519Signature2018: KeyType.ED25519, + Ed25519Signature2018: ED25519, } if BbsBlsSignature2020.BBS_SUPPORTED: - ISSUE_SIGNATURE_SUITE_KEY_TYPE_MAPPING[BbsBlsSignature2020] = KeyType.BLS12381G2 - ISSUE_SIGNATURE_SUITE_KEY_TYPE_MAPPING[ - BbsBlsSignatureProof2020 - ] = KeyType.BLS12381G2 + ISSUE_SIGNATURE_SUITE_KEY_TYPE_MAPPING[BbsBlsSignature2020] = BLS12381G2 + ISSUE_SIGNATURE_SUITE_KEY_TYPE_MAPPING[BbsBlsSignatureProof2020] = BLS12381G2 async def _get_all_suites(self, wallet: BaseWallet): """Get all supported suites for verifying presentation.""" @@ -151,9 +150,18 @@ async def create_bound_request( A tuple (updated presentation exchange record, presentation request message) """ - dif_proof_request = pres_ex_record.pres_proposal.attachment( + dif_proof_request = {} + pres_proposal_dict = pres_ex_record.pres_proposal.attachment( DIFPresFormatHandler.format ) + if "options" not in pres_proposal_dict: + dif_proof_request["options"] = {"challenge": str(uuid4())} + else: + dif_proof_request["options"] = pres_proposal_dict["options"] + del pres_proposal_dict["options"] + if "challenge" not in dif_proof_request.get("options"): + dif_proof_request["options"]["challenge"] = str(uuid4()) + dif_proof_request["presentation_definition"] = pres_proposal_dict return self.get_format_data(PRES_20_REQUEST, dif_proof_request) @@ -459,21 +467,27 @@ async def verify_pres(self, pres_ex_record: V20PresExRecord) -> V20PresExRecord: pres_request = pres_ex_record.pres_request.attachment( DIFPresFormatHandler.format ) + challenge = None if "options" in pres_request: - challenge = pres_request["options"].get("challenge") - else: - raise V20PresFormatHandlerError( - "No options [challenge] set for the presentation request" - ) + challenge = pres_request["options"].get("challenge", str(uuid4())) if not challenge: - raise V20PresFormatHandlerError( - "No challenge is set for the presentation request" + challenge = str(uuid4()) + if isinstance(dif_proof, Sequence): + for proof in dif_proof: + pres_ver_result = await verify_presentation( + presentation=proof, + suites=await self._get_all_suites(wallet=wallet), + document_loader=self._profile.inject(DocumentLoader), + challenge=challenge, + ) + if not pres_ver_result.verified: + break + else: + pres_ver_result = await verify_presentation( + presentation=dif_proof, + suites=await self._get_all_suites(wallet=wallet), + document_loader=self._profile.inject(DocumentLoader), + challenge=challenge, ) - pres_ver_result = await verify_presentation( - presentation=dif_proof, - suites=await self._get_all_suites(wallet=wallet), - document_loader=self._profile.inject(DocumentLoader), - challenge=challenge, - ) - pres_ex_record.verified = pres_ver_result.verified + pres_ex_record.verified = json.dumps(pres_ver_result.verified) return pres_ex_record diff --git a/aries_cloudagent/protocols/present_proof/v2_0/formats/dif/tests/test_handler.py b/aries_cloudagent/protocols/present_proof/v2_0/formats/dif/tests/test_handler.py index 4ac318c506..5d7eb47a39 100644 --- a/aries_cloudagent/protocols/present_proof/v2_0/formats/dif/tests/test_handler.py +++ b/aries_cloudagent/protocols/present_proof/v2_0/formats/dif/tests/test_handler.py @@ -134,6 +134,75 @@ }, } +DIF_PRES_REQUEST_SEQUENCE = { + "options": { + "challenge": "3fa85f64-5717-4562-b3fc-2c963f66afa7", + "domain": "4jt78h47fh47", + }, + "presentation_definition": { + "id": "32f54163-7166-48f1-93d8-ff217bdb0654", + "submission_requirements": [ + { + "name": "Citizenship Information", + "rule": "all", + "from": "A", + }, + { + "name": "Citizenship Information v2", + "rule": "pick", + "min": 1, + "from": "B", + }, + ], + "input_descriptors": [ + { + "id": "citizenship_input_1", + "name": "EU Driver's License", + "group": ["A"], + "schema": [ + {"uri": "https://www.w3.org/2018/credentials#VerifiableCredential"}, + {"uri": "https://w3id.org/citizenship#PermanentResidentCard"}, + ], + "constraints": { + "limit_disclosure": "required", + "fields": [ + { + "path": ["$.credentialSubject.givenName"], + "purpose": "The claim must be from one of the specified issuers", + "filter": { + "type": "string", + "enum": ["JOHN", "CAI"], + }, + } + ], + }, + }, + { + "id": "citizenship_input_2", + "name": "EU Driver's License v2", + "group": ["B"], + "schema": [ + {"uri": "https://www.w3.org/2018/credentials#VerifiableCredential"}, + {"uri": "https://w3id.org/citizenship#PermanentResidentCard"}, + ], + "constraints": { + "limit_disclosure": "required", + "fields": [ + { + "path": ["$.credentialSubject.givenName"], + "purpose": "The claim must be from one of the specified issuers", + "filter": { + "type": "string", + "enum": ["JOHN", "CAI"], + }, + } + ], + }, + }, + ], + }, +} + DIF_PRES_PROPOSAL = { "input_descriptors": [ { @@ -199,7 +268,7 @@ "descriptor_map": [ { "id": "citizenship_input_1", - "format": "ldp_vp", + "format": "ldp_vc", "path": "$.verifiableCredential[0]", } ], @@ -214,6 +283,60 @@ }, } +DIF_PRES_SEQUENCE = [ + DIF_PRES, + { + "@context": ["https://www.w3.org/2018/credentials/v1"], + "type": ["VerifiablePresentation"], + "verifiableCredential": [ + { + "@context": [ + "https://www.w3.org/2018/credentials/v1", + "https://w3id.org/citizenship/v1", + "https://w3id.org/security/bbs/v1", + ], + "id": "https://issuer.oidp.uscis.gov/credentials/83627465", + "type": ["PermanentResidentCard", "VerifiableCredential"], + "credentialSubject": { + "id": "did:example:b34ca6cd37bbf23", + "type": ["Person", "PermanentResident"], + "givenName": "JOHN", + }, + "issuanceDate": "2010-01-01T19:53:24Z", + "issuer": "did:key:zUC74bgefTdc43KS1psXgXf4jLaHyaj2qCQqQTXrtmSYGf1PxiJhrH6LGpaBMyj6tqAKmjGyMaS4RfNo2an77vT1HfzJUNPk4H7TCuJvSp4vet4Cu67kn2JSegoQNFSA1tbwU8v", + "proof": { + "type": "BbsBlsSignatureProof2020", + "nonce": "3AuruhJQrXtEgiagiJ+FwVf2S0SnzUDJvnO61YecQsJ7ImR1mPcoVjJJ0HOhfkFpoYI=", + "proofValue": "ABkBuAaPlP5A7JWY78Xf69oBnsMLcD1RXbIFYhcLoXPXW12CG9glnnqnPLsGri5xsA3LcP0kg74X+sAjKXGRGy3uvp412Dm0FuohYNboQcLne5KOAa5AxU4bjmwQsxdfduVqhriro1N+YTkuB4SMmO/5ooL0N3OHsYdExg7nSzWqmZoqgp+3CwIxF0a/oyKTcxJORuIqAAAAdInlL9teSIX49NJGEZfBO7IrdjT2iggH/G0AlPWoEvrWIbuCRQ69K83n5o7oJVjqhAAAAAIaVmlAD6+FEKA4eg0OaWOKPrd5Kq8rv0vIwjJ71egxll0Fqq4zDWQ/+yl3Pteh0Wyuyvpm19/sj6tiCWj4PkA+rpxtR2bXpnrCTKUffFFNBjVvVziXDS0KWkGUB7XU9mjUa4USC7Iub3bZZCnFjQA5AAAADzkGwGD837r33e7OTrGEti8eAkvFDcyCgA4ck/X+5HJjAJclHWbl4SNQR8CiNZyzJpvxW+jbNBcwmEvocYArddk3F78Ki0Qnp6aU9eDgfOOx1iW2BXLUjrhq5I2hP5/WQF3CEDYRjczGjzM9T8/coeC36YAp0zJunIXUKb8SPDSOISafibYRYFB4xhlWKXWloDelafyujOBST8KZNM8FmF4DSbXrO8vmZbjuR/8ntUcUK7X2rNbuZ3M5eWZDF8pL+SA9gQitKfPHEocoYAdhgEAM7ZNAJ+TgOcx9gtZIhDWKDNnFxIeoOAylbD1xZd9xbWtq3Bk3R79xqsKxFRJRNxk/9b6fJruP292+qM5lxcZ1jUz/dJUYFI93hH4Mso75CjGRN78MAY9SNifl6H8qcxTpBn4332LlFhRznLbtnc4YSWA/fvVqaN9h2zCH/6AdbLKXGffV34EF7DadwJsi9jsc+YlSMn6qaIUIDTdGLwh4KKpSH5bVbg/mVCcXPTJplFgYwRsOdiQbZY/740dJyo1lPjQ0Lvdio8W2M8c73ujeJU70CNLkgjJAMUPGrCFtGxBH2eeLBQ0P95qRZAIcJ7U0MibZLaRjoUOuTla5BIt2038PJ6XhcY6BEJaLyJOPEQ==", + "verificationMethod": "did:key:zUC74bgefTdc43KS1psXgXf4jLaHyaj2qCQqQTXrtmSYGf1PxiJhrH6LGpaBMyj6tqAKmjGyMaS4RfNo2an77vT1HfzJUNPk4H7TCuJvSp4vet4Cu67kn2JSegoQNFSA1tbwU8v#zUC74bgefTdc43KS1psXgXf4jLaHyaj2qCQqQTXrtmSYGf1PxiJhrH6LGpaBMyj6tqAKmjGyMaS4RfNo2an77vT1HfzJUNPk4H7TCuJvSp4vet4Cu67kn2JSegoQNFSA1tbwU8v", + "proofPurpose": "assertionMethod", + "created": "2021-05-05T15:22:30.523465", + }, + } + ], + "presentation_submission": { + "id": "a5fcfe44-2c30-497d-af02-98e539da9a0f", + "definition_id": "32f54163-7166-48f1-93d8-ff217bdb0653", + "descriptor_map": [ + { + "id": "citizenship_input_2", + "format": "ldp_vp", + "path": "$.verifiableCredential[0]", + } + ], + }, + "proof": { + "type": "Ed25519Signature2018", + "verificationMethod": "did:sov:4QxzWk3ajdnEA37NdNU5Kt#key-1", + "created": "2021-05-05T15:23:03.023971", + "proofPurpose": "authentication", + "challenge": "40429d49-5e8f-4ffc-baf8-e332412f1247", + "jws": "eyJhbGciOiAiRWREU0EiLCAiYjY0IjogZmFsc2UsICJjcml0IjogWyJiNjQiXX0..2uBYmg7muE9ZPVeAGo_ibVfLkCjf2hGshr2o5i8pAwFyNBM-kDHXofuq1MzJgb19wzb01VIu91hY_ajjt9KFAA", + }, + }, +] + + TEST_CRED = { "@context": [ "https://www.w3.org/2018/credentials/v1", @@ -284,7 +407,7 @@ async def test_get_all_suites(self): for suite in suites: assert type(suite) in types - async def test_create_bound_request(self): + async def test_create_bound_request_a(self): dif_proposal_dict = { "input_descriptors": [ { @@ -339,6 +462,118 @@ async def test_create_bound_request(self): output[1], AttachDecorator ) + async def test_create_bound_request_b(self): + dif_proposal_dict = { + "options": {"challenge": "test123"}, + "input_descriptors": [ + { + "id": "citizenship_input_1", + "name": "EU Driver's License", + "group": ["A"], + "schema": [ + { + "uri": "https://www.w3.org/2018/credentials#VerifiableCredential" + } + ], + "constraints": { + "limit_disclosure": "required", + "fields": [ + { + "path": ["$.credentialSubject.givenName"], + "purpose": "The claim must be from one of the specified issuers", + "filter": {"type": "string", "enum": ["JOHN", "CAI"]}, + } + ], + }, + } + ], + } + dif_pres_proposal = V20PresProposal( + formats=[ + V20PresFormat( + attach_id="dif", + format_=ATTACHMENT_FORMAT[PRES_20_PROPOSAL][ + V20PresFormat.Format.DIF.api + ], + ) + ], + proposals_attach=[ + AttachDecorator.data_json(dif_proposal_dict, ident="dif") + ], + ) + record = V20PresExRecord( + pres_ex_id="pxid", + thread_id="thid", + connection_id="conn_id", + initiator="init", + role="role", + state="state", + pres_proposal=dif_pres_proposal, + verified="false", + auto_present=True, + error_msg="error", + ) + output = await self.handler.create_bound_request(pres_ex_record=record) + assert isinstance(output[0], V20PresFormat) and isinstance( + output[1], AttachDecorator + ) + + async def test_create_bound_request_c(self): + dif_proposal_dict = { + "options": {"domain": "test123"}, + "input_descriptors": [ + { + "id": "citizenship_input_1", + "name": "EU Driver's License", + "group": ["A"], + "schema": [ + { + "uri": "https://www.w3.org/2018/credentials#VerifiableCredential" + } + ], + "constraints": { + "limit_disclosure": "required", + "fields": [ + { + "path": ["$.credentialSubject.givenName"], + "purpose": "The claim must be from one of the specified issuers", + "filter": {"type": "string", "enum": ["JOHN", "CAI"]}, + } + ], + }, + } + ], + } + dif_pres_proposal = V20PresProposal( + formats=[ + V20PresFormat( + attach_id="dif", + format_=ATTACHMENT_FORMAT[PRES_20_PROPOSAL][ + V20PresFormat.Format.DIF.api + ], + ) + ], + proposals_attach=[ + AttachDecorator.data_json(dif_proposal_dict, ident="dif") + ], + ) + record = V20PresExRecord( + pres_ex_id="pxid", + thread_id="thid", + connection_id="conn_id", + initiator="init", + role="role", + state="state", + pres_proposal=dif_pres_proposal, + verified="false", + auto_present=True, + error_msg="error", + ) + output = await self.handler.create_bound_request(pres_ex_record=record) + assert isinstance(output[0], V20PresFormat) and isinstance( + output[1], AttachDecorator + ) + async def test_create_pres(self): dif_pres_request = V20PresRequest( formats=[ @@ -889,7 +1124,7 @@ async def test_create_pres_pd_claim_format_bls12381g2(self): ) assert output[1].data.json_ == DIF_PRES - async def test_verify_pres(self): + async def test_verify_pres_sequence(self): dif_pres = V20Pres( formats=[ V20PresFormat( @@ -897,7 +1132,9 @@ async def test_verify_pres(self): format_=ATTACHMENT_FORMAT[PRES_20][V20PresFormat.Format.DIF.api], ) ], - presentations_attach=[AttachDecorator.data_json(DIF_PRES, ident="dif")], + presentations_attach=[ + AttachDecorator.data_json(DIF_PRES_SEQUENCE, ident="dif") + ], ) dif_pres_request = V20PresRequest( formats=[ @@ -909,7 +1146,7 @@ async def test_verify_pres(self): ) ], request_presentations_attach=[ - AttachDecorator.data_json(DIF_PRES_REQUEST_B, ident="dif") + AttachDecorator.data_json(DIF_PRES_REQUEST_SEQUENCE, ident="dif") ], ) record = V20PresExRecord( @@ -932,34 +1169,42 @@ async def test_verify_pres(self): async_mock.CoroutineMock( return_value=PresentationVerificationResult(verified=True) ), - ) as mock_vr: + ): output = await self.handler.verify_pres(record) assert output.verified - async def test_verify_pres_no_challenge(self): - test_pd = deepcopy(DIF_PRES_REQUEST_B) - del test_pd["options"]["challenge"] - dif_pres_request = V20PresRequest( + with async_mock.patch.object( + test_module, + "verify_presentation", + async_mock.CoroutineMock( + return_value=PresentationVerificationResult(verified=False) + ), + ): + output = await self.handler.verify_pres(record) + assert output.verified == "false" + + async def test_verify_pres(self): + dif_pres = V20Pres( formats=[ V20PresFormat( attach_id="dif", - format_=ATTACHMENT_FORMAT[PRES_20_REQUEST][ - V20PresFormat.Format.DIF.api - ], + format_=ATTACHMENT_FORMAT[PRES_20][V20PresFormat.Format.DIF.api], ) ], - request_presentations_attach=[ - AttachDecorator.data_json(test_pd, ident="dif") - ], + presentations_attach=[AttachDecorator.data_json(DIF_PRES, ident="dif")], ) - dif_pres = V20Pres( + dif_pres_request = V20PresRequest( formats=[ V20PresFormat( attach_id="dif", - format_=ATTACHMENT_FORMAT[PRES_20][V20PresFormat.Format.DIF.api], + format_=ATTACHMENT_FORMAT[PRES_20_REQUEST][ + V20PresFormat.Format.DIF.api + ], ) ], - presentations_attach=[AttachDecorator.data_json(DIF_PRES, ident="dif")], + request_presentations_attach=[ + AttachDecorator.data_json(DIF_PRES_REQUEST_B, ident="dif") + ], ) record = V20PresExRecord( pres_ex_id="pxid", @@ -975,12 +1220,19 @@ async def test_verify_pres_no_challenge(self): error_msg="error", ) - with self.assertRaises(V20PresFormatHandlerError): - await self.handler.verify_pres(record) + with async_mock.patch.object( + test_module, + "verify_presentation", + async_mock.CoroutineMock( + return_value=PresentationVerificationResult(verified=True) + ), + ) as mock_vr: + output = await self.handler.verify_pres(record) + assert output.verified - async def test_verify_pres_invalid_challenge(self): + async def test_verify_pres_no_challenge(self): test_pd = deepcopy(DIF_PRES_REQUEST_B) - del test_pd["options"] + del test_pd["options"]["challenge"] dif_pres_request = V20PresRequest( formats=[ V20PresFormat( @@ -1017,8 +1269,7 @@ async def test_verify_pres_invalid_challenge(self): error_msg="error", ) - with self.assertRaises(V20PresFormatHandlerError): - await self.handler.verify_pres(record) + assert await self.handler.verify_pres(record) async def test_create_pres_cred_limit_disclosure_no_bbs(self): test_pd = deepcopy(DIF_PRES_REQUEST_B) @@ -1445,6 +1696,49 @@ async def test_verify_received_pres_c(self): ) await self.handler.receive_pres(message=dif_pres, pres_ex_record=record) + async def test_verify_received_pres_sequence(self): + dif_pres = V20Pres( + formats=[ + V20PresFormat( + attach_id="dif", + format_=ATTACHMENT_FORMAT[PRES_20][V20PresFormat.Format.DIF.api], + ) + ], + presentations_attach=[ + AttachDecorator.data_json( + mapping=DIF_PRES_SEQUENCE, + ident="dif", + ) + ], + ) + dif_pres_request = V20PresRequest( + formats=[ + V20PresFormat( + attach_id="dif", + format_=ATTACHMENT_FORMAT[PRES_20_REQUEST][ + V20PresFormat.Format.DIF.api + ], + ) + ], + request_presentations_attach=[ + AttachDecorator.data_json(DIF_PRES_REQUEST_SEQUENCE, ident="dif") + ], + ) + record = V20PresExRecord( + pres_ex_id="pxid", + thread_id="thid", + connection_id="conn_id", + initiator="init", + role="role", + state="state", + pres_request=dif_pres_request, + pres=dif_pres, + verified="false", + auto_present=True, + error_msg="error", + ) + await self.handler.receive_pres(message=dif_pres, pres_ex_record=record) + async def test_verify_received_limit_disclosure_a(self): dif_proof = deepcopy(DIF_PRES) cred_dict = deepcopy(TEST_CRED_DICT) diff --git a/aries_cloudagent/protocols/present_proof/v2_0/formats/indy/handler.py b/aries_cloudagent/protocols/present_proof/v2_0/formats/indy/handler.py index 6158f3b96c..8f0a3e5057 100644 --- a/aries_cloudagent/protocols/present_proof/v2_0/formats/indy/handler.py +++ b/aries_cloudagent/protocols/present_proof/v2_0/formats/indy/handler.py @@ -114,21 +114,26 @@ async def create_bound_request( indy_proof_request = pres_ex_record.pres_proposal.attachment( IndyPresExchangeHandler.format ) - indy_proof_request["name"] = request_data.get("name") or "proof-request" - indy_proof_request["version"] = request_data.get("version") or "1.0" - indy_proof_request["nonce"] = ( - request_data.get("nonce") or await generate_pr_nonce() - ) + if request_data: + indy_proof_request["name"] = request_data.get("name", "proof-request") + indy_proof_request["version"] = request_data.get("version", "1.0") + indy_proof_request["nonce"] = ( + request_data.get("nonce") or await generate_pr_nonce() + ) + else: + indy_proof_request["name"] = "proof-request" + indy_proof_request["version"] = "1.0" + indy_proof_request["nonce"] = await generate_pr_nonce() return self.get_format_data(PRES_20_REQUEST, indy_proof_request) async def create_pres( self, pres_ex_record: V20PresExRecord, - request_data: dict = {}, + request_data: dict = None, ) -> Tuple[V20PresFormat, AttachDecorator]: """Create a presentation.""" requested_credentials = {} - if request_data == {}: + if not request_data: try: proof_request = pres_ex_record.pres_request indy_proof_request = proof_request.attachment( @@ -194,7 +199,10 @@ def _check_proof_vs_proposal(): f"attr::{name}::value": proof_value, } - if not any(r.items() <= criteria.items() for r in req_restrictions): + if ( + not any(r.items() <= criteria.items() for r in req_restrictions) + and len(req_restrictions) != 0 + ): raise V20PresFormatHandlerError( f"Presented attribute {reft} does not satisfy proof request " f"restrictions {req_restrictions}" @@ -229,7 +237,10 @@ def _check_proof_vs_proposal(): }, } - if not any(r.items() <= criteria.items() for r in req_restrictions): + if ( + not any(r.items() <= criteria.items() for r in req_restrictions) + and len(req_restrictions) != 0 + ): raise V20PresFormatHandlerError( f"Presented attr group {reft} does not satisfy proof request " f"restrictions {req_restrictions}" @@ -282,7 +293,10 @@ def _check_proof_vs_proposal(): "issuer_did": cred_def_id.split(":")[-5], } - if not any(r.items() <= criteria.items() for r in req_restrictions): + if ( + not any(r.items() <= criteria.items() for r in req_restrictions) + and len(req_restrictions) != 0 + ): raise V20PresFormatHandlerError( f"Presented predicate {reft} does not satisfy proof request " f"restrictions {req_restrictions}" @@ -315,14 +329,14 @@ async def verify_pres(self, pres_ex_record: V20PresExRecord) -> V20PresExRecord: ) = await indy_handler.process_pres_identifiers(indy_proof["identifiers"]) verifier = self._profile.inject(IndyVerifier) - pres_ex_record.verified = json.dumps( # tag: needs string value - await verifier.verify_presentation( - indy_proof_request, - indy_proof, - schemas, - cred_defs, - rev_reg_defs, - rev_reg_entries, - ) + (verified, verified_msgs) = await verifier.verify_presentation( + indy_proof_request, + indy_proof, + schemas, + cred_defs, + rev_reg_defs, + rev_reg_entries, ) + pres_ex_record.verified = json.dumps(verified) + pres_ex_record.verified_msgs = list(set(verified_msgs)) return pres_ex_record diff --git a/aries_cloudagent/protocols/present_proof/v2_0/handlers/pres_ack_handler.py b/aries_cloudagent/protocols/present_proof/v2_0/handlers/pres_ack_handler.py index 0a0d2a75c5..d7d20508f1 100644 --- a/aries_cloudagent/protocols/present_proof/v2_0/handlers/pres_ack_handler.py +++ b/aries_cloudagent/protocols/present_proof/v2_0/handlers/pres_ack_handler.py @@ -1,5 +1,6 @@ """Presentation ack message handler.""" +from .....core.oob_processor import OobMessageProcessor from .....messaging.base_handler import BaseHandler, HandlerException from .....messaging.request_context import RequestContext from .....messaging.responder import BaseResponder @@ -29,8 +30,20 @@ async def handle(self, context: RequestContext, responder: BaseResponder): context.message.serialize(as_string=True), ) - if not context.connection_ready: - raise HandlerException("No connection established for presentation ack") + # If connection is present it must be ready for use + if context.connection_record and not context.connection_ready: + raise HandlerException("Connection used for presentation ack not ready") + + # Find associated oob record + oob_processor = context.inject(OobMessageProcessor) + oob_record = await oob_processor.find_oob_record_for_inbound_message(context) + + # Either connection or oob context must be present + if not context.connection_record and not oob_record: + raise HandlerException( + "No connection or associated connectionless exchange found for" + " presentation ack" + ) pres_manager = V20PresManager(context.profile) await pres_manager.receive_pres_ack(context.message, context.connection_record) diff --git a/aries_cloudagent/protocols/present_proof/v2_0/handlers/pres_handler.py b/aries_cloudagent/protocols/present_proof/v2_0/handlers/pres_handler.py index 9736a66658..493cfad5c1 100644 --- a/aries_cloudagent/protocols/present_proof/v2_0/handlers/pres_handler.py +++ b/aries_cloudagent/protocols/present_proof/v2_0/handlers/pres_handler.py @@ -1,7 +1,8 @@ """Presentation message handler.""" +from .....core.oob_processor import OobMessageProcessor from .....ledger.error import LedgerError -from .....messaging.base_handler import BaseHandler +from .....messaging.base_handler import BaseHandler, HandlerException from .....messaging.models.base import BaseModelError from .....messaging.request_context import RequestContext from .....messaging.responder import BaseResponder @@ -35,10 +36,24 @@ async def handle(self, context: RequestContext, responder: BaseResponder): context.message.serialize(as_string=True), ) + # If connection is present it must be ready for use + if context.connection_record and not context.connection_ready: + raise HandlerException("Connection used for presentation not ready") + + # Find associated oob record. If the presentation request was created as an oob + # attachment the presentation exchange record won't have a connection id (yet) + oob_processor = context.inject(OobMessageProcessor) + oob_record = await oob_processor.find_oob_record_for_inbound_message(context) + + # Normally we would do a check here that there is either a connection or + # an associated oob record. However as present proof supported receiving + # presentation without oob record or connection record + # (aip-1 style connectionless) we can't perform this check here + pres_manager = V20PresManager(context.profile) pres_ex_record = await pres_manager.receive_pres( - context.message, context.connection_record + context.message, context.connection_record, oob_record ) r_time = trace_event( @@ -49,9 +64,13 @@ async def handle(self, context: RequestContext, responder: BaseResponder): ) # Automatically move to next state if flag is set - if context.settings.get("debug.auto_verify_presentation"): + if ( + pres_ex_record + and pres_ex_record.auto_verify + or context.settings.get("debug.auto_verify_presentation") + ): try: - await pres_manager.verify_pres(pres_ex_record) + await pres_manager.verify_pres(pres_ex_record, responder) except (BaseModelError, LedgerError, StorageError) as err: self._logger.exception(err) if pres_ex_record: diff --git a/aries_cloudagent/protocols/present_proof/v2_0/handlers/pres_proposal_handler.py b/aries_cloudagent/protocols/present_proof/v2_0/handlers/pres_proposal_handler.py index 773852b185..e20c1d620c 100644 --- a/aries_cloudagent/protocols/present_proof/v2_0/handlers/pres_proposal_handler.py +++ b/aries_cloudagent/protocols/present_proof/v2_0/handlers/pres_proposal_handler.py @@ -35,9 +35,14 @@ async def handle(self, context: RequestContext, responder: BaseResponder): context.message.serialize(as_string=True), ) - if not context.connection_ready: + if not context.connection_record: raise HandlerException( - "No connection established for presentation proposal" + "Connectionless not supported for presentation proposal" + ) + # If connection is present it must be ready for use + elif not context.connection_ready: + raise HandlerException( + "Connection used for presentation proposal not ready" ) profile = context.profile diff --git a/aries_cloudagent/protocols/present_proof/v2_0/handlers/pres_request_handler.py b/aries_cloudagent/protocols/present_proof/v2_0/handlers/pres_request_handler.py index 1cbca5838e..1d8abe88b6 100644 --- a/aries_cloudagent/protocols/present_proof/v2_0/handlers/pres_request_handler.py +++ b/aries_cloudagent/protocols/present_proof/v2_0/handlers/pres_request_handler.py @@ -1,5 +1,6 @@ """Presentation request message handler.""" +from .....core.oob_processor import OobMessageProcessor from .....indy.holder import IndyHolderError from .....ledger.error import LedgerError from .....messaging.base_handler import BaseHandler, HandlerException @@ -39,8 +40,26 @@ async def handle(self, context: RequestContext, responder: BaseResponder): context.message.serialize(as_string=True), ) - if not context.connection_ready: - raise HandlerException("No connection established for presentation request") + # If connection is present it must be ready for use + if context.connection_record and not context.connection_ready: + raise HandlerException("Connection used for presentation request not ready") + + # Find associated oob record + oob_processor = context.inject(OobMessageProcessor) + oob_record = await oob_processor.find_oob_record_for_inbound_message(context) + + # Either connection or oob context must be present + if not context.connection_record and not oob_record: + raise HandlerException( + "No connection or associated connectionless exchange found for" + " presentation request" + ) + + connection_id = ( + context.connection_record.connection_id + if context.connection_record + else None + ) profile = context.profile pres_manager = V20PresManager(profile) @@ -52,13 +71,16 @@ async def handle(self, context: RequestContext, responder: BaseResponder): pres_ex_record = await V20PresExRecord.retrieve_by_tag_filter( session, {"thread_id": context.message._thread_id}, - {"connection_id": context.connection_record.connection_id}, + { + "connection_id": connection_id, + "role": V20PresExRecord.ROLE_PROVER, + }, ) # holder initiated via proposal pres_ex_record.pres_request = context.message except StorageNotFoundError: # verifier sent this request free of any proposal pres_ex_record = V20PresExRecord( - connection_id=context.connection_record.connection_id, + connection_id=connection_id, thread_id=context.message._thread_id, initiator=V20PresExRecord.INITIATOR_EXTERNAL, role=V20PresExRecord.ROLE_PROVER, diff --git a/aries_cloudagent/protocols/present_proof/v2_0/handlers/tests/test_pres_ack_handler.py b/aries_cloudagent/protocols/present_proof/v2_0/handlers/tests/test_pres_ack_handler.py index 90ea997150..928a2df6be 100644 --- a/aries_cloudagent/protocols/present_proof/v2_0/handlers/tests/test_pres_ack_handler.py +++ b/aries_cloudagent/protocols/present_proof/v2_0/handlers/tests/test_pres_ack_handler.py @@ -1,5 +1,6 @@ from asynctest import mock as async_mock, TestCase as AsyncTestCase +from ......core.oob_processor import OobMessageProcessor from ......messaging.request_context import RequestContext from ......messaging.responder import MockResponder from ......transport.inbound.receipt import MessageReceipt @@ -15,6 +16,13 @@ async def test_called(self): request_context.message_receipt = MessageReceipt() session = request_context.session() + mock_oob_processor = async_mock.MagicMock( + find_oob_record_for_inbound_message=async_mock.CoroutineMock( + return_value=async_mock.MagicMock() + ) + ) + request_context.injector.bind_instance(OobMessageProcessor, mock_oob_processor) + with async_mock.patch.object( test_module, "V20PresManager", autospec=True ) as mock_pres_mgr: @@ -35,6 +43,7 @@ async def test_called(self): async def test_called_not_ready(self): request_context = RequestContext.test_context() request_context.message_receipt = MessageReceipt() + request_context.connection_record = async_mock.MagicMock() with async_mock.patch.object( test_module, "V20PresManager", autospec=True @@ -44,7 +53,32 @@ async def test_called_not_ready(self): request_context.connection_ready = False handler = test_module.V20PresAckHandler() responder = MockResponder() - with self.assertRaises(test_module.HandlerException): + with self.assertRaises(test_module.HandlerException) as err: await handler.handle(request_context, responder) + assert err.exception.message == "Connection used for presentation ack not ready" + + assert not responder.messages + + async def test_called_no_connection_no_oob(self): + request_context = RequestContext.test_context() + request_context.message_receipt = MessageReceipt() + + mock_oob_processor = async_mock.MagicMock( + find_oob_record_for_inbound_message=async_mock.CoroutineMock( + # No oob record found + return_value=None + ) + ) + request_context.injector.bind_instance(OobMessageProcessor, mock_oob_processor) + + request_context.message = V20PresAck() + handler = test_module.V20PresAckHandler() + responder = MockResponder() + with self.assertRaises(test_module.HandlerException) as err: + await handler.handle(request_context, responder) + assert ( + err.exception.message + == "No connection or associated connectionless exchange found for presentation ack" + ) assert not responder.messages diff --git a/aries_cloudagent/protocols/present_proof/v2_0/handlers/tests/test_pres_handler.py b/aries_cloudagent/protocols/present_proof/v2_0/handlers/tests/test_pres_handler.py index 9eb3ba1ae6..0603e77626 100644 --- a/aries_cloudagent/protocols/present_proof/v2_0/handlers/tests/test_pres_handler.py +++ b/aries_cloudagent/protocols/present_proof/v2_0/handlers/tests/test_pres_handler.py @@ -2,6 +2,7 @@ from asynctest import mock as async_mock, TestCase as AsyncTestCase +from ......core.oob_processor import OobMessageProcessor from ......messaging.request_context import RequestContext from ......messaging.responder import MockResponder from ......transport.inbound.receipt import MessageReceipt @@ -17,6 +18,14 @@ async def test_called(self): request_context.message_receipt = MessageReceipt() request_context.settings["debug.auto_verify_presentation"] = False + oob_record = async_mock.MagicMock() + mock_oob_processor = async_mock.MagicMock( + find_oob_record_for_inbound_message=async_mock.CoroutineMock( + return_value=oob_record + ) + ) + request_context.injector.bind_instance(OobMessageProcessor, mock_oob_processor) + with async_mock.patch.object( test_module, "V20PresManager", autospec=True ) as mock_pres_mgr: @@ -30,7 +39,7 @@ async def test_called(self): mock_pres_mgr.assert_called_once_with(request_context.profile) mock_pres_mgr.return_value.receive_pres.assert_called_once_with( - request_context.message, request_context.connection_record + request_context.message, request_context.connection_record, oob_record ) assert not responder.messages @@ -39,6 +48,14 @@ async def test_called_auto_verify(self): request_context.message_receipt = MessageReceipt() request_context.settings["debug.auto_verify_presentation"] = True + oob_record = async_mock.MagicMock() + mock_oob_processor = async_mock.MagicMock( + find_oob_record_for_inbound_message=async_mock.CoroutineMock( + return_value=oob_record + ) + ) + request_context.injector.bind_instance(OobMessageProcessor, mock_oob_processor) + with async_mock.patch.object( test_module, "V20PresManager", autospec=True ) as mock_pres_mgr: @@ -53,7 +70,7 @@ async def test_called_auto_verify(self): mock_pres_mgr.assert_called_once_with(request_context.profile) mock_pres_mgr.return_value.receive_pres.assert_called_once_with( - request_context.message, request_context.connection_record + request_context.message, request_context.connection_record, oob_record ) assert not responder.messages @@ -62,6 +79,14 @@ async def test_called_auto_verify_x(self): request_context.message_receipt = MessageReceipt() request_context.settings["debug.auto_verify_presentation"] = True + oob_record = async_mock.MagicMock() + mock_oob_processor = async_mock.MagicMock( + find_oob_record_for_inbound_message=async_mock.CoroutineMock( + return_value=oob_record + ) + ) + request_context.injector.bind_instance(OobMessageProcessor, mock_oob_processor) + with async_mock.patch.object( test_module, "V20PresManager", autospec=True ) as mock_pres_mgr: diff --git a/aries_cloudagent/protocols/present_proof/v2_0/handlers/tests/test_pres_proposal_handler.py b/aries_cloudagent/protocols/present_proof/v2_0/handlers/tests/test_pres_proposal_handler.py index 3f8a857b87..033742133e 100644 --- a/aries_cloudagent/protocols/present_proof/v2_0/handlers/tests/test_pres_proposal_handler.py +++ b/aries_cloudagent/protocols/present_proof/v2_0/handlers/tests/test_pres_proposal_handler.py @@ -1,5 +1,3 @@ -import pytest - from asynctest import mock as async_mock, TestCase as AsyncTestCase from ......messaging.request_context import RequestContext @@ -14,6 +12,7 @@ class TestV20PresProposalHandler(AsyncTestCase): async def test_called(self): request_context = RequestContext.test_context() + request_context.connection_record = async_mock.MagicMock() request_context.message_receipt = MessageReceipt() request_context.settings["debug.auto_respond_presentation_proposal"] = False @@ -39,6 +38,7 @@ async def test_called(self): async def test_called_auto_request(self): request_context = RequestContext.test_context() request_context.message = async_mock.MagicMock() + request_context.connection_record = async_mock.MagicMock() request_context.message.comment = "hello world" request_context.message_receipt = MessageReceipt() request_context.settings["debug.auto_respond_presentation_proposal"] = True @@ -76,6 +76,7 @@ async def test_called_auto_request(self): async def test_called_auto_request_x(self): request_context = RequestContext.test_context() + request_context.connection_record = async_mock.MagicMock() request_context.message = async_mock.MagicMock() request_context.message.comment = "hello world" request_context.message_receipt = MessageReceipt() @@ -107,6 +108,7 @@ async def test_called_auto_request_x(self): async def test_called_not_ready(self): request_context = RequestContext.test_context() request_context.message_receipt = MessageReceipt() + request_context.connection_record = async_mock.MagicMock() with async_mock.patch.object( test_module, "V20PresManager", autospec=True @@ -118,7 +120,27 @@ async def test_called_not_ready(self): request_context.connection_ready = False handler = test_module.V20PresProposalHandler() responder = MockResponder() - with self.assertRaises(test_module.HandlerException): + with self.assertRaises(test_module.HandlerException) as err: await handler.handle(request_context, responder) + assert ( + err.exception.message + == "Connection used for presentation proposal not ready" + ) + + assert not responder.messages + + async def test_called_no_connection(self): + request_context = RequestContext.test_context() + request_context.message_receipt = MessageReceipt() + + request_context.message = V20PresProposal() + handler = test_module.V20PresProposalHandler() + responder = MockResponder() + with self.assertRaises(test_module.HandlerException) as err: + await handler.handle(request_context, responder) + assert ( + err.exception.message + == "Connectionless not supported for presentation proposal" + ) assert not responder.messages diff --git a/aries_cloudagent/protocols/present_proof/v2_0/handlers/tests/test_pres_request_handler.py b/aries_cloudagent/protocols/present_proof/v2_0/handlers/tests/test_pres_request_handler.py index 12fac3c9bc..c0e28fa043 100644 --- a/aries_cloudagent/protocols/present_proof/v2_0/handlers/tests/test_pres_request_handler.py +++ b/aries_cloudagent/protocols/present_proof/v2_0/handlers/tests/test_pres_request_handler.py @@ -1,6 +1,8 @@ from asynctest import mock as async_mock, TestCase as AsyncTestCase from copy import deepcopy +from ......core.oob_processor import OobMessageProcessor +from ......indy.holder import IndyHolder from ......messaging.decorators.attach_decorator import AttachDecorator from ......messaging.request_context import RequestContext from ......messaging.responder import MockResponder @@ -189,6 +191,13 @@ async def test_called(self): return_value=async_mock.MagicMock() ) + mock_oob_processor = async_mock.MagicMock( + find_oob_record_for_inbound_message=async_mock.CoroutineMock( + return_value=async_mock.MagicMock() + ) + ) + request_context.injector.bind_instance(OobMessageProcessor, mock_oob_processor) + pres_proposal = V20PresProposal( formats=[ V20PresFormat( @@ -210,7 +219,6 @@ async def test_called(self): ) as mock_pres_mgr, async_mock.patch.object( test_module, "V20PresExRecord", autospec=True ) as mock_px_rec_cls: - mock_px_rec_cls.retrieve_by_tag_filter = async_mock.CoroutineMock( return_value=px_rec_instance ) @@ -227,6 +235,9 @@ async def test_called(self): mock_pres_mgr.return_value.receive_pres_request.assert_called_once_with( px_rec_instance ) + mock_oob_processor.find_oob_record_for_inbound_message.assert_called_once_with( + request_context + ) assert not responder.messages async def test_called_not_found(self): @@ -239,6 +250,13 @@ async def test_called_not_found(self): return_value=async_mock.MagicMock() ) + mock_oob_processor = async_mock.MagicMock( + find_oob_record_for_inbound_message=async_mock.CoroutineMock( + return_value=async_mock.MagicMock() + ) + ) + request_context.injector.bind_instance(OobMessageProcessor, mock_oob_processor) + pres_proposal = V20PresProposal( formats=[ V20PresFormat( @@ -260,7 +278,6 @@ async def test_called_not_found(self): ) as mock_pres_mgr, async_mock.patch.object( test_module, "V20PresExRecord", autospec=True ) as mock_px_rec_cls: - mock_px_rec_cls.retrieve_by_tag_filter = async_mock.CoroutineMock( side_effect=StorageNotFoundError ) @@ -278,6 +295,9 @@ async def test_called_not_found(self): mock_pres_mgr.return_value.receive_pres_request.assert_called_once_with( px_rec_instance ) + mock_oob_processor.find_oob_record_for_inbound_message.assert_called_once_with( + request_context + ) assert not responder.messages async def test_called_auto_present_x(self): @@ -307,21 +327,26 @@ async def test_called_auto_present_x(self): save_error_state=async_mock.CoroutineMock(), ) - with async_mock.patch.object( - test_module, "V20PresManager", autospec=True - ) as mock_pres_mgr, async_mock.patch.object( - test_module, "V20PresExRecord", autospec=True - ) as mock_pres_ex_rec_cls, async_mock.patch.object( - test_indy_handler, "IndyHolder", autospec=True - ) as mock_holder: - - mock_holder.get_credentials_for_presentation_request_by_referent = ( + mock_oob_processor = async_mock.MagicMock( + find_oob_record_for_inbound_message=async_mock.CoroutineMock( + return_value=async_mock.MagicMock() + ) + ) + mock_holder = async_mock.MagicMock( + get_credentials_for_presentation_request_by_referent=( async_mock.CoroutineMock( return_value=[{"cred_info": {"referent": "dummy"}}] ) ) - request_context.inject = async_mock.MagicMock(return_value=mock_holder) + ) + request_context.injector.bind_instance(IndyHolder, mock_holder) + request_context.injector.bind_instance(OobMessageProcessor, mock_oob_processor) + with async_mock.patch.object( + test_module, "V20PresManager", autospec=True + ) as mock_pres_mgr, async_mock.patch.object( + test_module, "V20PresExRecord", autospec=True + ) as mock_pres_ex_rec_cls: mock_pres_ex_rec_cls.return_value = mock_px_rec mock_pres_ex_rec_cls.retrieve_by_tag_filter = async_mock.CoroutineMock( return_value=mock_px_rec @@ -354,6 +379,13 @@ async def test_called_auto_present_indy(self): ) request_context.message_receipt = MessageReceipt() + mock_oob_processor = async_mock.MagicMock( + find_oob_record_for_inbound_message=async_mock.CoroutineMock( + return_value=async_mock.MagicMock() + ) + ) + request_context.injector.bind_instance(OobMessageProcessor, mock_oob_processor) + pres_proposal = V20PresProposal( formats=[ V20PresFormat( @@ -370,21 +402,20 @@ async def test_called_auto_present_indy(self): auto_present=True, ) - with async_mock.patch.object( - test_module, "V20PresManager", autospec=True - ) as mock_pres_mgr, async_mock.patch.object( - test_module, "V20PresExRecord", autospec=True - ) as mock_pres_ex_rec_cls, async_mock.patch.object( - test_indy_handler, "IndyHolder", autospec=True - ) as mock_holder: - - mock_holder.get_credentials_for_presentation_request_by_referent = ( + mock_holder = async_mock.MagicMock( + get_credentials_for_presentation_request_by_referent=( async_mock.CoroutineMock( return_value=[{"cred_info": {"referent": "dummy"}}] ) ) - request_context.inject = async_mock.MagicMock(return_value=mock_holder) + ) + request_context.injector.bind_instance(IndyHolder, mock_holder) + with async_mock.patch.object( + test_module, "V20PresManager", autospec=True + ) as mock_pres_mgr, async_mock.patch.object( + test_module, "V20PresExRecord", autospec=True + ) as mock_pres_ex_rec_cls: mock_pres_ex_rec_cls.return_value = mock_px_rec mock_pres_ex_rec_cls.retrieve_by_tag_filter = async_mock.CoroutineMock( return_value=mock_px_rec @@ -407,6 +438,9 @@ async def test_called_auto_present_indy(self): mock_pres_mgr.return_value.receive_pres_request.assert_called_once_with( mock_px_rec ) + mock_oob_processor.find_oob_record_for_inbound_message.assert_called_once_with( + request_context + ) messages = responder.messages assert len(messages) == 1 (result, target) = messages[0] @@ -429,6 +463,14 @@ async def test_called_auto_present_dif(self): return_value=DIF_PROOF_REQ ) request_context.message_receipt = MessageReceipt() + + mock_oob_processor = async_mock.MagicMock( + find_oob_record_for_inbound_message=async_mock.CoroutineMock( + return_value=async_mock.MagicMock() + ) + ) + request_context.injector.bind_instance(OobMessageProcessor, mock_oob_processor) + pres_proposal = V20PresProposal( formats=[ V20PresFormat( @@ -448,7 +490,6 @@ async def test_called_auto_present_dif(self): ) as mock_pres_mgr, async_mock.patch.object( test_module, "V20PresExRecord", autospec=True ) as mock_pres_ex_rec_cls: - mock_pres_ex_rec_cls.return_value = px_rec_instance mock_pres_ex_rec_cls.retrieve_by_tag_filter = async_mock.CoroutineMock( return_value=px_rec_instance @@ -469,6 +510,9 @@ async def test_called_auto_present_dif(self): mock_pres_mgr.return_value.receive_pres_request.assert_called_once_with( px_rec_instance ) + mock_oob_processor.find_oob_record_for_inbound_message.assert_called_once_with( + request_context + ) messages = responder.messages assert len(messages) == 1 (result, target) = messages[0] @@ -493,15 +537,14 @@ async def test_called_auto_present_no_preview(self): request_context.message_receipt = MessageReceipt() px_rec_instance = test_module.V20PresExRecord(auto_present=True) - with async_mock.patch.object( - test_module, "V20PresManager", autospec=True - ) as mock_pres_mgr, async_mock.patch.object( - test_module, "V20PresExRecord", autospec=True - ) as mock_pres_ex_rec_cls, async_mock.patch.object( - test_indy_handler, "IndyHolder", autospec=True - ) as mock_holder: - - mock_holder.get_credentials_for_presentation_request_by_referent = ( + mock_oob_processor = async_mock.MagicMock( + find_oob_record_for_inbound_message=async_mock.CoroutineMock( + return_value=async_mock.MagicMock() + ) + ) + request_context.injector.bind_instance(OobMessageProcessor, mock_oob_processor) + mock_holder = async_mock.MagicMock( + get_credentials_for_presentation_request_by_referent=( async_mock.CoroutineMock( return_value=[ {"cred_info": {"referent": "dummy-0"}}, @@ -509,8 +552,14 @@ async def test_called_auto_present_no_preview(self): ] ) ) - request_context.inject = async_mock.MagicMock(return_value=mock_holder) + ) + request_context.injector.bind_instance(IndyHolder, mock_holder) + with async_mock.patch.object( + test_module, "V20PresManager", autospec=True + ) as mock_pres_mgr, async_mock.patch.object( + test_module, "V20PresExRecord", autospec=True + ) as mock_pres_ex_rec_cls: mock_pres_ex_rec_cls.return_value = px_rec_instance mock_pres_ex_rec_cls.retrieve_by_tag_filter = async_mock.CoroutineMock( return_value=px_rec_instance @@ -531,6 +580,9 @@ async def test_called_auto_present_no_preview(self): mock_pres_mgr.return_value.receive_pres_request.assert_called_once_with( px_rec_instance ) + mock_oob_processor.find_oob_record_for_inbound_message.assert_called_once_with( + request_context + ) messages = responder.messages assert len(messages) == 1 (result, target) = messages[0] @@ -563,19 +615,25 @@ async def test_called_auto_present_pred_no_match(self): save_error_state=async_mock.CoroutineMock(), ) - with async_mock.patch.object( - test_module, "V20PresManager", autospec=True - ) as mock_pres_mgr, async_mock.patch.object( - test_module, "V20PresExRecord", autospec=True - ) as mock_pres_ex_rec_cls, async_mock.patch.object( - test_indy_handler, "IndyHolder", autospec=True - ) as mock_holder: + mock_oob_processor = async_mock.MagicMock( + find_oob_record_for_inbound_message=async_mock.CoroutineMock( + return_value=async_mock.MagicMock() + ) + ) + request_context.injector.bind_instance(OobMessageProcessor, mock_oob_processor) - mock_holder.get_credentials_for_presentation_request_by_referent = ( + mock_holder = async_mock.MagicMock( + get_credentials_for_presentation_request_by_referent=( async_mock.CoroutineMock(return_value=[]) ) - request_context.inject = async_mock.MagicMock(return_value=mock_holder) + ) + request_context.injector.bind_instance(IndyHolder, mock_holder) + with async_mock.patch.object( + test_module, "V20PresManager", autospec=True + ) as mock_pres_mgr, async_mock.patch.object( + test_module, "V20PresExRecord", autospec=True + ) as mock_pres_ex_rec_cls: mock_pres_ex_rec_cls.return_value = mock_px_rec mock_pres_ex_rec_cls.retrieve_by_tag_filter = async_mock.CoroutineMock( return_value=mock_px_rec @@ -597,6 +655,9 @@ async def test_called_auto_present_pred_no_match(self): mock_pres_mgr.return_value.receive_pres_request.assert_called_once_with( mock_px_rec ) + mock_oob_processor.find_oob_record_for_inbound_message.assert_called_once_with( + request_context + ) async def test_called_auto_present_pred_single_match(self): request_context = RequestContext.test_context() @@ -616,21 +677,27 @@ async def test_called_auto_present_pred_single_match(self): request_context.message_receipt = MessageReceipt() px_rec_instance = test_module.V20PresExRecord(auto_present=True) - with async_mock.patch.object( - test_module, "V20PresManager", autospec=True - ) as mock_pres_mgr, async_mock.patch.object( - test_module, "V20PresExRecord", autospec=True - ) as mock_pres_ex_rec_cls, async_mock.patch.object( - test_indy_handler, "IndyHolder", autospec=True - ) as mock_holder: + mock_oob_processor = async_mock.MagicMock( + find_oob_record_for_inbound_message=async_mock.CoroutineMock( + return_value=async_mock.MagicMock() + ) + ) + request_context.injector.bind_instance(OobMessageProcessor, mock_oob_processor) - mock_holder.get_credentials_for_presentation_request_by_referent = ( + mock_holder = async_mock.MagicMock( + get_credentials_for_presentation_request_by_referent=( async_mock.CoroutineMock( return_value=[{"cred_info": {"referent": "dummy-0"}}] ) ) - request_context.inject = async_mock.MagicMock(return_value=mock_holder) + ) + request_context.injector.bind_instance(IndyHolder, mock_holder) + with async_mock.patch.object( + test_module, "V20PresManager", autospec=True + ) as mock_pres_mgr, async_mock.patch.object( + test_module, "V20PresExRecord", autospec=True + ) as mock_pres_ex_rec_cls: mock_pres_ex_rec_cls.return_value = px_rec_instance mock_pres_ex_rec_cls.retrieve_by_tag_filter = async_mock.CoroutineMock( return_value=px_rec_instance @@ -651,6 +718,9 @@ async def test_called_auto_present_pred_single_match(self): mock_pres_mgr.return_value.receive_pres_request.assert_called_once_with( px_rec_instance ) + mock_oob_processor.find_oob_record_for_inbound_message.assert_called_once_with( + request_context + ) messages = responder.messages assert len(messages) == 1 (result, target) = messages[0] @@ -675,15 +745,15 @@ async def test_called_auto_present_pred_multi_match(self): request_context.message_receipt = MessageReceipt() px_rec_instance = test_module.V20PresExRecord(auto_present=True) - with async_mock.patch.object( - test_module, "V20PresManager", autospec=True - ) as mock_pres_mgr, async_mock.patch.object( - test_module, "V20PresExRecord", autospec=True - ) as mock_pres_ex_rec_cls, async_mock.patch.object( - test_indy_handler, "IndyHolder", autospec=True - ) as mock_holder: + mock_oob_processor = async_mock.MagicMock( + find_oob_record_for_inbound_message=async_mock.CoroutineMock( + return_value=async_mock.MagicMock() + ) + ) + request_context.injector.bind_instance(OobMessageProcessor, mock_oob_processor) - mock_holder.get_credentials_for_presentation_request_by_referent = ( + mock_holder = async_mock.MagicMock( + get_credentials_for_presentation_request_by_referent=( async_mock.CoroutineMock( return_value=[ {"cred_info": {"referent": "dummy-0"}}, @@ -691,8 +761,14 @@ async def test_called_auto_present_pred_multi_match(self): ] ) ) - request_context.inject = async_mock.MagicMock(return_value=mock_holder) + ) + request_context.injector.bind_instance(IndyHolder, mock_holder) + with async_mock.patch.object( + test_module, "V20PresManager", autospec=True + ) as mock_pres_mgr, async_mock.patch.object( + test_module, "V20PresExRecord", autospec=True + ) as mock_pres_ex_rec_cls: mock_pres_ex_rec_cls.return_value = px_rec_instance mock_pres_ex_rec_cls.retrieve_by_tag_filter = async_mock.CoroutineMock( return_value=px_rec_instance @@ -713,6 +789,9 @@ async def test_called_auto_present_pred_multi_match(self): mock_pres_mgr.return_value.receive_pres_request.assert_called_once_with( px_rec_instance ) + mock_oob_processor.find_oob_record_for_inbound_message.assert_called_once_with( + request_context + ) messages = responder.messages assert len(messages) == 1 (result, target) = messages[0] @@ -747,19 +826,15 @@ async def test_called_auto_present_multi_cred_match_reft(self): ], ) - px_rec_instance = test_module.V20PresExRecord( - pres_proposal=pres_proposal.serialize(), - auto_present=True, + mock_oob_processor = async_mock.MagicMock( + find_oob_record_for_inbound_message=async_mock.CoroutineMock( + return_value=async_mock.MagicMock() + ) ) - with async_mock.patch.object( - test_module, "V20PresManager", autospec=True - ) as mock_pres_mgr, async_mock.patch.object( - test_module, "V20PresExRecord", autospec=True - ) as mock_pres_ex_rec_cls, async_mock.patch.object( - test_indy_handler, "IndyHolder", autospec=True - ) as mock_holder: + request_context.injector.bind_instance(OobMessageProcessor, mock_oob_processor) - mock_holder.get_credentials_for_presentation_request_by_referent = ( + mock_holder = async_mock.MagicMock( + get_credentials_for_presentation_request_by_referent=( async_mock.CoroutineMock( return_value=[ { @@ -798,8 +873,18 @@ async def test_called_auto_present_multi_cred_match_reft(self): ] ) ) - request_context.inject = async_mock.MagicMock(return_value=mock_holder) + ) + request_context.injector.bind_instance(IndyHolder, mock_holder) + px_rec_instance = test_module.V20PresExRecord( + pres_proposal=pres_proposal.serialize(), + auto_present=True, + ) + with async_mock.patch.object( + test_module, "V20PresManager", autospec=True + ) as mock_pres_mgr, async_mock.patch.object( + test_module, "V20PresExRecord", autospec=True + ) as mock_pres_ex_rec_cls: mock_pres_ex_rec_cls.return_value = px_rec_instance mock_pres_ex_rec_cls.retrieve_by_tag_filter = async_mock.CoroutineMock( return_value=px_rec_instance @@ -820,6 +905,9 @@ async def test_called_auto_present_multi_cred_match_reft(self): mock_pres_mgr.return_value.receive_pres_request.assert_called_once_with( px_rec_instance ) + mock_oob_processor.find_oob_record_for_inbound_message.assert_called_once_with( + request_context + ) messages = responder.messages assert len(messages) == 1 (result, target) = messages[0] @@ -829,6 +917,7 @@ async def test_called_auto_present_multi_cred_match_reft(self): async def test_called_not_ready(self): request_context = RequestContext.test_context() request_context.message_receipt = MessageReceipt() + request_context.connection_record = async_mock.MagicMock() with async_mock.patch.object( test_module, "V20PresManager", autospec=True @@ -838,7 +927,35 @@ async def test_called_not_ready(self): request_context.connection_ready = False handler = test_module.V20PresRequestHandler() responder = MockResponder() - with self.assertRaises(test_module.HandlerException): + with self.assertRaises(test_module.HandlerException) as err: await handler.handle(request_context, responder) + assert ( + err.exception.message + == "Connection used for presentation request not ready" + ) + + assert not responder.messages + + async def test_no_conn_no_oob(self): + request_context = RequestContext.test_context() + request_context.message_receipt = MessageReceipt() + + mock_oob_processor = async_mock.MagicMock( + find_oob_record_for_inbound_message=async_mock.CoroutineMock( + # No oob record found + return_value=None + ) + ) + request_context.injector.bind_instance(OobMessageProcessor, mock_oob_processor) + + request_context.message = V20PresRequest() + handler = test_module.V20PresRequestHandler() + responder = MockResponder() + with self.assertRaises(test_module.HandlerException) as err: + await handler.handle(request_context, responder) + assert ( + err.exception.message + == "No connection or associated connectionless exchange found for presentation request" + ) assert not responder.messages diff --git a/aries_cloudagent/protocols/present_proof/v2_0/manager.py b/aries_cloudagent/protocols/present_proof/v2_0/manager.py index 688f4ca35f..b6c323d9eb 100644 --- a/aries_cloudagent/protocols/present_proof/v2_0/manager.py +++ b/aries_cloudagent/protocols/present_proof/v2_0/manager.py @@ -2,13 +2,13 @@ import logging -from typing import Tuple +from typing import Optional, Tuple +from ...out_of_band.v1_0.models.oob_record import OobRecord from ....connections.models.conn_record import ConnRecord from ....core.error import BaseError from ....core.profile import Profile from ....messaging.responder import BaseResponder -from ....storage.error import StorageNotFoundError from .messages.pres import V20Pres from .messages.pres_ack import V20PresAck @@ -161,7 +161,10 @@ async def create_bound_request( return pres_ex_record, pres_request_message async def create_exchange_for_request( - self, connection_id: str, pres_request_message: V20PresRequest + self, + connection_id: str, + pres_request_message: V20PresRequest, + auto_verify: bool = None, ): """ Create a presentation exchange record for input presentation request. @@ -182,6 +185,7 @@ async def create_exchange_for_request( role=V20PresExRecord.ROLE_VERIFIER, state=V20PresExRecord.STATE_REQUEST_SENT, pres_request=pres_request_message, + auto_verify=auto_verify, trace=(pres_request_message._trace is not None), ) async with self._profile.session() as session: @@ -259,9 +263,15 @@ async def create_pres( pres_exch_format = V20PresFormat.Format.get(format.format) if pres_exch_format: + if not request_data: + request_data_pres_exch = {} + else: + request_data_pres_exch = { + pres_exch_format.api: request_data.get(pres_exch_format.api) + } pres_tuple = await pres_exch_format.handler(self._profile).create_pres( pres_ex_record, - request_data, + request_data_pres_exch, ) if pres_tuple: pres_formats.append(pres_tuple) @@ -279,7 +289,8 @@ async def create_pres( presentations_attach=[attach for (_, attach) in pres_formats], ) - pres_message._thread = {"thid": pres_ex_record.thread_id} + # Assign thid (and optionally pthid) to message + pres_message.assign_thread_from(pres_ex_record.pres_request) pres_message.assign_trace_decorator( self._profile.settings, pres_ex_record.trace ) @@ -294,7 +305,12 @@ async def create_pres( await pres_ex_record.save(session, reason="create v2.0 presentation") return pres_ex_record, pres_message - async def receive_pres(self, message: V20Pres, conn_record: ConnRecord): + async def receive_pres( + self, + message: V20Pres, + connection_record: Optional[ConnRecord], + oob_record: Optional[OobRecord], + ): """ Receive a presentation, from message in context on manager creation. @@ -304,21 +320,30 @@ async def receive_pres(self, message: V20Pres, conn_record: ConnRecord): """ thread_id = message._thread_id - conn_id_filter = ( + # Normally we only set the connection_id to None if an oob record is present + # But present proof supports the old-style AIP-1 connectionless exchange that + # bypasses the oob record. So we can't verify if an oob record is associated with + # the exchange because it is possible that there is None + connection_id = ( None - if conn_record is None - else {"connection_id": conn_record.connection_id} + if oob_record + else connection_record.connection_id + if connection_record + else None ) + async with self._profile.session() as session: - try: - pres_ex_record = await V20PresExRecord.retrieve_by_tag_filter( - session, {"thread_id": thread_id}, conn_id_filter - ) - except StorageNotFoundError: - # Proof req not bound to any connection: requests_attach in OOB msg - pres_ex_record = await V20PresExRecord.retrieve_by_tag_filter( - session, {"thread_id": thread_id}, None - ) + pres_ex_record = await V20PresExRecord.retrieve_by_tag_filter( + session, + {"thread_id": thread_id}, + { + "role": V20PresExRecord.ROLE_VERIFIER, + }, + ) + + # Save connection id (if it wasn't already present) + if connection_record: + pres_ex_record.connection_id = connection_record.connection_id input_formats = message.formats @@ -339,13 +364,14 @@ async def receive_pres(self, message: V20Pres, conn_record: ConnRecord): ) pres_ex_record.pres = message pres_ex_record.state = V20PresExRecord.STATE_PRESENTATION_RECEIVED - async with self._profile.session() as session: await pres_ex_record.save(session, reason="receive v2.0 presentation") return pres_ex_record - async def verify_pres(self, pres_ex_record: V20PresExRecord): + async def verify_pres( + self, pres_ex_record: V20PresExRecord, responder: Optional[BaseResponder] = None + ): """ Verify a presentation. @@ -368,6 +394,8 @@ async def verify_pres(self, pres_ex_record: V20PresExRecord): ).verify_pres( pres_ex_record, ) + if pres_ex_record.verified == "false": + break pres_ex_record.state = V20PresExRecord.STATE_DONE @@ -375,11 +403,13 @@ async def verify_pres(self, pres_ex_record: V20PresExRecord): await pres_ex_record.save(session, reason="verify v2.0 presentation") if pres_request_msg.will_confirm: - await self.send_pres_ack(pres_ex_record) + await self.send_pres_ack(pres_ex_record, responder) return pres_ex_record - async def send_pres_ack(self, pres_ex_record: V20PresExRecord): + async def send_pres_ack( + self, pres_ex_record: V20PresExRecord, responder: Optional[BaseResponder] = None + ): """ Send acknowledgement of presentation receipt. @@ -387,10 +417,10 @@ async def send_pres_ack(self, pres_ex_record: V20PresExRecord): pres_ex_record: presentation exchange record with thread id """ - responder = self._profile.inject_or(BaseResponder) + responder = responder or self._profile.inject_or(BaseResponder) if responder: - pres_ack_message = V20PresAck() + pres_ack_message = V20PresAck(verification_result=pres_ex_record.verified) pres_ack_message._thread = {"thid": pres_ex_record.thread_id} pres_ack_message.assign_trace_decorator( self._profile.settings, pres_ex_record.trace @@ -398,6 +428,7 @@ async def send_pres_ack(self, pres_ex_record: V20PresExRecord): await responder.send_reply( pres_ack_message, + # connection_id can be none in case of connectionless connection_id=pres_ex_record.connection_id, ) else: @@ -414,13 +445,18 @@ async def receive_pres_ack(self, message: V20PresAck, conn_record: ConnRecord): presentation exchange record, retrieved and updated """ + connection_id = conn_record.connection_id if conn_record else None async with self._profile.session() as session: pres_ex_record = await V20PresExRecord.retrieve_by_tag_filter( session, {"thread_id": message._thread_id}, - {"connection_id": conn_record.connection_id}, + { + # connection_id can be null in connectionless + "connection_id": connection_id, + "role": V20PresExRecord.ROLE_PROVER, + }, ) - + pres_ex_record.verified = message._verification_result pres_ex_record.state = V20PresExRecord.STATE_DONE await pres_ex_record.save(session, reason="receive v2.0 presentation ack") @@ -439,12 +475,10 @@ async def receive_problem_report( """ # FIXME use transaction, fetch for_update async with self._profile.session() as session: - pres_ex_record = await ( - V20PresExRecord.retrieve_by_tag_filter( - session, - {"thread_id": message._thread_id}, - {"connection_id": connection_id}, - ) + pres_ex_record = await V20PresExRecord.retrieve_by_tag_filter( + session, + {"thread_id": message._thread_id}, + {"connection_id": connection_id}, ) pres_ex_record.state = V20PresExRecord.STATE_ABANDONED diff --git a/aries_cloudagent/protocols/present_proof/v2_0/messages/pres.py b/aries_cloudagent/protocols/present_proof/v2_0/messages/pres.py index d6b7861d9e..ae44c5a4d5 100644 --- a/aries_cloudagent/protocols/present_proof/v2_0/messages/pres.py +++ b/aries_cloudagent/protocols/present_proof/v2_0/messages/pres.py @@ -118,4 +118,8 @@ def get_attach_by_id(attach_id): atch = get_attach_by_id(fmt.attach_id) pres_format = V20PresFormat.Format.get(fmt.format) if pres_format: - pres_format.validate_fields(PRES_20, atch.content) + if isinstance(atch.content, Sequence): + for el in atch.content: + pres_format.validate_fields(PRES_20, el) + else: + pres_format.validate_fields(PRES_20, atch.content) diff --git a/aries_cloudagent/protocols/present_proof/v2_0/messages/pres_ack.py b/aries_cloudagent/protocols/present_proof/v2_0/messages/pres_ack.py index d9032f8d6f..27fba5ef3d 100644 --- a/aries_cloudagent/protocols/present_proof/v2_0/messages/pres_ack.py +++ b/aries_cloudagent/protocols/present_proof/v2_0/messages/pres_ack.py @@ -1,6 +1,6 @@ """Represents an explicit RFC 15 ack message, adopted into present-proof protocol.""" -from marshmallow import EXCLUDE +from marshmallow import EXCLUDE, fields, validate from ....notification.v1_0.messages.ack import V10Ack, V10AckSchema @@ -19,7 +19,7 @@ class Meta: message_type = PRES_20_ACK schema_class = "V20PresAckSchema" - def __init__(self, status: str = None, **kwargs): + def __init__(self, status: str = None, verification_result: str = None, **kwargs): """ Initialize an explicit ack message instance. @@ -28,6 +28,7 @@ def __init__(self, status: str = None, **kwargs): """ super().__init__(status, **kwargs) + self._verification_result = verification_result class V20PresAckSchema(V10AckSchema): @@ -38,3 +39,10 @@ class Meta: model_class = V20PresAck unknown = EXCLUDE + + verification_result = fields.Str( + required=False, + description="Whether presentation is verified: true or false", + example="true", + validate=validate.OneOf(["true", "false"]), + ) diff --git a/aries_cloudagent/protocols/present_proof/v2_0/messages/pres_webhook.py b/aries_cloudagent/protocols/present_proof/v2_0/messages/pres_webhook.py new file mode 100644 index 0000000000..cec96cd089 --- /dev/null +++ b/aries_cloudagent/protocols/present_proof/v2_0/messages/pres_webhook.py @@ -0,0 +1,39 @@ +"""v2.0 Presentation exchange record webhook.""" + + +class V20PresExRecordWebhook: + """Class representing a state only Presentation exchange record webhook.""" + + __acceptable_keys_list = [ + "connection_id", + "pres_ex_id", + "role", + "initiator", + "auto_present", + "auto_verify", + "error_msg", + "thread_id", + "state", + "trace", + "verified", + "verified_msgs", + "created_at", + "updated_at", + ] + + def __init__( + self, + **kwargs, + ): + """ + Initialize webhook object from V20PresExRecord. + + from a list of accepted attributes. + """ + [ + self.__setattr__(key, kwargs.get(key)) + for key in self.__acceptable_keys_list + if kwargs.get(key) is not None + ] + if kwargs.get("_id") is not None: + self.pres_ex_id = kwargs.get("_id") diff --git a/aries_cloudagent/protocols/present_proof/v2_0/messages/tests/test_pres.py b/aries_cloudagent/protocols/present_proof/v2_0/messages/tests/test_pres.py index a9c5112337..ccd14b0896 100644 --- a/aries_cloudagent/protocols/present_proof/v2_0/messages/tests/test_pres.py +++ b/aries_cloudagent/protocols/present_proof/v2_0/messages/tests/test_pres.py @@ -1662,6 +1662,129 @@ }""" ) +DIF_PROOF = json.loads( + """[ + { + "@context":[ + "https://www.w3.org/2018/credentials/v1" + ], + "type":[ + "VerifiablePresentation" + ], + "verifiableCredential":[ + { + "@context":[ + "https://www.w3.org/2018/credentials/v1", + "https://w3id.org/citizenship/v1", + "https://w3id.org/security/bbs/v1" + ], + "id":"https://issuer.oidp.uscis.gov/credentials/83627465", + "type":[ + "PermanentResidentCard", + "VerifiableCredential" + ], + "credentialSubject":{ + "id":"did:example:b34ca6cd37bbf23", + "type":[ + "Person", + "PermanentResident" + ], + "givenName":"JOHN" + }, + "issuanceDate":"2010-01-01T19:53:24Z", + "issuer":"did:key:zUC74bgefTdc43KS1psXgXf4jLaHyaj2qCQqQTXrtmSYGf1PxiJhrH6LGpaBMyj6tqAKmjGyMaS4RfNo2an77vT1HfzJUNPk4H7TCuJvSp4vet4Cu67kn2JSegoQNFSA1tbwU8v", + "proof":{ + "type":"BbsBlsSignatureProof2020", + "nonce":"3AuruhJQrXtEgiagiJ+FwVf2S0SnzUDJvnO61YecQsJ7ImR1mPcoVjJJ0HOhfkFpoYI=", + "proofValue":"ABkBuAaPlP5A7JWY78Xf69oBnsMLcD1RXbIFYhcLoXPXW12CG9glnnqnPLsGri5xsA3LcP0kg74X+sAjKXGRGy3uvp412Dm0FuohYNboQcLne5KOAa5AxU4bjmwQsxdfduVqhriro1N+YTkuB4SMmO/5ooL0N3OHsYdExg7nSzWqmZoqgp+3CwIxF0a/oyKTcxJORuIqAAAAdInlL9teSIX49NJGEZfBO7IrdjT2iggH/G0AlPWoEvrWIbuCRQ69K83n5o7oJVjqhAAAAAIaVmlAD6+FEKA4eg0OaWOKPrd5Kq8rv0vIwjJ71egxll0Fqq4zDWQ/+yl3Pteh0Wyuyvpm19/sj6tiCWj4PkA+rpxtR2bXpnrCTKUffFFNBjVvVziXDS0KWkGUB7XU9mjUa4USC7Iub3bZZCnFjQA5AAAADzkGwGD837r33e7OTrGEti8eAkvFDcyCgA4ck/X+5HJjAJclHWbl4SNQR8CiNZyzJpvxW+jbNBcwmEvocYArddk3F78Ki0Qnp6aU9eDgfOOx1iW2BXLUjrhq5I2hP5/WQF3CEDYRjczGjzM9T8/coeC36YAp0zJunIXUKb8SPDSOISafibYRYFB4xhlWKXWloDelafyujOBST8KZNM8FmF4DSbXrO8vmZbjuR/8ntUcUK7X2rNbuZ3M5eWZDF8pL+SA9gQitKfPHEocoYAdhgEAM7ZNAJ+TgOcx9gtZIhDWKDNnFxIeoOAylbD1xZd9xbWtq3Bk3R79xqsKxFRJRNxk/9b6fJruP292+qM5lxcZ1jUz/dJUYFI93hH4Mso75CjGRN78MAY9SNifl6H8qcxTpBn4332LlFhRznLbtnc4YSWA/fvVqaN9h2zCH/6AdbLKXGffV34EF7DadwJsi9jsc+YlSMn6qaIUIDTdGLwh4KKpSH5bVbg/mVCcXPTJplFgYwRsOdiQbZY/740dJyo1lPjQ0Lvdio8W2M8c73ujeJU70CNLkgjJAMUPGrCFtGxBH2eeLBQ0P95qRZAIcJ7U0MibZLaRjoUOuTla5BIt2038PJ6XhcY6BEJaLyJOPEQ==", + "verificationMethod":"did:key:zUC74bgefTdc43KS1psXgXf4jLaHyaj2qCQqQTXrtmSYGf1PxiJhrH6LGpaBMyj6tqAKmjGyMaS4RfNo2an77vT1HfzJUNPk4H7TCuJvSp4vet4Cu67kn2JSegoQNFSA1tbwU8v#zUC74bgefTdc43KS1psXgXf4jLaHyaj2qCQqQTXrtmSYGf1PxiJhrH6LGpaBMyj6tqAKmjGyMaS4RfNo2an77vT1HfzJUNPk4H7TCuJvSp4vet4Cu67kn2JSegoQNFSA1tbwU8v", + "proofPurpose":"assertionMethod", + "created":"2021-05-05T15:22:30.523465" + } + } + ], + "presentation_submission":{ + "id":"a5fcfe44-2c30-497d-af02-98e539da9a0f", + "definition_id":"32f54163-7166-48f1-93d8-ff217bdb0653", + "descriptor_map":[ + { + "id":"citizenship_input_1", + "format":"ldp_vp", + "path":"$.verifiableCredential[0]" + } + ] + }, + "proof":{ + "type":"Ed25519Signature2018", + "verificationMethod":"did:sov:4QxzWk3ajdnEA37NdNU5Kt#key-1", + "created":"2021-05-05T15:23:03.023971", + "proofPurpose":"authentication", + "challenge":"40429d49-5e8f-4ffc-baf8-e332412f1247", + "jws":"eyJhbGciOiAiRWREU0EiLCAiYjY0IjogZmFsc2UsICJjcml0IjogWyJiNjQiXX0..2uBYmg7muE9ZPVeAGo_ibVfLkCjf2hGshr2o5i8pAwFyNBM-kDHXofuq1MzJgb19wzb01VIu91hY_ajjt9KFAA" + } + }, + { + "@context":[ + "https://www.w3.org/2018/credentials/v1" + ], + "type":[ + "VerifiablePresentation" + ], + "verifiableCredential":[ + { + "@context":[ + "https://www.w3.org/2018/credentials/v1", + "https://w3id.org/citizenship/v1", + "https://w3id.org/security/bbs/v1" + ], + "id":"https://issuer.oidp.uscis.gov/credentials/83627465", + "type":[ + "PermanentResidentCard", + "VerifiableCredential" + ], + "credentialSubject":{ + "id":"did:example:b34ca6cd37bbf23", + "type":[ + "Person", + "PermanentResident" + ], + "givenName":"JOHN" + }, + "issuanceDate":"2010-01-01T19:53:24Z", + "issuer":"did:key:zUC74bgefTdc43KS1psXgXf4jLaHyaj2qCQqQTXrtmSYGf1PxiJhrH6LGpaBMyj6tqAKmjGyMaS4RfNo2an77vT1HfzJUNPk4H7TCuJvSp4vet4Cu67kn2JSegoQNFSA1tbwU8v", + "proof":{ + "type":"BbsBlsSignatureProof2020", + "nonce":"3AuruhJQrXtEgiagiJ+FwVf2S0SnzUDJvnO61YecQsJ7ImR1mPcoVjJJ0HOhfkFpoYI=", + "proofValue":"ABkBuAaPlP5A7JWY78Xf69oBnsMLcD1RXbIFYhcLoXPXW12CG9glnnqnPLsGri5xsA3LcP0kg74X+sAjKXGRGy3uvp412Dm0FuohYNboQcLne5KOAa5AxU4bjmwQsxdfduVqhriro1N+YTkuB4SMmO/5ooL0N3OHsYdExg7nSzWqmZoqgp+3CwIxF0a/oyKTcxJORuIqAAAAdInlL9teSIX49NJGEZfBO7IrdjT2iggH/G0AlPWoEvrWIbuCRQ69K83n5o7oJVjqhAAAAAIaVmlAD6+FEKA4eg0OaWOKPrd5Kq8rv0vIwjJ71egxll0Fqq4zDWQ/+yl3Pteh0Wyuyvpm19/sj6tiCWj4PkA+rpxtR2bXpnrCTKUffFFNBjVvVziXDS0KWkGUB7XU9mjUa4USC7Iub3bZZCnFjQA5AAAADzkGwGD837r33e7OTrGEti8eAkvFDcyCgA4ck/X+5HJjAJclHWbl4SNQR8CiNZyzJpvxW+jbNBcwmEvocYArddk3F78Ki0Qnp6aU9eDgfOOx1iW2BXLUjrhq5I2hP5/WQF3CEDYRjczGjzM9T8/coeC36YAp0zJunIXUKb8SPDSOISafibYRYFB4xhlWKXWloDelafyujOBST8KZNM8FmF4DSbXrO8vmZbjuR/8ntUcUK7X2rNbuZ3M5eWZDF8pL+SA9gQitKfPHEocoYAdhgEAM7ZNAJ+TgOcx9gtZIhDWKDNnFxIeoOAylbD1xZd9xbWtq3Bk3R79xqsKxFRJRNxk/9b6fJruP292+qM5lxcZ1jUz/dJUYFI93hH4Mso75CjGRN78MAY9SNifl6H8qcxTpBn4332LlFhRznLbtnc4YSWA/fvVqaN9h2zCH/6AdbLKXGffV34EF7DadwJsi9jsc+YlSMn6qaIUIDTdGLwh4KKpSH5bVbg/mVCcXPTJplFgYwRsOdiQbZY/740dJyo1lPjQ0Lvdio8W2M8c73ujeJU70CNLkgjJAMUPGrCFtGxBH2eeLBQ0P95qRZAIcJ7U0MibZLaRjoUOuTla5BIt2038PJ6XhcY6BEJaLyJOPEQ==", + "verificationMethod":"did:key:zUC74bgefTdc43KS1psXgXf4jLaHyaj2qCQqQTXrtmSYGf1PxiJhrH6LGpaBMyj6tqAKmjGyMaS4RfNo2an77vT1HfzJUNPk4H7TCuJvSp4vet4Cu67kn2JSegoQNFSA1tbwU8v#zUC74bgefTdc43KS1psXgXf4jLaHyaj2qCQqQTXrtmSYGf1PxiJhrH6LGpaBMyj6tqAKmjGyMaS4RfNo2an77vT1HfzJUNPk4H7TCuJvSp4vet4Cu67kn2JSegoQNFSA1tbwU8v", + "proofPurpose":"assertionMethod", + "created":"2021-05-05T15:22:30.523465" + } + } + ], + "presentation_submission":{ + "id":"a5fcfe44-2c30-497d-af02-98e539da9a0f", + "definition_id":"32f54163-7166-48f1-93d8-ff217bdb0653", + "descriptor_map":[ + { + "id":"citizenship_input_2", + "format":"ldp_vp", + "path":"$.verifiableCredential[0]" + } + ] + }, + "proof":{ + "type":"Ed25519Signature2018", + "verificationMethod":"did:sov:4QxzWk3ajdnEA37NdNU5Kt#key-1", + "created":"2021-05-05T15:23:03.023971", + "proofPurpose":"authentication", + "challenge":"40429d49-5e8f-4ffc-baf8-e332412f1247", + "jws":"eyJhbGciOiAiRWREU0EiLCAiYjY0IjogZmFsc2UsICJjcml0IjogWyJiNjQiXX0..2uBYmg7muE9ZPVeAGo_ibVfLkCjf2hGshr2o5i8pAwFyNBM-kDHXofuq1MzJgb19wzb01VIu91hY_ajjt9KFAA" + } + } + ]""" +) + PRES = V20Pres( comment="Test", formats=[ @@ -1678,6 +1801,22 @@ ], ) +PRES_DIF = V20Pres( + comment="Test", + formats=[ + V20PresFormat( + attach_id="dif", + format_=ATTACHMENT_FORMAT[PRES_20][V20PresFormat.Format.DIF.api], + ) + ], + presentations_attach=[ + AttachDecorator.data_json( + mapping=DIF_PROOF, + ident="dif", + ) + ], +) + class TestV20Pres(TestCase): """Presentation tests.""" @@ -1689,6 +1828,11 @@ def test_init_type(self): assert PRES.attachment(V20PresFormat.Format.INDY) == INDY_PROOF assert PRES._type == DIDCommPrefix.qualify_current(PRES_20) + assert PRES_DIF.presentations_attach[0].content == DIF_PROOF + assert len(PRES_DIF.formats) == len(PRES.presentations_attach) + assert PRES_DIF.attachment(V20PresFormat.Format.DIF) == DIF_PROOF + assert PRES_DIF._type == DIDCommPrefix.qualify_current(PRES_20) + def test_attachment_no_target_format(self): """Test attachment behaviour for only unknown formats.""" @@ -1740,3 +1884,9 @@ def test_serde(self): } ) V20Pres.deserialize(pres_dict) + + def test_serde_dif(self): + """Test deserialization dif.""" + pres_dict = PRES_DIF.serialize() + pres_obj = V20Pres.deserialize(pres_dict) + assert type(pres_obj) == V20Pres diff --git a/aries_cloudagent/protocols/present_proof/v2_0/models/pres_exchange.py b/aries_cloudagent/protocols/present_proof/v2_0/models/pres_exchange.py index db98cf0539..ec798b2803 100644 --- a/aries_cloudagent/protocols/present_proof/v2_0/models/pres_exchange.py +++ b/aries_cloudagent/protocols/present_proof/v2_0/models/pres_exchange.py @@ -15,6 +15,7 @@ from ..messages.pres_format import V20PresFormat from ..messages.pres_proposal import V20PresProposal, V20PresProposalSchema from ..messages.pres_request import V20PresRequest, V20PresRequestSchema +from ..messages.pres_webhook import V20PresExRecordWebhook from . import UNENCRYPTED_TAGS @@ -62,7 +63,9 @@ def __init__( pres_request: Union[V20PresRequest, Mapping] = None, # aries message pres: Union[V20Pres, Mapping] = None, # aries message verified: str = None, + verified_msgs: list = None, auto_present: bool = False, + auto_verify: bool = False, error_msg: str = None, trace: bool = False, # backward compat: BaseRecord.FromStorage() by_format: Mapping = None, # backward compat: BaseRecord.FromStorage() @@ -79,7 +82,9 @@ def __init__( self._pres_request = V20PresRequest.serde(pres_request) self._pres = V20Pres.serde(pres) self.verified = verified + self.verified_msgs = verified_msgs self.auto_present = auto_present + self.auto_verify = auto_verify self.error_msg = error_msg @property @@ -145,6 +150,7 @@ async def save_error_state( self, session: ProfileSession, *, + state: str = None, reason: str = None, log_params: Mapping[str, Any] = None, log_override: bool = False, @@ -159,10 +165,10 @@ async def save_error_state( override: Override configured logging regimen, print to stderr instead """ - if self._last_state == V20PresExRecord.STATE_ABANDONED: # already done + if self._last_state == state: # already done return - self.state = V20PresExRecord.STATE_ABANDONED + self.state = state or V20PresExRecord.STATE_ABANDONED if reason: self.error_msg = reason @@ -176,6 +182,33 @@ async def save_error_state( except StorageError as err: LOGGER.exception(err) + # Override + async def emit_event(self, session: ProfileSession, payload: Any = None): + """ + Emit an event. + + Args: + session: The profile session to use + payload: The event payload + """ + + if not self.RECORD_TOPIC: + return + + if self.state: + topic = f"{self.EVENT_NAMESPACE}::{self.RECORD_TOPIC}::{self.state}" + else: + topic = f"{self.EVENT_NAMESPACE}::{self.RECORD_TOPIC}" + + if session.profile.settings.get("debug.webhooks"): + if not payload: + payload = self.serialize() + else: + payload = V20PresExRecordWebhook(**self.__dict__) + payload = payload.__dict__ + + await session.profile.notify(topic, payload) + @property def record_value(self) -> Mapping: """Accessor for the JSON record value generated for this credential exchange.""" @@ -188,7 +221,9 @@ def record_value(self) -> Mapping: "role", "state", "verified", + "verified_msgs", "auto_present", + "auto_verify", "error_msg", "trace", ) @@ -237,11 +272,7 @@ class Meta: description="Present-proof exchange initiator: self or external", example=V20PresExRecord.INITIATOR_SELF, validate=validate.OneOf( - [ - getattr(V20PresExRecord, m) - for m in vars(V20PresExRecord) - if m.startswith("INITIATOR_") - ] + V20PresExRecord.get_attributes_by_prefix("INITIATOR_", walk_mro=False) ), ) role = fields.Str( @@ -249,22 +280,14 @@ class Meta: description="Present-proof exchange role: prover or verifier", example=V20PresExRecord.ROLE_PROVER, validate=validate.OneOf( - [ - getattr(V20PresExRecord, m) - for m in vars(V20PresExRecord) - if m.startswith("ROLE_") - ] + V20PresExRecord.get_attributes_by_prefix("ROLE_", walk_mro=False) ), ) state = fields.Str( required=False, description="Present-proof exchange state", validate=validate.OneOf( - [ - getattr(V20PresExRecord, m) - for m in vars(V20PresExRecord) - if m.startswith("STATE_") - ] + V20PresExRecord.get_attributes_by_prefix("STATE_", walk_mro=True) ), ) pres_proposal = fields.Nested( @@ -303,11 +326,21 @@ class Meta: example="true", validate=validate.OneOf(["true", "false"]), ) + verified_msgs = fields.List( + fields.Str( + required=False, + description="Proof verification warning or error information", + ), + required=False, + ) auto_present = fields.Bool( required=False, description="Prover choice to auto-present proof as verifier requests", example=False, ) + auto_verify = fields.Bool( + required=False, description="Verifier choice to auto-verify proof presentation" + ) error_msg = fields.Str( required=False, description="Error message", example="Invalid structure" ) diff --git a/aries_cloudagent/protocols/present_proof/v2_0/models/tests/test_record.py b/aries_cloudagent/protocols/present_proof/v2_0/models/tests/test_record.py index 3f1922cb8d..c22a6ff23b 100644 --- a/aries_cloudagent/protocols/present_proof/v2_0/models/tests/test_record.py +++ b/aries_cloudagent/protocols/present_proof/v2_0/models/tests/test_record.py @@ -110,7 +110,9 @@ async def test_record(self): "state": "state", "pres_proposal": pres_proposal.serialize(), "verified": "false", + "verified_msgs": None, "auto_present": True, + "auto_verify": False, "error_msg": "error", "trace": False, } diff --git a/aries_cloudagent/protocols/present_proof/v2_0/routes.py b/aries_cloudagent/protocols/present_proof/v2_0/routes.py index ede46b7d4f..e9d886b9ac 100644 --- a/aries_cloudagent/protocols/present_proof/v2_0/routes.py +++ b/aries_cloudagent/protocols/present_proof/v2_0/routes.py @@ -32,6 +32,7 @@ UUID4, ) from ....storage.error import StorageError, StorageNotFoundError +from ....storage.base import BaseStorage from ....storage.vc_holder.base import VCHolder from ....storage.vc_holder.vc_record import VCRecord from ....utils.tracing import trace_event, get_timer, AdminAPIMessageTracingSchema @@ -208,6 +209,11 @@ class V20PresCreateRequestRequestSchema(AdminAPIMessageTracingSchema): presentation_request = fields.Nested(V20PresRequestByFormatSchema(), required=True) comment = fields.Str(required=False, allow_none=True) + auto_verify = fields.Bool( + description="Verifier choice to auto-verify proof presentation", + required=False, + example=False, + ) trace = fields.Bool( description="Whether to trace event (default false)", required=False, @@ -223,6 +229,21 @@ class V20PresSendRequestRequestSchema(V20PresCreateRequestRequestSchema): ) +class V20PresentationSendRequestToProposalSchema(AdminAPIMessageTracingSchema): + """Request schema for sending a proof request bound to a proposal.""" + + auto_verify = fields.Bool( + description="Verifier choice to auto-verify proof presentation", + required=False, + example=False, + ) + trace = fields.Bool( + description="Whether to trace event (default false)", + required=False, + example=False, + ) + + class V20PresSpecByFormatRequestSchema(AdminAPIMessageTracingSchema): """Presentation specification schema by format, for send-presentation request.""" @@ -252,9 +273,10 @@ def validate_fields(self, data, **kwargs): ValidationError: if data does not have exactly one format. """ - if len(data.keys() & {f.api for f in V20PresFormat.Format}) != 1: + if len(data.keys() & {f.api for f in V20PresFormat.Format}) < 1: raise ValidationError( - "V20PresSpecByFormatRequestSchema must specify one presentation format" + "V20PresSpecByFormatRequestSchema must specify " + "at least one presentation format" ) @@ -309,7 +331,7 @@ async def _add_nonce(indy_proof_request: Mapping) -> Mapping: def _formats_attach(by_format: Mapping, msg_type: str, spec: str) -> Mapping: """Break out formats and proposals/requests/presentations for v2.0 messages.""" attach = [] - for (fmt_api, item_by_fmt) in by_format.items(): + for fmt_api, item_by_fmt in by_format.items(): if fmt_api == V20PresFormat.Format.INDY.api: attach.append( AttachDecorator.data_base64(mapping=item_by_fmt, ident=fmt_api) @@ -803,6 +825,10 @@ async def present_proof_create_request(request: web.BaseRequest): will_confirm=True, **_formats_attach(pres_request_spec, PRES_20_REQUEST, "request_presentations"), ) + pres_request_message.assign_thread_id(body.get("thread_id")) + auto_verify = body.get( + "auto_verify", context.settings.get("debug.auto_verify_presentation") + ) trace_msg = body.get("trace") pres_request_message.assign_trace_decorator( context.settings, @@ -815,6 +841,7 @@ async def present_proof_create_request(request: web.BaseRequest): pres_ex_record = await pres_manager.create_exchange_for_request( connection_id=None, pres_request_message=pres_request_message, + auto_verify=auto_verify, ) result = pres_ex_record.serialize() except (BaseModelError, StorageError) as err: @@ -880,6 +907,10 @@ async def present_proof_send_free_request(request: web.BaseRequest): will_confirm=True, **_formats_attach(pres_request_spec, PRES_20_REQUEST, "request_presentations"), ) + pres_request_message.assign_thread_id(body.get("thread_id")) + auto_verify = body.get( + "auto_verify", context.settings.get("debug.auto_verify_presentation") + ) trace_msg = body.get("trace") pres_request_message.assign_trace_decorator( context.settings, @@ -892,6 +923,7 @@ async def present_proof_send_free_request(request: web.BaseRequest): pres_ex_record = await pres_manager.create_exchange_for_request( connection_id=connection_id, pres_request_message=pres_request_message, + auto_verify=auto_verify, ) result = pres_ex_record.serialize() except (BaseModelError, StorageError) as err: @@ -918,7 +950,7 @@ async def present_proof_send_free_request(request: web.BaseRequest): summary="Sends a presentation request in reference to a proposal", ) @match_info_schema(V20PresExIdMatchInfoSchema()) -@request_schema(AdminAPIMessageTracingSchema()) +@request_schema(V20PresentationSendRequestToProposalSchema()) @response_schema(V20PresExRecordSchema(), 200, description="") async def present_proof_send_bound_request(request: web.BaseRequest): """ @@ -966,6 +998,9 @@ async def present_proof_send_bound_request(request: web.BaseRequest): if not conn_record.is_ready: raise web.HTTPForbidden(reason=f"Connection {connection_id} not ready") + pres_ex_record.auto_verify = body.get( + "auto_verify", context.settings.get("debug.auto_verify_presentation") + ) pres_manager = V20PresManager(profile) try: ( @@ -1025,11 +1060,8 @@ async def present_proof_send_presentation(request: web.BaseRequest): outbound_handler = request["outbound_message_router"] pres_ex_id = request.match_info["pres_ex_id"] body = await request.json() - if "dif" in body: - fmt = V20PresFormat.Format.get("dif").api - elif "indy" in body: - fmt = V20PresFormat.Format.get("indy").api - else: + supported_formats = ["dif", "indy"] + if not any(x in body for x in supported_formats): raise web.HTTPBadRequest( reason=( "No presentation format specification provided, " @@ -1055,22 +1087,27 @@ async def present_proof_send_presentation(request: web.BaseRequest): ) ) - connection_id = pres_ex_record.connection_id - try: - async with profile.session() as session: - conn_record = await ConnRecord.retrieve_by_id(session, connection_id) - except StorageNotFoundError as err: - raise web.HTTPBadRequest(reason=err.roll_up) from err + # Fetch connection if exchange has record + conn_record = None + if pres_ex_record.connection_id: + try: + async with profile.session() as session: + conn_record = await ConnRecord.retrieve_by_id( + session, pres_ex_record.connection_id + ) + except StorageNotFoundError as err: + raise web.HTTPBadRequest(reason=err.roll_up) from err - if not conn_record.is_ready: - raise web.HTTPForbidden(reason=f"Connection {connection_id} not ready") + if conn_record and not conn_record.is_ready: + raise web.HTTPForbidden( + reason=f"Connection {pres_ex_record.connection_id} not ready" + ) pres_manager = V20PresManager(profile) try: - request_data = {fmt: body.get(fmt)} pres_ex_record, pres_message = await pres_manager.create_pres( pres_ex_record, - request_data=request_data, + request_data=body, comment=comment, ) result = pres_ex_record.serialize() @@ -1097,7 +1134,7 @@ async def present_proof_send_presentation(request: web.BaseRequest): context.settings, trace_msg, ) - await outbound_handler(pres_message, connection_id=connection_id) + await outbound_handler(pres_message, connection_id=pres_ex_record.connection_id) trace_event( context.settings, @@ -1197,13 +1234,13 @@ async def present_proof_problem_report(request: web.BaseRequest): description = body["description"] try: - async with await context.profile.session() as session: + async with context.profile.session() as session: pres_ex_record = await V20PresExRecord.retrieve_by_id(session, pres_ex_id) + await pres_ex_record.save_error_state( + session, + reason=f"created problem report: {description}", + ) report = problem_report_for_record(pres_ex_record, description) - await pres_ex_record.save_error_state( - session, - reason=f"created problem report: {description}", - ) except StorageNotFoundError as err: # other party does not care about meta-problems raise web.HTTPNotFound(reason=err.roll_up) from err except StorageError as err: @@ -1234,8 +1271,17 @@ async def present_proof_remove(request: web.BaseRequest): pres_ex_record = None try: async with context.profile.session() as session: - pres_ex_record = await V20PresExRecord.retrieve_by_id(session, pres_ex_id) - await pres_ex_record.delete_record(session) + try: + pres_ex_record = await V20PresExRecord.retrieve_by_id( + session, pres_ex_id + ) + await pres_ex_record.delete_record(session) + except (BaseModelError, ValidationError): + storage = session.inject(BaseStorage) + storage_record = await storage.get_record( + record_type=V20PresExRecord.RECORD_TYPE, record_id=pres_ex_id + ) + await storage.delete_record(storage_record) except StorageNotFoundError as err: raise web.HTTPNotFound(reason=err.roll_up) from err except StorageError as err: diff --git a/aries_cloudagent/protocols/present_proof/v2_0/tests/test_manager.py b/aries_cloudagent/protocols/present_proof/v2_0/tests/test_manager.py index bf293f3cb6..14a22ce4d8 100644 --- a/aries_cloudagent/protocols/present_proof/v2_0/tests/test_manager.py +++ b/aries_cloudagent/protocols/present_proof/v2_0/tests/test_manager.py @@ -20,6 +20,8 @@ ) from .....messaging.decorators.attach_decorator import AttachDecorator from .....messaging.responder import BaseResponder, MockResponder +from .....multitenant.base import BaseMultitenantManager +from .....multitenant.manager import MultitenantManager from .....storage.error import StorageNotFoundError from ...indy import pres_exch_handler as test_indy_util_module @@ -29,6 +31,7 @@ from ..formats.dif.handler import DIFPresFormatHandler from ..formats.dif.tests.test_handler import ( DIF_PRES_REQUEST_B as DIF_PRES_REQ, + DIF_PRES_REQUEST_A as DIF_PRES_REQ_ALT, DIF_PRES, ) from ..formats.indy import handler as test_indy_handler @@ -46,6 +49,10 @@ from ..messages.pres_request import V20PresRequest from ..models.pres_exchange import V20PresExRecord +from .....vc.vc_ld.validation_result import PresentationVerificationResult +from .....vc.tests.document_loader import custom_document_loader +from .....vc.ld_proofs import DocumentLoader + CONN_ID = "connection_id" ISSUER_DID = "NcYxiDXkpYi6ov5FcYDi1e" S_ID = f"{ISSUER_DID}:2:vidya:1.0" @@ -474,7 +481,7 @@ async def setUp(self): Verifier = async_mock.MagicMock(IndyVerifier, autospec=True) self.verifier = Verifier() self.verifier.verify_presentation = async_mock.CoroutineMock( - return_value="true" + return_value=("true", []) ) injector.bind_instance(IndyVerifier, self.verifier) @@ -551,7 +558,7 @@ async def test_receive_proposal(self): assert px_rec.state == V20PresExRecord.STATE_PROPOSAL_RECEIVED - async def test_create_bound_request(self): + async def test_create_bound_request_a(self): comment = "comment" proposal = V20PresProposal( @@ -585,6 +592,34 @@ async def test_create_bound_request(self): assert ret_px_rec is px_rec px_rec.save.assert_called_once() + async def test_create_bound_request_b(self): + comment = "comment" + + proposal = V20PresProposal( + formats=[ + V20PresFormat( + attach_id="indy", + format_=ATTACHMENT_FORMAT[PRES_20_PROPOSAL][ + V20PresFormat.Format.INDY.api + ], + ) + ], + proposals_attach=[ + AttachDecorator.data_base64(INDY_PROOF_REQ_NAME, ident="indy") + ], + ) + px_rec = V20PresExRecord( + pres_proposal=proposal.serialize(), + role=V20PresExRecord.ROLE_VERIFIER, + ) + px_rec.save = async_mock.CoroutineMock() + (ret_px_rec, pres_req_msg) = await self.manager.create_bound_request( + pres_ex_record=px_rec, + comment=comment, + ) + assert ret_px_rec is px_rec + px_rec.save.assert_called_once() + async def test_create_bound_request_no_format(self): px_rec = V20PresExRecord( pres_proposal=V20PresProposal( @@ -688,15 +723,9 @@ async def test_receive_pres_catch_diferror(self): V20PresExRecord, "retrieve_by_tag_filter", autospec=True ) as retrieve_ex: mock_receive_pres.return_value = False - retrieve_ex.side_effect = [ - StorageNotFoundError("no such record"), # cover out-of-band - px_rec, - ] + retrieve_ex.side_effect = [px_rec] with self.assertRaises(V20PresManagerError) as context: - await self.manager.receive_pres( - pres_x, - connection_record, - ) + await self.manager.receive_pres(pres_x, connection_record, None) assert "Unable to verify received presentation." in str(context.exception) async def test_create_exchange_for_request(self): @@ -782,6 +811,67 @@ async def test_create_pres_indy(self): save_ex.assert_called_once() assert px_rec_out.state == V20PresExRecord.STATE_PRESENTATION_SENT + async def test_create_pres_indy_and_dif(self): + pres_request = V20PresRequest( + formats=[ + V20PresFormat( + attach_id="indy", + format_=ATTACHMENT_FORMAT[PRES_20_REQUEST][ + V20PresFormat.Format.INDY.api + ], + ), + V20PresFormat( + attach_id="dif", + format_=ATTACHMENT_FORMAT[PRES_20_REQUEST][ + V20PresFormat.Format.DIF.api + ], + ), + ], + request_presentations_attach=[ + AttachDecorator.data_base64(INDY_PROOF_REQ_NAME, ident="indy"), + AttachDecorator.data_json(DIF_PRES_REQ, ident="dif"), + ], + ) + px_rec_in = V20PresExRecord(pres_request=pres_request.serialize()) + more_magic_rr = async_mock.MagicMock( + get_or_fetch_local_tails_path=async_mock.CoroutineMock( + return_value="/tmp/sample/tails/path" + ) + ) + with async_mock.patch.object( + V20PresExRecord, "save", autospec=True + ) as save_ex, async_mock.patch.object( + test_indy_handler, "AttachDecorator", autospec=True + ) as mock_attach_decorator_indy, async_mock.patch.object( + test_indy_util_module, "RevocationRegistry", autospec=True + ) as mock_rr, async_mock.patch.object( + DIFPresFormatHandler, "create_pres", autospec=True + ) as mock_create_pres: + mock_rr.from_definition = async_mock.MagicMock(return_value=more_magic_rr) + + mock_attach_decorator_indy.data_base64 = async_mock.MagicMock( + return_value=mock_attach_decorator_indy + ) + + mock_create_pres.return_value = ( + PRES_20, + AttachDecorator.data_json(DIF_PRES, ident="dif"), + ) + + req_creds = await indy_proof_req_preview2indy_requested_creds( + INDY_PROOF_REQ_NAME, preview=None, holder=self.holder + ) + request_data = {"indy": req_creds, "dif": DIF_PRES_REQ} + assert not req_creds["self_attested_attributes"] + assert len(req_creds["requested_attributes"]) == 2 + assert len(req_creds["requested_predicates"]) == 1 + + (px_rec_out, pres_msg) = await self.manager.create_pres( + px_rec_in, request_data + ) + save_ex.assert_called_once() + assert px_rec_out.state == V20PresExRecord.STATE_PRESENTATION_SENT + async def test_create_pres_proof_req_non_revoc_interval_none(self): indy_proof_req_vcx = deepcopy(INDY_PROOF_REQ_NAME) indy_proof_req_vcx["non_revoked"] = None # simulate interop with indy-vcx @@ -805,7 +895,15 @@ async def test_create_pres_proof_req_non_revoc_interval_none(self): return_value="/tmp/sample/tails/path" ) ) + self.profile.context.injector.bind_instance( + BaseMultitenantManager, + async_mock.MagicMock(MultitenantManager, autospec=True), + ) with async_mock.patch.object( + IndyLedgerRequestsExecutor, + "get_ledger_for_identifier", + async_mock.CoroutineMock(return_value=("test_ledger_id", self.ledger)), + ), async_mock.patch.object( V20PresExRecord, "save", autospec=True ) as save_ex, async_mock.patch.object( test_indy_handler, "AttachDecorator", autospec=True @@ -1261,6 +1359,7 @@ async def test_receive_pres(self): AttachDecorator.data_base64(INDY_PROOF, ident="indy") ], ) + pres.assign_thread_id("thread-id") px_rec_dummy = V20PresExRecord( pres_proposal=pres_proposal.serialize(), @@ -1282,12 +1381,13 @@ async def test_receive_pres(self): "session", async_mock.MagicMock(return_value=self.profile.session()), ) as session: - retrieve_ex.side_effect = [ - StorageNotFoundError("no such record"), # cover out-of-band - px_rec_dummy, - ] - px_rec_out = await self.manager.receive_pres(pres, connection_record) - assert retrieve_ex.call_count == 2 + retrieve_ex.side_effect = [px_rec_dummy] + px_rec_out = await self.manager.receive_pres(pres, connection_record, None) + retrieve_ex.assert_called_once_with( + session.return_value, + {"thread_id": "thread-id"}, + {"role": V20PresExRecord.ROLE_VERIFIER, "connection_id": CONN_ID}, + ) save_ex.assert_called_once() assert px_rec_out.state == (V20PresExRecord.STATE_PRESENTATION_RECEIVED) @@ -1334,6 +1434,7 @@ async def test_receive_pres_receive_pred_value_mismatch_punt_to_indy(self): AttachDecorator.data_base64(INDY_PROOF, ident="indy") ], ) + pres.assign_thread_id("thread-id") px_rec_dummy = V20PresExRecord( pres_proposal=pres_proposal.serialize(), @@ -1355,12 +1456,171 @@ async def test_receive_pres_receive_pred_value_mismatch_punt_to_indy(self): "session", async_mock.MagicMock(return_value=self.profile.session()), ) as session: - retrieve_ex.side_effect = [ - StorageNotFoundError("no such record"), # cover out-of-band - px_rec_dummy, - ] - px_rec_out = await self.manager.receive_pres(pres, connection_record) - assert retrieve_ex.call_count == 2 + retrieve_ex.side_effect = [px_rec_dummy] + px_rec_out = await self.manager.receive_pres(pres, connection_record, None) + retrieve_ex.assert_called_once_with( + session.return_value, + {"thread_id": "thread-id"}, + {"role": V20PresExRecord.ROLE_VERIFIER, "connection_id": CONN_ID}, + ) + save_ex.assert_called_once() + assert px_rec_out.state == (V20PresExRecord.STATE_PRESENTATION_RECEIVED) + + async def test_receive_pres_indy_no_predicate_restrictions(self): + connection_record = async_mock.MagicMock(connection_id=CONN_ID) + indy_proof_req = { + "name": PROOF_REQ_NAME, + "version": PROOF_REQ_VERSION, + "nonce": PROOF_REQ_NONCE, + "requested_attributes": { + "0_player_uuid": { + "name": "player", + "restrictions": [{"cred_def_id": CD_ID}], + "non_revoked": {"from": NOW, "to": NOW}, + }, + "0_screencapture_uuid": { + "name": "screenCapture", + "restrictions": [{"cred_def_id": CD_ID}], + "non_revoked": {"from": NOW, "to": NOW}, + }, + }, + "requested_predicates": { + "0_highscore_GE_uuid": { + "name": "highScore", + "p_type": ">=", + "p_value": 1000000, + "restrictions": [], + "non_revoked": {"from": NOW, "to": NOW}, + } + }, + } + pres_request = V20PresRequest( + formats=[ + V20PresFormat( + attach_id="indy", + format_=ATTACHMENT_FORMAT[PRES_20_REQUEST][ + V20PresFormat.Format.INDY.api + ], + ) + ], + request_presentations_attach=[ + AttachDecorator.data_base64(indy_proof_req, ident="indy") + ], + ) + pres = V20Pres( + formats=[ + V20PresFormat( + attach_id="indy", + format_=ATTACHMENT_FORMAT[PRES_20][V20PresFormat.Format.INDY.api], + ) + ], + presentations_attach=[ + AttachDecorator.data_base64(INDY_PROOF, ident="indy") + ], + ) + pres.assign_thread_id("thread-id") + + px_rec_dummy = V20PresExRecord( + pres_request=pres_request.serialize(), + ) + + # cover by_format property + by_format = px_rec_dummy.by_format + + assert by_format.get("pres_request").get("indy") == indy_proof_req + + with async_mock.patch.object( + V20PresExRecord, "save", autospec=True + ) as save_ex, async_mock.patch.object( + V20PresExRecord, "retrieve_by_tag_filter", autospec=True + ) as retrieve_ex, async_mock.patch.object( + self.profile, + "session", + async_mock.MagicMock(return_value=self.profile.session()), + ) as session: + retrieve_ex.side_effect = [px_rec_dummy] + px_rec_out = await self.manager.receive_pres(pres, connection_record, None) + retrieve_ex.assert_called_once_with( + session.return_value, + {"thread_id": "thread-id"}, + {"role": V20PresExRecord.ROLE_VERIFIER, "connection_id": CONN_ID}, + ) + save_ex.assert_called_once() + assert px_rec_out.state == (V20PresExRecord.STATE_PRESENTATION_RECEIVED) + + async def test_receive_pres_indy_no_attr_restrictions(self): + connection_record = async_mock.MagicMock(connection_id=CONN_ID) + indy_proof_req = { + "name": PROOF_REQ_NAME, + "version": PROOF_REQ_VERSION, + "nonce": PROOF_REQ_NONCE, + "requested_attributes": { + "0_player_uuid": { + "name": "player", + "restrictions": [], + "non_revoked": {"from": NOW, "to": NOW}, + } + }, + "requested_predicates": {}, + } + proof = deepcopy(INDY_PROOF) + proof["requested_proof"]["revealed_attrs"] = { + "0_player_uuid": { + "sub_proof_index": 0, + "raw": "Richie Knucklez", + "encoded": "516439982", + } + } + proof["requested_proof"]["predicates"] = {} + pres_request = V20PresRequest( + formats=[ + V20PresFormat( + attach_id="indy", + format_=ATTACHMENT_FORMAT[PRES_20_REQUEST][ + V20PresFormat.Format.INDY.api + ], + ) + ], + request_presentations_attach=[ + AttachDecorator.data_base64(indy_proof_req, ident="indy") + ], + ) + pres = V20Pres( + formats=[ + V20PresFormat( + attach_id="indy", + format_=ATTACHMENT_FORMAT[PRES_20][V20PresFormat.Format.INDY.api], + ) + ], + presentations_attach=[AttachDecorator.data_base64(proof, ident="indy")], + ) + pres.assign_thread_id("thread-id") + + px_rec_dummy = V20PresExRecord( + pres_request=pres_request.serialize(), + ) + + # cover by_format property + by_format = px_rec_dummy.by_format + + assert by_format.get("pres_request").get("indy") == indy_proof_req + + with async_mock.patch.object( + V20PresExRecord, "save", autospec=True + ) as save_ex, async_mock.patch.object( + V20PresExRecord, "retrieve_by_tag_filter", autospec=True + ) as retrieve_ex, async_mock.patch.object( + self.profile, + "session", + async_mock.MagicMock(return_value=self.profile.session()), + ) as session: + retrieve_ex.side_effect = [px_rec_dummy] + px_rec_out = await self.manager.receive_pres(pres, connection_record, None) + retrieve_ex.assert_called_once_with( + session.return_value, + {"thread_id": "thread-id"}, + {"role": V20PresExRecord.ROLE_VERIFIER, "connection_id": CONN_ID}, + ) save_ex.assert_called_once() assert px_rec_out.state == (V20PresExRecord.STATE_PRESENTATION_RECEIVED) @@ -1421,7 +1681,7 @@ async def test_receive_pres_bait_and_switch_attr_name(self): ) as retrieve_ex: retrieve_ex.return_value = px_rec_dummy with self.assertRaises(V20PresFormatHandlerError) as context: - await self.manager.receive_pres(pres_x, connection_record) + await self.manager.receive_pres(pres_x, connection_record, None) assert "does not satisfy proof request restrictions" in str( context.exception ) @@ -1477,7 +1737,7 @@ async def test_receive_pres_bait_and_switch_attr_name(self): ) as retrieve_ex: retrieve_ex.return_value = px_rec_dummy with self.assertRaises(V20PresFormatHandlerError) as context: - await self.manager.receive_pres(pres_x, connection_record) + await self.manager.receive_pres(pres_x, connection_record, None) assert "Presentation referent" in str(context.exception) async def test_receive_pres_bait_and_switch_attr_names(self): @@ -1536,7 +1796,7 @@ async def test_receive_pres_bait_and_switch_attr_names(self): ) as retrieve_ex: retrieve_ex.return_value = px_rec_dummy with self.assertRaises(V20PresFormatHandlerError) as context: - await self.manager.receive_pres(pres_x, connection_record) + await self.manager.receive_pres(pres_x, connection_record, None) assert "does not satisfy proof request restrictions " in str( context.exception ) @@ -1592,7 +1852,7 @@ async def test_receive_pres_bait_and_switch_attr_names(self): ) as retrieve_ex: retrieve_ex.return_value = px_rec_dummy with self.assertRaises(V20PresFormatHandlerError) as context: - await self.manager.receive_pres(pres_x, connection_record) + await self.manager.receive_pres(pres_x, connection_record, None) assert "Presentation referent" in str(context.exception) async def test_receive_pres_bait_and_switch_pred(self): @@ -1649,7 +1909,7 @@ async def test_receive_pres_bait_and_switch_pred(self): ) as retrieve_ex: retrieve_ex.return_value = px_rec_dummy with self.assertRaises(V20PresFormatHandlerError) as context: - await self.manager.receive_pres(pres_x, connection_record) + await self.manager.receive_pres(pres_x, connection_record, None) assert "not in proposal request" in str(context.exception) indy_proof_req["requested_predicates"]["0_highscore_GE_uuid"] = { @@ -1707,7 +1967,7 @@ async def test_receive_pres_bait_and_switch_pred(self): ) as retrieve_ex: retrieve_ex.return_value = px_rec_dummy with self.assertRaises(V20PresFormatHandlerError) as context: - await self.manager.receive_pres(pres_x, connection_record) + await self.manager.receive_pres(pres_x, connection_record, None) assert "shenanigans not in presentation" in str(context.exception) indy_proof_req["requested_predicates"]["0_highscore_GE_uuid"] = { @@ -1765,7 +2025,7 @@ async def test_receive_pres_bait_and_switch_pred(self): ) as retrieve_ex: retrieve_ex.return_value = px_rec_dummy with self.assertRaises(V20PresFormatHandlerError) as context: - await self.manager.receive_pres(pres_x, connection_record) + await self.manager.receive_pres(pres_x, connection_record, None) assert "highScore mismatches proposal request" in str(context.exception) indy_proof_req["requested_predicates"]["0_highscore_GE_uuid"] = { @@ -1823,7 +2083,7 @@ async def test_receive_pres_bait_and_switch_pred(self): ) as retrieve_ex: retrieve_ex.return_value = px_rec_dummy with self.assertRaises(V20PresFormatHandlerError) as context: - await self.manager.receive_pres(pres_x, connection_record) + await self.manager.receive_pres(pres_x, connection_record, None) assert "does not satisfy proof request restrictions " in str( context.exception ) @@ -1858,13 +2118,103 @@ async def test_verify_pres(self): pres_request=pres_request, pres=pres, ) + self.profile.context.injector.bind_instance( + BaseMultitenantManager, + async_mock.MagicMock(MultitenantManager, autospec=True), + ) + with async_mock.patch.object( + IndyLedgerRequestsExecutor, + "get_ledger_for_identifier", + async_mock.CoroutineMock(return_value=("test_ledger_id", self.ledger)), + ), async_mock.patch.object(V20PresExRecord, "save", autospec=True) as save_ex: + px_rec_out = await self.manager.verify_pres(px_rec_in) + save_ex.assert_called_once() - with async_mock.patch.object(V20PresExRecord, "save", autospec=True) as save_ex: + assert px_rec_out.state == (V20PresExRecord.STATE_DONE) + + async def test_verify_pres_indy_and_dif(self): + pres_request = V20PresRequest( + formats=[ + V20PresFormat( + attach_id="indy", + format_=ATTACHMENT_FORMAT[PRES_20_REQUEST][ + V20PresFormat.Format.INDY.api + ], + ), + V20PresFormat( + attach_id="dif", + format_=ATTACHMENT_FORMAT[PRES_20_REQUEST][ + V20PresFormat.Format.DIF.api + ], + ), + ], + will_confirm=True, + request_presentations_attach=[ + AttachDecorator.data_base64(INDY_PROOF_REQ_NAME, ident="indy"), + AttachDecorator.data_json(DIF_PRES_REQ, ident="dif"), + ], + ) + pres = V20Pres( + formats=[ + V20PresFormat( + attach_id="indy", + format_=ATTACHMENT_FORMAT[PRES_20][V20PresFormat.Format.INDY.api], + ), + V20PresFormat( + attach_id="dif", + format_=ATTACHMENT_FORMAT[PRES_20][V20PresFormat.Format.DIF.api], + ), + ], + presentations_attach=[ + AttachDecorator.data_base64(INDY_PROOF, ident="indy"), + AttachDecorator.data_json(DIF_PRES, ident="dif"), + ], + ) + px_rec_in = V20PresExRecord( + pres_request=pres_request, + pres=pres, + ) + + self.profile.context.injector.bind_instance( + DocumentLoader, custom_document_loader + ) + self.profile.context.injector.bind_instance( + BaseMultitenantManager, + async_mock.MagicMock(MultitenantManager, autospec=True), + ) + with async_mock.patch.object( + IndyLedgerRequestsExecutor, + "get_ledger_for_identifier", + async_mock.CoroutineMock(return_value=("test_ledger_id", self.ledger)), + ), async_mock.patch.object(V20PresExRecord, "save", autospec=True) as save_ex: px_rec_out = await self.manager.verify_pres(px_rec_in) save_ex.assert_called_once() assert px_rec_out.state == (V20PresExRecord.STATE_DONE) + with async_mock.patch.object( + IndyLedgerRequestsExecutor, + "get_ledger_for_identifier", + async_mock.CoroutineMock(return_value=("test_ledger_id", self.ledger)), + ), async_mock.patch( + "aries_cloudagent.vc.vc_ld.verify.verify_presentation", + async_mock.CoroutineMock( + return_value=PresentationVerificationResult(verified=False) + ), + ), async_mock.patch.object( + IndyVerifier, + "verify_presentation", + async_mock.CoroutineMock( + return_value=PresentationVerificationResult(verified=True) + ), + ), async_mock.patch.object( + V20PresExRecord, "save", autospec=True + ) as save_ex: + px_rec_out = await self.manager.verify_pres(px_rec_in) + save_ex.assert_called_once() + assert px_rec_out.state == (V20PresExRecord.STATE_DONE) + assert px_rec_out.verified == "false" + async def test_send_pres_ack(self): px_rec = V20PresExRecord() @@ -1875,13 +2225,31 @@ async def test_send_pres_ack(self): messages = responder.messages assert len(messages) == 1 + px_rec = V20PresExRecord(verified="true") + + responder = MockResponder() + self.profile.context.injector.bind_instance(BaseResponder, responder) + + await self.manager.send_pres_ack(px_rec) + messages = responder.messages + assert len(messages) == 1 + + px_rec = V20PresExRecord(verified="false") + + responder = MockResponder() + self.profile.context.injector.bind_instance(BaseResponder, responder) + + await self.manager.send_pres_ack(px_rec) + messages = responder.messages + assert len(messages) == 1 + async def test_send_pres_ack_no_responder(self): px_rec = V20PresExRecord() self.profile.context.injector.clear_binding(BaseResponder) await self.manager.send_pres_ack(px_rec) - async def test_receive_pres_ack(self): + async def test_receive_pres_ack_a(self): conn_record = async_mock.MagicMock(connection_id=CONN_ID) px_rec_dummy = V20PresExRecord() @@ -1898,6 +2266,24 @@ async def test_receive_pres_ack(self): assert px_rec_out.state == V20PresExRecord.STATE_DONE + async def test_receive_pres_ack_b(self): + conn_record = async_mock.MagicMock(connection_id=CONN_ID) + + px_rec_dummy = V20PresExRecord() + message = async_mock.MagicMock(_verification_result="true") + + with async_mock.patch.object( + V20PresExRecord, "save", autospec=True + ) as save_ex, async_mock.patch.object( + V20PresExRecord, "retrieve_by_tag_filter", autospec=True + ) as retrieve_ex: + retrieve_ex.return_value = px_rec_dummy + px_rec_out = await self.manager.receive_pres_ack(message, conn_record) + save_ex.assert_called_once() + + assert px_rec_out.state == V20PresExRecord.STATE_DONE + assert px_rec_out.verified == "true" + async def test_receive_problem_report(self): connection_id = "connection-id" stored_exchange = V20PresExRecord( @@ -1962,7 +2348,7 @@ async def test_receive_problem_report_x(self): "retrieve_by_tag_filter", async_mock.CoroutineMock(), ) as retrieve_ex: - retrieve_ex.side_effect = test_module.StorageNotFoundError("No such record") + retrieve_ex.side_effect = StorageNotFoundError("No such record") - with self.assertRaises(test_module.StorageNotFoundError): + with self.assertRaises(StorageNotFoundError): await self.manager.receive_problem_report(problem, connection_id) diff --git a/aries_cloudagent/protocols/present_proof/v2_0/tests/test_routes.py b/aries_cloudagent/protocols/present_proof/v2_0/tests/test_routes.py index 7678bed49d..0d13acc826 100644 --- a/aries_cloudagent/protocols/present_proof/v2_0/tests/test_routes.py +++ b/aries_cloudagent/protocols/present_proof/v2_0/tests/test_routes.py @@ -212,8 +212,7 @@ async def test_validate(self): schema = test_module.V20PresSpecByFormatRequestSchema() schema.validate_fields({"indy": {"...": "..."}}) schema.validate_fields({"dif": {"...": "..."}}) - with self.assertRaises(test_module.ValidationError): - schema.validate_fields({"indy": {"...": "..."}, "dif": {"...": "..."}}) + schema.validate_fields({"indy": {"...": "..."}, "dif": {"...": "..."}}) with self.assertRaises(test_module.ValidationError): schema.validate_fields({}) with self.assertRaises(test_module.ValidationError): @@ -2048,7 +2047,7 @@ async def test_present_proof_send_presentation_bad_state(self): test_module, "V20PresExRecord", autospec=True ) as mock_px_rec_cls: mock_px_rec_inst = async_mock.MagicMock( - connection_id="dummy", + connection_id=None, state=test_module.V20PresExRecord.STATE_DONE, serialize=async_mock.MagicMock( return_value={"thread_id": "sample-thread-id"} diff --git a/aries_cloudagent/protocols/revocation_notification/definition.py b/aries_cloudagent/protocols/revocation_notification/definition.py index 62bddef6f5..baf2b7b433 100644 --- a/aries_cloudagent/protocols/revocation_notification/definition.py +++ b/aries_cloudagent/protocols/revocation_notification/definition.py @@ -6,5 +6,11 @@ "minimum_minor_version": 0, "current_minor_version": 0, "path": "v1_0", - } + }, + { + "major_version": 2, + "minimum_minor_version": 0, + "current_minor_version": 0, + "path": "v2_0", + }, ] diff --git a/aries_cloudagent/protocols/revocation_notification/v1_0/models/rev_notification_record.py b/aries_cloudagent/protocols/revocation_notification/v1_0/models/rev_notification_record.py index eac5bd2cee..3b43b233ff 100644 --- a/aries_cloudagent/protocols/revocation_notification/v1_0/models/rev_notification_record.py +++ b/aries_cloudagent/protocols/revocation_notification/v1_0/models/rev_notification_record.py @@ -27,6 +27,7 @@ class Meta: "rev_reg_id", "cred_rev_id", "connection_id", + "version", } def __init__( @@ -38,6 +39,7 @@ def __init__( connection_id: str = None, thread_id: str = None, comment: str = None, + version: str = None, **kwargs, ): """Construct record.""" @@ -47,6 +49,7 @@ def __init__( self.connection_id = connection_id self.thread_id = thread_id self.comment = comment + self.version = version @property def revocation_notification_id(self) -> Optional[str]: @@ -73,6 +76,7 @@ async def query_by_ids( rev_reg_id: the rev reg id by which to filter """ tag_filter = { + **{"version": "v1_0"}, **{"cred_rev_id": cred_rev_id for _ in [""] if cred_rev_id}, **{"rev_reg_id": rev_reg_id for _ in [""] if rev_reg_id}, } @@ -101,6 +105,7 @@ async def query_by_rev_reg_id( rev_reg_id: the rev reg id by which to filter """ tag_filter = { + **{"version": "v1_0"}, **{"rev_reg_id": rev_reg_id for _ in [""] if rev_reg_id}, } @@ -157,3 +162,7 @@ class Meta: description="Optional comment to include in revocation notification", required=False, ) + version = fields.Str( + description="Version of Revocation Notification to send out", + required=False, + ) diff --git a/aries_cloudagent/protocols/revocation_notification/v1_0/models/tests/test_rev_notification_record.py b/aries_cloudagent/protocols/revocation_notification/v1_0/models/tests/test_rev_notification_record.py index c845f715ca..304ec37a90 100644 --- a/aries_cloudagent/protocols/revocation_notification/v1_0/models/tests/test_rev_notification_record.py +++ b/aries_cloudagent/protocols/revocation_notification/v1_0/models/tests/test_rev_notification_record.py @@ -21,6 +21,7 @@ def rec(): connection_id="mock_connection_id", thread_id="mock_thread_id", comment="mock_comment", + version="v1_0", ) @@ -50,6 +51,7 @@ async def test_storage(profile, rec): another = RevNotificationRecord( rev_reg_id="mock_rev_reg_id", cred_rev_id="mock_cred_rev_id", + version="v1_0", ) await another.save(session) await RevNotificationRecord.query_by_ids( diff --git a/aries_cloudagent/protocols/revocation_notification/v1_0/routes.py b/aries_cloudagent/protocols/revocation_notification/v1_0/routes.py index 83ba81fe63..cdefdaf642 100644 --- a/aries_cloudagent/protocols/revocation_notification/v1_0/routes.py +++ b/aries_cloudagent/protocols/revocation_notification/v1_0/routes.py @@ -32,7 +32,6 @@ async def on_revocation_published(profile: Profile, event: Event): """Handle issuer revoke event.""" LOGGER.debug("Sending notification of revocation to recipient: %s", event.payload) - should_notify = profile.settings.get("revocation.notify", False) responder = profile.inject(BaseResponder) crids = event.payload.get("crids") or [] @@ -46,10 +45,9 @@ async def on_revocation_published(profile: Profile, event: Event): for record in records: await record.delete_record(session) - if should_notify: - await responder.send( - record.to_message(), connection_id=record.connection_id - ) + await responder.send( + record.to_message(), connection_id=record.connection_id + ) except StorageNotFoundError: LOGGER.info( diff --git a/aries_cloudagent/protocols/revocation_notification/v1_0/tests/test_routes.py b/aries_cloudagent/protocols/revocation_notification/v1_0/tests/test_routes.py index 6fe38c848b..b4805d8059 100644 --- a/aries_cloudagent/protocols/revocation_notification/v1_0/tests/test_routes.py +++ b/aries_cloudagent/protocols/revocation_notification/v1_0/tests/test_routes.py @@ -50,7 +50,6 @@ async def test_on_revocation_published(profile: Profile, responder: MockResponde event = Event(topic, {"rev_reg_id": "mock", "crids": ["mock"]}) assert isinstance(profile.settings, Settings) - profile.settings["revocation.notify"] = True with mock.patch.object(test_module, "RevNotificationRecord", MockRec): await test_module.on_revocation_published(profile, event) @@ -60,32 +59,6 @@ async def test_on_revocation_published(profile: Profile, responder: MockResponde assert responder.messages -@pytest.mark.asyncio -async def test_on_revocation_published_no_notify( - profile: Profile, responder: MockResponder -): - """Test revocation published event handler.""" - mock_rec = mock.MagicMock() - mock_rec.cred_rev_id = "mock" - mock_rec.delete_record = mock.CoroutineMock() - - MockRec = mock.MagicMock() - MockRec.query_by_rev_reg_id = mock.CoroutineMock(return_value=[mock_rec]) - - topic = f"{REVOCATION_EVENT_PREFIX}{REVOCATION_PUBLISHED_EVENT}::mock" - event = Event(topic, {"rev_reg_id": "mock", "crids": ["mock"]}) - - assert isinstance(profile.settings, Settings) - profile.settings["revocation.notify"] = False - - with mock.patch.object(test_module, "RevNotificationRecord", MockRec): - await test_module.on_revocation_published(profile, event) - - MockRec.query_by_rev_reg_id.assert_called_once() - mock_rec.delete_record.assert_called_once() - assert not responder.messages - - @pytest.mark.asyncio async def test_on_revocation_published_x_not_found( profile: Profile, responder: MockResponder diff --git a/aries_cloudagent/transport/outbound/queue/__init__.py b/aries_cloudagent/protocols/revocation_notification/v2_0/__init__.py similarity index 100% rename from aries_cloudagent/transport/outbound/queue/__init__.py rename to aries_cloudagent/protocols/revocation_notification/v2_0/__init__.py diff --git a/aries_cloudagent/transport/outbound/queue/tests/__init__.py b/aries_cloudagent/protocols/revocation_notification/v2_0/handlers/__init__.py similarity index 100% rename from aries_cloudagent/transport/outbound/queue/tests/__init__.py rename to aries_cloudagent/protocols/revocation_notification/v2_0/handlers/__init__.py diff --git a/aries_cloudagent/protocols/revocation_notification/v2_0/handlers/revoke_handler.py b/aries_cloudagent/protocols/revocation_notification/v2_0/handlers/revoke_handler.py new file mode 100644 index 0000000000..f2ffafe7e0 --- /dev/null +++ b/aries_cloudagent/protocols/revocation_notification/v2_0/handlers/revoke_handler.py @@ -0,0 +1,44 @@ +"""Handler for revoke message.""" + +from .....messaging.base_handler import BaseHandler +from .....messaging.request_context import RequestContext +from .....messaging.responder import BaseResponder + +from ..messages.revoke import Revoke + + +class RevokeHandler(BaseHandler): + """Handler for revoke message.""" + + RECIEVED_TOPIC = "acapy::revocation-notification-v2::received" + WEBHOOK_TOPIC = "acapy::webhook::revocation-notification-v2" + + async def handle(self, context: RequestContext, responder: BaseResponder): + """Handle revoke message.""" + assert isinstance(context.message, Revoke) + self._logger.debug( + "Received notification of revocation for %s cred %s with comment: %s", + context.message.revocation_format, + context.message.credential_id, + context.message.comment, + ) + # Emit a webhook + if context.settings.get("revocation.monitor_notification"): + await context.profile.notify( + self.WEBHOOK_TOPIC, + { + "revocation_format": context.message.revocation_format, + "credential_id": context.message.credential_id, + "comment": context.message.comment, + }, + ) + + # Emit an event + await context.profile.notify( + self.RECIEVED_TOPIC, + { + "revocation_format": context.message.revocation_format, + "credential_id": context.message.credential_id, + "comment": context.message.comment, + }, + ) diff --git a/aries_cloudagent/protocols/revocation_notification/v2_0/handlers/tests/__init__.py b/aries_cloudagent/protocols/revocation_notification/v2_0/handlers/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/aries_cloudagent/protocols/revocation_notification/v2_0/handlers/tests/test_revoke_handler.py b/aries_cloudagent/protocols/revocation_notification/v2_0/handlers/tests/test_revoke_handler.py new file mode 100644 index 0000000000..a93a314e23 --- /dev/null +++ b/aries_cloudagent/protocols/revocation_notification/v2_0/handlers/tests/test_revoke_handler.py @@ -0,0 +1,75 @@ +"""Test RevokeHandler.""" + +import pytest + +from ......config.settings import Settings +from ......core.event_bus import EventBus, MockEventBus +from ......core.in_memory import InMemoryProfile +from ......core.profile import Profile +from ......messaging.request_context import RequestContext +from ......messaging.responder import MockResponder, BaseResponder +from ...messages.revoke import Revoke +from ..revoke_handler import RevokeHandler + + +@pytest.fixture +def event_bus(): + yield MockEventBus() + + +@pytest.fixture +def responder(): + yield MockResponder() + + +@pytest.fixture +def profile(event_bus): + yield InMemoryProfile.test_profile(bind={EventBus: event_bus}) + + +@pytest.fixture +def message(): + yield Revoke( + revocation_format="indy-anoncreds", + credential_id="mock_cred_revocation_id", + comment="mock_comment", + ) + + +@pytest.fixture +def context(profile: Profile, message: Revoke): + request_context = RequestContext(profile) + request_context.message = message + yield request_context + + +@pytest.mark.asyncio +async def test_handle( + context: RequestContext, responder: BaseResponder, event_bus: MockEventBus +): + await RevokeHandler().handle(context, responder) + assert event_bus.events + [(_, received)] = event_bus.events + assert received.topic == RevokeHandler.RECIEVED_TOPIC + assert "revocation_format" in received.payload + assert "credential_id" in received.payload + assert "comment" in received.payload + + +@pytest.mark.asyncio +async def test_handle_monitor( + context: RequestContext, responder: BaseResponder, event_bus: MockEventBus +): + context.settings["revocation.monitor_notification"] = True + await RevokeHandler().handle(context, responder) + [(_, webhook), (_, received)] = event_bus.events + + assert webhook.topic == RevokeHandler.WEBHOOK_TOPIC + assert "revocation_format" in received.payload + assert "credential_id" in received.payload + assert "comment" in webhook.payload + + assert received.topic == RevokeHandler.RECIEVED_TOPIC + assert "revocation_format" in received.payload + assert "credential_id" in received.payload + assert "comment" in received.payload diff --git a/aries_cloudagent/protocols/revocation_notification/v2_0/message_types.py b/aries_cloudagent/protocols/revocation_notification/v2_0/message_types.py new file mode 100644 index 0000000000..4033d5c8b7 --- /dev/null +++ b/aries_cloudagent/protocols/revocation_notification/v2_0/message_types.py @@ -0,0 +1,20 @@ +"""Message type identifiers for Revocation Notification protocol.""" + +from ...didcomm_prefix import DIDCommPrefix + + +SPEC_URI = ( + "https://github.com/hyperledger/aries-rfcs/blob/main/features/" + "0721-revocation-notification-v2/README.md" +) +PROTOCOL = "revocation_notification" +VERSION = "2.0" +BASE = f"{PROTOCOL}/{VERSION}" + +# Message types +REVOKE = f"{BASE}/revoke" + +PROTOCOL_PACKAGE = "aries_cloudagent.protocols.revocation_notification.v2_0" +MESSAGE_TYPES = DIDCommPrefix.qualify_all( + {REVOKE: f"{PROTOCOL_PACKAGE}.messages.revoke.Revoke"} +) diff --git a/aries_cloudagent/protocols/revocation_notification/v2_0/messages/__init__.py b/aries_cloudagent/protocols/revocation_notification/v2_0/messages/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/aries_cloudagent/protocols/revocation_notification/v2_0/messages/revoke.py b/aries_cloudagent/protocols/revocation_notification/v2_0/messages/revoke.py new file mode 100644 index 0000000000..93c0829a2a --- /dev/null +++ b/aries_cloudagent/protocols/revocation_notification/v2_0/messages/revoke.py @@ -0,0 +1,69 @@ +"""Revoke message.""" + +from marshmallow import fields, validate +from .....messaging.agent_message import AgentMessage, AgentMessageSchema +from .....messaging.decorators.please_ack_decorator import ( + PleaseAckDecorator, + PleaseAckDecoratorSchema, +) +from .....messaging.valid import UUIDFour +from ..message_types import PROTOCOL_PACKAGE, REVOKE + +HANDLER_CLASS = f"{PROTOCOL_PACKAGE}.handlers.revoke_handler.RevokeHandler" + + +class Revoke(AgentMessage): + """Class representing revoke message.""" + + class Meta: + """Revoke Meta.""" + + handler_class = HANDLER_CLASS + message_type = REVOKE + schema_class = "RevokeSchema" + + def __init__( + self, + *, + revocation_format: str, + credential_id: str, + please_ack: PleaseAckDecorator = None, + comment: str = None, + **kwargs, + ): + """Initialize revoke message.""" + super().__init__(**kwargs) + self.revocation_format = revocation_format + self.credential_id = credential_id + self.comment = comment + + +class RevokeSchema(AgentMessageSchema): + """Schema of Revoke message.""" + + class Meta: + """RevokeSchema Meta.""" + + model_class = Revoke + + revocation_format = fields.Str( + required=True, + description=("The format of the credential revocation ID"), + example="indy-anoncreds", + validate=validate.OneOf(["indy-anoncreds"]), + ) + credential_id = fields.Str( + required=True, + description=("Credential ID of the issued credential to be revoked"), + example=UUIDFour.EXAMPLE, + ) + please_ack = fields.Nested( + PleaseAckDecoratorSchema, + required=False, + description="Whether or not the holder should acknowledge receipt", + data_key="~please_ack", + ) + comment = fields.Str( + required=False, + description="Human readable information about revocation notification", + ) diff --git a/aries_cloudagent/protocols/revocation_notification/v2_0/messages/tests/__init__.py b/aries_cloudagent/protocols/revocation_notification/v2_0/messages/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/aries_cloudagent/protocols/revocation_notification/v2_0/messages/tests/test_revoke.py b/aries_cloudagent/protocols/revocation_notification/v2_0/messages/tests/test_revoke.py new file mode 100644 index 0000000000..c20c93ee1a --- /dev/null +++ b/aries_cloudagent/protocols/revocation_notification/v2_0/messages/tests/test_revoke.py @@ -0,0 +1,14 @@ +"""Test Revoke Message.""" + +from ..revoke import Revoke + + +def test_instantiate(): + msg = Revoke( + revocation_format="indy-anoncreds", + credential_id="test-id", + comment="test", + ) + assert msg.revocation_format == "indy-anoncreds" + assert msg.credential_id == "test-id" + assert msg.comment == "test" diff --git a/aries_cloudagent/protocols/revocation_notification/v2_0/models/__init__.py b/aries_cloudagent/protocols/revocation_notification/v2_0/models/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/aries_cloudagent/protocols/revocation_notification/v2_0/models/rev_notification_record.py b/aries_cloudagent/protocols/revocation_notification/v2_0/models/rev_notification_record.py new file mode 100644 index 0000000000..b91cc74967 --- /dev/null +++ b/aries_cloudagent/protocols/revocation_notification/v2_0/models/rev_notification_record.py @@ -0,0 +1,169 @@ +"""Store revocation notification details until revocation is published.""" + +from typing import Optional, Sequence + +from marshmallow import fields +from marshmallow.utils import EXCLUDE + + +from .....core.profile import ProfileSession +from .....messaging.models.base_record import BaseRecord, BaseRecordSchema +from .....messaging.valid import INDY_CRED_REV_ID, INDY_REV_REG_ID, UUID4 +from .....storage.error import StorageNotFoundError, StorageDuplicateError +from ..messages.revoke import Revoke + + +class RevNotificationRecord(BaseRecord): + """Revocation Notification Record.""" + + class Meta: + """RevNotificationRecord Meta.""" + + schema_class = "RevNotificationRecordSchema" + + RECORD_TYPE = "revocation_notification" + RECORD_ID_NAME = "revocation_notification_id" + TAG_NAMES = { + "rev_reg_id", + "cred_rev_id", + "connection_id", + "version", + } + + def __init__( + self, + *, + revocation_notification_id: str = None, + rev_reg_id: str = None, + cred_rev_id: str = None, + connection_id: str = None, + thread_id: str = None, + comment: str = None, + version: str = None, + **kwargs, + ): + """Construct record.""" + super().__init__(revocation_notification_id, **kwargs) + self.rev_reg_id = rev_reg_id + self.cred_rev_id = cred_rev_id + self.connection_id = connection_id + self.thread_id = thread_id + self.comment = comment + self.version = version + + @property + def revocation_notification_id(self) -> Optional[str]: + """Return record id.""" + return self._id + + @property + def record_value(self) -> dict: + """Return record value.""" + return {prop: getattr(self, prop) for prop in ("thread_id", "comment")} + + @classmethod + async def query_by_ids( + cls, + session: ProfileSession, + cred_rev_id: str, + rev_reg_id: str, + ) -> "RevNotificationRecord": + """Retrieve revocation notification record by cred rev id and/or rev reg id. + + Args: + session: the profile session to use + cred_rev_id: the cred rev id by which to filter + rev_reg_id: the rev reg id by which to filter + """ + tag_filter = { + **{"version": "v2_0"}, + **{"cred_rev_id": cred_rev_id for _ in [""] if cred_rev_id}, + **{"rev_reg_id": rev_reg_id for _ in [""] if rev_reg_id}, + } + + result = await cls.query(session, tag_filter) + if len(result) > 1: + raise StorageDuplicateError( + "More than one RevNotificationRecord was found for the given IDs" + ) + if not result: + raise StorageNotFoundError( + "No RevNotificationRecord found for the given IDs" + ) + return result[0] + + @classmethod + async def query_by_rev_reg_id( + cls, + session: ProfileSession, + rev_reg_id: str, + ) -> Sequence["RevNotificationRecord"]: + """Retrieve revocation notification records by rev reg id. + + Args: + session: the profile session to use + rev_reg_id: the rev reg id by which to filter + """ + tag_filter = { + **{"version": "v2_0"}, + **{"rev_reg_id": rev_reg_id for _ in [""] if rev_reg_id}, + } + + return await cls.query(session, tag_filter) + + def to_message(self): + """Return a revocation notification constructed from this record.""" + if not self.thread_id: + raise ValueError( + "No thread ID set on revocation notification record, " + "cannot create message" + ) + return Revoke( + revocation_format="indy-anoncreds", + credential_id=f"{self.rev_reg_id}::{self.cred_rev_id}", + comment=self.comment, + ) + + +class RevNotificationRecordSchema(BaseRecordSchema): + """Revocation Notification Record Schema.""" + + class Meta: + """RevNotificationRecordSchema Meta.""" + + model_class = "RevNotificationRecord" + unknown = EXCLUDE + + rev_reg_id = fields.Str( + required=False, + description="Revocation registry identifier", + **INDY_REV_REG_ID, + ) + cred_rev_id = fields.Str( + required=False, + description="Credential revocation identifier", + **INDY_CRED_REV_ID, + ) + connection_id = fields.Str( + description=( + "Connection ID to which the revocation notification will be sent; " + "required if notify is true" + ), + required=False, + **UUID4, + ) + thread_id = fields.Str( + description=( + "Thread ID of the credential exchange message thread resulting in " + "the credential now being revoked; required if notify is true" + ), + required=False, + ) + comment = fields.Str( + description="Optional comment to include in revocation notification", + required=False, + ) + version = fields.Str( + description="Version of Revocation Notification to send out", + required=False, + ) diff --git a/aries_cloudagent/protocols/revocation_notification/v2_0/models/tests/__init__.py b/aries_cloudagent/protocols/revocation_notification/v2_0/models/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/aries_cloudagent/protocols/revocation_notification/v2_0/models/tests/test_rev_notification_record.py b/aries_cloudagent/protocols/revocation_notification/v2_0/models/tests/test_rev_notification_record.py new file mode 100644 index 0000000000..e6bb64e5c7 --- /dev/null +++ b/aries_cloudagent/protocols/revocation_notification/v2_0/models/tests/test_rev_notification_record.py @@ -0,0 +1,70 @@ +"""Test RevNotificationRecord.""" + +import pytest + +from ......core.in_memory import InMemoryProfile +from ......storage.error import StorageDuplicateError, StorageNotFoundError +from ...messages.revoke import Revoke +from ..rev_notification_record import RevNotificationRecord + + +@pytest.fixture +def profile(): + yield InMemoryProfile.test_profile() + + +@pytest.fixture +def rec(): + yield RevNotificationRecord( + rev_reg_id="mock_rev_reg_id", + cred_rev_id="mock_cred_rev_id", + connection_id="mock_connection_id", + thread_id="mock_thread_id", + comment="mock_comment", + version="v2_0", + ) + + +@pytest.mark.asyncio +async def test_storage(profile, rec): + async with profile.session() as session: + await rec.save(session) + recalled = await RevNotificationRecord.retrieve_by_id( + session, rec.revocation_notification_id + ) + assert recalled == rec + recalled = await RevNotificationRecord.query_by_ids( + session, cred_rev_id="mock_cred_rev_id", rev_reg_id="mock_rev_reg_id" + ) + assert recalled == rec + [recalled] = await RevNotificationRecord.query_by_rev_reg_id( + session, rev_reg_id="mock_rev_reg_id" + ) + assert recalled == rec + + with pytest.raises(StorageNotFoundError): + await RevNotificationRecord.query_by_ids( + session, cred_rev_id="unknown", rev_reg_id="unknown" + ) + + with pytest.raises(StorageDuplicateError): + another = RevNotificationRecord( + rev_reg_id="mock_rev_reg_id", + cred_rev_id="mock_cred_rev_id", + version="v2_0", + ) + await another.save(session) + await RevNotificationRecord.query_by_ids( + session, cred_rev_id="mock_cred_rev_id", rev_reg_id="mock_rev_reg_id" + ) + + +def test_to_message(rec): + message = rec.to_message() + assert isinstance(message, Revoke) + assert message.credential_id == f"{rec.rev_reg_id}::{rec.cred_rev_id}" + assert message.comment == rec.comment + + with pytest.raises(ValueError): + rec.thread_id = None + rec.to_message() diff --git a/aries_cloudagent/protocols/revocation_notification/v2_0/routes.py b/aries_cloudagent/protocols/revocation_notification/v2_0/routes.py new file mode 100644 index 0000000000..83ba81fe63 --- /dev/null +++ b/aries_cloudagent/protocols/revocation_notification/v2_0/routes.py @@ -0,0 +1,76 @@ +"""Routes for revocation notification.""" +import logging +import re + +from ....core.event_bus import Event, EventBus +from ....core.profile import Profile +from ....messaging.responder import BaseResponder +from ....revocation.util import ( + REVOCATION_CLEAR_PENDING_EVENT, + REVOCATION_PUBLISHED_EVENT, + REVOCATION_EVENT_PREFIX, +) +from ....storage.error import StorageError, StorageNotFoundError +from .models.rev_notification_record import RevNotificationRecord + +LOGGER = logging.getLogger(__name__) + + +def register_events(event_bus: EventBus): + """Register to handle events.""" + event_bus.subscribe( + re.compile(f"^{REVOCATION_EVENT_PREFIX}{REVOCATION_PUBLISHED_EVENT}.*"), + on_revocation_published, + ) + event_bus.subscribe( + re.compile(f"^{REVOCATION_EVENT_PREFIX}{REVOCATION_CLEAR_PENDING_EVENT}.*"), + on_pending_cleared, + ) + + +async def on_revocation_published(profile: Profile, event: Event): + """Handle issuer revoke event.""" + LOGGER.debug("Sending notification of revocation to recipient: %s", event.payload) + + should_notify = profile.settings.get("revocation.notify", False) + responder = profile.inject(BaseResponder) + crids = event.payload.get("crids") or [] + + try: + async with profile.session() as session: + records = await RevNotificationRecord.query_by_rev_reg_id( + session, + rev_reg_id=event.payload["rev_reg_id"], + ) + records = [record for record in records if record.cred_rev_id in crids] + + for record in records: + await record.delete_record(session) + if should_notify: + await responder.send( + record.to_message(), connection_id=record.connection_id + ) + + except StorageNotFoundError: + LOGGER.info( + "No revocation notification record found for revoked credential; " + "no notification will be sent" + ) + except StorageError: + LOGGER.exception("Failed to retrieve revocation notification record") + + +async def on_pending_cleared(profile: Profile, event: Event): + """Handle pending cleared event.""" + + # Query by rev reg ID + async with profile.session() as session: + notifications = await RevNotificationRecord.query_by_rev_reg_id( + session, event.payload["rev_reg_id"] + ) + + # Delete + async with profile.transaction() as txn: + for notification in notifications: + await notification.delete_record(txn) + await txn.commit() diff --git a/aries_cloudagent/protocols/revocation_notification/v2_0/tests/__init__.py b/aries_cloudagent/protocols/revocation_notification/v2_0/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/aries_cloudagent/protocols/revocation_notification/v2_0/tests/test_routes.py b/aries_cloudagent/protocols/revocation_notification/v2_0/tests/test_routes.py new file mode 100644 index 0000000000..6fe38c848b --- /dev/null +++ b/aries_cloudagent/protocols/revocation_notification/v2_0/tests/test_routes.py @@ -0,0 +1,140 @@ +"""Test routes.py""" +from asynctest import mock +import pytest + +from .. import routes as test_module +from .....config.settings import Settings +from .....core.event_bus import Event, MockEventBus +from .....core.in_memory import InMemoryProfile +from .....core.profile import Profile +from .....messaging.responder import BaseResponder, MockResponder +from .....revocation.util import ( + REVOCATION_CLEAR_PENDING_EVENT, + REVOCATION_EVENT_PREFIX, + REVOCATION_PUBLISHED_EVENT, +) +from .....storage.error import StorageError, StorageNotFoundError + + +@pytest.fixture +def responder(): + yield MockResponder() + + +@pytest.fixture +def profile(responder): + yield InMemoryProfile.test_profile(bind={BaseResponder: responder}) + + +def test_register_events(): + """Test handlers are added on register. + + This test need not be particularly in depth to keep it from getting brittle. + """ + event_bus = MockEventBus() + test_module.register_events(event_bus) + assert event_bus.topic_patterns_to_subscribers + + +@pytest.mark.asyncio +async def test_on_revocation_published(profile: Profile, responder: MockResponder): + """Test revocation published event handler.""" + mock_rec = mock.MagicMock() + mock_rec.cred_rev_id = "mock" + mock_rec.delete_record = mock.CoroutineMock() + + MockRec = mock.MagicMock() + MockRec.query_by_rev_reg_id = mock.CoroutineMock(return_value=[mock_rec]) + + topic = f"{REVOCATION_EVENT_PREFIX}{REVOCATION_PUBLISHED_EVENT}::mock" + event = Event(topic, {"rev_reg_id": "mock", "crids": ["mock"]}) + + assert isinstance(profile.settings, Settings) + profile.settings["revocation.notify"] = True + + with mock.patch.object(test_module, "RevNotificationRecord", MockRec): + await test_module.on_revocation_published(profile, event) + + MockRec.query_by_rev_reg_id.assert_called_once() + mock_rec.delete_record.assert_called_once() + assert responder.messages + + +@pytest.mark.asyncio +async def test_on_revocation_published_no_notify( + profile: Profile, responder: MockResponder +): + """Test revocation published event handler.""" + mock_rec = mock.MagicMock() + mock_rec.cred_rev_id = "mock" + mock_rec.delete_record = mock.CoroutineMock() + + MockRec = mock.MagicMock() + MockRec.query_by_rev_reg_id = mock.CoroutineMock(return_value=[mock_rec]) + + topic = f"{REVOCATION_EVENT_PREFIX}{REVOCATION_PUBLISHED_EVENT}::mock" + event = Event(topic, {"rev_reg_id": "mock", "crids": ["mock"]}) + + assert isinstance(profile.settings, Settings) + profile.settings["revocation.notify"] = False + + with mock.patch.object(test_module, "RevNotificationRecord", MockRec): + await test_module.on_revocation_published(profile, event) + + MockRec.query_by_rev_reg_id.assert_called_once() + mock_rec.delete_record.assert_called_once() + assert not responder.messages + + +@pytest.mark.asyncio +async def test_on_revocation_published_x_not_found( + profile: Profile, responder: MockResponder +): + """Test revocation published event handler.""" + MockRec = mock.MagicMock() + MockRec.query_by_rev_reg_id = mock.CoroutineMock(side_effect=StorageNotFoundError) + + topic = f"{REVOCATION_EVENT_PREFIX}{REVOCATION_PUBLISHED_EVENT}::mock" + event = Event(topic, {"rev_reg_id": "mock", "crids": ["mock"]}) + + with mock.patch.object(test_module, "RevNotificationRecord", MockRec): + await test_module.on_revocation_published(profile, event) + + MockRec.query_by_rev_reg_id.assert_called_once() + assert not responder.messages + + +@pytest.mark.asyncio +async def test_on_revocation_published_x_storage_error( + profile: Profile, responder: MockResponder +): + """Test revocation published event handler.""" + MockRec = mock.MagicMock() + MockRec.query_by_rev_reg_id = mock.CoroutineMock(side_effect=StorageError) + + topic = f"{REVOCATION_EVENT_PREFIX}{REVOCATION_PUBLISHED_EVENT}::mock" + event = Event(topic, {"rev_reg_id": "mock", "crids": ["mock"]}) + + with mock.patch.object(test_module, "RevNotificationRecord", MockRec): + await test_module.on_revocation_published(profile, event) + + MockRec.query_by_rev_reg_id.assert_called_once() + assert not responder.messages + + +@pytest.mark.asyncio +async def test_on_pending_cleared(profile: Profile): + """Test pending revocation cleared event.""" + mock_rec = mock.MagicMock() + mock_rec.delete_record = mock.CoroutineMock() + + MockRec = mock.MagicMock() + MockRec.query_by_rev_reg_id = mock.CoroutineMock(return_value=[mock_rec]) + + topic = f"{REVOCATION_EVENT_PREFIX}{REVOCATION_CLEAR_PENDING_EVENT}::mock" + event = Event(topic, {"rev_reg_id": "mock"}) + + with mock.patch.object(test_module, "RevNotificationRecord", MockRec): + await test_module.on_pending_cleared(profile, event) + + mock_rec.delete_record.assert_called_once() diff --git a/aries_cloudagent/protocols/routing/v1_0/manager.py b/aries_cloudagent/protocols/routing/v1_0/manager.py index 44c1b7b7c6..d200a30b59 100644 --- a/aries_cloudagent/protocols/routing/v1_0/manager.py +++ b/aries_cloudagent/protocols/routing/v1_0/manager.py @@ -1,5 +1,7 @@ """Routing manager classes for tracking and inspecting routing records.""" +import asyncio +import logging from typing import Coroutine, Sequence from ....core.error import BaseError @@ -16,6 +18,12 @@ from .models.route_updated import RouteUpdated +LOGGER = logging.getLogger(__name__) + +RECIP_ROUTE_PAUSE = 0.1 +RECIP_ROUTE_RETRY = 10 + + class RoutingManagerError(BaseError): """Generic routing error.""" @@ -54,21 +62,30 @@ async def get_recipient(self, recip_verkey: str) -> RouteRecord: if not recip_verkey: raise RoutingManagerError("Must pass non-empty recip_verkey") - try: - async with self._profile.session() as session: - record = await RouteRecord.retrieve_by_recipient_key( - session, recip_verkey + i = 0 + record = None + while not record: + try: + LOGGER.info(">>> fetching routing record for verkey: " + recip_verkey) + async with self._profile.session() as session: + record = await RouteRecord.retrieve_by_recipient_key( + session, recip_verkey + ) + LOGGER.info(">>> FOUND routing record for verkey: " + recip_verkey) + return record + except StorageDuplicateError: + LOGGER.info(">>> DUPLICATE routing record for verkey: " + recip_verkey) + raise RouteNotFoundError( + f"More than one route record found with recipient key: {recip_verkey}" ) - except StorageDuplicateError: - raise RouteNotFoundError( - f"More than one route record found with recipient key: {recip_verkey}" - ) - except StorageNotFoundError: - raise RouteNotFoundError( - f"No route found with recipient key: {recip_verkey}" - ) - - return record + except StorageNotFoundError: + LOGGER.info(">>> NOT FOUND routing record for verkey: " + recip_verkey) + i += 1 + if i > RECIP_ROUTE_RETRY: + raise RouteNotFoundError( + f"No route found with recipient key: {recip_verkey}" + ) + await asyncio.sleep(RECIP_ROUTE_PAUSE) async def get_routes( self, client_connection_id: str = None, tag_filter: dict = None @@ -136,6 +153,7 @@ async def create_route_record( ) if not recipient_key: raise RoutingManagerError("Missing recipient_key") + LOGGER.info(">>> creating routing record for verkey: " + recipient_key) route = RouteRecord( connection_id=client_connection_id, wallet_id=internal_wallet_id, @@ -143,6 +161,7 @@ async def create_route_record( ) async with self._profile.session() as session: await route.save(session, reason="Created new route") + LOGGER.info(">>> CREATED routing record for verkey: " + recipient_key) return route async def update_routes( diff --git a/aries_cloudagent/protocols/routing/v1_0/messages/tests/test_forward.py b/aries_cloudagent/protocols/routing/v1_0/messages/tests/test_forward.py index 4988e052b0..2dea1d988d 100644 --- a/aries_cloudagent/protocols/routing/v1_0/messages/tests/test_forward.py +++ b/aries_cloudagent/protocols/routing/v1_0/messages/tests/test_forward.py @@ -9,7 +9,6 @@ class TestForward(TestCase): - to = "to" msg = {"msg": "body"} diff --git a/aries_cloudagent/resolver/__init__.py b/aries_cloudagent/resolver/__init__.py index d7dceffb03..e4b82549d7 100644 --- a/aries_cloudagent/resolver/__init__.py +++ b/aries_cloudagent/resolver/__init__.py @@ -5,30 +5,30 @@ from ..config.injection_context import InjectionContext from ..config.provider import ClassProvider -from .did_resolver_registry import DIDResolverRegistry +from ..resolver.did_resolver import DIDResolver LOGGER = logging.getLogger(__name__) async def setup(context: InjectionContext): """Set up default resolvers.""" - registry = context.inject_or(DIDResolverRegistry) + registry = context.inject_or(DIDResolver) if not registry: - LOGGER.warning("No DID Resolver Registry instance found in context") + LOGGER.warning("No DID Resolver instance found in context") return key_resolver = ClassProvider( "aries_cloudagent.resolver.default.key.KeyDIDResolver" ).provide(context.settings, context.injector) await key_resolver.setup(context) - registry.register(key_resolver) + registry.register_resolver(key_resolver) if not context.settings.get("ledger.disabled"): indy_resolver = ClassProvider( "aries_cloudagent.resolver.default.indy.IndyDIDResolver" ).provide(context.settings, context.injector) await indy_resolver.setup(context) - registry.register(indy_resolver) + registry.register_resolver(indy_resolver) else: LOGGER.warning("Ledger is not configured, not loading IndyDIDResolver") @@ -36,4 +36,11 @@ async def setup(context: InjectionContext): "aries_cloudagent.resolver.default.web.WebDIDResolver" ).provide(context.settings, context.injector) await web_resolver.setup(context) - registry.register(web_resolver) + registry.register_resolver(web_resolver) + + if context.settings.get("resolver.universal"): + universal_resolver = ClassProvider( + "aries_cloudagent.resolver.default.universal.UniversalResolver" + ).provide(context.settings, context.injector) + await universal_resolver.setup(context) + registry.register_resolver(universal_resolver) diff --git a/aries_cloudagent/resolver/base.py b/aries_cloudagent/resolver/base.py index 994bfe94b2..df31cf4dd3 100644 --- a/aries_cloudagent/resolver/base.py +++ b/aries_cloudagent/resolver/base.py @@ -5,7 +5,7 @@ from abc import ABC, abstractmethod from enum import Enum -from typing import NamedTuple, Pattern, Sequence, Union +from typing import Optional, NamedTuple, Pattern, Sequence, Union, Text from pydid import DID @@ -132,7 +132,12 @@ async def supports(self, profile: Profile, did: str) -> bool: return bool(supported_did_regex.match(did)) - async def resolve(self, profile: Profile, did: Union[str, DID]) -> dict: + async def resolve( + self, + profile: Profile, + did: Union[str, DID], + service_accept: Optional[Sequence[Text]] = None, + ) -> dict: """Resolve a DID using this resolver.""" if isinstance(did, DID): did = str(did) @@ -143,8 +148,13 @@ async def resolve(self, profile: Profile, did: Union[str, DID]) -> dict: f"{self.__class__.__name__} does not support DID method for: {did}" ) - return await self._resolve(profile, did) + return await self._resolve(profile, did, service_accept) @abstractmethod - async def _resolve(self, profile: Profile, did: str) -> dict: + async def _resolve( + self, + profile: Profile, + did: str, + service_accept: Optional[Sequence[Text]] = None, + ) -> dict: """Resolve a DID using this resolver.""" diff --git a/aries_cloudagent/resolver/default/indy.py b/aries_cloudagent/resolver/default/indy.py index a7364306d9..ce18f75c67 100644 --- a/aries_cloudagent/resolver/default/indy.py +++ b/aries_cloudagent/resolver/default/indy.py @@ -3,13 +3,15 @@ Resolution is performed using the IndyLedger class. """ -from typing import Pattern +import logging +from typing import Optional, Pattern, Sequence, Text from pydid import DID, DIDDocumentBuilder -from pydid.verification_method import Ed25519VerificationKey2018 +from pydid.verification_method import Ed25519VerificationKey2018, VerificationMethod from ...config.injection_context import InjectionContext from ...core.profile import Profile +from ...did.did_key import DIDKey from ...ledger.endpoint_type import EndpointType from ...ledger.error import LedgerError from ...ledger.multiple_ledger.ledger_requests_executor import ( @@ -17,18 +19,44 @@ IndyLedgerRequestsExecutor, ) from ...messaging.valid import IndyDID - +from ...multitenant.base import BaseMultitenantManager +from ...wallet.key_type import ED25519 from ..base import BaseDIDResolver, DIDNotFound, ResolverError, ResolverType +LOGGER = logging.getLogger(__name__) + class NoIndyLedger(ResolverError): """Raised when there is no Indy ledger instance configured.""" +def _routing_keys_as_did_key_urls(routing_keys: Sequence[str]) -> Sequence[str]: + """Convert raw base58 keys to did:key values. + + If a did:key is passed in, convert to a did:key URL. + """ + + did_key_urls = [] + for routing_key in routing_keys: + if not routing_key.startswith("did:key:"): + did_key_urls.append(DIDKey.from_public_key_b58(routing_key, ED25519).key_id) + else: + if "#" not in routing_key: + did_key_urls.append( + f"{routing_key}#{DIDKey.from_did(routing_key).fingerprint}" + ) + else: + return routing_keys + return did_key_urls + + class IndyDIDResolver(BaseDIDResolver): """Indy DID Resolver.""" - AGENT_SERVICE_TYPE = "did-communication" + SERVICE_TYPE_DID_COMMUNICATION = "did-communication" + SERVICE_TYPE_DIDCOMM = "DIDComm" + SERVICE_TYPE_ENDPOINT = "endpoint" + CONTEXT_DIDCOMM_V2 = "https://didcomm.org/messaging/contexts/v2" def __init__(self): """Initialize Indy Resolver.""" @@ -42,9 +70,102 @@ def supported_did_regex(self) -> Pattern: """Return supported_did_regex of Indy DID Resolver.""" return IndyDID.PATTERN - async def _resolve(self, profile: Profile, did: str) -> dict: + def process_endpoint_types(self, types): + """Process endpoint types. + + Returns expected types, subset of expected types, + or default types. + """ + expected_types = ["endpoint", "did-communication", "DIDComm"] + default_types = ["endpoint", "did-communication"] + if len(types) <= 0: + return default_types + for type in types: + if type not in expected_types: + return default_types + return types + + def add_services( + self, + builder: DIDDocumentBuilder, + endpoints: Optional[dict], + recipient_key: VerificationMethod = None, + service_accept: Optional[Sequence[Text]] = None, + ): + """Add services.""" + if not endpoints: + return + + endpoint = endpoints.get("endpoint") + routing_keys = endpoints.get("routingKeys", []) + types = endpoints.get("types", [self.SERVICE_TYPE_DID_COMMUNICATION]) + + other_endpoints = { + key: endpoints[key] + for key in ("profile", "linked_domains") + if key in endpoints + } + + if endpoint: + processed_types = self.process_endpoint_types(types) + + if self.SERVICE_TYPE_ENDPOINT in processed_types: + builder.service.add( + ident="endpoint", + service_endpoint=endpoint, + type_=self.SERVICE_TYPE_ENDPOINT, + ) + + if self.SERVICE_TYPE_DID_COMMUNICATION in processed_types: + builder.service.add( + ident="did-communication", + type_=self.SERVICE_TYPE_DID_COMMUNICATION, + service_endpoint=endpoint, + priority=1, + routing_keys=_routing_keys_as_did_key_urls(routing_keys), + recipient_keys=[recipient_key.id], + accept=( + service_accept if service_accept else ["didcomm/aip2;env=rfc19"] + ), + ) + + if self.SERVICE_TYPE_DIDCOMM in types: + builder.service.add( + ident="#didcomm-1", + type_=self.SERVICE_TYPE_DIDCOMM, + service_endpoint=endpoint, + recipient_keys=[recipient_key.id], + routing_keys=_routing_keys_as_did_key_urls(routing_keys), + # CHECKME + # accept=(service_accept if service_accept else ["didcomm/v2"]), + accept=["didcomm/v2"], + ) + builder.context.append(self.CONTEXT_DIDCOMM_V2) + else: + LOGGER.warning( + "No endpoint for DID although endpoint attrib was resolvable" + ) + + if other_endpoints: + for type_, endpoint in other_endpoints.items(): + builder.service.add( + ident=type_, + type_=EndpointType.get(type_).w3c, + service_endpoint=endpoint, + ) + + async def _resolve( + self, + profile: Profile, + did: str, + service_accept: Optional[Sequence[Text]] = None, + ) -> dict: """Resolve an indy DID.""" - ledger_exec_inst = profile.inject(IndyLedgerRequestsExecutor) + multitenant_mgr = profile.inject_or(BaseMultitenantManager) + if multitenant_mgr: + ledger_exec_inst = IndyLedgerRequestsExecutor(profile) + else: + ledger_exec_inst = profile.inject(IndyLedgerRequestsExecutor) ledger = ( await ledger_exec_inst.get_ledger_for_identifier( did, @@ -57,7 +178,7 @@ async def _resolve(self, profile: Profile, did: str) -> dict: try: async with ledger: recipient_key = await ledger.get_key_for_did(did) - endpoints = await ledger.get_all_endpoints_for_did(did) + endpoints: Optional[dict] = await ledger.get_all_endpoints_for_did(did) except LedgerError as err: raise DIDNotFound(f"DID {did} could not be resolved") from err @@ -68,24 +189,7 @@ async def _resolve(self, profile: Profile, did: str) -> dict: ) builder.authentication.reference(vmethod.id) builder.assertion_method.reference(vmethod.id) - if endpoints: - for type_, endpoint in endpoints.items(): - if type_ == EndpointType.ENDPOINT.indy: - builder.service.add_didcomm( - ident=self.AGENT_SERVICE_TYPE, - type_=self.AGENT_SERVICE_TYPE, - service_endpoint=endpoint, - priority=1, - recipient_keys=[vmethod], - routing_keys=[], - ) - else: - # Accept all service types for now - builder.service.add( - ident=type_, - type_=type_, - service_endpoint=endpoint, - ) + self.add_services(builder, endpoints, vmethod, service_accept) result = builder.build() return result.serialize() diff --git a/aries_cloudagent/resolver/default/key.py b/aries_cloudagent/resolver/default/key.py index 2f9f10edf9..0217156f81 100644 --- a/aries_cloudagent/resolver/default/key.py +++ b/aries_cloudagent/resolver/default/key.py @@ -3,7 +3,7 @@ Resolution is performed using the IndyLedger class. """ -from typing import Pattern +from typing import Optional, Pattern, Sequence, Text from ...did.did_key import DIDKey from ...config.injection_context import InjectionContext @@ -28,7 +28,12 @@ def supported_did_regex(self) -> Pattern: """Return supported_did_regex of Key DID Resolver.""" return DIDKeyType.PATTERN - async def _resolve(self, profile: Profile, did: str) -> dict: + async def _resolve( + self, + profile: Profile, + did: str, + service_accept: Optional[Sequence[Text]] = None, + ) -> dict: """Resolve a Key DID.""" try: did_key = DIDKey.from_did(did) diff --git a/aries_cloudagent/resolver/default/tests/test_indy.py b/aries_cloudagent/resolver/default/tests/test_indy.py index f9d4ad944a..6287fb5352 100644 --- a/aries_cloudagent/resolver/default/tests/test_indy.py +++ b/aries_cloudagent/resolver/default/tests/test_indy.py @@ -3,6 +3,7 @@ import pytest from asynctest import mock as async_mock +from pydid.verification_method import VerificationMethod from ....core.in_memory import InMemoryProfile from ....core.profile import Profile @@ -12,10 +13,11 @@ IndyLedgerRequestsExecutor, ) from ....messaging.valid import IndyDID +from ....multitenant.base import BaseMultitenantManager +from ....multitenant.manager import MultitenantManager from ...base import DIDNotFound, ResolverError -from .. import indy as test_module -from ..indy import IndyDIDResolver +from ..indy import IndyDIDResolver, _routing_keys_as_did_key_urls # pylint: disable=W0621 TEST_DID0 = "did:sov:WgWxqztrNooG92RXvxSTWv" @@ -31,8 +33,11 @@ def resolver(): def ledger(): """Ledger fixture.""" ledger = async_mock.MagicMock(spec=BaseLedger) - ledger.get_endpoint_for_did = async_mock.CoroutineMock( - return_value="https://github.com/" + ledger.get_all_endpoints_for_did = async_mock.CoroutineMock( + return_value={ + "endpoint": "https://github.com/", + "profile": "https://example.com/profile", + } ) ledger.get_key_for_did = async_mock.CoroutineMock(return_value="key") yield ledger @@ -65,6 +70,31 @@ async def test_resolve(self, profile: Profile, resolver: IndyDIDResolver): """Test resolve method.""" assert await resolver.resolve(profile, TEST_DID0) + @pytest.mark.asyncio + async def test_resolve_with_accept( + self, profile: Profile, resolver: IndyDIDResolver + ): + """Test resolve method.""" + assert await resolver.resolve( + profile, TEST_DID0, ["didcomm/aip1", "didcomm/aip2;env=rfc19"] + ) + + @pytest.mark.asyncio + async def test_resolve_multitenant( + self, profile: Profile, resolver: IndyDIDResolver, ledger: BaseLedger + ): + """Test resolve method.""" + profile.context.injector.bind_instance( + BaseMultitenantManager, + async_mock.MagicMock(MultitenantManager, autospec=True), + ) + with async_mock.patch.object( + IndyLedgerRequestsExecutor, + "get_ledger_for_identifier", + async_mock.CoroutineMock(return_value=("test_ledger_id", ledger)), + ): + assert await resolver.resolve(profile, TEST_DID0) + @pytest.mark.asyncio async def test_resolve_x_no_ledger( self, profile: Profile, resolver: IndyDIDResolver @@ -89,3 +119,76 @@ async def test_resolve_x_did_not_found( ledger.get_key_for_did.side_effect = LedgerError with pytest.raises(DIDNotFound): await resolver.resolve(profile, TEST_DID0) + + @pytest.mark.asyncio + async def test_supports_updated_did_sov_rules( + self, resolver: IndyDIDResolver, ledger: BaseLedger, profile: Profile + ): + """Test that new attrib structure is supported.""" + example = { + "endpoint": "https://example.com/endpoint", + "routingKeys": ["HQhjaj4mcaS3Xci27a9QhnBrNpS91VNFUU4TDrtMxa9j"], + "types": ["DIDComm", "did-communication", "endpoint"], + "profile": "https://example.com", + "linked_domains": "https://example.com", + } + + ledger.get_all_endpoints_for_did = async_mock.CoroutineMock( + return_value=example + ) + assert await resolver.resolve(profile, TEST_DID0) + + @pytest.mark.asyncio + async def test_supports_updated_did_sov_rules_no_endpoint_url( + self, resolver: IndyDIDResolver, ledger: BaseLedger, profile: Profile + ): + """Test that new attrib structure is supported.""" + example = { + "routingKeys": ["a-routing-key"], + "types": ["DIDComm", "did-communication", "endpoint"], + } + + ledger.get_all_endpoints_for_did = async_mock.CoroutineMock( + return_value=example + ) + result = await resolver.resolve(profile, TEST_DID0) + assert "service" not in result + + @pytest.mark.parametrize( + "types, result", + [ + ( + [], + ["endpoint", "did-communication"], + ), + ( + ["did-communication"], + ["did-communication"], + ), + ( + ["endpoint", "did-communication", "DIDComm", "other-endpoint-type"], + ["endpoint", "did-communication"], + ), + ( + ["endpoint", "did-communication", "DIDComm"], + ["endpoint", "did-communication", "DIDComm"], + ), + ], + ) + def test_process_endpoint_types(self, resolver: IndyDIDResolver, types, result): + assert resolver.process_endpoint_types(types) == result + + @pytest.mark.parametrize( + "keys", + [ + ["3YJCx3TqotDWFGv7JMR5erEvrmgu5y4FDqjR7sKWxgXn"], + ["did:key:z6MkgzZFYHiH9RhyMmkoyvNvVwnvgLxkVrJbureLx9HXsuKA"], + [ + "did:key:z6MkgzZFYHiH9RhyMmkoyvNvVwnvgLxkVrJbureLx9HXsuKA#z6MkgzZFYHiH9RhyMmkoyvNvVwnvgLxkVrJbureLx9HXsuKA" + ], + ], + ) + def test_routing_keys_as_did_key_urls(self, keys): + for key in _routing_keys_as_did_key_urls(keys): + assert key.startswith("did:key:") + assert "#" in key diff --git a/aries_cloudagent/resolver/default/tests/test_universal.py b/aries_cloudagent/resolver/default/tests/test_universal.py new file mode 100644 index 0000000000..8d9df9638d --- /dev/null +++ b/aries_cloudagent/resolver/default/tests/test_universal.py @@ -0,0 +1,222 @@ +"""Test universal resolver with http bindings.""" + +import re +from typing import Dict, Union + +from asynctest import mock as async_mock +import pytest + +from aries_cloudagent.config.settings import Settings + +from .. import universal as test_module +from ...base import DIDNotFound, ResolverError +from ..universal import UniversalResolver + + +@pytest.fixture +async def resolver(): + """Resolver fixture.""" + yield UniversalResolver( + endpoint="https://example.com", supported_did_regex=re.compile("^did:sov:.*$") + ) + + +@pytest.fixture +def profile(): + """Profile fixture.""" + yield async_mock.MagicMock() + + +class MockResponse: + """Mock http response.""" + + def __init__(self, status: int, body: Union[str, Dict]): + self.status = status + self.body = body + + async def json(self): + return self.body + + async def text(self): + return self.body + + async def __aenter__(self): + """For use as async context.""" + return self + + async def __aexit__(self, err_type, err_value, err_exc): + """For use as async context.""" + + +class MockClientSession: + """Mock client session.""" + + def __init__(self, response: MockResponse = None): + self.response = response + + def __call__(self, headers): + return self + + async def __aenter__(self): + """For use as async context.""" + return self + + async def __aexit__(self, err_type, err_value, err_exc): + """For use as async context.""" + + def get(self, endpoint): + """Return response.""" + return self.response + + +@pytest.fixture +def mock_client_session(): + temp = test_module.aiohttp.ClientSession + session = MockClientSession() + test_module.aiohttp.ClientSession = session + yield session + test_module.aiohttp.ClientSession = temp + + +@pytest.mark.asyncio +async def test_resolve(profile, resolver, mock_client_session): + mock_client_session.response = MockResponse( + 200, + { + "didDocument": { + "id": "did:example:123", + "@context": "https://www.w3.org/ns/did/v1", + } + }, + ) + doc = await resolver.resolve(profile, "did:sov:WRfXPg8dantKVubE3HX8pw") + assert doc.get("id") == "did:example:123" + + +@pytest.mark.asyncio +async def test_resolve_not_found(profile, resolver, mock_client_session): + mock_client_session.response = MockResponse(404, "Not found") + with pytest.raises(DIDNotFound): + await resolver.resolve(profile, "did:sov:WRfXPg8dantKVubE3HX8pw") + + +@pytest.mark.asyncio +async def test_resolve_unexpected_status(profile, resolver, mock_client_session): + mock_client_session.response = MockResponse( + 500, "Server failed to complete request" + ) + with pytest.raises(ResolverError): + await resolver.resolve(profile, "did:sov:WRfXPg8dantKVubE3HX8pw") + + +@pytest.mark.asyncio +async def test_fetch_resolver_props(mock_client_session: MockClientSession): + mock_client_session.response = MockResponse(200, {"test": "json"}) + assert await UniversalResolver()._fetch_resolver_props() == {"test": "json"} + mock_client_session.response = MockResponse(404, "Not found") + with pytest.raises(ResolverError): + await UniversalResolver()._fetch_resolver_props() + + +@pytest.mark.asyncio +async def test_get_supported_did_regex(): + props = {"example": {"http": {"pattern": "match a test string"}}} + with async_mock.patch.object( + UniversalResolver, + "_fetch_resolver_props", + async_mock.CoroutineMock(return_value=props), + ): + pattern = await UniversalResolver()._get_supported_did_regex() + assert pattern.fullmatch("match a test string") + + +def test_compile_supported_did_regex(): + patterns = ["one", "two", "three"] + compiled = test_module._compile_supported_did_regex(patterns) + assert compiled.match("one") + assert compiled.match("two") + assert compiled.match("three") + + +@pytest.mark.asyncio +async def test_setup_endpoint_regex_set(resolver: UniversalResolver): + settings = Settings( + { + "resolver.universal": "http://example.com", + "resolver.universal.supported": "test", + } + ) + context = async_mock.MagicMock() + context.settings = settings + with async_mock.patch.object( + test_module, + "_compile_supported_did_regex", + async_mock.MagicMock(return_value="pattern"), + ): + await resolver.setup(context) + + assert resolver._endpoint == "http://example.com" + assert resolver._supported_did_regex == "pattern" + + +@pytest.mark.asyncio +async def test_setup_endpoint_set(resolver: UniversalResolver): + settings = Settings( + { + "resolver.universal": "http://example.com", + } + ) + context = async_mock.MagicMock() + context.settings = settings + with async_mock.patch.object( + UniversalResolver, + "_get_supported_did_regex", + async_mock.CoroutineMock(return_value="pattern"), + ): + await resolver.setup(context) + + assert resolver._endpoint == "http://example.com" + assert resolver._supported_did_regex == "pattern" + + +@pytest.mark.asyncio +async def test_setup_endpoint_default(resolver: UniversalResolver): + settings = Settings( + { + "resolver.universal": "DEFAULT", + } + ) + context = async_mock.MagicMock() + context.settings = settings + with async_mock.patch.object( + UniversalResolver, + "_get_supported_did_regex", + async_mock.CoroutineMock(return_value="pattern"), + ): + await resolver.setup(context) + + assert resolver._endpoint == test_module.DEFAULT_ENDPOINT + assert resolver._supported_did_regex == "pattern" + + +@pytest.mark.asyncio +async def test_setup_endpoint_unset(resolver: UniversalResolver): + settings = Settings() + context = async_mock.MagicMock() + context.settings = settings + with async_mock.patch.object( + UniversalResolver, + "_get_supported_did_regex", + async_mock.CoroutineMock(return_value="pattern"), + ): + await resolver.setup(context) + + assert resolver._endpoint == test_module.DEFAULT_ENDPOINT + assert resolver._supported_did_regex == "pattern" + + +@pytest.mark.asyncio +async def test_supported_did_regex_not_setup(): + resolver = UniversalResolver() + with pytest.raises(ResolverError): + resolver.supported_did_regex diff --git a/aries_cloudagent/resolver/default/universal.py b/aries_cloudagent/resolver/default/universal.py new file mode 100644 index 0000000000..2efee46009 --- /dev/null +++ b/aries_cloudagent/resolver/default/universal.py @@ -0,0 +1,117 @@ +"""HTTP Universal DID Resolver.""" + +import logging +import re +from typing import Iterable, Optional, Pattern, Sequence, Union, Text + +import aiohttp + +from ...config.injection_context import InjectionContext +from ...core.profile import Profile +from ..base import BaseDIDResolver, DIDNotFound, ResolverError, ResolverType + +LOGGER = logging.getLogger(__name__) +DEFAULT_ENDPOINT = "https://dev.uniresolver.io/1.0" + + +def _compile_supported_did_regex(patterns: Iterable[Union[str, Pattern]]): + """Create regex from list of regex.""" + return re.compile( + "(?:" + + "|".join( + [ + pattern.pattern if isinstance(pattern, Pattern) else pattern + for pattern in patterns + ] + ) + + ")" + ) + + +class UniversalResolver(BaseDIDResolver): + """Universal DID Resolver with HTTP bindings.""" + + def __init__( + self, + *, + endpoint: Optional[str] = None, + supported_did_regex: Optional[Pattern] = None, + bearer_token: Optional[str] = None, + ): + """Initialize UniversalResolver.""" + super().__init__(ResolverType.NON_NATIVE) + self._endpoint = endpoint + self._supported_did_regex = supported_did_regex + + self.__default_headers = ( + {"Authorization": f"Bearer {bearer_token}"} if bearer_token else {} + ) + + async def setup(self, context: InjectionContext): + """Perform setup, populate supported method list, configuration.""" + + # configure endpoint + endpoint = context.settings.get_str("resolver.universal") + if endpoint == "DEFAULT" or not endpoint: + endpoint = DEFAULT_ENDPOINT + self._endpoint = endpoint + + # configure authorization + token = context.settings.get_str("resolver.universal.token") + self.__default_headers = {"Authorization": f"Bearer {token}"} if token else {} + + # configure supported methods + supported = context.settings.get("resolver.universal.supported") + if supported is None: + supported_did_regex = await self._get_supported_did_regex() + else: + supported_did_regex = _compile_supported_did_regex(supported) + + self._supported_did_regex = supported_did_regex + + @property + def supported_did_regex(self) -> Pattern: + """Return supported methods regex.""" + if not self._supported_did_regex: + raise ResolverError("Resolver has not been set up") + + return self._supported_did_regex + + async def _resolve( + self, + _profile: Profile, + did: str, + service_accept: Optional[Sequence[Text]] = None, + ) -> dict: + """Resolve DID through remote universal resolver.""" + + async with aiohttp.ClientSession(headers=self.__default_headers) as session: + async with session.get(f"{self._endpoint}/identifiers/{did}") as resp: + if resp.status == 200: + doc = await resp.json() + did_doc = doc["didDocument"] + LOGGER.info("Retrieved doc: %s", did_doc) + return did_doc + if resp.status == 404: + raise DIDNotFound(f"{did} not found by {self.__class__.__name__}") + + text = await resp.text() + raise ResolverError( + f"Unexpected status from universal resolver ({resp.status}): {text}" + ) + + async def _fetch_resolver_props(self) -> dict: + """Retrieve universal resolver properties.""" + async with aiohttp.ClientSession(headers=self.__default_headers) as session: + async with session.get(f"{self._endpoint}/properties/") as resp: + if 200 <= resp.status < 400: + return await resp.json() + raise ResolverError( + "Failed to retrieve resolver properties: " + await resp.text() + ) + + async def _get_supported_did_regex(self) -> Pattern: + props = await self._fetch_resolver_props() + return _compile_supported_did_regex( + driver["http"]["pattern"] for driver in props.values() + ) diff --git a/aries_cloudagent/resolver/default/web.py b/aries_cloudagent/resolver/default/web.py index 30aa5ce440..df2d99e6cc 100644 --- a/aries_cloudagent/resolver/default/web.py +++ b/aries_cloudagent/resolver/default/web.py @@ -2,7 +2,7 @@ import urllib.parse -from typing import Pattern +from typing import Optional, Pattern, Sequence, Text import aiohttp @@ -57,7 +57,12 @@ def __transform_to_url(self, did): return "https://" + url + "/did.json" - async def _resolve(self, profile: Profile, did: str) -> dict: + async def _resolve( + self, + profile: Profile, + did: str, + service_accept: Optional[Sequence[Text]] = None, + ) -> dict: """Resolve did:web DIDs.""" url = self.__transform_to_url(did) diff --git a/aries_cloudagent/resolver/did_resolver.py b/aries_cloudagent/resolver/did_resolver.py index f57ec47fd9..b523b59607 100644 --- a/aries_cloudagent/resolver/did_resolver.py +++ b/aries_cloudagent/resolver/did_resolver.py @@ -8,10 +8,11 @@ from datetime import datetime from itertools import chain import logging -from typing import Sequence, Tuple, Type, TypeVar, Union +from typing import List, Optional, Sequence, Text, Tuple, Union -from pydid import DID, DIDError, DIDUrl, Resource, NonconformantDocument -from pydid.doc.doc import IDNotFoundError +from pydid import DID, DIDError, DIDUrl, Resource +import pydid +from pydid.doc.doc import BaseDIDDocument, IDNotFoundError from ..core.profile import Profile from .base import ( @@ -22,23 +23,26 @@ ResolutionResult, ResolverError, ) -from .did_resolver_registry import DIDResolverRegistry LOGGER = logging.getLogger(__name__) -ResourceType = TypeVar("ResourceType", bound=Resource) - - class DIDResolver: """did resolver singleton.""" - def __init__(self, registry: DIDResolverRegistry): + def __init__(self, resolvers: List[BaseDIDResolver] = None): """Create DID Resolver.""" - self.did_resolver_registry = registry + self.resolvers = resolvers or [] + + def register_resolver(self, resolver: BaseDIDResolver): + """Register a new resolver.""" + self.resolvers.append(resolver) async def _resolve( - self, profile: Profile, did: Union[str, DID] + self, + profile: Profile, + did: Union[str, DID], + service_accept: Optional[Sequence[Text]] = None, ) -> Tuple[BaseDIDResolver, dict]: """Retrieve doc and return with resolver.""" # TODO Cache results @@ -52,6 +56,7 @@ async def _resolve( document = await resolver.resolve( profile, did, + service_accept, ) return resolver, document except DIDNotFound: @@ -59,9 +64,14 @@ async def _resolve( raise DIDNotFound(f"DID {did} could not be resolved") - async def resolve(self, profile: Profile, did: Union[str, DID]) -> dict: + async def resolve( + self, + profile: Profile, + did: Union[str, DID], + service_accept: Optional[Sequence[Text]] = None, + ) -> dict: """Resolve a DID.""" - _, doc = await self._resolve(profile, did) + _, doc = await self._resolve(profile, did, service_accept) return doc async def resolve_with_metadata( @@ -90,7 +100,7 @@ async def _match_did_to_resolver( """ valid_resolvers = [ resolver - for resolver in self.did_resolver_registry.resolvers + for resolver in self.resolvers if await resolver.supports(profile, did) ] native_resolvers = filter(lambda resolver: resolver.native, valid_resolvers) @@ -103,8 +113,12 @@ async def _match_did_to_resolver( return resolvers async def dereference( - self, profile: Profile, did_url: str, *, cls: Type[ResourceType] = Resource - ) -> ResourceType: + self, + profile: Profile, + did_url: str, + *, + document: Optional[BaseDIDDocument] = None, + ) -> Resource: """Dereference a DID URL to its corresponding DID Doc object.""" # TODO Use cached DID Docs when possible try: @@ -116,12 +130,15 @@ async def dereference( "Failed to parse DID URL from {}".format(did_url) ) from err - doc_dict = await self.resolve(profile, parsed.did) - # Use non-conformant doc as the "least common denominator" + if document and parsed.did != document.id: + document = None + + if not document: + doc_dict = await self.resolve(profile, parsed.did) + document = pydid.deserialize_document(doc_dict) + try: - return NonconformantDocument.deserialize(doc_dict).dereference_as( - cls, parsed - ) + return document.dereference(parsed) except IDNotFoundError as error: raise ResolverError( "Failed to dereference DID URL: {}".format(error) diff --git a/aries_cloudagent/resolver/did_resolver_registry.py b/aries_cloudagent/resolver/did_resolver_registry.py deleted file mode 100644 index 4154454236..0000000000 --- a/aries_cloudagent/resolver/did_resolver_registry.py +++ /dev/null @@ -1,28 +0,0 @@ -"""In memmory storage for registering did resolvers.""" - -import logging -from typing import Sequence - -from .base import BaseDIDResolver - -LOGGER = logging.getLogger(__name__) - - -class DIDResolverRegistry: - """Registry for did resolvers.""" - - def __init__(self): - """Initialize list for did resolvers.""" - self._resolvers = [] - - @property - def resolvers( - self, - ) -> Sequence[BaseDIDResolver]: - """Accessor for a list of all did resolvers.""" - return self._resolvers - - def register(self, resolver) -> None: - """Register a resolver.""" - LOGGER.debug("Registering resolver %s", resolver) - self._resolvers.append(resolver) diff --git a/aries_cloudagent/resolver/routes.py b/aries_cloudagent/resolver/routes.py index 680353023a..515a653cb4 100644 --- a/aries_cloudagent/resolver/routes.py +++ b/aries_cloudagent/resolver/routes.py @@ -53,7 +53,7 @@ class ResolutionResultSchema(OpenAPISchema): """Result schema for did document query.""" - did_doc = fields.Dict(description="DID Document", required=True) + did_document = fields.Dict(description="DID Document", required=True) metadata = fields.Dict(description="Resolution metadata", required=True) diff --git a/aries_cloudagent/resolver/tests/test_base.py b/aries_cloudagent/resolver/tests/test_base.py index cdae76a91e..2108468458 100644 --- a/aries_cloudagent/resolver/tests/test_base.py +++ b/aries_cloudagent/resolver/tests/test_base.py @@ -22,7 +22,7 @@ async def setup(self, context): def supported_did_regex(self): return re.compile("^did:example:[a-zA-Z0-9_.-]+$") - async def _resolve(self, profile, did) -> DIDDocument: + async def _resolve(self, profile, did, accept) -> DIDDocument: return DIDDocument("did:example:123") @@ -74,7 +74,7 @@ async def setup(self, context): def supported_methods(self): return ["example"] - async def _resolve(self, profile, did) -> DIDDocument: + async def _resolve(self, profile, did, accept) -> DIDDocument: return DIDDocument("did:example:123") with pytest.deprecated_call(): diff --git a/aries_cloudagent/resolver/tests/test_did_resolver.py b/aries_cloudagent/resolver/tests/test_did_resolver.py index b08480e805..58c459b307 100644 --- a/aries_cloudagent/resolver/tests/test_did_resolver.py +++ b/aries_cloudagent/resolver/tests/test_did_resolver.py @@ -7,7 +7,7 @@ import pytest from asynctest import mock as async_mock -from pydid import DID, DIDDocument, VerificationMethod +from pydid import DID, DIDDocument, VerificationMethod, BasicDIDDocument from ..base import ( BaseDIDResolver, @@ -18,7 +18,6 @@ ResolverType, ) from ..did_resolver import DIDResolver -from ..did_resolver_registry import DIDResolverRegistry from . import DOC @@ -80,7 +79,7 @@ def supported_did_regex(self) -> Pattern: async def setup(self, context): pass - async def _resolve(self, profile, did): + async def _resolve(self, profile, did, accept): if isinstance(self.resolved, Exception): raise self.resolved return self.resolved.serialize() @@ -88,10 +87,10 @@ async def _resolve(self, profile, did): @pytest.fixture def resolver(): - did_resolver_registry = DIDResolverRegistry() + did_resolver_registry = [] for method in TEST_DID_METHODS: resolver = MockResolver([method], DIDDocument.deserialize(DOC)) - did_resolver_registry.register(resolver) + did_resolver_registry.append(resolver) return DIDResolver(did_resolver_registry) @@ -101,7 +100,7 @@ def profile(): def test_create_resolver(resolver): - assert len(resolver.did_resolver_registry.resolvers) == len(TEST_DID_METHODS) + assert len(resolver.resolvers) == len(TEST_DID_METHODS) @pytest.mark.asyncio @@ -121,11 +120,9 @@ async def test_match_did_to_resolver_x_not_supported(resolver): @pytest.mark.asyncio async def test_match_did_to_resolver_native_priority(profile): - registry = DIDResolverRegistry() native = MockResolver(["sov"], native=True) non_native = MockResolver(["sov"], native=False) - registry.register(non_native) - registry.register(native) + registry = [non_native, native] resolver = DIDResolver(registry) assert [native, non_native] == await resolver._match_did_to_resolver( profile, TEST_DID0 @@ -134,15 +131,11 @@ async def test_match_did_to_resolver_native_priority(profile): @pytest.mark.asyncio async def test_match_did_to_resolver_registration_order(profile): - registry = DIDResolverRegistry() native1 = MockResolver(["sov"], native=True) - registry.register(native1) native2 = MockResolver(["sov"], native=True) - registry.register(native2) non_native3 = MockResolver(["sov"], native=False) - registry.register(non_native3) native4 = MockResolver(["sov"], native=True) - registry.register(native4) + registry = [native1, native2, non_native3, native4] resolver = DIDResolver(registry) assert [ native1, @@ -160,6 +153,17 @@ async def test_dereference(resolver, profile): assert expected == actual.serialize() +@pytest.mark.asyncio +async def test_dereference_diddoc(resolver, profile): + url = "did:example:1234abcd#4" + doc = BasicDIDDocument( + id="did:example:z6Mkmpe2DyE4NsDiAb58d75hpi1BjqbH6wYMschUkjWDEEuR" + ) + result = await resolver.dereference(profile, url, document=doc) + assert isinstance(result, VerificationMethod) + assert result.id == url + + @pytest.mark.asyncio async def test_dereference_x(resolver, profile): url = "non-did" @@ -200,8 +204,6 @@ async def test_resolve_did_x_not_supported(resolver, profile): async def test_resolve_did_x_not_found(profile): py_did = DID("did:cowsay:EiDahaOGH-liLLdDtTxEAdc8i-cfCz-WUcQdRJheMVNn3A") cowsay_resolver_not_found = MockResolver(["cowsay"], resolved=DIDNotFound()) - registry = DIDResolverRegistry() - registry.register(cowsay_resolver_not_found) - resolver = DIDResolver(registry) + resolver = DIDResolver([cowsay_resolver_not_found]) with pytest.raises(DIDNotFound): await resolver.resolve(profile, py_did) diff --git a/aries_cloudagent/resolver/tests/test_did_resolver_registry.py b/aries_cloudagent/resolver/tests/test_did_resolver_registry.py deleted file mode 100644 index dba7afffbd..0000000000 --- a/aries_cloudagent/resolver/tests/test_did_resolver_registry.py +++ /dev/null @@ -1,12 +0,0 @@ -"""Test did resolver registery.""" - -import pytest -import unittest -from ..did_resolver_registry import DIDResolverRegistry - - -def test_create_registry(): - did_resolver_registry = DIDResolverRegistry() - test_resolver = unittest.mock.MagicMock() - did_resolver_registry.register(test_resolver) - assert did_resolver_registry.resolvers == [test_resolver] diff --git a/aries_cloudagent/revocation/indy.py b/aries_cloudagent/revocation/indy.py index 3dcab450f7..57c2af53cf 100644 --- a/aries_cloudagent/revocation/indy.py +++ b/aries_cloudagent/revocation/indy.py @@ -1,18 +1,30 @@ """Indy revocation registry management.""" -from typing import Sequence +from typing import Optional, Sequence, Tuple +from uuid import uuid4 from ..core.profile import Profile +from ..ledger.base import BaseLedger from ..ledger.multiple_ledger.ledger_requests_executor import ( GET_CRED_DEF, GET_REVOC_REG_DEF, IndyLedgerRequestsExecutor, ) +from ..multitenant.base import BaseMultitenantManager +from ..protocols.endorse_transaction.v1_0.util import ( + get_endorser_connection_id, + is_author_role, +) from ..storage.base import StorageNotFoundError -from .error import RevocationNotSupportedError, RevocationRegistryBadSizeError +from .error import ( + RevocationError, + RevocationNotSupportedError, + RevocationRegistryBadSizeError, +) from .models.issuer_rev_reg_record import IssuerRevRegRecord from .models.revocation_registry import RevocationRegistry +from .util import notify_revocation_reg_init_event class IndyRevocation: @@ -30,9 +42,16 @@ async def init_issuer_registry( max_cred_num: int = None, revoc_def_type: str = None, tag: str = None, - ) -> "IssuerRevRegRecord": + create_pending_rev_reg: bool = False, + endorser_connection_id: str = None, + notify: bool = True, + ) -> IssuerRevRegRecord: """Create a new revocation registry record for a credential definition.""" - ledger_exec_inst = self._profile.inject(IndyLedgerRequestsExecutor) + multitenant_mgr = self._profile.inject_or(BaseMultitenantManager) + if multitenant_mgr: + ledger_exec_inst = IndyLedgerRequestsExecutor(self._profile) + else: + ledger_exec_inst = self._profile.inject(IndyLedgerRequestsExecutor) ledger = ( await ledger_exec_inst.get_ledger_for_identifier( cred_def_id, @@ -54,20 +73,61 @@ async def init_issuer_registry( f"Bad revocation registry size: {max_cred_num}" ) + record_id = str(uuid4()) + issuer_did = cred_def_id.split(":")[0] record = IssuerRevRegRecord( + new_with_id=True, + record_id=record_id, cred_def_id=cred_def_id, - issuer_did=cred_def_id.split(":")[0], + issuer_did=issuer_did, max_cred_num=max_cred_num, revoc_def_type=revoc_def_type, tag=tag, ) + revoc_def_type = record.revoc_def_type + rtag = record.tag or record_id + record.revoc_reg_id = f"{issuer_did}:4:{cred_def_id}:{revoc_def_type}:{rtag}" async with self._profile.session() as session: await record.save(session, reason="Init revocation registry") + + if endorser_connection_id is None and is_author_role(self._profile): + endorser_connection_id = await get_endorser_connection_id(self._profile) + if not endorser_connection_id: + raise RevocationError(reason="Endorser connection not found") + + if notify: + await notify_revocation_reg_init_event( + self._profile, + record.record_id, + create_pending_rev_reg=create_pending_rev_reg, + endorser_connection_id=endorser_connection_id, + ) + return record + async def handle_full_registry(self, revoc_reg_id: str): + """Update the registry status and start the next registry generation.""" + async with self._profile.transaction() as txn: + registry = await IssuerRevRegRecord.retrieve_by_revoc_reg_id( + txn, revoc_reg_id, for_update=True + ) + if registry.state == IssuerRevRegRecord.STATE_FULL: + return + await registry.set_state( + txn, + IssuerRevRegRecord.STATE_FULL, + ) + await txn.commit() + + await self.init_issuer_registry( + registry.cred_def_id, + registry.max_cred_num, + registry.revoc_def_type, + ) + async def get_active_issuer_rev_reg_record( self, cred_def_id: str - ) -> "IssuerRevRegRecord": + ) -> IssuerRevRegRecord: """Return current active registry for issuing a given credential definition. Args: @@ -85,9 +145,7 @@ async def get_active_issuer_rev_reg_record( f"No active issuer revocation record found for cred def id {cred_def_id}" ) - async def get_issuer_rev_reg_record( - self, revoc_reg_id: str - ) -> "IssuerRevRegRecord": + async def get_issuer_rev_reg_record(self, revoc_reg_id: str) -> IssuerRevRegRecord: """Return a revocation registry record by identifier. Args: @@ -98,26 +156,85 @@ async def get_issuer_rev_reg_record( session, revoc_reg_id ) - async def list_issuer_registries(self) -> Sequence["IssuerRevRegRecord"]: + async def list_issuer_registries(self) -> Sequence[IssuerRevRegRecord]: """List the issuer's current revocation registries.""" async with self._profile.session() as session: return await IssuerRevRegRecord.query(session) - async def get_ledger_registry(self, revoc_reg_id: str) -> "RevocationRegistry": + async def get_issuer_rev_reg_delta( + self, rev_reg_id: str, fro: int = None, to: int = None + ) -> dict: + """ + Check ledger for revocation status for a given revocation registry. + + Args: + rev_reg_id: ID of the revocation registry + + """ + ledger = await self.get_ledger_for_registry(rev_reg_id) + async with ledger: + (rev_reg_delta, _) = await ledger.get_revoc_reg_delta( + rev_reg_id, + fro, + to, + ) + + return rev_reg_delta + + async def get_or_create_active_registry( + self, cred_def_id: str, max_cred_num: int = None + ) -> Optional[Tuple[IssuerRevRegRecord, RevocationRegistry]]: + """Fetch the active revocation registry. + + If there is no active registry then creation of a new registry will be + triggered and the caller should retry after a delay. + """ + try: + active_rev_reg_rec = await self.get_active_issuer_rev_reg_record( + cred_def_id + ) + rev_reg = active_rev_reg_rec.get_registry() + await rev_reg.get_or_fetch_local_tails_path() + return active_rev_reg_rec, rev_reg + except StorageNotFoundError: + pass + + async with self._profile.session() as session: + rev_reg_recs = await IssuerRevRegRecord.query_by_cred_def_id( + session, cred_def_id, {"$neq": IssuerRevRegRecord.STATE_FULL} + ) + if not rev_reg_recs: + await self.init_issuer_registry( + cred_def_id, + max_cred_num=max_cred_num, + ) + return None + + async def get_ledger_registry(self, revoc_reg_id: str) -> RevocationRegistry: """Get a revocation registry from the ledger, fetching as necessary.""" if revoc_reg_id in IndyRevocation.REV_REG_CACHE: return IndyRevocation.REV_REG_CACHE[revoc_reg_id] - ledger_exec_inst = self._profile.inject(IndyLedgerRequestsExecutor) - ledger = ( - await ledger_exec_inst.get_ledger_for_identifier( - revoc_reg_id, - txn_record_type=GET_REVOC_REG_DEF, - ) - )[1] + ledger = await self.get_ledger_for_registry(revoc_reg_id) + async with ledger: rev_reg = RevocationRegistry.from_definition( await ledger.get_revoc_reg_def(revoc_reg_id), True ) IndyRevocation.REV_REG_CACHE[revoc_reg_id] = rev_reg return rev_reg + + async def get_ledger_for_registry(self, revoc_reg_id: str) -> BaseLedger: + """Get the ledger for the given registry.""" + multitenant_mgr = self._profile.inject_or(BaseMultitenantManager) + if multitenant_mgr: + ledger_exec_inst = IndyLedgerRequestsExecutor(self._profile) + else: + ledger_exec_inst = self._profile.inject(IndyLedgerRequestsExecutor) + ledger = ( + await ledger_exec_inst.get_ledger_for_identifier( + revoc_reg_id, + txn_record_type=GET_REVOC_REG_DEF, + ) + )[1] + return ledger diff --git a/aries_cloudagent/revocation/manager.py b/aries_cloudagent/revocation/manager.py index 4bdf222655..592a879c7a 100644 --- a/aries_cloudagent/revocation/manager.py +++ b/aries_cloudagent/revocation/manager.py @@ -45,6 +45,7 @@ async def revoke_credential_by_cred_ex_id( cred_ex_id: str, publish: bool = False, notify: bool = False, + notify_version: str = None, thread_id: str = None, connection_id: str = None, comment: str = None, @@ -77,6 +78,7 @@ async def revoke_credential_by_cred_ex_id( cred_rev_id=rec.cred_rev_id, publish=publish, notify=notify, + notify_version=notify_version, thread_id=thread_id, connection_id=connection_id, comment=comment, @@ -88,6 +90,7 @@ async def revoke_credential( cred_rev_id: str, publish: bool = False, notify: bool = False, + notify_version: str = None, thread_id: str = None, connection_id: str = None, comment: str = None, @@ -110,7 +113,7 @@ async def revoke_credential( issuer_rr_rec = await revoc.get_issuer_rev_reg_record(rev_reg_id) if not issuer_rr_rec: raise RevocationManagerError( - f"No revocation registry record found for id {rev_reg_id}" + f"No revocation registry record found for id: {rev_reg_id}" ) if notify: @@ -121,6 +124,7 @@ async def revoke_credential( thread_id=thread_id, connection_id=connection_id, comment=comment, + version=notify_version, ) async with self._profile.session() as session: await rev_notify_rec.save(session, reason="New revocation notification") @@ -129,23 +133,52 @@ async def revoke_credential( rev_reg = await revoc.get_ledger_registry(rev_reg_id) await rev_reg.get_or_fetch_local_tails_path() # pick up pending revocations on input revocation registry - crids = list(set(issuer_rr_rec.pending_pub + [cred_rev_id])) + crids = (issuer_rr_rec.pending_pub or []) + [cred_rev_id] (delta_json, _) = await issuer.revoke_credentials( issuer_rr_rec.revoc_reg_id, issuer_rr_rec.tails_local_path, crids ) - if delta_json: - issuer_rr_rec.revoc_reg_entry = json.loads(delta_json) - await issuer_rr_rec.send_entry(self._profile) - async with self._profile.session() as session: - await issuer_rr_rec.clear_pending(session) - await self.set_cred_revoked_state(rev_reg_id, [cred_rev_id]) - await notify_revocation_published_event( - self._profile, rev_reg_id, [cred_rev_id] + async with self._profile.transaction() as txn: + issuer_rr_upd = await IssuerRevRegRecord.retrieve_by_id( + txn, issuer_rr_rec.record_id, for_update=True ) + if delta_json: + issuer_rr_upd.revoc_reg_entry = json.loads(delta_json) + await issuer_rr_upd.clear_pending(txn, crids) + await txn.commit() + await self.set_cred_revoked_state(rev_reg_id, crids) + if delta_json: + await issuer_rr_upd.send_entry(self._profile) + await notify_revocation_published_event( + self._profile, rev_reg_id, [cred_rev_id] + ) else: - async with self._profile.session() as session: - await issuer_rr_rec.mark_pending(session, cred_rev_id) + async with self._profile.transaction() as txn: + await issuer_rr_rec.mark_pending(txn, cred_rev_id) + await txn.commit() + + async def update_rev_reg_revoked_state( + self, + apply_ledger_update: bool, + rev_reg_record: IssuerRevRegRecord, + genesis_transactions: dict, + ) -> (dict, dict, dict): + """ + Request handler to fix ledger entry of credentials revoked against registry. + + Args: + rev_reg_id: revocation registry id + apply_ledger_update: whether to apply an update to the ledger + + Returns: + Number of credentials posted to ledger + + """ + return await rev_reg_record.fix_ledger_entry( + self._profile, + apply_ledger_update, + genesis_transactions, + ) async def publish_pending_revocations( self, @@ -177,37 +210,42 @@ async def publish_pending_revocations( result = {} issuer = self._profile.inject(IndyIssuer) - async with self._profile.transaction() as txn: - issuer_rr_recs = await IssuerRevRegRecord.query_by_pending(txn) - for issuer_rr_rec in issuer_rr_recs: - rrid = issuer_rr_rec.revoc_reg_id - crids = [] - if not rrid2crid: - crids = issuer_rr_rec.pending_pub - elif rrid in rrid2crid: - crids = [ - crid - for crid in issuer_rr_rec.pending_pub - if crid in (rrid2crid[rrid] or []) or not rrid2crid[rrid] - ] - if crids: - # FIXME - must use the same transaction - (delta_json, failed_crids) = await issuer.revoke_credentials( - issuer_rr_rec.revoc_reg_id, - issuer_rr_rec.tails_local_path, - crids, - transaction=txn, + async with self._profile.session() as session: + issuer_rr_recs = await IssuerRevRegRecord.query_by_pending(session) + + for issuer_rr_rec in issuer_rr_recs: + rrid = issuer_rr_rec.revoc_reg_id + if rrid2crid: + if rrid not in rrid2crid: + continue + limit_crids = rrid2crid[rrid] + else: + limit_crids = () + crids = set(issuer_rr_rec.pending_pub or ()) + if limit_crids: + crids = crids.intersection(limit_crids) + if crids: + (delta_json, failed_crids) = await issuer.revoke_credentials( + issuer_rr_rec.revoc_reg_id, + issuer_rr_rec.tails_local_path, + crids, + ) + async with self._profile.transaction() as txn: + issuer_rr_upd = await IssuerRevRegRecord.retrieve_by_id( + txn, issuer_rr_rec.record_id, for_update=True ) - issuer_rr_rec.revoc_reg_entry = json.loads(delta_json) - await issuer_rr_rec.send_entry(self._profile) - published = [crid for crid in crids if crid not in failed_crids] - result[issuer_rr_rec.revoc_reg_id] = published - await issuer_rr_rec.clear_pending(txn, published) + if delta_json: + issuer_rr_upd.revoc_reg_entry = json.loads(delta_json) + await issuer_rr_upd.clear_pending(txn, crids) await txn.commit() - await self.set_cred_revoked_state(issuer_rr_rec.revoc_reg_id, crids) - await notify_revocation_published_event( - self._profile, issuer_rr_rec.revoc_reg_id, crids - ) + await self.set_cred_revoked_state(issuer_rr_rec.revoc_reg_id, crids) + if delta_json: + await issuer_rr_upd.send_entry(self._profile) + published = sorted(crid for crid in crids if crid not in failed_crids) + result[issuer_rr_rec.revoc_reg_id] = published + await notify_revocation_published_event( + self._profile, issuer_rr_rec.revoc_reg_id, crids + ) return result @@ -242,6 +280,7 @@ async def clear_pending_revocations( """ result = {} + notify = [] async with self._profile.transaction() as txn: issuer_rr_recs = await IssuerRevRegRecord.query_by_pending(txn) @@ -250,9 +289,12 @@ async def clear_pending_revocations( await issuer_rr_rec.clear_pending(txn, (purge or {}).get(rrid)) if issuer_rr_rec.pending_pub: result[rrid] = issuer_rr_rec.pending_pub - await notify_pending_cleared_event(self._profile, rrid) + notify.append(rrid) await txn.commit() + for rrid in notify: + await notify_pending_cleared_event(self._profile, rrid) + return result async def set_cred_revoked_state( @@ -270,34 +312,49 @@ async def set_cred_revoked_state( """ for cred_rev_id in cred_rev_ids: - async with self._profile.session() as session: - try: + cred_ex_id = None + + try: + async with self._profile.transaction() as txn: rev_rec = await IssuerCredRevRecord.retrieve_by_ids( - session, rev_reg_id, cred_rev_id + txn, rev_reg_id, cred_rev_id, for_update=True ) + cred_ex_id = rev_rec.cred_ex_id + cred_ex_version = rev_rec.cred_ex_version + rev_rec.state = IssuerCredRevRecord.STATE_REVOKED + await rev_rec.save(txn, reason="revoke credential") + await txn.commit() + except StorageNotFoundError: + continue + + async with self._profile.transaction() as txn: + if ( + not cred_ex_version + or cred_ex_version == IssuerCredRevRecord.VERSION_1 + ): try: cred_ex_record = await V10CredentialExchange.retrieve_by_id( - session, rev_rec.cred_ex_id + txn, cred_ex_id, for_update=True ) cred_ex_record.state = ( V10CredentialExchange.STATE_CREDENTIAL_REVOKED ) - await cred_ex_record.save(session, reason="revoke credential") + await cred_ex_record.save(txn, reason="revoke credential") + await txn.commit() + continue # skip 2.0 record check + except StorageNotFoundError: + pass + if ( + not cred_ex_version + or cred_ex_version == IssuerCredRevRecord.VERSION_2 + ): + try: + cred_ex_record = await V20CredExRecord.retrieve_by_id( + txn, cred_ex_id, for_update=True + ) + cred_ex_record.state = V20CredExRecord.STATE_CREDENTIAL_REVOKED + await cred_ex_record.save(txn, reason="revoke credential") + await txn.commit() except StorageNotFoundError: - try: - cred_ex_record = await V20CredExRecord.retrieve_by_id( - session, rev_rec.cred_ex_id - ) - cred_ex_record.state = ( - V20CredExRecord.STATE_CREDENTIAL_REVOKED - ) - await cred_ex_record.save( - session, reason="revoke credential" - ) - - except StorageNotFoundError: - pass - - except StorageNotFoundError: - pass + pass diff --git a/aries_cloudagent/revocation/models/issuer_cred_rev_record.py b/aries_cloudagent/revocation/models/issuer_cred_rev_record.py index cb87c070b9..10ba69ef53 100644 --- a/aries_cloudagent/revocation/models/issuer_cred_rev_record.py +++ b/aries_cloudagent/revocation/models/issuer_cred_rev_record.py @@ -27,6 +27,7 @@ class Meta: RECORD_TOPIC = "issuer_cred_rev" TAG_NAMES = { "cred_ex_id", + "cred_ex_version", "cred_def_id", "rev_reg_id", "cred_rev_id", @@ -36,6 +37,9 @@ class Meta: STATE_ISSUED = "issued" STATE_REVOKED = "revoked" + VERSION_1 = "1" + VERSION_2 = "2" + def __init__( self, *, @@ -45,6 +49,7 @@ def __init__( rev_reg_id: str = None, cred_rev_id: str = None, cred_def_id: str = None, # Marshmallow formalism: leave None + cred_ex_version: str = None, **kwargs, ): """Initialize a new IssuerCredRevRecord.""" @@ -53,6 +58,7 @@ def __init__( self.rev_reg_id = rev_reg_id self.cred_rev_id = cred_rev_id self.cred_def_id = ":".join(rev_reg_id.split(":")[-7:-2]) + self.cred_ex_version = cred_ex_version @property def record_id(self) -> str: @@ -90,10 +96,15 @@ async def retrieve_by_ids( session: ProfileSession, rev_reg_id: str, cred_rev_id: str, + *, + for_update: bool = False, ) -> "IssuerCredRevRecord": """Retrieve an issuer cred rev record by rev reg id and cred rev id.""" return await cls.retrieve_by_tag_filter( - session, {"rev_reg_id": rev_reg_id}, {"cred_rev_id": cred_rev_id} + session, + {"rev_reg_id": rev_reg_id}, + {"cred_rev_id": cred_rev_id}, + for_update=for_update, ) @classmethod @@ -153,3 +164,7 @@ class Meta: description="Credential revocation identifier", **INDY_CRED_REV_ID, ) + cred_ex_version = fields.Str( + required=False, + description="Credential exchange version", + ) diff --git a/aries_cloudagent/revocation/models/issuer_rev_reg_record.py b/aries_cloudagent/revocation/models/issuer_rev_reg_record.py index fbbaac9e43..1e3d41a9da 100644 --- a/aries_cloudagent/revocation/models/issuer_rev_reg_record.py +++ b/aries_cloudagent/revocation/models/issuer_rev_reg_record.py @@ -5,8 +5,9 @@ import uuid from functools import total_ordering from os.path import join +from pathlib import Path from shutil import move -from typing import Any, Mapping, Sequence, Union +from typing import Any, Mapping, Sequence, Union, Tuple from urllib.parse import urlparse from marshmallow import fields, validate @@ -21,6 +22,7 @@ ) from ...indy.util import indy_client_dir from ...ledger.base import BaseLedger +from ...ledger.error import LedgerError, LedgerTransactionError from ...messaging.models.base_record import BaseRecord, BaseRecordSchema from ...messaging.valid import ( BASE58_SHA256_HASH, @@ -29,7 +31,12 @@ INDY_REV_REG_ID, UUIDFour, ) +from ...tails.base import BaseTailsServer + from ..error import RevocationError +from ..recover import generate_ledger_rrrecovery_txn + +from .issuer_cred_rev_record import IssuerCredRevRecord from .revocation_registry import RevocationRegistry DEFAULT_REGISTRY_SIZE = 1000 @@ -62,7 +69,7 @@ class Meta: STATE_INIT = "init" STATE_GENERATED = "generated" - STATE_POSTED = "posted" # definition published: ephemeral, should last milliseconds + STATE_POSTED = "posted" # definition published STATE_ACTIVE = "active" # initial entry published, possibly subsequent entries STATE_FULL = "full" # includes corrupt @@ -195,6 +202,8 @@ async def generate_registry(self, profile: Profile): except IndyIssuerError as err: raise RevocationError() from err + if self.revoc_reg_id and revoc_reg_id != self.revoc_reg_id: + raise RevocationError("Generated registry ID does not match assigned value") self.revoc_reg_id = revoc_reg_id self.revoc_reg_def = json.loads(revoc_reg_def_json) self.revoc_reg_entry = json.loads(revoc_reg_entry_json) @@ -227,7 +236,7 @@ async def send_def( profile: Profile, write_ledger: bool = True, endorser_did: str = None, - ): + ) -> dict: """Send the revocation registry definition to the ledger.""" if not (self.revoc_reg_def and self.issuer_did): raise RevocationError(f"Revocation registry {self.revoc_reg_id} undefined") @@ -261,7 +270,7 @@ async def send_entry( profile: Profile, write_ledger: bool = True, endorser_did: str = None, - ): + ) -> dict: """Send a registry entry to the ledger.""" if not ( self.revoc_reg_id @@ -286,14 +295,44 @@ async def send_entry( ledger = profile.inject(BaseLedger) async with ledger: - rev_entry_res = await ledger.send_revoc_reg_entry( - self.revoc_reg_id, - self.revoc_def_type, - self._revoc_reg_entry.ser, - self.issuer_did, - write_ledger=write_ledger, - endorser_did=endorser_did, - ) + try: + rev_entry_res = await ledger.send_revoc_reg_entry( + self.revoc_reg_id, + self.revoc_def_type, + self._revoc_reg_entry.ser, + self.issuer_did, + write_ledger=write_ledger, + endorser_did=endorser_did, + ) + except LedgerTransactionError as err: + if "InvalidClientRequest" in err.roll_up: + # ... if the ledger write fails (with "InvalidClientRequest") + # e.g. aries_cloudagent.ledger.error.LedgerTransactionError: + # Ledger rejected transaction request: client request invalid: + # InvalidClientRequest(...) + # In this scenario we try to post a correction + LOGGER.warn("Retry ledger update/fix due to error") + LOGGER.warn(err) + (_, _, res) = await self.fix_ledger_entry( + profile, + True, + ledger.pool.genesis_txns, + ) + rev_entry_res = {"result": res} + LOGGER.warn("Ledger update/fix applied") + elif "InvalidClientTaaAcceptanceError" in err.roll_up: + # if no write access (with "InvalidClientTaaAcceptanceError") + # e.g. aries_cloudagent.ledger.error.LedgerTransactionError: + # Ledger rejected transaction request: client request invalid: + # InvalidClientTaaAcceptanceError(...) + LOGGER.error("Ledger update failed due to TAA issue") + LOGGER.error(err) + raise err + else: + # not sure what happened, raise an error + LOGGER.error("Ledger update failed due to unknown issue") + LOGGER.error(err) + raise err if self.state == IssuerRevRegRecord.STATE_POSTED: self.state = IssuerRevRegRecord.STATE_ACTIVE # initial entry activates async with profile.session() as session: @@ -303,6 +342,107 @@ async def send_entry( return rev_entry_res + async def fix_ledger_entry( + self, + profile: Profile, + apply_ledger_update: bool, + genesis_transactions: str, + ) -> Tuple[dict, dict, dict]: + """Fix the ledger entry to match wallet-recorded credentials.""" + # get rev reg delta (revocations published to ledger) + ledger = profile.inject(BaseLedger) + async with ledger: + (rev_reg_delta, _) = await ledger.get_revoc_reg_delta(self.revoc_reg_id) + + # get rev reg records from wallet (revocations and status) + recs = [] + rec_count = 0 + accum_count = 0 + recovery_txn = {} + applied_txn = {} + async with profile.session() as session: + recs = await IssuerCredRevRecord.query_by_ids( + session, rev_reg_id=self.revoc_reg_id + ) + + revoked_ids = [] + for rec in recs: + if rec.state == IssuerCredRevRecord.STATE_REVOKED: + revoked_ids.append(int(rec.cred_rev_id)) + if int(rec.cred_rev_id) not in rev_reg_delta["value"]["revoked"]: + # await rec.set_state(session, IssuerCredRevRecord.STATE_ISSUED) + rec_count += 1 + + LOGGER.debug(">>> fixed entry recs count = %s", rec_count) + LOGGER.debug( + ">>> rev_reg_record.revoc_reg_entry.value: %s", + self.revoc_reg_entry.value, + ) + LOGGER.debug( + '>>> rev_reg_delta.get("value"): %s', rev_reg_delta.get("value") + ) + + # if we had any revocation discrepencies, check the accumulator value + if rec_count > 0: + if (self.revoc_reg_entry.value and rev_reg_delta.get("value")) and not ( + self.revoc_reg_entry.value.accum == rev_reg_delta["value"]["accum"] + ): + # self.revoc_reg_entry = rev_reg_delta["value"] + # await self.save(session) + accum_count += 1 + + calculated_txn = await generate_ledger_rrrecovery_txn( + genesis_transactions, + self.revoc_reg_id, + revoked_ids, + ) + recovery_txn = json.loads(calculated_txn.to_json()) + + LOGGER.debug(">>> apply_ledger_update = %s", apply_ledger_update) + if apply_ledger_update: + ledger = session.inject_or(BaseLedger) + if not ledger: + reason = "No ledger available" + if not session.context.settings.get_value("wallet.type"): + reason += ": missing wallet-type?" + raise LedgerError(reason=reason) + + async with ledger: + ledger_response = await ledger.send_revoc_reg_entry( + self.revoc_reg_id, "CL_ACCUM", recovery_txn + ) + + applied_txn = ledger_response["result"] + + return (rev_reg_delta, recovery_txn, applied_txn) + + @property + def has_local_tails_file(self) -> bool: + """Check if a local copy of the tails file is available.""" + return bool(self.tails_local_path) and Path(self.tails_local_path).is_file() + + async def upload_tails_file(self, profile: Profile): + """Upload the local tails file to the tails server.""" + tails_server = profile.inject_or(BaseTailsServer) + if not tails_server: + raise RevocationError("Tails server not configured") + if not self.has_local_tails_file: + raise RevocationError("Local tails file not found") + + (upload_success, result) = await tails_server.upload_tails_file( + profile.context, + self.revoc_reg_id, + self.tails_local_path, + interval=0.8, + backoff=-0.5, + max_attempts=5, # heuristic: respect HTTP timeout + ) + if not upload_success: + raise RevocationError( + f"Tails file for rev reg {self.revoc_reg_id} failed to upload: {result}" + ) + await self.set_tails_file_public_uri(profile, result) + async def mark_pending(self, session: ProfileSession, cred_rev_id: str) -> None: """Mark a credential revocation id as revoked pending publication to ledger. @@ -334,7 +474,7 @@ async def clear_pending( self.pending_pub.clear() await self.save(session, reason="Cleared pending revocations") - async def get_registry(self) -> RevocationRegistry: + def get_registry(self) -> RevocationRegistry: """Create a `RevocationRegistry` instance from this record.""" return RevocationRegistry( self.revoc_reg_id, @@ -359,10 +499,12 @@ async def query_by_cred_def_id( cred_def_id: The credential definition ID to filter by state: A state value to filter by """ - tag_filter = { - **{"cred_def_id": cred_def_id for _ in [""] if cred_def_id}, - **{"state": state for _ in [""] if state}, - } + tag_filter = dict( + filter( + lambda f: f[1] is not None, + (("cred_def_id", cred_def_id), ("state", state)), + ) + ) return await cls.query(session, tag_filter) @classmethod @@ -383,16 +525,19 @@ async def query_by_pending( @classmethod async def retrieve_by_revoc_reg_id( - cls, session: ProfileSession, revoc_reg_id: str + cls, session: ProfileSession, revoc_reg_id: str, for_update: bool = False ) -> "IssuerRevRegRecord": """Retrieve a revocation registry record by revocation registry ID. Args: session: The profile session to use revoc_reg_id: The revocation registry ID + for_update: Retrieve for update """ tag_filter = {"revoc_reg_id": revoc_reg_id} - return await cls.retrieve_by_tag_filter(session, tag_filter) + return await cls.retrieve_by_tag_filter( + session, tag_filter, for_update=for_update + ) async def set_state(self, session: ProfileSession, state: str = None): """Change the registry state (default full).""" diff --git a/aries_cloudagent/revocation/models/revocation_registry.py b/aries_cloudagent/revocation/models/revocation_registry.py index e6c98a41bd..cfb97eb467 100644 --- a/aries_cloudagent/revocation/models/revocation_registry.py +++ b/aries_cloudagent/revocation/models/revocation_registry.py @@ -198,7 +198,7 @@ async def retrieve_tails(self): "The hash of the downloaded tails file does not match." ) - self.tails_local_path = tails_file_path + self.tails_local_path = str(tails_file_path) return self.tails_local_path async def get_or_fetch_local_tails_path(self): diff --git a/aries_cloudagent/revocation/models/tests/test_issuer_rev_reg_record.py b/aries_cloudagent/revocation/models/tests/test_issuer_rev_reg_record.py index c023caa68b..8154884aeb 100644 --- a/aries_cloudagent/revocation/models/tests/test_issuer_rev_reg_record.py +++ b/aries_cloudagent/revocation/models/tests/test_issuer_rev_reg_record.py @@ -65,7 +65,7 @@ async def setUp(self): TailsServer = async_mock.MagicMock(BaseTailsServer, autospec=True) self.tails_server = TailsServer() self.tails_server.upload_tails_file = async_mock.CoroutineMock( - return_value=(False, "Internal Server Error") + return_value=(True, "http://1.2.3.4:8088/rev-reg-id") ) self.profile.context.injector.bind_instance(BaseTailsServer, self.tails_server) @@ -124,11 +124,19 @@ async def test_generate_registry_etc(self): assert rec.state == IssuerRevRegRecord.STATE_POSTED self.ledger.send_revoc_reg_def.assert_called_once() + with async_mock.patch.object(test_module.Path, "is_file", lambda _: True): + await rec.upload_tails_file(self.profile) + assert ( + rec.tails_public_uri + and rec.revoc_reg_def.value.tails_location == rec.tails_public_uri + ) + self.tails_server.upload_tails_file.assert_called_once() + await rec.send_entry(self.profile) assert rec.state == IssuerRevRegRecord.STATE_ACTIVE self.ledger.send_revoc_reg_entry.assert_called_once() - rev_reg = await rec.get_registry() + rev_reg = rec.get_registry() assert type(rev_reg) == RevocationRegistry async with self.profile.session() as session: diff --git a/aries_cloudagent/revocation/recover.py b/aries_cloudagent/revocation/recover.py new file mode 100644 index 0000000000..49adcbc5ed --- /dev/null +++ b/aries_cloudagent/revocation/recover.py @@ -0,0 +1,119 @@ +"""Recover a revocation registry.""" + +import hashlib +import importlib +import logging +import tempfile +import time + +import aiohttp +import base58 + + +LOGGER = logging.getLogger(__name__) + + +""" +This module calculates a new ledger accumulator, based on the revocation status +on the ledger vs revocations recorded in the wallet. +The calculated transaction can be written to the ledger to get the ledger back +in sync with the wallet. +This function can be used if there were previous revocation errors (i.e. the +credential revocation was successfully written to the wallet but the ledger write +failed.) +""" + + +class RevocRecoveryException(Exception): + """Raise exception generating the recovery transaction.""" + + +async def fetch_txns(genesis_txns, registry_id): + """Fetch tails file and revocation registry information.""" + + try: + vdr_module = importlib.import_module("indy_vdr") + credx_module = importlib.import_module("indy_credx") + except Exception as e: + raise RevocRecoveryException(f"Failed to import library {e}") + + pool = await vdr_module.open_pool(transactions=genesis_txns) + LOGGER.debug("Connected to pool") + + LOGGER.debug("Fetch registry: %s", registry_id) + fetch = vdr_module.ledger.build_get_revoc_reg_def_request(None, registry_id) + result = await pool.submit_request(fetch) + if not result["data"]: + raise RevocRecoveryException(f"Registry definition not found for {registry_id}") + data = result["data"] + data["ver"] = "1.0" + defn = credx_module.RevocationRegistryDefinition.load(data) + LOGGER.debug("Tails URL: %s", defn.tails_location) + + async with aiohttp.ClientSession() as session: + data = await session.get(defn.tails_location) + tails_data = await data.read() + tails_hash = base58.b58encode(hashlib.sha256(tails_data).digest()).decode( + "utf-8" + ) + if tails_hash != defn.tails_hash: + raise RevocRecoveryException( + f"Tails hash mismatch {tails_hash} {defn.tails_hash}" + ) + else: + LOGGER.debug("Checked tails hash: %s", tails_hash) + tails_temp = tempfile.NamedTemporaryFile(delete=False) + tails_temp.write(tails_data) + tails_temp.close() + + to_timestamp = int(time.time()) + fetch = vdr_module.ledger.build_get_revoc_reg_delta_request( + None, registry_id, None, to_timestamp + ) + result = await pool.submit_request(fetch) + if not result["data"]: + raise RevocRecoveryException("Error fetching delta from ledger") + + accum_to = result["data"]["value"]["accum_to"] + accum_to["ver"] = "1.0" + delta = credx_module.RevocationRegistryDelta.load(accum_to) + registry = credx_module.RevocationRegistry.load(accum_to) + LOGGER.debug("Ledger registry state: %s", registry.to_json()) + revoked = set(result["data"]["value"]["revoked"]) + LOGGER.debug("Ledger revoked indexes: %s", revoked) + + return defn, registry, delta, revoked, tails_temp + + +async def generate_ledger_rrrecovery_txn(genesis_txns, registry_id, set_revoked): + """Generate a new ledger accum entry, based on wallet vs ledger revocation state.""" + + new_delta = None + + ledger_data = await fetch_txns(genesis_txns, registry_id) + if not ledger_data: + return new_delta + defn, registry, delta, prev_revoked, tails_temp = ledger_data + + set_revoked = set(set_revoked) + mismatch = prev_revoked - set_revoked + if mismatch: + LOGGER.warn( + "Credential index(es) revoked on the ledger, but not in wallet: %s", + mismatch, + ) + + updates = set_revoked - prev_revoked + if not updates: + LOGGER.debug("No updates to perform") + else: + LOGGER.debug("New revoked indexes: %s", updates) + + LOGGER.debug("tails_temp: %s", tails_temp.name) + update_registry = registry.copy() + new_delta = update_registry.update(defn, [], updates, tails_temp.name) + + LOGGER.debug("New delta:") + LOGGER.debug(new_delta.to_json()) + + return new_delta diff --git a/aries_cloudagent/revocation/routes.py b/aries_cloudagent/revocation/routes.py index b68a187e9d..1350ff3063 100644 --- a/aries_cloudagent/revocation/routes.py +++ b/aries_cloudagent/revocation/routes.py @@ -2,6 +2,8 @@ import json import logging +import os +import shutil from asyncio import shield import re @@ -21,7 +23,8 @@ from ..core.event_bus import Event, EventBus from ..core.profile import Profile from ..indy.issuer import IndyIssuerError -from ..indy.util import tails_path +from ..ledger.base import BaseLedger +from ..ledger.multiple_ledger.base_manager import BaseMultipleLedgerManager from ..ledger.error import LedgerError from ..messaging.credential_definitions.util import CRED_DEF_SENT_RECORD_TYPE from ..messaging.models.base import BaseModelError @@ -49,7 +52,6 @@ ) from ..storage.base import BaseStorage from ..storage.error import StorageError, StorageNotFoundError -from ..tails.base import BaseTailsServer from .error import RevocationError, RevocationNotSupportedError from .indy import IndyRevocation @@ -61,14 +63,13 @@ from .models.issuer_rev_reg_record import IssuerRevRegRecord, IssuerRevRegRecordSchema from .util import ( REVOCATION_EVENT_PREFIX, - REVOCATION_REG_EVENT, + REVOCATION_REG_INIT_EVENT, + REVOCATION_REG_ENDORSED_EVENT, REVOCATION_ENTRY_EVENT, - REVOCATION_TAILS_EVENT, - notify_revocation_reg_event, notify_revocation_entry_event, - notify_revocation_tails_file_event, ) + LOGGER = logging.getLogger(__name__) @@ -147,6 +148,31 @@ def validate_fields(self, data, **kwargs): ) +class RevRegId(OpenAPISchema): + """Parameters and validators for delete tails file request.""" + + @validates_schema + def validate_fields(self, data, **kwargs): + """Validate schema fields - must have either rr-id or cr-id.""" + + rev_reg_id = data.get("rev_reg_id") + cred_def_id = data.get("cred_def_id") + + if not (rev_reg_id or cred_def_id): + raise ValidationError("Request must have either rev_reg_id or cred_def_id") + + rev_reg_id = fields.Str( + description="Revocation registry identifier", + required=False, + **INDY_REV_REG_ID, + ) + cred_def_id = fields.Str( + description="Credential definition identifier", + required=False, + **INDY_CRED_DEF_ID, + ) + + class RevokeRequestSchema(CredRevRecordQueryStringSchema): """Parameters and validators for revocation request.""" @@ -157,11 +183,16 @@ def validate_fields(self, data, **kwargs): notify = data.get("notify") connection_id = data.get("connection_id") + notify_version = data.get("notify_version", "v1_0") if notify and not connection_id: raise ValidationError( "Request must specify connection_id if notify is true" ) + if notify and not notify_version: + raise ValidationError( + "Request must specify notify_version if notify is true" + ) publish = fields.Boolean( description=( @@ -174,6 +205,11 @@ def validate_fields(self, data, **kwargs): description="Send a notification to the credential recipient", required=False, ) + notify_version = fields.String( + description="Specify which version of the revocation notification should be sent", + validate=validate.OneOf(["v1_0", "v2_0"]), + required=False, + ) connection_id = fields.Str( description=( "Connection ID to which the revocation notification will be sent; " @@ -249,6 +285,20 @@ class CredRevRecordResultSchema(OpenAPISchema): result = fields.Nested(IssuerCredRevRecordSchema()) +class CredRevRecordDetailsResultSchema(OpenAPISchema): + """Result schema for credential revocation record request.""" + + results = fields.List(fields.Nested(IssuerCredRevRecordSchema())) + + +class CredRevIndyRecordsResultSchema(OpenAPISchema): + """Result schema for revoc reg delta.""" + + rev_reg_delta = fields.Dict( + description="Indy revocation registry delta", + ) + + class RevRegIssuedResultSchema(OpenAPISchema): """Result schema for revocation registry credentials issued request.""" @@ -259,6 +309,29 @@ class RevRegIssuedResultSchema(OpenAPISchema): ) +class RevRegUpdateRequestMatchInfoSchema(OpenAPISchema): + """Path parameters and validators for request taking rev reg id.""" + + apply_ledger_update = fields.Bool( + description="Apply updated accumulator transaction to ledger", + required=True, + ) + + +class RevRegWalletUpdatedResultSchema(OpenAPISchema): + """Number of wallet revocation entries status updated.""" + + rev_reg_delta = fields.Dict( + description="Indy revocation registry delta", + ) + accum_calculated = fields.Dict( + description="Calculated accumulator for phantom revocations", + ) + accum_fixed = fields.Dict( + description="Applied ledger transaction to fix revocations", + ) + + class RevRegsCreatedSchema(OpenAPISchema): """Result schema for request for revocation registries created.""" @@ -377,9 +450,15 @@ async def revoke(request: web.BaseRequest): body["notify"] = body.get("notify", context.settings.get("revocation.notify")) notify = body.get("notify") connection_id = body.get("connection_id") + body["notify_version"] = body.get("notify_version", "v1_0") + notify_version = body["notify_version"] if notify and not connection_id: raise web.HTTPBadRequest(reason="connection_id must be set when notify is true") + if notify and not notify_version: + raise web.HTTPBadRequest( + reason="Request must specify notify_version if notify is true" + ) rev_manager = RevocationManager(context.profile) try: @@ -498,6 +577,7 @@ async def create_rev_reg(request: web.BaseRequest): issuer_rev_reg_rec = await revoc.init_issuer_registry( credential_definition_id, max_cred_num=max_cred_num, + notify=False, ) except RevocationNotSupportedError as e: raise web.HTTPBadRequest(reason=e.message) from e @@ -532,9 +612,19 @@ async def rev_regs_created(request: web.BaseRequest): tag: request.query[tag] for tag in search_tags if tag in request.query } async with context.profile.session() as session: - found = await IssuerRevRegRecord.query(session, tag_filter) + found = await IssuerRevRegRecord.query( + session, + tag_filter, + post_filter_negative={"state": IssuerRevRegRecord.STATE_INIT}, + ) - return web.json_response({"rev_reg_ids": [record.revoc_reg_id for record in found]}) + return web.json_response( + { + "rev_reg_ids": [ + record.revoc_reg_id for record in found if record.revoc_reg_id + ] + } + ) @docs( @@ -573,7 +663,7 @@ async def get_rev_reg(request: web.BaseRequest): ) @match_info_schema(RevRegIdMatchInfoSchema()) @response_schema(RevRegIssuedResultSchema(), 200, description="") -async def get_rev_reg_issued(request: web.BaseRequest): +async def get_rev_reg_issued_count(request: web.BaseRequest): """ Request handler to get number of credentials issued against revocation registry. @@ -600,6 +690,160 @@ async def get_rev_reg_issued(request: web.BaseRequest): return web.json_response({"result": count}) +@docs( + tags=["revocation"], + summary="Get details of credentials issued against revocation registry", +) +@match_info_schema(RevRegIdMatchInfoSchema()) +@response_schema(CredRevRecordDetailsResultSchema(), 200, description="") +async def get_rev_reg_issued(request: web.BaseRequest): + """ + Request handler to get credentials issued against revocation registry. + + Args: + request: aiohttp request object + + Returns: + Number of credentials issued against revocation registry + + """ + context: AdminRequestContext = request["context"] + + rev_reg_id = request.match_info["rev_reg_id"] + + recs = [] + async with context.profile.session() as session: + try: + await IssuerRevRegRecord.retrieve_by_revoc_reg_id(session, rev_reg_id) + except StorageNotFoundError as err: + raise web.HTTPNotFound(reason=err.roll_up) from err + recs = await IssuerCredRevRecord.query_by_ids(session, rev_reg_id=rev_reg_id) + results = [] + for rec in recs: + results.append(rec.serialize()) + + return web.json_response(results) + + +@docs( + tags=["revocation"], + summary="Get details of revoked credentials from ledger", +) +@match_info_schema(RevRegIdMatchInfoSchema()) +@response_schema(CredRevIndyRecordsResultSchema(), 200, description="") +async def get_rev_reg_indy_recs(request: web.BaseRequest): + """ + Request handler to get details of revoked credentials from ledger. + + Args: + request: aiohttp request object + + Returns: + Detailes of revoked credentials from ledger + + """ + context: AdminRequestContext = request["context"] + + rev_reg_id = request.match_info["rev_reg_id"] + + revoc = IndyRevocation(context.profile) + rev_reg_delta = await revoc.get_issuer_rev_reg_delta(rev_reg_id) + + return web.json_response( + { + "rev_reg_delta": rev_reg_delta, + } + ) + + +@docs( + tags=["revocation"], + summary="Fix revocation state in wallet and return number of updated entries", +) +@match_info_schema(RevRegIdMatchInfoSchema()) +@querystring_schema(RevRegUpdateRequestMatchInfoSchema()) +@response_schema(RevRegWalletUpdatedResultSchema(), 200, description="") +async def update_rev_reg_revoked_state(request: web.BaseRequest): + """ + Request handler to fix ledger entry of credentials revoked against registry. + + Args: + request: aiohttp request object + + Returns: + Number of credentials posted to ledger + + """ + context: AdminRequestContext = request["context"] + + rev_reg_id = request.match_info["rev_reg_id"] + + apply_ledger_update_json = request.query.get("apply_ledger_update", "false") + LOGGER.debug(">>> apply_ledger_update_json = %s", apply_ledger_update_json) + apply_ledger_update = json.loads(request.query.get("apply_ledger_update", "false")) + + rev_reg_record = None + genesis_transactions = None + async with context.profile.session() as session: + try: + rev_reg_record = await IssuerRevRegRecord.retrieve_by_revoc_reg_id( + session, rev_reg_id + ) + except StorageNotFoundError as err: + raise web.HTTPNotFound(reason=err.roll_up) from err + + genesis_transactions = context.settings.get("ledger.genesis_transactions") + if not genesis_transactions: + ledger_manager = context.injector.inject(BaseMultipleLedgerManager) + write_ledgers = await ledger_manager.get_write_ledger() + LOGGER.debug(f"write_ledgers = {write_ledgers}") + pool = write_ledgers[1].pool + LOGGER.debug(f"write_ledger pool = {pool}") + + genesis_transactions = pool.genesis_txns + + if not genesis_transactions: + raise web.HTTPInternalServerError( + reason="no genesis_transactions for writable ledger" + ) + + if apply_ledger_update: + ledger = session.inject_or(BaseLedger) + if not ledger: + reason = "No ledger available" + if not session.context.settings.get_value("wallet.type"): + reason += ": missing wallet-type?" + raise web.HTTPInternalServerError(reason=reason) + + rev_manager = RevocationManager(context.profile) + try: + ( + rev_reg_delta, + recovery_txn, + applied_txn, + ) = await rev_manager.update_rev_reg_revoked_state( + apply_ledger_update, rev_reg_record, genesis_transactions + ) + except ( + RevocationManagerError, + RevocationError, + StorageError, + IndyIssuerError, + LedgerError, + ) as err: + raise web.HTTPBadRequest(reason=err.roll_up) + except Exception as err: + raise web.HTTPBadRequest(reason=str(err)) + + return web.json_response( + { + "rev_reg_delta": rev_reg_delta, + "accum_calculated": recovery_txn, + "accum_fixed": applied_txn, + } + ) + + @docs( tags=["revocation"], summary="Get credential revocation status", @@ -717,24 +961,19 @@ async def upload_tails_file(request: web.BaseRequest): context: AdminRequestContext = request["context"] rev_reg_id = request.match_info["rev_reg_id"] + try: + revoc = IndyRevocation(context.profile) + rev_reg = await revoc.get_issuer_rev_reg_record(rev_reg_id) + except StorageNotFoundError as err: + raise web.HTTPNotFound(reason=err.roll_up) from err - tails_server = context.inject_or(BaseTailsServer) - if not tails_server: - raise web.HTTPForbidden(reason="No tails server configured") - - loc_tails_path = tails_path(rev_reg_id) - if not loc_tails_path: + if not rev_reg.has_local_tails_file: raise web.HTTPNotFound(reason=f"No local tails file for rev reg {rev_reg_id}") - (upload_success, reason) = await tails_server.upload_tails_file( - context, - rev_reg_id, - loc_tails_path, - interval=0.8, - backoff=-0.5, - max_attempts=16, - ) - if not upload_success: - raise web.HTTPInternalServerError(reason=reason) + + try: + await rev_reg.upload_tails_file(context.profile) + except RevocationError as e: + raise web.HTTPInternalServerError(reason=str(e)) return web.json_response({}) @@ -896,17 +1135,16 @@ async def send_rev_reg_entry(request: web.BaseRequest): raise web.HTTPBadRequest(reason="No endorser connection found") if not write_ledger: - try: - async with profile.session() as session: + async with profile.session() as session: + try: connection_record = await ConnRecord.retrieve_by_id( session, connection_id ) - except StorageNotFoundError as err: - raise web.HTTPNotFound(reason=err.roll_up) from err - except BaseModelError as err: - raise web.HTTPBadRequest(reason=err.roll_up) from err + except StorageNotFoundError as err: + raise web.HTTPNotFound(reason=err.roll_up) from err + except BaseModelError as err: + raise web.HTTPBadRequest(reason=err.roll_up) from err - async with profile.session() as session: endorser_info = await connection_record.metadata_get( session, "endorser_info" ) @@ -934,7 +1172,6 @@ async def send_rev_reg_entry(request: web.BaseRequest): except StorageNotFoundError as err: raise web.HTTPNotFound(reason=err.roll_up) from err - except RevocationError as err: raise web.HTTPBadRequest(reason=err.roll_up) from err @@ -1046,105 +1283,84 @@ async def set_rev_reg_state(request: web.BaseRequest): def register_events(event_bus: EventBus): """Subscribe to any events we need to support.""" event_bus.subscribe( - re.compile(f"^{REVOCATION_EVENT_PREFIX}{REVOCATION_REG_EVENT}.*"), - on_revocation_registry_event, + re.compile(f"^{REVOCATION_EVENT_PREFIX}{REVOCATION_REG_INIT_EVENT}.*"), + on_revocation_registry_init_event, ) event_bus.subscribe( - re.compile(f"^{REVOCATION_EVENT_PREFIX}{REVOCATION_ENTRY_EVENT}.*"), - on_revocation_entry_event, + re.compile(f"^{REVOCATION_EVENT_PREFIX}{REVOCATION_REG_ENDORSED_EVENT}.*"), + on_revocation_registry_endorsed_event, ) event_bus.subscribe( - re.compile(f"^{REVOCATION_EVENT_PREFIX}{REVOCATION_TAILS_EVENT}.*"), - on_revocation_tails_file_event, + re.compile(f"^{REVOCATION_EVENT_PREFIX}{REVOCATION_ENTRY_EVENT}.*"), + on_revocation_entry_event, ) -async def on_revocation_registry_event(profile: Profile, event: Event): - """Handle revocation registry event.""" - if "endorser" in event.payload: +async def on_revocation_registry_init_event(profile: Profile, event: Event): + """Handle revocation registry initiation event.""" + meta_data = event.payload + if "endorser" in meta_data: # TODO error handling - for now just let exceptions get raised + endorser_connection_id = meta_data["endorser"]["connection_id"] async with profile.session() as session: connection = await ConnRecord.retrieve_by_id( - session, event.payload["endorser"]["connection_id"] + session, endorser_connection_id ) endorser_info = await connection.metadata_get(session, "endorser_info") endorser_did = endorser_info["endorser_did"] write_ledger = False - create_transaction_for_endorser = True else: + endorser_connection_id = None endorser_did = None write_ledger = True - create_transaction_for_endorser = False - - cred_def_id = event.payload["context"]["cred_def_id"] - rev_reg_size = event.payload["context"]["rev_reg_size"] - try: - tails_base_url = profile.settings.get("tails_server_base_url") - if not tails_base_url: - raise RevocationError("tails_server_base_url not configured") - - # Create registry - revoc = IndyRevocation(profile) - registry_record = await revoc.init_issuer_registry( - cred_def_id, - max_cred_num=rev_reg_size, - ) - await shield(registry_record.generate_registry(profile)) + tails_base_url = profile.settings.get("tails_server_base_url") + if not tails_base_url: + raise RevocationError("tails_server_base_url not configured") - await registry_record.set_tails_file_public_uri( + # Generate the registry and upload the tails file + async def generate(rr_record: IssuerRevRegRecord) -> dict: + await rr_record.generate_registry(profile) + public_uri = tails_base_url.rstrip("/") + f"/{registry_record.revoc_reg_id}" + await rr_record.set_tails_file_public_uri(profile, public_uri) + rev_reg_resp = await rr_record.send_def( profile, - f"{tails_base_url}/{registry_record.revoc_reg_id}", + write_ledger=write_ledger, + endorser_did=endorser_did, ) - async with profile.session() as session: - rev_reg_resp = await registry_record.send_def( - session.profile, - write_ledger=write_ledger, - endorser_did=endorser_did, - ) - except RevocationError as e: - raise RevocationError(e.message) from e - except RevocationNotSupportedError as e: - raise RevocationNotSupportedError(reason=e.message) from e + if write_ledger: + # Upload the tails file + await rr_record.upload_tails_file(profile) - if not create_transaction_for_endorser: - meta_data = event.payload - rev_reg_id = registry_record.revoc_reg_id - meta_data["context"]["rev_reg_id"] = rev_reg_id - auto_create_rev_reg = meta_data["processing"].get("auto_create_rev_reg", False) - - # Notify event - if auto_create_rev_reg: - await notify_revocation_entry_event(profile, rev_reg_id, meta_data) - - else: - transaction_manager = TransactionManager(profile) - try: - revo_transaction = await transaction_manager.create_record( - messages_attach=rev_reg_resp["result"], - connection_id=connection.connection_id, - meta_data=event.payload, - ) - except StorageError as err: - raise TransactionManagerError(reason=err.roll_up) from err - - # if auto-request, send the request to the endorser - if profile.settings.get_value("endorser.auto_request"): + # Post the initial revocation entry + await notify_revocation_entry_event(profile, record_id, meta_data) + else: + transaction_manager = TransactionManager(profile) try: - ( - revo_transaction, - revo_transaction_request, - ) = await transaction_manager.create_request( - transaction=revo_transaction, - # TODO see if we need to parameterize these params - # expires_time=expires_time, - # endorser_write_txn=endorser_write_txn, + revo_transaction = await transaction_manager.create_record( + messages_attach=rev_reg_resp["result"], + connection_id=connection.connection_id, + meta_data=event.payload, ) - except (StorageError, TransactionManagerError) as err: + except StorageError as err: raise TransactionManagerError(reason=err.roll_up) from err - async with profile.session() as session: - responder = session.inject_or(BaseResponder) + # if auto-request, send the request to the endorser + if profile.settings.get_value("endorser.auto_request"): + try: + ( + revo_transaction, + revo_transaction_request, + ) = await transaction_manager.create_request( + transaction=revo_transaction, + # TODO see if we need to parameterize these params + # expires_time=expires_time, + # endorser_write_txn=endorser_write_txn, + ) + except (StorageError, TransactionManagerError) as err: + raise TransactionManagerError(reason=err.roll_up) from err + + responder = profile.inject_or(BaseResponder) if responder: await responder.send( revo_transaction_request, @@ -1153,61 +1369,60 @@ async def on_revocation_registry_event(profile: Profile, event: Event): else: LOGGER.warning( "Configuration has no BaseResponder: cannot update " - "revocation on cred def %s", - cred_def_id, + "revocation on registry ID: %s", + record_id, ) + record_id = meta_data["context"]["issuer_rev_id"] + async with profile.session() as session: + registry_record = await IssuerRevRegRecord.retrieve_by_id(session, record_id) + await shield(generate(registry_record)) + + create_pending_rev_reg = meta_data["processing"].get( + "create_pending_rev_reg", False + ) + if write_ledger and create_pending_rev_reg: + revoc = IndyRevocation(profile) + await revoc.init_issuer_registry( + registry_record.cred_def_id, + registry_record.max_cred_num, + registry_record.revoc_def_type, + endorser_connection_id=endorser_connection_id, + ) + async def on_revocation_entry_event(profile: Profile, event: Event): """Handle revocation entry event.""" - if "endorser" in event.payload: + meta_data = event.payload + if "endorser" in meta_data: # TODO error handling - for now just let exceptions get raised async with profile.session() as session: connection = await ConnRecord.retrieve_by_id( - session, event.payload["endorser"]["connection_id"] + session, meta_data["endorser"]["connection_id"] ) endorser_info = await connection.metadata_get(session, "endorser_info") endorser_did = endorser_info["endorser_did"] write_ledger = False - create_transaction_for_endorser = True else: endorser_did = None write_ledger = True - create_transaction_for_endorser = False - - rev_reg_id = event.payload["context"]["rev_reg_id"] - try: - tails_base_url = profile.settings.get("tails_server_base_url") - if not tails_base_url: - raise RevocationError("tails_server_base_url not configured") - revoc = IndyRevocation(profile) - registry_record = await revoc.get_issuer_rev_reg_record(rev_reg_id) - rev_entry_resp = await registry_record.send_entry( - profile, - write_ledger=write_ledger, - endorser_did=endorser_did, - ) - except RevocationError as e: - raise RevocationError(e.message) from e - except RevocationNotSupportedError as e: - raise RevocationError(e.message) from e - - if not create_transaction_for_endorser: - meta_data = event.payload - auto_create_rev_reg = meta_data["processing"].get("auto_create_rev_reg", False) - - # Notify event - if auto_create_rev_reg: - await notify_revocation_tails_file_event(profile, rev_reg_id, meta_data) + record_id = meta_data["context"]["issuer_rev_id"] + async with profile.session() as session: + registry_record = await IssuerRevRegRecord.retrieve_by_id(session, record_id) + rev_entry_resp = await registry_record.send_entry( + profile, + write_ledger=write_ledger, + endorser_did=endorser_did, + ) - else: + if not write_ledger: transaction_manager = TransactionManager(profile) try: revo_transaction = await transaction_manager.create_record( messages_attach=rev_entry_resp["result"], connection_id=connection.connection_id, - meta_data=event.payload, + meta_data=meta_data, ) except StorageError as err: raise RevocationError(err.roll_up) from err @@ -1227,72 +1442,110 @@ async def on_revocation_entry_event(profile: Profile, event: Event): except (StorageError, TransactionManagerError) as err: raise RevocationError(err.roll_up) from err - async with profile.session() as session: - responder = session.inject_or(BaseResponder) - if responder: - await responder.send( - revo_transaction_request, - connection_id=connection.connection_id, - ) - else: - LOGGER.warning( - "Configuration has no BaseResponder: cannot update " - "revocation on cred def %s", - event.payload["endorser"]["cred_def_id"], - ) + responder = profile.inject_or(BaseResponder) + if responder: + await responder.send( + revo_transaction_request, + connection_id=connection.connection_id, + ) + else: + LOGGER.warning( + "Configuration has no BaseResponder: cannot update " + "revocation on cred def %s", + meta_data["endorser"]["cred_def_id"], + ) -async def on_revocation_tails_file_event(profile: Profile, event: Event): - """Handle revocation tails file event.""" - tails_base_url = profile.settings.get("tails_server_base_url") - if not tails_base_url: - raise RevocationError("tails_server_base_url not configured") +async def on_revocation_registry_endorsed_event(profile: Profile, event: Event): + """Handle revocation registry endorsement event.""" + meta_data = event.payload + rev_reg_id = meta_data["context"]["rev_reg_id"] + revoc = IndyRevocation(profile) + registry_record = await revoc.get_issuer_rev_reg_record(rev_reg_id) - tails_server = profile.inject(BaseTailsServer) - revoc_reg_id = event.payload["context"]["rev_reg_id"] - tails_local_path = tails_path(revoc_reg_id) - (upload_success, reason) = await tails_server.upload_tails_file( - profile, - revoc_reg_id, - tails_local_path, - interval=0.8, - backoff=-0.5, - max_attempts=5, # heuristic: respect HTTP timeout - ) - if not upload_success: - raise RevocationError( - f"Tails file for rev reg {revoc_reg_id} failed to upload: {reason}" + if profile.settings.get_value("endorser.auto_request"): + # NOTE: if there are multiple pods, then the one processing this + # event may not be the one that generated the tails file. + await registry_record.upload_tails_file(profile) + + # Post the initial revocation entry + await notify_revocation_entry_event( + profile, registry_record.record_id, meta_data ) # create a "pending" registry if one is requested # (this is done automatically when creating a credential definition, so that when a - # revocation registry fills up, we ca continue to issue credentials without a + # revocation registry fills up, we can continue to issue credentials without a # delay) - create_pending_rev_reg = event.payload["processing"].get( + create_pending_rev_reg = meta_data["processing"].get( "create_pending_rev_reg", False ) if create_pending_rev_reg: - meta_data = event.payload - del meta_data["context"]["rev_reg_id"] - del meta_data["processing"]["create_pending_rev_reg"] - cred_def_id = meta_data["context"]["cred_def_id"] - rev_reg_size = meta_data["context"].get("rev_reg_size", None) - auto_create_rev_reg = meta_data["processing"].get("auto_create_rev_reg", False) endorser_connection_id = ( meta_data["endorser"].get("connection_id", None) if "endorser" in meta_data else None ) - - await notify_revocation_reg_event( - profile, - cred_def_id, - rev_reg_size, - auto_create_rev_reg=auto_create_rev_reg, + await revoc.init_issuer_registry( + registry_record.cred_def_id, + registry_record.max_cred_num, + registry_record.revoc_def_type, endorser_connection_id=endorser_connection_id, ) +class TailsDeleteResponseSchema(OpenAPISchema): + """Return schema for tails failes deletion.""" + + message = fields.Str() + + +@querystring_schema(RevRegId()) +@response_schema(TailsDeleteResponseSchema()) +@docs(tags=["revocation"], summary="Delete the tail files") +async def delete_tails(request: web.BaseRequest) -> json: + """Delete Tails Files.""" + context: AdminRequestContext = request["context"] + rev_reg_id = request.query.get("rev_reg_id") + cred_def_id = request.query.get("cred_def_id") + revoc = IndyRevocation(context.profile) + session = revoc._profile.session() + if rev_reg_id: + rev_reg = await revoc.get_issuer_rev_reg_record(rev_reg_id) + tails_path = rev_reg.tails_local_path + main_dir_rev = os.path.dirname(tails_path) + try: + shutil.rmtree(main_dir_rev) + return web.json_response({"message": "All files deleted successfully"}) + except Exception as e: + return web.json_response({"message": str(e)}) + elif cred_def_id: + async with session: + cred_reg = sorted( + await IssuerRevRegRecord.query_by_cred_def_id( + session, cred_def_id, IssuerRevRegRecord.STATE_GENERATED + ) + )[0] + tails_path = cred_reg.tails_local_path + main_dir_rev = os.path.dirname(tails_path) + main_dir_cred = os.path.dirname(main_dir_rev) + filenames = os.listdir(main_dir_cred) + try: + flag = 0 + for i in filenames: + safe_cred_def_id = re.escape(cred_def_id) + if re.search(safe_cred_def_id, i): + shutil.rmtree(main_dir_cred + "/" + i) + flag = 1 + if flag: + return web.json_response({"message": "All files deleted successfully"}) + else: + return web.json_response({"message": "No such file or directory"}) + + except Exception as e: + return web.json_response({"message": str(e)}) + + async def register(app: web.Application): """Register routes.""" app.add_routes( @@ -1319,9 +1572,19 @@ async def register(app: web.Application): ), web.get( "/revocation/registry/{rev_reg_id}/issued", + get_rev_reg_issued_count, + allow_head=False, + ), + web.get( + "/revocation/registry/{rev_reg_id}/issued/details", get_rev_reg_issued, allow_head=False, ), + web.get( + "/revocation/registry/{rev_reg_id}/issued/indy_recs", + get_rev_reg_indy_recs, + allow_head=False, + ), web.post("/revocation/create-registry", create_rev_reg), web.post("/revocation/registry/{rev_reg_id}/definition", send_rev_reg_def), web.post("/revocation/registry/{rev_reg_id}/entry", send_rev_reg_entry), @@ -1336,6 +1599,11 @@ async def register(app: web.Application): "/revocation/registry/{rev_reg_id}/set-state", set_rev_reg_state, ), + web.put( + "/revocation/registry/{rev_reg_id}/fix-revocation-entry-state", + update_rev_reg_revoked_state, + ), + web.delete("/revocation/registry/delete-tails-file", delete_tails), ] ) diff --git a/aries_cloudagent/revocation/tests/test_indy.py b/aries_cloudagent/revocation/tests/test_indy.py index 10708c6d28..08fd5eb828 100644 --- a/aries_cloudagent/revocation/tests/test_indy.py +++ b/aries_cloudagent/revocation/tests/test_indy.py @@ -7,6 +7,8 @@ from ...ledger.multiple_ledger.ledger_requests_executor import ( IndyLedgerRequestsExecutor, ) +from ...multitenant.base import BaseMultitenantManager +from ...multitenant.manager import MultitenantManager from ...storage.error import StorageNotFoundError from ..error import ( @@ -18,7 +20,6 @@ from ..models.revocation_registry import RevocationRegistry -@pytest.mark.indy class TestIndyRevocation(AsyncTestCase): def setUp(self): self.profile = InMemoryProfile.test_profile() @@ -54,6 +55,22 @@ async def test_init_issuer_registry(self): assert result.revoc_def_type == IssuerRevRegRecord.REVOC_DEF_TYPE_CL assert result.tag is None + self.context.injector.bind_instance( + BaseMultitenantManager, + async_mock.MagicMock(MultitenantManager, autospec=True), + ) + with async_mock.patch.object( + IndyLedgerRequestsExecutor, + "get_ledger_for_identifier", + async_mock.CoroutineMock(return_value=(None, self.ledger)), + ): + result = await self.revoc.init_issuer_registry(CRED_DEF_ID) + assert result.cred_def_id == CRED_DEF_ID + assert result.issuer_did == self.test_did + assert result.max_cred_num == DEFAULT_REGISTRY_SIZE + assert result.revoc_def_type == IssuerRevRegRecord.REVOC_DEF_TYPE_CL + assert result.tag is None + async def test_init_issuer_registry_no_cred_def(self): CRED_DEF_ID = f"{self.test_did}:3:CL:1234:default" @@ -152,3 +169,24 @@ async def test_get_ledger_registry(self): mock_from_def.assert_called_once_with( self.ledger.get_revoc_reg_def.return_value, True ) + + self.context.injector.bind_instance( + BaseMultitenantManager, + async_mock.MagicMock(MultitenantManager, autospec=True), + ) + with async_mock.patch.object( + IndyLedgerRequestsExecutor, + "get_ledger_for_identifier", + async_mock.CoroutineMock(return_value=(None, self.ledger)), + ), async_mock.patch.object( + RevocationRegistry, "from_definition", async_mock.MagicMock() + ) as mock_from_def: + result = await self.revoc.get_ledger_registry("dummy2") + assert result == mock_from_def.return_value + assert "dummy2" in IndyRevocation.REV_REG_CACHE + + await self.revoc.get_ledger_registry("dummy2") + + mock_from_def.assert_called_once_with( + self.ledger.get_revoc_reg_def.return_value, True + ) diff --git a/aries_cloudagent/revocation/tests/test_manager.py b/aries_cloudagent/revocation/tests/test_manager.py index 72163e2830..ec026b9afd 100644 --- a/aries_cloudagent/revocation/tests/test_manager.py +++ b/aries_cloudagent/revocation/tests/test_manager.py @@ -3,11 +3,17 @@ from asynctest import mock as async_mock from asynctest import TestCase as AsyncTestCase +from aries_cloudagent.revocation.models.issuer_cred_rev_record import ( + IssuerCredRevRecord, +) + from ...core.in_memory import InMemoryProfile from ...indy.issuer import IndyIssuer from ...protocols.issue_credential.v1_0.models.credential_exchange import ( V10CredentialExchange, ) +from ...protocols.issue_credential.v2_0.models.cred_ex_record import V20CredExRecord + from ..manager import RevocationManager, RevocationManagerError @@ -32,22 +38,45 @@ async def setUp(self): async def test_revoke_credential_publish(self): CRED_EX_ID = "dummy-cxid" CRED_REV_ID = "1" + mock_issuer_rev_reg_record = async_mock.MagicMock( + revoc_reg_id=REV_REG_ID, + tails_local_path=TAILS_LOCAL, + send_entry=async_mock.CoroutineMock(), + clear_pending=async_mock.CoroutineMock(), + pending_pub=["2"], + ) + issuer = async_mock.MagicMock(IndyIssuer, autospec=True) + issuer.revoke_credentials = async_mock.CoroutineMock( + return_value=( + json.dumps( + { + "ver": "1.0", + "value": { + "prevAccum": "1 ...", + "accum": "21 ...", + "issued": [1], + }, + } + ), + [], + ) + ) + self.profile.context.injector.bind_instance(IndyIssuer, issuer) + with async_mock.patch.object( test_module.IssuerCredRevRecord, "retrieve_by_cred_ex_id", async_mock.CoroutineMock(), ) as mock_retrieve, async_mock.patch.object( test_module, "IndyRevocation", autospec=True - ) as revoc: + ) as revoc, async_mock.patch.object( + test_module.IssuerRevRegRecord, + "retrieve_by_id", + async_mock.CoroutineMock(return_value=mock_issuer_rev_reg_record), + ): mock_retrieve.return_value = async_mock.MagicMock( rev_reg_id="dummy-rr-id", cred_rev_id=CRED_REV_ID ) - mock_issuer_rev_reg_record = async_mock.MagicMock( - revoc_reg_id=REV_REG_ID, - tails_local_path=TAILS_LOCAL, - send_entry=async_mock.CoroutineMock(), - clear_pending=async_mock.CoroutineMock(), - ) mock_rev_reg = async_mock.MagicMock( get_or_fetch_local_tails_path=async_mock.CoroutineMock() ) @@ -58,29 +87,16 @@ async def test_revoke_credential_publish(self): return_value=mock_rev_reg ) - issuer = async_mock.MagicMock(IndyIssuer, autospec=True) - issuer.revoke_credentials = async_mock.CoroutineMock( - return_value=( - json.dumps( - { - "ver": "1.0", - "value": { - "prevAccum": "1 ...", - "accum": "21 ...", - "issued": [1], - }, - } - ), - [], - ) - ) - self.profile.context.injector.bind_instance(IndyIssuer, issuer) - await self.manager.revoke_credential_by_cred_ex_id(CRED_EX_ID, publish=True) + issuer.revoke_credentials.assert_awaited_once_with( + mock_issuer_rev_reg_record.revoc_reg_id, + mock_issuer_rev_reg_record.tails_local_path, + ["2", "1"], + ) + async def test_revoke_cred_by_cxid_not_found(self): CRED_EX_ID = "dummy-cxid" - CRED_REV_ID = "1" with async_mock.patch.object( test_module.IssuerCredRevRecord, @@ -120,29 +136,39 @@ async def test_revoke_credential_no_rev_reg_rec(self): async def test_revoke_credential_pend(self): CRED_REV_ID = "1" + mock_issuer_rev_reg_record = async_mock.MagicMock( + mark_pending=async_mock.CoroutineMock() + ) + issuer = async_mock.MagicMock(IndyIssuer, autospec=True) + self.profile.context.injector.bind_instance(IndyIssuer, issuer) + with async_mock.patch.object( test_module, "IndyRevocation", autospec=True ) as revoc, async_mock.patch.object( self.profile, "session", async_mock.MagicMock(return_value=self.profile.session()), - ) as session: - mock_issuer_rev_reg_record = async_mock.MagicMock( - mark_pending=async_mock.CoroutineMock() - ) + ) as session, async_mock.patch.object( + self.profile, + "transaction", + async_mock.MagicMock(return_value=session.return_value), + ) as session, async_mock.patch.object( + test_module.IssuerRevRegRecord, + "retrieve_by_id", + async_mock.CoroutineMock(return_value=mock_issuer_rev_reg_record), + ): revoc.return_value.get_issuer_rev_reg_record = async_mock.CoroutineMock( return_value=mock_issuer_rev_reg_record ) - issuer = async_mock.MagicMock(IndyIssuer, autospec=True) - self.profile.context.injector.bind_instance(IndyIssuer, issuer) - await self.manager.revoke_credential(REV_REG_ID, CRED_REV_ID, False) mock_issuer_rev_reg_record.mark_pending.assert_called_once_with( session.return_value, CRED_REV_ID ) - async def test_publish_pending_revocations(self): + issuer.revoke_credentials.assert_not_awaited() + + async def test_publish_pending_revocations_basic(self): deltas = [ { "ver": "1.0", @@ -169,7 +195,11 @@ async def test_publish_pending_revocations(self): test_module.IssuerRevRegRecord, "query_by_pending", async_mock.CoroutineMock(return_value=[mock_issuer_rev_reg_record]), - ) as record_query: + ), async_mock.patch.object( + test_module.IssuerRevRegRecord, + "retrieve_by_id", + async_mock.CoroutineMock(return_value=mock_issuer_rev_reg_record), + ): issuer = async_mock.MagicMock(IndyIssuer, autospec=True) issuer.merge_revocation_registry_deltas = async_mock.CoroutineMock( side_effect=deltas @@ -202,6 +232,7 @@ async def test_publish_pending_revocations_1_rev_reg_all(self): mock_issuer_rev_reg_records = [ async_mock.MagicMock( + record_id=0, revoc_reg_id=REV_REG_ID, tails_local_path=TAILS_LOCAL, pending_pub=["1", "2"], @@ -209,6 +240,7 @@ async def test_publish_pending_revocations_1_rev_reg_all(self): clear_pending=async_mock.CoroutineMock(), ), async_mock.MagicMock( + record_id=1, revoc_reg_id=f"{TEST_DID}:4:{CRED_DEF_ID}:CL_ACCUM:tag2", tails_local_path=TAILS_LOCAL, pending_pub=["9", "99"], @@ -220,7 +252,13 @@ async def test_publish_pending_revocations_1_rev_reg_all(self): test_module.IssuerRevRegRecord, "query_by_pending", async_mock.CoroutineMock(return_value=mock_issuer_rev_reg_records), - ) as record: + ), async_mock.patch.object( + test_module.IssuerRevRegRecord, + "retrieve_by_id", + async_mock.CoroutineMock( + side_effect=lambda _, id, **args: mock_issuer_rev_reg_records[id] + ), + ): issuer = async_mock.MagicMock(IndyIssuer, autospec=True) issuer.merge_revocation_registry_deltas = async_mock.CoroutineMock( side_effect=deltas @@ -254,6 +292,7 @@ async def test_publish_pending_revocations_1_rev_reg_some(self): mock_issuer_rev_reg_records = [ async_mock.MagicMock( + record_id=0, revoc_reg_id=REV_REG_ID, tails_local_path=TAILS_LOCAL, pending_pub=["1", "2"], @@ -261,6 +300,7 @@ async def test_publish_pending_revocations_1_rev_reg_some(self): clear_pending=async_mock.CoroutineMock(), ), async_mock.MagicMock( + record_id=1, revoc_reg_id=f"{TEST_DID}:4:{CRED_DEF_ID}:CL_ACCUM:tag2", tails_local_path=TAILS_LOCAL, pending_pub=["9", "99"], @@ -272,7 +312,13 @@ async def test_publish_pending_revocations_1_rev_reg_some(self): test_module.IssuerRevRegRecord, "query_by_pending", async_mock.CoroutineMock(return_value=mock_issuer_rev_reg_records), - ) as record: + ), async_mock.patch.object( + test_module.IssuerRevRegRecord, + "retrieve_by_id", + async_mock.CoroutineMock( + side_effect=lambda _, id, **args: mock_issuer_rev_reg_records[id] + ), + ): issuer = async_mock.MagicMock(IndyIssuer, autospec=True) issuer.merge_revocation_registry_deltas = async_mock.CoroutineMock( side_effect=deltas @@ -381,3 +427,80 @@ async def test_retrieve_records(self): ) assert ret_ex.connection_id == str(index) assert ret_ex.thread_id == str(1000 + index) + + async def test_set_revoked_state_v1(self): + CRED_REV_ID = "1" + + async with self.profile.session() as session: + exchange_record = V10CredentialExchange( + connection_id="mark-revoked-cid", + thread_id="mark-revoked-tid", + initiator=V10CredentialExchange.INITIATOR_SELF, + revoc_reg_id=REV_REG_ID, + revocation_id=CRED_REV_ID, + role=V10CredentialExchange.ROLE_ISSUER, + state=V10CredentialExchange.STATE_ISSUED, + ) + await exchange_record.save(session) + + crev_record = IssuerCredRevRecord( + cred_ex_id=exchange_record.credential_exchange_id, + cred_def_id=CRED_DEF_ID, + rev_reg_id=REV_REG_ID, + cred_rev_id=CRED_REV_ID, + state=IssuerCredRevRecord.STATE_ISSUED, + ) + await crev_record.save(session) + + await self.manager.set_cred_revoked_state(REV_REG_ID, [CRED_REV_ID]) + + async with self.profile.session() as session: + check_exchange_record = await V10CredentialExchange.retrieve_by_id( + session, exchange_record.credential_exchange_id + ) + assert ( + check_exchange_record.state + == V10CredentialExchange.STATE_CREDENTIAL_REVOKED + ) + + check_crev_record = await IssuerCredRevRecord.retrieve_by_id( + session, crev_record.record_id + ) + assert check_crev_record.state == IssuerCredRevRecord.STATE_REVOKED + + async def test_set_revoked_state_v2(self): + CRED_REV_ID = "1" + + async with self.profile.session() as session: + exchange_record = V20CredExRecord( + connection_id="mark-revoked-cid", + thread_id="mark-revoked-tid", + initiator=V20CredExRecord.INITIATOR_SELF, + role=V20CredExRecord.ROLE_ISSUER, + state=V20CredExRecord.STATE_ISSUED, + ) + await exchange_record.save(session) + + crev_record = IssuerCredRevRecord( + cred_ex_id=exchange_record.cred_ex_id, + cred_def_id=CRED_DEF_ID, + rev_reg_id=REV_REG_ID, + cred_rev_id=CRED_REV_ID, + state=IssuerCredRevRecord.STATE_ISSUED, + ) + await crev_record.save(session) + + await self.manager.set_cred_revoked_state(REV_REG_ID, [CRED_REV_ID]) + + async with self.profile.session() as session: + check_exchange_record = await V20CredExRecord.retrieve_by_id( + session, exchange_record.cred_ex_id + ) + assert ( + check_exchange_record.state == V20CredExRecord.STATE_CREDENTIAL_REVOKED + ) + + check_crev_record = await IssuerCredRevRecord.retrieve_by_id( + session, crev_record.record_id + ) + assert check_crev_record.state == IssuerCredRevRecord.STATE_REVOKED diff --git a/aries_cloudagent/revocation/tests/test_routes.py b/aries_cloudagent/revocation/tests/test_routes.py index ef3011edf2..9972d1b146 100644 --- a/aries_cloudagent/revocation/tests/test_routes.py +++ b/aries_cloudagent/revocation/tests/test_routes.py @@ -1,27 +1,24 @@ +import os +import shutil +import unittest + from aiohttp.web import HTTPBadRequest, HTTPNotFound from asynctest import TestCase as AsyncTestCase from asynctest import mock as async_mock from aries_cloudagent.core.in_memory import InMemoryProfile +from aries_cloudagent.revocation.error import RevocationError -from ...admin.request_context import AdminRequestContext from ...storage.in_memory import InMemoryStorage -from ...tails.base import BaseTailsServer from .. import routes as test_module class TestRevocationRoutes(AsyncTestCase): def setUp(self): - TailsServer = async_mock.MagicMock(BaseTailsServer, autospec=True) - self.tails_server = TailsServer() - self.tails_server.upload_tails_file = async_mock.CoroutineMock( - return_value=(True, None) - ) self.profile = InMemoryProfile.test_profile() self.context = self.profile.context setattr(self.context, "profile", self.profile) - self.context.injector.bind_instance(BaseTailsServer, self.tails_server) self.request_dict = { "context": self.context, "outbound_message_router": async_mock.CoroutineMock(), @@ -94,7 +91,6 @@ async def test_revoke(self): ) as mock_mgr, async_mock.patch.object( test_module.web, "json_response" ) as mock_response: - mock_mgr.return_value.revoke_credential = async_mock.CoroutineMock() await test_module.revoke(self.request) @@ -114,7 +110,6 @@ async def test_revoke_by_cred_ex_id(self): ) as mock_mgr, async_mock.patch.object( test_module.web, "json_response" ) as mock_response: - mock_mgr.return_value.revoke_credential = async_mock.CoroutineMock() await test_module.revoke(self.request) @@ -135,7 +130,6 @@ async def test_revoke_not_found(self): ) as mock_mgr, async_mock.patch.object( test_module.web, "json_response" ) as mock_response: - mock_mgr.return_value.revoke_credential = async_mock.CoroutineMock( side_effect=test_module.StorageNotFoundError() ) @@ -289,7 +283,6 @@ async def test_create_rev_reg_no_revo_support(self): async def test_rev_regs_created(self): CRED_DEF_ID = f"{self.test_did}:3:CL:1234:default" - STATE = "active" self.request.query = { "cred_def_id": CRED_DEF_ID, "state": test_module.IssuerRevRegRecord.STATE_ACTIVE, @@ -368,7 +361,7 @@ async def test_get_rev_reg_issued(self): test_module.web, "json_response", async_mock.Mock() ) as mock_json_response: mock_query.return_value = return_value = [{"...": "..."}, {"...": "..."}] - result = await test_module.get_rev_reg_issued(self.request) + result = await test_module.get_rev_reg_issued_count(self.request) mock_json_response.assert_called_once_with({"result": 2}) assert result is mock_json_response.return_value @@ -539,34 +532,32 @@ async def test_get_tails_file_not_found(self): result = await test_module.get_tails_file(self.request) mock_file_response.assert_not_called() - async def test_upload_tails_file(self): + async def test_upload_tails_file_basic(self): REV_REG_ID = "{}:4:{}:3:CL:1234:default:CL_ACCUM:default".format( self.test_did, self.test_did ) self.request.match_info = {"rev_reg_id": REV_REG_ID} with async_mock.patch.object( - test_module, "tails_path", async_mock.MagicMock() - ) as mock_tails_path, async_mock.patch.object( + test_module, "IndyRevocation", autospec=True + ) as mock_indy_revoc, async_mock.patch.object( test_module.web, "json_response", async_mock.Mock() ) as mock_json_response: - mock_tails_path.return_value = f"/tmp/tails/{REV_REG_ID}" - + mock_upload = async_mock.CoroutineMock() + mock_indy_revoc.return_value = async_mock.MagicMock( + get_issuer_rev_reg_record=async_mock.CoroutineMock( + return_value=async_mock.MagicMock( + tails_local_path=f"/tmp/tails/{REV_REG_ID}", + has_local_tails_file=True, + upload_tails_file=mock_upload, + ) + ) + ) result = await test_module.upload_tails_file(self.request) + mock_upload.assert_awaited_once() mock_json_response.assert_called_once_with({}) assert result is mock_json_response.return_value - async def test_upload_tails_file_no_tails_server(self): - REV_REG_ID = "{}:4:{}:3:CL:1234:default:CL_ACCUM:default".format( - self.test_did, self.test_did - ) - self.request.match_info = {"rev_reg_id": REV_REG_ID} - - self.context.injector.clear_binding(BaseTailsServer) - - with self.assertRaises(test_module.web.HTTPForbidden): - await test_module.upload_tails_file(self.request) - async def test_upload_tails_file_no_local_tails_file(self): REV_REG_ID = "{}:4:{}:3:CL:1234:default:CL_ACCUM:default".format( self.test_did, self.test_did @@ -574,9 +565,16 @@ async def test_upload_tails_file_no_local_tails_file(self): self.request.match_info = {"rev_reg_id": REV_REG_ID} with async_mock.patch.object( - test_module, "tails_path", async_mock.MagicMock() - ) as mock_tails_path: - mock_tails_path.return_value = None + test_module, "IndyRevocation", autospec=True + ) as mock_indy_revoc: + mock_indy_revoc.return_value = async_mock.MagicMock( + get_issuer_rev_reg_record=async_mock.CoroutineMock( + return_value=async_mock.MagicMock( + tails_local_path=f"/tmp/tails/{REV_REG_ID}", + has_local_tails_file=False, + ) + ) + ) with self.assertRaises(test_module.web.HTTPNotFound): await test_module.upload_tails_file(self.request) @@ -587,17 +585,20 @@ async def test_upload_tails_file_fail(self): ) self.request.match_info = {"rev_reg_id": REV_REG_ID} - TailsServer = async_mock.MagicMock(BaseTailsServer, autospec=True) - self.tails_server = TailsServer() - self.tails_server.upload_tails_file = async_mock.CoroutineMock( - return_value=(False, "Internal Server Error") - ) - self.context.injector.clear_binding(BaseTailsServer) - self.context.injector.bind_instance(BaseTailsServer, self.tails_server) - with async_mock.patch.object( - test_module, "tails_path", async_mock.MagicMock() - ) as mock_tails_path: + test_module, "IndyRevocation", autospec=True + ) as mock_indy_revoc: + mock_upload = async_mock.CoroutineMock(side_effect=RevocationError("test")) + mock_indy_revoc.return_value = async_mock.MagicMock( + get_issuer_rev_reg_record=async_mock.CoroutineMock( + return_value=async_mock.MagicMock( + tails_local_path=f"/tmp/tails/{REV_REG_ID}", + has_local_tails_file=True, + upload_tails_file=mock_upload, + ) + ) + ) + with self.assertRaises(test_module.web.HTTPInternalServerError): await test_module.upload_tails_file(self.request) @@ -906,3 +907,63 @@ async def test_post_process_routes(self): ]["get"]["responses"]["200"]["schema"] == {"type": "string", "format": "binary"} assert "tags" in mock_app._state["swagger_dict"] + + +class TestDeleteTails(unittest.TestCase): + def setUp(self): + self.rev_reg_id = "rev_reg_id_123" + self.cred_def_id = "cred_def_id_456" + + self.main_dir_rev = "path/to/main/dir/rev" + self.tails_path = os.path.join(self.main_dir_rev, "tails") + if not (os.path.exists(self.main_dir_rev)): + os.makedirs(self.main_dir_rev) + open(self.tails_path, "w").close() + + async def test_delete_tails_by_rev_reg_id(self): + # Setup + rev_reg_id = self.rev_reg_id + + # Test + result = await test_module.delete_tails( + {"context": None, "query": {"rev_reg_id": rev_reg_id}} + ) + + # Assert + self.assertEqual(result, {"message": "All files deleted successfully"}) + self.assertFalse(os.path.exists(self.tails_path)) + + async def test_delete_tails_by_cred_def_id(self): + # Setup + cred_def_id = self.cred_def_id + main_dir_cred = "path/to/main/dir/cred" + os.makedirs(main_dir_cred) + cred_dir = os.path.join(main_dir_cred, cred_def_id) + os.makedirs(cred_dir) + + # Test + result = await test_module.delete_tails( + {"context": None, "query": {"cred_def_id": cred_def_id}} + ) + + # Assert + self.assertEqual(result, {"message": "All files deleted successfully"}) + self.assertFalse(os.path.exists(cred_dir)) + self.assertTrue(os.path.exists(main_dir_cred)) + + async def test_delete_tails_not_found(self): + # Setup + cred_def_id = "invalid_cred_def_id" + + # Test + result = await test_module.delete_tails( + {"context": None, "query": {"cred_def_id": cred_def_id}} + ) + + # Assert + self.assertEqual(result, {"message": "No such file or directory"}) + self.assertTrue(os.path.exists(self.main_dir_rev)) + + async def tearDown(self): + if os.path.exists(self.main_dir_rev): + shutil.rmtree(self.main_dir_rev) diff --git a/aries_cloudagent/revocation/util.py b/aries_cloudagent/revocation/util.py index 50dd08a50c..df40a17630 100644 --- a/aries_cloudagent/revocation/util.py +++ b/aries_cloudagent/revocation/util.py @@ -4,79 +4,59 @@ from typing import Sequence from ..core.profile import Profile -from ..protocols.endorse_transaction.v1_0.util import ( - get_endorser_connection_id, - is_author_role, -) REVOCATION_EVENT_PREFIX = "acapy::REVOCATION::" EVENT_LISTENER_PATTERN = re.compile(f"^{REVOCATION_EVENT_PREFIX}(.*)?$") -REVOCATION_REG_EVENT = "REGISTRY" -REVOCATION_ENTRY_EVENT = "ENTRY" -REVOCATION_TAILS_EVENT = "TAILS" +REVOCATION_REG_INIT_EVENT = "REGISTRY_INIT" +REVOCATION_REG_ENDORSED_EVENT = "REGISTRY_ENDORSED" +REVOCATION_ENTRY_ENDORSED_EVENT = "ENTRY_ENDORSED" +REVOCATION_ENTRY_EVENT = "SEND_ENTRY" REVOCATION_PUBLISHED_EVENT = "published" REVOCATION_CLEAR_PENDING_EVENT = "clear-pending" -async def notify_revocation_reg_event( +async def notify_revocation_reg_init_event( profile: Profile, - cred_def_id: str, - rev_reg_size: int, - auto_create_rev_reg: bool = False, + issuer_rev_id: str, create_pending_rev_reg: bool = False, endorser_connection_id: str = None, ): - """Send notification for a revocation registry event.""" + """Send notification for a revocation registry init event.""" meta_data = { "context": { - "cred_def_id": cred_def_id, - "support_revocation": True, - "rev_reg_size": rev_reg_size, - }, - "processing": { - "auto_create_rev_reg": auto_create_rev_reg, + "issuer_rev_id": issuer_rev_id, }, + "processing": {"create_pending_rev_reg": create_pending_rev_reg}, } - if ( - (not endorser_connection_id) - and is_author_role(profile) - and "endorser" not in meta_data - ): - endorser_connection_id = await get_endorser_connection_id(profile) - if not endorser_connection_id: - raise Exception(reason="No endorser connection found") - if create_pending_rev_reg: - meta_data["processing"]["create_pending_rev_reg"] = create_pending_rev_reg if endorser_connection_id: meta_data["endorser"] = {"connection_id": endorser_connection_id} - event_id = REVOCATION_EVENT_PREFIX + REVOCATION_REG_EVENT + "::" + cred_def_id - await profile.notify( - event_id, - meta_data, - ) + topic = f"{REVOCATION_EVENT_PREFIX}{REVOCATION_REG_INIT_EVENT}::{issuer_rev_id}" + await profile.notify(topic, meta_data) async def notify_revocation_entry_event( + profile: Profile, issuer_rev_id: str, meta_data: dict +): + """Send notification for a revocation registry entry event.""" + topic = f"{REVOCATION_EVENT_PREFIX}{REVOCATION_ENTRY_EVENT}::{issuer_rev_id}" + await profile.notify(topic, meta_data) + + +async def notify_revocation_reg_endorsed_event( profile: Profile, rev_reg_id: str, meta_data: dict ): - """Send notification for a revocation registry event.""" - event_id = REVOCATION_EVENT_PREFIX + REVOCATION_ENTRY_EVENT + "::" + rev_reg_id - await profile.notify( - event_id, - meta_data, - ) + """Send notification for a revocation registry endorsement event.""" + topic = f"{REVOCATION_EVENT_PREFIX}{REVOCATION_REG_ENDORSED_EVENT}::{rev_reg_id}" + await profile.notify(topic, meta_data) -async def notify_revocation_tails_file_event( +async def notify_revocation_entry_endorsed_event( profile: Profile, rev_reg_id: str, meta_data: dict ): - """Send notification for a revocation tails file event.""" - event_id = REVOCATION_EVENT_PREFIX + REVOCATION_TAILS_EVENT + "::" + rev_reg_id - await profile.notify( - event_id, - meta_data, - ) + """Send notification for a revocation registry entry endorsement event.""" + topic = f"{REVOCATION_EVENT_PREFIX}{REVOCATION_ENTRY_ENDORSED_EVENT}::{rev_reg_id}" + await profile.notify(topic, meta_data) async def notify_revocation_published_event( diff --git a/aries_cloudagent/settings/__init__.py b/aries_cloudagent/settings/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/aries_cloudagent/settings/routes.py b/aries_cloudagent/settings/routes.py new file mode 100644 index 0000000000..9a576da73d --- /dev/null +++ b/aries_cloudagent/settings/routes.py @@ -0,0 +1,176 @@ +"""Settings routes.""" + +import logging + +from aiohttp import web +from aiohttp_apispec import docs, request_schema, response_schema +from marshmallow import fields + +from ..admin.request_context import AdminRequestContext +from ..multitenant.base import BaseMultitenantManager +from ..core.error import BaseError +from ..core.profile import Profile +from ..messaging.models.openapi import OpenAPISchema +from ..multitenant.admin.routes import ( + get_extra_settings_dict_per_tenant, + ACAPY_LIFECYCLE_CONFIG_FLAG_ARGS_MAP, +) + +LOGGER = logging.getLogger(__name__) + + +class UpdateProfileSettingsSchema(OpenAPISchema): + """Schema to update profile settings.""" + + extra_settings = fields.Dict( + description="Agent config key-value pairs", + required=False, + example={ + "log-level": "INFO", + "ACAPY_INVITE_PUBLIC": True, + "public-invites": False, + }, + ) + + +class ProfileSettingsSchema(OpenAPISchema): + """Profile settings response schema.""" + + settings = fields.Dict( + description="Profile settings dict", + example={ + "log.level": "INFO", + "debug.invite_public": True, + "public_invites": False, + }, + ) + + +def _get_filtered_settings_dict(wallet_settings: dict): + """Get filtered settings dict to display.""" + filter_param_list = list(ACAPY_LIFECYCLE_CONFIG_FLAG_ARGS_MAP.values()) + settings_dict = {} + for param in filter_param_list: + if param in wallet_settings: + settings_dict[param] = wallet_settings.get(param) + return settings_dict + + +def _get_multitenant_settings_dict( + profile_settings: dict, + wallet_settings: dict, +): + """Get filtered settings dict when multitenant manager is present.""" + all_settings = {**profile_settings, **wallet_settings} + settings_dict = _get_filtered_settings_dict(all_settings) + return settings_dict + + +def _get_settings_dict( + profile: Profile, +): + """Get filtered settings dict when multitenant manager is not present.""" + settings_dict = _get_filtered_settings_dict((profile.settings).to_dict()) + return settings_dict + + +@docs( + tags=["settings"], + summary="Update configurable settings associated with the profile.", +) +@request_schema(UpdateProfileSettingsSchema()) +@response_schema(ProfileSettingsSchema(), 200, description="") +async def update_profile_settings(request: web.BaseRequest): + """ + Request handler for updating setting associated with profile. + + Args: + request: aiohttp request object + """ + context: AdminRequestContext = request["context"] + root_profile = context.root_profile or context.profile + try: + body = await request.json() + extra_settings = get_extra_settings_dict_per_tenant( + body.get("extra_settings") or {} + ) + async with root_profile.session() as session: + multitenant_mgr = session.inject_or(BaseMultitenantManager) + if multitenant_mgr: + wallet_id = context.metadata.get("wallet_id") + wallet_key = context.metadata.get("wallet_key") + wallet_record = await multitenant_mgr.update_wallet( + wallet_id, extra_settings + ) + wallet_record, profile = await multitenant_mgr.get_wallet_and_profile( + root_profile.context, wallet_id, wallet_key + ) + settings_dict = _get_multitenant_settings_dict( + profile_settings=profile.settings.to_dict(), + wallet_settings=wallet_record.settings, + ) + else: + root_profile.context.update_settings(extra_settings) + settings_dict = _get_settings_dict(profile=root_profile) + except BaseError as err: + raise web.HTTPBadRequest(reason=err.roll_up) from err + return web.json_response(settings_dict) + + +@docs( + tags=["settings"], + summary="Get the configurable settings associated with the profile.", +) +@response_schema(ProfileSettingsSchema(), 200, description="") +async def get_profile_settings(request: web.BaseRequest): + """ + Request handler for getting setting associated with profile. + + Args: + request: aiohttp request object + """ + context: AdminRequestContext = request["context"] + root_profile = context.root_profile or context.profile + try: + async with root_profile.session() as session: + multitenant_mgr = session.inject_or(BaseMultitenantManager) + if multitenant_mgr: + wallet_id = context.metadata.get("wallet_id") + wallet_key = context.metadata.get("wallet_key") + wallet_record, profile = await multitenant_mgr.get_wallet_and_profile( + root_profile.context, wallet_id, wallet_key + ) + settings_dict = _get_multitenant_settings_dict( + profile_settings=profile.settings.to_dict(), + wallet_settings=wallet_record.settings, + ) + else: + settings_dict = _get_settings_dict(profile=root_profile) + except BaseError as err: + raise web.HTTPBadRequest(reason=err.roll_up) from err + return web.json_response(settings_dict) + + +async def register(app: web.Application): + """Register routes.""" + + app.add_routes( + [ + web.put("/settings", update_profile_settings), + web.get("/settings", get_profile_settings, allow_head=False), + ] + ) + + +def post_process_routes(app: web.Application): + """Amend swagger API.""" + + # Add top-level tags description + if "tags" not in app._state["swagger_dict"]: + app._state["swagger_dict"]["tags"] = [] + app._state["swagger_dict"]["tags"].append( + { + "name": "settings", + "description": "Agent settings interface.", + } + ) diff --git a/aries_cloudagent/settings/tests/__init__.py b/aries_cloudagent/settings/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/aries_cloudagent/settings/tests/test_routes.py b/aries_cloudagent/settings/tests/test_routes.py new file mode 100644 index 0000000000..b17774c050 --- /dev/null +++ b/aries_cloudagent/settings/tests/test_routes.py @@ -0,0 +1,220 @@ +"""Test settings routes.""" + +# pylint: disable=redefined-outer-name + +import pytest +from asynctest import mock as async_mock + +from ...admin.request_context import AdminRequestContext +from ...core.in_memory import InMemoryProfile +from ...multitenant.base import BaseMultitenantManager +from ...multitenant.manager import MultitenantManager + +from .. import routes as test_module + + +@pytest.fixture +def mock_response(): + json_response = async_mock.MagicMock() + temp_value = test_module.web.json_response + test_module.web.json_response = json_response + yield json_response + test_module.web.json_response = temp_value + + +@pytest.mark.asyncio +async def test_get_profile_settings(mock_response): + profile = InMemoryProfile.test_profile() + profile.settings.update( + { + "admin.admin_client_max_request_size": 1, + "debug.auto_respond_credential_offer": True, + "debug.auto_respond_credential_request": True, + "debug.auto_respond_presentation_proposal": True, + "debug.auto_verify_presentation": True, + "debug.auto_accept_invites": True, + "debug.auto_accept_requests": True, + } + ) + request_dict = { + "context": AdminRequestContext( + profile=profile, + ), + } + request = async_mock.MagicMock( + query={}, + json=async_mock.CoroutineMock(return_value={}), + __getitem__=lambda _, k: request_dict[k], + ) + await test_module.get_profile_settings(request) + assert mock_response.call_args[0][0] == { + "debug.auto_respond_credential_offer": True, + "debug.auto_respond_credential_request": True, + "debug.auto_verify_presentation": True, + "debug.auto_accept_invites": True, + "debug.auto_accept_requests": True, + } + # Multitenant + profile = InMemoryProfile.test_profile() + multi_tenant_manager = MultitenantManager(profile) + profile.context.injector.bind_instance( + BaseMultitenantManager, + multi_tenant_manager, + ) + request_dict = { + "context": AdminRequestContext( + profile=profile, + root_profile=profile, + metadata={ + "wallet_id": "walletid", + "wallet_key": "walletkey", + }, + ), + } + request = async_mock.MagicMock( + query={}, + json=async_mock.CoroutineMock(return_value={}), + __getitem__=lambda _, k: request_dict[k], + ) + with async_mock.patch.object( + multi_tenant_manager, "get_wallet_and_profile" + ) as get_wallet_and_profile: + get_wallet_and_profile.return_value = ( + async_mock.MagicMock( + settings={ + "admin.admin_client_max_request_size": 1, + "debug.auto_respond_credential_offer": True, + "debug.auto_respond_credential_request": True, + "debug.auto_respond_presentation_proposal": True, + "debug.auto_verify_presentation": True, + "debug.auto_accept_invites": True, + "debug.auto_accept_requests": True, + } + ), + profile, + ) + await test_module.get_profile_settings(request) + assert mock_response.call_args[0][0] == { + "debug.auto_respond_credential_offer": True, + "debug.auto_respond_credential_request": True, + "debug.auto_verify_presentation": True, + "debug.auto_accept_invites": True, + "debug.auto_accept_requests": True, + } + + +@pytest.mark.asyncio +async def test_update_profile_settings(mock_response): + profile = InMemoryProfile.test_profile() + profile.settings.update( + { + "public_invites": True, + "debug.invite_public": True, + "debug.auto_accept_invites": True, + "debug.auto_accept_requests": True, + "auto_ping_connection": True, + } + ) + request_dict = { + "context": AdminRequestContext( + profile=profile, + ), + } + request = async_mock.MagicMock( + query={}, + json=async_mock.CoroutineMock( + return_value={ + "extra_settings": { + "ACAPY_INVITE_PUBLIC": False, + "ACAPY_PUBLIC_INVITES": False, + "ACAPY_AUTO_ACCEPT_INVITES": False, + "ACAPY_AUTO_ACCEPT_REQUESTS": False, + "ACAPY_AUTO_PING_CONNECTION": False, + } + } + ), + __getitem__=lambda _, k: request_dict[k], + ) + await test_module.update_profile_settings(request) + assert mock_response.call_args[0][0] == { + "public_invites": False, + "debug.invite_public": False, + "debug.auto_accept_invites": False, + "debug.auto_accept_requests": False, + "auto_ping_connection": False, + } + # Multitenant + profile = InMemoryProfile.test_profile() + multi_tenant_manager = MultitenantManager(profile) + profile.context.injector.bind_instance( + BaseMultitenantManager, + multi_tenant_manager, + ) + + request_dict = { + "context": AdminRequestContext( + profile=profile, + root_profile=profile, + metadata={ + "wallet_id": "walletid", + "wallet_key": "walletkey", + }, + ), + } + request = async_mock.MagicMock( + query={}, + json=async_mock.CoroutineMock( + return_value={ + "extra_settings": { + "ACAPY_INVITE_PUBLIC": False, + "ACAPY_PUBLIC_INVITES": False, + "ACAPY_AUTO_ACCEPT_INVITES": False, + "ACAPY_AUTO_ACCEPT_REQUESTS": False, + "ACAPY_AUTO_PING_CONNECTION": False, + } + } + ), + __getitem__=lambda _, k: request_dict[k], + ) + with async_mock.patch.object( + multi_tenant_manager, "update_wallet" + ) as update_wallet, async_mock.patch.object( + multi_tenant_manager, "get_wallet_and_profile" + ) as get_wallet_and_profile: + get_wallet_and_profile.return_value = ( + async_mock.MagicMock( + settings={ + "admin.admin_client_max_request_size": 1, + "debug.auto_respond_credential_offer": True, + "debug.auto_respond_credential_request": True, + "debug.auto_respond_presentation_proposal": True, + "debug.auto_verify_presentation": True, + "public_invites": False, + "debug.invite_public": False, + "debug.auto_accept_invites": False, + "debug.auto_accept_requests": False, + "auto_ping_connection": False, + } + ), + profile, + ) + update_wallet.return_value = async_mock.MagicMock( + settings={ + "public_invites": False, + "debug.invite_public": False, + "debug.auto_accept_invites": False, + "debug.auto_accept_requests": False, + "auto_ping_connection": False, + } + ) + await test_module.update_profile_settings(request) + assert mock_response.call_args[0][0] == { + "public_invites": False, + "debug.invite_public": False, + "debug.auto_accept_invites": False, + "debug.auto_accept_requests": False, + "auto_ping_connection": False, + "debug.auto_respond_credential_offer": True, + "debug.auto_respond_credential_request": True, + "debug.auto_verify_presentation": True, + } diff --git a/aries_cloudagent/storage/askar.py b/aries_cloudagent/storage/askar.py index 75683d4af9..2a48cfb2a7 100644 --- a/aries_cloudagent/storage/askar.py +++ b/aries_cloudagent/storage/askar.py @@ -84,10 +84,11 @@ async def get_record( raise StorageError("Record type not provided") if not record_id: raise StorageError("Record ID not provided") - if not options: - options = {} + for_update = bool(options and options.get("forUpdate")) try: - item = await self._session.handle.fetch(record_type, record_id) + item = await self._session.handle.fetch( + record_type, record_id, for_update=for_update + ) except AskarError as err: raise StorageError("Error when fetching storage record") from err if not item: @@ -155,9 +156,10 @@ async def find_record( tag_query: Tags to query options: Dictionary of backend-specific options """ + for_update = bool(options and options.get("forUpdate")) try: results = await self._session.handle.fetch_all( - type_filter, tag_query, limit=2 + type_filter, tag_query, limit=2, for_update=for_update ) except AskarError as err: raise StorageError("Error when finding storage record") from err @@ -180,8 +182,11 @@ async def find_all_records( options: Mapping = None, ): """Retrieve all records matching a particular type filter and tag query.""" + for_update = bool(options and options.get("forUpdate")) results = [] - for row in await self._session.handle.fetch_all(type_filter, tag_query): + for row in await self._session.handle.fetch_all( + type_filter, tag_query, for_update=for_update + ): results.append( StorageRecord( type=row.category, diff --git a/aries_cloudagent/storage/in_memory.py b/aries_cloudagent/storage/in_memory.py index c94a9d6f93..296858f1d2 100644 --- a/aries_cloudagent/storage/in_memory.py +++ b/aries_cloudagent/storage/in_memory.py @@ -70,8 +70,7 @@ async def get_record( row = self.profile.records.get(record_id) if row and row.type == record_type: return row - if not row: - raise StorageNotFoundError("Record not found: {}".format(record_id)) + raise StorageNotFoundError("Record not found: {}".format(record_id)) async def update_record(self, record: StorageRecord, value: str, tags: Mapping): """ diff --git a/aries_cloudagent/storage/tests/test_indy_storage.py b/aries_cloudagent/storage/tests/test_indy_storage.py index cca1cb8ed0..ff1092cf61 100644 --- a/aries_cloudagent/storage/tests/test_indy_storage.py +++ b/aries_cloudagent/storage/tests/test_indy_storage.py @@ -12,7 +12,7 @@ from asynctest import mock as async_mock from ...config.injection_context import InjectionContext -from ...indy.sdk.profile import IndySdkProfileManager +from ...indy.sdk.profile import IndySdkProfileManager, IndySdkProfile from ...storage.base import BaseStorage from ...storage.error import StorageError, StorageSearchError from ...storage.indy import IndySdkStorage @@ -28,16 +28,17 @@ async def make_profile(): key = await IndySdkWallet.generate_wallet_key() context = InjectionContext() context.injector.bind_instance(IndySdkLedgerPool, IndySdkLedgerPool("name")) - return await IndySdkProfileManager().provision( - context, - { - "auto_recreate": True, - "auto_remove": True, - "name": "test-wallet", - "key": key, - "key_derivation_method": "RAW", # much slower tests with argon-hashed keys - }, - ) + with async_mock.patch.object(IndySdkProfile, "_make_finalizer"): + return await IndySdkProfileManager().provision( + context, + { + "auto_recreate": True, + "auto_remove": True, + "name": "test-wallet", + "key": key, + "key_derivation_method": "RAW", # much slower tests with argon-hashed keys + }, + ) @pytest.fixture() @@ -75,7 +76,9 @@ async def test_record(self): indy.wallet, "close_wallet", async_mock.CoroutineMock() ) as mock_close, async_mock.patch.object( indy.wallet, "delete_wallet", async_mock.CoroutineMock() - ) as mock_delete: + ) as mock_delete, async_mock.patch.object( + IndySdkProfile, "_make_finalizer" + ): config = { "auto_recreate": True, "auto_remove": True, @@ -244,7 +247,9 @@ async def test_storage_search_x(self): indy.wallet, "close_wallet", async_mock.CoroutineMock() ) as mock_close, async_mock.patch.object( indy.wallet, "delete_wallet", async_mock.CoroutineMock() - ) as mock_delete: + ) as mock_delete, async_mock.patch.object( + IndySdkProfile, "_make_finalizer" + ): context = InjectionContext() context.injector.bind_instance(IndySdkLedgerPool, IndySdkLedgerPool("name")) fake_profile = await IndySdkProfileManager().provision( @@ -322,7 +327,9 @@ async def test_storage_del_close(self): indy.wallet, "close_wallet", async_mock.CoroutineMock() ) as mock_close, async_mock.patch.object( indy.wallet, "delete_wallet", async_mock.CoroutineMock() - ) as mock_delete: + ) as mock_delete, async_mock.patch.object( + IndySdkProfile, "_make_finalizer" + ): context = InjectionContext() context.injector.bind_instance(IndySdkLedgerPool, IndySdkLedgerPool("name")) fake_profile = await IndySdkProfileManager().provision( @@ -354,7 +361,7 @@ async def test_storage_del_close(self): while not mock_indy_close_search.await_count and c < 10: await asyncio.sleep(0.1) c += 1 - mock_indy_close_search.assert_awaited_once_with(1) + mock_indy_close_search.assert_awaited_with(1) with async_mock.patch.object( # error on close indy.non_secrets, "open_wallet_search", async_mock.CoroutineMock() diff --git a/aries_cloudagent/storage/vc_holder/tests/test_indy_vc_holder.py b/aries_cloudagent/storage/vc_holder/tests/test_indy_vc_holder.py index 401b0c8750..fa092511b4 100644 --- a/aries_cloudagent/storage/vc_holder/tests/test_indy_vc_holder.py +++ b/aries_cloudagent/storage/vc_holder/tests/test_indy_vc_holder.py @@ -1,8 +1,9 @@ import pytest +from asynctest import mock as async_mock from ....config.injection_context import InjectionContext -from ....indy.sdk.profile import IndySdkProfileManager +from ....indy.sdk.profile import IndySdkProfileManager, IndySdkProfile from ....ledger.indy import IndySdkLedgerPool from ....wallet.indy import IndySdkWallet @@ -25,16 +26,18 @@ async def make_profile(): key = await IndySdkWallet.generate_wallet_key() context = InjectionContext() context.injector.bind_instance(IndySdkLedgerPool, IndySdkLedgerPool("name")) - return await IndySdkProfileManager().provision( - context, - { - "auto_recreate": True, - "auto_remove": True, - "name": "test-wallet", - "key": key, - "key_derivation_method": "RAW", # much slower tests with argon-hashed keys - }, - ) + + with async_mock.patch.object(IndySdkProfile, "_make_finalizer"): + return await IndySdkProfileManager().provision( + context, + { + "auto_recreate": True, + "auto_remove": True, + "name": "test-wallet", + "key": key, + "key_derivation_method": "RAW", # much slower tests with argon-hashed keys + }, + ) @pytest.fixture() diff --git a/aries_cloudagent/tails/indy_tails_server.py b/aries_cloudagent/tails/indy_tails_server.py index 8646a83ce7..0c5ebb6ab4 100644 --- a/aries_cloudagent/tails/indy_tails_server.py +++ b/aries_cloudagent/tails/indy_tails_server.py @@ -1,19 +1,26 @@ """Indy tails server interface class.""" +import logging + from typing import Tuple +from ..config.injection_context import InjectionContext +from ..ledger.multiple_ledger.base_manager import BaseMultipleLedgerManager from ..utils.http import put_file, PutError from .base import BaseTailsServer from .error import TailsServerNotConfiguredError +LOGGER = logging.getLogger(__name__) + + class IndyTailsServer(BaseTailsServer): """Indy tails server interface.""" async def upload_tails_file( self, - context, + context: InjectionContext, rev_reg_id: str, tails_file_path: str, interval: float = 1.0, @@ -30,26 +37,40 @@ async def upload_tails_file( backoff: exponential backoff in retry interval max_attempts: maximum number of attempts to make """ - - genesis_transactions = context.settings.get("ledger.genesis_transactions") tails_server_upload_url = context.settings.get("tails_server_upload_url") + genesis_transactions = context.settings.get("ledger.genesis_transactions") + + if not genesis_transactions: + ledger_manager = context.injector.inject(BaseMultipleLedgerManager) + write_ledgers = await ledger_manager.get_write_ledger() + LOGGER.debug(f"write_ledgers = {write_ledgers}") + pool = write_ledgers[1].pool + LOGGER.debug(f"write_ledger pool = {pool}") + + genesis_transactions = pool.genesis_txns + + if not genesis_transactions: + raise TailsServerNotConfiguredError( + "no genesis_transactions for writable ledger" + ) if not tails_server_upload_url: raise TailsServerNotConfiguredError( "tails_server_upload_url setting is not set" ) + upload_url = tails_server_upload_url.rstrip("/") + f"/{rev_reg_id}" + try: - return ( - True, - await put_file( - f"{tails_server_upload_url}/{rev_reg_id}", - {"tails": tails_file_path}, - {"genesis": genesis_transactions}, - interval=interval, - backoff=backoff, - max_attempts=max_attempts, - ), + await put_file( + upload_url, + {"tails": tails_file_path}, + {"genesis": genesis_transactions}, + interval=interval, + backoff=backoff, + max_attempts=max_attempts, ) except PutError as x_put: return (False, x_put.message) + + return True, upload_url diff --git a/aries_cloudagent/tails/tests/test_indy.py b/aries_cloudagent/tails/tests/test_indy.py index 4dc479c5c6..65d026a59e 100644 --- a/aries_cloudagent/tails/tests/test_indy.py +++ b/aries_cloudagent/tails/tests/test_indy.py @@ -1,6 +1,8 @@ from asynctest import mock as async_mock, TestCase as AsyncTestCase from ...config.injection_context import InjectionContext +from ...core.in_memory import InMemoryProfile +from ...ledger.multiple_ledger.base_manager import BaseMultipleLedgerManager from .. import indy_tails_server as test_module @@ -36,7 +38,73 @@ async def test_upload(self): "/tmp/dummy/path", ) assert ok - assert text == "tails-hash" + assert ( + text == context.settings["tails_server_upload_url"] + "/" + REV_REG_ID + ) + + async def test_upload_indy_sdk(self): + profile = InMemoryProfile.test_profile() + profile.settings["tails_server_upload_url"] = "http://1.2.3.4:8088" + profile.context.injector.bind_instance( + BaseMultipleLedgerManager, + async_mock.MagicMock( + get_write_ledger=async_mock.CoroutineMock( + return_value=( + "test_ledger_id", + async_mock.MagicMock( + pool=async_mock.MagicMock(genesis_transactions="dummy") + ), + ) + ) + ), + ) + indy_tails = test_module.IndyTailsServer() + + with async_mock.patch.object( + test_module, "put_file", async_mock.CoroutineMock() + ) as mock_put: + mock_put.return_value = "tails-hash" + (ok, text) = await indy_tails.upload_tails_file( + profile.context, + REV_REG_ID, + "/tmp/dummy/path", + ) + assert ok + assert ( + text == profile.settings["tails_server_upload_url"] + "/" + REV_REG_ID + ) + + async def test_upload_indy_vdr(self): + profile = InMemoryProfile.test_profile() + profile.settings["tails_server_upload_url"] = "http://1.2.3.4:8088" + profile.context.injector.bind_instance( + BaseMultipleLedgerManager, + async_mock.MagicMock( + get_write_ledger=async_mock.CoroutineMock( + return_value=( + "test_ledger_id", + async_mock.MagicMock( + pool=async_mock.MagicMock(genesis_txns_cache="dummy") + ), + ) + ) + ), + ) + indy_tails = test_module.IndyTailsServer() + + with async_mock.patch.object( + test_module, "put_file", async_mock.CoroutineMock() + ) as mock_put: + mock_put.return_value = "tails-hash" + (ok, text) = await indy_tails.upload_tails_file( + profile.context, + REV_REG_ID, + "/tmp/dummy/path", + ) + assert ok + assert ( + text == profile.settings["tails_server_upload_url"] + "/" + REV_REG_ID + ) async def test_upload_x(self): context = InjectionContext( diff --git a/aries_cloudagent/transport/inbound/base.py b/aries_cloudagent/transport/inbound/base.py index 648514a6be..66f23179cf 100644 --- a/aries_cloudagent/transport/inbound/base.py +++ b/aries_cloudagent/transport/inbound/base.py @@ -19,6 +19,7 @@ def __init__( create_session: Callable, *, max_message_size: int = 0, + is_external: bool = False, wire_format: BaseWireFormat = None, root_profile: Profile = None, ): @@ -35,6 +36,7 @@ def __init__( self._scheme = scheme self.wire_format: BaseWireFormat = wire_format self.root_profile: Profile = root_profile + self._is_external = is_external @property def max_message_size(self): @@ -46,6 +48,11 @@ def scheme(self): """Accessor for this transport's scheme.""" return self._scheme + @property + def is_external(self): + """Accessor for this transport's is_external.""" + return self._is_external + def create_session( self, *, diff --git a/aries_cloudagent/transport/inbound/http.py b/aries_cloudagent/transport/inbound/http.py index 8179023379..696cab4bd3 100644 --- a/aries_cloudagent/transport/inbound/http.py +++ b/aries_cloudagent/transport/inbound/http.py @@ -96,7 +96,12 @@ async def inbound_message_handler(self, request: web.BaseRequest): raise web.HTTPBadRequest() if inbound.receipt.direct_response_requested: - response = await session.wait_response() + # Wait for the message to be processed. Only send a response if a response + # buffer is present. + await inbound.wait_processing_complete() + response = ( + await session.wait_response() if session.response_buffer else None + ) # no more responses session.can_respond = False diff --git a/aries_cloudagent/transport/inbound/manager.py b/aries_cloudagent/transport/inbound/manager.py index 3ae54122dc..909703e7c5 100644 --- a/aries_cloudagent/transport/inbound/manager.py +++ b/aries_cloudagent/transport/inbound/manager.py @@ -1,6 +1,5 @@ """Inbound transport manager.""" -import asyncio import logging import uuid from collections import OrderedDict @@ -43,7 +42,6 @@ def __init__( self.registered_transports = {} self.running_transports = {} self.sessions = OrderedDict() - self.session_limit: asyncio.Semaphore = None self.task_queue = TaskQueue() self.undelivered_queue: DeliveryQueue = None @@ -68,8 +66,6 @@ async def setup(self): if self.profile.context.settings.get("transport.enable_undelivered_queue"): self.undelivered_queue = DeliveryQueue() - # self.session_limit = asyncio.Semaphore(50) - def register(self, config: InboundTransportConfiguration) -> str: """ Register transport module. @@ -163,8 +159,6 @@ async def create_session( client_info: An optional dict describing the client wire_format: Override the wire format for this session """ - if self.session_limit: - await self.session_limit if not wire_format: wire_format = self.profile.context.inject(BaseWireFormat) session = InboundSession( @@ -187,6 +181,8 @@ def dispatch_complete(self, message: InboundMessage, completed: CompletedTask): if session and session.accept_undelivered and not session.response_buffered: self.process_undelivered(session) + message.dispatch_processing_complete() + def closed_session(self, session: InboundSession): """ Clean up a closed session. @@ -195,8 +191,6 @@ def closed_session(self, session: InboundSession): """ if session.session_id in self.sessions: del self.sessions[session.session_id] - if self.session_limit: - self.session_limit.release() if session.response_buffer: if self.return_inbound: self.return_inbound(session.profile, session.response_buffer) diff --git a/aries_cloudagent/transport/inbound/message.py b/aries_cloudagent/transport/inbound/message.py index 169b2dc35c..2def0e1d92 100644 --- a/aries_cloudagent/transport/inbound/message.py +++ b/aries_cloudagent/transport/inbound/message.py @@ -1,5 +1,6 @@ """Classes representing inbound messages.""" +import asyncio from typing import Union from .receipt import MessageReceipt @@ -23,3 +24,12 @@ def __init__( self.receipt = receipt self.session_id = session_id self.transport_type = transport_type + self.processing_complete_event = asyncio.Event() + + def dispatch_processing_complete(self): + """Dispatch processing complete.""" + self.processing_complete_event.set() + + async def wait_processing_complete(self): + """Wait for processing to complete.""" + await self.processing_complete_event.wait() diff --git a/aries_cloudagent/transport/inbound/receipt.py b/aries_cloudagent/transport/inbound/receipt.py index 653bbc37d0..f36412458e 100644 --- a/aries_cloudagent/transport/inbound/receipt.py +++ b/aries_cloudagent/transport/inbound/receipt.py @@ -1,6 +1,7 @@ """Classes for representing message receipt details.""" from datetime import datetime +from typing import Optional class MessageReceipt: @@ -25,6 +26,7 @@ def __init__( sender_did: str = None, sender_verkey: str = None, thread_id: str = None, + parent_thread_id: str = None, ): """Initialize the message delivery instance.""" self._connection_id = connection_id @@ -37,6 +39,7 @@ def __init__( self._sender_did = sender_did self._sender_verkey = sender_verkey self._thread_id = thread_id + self._parent_thread_id = parent_thread_id @property def connection_id(self) -> str: @@ -266,6 +269,28 @@ def thread_id(self, thread: str): """ self._thread_id = thread + @property + def parent_thread_id(self) -> Optional[str]: + """ + Accessor for the identifier of the message parent thread. + + Returns: + The delivery parent thread ID + + """ + return self._parent_thread_id + + @parent_thread_id.setter + def parent_thread_id(self, thread: Optional[str]): + """ + Setter for the message parent thread identifier. + + Args: + thread: The new parent thread identifier + + """ + self._parent_thread_id = thread + def __repr__(self) -> str: """ Provide a human readable representation of this object. diff --git a/aries_cloudagent/transport/inbound/tests/test_http_transport.py b/aries_cloudagent/transport/inbound/tests/test_http_transport.py index 4a594f526d..fd74775572 100644 --- a/aries_cloudagent/transport/inbound/tests/test_http_transport.py +++ b/aries_cloudagent/transport/inbound/tests/test_http_transport.py @@ -62,6 +62,7 @@ def receive_message( message: InboundMessage, can_respond: bool = False, ): + message.wait_processing_complete = async_mock.CoroutineMock() self.message_results.append((message.payload, message.receipt, can_respond)) if self.result_event: self.result_event.set() @@ -71,7 +72,6 @@ def receive_message( def get_application(self): return self.transport.make_application() - @unittest_run_loop async def test_start_x(self): with async_mock.patch.object( test_module.web, "TCPSite", async_mock.MagicMock() @@ -82,7 +82,6 @@ async def test_start_x(self): with pytest.raises(test_module.InboundTransportSetupError): await self.transport.start() - @unittest_run_loop async def test_send_message(self): await self.transport.start() @@ -97,7 +96,6 @@ async def test_send_message(self): await self.transport.stop() - @unittest_run_loop async def test_send_receive_message(self): await self.transport.start() @@ -112,7 +110,6 @@ async def test_send_receive_message(self): await self.transport.stop() - @unittest_run_loop async def test_send_message_outliers(self): await self.transport.start() @@ -123,13 +120,15 @@ async def test_send_message_outliers(self): mock_session.return_value = async_mock.MagicMock( receive=async_mock.CoroutineMock( return_value=async_mock.MagicMock( - receipt=async_mock.MagicMock(direct_response_requested=True) + receipt=async_mock.MagicMock(direct_response_requested=True), + wait_processing_complete=async_mock.CoroutineMock(), ) ), can_respond=True, profile=InMemoryProfile.test_profile(), clear_response=async_mock.MagicMock(), wait_response=async_mock.CoroutineMock(return_value=b"Hello world"), + response_buffer="something", ) async with self.client.post("/", data=test_message) as resp: result = await resp.text() @@ -149,7 +148,6 @@ async def test_send_message_outliers(self): await self.transport.stop() - @unittest_run_loop async def test_invite_message_handler(self): await self.transport.start() diff --git a/aries_cloudagent/transport/inbound/tests/test_manager.py b/aries_cloudagent/transport/inbound/tests/test_manager.py index f5ab7eeaa3..ae13051416 100644 --- a/aries_cloudagent/transport/inbound/tests/test_manager.py +++ b/aries_cloudagent/transport/inbound/tests/test_manager.py @@ -83,7 +83,6 @@ async def test_create_session(self): test_accept = True test_can_respond = True test_client_info = {"client": "info"} - mgr.session_limit = asyncio.Semaphore(16) session = await mgr.create_session( test_transport, accept_undelivered=test_accept, diff --git a/aries_cloudagent/transport/inbound/tests/test_message.py b/aries_cloudagent/transport/inbound/tests/test_message.py new file mode 100644 index 0000000000..71a8defee8 --- /dev/null +++ b/aries_cloudagent/transport/inbound/tests/test_message.py @@ -0,0 +1,30 @@ +import asyncio + +from asynctest import TestCase + +from ..message import InboundMessage +from ..receipt import MessageReceipt + + +class TestInboundMessage(TestCase): + async def test_wait_response(self): + message = InboundMessage( + payload="test", + connection_id="conn_id", + receipt=MessageReceipt(), + session_id="session_id", + ) + assert not message.processing_complete_event.is_set() + message.dispatch_processing_complete() + assert message.processing_complete_event.is_set() + + message = InboundMessage( + payload="test", + connection_id="conn_id", + receipt=MessageReceipt(), + session_id="session_id", + ) + assert not message.processing_complete_event.is_set() + task = message.wait_processing_complete() + message.dispatch_processing_complete() + await asyncio.wait_for(task, 1) diff --git a/aries_cloudagent/transport/inbound/tests/test_ws_transport.py b/aries_cloudagent/transport/inbound/tests/test_ws_transport.py index 472d69f50c..0e47bbecde 100644 --- a/aries_cloudagent/transport/inbound/tests/test_ws_transport.py +++ b/aries_cloudagent/transport/inbound/tests/test_ws_transport.py @@ -67,7 +67,6 @@ def receive_message( if self.result_event: self.result_event.set() - @unittest_run_loop async def test_start_x(self): with async_mock.patch.object( test_module.web, "TCPSite", async_mock.MagicMock() @@ -78,7 +77,6 @@ async def test_start_x(self): with pytest.raises(test_module.InboundTransportSetupError): await self.transport.start() - @unittest_run_loop async def test_message_and_response(self): await self.transport.start() diff --git a/aries_cloudagent/transport/inbound/ws.py b/aries_cloudagent/transport/inbound/ws.py index 0b74a46990..0348146b9e 100644 --- a/aries_cloudagent/transport/inbound/ws.py +++ b/aries_cloudagent/transport/inbound/ws.py @@ -2,6 +2,7 @@ import asyncio import logging +from typing import Optional from aiohttp import WSMessage, WSMsgType, web @@ -30,10 +31,10 @@ def __init__(self, host: str, port: int, create_session, **kwargs) -> None: self.host = host self.port = port self.site: web.BaseSite = None - self.heartbeat_interval: int = self.root_profile.settings.get_int( + self.heartbeat_interval: Optional[int] = self.root_profile.settings.get_int( "transport.ws.heartbeat_interval" ) - self.timout_interval: int = self.root_profile.settings.get_int( + self.timout_interval: Optional[int] = self.root_profile.settings.get_int( "transport.ws.timout_interval" ) diff --git a/aries_cloudagent/transport/outbound/base.py b/aries_cloudagent/transport/outbound/base.py index 467ef69d55..2186b8bfc0 100644 --- a/aries_cloudagent/transport/outbound/base.py +++ b/aries_cloudagent/transport/outbound/base.py @@ -15,7 +15,9 @@ class BaseOutboundTransport(ABC): """Base outbound transport class.""" def __init__( - self, wire_format: BaseWireFormat = None, root_profile: Profile = None + self, + wire_format: BaseWireFormat = None, + root_profile: Profile = None, ) -> None: """Initialize a `BaseOutboundTransport` instance.""" self._collector = None @@ -32,6 +34,16 @@ def collector(self, coll: Collector): """Assign a new stats collector instance.""" self._collector = coll + @property + def wire_format(self) -> BaseWireFormat: + """Accessor for a custom wire format for the transport.""" + return self._wire_format + + @wire_format.setter + def wire_format(self, format: BaseWireFormat): + """Setter for a custom wire format for the transport.""" + self._wire_format = format + async def __aenter__(self): """Async context manager enter.""" await self.start() @@ -50,16 +62,6 @@ async def start(self): async def stop(self): """Shut down the transport.""" - @property - def wire_format(self) -> BaseWireFormat: - """Accessor for a custom wire format for the transport.""" - return self._wire_format - - @wire_format.setter - def wire_format(self, format: BaseWireFormat): - """Setter for a custom wire format for the transport.""" - self._wire_format = format - @abstractmethod async def handle_message( self, @@ -69,7 +71,7 @@ async def handle_message( metadata: dict = None, ): """ - Handle message from queue. + Handle message. Args: profile: the profile that produced the message diff --git a/aries_cloudagent/transport/outbound/http.py b/aries_cloudagent/transport/outbound/http.py index 363e59ee2a..63c1c52980 100644 --- a/aries_cloudagent/transport/outbound/http.py +++ b/aries_cloudagent/transport/outbound/http.py @@ -17,6 +17,7 @@ class HttpTransport(BaseOutboundTransport): """Http outbound transport class.""" schemes = ("http", "https") + is_external = False def __init__(self, **kwargs) -> None: """Initialize an `HttpTransport` instance.""" diff --git a/aries_cloudagent/transport/outbound/manager.py b/aries_cloudagent/transport/outbound/manager.py index 18fdf4b182..094f537f32 100644 --- a/aries_cloudagent/transport/outbound/manager.py +++ b/aries_cloudagent/transport/outbound/manager.py @@ -100,12 +100,12 @@ async def setup(self): for outbound_transport in outbound_transports: self.register(outbound_transport) - def register(self, module: str) -> str: + def register(self, module_name: str) -> str: """ Register a new outbound transport by module path. Args: - module: Module name to register + module_name: Module name to register Raises: OutboundTransportRegistrationError: If the imported class cannot @@ -117,13 +117,19 @@ def register(self, module: str) -> str: """ try: + if "." in module_name: + package, module = module_name.split(".", 1) + else: + package = MODULE_BASE_PATH + module = module_name + imported_class = ClassLoader.load_subclass_of( - BaseOutboundTransport, module, MODULE_BASE_PATH + BaseOutboundTransport, module, package ) - except (ModuleLoadError, ClassNotFoundError): + except (ModuleLoadError, ClassNotFoundError) as e: raise OutboundTransportRegistrationError( f"Outbound transport module {module} could not be resolved." - ) + ) from e return self.register_class(imported_class) @@ -214,6 +220,17 @@ def get_running_transport_for_scheme(self, scheme: str) -> str: except StopIteration: pass + def get_external_running_transport(self) -> str: + """Find the external running transport ID.""" + try: + return next( + transport_id + for transport_id, transport in self.running_transports.items() + if transport.is_external + ) + except StopIteration: + pass + def get_running_transport_for_endpoint(self, endpoint: str): """Find the running transport ID to use for a given endpoint.""" # Grab the scheme from the uri @@ -235,7 +252,7 @@ def get_transport_instance(self, transport_id: str) -> BaseOutboundTransport: """Get an instance of a running transport by ID.""" return self.running_transports[transport_id] - def enqueue_message(self, profile: Profile, outbound: OutboundMessage): + async def enqueue_message(self, profile: Profile, outbound: OutboundMessage): """ Add an outbound message to the queue. @@ -248,18 +265,28 @@ def enqueue_message(self, profile: Profile, outbound: OutboundMessage): for target in targets: endpoint = target.endpoint try: - transport_id = self.get_running_transport_for_endpoint(endpoint) + transport_id = self.get_external_running_transport() + if not transport_id: + transport_id = self.get_running_transport_for_endpoint(endpoint) except OutboundDeliveryError: pass if transport_id: break if not transport_id: raise OutboundDeliveryError("No supported transport for outbound message") - - queued = QueuedOutboundMessage(profile, outbound, target, transport_id) - queued.retries = self.MAX_RETRY_COUNT - self.outbound_new.append(queued) - self.process_queued() + transport = self.get_transport_instance(transport_id) + if transport.is_external: + encoded_outbound_message = await self.encode_outbound_message( + profile, outbound, target + ) + await transport.handle_message( + profile, encoded_outbound_message.payload, target.endpoint + ) + else: + queued = QueuedOutboundMessage(profile, outbound, target, transport_id) + queued.retries = self.MAX_RETRY_COUNT + self.outbound_new.append(queued) + self.process_queued() async def encode_outbound_message( self, profile: Profile, outbound: OutboundMessage, target: ConnectionTarget diff --git a/aries_cloudagent/transport/outbound/queue/base.py b/aries_cloudagent/transport/outbound/queue/base.py deleted file mode 100644 index 65153abbe3..0000000000 --- a/aries_cloudagent/transport/outbound/queue/base.py +++ /dev/null @@ -1,62 +0,0 @@ -"""Base classes for the queue module.""" -from abc import ABC, abstractmethod -import asyncio -import logging -from typing import Union - -from ....core.profile import Profile -from ...error import BaseError, TransportError - - -class BaseOutboundQueue(ABC): - """Base class for the outbound queue generic type.""" - - def __init__(self, root_profile: Profile): - """Initialize base queue type.""" - self.logger = logging.getLogger(__name__) - - def __str__(self): - """Return string representation used in banner on startup.""" - return type(self).__name__ - - async def __aenter__(self): - """Async context manager enter.""" - await self.open() - - async def __aexit__(self, err_type, err_value, err_t): - """Async context manager exit.""" - if err_type and err_type != asyncio.CancelledError: - self.logger.exception("Exception in outbound queue") - await self.close() - - async def start(self): - """Start the queue.""" - - async def stop(self): - """Stop the queue.""" - - async def open(self): - """Start the queue.""" - - async def close(self): - """Stop the queue.""" - - @abstractmethod - async def enqueue_message( - self, - payload: Union[str, bytes], - endpoint: str, - ): - """Prepare and send message to external queue.""" - - -class OutboundQueueError(TransportError): - """Generic outbound transport error.""" - - -class OutboundQueueConfigurationError(BaseError): - """An error with the queue configuration.""" - - def __init__(self, message): - """Initialize the exception instance.""" - super().__init__(message) diff --git a/aries_cloudagent/transport/outbound/queue/loader.py b/aries_cloudagent/transport/outbound/queue/loader.py deleted file mode 100644 index e184ad23ad..0000000000 --- a/aries_cloudagent/transport/outbound/queue/loader.py +++ /dev/null @@ -1,26 +0,0 @@ -"""Dynamic loading of pluggable outbound queue engine classes.""" -import logging -from typing import Optional, cast - -from ....core.profile import Profile -from ....utils.classloader import ClassLoader -from .base import BaseOutboundQueue, OutboundQueueConfigurationError - - -LOGGER = logging.getLogger(__name__) - - -def get_outbound_queue(root_profile: Profile) -> Optional[BaseOutboundQueue]: - """Given settings, return instantiated outbound queue class.""" - class_path = root_profile.settings.get("transport.outbound_queue") - if not class_path: - LOGGER.info("No outbound queue loaded") - return None - class_path = cast(str, class_path) - klass = ClassLoader.load_class(class_path) - instance = klass(root_profile) - if not isinstance(instance, BaseOutboundQueue): - raise OutboundQueueConfigurationError( - "Configured class is not a subclass of BaseOutboundQueue" - ) - return instance diff --git a/aries_cloudagent/transport/outbound/queue/redis.py b/aries_cloudagent/transport/outbound/queue/redis.py deleted file mode 100644 index 79f596ffca..0000000000 --- a/aries_cloudagent/transport/outbound/queue/redis.py +++ /dev/null @@ -1,91 +0,0 @@ -"""Redis outbound transport.""" - -from typing import Union - -import aioredis -import msgpack - -from ....core.profile import Profile -from .base import BaseOutboundQueue, OutboundQueueConfigurationError, OutboundQueueError - - -class RedisOutboundQueue(BaseOutboundQueue): - """Redis outbound transport class.""" - - config_key = "redis_queue" - - def __init__(self, root_profile: Profile) -> None: - """Set initial state.""" - try: - plugin_config = root_profile.settings["plugin_config"] or {} - config = plugin_config[self.config_key] - self.connection = config["connection"] - except KeyError as error: - raise OutboundQueueConfigurationError( - "Configuration missing for redis queue" - ) from error - - self.prefix = config.get("prefix", "acapy") - self.pool = aioredis.ConnectionPool.from_url( - self.connection, max_connections=10 - ) - self.redis = aioredis.Redis(connection_pool=self.pool) - - def __str__(self): - """Return string representation of the outbound queue.""" - return ( - f"RedisOutboundQueue(" - f"connection={self.connection}, " - f"prefix={self.prefix}" - f")" - ) - - async def start(self): - """Start the transport.""" - # aioredis will lazily connect but we can eagerly trigger connection with: - # await self.redis.ping() - # Calling this on enter to `async with` just before another queue - # operation is made does not make sense and we should just let aioredis - # do lazy connection. - - async def stop(self): - """Stop the transport.""" - # aioredis cleans up automatically but we can clean up manually with: - # await self.pool.disconnect() - # However, calling this on exit of `async with` does not make sense and - # we should just let aioredis handle the connection lifecycle. - - async def push(self, key: bytes, message: bytes): - """Push a ``message`` to redis on ``key``.""" - try: - return await self.redis.rpush(key, message) - except aioredis.RedisError as error: - raise OutboundQueueError("Unexpected redis client exception") from error - - async def enqueue_message( - self, - payload: Union[str, bytes], - endpoint: str, - ): - """Prepare and send message to external redis. - - Args: - payload: message payload in string or byte format - endpoint: URI endpoint for delivery - """ - if not endpoint: - raise OutboundQueueError("No endpoint provided") - if isinstance(payload, bytes): - content_type = "application/ssi-agent-wire" - else: - content_type = "application/json" - payload = payload.encode(encoding="utf-8") - message = msgpack.packb( - { - "headers": {"Content-Type": content_type}, - "endpoint": endpoint, - "payload": payload, - } - ) - key = f"{self.prefix}.outbound_transport".encode() - return await self.push(key, message) diff --git a/aries_cloudagent/transport/outbound/queue/tests/fixtures.py b/aries_cloudagent/transport/outbound/queue/tests/fixtures.py deleted file mode 100644 index cc92f7e890..0000000000 --- a/aries_cloudagent/transport/outbound/queue/tests/fixtures.py +++ /dev/null @@ -1,20 +0,0 @@ -from ..base import BaseOutboundQueue - - -class QueueClassNoBaseClass: - def __init__(self, settings): - pass - - -class QueueClassValid(BaseOutboundQueue): - async def enqueue_message(self, payload, endpoint): - pass - - async def push(self, key, message): - pass - - async def start(self): - pass - - async def stop(self): - pass diff --git a/aries_cloudagent/transport/outbound/queue/tests/test_loader.py b/aries_cloudagent/transport/outbound/queue/tests/test_loader.py deleted file mode 100644 index 32b3de7b72..0000000000 --- a/aries_cloudagent/transport/outbound/queue/tests/test_loader.py +++ /dev/null @@ -1,48 +0,0 @@ -import pytest - -from .....core.in_memory import InMemoryProfile -from .....utils.classloader import ClassNotFoundError -from ..base import OutboundQueueConfigurationError -from ..loader import get_outbound_queue -from .fixtures import QueueClassValid - - -@pytest.fixture -def profile(): - yield InMemoryProfile.test_profile( - settings={ - "transport.outbound_queue": "aries_cloudagent.transport.outbound.queue.tests.fixtures.QueueClassValid" - } - ) - - -def test_get_outbound_queue_valid(profile): - queue = get_outbound_queue(profile) - assert isinstance(queue, QueueClassValid) - - -@pytest.mark.parametrize( - "queue", - [ - None, - "", - ], -) -def test_get_outbound_not_set(queue, profile): - profile.settings["transport.outbound_queue"] = queue - assert get_outbound_queue(profile) is None - - -def test_get_outbound_x_no_class(profile): - profile.settings["transport.outbound_queue"] = "invalid queue class path" - with pytest.raises(ClassNotFoundError): - get_outbound_queue(profile) - - -def test_get_outbound_x_bad_instance(profile): - profile.settings["transport.outbound_queue"] = ( - "aries_cloudagent.transport.outbound.queue.tests.fixtures." - "QueueClassNoBaseClass" - ) - with pytest.raises(OutboundQueueConfigurationError): - get_outbound_queue(profile) diff --git a/aries_cloudagent/transport/outbound/queue/tests/test_redis.py b/aries_cloudagent/transport/outbound/queue/tests/test_redis.py deleted file mode 100644 index ebdb147957..0000000000 --- a/aries_cloudagent/transport/outbound/queue/tests/test_redis.py +++ /dev/null @@ -1,111 +0,0 @@ -import os -import string - -import aioredis -from asynctest import mock as async_mock -import msgpack -import pytest - -from .....config.settings import Settings -from .....core.in_memory.profile import InMemoryProfile -from ..base import OutboundQueueConfigurationError, OutboundQueueError -from ..redis import RedisOutboundQueue - - -ENDPOINT = "http://localhost:9000" -KEYNAME = "acapy.outbound_transport" - -REDIS_CONF = os.environ.get("TEST_REDIS_CONFIG", None) - - -@pytest.fixture -async def mock_redis(): - with async_mock.patch( - "aioredis.ConnectionPool.from_url", async_mock.MagicMock() - ), async_mock.patch("aioredis.Redis", async_mock.MagicMock()): - yield - - -@pytest.fixture -def profile(): - def _profile_factory(connection=None, prefix=None): - return InMemoryProfile.test_profile( - settings={ - "plugin_config": { - RedisOutboundQueue.config_key: { - "connection": connection or "connection", - "prefix": prefix or "acapy", - } - } - } - ) - - yield _profile_factory - - -@pytest.fixture -def queue(profile, mock_redis): - yield RedisOutboundQueue(profile()) - - -@pytest.fixture -def mock_rpush(queue): - pushed = [] - - async def _mock_rpush(key, message): - pushed.append((key, message)) - - queue.redis.rpush = _mock_rpush - yield pushed - - -def test_init(mock_redis, profile): - q = RedisOutboundQueue(profile()) - q.prefix == "acapy" - q.connection = "connection" - assert str(q) - - -def test_init_x(mock_redis): - with pytest.raises(OutboundQueueConfigurationError): - RedisOutboundQueue(InMemoryProfile.test_profile()) - - -@pytest.mark.asyncio -async def test_enqueue_message_str(queue, mock_rpush): - await queue.enqueue_message( - payload=string.ascii_letters + string.digits, - endpoint=ENDPOINT, - ) - [(key, message)] = mock_rpush - assert ( - msgpack.unpackb(message).get("headers", {}).get("Content-Type") - == "application/json" - ) - - -@pytest.mark.asyncio -async def test_enqueue_message_bytes(queue, mock_rpush): - await queue.enqueue_message( - payload=bytes(range(0, 256)), - endpoint=ENDPOINT, - ) - [(key, message)] = mock_rpush - assert ( - msgpack.unpackb(message).get("headers", {}).get("Content-Type") - == "application/ssi-agent-wire" - ) - - -@pytest.mark.asyncio -async def test_enqueue_message_x_redis_error(queue): - queue.redis.rpush = async_mock.CoroutineMock(side_effect=aioredis.RedisError) - with pytest.raises(OutboundQueueError): - await queue.enqueue_message(payload="", endpoint=ENDPOINT) - - -@pytest.mark.asyncio -async def test_enqueue_message_x_no_endpoint(queue): - queue.redis.rpush = async_mock.CoroutineMock(side_effect=aioredis.RedisError) - with pytest.raises(OutboundQueueError): - await queue.enqueue_message(payload="", endpoint=None) diff --git a/aries_cloudagent/transport/outbound/status.py b/aries_cloudagent/transport/outbound/status.py index 42576a52ec..e1fba403ec 100644 --- a/aries_cloudagent/transport/outbound/status.py +++ b/aries_cloudagent/transport/outbound/status.py @@ -2,6 +2,8 @@ from enum import Enum +OUTBOUND_STATUS_PREFIX = "acapy::outbound-message::" + class OutboundSendStatus(Enum): """Send status of outbound messages.""" @@ -21,3 +23,8 @@ class OutboundSendStatus(Enum): # No endpoint available, and no internal queue for messages. UNDELIVERABLE = "undeliverable" + + @property + def topic(self): + """Return an event topic associated with a given status.""" + return f"{OUTBOUND_STATUS_PREFIX}{self.value}" diff --git a/aries_cloudagent/transport/outbound/tests/test_http_transport.py b/aries_cloudagent/transport/outbound/tests/test_http_transport.py index a8cc6bf344..0954f2b9d7 100644 --- a/aries_cloudagent/transport/outbound/tests/test_http_transport.py +++ b/aries_cloudagent/transport/outbound/tests/test_http_transport.py @@ -19,6 +19,7 @@ async def setUpAsync(self): self.profile = InMemoryProfile.test_profile() self.message_results = [] self.headers = {} + await super().setUpAsync() async def receive_message(self, request): payload = await request.json() @@ -34,7 +35,6 @@ async def get_application(self): app.add_routes([web.post("/", self.receive_message)]) return app - @unittest_run_loop async def test_handle_message_no_api_key(self): server_addr = f"http://localhost:{self.server.port}" @@ -49,7 +49,6 @@ async def send_message(transport, payload, endpoint): assert self.headers.get("x-api-key") is None assert self.headers.get("content-type") == "application/json" - @unittest_run_loop async def test_handle_message_api_key(self): server_addr = f"http://localhost:{self.server.port}" api_key = "test1234" @@ -68,7 +67,6 @@ async def send_message(transport, payload, endpoint, api_key): assert self.message_results == [{}] assert self.headers.get("x-api-key") == api_key - @unittest_run_loop async def test_handle_message_packed_compat_mime_type(self): server_addr = f"http://localhost:{self.server.port}" @@ -84,7 +82,6 @@ async def send_message(transport, payload, endpoint): assert self.message_results == [{}] assert self.headers.get("content-type") == "application/ssi-agent-wire" - @unittest_run_loop async def test_handle_message_packed_standard_mime_type(self): server_addr = f"http://localhost:{self.server.port}" @@ -101,7 +98,6 @@ async def send_message(transport, payload, endpoint): assert self.message_results == [{}] assert self.headers.get("content-type") == "application/didcomm-envelope-enc" - @unittest_run_loop async def test_stats(self): server_addr = f"http://localhost:{self.server.port}" @@ -122,7 +118,6 @@ async def send_message(transport, payload, endpoint): "outbound-http:POST": 1, } - @unittest_run_loop async def test_transport_coverage(self): transport = HttpTransport() assert transport.wire_format is None diff --git a/aries_cloudagent/transport/outbound/tests/test_manager.py b/aries_cloudagent/transport/outbound/tests/test_manager.py index 834184b208..a2e958f2cf 100644 --- a/aries_cloudagent/transport/outbound/tests/test_manager.py +++ b/aries_cloudagent/transport/outbound/tests/test_manager.py @@ -59,6 +59,7 @@ async def test_send_message(self): transport.start = async_mock.CoroutineMock() transport.stop = async_mock.CoroutineMock() transport.schemes = ["http"] + transport.is_external = False transport_cls = async_mock.MagicMock() transport_cls.schemes = ["http"] @@ -85,7 +86,7 @@ async def test_send_message(self): setattr( send_profile, "session", async_mock.MagicMock(return_value=send_session) ) - mgr.enqueue_message(send_profile, message) + await mgr.enqueue_message(send_profile, message) await mgr.flush() transport.wire_format.encode_message.assert_awaited_once_with( @@ -113,7 +114,7 @@ async def test_send_message(self): sender_key=4, ) with self.assertRaises(OutboundDeliveryError) as context: - mgr.enqueue_message(send_profile, message) + await mgr.enqueue_message(send_profile, message) assert "No supported transport" in str(context.exception) await mgr.stop() diff --git a/aries_cloudagent/transport/outbound/tests/test_ws_transport.py b/aries_cloudagent/transport/outbound/tests/test_ws_transport.py index 061f8f37df..12b2a98a75 100644 --- a/aries_cloudagent/transport/outbound/tests/test_ws_transport.py +++ b/aries_cloudagent/transport/outbound/tests/test_ws_transport.py @@ -13,6 +13,7 @@ class TestWsTransport(AioHTTPTestCase): async def setUpAsync(self): self.profile = InMemoryProfile.test_profile() self.message_results = [] + await super().setUpAsync() async def receive_message(self, request): ws = web.WebSocketResponse() @@ -35,7 +36,6 @@ async def get_application(self): app.add_routes([web.get("/", self.receive_message)]) return app - @unittest_run_loop async def test_handle_message(self): server_addr = f"ws://localhost:{self.server.port}" diff --git a/aries_cloudagent/transport/outbound/ws.py b/aries_cloudagent/transport/outbound/ws.py index 4ffcfdac08..6ec531c494 100644 --- a/aries_cloudagent/transport/outbound/ws.py +++ b/aries_cloudagent/transport/outbound/ws.py @@ -14,6 +14,7 @@ class WsTransport(BaseOutboundTransport): """Websockets outbound transport class.""" schemes = ("ws", "wss") + is_external = False def __init__(self, **kwargs) -> None: """Initialize an `WsTransport` instance.""" diff --git a/aries_cloudagent/transport/pack_format.py b/aries_cloudagent/transport/pack_format.py index 2423e47a40..76ac70b87f 100644 --- a/aries_cloudagent/transport/pack_format.py +++ b/aries_cloudagent/transport/pack_format.py @@ -69,7 +69,6 @@ async def parse_message( # packed messages are detected by the absence of @type if "@type" not in message_dict: - try: unpack = self.unpack(session, message_body, receipt) message_json = await ( @@ -91,6 +90,7 @@ async def parse_message( receipt.thread_id = ( thread_dec and thread_dec.get("thid") or message_dict.get("@id") ) + receipt.parent_thread_id = thread_dec.get("pthid") if thread_dec else None # handle transport decorator transport_dec = message_dict.get("~transport") diff --git a/aries_cloudagent/transport/tests/test_pack_format.py b/aries_cloudagent/transport/tests/test_pack_format.py index 1ca7b7f46f..764e4a54bf 100644 --- a/aries_cloudagent/transport/tests/test_pack_format.py +++ b/aries_cloudagent/transport/tests/test_pack_format.py @@ -1,19 +1,18 @@ import json - from base64 import b64encode -from asynctest import TestCase as AsyncTestCase, mock as async_mock +from asynctest import TestCase as AsyncTestCase +from asynctest import mock as async_mock from ...core.in_memory import InMemoryProfile -from ...protocols.routing.v1_0.message_types import FORWARD from ...protocols.didcomm_prefix import DIDCommPrefix +from ...protocols.routing.v1_0.message_types import FORWARD from ...wallet.base import BaseWallet +from ...wallet.did_method import SOV, DIDMethods from ...wallet.error import WalletError -from ...wallet.key_type import KeyType -from ...wallet.did_method import DIDMethod - +from ...wallet.key_type import ED25519 from .. import pack_format as test_module -from ..error import WireFormatEncodeError, WireFormatParseError, RecipientKeysError +from ..error import RecipientKeysError, WireFormatEncodeError, WireFormatParseError from ..pack_format import PackWireFormat @@ -34,6 +33,7 @@ class TestPackWireFormat(AsyncTestCase): def setUp(self): self.session = InMemoryProfile.test_session() + self.session.profile.context.injector.bind_instance(DIDMethods, DIDMethods()) self.wallet = self.session.inject(BaseWallet) async def test_errors(self): @@ -140,7 +140,7 @@ async def test_fallback(self): async def test_encode_decode(self): local_did = await self.wallet.create_local_did( - method=DIDMethod.SOV, key_type=KeyType.ED25519, seed=self.test_seed + method=SOV, key_type=ED25519, seed=self.test_seed ) serializer = PackWireFormat() recipient_keys = (local_did.verkey,) @@ -174,10 +174,10 @@ async def test_encode_decode(self): async def test_forward(self): local_did = await self.wallet.create_local_did( - method=DIDMethod.SOV, key_type=KeyType.ED25519, seed=self.test_seed + method=SOV, key_type=ED25519, seed=self.test_seed ) router_did = await self.wallet.create_local_did( - method=DIDMethod.SOV, key_type=KeyType.ED25519, seed=self.test_routing_seed + method=SOV, key_type=ED25519, seed=self.test_routing_seed ) serializer = PackWireFormat() recipient_keys = (local_did.verkey,) diff --git a/aries_cloudagent/transport/wire_format.py b/aries_cloudagent/transport/wire_format.py index d03a8eb40a..f70521dd4a 100644 --- a/aries_cloudagent/transport/wire_format.py +++ b/aries_cloudagent/transport/wire_format.py @@ -134,6 +134,7 @@ async def parse_message( receipt.thread_id = ( thread_dec and thread_dec.get("thid") or message_dict.get("@id") ) + receipt.parent_thread_id = thread_dec.get("pthid") if thread_dec else None # handle transport decorator transport_dec = message_dict.get("~transport") diff --git a/aries_cloudagent/utils/classloader.py b/aries_cloudagent/utils/classloader.py index 7c429b0ea9..2b4de2a207 100644 --- a/aries_cloudagent/utils/classloader.py +++ b/aries_cloudagent/utils/classloader.py @@ -7,7 +7,7 @@ from importlib import import_module from importlib.util import find_spec, resolve_name from types import ModuleType -from typing import Sequence, Type +from typing import Optional, Sequence, Type from ..core.error import BaseError @@ -75,7 +75,10 @@ def load_module(cls, mod_path: str, package: str = None) -> ModuleType: @classmethod def load_class( - cls, class_name: str, default_module: str = None, package: str = None + cls, + class_name: str, + default_module: Optional[str] = None, + package: Optional[str] = None, ): """ Resolve a complete class path (ie. typing.Dict) to the class itself. diff --git a/aries_cloudagent/utils/http.py b/aries_cloudagent/utils/http.py index 8e780338c3..d17fe234f2 100644 --- a/aries_cloudagent/utils/http.py +++ b/aries_cloudagent/utils/http.py @@ -1,8 +1,16 @@ """HTTP utility methods.""" import asyncio - -from aiohttp import BaseConnector, ClientError, ClientResponse, ClientSession +import logging +import urllib.parse + +from aiohttp import ( + BaseConnector, + ClientError, + ClientResponse, + ClientSession, + FormData, +) from aiohttp.web import HTTPConflict from ..core.error import BaseError @@ -10,6 +18,9 @@ from .repeat import RepeatSequence +LOGGER = logging.getLogger(__name__) + + class FetchError(BaseError): """Error raised when an HTTP fetch fails.""" @@ -147,7 +158,6 @@ async def put_file( """ (data_key, file_path) = [k for k in file_data.items()][0] - data = {**extra_data} limit = max_attempts if retry else 1 if not session: @@ -158,17 +168,51 @@ async def put_file( async for attempt in RepeatSequence(limit, interval, backoff): try: async with attempt.timeout(request_timeout): - with open(file_path, "rb") as f: - data[data_key] = f - response: ClientResponse = await session.put(url, data=data) - if (response.status < 200 or response.status >= 300) and ( - response.status != HTTPConflict.status_code - ): - raise ClientError( - f"Bad response from server: {response.status}, " - f"{response.reason}" - ) + formdata = FormData() + try: + fp = open(file_path, "rb") + except OSError as e: + raise PutError("Error opening file for upload") from e + if extra_data: + for k, v in extra_data.items(): + formdata.add_field(k, v) + formdata.add_field( + data_key, fp, content_type="application/octet-stream" + ) + response: ClientResponse = await session.put( + url, data=formdata, allow_redirects=False + ) + if ( + # redirect codes + response.status in (301, 302, 303, 307, 308) + and not attempt.final + ): + # NOTE: a redirect counts as another upload attempt + to_url = response.headers.get("Location") + if not to_url: + raise PutError("Redirect missing target URL") + try: + parsed_to = urllib.parse.urlsplit(to_url) + parsed_from = urllib.parse.urlsplit(url) + except ValueError: + raise PutError("Invalid redirect URL") + if parsed_to.hostname != parsed_from.hostname: + raise PutError("Redirect denied: hostname mismatch") + url = to_url + LOGGER.info("Upload redirect: %s", to_url) + elif (response.status < 200 or response.status >= 300) and ( + response.status != HTTPConflict.status_code + ): + raise ClientError( + f"Bad response from server: {response.status}, " + f"{response.reason}" + ) + else: return await (response.json() if json else response.text()) except (ClientError, asyncio.TimeoutError) as e: + if isinstance(e, ClientError): + LOGGER.warning("Upload error: %s", e) + else: + LOGGER.warning("Upload error: request timed out") if attempt.final: - raise PutError("Exceeded maximum put attempts") from e + raise PutError("Exceeded maximum upload attempts") from e diff --git a/aries_cloudagent/utils/tests/test_http.py b/aries_cloudagent/utils/tests/test_http.py index 5a2886723b..6e2ff84adb 100644 --- a/aries_cloudagent/utils/tests/test_http.py +++ b/aries_cloudagent/utils/tests/test_http.py @@ -1,14 +1,34 @@ +import os +import tempfile + from aiohttp import web -from aiohttp.test_utils import AioHTTPTestCase, unittest_run_loop -from asynctest import mock as async_mock, mock_open +from aiohttp.test_utils import AioHTTPTestCase from ..http import fetch, fetch_stream, FetchError, put_file, PutError +class TempFile: + def __init__(self): + self.name = None + + def __enter__(self): + file = tempfile.NamedTemporaryFile(delete=False) + file.write(b"test") + file.close() + self.name = file.name + return self.name + + def __exit__(self, *args): + if self.name: + os.unlink(self.name) + + class TestTransportUtils(AioHTTPTestCase): async def setUpAsync(self): self.fail_calls = 0 self.succeed_calls = 0 + self.redirects = 0 + await super().setUpAsync() async def get_application(self): app = web.Application() @@ -18,12 +38,15 @@ async def get_application(self): web.get("/succeed", self.succeed_route), web.put("/fail", self.fail_route), web.put("/succeed", self.succeed_route), + web.put("/redirect", self.redirect_route), ] ) return app async def fail_route(self, request): self.fail_calls += 1 + # avoid aiohttp test server issue: https://github.com/aio-libs/aiohttp/issues/3968 + await request.read() raise web.HTTPForbidden() async def succeed_route(self, request): @@ -31,7 +54,14 @@ async def succeed_route(self, request): ret = web.json_response([True]) return ret - @unittest_run_loop + async def redirect_route(self, request): + if self.redirects > 0: + self.redirects -= 1 + # avoid aiohttp test server issue: https://github.com/aio-libs/aiohttp/issues/3968 + await request.read() + raise web.HTTPRedirection(f"http://localhost:{self.server.port}/success") + return await self.succeed_route(request) + async def test_fetch_stream(self): server_addr = f"http://localhost:{self.server.port}" stream = await fetch_stream( @@ -41,7 +71,6 @@ async def test_fetch_stream(self): assert result == b"[true]" assert self.succeed_calls == 1 - @unittest_run_loop async def test_fetch_stream_default_client(self): server_addr = f"http://localhost:{self.server.port}" stream = await fetch_stream(f"{server_addr}/succeed") @@ -49,7 +78,6 @@ async def test_fetch_stream_default_client(self): assert result == b"[true]" assert self.succeed_calls == 1 - @unittest_run_loop async def test_fetch_stream_fail(self): server_addr = f"http://localhost:{self.server.port}" with self.assertRaises(FetchError): @@ -61,7 +89,6 @@ async def test_fetch_stream_fail(self): ) assert self.fail_calls == 2 - @unittest_run_loop async def test_fetch(self): server_addr = f"http://localhost:{self.server.port}" result = await fetch( @@ -70,14 +97,12 @@ async def test_fetch(self): assert result == [1] assert self.succeed_calls == 1 - @unittest_run_loop async def test_fetch_default_client(self): server_addr = f"http://localhost:{self.server.port}" result = await fetch(f"{server_addr}/succeed", json=True) assert result == [1] assert self.succeed_calls == 1 - @unittest_run_loop async def test_fetch_fail(self): server_addr = f"http://localhost:{self.server.port}" with self.assertRaises(FetchError): @@ -89,43 +114,55 @@ async def test_fetch_fail(self): ) assert self.fail_calls == 2 - @unittest_run_loop - async def test_put_file(self): + async def test_put_file_with_session(self): server_addr = f"http://localhost:{self.server.port}" - with async_mock.patch("builtins.open", mock_open(read_data="data")): + with TempFile() as tails: result = await put_file( f"{server_addr}/succeed", - {"tails": "/tmp/dummy/path"}, + {"tails": tails}, {"genesis": "..."}, session=self.client.session, json=True, ) - assert result == [1] + assert result == [True] assert self.succeed_calls == 1 - @unittest_run_loop async def test_put_file_default_client(self): server_addr = f"http://localhost:{self.server.port}" - with async_mock.patch("builtins.open", mock_open(read_data="data")): + with TempFile() as tails: result = await put_file( f"{server_addr}/succeed", - {"tails": "/tmp/dummy/path"}, + {"tails": tails}, {"genesis": "..."}, json=True, ) - assert result == [1] + assert result == [True] assert self.succeed_calls == 1 - @unittest_run_loop async def test_put_file_fail(self): server_addr = f"http://localhost:{self.server.port}" - with async_mock.patch("builtins.open", mock_open(read_data="data")): + with TempFile() as tails: with self.assertRaises(PutError): - result = await put_file( + _ = await put_file( f"{server_addr}/fail", - {"tails": "/tmp/dummy/path"}, + {"tails": tails}, {"genesis": "..."}, max_attempts=2, json=True, ) assert self.fail_calls == 2 + + async def test_put_file_redirect(self): + server_addr = f"http://localhost:{self.server.port}" + self.redirects = 1 + with TempFile() as tails: + result = await put_file( + f"{server_addr}/redirect", + {"tails": tails}, + {"genesis": "..."}, + max_attempts=2, + json=True, + ) + assert result == [True] + assert self.succeed_calls == 1 + assert self.redirects == 0 diff --git a/aries_cloudagent/utils/tests/test_outofband.py b/aries_cloudagent/utils/tests/test_outofband.py index 22c4c059ee..6e3821ff57 100644 --- a/aries_cloudagent/utils/tests/test_outofband.py +++ b/aries_cloudagent/utils/tests/test_outofband.py @@ -1,23 +1,20 @@ from asynctest import TestCase from ...protocols.out_of_band.v1_0.messages.invitation import InvitationMessage -from ...wallet.key_type import KeyType -from ...wallet.did_method import DIDMethod from ...wallet.did_info import DIDInfo - +from ...wallet.did_method import SOV +from ...wallet.key_type import ED25519 from .. import outofband as test_module class TestOutOfBand(TestCase): test_did = "55GkHamhTU1ZbTbV2ab9DE" test_verkey = "3Dn1SJNPaCXcvvJvSbsFWP2xaCjMom3can8CQNhWrTRx" - test_did_info = DIDInfo( - test_did, test_verkey, None, method=DIDMethod.SOV, key_type=KeyType.ED25519 - ) + test_did_info = DIDInfo(test_did, test_verkey, None, method=SOV, key_type=ED25519) def test_serialize_oob(self): invi = InvitationMessage( - comment="my sister", label=u"ma sœur", services=[TestOutOfBand.test_did] + comment="my sister", label="ma sœur", services=[TestOutOfBand.test_did] ) result = test_module.serialize_outofband( diff --git a/aries_cloudagent/utils/tests/test_repeat.py b/aries_cloudagent/utils/tests/test_repeat.py index 63ac160867..0056334a8a 100644 --- a/aries_cloudagent/utils/tests/test_repeat.py +++ b/aries_cloudagent/utils/tests/test_repeat.py @@ -4,7 +4,7 @@ class TestRepeat(TestCase): - def test_iter(self): + async def test_iter(self): expect = [5, 7, 11, 17, 25] seq = test_module.RepeatSequence(5, interval=5.0, backoff=0.25) assert [round(attempt.next_interval) for attempt in seq] == expect @@ -12,9 +12,9 @@ def test_iter(self): seq = test_module.RepeatSequence(2, interval=5.0, backoff=0.25) attempt = seq.start() attempt = attempt.next() - attempt.timeout(interval=0.01) - with self.assertRaises(StopIteration): - attempt.next() + async with attempt.timeout(interval=0.01): + with self.assertRaises(StopIteration): + attempt.next() async def test_aiter(self): seq = test_module.RepeatSequence(5, interval=5.0, backoff=0.25) diff --git a/aries_cloudagent/vc/ld_proofs/crypto/tests/test_wallet_key_pair.py b/aries_cloudagent/vc/ld_proofs/crypto/tests/test_wallet_key_pair.py index 705ca244be..6c82afad01 100644 --- a/aries_cloudagent/vc/ld_proofs/crypto/tests/test_wallet_key_pair.py +++ b/aries_cloudagent/vc/ld_proofs/crypto/tests/test_wallet_key_pair.py @@ -1,5 +1,7 @@ from asynctest import TestCase, mock as async_mock +from aries_cloudagent.wallet.key_type import ED25519 + from .....wallet.key_pair import KeyType from ...error import LinkedDataProofException @@ -12,7 +14,7 @@ async def setUp(self): self.wallet = async_mock.MagicMock() async def test_sign_x_no_public_key(self): - key_pair = WalletKeyPair(wallet=self.wallet, key_type=KeyType.ED25519) + key_pair = WalletKeyPair(wallet=self.wallet, key_type=ED25519) with self.assertRaises(LinkedDataProofException) as context: await key_pair.sign(b"Message") @@ -22,7 +24,7 @@ async def test_sign(self): public_key_base58 = "verkey" key_pair = WalletKeyPair( wallet=self.wallet, - key_type=KeyType.ED25519, + key_type=ED25519, public_key_base58=public_key_base58, ) signed = async_mock.MagicMock() @@ -37,7 +39,7 @@ async def test_sign(self): ) async def test_verify_x_no_public_key(self): - key_pair = WalletKeyPair(wallet=self.wallet, key_type=KeyType.ED25519) + key_pair = WalletKeyPair(wallet=self.wallet, key_type=ED25519) with self.assertRaises(LinkedDataProofException) as context: await key_pair.verify(b"Message", b"signature") @@ -47,7 +49,7 @@ async def test_verify(self): public_key_base58 = "verkey" key_pair = WalletKeyPair( wallet=self.wallet, - key_type=KeyType.ED25519, + key_type=ED25519, public_key_base58=public_key_base58, ) self.wallet.verify_message = async_mock.CoroutineMock(return_value=True) @@ -59,11 +61,11 @@ async def test_verify(self): message=b"Message", signature=b"signature", from_verkey=public_key_base58, - key_type=KeyType.ED25519, + key_type=ED25519, ) async def test_from_verification_method_x_no_public_key_base58(self): - key_pair = WalletKeyPair(wallet=self.wallet, key_type=KeyType.ED25519) + key_pair = WalletKeyPair(wallet=self.wallet, key_type=ED25519) with self.assertRaises(LinkedDataProofException) as context: key_pair.from_verification_method({}) diff --git a/aries_cloudagent/vc/ld_proofs/document_loader.py b/aries_cloudagent/vc/ld_proofs/document_loader.py index 2f4eab5e4e..3c35d7d985 100644 --- a/aries_cloudagent/vc/ld_proofs/document_loader.py +++ b/aries_cloudagent/vc/ld_proofs/document_loader.py @@ -14,6 +14,10 @@ from .error import LinkedDataProofException +import nest_asyncio + +nest_asyncio.apply() + class DocumentLoader: """JSON-LD document loader.""" @@ -32,6 +36,7 @@ def __init__(self, profile: Profile, cache_ttl: int = 300) -> None: self.requests_loader = requests.requests_document_loader() self.executor = concurrent.futures.ThreadPoolExecutor(max_workers=1) self.cache_ttl = cache_ttl + self._event_loop = asyncio.get_event_loop() async def _load_did_document(self, did: str, options: dict): # Resolver expects plain did without path, query, etc... @@ -59,14 +64,6 @@ def _load_http_document(self, url: str, options: dict): async def _load_async(self, url: str, options: dict): """Retrieve http(s) or did document.""" - cache_key = f"json_ld_document_resolver::{url}" - - # Try to get from cache - if self.cache: - document = await self.cache.get(cache_key) - if document: - return document - # Resolve DIDs using did resolver if url.startswith("did:"): document = await self._load_did_document(url, options) @@ -78,22 +75,9 @@ async def _load_async(self, url: str, options: dict): "'did:', 'http://' or 'https://'" ) - # Cache document, if cache is available - if self.cache: - await self.cache.set(cache_key, document, self.cache_ttl) - return document - def _load_sync(self, url: str, options: dict): - """Run document loader in event loop to make it async. - - NOTE: This should be called in a thread where an event loop is not already - running, such as a new thread. - """ - loop = asyncio.new_event_loop() - return loop.run_until_complete(self._load_async(url, options)) - - def load_document(self, url: str, options: dict): + async def load_document(self, url: str, options: dict): """Load JSON-LD document. Method signature conforms to PyLD document loader interface @@ -101,13 +85,30 @@ def load_document(self, url: str, options: dict): Document loading is processed in separate thread to deal with async to sync transformation. """ - future = self.executor.submit(self._load_sync, url, options) - return future.result() + cache_key = f"json_ld_document_resolver::{url}" + + # Try to get from cache + if self.cache: + document = await self.cache.get(cache_key) + if document: + return document + + document = await self._load_async(url, options) + + # Cache document, if cache is available + if self.cache: + await self.cache.set(cache_key, document, self.cache_ttl) + + return document def __call__(self, url: str, options: dict): """Load JSON-LD Document.""" - return self.load_document(url, options) + loop = self._event_loop + coroutine = self.load_document(url, options) + document = loop.run_until_complete(coroutine) + + return document DocumentLoaderMethod = Callable[[str, dict], dict] diff --git a/aries_cloudagent/vc/ld_proofs/suites/tests/test_bbs_bls_signature_2020.py b/aries_cloudagent/vc/ld_proofs/suites/tests/test_bbs_bls_signature_2020.py index f8bfdf6533..bb3bc4d523 100644 --- a/aries_cloudagent/vc/ld_proofs/suites/tests/test_bbs_bls_signature_2020.py +++ b/aries_cloudagent/vc/ld_proofs/suites/tests/test_bbs_bls_signature_2020.py @@ -1,6 +1,8 @@ from asynctest import TestCase, mock as async_mock import pytest +from aries_cloudagent.wallet.key_type import BLS12381G2 + from .....did.did_key import DIDKey from .....wallet.key_pair import KeyType from .....wallet.in_memory import InMemoryWallet @@ -30,20 +32,18 @@ async def setUp(self): self.profile = InMemoryProfile.test_profile() self.wallet = InMemoryWallet(self.profile) self.key = await self.wallet.create_signing_key( - key_type=KeyType.BLS12381G2, seed=self.test_seed + key_type=BLS12381G2, seed=self.test_seed ) self.verification_method = DIDKey.from_public_key_b58( - self.key.verkey, KeyType.BLS12381G2 + self.key.verkey, BLS12381G2 ).key_id self.sign_key_pair = WalletKeyPair( wallet=self.wallet, - key_type=KeyType.BLS12381G2, + key_type=BLS12381G2, public_key_base58=self.key.verkey, ) - self.verify_key_pair = WalletKeyPair( - wallet=self.wallet, key_type=KeyType.BLS12381G2 - ) + self.verify_key_pair = WalletKeyPair(wallet=self.wallet, key_type=BLS12381G2) async def test_sign_ld_proofs(self): signed = await sign( diff --git a/aries_cloudagent/vc/ld_proofs/suites/tests/test_bbs_bls_signature_proof_2020.py b/aries_cloudagent/vc/ld_proofs/suites/tests/test_bbs_bls_signature_proof_2020.py index e8a79e2297..67d027d770 100644 --- a/aries_cloudagent/vc/ld_proofs/suites/tests/test_bbs_bls_signature_proof_2020.py +++ b/aries_cloudagent/vc/ld_proofs/suites/tests/test_bbs_bls_signature_proof_2020.py @@ -1,6 +1,8 @@ from asynctest import TestCase, mock as async_mock import pytest +from aries_cloudagent.wallet.key_type import BLS12381G2 + from .....did.did_key import DIDKey from .....wallet.key_pair import KeyType from .....wallet.in_memory import InMemoryWallet @@ -39,13 +41,13 @@ async def setUp(self): self.profile = InMemoryProfile.test_profile() self.wallet = InMemoryWallet(self.profile) self.key = await self.wallet.create_signing_key( - key_type=KeyType.BLS12381G2, seed=self.test_seed + key_type=BLS12381G2, seed=self.test_seed ) self.verification_method = DIDKey.from_public_key_b58( - self.key.verkey, KeyType.BLS12381G2 + self.key.verkey, BLS12381G2 ).key_id - self.key_pair = WalletKeyPair(wallet=self.wallet, key_type=KeyType.BLS12381G2) + self.key_pair = WalletKeyPair(wallet=self.wallet, key_type=BLS12381G2) async def test_derive_ld_proofs(self): derived = await derive( diff --git a/aries_cloudagent/vc/ld_proofs/suites/tests/test_ed25519_signature_2018.py b/aries_cloudagent/vc/ld_proofs/suites/tests/test_ed25519_signature_2018.py index 613aec46ab..60bf0389f6 100644 --- a/aries_cloudagent/vc/ld_proofs/suites/tests/test_ed25519_signature_2018.py +++ b/aries_cloudagent/vc/ld_proofs/suites/tests/test_ed25519_signature_2018.py @@ -1,5 +1,7 @@ from asynctest import TestCase +from aries_cloudagent.wallet.key_type import ED25519 + from .....did.did_key import DIDKey from .....wallet.key_pair import KeyType @@ -29,20 +31,18 @@ async def setUp(self): self.profile = InMemoryProfile.test_profile() self.wallet = InMemoryWallet(self.profile) self.key = await self.wallet.create_signing_key( - key_type=KeyType.ED25519, seed=self.test_seed + key_type=ED25519, seed=self.test_seed ) self.verification_method = DIDKey.from_public_key_b58( - self.key.verkey, KeyType.ED25519 + self.key.verkey, ED25519 ).key_id self.sign_key_pair = WalletKeyPair( wallet=self.wallet, - key_type=KeyType.ED25519, + key_type=ED25519, public_key_base58=self.key.verkey, ) - self.verify_key_pair = WalletKeyPair( - wallet=self.wallet, key_type=KeyType.ED25519 - ) + self.verify_key_pair = WalletKeyPair(wallet=self.wallet, key_type=ED25519) async def test_sign_ld_proofs(self): signed = await sign( diff --git a/aries_cloudagent/vc/ld_proofs/tests/test_ld_proofs.py b/aries_cloudagent/vc/ld_proofs/tests/test_ld_proofs.py index 15d9ea5e90..0143cfe17f 100644 --- a/aries_cloudagent/vc/ld_proofs/tests/test_ld_proofs.py +++ b/aries_cloudagent/vc/ld_proofs/tests/test_ld_proofs.py @@ -4,7 +4,7 @@ from asynctest import TestCase -from ....wallet.key_type import KeyType +from ....wallet.key_type import BLS12381G2, ED25519 from ....did.did_key import DIDKey from ....wallet.in_memory import InMemoryWallet from ....core.in_memory import InMemoryProfile @@ -40,18 +40,18 @@ async def setUp(self): self.wallet = InMemoryWallet(self.profile) self.ed25519_key_info = await self.wallet.create_signing_key( - key_type=KeyType.ED25519, seed=self.test_seed + key_type=ED25519, seed=self.test_seed ) self.ed25519_verification_method = DIDKey.from_public_key_b58( - self.ed25519_key_info.verkey, KeyType.ED25519 + self.ed25519_key_info.verkey, ED25519 ).key_id self.bls12381g2_key_info = await self.wallet.create_signing_key( - key_type=KeyType.BLS12381G2, seed=self.test_seed + key_type=BLS12381G2, seed=self.test_seed ) self.bls12381g2_verification_method = DIDKey.from_public_key_b58( - self.bls12381g2_key_info.verkey, KeyType.BLS12381G2 + self.bls12381g2_key_info.verkey, BLS12381G2 ).key_id async def test_sign_Ed25519Signature2018(self): @@ -62,7 +62,7 @@ async def test_sign_Ed25519Signature2018(self): verification_method=self.ed25519_verification_method, key_pair=WalletKeyPair( wallet=self.wallet, - key_type=KeyType.ED25519, + key_type=ED25519, public_key_base58=self.ed25519_key_info.verkey, ), date=datetime(2019, 12, 11, 3, 50, 55, 0, timezone.utc), @@ -79,7 +79,7 @@ async def test_sign_Ed25519Signature2018(self): async def test_verify_Ed25519Signature2018(self): # Verification requires lot less input parameters suite = Ed25519Signature2018( - key_pair=WalletKeyPair(wallet=self.wallet, key_type=KeyType.ED25519), + key_pair=WalletKeyPair(wallet=self.wallet, key_type=ED25519), ) result = await verify( @@ -100,7 +100,7 @@ async def test_sign_BbsBlsSignature2020(self): verification_method=self.bls12381g2_verification_method, key_pair=WalletKeyPair( wallet=self.wallet, - key_type=KeyType.BLS12381G2, + key_type=BLS12381G2, public_key_base58=self.bls12381g2_key_info.verkey, ), date=datetime(2019, 12, 11, 3, 50, 55, 0), @@ -128,7 +128,7 @@ async def test_sign_BbsBlsSignature2020(self): async def test_verify_BbsBlsSignature2020(self): # Verification requires lot less input parameters suite = BbsBlsSignature2020( - key_pair=WalletKeyPair(wallet=self.wallet, key_type=KeyType.BLS12381G2), + key_pair=WalletKeyPair(wallet=self.wallet, key_type=BLS12381G2), ) result = await verify( @@ -144,7 +144,7 @@ async def test_verify_BbsBlsSignature2020(self): async def test_derive_BbsBlsSignatureProof2020(self): # Verification requires lot less input parameters suite = BbsBlsSignatureProof2020( - key_pair=WalletKeyPair(wallet=self.wallet, key_type=KeyType.BLS12381G2), + key_pair=WalletKeyPair(wallet=self.wallet, key_type=BLS12381G2), ) result = await derive( @@ -160,7 +160,7 @@ async def test_derive_BbsBlsSignatureProof2020(self): async def test_verify_BbsBlsSignatureProof2020(self): # Verification requires lot less input parameters suite = BbsBlsSignatureProof2020( - key_pair=WalletKeyPair(wallet=self.wallet, key_type=KeyType.BLS12381G2), + key_pair=WalletKeyPair(wallet=self.wallet, key_type=BLS12381G2), ) result = await verify( diff --git a/aries_cloudagent/vc/tests/test_bbs_mattr_interop.py b/aries_cloudagent/vc/tests/test_bbs_mattr_interop.py index 707dec1e6b..d9f4dde009 100644 --- a/aries_cloudagent/vc/tests/test_bbs_mattr_interop.py +++ b/aries_cloudagent/vc/tests/test_bbs_mattr_interop.py @@ -1,7 +1,7 @@ from asynctest import TestCase import pytest -from ...wallet.key_type import KeyType +from ...wallet.key_type import BLS12381G2 from ...wallet.util import b58_to_bytes from ...wallet.in_memory import InMemoryWallet from ...core.in_memory import InMemoryProfile @@ -45,23 +45,23 @@ async def setUp(self): "secret": b58_to_bytes(private_key_base58), "verkey": public_key_base58, "metadata": {}, - "key_type": KeyType.BLS12381G2, + "key_type": BLS12381G2, } self.signature_issuer_suite = BbsBlsSignature2020( verification_method="did:example:489398593#test", key_pair=WalletKeyPair( wallet=self.wallet, - key_type=KeyType.BLS12381G2, + key_type=BLS12381G2, public_key_base58=public_key_base58, ), ) self.signature_suite = BbsBlsSignature2020( - key_pair=WalletKeyPair(wallet=self.wallet, key_type=KeyType.BLS12381G2), + key_pair=WalletKeyPair(wallet=self.wallet, key_type=BLS12381G2), ) self.proof_suite = BbsBlsSignatureProof2020( - key_pair=WalletKeyPair(wallet=self.wallet, key_type=KeyType.BLS12381G2) + key_pair=WalletKeyPair(wallet=self.wallet, key_type=BLS12381G2) ) async def test_sign_bbs_vc_mattr(self): diff --git a/aries_cloudagent/vc/vc_ld/tests/test_vc_ld.py b/aries_cloudagent/vc/vc_ld/tests/test_vc_ld.py index 471220e09f..c09f9345dc 100644 --- a/aries_cloudagent/vc/vc_ld/tests/test_vc_ld.py +++ b/aries_cloudagent/vc/vc_ld/tests/test_vc_ld.py @@ -3,7 +3,7 @@ import pytest -from ....wallet.key_type import KeyType +from ....wallet.key_type import BLS12381G2, ED25519 from ....did.did_key import DIDKey from ....wallet.in_memory import InMemoryWallet from ....core.in_memory import InMemoryProfile @@ -37,18 +37,18 @@ async def setUp(self): self.wallet = InMemoryWallet(self.profile) self.ed25519_key_info = await self.wallet.create_signing_key( - key_type=KeyType.ED25519, seed=self.test_seed + key_type=ED25519, seed=self.test_seed ) self.ed25519_verification_method = DIDKey.from_public_key_b58( - self.ed25519_key_info.verkey, KeyType.ED25519 + self.ed25519_key_info.verkey, ED25519 ).key_id self.bls12381g2_key_info = await self.wallet.create_signing_key( - key_type=KeyType.BLS12381G2, seed=self.test_seed + key_type=BLS12381G2, seed=self.test_seed ) self.bls12381g2_verification_method = DIDKey.from_public_key_b58( - self.bls12381g2_key_info.verkey, KeyType.BLS12381G2 + self.bls12381g2_key_info.verkey, BLS12381G2 ).key_id self.presentation_challenge = "2b1bbff6-e608-4368-bf84-67471b27e41c" @@ -61,7 +61,7 @@ async def test_issue_Ed25519Signature2018(self): verification_method=self.ed25519_verification_method, key_pair=WalletKeyPair( wallet=self.wallet, - key_type=KeyType.ED25519, + key_type=ED25519, public_key_base58=self.ed25519_key_info.verkey, ), date=datetime.strptime("2019-12-11T03:50:55Z", "%Y-%m-%dT%H:%M:%SZ"), @@ -103,7 +103,7 @@ async def test_derive_x_invalid_credential_structure(self): async def test_verify_Ed25519Signature2018(self): # Verification requires lot less input parameters suite = Ed25519Signature2018( - key_pair=WalletKeyPair(wallet=self.wallet, key_type=KeyType.ED25519), + key_pair=WalletKeyPair(wallet=self.wallet, key_type=ED25519), ) verified = await verify_credential( credential=CREDENTIAL_ISSUED, @@ -135,7 +135,7 @@ async def test_issue_BbsBlsSignature2020(self): verification_method=self.bls12381g2_verification_method, key_pair=WalletKeyPair( wallet=self.wallet, - key_type=KeyType.BLS12381G2, + key_type=BLS12381G2, public_key_base58=self.bls12381g2_key_info.verkey, ), date=datetime.strptime("2019-12-11T03:50:55Z", "%Y-%m-%dT%H:%M:%SZ"), @@ -158,7 +158,7 @@ async def test_issue_BbsBlsSignature2020(self): async def test_verify_BbsBlsSignature2020(self): # Verification requires lot less input parameters suite = BbsBlsSignature2020( - key_pair=WalletKeyPair(wallet=self.wallet, key_type=KeyType.BLS12381G2), + key_pair=WalletKeyPair(wallet=self.wallet, key_type=BLS12381G2), ) result = await verify_credential( credential=CREDENTIAL_ISSUED_BBS, @@ -185,7 +185,7 @@ async def test_create_presentation_x_invalid_credential_structures(self): verification_method=self.ed25519_verification_method, key_pair=WalletKeyPair( wallet=self.wallet, - key_type=KeyType.ED25519, + key_type=ED25519, public_key_base58=self.ed25519_key_info.verkey, ), date=datetime.strptime("2020-12-11T03:50:55Z", "%Y-%m-%dT%H:%M:%SZ"), @@ -208,6 +208,7 @@ async def test_create_presentation_x_invalid_credential_structures(self): presentation_id="https://presentation_id.com", ) + @pytest.mark.ursa_bbs_signatures async def test_sign_presentation_bbsbls(self): unsigned_presentation = await create_presentation( credentials=[CREDENTIAL_ISSUED] @@ -217,7 +218,7 @@ async def test_sign_presentation_bbsbls(self): verification_method=self.bls12381g2_verification_method, key_pair=WalletKeyPair( wallet=self.wallet, - key_type=KeyType.BLS12381G2, + key_type=BLS12381G2, public_key_base58=self.bls12381g2_key_info.verkey, ), date=datetime.strptime("2020-12-11T03:50:55Z", "%Y-%m-%dT%H:%M:%SZ"), @@ -234,7 +235,7 @@ async def test_sign_presentation_bbsbls(self): async def test_verify_presentation(self): suite = Ed25519Signature2018( - key_pair=WalletKeyPair(wallet=self.wallet, key_type=KeyType.ED25519), + key_pair=WalletKeyPair(wallet=self.wallet, key_type=ED25519), ) verification_result = await verify_presentation( presentation=PRESENTATION_SIGNED, @@ -258,7 +259,6 @@ async def test_verify_presentation_x_no_purpose_challenge(self): ) async def test_sign_presentation_x_no_purpose_challenge(self): - with self.assertRaises(LinkedDataProofException) as context: await sign_presentation( presentation=PRESENTATION_UNSIGNED, diff --git a/aries_cloudagent/version.py b/aries_cloudagent/version.py index 653ad55847..2ec2b6d7de 100644 --- a/aries_cloudagent/version.py +++ b/aries_cloudagent/version.py @@ -1,4 +1,4 @@ """Library version information.""" -__version__ = "0.7.3" +__version__ = "0.8.2" RECORD_TYPE_ACAPY_VERSION = "acapy_version" diff --git a/aries_cloudagent/wallet/askar.py b/aries_cloudagent/wallet/askar.py index 63dd4f4225..dac9a4cf5d 100644 --- a/aries_cloudagent/wallet/askar.py +++ b/aries_cloudagent/wallet/askar.py @@ -13,12 +13,11 @@ Key, KeyAlg, SeedMethod, - Session, ) +from .did_parameters_validation import DIDParametersValidation from ..askar.didcomm.v1 import pack_message, unpack_message from ..askar.profile import AskarProfileSession -from ..did.did_key import DIDKey from ..ledger.base import BaseLedger from ..ledger.endpoint_type import EndpointType from ..ledger.error import LedgerConfigError @@ -31,9 +30,9 @@ validate_seed, verify_signed_message, ) -from .did_method import DIDMethod +from .did_method import SOV, DIDMethod, DIDMethods from .error import WalletError, WalletDuplicateError, WalletNotFoundError -from .key_type import KeyType +from .key_type import BLS12381G2, ED25519, KeyType, KeyTypes from .util import b58_to_bytes, bytes_to_b58 CATEGORY_DID = "did" @@ -56,7 +55,7 @@ def __init__(self, session: AskarProfileSession): self._session = session @property - def session(self) -> Session: + def session(self) -> AskarProfileSession: """Accessor for Askar profile session instance.""" return self._session @@ -119,7 +118,7 @@ async def get_signing_key(self, verkey: str) -> KeyInfo: raise WalletNotFoundError("Unknown key: {}".format(verkey)) metadata = json.loads(key.metadata or "{}") # FIXME implement key types - return KeyInfo(verkey=verkey, metadata=metadata, key_type=KeyType.ED25519) + return KeyInfo(verkey=verkey, metadata=metadata, key_type=ED25519) async def replace_signing_key_metadata(self, verkey: str, metadata: dict): """ @@ -172,29 +171,23 @@ async def create_local_did( WalletError: If there is another backend error """ - - # validate key_type - if not method.supports_key_type(key_type): - raise WalletError( - f"Invalid key type {key_type.key_type}" - f" for DID method {method.method_name}" - ) - - if method == DIDMethod.KEY and did: - raise WalletError("Not allowed to set DID for DID method 'key'") + did_validation = DIDParametersValidation( + self._session.context.inject(DIDMethods) + ) + did_validation.validate_key_type(method, key_type) if not metadata: metadata = {} - if method not in [DIDMethod.SOV, DIDMethod.KEY]: - raise WalletError( - f"Unsupported DID method for askar storage: {method.method_name}" - ) try: keypair = _create_keypair(key_type, seed) verkey_bytes = keypair.get_public_bytes() verkey = bytes_to_b58(verkey_bytes) + did = did_validation.validate_or_derive_did( + method, key_type, verkey_bytes, did + ) + try: await self._session.handle.insert_key( verkey, keypair, metadata=json.dumps(metadata) @@ -206,11 +199,6 @@ async def create_local_did( else: raise WalletError("Error inserting key") from err - if method == DIDMethod.KEY: - did = DIDKey.from_public_key(verkey_bytes, key_type).did - elif not did: - did = bytes_to_b58(verkey_bytes[:16]) - item = await self._session.handle.fetch(CATEGORY_DID, did, for_update=True) if item: did_info = item.value_json @@ -257,7 +245,7 @@ async def get_local_dids(self) -> Sequence[DIDInfo]: ret = [] for item in await self._session.handle.fetch_all(CATEGORY_DID): - ret.append(_load_did_entry(item)) + ret.append(self._load_did_entry(item)) return ret async def get_local_did(self, did: str) -> DIDInfo: @@ -279,12 +267,12 @@ async def get_local_did(self, did: str) -> DIDInfo: if not did: raise WalletNotFoundError("No identifier provided") try: - did = await self._session.handle.fetch(CATEGORY_DID, did) + did_entry = await self._session.handle.fetch(CATEGORY_DID, did) except AskarError as err: raise WalletError("Error when fetching local DID") from err - if not did: + if not did_entry: raise WalletNotFoundError("Unknown DID: {}".format(did)) - return _load_did_entry(did) + return self._load_did_entry(did_entry) async def get_local_did_for_verkey(self, verkey: str) -> DIDInfo: """ @@ -308,7 +296,7 @@ async def get_local_did_for_verkey(self, verkey: str) -> DIDInfo: except AskarError as err: raise WalletError("Error when fetching local DID for verkey") from err if dids: - return _load_did_entry(dids[0]) + return self._load_did_entry(dids[0]) raise WalletNotFoundError("No DID defined for verkey: {}".format(verkey)) async def replace_local_did_metadata(self, did: str, metadata: dict): @@ -403,14 +391,11 @@ async def set_public_did(self, did: Union[str, DIDInfo]) -> DIDInfo: raise WalletError("Error when fetching local DID") from err if not item: raise WalletNotFoundError("Unknown DID: {}".format(did)) - info = _load_did_entry(item) + info = self._load_did_entry(item) else: info = did item = None - if info.method != DIDMethod.SOV: - raise WalletError("Setting public DID is only allowed for did:sov DIDs") - public = await self.get_public_did() if not public or public.did != info.did: storage = AskarStorage(self._session) @@ -446,6 +431,9 @@ async def set_did_endpoint( endpoint: str, ledger: BaseLedger, endpoint_type: EndpointType = None, + write_ledger: bool = True, + endorser_did: str = None, + routing_keys: List[str] = None, ): """ Update the endpoint for a DID in the wallet, send to ledger if public or posted. @@ -459,7 +447,7 @@ async def set_did_endpoint( 'endpoint' affects local wallet """ did_info = await self.get_local_did(did) - if did_info.method != DIDMethod.SOV: + if did_info.method != SOV: raise WalletError("Setting DID endpoint is only allowed for did:sov DIDs") metadata = {**did_info.metadata} if not endpoint_type: @@ -478,7 +466,16 @@ async def set_did_endpoint( ) if not ledger.read_only: async with ledger: - await ledger.update_endpoint_for_did(did, endpoint, endpoint_type) + attrib_def = await ledger.update_endpoint_for_did( + did, + endpoint, + endpoint_type, + write_ledger=write_ledger, + endorser_did=endorser_did, + routing_keys=routing_keys, + ) + if not write_ledger: + return attrib_def await self.replace_local_did_metadata(did, metadata) @@ -495,14 +492,15 @@ async def rotate_did_keypair_start(self, did: str, next_seed: str = None) -> str """ # Check if DID can rotate keys - did_method = DIDMethod.from_did(did) + did_methods = self._session.inject(DIDMethods) + did_method: DIDMethod = did_methods.from_did(did) if not did_method.supports_rotation: raise WalletError( f"DID method '{did_method.method_name}' does not support key rotation." ) # create a new key to be rotated to (only did:sov/ED25519 supported for now) - keypair = _create_keypair(KeyType.ED25519, next_seed) + keypair = _create_keypair(ED25519, next_seed) verkey = bytes_to_b58(keypair.get_public_bytes()) try: await self._session.handle.insert_key( @@ -595,7 +593,7 @@ async def sign_message( return sign_message( message=message, secret=key.get_secret_bytes(), - key_type=KeyType.BLS12381G2, + key_type=BLS12381G2, ) else: @@ -638,7 +636,7 @@ async def verify_message( verkey = b58_to_bytes(from_verkey) - if key_type == KeyType.ED25519: + if key_type == ED25519: try: pk = Key.from_public_bytes(KeyAlg.ED25519, verkey) return pk.verify_signature(message, signature) @@ -715,24 +713,38 @@ async def unpack_message(self, enc_message: bytes) -> Tuple[str, str, str]: raise WalletError("Exception when unpacking message") from err return unpacked_json.decode("utf-8"), sender, recipient + def _load_did_entry(self, entry: Entry) -> DIDInfo: + """Convert a DID record into the expected DIDInfo format.""" + did_info = entry.value_json + did_methods: DIDMethods = self._session.inject(DIDMethods) + key_types: KeyTypes = self._session.inject(KeyTypes) + return DIDInfo( + did=did_info["did"], + verkey=did_info["verkey"], + metadata=did_info.get("metadata"), + method=did_methods.from_method(did_info.get("method", "sov")) or SOV, + key_type=key_types.from_key_type(did_info.get("verkey_type", "ed25519")) + or ED25519, + ) + def _create_keypair(key_type: KeyType, seed: Union[str, bytes] = None) -> Key: """Instantiate a new keypair with an optional seed value.""" - if key_type == KeyType.ED25519: + if key_type == ED25519: alg = KeyAlg.ED25519 method = None - # elif key_type == KeyType.BLS12381G1: + # elif key_type == BLS12381G1: # alg = KeyAlg.BLS12_381_G1 - elif key_type == KeyType.BLS12381G2: + elif key_type == BLS12381G2: alg = KeyAlg.BLS12_381_G2 method = SeedMethod.BlsKeyGen - # elif key_type == KeyType.BLS12381G1G2: + # elif key_type == BLS12381G1G2: # alg = KeyAlg.BLS12_381_G1G2 else: raise WalletError(f"Unsupported key algorithm: {key_type}") if seed: try: - if key_type == KeyType.ED25519: + if key_type == ED25519: # not a seed - it is the secret key seed = validate_seed(seed) return Key.from_secret_bytes(alg, seed) @@ -743,15 +755,3 @@ def _create_keypair(key_type: KeyType, seed: Union[str, bytes] = None) -> Key: raise WalletError("Invalid seed for key generation") from None else: return Key.generate(alg) - - -def _load_did_entry(entry: Entry) -> DIDInfo: - """Convert a DID record into the expected DIDInfo format.""" - did_info = entry.value_json - return DIDInfo( - did=did_info["did"], - verkey=did_info["verkey"], - metadata=did_info.get("metadata"), - method=DIDMethod.from_method(did_info.get("method", "sov")), - key_type=KeyType.from_key_type(did_info.get("verkey_type", "ed25519")), - ) diff --git a/aries_cloudagent/wallet/base.py b/aries_cloudagent/wallet/base.py index d15cfce3ab..46bbc3eab9 100644 --- a/aries_cloudagent/wallet/base.py +++ b/aries_cloudagent/wallet/base.py @@ -9,7 +9,7 @@ from .did_info import DIDInfo, KeyInfo from .key_type import KeyType -from .did_method import DIDMethod +from .did_method import SOV, DIDMethod class BaseWallet(ABC): @@ -224,6 +224,9 @@ async def set_did_endpoint( endpoint: str, _ledger: BaseLedger, endpoint_type: EndpointType = None, + write_ledger: bool = True, + endorser_did: str = None, + routing_keys: List[str] = None, ): """ Update the endpoint for a DID in the wallet, send to ledger if public or posted. @@ -238,7 +241,7 @@ async def set_did_endpoint( """ did_info = await self.get_local_did(did) - if did_info.method != DIDMethod.SOV: + if did_info.method != SOV: raise WalletError("Setting DID endpoint is only allowed for did:sov DIDs") metadata = {**did_info.metadata} if not endpoint_type: diff --git a/aries_cloudagent/wallet/bbs.py b/aries_cloudagent/wallet/bbs.py index fd80e24fb7..a204682a73 100644 --- a/aries_cloudagent/wallet/bbs.py +++ b/aries_cloudagent/wallet/bbs.py @@ -102,5 +102,5 @@ def create_bls12381g2_keypair(seed: bytes = None) -> Tuple[bytes, bytes]: try: key_pair = BlsKeyPair.generate_g2(seed) return key_pair.public_key, key_pair.secret_key - except (Exception) as error: + except Exception as error: raise BbsException("Unable to create keypair") from error diff --git a/aries_cloudagent/wallet/crypto.py b/aries_cloudagent/wallet/crypto.py index 5e79c3c15f..f44e9cff77 100644 --- a/aries_cloudagent/wallet/crypto.py +++ b/aries_cloudagent/wallet/crypto.py @@ -14,7 +14,7 @@ from ..utils.jwe import JweRecipient, b64url, JweEnvelope, from_b64url from .error import WalletError from .util import bytes_to_b58, b64_to_bytes, b58_to_bytes, random_seed -from .key_type import KeyType +from .key_type import ED25519, BLS12381G2, KeyType from .bbs import ( create_bls12381g2_keypair, verify_signed_messages_bls12381g2, @@ -38,9 +38,9 @@ def create_keypair(key_type: KeyType, seed: bytes = None) -> Tuple[bytes, bytes] A tuple of (public key, secret key) """ - if key_type == KeyType.ED25519: + if key_type == ED25519: return create_ed25519_keypair(seed) - elif key_type == KeyType.BLS12381G2: + elif key_type == BLS12381G2: # This ensures python won't crash if bbs is not installed and not used return create_bls12381g2_keypair(seed) @@ -149,7 +149,7 @@ def sign_message( # Make messages list if not already for easier checking going forward messages = message if isinstance(message, list) else [message] - if key_type == KeyType.ED25519: + if key_type == ED25519: if len(messages) > 1: raise WalletError("ed25519 can only sign a single message") @@ -157,7 +157,7 @@ def sign_message( message=messages[0], secret=secret, ) - elif key_type == KeyType.BLS12381G2: + elif key_type == BLS12381G2: return sign_messages_bls12381g2(messages=messages, secret=secret) else: raise WalletError(f"Unsupported key type: {key_type.key_type}") @@ -201,14 +201,14 @@ def verify_signed_message( # Make messages list if not already for easier checking going forward messages = message if isinstance(message, list) else [message] - if key_type == KeyType.ED25519: + if key_type == ED25519: if len(messages) > 1: raise WalletError("ed25519 can only verify a single message") return verify_signed_message_ed25519( message=messages[0], signature=signature, verkey=verkey ) - elif key_type == KeyType.BLS12381G2: + elif key_type == BLS12381G2: try: return verify_signed_messages_bls12381g2( messages=messages, signature=signature, public_key=verkey diff --git a/aries_cloudagent/wallet/default_verification_key_strategy.py b/aries_cloudagent/wallet/default_verification_key_strategy.py new file mode 100644 index 0000000000..4f580618b3 --- /dev/null +++ b/aries_cloudagent/wallet/default_verification_key_strategy.py @@ -0,0 +1,65 @@ +"""Utilities for specifying which verification method is in use for a given DID.""" +from abc import ABC, abstractmethod +from typing import Optional, List + +from aries_cloudagent.core.profile import Profile + +from aries_cloudagent.wallet.key_type import KeyType + +from aries_cloudagent.did.did_key import DIDKey + + +class BaseVerificationKeyStrategy(ABC): + """Base class for defining which verification method is in use.""" + + @abstractmethod + async def get_verification_method_id_for_did( + self, + did: str, + profile: Optional[Profile], + allowed_verification_method_types: Optional[List[KeyType]] = None, + proof_purpose: Optional[str] = None, + ) -> Optional[str]: + """Given a DID, returns the verification key ID in use. + + Returns None if no strategy is specified for this DID. + + :params did: the did + :params profile: context of the call + :params allowed_verification_method_types: list of accepted key types + :params proof_purpose: the verkey relationship (assertionMethod, keyAgreement, ..) + :returns Optional[str]: the current verkey ID + """ + pass + + +class DefaultVerificationKeyStrategy(BaseVerificationKeyStrategy): + """A basic implementation for verkey strategy. + + Supports did:key: and did:sov only. + """ + + async def get_verification_method_id_for_did( + self, + did: str, + profile: Optional[Profile], + allowed_verification_method_types: Optional[List[KeyType]] = None, + proof_purpose: Optional[str] = None, + ) -> Optional[str]: + """Given a did:key or did:sov, returns the verification key ID in use. + + Returns None if no strategy is specified for this DID. + + :params did: the did + :params profile: context of the call + :params allowed_verification_method_types: list of accepted key types + :params proof_purpose: the verkey relationship (assertionMethod, keyAgreement, ..) + :returns Optional[str]: the current verkey ID + """ + if did.startswith("did:key:"): + return DIDKey.from_did(did).key_id + elif did.startswith("did:sov:"): + # key-1 is what uniresolver uses for key id + return did + "#key-1" + + return None diff --git a/aries_cloudagent/wallet/did_method.py b/aries_cloudagent/wallet/did_method.py index 82382ea39b..cbe1361b39 100644 --- a/aries_cloudagent/wallet/did_method.py +++ b/aries_cloudagent/wallet/did_method.py @@ -1,88 +1,111 @@ -"""Did method enum.""" +"""did method.py contains registry for did methods.""" -from typing import List, Mapping, NamedTuple, Optional from enum import Enum +from typing import Dict, List, Mapping, Optional -from .key_type import KeyType from .error import BaseError +from .key_type import BLS12381G2, ED25519, KeyType -DIDMethodSpec = NamedTuple( - "DIDMethodSpec", - [ - ("method_name", str), - ("supported_key_types", List[KeyType]), - ("supports_rotation", bool), - ], -) +class HolderDefinedDid(Enum): + """Define if a holder can specify its own did for a given method.""" -class DIDMethod(Enum): - """DID Method class specifying DID methods with supported key types.""" + NO = "no" # holder CANNOT provide a DID + ALLOWED = "allowed" # holder CAN provide a DID + REQUIRED = "required" # holder MUST provide a DID + + +class DIDMethod: + """Class to represent a did method.""" - SOV = DIDMethodSpec( - method_name="sov", supported_key_types=[KeyType.ED25519], supports_rotation=True - ) - KEY = DIDMethodSpec( - method_name="key", - supported_key_types=[KeyType.ED25519, KeyType.BLS12381G2], - supports_rotation=False, - ) + def __init__( + self, + name: str, + key_types: List[KeyType], + rotation: bool = False, + holder_defined_did: HolderDefinedDid = HolderDefinedDid.NO, + ): + """Construct did method class.""" + self._method_name: str = name + self._supported_key_types: List[KeyType] = key_types + self._supports_rotation: bool = rotation + self._holder_defined_did: HolderDefinedDid = holder_defined_did @property - def method_name(self) -> str: - """Getter for did method name. e.g. sov or key.""" - return self.value.method_name + def method_name(self): + """Get method name.""" + return self._method_name @property - def supported_key_types(self) -> List[KeyType]: - """Getter for supported key types of method.""" - return self.value.supported_key_types + def supports_rotation(self): + """Check rotation support.""" + return self._supports_rotation @property - def supports_rotation(self) -> bool: - """Check whether the current method supports key rotation.""" - return self.value.supports_rotation + def supported_key_types(self): + """Get supported key types.""" + return self._supported_key_types def supports_key_type(self, key_type: KeyType) -> bool: """Check whether the current method supports the key type.""" return key_type in self.supported_key_types - def from_metadata(metadata: Mapping) -> "DIDMethod": - """Get DID method instance from metadata object. + def holder_defined_did(self) -> HolderDefinedDid: + """Return the did derivation policy. - Returns SOV if no metadata was found for backwards compatability. + eg: did:key DIDs are derived from the verkey -> HolderDefinedDid.NO + eg: did:web DIDs cannot be derived from key material -> HolderDefinedDid.REQUIRED """ - method = metadata.get("method") + return self._holder_defined_did + + +SOV = DIDMethod( + name="sov", + key_types=[ED25519], + rotation=True, + holder_defined_did=HolderDefinedDid.ALLOWED, +) +KEY = DIDMethod( + name="key", + key_types=[ED25519, BLS12381G2], + rotation=False, +) - # extract from metadata object - if method: - for did_method in DIDMethod: - if method == did_method.method_name: - return did_method - # return default SOV for backward compat - return DIDMethod.SOV +class DIDMethods: + """DID Method class specifying DID methods with supported key types.""" - def from_method(method: str) -> Optional["DIDMethod"]: - """Get DID method instance from the method name.""" - for did_method in DIDMethod: - if method == did_method.method_name: - return did_method + def __init__(self) -> None: + """Construct did method registry.""" + self._registry: Dict[str, DIDMethod] = { + SOV.method_name: SOV, + KEY.method_name: KEY, + } - return None + def registered(self, method: str) -> bool: + """Check for a supported method.""" + return method in self._registry.keys() - def from_did(did: str) -> "DIDMethod": - """Get DID method instance from the method name.""" - if not did.startswith("did:"): - # sov has no prefix - return DIDMethod.SOV + def register(self, method: DIDMethod): + """Register a new did method.""" + self._registry[method.method_name] = method - parts = did.split(":") - method_str = parts[1] + def from_method(self, method_name: str) -> Optional[DIDMethod]: + """Retrieve a did method from method name.""" + return self._registry.get(method_name) - method = DIDMethod.from_method(method_str) + def from_metadata(self, metadata: Mapping) -> Optional[DIDMethod]: + """Get DID method instance from metadata object. - if not method: - raise BaseError(f"Unsupported did method: {method_str}") + Returns SOV if no metadata was found for backwards compatibility. + """ + method_name: str = metadata.get("method", "sov") + return self.from_method(method_name) + def from_did(self, did: str) -> DIDMethod: + """Get DID method instance from the did url.""" + method_name = did.split(":")[1] if did.startswith("did:") else SOV.method_name + method: DIDMethod | None = self.from_method(method_name) + if not method: + raise BaseError(f"Unsupported did method: {method_name}") return method diff --git a/aries_cloudagent/wallet/did_parameters_validation.py b/aries_cloudagent/wallet/did_parameters_validation.py new file mode 100644 index 0000000000..04572c77bf --- /dev/null +++ b/aries_cloudagent/wallet/did_parameters_validation.py @@ -0,0 +1,65 @@ +"""Tooling to validate DID creation parameters.""" + +from typing import Optional + +from aries_cloudagent.did.did_key import DIDKey +from aries_cloudagent.wallet.did_method import ( + DIDMethods, + DIDMethod, + HolderDefinedDid, + KEY, + SOV, +) +from aries_cloudagent.wallet.error import WalletError +from aries_cloudagent.wallet.key_type import KeyType +from aries_cloudagent.wallet.util import bytes_to_b58 + + +class DIDParametersValidation: + """A utility class to check compatibility of provided DID creation parameters.""" + + def __init__(self, did_methods: DIDMethods): + """:param did_methods: DID method registry relevant for the validation.""" + self.did_methods = did_methods + + @staticmethod + def validate_key_type(method: DIDMethod, key_type: KeyType): + """Validate compatibility of the DID method with the desired key type.""" + # validate key_type + if not method.supports_key_type(key_type): + raise WalletError( + f"Invalid key type {key_type.key_type}" + f" for DID method {method.method_name}" + ) + + def validate_or_derive_did( + self, + method: DIDMethod, + key_type: KeyType, + verkey: bytes, + did: Optional[str], + ) -> str: + """ + Validate compatibility of the provided did (if any) with the given DID method. + + If no DID was provided, automatically derive one for methods that support it. + """ + if method.holder_defined_did() == HolderDefinedDid.NO and did: + raise WalletError( + f"Not allowed to set DID for DID method '{method.method_name}'" + ) + elif method.holder_defined_did() == HolderDefinedDid.REQUIRED and not did: + raise WalletError(f"Providing a DID is required {method.method_name}") + elif not self.did_methods.registered(method.method_name): + raise WalletError( + f"Unsupported DID method for current storage: {method.method_name}" + ) + + # We need some did method specific handling. If more did methods + # are added it is probably better create a did method specific handler + elif method == KEY: + return DIDKey.from_public_key(verkey, key_type).did + elif method == SOV: + return bytes_to_b58(verkey[:16]) if not did else did + + return did diff --git a/aries_cloudagent/wallet/in_memory.py b/aries_cloudagent/wallet/in_memory.py index f005ef1d21..03fc4fb8aa 100644 --- a/aries_cloudagent/wallet/in_memory.py +++ b/aries_cloudagent/wallet/in_memory.py @@ -3,8 +3,8 @@ import asyncio from typing import List, Sequence, Tuple, Union +from .did_parameters_validation import DIDParametersValidation from ..core.in_memory import InMemoryProfile -from ..did.did_key import DIDKey from .base import BaseWallet from .crypto import ( @@ -17,7 +17,7 @@ ) from .did_info import KeyInfo, DIDInfo from .did_posture import DIDPosture -from .did_method import DIDMethod +from .did_method import DIDMethod, DIDMethods from .error import WalletError, WalletDuplicateError, WalletNotFoundError from .key_type import KeyType from .util import b58_to_bytes, bytes_to_b58, random_seed @@ -132,8 +132,8 @@ async def rotate_did_keypair_start(self, did: str, next_seed: str = None) -> str local_did = self.profile.local_dids.get(did) if not local_did: raise WalletNotFoundError("Wallet owns no such DID: {}".format(did)) - - did_method = DIDMethod.from_did(did) + did_methods: DIDMethods = self.profile.context.inject(DIDMethods) + did_method: DIDMethod = did_methods.from_did(did) if not did_method.supports_rotation: raise WalletError( f"DID method '{did_method.method_name}' does not support key rotation." @@ -212,27 +212,15 @@ async def create_local_did( """ seed = validate_seed(seed) or random_seed() - # validate key_type - if not method.supports_key_type(key_type): - raise WalletError( - f"Invalid key type {key_type.key_type} for method {method.method_name}" - ) + did_methods: DIDMethods = self.profile.context.inject(DIDMethods) + did_validation = DIDParametersValidation(did_methods) + + did_validation.validate_key_type(method, key_type) verkey, secret = create_keypair(key_type, seed) verkey_enc = bytes_to_b58(verkey) - # We need some did method specific handling. If more did methods - # are added it is probably better create a did method specific handler - if method == DIDMethod.KEY: - if did: - raise WalletError("Not allowed to set DID for DID method 'key'") - - did = DIDKey.from_public_key(verkey, key_type).did - elif method == DIDMethod.SOV: - if not did: - did = bytes_to_b58(verkey[:16]) - else: - raise WalletError(f"Unsupported DID method: {method.method_name}") + did = did_validation.validate_or_derive_did(method, key_type, verkey, did) if ( did in self.profile.local_dids @@ -395,9 +383,6 @@ async def set_public_did(self, did: Union[str, DIDInfo]) -> DIDInfo: info = did did = info.did - if info.method != DIDMethod.SOV: - raise WalletError("Setting public DID is only allowed for did:sov DIDs") - public = await self.get_public_did() if public and public.did == did: info = public diff --git a/aries_cloudagent/wallet/indy.py b/aries_cloudagent/wallet/indy.py index a5d40732f6..7a99858044 100644 --- a/aries_cloudagent/wallet/indy.py +++ b/aries_cloudagent/wallet/indy.py @@ -1,6 +1,7 @@ """Indy implementation of BaseWallet interface.""" import json +import logging from typing import List, Sequence, Tuple, Union @@ -29,13 +30,15 @@ verify_signed_message, ) from .did_info import DIDInfo, KeyInfo -from .did_method import DIDMethod +from .did_method import SOV, KEY, DIDMethod from .error import WalletError, WalletDuplicateError, WalletNotFoundError from .key_pair import KeyPairStorageManager -from .key_type import KeyType +from .key_type import BLS12381G2, ED25519, KeyType, KeyTypes from .util import b58_to_bytes, bytes_to_b58, bytes_to_b64 +LOGGER = logging.getLogger(__name__) + RECORD_TYPE_CONFIG = "config" RECORD_NAME_PUBLIC_DID = "default_public_did" @@ -45,17 +48,17 @@ class IndySdkWallet(BaseWallet): def __init__(self, opened: IndyOpenWallet): """Create a new IndySdkWallet instance.""" - self.opened = opened + self.opened: IndyOpenWallet = opened def __did_info_from_indy_info(self, info): metadata = json.loads(info["metadata"]) if info["metadata"] else {} did: str = info["did"] verkey = info["verkey"] - method = DIDMethod.KEY if did.startswith("did:key") else DIDMethod.SOV - key_type = KeyType.ED25519 + method = KEY if did.startswith("did:key") else SOV + key_type = ED25519 - if method == DIDMethod.KEY: + if method == KEY: did = DIDKey.from_public_key_b58(info["verkey"], key_type).did return DIDInfo( @@ -66,11 +69,13 @@ def __did_info_from_key_pair_info(self, info: dict): metadata = info["metadata"] verkey = info["verkey"] - # this needs to change if other did methods are added - method = DIDMethod.from_method(info["metadata"].get("method", "key")) - key_type = KeyType.from_key_type(info["key_type"]) + # TODO: inject context to support did method registry + method = SOV if metadata.get("method", "key") == SOV.method_name else KEY + # TODO: inject context to support keytype registry + key_types = KeyTypes() + key_type = key_types.from_key_type(info["key_type"]) - if method == DIDMethod.KEY: + if method == KEY: did = DIDKey.from_public_key_b58(info["verkey"], key_type).did return DIDInfo( @@ -80,7 +85,7 @@ def __did_info_from_key_pair_info(self, info: dict): async def __create_indy_signing_key( self, key_type: KeyType, metadata: dict, seed: str = None ) -> str: - if key_type != KeyType.ED25519: + if key_type != ED25519: raise WalletError(f"Unsupported key type: {key_type.key_type}") args = {} @@ -104,7 +109,7 @@ async def __create_indy_signing_key( async def __create_keypair_signing_key( self, key_type: KeyType, metadata: dict, seed: str = None ) -> str: - if key_type != KeyType.BLS12381G2: + if key_type != BLS12381G2: raise WalletError(f"Unsupported key type: {key_type.key_type}") public_key, secret_key = create_keypair(key_type, validate_seed(seed)) @@ -155,7 +160,7 @@ async def create_signing_key( metadata = {} # All ed25519 keys are handled by indy - if key_type == KeyType.ED25519: + if key_type == ED25519: verkey = await self.__create_indy_signing_key(key_type, metadata, seed) # All other (only bls12381g2 atm) are handled outside of indy else: @@ -170,7 +175,7 @@ async def __get_indy_signing_key(self, verkey: str) -> KeyInfo: return KeyInfo( verkey=verkey, metadata=json.loads(metadata) if metadata else {}, - key_type=KeyType.ED25519, + key_type=ED25519, ) except IndyError as x_indy: if x_indy.error_code == ErrorCode.WalletItemNotFound: @@ -187,14 +192,16 @@ async def __get_keypair_signing_key(self, verkey: str) -> KeyInfo: try: key_pair_mgr = KeyPairStorageManager(IndySdkStorage(self.opened)) key_pair = await key_pair_mgr.get_key_pair(verkey) + # TODO: inject context to support more keytypes + key_types = KeyTypes() return KeyInfo( verkey=verkey, metadata=key_pair["metadata"], - key_type=KeyType.from_key_type(key_pair["key_type"]), + key_type=key_types.from_key_type(key_pair["key_type"]) or BLS12381G2, ) - except (StorageNotFoundError): + except StorageNotFoundError: raise WalletNotFoundError(f"Unknown key: {verkey}") - except (StorageDuplicateError): + except StorageDuplicateError: raise WalletDuplicateError(f"Multiple keys exist for verkey: {verkey}") async def get_signing_key(self, verkey: str) -> KeyInfo: @@ -243,7 +250,7 @@ async def replace_signing_key_metadata(self, verkey: str, metadata: dict): key_info = await self.get_signing_key(verkey) # All ed25519 keys are handled by indy - if key_info.key_type == KeyType.ED25519: + if key_info.key_type == ED25519: await indy.crypto.set_key_metadata( self.opened.handle, verkey, json.dumps(metadata) ) @@ -267,7 +274,9 @@ async def rotate_did_keypair_start(self, did: str, next_seed: str = None) -> str """ # Check if DID can rotate keys - did_method = DIDMethod.from_did(did) + # TODO: inject context for did method registry support + method_name = did.split(":")[1] if did.startswith("did:") else SOV.method_name + did_method = SOV if method_name == SOV.method_name else KEY if not did_method.supports_rotation: raise WalletError( f"DID method '{did_method.method_name}' does not support key rotation." @@ -321,11 +330,11 @@ async def __create_indy_local_did( *, did: str = None, ) -> DIDInfo: - if method not in [DIDMethod.SOV, DIDMethod.KEY]: + if method not in [SOV, KEY]: raise WalletError( f"Unsupported DID method for indy storage: {method.method_name}" ) - if key_type != KeyType.ED25519: + if key_type != ED25519: raise WalletError( f"Unsupported key type for indy storage: {key_type.key_type}" ) @@ -337,7 +346,7 @@ async def __create_indy_local_did( cfg["did"] = did # Create fully qualified did. This helps with determining the # did method when retrieving - if method != DIDMethod.SOV: + if method != SOV: cfg["method_name"] = method.method_name did_json = json.dumps(cfg) # crypto_type, cid - optional parameters skipped @@ -353,7 +362,7 @@ async def __create_indy_local_did( ) from x_indy # did key uses different format - if method == DIDMethod.KEY: + if method == KEY: did = DIDKey.from_public_key_b58(verkey, key_type).did await self.replace_local_did_metadata(did, metadata or {}) @@ -373,11 +382,11 @@ async def __create_keypair_local_did( metadata: dict = None, seed: str = None, ) -> DIDInfo: - if method != DIDMethod.KEY: + if method != KEY: raise WalletError( f"Unsupported DID method for keypair storage: {method.method_name}" ) - if key_type != KeyType.BLS12381G2: + if key_type != BLS12381G2: raise WalletError( f"Unsupported key type for keypair storage: {key_type.key_type}" ) @@ -441,11 +450,11 @@ async def create_local_did( f" for DID method {method.method_name}" ) - if method == DIDMethod.KEY and did: + if method == KEY and did: raise WalletError("Not allowed to set DID for DID method 'key'") # All ed25519 keys are handled by indy - if key_type == KeyType.ED25519: + if key_type == ED25519: return await self.__create_indy_local_did( method, key_type, metadata, seed, did=did ) @@ -474,7 +483,7 @@ async def get_local_dids(self) -> Sequence[DIDInfo]: # this needs to change if more did methods are added key_pair_mgr = KeyPairStorageManager(IndySdkStorage(self.opened)) key_pairs = await key_pair_mgr.find_key_pairs( - tag_query={"method": DIDMethod.KEY.method_name} + tag_query={"method": KEY.method_name} ) for key_pair in key_pairs: ret.append(self.__did_info_from_key_pair_info(key_pair)) @@ -484,17 +493,17 @@ async def get_local_dids(self) -> Sequence[DIDInfo]: async def __get_indy_local_did( self, method: DIDMethod, key_type: KeyType, did: str ) -> DIDInfo: - if method not in [DIDMethod.SOV, DIDMethod.KEY]: + if method not in [SOV, KEY]: raise WalletError( f"Unsupported DID method for indy storage: {method.method_name}" ) - if key_type != KeyType.ED25519: + if key_type != ED25519: raise WalletError( f"Unsupported DID type for indy storage: {key_type.key_type}" ) # key type is always ed25519, method not always key - if method == DIDMethod.KEY and key_type == KeyType.ED25519: + if method == KEY and key_type == ED25519: did_key = DIDKey.from_did(did) # Ed25519 did:keys are masked indy dids so transform to indy @@ -514,11 +523,11 @@ async def __get_indy_local_did( async def __get_keypair_local_did( self, method: DIDMethod, key_type: KeyType, did: str ): - if method != DIDMethod.KEY: + if method != KEY: raise WalletError( f"Unsupported DID method for keypair storage: {method.method_name}" ) - if key_type != KeyType.BLS12381G2: + if key_type != BLS12381G2: raise WalletError( f"Unsupported DID type for keypair storage: {key_type.key_type}" ) @@ -545,15 +554,17 @@ async def get_local_did(self, did: str) -> DIDInfo: WalletError: If there is a libindy error """ - method = DIDMethod.from_did(did) - key_type = KeyType.ED25519 + # TODO: inject context for did method registry support + method_name = did.split(":")[1] if did.startswith("did:") else SOV.method_name + method = SOV if method_name == SOV.method_name else KEY + key_type = ED25519 # If did key, the key type can differ - if method == DIDMethod.KEY: + if method == KEY: did_key = DIDKey.from_did(did) key_type = did_key.key_type - if key_type == KeyType.ED25519: + if key_type == ED25519: return await self.__get_indy_local_did(method, key_type, did) else: return await self.__get_keypair_local_did(method, key_type, did) @@ -593,7 +604,7 @@ async def replace_local_did_metadata(self, did: str, metadata: dict): did_info = await self.get_local_did(did) # throw exception if undefined # ed25519 keys are handled by indy - if did_info.key_type == KeyType.ED25519: + if did_info.key_type == ED25519: try: await indy.did.set_did_metadata( self.opened.handle, did, json.dumps(metadata) @@ -676,9 +687,6 @@ async def set_public_did(self, did: Union[str, DIDInfo]) -> DIDInfo: else: info = did - if info.method != DIDMethod.SOV: - raise WalletError("Setting public DID is only allowed for did:sov DIDs") - public = await self.get_public_did() if not public or public.did != info.did: if not info.metadata.get("posted"): @@ -705,6 +713,9 @@ async def set_did_endpoint( endpoint: str, ledger: BaseLedger, endpoint_type: EndpointType = None, + write_ledger: bool = True, + endorser_did: str = None, + routing_keys: List[str] = None, ): """ Update the endpoint for a DID in the wallet, send to ledger if public or posted. @@ -718,7 +729,7 @@ async def set_did_endpoint( 'endpoint' affects local wallet """ did_info = await self.get_local_did(did) - if did_info.method != DIDMethod.SOV: + if did_info.method != SOV: raise WalletError("Setting DID endpoint is only allowed for did:sov DIDs") metadata = {**did_info.metadata} @@ -738,7 +749,16 @@ async def set_did_endpoint( ) if not ledger.read_only: async with ledger: - await ledger.update_endpoint_for_did(did, endpoint, endpoint_type) + attrib_def = await ledger.update_endpoint_for_did( + did, + endpoint, + endpoint_type, + write_ledger=write_ledger, + endorser_did=endorser_did, + routing_keys=routing_keys, + ) + if not write_ledger: + return attrib_def await self.replace_local_did_metadata(did, metadata) @@ -770,7 +790,7 @@ async def sign_message(self, message: bytes, from_verkey: str) -> bytes: key_info = await self.get_local_did_for_verkey(from_verkey) # ed25519 keys are handled by indy - if key_info.key_type == KeyType.ED25519: + if key_info.key_type == ED25519: try: result = await indy.crypto.crypto_sign( self.opened.handle, from_verkey, message @@ -822,7 +842,7 @@ async def verify_message( raise WalletError("Message not provided") # ed25519 keys are handled by indy - if key_type == KeyType.ED25519: + if key_type == ED25519: try: result = await indy.crypto.crypto_verify( from_verkey, message, signature diff --git a/aries_cloudagent/wallet/key_type.py b/aries_cloudagent/wallet/key_type.py index 57defdb521..9e16bb1dde 100644 --- a/aries_cloudagent/wallet/key_type.py +++ b/aries_cloudagent/wallet/key_type.py @@ -1,78 +1,95 @@ -"""Key type enum.""" +"""Key type code.""" -from enum import Enum -from typing import NamedTuple, Optional +from typing import Optional -# Define keys -KeySpec = NamedTuple( - "KeySpec", - [("key_type", str), ("multicodec_name", str), ("multicodec_prefix", int)], -) +class KeyType: + """Key Type class.""" -class KeyTypeException(BaseException): - """Key type exception.""" - - -class KeyType(Enum): - """KeyType Enum specifying key types with multicodec name.""" - - # NOTE: the py_multicodec library is outdated. We use hardcoded prefixes here - # until this PR gets released: https://github.com/multiformats/py-multicodec/pull/14 - # multicodec is also not used now, but may be used again if py_multicodec is updated - ED25519 = KeySpec("ed25519", "ed25519-pub", b"\xed\x01") - X25519 = KeySpec("x25519", "x25519-pub", b"\xec\x01") - BLS12381G1 = KeySpec("bls12381g1", "bls12_381-g1-pub", b"\xea\x01") - BLS12381G2 = KeySpec("bls12381g2", "bls12_381-g2-pub", b"\xeb\x01") - BLS12381G1G2 = KeySpec("bls12381g1g2", "bls12_381-g1g2-pub", b"\xee\x01") + def __init__(self, key_type: str, multicodec_name: str, multicodec_prefix: bytes): + """Construct key type.""" + self._type: str = key_type + self._name: str = multicodec_name + self._prefix: bytes = multicodec_prefix @property def key_type(self) -> str: - """Getter for key type identifier.""" - return self.value.key_type + """Get Key type, type.""" + return self._type @property def multicodec_name(self) -> str: - """Getter for multicodec name.""" - return self.value.multicodec_name + """Get key type multicodec name.""" + return self._name @property def multicodec_prefix(self) -> bytes: - """Getter for multicodec prefix.""" - return self.value.multicodec_prefix - - @classmethod - def from_multicodec_name(cls, multicodec_name: str) -> Optional["KeyType"]: + """Get key type multicodec prefix.""" + return self._prefix + + +# NOTE: the py_multicodec library is outdated. We use hardcoded prefixes here +# until this PR gets released: https://github.com/multiformats/py-multicodec/pull/14 +# multicodec is also not used now, but may be used again if py_multicodec is updated +ED25519: KeyType = KeyType("ed25519", "ed25519-pub", b"\xed\x01") +X25519: KeyType = KeyType("x25519", "x25519-pub", b"\xec\x01") +BLS12381G1: KeyType = KeyType("bls12381g1", "bls12_381-g1-pub", b"\xea\x01") +BLS12381G2: KeyType = KeyType("bls12381g2", "bls12_381-g2-pub", b"\xeb\x01") +BLS12381G1G2: KeyType = KeyType("bls12381g1g2", "bls12_381-g1g2-pub", b"\xee\x01") + + +class KeyTypes: + """KeyType class specifying key types with multicodec name.""" + + def __init__(self) -> None: + """Construct key type registry.""" + self._type_registry: dict[str, KeyType] = { + ED25519.key_type: ED25519, + X25519.key_type: X25519, + BLS12381G1.key_type: BLS12381G1, + BLS12381G2.key_type: BLS12381G2, + BLS12381G1G2.key_type: BLS12381G1G2, + } + self._name_registry: dict[str, KeyType] = { + ED25519.multicodec_name: ED25519, + X25519.multicodec_name: X25519, + BLS12381G1.multicodec_name: BLS12381G1, + BLS12381G2.multicodec_name: BLS12381G2, + BLS12381G1G2.multicodec_name: BLS12381G1G2, + } + self._prefix_registry: dict[bytes, KeyType] = { + ED25519.multicodec_prefix: ED25519, + X25519.multicodec_prefix: X25519, + BLS12381G1.multicodec_prefix: BLS12381G1, + BLS12381G2.multicodec_prefix: BLS12381G2, + BLS12381G1G2.multicodec_prefix: BLS12381G1G2, + } + + def register(self, key_type: KeyType): + """Register a new key type.""" + self._type_registry[key_type.key_type] = key_type + self._name_registry[key_type.multicodec_name] = key_type + self._prefix_registry[key_type.multicodec_prefix] = key_type + + def from_multicodec_name(self, multicodec_name: str) -> Optional["KeyType"]: """Get KeyType instance based on multicodec name. Returns None if not found.""" - for key_type in KeyType: - if key_type.multicodec_name == multicodec_name: - return key_type - - return None + return self._name_registry.get(multicodec_name) - @classmethod - def from_multicodec_prefix(cls, multicodec_prefix: bytes) -> Optional["KeyType"]: + def from_multicodec_prefix(self, multicodec_prefix: bytes) -> Optional["KeyType"]: """Get KeyType instance based on multicodec prefix. Returns None if not found.""" - for key_type in KeyType: - if key_type.multicodec_prefix == multicodec_prefix: - return key_type + return self._prefix_registry.get(multicodec_prefix) - return None - - @classmethod - def from_prefixed_bytes(cls, prefixed_bytes: bytes) -> Optional["KeyType"]: + def from_prefixed_bytes(self, prefixed_bytes: bytes) -> Optional["KeyType"]: """Get KeyType instance based on prefix in bytes. Returns None if not found.""" - for key_type in KeyType: - if prefixed_bytes.startswith(key_type.multicodec_prefix): - return key_type - - return None - - @classmethod - def from_key_type(cls, key_type: str) -> Optional["KeyType"]: + return next( + ( + key_type + for key_type in self._name_registry.values() + if prefixed_bytes.startswith(key_type.multicodec_prefix) + ), + None, + ) + + def from_key_type(self, key_type: str) -> Optional["KeyType"]: """Get KeyType instance from the key type identifier.""" - for _key_type in KeyType: - if _key_type.key_type == key_type: - return _key_type - - return None + return self._type_registry.get(key_type) diff --git a/aries_cloudagent/wallet/models/wallet_record.py b/aries_cloudagent/wallet/models/wallet_record.py index 3421968d34..b81866839a 100644 --- a/aries_cloudagent/wallet/models/wallet_record.py +++ b/aries_cloudagent/wallet/models/wallet_record.py @@ -39,11 +39,13 @@ def __init__( # MTODO: how to make this a tag without making it # a constructor param wallet_name: str = None, + jwt_iat: Optional[int] = None, **kwargs, ): """Initialize a new WalletRecord.""" super().__init__(wallet_id, **kwargs) self.key_management_mode = key_management_mode + self.jwt_iat = jwt_iat self._settings = settings @property @@ -81,11 +83,17 @@ def wallet_key(self) -> Optional[str]: """Accessor for the key of the wallet.""" return self.settings.get("wallet.key") + @property + def wallet_key_derivation_method(self): + """Accessor for the key derivation method of the wallet.""" + return self.settings.get("wallet.key_derivation_method") + @property def record_value(self) -> dict: """Accessor for the JSON record value generated for this record.""" return { - prop: getattr(self, prop) for prop in ("settings", "key_management_mode") + prop: getattr(self, prop) + for prop in ("settings", "key_management_mode", "jwt_iat") } @property diff --git a/aries_cloudagent/wallet/routes.py b/aries_cloudagent/wallet/routes.py index 62002f1bb6..7ec93334c5 100644 --- a/aries_cloudagent/wallet/routes.py +++ b/aries_cloudagent/wallet/routes.py @@ -1,36 +1,50 @@ """Wallet admin routes.""" -from aiohttp import web -from aiohttp_apispec import ( - docs, - querystring_schema, - request_schema, - response_schema, -) +import json +import logging +from typing import List +from aiohttp import web +from aiohttp_apispec import docs, querystring_schema, request_schema, response_schema from marshmallow import fields, validate from ..admin.request_context import AdminRequestContext +from ..connections.models.conn_record import ConnRecord +from ..core.event_bus import Event, EventBus +from ..core.profile import Profile from ..ledger.base import BaseLedger from ..ledger.endpoint_type import EndpointType from ..ledger.error import LedgerConfigError, LedgerError +from ..messaging.models.base import BaseModelError from ..messaging.models.openapi import OpenAPISchema +from ..messaging.responder import BaseResponder from ..messaging.valid import ( DID_POSTURE, - INDY_OR_KEY_DID, - INDY_DID, ENDPOINT, ENDPOINT_TYPE, + INDY_DID, INDY_RAW_PUBLIC_KEY, + GENERIC_DID, ) -from ..multitenant.base import BaseMultitenantManager - +from ..protocols.coordinate_mediation.v1_0.route_manager import RouteManager +from ..protocols.endorse_transaction.v1_0.manager import ( + TransactionManager, + TransactionManagerError, +) +from ..protocols.endorse_transaction.v1_0.util import ( + get_endorser_connection_id, + is_author_role, +) +from ..storage.error import StorageError, StorageNotFoundError from .base import BaseWallet from .did_info import DIDInfo +from .did_method import SOV, KEY, DIDMethod, DIDMethods, HolderDefinedDid from .did_posture import DIDPosture -from .did_method import DIDMethod from .error import WalletError, WalletNotFoundError -from .key_type import KeyType +from .key_type import BLS12381G2, ED25519, KeyTypes +from .util import EVENT_LISTENER_PATTERN + +LOGGER = logging.getLogger(__name__) class WalletModuleResponseSchema(OpenAPISchema): @@ -40,7 +54,7 @@ class WalletModuleResponseSchema(OpenAPISchema): class DIDSchema(OpenAPISchema): """Result schema for a DID.""" - did = fields.Str(description="DID of interest", **INDY_OR_KEY_DID) + did = fields.Str(description="DID of interest", **GENERIC_DID) verkey = fields.Str(description="Public verification key", **INDY_RAW_PUBLIC_KEY) posture = fields.Str( description=( @@ -51,16 +65,12 @@ class DIDSchema(OpenAPISchema): **DID_POSTURE, ) method = fields.Str( - description="Did method associated with the DID", - example=DIDMethod.SOV.method_name, - validate=validate.OneOf([method.method_name for method in DIDMethod]), + description="Did method associated with the DID", example=SOV.method_name ) key_type = fields.Str( description="Key type associated with the DID", - example=KeyType.ED25519.key_type, - validate=validate.OneOf( - [KeyType.ED25519.key_type, KeyType.BLS12381G2.key_type] - ), + example=ED25519.key_type, + validate=validate.OneOf([ED25519.key_type, BLS12381G2.key_type]), ) @@ -105,7 +115,7 @@ class DIDEndpointSchema(OpenAPISchema): class DIDListQueryStringSchema(OpenAPISchema): """Parameters and validators for DID list request query string.""" - did = fields.Str(description="DID of interest", required=False, **INDY_OR_KEY_DID) + did = fields.Str(description="DID of interest", required=False, **GENERIC_DID) verkey = fields.Str( description="Verification key of interest", required=False, @@ -122,16 +132,14 @@ class DIDListQueryStringSchema(OpenAPISchema): ) method = fields.Str( required=False, - example=DIDMethod.KEY.method_name, - validate=validate.OneOf([DIDMethod.KEY.method_name, DIDMethod.SOV.method_name]), + example=KEY.method_name, + validate=validate.OneOf([KEY.method_name, SOV.method_name]), description="DID method to query for. e.g. sov to only fetch indy/sov DIDs", ) key_type = fields.Str( required=False, - example=KeyType.ED25519.key_type, - validate=validate.OneOf( - [KeyType.ED25519.key_type, KeyType.BLS12381G2.key_type] - ), + example=ED25519.key_type, + validate=validate.OneOf([ED25519.key_type, BLS12381G2.key_type]), description="Key type to query for.", ) @@ -147,10 +155,17 @@ class DIDCreateOptionsSchema(OpenAPISchema): key_type = fields.Str( required=True, - example=KeyType.ED25519.key_type, - validate=validate.OneOf( - [KeyType.ED25519.key_type, KeyType.BLS12381G2.key_type] - ), + example=ED25519.key_type, + description="Key type to use for the DID keypair. " + + "Validated with the chosen DID method's supported key types.", + validate=validate.OneOf([ED25519.key_type, BLS12381G2.key_type]), + ) + + did = fields.Str( + required=False, + description="Specify final value of the did (including did:: prefix)" + + "if the method supports or requires so.", + **GENERIC_DID, ) @@ -159,18 +174,49 @@ class DIDCreateSchema(OpenAPISchema): method = fields.Str( required=False, - default=DIDMethod.SOV.method_name, - example=DIDMethod.SOV.method_name, - validate=validate.OneOf([DIDMethod.KEY.method_name, DIDMethod.SOV.method_name]), + default=SOV.method_name, + example=SOV.method_name, + description="Method for the requested DID." + + "Supported methods are 'key', 'sov', and any other registered method.", ) options = fields.Nested( DIDCreateOptionsSchema, required=False, - description="To define a key type for a did:key", + description="To define a key type and/or a did depending on chosen DID method.", + ) + + seed = fields.Str( + required=False, + description=( + "Optional seed to use for DID, Must be" + "enabled in configuration before use." + ), + example="000000000000000000000000Trustee1", ) +class CreateAttribTxnForEndorserOptionSchema(OpenAPISchema): + """Class for user to input whether to create a transaction for endorser or not.""" + + create_transaction_for_endorser = fields.Boolean( + description="Create Transaction For Endorser's signature", + required=False, + ) + + +class AttribConnIdMatchInfoSchema(OpenAPISchema): + """Path parameters and validators for request taking connection id.""" + + conn_id = fields.Str(description="Connection identifier", required=False) + + +class MediationIDSchema(OpenAPISchema): + """Class for user to optionally input a mediation_id.""" + + mediation_id = fields.Str(description="Mediation identifier", required=False) + + def format_did_info(info: DIDInfo): """Serialize a DIDInfo object.""" if info: @@ -200,12 +246,16 @@ async def wallet_did_list(request: web.BaseRequest): context: AdminRequestContext = request["context"] filter_did = request.query.get("did") filter_verkey = request.query.get("verkey") - filter_method = DIDMethod.from_method(request.query.get("method")) filter_posture = DIDPosture.get(request.query.get("posture")) - filter_key_type = KeyType.from_key_type(request.query.get("key_type")) results = [] async with context.session() as session: - wallet = session.inject_or(BaseWallet) + did_methods: DIDMethods = session.inject(DIDMethods) + filter_method: DIDMethod | None = did_methods.from_method( + request.query.get("method") + ) + key_types = session.inject(KeyTypes) + filter_key_type = key_types.from_key_type(request.query.get("key_type", "")) + wallet: BaseWallet | None = session.inject_or(BaseWallet) if not wallet: raise web.HTTPForbidden(reason="No wallet available") if filter_posture is DIDPosture.PUBLIC: @@ -309,26 +359,54 @@ async def wallet_create_did(request: web.BaseRequest): body = {} # set default method and key type for backwards compat - key_type = ( - KeyType.from_key_type(body.get("options", {}).get("key_type")) - or KeyType.ED25519 - ) - method = DIDMethod.from_method(body.get("method")) or DIDMethod.SOV - if not method.supports_key_type(key_type): - raise web.HTTPForbidden( - reason=( - f"method {method.method_name} does not" - f" support key type {key_type.key_type}" - ) - ) + seed = body.get("seed") or None + if seed and not context.settings.get("wallet.allow_insecure_seed"): + raise web.HTTPBadRequest(reason="Seed support is not enabled") info = None async with context.session() as session: + did_methods = session.inject(DIDMethods) + + method = did_methods.from_method(body.get("method", "sov")) + if not method: + raise web.HTTPForbidden( + reason=(f"method {body.get('method')} is not supported by the agent.") + ) + + key_types = session.inject(KeyTypes) + # set default method and key type for backwards compat + key_type = ( + key_types.from_key_type(body.get("options", {}).get("key_type", "")) + or ED25519 + ) + if not method.supports_key_type(key_type): + raise web.HTTPForbidden( + reason=( + f"method {method.method_name} does not" + f" support key type {key_type.key_type}" + ) + ) + + did = body.get("options", {}).get("did") + if method.holder_defined_did() == HolderDefinedDid.NO and did: + raise web.HTTPForbidden( + reason=( + f"method {method.method_name} does not" + f" support user-defined DIDs" + ) + ) + elif method.holder_defined_did() == HolderDefinedDid.REQUIRED and not did: + raise web.HTTPBadRequest( + reason=f"method {method.method_name} requires a user-defined DIDs" + ) + wallet = session.inject_or(BaseWallet) if not wallet: raise web.HTTPForbidden(reason="No wallet available") try: - info = await wallet.create_local_did(method=method, key_type=key_type) + info = await wallet.create_local_did( + method=method, key_type=key_type, seed=seed, did=did + ) except WalletError as err: raise web.HTTPBadRequest(reason=err.roll_up) from err @@ -365,6 +443,9 @@ async def wallet_get_public_did(request: web.BaseRequest): @docs(tags=["wallet"], summary="Assign the current public DID") @querystring_schema(DIDQueryStringSchema()) +@querystring_schema(CreateAttribTxnForEndorserOptionSchema()) +@querystring_schema(AttribConnIdMatchInfoSchema()) +@querystring_schema(MediationIDSchema()) @response_schema(DIDResultSchema, 200, description="") async def wallet_set_public_did(request: web.BaseRequest): """ @@ -378,66 +459,196 @@ async def wallet_set_public_did(request: web.BaseRequest): """ context: AdminRequestContext = request["context"] - async with context.session() as session: - wallet = session.inject_or(BaseWallet) - if not wallet: - raise web.HTTPForbidden(reason="No wallet available") + session = await context.session() + + outbound_handler = request["outbound_message_router"] + create_transaction_for_endorser = json.loads( + request.query.get("create_transaction_for_endorser", "false") + ) + write_ledger = not create_transaction_for_endorser + connection_id = request.query.get("conn_id") + attrib_def = None + + # check if we need to endorse + if is_author_role(context.profile): + # authors cannot write to the ledger + write_ledger = False + create_transaction_for_endorser = True + if not connection_id: + # author has not provided a connection id, so determine which to use + connection_id = await get_endorser_connection_id(context.profile) + if not connection_id: + raise web.HTTPBadRequest(reason="No endorser connection found") + + wallet = session.inject_or(BaseWallet) + if not wallet: + raise web.HTTPForbidden(reason="No wallet available") did = request.query.get("did") if not did: raise web.HTTPBadRequest(reason="Request query must include DID") - wallet_id = context.settings.get("wallet.id") info: DIDInfo = None - try: - ledger = context.profile.inject_or(BaseLedger) - if not ledger: - reason = "No ledger available" - if not context.settings.get_value("wallet.type"): - reason += ": missing wallet-type?" - raise web.HTTPForbidden(reason=reason) - - async with ledger: - if not await ledger.get_key_for_did(did): - raise web.HTTPNotFound(reason=f"DID {did} is not posted to the ledger") - did_info: DIDInfo = None - async with context.session() as session: - wallet = session.inject_or(BaseWallet) - did_info = await wallet.get_local_did(did) - info = await wallet.set_public_did(did_info) - if info: - # Publish endpoint if necessary - endpoint = did_info.metadata.get("endpoint") - if not endpoint: - async with context.session() as session: - wallet = session.inject_or(BaseWallet) - endpoint = context.settings.get("default_endpoint") - await wallet.set_did_endpoint(info.did, endpoint, ledger) - - async with ledger: - await ledger.update_endpoint_for_did(info.did, endpoint) - - # Multitenancy setup - multitenant_mgr = context.profile.inject_or(BaseMultitenantManager) - # Add multitenant relay mapping so implicit invitations are still routed - if multitenant_mgr and wallet_id: - await multitenant_mgr.add_key( - wallet_id, info.verkey, skip_if_exists=True - ) + mediation_id = request.query.get("mediation_id") + profile = context.profile + route_manager = profile.inject(RouteManager) + mediation_record = await route_manager.mediation_record_if_id( + profile=profile, mediation_id=mediation_id, or_default=True + ) + routing_keys = None + mediator_endpoint = None + if mediation_record: + routing_keys = mediation_record.routing_keys + mediator_endpoint = mediation_record.endpoint + try: + info, attrib_def = await promote_wallet_public_did( + context.profile, + context, + context.session, + did, + write_ledger=write_ledger, + connection_id=connection_id, + routing_keys=routing_keys, + mediator_endpoint=mediator_endpoint, + ) + except LookupError as err: + raise web.HTTPNotFound(reason=str(err)) from err + except PermissionError as err: + raise web.HTTPForbidden(reason=str(err)) from err except WalletNotFoundError as err: raise web.HTTPNotFound(reason=err.roll_up) from err except (LedgerError, WalletError) as err: raise web.HTTPBadRequest(reason=err.roll_up) from err - return web.json_response({"result": format_did_info(info)}) + if not create_transaction_for_endorser: + return web.json_response({"result": format_did_info(info)}) + + else: + transaction_mgr = TransactionManager(context.profile) + try: + transaction = await transaction_mgr.create_record( + messages_attach=attrib_def["signed_txn"], connection_id=connection_id + ) + except StorageError as err: + raise web.HTTPBadRequest(reason=err.roll_up) from err + + # if auto-request, send the request to the endorser + if context.settings.get_value("endorser.auto_request"): + try: + transaction, transaction_request = await transaction_mgr.create_request( + transaction=transaction, + # TODO see if we need to parameterize these params + # expires_time=expires_time, + # endorser_write_txn=endorser_write_txn, + ) + except (StorageError, TransactionManagerError) as err: + raise web.HTTPBadRequest(reason=err.roll_up) from err + + await outbound_handler(transaction_request, connection_id=connection_id) + + return web.json_response({"txn": transaction.serialize()}) + + +async def promote_wallet_public_did( + profile: Profile, + context: AdminRequestContext, + session_fn, + did: str, + write_ledger: bool = False, + connection_id: str = None, + routing_keys: List[str] = None, + mediator_endpoint: str = None, +) -> DIDInfo: + """Promote supplied DID to the wallet public DID.""" + info: DIDInfo = None + endorser_did = None + ledger = profile.inject_or(BaseLedger) + if not ledger: + reason = "No ledger available" + if not context.settings.get_value("wallet.type"): + reason += ": missing wallet-type?" + raise PermissionError(reason) + + async with ledger: + if not await ledger.get_key_for_did(did): + raise LookupError(f"DID {did} is not posted to the ledger") + + # check if we need to endorse + if is_author_role(profile): + # authors cannot write to the ledger + write_ledger = False + + # author has not provided a connection id, so determine which to use + if not connection_id: + connection_id = await get_endorser_connection_id(profile) + if not connection_id: + raise web.HTTPBadRequest(reason="No endorser connection found") + if not write_ledger: + try: + async with profile.session() as session: + connection_record = await ConnRecord.retrieve_by_id( + session, connection_id + ) + except StorageNotFoundError as err: + raise web.HTTPNotFound(reason=err.roll_up) from err + except BaseModelError as err: + raise web.HTTPBadRequest(reason=err.roll_up) from err + + async with profile.session() as session: + endorser_info = await connection_record.metadata_get( + session, "endorser_info" + ) + if not endorser_info: + raise web.HTTPForbidden( + reason="Endorser Info is not set up in " + "connection metadata for this connection record" + ) + if "endorser_did" not in endorser_info.keys(): + raise web.HTTPForbidden( + reason=' "endorser_did" is not set in "endorser_info"' + " in connection metadata for this connection record" + ) + endorser_did = endorser_info["endorser_did"] + + did_info: DIDInfo = None + attrib_def = None + async with session_fn() as session: + wallet = session.inject_or(BaseWallet) + did_info = await wallet.get_local_did(did) + info = await wallet.set_public_did(did_info) + + if info: + # Publish endpoint if necessary + endpoint = did_info.metadata.get("endpoint") + + if not endpoint: + async with session_fn() as session: + wallet = session.inject_or(BaseWallet) + endpoint = mediator_endpoint or context.settings.get("default_endpoint") + attrib_def = await wallet.set_did_endpoint( + info.did, + endpoint, + ledger, + write_ledger=write_ledger, + endorser_did=endorser_did, + routing_keys=routing_keys, + ) + + # Route the public DID + route_manager = profile.inject(RouteManager) + await route_manager.route_verkey(profile, info.verkey) + + return info, attrib_def @docs( tags=["wallet"], summary="Update endpoint in wallet and on ledger if posted to it" ) @request_schema(DIDEndpointWithTypeSchema) +@querystring_schema(CreateAttribTxnForEndorserOptionSchema()) +@querystring_schema(AttribConnIdMatchInfoSchema()) @response_schema(WalletModuleResponseSchema(), description="") async def wallet_set_did_endpoint(request: web.BaseRequest): """ @@ -447,20 +658,76 @@ async def wallet_set_did_endpoint(request: web.BaseRequest): request: aiohttp request object """ context: AdminRequestContext = request["context"] + + outbound_handler = request["outbound_message_router"] + body = await request.json() did = body["did"] endpoint = body.get("endpoint") endpoint_type = EndpointType.get( body.get("endpoint_type", EndpointType.ENDPOINT.w3c) ) + + create_transaction_for_endorser = json.loads( + request.query.get("create_transaction_for_endorser", "false") + ) + write_ledger = not create_transaction_for_endorser + endorser_did = None + connection_id = request.query.get("conn_id") + attrib_def = None + + # check if we need to endorse + if is_author_role(context.profile): + # authors cannot write to the ledger + write_ledger = False + create_transaction_for_endorser = True + if not connection_id: + # author has not provided a connection id, so determine which to use + connection_id = await get_endorser_connection_id(context.profile) + if not connection_id: + raise web.HTTPBadRequest(reason="No endorser connection found") + + if not write_ledger: + try: + async with context.session() as session: + connection_record = await ConnRecord.retrieve_by_id( + session, connection_id + ) + except StorageNotFoundError as err: + raise web.HTTPNotFound(reason=err.roll_up) from err + except BaseModelError as err: + raise web.HTTPBadRequest(reason=err.roll_up) from err + + async with context.session() as session: + endorser_info = await connection_record.metadata_get( + session, "endorser_info" + ) + if not endorser_info: + raise web.HTTPForbidden( + reason="Endorser Info is not set up in " + "connection metadata for this connection record" + ) + if "endorser_did" not in endorser_info.keys(): + raise web.HTTPForbidden( + reason=' "endorser_did" is not set in "endorser_info"' + " in connection metadata for this connection record" + ) + endorser_did = endorser_info["endorser_did"] + async with context.session() as session: wallet = session.inject_or(BaseWallet) if not wallet: raise web.HTTPForbidden(reason="No wallet available") - try: ledger = context.profile.inject_or(BaseLedger) - await wallet.set_did_endpoint(did, endpoint, ledger, endpoint_type) + attrib_def = await wallet.set_did_endpoint( + did, + endpoint, + ledger, + endpoint_type, + write_ledger=write_ledger, + endorser_did=endorser_did, + ) except WalletNotFoundError as err: raise web.HTTPNotFound(reason=err.roll_up) from err except LedgerConfigError as err: @@ -468,7 +735,32 @@ async def wallet_set_did_endpoint(request: web.BaseRequest): except (LedgerError, WalletError) as err: raise web.HTTPBadRequest(reason=err.roll_up) from err - return web.json_response({}) + if not create_transaction_for_endorser: + return web.json_response({}) + else: + transaction_mgr = TransactionManager(context.profile) + try: + transaction = await transaction_mgr.create_record( + messages_attach=attrib_def["signed_txn"], connection_id=connection_id + ) + except StorageError as err: + raise web.HTTPBadRequest(reason=err.roll_up) from err + + # if auto-request, send the request to the endorser + if context.settings.get_value("endorser.auto_request"): + try: + transaction, transaction_request = await transaction_mgr.create_request( + transaction=transaction, + # TODO see if we need to parameterize these params + # expires_time=expires_time, + # endorser_write_txn=endorser_write_txn, + ) + except (StorageError, TransactionManagerError) as err: + raise web.HTTPBadRequest(reason=err.roll_up) from err + + await outbound_handler(transaction_request, connection_id=connection_id) + + return web.json_response({"txn": transaction.serialize()}) @docs(tags=["wallet"], summary="Query DID endpoint in wallet") @@ -544,6 +836,77 @@ async def wallet_rotate_did_keypair(request: web.BaseRequest): return web.json_response({}) +def register_events(event_bus: EventBus): + """Subscribe to any events we need to support.""" + event_bus.subscribe(EVENT_LISTENER_PATTERN, on_register_nym_event) + + +async def on_register_nym_event(profile: Profile, event: Event): + """Handle any events we need to support.""" + + # after the nym record is written, promote to wallet public DID + if is_author_role(profile) and profile.context.settings.get_value( + "endorser.auto_promote_author_did" + ): + did = event.payload["did"] + connection_id = event.payload.get("connection_id") + try: + info, attrib_def = await promote_wallet_public_did( + profile, profile.context, profile.session, did, connection_id + ) + except Exception as err: + # log the error, but continue + LOGGER.exception( + "Error promoting to public DID: %s", + err, + ) + return + + transaction_mgr = TransactionManager(profile) + try: + transaction = await transaction_mgr.create_record( + messages_attach=attrib_def["signed_txn"], connection_id=connection_id + ) + except StorageError as err: + # log the error, but continue + LOGGER.exception( + "Error accepting endorser invitation/configuring endorser connection: %s", + err, + ) + return + + # if auto-request, send the request to the endorser + if profile.settings.get_value("endorser.auto_request"): + try: + transaction, transaction_request = await transaction_mgr.create_request( + transaction=transaction, + # TODO see if we need to parameterize these params + # expires_time=expires_time, + # endorser_write_txn=endorser_write_txn, + ) + except (StorageError, TransactionManagerError) as err: + # log the error, but continue + LOGGER.exception( + "Error creating endorser transaction request: %s", + err, + ) + + # TODO not sure how to get outbound_handler in an event ... + # await outbound_handler(transaction_request, connection_id=connection_id) + responder = profile.inject_or(BaseResponder) + if responder: + await responder.send( + transaction_request, + connection_id=connection_id, + ) + else: + LOGGER.warning( + "Configuration has no BaseResponder: cannot update " + "ATTRIB record on DID: %s", + did, + ) + + async def register(app: web.Application): """Register routes.""" diff --git a/aries_cloudagent/wallet/tests/test_askar_wallet.py b/aries_cloudagent/wallet/tests/test_askar_wallet.py deleted file mode 100644 index 26949b0e12..0000000000 --- a/aries_cloudagent/wallet/tests/test_askar_wallet.py +++ /dev/null @@ -1,240 +0,0 @@ -import pytest - -from asynctest import mock as async_mock - -from aries_askar import AskarError, AskarErrorCode - -from ...askar.profile import AskarProfileManager -from ...config.injection_context import InjectionContext -from ...core.in_memory import InMemoryProfile -from ...ledger.endpoint_type import EndpointType - -from ..base import BaseWallet -from ..did_method import DIDMethod -from ..in_memory import InMemoryWallet -from ..key_type import KeyType -from .. import askar as test_module - -from . import test_in_memory_wallet - - -@pytest.fixture() -async def in_memory_wallet(): - profile = InMemoryProfile.test_profile() - wallet = InMemoryWallet(profile) - yield wallet - - -@pytest.fixture() -async def wallet(): - context = InjectionContext() - profile = await AskarProfileManager().provision( - context, - { - # "auto_recreate": True, - # "auto_remove": True, - "name": ":memory:", - "key": await AskarProfileManager.generate_store_key(), - "key_derivation_method": "RAW", # much faster than using argon-hashed keys - }, - ) - async with profile.session() as session: - yield session.inject(BaseWallet) - del session - # this will block indefinitely if session or profile references remain - # await profile.close() - - -@pytest.mark.askar -class TestAskarWallet(test_in_memory_wallet.TestInMemoryWallet): - """Apply all InMemoryWallet tests against AskarWallet""" - - # overriding derived values - Askar follows bls signatures draft 4 in key generation - test_key_bls12381g2_did = "did:key:zUC74E9UD2W6Q1MgPexCEdpstiCsY1Vbnyqepygk7McZVce38L1tGX7qZ2SgY4Zz2m9FUB4Xb5cEHSujks9XeKDzqe4QzW3CyyJ1cv8iBLNqU61EfkBoW2yEkg6VgqHTDtANYRS" - test_bls12381g2_verkey = "pPbb9Lqs3PVTyiHM4h8fbQqxHjBPm1Hixb6vdW9kkjHEij4FZrigkaV1P5DjWTbcKxeeYfkQuZMmozRQV3tH1gXhCA972LAXMGSKH7jxz8sNJqrCR6o8asgXDeYZeL1W3p8" - - @pytest.mark.skip - @pytest.mark.asyncio - async def test_rotate_did_keypair_x(self, wallet): - info = await wallet.create_local_did( - DIDMethod.SOV, KeyType.ED25519, self.test_seed, self.test_did - ) - - with async_mock.patch.object( - indy.did, "replace_keys_start", async_mock.CoroutineMock() - ) as mock_repl_start: - mock_repl_start.side_effect = test_module.IndyError( - test_module.ErrorCode.CommonIOError, {"message": "outlier"} - ) - with pytest.raises(test_module.WalletError) as excinfo: - await wallet.rotate_did_keypair_start(self.test_did) - assert "outlier" in str(excinfo.value) - - with async_mock.patch.object( - indy.did, "replace_keys_apply", async_mock.CoroutineMock() - ) as mock_repl_apply: - mock_repl_apply.side_effect = test_module.IndyError( - test_module.ErrorCode.CommonIOError, {"message": "outlier"} - ) - with pytest.raises(test_module.WalletError) as excinfo: - await wallet.rotate_did_keypair_apply(self.test_did) - assert "outlier" in str(excinfo.value) - - @pytest.mark.skip - @pytest.mark.asyncio - async def test_create_signing_key_x(self, wallet): - with async_mock.patch.object( - indy.crypto, "create_key", async_mock.CoroutineMock() - ) as mock_create_key: - mock_create_key.side_effect = test_module.IndyError( - test_module.ErrorCode.CommonIOError, {"message": "outlier"} - ) - with pytest.raises(test_module.WalletError) as excinfo: - await wallet.create_signing_key() - assert "outlier" in str(excinfo.value) - - @pytest.mark.skip - @pytest.mark.asyncio - async def test_create_local_did_x(self, wallet): - with async_mock.patch.object( - indy.did, "create_and_store_my_did", async_mock.CoroutineMock() - ) as mock_create: - mock_create.side_effect = test_module.IndyError( - test_module.ErrorCode.CommonIOError, {"message": "outlier"} - ) - with pytest.raises(test_module.WalletError) as excinfo: - await wallet.create_local_did( - DIDMethod.SOV, - KeyType.ED25519, - ) - assert "outlier" in str(excinfo.value) - - @pytest.mark.asyncio - async def test_set_did_endpoint_ledger(self, wallet): - mock_ledger = async_mock.MagicMock( - read_only=False, update_endpoint_for_did=async_mock.CoroutineMock() - ) - info_pub = await wallet.create_public_did( - DIDMethod.SOV, - KeyType.ED25519, - ) - await wallet.set_did_endpoint(info_pub.did, "http://1.2.3.4:8021", mock_ledger) - mock_ledger.update_endpoint_for_did.assert_called_once_with( - info_pub.did, "http://1.2.3.4:8021", EndpointType.ENDPOINT - ) - info_pub2 = await wallet.get_public_did() - assert info_pub2.metadata["endpoint"] == "http://1.2.3.4:8021" - - with pytest.raises(test_module.LedgerConfigError) as excinfo: - await wallet.set_did_endpoint(info_pub.did, "http://1.2.3.4:8021", None) - assert "No ledger available" in str(excinfo.value) - - @pytest.mark.asyncio - async def test_set_did_endpoint_readonly_ledger(self, wallet): - mock_ledger = async_mock.MagicMock( - read_only=True, update_endpoint_for_did=async_mock.CoroutineMock() - ) - info_pub = await wallet.create_public_did( - DIDMethod.SOV, - KeyType.ED25519, - ) - await wallet.set_did_endpoint(info_pub.did, "http://1.2.3.4:8021", mock_ledger) - mock_ledger.update_endpoint_for_did.assert_not_called() - info_pub2 = await wallet.get_public_did() - assert info_pub2.metadata["endpoint"] == "http://1.2.3.4:8021" - - with pytest.raises(test_module.LedgerConfigError) as excinfo: - await wallet.set_did_endpoint(info_pub.did, "http://1.2.3.4:8021", None) - assert "No ledger available" in str(excinfo.value) - - @pytest.mark.skip - @pytest.mark.asyncio - async def test_get_signing_key_x(self, wallet): - with async_mock.patch.object( - indy.crypto, "get_key_metadata", async_mock.CoroutineMock() - ) as mock_signing: - mock_signing.side_effect = test_module.IndyError( - test_module.ErrorCode.CommonIOError, {"message": "outlier"} - ) - with pytest.raises(test_module.WalletError) as excinfo: - await wallet.get_signing_key(None) - assert "outlier" in str(excinfo.value) - - @pytest.mark.skip - @pytest.mark.asyncio - async def test_get_local_did_x(self, wallet): - with async_mock.patch.object( - indy.did, "get_my_did_with_meta", async_mock.CoroutineMock() - ) as mock_my: - mock_my.side_effect = test_module.IndyError( - test_module.ErrorCode.CommonIOError, {"message": "outlier"} - ) - with pytest.raises(test_module.WalletError) as excinfo: - await wallet.get_local_did(None) - assert "outlier" in str(excinfo.value) - - @pytest.mark.asyncio - async def test_verify_message_x(self, wallet): - with async_mock.patch.object( - test_module.Key, "verify_signature" - ) as mock_verify: - mock_verify.side_effect = test_module.AskarError( # outlier - AskarErrorCode.BACKEND, {"message": "outlier"} - ) - with pytest.raises(test_module.WalletError) as excinfo: - await wallet.verify_message( - b"hello world", - b"signature", - self.test_ed25519_verkey, - KeyType.ED25519, - ) - - @pytest.mark.asyncio - async def test_pack_message_x(self, wallet): - with async_mock.patch.object( - test_module, - "pack_message", - ) as mock_pack: - mock_pack.side_effect = AskarError( # outlier - AskarErrorCode.BACKEND, {"message": "outlier"} - ) - with pytest.raises(test_module.WalletError) as excinfo: - await wallet.pack_message( - b"hello world", - [ - self.test_ed25519_verkey, - ], - ) - - -@pytest.mark.askar -class TestWalletCompat: - """Tests for wallet compatibility.""" - - test_seed = "testseed000000000000000000000001" - test_did = "55GkHamhTU1ZbTbV2ab9DE" - test_verkey = "3Dn1SJNPaCXcvvJvSbsFWP2xaCjMom3can8CQNhWrTRx" - test_message = "test message" - - @pytest.mark.asyncio - async def test_compare_pack_unpack(self, in_memory_wallet, wallet): - """ - Ensure that python-based pack/unpack is compatible with indy-sdk implementation - """ - await in_memory_wallet.create_local_did( - DIDMethod.SOV, KeyType.ED25519, self.test_seed - ) - py_packed = await in_memory_wallet.pack_message( - self.test_message, [self.test_verkey], self.test_verkey - ) - - await wallet.create_local_did(DIDMethod.SOV, KeyType.ED25519, self.test_seed) - packed = await wallet.pack_message( - self.test_message, [self.test_verkey], self.test_verkey - ) - - py_unpacked, from_vk, to_vk = await in_memory_wallet.unpack_message(packed) - assert self.test_message == py_unpacked - - unpacked, from_vk, to_vk = await wallet.unpack_message(py_packed) - assert self.test_message == unpacked diff --git a/aries_cloudagent/wallet/tests/test_crypto.py b/aries_cloudagent/wallet/tests/test_crypto.py index 66f6fc770a..43b0d03452 100644 --- a/aries_cloudagent/wallet/tests/test_crypto.py +++ b/aries_cloudagent/wallet/tests/test_crypto.py @@ -3,7 +3,7 @@ from unittest import mock, TestCase -from ..key_type import KeyType +from ..key_type import BLS12381G1, ED25519 from ..error import WalletError from ...utils.jwe import JweRecipient from ..util import str_to_b64 @@ -32,9 +32,7 @@ def test_validate_seed(self): def test_seeds_keys(self): assert len(test_module.seed_to_did(SEED)) in (22, 23) - (public_key, secret_key) = test_module.create_keypair( - test_module.KeyType.ED25519 - ) + (public_key, secret_key) = test_module.create_keypair(test_module.ED25519) assert public_key assert secret_key @@ -156,28 +154,24 @@ def test_extract_pack_recipients_x(self): def test_sign_ed25519_x_multiple_messages(self): with self.assertRaises(WalletError) as context: - test_module.sign_message( - [b"message1", b"message2"], b"secret", KeyType.ED25519 - ) + test_module.sign_message([b"message1", b"message2"], b"secret", ED25519) assert "ed25519 can only sign a single message" in str(context.exception) def test_sign_x_unsupported_key_type(self): with self.assertRaises(WalletError) as context: - test_module.sign_message( - [b"message1", b"message2"], b"secret", KeyType.BLS12381G1 - ) + test_module.sign_message([b"message1", b"message2"], b"secret", BLS12381G1) assert "Unsupported key type: bls12381g1" in str(context.exception) def test_verify_ed25519_x_multiple_messages(self): with self.assertRaises(WalletError) as context: test_module.verify_signed_message( - [b"message1", b"message2"], b"signature", b"verkey", KeyType.ED25519 + [b"message1", b"message2"], b"signature", b"verkey", ED25519 ) assert "ed25519 can only verify a single message" in str(context.exception) def test_verify_x_unsupported_key_type(self): with self.assertRaises(WalletError) as context: test_module.verify_signed_message( - [b"message1", b"message2"], b"signature", b"verkey", KeyType.BLS12381G1 + [b"message1", b"message2"], b"signature", b"verkey", BLS12381G1 ) assert "Unsupported key type: bls12381g1" in str(context.exception) diff --git a/aries_cloudagent/wallet/tests/test_default_verification_key_strategy.py b/aries_cloudagent/wallet/tests/test_default_verification_key_strategy.py new file mode 100644 index 0000000000..1d610f53fe --- /dev/null +++ b/aries_cloudagent/wallet/tests/test_default_verification_key_strategy.py @@ -0,0 +1,37 @@ +from unittest import TestCase + +from aries_cloudagent.core.profile import Profile + +from aries_cloudagent.did.did_key import DIDKey + +from aries_cloudagent.wallet.default_verification_key_strategy import ( + DefaultVerificationKeyStrategy, +) + +TEST_DID_SOV = "did:sov:LjgpST2rjsoxYegQDRm7EL" +TEST_DID_KEY = "did:key:z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL" + + +class TestDefaultVerificationKeyStrategy(TestCase): + async def test_with_did_sov(self): + strategy = DefaultVerificationKeyStrategy() + assert ( + await strategy.get_verification_method_id_for_did(TEST_DID_SOV, Profile()) + == TEST_DID_SOV + "#key-1" + ) + + async def test_with_did_key(self): + strategy = DefaultVerificationKeyStrategy() + assert ( + await strategy.get_verification_method_id_for_did(TEST_DID_KEY, Profile()) + == DIDKey.from_did(TEST_DID_KEY).key_id + ) + + async def test_unsupported_did_method(self): + strategy = DefaultVerificationKeyStrategy() + assert ( + await strategy.get_verification_method_id_for_did( + "did:test:test", Profile() + ) + is None + ) diff --git a/aries_cloudagent/wallet/tests/test_did_method.py b/aries_cloudagent/wallet/tests/test_did_method.py index 2134d01555..a952844383 100644 --- a/aries_cloudagent/wallet/tests/test_did_method.py +++ b/aries_cloudagent/wallet/tests/test_did_method.py @@ -1,7 +1,7 @@ from unittest import TestCase -from ..key_type import KeyType +from ..key_type import BLS12381G1, BLS12381G1G2, BLS12381G2, ED25519, X25519, KeyTypes ED25519_PREFIX_BYTES = b"\xed\x01" BLS12381G1_PREFIX_BYTES = b"\xea\x01" @@ -24,89 +24,77 @@ class TestKeyType(TestCase): def test_from_multicodec_name(self): - - assert KeyType.from_multicodec_name(ED25519_MULTICODEC_NAME) == KeyType.ED25519 - assert KeyType.from_multicodec_name(X25519_MULTICODEC_NAME) == KeyType.X25519 - assert ( - KeyType.from_multicodec_name(BLS12381G1_MULTICODEC_NAME) - == KeyType.BLS12381G1 - ) + key_types = KeyTypes() + assert key_types.from_multicodec_name(ED25519_MULTICODEC_NAME) == ED25519 + assert key_types.from_multicodec_name(X25519_MULTICODEC_NAME) == X25519 + assert key_types.from_multicodec_name(BLS12381G1_MULTICODEC_NAME) == BLS12381G1 + assert key_types.from_multicodec_name(BLS12381G2_MULTICODEC_NAME) == BLS12381G2 assert ( - KeyType.from_multicodec_name(BLS12381G2_MULTICODEC_NAME) - == KeyType.BLS12381G2 + key_types.from_multicodec_name(BLS12381G1G2_MULTICODEC_NAME) == BLS12381G1G2 ) - assert ( - KeyType.from_multicodec_name(BLS12381G1G2_MULTICODEC_NAME) - == KeyType.BLS12381G1G2 - ) - assert KeyType.from_multicodec_name("non-existing") == None + assert key_types.from_multicodec_name("non-existing") == None def test_from_key_type(self): - - assert KeyType.from_key_type(ED25519_KEY_NAME) == KeyType.ED25519 - assert KeyType.from_key_type(X25519_KEY_NAME) == KeyType.X25519 - assert KeyType.from_key_type(BLS12381G1_KEY_NAME) == KeyType.BLS12381G1 - assert KeyType.from_key_type(BLS12381G2_KEY_NAME) == KeyType.BLS12381G2 - assert KeyType.from_key_type(BLS12381G1G2_KEY_NAME) == KeyType.BLS12381G1G2 - assert KeyType.from_key_type("non-existing") == None + key_types = KeyTypes() + assert key_types.from_key_type(ED25519_KEY_NAME) == ED25519 + assert key_types.from_key_type(X25519_KEY_NAME) == X25519 + assert key_types.from_key_type(BLS12381G1_KEY_NAME) == BLS12381G1 + assert key_types.from_key_type(BLS12381G2_KEY_NAME) == BLS12381G2 + assert key_types.from_key_type(BLS12381G1G2_KEY_NAME) == BLS12381G1G2 + assert key_types.from_key_type("non-existing") == None def test_from_multicodec_prefix(self): - - assert KeyType.from_multicodec_prefix(ED25519_PREFIX_BYTES) == KeyType.ED25519 - assert KeyType.from_multicodec_prefix(X25519_PREFIX_BYTES) == KeyType.X25519 + key_types = KeyTypes() + assert key_types.from_multicodec_prefix(ED25519_PREFIX_BYTES) == ED25519 + assert key_types.from_multicodec_prefix(X25519_PREFIX_BYTES) == X25519 + assert key_types.from_multicodec_prefix(BLS12381G1_PREFIX_BYTES) == BLS12381G1 + assert key_types.from_multicodec_prefix(BLS12381G2_PREFIX_BYTES) == BLS12381G2 assert ( - KeyType.from_multicodec_prefix(BLS12381G1_PREFIX_BYTES) - == KeyType.BLS12381G1 + key_types.from_multicodec_prefix(BLS12381G1G2_PREFIX_BYTES) == BLS12381G1G2 ) - assert ( - KeyType.from_multicodec_prefix(BLS12381G2_PREFIX_BYTES) - == KeyType.BLS12381G2 - ) - assert ( - KeyType.from_multicodec_prefix(BLS12381G1G2_PREFIX_BYTES) - == KeyType.BLS12381G1G2 - ) - assert KeyType.from_multicodec_prefix(b"\xef\x01") == None + assert key_types.from_multicodec_prefix(b"\xef\x01") == None def test_from_prefixed_bytes(self): - + key_types = KeyTypes() assert ( - KeyType.from_prefixed_bytes( + key_types.from_prefixed_bytes( b"".join([ED25519_PREFIX_BYTES, b"random-bytes"]) ) - == KeyType.ED25519 + == ED25519 ) assert ( - KeyType.from_prefixed_bytes( + key_types.from_prefixed_bytes( b"".join([X25519_PREFIX_BYTES, b"random-bytes"]) ) - == KeyType.X25519 + == X25519 ) assert ( - KeyType.from_prefixed_bytes( + key_types.from_prefixed_bytes( b"".join([BLS12381G1_PREFIX_BYTES, b"random-bytes"]) ) - == KeyType.BLS12381G1 + == BLS12381G1 ) assert ( - KeyType.from_prefixed_bytes( + key_types.from_prefixed_bytes( b"".join([BLS12381G2_PREFIX_BYTES, b"random-bytes"]) ) - == KeyType.BLS12381G2 + == BLS12381G2 ) assert ( - KeyType.from_prefixed_bytes( + key_types.from_prefixed_bytes( b"".join([BLS12381G1G2_PREFIX_BYTES, b"random-bytes"]) ) - == KeyType.BLS12381G1G2 + == BLS12381G1G2 ) assert ( - KeyType.from_prefixed_bytes(b"".join([b"\xef\x01", b"other-random-bytes"])) + key_types.from_prefixed_bytes( + b"".join([b"\xef\x01", b"other-random-bytes"]) + ) == None ) def test_properties(self): - key_type = KeyType.ED25519 + key_type = ED25519 assert key_type.key_type == ED25519_KEY_NAME assert key_type.multicodec_name == ED25519_MULTICODEC_NAME diff --git a/aries_cloudagent/wallet/tests/test_did_parameters_validation.py b/aries_cloudagent/wallet/tests/test_did_parameters_validation.py new file mode 100644 index 0000000000..73565f827f --- /dev/null +++ b/aries_cloudagent/wallet/tests/test_did_parameters_validation.py @@ -0,0 +1,83 @@ +import pytest + +from aries_cloudagent.wallet.did_method import DIDMethods, DIDMethod, HolderDefinedDid +from aries_cloudagent.wallet.did_parameters_validation import DIDParametersValidation +from aries_cloudagent.wallet.error import WalletError +from aries_cloudagent.wallet.key_type import ED25519, BLS12381G1 + + +@pytest.fixture +def did_methods_registry(): + return DIDMethods() + + +def test_validate_key_type_uses_didmethod_when_validating_key_type( + did_methods_registry, +): + # given + ed_method = DIDMethod("ed-method", [ED25519]) + did_methods_registry.register(ed_method) + did_validation = DIDParametersValidation(did_methods_registry) + + # when - then + assert did_validation.validate_key_type(ed_method, ED25519) is None + with pytest.raises(WalletError): + did_validation.validate_key_type(ed_method, BLS12381G1) + + +def test_validate_key_type_raises_exception_when_validating_unknown_did_method( + did_methods_registry, +): + # given + unknown_method = DIDMethod("unknown", []) + did_validation = DIDParametersValidation(did_methods_registry) + + # when - then + with pytest.raises(WalletError): + did_validation.validate_key_type(unknown_method, ED25519) + + +def test_set_did_raises_error_when_did_is_provided_and_method_doesnt_allow( + did_methods_registry, +): + # given + ed_method = DIDMethod( + "derived-did", [ED25519], holder_defined_did=HolderDefinedDid.NO + ) + did_methods_registry.register(ed_method) + did_validation = DIDParametersValidation(did_methods_registry) + + # when - then + with pytest.raises(WalletError): + did_validation.validate_or_derive_did( + ed_method, ED25519, b"verkey", "did:edward:self-defined" + ) + + +def test_validate_or_derive_did_raises_error_when_no_did_is_provided_and_method_requires_one( + did_methods_registry, +): + # given + ed_method = DIDMethod( + "self-defined-did", [ED25519], holder_defined_did=HolderDefinedDid.REQUIRED + ) + did_methods_registry.register(ed_method) + did_validation = DIDParametersValidation(did_methods_registry) + + # when - then + with pytest.raises(WalletError): + did_validation.validate_or_derive_did(ed_method, ED25519, b"verkey", did=None) + + +def test_validate_or_derive_did_raises_exception_when_validating_unknown_did_method( + did_methods_registry, +): + # given + unknown_method = DIDMethod("unknown", []) + did_validation = DIDParametersValidation(did_methods_registry) + + # when - then + with pytest.raises(WalletError): + did_validation.validate_or_derive_did( + unknown_method, ED25519, b"verkey", did=None + ) diff --git a/aries_cloudagent/wallet/tests/test_in_memory_wallet.py b/aries_cloudagent/wallet/tests/test_in_memory_wallet.py index 6554336908..2558f60914 100644 --- a/aries_cloudagent/wallet/tests/test_in_memory_wallet.py +++ b/aries_cloudagent/wallet/tests/test_in_memory_wallet.py @@ -1,21 +1,19 @@ -import pytest import time +import pytest + from ...core.in_memory import InMemoryProfile from ...messaging.decorators.signature_decorator import SignatureDecorator +from ...wallet.did_method import KEY, SOV, DIDMethods +from ...wallet.error import WalletDuplicateError, WalletError, WalletNotFoundError from ...wallet.in_memory import InMemoryWallet -from ...wallet.key_type import KeyType -from ...wallet.did_method import DIDMethod -from ...wallet.error import ( - WalletError, - WalletDuplicateError, - WalletNotFoundError, -) +from ...wallet.key_type import BLS12381G1, BLS12381G1G2, BLS12381G2, ED25519, X25519 @pytest.fixture() async def wallet(): profile = InMemoryProfile.test_profile() + profile.context.injector.bind_instance(DIDMethods, DIDMethods()) wallet = InMemoryWallet(profile) yield wallet @@ -45,56 +43,56 @@ class TestInMemoryWallet: @pytest.mark.asyncio async def test_create_signing_key_ed25519_random(self, wallet: InMemoryWallet): assert str(wallet) - info = await wallet.create_signing_key(KeyType.ED25519) + info = await wallet.create_signing_key(ED25519) assert info and info.verkey @pytest.mark.asyncio @pytest.mark.ursa_bbs_signatures async def test_create_signing_key_bls12381g2_random(self, wallet: InMemoryWallet): assert str(wallet) - info = await wallet.create_signing_key(KeyType.BLS12381G2) + info = await wallet.create_signing_key(BLS12381G2) assert info and info.verkey @pytest.mark.asyncio async def test_create_signing_key_ed25519_seeded(self, wallet: InMemoryWallet): - info = await wallet.create_signing_key(KeyType.ED25519, self.test_seed) + info = await wallet.create_signing_key(ED25519, self.test_seed) assert info.verkey == self.test_ed25519_verkey with pytest.raises(WalletDuplicateError): - await wallet.create_signing_key(KeyType.ED25519, self.test_seed) + await wallet.create_signing_key(ED25519, self.test_seed) with pytest.raises(WalletError): - await wallet.create_signing_key(KeyType.ED25519, "invalid-seed", None) + await wallet.create_signing_key(ED25519, "invalid-seed", None) @pytest.mark.asyncio @pytest.mark.ursa_bbs_signatures async def test_create_signing_key_bls12381g2_seeded(self, wallet: InMemoryWallet): - info = await wallet.create_signing_key(KeyType.BLS12381G2, self.test_seed) + info = await wallet.create_signing_key(BLS12381G2, self.test_seed) assert info.verkey == self.test_bls12381g2_verkey with pytest.raises(WalletDuplicateError): - await wallet.create_signing_key(KeyType.BLS12381G2, self.test_seed) + await wallet.create_signing_key(BLS12381G2, self.test_seed) with pytest.raises(WalletError): - await wallet.create_signing_key(KeyType.BLS12381G2, "invalid-seed", None) + await wallet.create_signing_key(BLS12381G2, "invalid-seed", None) @pytest.mark.asyncio async def test_create_signing_key_unsupported_key_type( self, wallet: InMemoryWallet ): with pytest.raises(WalletError): - await wallet.create_signing_key(KeyType.X25519) + await wallet.create_signing_key(X25519) with pytest.raises(WalletError): - await wallet.create_signing_key(KeyType.BLS12381G1) + await wallet.create_signing_key(BLS12381G1) with pytest.raises(WalletError): - await wallet.create_signing_key(KeyType.BLS12381G1G2) + await wallet.create_signing_key(BLS12381G1G2) @pytest.mark.asyncio async def test_signing_key_metadata(self, wallet: InMemoryWallet): info = await wallet.create_signing_key( - KeyType.ED25519, self.test_seed, self.test_metadata + ED25519, self.test_seed, self.test_metadata ) assert info.metadata == self.test_metadata info2 = await wallet.get_signing_key(self.test_ed25519_verkey) @@ -117,7 +115,7 @@ async def test_signing_key_metadata(self, wallet: InMemoryWallet): @pytest.mark.ursa_bbs_signatures async def test_signing_key_metadata_bls(self, wallet: InMemoryWallet): info = await wallet.create_signing_key( - KeyType.BLS12381G2, self.test_seed, self.test_metadata + BLS12381G2, self.test_seed, self.test_metadata ) assert info.metadata == self.test_metadata info2 = await wallet.get_signing_key(self.test_bls12381g2_verkey) @@ -130,20 +128,18 @@ async def test_signing_key_metadata_bls(self, wallet: InMemoryWallet): @pytest.mark.asyncio async def test_create_local_sov_random(self, wallet: InMemoryWallet): - info = await wallet.create_local_did(DIDMethod.SOV, KeyType.ED25519, None, None) + info = await wallet.create_local_did(SOV, ED25519, None, None) assert info and info.did and info.verkey @pytest.mark.asyncio async def test_create_local_key_random_ed25519(self, wallet: InMemoryWallet): - info = await wallet.create_local_did(DIDMethod.KEY, KeyType.ED25519, None, None) + info = await wallet.create_local_did(KEY, ED25519, None, None) assert info and info.did and info.verkey @pytest.mark.asyncio @pytest.mark.ursa_bbs_signatures async def test_create_local_key_random_bls12381g2(self, wallet: InMemoryWallet): - info = await wallet.create_local_did( - DIDMethod.KEY, KeyType.BLS12381G2, None, None - ) + info = await wallet.create_local_did(KEY, BLS12381G2, None, None) assert info and info.did and info.verkey @pytest.mark.asyncio @@ -151,65 +147,49 @@ async def test_create_local_incorrect_key_type_for_did_method( self, wallet: InMemoryWallet ): with pytest.raises(WalletError): - await wallet.create_local_did(DIDMethod.SOV, KeyType.BLS12381G2, None, None) + await wallet.create_local_did(SOV, BLS12381G2, None, None) @pytest.mark.asyncio async def test_create_local_sov_seeded(self, wallet: InMemoryWallet): - info = await wallet.create_local_did( - DIDMethod.SOV, KeyType.ED25519, self.test_seed, None - ) + info = await wallet.create_local_did(SOV, ED25519, self.test_seed, None) assert info.did == self.test_sov_did assert info.verkey == self.test_ed25519_verkey # should not raise WalletDuplicateError - same verkey - await wallet.create_local_did( - DIDMethod.SOV, KeyType.ED25519, self.test_seed, None - ) + await wallet.create_local_did(SOV, ED25519, self.test_seed, None) with pytest.raises(WalletError): - _ = await wallet.create_local_did( - DIDMethod.SOV, KeyType.ED25519, "invalid-seed", None - ) + _ = await wallet.create_local_did(SOV, ED25519, "invalid-seed", None) @pytest.mark.asyncio @pytest.mark.ursa_bbs_signatures async def test_create_local_key_seeded_bls12381g2(self, wallet: InMemoryWallet): - info = await wallet.create_local_did( - DIDMethod.KEY, KeyType.BLS12381G2, self.test_seed, None - ) + info = await wallet.create_local_did(KEY, BLS12381G2, self.test_seed, None) assert info.did == self.test_key_bls12381g2_did assert info.verkey == self.test_bls12381g2_verkey # should not raise WalletDuplicateError - same verkey - await wallet.create_local_did( - DIDMethod.KEY, KeyType.BLS12381G2, self.test_seed, None - ) + await wallet.create_local_did(KEY, BLS12381G2, self.test_seed, None) with pytest.raises(WalletError): - _ = await wallet.create_local_did( - DIDMethod.KEY, KeyType.BLS12381G2, "invalid-seed", None - ) + _ = await wallet.create_local_did(KEY, BLS12381G2, "invalid-seed", None) @pytest.mark.asyncio async def test_create_local_key_seeded_ed25519(self, wallet: InMemoryWallet): - info = await wallet.create_local_did( - DIDMethod.KEY, KeyType.ED25519, self.test_seed, None - ) + info = await wallet.create_local_did(KEY, ED25519, self.test_seed, None) assert info.did == self.test_key_ed25519_did assert info.verkey == self.test_ed25519_verkey # should not raise WalletDuplicateError - same verkey - await wallet.create_local_did( - DIDMethod.KEY, KeyType.ED25519, self.test_seed, None - ) + await wallet.create_local_did(KEY, ED25519, self.test_seed, None) with pytest.raises(WalletError): - _ = await wallet.create_local_did( - DIDMethod.KEY, KeyType.ED25519, "invalid-seed", None - ) + _ = await wallet.create_local_did(KEY, ED25519, "invalid-seed", None) @pytest.mark.asyncio async def test_rotate_did_keypair(self, wallet: InMemoryWallet): + if hasattr(wallet, "profile"): # check incase indysdkwallet is being used + wallet.profile.context.injector.bind_instance(DIDMethods, DIDMethods()) with pytest.raises(WalletNotFoundError): await wallet.rotate_did_keypair_start(self.test_sov_did) @@ -217,9 +197,9 @@ async def test_rotate_did_keypair(self, wallet: InMemoryWallet): await wallet.rotate_did_keypair_apply(self.test_sov_did) info = await wallet.create_local_did( - DIDMethod.SOV, KeyType.ED25519, self.test_seed, self.test_sov_did + SOV, ED25519, self.test_seed, self.test_sov_did ) - key_info = await wallet.create_local_did(DIDMethod.KEY, KeyType.ED25519) + key_info = await wallet.create_local_did(KEY, ED25519) with pytest.raises(WalletError): await wallet.rotate_did_keypair_apply(self.test_sov_did) @@ -238,26 +218,20 @@ async def test_rotate_did_keypair(self, wallet: InMemoryWallet): @pytest.mark.asyncio async def test_create_local_with_did(self, wallet: InMemoryWallet): - info = await wallet.create_local_did( - DIDMethod.SOV, KeyType.ED25519, None, self.test_sov_did - ) + info = await wallet.create_local_did(SOV, ED25519, None, self.test_sov_did) assert info.did == self.test_sov_did with pytest.raises(WalletDuplicateError): - await wallet.create_local_did( - DIDMethod.SOV, KeyType.ED25519, None, self.test_sov_did - ) + await wallet.create_local_did(SOV, ED25519, None, self.test_sov_did) with pytest.raises(WalletError) as context: - await wallet.create_local_did( - DIDMethod.KEY, KeyType.ED25519, None, "did:sov:random" - ) + await wallet.create_local_did(KEY, ED25519, None, "did:sov:random") assert "Not allowed to set DID for DID method 'key'" in str(context.value) @pytest.mark.asyncio async def test_local_verkey(self, wallet: InMemoryWallet): info = await wallet.create_local_did( - DIDMethod.SOV, KeyType.ED25519, self.test_seed, self.test_sov_did + SOV, ED25519, self.test_seed, self.test_sov_did ) assert info.did == self.test_sov_did assert info.verkey == self.test_ed25519_verkey @@ -273,7 +247,7 @@ async def test_local_verkey(self, wallet: InMemoryWallet): @pytest.mark.asyncio @pytest.mark.ursa_bbs_signatures async def test_local_verkey_bls12381g2(self, wallet: InMemoryWallet): - await wallet.create_local_did(DIDMethod.KEY, KeyType.BLS12381G2, self.test_seed) + await wallet.create_local_did(KEY, BLS12381G2, self.test_seed) bls_info_get = await wallet.get_local_did_for_verkey( self.test_bls12381g2_verkey ) @@ -288,8 +262,8 @@ async def test_local_verkey_bls12381g2(self, wallet: InMemoryWallet): @pytest.mark.asyncio async def test_local_metadata(self, wallet: InMemoryWallet): info = await wallet.create_local_did( - DIDMethod.SOV, - KeyType.ED25519, + SOV, + ED25519, self.test_seed, self.test_sov_did, self.test_metadata, @@ -318,8 +292,8 @@ async def test_local_metadata(self, wallet: InMemoryWallet): @pytest.mark.ursa_bbs_signatures async def test_local_metadata_bbs(self, wallet: InMemoryWallet): info = await wallet.create_local_did( - DIDMethod.KEY, - KeyType.BLS12381G2, + KEY, + BLS12381G2, self.test_seed, None, self.test_metadata, @@ -338,8 +312,8 @@ async def test_local_metadata_bbs(self, wallet: InMemoryWallet): @pytest.mark.asyncio async def test_create_public_did(self, wallet: InMemoryWallet): info = await wallet.create_local_did( - DIDMethod.SOV, - KeyType.ED25519, + SOV, + ED25519, self.test_seed, self.test_sov_did, self.test_metadata, @@ -350,8 +324,8 @@ async def test_create_public_did(self, wallet: InMemoryWallet): assert not posted info_public = await wallet.create_public_did( - DIDMethod.SOV, - KeyType.ED25519, + SOV, + ED25519, ) assert info_public.metadata.get("posted") posted = await wallet.get_posted_dids() @@ -359,8 +333,8 @@ async def test_create_public_did(self, wallet: InMemoryWallet): # test replace info_replace = await wallet.create_public_did( - DIDMethod.SOV, - KeyType.ED25519, + SOV, + ED25519, ) assert info_replace.metadata.get("posted") info_check = await wallet.get_local_did(info_public.did) @@ -372,33 +346,22 @@ async def test_create_public_did(self, wallet: InMemoryWallet): info_replace.did, } - @pytest.mark.asyncio - async def test_create_public_did_x_not_sov(self, wallet: InMemoryWallet): - with pytest.raises(WalletError) as context: - await wallet.create_public_did( - DIDMethod.KEY, - KeyType.ED25519, - ) - assert "Setting public DID is only allowed for did:sov DIDs" in str( - context.value - ) - @pytest.mark.asyncio async def test_create_public_did_x_unsupported_key_type_method( self, wallet: InMemoryWallet ): with pytest.raises(WalletError) as context: await wallet.create_public_did( - DIDMethod.SOV, - KeyType.BLS12381G2, + SOV, + BLS12381G2, ) assert "Invalid key type" in str(context.value) @pytest.mark.asyncio async def test_set_public_did(self, wallet: InMemoryWallet): info = await wallet.create_local_did( - DIDMethod.SOV, - KeyType.ED25519, + SOV, + ED25519, self.test_seed, self.test_sov_did, self.test_metadata, @@ -413,7 +376,7 @@ async def test_set_public_did(self, wallet: InMemoryWallet): assert info_same.did == info.did assert info_same.metadata.get("posted") - info_new = await wallet.create_local_did(DIDMethod.SOV, KeyType.ED25519) + info_new = await wallet.create_local_did(SOV, ED25519) assert info_new.did != info_same.did loc = await wallet.get_local_did(self.test_sov_did) @@ -426,43 +389,27 @@ async def test_set_public_did(self, wallet: InMemoryWallet): assert info_final.did == info_new.did assert info_final.metadata.get("posted") - @pytest.mark.asyncio - async def test_set_public_did_x_not_sov(self, wallet: InMemoryWallet): - info = await wallet.create_local_did( - DIDMethod.KEY, - KeyType.ED25519, - ) - with pytest.raises(WalletError) as context: - await wallet.set_public_did(info.did) - assert "Setting public DID is only allowed for did:sov DIDs" in str( - context.value - ) - @pytest.mark.asyncio async def test_sign_verify(self, wallet: InMemoryWallet): info = await wallet.create_local_did( - DIDMethod.SOV, KeyType.ED25519, self.test_seed, self.test_sov_did + SOV, ED25519, self.test_seed, self.test_sov_did ) message_bin = self.test_message.encode("ascii") signature = await wallet.sign_message(message_bin, info.verkey) assert signature == self.test_signature verify = await wallet.verify_message( - message_bin, signature, info.verkey, KeyType.ED25519 + message_bin, signature, info.verkey, ED25519 ) assert verify bad_sig = b"x" + signature[1:] - verify = await wallet.verify_message( - message_bin, bad_sig, info.verkey, KeyType.ED25519 - ) + verify = await wallet.verify_message(message_bin, bad_sig, info.verkey, ED25519) assert not verify bad_msg = b"x" + message_bin[1:] - verify = await wallet.verify_message( - bad_msg, signature, info.verkey, KeyType.ED25519 - ) + verify = await wallet.verify_message(bad_msg, signature, info.verkey, ED25519) assert not verify verify = await wallet.verify_message( - message_bin, signature, self.test_target_verkey, KeyType.ED25519 + message_bin, signature, self.test_target_verkey, ED25519 ) assert not verify @@ -478,46 +425,44 @@ async def test_sign_verify(self, wallet: InMemoryWallet): assert "Verkey not provided" in str(excinfo.value) with pytest.raises(WalletError) as excinfo: - await wallet.verify_message(message_bin, signature, None, KeyType.ED25519) + await wallet.verify_message(message_bin, signature, None, ED25519) assert "Verkey not provided" in str(excinfo.value) with pytest.raises(WalletError) as excinfo: - await wallet.verify_message(message_bin, None, info.verkey, KeyType.ED25519) + await wallet.verify_message(message_bin, None, info.verkey, ED25519) assert "Signature not provided" in str(excinfo.value) with pytest.raises(WalletError) as excinfo: - await wallet.verify_message(None, message_bin, info.verkey, KeyType.ED25519) + await wallet.verify_message(None, message_bin, info.verkey, ED25519) assert "Message not provided" in str(excinfo.value) @pytest.mark.asyncio @pytest.mark.ursa_bbs_signatures async def test_sign_verify_bbs(self, wallet: InMemoryWallet): - info = await wallet.create_local_did( - DIDMethod.KEY, KeyType.BLS12381G2, self.test_seed - ) + info = await wallet.create_local_did(KEY, BLS12381G2, self.test_seed) message_bin = self.test_message.encode("ascii") signature = await wallet.sign_message(message_bin, info.verkey) assert signature verify = await wallet.verify_message( - message_bin, signature, info.verkey, KeyType.BLS12381G2 + message_bin, signature, info.verkey, BLS12381G2 ) assert verify bad_msg = b"x" + message_bin[1:] verify = await wallet.verify_message( - bad_msg, signature, info.verkey, KeyType.BLS12381G2 + bad_msg, signature, info.verkey, BLS12381G2 ) assert not verify with pytest.raises(WalletError): bad_sig = b"x" + signature[1:] verify = await wallet.verify_message( - message_bin, bad_sig, info.verkey, KeyType.BLS12381G2 + message_bin, bad_sig, info.verkey, BLS12381G2 ) with pytest.raises(WalletError): await wallet.verify_message( - message_bin, signature, self.test_target_verkey, KeyType.BLS12381G2 + message_bin, signature, self.test_target_verkey, BLS12381G2 ) with pytest.raises(WalletError): @@ -532,28 +477,20 @@ async def test_sign_verify_bbs(self, wallet: InMemoryWallet): assert "Verkey not provided" in str(excinfo.value) with pytest.raises(WalletError) as excinfo: - await wallet.verify_message( - message_bin, signature, None, KeyType.BLS12381G2 - ) + await wallet.verify_message(message_bin, signature, None, BLS12381G2) assert "Verkey not provided" in str(excinfo.value) with pytest.raises(WalletError) as excinfo: - await wallet.verify_message( - message_bin, None, info.verkey, KeyType.BLS12381G2 - ) + await wallet.verify_message(message_bin, None, info.verkey, BLS12381G2) assert "Signature not provided" in str(excinfo.value) with pytest.raises(WalletError) as excinfo: - await wallet.verify_message( - None, message_bin, info.verkey, KeyType.BLS12381G2 - ) + await wallet.verify_message(None, message_bin, info.verkey, BLS12381G2) assert "Message not provided" in str(excinfo.value) @pytest.mark.asyncio async def test_pack_unpack(self, wallet: InMemoryWallet): - await wallet.create_local_did( - DIDMethod.SOV, KeyType.ED25519, self.test_seed, self.test_sov_did - ) + await wallet.create_local_did(SOV, ED25519, self.test_seed, self.test_sov_did) packed_anon = await wallet.pack_message( self.test_message, [self.test_ed25519_verkey] @@ -568,7 +505,7 @@ async def test_pack_unpack(self, wallet: InMemoryWallet): assert "Message not provided" in str(excinfo.value) await wallet.create_local_did( - DIDMethod.SOV, KeyType.ED25519, self.test_target_seed, self.test_target_did + SOV, ED25519, self.test_target_seed, self.test_target_did ) packed_auth = await wallet.pack_message( self.test_message, [self.test_target_verkey], self.test_ed25519_verkey @@ -587,7 +524,7 @@ async def test_pack_unpack(self, wallet: InMemoryWallet): @pytest.mark.asyncio async def test_signature_round_trip(self, wallet: InMemoryWallet): - key_info = await wallet.create_signing_key(KeyType.ED25519) + key_info = await wallet.create_signing_key(ED25519) msg = {"test": "signed field"} timestamp = int(time.time()) sig = await SignatureDecorator.create(msg, key_info.verkey, wallet, timestamp) @@ -600,8 +537,8 @@ async def test_signature_round_trip(self, wallet: InMemoryWallet): @pytest.mark.asyncio async def test_set_did_endpoint_x_not_sov(self, wallet: InMemoryWallet): info = await wallet.create_local_did( - DIDMethod.KEY, - KeyType.ED25519, + KEY, + ED25519, ) with pytest.raises(WalletError) as context: await wallet.set_did_endpoint( diff --git a/aries_cloudagent/wallet/tests/test_indy_wallet.py b/aries_cloudagent/wallet/tests/test_indy_wallet.py index fc6234256c..47ae72cdec 100644 --- a/aries_cloudagent/wallet/tests/test_indy_wallet.py +++ b/aries_cloudagent/wallet/tests/test_indy_wallet.py @@ -1,36 +1,34 @@ import json import os +from typing import cast import indy.anoncreds import indy.crypto import indy.did import indy.wallet import pytest - from asynctest import mock as async_mock -from ...core.in_memory import InMemoryProfile from ...config.injection_context import InjectionContext -from ...core.error import ProfileError, ProfileDuplicateError, ProfileNotFoundError +from ...core.error import ProfileDuplicateError, ProfileError, ProfileNotFoundError +from ...core.in_memory import InMemoryProfile from ...indy.sdk import wallet_setup as test_setup_module -from ...indy.sdk.profile import IndySdkProfileManager +from ...indy.sdk.profile import IndySdkProfile, IndySdkProfileManager from ...indy.sdk.wallet_setup import IndyWalletConfig from ...ledger.endpoint_type import EndpointType -from ...wallet.key_type import KeyType -from ...wallet.did_method import DIDMethod from ...ledger.indy import IndySdkLedgerPool - +from ...wallet.did_method import SOV, DIDMethods +from ...wallet.key_type import ED25519 from .. import indy as test_module from ..base import BaseWallet from ..in_memory import InMemoryWallet from ..indy import IndySdkWallet - from . import test_in_memory_wallet @pytest.fixture() async def in_memory_wallet(): - profile = InMemoryProfile.test_profile() + profile = InMemoryProfile.test_profile(bind={DIDMethods: DIDMethods()}) wallet = InMemoryWallet(profile) yield wallet @@ -40,16 +38,21 @@ async def wallet(): key = await IndySdkWallet.generate_wallet_key() context = InjectionContext() context.injector.bind_instance(IndySdkLedgerPool, IndySdkLedgerPool("name")) - profile = await IndySdkProfileManager().provision( - context, - { - "auto_recreate": True, - "auto_remove": True, - "name": "test-wallet", - "key": key, - "key_derivation_method": "RAW", # much slower tests with argon-hashed keys - }, - ) + context.injector.bind_instance(DIDMethods, DIDMethods()) + with async_mock.patch.object(IndySdkProfile, "_make_finalizer"): + profile = cast( + IndySdkProfile, + await IndySdkProfileManager().provision( + context, + { + "auto_recreate": True, + "auto_remove": True, + "name": "test-wallet", + "key": key, + "key_derivation_method": "RAW", # much slower tests with argon-hashed keys + }, + ), + ) async with profile.session() as session: yield session.inject(BaseWallet) await profile.close() @@ -62,7 +65,7 @@ class TestIndySdkWallet(test_in_memory_wallet.TestInMemoryWallet): @pytest.mark.asyncio async def test_rotate_did_keypair_x(self, wallet: IndySdkWallet): info = await wallet.create_local_did( - DIDMethod.SOV, KeyType.ED25519, self.test_seed, self.test_sov_did + SOV, ED25519, self.test_seed, self.test_sov_did ) with async_mock.patch.object( @@ -94,7 +97,7 @@ async def test_create_signing_key_x(self, wallet: IndySdkWallet): test_module.ErrorCode.CommonIOError, {"message": "outlier"} ) with pytest.raises(test_module.WalletError) as excinfo: - await wallet.create_signing_key(KeyType.ED25519) + await wallet.create_signing_key(ED25519) assert "outlier" in str(excinfo.value) @pytest.mark.asyncio @@ -106,7 +109,7 @@ async def test_create_local_did_x(self, wallet: IndySdkWallet): test_module.ErrorCode.CommonIOError, {"message": "outlier"} ) with pytest.raises(test_module.WalletError) as excinfo: - await wallet.create_local_did(DIDMethod.SOV, KeyType.ED25519) + await wallet.create_local_did(SOV, ED25519) assert "outlier" in str(excinfo.value) @pytest.mark.asyncio @@ -115,36 +118,63 @@ async def test_set_did_endpoint_ledger(self, wallet: IndySdkWallet): read_only=False, update_endpoint_for_did=async_mock.CoroutineMock() ) info_pub = await wallet.create_public_did( - DIDMethod.SOV, - KeyType.ED25519, + SOV, + ED25519, ) - await wallet.set_did_endpoint(info_pub.did, "http://1.2.3.4:8021", mock_ledger) + await wallet.set_did_endpoint(info_pub.did, "https://example.com", mock_ledger) mock_ledger.update_endpoint_for_did.assert_called_once_with( - info_pub.did, "http://1.2.3.4:8021", EndpointType.ENDPOINT + info_pub.did, + "https://example.com", + EndpointType.ENDPOINT, + endorser_did=None, + write_ledger=True, + routing_keys=None, ) info_pub2 = await wallet.get_public_did() - assert info_pub2.metadata["endpoint"] == "http://1.2.3.4:8021" + assert info_pub2.metadata["endpoint"] == "https://example.com" with pytest.raises(test_module.LedgerConfigError) as excinfo: - await wallet.set_did_endpoint(info_pub.did, "http://1.2.3.4:8021", None) + await wallet.set_did_endpoint(info_pub.did, "https://example.com", None) assert "No ledger available" in str(excinfo.value) + @pytest.mark.asyncio + async def test_set_did_endpoint_ledger_with_routing_keys( + self, wallet: IndySdkWallet + ): + routing_keys = ["3YJCx3TqotDWFGv7JMR5erEvrmgu5y4FDqjR7sKWxgXn"] + mock_ledger = async_mock.MagicMock( + read_only=False, update_endpoint_for_did=async_mock.CoroutineMock() + ) + info_pub = await wallet.create_public_did(SOV, ED25519) + await wallet.set_did_endpoint( + info_pub.did, "https://example.com", mock_ledger, routing_keys=routing_keys + ) + + mock_ledger.update_endpoint_for_did.assert_called_once_with( + info_pub.did, + "https://example.com", + EndpointType.ENDPOINT, + endorser_did=None, + write_ledger=True, + routing_keys=routing_keys, + ) + @pytest.mark.asyncio async def test_set_did_endpoint_readonly_ledger(self, wallet: IndySdkWallet): mock_ledger = async_mock.MagicMock( read_only=True, update_endpoint_for_did=async_mock.CoroutineMock() ) info_pub = await wallet.create_public_did( - DIDMethod.SOV, - KeyType.ED25519, + SOV, + ED25519, ) - await wallet.set_did_endpoint(info_pub.did, "http://1.2.3.4:8021", mock_ledger) + await wallet.set_did_endpoint(info_pub.did, "https://example.com", mock_ledger) mock_ledger.update_endpoint_for_did.assert_not_called() info_pub2 = await wallet.get_public_did() - assert info_pub2.metadata["endpoint"] == "http://1.2.3.4:8021" + assert info_pub2.metadata["endpoint"] == "https://example.com" with pytest.raises(test_module.LedgerConfigError) as excinfo: - await wallet.set_did_endpoint(info_pub.did, "http://1.2.3.4:8021", None) + await wallet.set_did_endpoint(info_pub.did, "https://example.com", None) assert "No ledger available" in str(excinfo.value) @pytest.mark.asyncio @@ -174,8 +204,8 @@ async def test_get_local_did_x(self, wallet: IndySdkWallet): @pytest.mark.asyncio async def test_replace_local_did_metadata_x(self, wallet: IndySdkWallet): info = await wallet.create_local_did( - DIDMethod.SOV, - KeyType.ED25519, + SOV, + ED25519, self.test_seed, self.test_sov_did, self.test_metadata, @@ -207,7 +237,7 @@ async def test_verify_message_x(self, wallet: IndySdkWallet): b"hello world", b"signature", self.test_ed25519_verkey, - KeyType.ED25519, + ED25519, ) assert "outlier" in str(excinfo.value) @@ -215,7 +245,7 @@ async def test_verify_message_x(self, wallet: IndySdkWallet): test_module.ErrorCode.CommonInvalidStructure ) assert not await wallet.verify_message( - b"hello world", b"signature", self.test_ed25519_verkey, KeyType.ED25519 + b"hello world", b"signature", self.test_ed25519_verkey, ED25519 ) @pytest.mark.asyncio @@ -250,14 +280,12 @@ async def test_compare_pack_unpack(self, in_memory_wallet, wallet: IndySdkWallet """ Ensure that python-based pack/unpack is compatible with indy-sdk implementation """ - await in_memory_wallet.create_local_did( - DIDMethod.SOV, KeyType.ED25519, self.test_seed - ) + await in_memory_wallet.create_local_did(SOV, ED25519, self.test_seed) py_packed = await in_memory_wallet.pack_message( self.test_message, [self.test_verkey], self.test_verkey ) - await wallet.create_local_did(DIDMethod.SOV, KeyType.ED25519, self.test_seed) + await wallet.create_local_did(SOV, ED25519, self.test_seed) packed = await wallet.pack_message( self.test_message, [self.test_verkey], self.test_verkey ) @@ -788,7 +816,7 @@ async def test_postgres_wallet_works(self): opened = await postgres_wallet.create_wallet() wallet = IndySdkWallet(opened) - await wallet.create_local_did(DIDMethod.SOV, KeyType.ED25519, self.test_seed) + await wallet.create_local_did(SOV, ED25519, self.test_seed) py_packed = await wallet.pack_message( self.test_message, [self.test_verkey], self.test_verkey ) @@ -830,7 +858,7 @@ async def test_postgres_wallet_scheme_works(self): assert "Wallet was not removed" in str(excinfo.value) wallet = IndySdkWallet(opened) - await wallet.create_local_did(DIDMethod.SOV, KeyType.ED25519, self.test_seed) + await wallet.create_local_did(SOV, ED25519, self.test_seed) py_packed = await wallet.pack_message( self.test_message, [self.test_verkey], self.test_verkey ) @@ -867,7 +895,7 @@ async def test_postgres_wallet_scheme2_works(self): opened = await postgres_wallet.create_wallet() wallet = IndySdkWallet(opened) - await wallet.create_local_did(DIDMethod.SOV, KeyType.ED25519, self.test_seed) + await wallet.create_local_did(SOV, ED25519, self.test_seed) py_packed = await wallet.pack_message( self.test_message, [self.test_verkey], self.test_verkey ) diff --git a/aries_cloudagent/wallet/tests/test_key_pair.py b/aries_cloudagent/wallet/tests/test_key_pair.py index b81062d856..6d0ddccb67 100644 --- a/aries_cloudagent/wallet/tests/test_key_pair.py +++ b/aries_cloudagent/wallet/tests/test_key_pair.py @@ -4,7 +4,7 @@ from ...storage.error import StorageNotFoundError from ..util import bytes_to_b58 -from ..key_type import KeyType +from ..key_type import ED25519 from ...core.in_memory import InMemoryProfile from ...storage.in_memory import InMemoryStorage from ..key_pair import KeyPairStorageManager, KEY_PAIR_STORAGE_TYPE @@ -23,7 +23,7 @@ async def test_create_key_pair(self): await self.key_pair_mgr.store_key_pair( public_key=self.test_public_key, secret_key=self.test_secret, - key_type=KeyType.ED25519, + key_type=ED25519, ) verkey = bytes_to_b58(self.test_public_key) @@ -34,17 +34,17 @@ async def test_create_key_pair(self): value = json.loads(record.value) - assert record.tags == {"verkey": verkey, "key_type": KeyType.ED25519.key_type} + assert record.tags == {"verkey": verkey, "key_type": ED25519.key_type} assert value["verkey"] == verkey assert value["secret_key"] == bytes_to_b58(self.test_secret) assert value["metadata"] == {} - assert value["key_type"] == KeyType.ED25519.key_type + assert value["key_type"] == ED25519.key_type async def test_get_key_pair(self): await self.key_pair_mgr.store_key_pair( public_key=self.test_public_key, secret_key=self.test_secret, - key_type=KeyType.ED25519, + key_type=ED25519, ) verkey = bytes_to_b58(self.test_public_key) @@ -54,7 +54,7 @@ async def test_get_key_pair(self): assert key_pair["verkey"] == verkey assert key_pair["secret_key"] == bytes_to_b58(self.test_secret) assert key_pair["metadata"] == {} - assert key_pair["key_type"] == KeyType.ED25519.key_type + assert key_pair["key_type"] == ED25519.key_type async def test_get_key_pair_x_not_found(self): with self.assertRaises(StorageNotFoundError): @@ -64,7 +64,7 @@ async def test_delete_key_pair(self): await self.key_pair_mgr.store_key_pair( public_key=self.test_public_key, secret_key=self.test_secret, - key_type=KeyType.ED25519, + key_type=ED25519, ) verkey = bytes_to_b58(self.test_public_key) @@ -86,7 +86,7 @@ async def test_update_key_pair_metadata(self): await self.key_pair_mgr.store_key_pair( public_key=self.test_public_key, secret_key=self.test_secret, - key_type=KeyType.ED25519, + key_type=ED25519, metadata={"some": "data"}, ) diff --git a/aries_cloudagent/wallet/tests/test_key_type.py b/aries_cloudagent/wallet/tests/test_key_type.py index 0dc025a099..67730cbc55 100644 --- a/aries_cloudagent/wallet/tests/test_key_type.py +++ b/aries_cloudagent/wallet/tests/test_key_type.py @@ -1,41 +1,49 @@ from unittest import TestCase from ...core.error import BaseError -from ..did_method import DIDMethod -from ..key_type import KeyType +from ..did_method import KEY, SOV, DIDMethods +from ..key_type import BLS12381G2, ED25519 SOV_DID_METHOD_NAME = "sov" -SOV_SUPPORTED_KEY_TYPES = [KeyType.ED25519] +SOV_SUPPORTED_KEY_TYPES = [ED25519] KEY_DID_METHOD_NAME = "key" class TestDidMethod(TestCase): + """TestCases for did method""" + + did_methods = DIDMethods() + def test_from_metadata(self): - assert DIDMethod.from_metadata({"method": SOV_DID_METHOD_NAME}) == DIDMethod.SOV - assert DIDMethod.from_metadata({"method": KEY_DID_METHOD_NAME}) == DIDMethod.KEY + """Testing 'from_metadata'""" + assert self.did_methods.from_metadata({"method": SOV_DID_METHOD_NAME}) == SOV + assert self.did_methods.from_metadata({"method": KEY_DID_METHOD_NAME}) == KEY # test backwards compat - assert DIDMethod.from_metadata({}) == DIDMethod.SOV + assert self.did_methods.from_metadata({}) == SOV def test_from_method(self): - assert DIDMethod.from_method(SOV_DID_METHOD_NAME) == DIDMethod.SOV - assert DIDMethod.from_method(KEY_DID_METHOD_NAME) == DIDMethod.KEY - assert DIDMethod.from_method("random") == None + """Testing 'from_method'""" + assert self.did_methods.from_method(SOV_DID_METHOD_NAME) == SOV + assert self.did_methods.from_method(KEY_DID_METHOD_NAME) == KEY + assert self.did_methods.from_method("random") is None def test_from_did(self): - assert DIDMethod.from_did(f"did:{SOV_DID_METHOD_NAME}:xxxx") == DIDMethod.SOV - assert DIDMethod.from_did(f"did:{KEY_DID_METHOD_NAME}:xxxx") == DIDMethod.KEY + """Testing 'from_did'""" + assert self.did_methods.from_did(f"did:{SOV_DID_METHOD_NAME}:xxxx") == SOV + assert self.did_methods.from_did(f"did:{KEY_DID_METHOD_NAME}:xxxx") == KEY with self.assertRaises(BaseError) as context: - DIDMethod.from_did("did:unknown:something") + self.did_methods.from_did("did:unknown:something") assert "Unsupported did method: unknown" in str(context.exception) def test_properties(self): - method = DIDMethod.SOV + """Testing 'properties'""" + method = SOV assert method.method_name == SOV_DID_METHOD_NAME assert method.supported_key_types == SOV_SUPPORTED_KEY_TYPES - assert method.supports_rotation == True + assert method.supports_rotation is True - assert method.supports_key_type(KeyType.ED25519) == True - assert method.supports_key_type(KeyType.BLS12381G2) == False + assert method.supports_key_type(ED25519) == True + assert method.supports_key_type(BLS12381G2) == False diff --git a/aries_cloudagent/wallet/tests/test_routes.py b/aries_cloudagent/wallet/tests/test_routes.py index 33df4b553a..c4ac8839c1 100644 --- a/aries_cloudagent/wallet/tests/test_routes.py +++ b/aries_cloudagent/wallet/tests/test_routes.py @@ -1,21 +1,21 @@ -from asynctest import mock as async_mock, TestCase as AsyncTestCase +import mock as async_mock +import pytest from aiohttp.web import HTTPForbidden +from async_case import IsolatedAsyncioTestCase from ...admin.request_context import AdminRequestContext from ...core.in_memory import InMemoryProfile from ...ledger.base import BaseLedger -from ...multitenant.base import BaseMultitenantManager -from ...multitenant.manager import MultitenantManager -from ...wallet.key_type import KeyType -from ...wallet.did_method import DIDMethod - +from ...protocols.coordinate_mediation.v1_0.route_manager import RouteManager +from ...wallet.did_method import SOV, DIDMethods, DIDMethod, HolderDefinedDid +from ...wallet.key_type import ED25519, KeyTypes from .. import routes as test_module from ..base import BaseWallet from ..did_info import DIDInfo from ..did_posture import DIDPosture -class TestWalletRoutes(AsyncTestCase): +class TestWalletRoutes(IsolatedAsyncioTestCase): def setUp(self): self.wallet = async_mock.create_autospec(BaseWallet) self.session_inject = {BaseWallet: self.wallet} @@ -23,9 +23,10 @@ def setUp(self): self.context = AdminRequestContext.test_context( self.session_inject, self.profile ) + self.context.injector.bind_instance(KeyTypes, KeyTypes()) self.request_dict = { "context": self.context, - "outbound_message_router": async_mock.CoroutineMock(), + "outbound_message_router": async_mock.AsyncMock(), } self.request = async_mock.MagicMock( app={}, @@ -38,6 +39,8 @@ def setUp(self): self.test_verkey = "verkey" self.test_posted_did = "posted-did" self.test_posted_verkey = "posted-verkey" + self.did_methods = DIDMethods() + self.context.injector.bind_instance(DIDMethods, self.did_methods) async def test_missing_wallet(self): self.session_inject[BaseWallet] = None @@ -55,7 +58,7 @@ async def test_missing_wallet(self): await test_module.wallet_set_public_did(self.request) with self.assertRaises(HTTPForbidden): - self.request.json = async_mock.CoroutineMock( + self.request.json = async_mock.AsyncMock( return_value={ "did": self.test_did, "endpoint": "https://my-endpoint.ca:8020", @@ -71,8 +74,8 @@ def test_format_did_info(self): self.test_did, self.test_verkey, DIDPosture.WALLET_ONLY.metadata, - DIDMethod.SOV, - KeyType.ED25519, + SOV, + ED25519, ) result = test_module.format_did_info(did_info) assert ( @@ -85,8 +88,8 @@ def test_format_did_info(self): self.test_did, self.test_verkey, {"posted": True, "public": True}, - DIDMethod.SOV, - KeyType.ED25519, + SOV, + ED25519, ) result = test_module.format_did_info(did_info) assert result["posture"] == DIDPosture.PUBLIC.moniker @@ -95,8 +98,8 @@ def test_format_did_info(self): self.test_did, self.test_verkey, {"posted": True, "public": False}, - DIDMethod.SOV, - KeyType.ED25519, + SOV, + ED25519, ) result = test_module.format_did_info(did_info) assert result["posture"] == DIDPosture.POSTED.moniker @@ -109,8 +112,8 @@ async def test_create_did(self): self.test_did, self.test_verkey, DIDPosture.WALLET_ONLY.metadata, - DIDMethod.SOV, - KeyType.ED25519, + SOV, + ED25519, ) result = await test_module.wallet_create_did(self.request) json_response.assert_called_once_with( @@ -119,20 +122,73 @@ async def test_create_did(self): "did": self.test_did, "verkey": self.test_verkey, "posture": DIDPosture.WALLET_ONLY.moniker, - "key_type": KeyType.ED25519.key_type, - "method": DIDMethod.SOV.method_name, + "key_type": ED25519.key_type, + "method": SOV.method_name, } } ) assert result is json_response.return_value + async def test_create_did_unsupported_method(self): + self.request.json = async_mock.AsyncMock( + return_value={ + "method": "madeupmethod", + "options": {"key_type": "bls12381g2"}, + } + ) + + with self.assertRaises(test_module.web.HTTPForbidden): + await test_module.wallet_create_did(self.request) + async def test_create_did_unsupported_key_type(self): - self.request.json = async_mock.CoroutineMock( + self.request.json = async_mock.AsyncMock( return_value={"method": "sov", "options": {"key_type": "bls12381g2"}} ) with self.assertRaises(test_module.web.HTTPForbidden): await test_module.wallet_create_did(self.request) + async def test_create_did_method_requires_user_defined_did(self): + # given + did_custom = DIDMethod( + name="custom", + key_types=[ED25519], + rotation=True, + holder_defined_did=HolderDefinedDid.REQUIRED, + ) + self.did_methods.register(did_custom) + + self.request.json = async_mock.AsyncMock( + return_value={"method": "custom", "options": {"key_type": "ed25519"}} + ) + + # when - then + with self.assertRaises(test_module.web.HTTPBadRequest): + await test_module.wallet_create_did(self.request) + + async def test_create_did_method_doesnt_support_user_defined_did(self): + did_custom = DIDMethod( + name="custom", + key_types=[ED25519], + rotation=True, + holder_defined_did=HolderDefinedDid.NO, + ) + self.did_methods.register(did_custom) + + # when + self.request.json = async_mock.AsyncMock( + return_value={ + "method": "custom", + "options": { + "key_type": ED25519.key_type, + "did": "did:custom:aCustomUserDefinedDID", + }, + } + ) + + # then + with self.assertRaises(test_module.web.HTTPForbidden): + await test_module.wallet_create_did(self.request) + async def test_create_did_x(self): self.wallet.create_local_did.side_effect = test_module.WalletError() with self.assertRaises(test_module.web.HTTPBadRequest): @@ -147,15 +203,15 @@ async def test_did_list(self): self.test_did, self.test_verkey, DIDPosture.WALLET_ONLY.metadata, - DIDMethod.SOV, - KeyType.ED25519, + SOV, + ED25519, ), DIDInfo( self.test_posted_did, self.test_posted_verkey, DIDPosture.POSTED.metadata, - DIDMethod.SOV, - KeyType.ED25519, + SOV, + ED25519, ), ] result = await test_module.wallet_did_list(self.request) @@ -166,15 +222,15 @@ async def test_did_list(self): "did": self.test_posted_did, "verkey": self.test_posted_verkey, "posture": DIDPosture.POSTED.moniker, - "key_type": KeyType.ED25519.key_type, - "method": DIDMethod.SOV.method_name, + "key_type": ED25519.key_type, + "method": SOV.method_name, }, { "did": self.test_did, "verkey": self.test_verkey, "posture": DIDPosture.WALLET_ONLY.moniker, - "key_type": KeyType.ED25519.key_type, - "method": DIDMethod.SOV.method_name, + "key_type": ED25519.key_type, + "method": SOV.method_name, }, ] } @@ -191,16 +247,16 @@ async def test_did_list_filter_public(self): self.test_did, self.test_verkey, DIDPosture.PUBLIC.metadata, - DIDMethod.SOV, - KeyType.ED25519, + SOV, + ED25519, ) self.wallet.get_posted_dids.return_value = [ DIDInfo( self.test_posted_did, self.test_posted_verkey, DIDPosture.POSTED.metadata, - DIDMethod.SOV, - KeyType.ED25519, + SOV, + ED25519, ) ] result = await test_module.wallet_did_list(self.request) @@ -211,8 +267,8 @@ async def test_did_list_filter_public(self): "did": self.test_did, "verkey": self.test_verkey, "posture": DIDPosture.PUBLIC.moniker, - "key_type": KeyType.ED25519.key_type, - "method": DIDMethod.SOV.method_name, + "key_type": ED25519.key_type, + "method": SOV.method_name, } ] } @@ -229,8 +285,8 @@ async def test_did_list_filter_posted(self): self.test_did, self.test_verkey, DIDPosture.PUBLIC.metadata, - DIDMethod.SOV, - KeyType.ED25519, + SOV, + ED25519, ) self.wallet.get_posted_dids.return_value = [ DIDInfo( @@ -240,8 +296,8 @@ async def test_did_list_filter_posted(self): "posted": True, "public": False, }, - DIDMethod.SOV, - KeyType.ED25519, + SOV, + ED25519, ) ] result = await test_module.wallet_did_list(self.request) @@ -252,8 +308,8 @@ async def test_did_list_filter_posted(self): "did": self.test_posted_did, "verkey": self.test_posted_verkey, "posture": DIDPosture.POSTED.moniker, - "key_type": KeyType.ED25519.key_type, - "method": DIDMethod.SOV.method_name, + "key_type": ED25519.key_type, + "method": SOV.method_name, } ] } @@ -270,8 +326,8 @@ async def test_did_list_filter_did(self): self.test_did, self.test_verkey, DIDPosture.WALLET_ONLY.metadata, - DIDMethod.SOV, - KeyType.ED25519, + SOV, + ED25519, ) result = await test_module.wallet_did_list(self.request) json_response.assert_called_once_with( @@ -281,8 +337,8 @@ async def test_did_list_filter_did(self): "did": self.test_did, "verkey": self.test_verkey, "posture": DIDPosture.WALLET_ONLY.moniker, - "key_type": KeyType.ED25519.key_type, - "method": DIDMethod.SOV.method_name, + "key_type": ED25519.key_type, + "method": SOV.method_name, } ] } @@ -310,8 +366,8 @@ async def test_did_list_filter_verkey(self): self.test_did, self.test_verkey, DIDPosture.WALLET_ONLY.metadata, - DIDMethod.SOV, - KeyType.ED25519, + SOV, + ED25519, ) result = await test_module.wallet_did_list(self.request) json_response.assert_called_once_with( @@ -321,8 +377,8 @@ async def test_did_list_filter_verkey(self): "did": self.test_did, "verkey": self.test_verkey, "posture": DIDPosture.WALLET_ONLY.moniker, - "key_type": KeyType.ED25519.key_type, - "method": DIDMethod.SOV.method_name, + "key_type": ED25519.key_type, + "method": SOV.method_name, } ] } @@ -349,8 +405,8 @@ async def test_get_public_did(self): self.test_did, self.test_verkey, DIDPosture.PUBLIC.metadata, - DIDMethod.SOV, - KeyType.ED25519, + SOV, + ED25519, ) result = await test_module.wallet_get_public_did(self.request) json_response.assert_called_once_with( @@ -359,8 +415,8 @@ async def test_get_public_did(self): "did": self.test_did, "verkey": self.test_verkey, "posture": DIDPosture.PUBLIC.moniker, - "key_type": KeyType.ED25519.key_type, - "method": DIDMethod.SOV.method_name, + "key_type": ED25519.key_type, + "method": SOV.method_name, } } ) @@ -376,11 +432,19 @@ async def test_set_public_did(self): Ledger = async_mock.MagicMock() ledger = Ledger() - ledger.get_key_for_did = async_mock.CoroutineMock() - ledger.update_endpoint_for_did = async_mock.CoroutineMock() - ledger.__aenter__ = async_mock.CoroutineMock(return_value=ledger) + ledger.get_key_for_did = async_mock.AsyncMock() + ledger.update_endpoint_for_did = async_mock.AsyncMock() + ledger.__aenter__ = async_mock.AsyncMock(return_value=ledger) self.profile.context.injector.bind_instance(BaseLedger, ledger) + mock_route_manager = async_mock.MagicMock() + mock_route_manager.route_verkey = async_mock.AsyncMock() + mock_route_manager.mediation_record_if_id = async_mock.AsyncMock() + mock_route_manager.__aenter__ = async_mock.AsyncMock( + return_value=mock_route_manager + ) + self.profile.context.injector.bind_instance(RouteManager, mock_route_manager) + with async_mock.patch.object( test_module.web, "json_response", async_mock.Mock() ) as json_response: @@ -388,8 +452,8 @@ async def test_set_public_did(self): self.test_did, self.test_verkey, DIDPosture.PUBLIC.metadata, - DIDMethod.SOV, - KeyType.ED25519, + SOV, + ED25519, ) result = await test_module.wallet_set_public_did(self.request) self.wallet.set_public_did.assert_awaited_once() @@ -399,52 +463,24 @@ async def test_set_public_did(self): "did": self.test_did, "verkey": self.test_verkey, "posture": DIDPosture.PUBLIC.moniker, - "key_type": KeyType.ED25519.key_type, - "method": DIDMethod.SOV.method_name, + "key_type": ED25519.key_type, + "method": SOV.method_name, } } ) assert result is json_response.return_value - async def test_set_public_did_multitenant(self): - self.context.update_settings( - {"multitenant.enabled": True, "wallet.id": "test_wallet"} - ) - - self.request.query = {"did": self.test_did} - - Ledger = async_mock.MagicMock() - ledger = Ledger() - ledger.get_key_for_did = async_mock.CoroutineMock() - ledger.update_endpoint_for_did = async_mock.CoroutineMock() - ledger.__aenter__ = async_mock.CoroutineMock(return_value=ledger) - self.profile.context.injector.bind_instance(BaseLedger, ledger) - - multitenant_mgr = async_mock.MagicMock(MultitenantManager, autospec=True) - self.profile.context.injector.bind_instance( - BaseMultitenantManager, multitenant_mgr - ) - with async_mock.patch.object( - test_module.web, "json_response", async_mock.Mock() - ): - self.wallet.set_public_did.return_value = DIDInfo( - self.test_did, - self.test_verkey, - DIDPosture.PUBLIC.metadata, - DIDMethod.SOV, - KeyType.ED25519, - ) - await test_module.wallet_set_public_did(self.request) - - multitenant_mgr.add_key.assert_called_once_with( - "test_wallet", self.test_verkey, skip_if_exists=True - ) - async def test_set_public_did_no_query_did(self): with self.assertRaises(test_module.web.HTTPBadRequest): await test_module.wallet_set_public_did(self.request) async def test_set_public_did_no_ledger(self): + mock_route_manager = async_mock.MagicMock() + mock_route_manager.mediation_record_if_id = async_mock.AsyncMock() + mock_route_manager.__aenter__ = async_mock.AsyncMock( + return_value=mock_route_manager + ) + self.profile.context.injector.bind_instance(RouteManager, mock_route_manager) self.request.query = {"did": self.test_did} with self.assertRaises(test_module.web.HTTPForbidden): @@ -455,10 +491,17 @@ async def test_set_public_did_not_public(self): Ledger = async_mock.MagicMock() ledger = Ledger() - ledger.get_key_for_did = async_mock.CoroutineMock(return_value=None) - ledger.__aenter__ = async_mock.CoroutineMock(return_value=ledger) + ledger.get_key_for_did = async_mock.AsyncMock(return_value=None) + ledger.__aenter__ = async_mock.AsyncMock(return_value=ledger) self.profile.context.injector.bind_instance(BaseLedger, ledger) + mock_route_manager = async_mock.MagicMock() + mock_route_manager.mediation_record_if_id = async_mock.AsyncMock() + mock_route_manager.__aenter__ = async_mock.AsyncMock( + return_value=mock_route_manager + ) + self.profile.context.injector.bind_instance(RouteManager, mock_route_manager) + with self.assertRaises(test_module.web.HTTPNotFound): await test_module.wallet_set_public_did(self.request) @@ -467,10 +510,17 @@ async def test_set_public_did_not_found(self): Ledger = async_mock.MagicMock() ledger = Ledger() - ledger.get_key_for_did = async_mock.CoroutineMock(return_value=None) - ledger.__aenter__ = async_mock.CoroutineMock(return_value=ledger) + ledger.get_key_for_did = async_mock.AsyncMock(return_value=None) + ledger.__aenter__ = async_mock.AsyncMock(return_value=ledger) self.profile.context.injector.bind_instance(BaseLedger, ledger) + mock_route_manager = async_mock.MagicMock() + mock_route_manager.mediation_record_if_id = async_mock.AsyncMock() + mock_route_manager.__aenter__ = async_mock.AsyncMock( + return_value=mock_route_manager + ) + self.profile.context.injector.bind_instance(RouteManager, mock_route_manager) + self.wallet.get_local_did.side_effect = test_module.WalletNotFoundError() with self.assertRaises(test_module.web.HTTPNotFound): await test_module.wallet_set_public_did(self.request) @@ -480,10 +530,18 @@ async def test_set_public_did_x(self): Ledger = async_mock.MagicMock() ledger = Ledger() - ledger.update_endpoint_for_did = async_mock.CoroutineMock() - ledger.get_key_for_did = async_mock.CoroutineMock() - ledger.__aenter__ = async_mock.CoroutineMock(return_value=ledger) + ledger.update_endpoint_for_did = async_mock.AsyncMock() + ledger.get_key_for_did = async_mock.AsyncMock() + ledger.__aenter__ = async_mock.AsyncMock(return_value=ledger) self.profile.context.injector.bind_instance(BaseLedger, ledger) + + mock_route_manager = async_mock.MagicMock() + mock_route_manager.mediation_record_if_id = async_mock.AsyncMock() + mock_route_manager.__aenter__ = async_mock.AsyncMock( + return_value=mock_route_manager + ) + self.profile.context.injector.bind_instance(RouteManager, mock_route_manager) + with async_mock.patch.object( test_module.web, "json_response", async_mock.Mock() ) as json_response: @@ -491,8 +549,8 @@ async def test_set_public_did_x(self): self.test_did, self.test_verkey, DIDPosture.PUBLIC.metadata, - DIDMethod.SOV, - KeyType.ED25519, + SOV, + ED25519, ) self.wallet.set_public_did.side_effect = test_module.WalletError() with self.assertRaises(test_module.web.HTTPBadRequest): @@ -503,11 +561,18 @@ async def test_set_public_did_no_wallet_did(self): Ledger = async_mock.MagicMock() ledger = Ledger() - ledger.update_endpoint_for_did = async_mock.CoroutineMock() - ledger.get_key_for_did = async_mock.CoroutineMock() - ledger.__aenter__ = async_mock.CoroutineMock(return_value=ledger) + ledger.update_endpoint_for_did = async_mock.AsyncMock() + ledger.get_key_for_did = async_mock.AsyncMock() + ledger.__aenter__ = async_mock.AsyncMock(return_value=ledger) self.profile.context.injector.bind_instance(BaseLedger, ledger) + mock_route_manager = async_mock.MagicMock() + mock_route_manager.mediation_record_if_id = async_mock.AsyncMock() + mock_route_manager.__aenter__ = async_mock.AsyncMock( + return_value=mock_route_manager + ) + self.profile.context.injector.bind_instance(RouteManager, mock_route_manager) + with async_mock.patch.object( test_module.web, "json_response", async_mock.Mock() ) as json_response: @@ -515,8 +580,8 @@ async def test_set_public_did_no_wallet_did(self): self.test_did, self.test_verkey, DIDPosture.PUBLIC.metadata, - DIDMethod.SOV, - KeyType.ED25519, + SOV, + ED25519, ) self.wallet.set_public_did.side_effect = test_module.WalletNotFoundError() with self.assertRaises(test_module.web.HTTPNotFound): @@ -527,11 +592,19 @@ async def test_set_public_did_update_endpoint(self): Ledger = async_mock.MagicMock() ledger = Ledger() - ledger.update_endpoint_for_did = async_mock.CoroutineMock() - ledger.get_key_for_did = async_mock.CoroutineMock() - ledger.__aenter__ = async_mock.CoroutineMock(return_value=ledger) + ledger.update_endpoint_for_did = async_mock.AsyncMock() + ledger.get_key_for_did = async_mock.AsyncMock() + ledger.__aenter__ = async_mock.AsyncMock(return_value=ledger) self.profile.context.injector.bind_instance(BaseLedger, ledger) + mock_route_manager = async_mock.MagicMock() + mock_route_manager.route_verkey = async_mock.AsyncMock() + mock_route_manager.mediation_record_if_id = async_mock.AsyncMock() + mock_route_manager.__aenter__ = async_mock.AsyncMock( + return_value=mock_route_manager + ) + self.profile.context.injector.bind_instance(RouteManager, mock_route_manager) + with async_mock.patch.object( test_module.web, "json_response", async_mock.Mock() ) as json_response: @@ -539,8 +612,8 @@ async def test_set_public_did_update_endpoint(self): self.test_did, self.test_verkey, {**DIDPosture.PUBLIC.metadata, "endpoint": "https://endpoint.com"}, - DIDMethod.SOV, - KeyType.ED25519, + SOV, + ED25519, ) result = await test_module.wallet_set_public_did(self.request) self.wallet.set_public_did.assert_awaited_once() @@ -550,8 +623,8 @@ async def test_set_public_did_update_endpoint(self): "did": self.test_did, "verkey": self.test_verkey, "posture": DIDPosture.PUBLIC.moniker, - "key_type": KeyType.ED25519.key_type, - "method": DIDMethod.SOV.method_name, + "key_type": ED25519.key_type, + "method": SOV.method_name, } } ) @@ -565,11 +638,21 @@ async def test_set_public_did_update_endpoint_use_default_update_in_wallet(self) Ledger = async_mock.MagicMock() ledger = Ledger() - ledger.update_endpoint_for_did = async_mock.CoroutineMock() - ledger.get_key_for_did = async_mock.CoroutineMock() - ledger.__aenter__ = async_mock.CoroutineMock(return_value=ledger) + ledger.update_endpoint_for_did = async_mock.AsyncMock() + ledger.get_key_for_did = async_mock.AsyncMock() + ledger.__aenter__ = async_mock.AsyncMock(return_value=ledger) self.profile.context.injector.bind_instance(BaseLedger, ledger) + mock_route_manager = async_mock.MagicMock() + mock_route_manager.route_verkey = async_mock.AsyncMock() + mock_route_manager.mediation_record_if_id = async_mock.AsyncMock( + return_value=None + ) + mock_route_manager.__aenter__ = async_mock.AsyncMock( + return_value=mock_route_manager + ) + self.profile.context.injector.bind_instance(RouteManager, mock_route_manager) + with async_mock.patch.object( test_module.web, "json_response", async_mock.Mock() ) as json_response: @@ -577,15 +660,20 @@ async def test_set_public_did_update_endpoint_use_default_update_in_wallet(self) self.test_did, self.test_verkey, DIDPosture.PUBLIC.metadata, - DIDMethod.SOV, - KeyType.ED25519, + SOV, + ED25519, ) self.wallet.get_local_did.return_value = did_info self.wallet.set_public_did.return_value = did_info result = await test_module.wallet_set_public_did(self.request) self.wallet.set_public_did.assert_awaited_once() self.wallet.set_did_endpoint.assert_awaited_once_with( - did_info.did, "https://default_endpoint.com", ledger + did_info.did, + "https://default_endpoint.com", + ledger, + write_ledger=True, + endorser_did=None, + routing_keys=None, ) json_response.assert_called_once_with( { @@ -593,15 +681,15 @@ async def test_set_public_did_update_endpoint_use_default_update_in_wallet(self) "did": self.test_did, "verkey": self.test_verkey, "posture": DIDPosture.PUBLIC.moniker, - "key_type": KeyType.ED25519.key_type, - "method": DIDMethod.SOV.method_name, + "key_type": ED25519.key_type, + "method": SOV.method_name, } } ) assert result is json_response.return_value async def test_set_did_endpoint(self): - self.request.json = async_mock.CoroutineMock( + self.request.json = async_mock.AsyncMock( return_value={ "did": self.test_did, "endpoint": "https://my-endpoint.ca:8020", @@ -610,23 +698,23 @@ async def test_set_did_endpoint(self): Ledger = async_mock.MagicMock() ledger = Ledger() - ledger.update_endpoint_for_did = async_mock.CoroutineMock() - ledger.__aenter__ = async_mock.CoroutineMock(return_value=ledger) + ledger.update_endpoint_for_did = async_mock.AsyncMock() + ledger.__aenter__ = async_mock.AsyncMock(return_value=ledger) self.profile.context.injector.bind_instance(BaseLedger, ledger) self.wallet.get_local_did.return_value = DIDInfo( self.test_did, self.test_verkey, {"public": False, "endpoint": "http://old-endpoint.ca"}, - DIDMethod.SOV, - KeyType.ED25519, + SOV, + ED25519, ) self.wallet.get_public_did.return_value = DIDInfo( self.test_did, self.test_verkey, DIDPosture.PUBLIC.metadata, - DIDMethod.SOV, - KeyType.ED25519, + SOV, + ED25519, ) with async_mock.patch.object( @@ -636,7 +724,7 @@ async def test_set_did_endpoint(self): json_response.assert_called_once_with({}) async def test_set_did_endpoint_public_did_no_ledger(self): - self.request.json = async_mock.CoroutineMock( + self.request.json = async_mock.AsyncMock( return_value={ "did": self.test_did, "endpoint": "https://my-endpoint.ca:8020", @@ -647,15 +735,15 @@ async def test_set_did_endpoint_public_did_no_ledger(self): self.test_did, self.test_verkey, {"public": False, "endpoint": "http://old-endpoint.ca"}, - DIDMethod.SOV, - KeyType.ED25519, + SOV, + ED25519, ) self.wallet.get_public_did.return_value = DIDInfo( self.test_did, self.test_verkey, DIDPosture.PUBLIC.metadata, - DIDMethod.SOV, - KeyType.ED25519, + SOV, + ED25519, ) self.wallet.set_did_endpoint.side_effect = test_module.LedgerConfigError() @@ -663,7 +751,7 @@ async def test_set_did_endpoint_public_did_no_ledger(self): await test_module.wallet_set_did_endpoint(self.request) async def test_set_did_endpoint_x(self): - self.request.json = async_mock.CoroutineMock( + self.request.json = async_mock.AsyncMock( return_value={ "did": self.test_did, "endpoint": "https://my-endpoint.ca:8020", @@ -672,8 +760,8 @@ async def test_set_did_endpoint_x(self): Ledger = async_mock.MagicMock() ledger = Ledger() - ledger.update_endpoint_for_did = async_mock.CoroutineMock() - ledger.__aenter__ = async_mock.CoroutineMock(return_value=ledger) + ledger.update_endpoint_for_did = async_mock.AsyncMock() + ledger.__aenter__ = async_mock.AsyncMock(return_value=ledger) self.profile.context.injector.bind_instance(BaseLedger, ledger) self.wallet.set_did_endpoint.side_effect = test_module.WalletError() @@ -682,7 +770,7 @@ async def test_set_did_endpoint_x(self): await test_module.wallet_set_did_endpoint(self.request) async def test_set_did_endpoint_no_wallet_did(self): - self.request.json = async_mock.CoroutineMock( + self.request.json = async_mock.AsyncMock( return_value={ "did": self.test_did, "endpoint": "https://my-endpoint.ca:8020", @@ -691,8 +779,8 @@ async def test_set_did_endpoint_no_wallet_did(self): Ledger = async_mock.MagicMock() ledger = Ledger() - ledger.update_endpoint_for_did = async_mock.CoroutineMock() - ledger.__aenter__ = async_mock.CoroutineMock(return_value=ledger) + ledger.update_endpoint_for_did = async_mock.AsyncMock() + ledger.__aenter__ = async_mock.AsyncMock(return_value=ledger) self.profile.context.injector.bind_instance(BaseLedger, ledger) self.wallet.set_did_endpoint.side_effect = test_module.WalletNotFoundError() @@ -707,8 +795,8 @@ async def test_get_did_endpoint(self): self.test_did, self.test_verkey, {"public": False, "endpoint": "http://old-endpoint.ca"}, - DIDMethod.SOV, - KeyType.ED25519, + SOV, + ED25519, ) with async_mock.patch.object( @@ -750,17 +838,17 @@ async def test_rotate_did_keypair(self): with async_mock.patch.object( test_module.web, "json_response", async_mock.Mock() ) as json_response: - self.wallet.get_local_did = async_mock.CoroutineMock( + self.wallet.get_local_did = async_mock.AsyncMock( return_value=DIDInfo( "did", "verkey", {"public": False}, - DIDMethod.SOV, - KeyType.ED25519, + SOV, + ED25519, ) ) - self.wallet.rotate_did_keypair_start = async_mock.CoroutineMock() - self.wallet.rotate_did_keypair_apply = async_mock.CoroutineMock() + self.wallet.rotate_did_keypair_start = async_mock.AsyncMock() + self.wallet.rotate_did_keypair_apply = async_mock.AsyncMock() await test_module.wallet_rotate_did_keypair(self.request) json_response.assert_called_once_with({}) @@ -779,19 +867,19 @@ async def test_rotate_did_keypair_no_query_did(self): async def test_rotate_did_keypair_did_not_local(self): self.request.query = {"did": "did"} - self.wallet.get_local_did = async_mock.CoroutineMock( + self.wallet.get_local_did = async_mock.AsyncMock( side_effect=test_module.WalletNotFoundError("Unknown DID") ) with self.assertRaises(test_module.web.HTTPNotFound): await test_module.wallet_rotate_did_keypair(self.request) - self.wallet.get_local_did = async_mock.CoroutineMock( + self.wallet.get_local_did = async_mock.AsyncMock( return_value=DIDInfo( "did", "verkey", {"posted": True, "public": True}, - DIDMethod.SOV, - KeyType.ED25519, + SOV, + ED25519, ) ) with self.assertRaises(test_module.web.HTTPBadRequest): @@ -800,16 +888,16 @@ async def test_rotate_did_keypair_did_not_local(self): async def test_rotate_did_keypair_x(self): self.request.query = {"did": "did"} - self.wallet.get_local_did = async_mock.CoroutineMock( + self.wallet.get_local_did = async_mock.AsyncMock( return_value=DIDInfo( "did", "verkey", {"public": False}, - DIDMethod.SOV, - KeyType.ED25519, + SOV, + ED25519, ) ) - self.wallet.rotate_did_keypair_start = async_mock.CoroutineMock( + self.wallet.rotate_did_keypair_start = async_mock.AsyncMock( side_effect=test_module.WalletError() ) with self.assertRaises(test_module.web.HTTPBadRequest): diff --git a/aries_cloudagent/wallet/util.py b/aries_cloudagent/wallet/util.py index b8ecc67ccd..942744bca8 100644 --- a/aries_cloudagent/wallet/util.py +++ b/aries_cloudagent/wallet/util.py @@ -1,11 +1,15 @@ """Wallet utility functions.""" +import re + import base58 import base64 import nacl.utils import nacl.bindings +from ..core.profile import Profile + def random_seed() -> bytes: """ @@ -82,7 +86,38 @@ def full_verkey(did: str, abbr_verkey: str) -> str: ) +def default_did_from_verkey(verkey: str) -> str: + """Given a verkey, return the default indy did. + + By default the did is the first 16 bytes of the verkey. + """ + did = bytes_to_b58(b58_to_bytes(verkey)[:16]) + return did + + def abbr_verkey(full_verkey: str, did: str = None) -> str: """Given a full verkey and DID, return the abbreviated verkey.""" did_len = len(b58_to_bytes(did.split(":")[-1])) if did else 16 return f"~{bytes_to_b58(b58_to_bytes(full_verkey)[did_len:])}" + + +DID_EVENT_PREFIX = "acapy::ENDORSE_DID::" +DID_ATTRIB_EVENT_PREFIX = "acapy::ENDORSE_DID_ATTRIB::" +EVENT_LISTENER_PATTERN = re.compile(f"^{DID_EVENT_PREFIX}(.*)?$") +ATTRIB_EVENT_LISTENER_PATTERN = re.compile(f"^{DID_ATTRIB_EVENT_PREFIX}(.*)?$") + + +async def notify_endorse_did_event(profile: Profile, did: str, meta_data: dict): + """Send notification for a DID post-process event.""" + await profile.notify( + DID_EVENT_PREFIX + did, + meta_data, + ) + + +async def notify_endorse_did_attrib_event(profile: Profile, did: str, meta_data: dict): + """Send notification for a DID ATTRIB post-process event.""" + await profile.notify( + DID_ATTRIB_EVENT_PREFIX + did, + meta_data, + ) diff --git a/demo/AcmeDemoWorkshop.md b/demo/AcmeDemoWorkshop.md index e8ec561a72..05a956c71e 100644 --- a/demo/AcmeDemoWorkshop.md +++ b/demo/AcmeDemoWorkshop.md @@ -61,9 +61,7 @@ from uuid import uuid4 ``` python TAILS_FILE_COUNT = int(os.getenv("TAILS_FILE_COUNT", 100)) -CRED_PREVIEW_TYPE = ( - "did:sov:BzCbsNYhMrjHiqZDTUASHg;spec/issue-credential/2.0/credential-preview" -) +CRED_PREVIEW_TYPE = "https://didcomm.org/issue-credential/2.0/credential-preview" ``` Next locate the code that is triggered by option ```2```: @@ -142,10 +140,16 @@ then replace the ```# TODO``` comment and the ```pass``` statement: if is_proof_of_education: log_status("#28.1 Received proof of education, check claims") for (referent, attr_spec) in pres_req["requested_attributes"].items(): - self.log( - f"{attr_spec['name']}: " - f"{pres['requested_proof']['revealed_attrs'][referent]['raw']}" - ) + if referent in pres['requested_proof']['revealed_attrs']: + self.log( + f"{attr_spec['name']}: " + f"{pres['requested_proof']['revealed_attrs'][referent]['raw']}" + ) + else: + self.log( + f"{attr_spec['name']}: " + "(attribute not revealed)" + ) for id_spec in pres["identifiers"]: # just print out the schema/cred def id's of presented claims self.log(f"schema_id: {id_spec['schema_id']}") @@ -153,7 +157,7 @@ then replace the ```# TODO``` comment and the ```pass``` statement: # TODO placeholder for the next step else: # in case there are any other kinds of proofs received - self.log("#28.1 Received ", message["presentation_request"]["name"]) + self.log("#28.1 Received ", pres_req["name"]) ``` Right now this just verifies the proof received and prints out the attributes it reveals, but in "real life" your application could do something useful with this information. diff --git a/demo/AgentTracing.md b/demo/AgentTracing.md index ccb009e19e..819d9af6b3 100644 --- a/demo/AgentTracing.md +++ b/demo/AgentTracing.md @@ -34,9 +34,9 @@ Environment variables: ``` TRACE_ENABLED Flag to enable tracing -TRACE_TARGET_URL Host:port of endpoint to log trace events (e.g. fluentd:8088) +TRACE_TARGET_URL Host:port of endpoint to log trace events (e.g. logstash:9700) -DOCKER_NET Docker network to join (must be used if EFK stack is running in docker) +DOCKER_NET Docker network to join (must be used if ELK stack is running in docker) TRACE_TAG Tag to be included in all logged trace events ``` @@ -83,16 +83,16 @@ Faber | Connected When `Exchange Tracing` is `ON`, all exchanges will include tracing. -## Logging Trace Events to an EFK Stack +## Logging Trace Events to an ELK Stack -You can use the `EFK` stack in the [EFK sub-directory](./EFK-stack) as a target for trace events, just start the EFK stack using the docker-compose file and then in two separate bash shells, startup the demo as follows: +You can use the `ELK` stack in the [ELK Stack sub-directory](./elk-stack) as a target for trace events, just start the ELK stack using the docker-compose file and then in two separate bash shells, startup the demo as follows: ```bash -DOCKER_NET=efk-stack_efk_net TRACE_TARGET_URL=fluentd:8088 ./run_demo faber --trace-http +DOCKER_NET=elknet TRACE_TARGET_URL=logstash:9700 ./run_demo faber --trace-http ``` ```bash -DOCKER_NET=efk-stack_efk_net TRACE_TARGET_URL=fluentd:8088 ./run_demo alice --trace-http +DOCKER_NET=elknet TRACE_TARGET_URL=logstash:9700 ./run_demo alice --trace-http ``` ## Hooking into event messaging diff --git a/demo/AliceGetsAPhone.md b/demo/AliceGetsAPhone.md index 4f0b63d710..fee1074b64 100644 --- a/demo/AliceGetsAPhone.md +++ b/demo/AliceGetsAPhone.md @@ -206,7 +206,7 @@ https://abfde260.ngrok.io?c_i=eyJAdHlwZSI6ICJkaWQ6c292OkJ6Q2JzTlloTXJqSGlxWkRUVU Or this: ```bash -http://ip10-0-121-4-bquqo816b480a4bfn3kg-8020.direct.play-with-von.vonx.io?c_i=eyJAdHlwZSI6ICJkaWQ6c292OkJ6Q2JzTlloTXJqSGlxWkRUVUFTSGc7c3BlYy9jb25uZWN0aW9ucy8xLjAvaW52aXRhdGlvbiIsICJAaWQiOiAiZWI2MTI4NDUtYmU1OC00YTNiLTk2MGUtZmE3NDUzMGEwNzkyIiwgInJlY2lwaWVudEtleXMiOiBbIkFacEdoMlpIOTJVNnRFRTlmYk13Z3BqQkp3TEUzRFJIY1dCbmg4Y2FqdzNiIl0sICJzZXJ2aWNlRW5kcG9pbnQiOiAiaHR0cDovL2lwMTAtMC0xMjEtNC1icXVxbzgxNmI0ODBhNGJmbjNrZy04MDIwLmRpcmVjdC5wbGF5LXdpdGgtdm9uLnZvbnguaW8iLCAibGFiZWwiOiAiRmFiZXIuQWdlbnQifQ== +http://ip10-0-121-4-bquqo816b480a4bfn3kg-8020.direct.play-with-docker.com?c_i=eyJAdHlwZSI6ICJkaWQ6c292OkJ6Q2JzTlloTXJqSGlxWkRUVUFTSGc7c3BlYy9jb25uZWN0aW9ucy8xLjAvaW52aXRhdGlvbiIsICJAaWQiOiAiZWI2MTI4NDUtYmU1OC00YTNiLTk2MGUtZmE3NDUzMGEwNzkyIiwgInJlY2lwaWVudEtleXMiOiBbIkFacEdoMlpIOTJVNnRFRTlmYk13Z3BqQkp3TEUzRFJIY1dCbmg4Y2FqdzNiIl0sICJzZXJ2aWNlRW5kcG9pbnQiOiAiaHR0cDovL2lwMTAtMC0xMjEtNC1icXVxbzgxNmI0ODBhNGJmbjNrZy04MDIwLmRpcmVjdC5wbGF5LXdpdGgtdm9uLnZvbnguaW8iLCAibGFiZWwiOiAiRmFiZXIuQWdlbnQifQ== ``` Note that this will use the ngrok endpoint if you are running locally, or your PWD endpoint if you are running on PWD. @@ -306,7 +306,7 @@ Once that is done, try sending another proof request and see what happens! Exper A connectionless proof request works the same way as a regular proof request, however it does not require a connection to be established between the Verifier and Holder/Prover. -This is supported in the Faber demo, however note that it will only work when running Faber on the Docker playground service [Play with Docker](https://labs.play-with-docker.com/) (or on [Play with VON](http://play-with-von.vonx.io)). (This is because both the Faber agent *and* controller both need to be exposed to the mobile agent.) +This is supported in the Faber demo, however note that it will only work when running Faber on the Docker playground service [Play with Docker](https://labs.play-with-docker.com/). (This is because both the Faber agent *and* controller both need to be exposed to the mobile agent.) If you have gone through the above steps, you can delete the Faber connection in your mobile agent (however *do not* delete the credential that Faber issued to you). diff --git a/demo/AliceWantsAJsonCredential.md b/demo/AliceWantsAJsonCredential.md index 8f37bbbc47..e5afad3213 100644 --- a/demo/AliceWantsAJsonCredential.md +++ b/demo/AliceWantsAJsonCredential.md @@ -18,21 +18,27 @@ cd aries-cloudagent-python/demo Open up a second shell (so you have 2 shells open in the `demo` directory) and in one shell: ```bash -./run_demo faber --did-exchange --aip 20 --cred-type json-ld +LEDGER_URL=http://dev.greenlight.bcovrin.vonx.io ./run_demo faber --did-exchange --aip 20 --cred-type json-ld ``` ... and in the other: ```bash -./run_demo alice +LEDGER_URL=http://dev.greenlight.bcovrin.vonx.io ./run_demo alice ``` -Note that you start the `faber` agent with AIP2.0 options. (When you specify `--cred-type json-ld` faber will set aip to `20` automatically, so the `--aip` option is not strictly required.) +Note that you start the `faber` agent with AIP2.0 options. (When you specify `--cred-type json-ld` faber will set aip to `20` automatically, +so the `--aip` option is not strictly required). Note as well the use of the `LEDGER_URL`. Technically, that should not be needed if we aren't +doing anything with an Indy ledger-based credentials. However, there must be something in the way that the Faber and Alice controllers are starting up that requires access to a ledger. + +Also note that the above will only work with the `/issue-credential-2.0/create-offer` endpoint. If you want to use the `/issue-credential-2.0/send` endpoint - which automates each step of the credential exchange - you will need to include the `--no-auto` option when starting each of the alice and faber agents (since the alice and faber controllers *also* automatically respond to each step in the credential exchange). (Alternately you can run run Alice and Faber agents locally, see the `./faber-local.sh` and `./alice-local.sh` scripts in the `demo` directory.) Copy the "invitation" json text from the Faber shell and paste into the Alice shell to establish a connection between the two agents. +(If you are running with `--no-auto` you will also need to call the `/connections/{conn_id}/accept-invitation` endpoint in alice's admin api swagger page.) + Now open up two browser windows to the [Faber](http://localhost:8021/api/doc) and [Alice](http://localhost:8031/api/doc) admin api swagger pages. Using the Faber admin api, you have to create a DID with the appropriate: @@ -78,7 +84,9 @@ Congradulations, you are now ready to start issuing JSON-LD credentials! - You have created a (non-public) DID for Faber to use to sign/issue the credentials - you will need to copy the DID that you created above into the examples below (as `issuer`). - You have created a (non-public) DID for Alice to use as her `credentialSubject.id` - this is required for Alice to sign the proof (the `credentialSubject.id` is not required, but then the provided presentation can't be verified). -To issue a credential, use the `/issue-credential-2.0/send` (or `/issue-credential-2.0/create-offer`) endpoint, you can test with this example payload (just replace the "connection_id", "issuer" key, "credentialSubject.id" and "proofType" with appropriate values: +To issue a credential, use the `/issue-credential-2.0/send-offer` endpoint. (You can also use the `/issue-credential-2.0/send`) endpoint, if, as mentioned above, you have included the `--no-auto` when starting both of the agents.) + +You can test with this example payload (just replace the "connection_id", "issuer" key, "credentialSubject.id" and "proofType" with appropriate values: ``` { @@ -174,6 +182,8 @@ To see the issued credential, call the `/credentials/w3c` endpoint on Alice's ad } ``` +If you *don't* see the credential in your wallet, look up the credential exchange record (in alice's admin api - `/issue-credential-2.0/records`) and check the state. If the state is `credential-received`, then the credential has been received but not stored, in this case just call the `/store` endpoint for this credential exchange. + ## Building More Realistic JSON-LD Credentials @@ -328,7 +338,7 @@ In Alice's swagger page, submit the `/credentials/records/w3c` endpoint to see t ### Request Presentation Example -To request a proof, submit the following (with appropriate `connection_id`) to Faber's `/request-presentation-2.0/request-proof` endpoint: +To request a proof, submit the following (with appropriate `connection_id`) to Faber's `/present-proof-2.0/send-request` endpoint: ``` { @@ -368,7 +378,7 @@ To request a proof, submit the following (with appropriate `connection_id`) to F "directive": "required", "field_id": [ "1f44d55f-f161-4938-a659-f8026467f126" - ], + ] } ], "fields": [ @@ -400,7 +410,7 @@ To request a proof, submit the following (with appropriate `connection_id`) to F Note that the `is_holder` property can be used by Faber to verify that the holder of credential is the same as the subject of the attribute (`familyName`). Later on, the received presentation will be signed and verifiable only if `is_holder` with ` "directive": "required"` is included in the presentation request. -There are several ways that Alice can respond with a presentation. The simplest will just tell aca-py to put the presentation together and send it to Faber - submit the following to Alice's `/request-presentation-2.0/{pres_ex_id}/send-presentation`: +There are several ways that Alice can respond with a presentation. The simplest will just tell aca-py to put the presentation together and send it to Faber - submit the following to Alice's `/present-proof-2.0/records/{pres_ex_id}/send-presentation`: ``` { diff --git a/demo/AriesOpenAPIDemo.md b/demo/AriesOpenAPIDemo.md index 7e3709a032..e392cbde49 100644 --- a/demo/AriesOpenAPIDemo.md +++ b/demo/AriesOpenAPIDemo.md @@ -601,7 +601,7 @@ Alice now has her Faber credential. Let’s have the Faber agent send a request ### Faber sends a Proof Request -From the Faber browser tab, get ready to execute the **`POST /present-proof/send-request`** endpoint. After hitting `Try it Now`, erase the data in the block labelled "Edit Value Model", replacing it with the text below. Once that is done, replace in the JSON each instance of `cred_def_id` (there are four instances) and `connection_id` with the values found using the same techniques we've used earlier in this tutorial. Both can be found by scrolling back a little in the Faber terminal, or you can execute API endpoints we've already covered. You can also change the value of the `comment` item to whatever you want. +From the Faber browser tab, get ready to execute the **`POST /present-proof-2.0/send-request`** endpoint. After hitting `Try it Now`, erase the data in the block labelled "Edit Value Model", replacing it with the text below. Once that is done, replace in the JSON each instance of `cred_def_id` (there are four instances) and `connection_id` with the values found using the same techniques we've used earlier in this tutorial. Both can be found by scrolling back a little in the Faber terminal, or you can execute API endpoints we've already covered. You can also change the value of the `comment` item to whatever you want. ```jsonc { diff --git a/demo/EFK-stack/README.md b/demo/EFK-stack/README.md deleted file mode 100644 index e7e956055e..0000000000 --- a/demo/EFK-stack/README.md +++ /dev/null @@ -1,99 +0,0 @@ -# EFK stack - -Note - this code was originally obtained from [https://github.com/giefferre/EFK-stack](https://github.com/giefferre/EFK-stack) - -A sample environment running an [EFK stack][efk] on your local machine. - -Includes: - -- [Elasticsearch][elasticsearch] -- [Fluentd][fluentd] -- [Kibana][kibana] - -## Introduction - -As software systems grow and become more and more decoupled, log aggregation is a key aspect to take care of. - -The issues to tackle down with logging are: - -- Having a centralized overview of all log events -- Normalizing different log types -- Automated processing of log messages -- Supporting several and very different event sources - -While [Elasticsearch][elasticsearch] and [Kibana][kibana] are the reference products *de facto* for log searching and visualization in the open source community, there's no such agreement for log collectors. - -The two most-popular data collectors are: - -- [Logstash][logstash], most known for being part of the [ELK Stack][elk] -- [Fluentd][fluentd], used by communities of users of software such as [Docker][docker-fluentd] and [GCP][gcp-fluentd] - -Logging systems using Fluentd as collector are usually referenced as [EFK stack][efk]. - -Aim of this repository is to run an EFK stack on your local machine using docker-compose. - -I'm not personally involved with companies supporting Logstash nor Fluentd. - -If you need help to choose between Logstash and Fluent, take a look to the [reference](#reference). - -## Launching the EFK stack - -### Requirements - -On your machine, make sure you have installed: - -- [Docker][docker] -- [Docker Compose][docker-compose] - -### Run - -```bash -docker-compose up -``` - -Please note: in this example Fluentd will run on port `8080` instead of the default `24224`. - -This settings has been changed to show how to configure Fluentd to listen on a different port. - -Kibana is exposed on port `5601`. - -### Testing with sample data - -If you are running macOS and you want to send sample data to test the EFK stack, you'll need [RESTed][rested]. - -Files are available in the [examples](examples) folder. - -Please note that RESTed is not strictly necessary as any other REST client application will work fine. - -## Running the aca-py Alice/Faber Demo Tracing using EFK - -In two separate bash shells, startup the demo as follows: - -```bash -DOCKER_NET=efk-stack_efk_net TRACE_TARGET_URL=fluentd:8088 ./run_demo faber --trace-http -``` - -```bash -DOCKER_NET=efk-stack_efk_net TRACE_TARGET_URL=fluentd:8088 ./run_demo alice --trace-http -``` - -## Reference - -- [Quora - What is the ELK stack](https://www.quora.com/What-is-the-ELK-stack) -- [Fluentd vs. LogStash: A Feature Comparison](https://www.loomsystems.com/blog/single-post/2017/01/30/a-comparison-of-fluentd-vs-logstash-log-collector) -- [Panda Strike: Fluentd vs Logstash](https://www.pandastrike.com/posts/20150807-fluentd-vs-logstash) -- [Log Aggregation with Fluentd, Elasticsearch and Kibana - Haufe-Lexware.github.io](http://work.haufegroup.io/log-aggregation/) -- [Fluentd vs Logstash, An unbiased comparison](https://techstricks.com/fluentd-vs-logstash/) -- [Fluentd vs. Logstash: A Comparison of Log Collectors | Logz.io](https://logz.io/blog/fluentd-logstash/) - -[elasticsearch]: https://www.elastic.co/products/elasticsearch -[fluentd]: https://www.fluentd.org/ -[kibana]: https://www.elastic.co/products/kibana -[logstash]: https://www.elastic.co/products/logstash -[elk]: https://www.elastic.co/videos/introduction-to-the-elk-stack -[docker-fluentd]: https://docs.docker.com/reference/logging/fluentd/ -[gcp-fluentd]: https://github.com/GoogleCloudPlatform/google-fluentd -[efk]: https://docs.openshift.com/enterprise/3.1/install_config/aggregate_logging.html#overview -[docker]: https://www.docker.com/ -[docker-compose]: https://docs.docker.com/compose/ -[rested]: https://itunes.apple.com/au/app/rested-simple-http-requests/id421879749?mt=12 \ No newline at end of file diff --git a/demo/EFK-stack/docker-compose.yml b/demo/EFK-stack/docker-compose.yml deleted file mode 100644 index 9434ff5ada..0000000000 --- a/demo/EFK-stack/docker-compose.yml +++ /dev/null @@ -1,35 +0,0 @@ -version: '3' -services: - elasticsearch: - image: docker.elastic.co/elasticsearch/elasticsearch:7.4.2 - environment: - discovery.type: single-node - ports: - - 9200:9200 - - 9300:9300 - networks: - - efk_net - - fluentd: - build: ./fluentd - volumes: - - ./fluentd/conf:/fluentd/etc - - ./logs:/logs - ports: - - 8088:8088 - networks: - - efk_net - - kibana: - image: docker.elastic.co/kibana/kibana:7.4.2 - environment: - ELASTICSEARCH_URL: http://elasticsearch:9200 - volumes: - - ./kibana/conf:/usr/share/kibana/config - ports: - - 5601:5601 - networks: - - efk_net - -networks: - efk_net: diff --git a/demo/EFK-stack/examples/anotherapp.log.request b/demo/EFK-stack/examples/anotherapp.log.request deleted file mode 100644 index e2532a34cd..0000000000 --- a/demo/EFK-stack/examples/anotherapp.log.request +++ /dev/null @@ -1,41 +0,0 @@ - - - - - baseURL - http://localhost:8080/anotherapp.log - bodyString - json={"action":"logout","userId":"5b07fbbb4e6b8"} - followRedirect - - handleJSONPCallbacks - - headers - - - header - Content-Type - inUse - - value - application/x-www-form-urlencoded - - - httpMethod - POST - jsonpScript - - paramBodyUIChoice - 0 - parameters - - parametersType - 0 - presentBeforeChallenge - - stringEncoding - 4 - usingHTTPBody - - - diff --git a/demo/EFK-stack/examples/myapp.log.request b/demo/EFK-stack/examples/myapp.log.request deleted file mode 100644 index c91f6f2423..0000000000 --- a/demo/EFK-stack/examples/myapp.log.request +++ /dev/null @@ -1,41 +0,0 @@ - - - - - baseURL - http://localhost:8080/myapp.log - bodyString - json={"action":"login","userId":"5b07fbbb4e6b8"} - followRedirect - - handleJSONPCallbacks - - headers - - - header - Content-Type - inUse - - value - application/x-www-form-urlencoded - - - httpMethod - POST - jsonpScript - - paramBodyUIChoice - 0 - parameters - - parametersType - 0 - presentBeforeChallenge - - stringEncoding - 4 - usingHTTPBody - - - diff --git a/demo/EFK-stack/fluentd/Dockerfile b/demo/EFK-stack/fluentd/Dockerfile deleted file mode 100644 index 8f8a4f94d3..0000000000 --- a/demo/EFK-stack/fluentd/Dockerfile +++ /dev/null @@ -1,9 +0,0 @@ -FROM fluent/fluentd:stable -RUN apk add --update --virtual .build-deps \ - sudo build-base ruby-dev \ - && sudo gem install \ - fluent-plugin-elasticsearch \ - && sudo gem sources --clear-all \ - && apk del .build-deps \ - && rm -rf /var/cache/apk/* \ - /home/fluent/.gem/ruby/2.3.0/cache/*.gem diff --git a/demo/EFK-stack/fluentd/conf/fluent.conf b/demo/EFK-stack/fluentd/conf/fluent.conf deleted file mode 100644 index 9c665c2a7c..0000000000 --- a/demo/EFK-stack/fluentd/conf/fluent.conf +++ /dev/null @@ -1,65 +0,0 @@ -# Fluentd main configuration file -# Reference: https://docs.fluentd.org/v1.0/articles/config-file - -# Set Fluentd to listen via http on port 8088, listening on all hosts - - @type http - port 8088 - bind 0.0.0.0 - - -# watch aca-py log files -# -# @type tail path /logs/acapy-*.log -# pos_file /logs/td-agent/acapy.log.pos -# tag acapy.event -# read_from_head true -# -# @type apache2 -# -# - - - @type copy - - @type elasticsearch - host elasticsearch - port 9200 - index_name fluentd - type_name fluentd - logstash_format true - logstash_prefix fluentd - logstash_dateformat %Y%m%d - include_tag_key true - tag_key @log_name - flush_interval 1s - - - -# Events having prefix 'myapp.' will be stored both on Elasticsearch and files. - - @type copy - - @type elasticsearch - host elasticsearch - port 9200 - index_name fluentd - type_name fluentd - logstash_format true - logstash_prefix fluentd - logstash_dateformat %Y%m%d - include_tag_key true - tag_key @log_name - flush_interval 1s - - - @type file - path /logs/myapp - flush_interval 30s - - - -# All other events will be printed to stdout - - @type stdout - \ No newline at end of file diff --git a/demo/EFK-stack/kibana/conf/kibana.yml b/demo/EFK-stack/kibana/conf/kibana.yml deleted file mode 100644 index cf19a76416..0000000000 --- a/demo/EFK-stack/kibana/conf/kibana.yml +++ /dev/null @@ -1,7 +0,0 @@ -# Default Kibana configuration for docker target -server.name: kibana -server.host: "0" -elasticsearch.hosts: [ "http://elasticsearch:9200" ] -xpack.monitoring.ui.container.elasticsearch.enabled: true -xpack.reporting.enabled: true -xpack.reporting.csv.maxSizeBytes: 10485760 diff --git a/demo/EFK-stack/requirements.txt b/demo/EFK-stack/requirements.txt deleted file mode 100644 index 78f00d2cf5..0000000000 --- a/demo/EFK-stack/requirements.txt +++ /dev/null @@ -1,5 +0,0 @@ -# Elasticsearch 7.x -elasticsearch>=7.0.0,<8.0.0 - -# Elasticsearch 7.x -elasticsearch-dsl>=7.0.0,<8.0.0 diff --git a/demo/EFK-stack/search.py b/demo/EFK-stack/search.py deleted file mode 100644 index db60979025..0000000000 --- a/demo/EFK-stack/search.py +++ /dev/null @@ -1,94 +0,0 @@ -import csv - -from elasticsearch_dsl import connections -from elasticsearch_dsl import Search - - -connections.create_connection(hosts=["localhost"], timeout=20) - -s = Search(index="fluentd-*") -# only return the selected fields -s = s.source( - [ - "str_time", - "timestamp", - "handler", - "ellapsed_milli", - "thread_id", - "msg_id", - "outcome", - "traced_type", - ] -) -s = s.sort("timestamp") -events = [] -for x in s.scan(): - events.append( - { - "str_time": x.str_time, - "timestamp": x.timestamp, - "handler": x.handler, - "ellapsed_milli": x.ellapsed_milli, - "thread_id": x.thread_id, - "msg_id": x.msg_id, - "outcome": x.outcome, - "traced_type": x.traced_type, - } - ) -sorted_events = sorted(events, key=lambda i: i["timestamp"]) - -threads = {} -thread_count = 0 -agents = {} -with open("agent-events.csv", "w", newline="") as csvfile: - spamwriter = csv.writer(csvfile) - i = 0 - spamwriter.writerow( - [ - "idx", - "str_time", - "timestamp", - "handler", - "ellapsed_milli", - "thread_id", - "msg_id", - "outcome", - "traced_type", - "delta_agent", - "delta_thread", - ] - ) - for x in sorted_events: - if x["handler"] in agents: - delta_agent = x["timestamp"] - agents[x["handler"]] - if delta_agent < 0: - print(i, delta_agent) - else: - delta_agent = 0 - agents[x["handler"]] = x["timestamp"] - if x["thread_id"] in threads: - delta_thread = x["timestamp"] - threads[x["thread_id"]] - if delta_thread < 0: - print(i, delta_thread) - else: - delta_thread = 0 - thread_count = thread_count + 1 - threads[x["thread_id"]] = x["timestamp"] - i = i + 1 - spamwriter.writerow( - [ - i, - x["str_time"], - x["timestamp"], - x["handler"], - x["ellapsed_milli"], - x["thread_id"], - x["msg_id"], - x["outcome"], - x["traced_type"], - delta_agent, - delta_thread, - ] - ) - -print("Total threads=", thread_count) diff --git a/demo/Endorser.md b/demo/Endorser.md index 5b81d70d66..2bf2543c3b 100644 --- a/demo/Endorser.md +++ b/demo/Endorser.md @@ -2,12 +2,14 @@ There are two ways to run the alice/faber demo with endorser support enabled. - ## Run Faber as an Author, with a dedicated Endorser agent -This approach runs Faber as an un-privileged agent, and starts a dedicated Endorser sub-process to endorse Faber's transactions. +This approach runs Faber as an un-privileged agent, and starts a dedicated Endorser Agent in a sub-process (an instance of ACA-Py) to endorse Faber's transactions. + +Start a VON Network instance and a Tails server: -Start a VON Network and a Tails server. +- Following the [Building and Starting](https://github.com/bcgov/von-network/blob/main/docs/UsingVONNetwork.md#building-and-starting) section of the VON Network Tutorial to get ledger started. You can leave off the `--logs` option if you want to use the same terminal for running both VON Network and the Tails server. When you are finished with VON Network, follow the [Stopping And Removing a VON Network](https://github.com/bcgov/von-network/blob/main/docs/UsingVONNetwork.md#stopping-and-removing-a-von-network) instructions. +- Run an AnonCreds revocation registry tails server in order to support revocation by following the instructions in the [Alice gets a Phone](https://github.com/hyperledger/aries-cloudagent-python/blob/master/demo/AliceGetsAPhone.md#run-an-instance-of-indy-tails-server) demo. Start up Faber as Author (note the tails file size override, to allow testing of the revocation registry roll-over): @@ -15,7 +17,7 @@ Start up Faber as Author (note the tails file size override, to allow testing of TAILS_FILE_COUNT=5 ./run_demo faber --endorser-role author --revocation ``` -Start up Alcie as normal: +Start up Alice as normal: ```bash ./run_demo alice @@ -33,7 +35,7 @@ This approach sets up the endorser roles to allow manual testing using the agent - Faber runs as an Endorser (all of Faber's functions - issue credential, request proof, etc.) run normally, since Faber has ledger write access - Alice starts up with a DID aith Author privileges (no ledger write access) and Faber is setup as Alice's Endorser -Start a VON Network and a Tails server. +Start a VON Network and a Tails server using the instructions above. Start up Faber as Endorser: diff --git a/demo/INTEGRATION-TESTS.md b/demo/INTEGRATION-TESTS.md index 2d4726398a..b8a8bf456c 100644 --- a/demo/INTEGRATION-TESTS.md +++ b/demo/INTEGRATION-TESTS.md @@ -2,6 +2,12 @@ Integration tests for aca-py are implemented using Behave functional tests to drive aca-py agents based on the alice/faber demo framework. +If you are new to the ACA-Py integration test suite, this [video](https://youtu.be/AbuPg4J8Pd4) from ACA-Py Maintainer @ianco describes +the Integration Tests in ACA-Py, how to run them and how to add more tests. See also the video at the end of this document about running +Aries Agent Test Harness tests before you submit your pull requests. + +## Getting Started + To run the aca-py Behave tests, open a bash shell run the following: ```bash @@ -17,11 +23,13 @@ cd indy-tails-server/docker cd ../.. git clone https://github.com/hyperledger/aries-cloudagent-python cd aries-cloudagent-python/demo -./run_bdd +./run_bdd -t ~@taa_required ``` Note that an Indy ledger and tails server are both required (these can also be specified using environment variables). +Note also that some tests require a ledger with TAA enabled, how to run these tests will be described later. + By default the test suite runs using a default (SQLite) wallet, to run the tests using postgres run the following: ```bash @@ -33,7 +41,7 @@ ACAPY_ARG_FILE=postgres-indy-args.yml ./run_bdd To run the tests against the back-end `askar` libraries (as opposed to indy-sdk) run the following: ```bash -BDD_EXTRA_AGENT_ARGS="{\"wallet-type\":\"askar\"}" ./run_bdd +BDD_EXTRA_AGENT_ARGS="{\"wallet-type\":\"askar\"}" ./run_bdd -t ~@taa_required ``` (Note that `wallet-type` is currently the only extra argument supported.) @@ -44,6 +52,29 @@ You can run individual tests by specifying the tag(s): ./run_bdd -t @T001-AIP10-RFC0037 ``` +## Running Integration Tests which require TAA + +To run a local von-network with TAA enabled,run the following: + +```bash +git clone https://github.com/bcgov/von-network +cd von-network +./manage build +./manage start --taa-sample --logs +``` + +You can then run the TAA-enabled tests as follows: + +```bash +./run_bdd -t @taa_required +``` + +or: + +```bash +BDD_EXTRA_AGENT_ARGS="{\"wallet-type\":\"askar\"}" ./run_bdd -t @taa_required +``` + ## Aca-py Integration Tests vs Aries Agent Test Harness (AATH) Aca-py Behave tests are based on the interoperability tests that are implemented in the [Aries Agent Test Harness (AATH)](https://github.com/hyperledger/aries-agent-test-harness). Both use [Behave (Gherkin)](https://behave.readthedocs.io/en/stable/) to execute tests against a running aca-py agent (or in the case of AATH, against any compatible Aries agent), however the aca-py integration tests focus on aca-py specific features. @@ -131,3 +162,7 @@ To run a specific set of Aca-py integration tests (or exclude specific tests): (All command line parameters are passed to the `behave` command, so [all parameters supported by behave](https://behave.readthedocs.io/en/stable/behave.html) can be used.) +## Aries Agent Test Harness ACA-Py Tests + +This [video](https://youtu.be/1dwyEBxQqWI) is a presentation by Aries Cloud Agent Python (ACA-Py) developer @ianco about using the Aries Agent Test Harness for local pre-release testing of ACA-Py. Have a big change that you want to test with other Aries Frameworks? Following this guidance to run AATH tests with your under-development branch of ACA-Py. + diff --git a/demo/README.md b/demo/README.md index a3db767adf..a8c4a63573 100644 --- a/demo/README.md +++ b/demo/README.md @@ -4,13 +4,12 @@ There are several demos available for ACA-Py mostly (but not only) aimed at deve ## Table of Contents -- [The IIWBook Demo](#the-iiwbook-demo) - [The Alice/Faber Python demo](#the-alicefaber-python-demo) - [Running in a Browser](#running-in-a-browser) - [Running in Docker](#running-in-docker) - [Running Locally](#running-locally) - [Installing Prerequisites](#installing-prerequisites) - - [Start a local indy ledger](#start-a-local-indy-ledger) + - [Start a local Indy ledger](#start-a-local-indy-ledger) - [Genesis File handling](#genesis-file-handling) - [Run a local Postgres instance](#run-a-local-postgres-instance) - [Optional: Run a von-network ledger browser](#optional-run-a-von-network-ledger-browser) @@ -20,28 +19,25 @@ There are several demos available for ACA-Py mostly (but not only) aimed at deve - [Issuing and Proving Credentials](#issuing-and-proving-credentials) - [Additional Options in the Alice/Faber demo](#additional-options-in-the-alicefaber-demo) - [Revocation](#revocation) - - [Mediation](#mediation) - - [Multi-tenancy](#multi-tenancy) - - [Multi-ledger](#multi-ledger) - [DID Exchange](#did-exchange) - [Endorser](#endorser) - - [Run Askar Backend](#run-askar-backend) + - [Run Indy-SDK Backend](#run-indy-sdk-backend) + - [Mediation](#mediation) + - [Multi-ledger](#multi-ledger) + - [Multi-tenancy](#multi-tenancy) + - [Multi-tenancy *with Mediation*!!!](#multi-tenancy-with-mediation) - [Learning about the Alice/Faber code](#learning-about-the-alicefaber-code) - [OpenAPI (Swagger) Demo](#openapi-swagger-demo) - [Performance Demo](#performance-demo) - [Coding Challenge: Adding ACME](#coding-challenge-adding-acme) -## The IIWBook Demo - -The IIWBook demo is a real (play) self-sovereign identity demonstration. During the demo, you will get a mobile agent (sorry - IOS only right now), and use that agent to connect with several enterprise services to collect and prove credentials. The two services in the demo (the [email verification service](https://github.com/bcgov/indy-email-verification) and [IIWBook](https://github.com/bcgov/iiwbook)) are both instances of ACA-Py, and all the agents are using DIDComm to communicate. Learn about and run the demo at [https://vonx.io/how_to/iiwbook](https://vonx.io/how_to/iiwbook). Developers, when you are ready, check out the code in the repos of the two services to see how they implement Django web server-based controller and agent. - ## The Alice/Faber Python demo The Alice/Faber demo is the (in)famous first verifiable credentials demo. Alice, a former student of Faber College ("Knowledge is Good"), connects with the College, is issued a credential about her degree and then is asked by the College for a proof. There are a variety of ways of running the demo. The easiest is in your browser using a site ("Play with VON") that let's you run docker containers without installing anything. Alternatively, you can run locally on docker (our recommendation), or using python on your local machine. Each approach is covered below. ### Running in a Browser -In your browser, go to the docker playground service [Play with VON](http://play-with-von.vonx.io) (from the BC Gov). On the title screen, click "Start". On the next screen, click (in the left menu) "+Add a new instance". That will start up a terminal in your browser. Run the following commands to start the Faber agent: +In your browser, go to the docker playground service [Play with Docker](https://labs.play-with-docker.com/). On the title screen, click "Start". On the next screen, click (in the left menu) "+Add a new instance". That will start up a terminal in your browser. Run the following commands to start the Faber agent: ```bash git clone https://github.com/hyperledger/aries-cloudagent-python @@ -63,19 +59,20 @@ Jump to the [Follow the Script](#follow-the-script) section below for further in ### Running in Docker -Running the demo in docker requires having a `von-network` (a Hyperledger Indy public ledger sandbox) instance running in docker locally. See the [Running the Network Locally](https://github.com/bcgov/von-network#running-the-network-locally) section of the `von-network` readme file for more info. +Running the demo in docker requires having a `von-network` (a Hyperledger Indy public ledger sandbox) instance running in docker locally. See the [VON Network Tutorial](https://github.com/bcgov/von-network/blob/main/docs/UsingVONNetwork.md) for guidance +on starting and stopping your own local Hyperledger Indy instance. Open three `bash` shells. For Windows users, `git-bash` is highly recommended. bash is the default shell in Linux and Mac terminal sessions. -In the first terminal window, start `von-network` by following the [Running the Network Locally](https://github.com/bcgov/von-network#running-the-network-locally) instructions. +In the first terminal window, start `von-network` by following the [Building and Starting](https://github.com/bcgov/von-network/blob/main/docs/UsingVONNetwork.md#building-and-starting) instructions. -In the second terminal, change directory into `demo` directory of your clone of this repository. Start the `faber` agent by issuing the following command: +In the second terminal, change directory into `demo` directory of your clone of the Aries Cloud Agent Python repository. Start the `faber` agent by issuing the following command: ``` bash ./run_demo faber ``` -In the third terminal, change directory into `demo` directory of your clone of this repository. Start the `alice` agent by issuing the following command: +In the third terminal, change directory into `demo` directory of your clone of the Aries Cloud Agent Python repository. Start the `alice` agent by issuing the following command: ``` bash ./run_demo alice @@ -87,6 +84,8 @@ Jump to the [Follow the Script](#follow-the-script) section below for further in The following is an approach to to running the Alice and Faber demo using Python3 running on a bare machine. There are other ways to run the components, but this covers the general approach. +We don't recommend this approach if you are just trying this demo, as you will likely run into issues with the specific setup of your machine. + #### Installing Prerequisites We assume you have a running Python 3 environment. To install the prerequisites specific to running the agent/controller examples in your Python environment, run the following command from this repo's `demo` folder. The precise command to run may vary based on your Python environment setup. @@ -97,12 +96,16 @@ pip3 install -r demo/requirements.txt While that process will include the installation of the Indy python prerequisite, you still have to build and install the `libindy` code for your platform. Follow the [installation instructions](https://github.com/hyperledger/indy-sdk#installing-the-sdk) in the indy-sdk repo for your platform. -#### Start a local indy ledger +#### Start a local Indy ledger + +Start a local `von-network` Hyperledger Indy network running in Docker by following the VON Network [Building and Starting](https://github.com/bcgov/von-network/blob/main/docs/UsingVONNetwork.md#building-and-starting) instructions. -Use instructions in the [indy-sdk repo](https://github.com/hyperledger/indy-sdk#how-to-start-local-nodes-pool-with-docker) to run a local ledger. +We strongly recommend you use Docker for the local Indy network until you really, really need to know the details of running an Indy Node instance on a bare machine. #### Genesis File handling +> Assuming you followed our advice and are using a VON Network instance of Hyperledger Indy, you can ignore this section. If you started the Indy ledger **without** using VON Network, this information might be helpful. + An Aries agent (or other client) connecting to an Indy ledger must know the contents of the `genesis` file for the ledger. The genesis file lets the agent/client know the IP addresses of the initial nodes of the ledger, and the agent/client sends ledger requests to those IP addresses. When using the `indy-sdk` ledger, look for the instructions in that repo for how to find/update the ledger genesis file, and note the path to that file on your local system. The envrionment variable `GENESIS_FILE` is used to let the Aries demo agents know the location of the genesis file. Use the path to that file as value of the `GENESIS_FILE` environment variable in the instructions below. You might want to copy that file to be local to the demo so the path is shorter. @@ -117,7 +120,9 @@ docker run --name some-postgres -e POSTGRES_PASSWORD=mysecretpassword -d -p 5432 #### Optional: Run a von-network ledger browser -If you want to be able to browse your local ledger as you run the demo, clone the [von-network](https://github.com/bcgov/von-network) repo, go into the root of the cloned instance and run the following command, replacing the `/path/to/local-genesis.txt` with a path to the same genesis file as was used in starting the ledger. +If you followed our advice and are using a VON Network instance of Hyperledger Indy, you can ignore this section, as you already have a Ledger browser running, accessible on http://localhost:9000. + + If you started the Indy ledger **without** using VON Network, and you want to be able to browse your local ledger as you run the demo, clone the [von-network](https://github.com/bcgov/von-network) repo, go into the root of the cloned instance and run the following command, replacing the `/path/to/local-genesis.txt` with a path to the same genesis file as was used in starting the ledger. ``` bash GENESIS_FILE=/path/to/local-genesis.txt PORT=9000 REGISTER_NEW_DIDS=true python -m server.server @@ -125,7 +130,19 @@ GENESIS_FILE=/path/to/local-genesis.txt PORT=9000 REGISTER_NEW_DIDS=true python #### Run the Alice and Faber Controllers/Agents -With the rest of the pieces running, you can run the Alice and Faber controllers and agents. To do so, `cd` into the `demo` folder your clone of this repo in two terminal windows and run the following, replacing the `/path/to/local-genesis.txt`. +With the rest of the pieces running, you can run the Alice and Faber controllers and agents. To do so, `cd` into the `demo` folder your clone of this repo in two terminal windows. + +If you are using a VON Network instance of Hyperledger, run the following commands: + +``` bash +DEFAULT_POSTGRES=true python3 -m runners.faber --port 8020 +``` + +``` bash +DEFAULT_POSTGRES=true python3 -m runners.alice --port 8030 +``` + +If you started the Indy ledger **without** using VON Network, use the following commands, replacing the `/path/to/local-genesis.txt` with the one for your configuration. ``` bash GENESIS_FILE=/path/to/local-genesis.txt DEFAULT_POSTGRES=true python3 -m runners.faber --port 8020 @@ -135,7 +152,7 @@ GENESIS_FILE=/path/to/local-genesis.txt DEFAULT_POSTGRES=true python3 -m runners GENESIS_FILE=/path/to/local-genesis.txt DEFAULT_POSTGRES=true python3 -m runners.alice --port 8030 ``` -Note that Alice and Faber will each use 5 ports, e.g. using the parameter `... --port 8020` actually uses ports 8020 through 8024. Feel free to use different ports if you want. +Note that Alice and Faber will each use 5 ports, e.g., using the parameter `... --port 8020` actually uses ports 8020 through 8024. Feel free to use different ports if you want. Everything running? See the [Follow the Script](#follow-the-script) section below for further instructions. @@ -201,7 +218,7 @@ To enable support for revoking credentials, run the `faber` demo with the `--rev Note that you don't specify this option with `alice` because it's only applicable for the credential `issuer` (who has to enable revocation when creating a credential definition, and explicitely revoke credentials as appropriate; alice doesn't have to do anything special when revocation is enabled). -You need to run a revocation registry in order to support revocation - the details are described in the [Alice gets a Phone](https://github.com/hyperledger/aries-cloudagent-python/blob/master/demo/AliceGetsAPhone.md#run-an-instance-of-indy-tails-server) demo instructions. +You need to run an AnonCreds revocation registry tails server in order to support revocation - the details are described in the [Alice gets a Phone](https://github.com/hyperledger/aries-cloudagent-python/blob/master/demo/AliceGetsAPhone.md#run-an-instance-of-indy-tails-server) demo instructions. Faber will setup support for revocation automatically, and you will see an extra option in faber's menu to revoke a credential: @@ -247,12 +264,12 @@ Note that you can't (currently) use the DID Exchange protocol to connect with an This is described in [Endorser.md](Endorser.md) -### Run Askar Backend +### Run Indy-SDK Backend -This runs using the askar libraries instead of indy-sdk: +This runs using the older (and not recommended) indy-sdk libraries instead of [Aries Askar](:uhttps://github.com/hyperledger/aries-ask): ```bash -./run_demo faber --wallet-type askar +./run_demo faber --wallet-type indy ``` ### Mediation @@ -263,27 +280,27 @@ To enable mediation, run the `alice` or `faber` demo with the `--mediation` opti ./run_demo faber --mediation ``` -This will start up a second "mediator" agent and automatically set the alice/faber connection to use the mediator. +This will start up a "mediator" agent with Alice or Faber and automatically set the alice/faber connection to use the mediator. -### Multi-tenancy +### Multi-ledger -To enable support for multi-tenancy, run the `alice` or `faber` demo with the `--multitenant` option: +To enable multiple ledger mode, run the `alice` or `faber` demo with the `--multi-ledger` option: ```bash -./run_demo faber --multitenant +./run_demo faber --multi-ledger ``` -(This option can be used with both (or either) `alice` and/or `faber`.) +The configuration file for setting up multiple ledgers (for the demo) can be found at `./demo/multiple_ledger_config.yml`. -### Multi-ledger +### Multi-tenancy -To enable multiple ledger mode, run the `alice` or `faber` demo with the `--multi-ledger` option: +To enable support for multi-tenancy, run the `alice` or `faber` demo with the `--multitenant` option: ```bash -./run_demo faber --multi-ledger +./run_demo faber --multitenant ``` -The configuration file for setting up multiple ledgers (for the demo) can be found at `./demo/multiple_ledger_config.yml`. +(This option can be used with both (or either) `alice` and/or `faber`.) You will see an additional menu option to create new sub-wallets (or they can be considered to be "virtual agents"). @@ -396,6 +413,28 @@ The script starts both agents, runs the performance test, spits out performance A second version of the performance test can be run by adding the parameter `--routing` to the invocation above. The parameter triggers the example to run with Alice using a routing agent such that all messages pass through the routing agent between Alice and Faber. This is a good, simple example of how routing can be implemented with DIDComm agents. +You can also run the demo against a postgres database using the following: + +```bash +./run_demo performance --arg-file demo/postgres-indy-args.yml +``` + +(Obvs you need to be running a postgres database - the command to start postgres is in the yml file provided above.) + +You can tweak the number of credentials issued using the `--count` and `--batch` parameters, and you can run against an Askar database using the `--wallet-type askar` option (or run using indy-sdk using `--wallet-type indy`). + +An example full set of options is: + +```bash +./run_demo performance --arg-file demo/postgres-indy-args.yml -c 10000 -b 10 --wallet-type askar +``` + +Or: + +```bash +./run_demo performance --arg-file demo/postgres-indy-args.yml -c 10000 -b 10 --wallet-type indy +``` + ## Coding Challenge: Adding ACME Now that you have a solid foundation in using ACA-Py, time for a coding challenge. In this challenge, we extend the Alice-Faber command line demo by adding in ACME Corp, a place where Alice wants to work. The demo adds: diff --git a/demo/alice-local.sh b/demo/alice-local.sh index d24e50945e..b1639fc90a 100755 --- a/demo/alice-local.sh +++ b/demo/alice-local.sh @@ -1,7 +1,7 @@ #!/bin/bash # this runs the Faber example as a local instace of instance of aca-py # you need to run a local von-network (in the von-network directory run "./manage start --logs") -# ... and you need to install the local aca-py python libraries locally ("pip install -r ../requriements.txt -r ../requirements.indy.txt -r ../requirements.bbs.txt") +# ... and you need to install the local aca-py python libraries locally ("pip install -r ../requirements.txt -r ../requirements.indy.txt -r ../requirements.bbs.txt") # the following will auto-respond on connection and credential requests, but not proof requests PYTHONPATH=.. ../bin/aca-py start \ @@ -11,7 +11,7 @@ PYTHONPATH=.. ../bin/aca-py start \ --outbound-transport http \ --admin 0.0.0.0 8031 \ --admin-insecure-mode \ - --wallet-type indy \ + --wallet-type askar \ --wallet-name alice.agent420695 \ --wallet-key alice.agent420695 \ --preserve-exchange-records \ diff --git a/demo/bdd_support/agent_backchannel_client.py b/demo/bdd_support/agent_backchannel_client.py index 5a2f312812..668defd807 100644 --- a/demo/bdd_support/agent_backchannel_client.py +++ b/demo/bdd_support/agent_backchannel_client.py @@ -1,19 +1,8 @@ import asyncio -from aiohttp import ( - web, - ClientSession, - ClientRequest, - ClientResponse, - ClientError, - ClientTimeout, -) import json -import os -from time import sleep import uuid from runners.agent_container import AgentContainer, create_agent_with_args_list -from runners.support.agent import DemoAgent ###################################################################### @@ -21,31 +10,7 @@ ###################################################################### -def run_coroutine(coroutine): - loop = asyncio.get_event_loop() - if not loop: - loop = asyncio.new_event_loop() - asyncio.set_event_loop(loop) - try: - return loop.run_until_complete(coroutine()) - finally: - pass - # loop.close() - - -def run_coroutine_with_args(coroutine, *args): - loop = asyncio.get_event_loop() - if not loop: - loop = asyncio.new_event_loop() - asyncio.set_event_loop(loop) - try: - return loop.run_until_complete(coroutine(*args)) - finally: - pass - # loop.close() - - -def run_coroutine_with_kwargs(coroutine, *args, **kwargs): +def run_coroutine(coroutine, *args, **kwargs): loop = asyncio.get_event_loop() if not loop: loop = asyncio.new_event_loop() @@ -58,14 +23,14 @@ def run_coroutine_with_kwargs(coroutine, *args, **kwargs): def async_sleep(delay): - run_coroutine_with_args(asyncio.sleep, delay) + run_coroutine(asyncio.sleep, delay) ###################################################################### # high level aries agent interface ###################################################################### def create_agent_container_with_args(in_args: list): - return run_coroutine_with_args(create_agent_with_args_list, in_args) + return run_coroutine(create_agent_with_args_list, in_args) def aries_container_initialize( @@ -73,7 +38,7 @@ def aries_container_initialize( schema_name: str = None, schema_attrs: list = None, ): - run_coroutine_with_kwargs( + run_coroutine( the_container.initialize, schema_name=schema_name, schema_attrs=schema_attrs, @@ -86,7 +51,7 @@ def agent_container_register_did( verkey: str, role: str, ): - run_coroutine_with_args( + run_coroutine( the_container.register_did, did, verkey, @@ -103,7 +68,7 @@ def aries_container_terminate( def aries_container_generate_invitation( the_container: AgentContainer, ): - return run_coroutine_with_kwargs( + return run_coroutine( the_container.generate_invitation, ) @@ -112,7 +77,7 @@ def aries_container_receive_invitation( the_container: AgentContainer, invite_details: dict, ): - return run_coroutine_with_kwargs( + return run_coroutine( the_container.input_invitation, invite_details, ) @@ -130,7 +95,7 @@ def aries_container_create_schema_cred_def( schema_attrs: list, version: str = None, ): - return run_coroutine_with_kwargs( + return run_coroutine( the_container.create_schema_and_cred_def, schema_name, schema_attrs, @@ -143,7 +108,7 @@ def aries_container_issue_credential( cred_def_id: str, cred_attrs: list, ): - return run_coroutine_with_args( + return run_coroutine( the_container.issue_credential, cred_def_id, cred_attrs, @@ -155,7 +120,7 @@ def aries_container_receive_credential( cred_def_id: str, cred_attrs: list, ): - return run_coroutine_with_args( + return run_coroutine( the_container.receive_credential, cred_def_id, cred_attrs, @@ -165,10 +130,12 @@ def aries_container_receive_credential( def aries_container_request_proof( the_container: AgentContainer, proof_request: dict, + explicit_revoc_required: bool = False, ): - return run_coroutine_with_args( + return run_coroutine( the_container.request_proof, proof_request, + explicit_revoc_required=explicit_revoc_required, ) @@ -176,7 +143,7 @@ def aries_container_verify_proof( the_container: AgentContainer, proof_request: dict, ): - return run_coroutine_with_args( + return run_coroutine( the_container.verify_proof, proof_request, ) @@ -228,7 +195,7 @@ def agent_container_GET( text: bool = False, params: dict = None, ) -> dict: - return run_coroutine_with_kwargs( + return run_coroutine( the_container.admin_GET, path, text=text, @@ -243,7 +210,7 @@ def agent_container_POST( text: bool = False, params: dict = None, ) -> dict: - return run_coroutine_with_kwargs( + return run_coroutine( the_container.admin_POST, path, data=data, @@ -259,7 +226,7 @@ def agent_container_PATCH( text: bool = False, params: dict = None, ) -> dict: - return run_coroutine_with_kwargs( + return run_coroutine( the_container.admin_PATCH, path, data=data, @@ -275,7 +242,7 @@ def agent_container_PUT( text: bool = False, params: dict = None, ) -> dict: - return run_coroutine_with_kwargs( + return run_coroutine( the_container.admin_PUT, path, data=data, diff --git a/demo/docker-agent/Dockerfile.acapy b/demo/docker-agent/Dockerfile.acapy new file mode 100644 index 0000000000..a8eee30ae0 --- /dev/null +++ b/demo/docker-agent/Dockerfile.acapy @@ -0,0 +1,10 @@ +FROM bcgovimages/aries-cloudagent:py36-1.16-1_1.0.0-rc0 + +USER root + +ADD https://github.com/stedolan/jq/releases/download/jq-1.6/jq-linux64 ./jq +RUN chmod +x ./jq +COPY ngrok-wait.sh ngrok-wait.sh +RUN chmod +x ./ngrok-wait.sh + +USER $user diff --git a/demo/docker-agent/README.md b/demo/docker-agent/README.md new file mode 100644 index 0000000000..c5fb56dc59 --- /dev/null +++ b/demo/docker-agent/README.md @@ -0,0 +1,87 @@ +# Running an Author Agent and connecting to an Endorser + +This directory contains scripts to run an aca-py agent as an Author, that can conenct to an Endorser service. + +## Running the Author Agent + +The docker-compose script runs ngrok to expose the agent's port publicly, and stores wallet data in a postgres database. + +To run the Author agent in this repo, open a command shell in this directory and run: + +- to build the containers: + +```bash +docker-compose build +``` + +- to run the author agent: + +```bash +docker-compose up +``` + +You can connect to the [agent's api service here](http://localhost:8010). + +Note that all the configuration settings are hard-coded in the docker-compose file and ngrok-wait.sh script, so if you change any configs you need to rebuild the docker images. + +- to shut down the agent: + +```bash +docker-compose stop +docker-compose rm -f +``` + +This will leave the agent's wallet data, so if you restart the agent it will maintain any created data. + +- to remove the agent's wallet: + +```bash +docker volume rm docker-agent_wallet-db-data +``` + +Note that the Author agent is not (yet) configured with revocations enabled or a tails server, so revocation is not supported. + +## Connecting to an Endorser Service + +For this example, we will connect to [this endorser service](https://github.com/bcgov/aries-endorser-service), which you can connect to locally at `http://localhost:5050/endorser/docs`. + +Make sure you start the endorser service on the same ledger as your author, and make sure the endorser has a public DID with ENDORSER role. + +For example start the endorser service as `LEDGER_URL=http://test.bcovrin.vonx.io TAILS_SERVER_URL=https://tails-test.vonx.io ./manage start --logs` and then make sure the Author agent is started with `--genesis_url http://test.bcovrin.vonx.io/genesis`. + +### Connecting the Author to the Endorser + +Endorser Service: Use the `GET /v1/admin/config` endpoint to fetch the endorser's configuration, including the public DID (which the author will need to know). Also confirm whether the `ENDORSER_AUTO_ACCEPT_CONNECTIONS` and `ENDORSER_AUTO_ENDORSE_REQUESTS` settings are `True` or `False` - for the following we will assume that both are `False` and the endorser must explicitely respond to all requests. + +Author Agent: Use the `POST /didexchange/create-request` to request a connection with the endorser, using the endorser's public DID. Set the `alias` to `Endorser` - this *MUST* match the `--endorser-alias 'Endorser'` setting (in the ngrok-wait.sh script). Use the `GET /connections` endpoint to verify the connection is in `request` state. + +Endorser Service: Use the `GET /v1/connections` endpoint to see the connection request (state `request`). Using the `connection_id`, call the `POST /connections/{connection_id}/accept` endpoint to accept the request. Verify that the connection state goes to `active`. + +Author Agent: Verify the connection state goes to `active`. Use the `POST /transactions/{conn_id}/set-endorser-role` to set the connection role to `TRANSACTION_AUTHOR`, and then use `POST /transactions/{conn_id}/set-endorser-info` to set the endorser's alias to `Endorser` and the public DID to the endorser's public DID. Verify the settings using the `GET /connections/{conn_id}/meta-data` endpoint. + +The connection is now setup between the two agents! + +### Creating a Public Author DID + +Author Agent: Use the `POST /wallet/did/create` (use an empty `{}` POST body) to create a local did. Then use `POST /ledger/register-nym` to send the data to the ledger - this will create a transaction and send it to the endorser service. + +Endorser Service: Use the `GET /v1/endorse/transactions` endpoint to see the endorse request - it should be in state `request_received`. Using the `POST /v1/endorse/transactions/{transaction_id}/endorse` endpoint and the `transaction_id`, approve the request. The state should now (eventually) go to `transaction_acked`. + +Author Service: Use the `GET /transactions` endpoint to verify the transaction is in `transaction_acked` state. Then use the `POST /wallet/did/public` to set the new DID to be the Author's public DID. This will generate another endorser transaction to set the DID's endpoint (ATTRIB transaction) on the ledger. + +Endorser Service: Use the same endpoints as above (`GET /v1/endorse/transactions` and then `POST /v1/endorse/transactions/{transaction_id}/endorse`) to view the endorse request and approve it. + +### Endorsing Author Requests + +Author requests to create schema, create credential definition and create revocation registries will all now generate endorse requests to the endorser. + +Author Agent: To create a schema use the `POST /schemas` endpoint. This will create an endorse request. + +Endorser Service: Use the same endpoints as above (`GET /v1/endorse/transactions` and then `POST /v1/endorse/transactions/{transaction_id}/endorse`) to view the endorse request and approve it. + +Author Agent: To create a cred def use the `POST /credential-definitions` endpoint. This will create an endorse request. + +Endorser Service: Use the same endpoints as above (`GET /v1/endorse/transactions` and then `POST /v1/endorse/transactions/{transaction_id}/endorse`) to view the endorse request and approve it. + + + diff --git a/demo/docker-agent/docker-compose.yml b/demo/docker-agent/docker-compose.yml new file mode 100644 index 0000000000..35b7b2ac57 --- /dev/null +++ b/demo/docker-agent/docker-compose.yml @@ -0,0 +1,45 @@ +# Sample docker-compose to start a local aca-py author agent +# To start aca-py and the postgres database, just run `docker-compose up` +# To shut down the services run `docker-compose rm` - this will retain the postgres database, so you can change aca-py startup parameters +# and restart the docker containers without losing your wallet data +# If you want to delete your wallet data just run `docker volume ls -q | xargs docker volume rm` +version: "3" +services: + ngrok-agent: + image: wernight/ngrok + ports: + - 4067:4040 + command: ngrok http author-agent:8001 --log stdout + + author-agent: + build: + context: . + dockerfile: Dockerfile.acapy + environment: + - NGROK_NAME=ngrok-agent + ports: + - 8010:8010 + - 8001:8001 + depends_on: + - wallet-db + entrypoint: /bin/bash + command: [ + "-c", + "sleep 5; \ + ./ngrok-wait.sh" + ] + volumes: + - ./ngrok-wait.sh:/home/indy/ngrok-wait.sh + + wallet-db: + image: postgres:12 + environment: + - POSTGRES_USER=DB_USER + - POSTGRES_PASSWORD=DB_PASSWORD + ports: + - 5433:5432 + volumes: + - wallet-db-data:/var/lib/pgsql/data + +volumes: + wallet-db-data: diff --git a/demo/docker-agent/ngrok-wait.sh b/demo/docker-agent/ngrok-wait.sh new file mode 100755 index 0000000000..4c7ccde9db --- /dev/null +++ b/demo/docker-agent/ngrok-wait.sh @@ -0,0 +1,51 @@ +#!/bin/bash + +# based on code developed by Sovrin: https://github.com/hyperledger/aries-acapy-plugin-toolbox + +echo "using ngrok end point [$NGROK_NAME]" + +NGROK_ENDPOINT=null +while [ -z "$NGROK_ENDPOINT" ] || [ "$NGROK_ENDPOINT" = "null" ] +do + echo "Fetching end point from ngrok service" + NGROK_ENDPOINT=$(curl --silent $NGROK_NAME:4040/api/tunnels | ./jq -r '.tunnels[] | select(.proto=="https") | .public_url') + + if [ -z "$NGROK_ENDPOINT" ] || [ "$NGROK_ENDPOINT" = "null" ]; then + echo "ngrok not ready, sleeping 5 seconds...." + sleep 5 + fi +done + +export ACAPY_ENDPOINT=$NGROK_ENDPOINT + +echo "Starting aca-py agent with endpoint [$ACAPY_ENDPOINT]" + +# ... if you want to echo the aca-py startup command ... +set -x + +exec aca-py start \ + --auto-provision \ + --inbound-transport http '0.0.0.0' 8001 \ + --outbound-transport http \ + --genesis-url "http://test.bcovrin.vonx.io/genesis" \ + --endpoint "${ACAPY_ENDPOINT}" \ + --auto-ping-connection \ + --monitor-ping \ + --public-invites \ + --wallet-type "indy" \ + --wallet-name "test_author" \ + --wallet-key "secret_key" \ + --wallet-storage-type "postgres_storage" \ + --wallet-storage-config "{\"url\":\"wallet-db:5432\",\"max_connections\":5}" \ + --wallet-storage-creds "{\"account\":\"DB_USER\",\"password\":\"DB_PASSWORD\",\"admin_account\":\"DB_USER\",\"admin_password\":\"DB_PASSWORD\"}" \ + --admin '0.0.0.0' 8010 \ + --label "test_author" \ + --admin-insecure-mode \ + --endorser-protocol-role author \ + --endorser-alias 'Endorser' \ + --auto-request-endorsement \ + --auto-write-transactions \ + --auto-create-revocation-transactions \ + --log-level "error" + +# --genesis-url "https://raw.githubusercontent.com/ICCS-ISAC/dtrust-reconu/main/CANdy/dev/pool_transactions_genesis" \ diff --git a/demo/docker-test/README.md b/demo/docker-test/README.md new file mode 100644 index 0000000000..e415637cf4 --- /dev/null +++ b/demo/docker-test/README.md @@ -0,0 +1,39 @@ +# Aca-Py Docker Test Scripts + +These docker compose files allow you to run an aca-py instance against a postgres database. + +There are separate scripts for starting the database and agent to allow you to restart the agent against the same postgres database. + +This is useful for - for example - initializing a database with on older aca-py version, and then running an upgrade to a newer version. + +To start the database: + +```bash +docker-compose -f docker-compose-wallet.yml up +``` + +To start the agent: + +```bash +docker-compose -f docker-compose-agent.yml up +``` + +Note you can edit the docker-compose file to change the aca-py version. + +To stop the agent: + +```bash +docker-compose -f docker-compose-agent.yml rm +``` + +To stop the database + +```bash +docker-compose -f docker-compose-wallet.yml rm +``` + +To remove the database volume: + +```bash +docker volume rm docker-test_wallet-db-data +``` diff --git a/demo/docker-test/db/Dockerfile b/demo/docker-test/db/Dockerfile new file mode 100644 index 0000000000..5d4e896508 --- /dev/null +++ b/demo/docker-test/db/Dockerfile @@ -0,0 +1,3 @@ +FROM postgres:14 +COPY ./init-postgres-role.sh /docker-entrypoint-initdb.d/init-postgres-role.sh +CMD ["docker-entrypoint.sh", "postgres"] \ No newline at end of file diff --git a/demo/docker-test/db/init-postgres-role.sh b/demo/docker-test/db/init-postgres-role.sh new file mode 100644 index 0000000000..3178a06c97 --- /dev/null +++ b/demo/docker-test/db/init-postgres-role.sh @@ -0,0 +1,6 @@ +#!/bin/bash +set -e + +psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" --set username=$POSTGRES_ADMIN_USER --set password="'$POSTGRES_ADMIN_PASSWORD'"<<-EOSQL + CREATE ROLE :username WITH LOGIN SUPERUSER INHERIT CREATEDB CREATEROLE REPLICATION ENCRYPTED PASSWORD :password; +EOSQL \ No newline at end of file diff --git a/demo/docker-test/docker-compose-agent.yml b/demo/docker-test/docker-compose-agent.yml new file mode 100644 index 0000000000..b76c8e93f9 --- /dev/null +++ b/demo/docker-test/docker-compose-agent.yml @@ -0,0 +1,47 @@ +version: "3" +services: + vcr-agent: + #image: bcgovimages/aries-cloudagent:py36-1.16-1_0.7.5 + build: + context: ../../ + dockerfile: docker/Dockerfile.run + ports: + - 8010:8010 + - 8001:8001 + networks: + - wallet-net + entrypoint: /bin/bash + command: [ + "-c", + "sleep 5; \ + aca-py start \ + --auto-provision \ + --seed '00000000o_faber_secondary_school' \ + --inbound-transport http '0.0.0.0' 8001 \ + --endpoint 'http://host.docker.internal:8001' \ + --outbound-transport http \ + --genesis-url 'http://test.bcovrin.vonx.io/genesis' \ + --auto-accept-invites \ + --auto-accept-requests \ + --auto-ping-connection \ + --auto-respond-messages \ + --auto-respond-credential-proposal \ + --auto-respond-credential-offer \ + --auto-respond-credential-request \ + --auto-verify-presentation \ + --wallet-type 'askar' \ + --wallet-name 'acapy_agent_wallet' \ + --wallet-key 'key' \ + --wallet-storage-type 'postgres_storage' \ + --wallet-storage-config '{\"url\":\"wallet-db:5432\",\"max_connections\":5}' \ + --wallet-storage-creds '{\"account\":\"DB_USER\",\"password\":\"DB_PASSWORD\",\"admin_account\":\"postgres\",\"admin_password\":\"mysecretpassword\"}' \ + --admin '0.0.0.0' 8010 \ + --admin-insecure-mode \ + --label 'tester_agent' \ + --log-level 'info' ", + ] + +networks: + wallet-net: + external: + name: docker-test_wallet-net diff --git a/demo/docker-test/docker-compose-wallet.yml b/demo/docker-test/docker-compose-wallet.yml new file mode 100644 index 0000000000..c000ef7a14 --- /dev/null +++ b/demo/docker-test/docker-compose-wallet.yml @@ -0,0 +1,21 @@ +version: "3" +services: + wallet-db: + build: ./db + environment: + - POSTGRES_USER=DB_USER + - POSTGRES_PASSWORD=DB_PASSWORD + - POSTGRES_ADMIN_USER=postgres + - POSTGRES_ADMIN_PASSWORD=mysecretpassword + networks: + - wallet-net + ports: + - 5432:5432 + volumes: + - wallet-db-data:/var/lib/pgsql/data + +volumes: + wallet-db-data: + +networks: + wallet-net: diff --git a/demo/docker/docker-compose.yml b/demo/docker/docker-compose.yml new file mode 100644 index 0000000000..29facddf0f --- /dev/null +++ b/demo/docker/docker-compose.yml @@ -0,0 +1,81 @@ +# Sample docker-compose to start a local aca-py in multi-ledger mode +# To start aca-py and the postgres database, just run `docker-compose up` +# To shut down the services run `docker-compose rm` - this will retain the postgres database, so you can change aca-py startup parameters +# and restart the docker containers without losing your wallet data +# If you want to delete your wallet data just run `docker volume ls -q | xargs docker volume rm` + +# Note this requires von-network (https://github.com/bcgov/von-network) and indy-tails-server (https://github.com/bcgov/indy-tails-server) are already running + +version: "3" +services: + vcr-agent: + build: + context: ../../ + dockerfile: docker/Dockerfile.run + ports: + - 8010:8010 + - 8001:8001 + depends_on: + - wallet-db + entrypoint: /bin/bash + command: [ + "-c", + "sleep 5; \ + aca-py start \ + --auto-provision \ + --seed '00000000o_faber_secondary_school' \ + --inbound-transport http '0.0.0.0' 8001 \ + --endpoint 'http://host.docker.internal:8001' \ + --outbound-transport http \ + --genesis-url 'https://raw.githubusercontent.com/sovrin-foundation/sovrin/master/sovrin/pool_transactions_builder_genesis' \ + --auto-accept-invites \ + --auto-accept-requests \ + --auto-ping-connection \ + --auto-respond-messages \ + --auto-respond-credential-proposal \ + --auto-respond-credential-offer \ + --auto-respond-credential-request \ + --auto-verify-presentation \ + --tails-server-base-url 'https://tails-test.vonx.io' \ + --notify-revocation \ + --monitor-revocation-notification \ + --wallet-type 'askar' \ + --wallet-name 'acapy_agent_wallet' \ + --wallet-key 'key' \ + --wallet-storage-type 'postgres_storage' \ + --wallet-storage-config '{\"url\":\"wallet-db:5432\",\"max_connections\":5}' \ + --wallet-storage-creds '{\"account\":\"DB_USER\",\"password\":\"DB_PASSWORD\",\"admin_account\":\"DB_USER\",\"admin_password\":\"DB_PASSWORD\"}' \ + --admin '0.0.0.0' 8010 \ + --admin-insecure-mode \ + --label 'tester_agent' \ + --log-level 'info' ", + ] + volumes: + - ./ledgers.yaml:/home/indy/ledgers.yaml + networks: + - tails-network + +# note - if you want to start aca-py in single-ledger mode, replace the `--genesis-transactions-list` parameter above with: +# --genesis-url 'https://raw.githubusercontent.com/sovrin-foundation/sovrin/master/sovrin/pool_transactions_builder_genesis' \ +# --genesis-url 'http://host.docker.internal:9000/genesis' \ +# --genesis-transactions-list 'ledgers.yaml' \ + + wallet-db: + image: postgres:12 + environment: + - POSTGRES_USER=DB_USER + - POSTGRES_PASSWORD=DB_PASSWORD + ports: + - 5433:5432 + volumes: + - wallet-db-data:/var/lib/pgsql/data + networks: + - tails-network + +volumes: + wallet-db-data: + +networks: + tails-network: + external: + name: docker_tails-server diff --git a/demo/docker/ledgers.yaml b/demo/docker/ledgers.yaml new file mode 100644 index 0000000000..da87ee6cf0 --- /dev/null +++ b/demo/docker/ledgers.yaml @@ -0,0 +1,21 @@ +# the `id` is used as the `pool_name` in aca-py +# note that if you are upgrading from single- to multi-ledger, you need to *either*: +# - set the `id` of your `is_write: true` ledger to `default` (the `pool_name` used in single-ledger mode) +# *or*: +# - re-accept the TAA once you start aca-py in multi-ledger mode +# (the TAA acceptance is stored in a wallet record keyed on the `pool_name`) +#- id: localhost +# is_production: true +# is_write: true +# genesis_url: 'http://host.docker.internal:9000/genesis' +# register a Sovrin dev DID here: https://selfserve.sovrin.org/ +- id: SOVRINDevelopment + is_production: true + is_write: true + genesis_url: 'https://raw.githubusercontent.com/sovrin-foundation/sovrin/master/sovrin/pool_transactions_builder_genesis' +- id: BCovrinTest + is_production: true + genesis_url: 'http://test.bcovrin.vonx.io/genesis' +- id: CANdyDev + is_production: true + genesis_url: 'https://raw.githubusercontent.com/ICCS-ISAC/dtrust-reconu/main/CANdy/dev/pool_transactions_genesis' diff --git a/demo/elk-stack/.env.sample b/demo/elk-stack/.env.sample new file mode 100644 index 0000000000..f0a50a5c35 --- /dev/null +++ b/demo/elk-stack/.env.sample @@ -0,0 +1,47 @@ +ELASTIC_VERSION=8.7.0 + +## Passwords for stack users +# + +# User 'elastic' (built-in) +# +# Superuser role, full access to cluster management and data indices. +# https://www.elastic.co/guide/en/elasticsearch/reference/current/built-in-users.html +ELASTIC_PASSWORD='changeme' + +# User 'logstash_internal' (custom) +# +# The user Logstash uses to connect and send data to Elasticsearch. +# https://www.elastic.co/guide/en/logstash/current/ls-security.html +LOGSTASH_INTERNAL_PASSWORD='changeme' + +# User 'kibana_system' (built-in) +# +# The user Kibana uses to connect and communicate with Elasticsearch. +# https://www.elastic.co/guide/en/elasticsearch/reference/current/built-in-users.html +KIBANA_SYSTEM_PASSWORD='changeme' + +# Users 'metricbeat_internal', 'filebeat_internal' and 'heartbeat_internal' (custom) +# +# The users Beats use to connect and send data to Elasticsearch. +# https://www.elastic.co/guide/en/beats/metricbeat/current/feature-roles.html +METRICBEAT_INTERNAL_PASSWORD='' +FILEBEAT_INTERNAL_PASSWORD='' +HEARTBEAT_INTERNAL_PASSWORD='' + +# User 'monitoring_internal' (custom) +# +# The user Metricbeat uses to collect monitoring data from stack components. +# https://www.elastic.co/guide/en/elasticsearch/reference/current/how-monitoring-works.html +MONITORING_INTERNAL_PASSWORD='' + +# User 'beats_system' (built-in) +# +# The user the Beats use when storing monitoring information in Elasticsearch. +# https://www.elastic.co/guide/en/elasticsearch/reference/current/built-in-users.html +BEATS_SYSTEM_PASSWORD='' + +# Docker Network Name for docker-elk +# +# +ELK_NETWORK_NAME=elknet \ No newline at end of file diff --git a/demo/EFK-stack/LICENSE b/demo/elk-stack/LICENSE similarity index 94% rename from demo/EFK-stack/LICENSE rename to demo/elk-stack/LICENSE index 54de4db429..0dbd69f8e2 100644 --- a/demo/EFK-stack/LICENSE +++ b/demo/elk-stack/LICENSE @@ -1,6 +1,6 @@ -MIT License +The MIT License (MIT) -Copyright (c) 2018 Gianfranco Reppucci +Copyright (c) 2015 Anthony Lapenna Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -19,4 +19,3 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - diff --git a/demo/elk-stack/README.md b/demo/elk-stack/README.md new file mode 100644 index 0000000000..8e0fae75cf --- /dev/null +++ b/demo/elk-stack/README.md @@ -0,0 +1,573 @@ +# ACA-Py ELK Stack for demos + +Note - this code was originally obtained from [https://github.com/deviantony/docker-elk](https://github.com/deviantony/docker-elk). + +The following changes were made to better incorporate this stack into ACA-Py demos and tracing. + +- renamed the network to `elknet` and added environment variable `ELK_NETWORK_NAME` in `.env` to change the name of the docker network. +- set [elasticsearch](./elasticsearch/config/elasticsearch.yml) license to `basic` +- [logstash.conf](./logstash/pipeline/logstash.conf) set an http port (9700) and exposed this for pushing agent traces into ELK. + +## run + +``` +cp .env.sample .env +docker compose build +docker compose up +``` + +Using the default configuration, `elasticsearch`, `kibana` and `logstash` services will be started. Kibana can be accessed at [http://localhost:5601](http://localhost:5601), and you can log in with `elastic / changeme` as the username and password. + +A `log-*` index will be created, and you can refresh the [Discover Analytics](http://localhost:5601/app/discover#/?_g=(filters:!(),refreshInterval:(pause:!t,value:60000),time:(from:now-15m,to:now))&_a=(columns:!(traced_type,handler,ellapsed_milli,outcome,thread_id,msg_id),filters:!(),index:'logs-*',interval:auto,query:(language:kuery,query:''),sort:!(!('@timestamp',desc)))) to see any logged events. + + +We can run demos to see agent tracing events and attach them to the `elknet` network to push events to ELK. + +## demos + +Assuming the elk stack is running from above... from your demos directory, in two separate bash shells, startup the demo as follows: + +```bash +DOCKER_NET=elknet TRACE_TARGET_URL=logstash:9700 LEDGER_URL=http://test.bcovrin.vonx.io ./run_demo faber --trace-http +``` + +```bash +DOCKER_NET=elknet TRACE_TARGET_URL=logstash:9700 LEDGER_URL=http://test.bcovrin.vonx.io ./run_demo alice --trace-http +``` + +And run the demo scenarios as you wish. + +**Note**: the `LEDGER_URL` override is unnecessary for tracing; it is only used so `faber` and `alice` are on the same ledger as the following `multi-demo`. + +## multi-demo + +The `multi-demo` (running ACA-Py in multitenant mode) requires a few edits to [docker-compose.yml](../multi-demo/docker-compose.yml). + + +Note the reference to `elknet`. + +``` +networks: + app-network: + name: ${APP_NETWORK_NAME:-appnet} + driver: bridge + elk-network: + name: ${ELK_NETWORK_NAME:-elknet} + driver: bridge +``` + + +And uncomment the tracing environment variables... + +```docker-compose.yml + environment: + - NGROK_NAME=ngrok-agent + - ACAPY_AGENT_ACCESS=${ACAPY_AGENT_ACCESS:-public} + - ACAPY_TRACE=${ACAPY_TRACE:-1} + - ACAPY_TRACE_TARGET=${ACAPY_TRACE_TARGET:-http://logstash:9700/} + - ACAPY_TRACE_TAG=${ACAPY_TRACE_TAG:-acapy.events} + - ACAPY_TRACE_LABEL=${ACAPY_TRACE_LABEL:-multi.agent.trace} +``` + +Another change you may wish to make is setting `ACAPY_AGENT_ACCESS` to `local`. Use this if there is no need for ngrok tunnelling. For instance, if all your agents are local to the docker network (ie not phones) and connect only to themselves (or with some "public" agents), then we can eliminate `ngrok` throwing errors if we have too many simultaneous http connections (ie hundreds of wallets in a load test). + +Assuming the elk stack is running from above... from your multi-demo directory, startup the agent as follows: + +```bash +docker compose build +docker compose up +``` + + +# Elastic stack (ELK) on Docker + +[![Elastic Stack version](https://img.shields.io/badge/Elastic%20Stack-8.7.0-00bfb3?style=flat&logo=elastic-stack)](https://www.elastic.co/blog/category/releases) +[![Build Status](https://github.com/deviantony/docker-elk/workflows/CI/badge.svg?branch=main)](https://github.com/deviantony/docker-elk/actions?query=workflow%3ACI+branch%3Amain) +[![Join the chat](https://badges.gitter.im/Join%20Chat.svg)](https://app.gitter.im/#/room/#deviantony_docker-elk:gitter.im) + +Run the latest version of the [Elastic stack][elk-stack] with Docker and Docker Compose. + +It gives you the ability to analyze any data set by using the searching/aggregation capabilities of Elasticsearch and +the visualization power of Kibana. + +![Animated demo](https://user-images.githubusercontent.com/3299086/155972072-0c89d6db-707a-47a1-818b-5f976565f95a.gif) + +> **Note** +> [Platinum][subscriptions] features are enabled by default for a [trial][license-mngmt] duration of **30 days**. After +> this evaluation period, you will retain access to all the free features included in the Open Basic license seamlessly, +> without manual intervention required, and without losing any data. Refer to the [How to disable paid +> features](#how-to-disable-paid-features) section to opt out of this behaviour. + +Based on the official Docker images from Elastic: + +* [Elasticsearch](https://github.com/elastic/elasticsearch/tree/main/distribution/docker) +* [Logstash](https://github.com/elastic/logstash/tree/main/docker) +* [Kibana](https://github.com/elastic/kibana/tree/main/src/dev/build/tasks/os_packages/docker_generator) + +Other available stack variants: + +* [`tls`](https://github.com/deviantony/docker-elk/tree/tls): TLS encryption enabled in Elasticsearch, Kibana (opt in), + and Fleet +* [`searchguard`](https://github.com/deviantony/docker-elk/tree/searchguard): Search Guard support + +--- + +## Philosophy + +We aim at providing the simplest possible entry into the Elastic stack for anybody who feels like experimenting with +this powerful combo of technologies. This project's default configuration is purposely minimal and unopinionated. It +does not rely on any external dependency, and uses as little custom automation as necessary to get things up and +running. + +Instead, we believe in good documentation so that you can use this repository as a template, tweak it, and make it _your +own_. [sherifabdlnaby/elastdocker][elastdocker] is one example among others of project that builds upon this idea. + +--- + +## Contents + +1. [Requirements](#requirements) + * [Host setup](#host-setup) + * [Docker Desktop](#docker-desktop) + * [Windows](#windows) + * [macOS](#macos) +1. [Usage](#usage) + * [Bringing up the stack](#bringing-up-the-stack) + * [Initial setup](#initial-setup) + * [Setting up user authentication](#setting-up-user-authentication) + * [Injecting data](#injecting-data) + * [Cleanup](#cleanup) + * [Version selection](#version-selection) +1. [Configuration](#configuration) + * [How to configure Elasticsearch](#how-to-configure-elasticsearch) + * [How to configure Kibana](#how-to-configure-kibana) + * [How to configure Logstash](#how-to-configure-logstash) + * [How to disable paid features](#how-to-disable-paid-features) + * [How to scale out the Elasticsearch cluster](#how-to-scale-out-the-elasticsearch-cluster) + * [How to re-execute the setup](#how-to-re-execute-the-setup) + * [How to reset a password programmatically](#how-to-reset-a-password-programmatically) +1. [Extensibility](#extensibility) + * [How to add plugins](#how-to-add-plugins) + * [How to enable the provided extensions](#how-to-enable-the-provided-extensions) +1. [JVM tuning](#jvm-tuning) + * [How to specify the amount of memory used by a service](#how-to-specify-the-amount-of-memory-used-by-a-service) + * [How to enable a remote JMX connection to a service](#how-to-enable-a-remote-jmx-connection-to-a-service) +1. [Going further](#going-further) + * [Plugins and integrations](#plugins-and-integrations) + +## Requirements + +### Host setup + +* [Docker Engine][docker-install] version **18.06.0** or newer +* [Docker Compose][compose-install] version **1.26.0** or newer (including [Compose V2][compose-v2]) +* 1.5 GB of RAM + +> **Warning** +> While Compose versions between **1.22.0** and **1.25.5** can technically run this stack as well, these versions have a +> [known issue](https://github.com/deviantony/docker-elk/pull/678#issuecomment-1055555368) which prevents them from +> parsing quoted values properly inside `.env` files. + +> **Note** +> Especially on Linux, make sure your user has the [required permissions][linux-postinstall] to interact with the Docker +> daemon. + +By default, the stack exposes the following ports: + +* 5044: Logstash Beats input +* 50000: Logstash TCP input +* 9600: Logstash monitoring API +* 9200: Elasticsearch HTTP +* 9300: Elasticsearch TCP transport +* 5601: Kibana + +> **Warning** +> Elasticsearch's [bootstrap checks][bootstrap-checks] were purposely disabled to facilitate the setup of the Elastic +> stack in development environments. For production setups, we recommend users to set up their host according to the +> instructions from the Elasticsearch documentation: [Important System Configuration][es-sys-config]. + +### Docker Desktop + +#### Windows + +If you are using the legacy Hyper-V mode of _Docker Desktop for Windows_, ensure [File Sharing][win-filesharing] is +enabled for the `C:` drive. + +#### macOS + +The default configuration of _Docker Desktop for Mac_ allows mounting files from `/Users/`, `/Volume/`, `/private/`, +`/tmp` and `/var/folders` exclusively. Make sure the repository is cloned in one of those locations or follow the +instructions from the [documentation][mac-filesharing] to add more locations. + +## Usage + +> **Warning** +> You must rebuild the stack images with `docker-compose build` whenever you switch branch or update the +> [version](#version-selection) of an already existing stack. + +### Bringing up the stack + +Clone this repository onto the Docker host that will run the stack with the command below: + +```sh +git clone https://github.com/deviantony/docker-elk.git +``` + +Then, start the stack components locally with Docker Compose: + +```sh +docker-compose up +``` + +> **Note** +> You can also run all services in the background (detached mode) by appending the `-d` flag to the above command. + +Give Kibana about a minute to initialize, then access the Kibana web UI by opening in a web +browser and use the following (default) credentials to log in: + +* user: *elastic* +* password: *changeme* + +> **Note** +> Upon the initial startup, the `elastic`, `logstash_internal` and `kibana_system` Elasticsearch users are intialized +> with the values of the passwords defined in the [`.env`](.env) file (_"changeme"_ by default). The first one is the +> [built-in superuser][builtin-users], the other two are used by Kibana and Logstash respectively to communicate with +> Elasticsearch. This task is only performed during the _initial_ startup of the stack. To change users' passwords +> _after_ they have been initialized, please refer to the instructions in the next section. + +### Initial setup + +#### Setting up user authentication + +> **Note** +> Refer to [Security settings in Elasticsearch][es-security] to disable authentication. + +> **Warning** +> Starting with Elastic v8.0.0, it is no longer possible to run Kibana using the bootstraped privileged `elastic` user. + +The _"changeme"_ password set by default for all aforementioned users is **unsecure**. For increased security, we will +reset the passwords of all aforementioned Elasticsearch users to random secrets. + +1. Reset passwords for default users + + The commands below reset the passwords of the `elastic`, `logstash_internal` and `kibana_system` users. Take note + of them. + + ```sh + docker-compose exec elasticsearch bin/elasticsearch-reset-password --batch --user elastic + ``` + + ```sh + docker-compose exec elasticsearch bin/elasticsearch-reset-password --batch --user logstash_internal + ``` + + ```sh + docker-compose exec elasticsearch bin/elasticsearch-reset-password --batch --user kibana_system + ``` + + If the need for it arises (e.g. if you want to [collect monitoring information][ls-monitoring] through Beats and + other components), feel free to repeat this operation at any time for the rest of the [built-in + users][builtin-users]. + +1. Replace usernames and passwords in configuration files + + Replace the password of the `elastic` user inside the `.env` file with the password generated in the previous step. + Its value isn't used by any core component, but [extensions](#how-to-enable-the-provided-extensions) use it to + connect to Elasticsearch. + + > **Note** + > In case you don't plan on using any of the provided [extensions](#how-to-enable-the-provided-extensions), or + > prefer to create your own roles and users to authenticate these services, it is safe to remove the + > `ELASTIC_PASSWORD` entry from the `.env` file altogether after the stack has been initialized. + + Replace the password of the `logstash_internal` user inside the `.env` file with the password generated in the + previous step. Its value is referenced inside the Logstash pipeline file (`logstash/pipeline/logstash.conf`). + + Replace the password of the `kibana_system` user inside the `.env` file with the password generated in the previous + step. Its value is referenced inside the Kibana configuration file (`kibana/config/kibana.yml`). + + See the [Configuration](#configuration) section below for more information about these configuration files. + +1. Restart Logstash and Kibana to re-connect to Elasticsearch using the new passwords + + ```sh + docker-compose up -d logstash kibana + ``` + +> **Note** +> Learn more about the security of the Elastic stack at [Secure the Elastic Stack][sec-cluster]. + +#### Injecting data + +Launch the Kibana web UI by opening in a web browser, and use the following credentials to log +in: + +* user: *elastic* +* password: *\* + +Now that the stack is fully configured, you can go ahead and inject some log entries. + +The shipped Logstash configuration allows you to send data over the TCP port 50000. For example, you can use one of the +following commands — depending on your installed version of `nc` (Netcat) — to ingest the content of the log file +`/path/to/logfile.log` in Elasticsearch, via Logstash: + +```sh +# Execute `nc -h` to determine your `nc` version + +cat /path/to/logfile.log | nc -q0 localhost 50000 # BSD +cat /path/to/logfile.log | nc -c localhost 50000 # GNU +cat /path/to/logfile.log | nc --send-only localhost 50000 # nmap +``` + +You can also load the sample data provided by your Kibana installation. + +### Cleanup + +Elasticsearch data is persisted inside a volume by default. + +In order to entirely shutdown the stack and remove all persisted data, use the following Docker Compose command: + +```sh +docker-compose down -v +``` + +### Version selection + +This repository stays aligned with the latest version of the Elastic stack. The `main` branch tracks the current major +version (8.x). + +To use a different version of the core Elastic components, simply change the version number inside the [`.env`](.env) +file. If you are upgrading an existing stack, remember to rebuild all container images using the `docker-compose build` +command. + +> **Warning** +> Always pay attention to the [official upgrade instructions][upgrade] for each individual component before performing a +> stack upgrade. + +Older major versions are also supported on separate branches: + +* [`release-7.x`](https://github.com/deviantony/docker-elk/tree/release-7.x): 7.x series +* [`release-6.x`](https://github.com/deviantony/docker-elk/tree/release-6.x): 6.x series (End-of-life) +* [`release-5.x`](https://github.com/deviantony/docker-elk/tree/release-5.x): 5.x series (End-of-life) + +## Configuration + +> **Note** +> Configuration is not dynamically reloaded, you will need to restart individual components after any configuration +> change. + +### How to configure Elasticsearch + +The Elasticsearch configuration is stored in [`elasticsearch/config/elasticsearch.yml`][config-es]. + +You can also specify the options you want to override by setting environment variables inside the Compose file: + +```yml +elasticsearch: + + environment: + network.host: _non_loopback_ + cluster.name: my-cluster +``` + +Please refer to the following documentation page for more details about how to configure Elasticsearch inside Docker +containers: [Install Elasticsearch with Docker][es-docker]. + +### How to configure Kibana + +The Kibana default configuration is stored in [`kibana/config/kibana.yml`][config-kbn]. + +You can also specify the options you want to override by setting environment variables inside the Compose file: + +```yml +kibana: + + environment: + SERVER_NAME: kibana.example.org +``` + +Please refer to the following documentation page for more details about how to configure Kibana inside Docker +containers: [Install Kibana with Docker][kbn-docker]. + +### How to configure Logstash + +The Logstash configuration is stored in [`logstash/config/logstash.yml`][config-ls]. + +You can also specify the options you want to override by setting environment variables inside the Compose file: + +```yml +logstash: + + environment: + LOG_LEVEL: debug +``` + +Please refer to the following documentation page for more details about how to configure Logstash inside Docker +containers: [Configuring Logstash for Docker][ls-docker]. + +### How to disable paid features + +Switch the value of Elasticsearch's `xpack.license.self_generated.type` setting from `trial` to `basic` (see [License +settings][license-settings]). + +You can also cancel an ongoing trial before its expiry date — and thus revert to a basic license — either from the +[License Management][license-mngmt] panel of Kibana, or using Elasticsearch's [Licensing APIs][license-apis]. + +### How to scale out the Elasticsearch cluster + +Follow the instructions from the Wiki: [Scaling out Elasticsearch](https://github.com/deviantony/docker-elk/wiki/Elasticsearch-cluster) + +### How to re-execute the setup + +To run the setup container again and re-initialize all users for which a password was defined inside the `.env` file, +delete its volume and "up" the `setup` Compose service again manually: + +```console +$ docker-compose rm -f setup + ⠿ Container docker-elk-setup-1 Removed +``` + +```console +$ docker volume rm docker-elk_setup +docker-elk_setup +``` + +```console +$ docker-compose up setup + ⠿ Volume "docker-elk_setup" Created + ⠿ Container docker-elk-elasticsearch-1 Running + ⠿ Container docker-elk-setup-1 Created +Attaching to docker-elk-setup-1 +... +docker-elk-setup-1 | [+] User 'monitoring_internal' +docker-elk-setup-1 | ⠿ User does not exist, creating +docker-elk-setup-1 | [+] User 'beats_system' +docker-elk-setup-1 | ⠿ User exists, setting password +docker-elk-setup-1 exited with code 0 +``` + +### How to reset a password programmatically + +If for any reason your are unable to use Kibana to change the password of your users (including [built-in +users][builtin-users]), you can use the Elasticsearch API instead and achieve the same result. + +In the example below, we reset the password of the `elastic` user (notice "/user/elastic" in the URL): + +```sh +curl -XPOST -D- 'http://localhost:9200/_security/user/elastic/_password' \ + -H 'Content-Type: application/json' \ + -u elastic: \ + -d '{"password" : ""}' +``` + +## Extensibility + +### How to add plugins + +To add plugins to any ELK component you have to: + +1. Add a `RUN` statement to the corresponding `Dockerfile` (eg. `RUN logstash-plugin install logstash-filter-json`) +1. Add the associated plugin code configuration to the service configuration (eg. Logstash input/output) +1. Rebuild the images using the `docker-compose build` command + +### How to enable the provided extensions + +A few extensions are available inside the [`extensions`](extensions) directory. These extensions provide features which +are not part of the standard Elastic stack, but can be used to enrich it with extra integrations. + +The documentation for these extensions is provided inside each individual subdirectory, on a per-extension basis. Some +of them require manual changes to the default ELK configuration. + +## JVM tuning + +### How to specify the amount of memory used by a service + +The startup scripts for Elasticsearch and Logstash can append extra JVM options from the value of an environment +variable, allowing the user to adjust the amount of memory that can be used by each component: + +| Service | Environment variable | +|---------------|----------------------| +| Elasticsearch | ES_JAVA_OPTS | +| Logstash | LS_JAVA_OPTS | + +To accomodate environments where memory is scarce (Docker Desktop for Mac has only 2 GB available by default), the Heap +Size allocation is capped by default in the `docker-compose.yml` file to 512 MB for Elasticsearch and 256 MB for +Logstash. If you want to override the default JVM configuration, edit the matching environment variable(s) in the +`docker-compose.yml` file. + +For example, to increase the maximum JVM Heap Size for Logstash: + +```yml +logstash: + + environment: + LS_JAVA_OPTS: -Xms1g -Xmx1g +``` + +When these options are not set: + +* Elasticsearch starts with a JVM Heap Size that is [determined automatically][es-heap]. +* Logstash starts with a fixed JVM Heap Size of 1 GB. + +### How to enable a remote JMX connection to a service + +As for the Java Heap memory (see above), you can specify JVM options to enable JMX and map the JMX port on the Docker +host. + +Update the `{ES,LS}_JAVA_OPTS` environment variable with the following content (I've mapped the JMX service on the port +18080, you can change that). Do not forget to update the `-Djava.rmi.server.hostname` option with the IP address of your +Docker host (replace **DOCKER_HOST_IP**): + +```yml +logstash: + + environment: + LS_JAVA_OPTS: -Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.ssl=false -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.port=18080 -Dcom.sun.management.jmxremote.rmi.port=18080 -Djava.rmi.server.hostname=DOCKER_HOST_IP -Dcom.sun.management.jmxremote.local.only=false +``` + +## Going further + +### Plugins and integrations + +See the following Wiki pages: + +* [External applications](https://github.com/deviantony/docker-elk/wiki/External-applications) +* [Popular integrations](https://github.com/deviantony/docker-elk/wiki/Popular-integrations) + +[elk-stack]: https://www.elastic.co/what-is/elk-stack +[subscriptions]: https://www.elastic.co/subscriptions +[es-security]: https://www.elastic.co/guide/en/elasticsearch/reference/current/security-settings.html +[license-settings]: https://www.elastic.co/guide/en/elasticsearch/reference/current/license-settings.html +[license-mngmt]: https://www.elastic.co/guide/en/kibana/current/managing-licenses.html +[license-apis]: https://www.elastic.co/guide/en/elasticsearch/reference/current/licensing-apis.html + +[elastdocker]: https://github.com/sherifabdlnaby/elastdocker + +[docker-install]: https://docs.docker.com/get-docker/ +[compose-install]: https://docs.docker.com/compose/install/ +[compose-v2]: https://docs.docker.com/compose/compose-v2/ +[linux-postinstall]: https://docs.docker.com/engine/install/linux-postinstall/ + +[bootstrap-checks]: https://www.elastic.co/guide/en/elasticsearch/reference/current/bootstrap-checks.html +[es-sys-config]: https://www.elastic.co/guide/en/elasticsearch/reference/current/system-config.html +[es-heap]: https://www.elastic.co/guide/en/elasticsearch/reference/current/important-settings.html#heap-size-settings + +[win-filesharing]: https://docs.docker.com/desktop/settings/windows/#file-sharing +[mac-filesharing]: https://docs.docker.com/desktop/settings/mac/#file-sharing + +[builtin-users]: https://www.elastic.co/guide/en/elasticsearch/reference/current/built-in-users.html +[ls-monitoring]: https://www.elastic.co/guide/en/logstash/current/monitoring-with-metricbeat.html +[sec-cluster]: https://www.elastic.co/guide/en/elasticsearch/reference/current/secure-cluster.html + +[connect-kibana]: https://www.elastic.co/guide/en/kibana/current/connect-to-elasticsearch.html +[index-pattern]: https://www.elastic.co/guide/en/kibana/current/index-patterns.html + +[config-es]: ./elasticsearch/config/elasticsearch.yml +[config-kbn]: ./kibana/config/kibana.yml +[config-ls]: ./logstash/config/logstash.yml + +[es-docker]: https://www.elastic.co/guide/en/elasticsearch/reference/current/docker.html +[kbn-docker]: https://www.elastic.co/guide/en/kibana/current/docker.html +[ls-docker]: https://www.elastic.co/guide/en/logstash/current/docker-config.html + +[upgrade]: https://www.elastic.co/guide/en/elasticsearch/reference/current/setup-upgrade.html diff --git a/demo/elk-stack/docker-compose.yml b/demo/elk-stack/docker-compose.yml new file mode 100644 index 0000000000..eafecdbb72 --- /dev/null +++ b/demo/elk-stack/docker-compose.yml @@ -0,0 +1,110 @@ +version: '3.7' + +services: + + # The 'setup' service runs a one-off script which initializes users inside + # Elasticsearch — such as 'logstash_internal' and 'kibana_system' — with the + # values of the passwords defined in the '.env' file. + # + # This task is only performed during the *initial* startup of the stack. On all + # subsequent runs, the service simply returns immediately, without performing + # any modification to existing users. + setup: + build: + context: setup/ + args: + ELASTIC_VERSION: ${ELASTIC_VERSION} + init: true + volumes: + - ./setup/entrypoint.sh:/entrypoint.sh:ro,Z + - ./setup/lib.sh:/lib.sh:ro,Z + - ./setup/roles:/roles:ro,Z + - setup:/state:Z + environment: + ELASTIC_PASSWORD: ${ELASTIC_PASSWORD:-} + LOGSTASH_INTERNAL_PASSWORD: ${LOGSTASH_INTERNAL_PASSWORD:-} + KIBANA_SYSTEM_PASSWORD: ${KIBANA_SYSTEM_PASSWORD:-} + METRICBEAT_INTERNAL_PASSWORD: ${METRICBEAT_INTERNAL_PASSWORD:-} + FILEBEAT_INTERNAL_PASSWORD: ${FILEBEAT_INTERNAL_PASSWORD:-} + HEARTBEAT_INTERNAL_PASSWORD: ${HEARTBEAT_INTERNAL_PASSWORD:-} + MONITORING_INTERNAL_PASSWORD: ${MONITORING_INTERNAL_PASSWORD:-} + BEATS_SYSTEM_PASSWORD: ${BEATS_SYSTEM_PASSWORD:-} + networks: + - elknet + depends_on: + - elasticsearch + + elasticsearch: + build: + context: elasticsearch/ + args: + ELASTIC_VERSION: ${ELASTIC_VERSION} + volumes: + - ./elasticsearch/config/elasticsearch.yml:/usr/share/elasticsearch/config/elasticsearch.yml:ro,Z + - elasticsearch:/usr/share/elasticsearch/data:Z + ports: + - 9200:9200 + - 9300:9300 + environment: + node.name: elasticsearch + ES_JAVA_OPTS: -Xms512m -Xmx512m + # Bootstrap password. + # Used to initialize the keystore during the initial startup of + # Elasticsearch. Ignored on subsequent runs. + ELASTIC_PASSWORD: ${ELASTIC_PASSWORD:-} + # Use single node discovery in order to disable production mode and avoid bootstrap checks. + # see: https://www.elastic.co/guide/en/elasticsearch/reference/current/bootstrap-checks.html + discovery.type: single-node + networks: + - elknet + restart: unless-stopped + + logstash: + build: + context: logstash/ + args: + ELASTIC_VERSION: ${ELASTIC_VERSION} + volumes: + - ./logstash/config/logstash.yml:/usr/share/logstash/config/logstash.yml:ro,Z + - ./logstash/pipeline:/usr/share/logstash/pipeline:ro,Z + ports: + - 5044:5044 + - 50000:50000/tcp + - 50000:50000/udp + - 9600:9600 + - 9700:9700 + environment: + LS_JAVA_OPTS: -Xms256m -Xmx256m + LOGSTASH_INTERNAL_PASSWORD: ${LOGSTASH_INTERNAL_PASSWORD:-} + networks: + - elknet + depends_on: + - elasticsearch + restart: unless-stopped + + kibana: + build: + context: kibana/ + args: + ELASTIC_VERSION: ${ELASTIC_VERSION} + volumes: + - ./kibana/config/kibana.yml:/usr/share/kibana/config/kibana.yml:ro,Z + ports: + - 5601:5601 + environment: + KIBANA_SYSTEM_PASSWORD: ${KIBANA_SYSTEM_PASSWORD:-} + networks: + - elknet + depends_on: + - elasticsearch + restart: unless-stopped + +networks: + elknet: + driver: bridge + attachable: true + name: ${ELK_NETWORK_NAME} + +volumes: + setup: + elasticsearch: diff --git a/demo/elk-stack/elasticsearch/.dockerignore b/demo/elk-stack/elasticsearch/.dockerignore new file mode 100644 index 0000000000..37eef9d513 --- /dev/null +++ b/demo/elk-stack/elasticsearch/.dockerignore @@ -0,0 +1,6 @@ +# Ignore Docker build files +Dockerfile +.dockerignore + +# Ignore OS artifacts +**/.DS_Store diff --git a/demo/elk-stack/elasticsearch/Dockerfile b/demo/elk-stack/elasticsearch/Dockerfile new file mode 100644 index 0000000000..22528c6d7b --- /dev/null +++ b/demo/elk-stack/elasticsearch/Dockerfile @@ -0,0 +1,7 @@ +ARG ELASTIC_VERSION + +# https://www.docker.elastic.co/ +FROM docker.elastic.co/elasticsearch/elasticsearch:${ELASTIC_VERSION} + +# Add your elasticsearch plugins setup here +# Example: RUN elasticsearch-plugin install analysis-icu diff --git a/demo/elk-stack/elasticsearch/config/elasticsearch.yml b/demo/elk-stack/elasticsearch/config/elasticsearch.yml new file mode 100644 index 0000000000..d66f071aad --- /dev/null +++ b/demo/elk-stack/elasticsearch/config/elasticsearch.yml @@ -0,0 +1,12 @@ +--- +## Default Elasticsearch configuration from Elasticsearch base image. +## https://github.com/elastic/elasticsearch/blob/main/distribution/docker/src/docker/config/elasticsearch.yml +# +cluster.name: docker-cluster +network.host: 0.0.0.0 + +## X-Pack settings +## see https://www.elastic.co/guide/en/elasticsearch/reference/current/security-settings.html +# +xpack.license.self_generated.type: trial +xpack.security.enabled: true diff --git a/demo/elk-stack/extensions/README.md b/demo/elk-stack/extensions/README.md new file mode 100644 index 0000000000..50016fb6ce --- /dev/null +++ b/demo/elk-stack/extensions/README.md @@ -0,0 +1,3 @@ +# Extensions + +Third-party extensions that enable extra integrations with the Elastic stack. diff --git a/demo/elk-stack/extensions/curator/.dockerignore b/demo/elk-stack/extensions/curator/.dockerignore new file mode 100644 index 0000000000..37eef9d513 --- /dev/null +++ b/demo/elk-stack/extensions/curator/.dockerignore @@ -0,0 +1,6 @@ +# Ignore Docker build files +Dockerfile +.dockerignore + +# Ignore OS artifacts +**/.DS_Store diff --git a/demo/elk-stack/extensions/curator/Dockerfile b/demo/elk-stack/extensions/curator/Dockerfile new file mode 100644 index 0000000000..6cb8cdc681 --- /dev/null +++ b/demo/elk-stack/extensions/curator/Dockerfile @@ -0,0 +1,9 @@ +FROM untergeek/curator:8.0.2 + +USER root + +RUN >>/var/spool/cron/crontabs/nobody \ + echo '* * * * * /curator/curator /.curator/delete_log_files_curator.yml' + +ENTRYPOINT ["crond"] +CMD ["-f", "-d8"] diff --git a/demo/elk-stack/extensions/curator/README.md b/demo/elk-stack/extensions/curator/README.md new file mode 100644 index 0000000000..5c38786aac --- /dev/null +++ b/demo/elk-stack/extensions/curator/README.md @@ -0,0 +1,20 @@ +# Curator + +Elasticsearch Curator helps you curate or manage your indices. + +## Usage + +If you want to include the Curator extension, run Docker Compose from the root of the repository with an additional +command line argument referencing the `curator-compose.yml` file: + +```bash +$ docker-compose -f docker-compose.yml -f extensions/curator/curator-compose.yml up +``` + +This sample setup demonstrates how to run `curator` every minute using `cron`. + +All configuration files are available in the `config/` directory. + +## Documentation + +[Curator Reference](https://www.elastic.co/guide/en/elasticsearch/client/curator/current/index.html) diff --git a/demo/elk-stack/extensions/curator/config/curator.yml b/demo/elk-stack/extensions/curator/config/curator.yml new file mode 100644 index 0000000000..6777edc9cb --- /dev/null +++ b/demo/elk-stack/extensions/curator/config/curator.yml @@ -0,0 +1,13 @@ +# Curator configuration +# https://www.elastic.co/guide/en/elasticsearch/client/curator/current/configfile.html + +elasticsearch: + client: + hosts: [ http://elasticsearch:9200 ] + other_settings: + username: elastic + password: ${ELASTIC_PASSWORD} + +logging: + loglevel: INFO + logformat: default diff --git a/demo/elk-stack/extensions/curator/config/delete_log_files_curator.yml b/demo/elk-stack/extensions/curator/config/delete_log_files_curator.yml new file mode 100644 index 0000000000..779c67ac0d --- /dev/null +++ b/demo/elk-stack/extensions/curator/config/delete_log_files_curator.yml @@ -0,0 +1,21 @@ +actions: + 1: + action: delete_indices + description: >- + Delete indices. Find which to delete by first limiting the list to + logstash- prefixed indices. Then further filter those to prevent deletion + of anything less than the number of days specified by unit_count. + Ignore the error if the filter does not result in an actionable list of + indices (ignore_empty_list) and exit cleanly. + options: + ignore_empty_list: True + disable_action: False + filters: + - filtertype: pattern + kind: prefix + value: logstash- + - filtertype: age + source: creation_date + direction: older + unit: days + unit_count: 2 diff --git a/demo/elk-stack/extensions/curator/curator-compose.yml b/demo/elk-stack/extensions/curator/curator-compose.yml new file mode 100644 index 0000000000..1a4bb17e25 --- /dev/null +++ b/demo/elk-stack/extensions/curator/curator-compose.yml @@ -0,0 +1,16 @@ +version: '3.7' + +services: + curator: + build: + context: extensions/curator/ + init: true + volumes: + - ./extensions/curator/config/curator.yml:/.curator/curator.yml:ro,Z + - ./extensions/curator/config/delete_log_files_curator.yml:/.curator/delete_log_files_curator.yml:ro,Z + environment: + ELASTIC_PASSWORD: ${ELASTIC_PASSWORD:-} + networks: + - elk + depends_on: + - elasticsearch diff --git a/demo/elk-stack/extensions/enterprise-search/.dockerignore b/demo/elk-stack/extensions/enterprise-search/.dockerignore new file mode 100644 index 0000000000..37eef9d513 --- /dev/null +++ b/demo/elk-stack/extensions/enterprise-search/.dockerignore @@ -0,0 +1,6 @@ +# Ignore Docker build files +Dockerfile +.dockerignore + +# Ignore OS artifacts +**/.DS_Store diff --git a/demo/elk-stack/extensions/enterprise-search/Dockerfile b/demo/elk-stack/extensions/enterprise-search/Dockerfile new file mode 100644 index 0000000000..4f0752e55a --- /dev/null +++ b/demo/elk-stack/extensions/enterprise-search/Dockerfile @@ -0,0 +1,4 @@ +ARG ELASTIC_VERSION + +# https://www.docker.elastic.co/ +FROM docker.elastic.co/enterprise-search/enterprise-search:${ELASTIC_VERSION} diff --git a/demo/elk-stack/extensions/enterprise-search/README.md b/demo/elk-stack/extensions/enterprise-search/README.md new file mode 100644 index 0000000000..d6391dba67 --- /dev/null +++ b/demo/elk-stack/extensions/enterprise-search/README.md @@ -0,0 +1,144 @@ +# Enterprise Search extension + +Elastic Enterprise Search is a suite of products for search applications backed by the Elastic Stack. + +## Requirements + +* 2 GB of free RAM, on top of the resources required by the other stack components and extensions. + +The Enterprise Search web application is served on the TCP port `3002`. + +## Usage + +### Generate an encryption key + +Enterprise Search requires one or more [encryption keys][enterprisesearch-encryption] to be configured before the +initial startup. Failing to do so prevents the server from starting. + +Encryption keys can contain any series of characters. Elastic recommends using 256-bit keys for optimal security. + +Those encryption keys must be added manually to the [`config/enterprise-search.yml`][config-enterprisesearch] file. By +default, the list of encryption keys is empty and must be populated using one of the following formats: + +```yaml +secret_management.encryption_keys: + - my_first_encryption_key + - my_second_encryption_key + - ... +``` + +```yaml +secret_management.encryption_keys: [my_first_encryption_key, my_second_encryption_key, ...] +``` + +> **Note** +> To generate a strong random encryption key, you can use the OpenSSL utility or any other online/offline tool of your +> choice: +> +> ```console +> $ openssl rand -hex 32 +> 680f94e568c90364bedf927b2f0f49609702d3eab9098688585a375b14274546 +> ``` + +### Enable Elasticsearch's API key service + +Enterprise Search requires Elasticsearch's built-in [API key service][es-security] to be enabled in order to start. +Unless Elasticsearch is configured to enable TLS on the HTTP interface (disabled by default), this service is disabled +by default. + +To enable it, modify the Elasticsearch configuration file in [`elasticsearch/config/elasticsearch.yml`][config-es] and +add the following setting: + +```yaml +xpack.security.authc.api_key.enabled: true +``` + +### Configure the Enterprise Search host in Kibana + +Kibana acts as the [management interface][enterprisesearch-kb] to Enterprise Search. + +To enable the management experience for Enterprise Search, modify the Kibana configuration file in +[`kibana/config/kibana.yml`][config-kbn] and add the following setting: + +```yaml +enterpriseSearch.host: http://enterprise-search:3002 +``` + +### Start the server + +To include Enterprise Search in the stack, run Docker Compose from the root of the repository with an additional command +line argument referencing the `enterprise-search-compose.yml` file: + +```console +$ docker-compose -f docker-compose.yml -f extensions/enterprise-search/enterprise-search-compose.yml up +``` + +Allow a few minutes for the stack to start, then open your web browser at the address to see the +Enterprise Search home page. + +Enterprise Search is configured on first boot with the following default credentials: + +* user: *enterprise_search* +* password: *changeme* + +## Security + +The Enterprise Search password is defined inside the Compose file via the `ENT_SEARCH_DEFAULT_PASSWORD` environment +variable. We highly recommend choosing a more secure password than the default one for security reasons. + +To do so, change the value `ENT_SEARCH_DEFAULT_PASSWORD` environment variable inside the Compose file **before the first +boot**: + +```yaml +enterprise-search: + + environment: + ENT_SEARCH_DEFAULT_PASSWORD: {{some strong password}} +``` + +> **Warning** +> The default Enterprise Search password can only be set during the initial boot. Once the password is persisted in +> Elasticsearch, it can only be changed via the Elasticsearch API. + +For more information, please refer to [User Management and Security][enterprisesearch-security]. + +## Configuring Enterprise Search + +The Enterprise Search configuration is stored in [`config/enterprise-search.yml`][config-enterprisesearch]. You can +modify this file using the [Default Enterprise Search configuration][enterprisesearch-config] as a reference. + +You can also specify the options you want to override by setting environment variables inside the Compose file: + +```yaml +enterprise-search: + + environment: + ent_search.auth.source: standard + worker.threads: '6' +``` + +Any change to the Enterprise Search configuration requires a restart of the Enterprise Search container: + +```console +$ docker-compose -f docker-compose.yml -f extensions/enterprise-search/enterprise-search-compose.yml restart enterprise-search +``` + +Please refer to the following documentation page for more details about how to configure Enterprise Search inside a +Docker container: [Running Enterprise Search Using Docker][enterprisesearch-docker]. + +## See also + +[Enterprise Search documentation][enterprisesearch-docs] + +[config-enterprisesearch]: ./config/enterprise-search.yml + +[enterprisesearch-encryption]: https://www.elastic.co/guide/en/enterprise-search/current/encryption-keys.html +[enterprisesearch-security]: https://www.elastic.co/guide/en/workplace-search/current/workplace-search-security.html +[enterprisesearch-config]: https://www.elastic.co/guide/en/enterprise-search/current/configuration.html +[enterprisesearch-docker]: https://www.elastic.co/guide/en/enterprise-search/current/docker.html +[enterprisesearch-docs]: https://www.elastic.co/guide/en/enterprise-search/current/index.html +[enterprisesearch-kb]: https://www.elastic.co/guide/en/kibana/current/enterprise-search-settings-kb.html + +[es-security]: https://www.elastic.co/guide/en/elasticsearch/reference/current/security-settings.html#api-key-service-settings +[config-es]: ../../elasticsearch/config/elasticsearch.yml +[config-kbn]: ../../kibana/config/kibana.yml diff --git a/demo/elk-stack/extensions/enterprise-search/config/enterprise-search.yml b/demo/elk-stack/extensions/enterprise-search/config/enterprise-search.yml new file mode 100644 index 0000000000..a1f098dd2e --- /dev/null +++ b/demo/elk-stack/extensions/enterprise-search/config/enterprise-search.yml @@ -0,0 +1,28 @@ +--- +## Enterprise Search core configuration +## https://www.elastic.co/guide/en/enterprise-search/current/configuration.html +# + +## --------------------- REQUIRED --------------------- + +# Encryption keys to protect application secrets. +secret_management.encryption_keys: + # example: + #- 680f94e568c90364bedf927b2f0f49609702d3eab9098688585a375b14274546 + +## ---------------------------------------------------- + +# IP address Enterprise Search listens on +ent_search.listen_host: 0.0.0.0 + +# URL at which users reach Enterprise Search / Kibana +ent_search.external_url: http://localhost:3002 +kibana.host: http://localhost:5601 + +# Elasticsearch URL and credentials +elasticsearch.host: http://elasticsearch:9200 +elasticsearch.username: elastic +elasticsearch.password: ${ELASTIC_PASSWORD} + +# Allow Enterprise Search to modify Elasticsearch settings. Used to enable auto-creation of Elasticsearch indexes. +allow_es_settings_modification: true diff --git a/demo/elk-stack/extensions/enterprise-search/enterprise-search-compose.yml b/demo/elk-stack/extensions/enterprise-search/enterprise-search-compose.yml new file mode 100644 index 0000000000..585dda9380 --- /dev/null +++ b/demo/elk-stack/extensions/enterprise-search/enterprise-search-compose.yml @@ -0,0 +1,20 @@ +version: '3.7' + +services: + enterprise-search: + build: + context: extensions/enterprise-search/ + args: + ELASTIC_VERSION: ${ELASTIC_VERSION} + volumes: + - ./extensions/enterprise-search/config/enterprise-search.yml:/usr/share/enterprise-search/config/enterprise-search.yml:ro,Z + environment: + JAVA_OPTS: -Xms2g -Xmx2g + ENT_SEARCH_DEFAULT_PASSWORD: 'changeme' + ELASTIC_PASSWORD: ${ELASTIC_PASSWORD:-} + ports: + - 3002:3002 + networks: + - elk + depends_on: + - elasticsearch diff --git a/demo/elk-stack/extensions/filebeat/.dockerignore b/demo/elk-stack/extensions/filebeat/.dockerignore new file mode 100644 index 0000000000..37eef9d513 --- /dev/null +++ b/demo/elk-stack/extensions/filebeat/.dockerignore @@ -0,0 +1,6 @@ +# Ignore Docker build files +Dockerfile +.dockerignore + +# Ignore OS artifacts +**/.DS_Store diff --git a/demo/elk-stack/extensions/filebeat/Dockerfile b/demo/elk-stack/extensions/filebeat/Dockerfile new file mode 100644 index 0000000000..b8dd5f3f5a --- /dev/null +++ b/demo/elk-stack/extensions/filebeat/Dockerfile @@ -0,0 +1,3 @@ +ARG ELASTIC_VERSION + +FROM docker.elastic.co/beats/filebeat:${ELASTIC_VERSION} diff --git a/demo/elk-stack/extensions/filebeat/README.md b/demo/elk-stack/extensions/filebeat/README.md new file mode 100644 index 0000000000..f2bfd20608 --- /dev/null +++ b/demo/elk-stack/extensions/filebeat/README.md @@ -0,0 +1,42 @@ +# Filebeat + +Filebeat is a lightweight shipper for forwarding and centralizing log data. Installed as an agent on your servers, +Filebeat monitors the log files or locations that you specify, collects log events, and forwards them either to +Elasticsearch or Logstash for indexing. + +## Usage + +**This extension requires the `filebeat_internal` and `beats_system` users to be created and initialized with a +password.** In case you haven't done that during the initial startup of the stack, please refer to [How to re-execute +the setup][setup] to run the setup container again and initialize these users. + +To include Filebeat in the stack, run Docker Compose from the root of the repository with an additional command line +argument referencing the `filebeat-compose.yml` file: + +```console +$ docker-compose -f docker-compose.yml -f extensions/filebeat/filebeat-compose.yml up +``` + +## Configuring Filebeat + +The Filebeat configuration is stored in [`config/filebeat.yml`](./config/filebeat.yml). You can modify this file with +the help of the [Configuration reference][filebeat-config]. + +Any change to the Filebeat configuration requires a restart of the Filebeat container: + +```console +$ docker-compose -f docker-compose.yml -f extensions/filebeat/filebeat-compose.yml restart filebeat +``` + +Please refer to the following documentation page for more details about how to configure Filebeat inside a Docker +container: [Run Filebeat on Docker][filebeat-docker]. + +## See also + +[Filebeat documentation][filebeat-doc] + +[filebeat-config]: https://www.elastic.co/guide/en/beats/filebeat/current/filebeat-reference-yml.html +[filebeat-docker]: https://www.elastic.co/guide/en/beats/filebeat/current/running-on-docker.html +[filebeat-doc]: https://www.elastic.co/guide/en/beats/filebeat/current/index.html + +[setup]: ../../README.md#how-to-re-execute-the-setup diff --git a/demo/elk-stack/extensions/filebeat/config/filebeat.yml b/demo/elk-stack/extensions/filebeat/config/filebeat.yml new file mode 100644 index 0000000000..da8e2ea39f --- /dev/null +++ b/demo/elk-stack/extensions/filebeat/config/filebeat.yml @@ -0,0 +1,39 @@ +## Filebeat configuration +## https://github.com/elastic/beats/blob/main/deploy/docker/filebeat.docker.yml +# + +name: filebeat + +filebeat.config: + modules: + path: ${path.config}/modules.d/*.yml + reload.enabled: false + +filebeat.autodiscover: + providers: + # The Docker autodiscover provider automatically retrieves logs from Docker + # containers as they start and stop. + - type: docker + hints.enabled: true + +processors: + - add_cloud_metadata: ~ + +monitoring: + enabled: true + elasticsearch: + username: beats_system + password: ${BEATS_SYSTEM_PASSWORD} + +output.elasticsearch: + hosts: [ http://elasticsearch:9200 ] + username: filebeat_internal + password: ${FILEBEAT_INTERNAL_PASSWORD} + +## HTTP endpoint for health checking +## https://www.elastic.co/guide/en/beats/filebeat/current/http-endpoint.html +# + +http: + enabled: true + host: 0.0.0.0 diff --git a/demo/elk-stack/extensions/filebeat/filebeat-compose.yml b/demo/elk-stack/extensions/filebeat/filebeat-compose.yml new file mode 100644 index 0000000000..5c5960efe9 --- /dev/null +++ b/demo/elk-stack/extensions/filebeat/filebeat-compose.yml @@ -0,0 +1,35 @@ +version: '3.7' + +services: + filebeat: + build: + context: extensions/filebeat/ + args: + ELASTIC_VERSION: ${ELASTIC_VERSION} + # Run as 'root' instead of 'filebeat' (uid 1000) to allow reading + # 'docker.sock' and the host's filesystem. + user: root + command: + # Log to stderr. + - -e + # Disable config file permissions checks. Allows mounting + # 'config/filebeat.yml' even if it's not owned by root. + # see: https://www.elastic.co/guide/en/beats/libbeat/current/config-file-permissions.html + - --strict.perms=false + volumes: + - ./extensions/filebeat/config/filebeat.yml:/usr/share/filebeat/filebeat.yml:ro,Z + - type: bind + source: /var/lib/docker/containers + target: /var/lib/docker/containers + read_only: true + - type: bind + source: /var/run/docker.sock + target: /var/run/docker.sock + read_only: true + environment: + FILEBEAT_INTERNAL_PASSWORD: ${FILEBEAT_INTERNAL_PASSWORD:-} + BEATS_SYSTEM_PASSWORD: ${BEATS_SYSTEM_PASSWORD:-} + networks: + - elk + depends_on: + - elasticsearch diff --git a/demo/elk-stack/extensions/fleet/.dockerignore b/demo/elk-stack/extensions/fleet/.dockerignore new file mode 100644 index 0000000000..37eef9d513 --- /dev/null +++ b/demo/elk-stack/extensions/fleet/.dockerignore @@ -0,0 +1,6 @@ +# Ignore Docker build files +Dockerfile +.dockerignore + +# Ignore OS artifacts +**/.DS_Store diff --git a/demo/elk-stack/extensions/fleet/Dockerfile b/demo/elk-stack/extensions/fleet/Dockerfile new file mode 100644 index 0000000000..0b5a691dd0 --- /dev/null +++ b/demo/elk-stack/extensions/fleet/Dockerfile @@ -0,0 +1,8 @@ +ARG ELASTIC_VERSION + +FROM docker.elastic.co/beats/elastic-agent:${ELASTIC_VERSION} + +# Ensure the 'state' directory exists and is owned by the 'elastic-agent' user, +# otherwise mounting a named volume in that location creates a directory owned +# by root:root which the 'elastic-agent' user isn't allowed to write to. +RUN mkdir state diff --git a/demo/elk-stack/extensions/fleet/README.md b/demo/elk-stack/extensions/fleet/README.md new file mode 100644 index 0000000000..de800857ad --- /dev/null +++ b/demo/elk-stack/extensions/fleet/README.md @@ -0,0 +1,69 @@ +# Fleet Server + +> **Warning** +> This extension currently exists for preview purposes and should be considered **EXPERIMENTAL**. Expect regular changes +> to the default Fleet settings, both in the Elastic Agent and Kibana. +> +> See [Known Issues](#known-issues) for a list of issues that need to be addressed before this extension can be +> considered functional. + +Fleet provides central management capabilities for [Elastic Agents][fleet-doc] via an API and web UI served by Kibana, +with Elasticsearch acting as the communication layer. +Fleet Server is the central component which allows connecting Elastic Agents to the Fleet. + +## Requirements + +The Fleet Server exposes the TCP port `8220` for Agent to Server communications. + +## Usage + +To include Fleet Server in the stack, run Docker Compose from the root of the repository with an additional command line +argument referencing the `fleet-compose.yml` file: + +```console +$ docker-compose -f docker-compose.yml -f extensions/fleet/fleet-compose.yml up +``` + +## Configuring Fleet Server + +Fleet Server — like any Elastic Agent — is configured via [Agent Policies][fleet-pol] which can be either managed +through the Fleet management UI in Kibana, or statically pre-configured inside the Kibana configuration file. + +To ease the enrollment of Fleet Server in this extension, docker-elk comes with a pre-configured Agent Policy for Fleet +Server defined inside [`kibana/config/kibana.yml`][config-kbn]. + +Please refer to the following documentation page for more details about configuring Fleet Server through the Fleet +management UI: [Fleet UI Settings][fleet-cfg]. + +## Known Issues + +- Logs and metrics are only collected within the Fleet Server's container. Ultimately, we want to emulate the behaviour + of the existing Metricsbeat and Filebeat extensions, and collect logs and metrics from all ELK containers + out-of-the-box. Unfortunately, this kind of use-case isn't (yet) well supported by Fleet, and most advanced + configurations currently require running Elastic Agents in [standalone mode][fleet-standalone]. + (Relevant resource: [Migrate from Beats to Elastic Agent][fleet-beats]) +- The Elastic Agent auto-enrolls using the `elastic` super-user. With this approach, you do not need to generate a + service token — either using the Fleet management UI or [CLI utility][es-svc-token] — prior to starting this + extension. However convenient that is, this approach _does not follow security best practices_, and we recommend + generating a service token for Fleet Server instead. + +## See also + +[Fleet and Elastic Agent Guide][fleet-doc] + +## Screenshots + +![fleet-agents](https://user-images.githubusercontent.com/3299086/202701399-27518fe4-17b7-49d1-aefb-868dffeaa68a.png +"Fleet Agents") +![elastic-agent-dashboard](https://user-images.githubusercontent.com/3299086/202701404-958f8d80-a7a0-4044-bbf9-bf73f3bdd17a.png +"Elastic Agent Dashboard") + +[fleet-doc]: https://www.elastic.co/guide/en/fleet/current/fleet-overview.html +[fleet-pol]: https://www.elastic.co/guide/en/fleet/current/agent-policy.html +[fleet-cfg]: https://www.elastic.co/guide/en/fleet/current/fleet-settings.html + +[config-kbn]: ../../kibana/config/kibana.yml + +[fleet-standalone]: https://www.elastic.co/guide/en/fleet/current/elastic-agent-configuration.html +[fleet-beats]: https://www.elastic.co/guide/en/fleet/current/migrate-beats-to-agent.html +[es-svc-token]: https://www.elastic.co/guide/en/elasticsearch/reference/current/service-tokens-command.html diff --git a/demo/elk-stack/extensions/fleet/agent-apmserver-compose.yml b/demo/elk-stack/extensions/fleet/agent-apmserver-compose.yml new file mode 100644 index 0000000000..06e201a9ee --- /dev/null +++ b/demo/elk-stack/extensions/fleet/agent-apmserver-compose.yml @@ -0,0 +1,45 @@ +version: '3.7' + +# Example of Fleet-enrolled Elastic Agent pre-configured with an agent policy +# for running the APM Server integration (see kibana.yml). +# +# Run with +# docker-compose \ +# -f docker-compose.yml \ +# -f extensions/fleet/fleet-compose.yml \ +# -f extensions/fleet/agent-apmserver-compose.yml \ +# up + +services: + apm-server: + build: + context: extensions/fleet/ + args: + ELASTIC_VERSION: ${ELASTIC_VERSION} + volumes: + - apm-server:/usr/share/elastic-agent/state:Z + environment: + FLEET_ENROLL: '1' + FLEET_TOKEN_POLICY_NAME: Agent Policy APM Server + FLEET_INSECURE: '1' + FLEET_URL: http://fleet-server:8220 + # Enrollment. + # (a) Auto-enroll using basic authentication + ELASTICSEARCH_USERNAME: elastic + ELASTICSEARCH_PASSWORD: ${ELASTIC_PASSWORD:-} + # (b) Enroll using a pre-generated enrollment token + #FLEET_ENROLLMENT_TOKEN: + ports: + - 8200:8200 + hostname: apm-server + # Elastic Agent does not retry failed connections to Kibana upon the initial enrollment phase. + restart: on-failure + networks: + - elk + depends_on: + - elasticsearch + - kibana + - fleet-server + +volumes: + apm-server: diff --git a/demo/elk-stack/extensions/fleet/fleet-compose.yml b/demo/elk-stack/extensions/fleet/fleet-compose.yml new file mode 100644 index 0000000000..e33f47b0e6 --- /dev/null +++ b/demo/elk-stack/extensions/fleet/fleet-compose.yml @@ -0,0 +1,36 @@ +version: '3.7' + +services: + fleet-server: + build: + context: extensions/fleet/ + args: + ELASTIC_VERSION: ${ELASTIC_VERSION} + volumes: + - fleet-server:/usr/share/elastic-agent/state:Z + environment: + FLEET_SERVER_ENABLE: '1' + FLEET_SERVER_INSECURE_HTTP: '1' + FLEET_SERVER_HOST: 0.0.0.0 + FLEET_SERVER_POLICY_ID: fleet-server-policy + # Fleet plugin in Kibana + KIBANA_FLEET_SETUP: '1' + # Enrollment. + # (a) Auto-enroll using basic authentication + ELASTICSEARCH_USERNAME: elastic + ELASTICSEARCH_PASSWORD: ${ELASTIC_PASSWORD:-} + # (b) Enroll using a pre-generated service token + #FLEET_SERVER_SERVICE_TOKEN: + ports: + - 8220:8220 + hostname: fleet-server + # Elastic Agent does not retry failed connections to Kibana upon the initial enrollment phase. + restart: on-failure + networks: + - elk + depends_on: + - elasticsearch + - kibana + +volumes: + fleet-server: diff --git a/demo/elk-stack/extensions/heartbeat/.dockerignore b/demo/elk-stack/extensions/heartbeat/.dockerignore new file mode 100644 index 0000000000..37eef9d513 --- /dev/null +++ b/demo/elk-stack/extensions/heartbeat/.dockerignore @@ -0,0 +1,6 @@ +# Ignore Docker build files +Dockerfile +.dockerignore + +# Ignore OS artifacts +**/.DS_Store diff --git a/demo/elk-stack/extensions/heartbeat/Dockerfile b/demo/elk-stack/extensions/heartbeat/Dockerfile new file mode 100644 index 0000000000..0d7de1964a --- /dev/null +++ b/demo/elk-stack/extensions/heartbeat/Dockerfile @@ -0,0 +1,3 @@ +ARG ELASTIC_VERSION + +FROM docker.elastic.co/beats/heartbeat:${ELASTIC_VERSION} diff --git a/demo/elk-stack/extensions/heartbeat/README.md b/demo/elk-stack/extensions/heartbeat/README.md new file mode 100644 index 0000000000..82c938f512 --- /dev/null +++ b/demo/elk-stack/extensions/heartbeat/README.md @@ -0,0 +1,41 @@ +# Heartbeat + +Heartbeat is a lightweight daemon that periodically checks the status of your services and determines whether they are +available. + +## Usage + +**This extension requires the `heartbeat_internal` and `beats_system` users to be created and initialized with a +password.** In case you haven't done that during the initial startup of the stack, please refer to [How to re-execute +the setup][setup] to run the setup container again and initialize these users. + +To include Heartbeat in the stack, run Docker Compose from the root of the repository with an additional command line +argument referencing the `heartbeat-compose.yml` file: + +```console +$ docker-compose -f docker-compose.yml -f extensions/heartbeat/heartbeat-compose.yml up +``` + +## Configuring Heartbeat + +The Heartbeat configuration is stored in [`config/heartbeat.yml`](./config/heartbeat.yml). You can modify this file +with the help of the [Configuration reference][heartbeat-config]. + +Any change to the Heartbeat configuration requires a restart of the Heartbeat container: + +```console +$ docker-compose -f docker-compose.yml -f extensions/heartbeat/heartbeat-compose.yml restart heartbeat +``` + +Please refer to the following documentation page for more details about how to configure Heartbeat inside a +Docker container: [Run Heartbeat on Docker][heartbeat-docker]. + +## See also + +[Heartbeat documentation][heartbeat-doc] + +[heartbeat-config]: https://www.elastic.co/guide/en/beats/heartbeat/current/heartbeat-reference-yml.html +[heartbeat-docker]: https://www.elastic.co/guide/en/beats/heartbeat/current/running-on-docker.html +[heartbeat-doc]: https://www.elastic.co/guide/en/beats/heartbeat/current/index.html + +[setup]: ../../README.md#how-to-re-execute-the-setup diff --git a/demo/elk-stack/extensions/heartbeat/config/heartbeat.yml b/demo/elk-stack/extensions/heartbeat/config/heartbeat.yml new file mode 100644 index 0000000000..b1416ea4a9 --- /dev/null +++ b/demo/elk-stack/extensions/heartbeat/config/heartbeat.yml @@ -0,0 +1,40 @@ +## Heartbeat configuration +## https://github.com/elastic/beats/blob/main/deploy/docker/heartbeat.docker.yml +# + +name: heartbeat + +heartbeat.monitors: +- type: http + schedule: '@every 5s' + urls: + - http://elasticsearch:9200 + username: heartbeat_internal + password: ${HEARTBEAT_INTERNAL_PASSWORD} + +- type: icmp + schedule: '@every 5s' + hosts: + - elasticsearch + +processors: +- add_cloud_metadata: ~ + +monitoring: + enabled: true + elasticsearch: + username: beats_system + password: ${BEATS_SYSTEM_PASSWORD} + +output.elasticsearch: + hosts: [ http://elasticsearch:9200 ] + username: heartbeat_internal + password: ${HEARTBEAT_INTERNAL_PASSWORD} + +## HTTP endpoint for health checking +## https://www.elastic.co/guide/en/beats/heartbeat/current/http-endpoint.html +# + +http: + enabled: true + host: 0.0.0.0 diff --git a/demo/elk-stack/extensions/heartbeat/heartbeat-compose.yml b/demo/elk-stack/extensions/heartbeat/heartbeat-compose.yml new file mode 100644 index 0000000000..47e07084ed --- /dev/null +++ b/demo/elk-stack/extensions/heartbeat/heartbeat-compose.yml @@ -0,0 +1,24 @@ +version: '3.7' + +services: + heartbeat: + build: + context: extensions/heartbeat/ + args: + ELASTIC_VERSION: ${ELASTIC_VERSION} + command: + # Log to stderr. + - -e + # Disable config file permissions checks. Allows mounting + # 'config/heartbeat.yml' even if it's not owned by root. + # see: https://www.elastic.co/guide/en/beats/libbeat/current/config-file-permissions.html + - --strict.perms=false + volumes: + - ./extensions/heartbeat/config/heartbeat.yml:/usr/share/heartbeat/heartbeat.yml:ro,Z + environment: + HEARTBEAT_INTERNAL_PASSWORD: ${HEARTBEAT_INTERNAL_PASSWORD:-} + BEATS_SYSTEM_PASSWORD: ${BEATS_SYSTEM_PASSWORD:-} + networks: + - elk + depends_on: + - elasticsearch diff --git a/demo/elk-stack/extensions/logspout/.dockerignore b/demo/elk-stack/extensions/logspout/.dockerignore new file mode 100644 index 0000000000..37eef9d513 --- /dev/null +++ b/demo/elk-stack/extensions/logspout/.dockerignore @@ -0,0 +1,6 @@ +# Ignore Docker build files +Dockerfile +.dockerignore + +# Ignore OS artifacts +**/.DS_Store diff --git a/demo/elk-stack/extensions/logspout/Dockerfile b/demo/elk-stack/extensions/logspout/Dockerfile new file mode 100644 index 0000000000..9591df53b0 --- /dev/null +++ b/demo/elk-stack/extensions/logspout/Dockerfile @@ -0,0 +1,5 @@ +# uses ONBUILD instructions described here: +# https://github.com/gliderlabs/logspout/tree/master/custom + +FROM gliderlabs/logspout:master +ENV SYSLOG_FORMAT rfc3164 diff --git a/demo/elk-stack/extensions/logspout/README.md b/demo/elk-stack/extensions/logspout/README.md new file mode 100644 index 0000000000..2e34648568 --- /dev/null +++ b/demo/elk-stack/extensions/logspout/README.md @@ -0,0 +1,28 @@ +# Logspout extension + +Logspout collects all Docker logs using the Docker logs API, and forwards them to Logstash without any additional +configuration. + +## Usage + +If you want to include the Logspout extension, run Docker Compose from the root of the repository with an additional +command line argument referencing the `logspout-compose.yml` file: + +```bash +$ docker-compose -f docker-compose.yml -f extensions/logspout/logspout-compose.yml up +``` + +In your Logstash pipeline configuration, enable the `udp` input and set the input codec to `json`: + +```logstash +input { + udp { + port => 50000 + codec => json + } +} +``` + +## Documentation + + diff --git a/demo/elk-stack/extensions/logspout/build.sh b/demo/elk-stack/extensions/logspout/build.sh new file mode 100755 index 0000000000..c3ff938845 --- /dev/null +++ b/demo/elk-stack/extensions/logspout/build.sh @@ -0,0 +1,13 @@ +#!/bin/sh + +# source: https://github.com/gliderlabs/logspout/blob/621524e/custom/build.sh + +set -e +apk add --update go build-base git mercurial ca-certificates +cd /src +go build -ldflags "-X main.Version=$1" -o /bin/logspout +apk del go git mercurial build-base +rm -rf /root/go /var/cache/apk/* + +# backwards compatibility +ln -fs /tmp/docker.sock /var/run/docker.sock diff --git a/demo/elk-stack/extensions/logspout/logspout-compose.yml b/demo/elk-stack/extensions/logspout/logspout-compose.yml new file mode 100644 index 0000000000..8af149df36 --- /dev/null +++ b/demo/elk-stack/extensions/logspout/logspout-compose.yml @@ -0,0 +1,19 @@ +version: '3.7' + +services: + logspout: + build: + context: extensions/logspout + volumes: + - type: bind + source: /var/run/docker.sock + target: /var/run/docker.sock + read_only: true + environment: + ROUTE_URIS: logstash://logstash:50000 + LOGSTASH_TAGS: docker-elk + networks: + - elk + depends_on: + - logstash + restart: on-failure diff --git a/demo/elk-stack/extensions/logspout/modules.go b/demo/elk-stack/extensions/logspout/modules.go new file mode 100644 index 0000000000..f1a2258641 --- /dev/null +++ b/demo/elk-stack/extensions/logspout/modules.go @@ -0,0 +1,10 @@ +package main + +// installs the Logstash adapter for Logspout, and required dependencies +// https://github.com/looplab/logspout-logstash +import ( + _ "github.com/gliderlabs/logspout/healthcheck" + _ "github.com/gliderlabs/logspout/transports/tcp" + _ "github.com/gliderlabs/logspout/transports/udp" + _ "github.com/looplab/logspout-logstash" +) diff --git a/demo/elk-stack/extensions/metricbeat/.dockerignore b/demo/elk-stack/extensions/metricbeat/.dockerignore new file mode 100644 index 0000000000..37eef9d513 --- /dev/null +++ b/demo/elk-stack/extensions/metricbeat/.dockerignore @@ -0,0 +1,6 @@ +# Ignore Docker build files +Dockerfile +.dockerignore + +# Ignore OS artifacts +**/.DS_Store diff --git a/demo/elk-stack/extensions/metricbeat/Dockerfile b/demo/elk-stack/extensions/metricbeat/Dockerfile new file mode 100644 index 0000000000..6d05bf55f2 --- /dev/null +++ b/demo/elk-stack/extensions/metricbeat/Dockerfile @@ -0,0 +1,3 @@ +ARG ELASTIC_VERSION + +FROM docker.elastic.co/beats/metricbeat:${ELASTIC_VERSION} diff --git a/demo/elk-stack/extensions/metricbeat/README.md b/demo/elk-stack/extensions/metricbeat/README.md new file mode 100644 index 0000000000..1da1eaa216 --- /dev/null +++ b/demo/elk-stack/extensions/metricbeat/README.md @@ -0,0 +1,49 @@ +# Metricbeat + +Metricbeat is a lightweight shipper that you can install on your servers to periodically collect metrics from the +operating system and from services running on the server. Metricbeat takes the metrics and statistics that it collects +and ships them to the output that you specify, such as Elasticsearch or Logstash. + +## Usage + +**This extension requires the `metricbeat_internal`, `monitoring_internal` and `beats_system` users to be created and +initialized with a password.** In case you haven't done that during the initial startup of the stack, please refer to +[How to re-execute the setup][setup] to run the setup container again and initialize these users. + +To include Metricbeat in the stack, run Docker Compose from the root of the repository with an additional command line +argument referencing the `metricbeat-compose.yml` file: + +```console +$ docker-compose -f docker-compose.yml -f extensions/metricbeat/metricbeat-compose.yml up +``` + +## Configuring Metricbeat + +The Metricbeat configuration is stored in [`config/metricbeat.yml`](./config/metricbeat.yml). You can modify this file +with the help of the [Configuration reference][metricbeat-config]. + +Any change to the Metricbeat configuration requires a restart of the Metricbeat container: + +```console +$ docker-compose -f docker-compose.yml -f extensions/metricbeat/metricbeat-compose.yml restart metricbeat +``` + +Please refer to the following documentation page for more details about how to configure Metricbeat inside a +Docker container: [Run Metricbeat on Docker][metricbeat-docker]. + +## See also + +[Metricbeat documentation][metricbeat-doc] + +## Screenshots + +![stack-monitoring](https://user-images.githubusercontent.com/3299086/202710574-32a3d419-86ea-4334-b6f7-62d7826df18d.png +"Stack Monitoring") +![host-dashboard](https://user-images.githubusercontent.com/3299086/202710594-0deccf40-3a9a-4e63-8411-2e0d9cc6ad3a.png +"Host Overview Dashboard") + +[metricbeat-config]: https://www.elastic.co/guide/en/beats/metricbeat/current/metricbeat-reference-yml.html +[metricbeat-docker]: https://www.elastic.co/guide/en/beats/metricbeat/current/running-on-docker.html +[metricbeat-doc]: https://www.elastic.co/guide/en/beats/metricbeat/current/index.html + +[setup]: ../../README.md#how-to-re-execute-the-setup diff --git a/demo/elk-stack/extensions/metricbeat/config/metricbeat.yml b/demo/elk-stack/extensions/metricbeat/config/metricbeat.yml new file mode 100644 index 0000000000..1c2b6cb87d --- /dev/null +++ b/demo/elk-stack/extensions/metricbeat/config/metricbeat.yml @@ -0,0 +1,72 @@ +## Metricbeat configuration +## https://github.com/elastic/beats/blob/main/deploy/docker/metricbeat.docker.yml +# + +name: metricbeat + +metricbeat.config: + modules: + path: ${path.config}/modules.d/*.yml + # Reload module configs as they change: + reload.enabled: false + +metricbeat.autodiscover: + providers: + - type: docker + hints.enabled: true + +metricbeat.modules: +- module: elasticsearch + hosts: [ http://elasticsearch:9200 ] + username: monitoring_internal + password: ${MONITORING_INTERNAL_PASSWORD} + xpack.enabled: true + period: 10s + enabled: true +- module: logstash + hosts: [ http://logstash:9600 ] + xpack.enabled: true + period: 10s + enabled: true +- module: kibana + hosts: [ http://kibana:5601 ] + username: monitoring_internal + password: ${MONITORING_INTERNAL_PASSWORD} + xpack.enabled: true + period: 10s + enabled: true +- module: docker + metricsets: + - container + - cpu + - diskio + - healthcheck + - info + #- image + - memory + - network + hosts: [ unix:///var/run/docker.sock ] + period: 10s + enabled: true + +processors: + - add_cloud_metadata: ~ + +monitoring: + enabled: true + elasticsearch: + username: beats_system + password: ${BEATS_SYSTEM_PASSWORD} + +output.elasticsearch: + hosts: [ http://elasticsearch:9200 ] + username: metricbeat_internal + password: ${METRICBEAT_INTERNAL_PASSWORD} + +## HTTP endpoint for health checking +## https://www.elastic.co/guide/en/beats/metricbeat/current/http-endpoint.html +# + +http: + enabled: true + host: 0.0.0.0 diff --git a/demo/elk-stack/extensions/metricbeat/metricbeat-compose.yml b/demo/elk-stack/extensions/metricbeat/metricbeat-compose.yml new file mode 100644 index 0000000000..5b37a66c42 --- /dev/null +++ b/demo/elk-stack/extensions/metricbeat/metricbeat-compose.yml @@ -0,0 +1,47 @@ +version: '3.7' + +services: + metricbeat: + build: + context: extensions/metricbeat/ + args: + ELASTIC_VERSION: ${ELASTIC_VERSION} + # Run as 'root' instead of 'metricbeat' (uid 1000) to allow reading + # 'docker.sock' and the host's filesystem. + user: root + command: + # Log to stderr. + - -e + # Disable config file permissions checks. Allows mounting + # 'config/metricbeat.yml' even if it's not owned by root. + # see: https://www.elastic.co/guide/en/beats/libbeat/current/config-file-permissions.html + - --strict.perms=false + # Mount point of the host’s filesystem. Required to monitor the host + # from within a container. + - --system.hostfs=/hostfs + volumes: + - ./extensions/metricbeat/config/metricbeat.yml:/usr/share/metricbeat/metricbeat.yml:ro,Z + - type: bind + source: / + target: /hostfs + read_only: true + - type: bind + source: /sys/fs/cgroup + target: /hostfs/sys/fs/cgroup + read_only: true + - type: bind + source: /proc + target: /hostfs/proc + read_only: true + - type: bind + source: /var/run/docker.sock + target: /var/run/docker.sock + read_only: true + environment: + METRICBEAT_INTERNAL_PASSWORD: ${METRICBEAT_INTERNAL_PASSWORD:-} + MONITORING_INTERNAL_PASSWORD: ${MONITORING_INTERNAL_PASSWORD:-} + BEATS_SYSTEM_PASSWORD: ${BEATS_SYSTEM_PASSWORD:-} + networks: + - elk + depends_on: + - elasticsearch diff --git a/demo/elk-stack/kibana/.dockerignore b/demo/elk-stack/kibana/.dockerignore new file mode 100644 index 0000000000..37eef9d513 --- /dev/null +++ b/demo/elk-stack/kibana/.dockerignore @@ -0,0 +1,6 @@ +# Ignore Docker build files +Dockerfile +.dockerignore + +# Ignore OS artifacts +**/.DS_Store diff --git a/demo/elk-stack/kibana/Dockerfile b/demo/elk-stack/kibana/Dockerfile new file mode 100644 index 0000000000..9a075bedb9 --- /dev/null +++ b/demo/elk-stack/kibana/Dockerfile @@ -0,0 +1,7 @@ +ARG ELASTIC_VERSION + +# https://www.docker.elastic.co/ +FROM docker.elastic.co/kibana/kibana:${ELASTIC_VERSION} + +# Add your kibana plugins setup here +# Example: RUN kibana-plugin install diff --git a/demo/elk-stack/kibana/config/kibana.yml b/demo/elk-stack/kibana/config/kibana.yml new file mode 100644 index 0000000000..9d4e79ab44 --- /dev/null +++ b/demo/elk-stack/kibana/config/kibana.yml @@ -0,0 +1,94 @@ +--- +## Default Kibana configuration from Kibana base image. +## https://github.com/elastic/kibana/blob/main/src/dev/build/tasks/os_packages/docker_generator/templates/kibana_yml.template.ts +# +server.name: kibana +server.host: 0.0.0.0 +elasticsearch.hosts: [ http://elasticsearch:9200 ] + +monitoring.ui.container.elasticsearch.enabled: true +monitoring.ui.container.logstash.enabled: true + +## X-Pack security credentials +# +elasticsearch.username: kibana_system +elasticsearch.password: ${KIBANA_SYSTEM_PASSWORD} + +## Encryption keys (optional but highly recommended) +## +## Generate with either +## $ docker container run --rm docker.elastic.co/kibana/kibana:8.6.2 bin/kibana-encryption-keys generate +## $ openssl rand -hex 32 +## +## https://www.elastic.co/guide/en/kibana/current/using-kibana-with-security.html +## https://www.elastic.co/guide/en/kibana/current/kibana-encryption-keys.html +# +#xpack.security.encryptionKey: +#xpack.encryptedSavedObjects.encryptionKey: +#xpack.reporting.encryptionKey: + +## Fleet +## https://www.elastic.co/guide/en/kibana/current/fleet-settings-kb.html +# +xpack.fleet.agents.fleet_server.hosts: [ http://fleet-server:8220 ] + +xpack.fleet.outputs: + - id: fleet-default-output + name: default + type: elasticsearch + hosts: [ http://elasticsearch:9200 ] + is_default: true + is_default_monitoring: true + +xpack.fleet.packages: + - name: fleet_server + version: latest + - name: system + version: latest + - name: elastic_agent + version: latest + - name: apm + version: latest + +xpack.fleet.agentPolicies: + - name: Fleet Server Policy + id: fleet-server-policy + description: Static agent policy for Fleet Server + monitoring_enabled: + - logs + - metrics + package_policies: + - name: fleet_server-1 + package: + name: fleet_server + - name: system-1 + package: + name: system + - name: elastic_agent-1 + package: + name: elastic_agent + - name: Agent Policy APM Server + id: agent-policy-apm-server + description: Static agent policy for the APM Server integration + monitoring_enabled: + - logs + - metrics + package_policies: + - name: system-1 + package: + name: system + - name: elastic_agent-1 + package: + name: elastic_agent + - name: apm-1 + package: + name: apm + # See the APM package manifest for a list of possible inputs. + # https://github.com/elastic/apm-server/blob/v8.5.0/apmpackage/apm/manifest.yml#L41-L168 + inputs: + - type: apm + vars: + - name: host + value: 0.0.0.0:8200 + - name: url + value: http://apm-server:8200 diff --git a/demo/elk-stack/logstash/.dockerignore b/demo/elk-stack/logstash/.dockerignore new file mode 100644 index 0000000000..37eef9d513 --- /dev/null +++ b/demo/elk-stack/logstash/.dockerignore @@ -0,0 +1,6 @@ +# Ignore Docker build files +Dockerfile +.dockerignore + +# Ignore OS artifacts +**/.DS_Store diff --git a/demo/elk-stack/logstash/Dockerfile b/demo/elk-stack/logstash/Dockerfile new file mode 100644 index 0000000000..bde5808d98 --- /dev/null +++ b/demo/elk-stack/logstash/Dockerfile @@ -0,0 +1,7 @@ +ARG ELASTIC_VERSION + +# https://www.docker.elastic.co/ +FROM docker.elastic.co/logstash/logstash:${ELASTIC_VERSION} + +# Add your logstash plugins setup here +# Example: RUN logstash-plugin install logstash-filter-json diff --git a/demo/elk-stack/logstash/config/logstash.yml b/demo/elk-stack/logstash/config/logstash.yml new file mode 100644 index 0000000000..a81b89bc79 --- /dev/null +++ b/demo/elk-stack/logstash/config/logstash.yml @@ -0,0 +1,7 @@ +--- +## Default Logstash configuration from Logstash base image. +## https://github.com/elastic/logstash/blob/main/docker/data/logstash/config/logstash-full.yml +# +http.host: 0.0.0.0 + +node.name: logstash diff --git a/demo/elk-stack/logstash/pipeline/logstash.conf b/demo/elk-stack/logstash/pipeline/logstash.conf new file mode 100644 index 0000000000..4f5006373f --- /dev/null +++ b/demo/elk-stack/logstash/pipeline/logstash.conf @@ -0,0 +1,28 @@ +input { + beats { + port => 5044 + } + + tcp { + port => 50000 + } + + udp { + port => 50000 + codec => json + } + + http { + port => 9700 + } +} + +## Add your filters / logstash plugins configuration here + +output { + elasticsearch { + hosts => "elasticsearch:9200" + user => "logstash_internal" + password => "${LOGSTASH_INTERNAL_PASSWORD}" + } +} diff --git a/demo/elk-stack/setup/.dockerignore b/demo/elk-stack/setup/.dockerignore new file mode 100644 index 0000000000..02f2244078 --- /dev/null +++ b/demo/elk-stack/setup/.dockerignore @@ -0,0 +1,12 @@ +# Ignore Docker build files +Dockerfile +.dockerignore + +# Ignore OS artifacts +**/.DS_Store + +# Ignore Git files +.gitignore + +# Ignore setup state +state/ diff --git a/demo/elk-stack/setup/.gitignore b/demo/elk-stack/setup/.gitignore new file mode 100644 index 0000000000..a27475ad10 --- /dev/null +++ b/demo/elk-stack/setup/.gitignore @@ -0,0 +1 @@ +/state/ diff --git a/demo/elk-stack/setup/Dockerfile b/demo/elk-stack/setup/Dockerfile new file mode 100644 index 0000000000..5365a99d1d --- /dev/null +++ b/demo/elk-stack/setup/Dockerfile @@ -0,0 +1,15 @@ +ARG ELASTIC_VERSION + +# https://www.docker.elastic.co/ +FROM docker.elastic.co/elasticsearch/elasticsearch:${ELASTIC_VERSION} + +USER root + +RUN set -eux; \ + mkdir /state; \ + chmod 0775 /state; \ + chown elasticsearch:root /state + +USER elasticsearch:root + +ENTRYPOINT ["/entrypoint.sh"] diff --git a/demo/elk-stack/setup/entrypoint.sh b/demo/elk-stack/setup/entrypoint.sh new file mode 100755 index 0000000000..ec1e1ff411 --- /dev/null +++ b/demo/elk-stack/setup/entrypoint.sh @@ -0,0 +1,134 @@ +#!/usr/bin/env bash + +set -eu +set -o pipefail + +source "${BASH_SOURCE[0]%/*}"/lib.sh + + +# -------------------------------------------------------- +# Users declarations + +declare -A users_passwords +users_passwords=( + [logstash_internal]="${LOGSTASH_INTERNAL_PASSWORD:-}" + [kibana_system]="${KIBANA_SYSTEM_PASSWORD:-}" + [metricbeat_internal]="${METRICBEAT_INTERNAL_PASSWORD:-}" + [filebeat_internal]="${FILEBEAT_INTERNAL_PASSWORD:-}" + [heartbeat_internal]="${HEARTBEAT_INTERNAL_PASSWORD:-}" + [monitoring_internal]="${MONITORING_INTERNAL_PASSWORD:-}" + [beats_system]="${BEATS_SYSTEM_PASSWORD=:-}" +) + +declare -A users_roles +users_roles=( + [logstash_internal]='logstash_writer' + [metricbeat_internal]='metricbeat_writer' + [filebeat_internal]='filebeat_writer' + [heartbeat_internal]='heartbeat_writer' + [monitoring_internal]='remote_monitoring_collector' +) + +# -------------------------------------------------------- +# Roles declarations + +declare -A roles_files +roles_files=( + [logstash_writer]='logstash_writer.json' + [metricbeat_writer]='metricbeat_writer.json' + [filebeat_writer]='filebeat_writer.json' + [heartbeat_writer]='heartbeat_writer.json' +) + +# -------------------------------------------------------- + + +echo "-------- $(date --rfc-3339=seconds) --------" + +state_file="${BASH_SOURCE[0]%/*}"/state/.done +if [[ -e "$state_file" ]]; then + declare state_birthtime + state_birthtime="$(stat -c '%Y' "$state_file")" + state_birthtime="$(date --rfc-3339=seconds --date="@${state_birthtime}")" + + log "Setup has already run successfully on ${state_birthtime}. Skipping" + exit 0 +fi + +log 'Waiting for availability of Elasticsearch. This can take several minutes.' + +declare -i exit_code=0 +wait_for_elasticsearch || exit_code=$? + +if ((exit_code)); then + case $exit_code in + 6) + suberr 'Could not resolve host. Is Elasticsearch running?' + ;; + 7) + suberr 'Failed to connect to host. Is Elasticsearch healthy?' + ;; + 28) + suberr 'Timeout connecting to host. Is Elasticsearch healthy?' + ;; + *) + suberr "Connection to Elasticsearch failed. Exit code: ${exit_code}" + ;; + esac + + exit $exit_code +fi + +sublog 'Elasticsearch is running' + +log 'Waiting for initialization of built-in users' + +wait_for_builtin_users || exit_code=$? + +if ((exit_code)); then + suberr 'Timed out waiting for condition' + exit $exit_code +fi + +sublog 'Built-in users were initialized' + +for role in "${!roles_files[@]}"; do + log "Role '$role'" + + declare body_file + body_file="${BASH_SOURCE[0]%/*}/roles/${roles_files[$role]:-}" + if [[ ! -f "${body_file:-}" ]]; then + sublog "No role body found at '${body_file}', skipping" + continue + fi + + sublog 'Creating/updating' + ensure_role "$role" "$(<"${body_file}")" +done + +for user in "${!users_passwords[@]}"; do + log "User '$user'" + if [[ -z "${users_passwords[$user]:-}" ]]; then + sublog 'No password defined, skipping' + continue + fi + + declare -i user_exists=0 + user_exists="$(check_user_exists "$user")" + + if ((user_exists)); then + sublog 'User exists, setting password' + set_user_password "$user" "${users_passwords[$user]}" + else + if [[ -z "${users_roles[$user]:-}" ]]; then + suberr ' No role defined, skipping creation' + continue + fi + + sublog 'User does not exist, creating' + create_user "$user" "${users_passwords[$user]}" "${users_roles[$user]}" + fi +done + +mkdir -p "${state_file%/*}" +touch "$state_file" diff --git a/demo/elk-stack/setup/lib.sh b/demo/elk-stack/setup/lib.sh new file mode 100644 index 0000000000..7e635c6a8b --- /dev/null +++ b/demo/elk-stack/setup/lib.sh @@ -0,0 +1,240 @@ +#!/usr/bin/env bash + +# Log a message. +function log { + echo "[+] $1" +} + +# Log a message at a sub-level. +function sublog { + echo " ⠿ $1" +} + +# Log an error. +function err { + echo "[x] $1" >&2 +} + +# Log an error at a sub-level. +function suberr { + echo " ⠍ $1" >&2 +} + +# Poll the 'elasticsearch' service until it responds with HTTP code 200. +function wait_for_elasticsearch { + local elasticsearch_host="${ELASTICSEARCH_HOST:-elasticsearch}" + + local -a args=( '-s' '-D-' '-m15' '-w' '%{http_code}' "http://${elasticsearch_host}:9200/" ) + + if [[ -n "${ELASTIC_PASSWORD:-}" ]]; then + args+=( '-u' "elastic:${ELASTIC_PASSWORD}" ) + fi + + local -i result=1 + local output + + # retry for max 300s (60*5s) + for _ in $(seq 1 60); do + local -i exit_code=0 + output="$(curl "${args[@]}")" || exit_code=$? + + if ((exit_code)); then + result=$exit_code + fi + + if [[ "${output: -3}" -eq 200 ]]; then + result=0 + break + fi + + sleep 5 + done + + if ((result)) && [[ "${output: -3}" -ne 000 ]]; then + echo -e "\n${output::-3}" + fi + + return $result +} + +# Poll the Elasticsearch users API until it returns users. +function wait_for_builtin_users { + local elasticsearch_host="${ELASTICSEARCH_HOST:-elasticsearch}" + + local -a args=( '-s' '-D-' '-m15' "http://${elasticsearch_host}:9200/_security/user?pretty" ) + + if [[ -n "${ELASTIC_PASSWORD:-}" ]]; then + args+=( '-u' "elastic:${ELASTIC_PASSWORD}" ) + fi + + local -i result=1 + + local line + local -i exit_code + local -i num_users + + # retry for max 30s (30*1s) + for _ in $(seq 1 30); do + num_users=0 + + # read exits with a non-zero code if the last read input doesn't end + # with a newline character. The printf without newline that follows the + # curl command ensures that the final input not only contains curl's + # exit code, but causes read to fail so we can capture the return value. + # Ref. https://unix.stackexchange.com/a/176703/152409 + while IFS= read -r line || ! exit_code="$line"; do + if [[ "$line" =~ _reserved.+true ]]; then + (( num_users++ )) + fi + done < <(curl "${args[@]}"; printf '%s' "$?") + + if ((exit_code)); then + result=$exit_code + fi + + # we expect more than just the 'elastic' user in the result + if (( num_users > 1 )); then + result=0 + break + fi + + sleep 1 + done + + return $result +} + +# Verify that the given Elasticsearch user exists. +function check_user_exists { + local username=$1 + + local elasticsearch_host="${ELASTICSEARCH_HOST:-elasticsearch}" + + local -a args=( '-s' '-D-' '-m15' '-w' '%{http_code}' + "http://${elasticsearch_host}:9200/_security/user/${username}" + ) + + if [[ -n "${ELASTIC_PASSWORD:-}" ]]; then + args+=( '-u' "elastic:${ELASTIC_PASSWORD}" ) + fi + + local -i result=1 + local -i exists=0 + local output + + output="$(curl "${args[@]}")" + if [[ "${output: -3}" -eq 200 || "${output: -3}" -eq 404 ]]; then + result=0 + fi + if [[ "${output: -3}" -eq 200 ]]; then + exists=1 + fi + + if ((result)); then + echo -e "\n${output::-3}" + else + echo "$exists" + fi + + return $result +} + +# Set password of a given Elasticsearch user. +function set_user_password { + local username=$1 + local password=$2 + + local elasticsearch_host="${ELASTICSEARCH_HOST:-elasticsearch}" + + local -a args=( '-s' '-D-' '-m15' '-w' '%{http_code}' + "http://${elasticsearch_host}:9200/_security/user/${username}/_password" + '-X' 'POST' + '-H' 'Content-Type: application/json' + '-d' "{\"password\" : \"${password}\"}" + ) + + if [[ -n "${ELASTIC_PASSWORD:-}" ]]; then + args+=( '-u' "elastic:${ELASTIC_PASSWORD}" ) + fi + + local -i result=1 + local output + + output="$(curl "${args[@]}")" + if [[ "${output: -3}" -eq 200 ]]; then + result=0 + fi + + if ((result)); then + echo -e "\n${output::-3}\n" + fi + + return $result +} + +# Create the given Elasticsearch user. +function create_user { + local username=$1 + local password=$2 + local role=$3 + + local elasticsearch_host="${ELASTICSEARCH_HOST:-elasticsearch}" + + local -a args=( '-s' '-D-' '-m15' '-w' '%{http_code}' + "http://${elasticsearch_host}:9200/_security/user/${username}" + '-X' 'POST' + '-H' 'Content-Type: application/json' + '-d' "{\"password\":\"${password}\",\"roles\":[\"${role}\"]}" + ) + + if [[ -n "${ELASTIC_PASSWORD:-}" ]]; then + args+=( '-u' "elastic:${ELASTIC_PASSWORD}" ) + fi + + local -i result=1 + local output + + output="$(curl "${args[@]}")" + if [[ "${output: -3}" -eq 200 ]]; then + result=0 + fi + + if ((result)); then + echo -e "\n${output::-3}\n" + fi + + return $result +} + +# Ensure that the given Elasticsearch role is up-to-date, create it if required. +function ensure_role { + local name=$1 + local body=$2 + + local elasticsearch_host="${ELASTICSEARCH_HOST:-elasticsearch}" + + local -a args=( '-s' '-D-' '-m15' '-w' '%{http_code}' + "http://${elasticsearch_host}:9200/_security/role/${name}" + '-X' 'POST' + '-H' 'Content-Type: application/json' + '-d' "$body" + ) + + if [[ -n "${ELASTIC_PASSWORD:-}" ]]; then + args+=( '-u' "elastic:${ELASTIC_PASSWORD}" ) + fi + + local -i result=1 + local output + + output="$(curl "${args[@]}")" + if [[ "${output: -3}" -eq 200 ]]; then + result=0 + fi + + if ((result)); then + echo -e "\n${output::-3}\n" + fi + + return $result +} diff --git a/demo/elk-stack/setup/roles/filebeat_writer.json b/demo/elk-stack/setup/roles/filebeat_writer.json new file mode 100644 index 0000000000..118614bee8 --- /dev/null +++ b/demo/elk-stack/setup/roles/filebeat_writer.json @@ -0,0 +1,19 @@ +{ + "cluster": [ + "manage_ilm", + "manage_index_templates", + "monitor", + "read_pipeline" + ], + "indices": [ + { + "names": [ + "filebeat-*" + ], + "privileges": [ + "create_doc", + "manage" + ] + } + ] +} diff --git a/demo/elk-stack/setup/roles/heartbeat_writer.json b/demo/elk-stack/setup/roles/heartbeat_writer.json new file mode 100644 index 0000000000..9f64fa86a7 --- /dev/null +++ b/demo/elk-stack/setup/roles/heartbeat_writer.json @@ -0,0 +1,18 @@ +{ + "cluster": [ + "manage_ilm", + "manage_index_templates", + "monitor" + ], + "indices": [ + { + "names": [ + "heartbeat-*" + ], + "privileges": [ + "create_doc", + "manage" + ] + } + ] +} diff --git a/demo/elk-stack/setup/roles/logstash_writer.json b/demo/elk-stack/setup/roles/logstash_writer.json new file mode 100644 index 0000000000..b43861fed9 --- /dev/null +++ b/demo/elk-stack/setup/roles/logstash_writer.json @@ -0,0 +1,33 @@ +{ + "cluster": [ + "manage_index_templates", + "monitor", + "manage_ilm" + ], + "indices": [ + { + "names": [ + "logs-generic-default", + "logstash-*", + "ecs-logstash-*" + ], + "privileges": [ + "write", + "create", + "create_index", + "manage", + "manage_ilm" + ] + }, + { + "names": [ + "logstash", + "ecs-logstash" + ], + "privileges": [ + "write", + "manage" + ] + } + ] +} diff --git a/demo/elk-stack/setup/roles/metricbeat_writer.json b/demo/elk-stack/setup/roles/metricbeat_writer.json new file mode 100644 index 0000000000..279308c625 --- /dev/null +++ b/demo/elk-stack/setup/roles/metricbeat_writer.json @@ -0,0 +1,19 @@ +{ + "cluster": [ + "manage_ilm", + "manage_index_templates", + "monitor" + ], + "indices": [ + { + "names": [ + ".monitoring-*-mb", + "metricbeat-*" + ], + "privileges": [ + "create_doc", + "manage" + ] + } + ] +} diff --git a/demo/faber-local.sh b/demo/faber-local.sh index fcedc69bbf..4cfe6edae4 100755 --- a/demo/faber-local.sh +++ b/demo/faber-local.sh @@ -1,7 +1,7 @@ #!/bin/bash # this runs the Faber example as a local instace of instance of aca-py # you need to run a local von-network (in the von-network directory run "./manage start --logs") -# ... and you need to install the local aca-py python libraries locally ("pip install -r ../requriements.txt -r ../requirements.indy.txt -r ../requirements.bbs.txt") +# ... and you need to install the local aca-py python libraries locally ("pip install -r ../requirements.txt -r ../requirements.indy.txt -r ../requirements.bbs.txt") # the following will auto-respond on connection and credential requests, but not proof requests PYTHONPATH=.. ../bin/aca-py start \ @@ -11,7 +11,7 @@ PYTHONPATH=.. ../bin/aca-py start \ --outbound-transport http \ --admin 0.0.0.0 8021 \ --admin-insecure-mode \ - --wallet-type indy \ + --wallet-type askar \ --wallet-name faber.agent916333 \ --wallet-key faber.agent916333 \ --preserve-exchange-records \ diff --git a/demo/features/0453-issue-credential.feature b/demo/features/0453-issue-credential.feature index 5e069862ec..d8b0188604 100644 --- a/demo/features/0453-issue-credential.feature +++ b/demo/features/0453-issue-credential.feature @@ -1,3 +1,4 @@ +@RFC0453 Feature: RFC 0453 Aries agent issue credential @T003-RFC0453 @GHA @@ -54,5 +55,19 @@ Feature: RFC 0453 Aries agent issue credential | Acme_capabilities | Bob_capabilities | Schema_name | Credential_data | | --revocation --public-did | | driverslicense | Data_DL_NormalizedValues | | --revocation --public-did --did-exchange | --did-exchange | driverslicense | Data_DL_NormalizedValues | - | --revocation --public-did --mediation | --mediation | driverslicense | Data_DL_NormalizedValues | | --revocation --public-did --multitenant | --multitenant | driverslicense | Data_DL_NormalizedValues | + + @T004.1-RFC0453 + Scenario Outline: Issue a credential with revocation, with the Issuer beginning with an offer, and then revoking the credential + Given we have "2" agents + | name | role | capabilities | + | Acme | issuer | | + | Bob | holder | | + And "Acme" and "Bob" have an existing connection + And "Bob" has an issued credential from "Acme" + Then "Acme" revokes the credential + And "Bob" has the credential issued + + Examples: + | Acme_capabilities | Bob_capabilities | Schema_name | Credential_data | + | --revocation --public-did --mediation | --mediation | driverslicense | Data_DL_NormalizedValues | diff --git a/demo/features/0454-present-proof.feature b/demo/features/0454-present-proof.feature index 2092b46007..6d5ec1ed84 100644 --- a/demo/features/0454-present-proof.feature +++ b/demo/features/0454-present-proof.feature @@ -40,7 +40,7 @@ Feature: RFC 0454 Aries agent present proof @T001.2-RFC0454 @GHA Scenario Outline: Present Proof json-ld where the prover does not propose a presentation of the proof and is acknowledged - Given we have "2" agents + Given we have "3" agents | name | role | capabilities | | Acme | issuer | | | Faber | verifier | | @@ -96,3 +96,86 @@ Feature: RFC 0454 Aries agent present proof | Faber | --revocation --public-did | | driverslicense_v2 | Data_DL_MaxValues | DL_age_over_19_v2 | | Acme | --revocation --public-did --mediation | | driverslicense_v2 | Data_DL_MaxValues | DL_age_over_19_v2 | | Acme | --revocation --public-did --multitenant | --multitenant | driverslicense_v2 | Data_DL_MaxValues | DL_age_over_19_v2 | + + @T003-RFC0454.1 @GHA + Scenario Outline: Present Proof for multiple credentials where the one is revocable and one isn't, neither credential is revoked + Given we have "4" agents + | name | role | capabilities | + | Acme1 | issuer1 | | + | Acme2 | issuer2 | | + | Faber | verifier | | + | Bob | prover | | + And "" and "Bob" have an existing connection + And "Bob" has an issued credential from "" + And "" and "Bob" have an existing connection + And "Bob" has an issued credential from "" + And "Faber" and "Bob" have an existing connection + When "Faber" sends a request for proof presentation to "Bob" + Then "Faber" has the proof verified + + Examples: + | issuer1 | Acme1_capabilities | issuer2 | Acme2_capabilities | Bob_cap | Schema_name_1 | Credential_data_1 | Schema_name_2 | Credential_data_2 | Proof_request | + | Acme1 | --revocation --public-did | Acme2 | --public-did | | driverslicense_v2 | Data_DL_MaxValues | health_id | Data_DL_MaxValues | DL_age_over_19_v2_with_health_id | + + @T003-RFC0454.1f + Scenario Outline: Present Proof for multiple credentials where the one is revocable and one isn't, neither credential is revoked, fails due to requesting request-level revocation + Given we have "4" agents + | name | role | capabilities | + | Acme1 | issuer1 | | + | Acme2 | issuer2 | | + | Faber | verifier | | + | Bob | prover | | + And "" and "Bob" have an existing connection + And "Bob" has an issued credential from "" + And "" and "Bob" have an existing connection + And "Bob" has an issued credential from "" + And "Faber" and "Bob" have an existing connection + When "Faber" sends a request for proof presentation to "Bob" + Then "Faber" has the proof verification fail + + Examples: + | issuer1 | Acme1_capabilities | issuer2 | Acme2_capabilities | Bob_cap | Schema_name_1 | Credential_data_1 | Schema_name_2 | Credential_data_2 | Proof_request | + | Acme1 | --revocation --public-did | Acme2 | --public-did | | driverslicense_v2 | Data_DL_MaxValues | health_id | Data_DL_MaxValues | DL_age_over_19_v2_with_health_id_r2 | + + @T003-RFC0454.2 @GHA + Scenario Outline: Present Proof for multiple credentials where the one is revocable and one isn't, and the revocable credential is revoked, and the proof checks for revocation and fails + Given we have "4" agents + | name | role | capabilities | + | Acme1 | issuer1 | | + | Acme2 | issuer2 | | + | Faber | verifier | | + | Bob | prover | | + And "" and "Bob" have an existing connection + And "Bob" has an issued credential from "" + And "" revokes the credential + And "" and "Bob" have an existing connection + And "Bob" has an issued credential from "" + And "Faber" and "Bob" have an existing connection + When "Faber" sends a request for proof presentation to "Bob" + Then "Faber" has the proof verification fail + + Examples: + | issuer1 | Acme1_capabilities | issuer2 | Acme2_capabilities | Bob_cap | Schema_name_1 | Credential_data_1 | Schema_name_2 | Credential_data_2 | Proof_request | + | Acme1 | --revocation --public-did | Acme2 | --public-did | | driverslicense_v2 | Data_DL_MaxValues | health_id | Data_DL_MaxValues | DL_age_over_19_v2_with_health_id | + | Acme1 | --revocation --public-did | Acme2 | --public-did | | driverslicense_v2 | Data_DL_MaxValues | health_id | Data_DL_MaxValues | DL_age_over_19_v2_with_health_id_r2 | + + @T003-RFC0454.3 @GHA + Scenario Outline: Present Proof for multiple credentials where the one is revocable and one isn't, and the revocable credential is revoked, and the proof doesn't check for revocation and passes + Given we have "4" agents + | name | role | capabilities | + | Acme1 | issuer1 | | + | Acme2 | issuer2 | | + | Faber | verifier | | + | Bob | prover | | + And "" and "Bob" have an existing connection + And "Bob" has an issued credential from "" + And "" revokes the credential + And "" and "Bob" have an existing connection + And "Bob" has an issued credential from "" + And "Faber" and "Bob" have an existing connection + When "Faber" sends a request with explicit revocation status for proof presentation to "Bob" + Then "Faber" has the proof verified + + Examples: + | issuer1 | Acme1_capabilities | issuer2 | Acme2_capabilities | Bob_cap | Schema_name_1 | Credential_data_1 | Schema_name_2 | Credential_data_2 | Proof_request | + | Acme1 | --revocation --public-did | Acme2 | --public-did | | driverslicense_v2 | Data_DL_MaxValues | health_id | Data_DL_MaxValues | DL_age_over_19_v2_with_health_id_no_revoc | diff --git a/demo/features/0586-sign-transaction.feature b/demo/features/0586-sign-transaction.feature index fad7aa1dd5..5069f2974e 100644 --- a/demo/features/0586-sign-transaction.feature +++ b/demo/features/0586-sign-transaction.feature @@ -1,3 +1,4 @@ +@RFC0586 Feature: RFC 0586 Aries sign (endorse) transactions functions @T001-RFC0586 @@ -8,10 +9,10 @@ Feature: RFC 0586 Aries sign (endorse) transactions functions | Bob | author | | And "Acme" and "Bob" have an existing connection When "Acme" has a DID with role "ENDORSER" - And "Bob" has a DID with role "AUTHOR" And "Acme" connection has job role "TRANSACTION_ENDORSER" And "Bob" connection has job role "TRANSACTION_AUTHOR" And "Bob" connection sets endorser info + And "Bob" has a DID with role "AUTHOR" And "Bob" authors a schema transaction with And "Bob" requests endorsement for the transaction And "Acme" endorses the transaction @@ -24,6 +25,8 @@ Feature: RFC 0586 Aries sign (endorse) transactions functions | --mediation | --mediation | driverslicense | | --multitenant | --multitenant | driverslicense | | --mediation --multitenant | --mediation --multitenant | driverslicense | + | --multitenant --multi-ledger | --multitenant --multi-ledger | driverslicense | + | --multitenant --multi-ledger --revocation | --multitenant --multi-ledger --revocation | driverslicense | @T001.1-RFC0586 @GHA @@ -34,10 +37,10 @@ Feature: RFC 0586 Aries sign (endorse) transactions functions | Bob | author | | And "Acme" and "Bob" have an existing connection When "Acme" has a DID with role "ENDORSER" - And "Bob" has a DID with role "AUTHOR" And "Acme" connection has job role "TRANSACTION_ENDORSER" And "Bob" connection has job role "TRANSACTION_AUTHOR" And "Bob" connection sets endorser info + And "Bob" has a DID with role "AUTHOR" And "Bob" authors a schema transaction with And "Bob" requests endorsement for the transaction And "Acme" endorses the transaction @@ -57,10 +60,10 @@ Feature: RFC 0586 Aries sign (endorse) transactions functions | Bob | author | | And "Acme" and "Bob" have an existing connection When "Acme" has a DID with role "ENDORSER" - And "Bob" has a DID with role "AUTHOR" And "Acme" connection has job role "TRANSACTION_ENDORSER" And "Bob" connection has job role "TRANSACTION_AUTHOR" And "Bob" connection sets endorser info + And "Bob" has a DID with role "AUTHOR" And "Bob" authors a schema transaction with And "Bob" requests endorsement for the transaction And "Acme" endorses the transaction @@ -92,6 +95,7 @@ Feature: RFC 0586 Aries sign (endorse) transactions functions | --revocation --public-did --mediation | --revocation --mediation | driverslicense | Data_DL_NormalizedValues | | --revocation --public-did --multitenant | --revocation --multitenant | driverslicense | Data_DL_NormalizedValues | | --revocation --public-did --mediation --multitenant | --revocation --mediation --multitenant | driverslicense | Data_DL_NormalizedValues | + | --multitenant --multi-ledger --revocation --public-did | --multitenant --multi-ledger --revocation | driverslicense | Data_DL_NormalizedValues | @T002.1-RFC0586 @GHA Scenario Outline: endorse a schema and cred def transaction, write to the ledger, issue and revoke a credential, manually invoking each endorsement endpoint @@ -101,10 +105,10 @@ Feature: RFC 0586 Aries sign (endorse) transactions functions | Bob | author | | And "Acme" and "Bob" have an existing connection When "Acme" has a DID with role "ENDORSER" - And "Bob" has a DID with role "AUTHOR" And "Acme" connection has job role "TRANSACTION_ENDORSER" And "Bob" connection has job role "TRANSACTION_AUTHOR" And "Bob" connection sets endorser info + And "Bob" has a DID with role "AUTHOR" And "Bob" authors a schema transaction with And "Bob" requests endorsement for the transaction And "Acme" endorses the transaction @@ -142,10 +146,10 @@ Feature: RFC 0586 Aries sign (endorse) transactions functions | Bob | author | | And "Acme" and "Bob" have an existing connection When "Acme" has a DID with role "ENDORSER" - And "Bob" has a DID with role "AUTHOR" And "Acme" connection has job role "TRANSACTION_ENDORSER" And "Bob" connection has job role "TRANSACTION_AUTHOR" And "Bob" connection sets endorser info + And "Bob" has a DID with role "AUTHOR" And "Bob" authors a schema transaction with And "Bob" has written the schema to the ledger And "Bob" authors a credential definition transaction with @@ -172,10 +176,10 @@ Feature: RFC 0586 Aries sign (endorse) transactions functions | Bob | author | | And "Acme" and "Bob" have an existing connection When "Acme" has a DID with role "ENDORSER" - And "Bob" has a DID with role "AUTHOR" And "Acme" connection has job role "TRANSACTION_ENDORSER" And "Bob" connection has job role "TRANSACTION_AUTHOR" And "Bob" connection sets endorser info + And "Bob" has a DID with role "AUTHOR" And "Bob" authors a schema transaction with And "Bob" has written the schema to the ledger And "Bob" authors a credential definition transaction with diff --git a/demo/features/data/presentation_DL_age_over_19_v2_with_health_id.json b/demo/features/data/presentation_DL_age_over_19_v2_with_health_id.json new file mode 100644 index 0000000000..af35bfdaee --- /dev/null +++ b/demo/features/data/presentation_DL_age_over_19_v2_with_health_id.json @@ -0,0 +1,24 @@ +{ + "presentation": { + "comment": "This is a comment for the send presentation.", + "requested_attributes": { + "address_attrs": { + "cred_type_name": "Schema_DriversLicense_v2", + "revealed": true, + "cred_id": "replace_me" + }, + "health_attrs": { + "cred_type_name": "Schema_Health_ID", + "revealed": true, + "cred_id": "replace_me" + } + }, + "requested_predicates": { + "age": { + "cred_type_name": "Schema_DriversLicense_v2", + "cred_id": "replace me" + } + }, + "self_attested_attributes": {} + } +} \ No newline at end of file diff --git a/demo/features/data/presentation_DL_age_over_19_v2_with_health_id_no_revoc.json b/demo/features/data/presentation_DL_age_over_19_v2_with_health_id_no_revoc.json new file mode 100644 index 0000000000..af35bfdaee --- /dev/null +++ b/demo/features/data/presentation_DL_age_over_19_v2_with_health_id_no_revoc.json @@ -0,0 +1,24 @@ +{ + "presentation": { + "comment": "This is a comment for the send presentation.", + "requested_attributes": { + "address_attrs": { + "cred_type_name": "Schema_DriversLicense_v2", + "revealed": true, + "cred_id": "replace_me" + }, + "health_attrs": { + "cred_type_name": "Schema_Health_ID", + "revealed": true, + "cred_id": "replace_me" + } + }, + "requested_predicates": { + "age": { + "cred_type_name": "Schema_DriversLicense_v2", + "cred_id": "replace me" + } + }, + "self_attested_attributes": {} + } +} \ No newline at end of file diff --git a/demo/features/data/presentation_DL_age_over_19_v2_with_health_id_r2.json b/demo/features/data/presentation_DL_age_over_19_v2_with_health_id_r2.json new file mode 100644 index 0000000000..af35bfdaee --- /dev/null +++ b/demo/features/data/presentation_DL_age_over_19_v2_with_health_id_r2.json @@ -0,0 +1,24 @@ +{ + "presentation": { + "comment": "This is a comment for the send presentation.", + "requested_attributes": { + "address_attrs": { + "cred_type_name": "Schema_DriversLicense_v2", + "revealed": true, + "cred_id": "replace_me" + }, + "health_attrs": { + "cred_type_name": "Schema_Health_ID", + "revealed": true, + "cred_id": "replace_me" + } + }, + "requested_predicates": { + "age": { + "cred_type_name": "Schema_DriversLicense_v2", + "cred_id": "replace me" + } + }, + "self_attested_attributes": {} + } +} \ No newline at end of file diff --git a/demo/features/data/proof_request_DL_age_over_19_v2_with_health_id.json b/demo/features/data/proof_request_DL_age_over_19_v2_with_health_id.json new file mode 100644 index 0000000000..8130595ff8 --- /dev/null +++ b/demo/features/data/proof_request_DL_age_over_19_v2_with_health_id.json @@ -0,0 +1,41 @@ +{ + "presentation_proposal": { + "requested_attributes": { + "address_attrs": { + "name": "address", + "non_revoked": "change-me", + "restrictions": [ + { + "schema_name": "Schema_DriversLicense_v2", + "schema_version": "1.0.1" + } + ] + }, + "health_attrs": { + "name": "health_id_num", + "non_revoked": "change-me", + "restrictions": [ + { + "schema_name": "Schema_Health_ID", + "schema_version": "1.0.0" + } + ] + } + }, + "requested_predicates": { + "age": { + "name": "age", + "p_type": ">", + "p_value": 19, + "non_revoked": "change-me", + "restrictions": [ + { + "schema_name": "Schema_DriversLicense_v2", + "schema_version": "1.0.1" + } + ] + } + }, + "version": "0.1.0" + } +} \ No newline at end of file diff --git a/demo/features/data/proof_request_DL_age_over_19_v2_with_health_id_no_revoc.json b/demo/features/data/proof_request_DL_age_over_19_v2_with_health_id_no_revoc.json new file mode 100644 index 0000000000..e52e1e245a --- /dev/null +++ b/demo/features/data/proof_request_DL_age_over_19_v2_with_health_id_no_revoc.json @@ -0,0 +1,38 @@ +{ + "presentation_proposal": { + "requested_attributes": { + "address_attrs": { + "name": "address", + "restrictions": [ + { + "schema_name": "Schema_DriversLicense_v2", + "schema_version": "1.0.1" + } + ] + }, + "health_attrs": { + "name": "health_id_num", + "restrictions": [ + { + "schema_name": "Schema_Health_ID", + "schema_version": "1.0.0" + } + ] + } + }, + "requested_predicates": { + "age": { + "name": "age", + "p_type": ">", + "p_value": 19, + "restrictions": [ + { + "schema_name": "Schema_DriversLicense_v2", + "schema_version": "1.0.1" + } + ] + } + }, + "version": "0.1.0" + } +} \ No newline at end of file diff --git a/demo/features/data/proof_request_DL_age_over_19_v2_with_health_id_r2.json b/demo/features/data/proof_request_DL_age_over_19_v2_with_health_id_r2.json new file mode 100644 index 0000000000..e52e1e245a --- /dev/null +++ b/demo/features/data/proof_request_DL_age_over_19_v2_with_health_id_r2.json @@ -0,0 +1,38 @@ +{ + "presentation_proposal": { + "requested_attributes": { + "address_attrs": { + "name": "address", + "restrictions": [ + { + "schema_name": "Schema_DriversLicense_v2", + "schema_version": "1.0.1" + } + ] + }, + "health_attrs": { + "name": "health_id_num", + "restrictions": [ + { + "schema_name": "Schema_Health_ID", + "schema_version": "1.0.0" + } + ] + } + }, + "requested_predicates": { + "age": { + "name": "age", + "p_type": ">", + "p_value": 19, + "restrictions": [ + { + "schema_name": "Schema_DriversLicense_v2", + "schema_version": "1.0.1" + } + ] + } + }, + "version": "0.1.0" + } +} \ No newline at end of file diff --git a/demo/features/steps/0160-connection.py b/demo/features/steps/0160-connection.py index 479611964d..3814f847c6 100644 --- a/demo/features/steps/0160-connection.py +++ b/demo/features/steps/0160-connection.py @@ -28,7 +28,7 @@ @given("{n} agents") -@given(u"we have {n} agents") +@given("we have {n} agents") def step_impl(context, n): """Startup 'n' agents based on the options provided in the context table parameters.""" @@ -112,7 +112,7 @@ def step_impl(context, agent_name): @given('"{sender}" and "{receiver}" have an existing connection') def step_impl(context, sender, receiver): context.execute_steps( - u''' + ''' When "''' + sender + '''" generates a connection invitation diff --git a/demo/features/steps/0453-issue-credential.py b/demo/features/steps/0453-issue-credential.py index 7cac93d76c..b5fac909fc 100644 --- a/demo/features/steps/0453-issue-credential.py +++ b/demo/features/steps/0453-issue-credential.py @@ -30,6 +30,7 @@ @given('"{issuer}" is ready to issue a credential for {schema_name}') +@then('"{issuer}" is ready to issue a credential for {schema_name}') def step_impl(context, issuer, schema_name): agent = context.active_agents[issuer] @@ -82,8 +83,14 @@ def step_impl(context, holder): # get the required revocation info from the last credential exchange cred_exchange = context.cred_exchange + cred_ex_id = ( + cred_exchange["cred_ex_id"] + if "cred_ex_id" in cred_exchange + else cred_exchange["cred_ex_record"]["cred_ex_id"] + ) + cred_exchange = agent_container_GET( - agent["agent"], "/issue-credential-2.0/records/" + cred_exchange["cred_ex_id"] + agent["agent"], "/issue-credential-2.0/records/" + cred_ex_id ) context.cred_exchange = cred_exchange print("rev_reg_id:", cred_exchange["indy"]["rev_reg_id"]) @@ -106,6 +113,137 @@ def step_impl(context, holder): async_sleep(3.0) +@given('"{holder}" successfully revoked the credential') +@when('"{holder}" successfully revoked the credential') +@then('"{holder}" successfully revoked the credential') +def step_impl(context, holder): + agent = context.active_agents[holder] + + # get the required revocation info from the last credential exchange + cred_exchange = context.cred_exchange + print("rev_reg_id:", cred_exchange["indy"]["rev_reg_id"]) + print("cred_rev_id:", cred_exchange["indy"]["cred_rev_id"]) + print("connection_id:", cred_exchange["cred_ex_record"]["connection_id"]) + + # check wallet status + wallet_revoked_creds = agent_container_GET( + agent["agent"], + "/revocation/registry/" + + cred_exchange["indy"]["rev_reg_id"] + + "/issued/details", + ) + print("wallet_revoked_creds:", wallet_revoked_creds) + matched = False + for rec in wallet_revoked_creds: + if rec["cred_rev_id"] == cred_exchange["indy"]["cred_rev_id"]: + matched = True + assert rec["state"] == "revoked" + assert matched + + # check ledger status + ledger_revoked_creds = agent_container_GET( + agent["agent"], + "/revocation/registry/" + + cred_exchange["indy"]["rev_reg_id"] + + "/issued/indy_recs", + ) + print("ledger_revoked_creds:", ledger_revoked_creds) + print( + "assert", + cred_exchange["indy"]["cred_rev_id"], + "in", + ledger_revoked_creds["rev_reg_delta"]["value"]["revoked"], + ) + assert ( + int(cred_exchange["indy"]["cred_rev_id"]) + in ledger_revoked_creds["rev_reg_delta"]["value"]["revoked"] + ) + + +@given('"{holder}" attempts to revoke the credential') +@when('"{holder}" attempts to revoke the credential') +@then('"{holder}" attempts to revoke the credential') +def step_impl(context, holder): + agent = context.active_agents[holder] + + # get the required revocation info from the last credential exchange + cred_exchange = context.cred_exchange + print("cred_exchange:", json.dumps(cred_exchange)) + + cred_ex_id = ( + cred_exchange["cred_ex_id"] + if "cred_ex_id" in cred_exchange + else cred_exchange["cred_ex_record"]["cred_ex_id"] + ) + + cred_exchange = agent_container_GET( + agent["agent"], "/issue-credential-2.0/records/" + cred_ex_id + ) + context.cred_exchange = cred_exchange + print("rev_reg_id:", cred_exchange["indy"]["rev_reg_id"]) + print("cred_rev_id:", cred_exchange["indy"]["cred_rev_id"]) + print("connection_id:", cred_exchange["cred_ex_record"]["connection_id"]) + + # revoke the credential + try: + revoke_status = agent_container_POST( + agent["agent"], + "/revocation/revoke", + data={ + "rev_reg_id": cred_exchange["indy"]["rev_reg_id"], + "cred_rev_id": cred_exchange["indy"]["cred_rev_id"], + "publish": "Y", + "connection_id": cred_exchange["cred_ex_record"]["connection_id"], + }, + ) + except: + # ignore exceptions, we will check status later + pass + + # pause for a second + async_sleep(1.0) + + +@given('"{holder}" fails to publish the credential revocation') +@when('"{holder}" fails to publish the credential revocation') +@then('"{holder}" fails to publish the credential revocation') +def step_impl(context, holder): + agent = context.active_agents[holder] + + # get the required revocation info from the last credential exchange + cred_exchange = context.cred_exchange + print("rev_reg_id:", cred_exchange["indy"]["rev_reg_id"]) + print("cred_rev_id:", cred_exchange["indy"]["cred_rev_id"]) + print("connection_id:", cred_exchange["cred_ex_record"]["connection_id"]) + + # check wallet status + wallet_revoked_creds = agent_container_GET( + agent["agent"], + "/revocation/registry/" + + cred_exchange["indy"]["rev_reg_id"] + + "/issued/details", + ) + matched = False + for rec in wallet_revoked_creds: + if rec["cred_rev_id"] == cred_exchange["indy"]["cred_rev_id"]: + matched = True + assert rec["state"] == "revoked" + assert matched + + # check ledger status + ledger_revoked_creds = agent_container_GET( + agent["agent"], + "/revocation/registry/" + + cred_exchange["indy"]["rev_reg_id"] + + "/issued/indy_recs", + ) + print("ledger_revoked_creds:", ledger_revoked_creds) + assert ( + int(cred_exchange["indy"]["cred_rev_id"]) + not in ledger_revoked_creds["rev_reg_delta"]["value"]["revoked"] + ) + + @when('"{holder}" has the credential issued') @then('"{holder}" has the credential issued') def step_impl(context, holder): @@ -226,7 +364,7 @@ def step_impl(context, holder): ) def step_impl(context, holder, schema_name, credential_data, issuer): context.execute_steps( - u''' + ''' Given "''' + issuer + """" is ready to issue a json-ld credential for """ @@ -255,7 +393,7 @@ def step_impl(context, holder, schema_name, credential_data, issuer): ) def step_impl(context, holder, schema_name, credential_data, issuer): context.execute_steps( - u''' + ''' Given "''' + issuer + """" is ready to issue a credential for """ diff --git a/demo/features/steps/0454-present-proof.py b/demo/features/steps/0454-present-proof.py index 9b8b9cb8d6..816682ab2a 100644 --- a/demo/features/steps/0454-present-proof.py +++ b/demo/features/steps/0454-present-proof.py @@ -43,6 +43,22 @@ def step_impl(context, verifier, request_for_proof, prover): context.proof_exchange = proof_exchange +@when( + '"{verifier}" sends a request with explicit revocation status for proof presentation {request_for_proof} to "{prover}"' +) +def step_impl(context, verifier, request_for_proof, prover): + agent = context.active_agents[verifier] + + proof_request_info = read_proof_req_data(request_for_proof) + + proof_exchange = aries_container_request_proof( + agent["agent"], proof_request_info, explicit_revoc_required=True + ) + + context.proof_request = proof_request_info + context.proof_exchange = proof_exchange + + @then('"{verifier}" has the proof verified') def step_impl(context, verifier): agent = context.active_agents[verifier] diff --git a/demo/features/steps/0586-sign-transaction.py b/demo/features/steps/0586-sign-transaction.py index 531d8a7009..61702e84a8 100644 --- a/demo/features/steps/0586-sign-transaction.py +++ b/demo/features/steps/0586-sign-transaction.py @@ -34,11 +34,18 @@ def step_impl(context, agent_name, did_role): ) # make the new did the wallet's public did - created_did = agent_container_POST( + published_did = agent_container_POST( agent["agent"], "/wallet/did/public", params={"did": created_did["result"]["did"]}, ) + if "result" in published_did: + # published right away! + pass + elif "txn" in published_did: + # we are an author and need to go through the endorser process + # assume everything works! + async_sleep(3.0) if not "public_dids" in context: context.public_dids = {} @@ -366,8 +373,20 @@ def step_impl(context, agent_name): def step_impl(context, agent_name): agent = context.active_agents[agent_name] - # TODO not sure what to check here, let's just do a short pause - async_sleep(2.0) + # a registry is promoted to active when its initial entry is sent + i = 5 + while i > 0: + async_sleep(1.0) + reg_info = agent_container_GET( + agent["agent"], + f"/revocation/registry/{context.rev_reg_id}", + ) + state = reg_info["result"]["state"] + if state == "active": + return + i = i - 1 + + assert False @when( @@ -401,6 +420,9 @@ def step_impl(context, agent_name, schema_name): @when( '"{holder}" has an issued {schema_name} credential {credential_data} from "{issuer}"' ) +@then( + '"{holder}" has an issued {schema_name} credential {credential_data} from "{issuer}"' +) def step_impl(context, holder, schema_name, credential_data, issuer): context.execute_steps( ''' diff --git a/demo/features/steps/taa-txn-author-agreement.py b/demo/features/steps/taa-txn-author-agreement.py new file mode 100644 index 0000000000..6dc43cc22f --- /dev/null +++ b/demo/features/steps/taa-txn-author-agreement.py @@ -0,0 +1,89 @@ +from behave import given, when, then +import json +from time import sleep +import time + +from bdd_support.agent_backchannel_client import ( + agent_container_GET, + agent_container_POST, + agent_container_PUT, + async_sleep, +) + + +@given('"{issuer}" connects to a ledger that requires acceptance of the TAA') +def step_impl(context, issuer): + agent = context.active_agents[issuer] + + taa_info = agent_container_GET(agent["agent"], "/ledger/taa") + print("ledger taa_info:", taa_info) + assert taa_info["result"]["taa_required"] + + +@given('"{issuer}" accepts the TAA') +@when('"{issuer}" accepts the TAA') +@then('"{issuer}" accepts the TAA') +def step_impl(context, issuer): + agent = context.active_agents[issuer] + + taa_info = agent_container_GET(agent["agent"], "/ledger/taa") + print("ledger taa_info:", taa_info) + assert taa_info["result"]["taa_required"] + + taa_accept = { + "mechanism": list(taa_info["result"]["aml_record"]["aml"].keys())[0], + "version": taa_info["result"]["taa_record"]["version"], + "text": taa_info["result"]["taa_record"]["text"], + } + print("taa_acceptance:", taa_accept) + + taa_status = agent_container_POST( + agent["agent"], + "/ledger/taa/accept", + data=taa_accept, + ) + + +@given('"{issuer}" rejects the TAA') +@when('"{issuer}" rejects the TAA') +@then('"{issuer}" rejects the TAA') +def step_impl(context, issuer): + agent = context.active_agents[issuer] + + taa_info = agent_container_GET(agent["agent"], "/ledger/taa") + print("ledger taa_info:", taa_info) + assert taa_info["result"]["taa_required"] + + # reject by "accepting" with the wrong text (this can override a prior acceptance) + taa_accept = { + "mechanism": list(taa_info["result"]["aml_record"]["aml"].keys())[0], + "version": taa_info["result"]["taa_record"]["version"], + "text": "Unacceptable text", + } + print("taa_rejectance:", taa_accept) + + taa_status = agent_container_POST( + agent["agent"], + "/ledger/taa/accept", + data=taa_accept, + ) + + +@when('"{issuer}" posts a revocation correction to the ledger') +@then('"{issuer}" posts a revocation correction to the ledger') +def step_impl(context, issuer): + agent = context.active_agents[issuer] + + # get the required revocation info from the last credential exchange + cred_exchange = context.cred_exchange + + # post a correcting leger entry + ledger_status = agent_container_PUT( + agent["agent"], + "/revocation/registry/" + + cred_exchange["indy"]["rev_reg_id"] + + "/fix-revocation-entry-state", + params={ + "apply_ledger_update": "true", + }, + ) diff --git a/demo/features/taa-txn-author-acceptance.feature b/demo/features/taa-txn-author-acceptance.feature new file mode 100644 index 0000000000..ac8274c1ec --- /dev/null +++ b/demo/features/taa-txn-author-acceptance.feature @@ -0,0 +1,250 @@ +Feature: TAA Transaction Author Agreement related tests + +# Note that these tests require a ledger with TAA enabled +# you can run von-network as `./manage start --taa-sample --logs` + + @T001-TAA @taa_required + Scenario Outline: accept the ledger TAA and write to the ledger + Given we have "1" agents + | name | role | capabilities | + | Acme | issuer | | + And "Acme" connects to a ledger that requires acceptance of the TAA + When "Acme" accepts the TAA + Then "Acme" is ready to issue a credential for + + Examples: + | Acme_capabilities | Schema_name | + | --taa-accept | driverslicense | + | --taa-accept --multitenant | driverslicense | + | --taa-accept --revocation | driverslicense | + | --taa-accept --multi-ledger | driverslicense | + | --taa-accept --multitenant --multi-ledger | driverslicense | + + @T001a-TAA @taa_required + Scenario Outline: accept the ledger TAA and write to the ledger via endorser + Given we have "2" agents + | name | role | capabilities | + | Acme | endorser | | + | Bob | author | | + And "Acme" connects to a ledger that requires acceptance of the TAA + And "Bob" connects to a ledger that requires acceptance of the TAA + And "Acme" and "Bob" have an existing connection + When "Acme" accepts the TAA + And "Bob" accepts the TAA + And "Acme" has a DID with role "ENDORSER" + And "Acme" connection has job role "TRANSACTION_ENDORSER" + And "Bob" connection has job role "TRANSACTION_AUTHOR" + And "Bob" connection sets endorser info + And "Bob" has a DID with role "AUTHOR" + And "Bob" authors a schema transaction with + And "Bob" requests endorsement for the transaction + And "Acme" endorses the transaction + Then "Bob" can write the transaction to the ledger + And "Bob" has written the schema to the ledger + + Examples: + | Acme_capabilities | Bob_capabilities | Schema_name | + | --taa-accept | --taa-accept | driverslicense | + | --taa-accept --multitenant | --taa-accept --multitenant | driverslicense | + + @T002-TAA @taa_required + Scenario Outline: Revoke credential using a ledger with TAA required + Given we have "2" agents + | name | role | capabilities | + | Faber | verifier | | + | Bob | prover | | + And "Faber" connects to a ledger that requires acceptance of the TAA + And "Faber" accepts the TAA + And "Faber" and "Bob" have an existing connection + And "Bob" has an issued credential from "" + And "Faber" revokes the credential + And "Faber" successfully revoked the credential + When "Faber" sends a request for proof presentation to "Bob" + Then "Faber" has the proof verification fail + + Examples: + | issuer | Acme_capabilities | Bob_capabilities | Schema_name | Credential_data | Proof_request | + | Faber | --taa-accept --revocation --public-did | | driverslicense_v2 | Data_DL_MaxValues | DL_age_over_19_v2 | + + @T003-TAA @taa_required + Scenario Outline: Fail to publish revoked credential using a ledger with TAA required, and fix the ledger + Given we have "2" agents + | name | role | capabilities | + | Faber | verifier | | + | Bob | prover | | + And "Faber" connects to a ledger that requires acceptance of the TAA + And "Faber" accepts the TAA + And "Faber" and "Bob" have an existing connection + And "Bob" has an issued credential from "" + And "Faber" revokes the credential + And "Faber" successfully revoked the credential + When "Faber" rejects the TAA + And "Bob" has an issued credential from "" + And "Faber" attempts to revoke the credential + And "Faber" fails to publish the credential revocation + Then "Faber" accepts the TAA + And "Faber" posts a revocation correction to the ledger + And "Faber" successfully revoked the credential + And "Bob" has an issued credential from "" + And "Faber" revokes the credential + And "Faber" successfully revoked the credential + + Examples: + | issuer | Acme_capabilities | Bob_capabilities | Schema_name | Credential_data | Proof_request | + | Faber | --taa-accept --revocation --public-did | | driverslicense_v2 | Data_DL_MaxValues | DL_age_over_19_v2 | + + @T004-TAA @taa_required + Scenario Outline: Fail to publish revoked credential using a ledger with TAA required, and fix the ledger authomatically with the next revoked credential + Given we have "2" agents + | name | role | capabilities | + | Faber | verifier | | + | Bob | prover | | + And "Faber" connects to a ledger that requires acceptance of the TAA + And "Faber" accepts the TAA + And "Faber" and "Bob" have an existing connection + And "Bob" has an issued credential from "" + And "Faber" revokes the credential + And "Faber" successfully revoked the credential + When "Faber" rejects the TAA + And "Bob" has an issued credential from "" + And "Faber" attempts to revoke the credential + And "Faber" fails to publish the credential revocation + And "Faber" attempts to revoke the credential + And "Faber" fails to publish the credential revocation + And "Faber" accepts the TAA + And "Bob" has an issued credential from "" + And "Faber" revokes the credential + And "Faber" successfully revoked the credential + And "Bob" has an issued credential from "" + And "Faber" revokes the credential + And "Faber" successfully revoked the credential + + Examples: + | issuer | Acme_capabilities | Bob_capabilities | Schema_name | Credential_data | Proof_request | + | Faber | --taa-accept --revocation --public-did | | driverslicense_v2 | Data_DL_MaxValues | DL_age_over_19_v2 | + + @T004.0-TAA @taa_required + Scenario Outline: Fail to publish revoked credential using a ledger with TAA required, and fix the ledger manually before revoking more credentials + Given we have "2" agents + | name | role | capabilities | + | Faber | verifier | | + | Bob | prover | | + And "Faber" connects to a ledger that requires acceptance of the TAA + And "Faber" accepts the TAA + And "Faber" and "Bob" have an existing connection + And "Bob" has an issued credential from "" + And "Faber" revokes the credential + And "Faber" successfully revoked the credential + When "Faber" rejects the TAA + And "Bob" has an issued credential from "" + And "Faber" attempts to revoke the credential + And "Faber" fails to publish the credential revocation + And "Faber" attempts to revoke the credential + And "Faber" fails to publish the credential revocation + And "Faber" accepts the TAA + Then "Faber" posts a revocation correction to the ledger + And "Faber" successfully revoked the credential + And "Bob" has an issued credential from "" + And "Faber" revokes the credential + And "Faber" successfully revoked the credential + + Examples: + | issuer | Acme_capabilities | Bob_capabilities | Schema_name | Credential_data | Proof_request | + | Faber | --taa-accept --revocation --public-did | | driverslicense_v2 | Data_DL_MaxValues | DL_age_over_19_v2 | + + @T004.1-TAA @taa_required + Scenario Outline: Fail to publish revoked credential using a ledger with TAA required, and fix the ledger by manually applying a correction + Given we have "2" agents + | name | role | capabilities | + | Faber | verifier | | + | Bob | prover | | + And "Faber" connects to a ledger that requires acceptance of the TAA + And "Faber" accepts the TAA + And "Faber" and "Bob" have an existing connection + And "Bob" has an issued credential from "" + And "Faber" revokes the credential + And "Faber" successfully revoked the credential + When "Faber" rejects the TAA + And "Bob" has an issued credential from "" + And "Faber" attempts to revoke the credential + And "Faber" fails to publish the credential revocation + And "Faber" attempts to revoke the credential + And "Faber" fails to publish the credential revocation + And "Bob" has an issued credential from "" + And "Faber" attempts to revoke the credential + And "Faber" fails to publish the credential revocation + And "Faber" attempts to revoke the credential + And "Faber" fails to publish the credential revocation + And "Faber" accepts the TAA + Then "Faber" posts a revocation correction to the ledger + And "Faber" successfully revoked the credential + And "Bob" has an issued credential from "" + And "Faber" revokes the credential + And "Faber" successfully revoked the credential + + Examples: + | issuer | Acme_capabilities | Bob_capabilities | Schema_name | Credential_data | Proof_request | + | Faber | --taa-accept --revocation --public-did | | driverslicense_v2 | Data_DL_MaxValues | DL_age_over_19_v2 | + + @T004.2-TAA @taa_required + Scenario Outline: Fail to publish revoked credential using a ledger with TAA required, and fix the ledger automatically with the next revocation + Given we have "2" agents + | name | role | capabilities | + | Faber | verifier | | + | Bob | prover | | + And "Faber" connects to a ledger that requires acceptance of the TAA + And "Faber" accepts the TAA + And "Faber" and "Bob" have an existing connection + And "Bob" has an issued credential from "" + And "Faber" revokes the credential + And "Faber" successfully revoked the credential + When "Faber" rejects the TAA + And "Bob" has an issued credential from "" + And "Faber" attempts to revoke the credential + And "Faber" fails to publish the credential revocation + And "Bob" has an issued credential from "" + And "Faber" attempts to revoke the credential + And "Faber" fails to publish the credential revocation + And "Bob" has an issued credential from "" + And "Faber" attempts to revoke the credential + And "Faber" fails to publish the credential revocation + And "Faber" accepts the TAA + And "Bob" has an issued credential from "" + And "Faber" revokes the credential + And "Faber" successfully revoked the credential + And "Bob" has an issued credential from "" + And "Faber" revokes the credential + And "Faber" successfully revoked the credential + + Examples: + | issuer | Acme_capabilities | Bob_capabilities | Schema_name | Credential_data | Proof_request | + | Faber | --taa-accept --revocation --public-did | | driverslicense_v2 | Data_DL_MaxValues | DL_age_over_19_v2 | + + @T004.5-TAA @taa_required + Scenario Outline: Fail to publish revoked credential using a ledger with TAA required, and fix the ledger authomatically by revoking the last credential + Given we have "2" agents + | name | role | capabilities | + | Faber | verifier | | + | Bob | prover | | + And "Faber" connects to a ledger that requires acceptance of the TAA + And "Faber" accepts the TAA + And "Faber" and "Bob" have an existing connection + And "Bob" has an issued credential from "" + And "Faber" revokes the credential + And "Faber" successfully revoked the credential + When "Faber" rejects the TAA + And "Bob" has an issued credential from "" + And "Faber" attempts to revoke the credential + And "Faber" fails to publish the credential revocation + And "Faber" attempts to revoke the credential + And "Faber" fails to publish the credential revocation + And "Faber" accepts the TAA + And "Faber" attempts to revoke the credential + And "Faber" fails to publish the credential revocation + And "Bob" has an issued credential from "" + And "Faber" revokes the credential + And "Faber" successfully revoked the credential + + Examples: + | issuer | Acme_capabilities | Bob_capabilities | Schema_name | Credential_data | Proof_request | + | Faber | --taa-accept --revocation --public-did | | driverslicense_v2 | Data_DL_MaxValues | DL_age_over_19_v2 | diff --git a/demo/local-indy-args.yaml b/demo/local-indy-args.yaml index 204b04e3ea..60b0e4a91f 100644 --- a/demo/local-indy-args.yaml +++ b/demo/local-indy-args.yaml @@ -25,7 +25,7 @@ auto-ping-connection: true # curl -d '{"seed":"my_seed_000000000000000000000000", "role":"TRUST_ANCHOR", "alias":"My Agent"}' -X POST http://localhost:9000/register # note that the env var name is configured in argparse.py # seed = comes from ACAPY_WALLET_SEED -wallet-type: indy +wallet-type: askar wallet-name: testwallet # wallet-key = comes from ACAPY_WALLET_KEY # run a local postgres (docker) like: diff --git a/demo/multi-demo/Dockerfile.acapy b/demo/multi-demo/Dockerfile.acapy new file mode 100644 index 0000000000..a8eee30ae0 --- /dev/null +++ b/demo/multi-demo/Dockerfile.acapy @@ -0,0 +1,10 @@ +FROM bcgovimages/aries-cloudagent:py36-1.16-1_1.0.0-rc0 + +USER root + +ADD https://github.com/stedolan/jq/releases/download/jq-1.6/jq-linux64 ./jq +RUN chmod +x ./jq +COPY ngrok-wait.sh ngrok-wait.sh +RUN chmod +x ./ngrok-wait.sh + +USER $user diff --git a/demo/multi-demo/README.md b/demo/multi-demo/README.md new file mode 100644 index 0000000000..2f08e70b16 --- /dev/null +++ b/demo/multi-demo/README.md @@ -0,0 +1,62 @@ +# Running an Aca-Py Agent in Multitenant Mode + +This directory contains scripts to run an aca-py agent in multitenancy mode. + +## Running the Agent + +The docker-compose script runs ngrok to expose the agent's port publicly, and stores wallet data in a postgres database. + +To run the agent in this repo, open a command shell in this directory and run: + +- to build the containers: + +```bash +docker-compose build +``` + +- to run the agent: + +```bash +docker-compose up +``` + +You can connect to the [agent's api service here](http://localhost:8010). + +Note that all the configuration settings are hard-coded in the docker-compose file and ngrok-wait.sh script, so if you change any configs you need to rebuild the docker images. + +- to shut down the agent: + +```bash +docker-compose stop +docker-compose rm -f +``` + +This will leave the agent's wallet data, so if you restart the agent it will maintain any created data. + +- to remove the agent's wallet: + +```bash +docker volume rm multi-demo_wallet-db-data +``` + +# Run without NGrok +[Ngrok](https://ngrok.com) provides a tunneling service and a way to provide a public IP to your locally running instance of Aca-Py. There are restrictions with Ngrok most notably regarding inbound connections. + +``` +Too many connections! The tunnel session SESSION has violated the rate-limit policy of THRESHOLD connections per minute by initiating COUNT connections in the last SECONDS seconds. Please decrease your inbound connection volume or upgrade to a paid plan for additional capacity. + +ngrok limits the number of inbound connections to your tunnels. Limits are imposed on connections, not requests. If your HTTP clients use persistent connections aka HTTP keep-alive (most modern ones do), you'll likely never hit this limit. ngrok will return a 429 response to HTTP connections that exceed the rate limit. Connections to TCP and TLS tunnels violating the rate limit will be closed without a response. +``` + +If you do not require external access to your instance, consider turning NGrok off. NGrok tunneling can be disabled by changing the environment variable `ACAPY_AGENT_ACCESS` from "public" to "local". See [docker-compose file](docker-compose.yml). + +``` + environment: + - NGROK_NAME=ngrok-agent + - ACAPY_AGENT_ACCESS=local +``` + + +# ELK Stack / Tracing logging + +Please see [ELK Stack Readme](../elk-stack/README.md) to run the `multi-demo` with tracing enabled and pushing into an ELK Stack. \ No newline at end of file diff --git a/demo/multi-demo/docker-compose.yml b/demo/multi-demo/docker-compose.yml new file mode 100644 index 0000000000..1d5fbc0a53 --- /dev/null +++ b/demo/multi-demo/docker-compose.yml @@ -0,0 +1,74 @@ +# Sample docker-compose to start a local aca-py multitenancy agent +# To start aca-py and the postgres database, just run `docker compose up` +# To shut down the services run `docker compose rm` - this will retain the postgres database, so you can change aca-py startup parameters +# and restart the docker containers without losing your wallet data +# If you want to delete your wallet data just run `docker volume ls -q | xargs docker volume rm` +# +# If you want to enable tracing, see elk-stack. Ensure elk-stack is running, uncomment the ACAPY_TRACE environement variables and run `docker compose up` +version: "3" + + +networks: + app-network: + name: ${APP_NETWORK_NAME:-appnet} + driver: bridge + elk-network: + name: ${ELK_NETWORK_NAME:-elknet} + driver: bridge + +services: + ngrok-agent: + image: wernight/ngrok + ports: + - 4067:4040 + command: ngrok http multi-agent:8001 --log stdout + networks: + - app-network + + multi-agent: + build: + context: . + dockerfile: Dockerfile.acapy + environment: + - NGROK_NAME=ngrok-agent + - ACAPY_AGENT_ACCESS=${ACAPY_AGENT_ACCESS:-local} + - ACAPY_ENDPOINT=http://multi-agent:8001 + # - ACAPY_TRACE=${ACAPY_TRACE:-1} + # - ACAPY_TRACE_TARGET=${ACAPY_TRACE_TARGET:-http://logstash:9700/} + # - ACAPY_TRACE_TAG=${ACAPY_TRACE_TAG:-acapy.events} + # - ACAPY_TRACE_LABEL=${ACAPY_TRACE_LABEL:-multi.agent.trace} + - ACAPY_AUTO_ACCEPT_INVITES=true + - ACAPY_LOG_LEVEL=${LOG_LEVEL:-INFO} + - RUST_LOG=${RUST_LOG:-ERROR} + ports: + - 8010:8010 + - 8001:8001 + depends_on: + - wallet-db + entrypoint: /bin/bash + command: [ + "-c", + "sleep 5; \ + ./ngrok-wait.sh" + ] + volumes: + - ./ngrok-wait.sh:/home/indy/ngrok-wait.sh + networks: + - app-network + - elk-network + + wallet-db: + image: postgres:14 + environment: + - POSTGRES_USER=DB_USER + - POSTGRES_PASSWORD=DB_PASSWORD + ports: + - 5433:5432 + volumes: + - wallet-db-data:/var/lib/pgsql/data + - ./max_conns.sql:/docker-entrypoint-initdb.d/max_conns.sql + networks: + - app-network + +volumes: + wallet-db-data: diff --git a/demo/multi-demo/max_conns.sql b/demo/multi-demo/max_conns.sql new file mode 100644 index 0000000000..c49d0e4b0e --- /dev/null +++ b/demo/multi-demo/max_conns.sql @@ -0,0 +1 @@ +ALTER SYSTEM SET max_connections = 500; \ No newline at end of file diff --git a/demo/multi-demo/ngrok-wait.sh b/demo/multi-demo/ngrok-wait.sh new file mode 100755 index 0000000000..61d253a72e --- /dev/null +++ b/demo/multi-demo/ngrok-wait.sh @@ -0,0 +1,49 @@ +#!/bin/bash + +# based on code developed by Sovrin: https://github.com/hyperledger/aries-acapy-plugin-toolbox + +if [[ "${ACAPY_AGENT_ACCESS}" == "public" ]]; then + echo "using ngrok end point [$NGROK_NAME]" + + NGROK_ENDPOINT=null + while [ -z "$NGROK_ENDPOINT" ] || [ "$NGROK_ENDPOINT" = "null" ] + do + echo "Fetching end point from ngrok service" + NGROK_ENDPOINT=$(curl --silent $NGROK_NAME:4040/api/tunnels | ./jq -r '.tunnels[] | select(.proto=="https") | .public_url') + + if [ -z "$NGROK_ENDPOINT" ] || [ "$NGROK_ENDPOINT" = "null" ]; then + echo "ngrok not ready, sleeping 5 seconds...." + sleep 5 + fi + done + + export ACAPY_ENDPOINT=$NGROK_ENDPOINT +fi + + +echo "Starting aca-py agent with endpoint [$ACAPY_ENDPOINT]" + +# ... if you want to echo the aca-py startup command ... +set -x + +exec aca-py start \ + --auto-provision \ + --inbound-transport http '0.0.0.0' 8001 \ + --outbound-transport http \ + --genesis-url "http://test.bcovrin.vonx.io/genesis" \ + --endpoint "${ACAPY_ENDPOINT}" \ + --auto-ping-connection \ + --monitor-ping \ + --public-invites \ + --wallet-type "askar" \ + --wallet-name "test_multi" \ + --wallet-key "secret_key" \ + --wallet-storage-type "postgres_storage" \ + --wallet-storage-config "{\"url\":\"wallet-db:5432\",\"max_connections\":5,\"scheme\":\"MultiWalletSingleTable\"}" \ + --wallet-storage-creds "{\"account\":\"DB_USER\",\"password\":\"DB_PASSWORD\",\"admin_account\":\"DB_USER\",\"admin_password\":\"DB_PASSWORD\"}" \ + --admin '0.0.0.0' 8010 \ + --label "test_multi" \ + --admin-insecure-mode \ + --multitenant \ + --multitenant-admin \ + --jwt-secret "very_secret_secret" diff --git a/demo/multi_ledger_config.yml b/demo/multi_ledger_config.yml index 3290f50d5e..2c423b8d51 100644 --- a/demo/multi_ledger_config.yml +++ b/demo/multi_ledger_config.yml @@ -1,5 +1,9 @@ +#- id: local +# is_production: true +# genesis_url: 'http://$LEDGER_HOST:9000/genesis' - id: bcorvinTest is_production: true + is_write: true genesis_url: 'http://test.bcovrin.vonx.io/genesis' - id: greenlightTest is_production: true diff --git a/demo/multi_ledger_config_bdd.yml b/demo/multi_ledger_config_bdd.yml new file mode 100644 index 0000000000..14f7919fe1 --- /dev/null +++ b/demo/multi_ledger_config_bdd.yml @@ -0,0 +1,11 @@ +- id: local + is_production: true + is_write: true + genesis_url: 'http://$LEDGER_HOST:9000/genesis' +- id: bcorvinTest + is_production: true +# is_write: true + genesis_url: 'http://test.bcovrin.vonx.io/genesis' +- id: greenlightTest + is_production: true + genesis_url: 'http://dev.greenlight.bcovrin.vonx.io/genesis' diff --git a/demo/playground/.env.sample b/demo/playground/.env.sample new file mode 100644 index 0000000000..e8783006a9 --- /dev/null +++ b/demo/playground/.env.sample @@ -0,0 +1,147 @@ +NGROK_AUTHTOKEN= + +ACAPY_GENESIS_URL=http://test.bcovrin.vonx.io/genesis + +# +# database +# +POSTGRESQL_HOST=wallet-db +POSTGRESQL_PORT=5432 +POSTGRESQL_USER=postgres +POSTGRESQL_PASSWORD=development +POSTGRESQL_ADMIN_USER=postgres +POSTGRESQL_ADMIN_PASSWORD=development + +# +# wallet storage configuration +# +ACAPY_WALLET_STORAGE_CONFIG={"url":"${POSTGRESQL_HOST}:${POSTGRESQL_PORT}","wallet_scheme":"DatabasePerWallet"} +ACAPY_WALLET_STORAGE_CREDS={"account":"${POSTGRESQL_USER}","password":"${POSTGRESQL_PASSWORD}","admin_account":"${POSTGRESQL_ADMIN_USER}","admin_password":"${POSTGRESQL_ADMIN_PASSWORD}"} + +# +# logging +# +ACAPY_LOG_LEVEL=INFO +RUST_LOG=ERROR + +# +# tracing - uncomment in each service if needed +# +ACAPY_TRACE=0 +ACAPY_TRACE_TARGET=http://logstash:9700/ +ACAPY_TRACE_TAG=acapy.events +ACAPY_TRACE_LABEL=agent.trace + +# +# NGROK Tunnels/Hosts : ensure services and tunnel config match if you change service ports +# tunnels: +# : +# proto: http +# addr: : +# +NGROK_FABER_ALICE_HOST=ngrok-faber-alice +NGROK_ACME_MULTI_HOST=ngrok-acme-multi + +# +# Faber : faber-agent 9001/9011 +# +FABER_TUNNEL_HOST=${NGROK_FABER_ALICE_HOST} +FABER_TUNNEL_NAME=faber +FABER_HOST=faber-agent +FABER_AGENT_LABEL=faber +FABER_AGENT_HTTP_IN_PORT=9001 +FABER_AGENT_HTTP_ADMIN_PORT=9011 +FABER_AGENT_ARG_FILE=./configs/singletenant-auto-accept.yml +FABER_ACAPY_ENDPOINT=http://${FABER_HOST}:${FABER_AGENT_HTTP_IN_PORT} +FABER_ACAPY_WALLET_STORAGE_CONFIG=${ACAPY_WALLET_STORAGE_CONFIG} +FABER_ACAPY_WALLET_STORAGE_CREDS=${ACAPY_WALLET_STORAGE_CREDS} +FABER_ACAPY_WALLET_NAME=faber_wallet +FABER_ACAPY_WALLET_KEY=changeme +FABER_ACAPY_GENESIS_URL=${ACAPY_GENESIS_URL} +FABER_ACAPY_LOG_LEVEL=${ACAPY_LOG_LEVEL} +FABER_RUST_LOG=${RUST_LOG} +FABER_POSTGRESQL_HOST=${POSTGRESQL_HOST} +FABER_POSTGRESQL_PORT=${POSTGRESQL_PORT} +# tracing... +FABER_ACAPY_TRACE=${ACAPY_TRACE} +FABER_ACAPY_TRACE_TARGET=${ACAPY_TRACE_TARGET} +FABER_ACAPY_TRACE_TAG=${ACAPY_TRACE_TAG} +FABER_ACAPY_TRACE_LABEL=faber.agent.trace + +# +# Alice : alice-agent - 9002/9012 +# +ALICE_TUNNEL_HOST=${NGROK_FABER_ALICE_HOST} +ALICE_TUNNEL_NAME=alice +ALICE_HOST=alice-agent +ALICE_AGENT_LABEL=alice +ALICE_AGENT_HTTP_IN_PORT=9002 +ALICE_AGENT_HTTP_ADMIN_PORT=9012 +ALICE_AGENT_ARG_FILE=./configs/singletenant-auto-accept.yml +ALICE_ACAPY_ENDPOINT=http://${ALICE_HOST}:${ALICE_AGENT_HTTP_IN_PORT} +ALICE_ACAPY_WALLET_STORAGE_CONFIG=${ACAPY_WALLET_STORAGE_CONFIG} +ALICE_ACAPY_WALLET_STORAGE_CREDS=${ACAPY_WALLET_STORAGE_CREDS} +ALICE_ACAPY_WALLET_NAME=alice_wallet +ALICE_ACAPY_WALLET_KEY=changeme +ALICE_ACAPY_GENESIS_URL=${ACAPY_GENESIS_URL} +ALICE_ACAPY_LOG_LEVEL=${ACAPY_LOG_LEVEL} +ALICE_RUST_LOG=${RUST_LOG} +ALICE_POSTGRESQL_HOST=${POSTGRESQL_HOST} +ALICE_POSTGRESQL_PORT=${POSTGRESQL_PORT} +# tracing... +ALICE_ACAPY_TRACE=${ACAPY_TRACE} +ALICE_ACAPY_TRACE_TARGET=${ACAPY_TRACE_TARGET} +ALICE_ACAPY_TRACE_TAG=${ACAPY_TRACE_TAG} +ALICE_ACAPY_TRACE_LABEL=alice.agent.trace + +# +# Acme : acme-agent - 9003/9013 +# +ACME_TUNNEL_HOST=${NGROK_ACME_MULTI_HOST} +ACME_TUNNEL_NAME=acme +ACME_HOST=acme-agent +ACME_AGENT_LABEL=acme +ACME_AGENT_HTTP_IN_PORT=9003 +ACME_AGENT_HTTP_ADMIN_PORT=9013 +ACME_AGENT_ARG_FILE=./configs/singletenant-auto-accept.yml +ACME_ACAPY_ENDPOINT=http://${ACME_HOST}:${ACME_AGENT_HTTP_IN_PORT} +ACME_ACAPY_WALLET_STORAGE_CONFIG=${ACAPY_WALLET_STORAGE_CONFIG} +ACME_ACAPY_WALLET_STORAGE_CREDS=${ACAPY_WALLET_STORAGE_CREDS} +ACME_ACAPY_WALLET_NAME=acme_wallet +ACME_ACAPY_WALLET_KEY=changeme +ACME_ACAPY_GENESIS_URL=${ACAPY_GENESIS_URL} +ACME_ACAPY_LOG_LEVEL=${ACAPY_LOG_LEVEL} +ACME_RUST_LOG=${RUST_LOG} +ACME_POSTGRESQL_HOST=${POSTGRESQL_HOST} +ACME_POSTGRESQL_PORT=${POSTGRESQL_PORT} +# tracing... +ACME_ACAPY_TRACE=${ACAPY_TRACE} +ACME_ACAPY_TRACE_TARGET=${ACAPY_TRACE_TARGET} +ACME_ACAPY_TRACE_TAG=${ACAPY_TRACE_TAG} +ACME_ACAPY_TRACE_LABEL=acme.agent.trace + +# +# Multi : multi-agent - 9004/9014 +# +MULTI_TUNNEL_HOST=${NGROK_ACME_MULTI_HOST} +MULTI_TUNNEL_NAME=multi +MULTI_HOST=multi-agent +MULTI_AGENT_LABEL=multi +MULTI_AGENT_HTTP_IN_PORT=9004 +MULTI_AGENT_HTTP_ADMIN_PORT=9014 +MULTI_AGENT_ARG_FILE=./configs/multitenant-auto-accept.yml +MULTI_ACAPY_ENDPOINT=http://${MULTI_HOST}:${MULTI_AGENT_HTTP_IN_PORT} +MULTI_ACAPY_WALLET_STORAGE_CONFIG=${ACAPY_WALLET_STORAGE_CONFIG} +MULTI_ACAPY_WALLET_STORAGE_CREDS=${ACAPY_WALLET_STORAGE_CREDS} +MULTI_ACAPY_WALLET_NAME=multi_wallet +MULTI_ACAPY_WALLET_KEY=changeme +MULTI_ACAPY_GENESIS_URL=${ACAPY_GENESIS_URL} +MULTI_ACAPY_LOG_LEVEL=${ACAPY_LOG_LEVEL} +MULTI_RUST_LOG=${RUST_LOG} +MULTI_POSTGRESQL_HOST=${POSTGRESQL_HOST} +MULTI_POSTGRESQL_PORT=${POSTGRESQL_PORT} +# tracing... +MULTI_ACAPY_TRACE=${ACAPY_TRACE} +MULTI_ACAPY_TRACE_TARGET=${ACAPY_TRACE_TARGET} +MULTI_ACAPY_TRACE_TAG=${ACAPY_TRACE_TAG} +MULTI_ACAPY_TRACE_LABEL=multi.agent.trace diff --git a/demo/playground/Dockerfile.acapy b/demo/playground/Dockerfile.acapy new file mode 100644 index 0000000000..21b804aa0f --- /dev/null +++ b/demo/playground/Dockerfile.acapy @@ -0,0 +1,20 @@ +FROM ghcr.io/hyperledger/aries-cloudagent-python:py3.9-0.8.1 + +USER root + +RUN mkdir -p /acapy-agent +WORKDIR /acapy-agent + +ADD https://github.com/stedolan/jq/releases/download/jq-1.6/jq-linux64 /usr/bin/jq +RUN chmod +x /usr/bin/jq + +USER $user + +# Copy the necessary files +COPY ./start.sh start.sh +COPY ./configs configs + +RUN chmod +x start.sh && \ + aca-py --version > ./acapy-version.txt + +ENTRYPOINT ["bash", "./start.sh"] diff --git a/demo/playground/README.md b/demo/playground/README.md new file mode 100644 index 0000000000..1d3ab700fc --- /dev/null +++ b/demo/playground/README.md @@ -0,0 +1,147 @@ +# ACA-Py Playground + +This directory contains scripts to run several ACA-Py agents in various configurations for demonstration, development and testing scenarios. The agents are using Postgres (15) database storage (`askar`, `DatabasePerWallet`) and are running without security (`--admin-insecure-mode`). + +The inspiration for this playground was testing mediation and the differences between single-tenant and multi-tenant modes. These scripts allowed the developer to stand up 3 single-tenant agents and 1 multi-tenant agent and run various scenarios (see [scripts](./scripts) - for some basic examples). Running in `--admin-insecure-mode` simplifies creating tenants in multi-tenant mode and eliminates the need for adding headers for calls in single-tenant mode. + +- faber-agent +- alice-agent +- acme-agent +- multi-agent (the multi-tenant agent) + +By default, all the agents share the same Postgres Database Service (version 15) and all use [Ngrok](https://ngrok.com) for publicly accessible URLs. + +## Dependencies + +Docker Compose version v2.17.2 + +## Agent Configuration + +There are two simple configurations provided: + +- [`singletenant-auto-accept.yml`](./configs/singletenant-auto-accept.yml) +- [`multitenant-auto-accept.yml`](./configs/multitenant-auto-accept.yml) + +These configuration files are provided to the ACA-Py start command via the `AGENT_ARG_FILE` environment variable. See [`.env`](./.env.sample) and [`start.sh`](./start.sh). + +### Dockerfile and start.sh + +[`Dockerfile.acapy`](./Dockerfile.acapy) assembles the image to run. Currently based on [Aries Cloudagent Python 0.8.1](ghcr.io/hyperledger/aries-cloudagent-python:py3.9-indy-1.16.0-0.8.1), we need [jq](https://stedolan.github.io/jq/) to setup (or not) the ngrok tunnel and execute the Aca-py start command - see [`start.sh`](./start.sh). You may note that the start command is very sparse, additional configuration is done via environment variables in the [docker compose file](./docker-compose.yml). + +### ngrok + +Note that ngrok allows 2 tunnels per instance with an unpaid account. We have broken up the 4 default services into 2 ngrok services and tunnel configurations. If you need to alter port numbers for your agent services, you will have to update the ngrok tunnel files. + +- [ngrok-faber-alice](./ngrok-faber-alice.yml) +- [ngrok-acme-multi](./ngrok-acme-multi.yml) + +If you have a paid account, you can set the `NGROK_AUTHTOKEN` environment variable. See below. + +### .env + +Additional configuration (ie. port numbers for the services, `NGROK_AUTHTOKEN`, ...) are done in the [`.env`](./.env.sample) file. Change as needed and ensure ngrok configuration matches. + +```shell +cp .env.sample .env +``` + +## Running the Playground + +To run the agents in this repo, open a command shell in this directory and run: + +- to build the containers: + +```bash +docker compose build +``` + +- to run the agents: + +```bash +docker compose up +``` + +- to shut down: + +```bash +docker compose stop +docker compose rm -f +``` + +This will leave the agents wallet data, so if you restart the agent it will maintain any created data. + +- to remove the wallet data: + +```bash +docker compose down -v --remove-orphans +``` + +- individual services can be started by specifying the service name(s): + +```bash +docker compose up multi-agent +docker compose up faber-agent alice-agent +``` + +You can now access the agent Admin APIs via Swagger at: + +- faber: [http://localhost:9011/api/doc#](http://localhost:9011/api/doc#) +- alice: [http://localhost:9012/api/doc#](http://localhost:9012/api/doc#) +- acme: [http://localhost:9013/api/doc#](http://localhost:9013/api/doc#) +- multi: [http://localhost:9014/api/doc#](http://localhost:9014/api/doc#) + +## Scripts + +While having the Swagger Admin API is excellent, you may need to do something more complex than a single API call. You may need to see how agents with varying capabilities interact or validate that single-tenant and multi-tenant work the same. Jumping around from multiple browser tabs and cutting and pasting ids and JSON blocks can quickly grow tiresome. + +A few Python (3.9) [scripts](./scripts) are provided as examples of what you may do once your agents are up and running. + +```shell +cd scripts +pip install -r requirements.txt +python ping_agents.py +``` + +The [`ping_agents`](./scripts/ping_agents.py) script is a trivial example using the ACA-Py API to create tenants in the multi-agent instance and interact between the agents. We create and receive invitations and ping each other. + +The [`mediator_ping_agents`](./scripts/mediator_ping_agents) script requires that you have a mediator service running and have the mediator's invitation URL. See [Aries Mediator Service](https://github.com/hyperledger/aries-mediator-service) for standing up a local instance and how to find the invitation URL. In this script, each agent requests mediation and we can see the mediator forwarding messages between the agents. + +## Run without NGrok + +[Ngrok](https://ngrok.com) provides a tunneling service and a way to provide a public IP to your locally running instance of ACA-Py. There are restrictions with Ngrok most notably regarding inbound connections. + +```shell +Too many connections! The tunnel session SESSION has violated the rate-limit policy of THRESHOLD connections per minute by initiating COUNT connections in the last SECONDS seconds. Please decrease your inbound connection volume or upgrade to a paid plan for additional capacity. + +ngrok limits the number of inbound connections to your tunnels. Limits are imposed on connections, not requests. If your HTTP clients use persistent connections aka HTTP keep-alive (most modern ones do), you'll likely never hit this limit. ngrok will return a 429 response to HTTP connections that exceed the rate limit. Connections to TCP and TLS tunnels violating the rate limit will be closed without a response. +``` + +If you do not require external access to your instance, consider turning NGrok off. NGrok tunnelling can be disabled by changing an environment variable for each service. Set `TUNNEL_NAME` to null/empty, and no tunnel will be created. See [`.env`](.env.sample), [`docker-compose.yml`](./docker-compose.yml) and [`start.sh`](./start.sh), + +### update .env + +```shell +FABER_TUNNEL_NAME= +``` + +### docker-compose.yml + +```shell + environment: + - TUNNEL_HOST=${FABER_TUNNEL_HOST} +``` + +### start.sh + +```shell +# if $TUNNEL_NAME is not empty, grab the service's ngrok route and set our ACAPY_ENDPOINT +if [[ ! -z "$TUNNEL_NAME" ]]; then . . . +``` + +Set service value to empty in `.env` that will set the `TUNNEL_NAME` environment variable to empty which will circumvent the use of the tunnel for the service (`ACAPY_ENDPOINT`). + +## ELK Stack / Tracing logging + +Please see [ELK Stack Readme](../elk-stack/README.md). + +You may notice a series of environment variables for each agent service in [.env](./.env.sample) and commented-out network and agent configuration in the [docker compose file](./docker-compose.yml). Check the environment variables and uncomment as needed if wanting to send trace events to ELK. diff --git a/demo/playground/configs/multitenant-auto-accept.yml b/demo/playground/configs/multitenant-auto-accept.yml new file mode 100644 index 0000000000..ee8a3b61d2 --- /dev/null +++ b/demo/playground/configs/multitenant-auto-accept.yml @@ -0,0 +1,10 @@ +# Connections +debug-connections: true +auto-accept-invites: true +auto-accept-requests: true +auto-ping-connection: true + +# multitenant +multitenant: true +multitenant-admin: true +jwt-secret: changeme \ No newline at end of file diff --git a/demo/playground/configs/singletenant-auto-accept.yml b/demo/playground/configs/singletenant-auto-accept.yml new file mode 100644 index 0000000000..a9434151a4 --- /dev/null +++ b/demo/playground/configs/singletenant-auto-accept.yml @@ -0,0 +1,5 @@ +# Connections +debug-connections: true +auto-accept-invites: true +auto-accept-requests: true +auto-ping-connection: true \ No newline at end of file diff --git a/demo/playground/docker-compose.yml b/demo/playground/docker-compose.yml new file mode 100644 index 0000000000..da74281568 --- /dev/null +++ b/demo/playground/docker-compose.yml @@ -0,0 +1,265 @@ +# Sample docker-compose to start a local aca-py multitenancy agent +# To start aca-py and the postgres database, just run `docker compose up` +# To shut down the services run `docker compose rm` - this will retain the postgres database, so you can change aca-py startup parameters +# and restart the docker containers without losing your wallet data +# If you want to delete your wallet data just run `docker volume ls -q | xargs docker volume rm` +# +# If you want to enable tracing, see elk-stack. Ensure elk-stack is running, uncomment the ACAPY_TRACE environement variables and run `docker compose up` +version: "3" + + +networks: + app-network: + name: ${APP_NETWORK_NAME:-playgroundnet} + driver: bridge + elk-network: + name: ${ELK_NETWORK_NAME:-elknet} + driver: bridge + +services: + ngrok-faber-alice: + image: ngrok/ngrok:latest + restart: unless-stopped + hostname: ${NGROK_FABER_ALICE_HOST} + environment: + - NGROK_AUTHTOKEN=${NGROK_AUTHTOKEN:-} + command: + - "start" + - "--all" + - "--config" + - "/etc/ngrok.yml" + volumes: + - ./ngrok-faber-alice.yml:/etc/ngrok.yml + networks: + - app-network + # ports: + # - 4040:4040 + healthcheck: + test: /bin/bash -c "= "3.6" +aiosignal==1.3.1; python_version >= "3.7" +async-timeout==4.0.2; python_version >= "3.6" +attrs==23.1.0; python_version >= "3.7" +certifi==2023.5.7; python_version >= "3.7" +charset-normalizer==3.1.0; python_full_version >= "3.7.0" and python_version >= "3.7" +colorama==0.4.6; python_version >= "3.7" and python_full_version < "3.0.0" and sys_platform == "win32" or sys_platform == "win32" and python_version >= "3.7" and python_full_version >= "3.7.0" +exceptiongroup==1.1.1; python_version < "3.11" and python_version >= "3.7" +frozenlist==1.3.3; python_version >= "3.7" +idna==3.4; python_version >= "3.7" +iniconfig==2.0.0; python_version >= "3.7" +multidict==6.0.4; python_version >= "3.7" +packaging==23.1; python_version >= "3.7" +pluggy==1.0.0; python_version >= "3.7" +pytest==7.3.1; python_version >= "3.7" +pyyaml==6.0; python_version >= "3.6" +random-word==1.0.11; python_version >= "3" +repoze.lru==0.7 +requests==2.31.0; python_version >= "3.7" +routes==2.5.1 +six==1.16.0; python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.3.0" +tomli==2.0.1; python_version < "3.11" and python_version >= "3.7" +urllib3==2.0.2; python_version >= "3.7" +yarl==1.9.2; python_version >= "3.7" diff --git a/demo/playground/start.sh b/demo/playground/start.sh new file mode 100644 index 0000000000..ce41ecb8af --- /dev/null +++ b/demo/playground/start.sh @@ -0,0 +1,41 @@ +#!/bin/bash + +# set -euxo pipefail + +# if $TUNNEL_NAME is not empty, grab the service's ngrok route and set our ACAPY_ENDPOINT +if [[ ! -z "$TUNNEL_NAME" ]]; then + echo "using ngrok tunnel for [$TUNNEL_NAME]" + + NGROK_ENDPOINT=null + while [ -z "$NGROK_ENDPOINT" ] || [ "$NGROK_ENDPOINT" = "null" ] + do + echo "Fetching end point from ngrok service" + NGROK_ENDPOINT=$(curl -s $TUNNEL_HOST:4040/api/tunnels | jq -r '.tunnels[] | select(.name==env.TUNNEL_NAME) | select(.proto=="https") | .public_url') + + if [ -z "$NGROK_ENDPOINT" ] || [ "$NGROK_ENDPOINT" = "null" ]; then + echo "ngrok not ready, sleeping 5 seconds...." + sleep 5 + fi + done + + export ACAPY_ENDPOINT=$NGROK_ENDPOINT +fi + + +echo "Starting aca-py agent [$AGENT_LABEL] with endpoint [$ACAPY_ENDPOINT]" + +# ... if you want to echo the aca-py startup command ... +# set -x + +aca-py start \ + --auto-provision \ + --arg-file ${AGENT_ARG_FILE} \ + --label "${AGENT_LABEL}" \ + --inbound-transport http 0.0.0.0 ${AGENT_HTTP_IN_PORT} \ + --outbound-transport http \ + --emit-new-didcomm-prefix \ + --wallet-type askar \ + --wallet-storage-type postgres_storage \ + --admin-insecure-mode \ + --admin 0.0.0.0 ${AGENT_HTTP_ADMIN_PORT} \ + --endpoint "${ACAPY_ENDPOINT}" diff --git a/demo/requirements.txt b/demo/requirements.txt index 276088d6b5..26f58df943 100644 --- a/demo/requirements.txt +++ b/demo/requirements.txt @@ -1,5 +1,5 @@ asyncpg~=0.25.0 prompt_toolkit~=2.0.9 -git+https://github.com/webpy/webpy.git#egg=web.py +web.py~=0.62 pygments~=2.10 qrcode[pil]~=6.1 diff --git a/demo/run_demo b/demo/run_demo index ce2c193804..088685eab3 100755 --- a/demo/run_demo +++ b/demo/run_demo @@ -21,8 +21,10 @@ if [ -z "$DOCKER_NET" ]; then fi DOCKER_VOL="" +j=1 for i in "$@" do + ((j++)) if [ ! -z "$SKIP" ]; then SKIP="" continue @@ -65,12 +67,12 @@ do continue ;; --debug-pycharm-controller-port) - PYDEVD_PYCHARM_CONTROLLER_PORT=$2 + PYDEVD_PYCHARM_CONTROLLER_PORT=${!j} SKIP=1 continue ;; --debug-pycharm-agent-port) - PYDEVD_PYCHARM_AGENT_PORT=$2 + PYDEVD_PYCHARM_AGENT_PORT=${!j} SKIP=1 continue ;; @@ -154,7 +156,7 @@ if [ ! -z "$DOCKERHOST" ]; then export RUNMODE="docker" elif [ -z "${PWD_HOST_FQDN}" ]; then # getDockerHost; for details refer to https://github.com/bcgov/DITP-DevOps/tree/main/code/snippets#getdockerhost - . /dev/stdin <<<"$(cat <(curl -s --raw https://raw.githubusercontent.com/bcgov/DITP-DevOps/main/code/snippets/getDockerHost))" + . /dev/stdin <<<"$(cat <(curl -s --raw https://raw.githubusercontent.com/bcgov/DITP-DevOps/main/code/snippets/getDockerHost))" export DOCKERHOST=$(getDockerHost) export RUNMODE="docker" else @@ -188,7 +190,7 @@ if [ -z "$AGENT_ENDPOINT" ] && [ "$RUNMODE" == "docker" ]; then fi fi -echo $DOCKERHOST +echo "DOCKERHOST=$DOCKERHOST" DOCKER_ENV="-e LOG_LEVEL=${LOG_LEVEL} -e RUNMODE=${RUNMODE} -e DOCKERHOST=${DOCKERHOST}" if ! [ -z "$AGENT_PORT" ]; then @@ -241,6 +243,8 @@ if ! [ -z "${ENABLE_PYDEVD_PYCHARM}" ]; then DOCKER_ENV="${DOCKER_ENV} -e ENABLE_PYDEVD_PYCHARM=${ENABLE_PYDEVD_PYCHARM} -e PYDEVD_PYCHARM_CONTROLLER_PORT=${PYDEVD_PYCHARM_CONTROLLER_PORT} -e PYDEVD_PYCHARM_AGENT_PORT=${PYDEVD_PYCHARM_AGENT_PORT}" fi +echo "DOCKER_ENV=$DOCKER_ENV" + # on Windows, docker run needs to be prefixed by winpty if [ "$OSTYPE" = "msys" ]; then DOCKER="winpty docker" diff --git a/demo/runners/agent_container.py b/demo/runners/agent_container.py index 922bcd51d2..bbcd11be40 100644 --- a/demo/runners/agent_container.py +++ b/demo/runners/agent_container.py @@ -23,11 +23,8 @@ connect_wallet_to_endorser, CRED_FORMAT_INDY, CRED_FORMAT_JSON_LD, - DID_METHOD_SOV, DID_METHOD_KEY, - KEY_TYPE_ED255, KEY_TYPE_BLS, - SIG_TYPE_BLS, ) from runners.support.utils import ( # noqa:E402 check_requires, @@ -99,6 +96,10 @@ async def handle_oob_invitation(self, message): print("handle_oob_invitation()") pass + async def handle_out_of_band(self, message): + print("handle_out_of_band()") + pass + async def handle_connection_reuse(self, message): # we are reusing an existing connection, set our status to the existing connection if not self._connection_ready.done(): @@ -121,7 +122,7 @@ async def handle_connections(self, message): conn_id = message["connection_id"] # inviter: - if message["state"] == "invitation": + if message.get("state") == "invitation": self.connection_id = conn_id # invitee: @@ -158,7 +159,7 @@ async def handle_connections(self, message): ) async def handle_issue_credential(self, message): - state = message["state"] + state = message.get("state") credential_exchange_id = message["credential_exchange_id"] prev_state = self.cred_state.get(credential_exchange_id) if prev_state == state: @@ -221,8 +222,12 @@ async def handle_issue_credential(self, message): except ClientError: pass + elif state == "abandoned": + log_status("Credential exchange abandoned") + self.log("Problem report message:", message.get("error_msg")) + async def handle_issue_credential_v2_0(self, message): - state = message["state"] + state = message.get("state") cred_ex_id = message["cred_ex_id"] prev_state = self.cred_state.get(cred_ex_id) if prev_state == state: @@ -238,6 +243,7 @@ async def handle_issue_credential_v2_0(self, message): f"/issue-credential-2.0/records/{cred_ex_id}/issue", {"comment": f"Issuing credential, exchange {cred_ex_id}"}, ) + elif state == "offer-received": log_status("#15 After receiving credential offer, send credential request") if message["by_format"]["cred_offer"].get("indy"): @@ -253,10 +259,15 @@ async def handle_issue_credential_v2_0(self, message): await self.admin_POST( f"/issue-credential-2.0/records/{cred_ex_id}/send-request", data ) + elif state == "done": pass # Logic moved to detail record specific handler + elif state == "abandoned": + log_status("Credential exchange abandoned") + self.log("Problem report message:", message.get("error_msg")) + async def handle_issue_credential_v2_0_indy(self, message): rev_reg_id = message.get("rev_reg_id") cred_rev_id = message.get("cred_rev_id") @@ -284,7 +295,7 @@ async def handle_issuer_cred_rev(self, message): pass async def handle_present_proof(self, message): - state = message["state"] + state = message.get("state") presentation_exchange_id = message["presentation_exchange_id"] presentation_request = message["presentation_request"] @@ -321,14 +332,17 @@ async def handle_present_proof(self, message): if referent not in credentials_by_reft: credentials_by_reft[referent] = row + # submit the proof wit one unrevealed revealed attribute + revealed_flag = False for referent in presentation_request["requested_attributes"]: if referent in credentials_by_reft: revealed[referent] = { "cred_id": credentials_by_reft[referent]["cred_info"][ "referent" ], - "revealed": True, + "revealed": revealed_flag, } + revealed_flag = True else: self_attested[referent] = "my self-attested value" @@ -366,8 +380,12 @@ async def handle_present_proof(self, message): ) self.log("Proof =", proof["verified"]) + elif state == "abandoned": + log_status("Presentation exchange abandoned") + self.log("Problem report message:", message.get("error_msg")) + async def handle_present_proof_v2_0(self, message): - state = message["state"] + state = message.get("state") pres_ex_id = message["pres_ex_id"] self.log(f"Presentation: state = {state}, pres_ex_id = {pres_ex_id}") @@ -378,6 +396,10 @@ async def handle_present_proof_v2_0(self, message): ) pres_request_indy = message["by_format"].get("pres_request", {}).get("indy") pres_request_dif = message["by_format"].get("pres_request", {}).get("dif") + request = {} + + if not pres_request_dif and not pres_request_indy: + raise Exception("Invalid presentation request received") if pres_request_indy: # include self-attested attributes (not included in credentials) @@ -392,6 +414,8 @@ async def handle_present_proof_v2_0(self, message): f"/present-proof-2.0/records/{pres_ex_id}/credentials" ) if creds: + # select only indy credentials + creds = [x for x in creds if "cred_info" in x] if "timestamp" in creds[0]["cred_info"]["attrs"]: sorted_creds = sorted( creds, @@ -405,14 +429,17 @@ async def handle_present_proof_v2_0(self, message): if referent not in creds_by_reft: creds_by_reft[referent] = row + # submit the proof wit one unrevealed revealed attribute + revealed_flag = False for referent in pres_request_indy["requested_attributes"]: if referent in creds_by_reft: revealed[referent] = { "cred_id": creds_by_reft[referent]["cred_info"][ "referent" ], - "revealed": True, + "revealed": revealed_flag, } + revealed_flag = True else: self_attested[referent] = "my self-attested value" @@ -424,46 +451,62 @@ async def handle_present_proof_v2_0(self, message): ] } - log_status("#25 Generate the proof") - request = { + log_status("#25 Generate the indy proof") + indy_request = { "indy": { "requested_predicates": predicates, "requested_attributes": revealed, "self_attested_attributes": self_attested, } } + request.update(indy_request) except ClientError: pass - elif pres_request_dif: + if pres_request_dif: try: # select credentials to provide for the proof creds = await self.admin_GET( f"/present-proof-2.0/records/{pres_ex_id}/credentials" ) if creds and 0 < len(creds): + # select only dif credentials + creds = [x for x in creds if "issuanceDate" in x] creds = sorted( creds, key=lambda c: c["issuanceDate"], reverse=True, ) - record_id = creds[0]["record_id"] + records = creds else: - record_id = None + records = [] - log_status("#25 Generate the proof") - request = { + log_status("#25 Generate the dif proof") + dif_request = { "dif": {}, } # specify the record id for each input_descriptor id: - request["dif"]["record_ids"] = {} + dif_request["dif"]["record_ids"] = {} for input_descriptor in pres_request_dif["presentation_definition"][ "input_descriptors" ]: - request["dif"]["record_ids"][input_descriptor["id"]] = [ - record_id, - ] - log_msg("presenting ld-presentation:", request) + input_descriptor_schema_uri = [] + for element in input_descriptor["schema"]: + input_descriptor_schema_uri.append(element["uri"]) + + for record in records: + if self.check_input_descriptor_record_id( + input_descriptor_schema_uri, record + ): + record_id = record["record_id"] + dif_request["dif"]["record_ids"][ + input_descriptor["id"] + ] = [ + record_id, + ] + break + log_msg("presenting ld-presentation:", dif_request) + request.update(dif_request) # NOTE that the holder/prover can also/or specify constraints by including the whole proof request # and constraining the presented credentials by adding filters, for example: @@ -488,9 +531,6 @@ async def handle_present_proof_v2_0(self, message): except ClientError: pass - else: - raise Exception("Invalid presentation request received") - log_status("#26 Send the proof to X: " + json.dumps(request)) await self.admin_POST( f"/present-proof-2.0/records/{pres_ex_id}/send-presentation", @@ -507,11 +547,15 @@ async def handle_present_proof_v2_0(self, message): self.log("Proof =", proof["verified"]) self.last_proof_received = proof + elif state == "abandoned": + log_status("Presentation exchange abandoned") + self.log("Problem report message:", message.get("error_msg")) + async def handle_basicmessages(self, message): self.log("Received message:", message["content"]) async def handle_endorse_transaction(self, message): - self.log("Received transaction message:", message["state"]) + self.log("Received transaction message:", message.get("state")) async def handle_revocation_notification(self, message): self.log("Received revocation notification message:", message) @@ -578,7 +622,10 @@ async def create_schema_and_cred_def( random.randint(1, 101), ) ) - (_, cred_def_id,) = await self.register_schema_and_creddef( # schema id + ( + _, + cred_def_id, + ) = await self.register_schema_and_creddef( # schema id schema_name, version, schema_attrs, @@ -587,12 +634,26 @@ async def create_schema_and_cred_def( ) return cred_def_id + def check_input_descriptor_record_id( + self, input_descriptor_schema_uri, record + ) -> bool: + result = False + for uri in input_descriptor_schema_uri: + for record_type in record["type"]: + if record_type in uri: + result = True + break + result = False + + return result + class AgentContainer: def __init__( self, ident: str, start_port: int, + prefix: str = None, no_auto: bool = False, revocation: bool = False, genesis_txns: str = None, @@ -610,12 +671,14 @@ def __init__( arg_file: str = None, endorser_role: str = None, reuse_connections: bool = False, + taa_accept: bool = False, ): # configuration parameters self.genesis_txns = genesis_txns self.genesis_txn_list = genesis_txn_list self.ident = ident self.start_port = start_port + self.prefix = prefix or ident self.no_auto = no_auto self.revocation = revocation self.tails_server_base_url = tails_server_base_url @@ -629,6 +692,7 @@ def __init__( self.seed = seed self.aip = aip self.arg_file = arg_file + self.endorser_agent = None self.endorser_role = endorser_role if endorser_role: # endorsers and authors need public DIDs (assume cred_type is Indy) @@ -642,6 +706,7 @@ def __init__( # local agent(s) self.agent = None self.mediator_agent = None + self.taa_accept = taa_accept async def initialize( self, @@ -661,6 +726,7 @@ async def initialize( self.ident, self.start_port, self.start_port + 1, + prefix=self.prefix, genesis_data=self.genesis_txns, genesis_txn_list=self.genesis_txn_list, no_auto=self.no_auto, @@ -680,9 +746,11 @@ async def initialize( await self.agent.listen_webhooks(self.start_port + 2) - if self.public_did and self.cred_type != CRED_FORMAT_JSON_LD: - await self.agent.register_did(cred_type=CRED_FORMAT_INDY) - log_msg("Created public DID") + # create public DID ... UNLESS we are an author ... + if (not self.endorser_role) or (self.endorser_role == "endorser"): + if self.public_did and self.cred_type != CRED_FORMAT_JSON_LD: + await self.agent.register_did(cred_type=CRED_FORMAT_INDY) + log_msg("Created public DID") # if we are endorsing, create the endorser agent first, then we can use the # multi-use invitation to auto-connect the agent on startup @@ -724,15 +792,46 @@ async def initialize( rand_name = str(random.randint(100_000, 999_999)) await self.agent.register_or_switch_wallet( self.ident + ".initial." + rand_name, - public_did=self.public_did, + public_did=self.public_did + and ((not self.endorser_role) or (not self.endorser_role == "author")), webhook_port=None, mediator_agent=self.mediator_agent, endorser_agent=self.endorser_agent, + taa_accept=self.taa_accept, ) - elif self.mediation: - # we need to pre-connect the agent to its mediator - if not await connect_wallet_to_mediator(self.agent, self.mediator_agent): - raise Exception("Mediation setup FAILED :-(") + else: + if self.mediation: + # we need to pre-connect the agent to its mediator + self.agent.log("Connect wallet to mediator ...") + if not await connect_wallet_to_mediator( + self.agent, self.mediator_agent + ): + raise Exception("Mediation setup FAILED :-(") + if self.endorser_agent: + self.agent.log("Connect wallet to endorser ...") + if not await connect_wallet_to_endorser( + self.agent, self.endorser_agent + ): + raise Exception("Endorser setup FAILED :-(") + if self.taa_accept: + await self.agent.taa_accept() + + # if we are an author, create our public DID here ... + if ( + self.endorser_role + and self.endorser_role == "author" + and self.endorser_agent + ): + if self.public_did and self.cred_type != CRED_FORMAT_JSON_LD: + new_did = await self.agent.admin_POST("/wallet/did/create") + self.agent.did = new_did["result"]["did"] + await self.agent.register_did( + did=new_did["result"]["did"], + verkey=new_did["result"]["verkey"], + ) + await self.agent.admin_POST("/wallet/did/public?did=" + self.agent.did) + await asyncio.sleep(3.0) + log_msg("Created public DID") if self.public_did and self.cred_type == CRED_FORMAT_JSON_LD: # create did of appropriate type @@ -832,7 +931,7 @@ async def receive_credential( return matched - async def request_proof(self, proof_request): + async def request_proof(self, proof_request, explicit_revoc_required: bool = False): log_status("#20 Request proof of degree from alice") if self.cred_type == CRED_FORMAT_INDY: @@ -848,7 +947,42 @@ async def request_proof(self, proof_request): } if self.revocation: - indy_proof_request["non_revoked"] = {"to": int(time.time())} + non_revoked_supplied = False + # plug in revocation where requested in the supplied proof request + non_revoked = {"to": int(time.time())} + if "non_revoked" in proof_request: + indy_proof_request["non_revoked"] = non_revoked + non_revoked_supplied = True + for attr in proof_request["requested_attributes"]: + if "non_revoked" in proof_request["requested_attributes"][attr]: + indy_proof_request["requested_attributes"][attr][ + "non_revoked" + ] = non_revoked + non_revoked_supplied = True + for pred in proof_request["requested_predicates"]: + if "non_revoked" in proof_request["requested_predicates"][pred]: + indy_proof_request["requested_predicates"][pred][ + "non_revoked" + ] = non_revoked + non_revoked_supplied = True + + if not non_revoked_supplied and not explicit_revoc_required: + # else just make it global + indy_proof_request["non_revoked"] = non_revoked + + else: + # make sure we are not leaking non-revoc requests + if "non_revoked" in proof_request: + del proof_request["non_revoked"] + for attr in proof_request["requested_attributes"]: + if "non_revoked" in proof_request["requested_attributes"][attr]: + del proof_request["requested_attributes"][attr]["non_revoked"] + for pred in proof_request["requested_predicates"]: + if "non_revoked" in proof_request["requested_predicates"][pred]: + del proof_request["requested_predicates"][pred]["non_revoked"] + + log_status(f" >>> asking for proof for request: {indy_proof_request}") + proof_request_web_request = { "connection_id": self.agent.connection_id, "presentation_request": { @@ -879,6 +1013,8 @@ async def verify_proof(self, proof_request): print("No proof received") return None + # log_status(f">>> last proof received: {self.agent.last_proof_received}") + if self.cred_type == CRED_FORMAT_INDY: # return verified status return self.agent.last_proof_received["verified"] @@ -1126,6 +1262,11 @@ def arg_parser(ident: str = None, port: int = 8020): metavar="", help="Specify a file containing additional aca-py parameters", ) + parser.add_argument( + "--taa-accept", + action="store_true", + help="Accept the ledger's TAA, if required", + ) return parser @@ -1170,9 +1311,11 @@ async def create_agent_with_args(args, ident: str = None): ) multi_ledger_config_path = None + genesis = None if "multi_ledger" in args and args.multi_ledger: multi_ledger_config_path = "./demo/multi_ledger_config.yml" - genesis = await default_genesis_txns() + else: + genesis = await default_genesis_txns() if not genesis and not multi_ledger_config_path: print("Error retrieving ledger genesis transactions") sys.exit(1) @@ -1226,6 +1369,7 @@ async def create_agent_with_args(args, ident: str = None): aip=aip, endorser_role=args.endorser_role, reuse_connections=reuse_connections, + taa_accept=args.taa_accept, ) return agent diff --git a/demo/runners/alice.py b/demo/runners/alice.py index 3a63177c14..8bea579d29 100644 --- a/demo/runners/alice.py +++ b/demo/runners/alice.py @@ -169,11 +169,13 @@ async def main(args): target_wallet_name, webhook_port=alice_agent.agent.get_new_webhook_port(), mediator_agent=alice_agent.mediator_agent, + taa_accept=alice_agent.taa_accept, ) else: await alice_agent.agent.register_or_switch_wallet( target_wallet_name, mediator_agent=alice_agent.mediator_agent, + taa_accept=alice_agent.taa_accept, ) elif option == "3": diff --git a/demo/runners/faber.py b/demo/runners/faber.py index d7e7ce6ab5..8f89a23384 100644 --- a/demo/runners/faber.py +++ b/demo/runners/faber.py @@ -476,6 +476,7 @@ async def main(args): public_did=True, mediator_agent=faber_agent.mediator_agent, endorser_agent=faber_agent.endorser_agent, + taa_accept=faber_agent.taa_accept, ) else: created = await faber_agent.agent.register_or_switch_wallet( @@ -484,6 +485,7 @@ async def main(args): mediator_agent=faber_agent.mediator_agent, endorser_agent=faber_agent.endorser_agent, cred_type=faber_agent.cred_type, + taa_accept=faber_agent.taa_accept, ) # create a schema and cred def for the new wallet # TODO check first in case we are switching between existing wallets @@ -608,14 +610,15 @@ async def main(args): ) pres_req_id = proof_request["presentation_exchange_id"] url = ( - "http://" - + os.getenv("DOCKERHOST").replace( - "{PORT}", str(faber_agent.agent.admin_port + 1) + os.getenv("WEBHOOK_TARGET") + or ( + "http://" + + os.getenv("DOCKERHOST").replace( + "{PORT}", str(faber_agent.agent.admin_port + 1) + ) + + "/webhooks" ) - + "/webhooks/pres_req/" - + pres_req_id - + "/" - ) + ) + f"/pres_req/{pres_req_id}/" log_msg(f"Proof request url: {url}") qr = QRCode(border=1) qr.add_data(url) diff --git a/demo/runners/performance.py b/demo/runners/performance.py index e90c176087..4c79bf6437 100644 --- a/demo/runners/performance.py +++ b/demo/runners/performance.py @@ -73,6 +73,11 @@ async def handle_connections(self, payload): self.log("Connected") self._connection_ready.set_result(True) + async def handle_issue_credential(self, payload): + cred_ex_id = payload["credential_exchange_id"] + self.credential_state[cred_ex_id] = payload["state"] + self.credential_event.set() + async def handle_issue_credential_v2_0(self, payload): cred_ex_id = payload["cred_ex_id"] self.credential_state[cred_ex_id] = payload["state"] @@ -103,7 +108,15 @@ async def check_received_creds(self) -> Tuple[int, int]: pending = 0 total = len(self.credential_state) for result in self.credential_state.values(): - if result != "done": + # Since cred_ex_record is set to be auto-removed + # the state in self.credential_state for completed + # exchanges will be deleted. Any problematic + # exchanges will be in abandoned state. + if ( + result != "done" + and result != "deleted" + and result != "credential_acked" + ): pending += 1 if self.credential_event.is_set(): continue @@ -266,9 +279,10 @@ async def main( revocation: bool = False, tails_server_base_url: str = None, issue_count: int = 300, + batch_size: int = 30, wallet_type: str = None, + arg_file: str = None, ): - if multi_ledger: genesis = None multi_ledger_config_path = "./demo/multi_ledger_config.yml" @@ -295,6 +309,7 @@ async def main( multitenant=multitenant, mediation=mediation, wallet_type=wallet_type, + arg_file=arg_file, ) await alice.listen_webhooks(start_port + 2) @@ -307,6 +322,7 @@ async def main( multitenant=multitenant, mediation=mediation, wallet_type=wallet_type, + arg_file=arg_file, ) await faber.listen_webhooks(start_port + 5) await faber.register_did() @@ -371,8 +387,6 @@ async def main( await alice_mediator_agent.reset_timing() await faber_mediator_agent.reset_timing() - batch_size = 100 - semaphore = asyncio.Semaphore(threads) def done_propose(fut: asyncio.Task): @@ -416,7 +430,7 @@ async def check_received_creds(agent, issue_count, pb): pending, total = await agent.check_received_creds() complete = total - pending if reported == complete: - await asyncio.wait_for(agent.update_creds(), 30) + await asyncio.wait_for(agent.update_creds(), 45) continue if iter_pb and complete > reported: try: @@ -591,6 +605,13 @@ async def check_received_pings(agent, issue_count, pb): default=300, help="Set the number of credentials to issue", ) + parser.add_argument( + "-b", + "--batch", + type=int, + default=100, + help="Set the batch size of credentials to issue", + ) parser.add_argument( "-p", "--port", @@ -655,6 +676,12 @@ async def check_received_pings(agent, issue_count, pb): metavar="", help="Set the agent wallet type", ) + parser.add_argument( + "--arg-file", + type=str, + metavar="", + help="Specify a file containing additional aca-py parameters", + ) args = parser.parse_args() if args.did_exchange and args.mediation: @@ -690,7 +717,9 @@ async def check_received_pings(agent, issue_count, pb): args.revocation, tails_server_base_url, args.count, + args.batch, args.wallet_type, + args.arg_file, ) ) except KeyboardInterrupt: diff --git a/demo/runners/support/agent.py b/demo/runners/support/agent.py index b8f7c33ffd..06d1765eb0 100644 --- a/demo/runners/support/agent.py +++ b/demo/runners/support/agent.py @@ -186,7 +186,9 @@ def __init__( self.proc = None self.client_session: ClientSession = ClientSession() - if self.endorser_role and not seed: + if self.endorser_role and self.endorser_role == "author": + seed = None + elif self.endorser_role and not seed: seed = "random" rand_name = str(random.randint(100_000, 999_999)) self.seed = ( @@ -195,7 +197,7 @@ def __init__( else seed ) self.storage_type = params.get("storage_type") - self.wallet_type = params.get("wallet_type") or "indy" + self.wallet_type = params.get("wallet_type") or "askar" self.wallet_name = ( params.get("wallet_name") or self.ident.lower().replace(" ", "") + rand_name ) @@ -211,6 +213,7 @@ def __init__( self.agency_wallet_did = self.did self.agency_wallet_key = self.wallet_key + self.multi_write_ledger_url = None if self.genesis_txn_list: updated_config_list = [] with open(self.genesis_txn_list, "r") as stream: @@ -223,6 +226,10 @@ def __init__( "$LEDGER_HOST", str(self.external_host) ) updated_config_list.append(config) + if "is_write" in config and config["is_write"]: + self.multi_write_ledger_url = config["genesis_url"].replace( + "/genesis", "" + ) with open(self.genesis_txn_list, "w") as file: documents = yaml.dump(updated_config_list, file) @@ -334,6 +341,7 @@ def get_agent_args(self): "--preserve-exchange-records", "--auto-provision", "--public-invites", + # ("--log-level", "debug"), ] if self.aip == 20: result.append("--emit-new-didcomm-prefix") @@ -426,6 +434,7 @@ def get_agent_args(self): ("--auto-request-endorsement",), ("--auto-write-transactions",), ("--auto-create-revocation-transactions",), + ("--auto-promote-author-did"), ("--endorser-alias", "endorser"), ] ) @@ -476,7 +485,10 @@ async def register_did( # if registering a did for issuing indy credentials, publish the did on the ledger self.log(f"Registering {self.ident} ...") if not ledger_url: - ledger_url = LEDGER_URL + if self.multi_write_ledger_url: + ledger_url = self.multi_write_ledger_url + else: + ledger_url = LEDGER_URL if not ledger_url: ledger_url = f"http://{self.external_host}:9000" data = {"alias": alias or self.ident} @@ -484,26 +496,34 @@ async def register_did( if self.endorser_role == "endorser": role = "ENDORSER" else: - role = "" - data["role"] = role + role = None + if role: + data["role"] = role if did and verkey: data["did"] = did data["verkey"] = verkey else: data["seed"] = self.seed - async with self.client_session.post( - ledger_url + "/register", json=data - ) as resp: + if role is None or role == "": + # if author using endorser register nym and wait for endorser ... + resp = await self.admin_POST("/ledger/register-nym", params=data) + await asyncio.sleep(3.0) + nym_info = data + else: + log_msg("using ledger: " + ledger_url + "/register") + resp = await self.client_session.post( + ledger_url + "/register", json=data + ) if resp.status != 200: raise Exception( f"Error registering DID {data}, response code {resp.status}" ) nym_info = await resp.json() - self.did = nym_info["did"] - self.log(f"nym_info: {nym_info}") - if self.multitenant: - if not self.agency_wallet_did: - self.agency_wallet_did = self.did + self.did = nym_info["did"] + self.log(f"nym_info: {nym_info}") + if self.multitenant: + if not self.agency_wallet_did: + self.agency_wallet_did = self.did self.log(f"Registered DID: {self.did}") elif cred_type == CRED_FORMAT_JSON_LD: # TODO register a did:key with appropriate signature type @@ -519,6 +539,7 @@ async def register_or_switch_wallet( mediator_agent=None, cred_type: str = CRED_FORMAT_INDY, endorser_agent=None, + taa_accept=False, ): if webhook_port is not None: await self.listen_webhooks(webhook_port) @@ -534,6 +555,9 @@ async def register_or_switch_wallet( self.managed_wallet_params["wallet_id"] = wallet_params["id"] self.managed_wallet_params["token"] = wallet_params["token"] + if taa_accept: + await self.taa_accept() + self.log(f"Switching to AGENCY wallet {target_wallet_name}") return False @@ -553,6 +577,9 @@ async def register_or_switch_wallet( self.managed_wallet_params["wallet_id"] = wallet_params["id"] self.managed_wallet_params["token"] = wallet_params["token"] + if taa_accept: + await self.taa_accept() + self.log(f"Switching to EXISTING wallet {target_wallet_name}") return False @@ -571,6 +598,20 @@ async def register_or_switch_wallet( new_wallet = await self.agency_admin_POST("/multitenancy/wallet", wallet_params) self.log("New wallet params:", new_wallet) self.managed_wallet_params = new_wallet + + # if endorser, endorse the wallet ledger operations + if endorser_agent: + self.log("Connect sub-wallet to endorser ...") + if not await connect_wallet_to_endorser(self, endorser_agent): + raise Exception("Endorser setup FAILED :-(") + # if mediation, mediate the wallet connections + if mediator_agent: + if not await connect_wallet_to_mediator(self, mediator_agent): + log_msg("Mediation setup FAILED :-(") + raise Exception("Mediation setup FAILED :-(") + if taa_accept: + await self.taa_accept() + if public_did: if cred_type == CRED_FORMAT_INDY: # assign public did @@ -580,7 +621,13 @@ async def register_or_switch_wallet( did=new_did["result"]["did"], verkey=new_did["result"]["verkey"], ) - await self.admin_POST("/wallet/did/public?did=" + self.did) + if self.endorser_role and self.endorser_role == "author": + if endorser_agent: + await self.admin_POST("/wallet/did/public?did=" + self.did) + await asyncio.sleep(3.0) + else: + await self.admin_POST("/wallet/did/public?did=" + self.did) + await asyncio.sleep(3.0) elif cred_type == CRED_FORMAT_JSON_LD: # create did of appropriate type data = {"method": DID_METHOD_KEY, "options": {"key_type": KEY_TYPE_BLS}} @@ -592,17 +639,6 @@ async def register_or_switch_wallet( # todo ignore for now pass - # if mediation, mediate the wallet connections - if mediator_agent: - if not await connect_wallet_to_mediator(self, mediator_agent): - log_msg("Mediation setup FAILED :-(") - raise Exception("Mediation setup FAILED :-(") - - # if endorser, endorse the wallet ledger operations - if endorser_agent: - if not await connect_wallet_to_endorser(self, endorser_agent): - raise Exception("Endorser setup FAILED :-(") - self.log(f"Created NEW wallet {target_wallet_name}") return True @@ -627,7 +663,18 @@ def handle_output(self, *output, source: str = None, **kwargs): color = self.color or "fg:ansiblue" else: color = None - log_msg(*output, color=color, prefix=self.prefix_str, end=end, **kwargs) + try: + log_msg(*output, color=color, prefix=self.prefix_str, end=end, **kwargs) + except AssertionError as e: + if self.trace_enabled and self.trace_target == "log": + # when tracing to a log file, + # we hit an issue with the underlying prompt_toolkit. + # it attempts to output what is written by the log and can't find the + # correct terminal and throws an error. The trace log record does show + # in the terminal, so let's just ignore this error. + pass + else: + raise e def log(self, *msg, **kwargs): self.handle_output(*msg, **kwargs) @@ -689,7 +736,7 @@ def _terminate(self): if self.proc and self.proc.poll() is None: self.proc.terminate() try: - self.proc.wait(timeout=0.5) + self.proc.wait(timeout=1.5) self.log(f"Exited with return code {self.proc.returncode}") except subprocess.TimeoutExpired: msg = "Process did not terminate in time" @@ -714,7 +761,7 @@ async def listen_webhooks(self, webhook_port): if RUN_MODE == "pwd": self.webhook_url = f"http://localhost:{str(webhook_port)}/webhooks" else: - self.webhook_url = ( + self.webhook_url = self.external_webhook_target or ( f"http://{self.external_host}:{str(webhook_port)}/webhooks" ) app = web.Application() @@ -763,6 +810,8 @@ async def _send_connectionless_proof_req(self, request: ClientRequest): return web.Response(status=404) proof_reg_txn = proof_exch["presentation_request_dict"] proof_reg_txn["~service"] = await self.service_decorator() + if request.headers.get("Accept") == "application/json": + return web.json_response(proof_reg_txn) objJsonStr = json.dumps(proof_reg_txn) objJsonB64 = base64.b64encode(objJsonStr.encode("ascii")) service_url = self.webhook_url @@ -804,6 +853,27 @@ async def handle_revocation_registry(self, message): reg_id = message.get("revoc_reg_id", "(undetermined)") self.log(f"Revocation registry: {reg_id} state: {message['state']}") + async def handle_mediation(self, message): + self.log(f"Received mediation message ...\n") + + async def handle_keylist(self, message): + self.log(f"Received handle_keylist message ...\n") + self.log(json.dumps(message)) + + async def taa_accept(self): + taa_info = await self.admin_GET("/ledger/taa") + if taa_info["result"]["taa_required"]: + taa_accept = { + "mechanism": list(taa_info["result"]["aml_record"]["aml"].keys())[0], + "version": taa_info["result"]["taa_record"]["version"], + "text": taa_info["result"]["taa_record"]["text"], + } + self.log(f"Accepting TAA with: {taa_accept}") + await self.admin_POST( + "/ledger/taa/accept", + data=taa_accept, + ) + async def admin_request( self, method, path, data=None, text=False, params=None, headers=None ) -> ClientResponse: @@ -1161,6 +1231,8 @@ async def get_invite( "handshake_protocols": ["rfc23"], "use_public_did": reuse_connections, } + if self.mediation: + payload["mediation_id"] = self.mediator_request_id invi_rec = await self.admin_POST( "/out-of-band/create-invitation", payload, @@ -1171,9 +1243,10 @@ async def get_invite( invi_params = { "auto_accept": json.dumps(auto_accept), } + payload = {"mediation_id": self.mediator_request_id} invi_rec = await self.admin_POST( "/connections/create-invitation", - {"mediation_id": self.mediator_request_id}, + payload, params=invi_params, ) else: @@ -1186,6 +1259,8 @@ async def receive_invite(self, invite, auto_accept: bool = True): params = {"alias": "endorser"} else: params = {} + if self.mediation: + params["mediation_id"] = self.mediator_request_id if "/out-of-band/" in invite.get("@type", ""): # always reuse connections if possible params["use_existing_connection"] = "true" @@ -1283,12 +1358,12 @@ async def connect_wallet_to_mediator(agent, mediator_agent): log_msg("Connected agent to mediator:", agent.ident, mediator_agent.ident) # setup mediation on our connection - log_msg("Request mediation ...") + log_msg(f"Request mediation on connection {agent.mediator_connection_id} ...") mediation_request = await agent.admin_POST( "/mediation/request/" + agent.mediator_connection_id, {} ) agent.mediator_request_id = mediation_request["mediation_id"] - log_msg("Mediation request id:", agent.mediator_request_id) + log_msg(f"Mediation request id: {agent.mediator_request_id}") count = 3 while 0 < count: @@ -1340,7 +1415,8 @@ async def handle_connections(self, message): # author responds to a multi-use invitation if message["state"] == "request": self.endorser_connection_id = message["connection_id"] - self._connection_ready = asyncio.Future() + if not self._connection_ready: + self._connection_ready = asyncio.Future() # finish off the connection if message["connection_id"] == self.endorser_connection_id: @@ -1360,6 +1436,9 @@ async def handle_connections(self, message): async def handle_basicmessages(self, message): self.log("Received message:", message["content"]) + async def handle_out_of_band(self, message): + self.log("Received message:", message) + async def start_endorser_agent( start_port, @@ -1417,10 +1496,10 @@ async def connect_wallet_to_endorser(agent, endorser_agent): # Generate an invitation log_msg("Generate endorser invite ...") endorser_agent._connection_ready = asyncio.Future() - endorser_connection = await endorser_agent.admin_POST( - "/connections/create-invitation" + endorser_agent.endorser_connection_id = None + endorser_connection = await endorser_agent.get_invite( + use_did_exchange=endorser_agent.use_did_exchange, ) - endorser_agent.endorser_connection_id = endorser_connection["connection_id"] # accept the invitation log_msg("Accept endorser invite ...") diff --git a/demo/runners/support/utils.py b/demo/runners/support/utils.py index 5f908a883e..0056686fbf 100644 --- a/demo/runners/support/utils.py +++ b/demo/runners/support/utils.py @@ -113,7 +113,12 @@ def output_reader(handle, callback, *args, **kwargs): for line in iter(handle.readline, b""): if not line: break - run_in_terminal(functools.partial(callback, line, *args)) + try: + run_in_terminal(functools.partial(callback, line, *args)) + except AssertionError as e: + # see comment in DemoAgent.handle_output + # trace log and prompt_toolkit do not get along... + pass def log_msg(*msg, color="fg:ansimagenta", **kwargs): @@ -235,7 +240,7 @@ def progress(*args, **kwargs): def check_requires(args): - wtype = args.wallet_type or "indy" + wtype = args.wallet_type or "askar" if wtype == "indy": try: diff --git a/deployment/nginx.conf b/deployment/nginx.conf new file mode 100644 index 0000000000..8efb796f02 --- /dev/null +++ b/deployment/nginx.conf @@ -0,0 +1,24 @@ +events {} + +http{ + server { + listen 5000; + + # Notice the additional / at the end of the proxy_pass directive. + # NGINX will strip the matched prefix /cloudRunAdmin and pass the remainder + # to the backend server at the URI /. + # Therefore, http://myserver:80/cloudRunAdmin/api will + # post to the backend at http://localhost:11021/api + location / { + proxy_pass http://127.0.0.1:11021/; + } + + location /agent { + proxy_pass http://127.0.0.1:11020/; + client_max_body_size 0; + } + } +} + +daemon off; +pid /run/nginx.pid; \ No newline at end of file diff --git a/deployment/start.sh b/deployment/start.sh new file mode 100644 index 0000000000..930b2a0c00 --- /dev/null +++ b/deployment/start.sh @@ -0,0 +1,43 @@ +#!/usr/bin/env bash +set -e + +nginx -c "/etc/nginx/conf.d/default.conf" & + +aca-py start --inbound-transport http 0.0.0.0 11020 --outbound-transport http --admin 0.0.0.0 11021 & + +ping(){ + url="http://localhost:11021/status/ready" + local resp=$(curl -s --write-out '%{http_code}' --output /dev/null ${url}) + if [ $resp -eq 200 ]; then + return 0 + else + return 1 + fi +} + +wait_for_cloud_agent(){ + COUNT=${WAIT_SECOND:-60} # seconds + printf "waiting for cloud agent" + while ! ping ; do + printf "." + if [ $COUNT -eq 0 ];then + echo "\nThe Cloud agent failed to start within ${duration} seconds.\n" + exit 1 + fi + ((COUNT=COUNT-1)) + sleep 1 + done + printf "\n" +} + +healthcheck(){ + while ping ; do + sleep ${HEALTH_CHECK_PERIOD_SECOND:-300} # second + done + echo "\nAca-py is down" + exit 1 +} + +wait_for_cloud_agent + +healthcheck \ No newline at end of file diff --git a/docker/Dockerfile b/docker/Dockerfile new file mode 100644 index 0000000000..f0fe9c506f --- /dev/null +++ b/docker/Dockerfile @@ -0,0 +1,104 @@ +ARG python_version=3.9.16 +FROM python:${python_version}-slim-bullseye AS build + +WORKDIR /src + +ADD . . + +RUN pip install setuptools wheel +RUN python setup.py sdist bdist_wheel + +FROM python:${python_version}-slim-bullseye AS main + +ARG uid=1001 +ARG user=aries +ARG acapy_version +ARG acapy_reqs=[askar,bbs] + +ENV HOME="/home/$user" \ + APP_ROOT="$HOME" \ + LC_ALL=C.UTF-8 \ + LANG=C.UTF-8 \ + PIP_NO_CACHE_DIR=off \ + PYTHONUNBUFFERED=1 \ + PYTHONIOENCODING=UTF-8 \ + RUST_LOG=warning \ + SHELL=/bin/bash \ + SUMMARY="aries-cloudagent image" \ + DESCRIPTION="aries-cloudagent provides a base image for running Hyperledger Aries agents in Docker. \ + This image layers the python implementation of aries-cloudagent $acapy_version. Based on Debian Buster." + +LABEL summary="$SUMMARY" \ + description="$DESCRIPTION" \ + io.k8s.description="$DESCRIPTION" \ + io.k8s.display-name="aries-cloudagent $acapy_version" \ + name="aries-cloudagent" \ + acapy.version="$acapy_version" \ + maintainer="" + +# Add aries user +RUN useradd -U -ms /bin/bash -u $uid $user + +# Install environment +RUN apt-get update -y && \ + apt-get install -y --no-install-recommends \ + apt-transport-https \ + ca-certificates \ + build-essential \ + bzip2 \ + curl \ + git \ + less \ + libffi-dev \ + libgmp10 \ + liblzma5 \ + libncurses5 \ + libncursesw5 \ + libsecp256k1-0 \ + libzmq5 \ + net-tools \ + openssl \ + sqlite3 \ + vim-tiny \ + zlib1g && \ + rm -rf /var/lib/apt/lists/* /usr/share/doc/* + +WORKDIR $HOME + +# Add local binaries and aliases to path +ENV PATH="$HOME/.local/bin:$PATH" + +# - In order to drop the root user, we have to make some directories writable +# to the root group as OpenShift default security model is to run the container +# under random UID. +RUN usermod -a -G 0 $user + +# Create standard directories to allow volume mounting and set permissions +# Note: PIP_NO_CACHE_DIR environment variable should be cleared to allow caching +RUN mkdir -p \ + $HOME/.aries_cloudagent \ + $HOME/.cache/pip/http \ + $HOME/ledger/sandbox/data \ + $HOME/log + +# The root group needs access the directories under $HOME/.aries_cloudagent for the container to function in OpenShift. +RUN chown -R $user:root $HOME/.aries_cloudagent && \ + chmod -R ug+rw $HOME/log $HOME/ledger $HOME/.aries_cloudagent $HOME/.cache + +# Install ACA-py from the wheel as $user, +# and ensure the permissions on the python 'site-packages' and $HOME/.local folders are set correctly. +USER $user +COPY --from=build /src/dist/aries_cloudagent*.whl . +RUN aries_cloudagent_package=$(find ./ -name "aries_cloudagent*.whl" | head -n 1) && \ + echo "Installing ${aries_cloudagent_package} ..." && \ + pip install --no-cache-dir --find-links=. ${aries_cloudagent_package}${acapy_reqs} && \ + rm aries_cloudagent*.whl && \ + chmod +rx $(python -m site --user-site) $HOME/.local + +# Clean-up unneccessary build dependencies and reduce final image size +USER root +RUN apt-get purge -y --auto-remove build-essential + +USER $user + +ENTRYPOINT ["aca-py"] diff --git a/docker/Dockerfile.bdd b/docker/Dockerfile.bdd index cacf38b6aa..d2e6c2098a 100644 --- a/docker/Dockerfile.bdd +++ b/docker/Dockerfile.bdd @@ -4,5 +4,6 @@ FROM faber-alice-demo RUN pip3 install --no-cache-dir -r demo/requirements.behave.txt WORKDIR ./demo +ADD demo/multi_ledger_config_bdd.yml ./demo/multi_ledger_config.yml RUN chmod a+w . ENTRYPOINT ["behave"] diff --git a/docker/Dockerfile.demo b/docker/Dockerfile.demo index 89dcba682d..cd15363bf0 100644 --- a/docker/Dockerfile.demo +++ b/docker/Dockerfile.demo @@ -3,6 +3,7 @@ FROM bcgovimages/von-image:py36-1.15-1 ENV ENABLE_PTVSD 0 ENV ENABLE_PYDEVD_PYCHARM 0 ENV PYDEVD_PYCHARM_HOST "host.docker.internal" +ENV ACAPY_DEBUG_WEBHOOKS 1 RUN mkdir bin && curl -L -o bin/jq \ https://github.com/stedolan/jq/releases/download/jq-1.6/jq-linux64 && \ diff --git a/docker/Dockerfile.indy b/docker/Dockerfile.indy new file mode 100644 index 0000000000..edd7200b8a --- /dev/null +++ b/docker/Dockerfile.indy @@ -0,0 +1,263 @@ +ARG python_version=3.9.16 +ARG rust_version=1.46 + +# This image could be replaced with an "indy" image from another repo, +# such as the indy-sdk +FROM rust:${rust_version}-slim as indy-builder + +ARG user=indy +ENV HOME="/home/$user" +WORKDIR $HOME +RUN mkdir -p .local/bin .local/etc .local/lib + +# Install environment +RUN apt-get update -y && \ + apt-get install -y --no-install-recommends \ + automake \ + build-essential \ + ca-certificates \ + cmake \ + curl \ + git \ + libbz2-dev \ + libffi-dev \ + libgmp-dev \ + liblzma-dev \ + libncurses5-dev \ + libncursesw5-dev \ + libsecp256k1-dev \ + libsodium-dev \ + libsqlite3-dev \ + libssl-dev \ + libtool \ + libzmq3-dev \ + pkg-config \ + zlib1g-dev && \ + rm -rf /var/lib/apt/lists/* + +# set to --release for smaller, optimized library +ARG indy_build_flags=--release + +ARG indy_version=1.16.0 +ARG indy_sdk_url=https://codeload.github.com/hyperledger/indy-sdk/tar.gz/refs/tags/v${indy_version} + +# make local libs and binaries accessible +ENV PATH="$HOME/.local/bin:$PATH" +ENV LIBRARY_PATH="$HOME/.local/lib:$LIBRARY_PATH" + +# Download and extract indy-sdk +RUN mkdir indy-sdk && \ + curl "${indy_sdk_url}" | tar -xz -C indy-sdk + +# Build and install indy-sdk +WORKDIR $HOME/indy-sdk +RUN cd indy-sdk*/libindy && \ + cargo build ${indy_build_flags} && \ + cp target/*/libindy.so "$HOME/.local/lib" && \ + cargo clean + +# Package python3-indy +RUN tar czvf ../python3-indy.tgz -C indy-sdk*/wrappers/python . + +# grab the latest sdk code for the postgres plug-in +WORKDIR $HOME +ARG indy_postgres_url=${indy_sdk_url} +RUN mkdir indy-postgres && \ + curl "${indy_postgres_url}" | tar -xz -C indy-postgres + +# Build and install postgres_storage plugin +WORKDIR $HOME/indy-postgres +RUN cd indy-sdk*/experimental/plugins/postgres_storage && \ + cargo build ${indy_build_flags} && \ + cp target/*/libindystrgpostgres.so "$HOME/.local/lib" && \ + cargo clean + +# Clean up SDK +WORKDIR $HOME +RUN rm -rf indy-sdk indy-postgres + + +# Indy Base Image +# This image could be replaced with an "indy-python" image from another repo, +# such as the indy-sdk +FROM python:${python_version}-slim-bullseye as indy-base + +ARG uid=1001 +ARG user=indy +ARG indy_version + +ENV HOME="/home/$user" \ + APP_ROOT="$HOME" \ + LC_ALL=C.UTF-8 \ + LANG=C.UTF-8 \ + PIP_NO_CACHE_DIR=off \ + PYTHONUNBUFFERED=1 \ + PYTHONIOENCODING=UTF-8 \ + RUST_LOG=warning \ + SHELL=/bin/bash \ + SUMMARY="indy-python base image" \ + DESCRIPTION="aries-cloudagent provides a base image for running Hyperledger Aries agents in Docker. \ + This image provides all the necessary dependencies to use the indy-sdk in python. Based on Debian bullseye." + +LABEL summary="$SUMMARY" \ + description="$DESCRIPTION" \ + io.k8s.description="$DESCRIPTION" \ + io.k8s.display-name="indy-python $indy_version" \ + name="indy-python" \ + indy-sdk.version="$indy_version" \ + maintainer="" + +# Add indy user +RUN useradd -U -ms /bin/bash -u $uid $user + +# Install environment +RUN apt-get update -y && \ + apt-get install -y --no-install-recommends \ + apt-transport-https \ + ca-certificates \ + build-essential \ + bzip2 \ + curl \ + git \ + less \ + libffi-dev \ + libgmp10 \ + liblzma5 \ + libncurses5 \ + libncursesw5 \ + libsecp256k1-0 \ + libzmq5 \ + net-tools \ + openssl \ + sqlite3 \ + vim-tiny \ + zlib1g && \ + rm -rf /var/lib/apt/lists/* /usr/share/doc/* + +WORKDIR $HOME + +# Copy build results +COPY --from=indy-builder --chown=$user:$user $HOME . + +RUN mkdir -p $HOME/.local/bin + +# Add local binaries and aliases to path +ENV PATH="$HOME/.local/bin:$PATH" + +# Make libraries resolvable by python +ENV LD_LIBRARY_PATH="$HOME/.local/lib:$LD_LIBRARY_PATH" +RUN echo "$HOME/.local/lib" > /etc/ld.so.conf.d/local.conf && ldconfig + +# Install python3-indy +RUN pip install --no-cache-dir python3-indy.tgz && rm python3-indy.tgz + +# - In order to drop the root user, we have to make some directories writable +# to the root group as OpenShift default security model is to run the container +# under random UID. +RUN usermod -a -G 0 $user + +# Create standard directories to allow volume mounting and set permissions +# Note: PIP_NO_CACHE_DIR environment variable should be cleared to allow caching +RUN mkdir -p \ + $HOME/.aries_cloudagent \ + $HOME/.cache/pip/http \ + $HOME/.indy_client/wallet \ + $HOME/.indy_client/pool \ + $HOME/.indy_client/ledger-cache \ + $HOME/ledger/sandbox/data \ + $HOME/log + +# The root group needs access the directories under $HOME/.indy_client and $HOME/.aries_cloudagent for the container to function in OpenShift. +RUN chown -R $user:root $HOME/.indy_client $HOME/.aries_cloudagent && \ + chmod -R ug+rw $HOME/log $HOME/ledger $HOME/.aries_cloudagent $HOME/.cache $HOME/.indy_client + +USER $user + +CMD ["bash"] + + +# ACA-Py Test +# Used to run ACA-Py unit tests with Indy +FROM indy-base as acapy-test + +USER indy + +RUN mkdir src test-reports + +WORKDIR /home/indy/src + +RUN mkdir -p test-reports && chown -R indy:indy test-reports && chmod -R ug+rw test-reports + +ADD requirements*.txt ./ + +USER root +RUN pip3 install --no-cache-dir \ + -r requirements.txt \ + -r requirements.askar.txt \ + -r requirements.bbs.txt \ + -r requirements.dev.txt + +ADD --chown=indy:root . . +USER indy + +ENTRYPOINT ["/bin/bash", "-c", "pytest \"$@\"", "--"] + +# ACA-Py Builder +# Build ACA-Py wheel using setuptools +FROM python:${python_version}-slim-bullseye AS acapy-builder + +WORKDIR /src + +ADD . . + +RUN pip install setuptools wheel +RUN python setup.py sdist bdist_wheel + + +# ACA-Py Indy +# Install wheel from builder and commit final image +FROM indy-base AS main + +ARG uid=1001 +ARG user=indy +ARG acapy_version +ARG acapy_reqs=[askar,bbs] + +ENV HOME="/home/$user" \ + APP_ROOT="$HOME" \ + LC_ALL=C.UTF-8 \ + LANG=C.UTF-8 \ + PIP_NO_CACHE_DIR=off \ + PYTHONUNBUFFERED=1 \ + PYTHONIOENCODING=UTF-8 \ + RUST_LOG=warning \ + SHELL=/bin/bash \ + SUMMARY="aries-cloudagent image" \ + DESCRIPTION="aries-cloudagent provides a base image for running Hyperledger Aries agents in Docker. \ + This image layers the python implementation of aries-cloudagent $acapy_version. \ + This image includes indy-sdk and supporting libraries." + +LABEL summary="$SUMMARY" \ + description="$DESCRIPTION" \ + io.k8s.description="$DESCRIPTION" \ + io.k8s.display-name="aries-cloudagent $acapy_version" \ + name="aries-cloudagent" \ + acapy.version="$acapy_version" \ + maintainer="" + +# Install ACA-py from the wheel as $user, +# and ensure the permissions on the python 'site-packages' folder are set correctly. +COPY --from=acapy-builder /src/dist/aries_cloudagent*.whl . +RUN aries_cloudagent_package=$(find ./ -name "aries_cloudagent*.whl" | head -n 1) && \ + echo "Installing ${aries_cloudagent_package} ..." && \ + pip install --no-cache-dir --find-links=. ${aries_cloudagent_package}${acapy_reqs} && \ + rm aries_cloudagent*.whl && \ + chmod +rx $(python -m site --user-site) + +# Clean-up unneccessary build dependencies and reduce final image size +USER root +RUN apt-get purge -y --auto-remove build-essential + +USER $user + +ENTRYPOINT ["aca-py"] diff --git a/docker/Dockerfile.test b/docker/Dockerfile.test index e949e9274f..6a1b4df76a 100644 --- a/docker/Dockerfile.test +++ b/docker/Dockerfile.test @@ -1,11 +1,10 @@ -FROM python:3.6.13 +ARG python_version=3.6.13 +FROM python:${python_version}-slim-buster RUN apt-get update -y && \ apt-get install -y --no-install-recommends \ - python3 \ - python3-pip \ - python3-setuptools \ libsodium23 && \ + apt-get clean && \ rm -rf /var/lib/apt/lists/* WORKDIR /usr/src/app @@ -20,4 +19,4 @@ RUN pip3 install --no-cache-dir \ ADD . . -ENTRYPOINT ["/bin/bash", "-c", "pytest \"$@\"", "--"] \ No newline at end of file +ENTRYPOINT ["/bin/bash", "-c", "pytest \"$@\"", "--"] diff --git a/docker/Dockerfile.test-indy b/docker/Dockerfile.test-indy deleted file mode 100644 index 047b19187e..0000000000 --- a/docker/Dockerfile.test-indy +++ /dev/null @@ -1,21 +0,0 @@ -FROM bcgovimages/von-image:py36-1.15-1 - -USER indy - -RUN mkdir src test-reports - -WORKDIR /home/indy/src - -RUN mkdir -p test-reports && chown -R indy:indy test-reports && chmod -R ug+rw test-reports - -ADD requirements*.txt ./ - -RUN pip3 install --no-cache-dir \ - -r requirements.txt \ - -r requirements.askar.txt \ - -r requirements.bbs.txt \ - -r requirements.dev.txt - -ADD --chown=indy:root . . - -ENTRYPOINT ["/bin/bash", "-c", "pytest \"$@\"", "--"] diff --git a/docs/GettingStartedAriesDev/CredentialRevocation.md b/docs/GettingStartedAriesDev/CredentialRevocation.md index 8674a54e57..79eaad141c 100644 --- a/docs/GettingStartedAriesDev/CredentialRevocation.md +++ b/docs/GettingStartedAriesDev/CredentialRevocation.md @@ -151,3 +151,33 @@ further customize notification handling. If the argument `--monitor-revocation-notification` is used on startup, a webhook with the topic `revocation-notification` and a payload containing the thread ID and comment is emitted to registered webhook urls. + +## Manually Creating Revocation Registries + +The process for creating revocation registries is completely automated - when you create a Credential Definition with revocation enabled, a revocation registry is automatically created (in fact 2 registries are created), and when a registry fills up, a new one is automatically created. + +However the Aca-Py admin api supports endpoints to explicitely create a new revocation registry, if you desire. + +There are several endpoints that must be called, and they must be called in this order: + +1. Create revoc registry `POST /revocation/create-registry` + + - you need to provide the credential definition id and the size of the registry + +2. Fix the tails file URI `PATCH /revocation/registry/{rev_reg_id}` + + - here you need to provide the full URI that will be written to the ledger, for example: + +``` +{ + "tails_public_uri": "http://host.docker.internal:6543/VDKEEMMSRTEqK4m7iiq5ZL:4:VDKEEMMSRTEqK4m7iiq5ZL:3:CL:8:faber.agent.degree_schema:CL_ACCUM:3cb5c439-928c-483c-a9a8-629c307e6b2d" +} +``` + +3. Post the revoc def to the ledger `POST /revocation/registry/{rev_reg_id}/definition` + + - if you are an author (i.e. have a DID with restricted ledger write access) then this transaction may need to go through an endorser + +4. Write the tails file `PUT /revocation/registry/{rev_reg_id}/tails-file` + + - the tails server will check that the registry definition is already written to the ledger diff --git a/docs/GettingStartedAriesDev/PlugIns.md b/docs/GettingStartedAriesDev/PlugIns.md new file mode 100644 index 0000000000..0ea508d235 --- /dev/null +++ b/docs/GettingStartedAriesDev/PlugIns.md @@ -0,0 +1,190 @@ +# Deeper Dive: Aca-Py Plug-Ins + +## What's in a Plug-In and How does it Work? + +Plug-ins are loaded on Aca-Py startup based on the following parameters: + +* `--plugin` - identifies the plug-in library to load +* `--block-plugin` - identifies plug-ins (including built-ins) that are *not* to be loaded +* `--plugin-config` - identify a configuration parameter for a plug-in +* `--plugin-config-value` - identify a *value* for a plug-in configuration + + +The `--plug-in` parameter specifies a package that is loaded by Aca-Py at runtime, and extends Aca-Py by adding support for additional protocols and message types, and/or extending the Admin API with additional endpoints. + +The original plug-in design (which we will call the "old" model) explicitly indluded `message_types.py` `routes.py` (to add Admin API's). But functionality was added later (we'll call this the "new" model) to allow the plug-in to include a generic `setup` package that could perform arbitrary initialization. The "new" model also includes support for a `definition.py` file that can specify plug-in version information (major/minor plug-in version, as well as the minimum supported version (if another agent is running an older version of the plug-in)). + +You can discover which plug-ins are installed in an aca-py instance by calling (in the "server" section) the `GET /plugins` endpoint. (Note that this will return all loaded protocols, including the built-ins. You can call the `GET /status/config` to inspect the Aca-Py configuration, which will include the configuration for the *external* plug-ins.) + +### setup method + +If a setup method is provided, it will be called. If not, the `message_types.py` and `routes.py` will be explicitly loaded. + +This would be in the `package/module __init__.py`: + +``` +async def setup(context: InjectionContext): + pass +``` + +TODO I couldn't find an implementation of a custom `setup` in any of the existing plug-ins, so I'm not completely sure what are the best practices for this option. + +### message_types.py + +When loading a plug-in, if there is a `message_types.py` available, Aca-Py will check the following attributes to initialize the protocol(s): + +- `MESSAGE_TYPES` - identifies message types supported by the protocol +- `CONTROLLERS` - identifies protocol controllers + +### routes.py + +If `routes.py` is available, then Aca-Py will call the following functions to initialize the Admin endpoints: + +- `register()` - registers routes for the new Admin endpoints +- `register_events()` - registers an events this package will listen for/respond to + +### definition.py + +If `definition.py` is available, Aca-Py will read this package to determine protocol version information. An example follows (this is an example that specifies two protocol versions): + +``` +versions = [ + { + "major_version": 1, + "minimum_minor_version": 0, + "current_minor_version": 0, + "path": "v1_0", + }, + { + "major_version": 2, + "minimum_minor_version": 0, + "current_minor_version": 0, + "path": "v2_0", + }, +] +``` + +The attributes are: + +- `major_version` - specifies the protocol major version +- `current_minor_version` - specifies the protocol minor version +- `minimum_minor_version` - specifies the minimum supported version (if a lower version is installed in another agent) +- `path` - specifies the sub-path within the package for this version + + +## Loading Aca-Py Plug-Ins at Runtime + +The load sequence for a plug-in (the "Startup" class depends on how Aca-Py is running - `upgrade`, `provision` or `start`): + +```mermaid +sequenceDiagram + participant Startup + Note right of Startup: Configuration is loaded on startup
from aca-py config params + Startup->>+ArgParse: configure + ArgParse->>settings: ["external_plugins"] + ArgParse->>settings: ["blocked_plugins"] + + Startup->>+Conductor: setup() + Note right of Conductor: Each configured plug-in is validated and loaded + Conductor->>DefaultContext: build_context() + DefaultContext->>DefaultContext: load_plugins() + DefaultContext->>+PluginRegistry: register_package() (for built-in protocols) + PluginRegistry->>PluginRegistry: register_plugin() (for each sub-package) + DefaultContext->>PluginRegistry: register_plugin() (for non-protocol built-ins) + loop for each external plug-in + DefaultContext->>PluginRegistry: register_plugin() + alt if a setup method is provided + PluginRegistry->>ExternalPlugIn: has setup + else if routes and/or message_types are provided + PluginRegistry->>ExternalPlugIn: has routes + PluginRegistry->>ExternalPlugIn: has message_types + end + opt if definition is provided + PluginRegistry->>ExternalPlugIn: definition() + end + end + DefaultContext->>PluginRegistry: init_context() + loop for each external plug-in + alt if a setup method is provided + PluginRegistry->>ExternalPlugIn: setup() + else if a setup method is NOT provided + PluginRegistry->>PluginRegistry: load_protocols() + PluginRegistry->>PluginRegistry: load_protocol_version() + PluginRegistry->>ProtocolRegistry: register_message_types() + PluginRegistry->>ProtocolRegistry: register_controllers() + end + PluginRegistry->>PluginRegistry: register_protocol_events() + end + + Conductor->>Conductor: load_transports() + + Note right of Conductor: If the admin server is enabled, plug-in routes are added + Conductor->>AdminServer: create admin server if enabled + + Startup->>Conductor: start() + Conductor->>Conductor: start_transports() + Conductor->>AdminServer: start() + + Note right of Startup: the following represents an
admin server api request + Startup->>AdminServer: setup_context() (called on each request) + AdminServer->>PluginRegistry: register_admin_routes() + loop for each external plug-in + PluginRegistry->>ExternalPlugIn: routes.register() (to register endpoints) + end +``` + +## Developing a New Plug-In + +When developing a new plug-in: + +- If you are providing a new protocol or defining message types, you *should* include a `definition.py` file. +- If you are providing a new protocol or defining message types, you *should* include a `message_types.py` file. +- If you are providing additional Admin endpoints, you *should* include a `routes.py` file. +- If you are providing any other functionality, you should provide a `setup.py` file to initialize the custom functionality. No guidance is *currently* available for this option. + +### PIP vs Poetry Support + +Most Aca-Py plug-ins provide support for installing the plug-in using [poetry](https://python-poetry.org/). It is *recommended* to include support in your package for installing using *either* pip or poetry, to provide maximum support for users of your plug-in. + +### Plug-In Demo + +TBD + +# Aca-Py Plug-ins + +This list was originally published in [this hackmd document](https://hackmd.io/m2AZebwJRkm6sWgO64-5xQ). + +| Maintainer | Name | Features | Last Update | Link | +| ----------- | -------------------------- | -------------------------------- | ----------- | ----------------------------------------------------------------------- | +| BCGov | Redis Events | Inbound/Outbound message queue | Sep 2022 | https://github.com/bcgov/aries-acapy-plugin-redis-events | +| Hyperledger | Aries Toolbox | UI for ACA-py | Aug 2022 | https://github.com/hyperledger/aries-toolbox | +| Hyperledger | Aries ACApy Plugin Toolbox | Protocol Handlers | Aug 2022 | https://github.com/hyperledger/aries-acapy-plugin-toolbox | +| Indicio | Data Transfer | Specific Data import | Aug 2022 | https://github.com/Indicio-tech/aries-acapy-plugin-data-transfer | +| Indicio | Question & Answer | Non-Aries Protocol | Aug 2022 | https://github.com/Indicio-tech/acapy-plugin-qa | +| Indicio | Acapy-plugin-pickup | Fetching Messages from Mediator | Aug 2022 | https://github.com/Indicio-tech/acapy-plugin-pickup | +| Indicio | Machine Readable GF | Governance Framework | Mar 2022 | https://github.com/Indicio-tech/mrgf | +| Indicio | Cache Redis | Cache for Scaleability | Jul 2022 | https://github.com/Indicio-tech/aries-acapy-cache-redis | +| SICPA Dlab | Kafka Events | Event Bus Integration | Aug 2022 | https://github.com/sicpa-dlab/aries-acapy-plugin-kafka-events | +| SICPA Dlab | DidComm Resolver | Unversal Resolver for DIDComm | Aug 2022 | https://github.com/sicpa-dlab/acapy-resolver-didcomm | +| SICPA Dlab | Universal Resolver | Multi-ledger Reading | Jul 2021 | https://github.com/sicpa-dlab/acapy-resolver-universal | +| DDX | mydata-did-protocol | | Oct 2022 | https://github.com/decentralised-dataexchange/acapy-mydata-did-protocol | +| BCGov | Basic Message Storage | Basic message storage (traction) | Dec 2022 | https://github.com/bcgov/traction/tree/develop/plugins/basicmessage_storage | +| BCGov | Multi-tenant Provider | Multi-tenant Provider (traction) | Dec 2022 | https://github.com/bcgov/traction/tree/develop/plugins/multitenant_provider | +| BCGov | Traction Innkeeper | Innkeeper (traction) | Feb 2023 | https://github.com/bcgov/traction/tree/develop/plugins/traction_innkeeper | + + +# Reference + +The following links may be helpful or provide additional context for the current plug-in support. (These are links to issues or pull requests that were raised during plug-in development.) + +Configuration params: + https://github.com/hyperledger/aries-cloudagent-python/issues/1121 + https://hackmd.io/ROUzENdpQ12cz3UB9qk1nA + https://github.com/hyperledger/aries-cloudagent-python/pull/1226 + +Loading plug-ins: + https://github.com/hyperledger/aries-cloudagent-python/pull/1086 + +Versioning for plug-ins: + https://github.com/hyperledger/aries-cloudagent-python/pull/443 + diff --git a/docs/GettingStartedAriesDev/README.md b/docs/GettingStartedAriesDev/README.md index e6b98aa655..dad297c007 100644 --- a/docs/GettingStartedAriesDev/README.md +++ b/docs/GettingStartedAriesDev/README.md @@ -21,5 +21,6 @@ Note that in the guidance we have here, we include not only the links to look at * [Deeper Dive: Routing Example](AriesRoutingExample.md) * To Do: [Deeper Dive: Running and Connecting to an Indy Network](ConnectIndyNetwork.md) * [Steps and APIs to support credential revocation with Aries agent](CredentialRevocation.md) +* [Deeper Dive: Aca-Py Plug-Ins](PlugIns.md) Want to help with this guide? Please add issues or submit a pull request to improve the document. Point out things that are missing, things to improve and especially things that are wrong. diff --git a/docs/README.md b/docs/README.md index 6c59cfd2f1..d2a5864500 100644 --- a/docs/README.md +++ b/docs/README.md @@ -19,12 +19,8 @@ updated, as noted below. ### Before you start To test generate and view the RTD documentation locally, you must install [Sphinx](https://www.sphinx-doc.org/en/master/) and the -[Sphinx RTD theme](https://pypi.org/project/sphinx-rtd-theme/). Both can be installed from PyPi using pip. For example: - -``` bash -pip install -U sphinx -pip install -U sphinx-rtd-theme -``` +[Sphinx RTD theme](https://pypi.org/project/sphinx-rtd-theme/). Follow the instructions on the respective pages to install +and verify the installation on your system. ### Generate Module Files @@ -54,6 +50,8 @@ Once generated, go into the `_build` folder and open `index.html` in a browser. This is the hard part; looking for errors in docstrings added by devs. Some tips: +- missing imports (`No module named 'async_timeout'`) can be solved by adding the module to the +list of `autodoc_mock_imports` in the [conf.py](./conf.py) file. - Ignore any errors in .md files - Ignore the warnings about including `docs/README.md` - Ignore an dist-package errors @@ -78,4 +76,4 @@ You will see there are already several instances of that, notably "connections" The RTD documentation is **not** currently auto-generated, so a manual re-generation of the documentation is still required. -> TODO: Automate this when new tags are applied to the repository. \ No newline at end of file +> TODO: Automate this when new tags are applied to the repository. diff --git a/docs/assets/endorse-cred-def.png b/docs/assets/endorse-cred-def.png new file mode 100644 index 0000000000..ceb3d2fbb1 Binary files /dev/null and b/docs/assets/endorse-cred-def.png differ diff --git a/docs/assets/endorse-cred-def.puml b/docs/assets/endorse-cred-def.puml new file mode 100644 index 0000000000..a1a78c7772 --- /dev/null +++ b/docs/assets/endorse-cred-def.puml @@ -0,0 +1,75 @@ +@startuml +' List of actors for our use case +actor Admin +participant CredDefRoutes +participant RevocationRoutes +participant IndyRevocation +participant Ledger +participant TransactionManager +participant EventBus +participant OutboundHandler +participant EndorsedTxnHandler +boundary OtherAgent + +' Sequence for writing a new credential definition +Admin --> CredDefRoutes: POST /credential-definitions +group Endorse transaction process +CredDefRoutes --> Ledger: create_and_send_credential_definition() +CredDefRoutes --> TransactionManager: create_record() +CredDefRoutes --> TransactionManager: create_request() +CredDefRoutes --> OutboundHandler: send_outbound_msg() +OutboundHandler --> OtherAgent: send_msg() +OtherAgent --> OtherAgent: endorse_msg() +EndorsedTxnHandler <-- OtherAgent: send_msg() +TransactionManager <-- EndorsedTxnHandler: receive_endorse_response() +TransactionManager <-- EndorsedTxnHandler: complete_transaction() +Ledger <-- TransactionManager: txn_submit() +TransactionManager --> TransactionManager: endorsed_txn_post_processing() +TransactionManager --> EventBus: notify_cred_def_event() +end + +' Create the revocation registry once the credential definition is written +CredDefRoutes <-- EventBus: on_cred_def_event() +CredDefRoutes --> IndyRevocation: init_issuer_registry() +IndyRevocation --> EventBus: notify_revocation_reg_init_event() +RevocationRoutes <-- EventBus: on_revocation_registry_init_event() +RevocationRoutes --> RevocationRoutes: generate_tails() +group Endorse transaction process +RevocationRoutes --> Ledger:send_revoc_reg_def() +RevocationRoutes --> TransactionManager: create_record() +RevocationRoutes --> TransactionManager: create_request() +RevocationRoutes --> OutboundHandler: send_outbound_msg() +OutboundHandler --> OtherAgent: send_msg() +OtherAgent --> OtherAgent: endorse_msg() +EndorsedTxnHandler <-- OtherAgent: send_msg() +TransactionManager <-- EndorsedTxnHandler: receive_endorse_response() +TransactionManager <-- EndorsedTxnHandler: complete_transaction() +Ledger <-- TransactionManager: txn_submit() +TransactionManager --> TransactionManager: endorsed_txn_post_processing() +TransactionManager --> EventBus: notify_revocation_reg_endorsed_event() +end + +' Now create the revocation entry (accumulator) +RevocationRoutes <-- EventBus: on_revocation_registry_endorsed_event() +RevocationRoutes --> RevocationRoutes: upload_tails() +RevocationRoutes --> EventBus: notify_revocation_entry_event() +RevocationRoutes <-- EventBus: on_revocation_entry_event() +group Endorse transaction process +RevocationRoutes --> IndyRevocation: send_entry() +IndyRevocation --> Ledger: send_entry() +RevocationRoutes --> TransactionManager: create_record() +RevocationRoutes --> TransactionManager: create_request() +RevocationRoutes --> OutboundHandler: send_outbound_msg() +OutboundHandler --> OtherAgent: send_msg() +OtherAgent --> OtherAgent: endorse_msg() +EndorsedTxnHandler <-- OtherAgent: send_msg() +TransactionManager <-- EndorsedTxnHandler: receive_endorse_response() +TransactionManager <-- EndorsedTxnHandler: complete_transaction() +Ledger <-- TransactionManager: txn_submit() +TransactionManager --> TransactionManager: endorsed_txn_post_processing() + +' Notify that the revocation entry is completed (no one listens to this notification yet) +TransactionManager --> EventBus: notify_revocation_entry_endorsed_event() +end + +@enduml diff --git a/docs/assets/endorse-public-did.png b/docs/assets/endorse-public-did.png new file mode 100644 index 0000000000..275b4ab6de Binary files /dev/null and b/docs/assets/endorse-public-did.png differ diff --git a/docs/assets/endorse-public-did.puml b/docs/assets/endorse-public-did.puml new file mode 100644 index 0000000000..63de78bb50 --- /dev/null +++ b/docs/assets/endorse-public-did.puml @@ -0,0 +1,53 @@ +@startuml +' List of actors for our use case +actor Admin +participant WalletRoutes +participant IndyWallet +participant LedgerRoutes +participant Ledger +participant TransactionManager +participant EventBus +participant OutboundHandler +participant EndorsedTxnHandler +boundary OtherAgent + +' Sequence for writing a new DID on the ledger (assumes the author already has a DID) +Admin --> WalletRoutes: POST /wallet/did/create +Admin --> LedgerRoutes: POST /ledger/register-nym +group Endorse transaction process +LedgerRoutes --> Ledger: register_nym() +LedgerRoutes --> TransactionManager: create_record() +LedgerRoutes --> TransactionManager: create_request() +LedgerRoutes --> OutboundHandler: send_outbound_msg() +OutboundHandler --> OtherAgent: send_msg() +OtherAgent --> OtherAgent: endorse_msg() +EndorsedTxnHandler <-- OtherAgent: send_msg() +TransactionManager <-- EndorsedTxnHandler: receive_endorse_response() +TransactionManager <-- EndorsedTxnHandler: complete_transaction() +Ledger <-- TransactionManager: txn_submit() +TransactionManager --> TransactionManager: endorsed_txn_post_processing() +TransactionManager --> EventBus: notify_endorse_did_event() +end + +WalletRoutes <-- EventBus: on_register_nym_event() +WalletRoutes --> WalletRoutes:promote_wallet_public_did() +WalletRoutes --> IndyWallet:set_public_did() +group Endorse transaction process +WalletRoutes --> IndyWallet:set_did_endpoint() +IndyWallet --> Ledger:update_endpoint_for_did() +WalletRoutes --> TransactionManager: create_record() +WalletRoutes --> TransactionManager: create_request() +WalletRoutes --> OutboundHandler: send_outbound_msg() +OutboundHandler --> OtherAgent: send_msg() +OtherAgent --> OtherAgent: endorse_msg() +EndorsedTxnHandler <-- OtherAgent: send_msg() +TransactionManager <-- EndorsedTxnHandler: receive_endorse_response() +TransactionManager <-- EndorsedTxnHandler: complete_transaction() +Ledger <-- TransactionManager: txn_submit() +TransactionManager --> TransactionManager: endorsed_txn_post_processing() + +' notification that no one is listening to yet +TransactionManager --> EventBus: notify_endorse_did_attrib_event() +end + +@enduml diff --git a/docs/assets/endorser-design.png b/docs/assets/endorser-design.png new file mode 100644 index 0000000000..1c4b9fc555 Binary files /dev/null and b/docs/assets/endorser-design.png differ diff --git a/docs/assets/endorser-design.puml b/docs/assets/endorser-design.puml new file mode 100644 index 0000000000..39883ea66b --- /dev/null +++ b/docs/assets/endorser-design.puml @@ -0,0 +1,31 @@ +@startuml +interface AdminUser + +interface OtherAgent + +object TransactionRoutes + +object TransactionHandlers + +AdminUser --> TransactionRoutes: invoke_endpoint() + +OtherAgent --> TransactionHandlers: send_message() + +object TransactionManager + +object Wallet + +TransactionManager --> Wallet: manage_records() + +TransactionRoutes --> TransactionManager: invoke_api() +TransactionHandlers --> TransactionManager: handle_msg() + +object EventBus + +TransactionManager --> EventBus: notify() + +interface OtherProtocolRoutes + +OtherProtocolRoutes --> EventBus: subscribe() +EventBus --> OtherProtocolRoutes: notify() +@enduml diff --git a/docs/conf.py b/docs/conf.py index af761e29b2..34143cef8b 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -46,6 +46,12 @@ "unflatten", "qrcode", "rlp", + "nest_asyncio", + "marshmallow", + "typing_extensions", + "async_timeout", + "portalocker", + "pythonjsonlogger", ] # "aries_cloudagent.tests.test_conductor", @@ -58,7 +64,7 @@ # -- Project information ----------------------------------------------------- project = "Aries Cloud Agent - Python" -copyright = "2021, Province of British Columbia" +copyright = "2023, Province of British Columbia" author = "Province of British Columbia" # The short X.Y version @@ -101,7 +107,7 @@ # # This is also used if you do content translation via gettext catalogs. # Usually you set "language" from the command line for these cases. -language = None +language = "en" # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. @@ -241,6 +247,7 @@ # Example configuration for intersphinx: refer to the Python standard library. intersphinx_mapping = {"https://docs.python.org/": None} + # To supress cross-reference warnings # https://github.com/sphinx-doc/sphinx/issues/3866#issuecomment-768167824 class PatchedPythonDomain(PythonDomain): diff --git a/docs/generated/aries_cloudagent.commands.rst b/docs/generated/aries_cloudagent.commands.rst index f7051c0780..f4353c5984 100644 --- a/docs/generated/aries_cloudagent.commands.rst +++ b/docs/generated/aries_cloudagent.commands.rst @@ -32,3 +32,11 @@ aries\_cloudagent.commands.start module :members: :undoc-members: :show-inheritance: + +aries\_cloudagent.commands.upgrade module +----------------------------------------- + +.. automodule:: aries_cloudagent.commands.upgrade + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/generated/aries_cloudagent.config.rst b/docs/generated/aries_cloudagent.config.rst index 8fae3838c6..43b1ef34a5 100644 --- a/docs/generated/aries_cloudagent.config.rst +++ b/docs/generated/aries_cloudagent.config.rst @@ -89,6 +89,14 @@ aries\_cloudagent.config.logging module :undoc-members: :show-inheritance: +aries\_cloudagent.config.plugin\_settings module +------------------------------------------------ + +.. automodule:: aries_cloudagent.config.plugin_settings + :members: + :undoc-members: + :show-inheritance: + aries\_cloudagent.config.provider module ---------------------------------------- diff --git a/docs/generated/aries_cloudagent.connections.rst b/docs/generated/aries_cloudagent.connections.rst index ba841ef4b2..90d1f68626 100644 --- a/docs/generated/aries_cloudagent.connections.rst +++ b/docs/generated/aries_cloudagent.connections.rst @@ -24,11 +24,3 @@ aries\_cloudagent.connections.base\_manager module :members: :undoc-members: :show-inheritance: - -aries\_cloudagent.connections.util module ------------------------------------------ - -.. automodule:: aries_cloudagent.connections.util - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/generated/aries_cloudagent.core.rst b/docs/generated/aries_cloudagent.core.rst index b708e02226..f304ae0aa0 100644 --- a/docs/generated/aries_cloudagent.core.rst +++ b/docs/generated/aries_cloudagent.core.rst @@ -57,6 +57,14 @@ aries\_cloudagent.core.goal\_code\_registry module :undoc-members: :show-inheritance: +aries\_cloudagent.core.oob\_processor module +-------------------------------------------- + +.. automodule:: aries_cloudagent.core.oob_processor + :members: + :undoc-members: + :show-inheritance: + aries\_cloudagent.core.plugin\_registry module ---------------------------------------------- diff --git a/docs/generated/aries_cloudagent.messaging.decorators.rst b/docs/generated/aries_cloudagent.messaging.decorators.rst index 3bf19b2edd..64fb269fb1 100644 --- a/docs/generated/aries_cloudagent.messaging.decorators.rst +++ b/docs/generated/aries_cloudagent.messaging.decorators.rst @@ -49,6 +49,14 @@ aries\_cloudagent.messaging.decorators.please\_ack\_decorator module :undoc-members: :show-inheritance: +aries\_cloudagent.messaging.decorators.service\_decorator module +---------------------------------------------------------------- + +.. automodule:: aries_cloudagent.messaging.decorators.service_decorator + :members: + :undoc-members: + :show-inheritance: + aries\_cloudagent.messaging.decorators.signature\_decorator module ------------------------------------------------------------------ diff --git a/docs/generated/aries_cloudagent.multitenant.rst b/docs/generated/aries_cloudagent.multitenant.rst index ad9325f00b..913a17a652 100644 --- a/docs/generated/aries_cloudagent.multitenant.rst +++ b/docs/generated/aries_cloudagent.multitenant.rst @@ -33,6 +33,14 @@ aries\_cloudagent.multitenant.base module :undoc-members: :show-inheritance: +aries\_cloudagent.multitenant.cache module +------------------------------------------ + +.. automodule:: aries_cloudagent.multitenant.cache + :members: + :undoc-members: + :show-inheritance: + aries\_cloudagent.multitenant.error module ------------------------------------------ @@ -56,3 +64,11 @@ aries\_cloudagent.multitenant.manager\_provider module :members: :undoc-members: :show-inheritance: + +aries\_cloudagent.multitenant.route\_manager module +--------------------------------------------------- + +.. automodule:: aries_cloudagent.multitenant.route_manager + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/generated/aries_cloudagent.protocols.coordinate_mediation.v1_0.rst b/docs/generated/aries_cloudagent.protocols.coordinate_mediation.v1_0.rst index 80c49734be..fb05fd04d8 100644 --- a/docs/generated/aries_cloudagent.protocols.coordinate_mediation.v1_0.rst +++ b/docs/generated/aries_cloudagent.protocols.coordinate_mediation.v1_0.rst @@ -43,6 +43,30 @@ aries\_cloudagent.protocols.coordinate\_mediation.v1\_0.message\_types module :undoc-members: :show-inheritance: +aries\_cloudagent.protocols.coordinate\_mediation.v1\_0.normalization module +---------------------------------------------------------------------------- + +.. automodule:: aries_cloudagent.protocols.coordinate_mediation.v1_0.normalization + :members: + :undoc-members: + :show-inheritance: + +aries\_cloudagent.protocols.coordinate\_mediation.v1\_0.route\_manager module +----------------------------------------------------------------------------- + +.. automodule:: aries_cloudagent.protocols.coordinate_mediation.v1_0.route_manager + :members: + :undoc-members: + :show-inheritance: + +aries\_cloudagent.protocols.coordinate\_mediation.v1\_0.route\_manager\_provider module +--------------------------------------------------------------------------------------- + +.. automodule:: aries_cloudagent.protocols.coordinate_mediation.v1_0.route_manager_provider + :members: + :undoc-members: + :show-inheritance: + aries\_cloudagent.protocols.coordinate\_mediation.v1\_0.routes module --------------------------------------------------------------------- diff --git a/docs/generated/aries_cloudagent.protocols.issue_credential.v1_0.messages.rst b/docs/generated/aries_cloudagent.protocols.issue_credential.v1_0.messages.rst index 8279cea992..db091d4033 100644 --- a/docs/generated/aries_cloudagent.protocols.issue_credential.v1_0.messages.rst +++ b/docs/generated/aries_cloudagent.protocols.issue_credential.v1_0.messages.rst @@ -25,6 +25,14 @@ aries\_cloudagent.protocols.issue\_credential.v1\_0.messages.credential\_ack mod :undoc-members: :show-inheritance: +aries\_cloudagent.protocols.issue\_credential.v1\_0.messages.credential\_exchange\_webhook module +------------------------------------------------------------------------------------------------- + +.. automodule:: aries_cloudagent.protocols.issue_credential.v1_0.messages.credential_exchange_webhook + :members: + :undoc-members: + :show-inheritance: + aries\_cloudagent.protocols.issue\_credential.v1\_0.messages.credential\_issue module ------------------------------------------------------------------------------------- diff --git a/docs/generated/aries_cloudagent.protocols.issue_credential.v2_0.messages.rst b/docs/generated/aries_cloudagent.protocols.issue_credential.v2_0.messages.rst index c4d72caf1d..aedce87e4f 100644 --- a/docs/generated/aries_cloudagent.protocols.issue_credential.v2_0.messages.rst +++ b/docs/generated/aries_cloudagent.protocols.issue_credential.v2_0.messages.rst @@ -25,6 +25,14 @@ aries\_cloudagent.protocols.issue\_credential.v2\_0.messages.cred\_ack module :undoc-members: :show-inheritance: +aries\_cloudagent.protocols.issue\_credential.v2\_0.messages.cred\_ex\_record\_webhook module +--------------------------------------------------------------------------------------------- + +.. automodule:: aries_cloudagent.protocols.issue_credential.v2_0.messages.cred_ex_record_webhook + :members: + :undoc-members: + :show-inheritance: + aries\_cloudagent.protocols.issue\_credential.v2\_0.messages.cred\_format module -------------------------------------------------------------------------------- diff --git a/docs/generated/aries_cloudagent.protocols.out_of_band.v1_0.models.rst b/docs/generated/aries_cloudagent.protocols.out_of_band.v1_0.models.rst index 260dbd201f..b1b7bfd33c 100644 --- a/docs/generated/aries_cloudagent.protocols.out_of_band.v1_0.models.rst +++ b/docs/generated/aries_cloudagent.protocols.out_of_band.v1_0.models.rst @@ -16,3 +16,11 @@ aries\_cloudagent.protocols.out\_of\_band.v1\_0.models.invitation module :members: :undoc-members: :show-inheritance: + +aries\_cloudagent.protocols.out\_of\_band.v1\_0.models.oob\_record module +------------------------------------------------------------------------- + +.. automodule:: aries_cloudagent.protocols.out_of_band.v1_0.models.oob_record + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/generated/aries_cloudagent.protocols.present_proof.v1_0.messages.rst b/docs/generated/aries_cloudagent.protocols.present_proof.v1_0.messages.rst index a00cb2d278..dc6cf13bc5 100644 --- a/docs/generated/aries_cloudagent.protocols.present_proof.v1_0.messages.rst +++ b/docs/generated/aries_cloudagent.protocols.present_proof.v1_0.messages.rst @@ -48,3 +48,11 @@ aries\_cloudagent.protocols.present\_proof.v1\_0.messages.presentation\_request :members: :undoc-members: :show-inheritance: + +aries\_cloudagent.protocols.present\_proof.v1\_0.messages.presentation\_webhook module +-------------------------------------------------------------------------------------- + +.. automodule:: aries_cloudagent.protocols.present_proof.v1_0.messages.presentation_webhook + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/generated/aries_cloudagent.protocols.present_proof.v2_0.messages.rst b/docs/generated/aries_cloudagent.protocols.present_proof.v2_0.messages.rst index bcaf052ad7..907d55c539 100644 --- a/docs/generated/aries_cloudagent.protocols.present_proof.v2_0.messages.rst +++ b/docs/generated/aries_cloudagent.protocols.present_proof.v2_0.messages.rst @@ -56,3 +56,11 @@ aries\_cloudagent.protocols.present\_proof.v2\_0.messages.pres\_request module :members: :undoc-members: :show-inheritance: + +aries\_cloudagent.protocols.present\_proof.v2\_0.messages.pres\_webhook module +------------------------------------------------------------------------------ + +.. automodule:: aries_cloudagent.protocols.present_proof.v2_0.messages.pres_webhook + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/generated/aries_cloudagent.protocols.revocation_notification.rst b/docs/generated/aries_cloudagent.protocols.revocation_notification.rst index 013c0319ac..c88d26085d 100644 --- a/docs/generated/aries_cloudagent.protocols.revocation_notification.rst +++ b/docs/generated/aries_cloudagent.protocols.revocation_notification.rst @@ -13,6 +13,7 @@ Subpackages :maxdepth: 4 aries_cloudagent.protocols.revocation_notification.v1_0 + aries_cloudagent.protocols.revocation_notification.v2_0 Submodules ---------- diff --git a/docs/generated/aries_cloudagent.protocols.revocation_notification.v2_0.handlers.rst b/docs/generated/aries_cloudagent.protocols.revocation_notification.v2_0.handlers.rst new file mode 100644 index 0000000000..1fa7a93885 --- /dev/null +++ b/docs/generated/aries_cloudagent.protocols.revocation_notification.v2_0.handlers.rst @@ -0,0 +1,18 @@ +aries\_cloudagent.protocols.revocation\_notification.v2\_0.handlers package +=========================================================================== + +.. automodule:: aries_cloudagent.protocols.revocation_notification.v2_0.handlers + :members: + :undoc-members: + :show-inheritance: + +Submodules +---------- + +aries\_cloudagent.protocols.revocation\_notification.v2\_0.handlers.revoke\_handler module +------------------------------------------------------------------------------------------ + +.. automodule:: aries_cloudagent.protocols.revocation_notification.v2_0.handlers.revoke_handler + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/generated/aries_cloudagent.protocols.revocation_notification.v2_0.messages.rst b/docs/generated/aries_cloudagent.protocols.revocation_notification.v2_0.messages.rst new file mode 100644 index 0000000000..db465ff9e3 --- /dev/null +++ b/docs/generated/aries_cloudagent.protocols.revocation_notification.v2_0.messages.rst @@ -0,0 +1,18 @@ +aries\_cloudagent.protocols.revocation\_notification.v2\_0.messages package +=========================================================================== + +.. automodule:: aries_cloudagent.protocols.revocation_notification.v2_0.messages + :members: + :undoc-members: + :show-inheritance: + +Submodules +---------- + +aries\_cloudagent.protocols.revocation\_notification.v2\_0.messages.revoke module +--------------------------------------------------------------------------------- + +.. automodule:: aries_cloudagent.protocols.revocation_notification.v2_0.messages.revoke + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/generated/aries_cloudagent.protocols.revocation_notification.v2_0.models.rst b/docs/generated/aries_cloudagent.protocols.revocation_notification.v2_0.models.rst new file mode 100644 index 0000000000..cffa7e42ae --- /dev/null +++ b/docs/generated/aries_cloudagent.protocols.revocation_notification.v2_0.models.rst @@ -0,0 +1,18 @@ +aries\_cloudagent.protocols.revocation\_notification.v2\_0.models package +========================================================================= + +.. automodule:: aries_cloudagent.protocols.revocation_notification.v2_0.models + :members: + :undoc-members: + :show-inheritance: + +Submodules +---------- + +aries\_cloudagent.protocols.revocation\_notification.v2\_0.models.rev\_notification\_record module +-------------------------------------------------------------------------------------------------- + +.. automodule:: aries_cloudagent.protocols.revocation_notification.v2_0.models.rev_notification_record + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/generated/aries_cloudagent.protocols.revocation_notification.v2_0.rst b/docs/generated/aries_cloudagent.protocols.revocation_notification.v2_0.rst new file mode 100644 index 0000000000..8247c6853a --- /dev/null +++ b/docs/generated/aries_cloudagent.protocols.revocation_notification.v2_0.rst @@ -0,0 +1,36 @@ +aries\_cloudagent.protocols.revocation\_notification.v2\_0 package +================================================================== + +.. automodule:: aries_cloudagent.protocols.revocation_notification.v2_0 + :members: + :undoc-members: + :show-inheritance: + +Subpackages +----------- + +.. toctree:: + :maxdepth: 4 + + aries_cloudagent.protocols.revocation_notification.v2_0.handlers + aries_cloudagent.protocols.revocation_notification.v2_0.messages + aries_cloudagent.protocols.revocation_notification.v2_0.models + +Submodules +---------- + +aries\_cloudagent.protocols.revocation\_notification.v2\_0.message\_types module +-------------------------------------------------------------------------------- + +.. automodule:: aries_cloudagent.protocols.revocation_notification.v2_0.message_types + :members: + :undoc-members: + :show-inheritance: + +aries\_cloudagent.protocols.revocation\_notification.v2\_0.routes module +------------------------------------------------------------------------ + +.. automodule:: aries_cloudagent.protocols.revocation_notification.v2_0.routes + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/generated/aries_cloudagent.resolver.default.rst b/docs/generated/aries_cloudagent.resolver.default.rst index 375c99ee0f..b7dd8eac96 100644 --- a/docs/generated/aries_cloudagent.resolver.default.rst +++ b/docs/generated/aries_cloudagent.resolver.default.rst @@ -25,6 +25,14 @@ aries\_cloudagent.resolver.default.key module :undoc-members: :show-inheritance: +aries\_cloudagent.resolver.default.universal module +--------------------------------------------------- + +.. automodule:: aries_cloudagent.resolver.default.universal + :members: + :undoc-members: + :show-inheritance: + aries\_cloudagent.resolver.default.web module --------------------------------------------- diff --git a/docs/generated/aries_cloudagent.resolver.rst b/docs/generated/aries_cloudagent.resolver.rst index c3aa28279c..7732543e90 100644 --- a/docs/generated/aries_cloudagent.resolver.rst +++ b/docs/generated/aries_cloudagent.resolver.rst @@ -33,14 +33,6 @@ aries\_cloudagent.resolver.did\_resolver module :undoc-members: :show-inheritance: -aries\_cloudagent.resolver.did\_resolver\_registry module ---------------------------------------------------------- - -.. automodule:: aries_cloudagent.resolver.did_resolver_registry - :members: - :undoc-members: - :show-inheritance: - aries\_cloudagent.resolver.routes module ---------------------------------------- diff --git a/docs/generated/aries_cloudagent.revocation.rst b/docs/generated/aries_cloudagent.revocation.rst index 6c7c6957a5..7263263c71 100644 --- a/docs/generated/aries_cloudagent.revocation.rst +++ b/docs/generated/aries_cloudagent.revocation.rst @@ -41,6 +41,14 @@ aries\_cloudagent.revocation.manager module :undoc-members: :show-inheritance: +aries\_cloudagent.revocation.recover module +------------------------------------------- + +.. automodule:: aries_cloudagent.revocation.recover + :members: + :undoc-members: + :show-inheritance: + aries\_cloudagent.revocation.routes module ------------------------------------------ diff --git a/docs/generated/aries_cloudagent.rst b/docs/generated/aries_cloudagent.rst index ef01b45059..4e431df88e 100644 --- a/docs/generated/aries_cloudagent.rst +++ b/docs/generated/aries_cloudagent.rst @@ -28,6 +28,7 @@ Subpackages aries_cloudagent.protocols aries_cloudagent.resolver aries_cloudagent.revocation + aries_cloudagent.settings aries_cloudagent.storage aries_cloudagent.tails aries_cloudagent.transport diff --git a/docs/generated/aries_cloudagent.settings.rst b/docs/generated/aries_cloudagent.settings.rst new file mode 100644 index 0000000000..fff50e5c48 --- /dev/null +++ b/docs/generated/aries_cloudagent.settings.rst @@ -0,0 +1,18 @@ +aries\_cloudagent.settings package +================================== + +.. automodule:: aries_cloudagent.settings + :members: + :undoc-members: + :show-inheritance: + +Submodules +---------- + +aries\_cloudagent.settings.routes module +---------------------------------------- + +.. automodule:: aries_cloudagent.settings.routes + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/generated/aries_cloudagent.transport.outbound.queue.rst b/docs/generated/aries_cloudagent.transport.outbound.queue.rst deleted file mode 100644 index b23440d0dc..0000000000 --- a/docs/generated/aries_cloudagent.transport.outbound.queue.rst +++ /dev/null @@ -1,34 +0,0 @@ -aries\_cloudagent.transport.outbound.queue package -================================================== - -.. automodule:: aries_cloudagent.transport.outbound.queue - :members: - :undoc-members: - :show-inheritance: - -Submodules ----------- - -aries\_cloudagent.transport.outbound.queue.base module ------------------------------------------------------- - -.. automodule:: aries_cloudagent.transport.outbound.queue.base - :members: - :undoc-members: - :show-inheritance: - -aries\_cloudagent.transport.outbound.queue.loader module --------------------------------------------------------- - -.. automodule:: aries_cloudagent.transport.outbound.queue.loader - :members: - :undoc-members: - :show-inheritance: - -aries\_cloudagent.transport.outbound.queue.redis module -------------------------------------------------------- - -.. automodule:: aries_cloudagent.transport.outbound.queue.redis - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/generated/aries_cloudagent.transport.outbound.rst b/docs/generated/aries_cloudagent.transport.outbound.rst index eb749ee452..1fde7f0379 100644 --- a/docs/generated/aries_cloudagent.transport.outbound.rst +++ b/docs/generated/aries_cloudagent.transport.outbound.rst @@ -6,14 +6,6 @@ aries\_cloudagent.transport.outbound package :undoc-members: :show-inheritance: -Subpackages ------------ - -.. toctree:: - :maxdepth: 4 - - aries_cloudagent.transport.outbound.queue - Submodules ---------- diff --git a/docs/generated/aries_cloudagent.wallet.rst b/docs/generated/aries_cloudagent.wallet.rst index f5c66a5c5b..41de770ff4 100644 --- a/docs/generated/aries_cloudagent.wallet.rst +++ b/docs/generated/aries_cloudagent.wallet.rst @@ -49,6 +49,14 @@ aries\_cloudagent.wallet.crypto module :undoc-members: :show-inheritance: +aries\_cloudagent.wallet.default\_verification\_key\_strategy module +-------------------------------------------------------------------- + +.. automodule:: aries_cloudagent.wallet.default_verification_key_strategy + :members: + :undoc-members: + :show-inheritance: + aries\_cloudagent.wallet.did\_info module ----------------------------------------- @@ -65,6 +73,14 @@ aries\_cloudagent.wallet.did\_method module :undoc-members: :show-inheritance: +aries\_cloudagent.wallet.did\_parameters\_validation module +----------------------------------------------------------- + +.. automodule:: aries_cloudagent.wallet.did_parameters_validation + :members: + :undoc-members: + :show-inheritance: + aries\_cloudagent.wallet.did\_posture module -------------------------------------------- diff --git a/open-api/openapi.json b/open-api/openapi.json index 999ab9c622..17178dd66b 100644 --- a/open-api/openapi.json +++ b/open-api/openapi.json @@ -1,11244 +1,13673 @@ { - "swagger" : "2.0", + "openapi" : "3.0.1", "info" : { - "version" : "v0.7.3", - "title" : "Aries Cloud Agent" + "title" : "Aries Cloud Agent", + "version" : "v0.8.2" }, + "servers" : [ { + "url" : "/" + } ], + "security" : [ { + "AuthorizationHeader" : [ ] + } ], "tags" : [ { - "name" : "action-menu", - "description" : "Menu interaction over connection" + "description" : "Menu interaction over connection", + "name" : "action-menu" }, { - "name" : "basicmessage", "description" : "Simple messaging", "externalDocs" : { "description" : "Specification", "url" : "https://github.com/hyperledger/aries-rfcs/tree/527849ec3aa2a8fd47a7bb6c57f918ff8bcb5e8c/features/0095-basic-message" - } + }, + "name" : "basicmessage" }, { - "name" : "connection", "description" : "Connection management", "externalDocs" : { "description" : "Specification", "url" : "https://github.com/hyperledger/aries-rfcs/tree/9b0aaa39df7e8bd434126c4b33c097aae78d65bf/features/0160-connection-protocol" - } + }, + "name" : "connection" }, { - "name" : "credential-definition", "description" : "Credential definition operations", "externalDocs" : { "description" : "Specification", "url" : "https://github.com/hyperledger/indy-node/blob/master/design/anoncreds.md#cred_def" - } + }, + "name" : "credential-definition" }, { - "name" : "credentials", "description" : "Holder credential management", "externalDocs" : { "description" : "Overview", "url" : "https://w3c.github.io/vc-data-model/#credentials" - } + }, + "name" : "credentials" }, { - "name" : "did-exchange", "description" : "Connection management via DID exchange", "externalDocs" : { "description" : "Specification", "url" : "https://github.com/hyperledger/aries-rfcs/tree/25464a5c8f8a17b14edaa4310393df6094ace7b0/features/0023-did-exchange" - } + }, + "name" : "did-exchange" + }, { + "description" : "Feature discovery", + "externalDocs" : { + "description" : "Specification", + "url" : "https://github.com/hyperledger/aries-rfcs/tree/b3a3942ef052039e73cd23d847f42947f8287da2/features/0031-discover-features" + }, + "name" : "discover-features" + }, { + "description" : "Feature discovery v2", + "externalDocs" : { + "description" : "Specification", + "url" : "https://github.com/hyperledger/aries-rfcs/tree/b3a3942ef052039e73cd23d847f42947f8287da2/features/0557-discover-features-v2" + }, + "name" : "discover-features v2.0" }, { - "name" : "endorse-transaction", - "description" : "Endorse a Transaction" + "description" : "Endorse a Transaction", + "name" : "endorse-transaction" }, { - "name" : "introduction", - "description" : "Introduction of known parties" + "description" : "Introduction of known parties", + "name" : "introduction" }, { - "name" : "issue-credential v1.0", "description" : "Credential issue v1.0", "externalDocs" : { "description" : "Specification", "url" : "https://github.com/hyperledger/aries-rfcs/tree/bb42a6c35e0d5543718fb36dd099551ab192f7b0/features/0036-issue-credential" - } + }, + "name" : "issue-credential v1.0" }, { - "name" : "issue-credential v2.0", "description" : "Credential issue v2.0", "externalDocs" : { "description" : "Specification", "url" : "https://github.com/hyperledger/aries-rfcs/tree/cd27fc64aa2805f756a118043d7c880354353047/features/0453-issue-credential-v2" - } + }, + "name" : "issue-credential v2.0" }, { - "name" : "jsonld", "description" : "Sign and verify json-ld data", "externalDocs" : { "description" : "Specification", "url" : "https://tools.ietf.org/html/rfc7515" - } + }, + "name" : "jsonld" }, { - "name" : "ledger", "description" : "Interaction with ledger", "externalDocs" : { "description" : "Overview", "url" : "https://hyperledger-indy.readthedocs.io/projects/plenum/en/latest/storage.html#ledger" - } + }, + "name" : "ledger" }, { - "name" : "mediation", "description" : "Mediation management", "externalDocs" : { "description" : "Specification", "url" : "https://github.com/hyperledger/aries-rfcs/tree/fa8dc4ea1e667eb07db8f9ffeaf074a4455697c0/features/0211-route-coordination" - } + }, + "name" : "mediation" }, { - "name" : "multitenancy", - "description" : "Multitenant wallet management" + "description" : "Multitenant wallet management", + "name" : "multitenancy" }, { - "name" : "out-of-band", "description" : "Out-of-band connections", "externalDocs" : { "description" : "Design", "url" : "https://github.com/hyperledger/aries-rfcs/tree/2da7fc4ee043effa3a9960150e7ba8c9a4628b68/features/0434-outofband" - } + }, + "name" : "out-of-band" }, { - "name" : "present-proof v1.0", "description" : "Proof presentation v1.0", "externalDocs" : { "description" : "Specification", "url" : "https://github.com/hyperledger/aries-rfcs/tree/4fae574c03f9f1013db30bf2c0c676b1122f7149/features/0037-present-proof" - } + }, + "name" : "present-proof v1.0" }, { - "name" : "present-proof v2.0", "description" : "Proof presentation v2.0", "externalDocs" : { "description" : "Specification", "url" : "https://github.com/hyperledger/aries-rfcs/tree/eace815c3e8598d4a8dd7881d8c731fdb2bcc0aa/features/0454-present-proof-v2" - } + }, + "name" : "present-proof v2.0" }, { - "name" : "resolver", "description" : "did resolver interface.", "externalDocs" : { "description" : "Specification" - } + }, + "name" : "resolver" }, { - "name" : "revocation", "description" : "Revocation registry management", "externalDocs" : { "description" : "Overview", "url" : "https://github.com/hyperledger/indy-hipe/tree/master/text/0011-cred-revocation" - } + }, + "name" : "revocation" }, { - "name" : "schema", "description" : "Schema operations", "externalDocs" : { "description" : "Specification", "url" : "https://github.com/hyperledger/indy-node/blob/master/design/anoncreds.md#schema" - } - }, { - "name" : "server", - "description" : "Feature discovery", - "externalDocs" : { - "description" : "Specification", - "url" : "https://github.com/hyperledger/aries-rfcs/tree/9b7ab9814f2e7d1108f74aca6f3d2e5d62899473/features/0031-discover-features" - } + }, + "name" : "schema" }, { - "name" : "trustping", "description" : "Trust-ping over connection", "externalDocs" : { "description" : "Specification", "url" : "https://github.com/hyperledger/aries-rfcs/tree/527849ec3aa2a8fd47a7bb6c57f918ff8bcb5e8c/features/0048-trust-ping" - } + }, + "name" : "trustping" }, { - "name" : "wallet", "description" : "DID and tag policy management", "externalDocs" : { "description" : "Design", "url" : "https://github.com/hyperledger/indy-sdk/tree/master/docs/design/003-wallet-storage" - } - } ], - "security" : [ { - "AuthorizationHeader" : [ ] + }, + "name" : "wallet" } ], "paths" : { "/action-menu/{conn_id}/close" : { "post" : { - "tags" : [ "action-menu" ], - "summary" : "Close the active menu associated with a connection", - "produces" : [ "application/json" ], "parameters" : [ { - "name" : "conn_id", - "in" : "path", "description" : "Connection identifier", + "in" : "path", + "name" : "conn_id", "required" : true, - "type" : "string" + "schema" : { + "type" : "string" + } } ], "responses" : { "200" : { - "description" : "", - "schema" : { - "$ref" : "#/definitions/ActionMenuModulesResult" - } + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/ActionMenuModulesResult" + } + } + }, + "description" : "" } - } + }, + "summary" : "Close the active menu associated with a connection", + "tags" : [ "action-menu" ] } }, "/action-menu/{conn_id}/fetch" : { "post" : { - "tags" : [ "action-menu" ], - "summary" : "Fetch the active menu", - "produces" : [ "application/json" ], "parameters" : [ { - "name" : "conn_id", - "in" : "path", "description" : "Connection identifier", + "in" : "path", + "name" : "conn_id", "required" : true, - "type" : "string" + "schema" : { + "type" : "string" + } } ], "responses" : { "200" : { - "description" : "", - "schema" : { - "$ref" : "#/definitions/ActionMenuFetchResult" - } + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/ActionMenuFetchResult" + } + } + }, + "description" : "" } - } + }, + "summary" : "Fetch the active menu", + "tags" : [ "action-menu" ] } }, "/action-menu/{conn_id}/perform" : { "post" : { - "tags" : [ "action-menu" ], - "summary" : "Perform an action associated with the active menu", - "produces" : [ "application/json" ], "parameters" : [ { - "in" : "body", - "name" : "body", - "required" : false, - "schema" : { - "$ref" : "#/definitions/PerformRequest" - } - }, { - "name" : "conn_id", - "in" : "path", "description" : "Connection identifier", + "in" : "path", + "name" : "conn_id", "required" : true, - "type" : "string" + "schema" : { + "type" : "string" + } } ], + "requestBody" : { + "content" : { + "*/*" : { + "schema" : { + "$ref" : "#/components/schemas/PerformRequest" + } + } + }, + "required" : false + }, "responses" : { "200" : { - "description" : "", - "schema" : { - "$ref" : "#/definitions/ActionMenuModulesResult" - } + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/ActionMenuModulesResult" + } + } + }, + "description" : "" } - } + }, + "summary" : "Perform an action associated with the active menu", + "tags" : [ "action-menu" ], + "x-codegen-request-body-name" : "body" } }, "/action-menu/{conn_id}/request" : { "post" : { - "tags" : [ "action-menu" ], - "summary" : "Request the active menu", - "produces" : [ "application/json" ], "parameters" : [ { - "name" : "conn_id", - "in" : "path", "description" : "Connection identifier", + "in" : "path", + "name" : "conn_id", "required" : true, - "type" : "string" + "schema" : { + "type" : "string" + } } ], "responses" : { "200" : { - "description" : "", - "schema" : { - "$ref" : "#/definitions/ActionMenuModulesResult" - } + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/ActionMenuModulesResult" + } + } + }, + "description" : "" } - } + }, + "summary" : "Request the active menu", + "tags" : [ "action-menu" ] } }, "/action-menu/{conn_id}/send-menu" : { "post" : { - "tags" : [ "action-menu" ], - "summary" : "Send an action menu to a connection", - "produces" : [ "application/json" ], "parameters" : [ { - "in" : "body", - "name" : "body", - "required" : false, - "schema" : { - "$ref" : "#/definitions/SendMenu" - } - }, { - "name" : "conn_id", - "in" : "path", "description" : "Connection identifier", + "in" : "path", + "name" : "conn_id", "required" : true, - "type" : "string" + "schema" : { + "type" : "string" + } } ], + "requestBody" : { + "content" : { + "*/*" : { + "schema" : { + "$ref" : "#/components/schemas/SendMenu" + } + } + }, + "required" : false + }, "responses" : { "200" : { - "description" : "", - "schema" : { - "$ref" : "#/definitions/ActionMenuModulesResult" - } + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/ActionMenuModulesResult" + } + } + }, + "description" : "" } - } + }, + "summary" : "Send an action menu to a connection", + "tags" : [ "action-menu" ], + "x-codegen-request-body-name" : "body" } }, "/connections" : { "get" : { - "tags" : [ "connection" ], - "summary" : "Query agent-to-agent connections", - "produces" : [ "application/json" ], "parameters" : [ { - "name" : "alias", - "in" : "query", "description" : "Alias", - "required" : false, - "type" : "string" - }, { - "name" : "connection_protocol", "in" : "query", - "description" : "Connection protocol used", - "required" : false, - "type" : "string", - "enum" : [ "connections/1.0", "didexchange/1.0" ] + "name" : "alias", + "schema" : { + "type" : "string" + } }, { - "name" : "invitation_key", + "description" : "Connection protocol used", "in" : "query", + "name" : "connection_protocol", + "schema" : { + "enum" : [ "connections/1.0", "didexchange/1.0" ], + "type" : "string" + } + }, { "description" : "invitation key", - "required" : false, - "type" : "string", - "pattern" : "^[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{43,44}$" + "in" : "query", + "name" : "invitation_key", + "schema" : { + "pattern" : "^[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{43,44}$", + "type" : "string" + } }, { - "name" : "my_did", + "description" : "Identifier of the associated Invitation Mesage", "in" : "query", - "description" : "My DID", - "required" : false, - "type" : "string", - "pattern" : "^(did:sov:)?[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}$" + "name" : "invitation_msg_id", + "schema" : { + "format" : "uuid", + "type" : "string" + } }, { - "name" : "state", + "description" : "My DID", "in" : "query", - "description" : "Connection state", - "required" : false, - "type" : "string", - "enum" : [ "start", "abandoned", "active", "completed", "response", "init", "invitation", "error", "request" ] + "name" : "my_did", + "schema" : { + "pattern" : "^(did:sov:)?[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}$", + "type" : "string" + } }, { - "name" : "their_did", + "description" : "Connection state", "in" : "query", + "name" : "state", + "schema" : { + "enum" : [ "start", "invitation", "request", "abandoned", "error", "init", "response", "active", "completed" ], + "type" : "string" + } + }, { "description" : "Their DID", - "required" : false, - "type" : "string", - "pattern" : "^(did:sov:)?[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}$" + "in" : "query", + "name" : "their_did", + "schema" : { + "pattern" : "^(did:sov:)?[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}$", + "type" : "string" + } }, { - "name" : "their_role", + "description" : "Their Public DID", "in" : "query", + "name" : "their_public_did", + "schema" : { + "pattern" : "^(did:sov:)?[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}$", + "type" : "string" + } + }, { "description" : "Their role in the connection protocol", - "required" : false, - "type" : "string", - "enum" : [ "invitee", "requester", "inviter", "responder" ] + "in" : "query", + "name" : "their_role", + "schema" : { + "enum" : [ "invitee", "requester", "inviter", "responder" ], + "type" : "string" + } } ], "responses" : { "200" : { - "description" : "", - "schema" : { - "$ref" : "#/definitions/ConnectionList" - } + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/ConnectionList" + } + } + }, + "description" : "" } - } + }, + "summary" : "Query agent-to-agent connections", + "tags" : [ "connection" ] } }, "/connections/create-invitation" : { "post" : { - "tags" : [ "connection" ], - "summary" : "Create a new connection invitation", - "produces" : [ "application/json" ], "parameters" : [ { - "in" : "body", - "name" : "body", - "required" : false, + "description" : "Alias", + "in" : "query", + "name" : "alias", "schema" : { - "$ref" : "#/definitions/CreateInvitationRequest" + "type" : "string" } }, { - "name" : "alias", + "description" : "Auto-accept connection (defaults to configuration)", "in" : "query", - "description" : "Alias", - "required" : false, - "type" : "string" - }, { "name" : "auto_accept", - "in" : "query", - "description" : "Auto-accept connection (defaults to configuration)", - "required" : false, - "type" : "boolean" + "schema" : { + "type" : "boolean" + } }, { - "name" : "multi_use", - "in" : "query", "description" : "Create invitation for multiple use (default false)", - "required" : false, - "type" : "boolean" - }, { - "name" : "public", "in" : "query", + "name" : "multi_use", + "schema" : { + "type" : "boolean" + } + }, { "description" : "Create invitation from public DID (default false)", - "required" : false, - "type" : "boolean" + "in" : "query", + "name" : "public", + "schema" : { + "type" : "boolean" + } } ], + "requestBody" : { + "content" : { + "*/*" : { + "schema" : { + "$ref" : "#/components/schemas/CreateInvitationRequest" + } + } + }, + "required" : false + }, "responses" : { "200" : { - "description" : "", - "schema" : { - "$ref" : "#/definitions/InvitationResult" - } + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/InvitationResult" + } + } + }, + "description" : "" } - } + }, + "summary" : "Create a new connection invitation", + "tags" : [ "connection" ], + "x-codegen-request-body-name" : "body" } }, "/connections/create-static" : { "post" : { - "tags" : [ "connection" ], - "summary" : "Create a new static connection", - "produces" : [ "application/json" ], - "parameters" : [ { - "in" : "body", - "name" : "body", - "required" : false, - "schema" : { - "$ref" : "#/definitions/ConnectionStaticRequest" - } - } ], + "requestBody" : { + "content" : { + "*/*" : { + "schema" : { + "$ref" : "#/components/schemas/ConnectionStaticRequest" + } + } + }, + "required" : false + }, "responses" : { "200" : { - "description" : "", - "schema" : { - "$ref" : "#/definitions/ConnectionStaticResult" - } + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/ConnectionStaticResult" + } + } + }, + "description" : "" } - } + }, + "summary" : "Create a new static connection", + "tags" : [ "connection" ], + "x-codegen-request-body-name" : "body" } }, "/connections/receive-invitation" : { "post" : { - "tags" : [ "connection" ], - "summary" : "Receive a new connection invitation", - "produces" : [ "application/json" ], "parameters" : [ { - "in" : "body", - "name" : "body", - "required" : false, + "description" : "Alias", + "in" : "query", + "name" : "alias", "schema" : { - "$ref" : "#/definitions/ReceiveInvitationRequest" + "type" : "string" } }, { - "name" : "alias", + "description" : "Auto-accept connection (defaults to configuration)", "in" : "query", - "description" : "Alias", - "required" : false, - "type" : "string" - }, { "name" : "auto_accept", - "in" : "query", - "description" : "Auto-accept connection (defaults to configuration)", - "required" : false, - "type" : "boolean" + "schema" : { + "type" : "boolean" + } }, { - "name" : "mediation_id", - "in" : "query", "description" : "Identifier for active mediation record to be used", - "required" : false, - "type" : "string", - "pattern" : "[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-4[a-fA-F0-9]{3}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}" + "in" : "query", + "name" : "mediation_id", + "schema" : { + "pattern" : "[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-4[a-fA-F0-9]{3}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}", + "type" : "string" + } } ], + "requestBody" : { + "content" : { + "*/*" : { + "schema" : { + "$ref" : "#/components/schemas/ReceiveInvitationRequest" + } + } + }, + "required" : false + }, "responses" : { "200" : { - "description" : "", - "schema" : { - "$ref" : "#/definitions/ConnRecord" - } + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/ConnRecord" + } + } + }, + "description" : "" } - } + }, + "summary" : "Receive a new connection invitation", + "tags" : [ "connection" ], + "x-codegen-request-body-name" : "body" } }, "/connections/{conn_id}" : { - "get" : { - "tags" : [ "connection" ], - "summary" : "Fetch a single connection record", - "produces" : [ "application/json" ], + "delete" : { "parameters" : [ { - "name" : "conn_id", - "in" : "path", "description" : "Connection identifier", + "in" : "path", + "name" : "conn_id", "required" : true, - "type" : "string" + "schema" : { + "type" : "string" + } } ], "responses" : { "200" : { - "description" : "", - "schema" : { - "$ref" : "#/definitions/ConnRecord" - } + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/ConnectionModuleResponse" + } + } + }, + "description" : "" } - } - }, - "delete" : { - "tags" : [ "connection" ], + }, "summary" : "Remove an existing connection record", - "produces" : [ "application/json" ], + "tags" : [ "connection" ] + }, + "get" : { "parameters" : [ { - "name" : "conn_id", - "in" : "path", "description" : "Connection identifier", + "in" : "path", + "name" : "conn_id", "required" : true, - "type" : "string" + "schema" : { + "type" : "string" + } } ], "responses" : { "200" : { - "description" : "", - "schema" : { - "$ref" : "#/definitions/ConnectionModuleResponse" - } + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/ConnRecord" + } + } + }, + "description" : "" } - } + }, + "summary" : "Fetch a single connection record", + "tags" : [ "connection" ] } }, "/connections/{conn_id}/accept-invitation" : { "post" : { - "tags" : [ "connection" ], - "summary" : "Accept a stored connection invitation", - "produces" : [ "application/json" ], "parameters" : [ { - "name" : "conn_id", - "in" : "path", "description" : "Connection identifier", + "in" : "path", + "name" : "conn_id", "required" : true, - "type" : "string" + "schema" : { + "type" : "string" + } }, { - "name" : "mediation_id", - "in" : "query", "description" : "Identifier for active mediation record to be used", - "required" : false, - "type" : "string", - "pattern" : "[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-4[a-fA-F0-9]{3}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}" - }, { - "name" : "my_endpoint", "in" : "query", - "description" : "My URL endpoint", - "required" : false, - "type" : "string", - "pattern" : "^[A-Za-z0-9\\.\\-\\+]+://([A-Za-z0-9][.A-Za-z0-9-_]+[A-Za-z0-9])+(:[1-9][0-9]*)?(/[^?&#]+)?$" + "name" : "mediation_id", + "schema" : { + "pattern" : "[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-4[a-fA-F0-9]{3}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}", + "type" : "string" + } }, { - "name" : "my_label", + "description" : "My URL endpoint", "in" : "query", + "name" : "my_endpoint", + "schema" : { + "pattern" : "^[A-Za-z0-9\\.\\-\\+]+://([A-Za-z0-9][.A-Za-z0-9-_]+[A-Za-z0-9])+(:[1-9][0-9]*)?(/[^?&#]+)?$", + "type" : "string" + } + }, { "description" : "Label for connection", - "required" : false, - "type" : "string" + "in" : "query", + "name" : "my_label", + "schema" : { + "type" : "string" + } } ], "responses" : { "200" : { - "description" : "", - "schema" : { - "$ref" : "#/definitions/ConnRecord" - } + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/ConnRecord" + } + } + }, + "description" : "" } - } + }, + "summary" : "Accept a stored connection invitation", + "tags" : [ "connection" ] } }, "/connections/{conn_id}/accept-request" : { "post" : { - "tags" : [ "connection" ], - "summary" : "Accept a stored connection request", - "produces" : [ "application/json" ], "parameters" : [ { - "name" : "conn_id", - "in" : "path", "description" : "Connection identifier", + "in" : "path", + "name" : "conn_id", "required" : true, - "type" : "string" + "schema" : { + "type" : "string" + } }, { - "name" : "my_endpoint", - "in" : "query", "description" : "My URL endpoint", - "required" : false, - "type" : "string", - "pattern" : "^[A-Za-z0-9\\.\\-\\+]+://([A-Za-z0-9][.A-Za-z0-9-_]+[A-Za-z0-9])+(:[1-9][0-9]*)?(/[^?&#]+)?$" + "in" : "query", + "name" : "my_endpoint", + "schema" : { + "pattern" : "^[A-Za-z0-9\\.\\-\\+]+://([A-Za-z0-9][.A-Za-z0-9-_]+[A-Za-z0-9])+(:[1-9][0-9]*)?(/[^?&#]+)?$", + "type" : "string" + } } ], "responses" : { "200" : { - "description" : "", - "schema" : { - "$ref" : "#/definitions/ConnRecord" - } + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/ConnRecord" + } + } + }, + "description" : "" } - } + }, + "summary" : "Accept a stored connection request", + "tags" : [ "connection" ] } }, "/connections/{conn_id}/endpoints" : { "get" : { - "tags" : [ "connection" ], - "summary" : "Fetch connection remote endpoint", - "produces" : [ "application/json" ], "parameters" : [ { - "name" : "conn_id", - "in" : "path", "description" : "Connection identifier", + "in" : "path", + "name" : "conn_id", "required" : true, - "type" : "string" + "schema" : { + "type" : "string" + } } ], "responses" : { "200" : { - "description" : "", - "schema" : { - "$ref" : "#/definitions/EndpointsResult" - } + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/EndpointsResult" + } + } + }, + "description" : "" } - } + }, + "summary" : "Fetch connection remote endpoint", + "tags" : [ "connection" ] } }, "/connections/{conn_id}/establish-inbound/{ref_id}" : { "post" : { - "tags" : [ "connection" ], - "summary" : "Assign another connection as the inbound connection", - "produces" : [ "application/json" ], "parameters" : [ { - "name" : "conn_id", - "in" : "path", "description" : "Connection identifier", + "in" : "path", + "name" : "conn_id", "required" : true, - "type" : "string" + "schema" : { + "type" : "string" + } }, { - "name" : "ref_id", - "in" : "path", "description" : "Inbound connection identifier", + "in" : "path", + "name" : "ref_id", "required" : true, - "type" : "string" + "schema" : { + "type" : "string" + } } ], "responses" : { "200" : { - "description" : "", - "schema" : { - "$ref" : "#/definitions/ConnectionModuleResponse" - } + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/ConnectionModuleResponse" + } + } + }, + "description" : "" } - } + }, + "summary" : "Assign another connection as the inbound connection", + "tags" : [ "connection" ] } }, "/connections/{conn_id}/metadata" : { "get" : { - "tags" : [ "connection" ], - "summary" : "Fetch connection metadata", - "produces" : [ "application/json" ], "parameters" : [ { - "name" : "conn_id", - "in" : "path", "description" : "Connection identifier", + "in" : "path", + "name" : "conn_id", "required" : true, - "type" : "string" + "schema" : { + "type" : "string" + } }, { - "name" : "key", - "in" : "query", "description" : "Key to retrieve.", - "required" : false, - "type" : "string" + "in" : "query", + "name" : "key", + "schema" : { + "type" : "string" + } } ], "responses" : { "200" : { - "description" : "", - "schema" : { - "$ref" : "#/definitions/ConnectionMetadata" - } + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/ConnectionMetadata" + } + } + }, + "description" : "" } - } + }, + "summary" : "Fetch connection metadata", + "tags" : [ "connection" ] }, "post" : { - "tags" : [ "connection" ], - "summary" : "Set connection metadata", - "produces" : [ "application/json" ], "parameters" : [ { - "in" : "body", - "name" : "body", - "required" : false, - "schema" : { - "$ref" : "#/definitions/ConnectionMetadataSetRequest" - } - }, { - "name" : "conn_id", - "in" : "path", "description" : "Connection identifier", + "in" : "path", + "name" : "conn_id", "required" : true, - "type" : "string" + "schema" : { + "type" : "string" + } } ], + "requestBody" : { + "content" : { + "*/*" : { + "schema" : { + "$ref" : "#/components/schemas/ConnectionMetadataSetRequest" + } + } + }, + "required" : false + }, "responses" : { "200" : { - "description" : "", - "schema" : { - "$ref" : "#/definitions/ConnectionMetadata" - } + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/ConnectionMetadata" + } + } + }, + "description" : "" } - } + }, + "summary" : "Set connection metadata", + "tags" : [ "connection" ], + "x-codegen-request-body-name" : "body" } }, "/connections/{conn_id}/send-message" : { "post" : { - "tags" : [ "basicmessage" ], - "summary" : "Send a basic message to a connection", - "produces" : [ "application/json" ], "parameters" : [ { - "in" : "body", - "name" : "body", - "required" : false, - "schema" : { - "$ref" : "#/definitions/SendMessage" - } - }, { - "name" : "conn_id", - "in" : "path", "description" : "Connection identifier", + "in" : "path", + "name" : "conn_id", "required" : true, - "type" : "string" + "schema" : { + "type" : "string" + } } ], + "requestBody" : { + "content" : { + "*/*" : { + "schema" : { + "$ref" : "#/components/schemas/SendMessage" + } + } + }, + "required" : false + }, "responses" : { "200" : { - "description" : "", - "schema" : { - "$ref" : "#/definitions/BasicMessageModuleResponse" - } + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/BasicMessageModuleResponse" + } + } + }, + "description" : "" } - } + }, + "summary" : "Send a basic message to a connection", + "tags" : [ "basicmessage" ], + "x-codegen-request-body-name" : "body" } }, "/connections/{conn_id}/send-ping" : { "post" : { - "tags" : [ "trustping" ], - "summary" : "Send a trust ping to a connection", - "produces" : [ "application/json" ], "parameters" : [ { - "in" : "body", - "name" : "body", - "required" : false, - "schema" : { - "$ref" : "#/definitions/PingRequest" - } - }, { - "name" : "conn_id", - "in" : "path", "description" : "Connection identifier", + "in" : "path", + "name" : "conn_id", "required" : true, - "type" : "string" + "schema" : { + "type" : "string" + } } ], + "requestBody" : { + "content" : { + "*/*" : { + "schema" : { + "$ref" : "#/components/schemas/PingRequest" + } + } + }, + "required" : false + }, "responses" : { "200" : { - "description" : "", - "schema" : { - "$ref" : "#/definitions/PingRequestResponse" - } + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/PingRequestResponse" + } + } + }, + "description" : "" } - } + }, + "summary" : "Send a trust ping to a connection", + "tags" : [ "trustping" ], + "x-codegen-request-body-name" : "body" } }, "/connections/{conn_id}/start-introduction" : { "post" : { - "tags" : [ "introduction" ], - "summary" : "Start an introduction between two connections", - "produces" : [ "application/json" ], "parameters" : [ { - "name" : "conn_id", - "in" : "path", "description" : "Connection identifier", + "in" : "path", + "name" : "conn_id", "required" : true, - "type" : "string" + "schema" : { + "type" : "string" + } }, { - "name" : "target_connection_id", - "in" : "query", "description" : "Target connection identifier", + "in" : "query", + "name" : "target_connection_id", "required" : true, - "type" : "string" + "schema" : { + "type" : "string" + } }, { - "name" : "message", - "in" : "query", "description" : "Message", - "required" : false, - "type" : "string" + "in" : "query", + "name" : "message", + "schema" : { + "type" : "string" + } } ], "responses" : { "200" : { - "description" : "", - "schema" : { - "$ref" : "#/definitions/IntroModuleResponse" - } + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/IntroModuleResponse" + } + } + }, + "description" : "" } - } + }, + "summary" : "Start an introduction between two connections", + "tags" : [ "introduction" ] } }, "/credential-definitions" : { "post" : { - "tags" : [ "credential-definition" ], - "summary" : "Sends a credential definition to the ledger", - "produces" : [ "application/json" ], "parameters" : [ { - "in" : "body", - "name" : "body", - "required" : false, + "description" : "Connection identifier", + "in" : "query", + "name" : "conn_id", "schema" : { - "$ref" : "#/definitions/CredentialDefinitionSendRequest" + "type" : "string" } }, { - "name" : "conn_id", + "description" : "Create Transaction For Endorser's signature", "in" : "query", - "description" : "Connection identifier", - "required" : false, - "type" : "string" - }, { "name" : "create_transaction_for_endorser", - "in" : "query", - "description" : "Create Transaction For Endorser's signature", - "required" : false, - "type" : "boolean" + "schema" : { + "type" : "boolean" + } } ], + "requestBody" : { + "content" : { + "*/*" : { + "schema" : { + "$ref" : "#/components/schemas/CredentialDefinitionSendRequest" + } + } + }, + "required" : false + }, "responses" : { "200" : { - "description" : "", - "schema" : { - "$ref" : "#/definitions/TxnOrCredentialDefinitionSendResult" - } + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/TxnOrCredentialDefinitionSendResult" + } + } + }, + "description" : "" } - } + }, + "summary" : "Sends a credential definition to the ledger", + "tags" : [ "credential-definition" ], + "x-codegen-request-body-name" : "body" } }, "/credential-definitions/created" : { "get" : { - "tags" : [ "credential-definition" ], - "summary" : "Search for matching credential definitions that agent originated", - "produces" : [ "application/json" ], "parameters" : [ { - "name" : "cred_def_id", - "in" : "query", "description" : "Credential definition id", - "required" : false, - "type" : "string", - "pattern" : "^([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}):3:CL:(([1-9][0-9]*)|([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}:2:.+:[0-9.]+)):(.+)?$" - }, { - "name" : "issuer_did", "in" : "query", - "description" : "Issuer DID", - "required" : false, - "type" : "string", - "pattern" : "^(did:sov:)?[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}$" + "name" : "cred_def_id", + "schema" : { + "pattern" : "^([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}):3:CL:(([1-9][0-9]*)|([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}:2:.+:[0-9.]+)):(.+)?$", + "type" : "string" + } }, { - "name" : "schema_id", + "description" : "Issuer DID", "in" : "query", - "description" : "Schema identifier", - "required" : false, - "type" : "string", - "pattern" : "^[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}:2:.+:[0-9.]+$" + "name" : "issuer_did", + "schema" : { + "pattern" : "^(did:sov:)?[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}$", + "type" : "string" + } }, { - "name" : "schema_issuer_did", + "description" : "Schema identifier", "in" : "query", - "description" : "Schema issuer DID", - "required" : false, - "type" : "string", - "pattern" : "^(did:sov:)?[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}$" + "name" : "schema_id", + "schema" : { + "pattern" : "^[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}:2:.+:[0-9.]+$", + "type" : "string" + } }, { - "name" : "schema_name", + "description" : "Schema issuer DID", "in" : "query", - "description" : "Schema name", - "required" : false, - "type" : "string" + "name" : "schema_issuer_did", + "schema" : { + "pattern" : "^(did:sov:)?[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}$", + "type" : "string" + } }, { - "name" : "schema_version", + "description" : "Schema name", "in" : "query", + "name" : "schema_name", + "schema" : { + "type" : "string" + } + }, { "description" : "Schema version", - "required" : false, - "type" : "string", - "pattern" : "^[0-9.]+$" + "in" : "query", + "name" : "schema_version", + "schema" : { + "pattern" : "^[0-9.]+$", + "type" : "string" + } } ], "responses" : { "200" : { - "description" : "", - "schema" : { - "$ref" : "#/definitions/CredentialDefinitionsCreatedResult" - } + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/CredentialDefinitionsCreatedResult" + } + } + }, + "description" : "" } - } + }, + "summary" : "Search for matching credential definitions that agent originated", + "tags" : [ "credential-definition" ] } }, "/credential-definitions/{cred_def_id}" : { "get" : { - "tags" : [ "credential-definition" ], - "summary" : "Gets a credential definition from the ledger", - "produces" : [ "application/json" ], "parameters" : [ { - "name" : "cred_def_id", - "in" : "path", "description" : "Credential definition identifier", + "in" : "path", + "name" : "cred_def_id", "required" : true, - "type" : "string", - "pattern" : "^([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}):3:CL:(([1-9][0-9]*)|([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}:2:.+:[0-9.]+)):(.+)?$" + "schema" : { + "pattern" : "^([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}):3:CL:(([1-9][0-9]*)|([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}:2:.+:[0-9.]+)):(.+)?$", + "type" : "string" + } } ], "responses" : { "200" : { - "description" : "", - "schema" : { - "$ref" : "#/definitions/CredentialDefinitionGetResult" - } + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/CredentialDefinitionGetResult" + } + } + }, + "description" : "" } - } + }, + "summary" : "Gets a credential definition from the ledger", + "tags" : [ "credential-definition" ] } }, "/credential-definitions/{cred_def_id}/write_record" : { "post" : { - "tags" : [ "credential-definition" ], - "summary" : "Writes a credential definition non-secret record to the wallet", - "produces" : [ "application/json" ], "parameters" : [ { - "name" : "cred_def_id", - "in" : "path", "description" : "Credential definition identifier", + "in" : "path", + "name" : "cred_def_id", "required" : true, - "type" : "string", - "pattern" : "^([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}):3:CL:(([1-9][0-9]*)|([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}:2:.+:[0-9.]+)):(.+)?$" + "schema" : { + "pattern" : "^([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}):3:CL:(([1-9][0-9]*)|([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}:2:.+:[0-9.]+)):(.+)?$", + "type" : "string" + } } ], "responses" : { "200" : { - "description" : "", - "schema" : { - "$ref" : "#/definitions/CredentialDefinitionGetResult" - } + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/CredentialDefinitionGetResult" + } + } + }, + "description" : "" } - } + }, + "summary" : "Writes a credential definition non-secret record to the wallet", + "tags" : [ "credential-definition" ] } }, "/credential/mime-types/{credential_id}" : { "get" : { - "tags" : [ "credentials" ], - "summary" : "Get attribute MIME types from wallet", - "produces" : [ "application/json" ], "parameters" : [ { - "name" : "credential_id", - "in" : "path", "description" : "Credential identifier", + "in" : "path", + "name" : "credential_id", "required" : true, - "type" : "string" + "schema" : { + "type" : "string" + } } ], "responses" : { "200" : { - "description" : "", - "schema" : { - "$ref" : "#/definitions/AttributeMimeTypesResult" - } + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/AttributeMimeTypesResult" + } + } + }, + "description" : "" } - } + }, + "summary" : "Get attribute MIME types from wallet", + "tags" : [ "credentials" ] } }, "/credential/revoked/{credential_id}" : { "get" : { - "tags" : [ "credentials" ], - "summary" : "Query credential revocation status by id", - "produces" : [ "application/json" ], "parameters" : [ { - "name" : "credential_id", - "in" : "path", "description" : "Credential identifier", + "in" : "path", + "name" : "credential_id", "required" : true, - "type" : "string" + "schema" : { + "type" : "string" + } }, { - "name" : "from", - "in" : "query", "description" : "Earliest epoch of revocation status interval of interest", - "required" : false, - "type" : "string", - "pattern" : "^[0-9]*$" - }, { - "name" : "to", "in" : "query", + "name" : "from", + "schema" : { + "pattern" : "^[0-9]*$", + "type" : "string" + } + }, { "description" : "Latest epoch of revocation status interval of interest", - "required" : false, - "type" : "string", - "pattern" : "^[0-9]*$" - } ], + "in" : "query", + "name" : "to", + "schema" : { + "pattern" : "^[0-9]*$", + "type" : "string" + } + } ], "responses" : { "200" : { - "description" : "", - "schema" : { - "$ref" : "#/definitions/CredRevokedResult" - } + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/CredRevokedResult" + } + } + }, + "description" : "" } - } + }, + "summary" : "Query credential revocation status by id", + "tags" : [ "credentials" ] } }, "/credential/w3c/{credential_id}" : { - "get" : { - "tags" : [ "credentials" ], - "summary" : "Fetch W3C credential from wallet by id", - "produces" : [ "application/json" ], + "delete" : { "parameters" : [ { - "name" : "credential_id", - "in" : "path", "description" : "Credential identifier", + "in" : "path", + "name" : "credential_id", "required" : true, - "type" : "string" + "schema" : { + "type" : "string" + } } ], "responses" : { "200" : { - "description" : "", - "schema" : { - "$ref" : "#/definitions/VCRecord" - } + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/HolderModuleResponse" + } + } + }, + "description" : "" } - } - }, - "delete" : { - "tags" : [ "credentials" ], + }, "summary" : "Remove W3C credential from wallet by id", - "produces" : [ "application/json" ], + "tags" : [ "credentials" ] + }, + "get" : { "parameters" : [ { - "name" : "credential_id", - "in" : "path", "description" : "Credential identifier", + "in" : "path", + "name" : "credential_id", "required" : true, - "type" : "string" + "schema" : { + "type" : "string" + } } ], "responses" : { "200" : { - "description" : "", - "schema" : { - "$ref" : "#/definitions/HolderModuleResponse" - } + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/VCRecord" + } + } + }, + "description" : "" } - } + }, + "summary" : "Fetch W3C credential from wallet by id", + "tags" : [ "credentials" ] } }, "/credential/{credential_id}" : { - "get" : { - "tags" : [ "credentials" ], - "summary" : "Fetch credential from wallet by id", - "produces" : [ "application/json" ], + "delete" : { "parameters" : [ { - "name" : "credential_id", - "in" : "path", "description" : "Credential identifier", + "in" : "path", + "name" : "credential_id", "required" : true, - "type" : "string" + "schema" : { + "type" : "string" + } } ], "responses" : { "200" : { - "description" : "", - "schema" : { - "$ref" : "#/definitions/IndyCredInfo" - } + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/HolderModuleResponse" + } + } + }, + "description" : "" } - } - }, - "delete" : { - "tags" : [ "credentials" ], + }, "summary" : "Remove credential from wallet by id", - "produces" : [ "application/json" ], + "tags" : [ "credentials" ] + }, + "get" : { "parameters" : [ { - "name" : "credential_id", - "in" : "path", "description" : "Credential identifier", + "in" : "path", + "name" : "credential_id", "required" : true, - "type" : "string" + "schema" : { + "type" : "string" + } } ], "responses" : { "200" : { - "description" : "", - "schema" : { - "$ref" : "#/definitions/HolderModuleResponse" - } + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/IndyCredInfo" + } + } + }, + "description" : "" } - } + }, + "summary" : "Fetch credential from wallet by id", + "tags" : [ "credentials" ] } }, "/credentials" : { "get" : { - "tags" : [ "credentials" ], - "summary" : "Fetch credentials from wallet", - "produces" : [ "application/json" ], "parameters" : [ { - "name" : "count", - "in" : "query", "description" : "Maximum number to retrieve", - "required" : false, - "type" : "string", - "pattern" : "^[1-9][0-9]*$" - }, { - "name" : "start", "in" : "query", - "description" : "Start index", - "required" : false, - "type" : "string", - "pattern" : "^[0-9]*$" + "name" : "count", + "schema" : { + "pattern" : "^[1-9][0-9]*$", + "type" : "string" + } }, { - "name" : "wql", + "description" : "Start index", "in" : "query", + "name" : "start", + "schema" : { + "pattern" : "^[0-9]*$", + "type" : "string" + } + }, { "description" : "(JSON) WQL query", - "required" : false, - "type" : "string", - "pattern" : "^{.*}$" + "in" : "query", + "name" : "wql", + "schema" : { + "pattern" : "^{.*}$", + "type" : "string" + } } ], "responses" : { "200" : { - "description" : "", - "schema" : { - "$ref" : "#/definitions/CredInfoList" - } + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/CredInfoList" + } + } + }, + "description" : "" } - } + }, + "summary" : "Fetch credentials from wallet", + "tags" : [ "credentials" ] } }, "/credentials/w3c" : { "post" : { - "tags" : [ "credentials" ], - "summary" : "Fetch W3C credentials from wallet", - "produces" : [ "application/json" ], "parameters" : [ { - "in" : "body", - "name" : "body", - "required" : false, + "description" : "Maximum number to retrieve", + "in" : "query", + "name" : "count", "schema" : { - "$ref" : "#/definitions/W3CCredentialsListRequest" + "pattern" : "^[1-9][0-9]*$", + "type" : "string" } }, { - "name" : "count", + "description" : "Start index", "in" : "query", - "description" : "Maximum number to retrieve", - "required" : false, - "type" : "string", - "pattern" : "^[1-9][0-9]*$" - }, { "name" : "start", - "in" : "query", - "description" : "Start index", - "required" : false, - "type" : "string", - "pattern" : "^[0-9]*$" + "schema" : { + "pattern" : "^[0-9]*$", + "type" : "string" + } }, { - "name" : "wql", - "in" : "query", "description" : "(JSON) WQL query", - "required" : false, - "type" : "string", - "pattern" : "^{.*}$" + "in" : "query", + "name" : "wql", + "schema" : { + "pattern" : "^{.*}$", + "type" : "string" + } } ], + "requestBody" : { + "content" : { + "*/*" : { + "schema" : { + "$ref" : "#/components/schemas/W3CCredentialsListRequest" + } + } + }, + "required" : false + }, "responses" : { "200" : { - "description" : "", - "schema" : { - "$ref" : "#/definitions/VCRecordList" - } + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/VCRecordList" + } + } + }, + "description" : "" } - } + }, + "summary" : "Fetch W3C credentials from wallet", + "tags" : [ "credentials" ], + "x-codegen-request-body-name" : "body" } }, "/didexchange/create-request" : { "post" : { - "tags" : [ "did-exchange" ], - "summary" : "Create and send a request against public DID's implicit invitation", - "produces" : [ "application/json" ], "parameters" : [ { - "name" : "their_public_did", - "in" : "query", "description" : "Qualified public DID to which to request connection", + "in" : "query", + "name" : "their_public_did", "required" : true, - "type" : "string", - "pattern" : "^(did:sov:)?[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}$|^did:([a-zA-Z0-9_]+):([a-zA-Z0-9_.%-]+(:[a-zA-Z0-9_.%-]+)*)((;[a-zA-Z0-9_.:%-]+=[a-zA-Z0-9_.:%-]*)*)(\\/[^#?]*)?([?][^#]*)?(\\#.*)?$$" + "schema" : { + "pattern" : "^(did:sov:)?[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}$|^did:([a-zA-Z0-9_]+):([a-zA-Z0-9_.%-]+(:[a-zA-Z0-9_.%-]+)*)((;[a-zA-Z0-9_.:%-]+=[a-zA-Z0-9_.:%-]*)*)(\\/[^#?]*)?([?][^#]*)?(\\#.*)?$$", + "type" : "string" + } }, { - "name" : "mediation_id", + "description" : "Alias for connection", "in" : "query", - "description" : "Identifier for active mediation record to be used", - "required" : false, - "type" : "string", - "pattern" : "[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-4[a-fA-F0-9]{3}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}" + "name" : "alias", + "schema" : { + "type" : "string" + } }, { - "name" : "my_endpoint", + "description" : "Identifier for active mediation record to be used", "in" : "query", - "description" : "My URL endpoint", - "required" : false, - "type" : "string", - "pattern" : "^[A-Za-z0-9\\.\\-\\+]+://([A-Za-z0-9][.A-Za-z0-9-_]+[A-Za-z0-9])+(:[1-9][0-9]*)?(/[^?&#]+)?$" + "name" : "mediation_id", + "schema" : { + "pattern" : "[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-4[a-fA-F0-9]{3}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}", + "type" : "string" + } }, { - "name" : "my_label", + "description" : "My URL endpoint", "in" : "query", - "description" : "Label for connection request", - "required" : false, - "type" : "string" + "name" : "my_endpoint", + "schema" : { + "pattern" : "^[A-Za-z0-9\\.\\-\\+]+://([A-Za-z0-9][.A-Za-z0-9-_]+[A-Za-z0-9])+(:[1-9][0-9]*)?(/[^?&#]+)?$", + "type" : "string" + } }, { - "name" : "use_public_did", + "description" : "Label for connection request", "in" : "query", + "name" : "my_label", + "schema" : { + "type" : "string" + } + }, { "description" : "Use public DID for this connection", - "required" : false, - "type" : "boolean" + "in" : "query", + "name" : "use_public_did", + "schema" : { + "type" : "boolean" + } } ], "responses" : { "200" : { - "description" : "", - "schema" : { - "$ref" : "#/definitions/ConnRecord" - } + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/ConnRecord" + } + } + }, + "description" : "" } - } + }, + "summary" : "Create and send a request against public DID's implicit invitation", + "tags" : [ "did-exchange" ] } }, "/didexchange/receive-request" : { "post" : { - "tags" : [ "did-exchange" ], - "summary" : "Receive request against public DID's implicit invitation", - "produces" : [ "application/json" ], "parameters" : [ { - "in" : "body", - "name" : "body", - "required" : false, + "description" : "Alias for connection", + "in" : "query", + "name" : "alias", "schema" : { - "$ref" : "#/definitions/DIDXRequest" + "type" : "string" } }, { - "name" : "alias", + "description" : "Auto-accept connection (defaults to configuration)", "in" : "query", - "description" : "Alias for connection", - "required" : false, - "type" : "string" - }, { "name" : "auto_accept", - "in" : "query", - "description" : "Auto-accept connection (defaults to configuration)", - "required" : false, - "type" : "boolean" + "schema" : { + "type" : "boolean" + } }, { - "name" : "mediation_id", - "in" : "query", "description" : "Identifier for active mediation record to be used", - "required" : false, - "type" : "string", - "pattern" : "[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-4[a-fA-F0-9]{3}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}" - }, { - "name" : "my_endpoint", "in" : "query", + "name" : "mediation_id", + "schema" : { + "pattern" : "[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-4[a-fA-F0-9]{3}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}", + "type" : "string" + } + }, { "description" : "My URL endpoint", - "required" : false, - "type" : "string", - "pattern" : "^[A-Za-z0-9\\.\\-\\+]+://([A-Za-z0-9][.A-Za-z0-9-_]+[A-Za-z0-9])+(:[1-9][0-9]*)?(/[^?&#]+)?$" + "in" : "query", + "name" : "my_endpoint", + "schema" : { + "pattern" : "^[A-Za-z0-9\\.\\-\\+]+://([A-Za-z0-9][.A-Za-z0-9-_]+[A-Za-z0-9])+(:[1-9][0-9]*)?(/[^?&#]+)?$", + "type" : "string" + } } ], + "requestBody" : { + "content" : { + "*/*" : { + "schema" : { + "$ref" : "#/components/schemas/DIDXRequest" + } + } + }, + "required" : false + }, "responses" : { "200" : { - "description" : "", - "schema" : { - "$ref" : "#/definitions/ConnRecord" - } + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/ConnRecord" + } + } + }, + "description" : "" } - } + }, + "summary" : "Receive request against public DID's implicit invitation", + "tags" : [ "did-exchange" ], + "x-codegen-request-body-name" : "body" } }, "/didexchange/{conn_id}/accept-invitation" : { "post" : { - "tags" : [ "did-exchange" ], - "summary" : "Accept a stored connection invitation", - "produces" : [ "application/json" ], "parameters" : [ { - "name" : "conn_id", - "in" : "path", "description" : "Connection identifier", + "in" : "path", + "name" : "conn_id", "required" : true, - "type" : "string" + "schema" : { + "type" : "string" + } }, { - "name" : "my_endpoint", - "in" : "query", "description" : "My URL endpoint", - "required" : false, - "type" : "string", - "pattern" : "^[A-Za-z0-9\\.\\-\\+]+://([A-Za-z0-9][.A-Za-z0-9-_]+[A-Za-z0-9])+(:[1-9][0-9]*)?(/[^?&#]+)?$" - }, { - "name" : "my_label", "in" : "query", + "name" : "my_endpoint", + "schema" : { + "pattern" : "^[A-Za-z0-9\\.\\-\\+]+://([A-Za-z0-9][.A-Za-z0-9-_]+[A-Za-z0-9])+(:[1-9][0-9]*)?(/[^?&#]+)?$", + "type" : "string" + } + }, { "description" : "Label for connection request", - "required" : false, - "type" : "string" + "in" : "query", + "name" : "my_label", + "schema" : { + "type" : "string" + } } ], "responses" : { "200" : { - "description" : "", - "schema" : { - "$ref" : "#/definitions/ConnRecord" - } + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/ConnRecord" + } + } + }, + "description" : "" } - } + }, + "summary" : "Accept a stored connection invitation", + "tags" : [ "did-exchange" ] } }, "/didexchange/{conn_id}/accept-request" : { "post" : { - "tags" : [ "did-exchange" ], - "summary" : "Accept a stored connection request", - "produces" : [ "application/json" ], "parameters" : [ { - "name" : "conn_id", - "in" : "path", "description" : "Connection identifier", + "in" : "path", + "name" : "conn_id", "required" : true, - "type" : "string" + "schema" : { + "type" : "string" + } }, { - "name" : "mediation_id", - "in" : "query", "description" : "Identifier for active mediation record to be used", - "required" : false, - "type" : "string", - "pattern" : "[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-4[a-fA-F0-9]{3}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}" - }, { - "name" : "my_endpoint", "in" : "query", + "name" : "mediation_id", + "schema" : { + "pattern" : "[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-4[a-fA-F0-9]{3}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}", + "type" : "string" + } + }, { "description" : "My URL endpoint", - "required" : false, - "type" : "string", - "pattern" : "^[A-Za-z0-9\\.\\-\\+]+://([A-Za-z0-9][.A-Za-z0-9-_]+[A-Za-z0-9])+(:[1-9][0-9]*)?(/[^?&#]+)?$" + "in" : "query", + "name" : "my_endpoint", + "schema" : { + "pattern" : "^[A-Za-z0-9\\.\\-\\+]+://([A-Za-z0-9][.A-Za-z0-9-_]+[A-Za-z0-9])+(:[1-9][0-9]*)?(/[^?&#]+)?$", + "type" : "string" + } } ], "responses" : { "200" : { - "description" : "", - "schema" : { - "$ref" : "#/definitions/ConnRecord" - } + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/ConnRecord" + } + } + }, + "description" : "" } - } + }, + "summary" : "Accept a stored connection request", + "tags" : [ "did-exchange" ] } }, - "/features" : { + "/discover-features-2.0/queries" : { "get" : { - "tags" : [ "server" ], + "parameters" : [ { + "description" : "Connection identifier, if none specified, then the query will provide features for this agent.", + "in" : "query", + "name" : "connection_id", + "schema" : { + "type" : "string" + } + }, { + "description" : "Goal-code feature-type query", + "in" : "query", + "name" : "query_goal_code", + "schema" : { + "type" : "string" + } + }, { + "description" : "Protocol feature-type query", + "in" : "query", + "name" : "query_protocol", + "schema" : { + "type" : "string" + } + } ], + "responses" : { + "200" : { + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/V20DiscoveryExchangeResult" + } + } + }, + "description" : "" + } + }, "summary" : "Query supported features", - "produces" : [ "application/json" ], + "tags" : [ "discover-features v2.0" ] + } + }, + "/discover-features-2.0/records" : { + "get" : { "parameters" : [ { - "name" : "query", + "description" : "Connection identifier", "in" : "query", - "description" : "Query", - "required" : false, - "type" : "string" + "name" : "connection_id", + "schema" : { + "type" : "string" + } } ], "responses" : { "200" : { - "description" : "", - "schema" : { - "$ref" : "#/definitions/QueryResult" - } + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/V20DiscoveryExchangeListResult" + } + } + }, + "description" : "" } - } + }, + "summary" : "Discover Features v2.0 records", + "tags" : [ "discover-features v2.0" ] } }, - "/issue-credential-2.0/create" : { - "post" : { - "tags" : [ "issue-credential v2.0" ], - "summary" : "Create credential from attribute values", - "produces" : [ "application/json" ], + "/discover-features/query" : { + "get" : { "parameters" : [ { - "in" : "body", - "name" : "body", - "required" : false, + "description" : "Comment", + "in" : "query", + "name" : "comment", "schema" : { - "$ref" : "#/definitions/V20IssueCredSchemaCore" + "type" : "string" + } + }, { + "description" : "Connection identifier, if none specified, then the query will provide features for this agent.", + "in" : "query", + "name" : "connection_id", + "schema" : { + "type" : "string" + } + }, { + "description" : "Protocol feature query", + "in" : "query", + "name" : "query", + "schema" : { + "type" : "string" } } ], "responses" : { "200" : { - "description" : "", - "schema" : { - "$ref" : "#/definitions/V20CredExRecord" - } + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/V10DiscoveryRecord" + } + } + }, + "description" : "" } - } + }, + "summary" : "Query supported features", + "tags" : [ "discover-features" ] } }, - "/issue-credential-2.0/create-offer" : { - "post" : { - "tags" : [ "issue-credential v2.0" ], - "summary" : "Create a credential offer, independent of any proposal or connection", - "produces" : [ "application/json" ], + "/discover-features/records" : { + "get" : { "parameters" : [ { - "in" : "body", - "name" : "body", - "required" : false, + "description" : "Connection identifier", + "in" : "query", + "name" : "connection_id", "schema" : { - "$ref" : "#/definitions/V20CredOfferConnFreeRequest" + "type" : "string" } } ], "responses" : { "200" : { - "description" : "", - "schema" : { - "$ref" : "#/definitions/V20CredExRecord" + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/V10DiscoveryExchangeListResult" + } + } + }, + "description" : "" + } + }, + "summary" : "Discover Features records", + "tags" : [ "discover-features" ] + } + }, + "/issue-credential-2.0/create" : { + "post" : { + "requestBody" : { + "content" : { + "*/*" : { + "schema" : { + "$ref" : "#/components/schemas/V20IssueCredSchemaCore" + } } + }, + "required" : false + }, + "responses" : { + "200" : { + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/V20CredExRecord" + } + } + }, + "description" : "" } - } + }, + "summary" : "Create a credential record without sending (generally for use with Out-Of-Band)", + "tags" : [ "issue-credential v2.0" ], + "x-codegen-request-body-name" : "body" + } + }, + "/issue-credential-2.0/create-offer" : { + "post" : { + "requestBody" : { + "content" : { + "*/*" : { + "schema" : { + "$ref" : "#/components/schemas/V20CredOfferConnFreeRequest" + } + } + }, + "required" : false + }, + "responses" : { + "200" : { + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/V20CredExRecord" + } + } + }, + "description" : "" + } + }, + "summary" : "Create a credential offer, independent of any proposal or connection", + "tags" : [ "issue-credential v2.0" ], + "x-codegen-request-body-name" : "body" } }, "/issue-credential-2.0/records" : { "get" : { - "tags" : [ "issue-credential v2.0" ], - "summary" : "Fetch all credential exchange records", - "produces" : [ "application/json" ], "parameters" : [ { - "name" : "connection_id", - "in" : "query", "description" : "Connection identifier", - "required" : false, - "type" : "string", - "format" : "uuid" - }, { - "name" : "role", "in" : "query", - "description" : "Role assigned in credential exchange", - "required" : false, - "type" : "string", - "enum" : [ "issuer", "holder" ] + "name" : "connection_id", + "schema" : { + "format" : "uuid", + "type" : "string" + } }, { - "name" : "state", + "description" : "Role assigned in credential exchange", "in" : "query", - "description" : "Credential exchange state", - "required" : false, - "type" : "string", - "enum" : [ "proposal-sent", "proposal-received", "offer-sent", "offer-received", "request-sent", "request-received", "credential-issued", "credential-received", "done" ] + "name" : "role", + "schema" : { + "enum" : [ "issuer", "holder" ], + "type" : "string" + } }, { - "name" : "thread_id", + "description" : "Credential exchange state", "in" : "query", + "name" : "state", + "schema" : { + "enum" : [ "proposal-sent", "proposal-received", "offer-sent", "offer-received", "request-sent", "request-received", "credential-issued", "credential-received", "done", "credential-revoked", "abandoned" ], + "type" : "string" + } + }, { "description" : "Thread identifier", - "required" : false, - "type" : "string", - "format" : "uuid" + "in" : "query", + "name" : "thread_id", + "schema" : { + "format" : "uuid", + "type" : "string" + } } ], "responses" : { "200" : { - "description" : "", - "schema" : { - "$ref" : "#/definitions/V20CredExRecordListResult" - } + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/V20CredExRecordListResult" + } + } + }, + "description" : "" } - } + }, + "summary" : "Fetch all credential exchange records", + "tags" : [ "issue-credential v2.0" ] } }, "/issue-credential-2.0/records/{cred_ex_id}" : { - "get" : { - "tags" : [ "issue-credential v2.0" ], - "summary" : "Fetch a single credential exchange record", - "produces" : [ "application/json" ], + "delete" : { "parameters" : [ { - "name" : "cred_ex_id", - "in" : "path", "description" : "Credential exchange identifier", + "in" : "path", + "name" : "cred_ex_id", "required" : true, - "type" : "string", - "pattern" : "[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-4[a-fA-F0-9]{3}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}" + "schema" : { + "pattern" : "[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-4[a-fA-F0-9]{3}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}", + "type" : "string" + } } ], "responses" : { "200" : { - "description" : "", - "schema" : { - "$ref" : "#/definitions/V20CredExRecordDetail" - } + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/V20IssueCredentialModuleResponse" + } + } + }, + "description" : "" } - } - }, - "delete" : { - "tags" : [ "issue-credential v2.0" ], + }, "summary" : "Remove an existing credential exchange record", - "produces" : [ "application/json" ], + "tags" : [ "issue-credential v2.0" ] + }, + "get" : { "parameters" : [ { - "name" : "cred_ex_id", - "in" : "path", "description" : "Credential exchange identifier", + "in" : "path", + "name" : "cred_ex_id", "required" : true, - "type" : "string", - "pattern" : "[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-4[a-fA-F0-9]{3}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}" + "schema" : { + "pattern" : "[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-4[a-fA-F0-9]{3}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}", + "type" : "string" + } } ], "responses" : { "200" : { - "description" : "", - "schema" : { - "$ref" : "#/definitions/V20IssueCredentialModuleResponse" - } + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/V20CredExRecordDetail" + } + } + }, + "description" : "" } - } + }, + "summary" : "Fetch a single credential exchange record", + "tags" : [ "issue-credential v2.0" ] } }, "/issue-credential-2.0/records/{cred_ex_id}/issue" : { "post" : { - "tags" : [ "issue-credential v2.0" ], - "summary" : "Send holder a credential", - "produces" : [ "application/json" ], "parameters" : [ { - "in" : "body", - "name" : "body", - "required" : false, - "schema" : { - "$ref" : "#/definitions/V20CredIssueRequest" - } - }, { - "name" : "cred_ex_id", - "in" : "path", "description" : "Credential exchange identifier", + "in" : "path", + "name" : "cred_ex_id", "required" : true, - "type" : "string", - "pattern" : "[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-4[a-fA-F0-9]{3}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}" + "schema" : { + "pattern" : "[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-4[a-fA-F0-9]{3}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}", + "type" : "string" + } } ], + "requestBody" : { + "content" : { + "*/*" : { + "schema" : { + "$ref" : "#/components/schemas/V20CredIssueRequest" + } + } + }, + "required" : false + }, "responses" : { "200" : { - "description" : "", - "schema" : { - "$ref" : "#/definitions/V20CredExRecordDetail" - } + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/V20CredExRecordDetail" + } + } + }, + "description" : "" } - } + }, + "summary" : "Send holder a credential", + "tags" : [ "issue-credential v2.0" ], + "x-codegen-request-body-name" : "body" } }, "/issue-credential-2.0/records/{cred_ex_id}/problem-report" : { "post" : { - "tags" : [ "issue-credential v2.0" ], - "summary" : "Send a problem report for credential exchange", - "produces" : [ "application/json" ], "parameters" : [ { - "in" : "body", - "name" : "body", - "required" : false, - "schema" : { - "$ref" : "#/definitions/V20CredIssueProblemReportRequest" - } - }, { - "name" : "cred_ex_id", - "in" : "path", "description" : "Credential exchange identifier", + "in" : "path", + "name" : "cred_ex_id", "required" : true, - "type" : "string", - "pattern" : "[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-4[a-fA-F0-9]{3}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}" + "schema" : { + "pattern" : "[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-4[a-fA-F0-9]{3}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}", + "type" : "string" + } } ], + "requestBody" : { + "content" : { + "*/*" : { + "schema" : { + "$ref" : "#/components/schemas/V20CredIssueProblemReportRequest" + } + } + }, + "required" : false + }, "responses" : { "200" : { - "description" : "", - "schema" : { - "$ref" : "#/definitions/V20IssueCredentialModuleResponse" - } + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/V20IssueCredentialModuleResponse" + } + } + }, + "description" : "" } - } + }, + "summary" : "Send a problem report for credential exchange", + "tags" : [ "issue-credential v2.0" ], + "x-codegen-request-body-name" : "body" } }, "/issue-credential-2.0/records/{cred_ex_id}/send-offer" : { "post" : { - "tags" : [ "issue-credential v2.0" ], - "summary" : "Send holder a credential offer in reference to a proposal with preview", - "produces" : [ "application/json" ], "parameters" : [ { - "in" : "body", - "name" : "body", - "required" : false, - "schema" : { - "$ref" : "#/definitions/V20CredBoundOfferRequest" - } - }, { - "name" : "cred_ex_id", - "in" : "path", "description" : "Credential exchange identifier", + "in" : "path", + "name" : "cred_ex_id", "required" : true, - "type" : "string", - "pattern" : "[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-4[a-fA-F0-9]{3}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}" + "schema" : { + "pattern" : "[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-4[a-fA-F0-9]{3}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}", + "type" : "string" + } } ], + "requestBody" : { + "content" : { + "*/*" : { + "schema" : { + "$ref" : "#/components/schemas/V20CredBoundOfferRequest" + } + } + }, + "required" : false + }, "responses" : { "200" : { - "description" : "", - "schema" : { - "$ref" : "#/definitions/V20CredExRecord" - } + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/V20CredExRecord" + } + } + }, + "description" : "" } - } + }, + "summary" : "Send holder a credential offer in reference to a proposal with preview", + "tags" : [ "issue-credential v2.0" ], + "x-codegen-request-body-name" : "body" } }, "/issue-credential-2.0/records/{cred_ex_id}/send-request" : { "post" : { - "tags" : [ "issue-credential v2.0" ], - "summary" : "Send issuer a credential request", - "produces" : [ "application/json" ], "parameters" : [ { - "in" : "body", - "name" : "body", - "required" : false, - "schema" : { - "$ref" : "#/definitions/V20CredRequestRequest" - } - }, { - "name" : "cred_ex_id", - "in" : "path", "description" : "Credential exchange identifier", + "in" : "path", + "name" : "cred_ex_id", "required" : true, - "type" : "string", - "pattern" : "[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-4[a-fA-F0-9]{3}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}" + "schema" : { + "pattern" : "[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-4[a-fA-F0-9]{3}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}", + "type" : "string" + } } ], + "requestBody" : { + "content" : { + "*/*" : { + "schema" : { + "$ref" : "#/components/schemas/V20CredRequestRequest" + } + } + }, + "required" : false + }, "responses" : { "200" : { - "description" : "", - "schema" : { - "$ref" : "#/definitions/V20CredExRecord" - } + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/V20CredExRecord" + } + } + }, + "description" : "" } - } + }, + "summary" : "Send issuer a credential request", + "tags" : [ "issue-credential v2.0" ], + "x-codegen-request-body-name" : "body" } }, "/issue-credential-2.0/records/{cred_ex_id}/store" : { "post" : { - "tags" : [ "issue-credential v2.0" ], - "summary" : "Store a received credential", - "produces" : [ "application/json" ], "parameters" : [ { - "in" : "body", - "name" : "body", - "required" : false, - "schema" : { - "$ref" : "#/definitions/V20CredStoreRequest" - } - }, { - "name" : "cred_ex_id", - "in" : "path", "description" : "Credential exchange identifier", + "in" : "path", + "name" : "cred_ex_id", "required" : true, - "type" : "string", - "pattern" : "[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-4[a-fA-F0-9]{3}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}" + "schema" : { + "pattern" : "[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-4[a-fA-F0-9]{3}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}", + "type" : "string" + } } ], + "requestBody" : { + "content" : { + "*/*" : { + "schema" : { + "$ref" : "#/components/schemas/V20CredStoreRequest" + } + } + }, + "required" : false + }, "responses" : { "200" : { - "description" : "", - "schema" : { - "$ref" : "#/definitions/V20CredExRecordDetail" - } + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/V20CredExRecordDetail" + } + } + }, + "description" : "" } - } + }, + "summary" : "Store a received credential", + "tags" : [ "issue-credential v2.0" ], + "x-codegen-request-body-name" : "body" } }, "/issue-credential-2.0/send" : { "post" : { - "tags" : [ "issue-credential v2.0" ], - "summary" : "Send holder a credential, automating entire flow", - "produces" : [ "application/json" ], - "parameters" : [ { - "in" : "body", - "name" : "body", - "required" : false, - "schema" : { - "$ref" : "#/definitions/V20CredExFree" - } - } ], + "requestBody" : { + "content" : { + "*/*" : { + "schema" : { + "$ref" : "#/components/schemas/V20CredExFree" + } + } + }, + "required" : false + }, "responses" : { "200" : { - "description" : "", - "schema" : { - "$ref" : "#/definitions/V20CredExRecord" - } + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/V20CredExRecord" + } + } + }, + "description" : "" } - } + }, + "summary" : "Send holder a credential, automating entire flow", + "tags" : [ "issue-credential v2.0" ], + "x-codegen-request-body-name" : "body" } }, "/issue-credential-2.0/send-offer" : { "post" : { - "tags" : [ "issue-credential v2.0" ], - "summary" : "Send holder a credential offer, independent of any proposal", - "produces" : [ "application/json" ], - "parameters" : [ { - "in" : "body", - "name" : "body", - "required" : false, - "schema" : { - "$ref" : "#/definitions/V20CredOfferRequest" - } - } ], + "requestBody" : { + "content" : { + "*/*" : { + "schema" : { + "$ref" : "#/components/schemas/V20CredOfferRequest" + } + } + }, + "required" : false + }, "responses" : { "200" : { - "description" : "", - "schema" : { - "$ref" : "#/definitions/V20CredExRecord" - } + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/V20CredExRecord" + } + } + }, + "description" : "" } - } + }, + "summary" : "Send holder a credential offer, independent of any proposal", + "tags" : [ "issue-credential v2.0" ], + "x-codegen-request-body-name" : "body" } }, "/issue-credential-2.0/send-proposal" : { "post" : { - "tags" : [ "issue-credential v2.0" ], - "summary" : "Send issuer a credential proposal", - "produces" : [ "application/json" ], - "parameters" : [ { - "in" : "body", - "name" : "body", - "required" : false, - "schema" : { - "$ref" : "#/definitions/V20CredExFree" - } - } ], + "requestBody" : { + "content" : { + "*/*" : { + "schema" : { + "$ref" : "#/components/schemas/V20CredExFree" + } + } + }, + "required" : false + }, "responses" : { "200" : { - "description" : "", - "schema" : { - "$ref" : "#/definitions/V20CredExRecord" - } + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/V20CredExRecord" + } + } + }, + "description" : "" } - } + }, + "summary" : "Send issuer a credential proposal", + "tags" : [ "issue-credential v2.0" ], + "x-codegen-request-body-name" : "body" } }, "/issue-credential-2.0/send-request" : { "post" : { - "tags" : [ "issue-credential v2.0" ], - "summary" : "Send issuer a credential request not bound to an existing thread. Indy credentials cannot start at a request", - "produces" : [ "application/json" ], - "parameters" : [ { - "in" : "body", - "name" : "body", - "required" : false, - "schema" : { - "$ref" : "#/definitions/V20CredRequestFree" - } - } ], + "requestBody" : { + "content" : { + "*/*" : { + "schema" : { + "$ref" : "#/components/schemas/V20CredRequestFree" + } + } + }, + "required" : false + }, "responses" : { "200" : { - "description" : "", - "schema" : { - "$ref" : "#/definitions/V20CredExRecord" - } + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/V20CredExRecord" + } + } + }, + "description" : "" } - } + }, + "summary" : "Send issuer a credential request not bound to an existing thread. Indy credentials cannot start at a request", + "tags" : [ "issue-credential v2.0" ], + "x-codegen-request-body-name" : "body" } }, "/issue-credential/create" : { "post" : { - "tags" : [ "issue-credential v1.0" ], - "summary" : "Send holder a credential, automating entire flow", - "produces" : [ "application/json" ], - "parameters" : [ { - "in" : "body", - "name" : "body", - "required" : false, - "schema" : { - "$ref" : "#/definitions/V10CredentialCreate" - } - } ], + "requestBody" : { + "content" : { + "*/*" : { + "schema" : { + "$ref" : "#/components/schemas/V10CredentialCreate" + } + } + }, + "required" : false + }, "responses" : { "200" : { - "description" : "", - "schema" : { - "$ref" : "#/definitions/V10CredentialExchange" - } + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/V10CredentialExchange" + } + } + }, + "description" : "" } - } + }, + "summary" : "Create a credential record without sending (generally for use with Out-Of-Band)", + "tags" : [ "issue-credential v1.0" ], + "x-codegen-request-body-name" : "body" } }, "/issue-credential/create-offer" : { "post" : { - "tags" : [ "issue-credential v1.0" ], - "summary" : "Create a credential offer, independent of any proposal or connection", - "produces" : [ "application/json" ], - "parameters" : [ { - "in" : "body", - "name" : "body", - "required" : false, - "schema" : { - "$ref" : "#/definitions/V10CredentialConnFreeOfferRequest" - } - } ], + "requestBody" : { + "content" : { + "*/*" : { + "schema" : { + "$ref" : "#/components/schemas/V10CredentialConnFreeOfferRequest" + } + } + }, + "required" : false + }, "responses" : { "200" : { - "description" : "", - "schema" : { - "$ref" : "#/definitions/V10CredentialExchange" - } + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/V10CredentialExchange" + } + } + }, + "description" : "" } - } + }, + "summary" : "Create a credential offer, independent of any proposal or connection", + "tags" : [ "issue-credential v1.0" ], + "x-codegen-request-body-name" : "body" } }, "/issue-credential/records" : { "get" : { - "tags" : [ "issue-credential v1.0" ], - "summary" : "Fetch all credential exchange records", - "produces" : [ "application/json" ], "parameters" : [ { - "name" : "connection_id", - "in" : "query", "description" : "Connection identifier", - "required" : false, - "type" : "string", - "format" : "uuid" - }, { - "name" : "role", "in" : "query", - "description" : "Role assigned in credential exchange", - "required" : false, - "type" : "string", - "enum" : [ "issuer", "holder" ] + "name" : "connection_id", + "schema" : { + "format" : "uuid", + "type" : "string" + } }, { - "name" : "state", + "description" : "Role assigned in credential exchange", "in" : "query", - "description" : "Credential exchange state", - "required" : false, - "type" : "string", - "enum" : [ "proposal_sent", "proposal_received", "offer_sent", "offer_received", "request_sent", "request_received", "credential_issued", "credential_received", "credential_acked" ] + "name" : "role", + "schema" : { + "enum" : [ "issuer", "holder" ], + "type" : "string" + } }, { - "name" : "thread_id", + "description" : "Credential exchange state", "in" : "query", + "name" : "state", + "schema" : { + "enum" : [ "proposal_sent", "proposal_received", "offer_sent", "offer_received", "request_sent", "request_received", "credential_issued", "credential_received", "credential_acked", "credential_revoked", "abandoned" ], + "type" : "string" + } + }, { "description" : "Thread identifier", - "required" : false, - "type" : "string", - "format" : "uuid" + "in" : "query", + "name" : "thread_id", + "schema" : { + "format" : "uuid", + "type" : "string" + } } ], "responses" : { "200" : { - "description" : "", - "schema" : { - "$ref" : "#/definitions/V10CredentialExchangeListResult" - } + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/V10CredentialExchangeListResult" + } + } + }, + "description" : "" } - } + }, + "summary" : "Fetch all credential exchange records", + "tags" : [ "issue-credential v1.0" ] } }, "/issue-credential/records/{cred_ex_id}" : { - "get" : { - "tags" : [ "issue-credential v1.0" ], - "summary" : "Fetch a single credential exchange record", - "produces" : [ "application/json" ], + "delete" : { "parameters" : [ { - "name" : "cred_ex_id", - "in" : "path", "description" : "Credential exchange identifier", + "in" : "path", + "name" : "cred_ex_id", "required" : true, - "type" : "string", - "pattern" : "[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-4[a-fA-F0-9]{3}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}" + "schema" : { + "pattern" : "[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-4[a-fA-F0-9]{3}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}", + "type" : "string" + } } ], "responses" : { "200" : { - "description" : "", - "schema" : { - "$ref" : "#/definitions/V10CredentialExchange" - } + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/IssueCredentialModuleResponse" + } + } + }, + "description" : "" } - } - }, - "delete" : { - "tags" : [ "issue-credential v1.0" ], + }, "summary" : "Remove an existing credential exchange record", - "produces" : [ "application/json" ], + "tags" : [ "issue-credential v1.0" ] + }, + "get" : { "parameters" : [ { - "name" : "cred_ex_id", - "in" : "path", "description" : "Credential exchange identifier", + "in" : "path", + "name" : "cred_ex_id", "required" : true, - "type" : "string", - "pattern" : "[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-4[a-fA-F0-9]{3}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}" + "schema" : { + "pattern" : "[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-4[a-fA-F0-9]{3}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}", + "type" : "string" + } } ], "responses" : { "200" : { - "description" : "", - "schema" : { - "$ref" : "#/definitions/IssueCredentialModuleResponse" - } + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/V10CredentialExchange" + } + } + }, + "description" : "" } - } + }, + "summary" : "Fetch a single credential exchange record", + "tags" : [ "issue-credential v1.0" ] } }, "/issue-credential/records/{cred_ex_id}/issue" : { "post" : { - "tags" : [ "issue-credential v1.0" ], - "summary" : "Send holder a credential", - "produces" : [ "application/json" ], "parameters" : [ { - "in" : "body", - "name" : "body", - "required" : false, - "schema" : { - "$ref" : "#/definitions/V10CredentialIssueRequest" - } - }, { - "name" : "cred_ex_id", - "in" : "path", "description" : "Credential exchange identifier", + "in" : "path", + "name" : "cred_ex_id", "required" : true, - "type" : "string", - "pattern" : "[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-4[a-fA-F0-9]{3}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}" + "schema" : { + "pattern" : "[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-4[a-fA-F0-9]{3}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}", + "type" : "string" + } } ], + "requestBody" : { + "content" : { + "*/*" : { + "schema" : { + "$ref" : "#/components/schemas/V10CredentialIssueRequest" + } + } + }, + "required" : false + }, "responses" : { "200" : { - "description" : "", - "schema" : { - "$ref" : "#/definitions/V10CredentialExchange" - } + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/V10CredentialExchange" + } + } + }, + "description" : "" } - } + }, + "summary" : "Send holder a credential", + "tags" : [ "issue-credential v1.0" ], + "x-codegen-request-body-name" : "body" } }, "/issue-credential/records/{cred_ex_id}/problem-report" : { "post" : { - "tags" : [ "issue-credential v1.0" ], - "summary" : "Send a problem report for credential exchange", - "produces" : [ "application/json" ], "parameters" : [ { - "in" : "body", - "name" : "body", - "required" : false, - "schema" : { - "$ref" : "#/definitions/V10CredentialProblemReportRequest" - } - }, { - "name" : "cred_ex_id", - "in" : "path", "description" : "Credential exchange identifier", + "in" : "path", + "name" : "cred_ex_id", "required" : true, - "type" : "string", - "pattern" : "[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-4[a-fA-F0-9]{3}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}" + "schema" : { + "pattern" : "[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-4[a-fA-F0-9]{3}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}", + "type" : "string" + } } ], + "requestBody" : { + "content" : { + "*/*" : { + "schema" : { + "$ref" : "#/components/schemas/V10CredentialProblemReportRequest" + } + } + }, + "required" : false + }, "responses" : { "200" : { - "description" : "", - "schema" : { - "$ref" : "#/definitions/IssueCredentialModuleResponse" - } + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/IssueCredentialModuleResponse" + } + } + }, + "description" : "" } - } + }, + "summary" : "Send a problem report for credential exchange", + "tags" : [ "issue-credential v1.0" ], + "x-codegen-request-body-name" : "body" } }, "/issue-credential/records/{cred_ex_id}/send-offer" : { "post" : { - "tags" : [ "issue-credential v1.0" ], - "summary" : "Send holder a credential offer in reference to a proposal with preview", - "produces" : [ "application/json" ], "parameters" : [ { - "in" : "body", - "name" : "body", - "required" : false, - "schema" : { - "$ref" : "#/definitions/V10CredentialBoundOfferRequest" - } - }, { - "name" : "cred_ex_id", - "in" : "path", "description" : "Credential exchange identifier", + "in" : "path", + "name" : "cred_ex_id", "required" : true, - "type" : "string", - "pattern" : "[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-4[a-fA-F0-9]{3}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}" + "schema" : { + "pattern" : "[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-4[a-fA-F0-9]{3}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}", + "type" : "string" + } } ], + "requestBody" : { + "content" : { + "*/*" : { + "schema" : { + "$ref" : "#/components/schemas/V10CredentialBoundOfferRequest" + } + } + }, + "required" : false + }, "responses" : { "200" : { - "description" : "", - "schema" : { - "$ref" : "#/definitions/V10CredentialExchange" - } + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/V10CredentialExchange" + } + } + }, + "description" : "" } - } + }, + "summary" : "Send holder a credential offer in reference to a proposal with preview", + "tags" : [ "issue-credential v1.0" ], + "x-codegen-request-body-name" : "body" } }, "/issue-credential/records/{cred_ex_id}/send-request" : { "post" : { - "tags" : [ "issue-credential v1.0" ], - "summary" : "Send issuer a credential request", - "produces" : [ "application/json" ], "parameters" : [ { - "name" : "cred_ex_id", - "in" : "path", "description" : "Credential exchange identifier", + "in" : "path", + "name" : "cred_ex_id", "required" : true, - "type" : "string", - "pattern" : "[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-4[a-fA-F0-9]{3}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}" + "schema" : { + "pattern" : "[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-4[a-fA-F0-9]{3}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}", + "type" : "string" + } } ], "responses" : { "200" : { - "description" : "", - "schema" : { - "$ref" : "#/definitions/V10CredentialExchange" - } + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/V10CredentialExchange" + } + } + }, + "description" : "" } - } + }, + "summary" : "Send issuer a credential request", + "tags" : [ "issue-credential v1.0" ] } }, "/issue-credential/records/{cred_ex_id}/store" : { "post" : { - "tags" : [ "issue-credential v1.0" ], - "summary" : "Store a received credential", - "produces" : [ "application/json" ], "parameters" : [ { - "in" : "body", - "name" : "body", - "required" : false, - "schema" : { - "$ref" : "#/definitions/V10CredentialStoreRequest" - } - }, { - "name" : "cred_ex_id", - "in" : "path", "description" : "Credential exchange identifier", + "in" : "path", + "name" : "cred_ex_id", "required" : true, - "type" : "string", - "pattern" : "[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-4[a-fA-F0-9]{3}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}" + "schema" : { + "pattern" : "[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-4[a-fA-F0-9]{3}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}", + "type" : "string" + } } ], + "requestBody" : { + "content" : { + "*/*" : { + "schema" : { + "$ref" : "#/components/schemas/V10CredentialStoreRequest" + } + } + }, + "required" : false + }, "responses" : { "200" : { - "description" : "", - "schema" : { - "$ref" : "#/definitions/V10CredentialExchange" - } + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/V10CredentialExchange" + } + } + }, + "description" : "" } - } + }, + "summary" : "Store a received credential", + "tags" : [ "issue-credential v1.0" ], + "x-codegen-request-body-name" : "body" } }, "/issue-credential/send" : { "post" : { - "tags" : [ "issue-credential v1.0" ], - "summary" : "Send holder a credential, automating entire flow", - "produces" : [ "application/json" ], - "parameters" : [ { - "in" : "body", - "name" : "body", - "required" : false, - "schema" : { - "$ref" : "#/definitions/V10CredentialProposalRequestMand" - } - } ], + "requestBody" : { + "content" : { + "*/*" : { + "schema" : { + "$ref" : "#/components/schemas/V10CredentialProposalRequestMand" + } + } + }, + "required" : false + }, "responses" : { "200" : { - "description" : "", - "schema" : { - "$ref" : "#/definitions/V10CredentialExchange" - } + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/V10CredentialExchange" + } + } + }, + "description" : "" } - } + }, + "summary" : "Send holder a credential, automating entire flow", + "tags" : [ "issue-credential v1.0" ], + "x-codegen-request-body-name" : "body" } }, "/issue-credential/send-offer" : { "post" : { - "tags" : [ "issue-credential v1.0" ], - "summary" : "Send holder a credential offer, independent of any proposal", - "produces" : [ "application/json" ], - "parameters" : [ { - "in" : "body", - "name" : "body", - "required" : false, - "schema" : { - "$ref" : "#/definitions/V10CredentialFreeOfferRequest" - } - } ], + "requestBody" : { + "content" : { + "*/*" : { + "schema" : { + "$ref" : "#/components/schemas/V10CredentialFreeOfferRequest" + } + } + }, + "required" : false + }, "responses" : { "200" : { - "description" : "", - "schema" : { - "$ref" : "#/definitions/V10CredentialExchange" - } + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/V10CredentialExchange" + } + } + }, + "description" : "" } - } + }, + "summary" : "Send holder a credential offer, independent of any proposal", + "tags" : [ "issue-credential v1.0" ], + "x-codegen-request-body-name" : "body" } }, "/issue-credential/send-proposal" : { "post" : { - "tags" : [ "issue-credential v1.0" ], - "summary" : "Send issuer a credential proposal", - "produces" : [ "application/json" ], - "parameters" : [ { - "in" : "body", - "name" : "body", - "required" : false, - "schema" : { - "$ref" : "#/definitions/V10CredentialProposalRequestOpt" - } - } ], + "requestBody" : { + "content" : { + "*/*" : { + "schema" : { + "$ref" : "#/components/schemas/V10CredentialProposalRequestOpt" + } + } + }, + "required" : false + }, "responses" : { "200" : { - "description" : "", - "schema" : { - "$ref" : "#/definitions/V10CredentialExchange" - } + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/V10CredentialExchange" + } + } + }, + "description" : "" } - } + }, + "summary" : "Send issuer a credential proposal", + "tags" : [ "issue-credential v1.0" ], + "x-codegen-request-body-name" : "body" } }, "/jsonld/sign" : { "post" : { - "tags" : [ "jsonld" ], - "summary" : "Sign a JSON-LD structure and return it", - "produces" : [ "application/json" ], - "parameters" : [ { - "in" : "body", - "name" : "body", - "required" : false, - "schema" : { - "$ref" : "#/definitions/SignRequest" - } - } ], + "requestBody" : { + "content" : { + "*/*" : { + "schema" : { + "$ref" : "#/components/schemas/SignRequest" + } + } + }, + "required" : false + }, "responses" : { "200" : { - "description" : "", - "schema" : { - "$ref" : "#/definitions/SignResponse" - } + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/SignResponse" + } + } + }, + "description" : "" } - } + }, + "summary" : "Sign a JSON-LD structure and return it", + "tags" : [ "jsonld" ], + "x-codegen-request-body-name" : "body" } }, "/jsonld/verify" : { "post" : { - "tags" : [ "jsonld" ], - "summary" : "Verify a JSON-LD structure.", - "produces" : [ "application/json" ], - "parameters" : [ { - "in" : "body", - "name" : "body", - "required" : false, - "schema" : { - "$ref" : "#/definitions/VerifyRequest" - } - } ], + "requestBody" : { + "content" : { + "*/*" : { + "schema" : { + "$ref" : "#/components/schemas/VerifyRequest" + } + } + }, + "required" : false + }, "responses" : { "200" : { - "description" : "", - "schema" : { - "$ref" : "#/definitions/VerifyResponse" - } + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/VerifyResponse" + } + } + }, + "description" : "" } - } + }, + "summary" : "Verify a JSON-LD structure.", + "tags" : [ "jsonld" ], + "x-codegen-request-body-name" : "body" } }, "/ledger/did-endpoint" : { "get" : { - "tags" : [ "ledger" ], - "summary" : "Get the endpoint for a DID from the ledger.", - "produces" : [ "application/json" ], "parameters" : [ { - "name" : "did", - "in" : "query", "description" : "DID of interest", + "in" : "query", + "name" : "did", "required" : true, - "type" : "string", - "pattern" : "^(did:sov:)?[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}$" + "schema" : { + "pattern" : "^(did:sov:)?[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}$", + "type" : "string" + } }, { - "name" : "endpoint_type", - "in" : "query", "description" : "Endpoint type of interest (default 'Endpoint')", - "required" : false, - "type" : "string", - "enum" : [ "Endpoint", "Profile", "LinkedDomains" ] + "in" : "query", + "name" : "endpoint_type", + "schema" : { + "enum" : [ "Endpoint", "Profile", "LinkedDomains" ], + "type" : "string" + } } ], "responses" : { "200" : { - "description" : "", - "schema" : { - "$ref" : "#/definitions/GetDIDEndpointResponse" - } + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/GetDIDEndpointResponse" + } + } + }, + "description" : "" } - } + }, + "summary" : "Get the endpoint for a DID from the ledger.", + "tags" : [ "ledger" ] } }, "/ledger/did-verkey" : { "get" : { - "tags" : [ "ledger" ], - "summary" : "Get the verkey for a DID from the ledger.", - "produces" : [ "application/json" ], "parameters" : [ { - "name" : "did", - "in" : "query", "description" : "DID of interest", + "in" : "query", + "name" : "did", "required" : true, - "type" : "string", - "pattern" : "^(did:sov:)?[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}$" + "schema" : { + "pattern" : "^(did:sov:)?[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}$", + "type" : "string" + } } ], "responses" : { "200" : { - "description" : "", - "schema" : { - "$ref" : "#/definitions/GetDIDVerkeyResponse" - } + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/GetDIDVerkeyResponse" + } + } + }, + "description" : "" } - } + }, + "summary" : "Get the verkey for a DID from the ledger.", + "tags" : [ "ledger" ] } }, "/ledger/get-nym-role" : { "get" : { - "tags" : [ "ledger" ], - "summary" : "Get the role from the NYM registration of a public DID.", - "produces" : [ "application/json" ], "parameters" : [ { - "name" : "did", - "in" : "query", "description" : "DID of interest", + "in" : "query", + "name" : "did", "required" : true, - "type" : "string", - "pattern" : "^(did:sov:)?[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}$" + "schema" : { + "pattern" : "^(did:sov:)?[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}$", + "type" : "string" + } } ], "responses" : { "200" : { - "description" : "", - "schema" : { - "$ref" : "#/definitions/GetNymRoleResponse" - } + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/GetNymRoleResponse" + } + } + }, + "description" : "" } - } + }, + "summary" : "Get the role from the NYM registration of a public DID.", + "tags" : [ "ledger" ] + } + }, + "/ledger/multiple/config" : { + "get" : { + "responses" : { + "200" : { + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/LedgerConfigList" + } + } + }, + "description" : "" + } + }, + "summary" : "Fetch the multiple ledger configuration currently in use", + "tags" : [ "ledger" ] + } + }, + "/ledger/multiple/get-write-ledger" : { + "get" : { + "responses" : { + "200" : { + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/WriteLedgerRequest" + } + } + }, + "description" : "" + } + }, + "summary" : "Fetch the current write ledger", + "tags" : [ "ledger" ] } }, "/ledger/register-nym" : { "post" : { - "tags" : [ "ledger" ], - "summary" : "Send a NYM registration to the ledger.", - "produces" : [ "application/json" ], "parameters" : [ { - "name" : "did", - "in" : "query", "description" : "DID to register", + "in" : "query", + "name" : "did", "required" : true, - "type" : "string", - "pattern" : "^(did:sov:)?[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}$" + "schema" : { + "pattern" : "^(did:sov:)?[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}$", + "type" : "string" + } }, { - "name" : "verkey", - "in" : "query", "description" : "Verification key", + "in" : "query", + "name" : "verkey", "required" : true, - "type" : "string", - "pattern" : "^[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{43,44}$" + "schema" : { + "pattern" : "^[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{43,44}$", + "type" : "string" + } }, { + "description" : "Alias", + "in" : "query", "name" : "alias", + "schema" : { + "type" : "string" + } + }, { + "description" : "Connection identifier", "in" : "query", - "description" : "Alias", - "required" : false, - "type" : "string" + "name" : "conn_id", + "schema" : { + "type" : "string" + } }, { - "name" : "role", + "description" : "Create Transaction For Endorser's signature", "in" : "query", + "name" : "create_transaction_for_endorser", + "schema" : { + "type" : "boolean" + } + }, { "description" : "Role", - "required" : false, - "type" : "string", - "enum" : [ "STEWARD", "TRUSTEE", "ENDORSER", "NETWORK_MONITOR", "reset" ] + "in" : "query", + "name" : "role", + "schema" : { + "enum" : [ "STEWARD", "TRUSTEE", "ENDORSER", "NETWORK_MONITOR", "reset" ], + "type" : "string" + } } ], "responses" : { "200" : { - "description" : "", - "schema" : { - "$ref" : "#/definitions/RegisterLedgerNymResponse" - } + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/TxnOrRegisterLedgerNymResponse" + } + } + }, + "description" : "" } - } + }, + "summary" : "Send a NYM registration to the ledger.", + "tags" : [ "ledger" ] } }, "/ledger/rotate-public-did-keypair" : { "patch" : { - "tags" : [ "ledger" ], - "summary" : "Rotate key pair for public DID.", - "produces" : [ "application/json" ], - "parameters" : [ ], "responses" : { "200" : { - "description" : "", - "schema" : { - "$ref" : "#/definitions/LedgerModulesResult" - } + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/LedgerModulesResult" + } + } + }, + "description" : "" } - } + }, + "summary" : "Rotate key pair for public DID.", + "tags" : [ "ledger" ] } }, "/ledger/taa" : { "get" : { - "tags" : [ "ledger" ], - "summary" : "Fetch the current transaction author agreement, if any", - "produces" : [ "application/json" ], - "parameters" : [ ], "responses" : { "200" : { - "description" : "", - "schema" : { - "$ref" : "#/definitions/TAAResult" - } + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/TAAResult" + } + } + }, + "description" : "" } - } + }, + "summary" : "Fetch the current transaction author agreement, if any", + "tags" : [ "ledger" ] } }, "/ledger/taa/accept" : { "post" : { - "tags" : [ "ledger" ], - "summary" : "Accept the transaction author agreement", - "produces" : [ "application/json" ], - "parameters" : [ { - "in" : "body", - "name" : "body", - "required" : false, - "schema" : { - "$ref" : "#/definitions/TAAAccept" - } - } ], + "requestBody" : { + "content" : { + "*/*" : { + "schema" : { + "$ref" : "#/components/schemas/TAAAccept" + } + } + }, + "required" : false + }, "responses" : { "200" : { - "description" : "", - "schema" : { - "$ref" : "#/definitions/LedgerModulesResult" - } + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/LedgerModulesResult" + } + } + }, + "description" : "" } - } + }, + "summary" : "Accept the transaction author agreement", + "tags" : [ "ledger" ], + "x-codegen-request-body-name" : "body" } }, "/mediation/default-mediator" : { - "get" : { - "tags" : [ "mediation" ], - "summary" : "Get default mediator", - "produces" : [ "application/json" ], - "parameters" : [ ], + "delete" : { "responses" : { - "200" : { - "description" : "null", - "schema" : { - "$ref" : "#/definitions/MediationRecord" - } + "201" : { + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/MediationRecord" + } + } + }, + "description" : "" } - } - }, - "delete" : { - "tags" : [ "mediation" ], + }, "summary" : "Clear default mediator", - "produces" : [ "application/json" ], - "parameters" : [ ], + "tags" : [ "mediation" ] + }, + "get" : { "responses" : { - "201" : { - "description" : "null", - "schema" : { - "$ref" : "#/definitions/MediationRecord" - } + "200" : { + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/MediationRecord" + } + } + }, + "description" : "" } - } + }, + "summary" : "Get default mediator", + "tags" : [ "mediation" ] } }, "/mediation/keylists" : { "get" : { - "tags" : [ "mediation" ], - "summary" : "Retrieve keylists by connection or role", - "produces" : [ "application/json" ], "parameters" : [ { - "name" : "conn_id", - "in" : "query", "description" : "Connection identifier (optional)", - "required" : false, - "type" : "string", - "format" : "uuid" - }, { - "name" : "role", "in" : "query", - "description" : "Filer on role, 'client' for keys mediated by other agents, 'server' for keys mediated by this agent", - "required" : false, - "type" : "string", - "default" : "server", - "enum" : [ "client", "server" ] - } ], - "responses" : { + "name" : "conn_id", + "schema" : { + "format" : "uuid", + "type" : "string" + } + }, { + "description" : "Filer on role, 'client' for keys mediated by other agents, 'server' for keys mediated by this agent", + "in" : "query", + "name" : "role", + "schema" : { + "default" : "server", + "enum" : [ "client", "server" ], + "type" : "string" + } + } ], + "responses" : { "200" : { - "description" : "null", - "schema" : { - "$ref" : "#/definitions/Keylist" - } + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/Keylist" + } + } + }, + "description" : "" } - } + }, + "summary" : "Retrieve keylists by connection or role", + "tags" : [ "mediation" ] } }, "/mediation/keylists/{mediation_id}/send-keylist-query" : { "post" : { - "tags" : [ "mediation" ], - "summary" : "Send keylist query to mediator", - "produces" : [ "application/json" ], "parameters" : [ { - "in" : "body", - "name" : "body", - "required" : false, - "schema" : { - "$ref" : "#/definitions/KeylistQueryFilterRequest" - } - }, { - "name" : "mediation_id", - "in" : "path", "description" : "Mediation record identifier", + "in" : "path", + "name" : "mediation_id", "required" : true, - "type" : "string", - "format" : "uuid" + "schema" : { + "format" : "uuid", + "type" : "string" + } }, { - "name" : "paginate_limit", - "in" : "query", "description" : "limit number of results", - "required" : false, - "type" : "integer", - "default" : -1, - "format" : "int32" - }, { - "name" : "paginate_offset", "in" : "query", + "name" : "paginate_limit", + "schema" : { + "default" : -1, + "format" : "int32", + "type" : "integer" + } + }, { "description" : "offset to use in pagination", - "required" : false, - "type" : "integer", - "default" : 0, - "format" : "int32" + "in" : "query", + "name" : "paginate_offset", + "schema" : { + "default" : 0, + "format" : "int32", + "type" : "integer" + } } ], + "requestBody" : { + "content" : { + "*/*" : { + "schema" : { + "$ref" : "#/components/schemas/KeylistQueryFilterRequest" + } + } + }, + "required" : false + }, "responses" : { "201" : { - "description" : "null", - "schema" : { - "$ref" : "#/definitions/KeylistQuery" - } + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/KeylistQuery" + } + } + }, + "description" : "" } - } + }, + "summary" : "Send keylist query to mediator", + "tags" : [ "mediation" ], + "x-codegen-request-body-name" : "body" } }, "/mediation/keylists/{mediation_id}/send-keylist-update" : { "post" : { - "tags" : [ "mediation" ], - "summary" : "Send keylist update to mediator", - "produces" : [ "application/json" ], "parameters" : [ { - "in" : "body", - "name" : "body", - "required" : false, - "schema" : { - "$ref" : "#/definitions/KeylistUpdateRequest" - } - }, { - "name" : "mediation_id", - "in" : "path", "description" : "Mediation record identifier", + "in" : "path", + "name" : "mediation_id", "required" : true, - "type" : "string", - "format" : "uuid" + "schema" : { + "format" : "uuid", + "type" : "string" + } } ], + "requestBody" : { + "content" : { + "*/*" : { + "schema" : { + "$ref" : "#/components/schemas/KeylistUpdateRequest" + } + } + }, + "required" : false + }, "responses" : { "201" : { - "description" : "null", - "schema" : { - "$ref" : "#/definitions/KeylistUpdate" - } + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/KeylistUpdate" + } + } + }, + "description" : "" } - } + }, + "summary" : "Send keylist update to mediator", + "tags" : [ "mediation" ], + "x-codegen-request-body-name" : "body" } }, "/mediation/request/{conn_id}" : { "post" : { - "tags" : [ "mediation" ], - "summary" : "Request mediation from connection", - "produces" : [ "application/json" ], "parameters" : [ { - "in" : "body", - "name" : "body", - "required" : false, - "schema" : { - "$ref" : "#/definitions/MediationCreateRequest" - } - }, { - "name" : "conn_id", - "in" : "path", "description" : "Connection identifier", + "in" : "path", + "name" : "conn_id", "required" : true, - "type" : "string" + "schema" : { + "type" : "string" + } } ], + "requestBody" : { + "content" : { + "*/*" : { + "schema" : { + "$ref" : "#/components/schemas/MediationCreateRequest" + } + } + }, + "required" : false + }, "responses" : { "201" : { - "description" : "null", - "schema" : { - "$ref" : "#/definitions/MediationRecord" - } + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/MediationRecord" + } + } + }, + "description" : "" } - } + }, + "summary" : "Request mediation from connection", + "tags" : [ "mediation" ], + "x-codegen-request-body-name" : "body" } }, "/mediation/requests" : { "get" : { - "tags" : [ "mediation" ], - "summary" : "Query mediation requests, returns list of all mediation records", - "produces" : [ "application/json" ], "parameters" : [ { - "name" : "conn_id", - "in" : "query", "description" : "Connection identifier (optional)", - "required" : false, - "type" : "string", - "format" : "uuid" - }, { - "name" : "mediator_terms", "in" : "query", + "name" : "conn_id", + "schema" : { + "format" : "uuid", + "type" : "string" + } + }, { "description" : "List of mediator rules for recipient", - "required" : false, - "type" : "array", - "items" : { - "type" : "string", - "description" : "Indicate terms to which the mediator requires the recipient to agree" + "explode" : true, + "in" : "query", + "name" : "mediator_terms", + "schema" : { + "items" : { + "description" : "Indicate terms to which the mediator requires the recipient to agree", + "type" : "string" + }, + "type" : "array" }, - "collectionFormat" : "multi" + "style" : "form" }, { - "name" : "recipient_terms", - "in" : "query", "description" : "List of recipient rules for mediation", - "required" : false, - "type" : "array", - "items" : { - "type" : "string", - "description" : "Indicate terms to which the recipient requires the mediator to agree" + "explode" : true, + "in" : "query", + "name" : "recipient_terms", + "schema" : { + "items" : { + "description" : "Indicate terms to which the recipient requires the mediator to agree", + "type" : "string" + }, + "type" : "array" }, - "collectionFormat" : "multi" + "style" : "form" }, { - "name" : "state", - "in" : "query", "description" : "Mediation state (optional)", - "required" : false, - "type" : "string", - "enum" : [ "request", "granted", "denied" ] + "in" : "query", + "name" : "state", + "schema" : { + "enum" : [ "request", "granted", "denied" ], + "type" : "string" + } } ], "responses" : { "200" : { - "description" : "null", - "schema" : { - "$ref" : "#/definitions/MediationList" - } + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/MediationList" + } + } + }, + "description" : "" } - } + }, + "summary" : "Query mediation requests, returns list of all mediation records", + "tags" : [ "mediation" ] } }, "/mediation/requests/{mediation_id}" : { - "get" : { - "tags" : [ "mediation" ], - "summary" : "Retrieve mediation request record", - "produces" : [ "application/json" ], + "delete" : { "parameters" : [ { - "name" : "mediation_id", - "in" : "path", "description" : "Mediation record identifier", + "in" : "path", + "name" : "mediation_id", "required" : true, - "type" : "string", - "format" : "uuid" + "schema" : { + "format" : "uuid", + "type" : "string" + } } ], "responses" : { "200" : { - "description" : "null", - "schema" : { - "$ref" : "#/definitions/MediationRecord" - } + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/MediationRecord" + } + } + }, + "description" : "" } - } - }, - "delete" : { - "tags" : [ "mediation" ], + }, "summary" : "Delete mediation request by ID", - "produces" : [ "application/json" ], + "tags" : [ "mediation" ] + }, + "get" : { "parameters" : [ { - "name" : "mediation_id", - "in" : "path", "description" : "Mediation record identifier", + "in" : "path", + "name" : "mediation_id", "required" : true, - "type" : "string", - "format" : "uuid" + "schema" : { + "format" : "uuid", + "type" : "string" + } } ], "responses" : { "200" : { - "description" : "null", - "schema" : { - "$ref" : "#/definitions/MediationRecord" - } + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/MediationRecord" + } + } + }, + "description" : "" } - } + }, + "summary" : "Retrieve mediation request record", + "tags" : [ "mediation" ] } }, "/mediation/requests/{mediation_id}/deny" : { "post" : { - "tags" : [ "mediation" ], - "summary" : "Deny a stored mediation request", - "produces" : [ "application/json" ], "parameters" : [ { - "in" : "body", - "name" : "body", - "required" : false, - "schema" : { - "$ref" : "#/definitions/AdminMediationDeny" - } - }, { - "name" : "mediation_id", - "in" : "path", "description" : "Mediation record identifier", + "in" : "path", + "name" : "mediation_id", "required" : true, - "type" : "string", - "format" : "uuid" + "schema" : { + "format" : "uuid", + "type" : "string" + } } ], + "requestBody" : { + "content" : { + "*/*" : { + "schema" : { + "$ref" : "#/components/schemas/AdminMediationDeny" + } + } + }, + "required" : false + }, "responses" : { "201" : { - "description" : "null", - "schema" : { - "$ref" : "#/definitions/MediationDeny" - } + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/MediationDeny" + } + } + }, + "description" : "" } - } + }, + "summary" : "Deny a stored mediation request", + "tags" : [ "mediation" ], + "x-codegen-request-body-name" : "body" } }, "/mediation/requests/{mediation_id}/grant" : { "post" : { - "tags" : [ "mediation" ], - "summary" : "Grant received mediation", - "produces" : [ "application/json" ], "parameters" : [ { - "name" : "mediation_id", - "in" : "path", "description" : "Mediation record identifier", + "in" : "path", + "name" : "mediation_id", "required" : true, - "type" : "string", - "format" : "uuid" + "schema" : { + "format" : "uuid", + "type" : "string" + } } ], "responses" : { "201" : { - "description" : "null", - "schema" : { - "$ref" : "#/definitions/MediationGrant" - } + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/MediationGrant" + } + } + }, + "description" : "" } - } + }, + "summary" : "Grant received mediation", + "tags" : [ "mediation" ] } }, - "/mediation/{mediation_id}/default-mediator" : { - "put" : { - "tags" : [ "mediation" ], - "summary" : "Set default mediator", - "produces" : [ "application/json" ], + "/mediation/update-keylist/{conn_id}" : { + "post" : { "parameters" : [ { - "name" : "mediation_id", + "description" : "Connection identifier", "in" : "path", - "description" : "Mediation record identifier", + "name" : "conn_id", "required" : true, - "type" : "string", - "format" : "uuid" + "schema" : { + "type" : "string" + } } ], - "responses" : { - "201" : { - "description" : "null", - "schema" : { - "$ref" : "#/definitions/MediationRecord" + "requestBody" : { + "content" : { + "*/*" : { + "schema" : { + "$ref" : "#/components/schemas/MediationIdMatchInfo" + } } - } - } + }, + "required" : false + }, + "responses" : { + "200" : { + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/KeylistUpdate" + } + } + }, + "description" : "" + } + }, + "summary" : "Update keylist for a connection", + "tags" : [ "mediation" ], + "x-codegen-request-body-name" : "body" } }, - "/multitenancy/wallet" : { - "post" : { - "tags" : [ "multitenancy" ], - "summary" : "Create a subwallet", - "produces" : [ "application/json" ], + "/mediation/{mediation_id}/default-mediator" : { + "put" : { "parameters" : [ { - "in" : "body", - "name" : "body", - "required" : false, + "description" : "Mediation record identifier", + "in" : "path", + "name" : "mediation_id", + "required" : true, "schema" : { - "$ref" : "#/definitions/CreateWalletRequest" + "format" : "uuid", + "type" : "string" } } ], "responses" : { - "200" : { - "description" : "", - "schema" : { - "$ref" : "#/definitions/CreateWalletResponse" + "201" : { + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/MediationRecord" + } + } + }, + "description" : "" + } + }, + "summary" : "Set default mediator", + "tags" : [ "mediation" ] + } + }, + "/multitenancy/wallet" : { + "post" : { + "requestBody" : { + "content" : { + "*/*" : { + "schema" : { + "$ref" : "#/components/schemas/CreateWalletRequest" + } } + }, + "required" : false + }, + "responses" : { + "200" : { + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/CreateWalletResponse" + } + } + }, + "description" : "" } - } + }, + "summary" : "Create a subwallet", + "tags" : [ "multitenancy" ], + "x-codegen-request-body-name" : "body" } }, "/multitenancy/wallet/{wallet_id}" : { "get" : { - "tags" : [ "multitenancy" ], - "summary" : "Get a single subwallet", - "produces" : [ "application/json" ], "parameters" : [ { - "name" : "wallet_id", - "in" : "path", "description" : "Subwallet identifier", + "in" : "path", + "name" : "wallet_id", "required" : true, - "type" : "string" + "schema" : { + "type" : "string" + } } ], "responses" : { "200" : { - "description" : "", - "schema" : { - "$ref" : "#/definitions/WalletRecord" - } + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/WalletRecord" + } + } + }, + "description" : "" } - } + }, + "summary" : "Get a single subwallet", + "tags" : [ "multitenancy" ] }, "put" : { - "tags" : [ "multitenancy" ], - "summary" : "Update a subwallet", - "produces" : [ "application/json" ], "parameters" : [ { - "in" : "body", - "name" : "body", - "required" : false, - "schema" : { - "$ref" : "#/definitions/UpdateWalletRequest" - } - }, { - "name" : "wallet_id", - "in" : "path", "description" : "Subwallet identifier", + "in" : "path", + "name" : "wallet_id", "required" : true, - "type" : "string" + "schema" : { + "type" : "string" + } } ], + "requestBody" : { + "content" : { + "*/*" : { + "schema" : { + "$ref" : "#/components/schemas/UpdateWalletRequest" + } + } + }, + "required" : false + }, "responses" : { "200" : { - "description" : "", - "schema" : { - "$ref" : "#/definitions/WalletRecord" - } + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/WalletRecord" + } + } + }, + "description" : "" } - } + }, + "summary" : "Update a subwallet", + "tags" : [ "multitenancy" ], + "x-codegen-request-body-name" : "body" } }, "/multitenancy/wallet/{wallet_id}/remove" : { "post" : { - "tags" : [ "multitenancy" ], - "summary" : "Remove a subwallet", - "produces" : [ "application/json" ], "parameters" : [ { - "in" : "body", - "name" : "body", - "required" : false, - "schema" : { - "$ref" : "#/definitions/RemoveWalletRequest" - } - }, { - "name" : "wallet_id", - "in" : "path", "description" : "Subwallet identifier", + "in" : "path", + "name" : "wallet_id", "required" : true, - "type" : "string" + "schema" : { + "type" : "string" + } } ], + "requestBody" : { + "content" : { + "*/*" : { + "schema" : { + "$ref" : "#/components/schemas/RemoveWalletRequest" + } + } + }, + "required" : false + }, "responses" : { "200" : { - "description" : "", - "schema" : { - "$ref" : "#/definitions/MultitenantModuleResponse" - } + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/MultitenantModuleResponse" + } + } + }, + "description" : "" } - } + }, + "summary" : "Remove a subwallet", + "tags" : [ "multitenancy" ], + "x-codegen-request-body-name" : "body" } }, "/multitenancy/wallet/{wallet_id}/token" : { "post" : { - "tags" : [ "multitenancy" ], - "summary" : "Get auth token for a subwallet", - "produces" : [ "application/json" ], "parameters" : [ { - "in" : "body", - "name" : "body", - "required" : false, - "schema" : { - "$ref" : "#/definitions/CreateWalletTokenRequest" - } - }, { - "name" : "wallet_id", "in" : "path", + "name" : "wallet_id", "required" : true, - "type" : "string" + "schema" : { + "type" : "string" + } } ], + "requestBody" : { + "content" : { + "*/*" : { + "schema" : { + "$ref" : "#/components/schemas/CreateWalletTokenRequest" + } + } + }, + "required" : false + }, "responses" : { "200" : { - "description" : "", - "schema" : { - "$ref" : "#/definitions/CreateWalletTokenResponse" - } + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/CreateWalletTokenResponse" + } + } + }, + "description" : "" } - } + }, + "summary" : "Get auth token for a subwallet", + "tags" : [ "multitenancy" ], + "x-codegen-request-body-name" : "body" } }, "/multitenancy/wallets" : { "get" : { - "tags" : [ "multitenancy" ], - "summary" : "Query subwallets", - "produces" : [ "application/json" ], "parameters" : [ { - "name" : "wallet_name", - "in" : "query", "description" : "Wallet name", - "required" : false, - "type" : "string" + "in" : "query", + "name" : "wallet_name", + "schema" : { + "type" : "string" + } } ], "responses" : { "200" : { - "description" : "", - "schema" : { - "$ref" : "#/definitions/WalletList" - } + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/WalletList" + } + } + }, + "description" : "" } - } + }, + "summary" : "Query subwallets", + "tags" : [ "multitenancy" ] } }, "/out-of-band/create-invitation" : { "post" : { - "tags" : [ "out-of-band" ], - "summary" : "Create a new connection invitation", - "produces" : [ "application/json" ], "parameters" : [ { - "in" : "body", - "name" : "body", - "required" : false, + "description" : "Auto-accept connection (defaults to configuration)", + "in" : "query", + "name" : "auto_accept", "schema" : { - "$ref" : "#/definitions/InvitationCreateRequest" + "type" : "boolean" } }, { - "name" : "auto_accept", + "description" : "Create invitation for multiple use (default false)", "in" : "query", - "description" : "Auto-accept connection (defaults to configuration)", - "required" : false, - "type" : "boolean" - }, { "name" : "multi_use", - "in" : "query", - "description" : "Create invitation for multiple use (default false)", - "required" : false, - "type" : "boolean" + "schema" : { + "type" : "boolean" + } } ], + "requestBody" : { + "content" : { + "*/*" : { + "schema" : { + "$ref" : "#/components/schemas/InvitationCreateRequest" + } + } + }, + "required" : false + }, "responses" : { "200" : { - "description" : "", - "schema" : { - "$ref" : "#/definitions/InvitationRecord" - } + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/InvitationRecord" + } + } + }, + "description" : "" } - } + }, + "summary" : "Create a new connection invitation", + "tags" : [ "out-of-band" ], + "x-codegen-request-body-name" : "body" } }, "/out-of-band/receive-invitation" : { "post" : { - "tags" : [ "out-of-band" ], - "summary" : "Receive a new connection invitation", - "produces" : [ "application/json" ], "parameters" : [ { - "in" : "body", - "name" : "body", - "required" : false, + "description" : "Alias for connection", + "in" : "query", + "name" : "alias", "schema" : { - "$ref" : "#/definitions/InvitationMessage" + "type" : "string" } }, { - "name" : "alias", + "description" : "Auto-accept connection (defaults to configuration)", "in" : "query", - "description" : "Alias for connection", - "required" : false, - "type" : "string" - }, { "name" : "auto_accept", - "in" : "query", - "description" : "Auto-accept connection (defaults to configuration)", - "required" : false, - "type" : "boolean" + "schema" : { + "type" : "boolean" + } }, { - "name" : "mediation_id", - "in" : "query", "description" : "Identifier for active mediation record to be used", - "required" : false, - "type" : "string", - "pattern" : "[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-4[a-fA-F0-9]{3}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}" - }, { - "name" : "use_existing_connection", "in" : "query", + "name" : "mediation_id", + "schema" : { + "pattern" : "[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-4[a-fA-F0-9]{3}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}", + "type" : "string" + } + }, { "description" : "Use an existing connection, if possible", - "required" : false, - "type" : "boolean" + "in" : "query", + "name" : "use_existing_connection", + "schema" : { + "type" : "boolean" + } } ], + "requestBody" : { + "content" : { + "*/*" : { + "schema" : { + "$ref" : "#/components/schemas/InvitationMessage" + } + } + }, + "required" : false + }, "responses" : { "200" : { - "description" : "", - "schema" : { - "$ref" : "#/definitions/ConnRecord" - } + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/OobRecord" + } + } + }, + "description" : "" } - } + }, + "summary" : "Receive a new connection invitation", + "tags" : [ "out-of-band" ], + "x-codegen-request-body-name" : "body" } }, "/plugins" : { "get" : { - "tags" : [ "server" ], - "summary" : "Fetch the list of loaded plugins", - "produces" : [ "application/json" ], - "parameters" : [ ], "responses" : { "200" : { - "description" : "", - "schema" : { - "$ref" : "#/definitions/AdminModules" - } + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/AdminModules" + } + } + }, + "description" : "" } - } + }, + "summary" : "Fetch the list of loaded plugins", + "tags" : [ "server" ] } }, "/present-proof-2.0/create-request" : { "post" : { - "tags" : [ "present-proof v2.0" ], - "summary" : "Creates a presentation request not bound to any proposal or connection", - "produces" : [ "application/json" ], - "parameters" : [ { - "in" : "body", - "name" : "body", - "required" : false, - "schema" : { - "$ref" : "#/definitions/V20PresCreateRequestRequest" - } - } ], - "responses" : { - "200" : { - "description" : "", - "schema" : { - "$ref" : "#/definitions/V20PresExRecord" + "requestBody" : { + "content" : { + "*/*" : { + "schema" : { + "$ref" : "#/components/schemas/V20PresCreateRequestRequest" + } } + }, + "required" : false + }, + "responses" : { + "200" : { + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/V20PresExRecord" + } + } + }, + "description" : "" } - } + }, + "summary" : "Creates a presentation request not bound to any proposal or connection", + "tags" : [ "present-proof v2.0" ], + "x-codegen-request-body-name" : "body" } }, "/present-proof-2.0/records" : { "get" : { - "tags" : [ "present-proof v2.0" ], - "summary" : "Fetch all present-proof exchange records", - "produces" : [ "application/json" ], "parameters" : [ { - "name" : "connection_id", - "in" : "query", "description" : "Connection identifier", - "required" : false, - "type" : "string", - "format" : "uuid" - }, { - "name" : "role", "in" : "query", - "description" : "Role assigned in presentation exchange", - "required" : false, - "type" : "string", - "enum" : [ "prover", "verifier" ] + "name" : "connection_id", + "schema" : { + "format" : "uuid", + "type" : "string" + } }, { - "name" : "state", + "description" : "Role assigned in presentation exchange", "in" : "query", - "description" : "Presentation exchange state", - "required" : false, - "type" : "string", - "enum" : [ "proposal-sent", "proposal-received", "request-sent", "request-received", "presentation-sent", "presentation-received", "done", "abandoned" ] + "name" : "role", + "schema" : { + "enum" : [ "prover", "verifier" ], + "type" : "string" + } }, { - "name" : "thread_id", + "description" : "Presentation exchange state", "in" : "query", + "name" : "state", + "schema" : { + "enum" : [ "proposal-sent", "proposal-received", "request-sent", "request-received", "presentation-sent", "presentation-received", "done", "abandoned" ], + "type" : "string" + } + }, { "description" : "Thread identifier", - "required" : false, - "type" : "string", - "format" : "uuid" + "in" : "query", + "name" : "thread_id", + "schema" : { + "format" : "uuid", + "type" : "string" + } } ], "responses" : { "200" : { - "description" : "", - "schema" : { - "$ref" : "#/definitions/V20PresExRecordList" - } + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/V20PresExRecordList" + } + } + }, + "description" : "" } - } + }, + "summary" : "Fetch all present-proof exchange records", + "tags" : [ "present-proof v2.0" ] } }, "/present-proof-2.0/records/{pres_ex_id}" : { - "get" : { - "tags" : [ "present-proof v2.0" ], - "summary" : "Fetch a single presentation exchange record", - "produces" : [ "application/json" ], + "delete" : { "parameters" : [ { - "name" : "pres_ex_id", - "in" : "path", "description" : "Presentation exchange identifier", + "in" : "path", + "name" : "pres_ex_id", "required" : true, - "type" : "string", - "pattern" : "[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-4[a-fA-F0-9]{3}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}" + "schema" : { + "pattern" : "[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-4[a-fA-F0-9]{3}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}", + "type" : "string" + } } ], "responses" : { "200" : { - "description" : "", - "schema" : { - "$ref" : "#/definitions/V20PresExRecord" - } + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/V20PresentProofModuleResponse" + } + } + }, + "description" : "" } - } - }, - "delete" : { - "tags" : [ "present-proof v2.0" ], + }, "summary" : "Remove an existing presentation exchange record", - "produces" : [ "application/json" ], + "tags" : [ "present-proof v2.0" ] + }, + "get" : { "parameters" : [ { - "name" : "pres_ex_id", - "in" : "path", "description" : "Presentation exchange identifier", + "in" : "path", + "name" : "pres_ex_id", "required" : true, - "type" : "string", - "pattern" : "[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-4[a-fA-F0-9]{3}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}" + "schema" : { + "pattern" : "[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-4[a-fA-F0-9]{3}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}", + "type" : "string" + } } ], "responses" : { "200" : { - "description" : "", - "schema" : { - "$ref" : "#/definitions/V20PresentProofModuleResponse" - } + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/V20PresExRecord" + } + } + }, + "description" : "" } - } + }, + "summary" : "Fetch a single presentation exchange record", + "tags" : [ "present-proof v2.0" ] } }, "/present-proof-2.0/records/{pres_ex_id}/credentials" : { "get" : { - "tags" : [ "present-proof v2.0" ], - "summary" : "Fetch credentials from wallet for presentation request", - "produces" : [ "application/json" ], "parameters" : [ { - "name" : "pres_ex_id", - "in" : "path", "description" : "Presentation exchange identifier", + "in" : "path", + "name" : "pres_ex_id", "required" : true, - "type" : "string", - "pattern" : "[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-4[a-fA-F0-9]{3}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}" + "schema" : { + "pattern" : "[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-4[a-fA-F0-9]{3}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}", + "type" : "string" + } }, { - "name" : "count", - "in" : "query", "description" : "Maximum number to retrieve", - "required" : false, - "type" : "string", - "pattern" : "^[1-9][0-9]*$" - }, { - "name" : "extra_query", "in" : "query", - "description" : "(JSON) object mapping referents to extra WQL queries", - "required" : false, - "type" : "string", - "pattern" : "^{\\s*\".*?\"\\s*:\\s*{.*?}\\s*(,\\s*\".*?\"\\s*:\\s*{.*?}\\s*)*\\s*}$" + "name" : "count", + "schema" : { + "pattern" : "^[1-9][0-9]*$", + "type" : "string" + } }, { - "name" : "referent", + "description" : "(JSON) object mapping referents to extra WQL queries", "in" : "query", - "description" : "Proof request referents of interest, comma-separated", - "required" : false, - "type" : "string" + "name" : "extra_query", + "schema" : { + "pattern" : "^{\\s*\".*?\"\\s*:\\s*{.*?}\\s*(,\\s*\".*?\"\\s*:\\s*{.*?}\\s*)*\\s*}$", + "type" : "string" + } }, { - "name" : "start", + "description" : "Proof request referents of interest, comma-separated", "in" : "query", + "name" : "referent", + "schema" : { + "type" : "string" + } + }, { "description" : "Start index", - "required" : false, - "type" : "string", - "pattern" : "^[0-9]*$" + "in" : "query", + "name" : "start", + "schema" : { + "pattern" : "^[0-9]*$", + "type" : "string" + } } ], "responses" : { "200" : { - "description" : "", - "schema" : { - "type" : "array", - "items" : { - "$ref" : "#/definitions/IndyCredPrecis" + "content" : { + "application/json" : { + "schema" : { + "items" : { + "$ref" : "#/components/schemas/IndyCredPrecis" + }, + "type" : "array" + } } - } + }, + "description" : "" } - } + }, + "summary" : "Fetch credentials from wallet for presentation request", + "tags" : [ "present-proof v2.0" ] } }, "/present-proof-2.0/records/{pres_ex_id}/problem-report" : { "post" : { - "tags" : [ "present-proof v2.0" ], - "summary" : "Send a problem report for presentation exchange", - "produces" : [ "application/json" ], "parameters" : [ { - "in" : "body", - "name" : "body", - "required" : false, - "schema" : { - "$ref" : "#/definitions/V20PresProblemReportRequest" - } - }, { - "name" : "pres_ex_id", - "in" : "path", "description" : "Presentation exchange identifier", + "in" : "path", + "name" : "pres_ex_id", "required" : true, - "type" : "string", - "pattern" : "[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-4[a-fA-F0-9]{3}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}" + "schema" : { + "pattern" : "[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-4[a-fA-F0-9]{3}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}", + "type" : "string" + } } ], + "requestBody" : { + "content" : { + "*/*" : { + "schema" : { + "$ref" : "#/components/schemas/V20PresProblemReportRequest" + } + } + }, + "required" : false + }, "responses" : { "200" : { - "description" : "", - "schema" : { - "$ref" : "#/definitions/V20PresentProofModuleResponse" - } + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/V20PresentProofModuleResponse" + } + } + }, + "description" : "" } - } + }, + "summary" : "Send a problem report for presentation exchange", + "tags" : [ "present-proof v2.0" ], + "x-codegen-request-body-name" : "body" } }, "/present-proof-2.0/records/{pres_ex_id}/send-presentation" : { "post" : { - "tags" : [ "present-proof v2.0" ], - "summary" : "Sends a proof presentation", - "produces" : [ "application/json" ], "parameters" : [ { - "in" : "body", - "name" : "body", - "required" : false, - "schema" : { - "$ref" : "#/definitions/V20PresSpecByFormatRequest" - } - }, { - "name" : "pres_ex_id", - "in" : "path", "description" : "Presentation exchange identifier", + "in" : "path", + "name" : "pres_ex_id", "required" : true, - "type" : "string", - "pattern" : "[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-4[a-fA-F0-9]{3}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}" + "schema" : { + "pattern" : "[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-4[a-fA-F0-9]{3}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}", + "type" : "string" + } } ], + "requestBody" : { + "content" : { + "*/*" : { + "schema" : { + "$ref" : "#/components/schemas/V20PresSpecByFormatRequest" + } + } + }, + "required" : false + }, "responses" : { "200" : { - "description" : "", - "schema" : { - "$ref" : "#/definitions/V20PresExRecord" - } + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/V20PresExRecord" + } + } + }, + "description" : "" } - } + }, + "summary" : "Sends a proof presentation", + "tags" : [ "present-proof v2.0" ], + "x-codegen-request-body-name" : "body" } }, "/present-proof-2.0/records/{pres_ex_id}/send-request" : { "post" : { - "tags" : [ "present-proof v2.0" ], - "summary" : "Sends a presentation request in reference to a proposal", - "produces" : [ "application/json" ], "parameters" : [ { - "in" : "body", - "name" : "body", - "required" : false, - "schema" : { - "$ref" : "#/definitions/AdminAPIMessageTracing" - } - }, { - "name" : "pres_ex_id", - "in" : "path", "description" : "Presentation exchange identifier", + "in" : "path", + "name" : "pres_ex_id", "required" : true, - "type" : "string", - "pattern" : "[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-4[a-fA-F0-9]{3}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}" + "schema" : { + "pattern" : "[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-4[a-fA-F0-9]{3}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}", + "type" : "string" + } } ], + "requestBody" : { + "content" : { + "*/*" : { + "schema" : { + "$ref" : "#/components/schemas/V20PresentationSendRequestToProposal" + } + } + }, + "required" : false + }, "responses" : { "200" : { - "description" : "", - "schema" : { - "$ref" : "#/definitions/V20PresExRecord" - } + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/V20PresExRecord" + } + } + }, + "description" : "" } - } + }, + "summary" : "Sends a presentation request in reference to a proposal", + "tags" : [ "present-proof v2.0" ], + "x-codegen-request-body-name" : "body" } }, "/present-proof-2.0/records/{pres_ex_id}/verify-presentation" : { "post" : { - "tags" : [ "present-proof v2.0" ], - "summary" : "Verify a received presentation", - "produces" : [ "application/json" ], "parameters" : [ { - "name" : "pres_ex_id", - "in" : "path", "description" : "Presentation exchange identifier", + "in" : "path", + "name" : "pres_ex_id", "required" : true, - "type" : "string", - "pattern" : "[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-4[a-fA-F0-9]{3}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}" + "schema" : { + "pattern" : "[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-4[a-fA-F0-9]{3}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}", + "type" : "string" + } } ], "responses" : { "200" : { - "description" : "", - "schema" : { - "$ref" : "#/definitions/V20PresExRecord" - } + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/V20PresExRecord" + } + } + }, + "description" : "" } - } + }, + "summary" : "Verify a received presentation", + "tags" : [ "present-proof v2.0" ] } }, "/present-proof-2.0/send-proposal" : { "post" : { - "tags" : [ "present-proof v2.0" ], - "summary" : "Sends a presentation proposal", - "produces" : [ "application/json" ], - "parameters" : [ { - "in" : "body", - "name" : "body", - "required" : false, - "schema" : { - "$ref" : "#/definitions/V20PresProposalRequest" - } - } ], + "requestBody" : { + "content" : { + "*/*" : { + "schema" : { + "$ref" : "#/components/schemas/V20PresProposalRequest" + } + } + }, + "required" : false + }, "responses" : { "200" : { - "description" : "", - "schema" : { - "$ref" : "#/definitions/V20PresExRecord" - } + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/V20PresExRecord" + } + } + }, + "description" : "" } - } + }, + "summary" : "Sends a presentation proposal", + "tags" : [ "present-proof v2.0" ], + "x-codegen-request-body-name" : "body" } }, "/present-proof-2.0/send-request" : { "post" : { - "tags" : [ "present-proof v2.0" ], - "summary" : "Sends a free presentation request not bound to any proposal", - "produces" : [ "application/json" ], - "parameters" : [ { - "in" : "body", - "name" : "body", - "required" : false, - "schema" : { - "$ref" : "#/definitions/V20PresSendRequestRequest" - } - } ], + "requestBody" : { + "content" : { + "*/*" : { + "schema" : { + "$ref" : "#/components/schemas/V20PresSendRequestRequest" + } + } + }, + "required" : false + }, "responses" : { "200" : { - "description" : "", - "schema" : { - "$ref" : "#/definitions/V20PresExRecord" - } + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/V20PresExRecord" + } + } + }, + "description" : "" } - } + }, + "summary" : "Sends a free presentation request not bound to any proposal", + "tags" : [ "present-proof v2.0" ], + "x-codegen-request-body-name" : "body" } }, "/present-proof/create-request" : { "post" : { - "tags" : [ "present-proof v1.0" ], - "summary" : "Creates a presentation request not bound to any proposal or connection", - "produces" : [ "application/json" ], - "parameters" : [ { - "in" : "body", - "name" : "body", - "required" : false, - "schema" : { - "$ref" : "#/definitions/V10PresentationCreateRequestRequest" - } - } ], + "requestBody" : { + "content" : { + "*/*" : { + "schema" : { + "$ref" : "#/components/schemas/V10PresentationCreateRequestRequest" + } + } + }, + "required" : false + }, "responses" : { "200" : { - "description" : "", - "schema" : { - "$ref" : "#/definitions/V10PresentationExchange" - } + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/V10PresentationExchange" + } + } + }, + "description" : "" } - } + }, + "summary" : "Creates a presentation request not bound to any proposal or connection", + "tags" : [ "present-proof v1.0" ], + "x-codegen-request-body-name" : "body" } }, "/present-proof/records" : { "get" : { - "tags" : [ "present-proof v1.0" ], - "summary" : "Fetch all present-proof exchange records", - "produces" : [ "application/json" ], "parameters" : [ { - "name" : "connection_id", - "in" : "query", "description" : "Connection identifier", - "required" : false, - "type" : "string", - "format" : "uuid" - }, { - "name" : "role", "in" : "query", - "description" : "Role assigned in presentation exchange", - "required" : false, - "type" : "string", - "enum" : [ "prover", "verifier" ] + "name" : "connection_id", + "schema" : { + "format" : "uuid", + "type" : "string" + } }, { - "name" : "state", + "description" : "Role assigned in presentation exchange", "in" : "query", - "description" : "Presentation exchange state", - "required" : false, - "type" : "string", - "enum" : [ "proposal_sent", "proposal_received", "request_sent", "request_received", "presentation_sent", "presentation_received", "verified", "presentation_acked" ] + "name" : "role", + "schema" : { + "enum" : [ "prover", "verifier" ], + "type" : "string" + } }, { - "name" : "thread_id", + "description" : "Presentation exchange state", "in" : "query", + "name" : "state", + "schema" : { + "enum" : [ "proposal_sent", "proposal_received", "request_sent", "request_received", "presentation_sent", "presentation_received", "verified", "presentation_acked", "abandoned" ], + "type" : "string" + } + }, { "description" : "Thread identifier", - "required" : false, - "type" : "string", - "format" : "uuid" + "in" : "query", + "name" : "thread_id", + "schema" : { + "format" : "uuid", + "type" : "string" + } } ], "responses" : { "200" : { - "description" : "", - "schema" : { - "$ref" : "#/definitions/V10PresentationExchangeList" - } + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/V10PresentationExchangeList" + } + } + }, + "description" : "" } - } + }, + "summary" : "Fetch all present-proof exchange records", + "tags" : [ "present-proof v1.0" ] } }, "/present-proof/records/{pres_ex_id}" : { - "get" : { - "tags" : [ "present-proof v1.0" ], - "summary" : "Fetch a single presentation exchange record", - "produces" : [ "application/json" ], + "delete" : { "parameters" : [ { - "name" : "pres_ex_id", - "in" : "path", "description" : "Presentation exchange identifier", + "in" : "path", + "name" : "pres_ex_id", "required" : true, - "type" : "string", - "pattern" : "[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-4[a-fA-F0-9]{3}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}" + "schema" : { + "pattern" : "[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-4[a-fA-F0-9]{3}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}", + "type" : "string" + } } ], "responses" : { "200" : { - "description" : "", - "schema" : { - "$ref" : "#/definitions/V10PresentationExchange" - } + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/V10PresentProofModuleResponse" + } + } + }, + "description" : "" } - } - }, - "delete" : { - "tags" : [ "present-proof v1.0" ], + }, "summary" : "Remove an existing presentation exchange record", - "produces" : [ "application/json" ], + "tags" : [ "present-proof v1.0" ] + }, + "get" : { "parameters" : [ { - "name" : "pres_ex_id", - "in" : "path", "description" : "Presentation exchange identifier", + "in" : "path", + "name" : "pres_ex_id", "required" : true, - "type" : "string", - "pattern" : "[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-4[a-fA-F0-9]{3}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}" + "schema" : { + "pattern" : "[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-4[a-fA-F0-9]{3}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}", + "type" : "string" + } } ], "responses" : { "200" : { - "description" : "", - "schema" : { - "$ref" : "#/definitions/V10PresentProofModuleResponse" - } + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/V10PresentationExchange" + } + } + }, + "description" : "" } - } + }, + "summary" : "Fetch a single presentation exchange record", + "tags" : [ "present-proof v1.0" ] } }, "/present-proof/records/{pres_ex_id}/credentials" : { "get" : { - "tags" : [ "present-proof v1.0" ], - "summary" : "Fetch credentials for a presentation request from wallet", - "produces" : [ "application/json" ], "parameters" : [ { - "name" : "pres_ex_id", - "in" : "path", "description" : "Presentation exchange identifier", + "in" : "path", + "name" : "pres_ex_id", "required" : true, - "type" : "string", - "pattern" : "[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-4[a-fA-F0-9]{3}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}" + "schema" : { + "pattern" : "[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-4[a-fA-F0-9]{3}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}", + "type" : "string" + } }, { - "name" : "count", - "in" : "query", "description" : "Maximum number to retrieve", - "required" : false, - "type" : "string", - "pattern" : "^[1-9][0-9]*$" - }, { - "name" : "extra_query", "in" : "query", - "description" : "(JSON) object mapping referents to extra WQL queries", - "required" : false, - "type" : "string", - "pattern" : "^{\\s*\".*?\"\\s*:\\s*{.*?}\\s*(,\\s*\".*?\"\\s*:\\s*{.*?}\\s*)*\\s*}$" + "name" : "count", + "schema" : { + "pattern" : "^[1-9][0-9]*$", + "type" : "string" + } }, { - "name" : "referent", + "description" : "(JSON) object mapping referents to extra WQL queries", "in" : "query", - "description" : "Proof request referents of interest, comma-separated", - "required" : false, - "type" : "string" + "name" : "extra_query", + "schema" : { + "pattern" : "^{\\s*\".*?\"\\s*:\\s*{.*?}\\s*(,\\s*\".*?\"\\s*:\\s*{.*?}\\s*)*\\s*}$", + "type" : "string" + } }, { - "name" : "start", + "description" : "Proof request referents of interest, comma-separated", "in" : "query", + "name" : "referent", + "schema" : { + "type" : "string" + } + }, { "description" : "Start index", - "required" : false, - "type" : "string", - "pattern" : "^[0-9]*$" + "in" : "query", + "name" : "start", + "schema" : { + "pattern" : "^[0-9]*$", + "type" : "string" + } } ], "responses" : { "200" : { - "description" : "", - "schema" : { - "type" : "array", - "items" : { - "$ref" : "#/definitions/IndyCredPrecis" + "content" : { + "application/json" : { + "schema" : { + "items" : { + "$ref" : "#/components/schemas/IndyCredPrecis" + }, + "type" : "array" + } } - } + }, + "description" : "" } - } + }, + "summary" : "Fetch credentials for a presentation request from wallet", + "tags" : [ "present-proof v1.0" ] } }, "/present-proof/records/{pres_ex_id}/problem-report" : { "post" : { - "tags" : [ "present-proof v1.0" ], - "summary" : "Send a problem report for presentation exchange", - "produces" : [ "application/json" ], "parameters" : [ { - "in" : "body", - "name" : "body", - "required" : false, - "schema" : { - "$ref" : "#/definitions/V10PresentationProblemReportRequest" - } - }, { - "name" : "pres_ex_id", - "in" : "path", "description" : "Presentation exchange identifier", + "in" : "path", + "name" : "pres_ex_id", "required" : true, - "type" : "string", - "pattern" : "[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-4[a-fA-F0-9]{3}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}" + "schema" : { + "pattern" : "[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-4[a-fA-F0-9]{3}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}", + "type" : "string" + } } ], + "requestBody" : { + "content" : { + "*/*" : { + "schema" : { + "$ref" : "#/components/schemas/V10PresentationProblemReportRequest" + } + } + }, + "required" : false + }, "responses" : { "200" : { - "description" : "", - "schema" : { - "$ref" : "#/definitions/V10PresentProofModuleResponse" - } + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/V10PresentProofModuleResponse" + } + } + }, + "description" : "" } - } + }, + "summary" : "Send a problem report for presentation exchange", + "tags" : [ "present-proof v1.0" ], + "x-codegen-request-body-name" : "body" } }, "/present-proof/records/{pres_ex_id}/send-presentation" : { "post" : { - "tags" : [ "present-proof v1.0" ], - "summary" : "Sends a proof presentation", - "produces" : [ "application/json" ], "parameters" : [ { - "in" : "body", - "name" : "body", - "required" : false, - "schema" : { - "$ref" : "#/definitions/IndyPresSpec" - } - }, { - "name" : "pres_ex_id", - "in" : "path", "description" : "Presentation exchange identifier", + "in" : "path", + "name" : "pres_ex_id", "required" : true, - "type" : "string", - "pattern" : "[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-4[a-fA-F0-9]{3}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}" + "schema" : { + "pattern" : "[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-4[a-fA-F0-9]{3}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}", + "type" : "string" + } } ], + "requestBody" : { + "content" : { + "*/*" : { + "schema" : { + "$ref" : "#/components/schemas/IndyPresSpec" + } + } + }, + "required" : false + }, "responses" : { "200" : { - "description" : "", - "schema" : { - "$ref" : "#/definitions/V10PresentationExchange" - } + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/V10PresentationExchange" + } + } + }, + "description" : "" } - } + }, + "summary" : "Sends a proof presentation", + "tags" : [ "present-proof v1.0" ], + "x-codegen-request-body-name" : "body" } }, "/present-proof/records/{pres_ex_id}/send-request" : { "post" : { - "tags" : [ "present-proof v1.0" ], - "summary" : "Sends a presentation request in reference to a proposal", - "produces" : [ "application/json" ], "parameters" : [ { - "in" : "body", - "name" : "body", - "required" : false, - "schema" : { - "$ref" : "#/definitions/AdminAPIMessageTracing" - } - }, { - "name" : "pres_ex_id", - "in" : "path", "description" : "Presentation exchange identifier", + "in" : "path", + "name" : "pres_ex_id", "required" : true, - "type" : "string", - "pattern" : "[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-4[a-fA-F0-9]{3}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}" + "schema" : { + "pattern" : "[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-4[a-fA-F0-9]{3}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}", + "type" : "string" + } } ], + "requestBody" : { + "content" : { + "*/*" : { + "schema" : { + "$ref" : "#/components/schemas/V10PresentationSendRequestToProposal" + } + } + }, + "required" : false + }, "responses" : { "200" : { - "description" : "", - "schema" : { - "$ref" : "#/definitions/V10PresentationExchange" - } + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/V10PresentationExchange" + } + } + }, + "description" : "" } - } + }, + "summary" : "Sends a presentation request in reference to a proposal", + "tags" : [ "present-proof v1.0" ], + "x-codegen-request-body-name" : "body" } }, "/present-proof/records/{pres_ex_id}/verify-presentation" : { "post" : { - "tags" : [ "present-proof v1.0" ], - "summary" : "Verify a received presentation", - "produces" : [ "application/json" ], "parameters" : [ { - "name" : "pres_ex_id", - "in" : "path", "description" : "Presentation exchange identifier", + "in" : "path", + "name" : "pres_ex_id", "required" : true, - "type" : "string", - "pattern" : "[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-4[a-fA-F0-9]{3}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}" + "schema" : { + "pattern" : "[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-4[a-fA-F0-9]{3}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}", + "type" : "string" + } } ], "responses" : { "200" : { - "description" : "", - "schema" : { - "$ref" : "#/definitions/V10PresentationExchange" - } + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/V10PresentationExchange" + } + } + }, + "description" : "" } - } + }, + "summary" : "Verify a received presentation", + "tags" : [ "present-proof v1.0" ] } }, "/present-proof/send-proposal" : { "post" : { - "tags" : [ "present-proof v1.0" ], - "summary" : "Sends a presentation proposal", - "produces" : [ "application/json" ], - "parameters" : [ { - "in" : "body", - "name" : "body", - "required" : false, - "schema" : { - "$ref" : "#/definitions/V10PresentationProposalRequest" - } - } ], + "requestBody" : { + "content" : { + "*/*" : { + "schema" : { + "$ref" : "#/components/schemas/V10PresentationProposalRequest" + } + } + }, + "required" : false + }, "responses" : { "200" : { - "description" : "", - "schema" : { - "$ref" : "#/definitions/V10PresentationExchange" - } + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/V10PresentationExchange" + } + } + }, + "description" : "" } - } + }, + "summary" : "Sends a presentation proposal", + "tags" : [ "present-proof v1.0" ], + "x-codegen-request-body-name" : "body" } }, "/present-proof/send-request" : { "post" : { - "tags" : [ "present-proof v1.0" ], - "summary" : "Sends a free presentation request not bound to any proposal", - "produces" : [ "application/json" ], - "parameters" : [ { - "in" : "body", - "name" : "body", - "required" : false, - "schema" : { - "$ref" : "#/definitions/V10PresentationSendRequestRequest" - } - } ], + "requestBody" : { + "content" : { + "*/*" : { + "schema" : { + "$ref" : "#/components/schemas/V10PresentationSendRequestRequest" + } + } + }, + "required" : false + }, "responses" : { "200" : { - "description" : "", - "schema" : { - "$ref" : "#/definitions/V10PresentationExchange" - } + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/V10PresentationExchange" + } + } + }, + "description" : "" } - } + }, + "summary" : "Sends a free presentation request not bound to any proposal", + "tags" : [ "present-proof v1.0" ], + "x-codegen-request-body-name" : "body" } }, "/resolver/resolve/{did}" : { "get" : { - "tags" : [ "resolver" ], - "summary" : "Retrieve doc for requested did", - "produces" : [ "application/json" ], "parameters" : [ { - "name" : "did", - "in" : "path", "description" : "DID", + "in" : "path", + "name" : "did", "required" : true, - "type" : "string", - "pattern" : "^did:([a-z0-9]+):((?:[a-zA-Z0-9._%-]*:)*[a-zA-Z0-9._%-]+)$" + "schema" : { + "pattern" : "^did:([a-z0-9]+):((?:[a-zA-Z0-9._%-]*:)*[a-zA-Z0-9._%-]+)$", + "type" : "string" + } } ], "responses" : { "200" : { - "description" : "null", - "schema" : { - "$ref" : "#/definitions/ResolutionResult" - } + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/ResolutionResult" + } + } + }, + "description" : "" } - } + }, + "summary" : "Retrieve doc for requested did", + "tags" : [ "resolver" ] } }, "/revocation/active-registry/{cred_def_id}" : { "get" : { - "tags" : [ "revocation" ], - "summary" : "Get current active revocation registry by credential definition id", - "produces" : [ "application/json" ], "parameters" : [ { - "name" : "cred_def_id", - "in" : "path", "description" : "Credential definition identifier", + "in" : "path", + "name" : "cred_def_id", "required" : true, - "type" : "string", - "pattern" : "^([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}):3:CL:(([1-9][0-9]*)|([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}:2:.+:[0-9.]+)):(.+)?$" + "schema" : { + "pattern" : "^([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}):3:CL:(([1-9][0-9]*)|([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}:2:.+:[0-9.]+)):(.+)?$", + "type" : "string" + } } ], "responses" : { "200" : { - "description" : "", - "schema" : { - "$ref" : "#/definitions/RevRegResult" - } + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/RevRegResult" + } + } + }, + "description" : "" } - } + }, + "summary" : "Get current active revocation registry by credential definition id", + "tags" : [ "revocation" ] } }, "/revocation/clear-pending-revocations" : { "post" : { - "tags" : [ "revocation" ], - "summary" : "Clear pending revocations", - "produces" : [ "application/json" ], - "parameters" : [ { - "in" : "body", - "name" : "body", - "required" : false, - "schema" : { - "$ref" : "#/definitions/ClearPendingRevocationsRequest" - } - } ], + "requestBody" : { + "content" : { + "*/*" : { + "schema" : { + "$ref" : "#/components/schemas/ClearPendingRevocationsRequest" + } + } + }, + "required" : false + }, "responses" : { "200" : { - "description" : "", - "schema" : { - "$ref" : "#/definitions/PublishRevocations" - } + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/PublishRevocations" + } + } + }, + "description" : "" } - } + }, + "summary" : "Clear pending revocations", + "tags" : [ "revocation" ], + "x-codegen-request-body-name" : "body" } }, "/revocation/create-registry" : { "post" : { - "tags" : [ "revocation" ], - "summary" : "Creates a new revocation registry", - "produces" : [ "application/json" ], - "parameters" : [ { - "in" : "body", - "name" : "body", - "required" : false, - "schema" : { - "$ref" : "#/definitions/RevRegCreateRequest" - } - } ], + "requestBody" : { + "content" : { + "*/*" : { + "schema" : { + "$ref" : "#/components/schemas/RevRegCreateRequest" + } + } + }, + "required" : false + }, "responses" : { "200" : { - "description" : "", - "schema" : { - "$ref" : "#/definitions/RevRegResult" - } + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/RevRegResult" + } + } + }, + "description" : "" } - } + }, + "summary" : "Creates a new revocation registry", + "tags" : [ "revocation" ], + "x-codegen-request-body-name" : "body" } }, "/revocation/credential-record" : { "get" : { - "tags" : [ "revocation" ], - "summary" : "Get credential revocation status", - "produces" : [ "application/json" ], "parameters" : [ { - "name" : "cred_ex_id", - "in" : "query", "description" : "Credential exchange identifier", - "required" : false, - "type" : "string", - "pattern" : "[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-4[a-fA-F0-9]{3}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}" - }, { - "name" : "cred_rev_id", "in" : "query", - "description" : "Credential revocation identifier", - "required" : false, - "type" : "string", - "pattern" : "^[1-9][0-9]*$" + "name" : "cred_ex_id", + "schema" : { + "pattern" : "[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-4[a-fA-F0-9]{3}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}", + "type" : "string" + } }, { - "name" : "rev_reg_id", + "description" : "Credential revocation identifier", "in" : "query", + "name" : "cred_rev_id", + "schema" : { + "pattern" : "^[1-9][0-9]*$", + "type" : "string" + } + }, { "description" : "Revocation registry identifier", - "required" : false, - "type" : "string", - "pattern" : "^([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}):4:([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}):3:CL:(([1-9][0-9]*)|([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}:2:.+:[0-9.]+))(:.+)?:CL_ACCUM:(.+$)" + "in" : "query", + "name" : "rev_reg_id", + "schema" : { + "pattern" : "^([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}):4:([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}):3:CL:(([1-9][0-9]*)|([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}:2:.+:[0-9.]+))(:.+)?:CL_ACCUM:(.+$)", + "type" : "string" + } } ], "responses" : { "200" : { - "description" : "", - "schema" : { - "$ref" : "#/definitions/CredRevRecordResult" - } + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/CredRevRecordResult" + } + } + }, + "description" : "" } - } + }, + "summary" : "Get credential revocation status", + "tags" : [ "revocation" ] } }, "/revocation/publish-revocations" : { "post" : { - "tags" : [ "revocation" ], + "requestBody" : { + "content" : { + "*/*" : { + "schema" : { + "$ref" : "#/components/schemas/PublishRevocations" + } + } + }, + "required" : false + }, + "responses" : { + "200" : { + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/TxnOrPublishRevocationsResult" + } + } + }, + "description" : "" + } + }, "summary" : "Publish pending revocations to ledger", - "produces" : [ "application/json" ], + "tags" : [ "revocation" ], + "x-codegen-request-body-name" : "body" + } + }, + "/revocation/registries/created" : { + "get" : { "parameters" : [ { - "in" : "body", - "name" : "body", - "required" : false, + "description" : "Credential definition identifier", + "in" : "query", + "name" : "cred_def_id", "schema" : { - "$ref" : "#/definitions/PublishRevocations" + "pattern" : "^([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}):3:CL:(([1-9][0-9]*)|([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}:2:.+:[0-9.]+)):(.+)?$", + "type" : "string" } }, { - "name" : "conn_id", - "in" : "query", - "description" : "Connection identifier", - "required" : false, - "type" : "string" - }, { - "name" : "create_transaction_for_endorser", + "description" : "Revocation registry state", "in" : "query", - "description" : "Create Transaction For Endorser's signature", - "required" : false, - "type" : "boolean" + "name" : "state", + "schema" : { + "enum" : [ "init", "generated", "posted", "active", "full" ], + "type" : "string" + } } ], "responses" : { "200" : { - "description" : "", - "schema" : { - "$ref" : "#/definitions/TxnOrPublishRevocationsResult" - } + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/RevRegsCreated" + } + } + }, + "description" : "" } - } + }, + "summary" : "Search for matching revocation registries that current agent created", + "tags" : [ "revocation" ] } }, - "/revocation/registries/created" : { - "get" : { - "tags" : [ "revocation" ], - "summary" : "Search for matching revocation registries that current agent created", - "produces" : [ "application/json" ], + "/revocation/registry/delete-tails-file" : { + "delete" : { "parameters" : [ { - "name" : "cred_def_id", - "in" : "query", "description" : "Credential definition identifier", - "required" : false, - "type" : "string", - "pattern" : "^([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}):3:CL:(([1-9][0-9]*)|([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}:2:.+:[0-9.]+)):(.+)?$" + "in" : "query", + "name" : "cred_def_id", + "schema" : { + "pattern" : "^([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}):3:CL:(([1-9][0-9]*)|([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}:2:.+:[0-9.]+)):(.+)?$", + "type" : "string" + } }, { - "name" : "state", + "description" : "Revocation registry identifier", "in" : "query", - "description" : "Revocation registry state", - "required" : false, - "type" : "string", - "enum" : [ "init", "generated", "posted", "active", "full" ] + "name" : "rev_reg_id", + "schema" : { + "pattern" : "^([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}):4:([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}):3:CL:(([1-9][0-9]*)|([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}:2:.+:[0-9.]+))(:.+)?:CL_ACCUM:(.+$)", + "type" : "string" + } } ], "responses" : { "200" : { - "description" : "", - "schema" : { - "$ref" : "#/definitions/RevRegsCreated" - } + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/TailsDeleteResponse" + } + } + }, + "description" : "" } - } + }, + "summary" : "Delete the tail files", + "tags" : [ "revocation" ] } }, "/revocation/registry/{rev_reg_id}" : { "get" : { - "tags" : [ "revocation" ], - "summary" : "Get revocation registry by revocation registry id", - "produces" : [ "application/json" ], "parameters" : [ { - "name" : "rev_reg_id", - "in" : "path", "description" : "Revocation Registry identifier", + "in" : "path", + "name" : "rev_reg_id", "required" : true, - "type" : "string", - "pattern" : "^([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}):4:([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}):3:CL:(([1-9][0-9]*)|([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}:2:.+:[0-9.]+))(:.+)?:CL_ACCUM:(.+$)" + "schema" : { + "pattern" : "^([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}):4:([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}):3:CL:(([1-9][0-9]*)|([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}:2:.+:[0-9.]+))(:.+)?:CL_ACCUM:(.+$)", + "type" : "string" + } } ], "responses" : { "200" : { - "description" : "", - "schema" : { - "$ref" : "#/definitions/RevRegResult" - } + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/RevRegResult" + } + } + }, + "description" : "" } - } + }, + "summary" : "Get revocation registry by revocation registry id", + "tags" : [ "revocation" ] }, "patch" : { - "tags" : [ "revocation" ], - "summary" : "Update revocation registry with new public URI to its tails file", - "produces" : [ "application/json" ], "parameters" : [ { - "in" : "body", - "name" : "body", - "required" : false, - "schema" : { - "$ref" : "#/definitions/RevRegUpdateTailsFileUri" - } - }, { - "name" : "rev_reg_id", - "in" : "path", "description" : "Revocation Registry identifier", + "in" : "path", + "name" : "rev_reg_id", "required" : true, - "type" : "string", - "pattern" : "^([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}):4:([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}):3:CL:(([1-9][0-9]*)|([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}:2:.+:[0-9.]+))(:.+)?:CL_ACCUM:(.+$)" + "schema" : { + "pattern" : "^([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}):4:([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}):3:CL:(([1-9][0-9]*)|([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}:2:.+:[0-9.]+))(:.+)?:CL_ACCUM:(.+$)", + "type" : "string" + } } ], - "responses" : { - "200" : { - "description" : "", - "schema" : { - "$ref" : "#/definitions/RevRegResult" + "requestBody" : { + "content" : { + "*/*" : { + "schema" : { + "$ref" : "#/components/schemas/RevRegUpdateTailsFileUri" + } } + }, + "required" : false + }, + "responses" : { + "200" : { + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/RevRegResult" + } + } + }, + "description" : "" } - } + }, + "summary" : "Update revocation registry with new public URI to its tails file", + "tags" : [ "revocation" ], + "x-codegen-request-body-name" : "body" } }, "/revocation/registry/{rev_reg_id}/definition" : { "post" : { - "tags" : [ "revocation" ], - "summary" : "Send revocation registry definition to ledger", - "produces" : [ "application/json" ], "parameters" : [ { - "name" : "rev_reg_id", - "in" : "path", "description" : "Revocation Registry identifier", + "in" : "path", + "name" : "rev_reg_id", "required" : true, - "type" : "string", - "pattern" : "^([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}):4:([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}):3:CL:(([1-9][0-9]*)|([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}:2:.+:[0-9.]+))(:.+)?:CL_ACCUM:(.+$)" + "schema" : { + "pattern" : "^([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}):4:([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}):3:CL:(([1-9][0-9]*)|([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}:2:.+:[0-9.]+))(:.+)?:CL_ACCUM:(.+$)", + "type" : "string" + } }, { - "name" : "conn_id", - "in" : "query", "description" : "Connection identifier", - "required" : false, - "type" : "string" - }, { - "name" : "create_transaction_for_endorser", "in" : "query", + "name" : "conn_id", + "schema" : { + "type" : "string" + } + }, { "description" : "Create Transaction For Endorser's signature", - "required" : false, - "type" : "boolean" + "in" : "query", + "name" : "create_transaction_for_endorser", + "schema" : { + "type" : "boolean" + } } ], "responses" : { "200" : { - "description" : "", - "schema" : { - "$ref" : "#/definitions/TxnOrRevRegResult" - } + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/TxnOrRevRegResult" + } + } + }, + "description" : "" } - } + }, + "summary" : "Send revocation registry definition to ledger", + "tags" : [ "revocation" ] } }, "/revocation/registry/{rev_reg_id}/entry" : { "post" : { - "tags" : [ "revocation" ], - "summary" : "Send revocation registry entry to ledger", - "produces" : [ "application/json" ], "parameters" : [ { - "name" : "rev_reg_id", - "in" : "path", "description" : "Revocation Registry identifier", + "in" : "path", + "name" : "rev_reg_id", "required" : true, - "type" : "string", - "pattern" : "^([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}):4:([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}):3:CL:(([1-9][0-9]*)|([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}:2:.+:[0-9.]+))(:.+)?:CL_ACCUM:(.+$)" + "schema" : { + "pattern" : "^([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}):4:([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}):3:CL:(([1-9][0-9]*)|([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}:2:.+:[0-9.]+))(:.+)?:CL_ACCUM:(.+$)", + "type" : "string" + } }, { - "name" : "conn_id", - "in" : "query", "description" : "Connection identifier", - "required" : false, - "type" : "string" + "in" : "query", + "name" : "conn_id", + "schema" : { + "type" : "string" + } }, { + "description" : "Create Transaction For Endorser's signature", + "in" : "query", "name" : "create_transaction_for_endorser", + "schema" : { + "type" : "boolean" + } + } ], + "responses" : { + "200" : { + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/RevRegResult" + } + } + }, + "description" : "" + } + }, + "summary" : "Send revocation registry entry to ledger", + "tags" : [ "revocation" ] + } + }, + "/revocation/registry/{rev_reg_id}/fix-revocation-entry-state" : { + "put" : { + "parameters" : [ { + "description" : "Revocation Registry identifier", + "in" : "path", + "name" : "rev_reg_id", + "required" : true, + "schema" : { + "pattern" : "^([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}):4:([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}):3:CL:(([1-9][0-9]*)|([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}:2:.+:[0-9.]+))(:.+)?:CL_ACCUM:(.+$)", + "type" : "string" + } + }, { + "description" : "Apply updated accumulator transaction to ledger", "in" : "query", - "description" : "Create Transaction For Endorser's signature", - "required" : false, - "type" : "boolean" + "name" : "apply_ledger_update", + "required" : true, + "schema" : { + "type" : "boolean" + } } ], "responses" : { "200" : { - "description" : "", - "schema" : { - "$ref" : "#/definitions/RevRegResult" - } + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/RevRegWalletUpdatedResult" + } + } + }, + "description" : "" } - } + }, + "summary" : "Fix revocation state in wallet and return number of updated entries", + "tags" : [ "revocation" ] } }, "/revocation/registry/{rev_reg_id}/issued" : { "get" : { - "tags" : [ "revocation" ], - "summary" : "Get number of credentials issued against revocation registry", - "produces" : [ "application/json" ], "parameters" : [ { + "description" : "Revocation Registry identifier", + "in" : "path", "name" : "rev_reg_id", + "required" : true, + "schema" : { + "pattern" : "^([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}):4:([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}):3:CL:(([1-9][0-9]*)|([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}:2:.+:[0-9.]+))(:.+)?:CL_ACCUM:(.+$)", + "type" : "string" + } + } ], + "responses" : { + "200" : { + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/RevRegIssuedResult" + } + } + }, + "description" : "" + } + }, + "summary" : "Get number of credentials issued against revocation registry", + "tags" : [ "revocation" ] + } + }, + "/revocation/registry/{rev_reg_id}/issued/details" : { + "get" : { + "parameters" : [ { + "description" : "Revocation Registry identifier", "in" : "path", + "name" : "rev_reg_id", + "required" : true, + "schema" : { + "pattern" : "^([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}):4:([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}):3:CL:(([1-9][0-9]*)|([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}:2:.+:[0-9.]+))(:.+)?:CL_ACCUM:(.+$)", + "type" : "string" + } + } ], + "responses" : { + "200" : { + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/CredRevRecordDetailsResult" + } + } + }, + "description" : "" + } + }, + "summary" : "Get details of credentials issued against revocation registry", + "tags" : [ "revocation" ] + } + }, + "/revocation/registry/{rev_reg_id}/issued/indy_recs" : { + "get" : { + "parameters" : [ { "description" : "Revocation Registry identifier", + "in" : "path", + "name" : "rev_reg_id", "required" : true, - "type" : "string", - "pattern" : "^([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}):4:([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}):3:CL:(([1-9][0-9]*)|([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}:2:.+:[0-9.]+))(:.+)?:CL_ACCUM:(.+$)" + "schema" : { + "pattern" : "^([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}):4:([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}):3:CL:(([1-9][0-9]*)|([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}:2:.+:[0-9.]+))(:.+)?:CL_ACCUM:(.+$)", + "type" : "string" + } } ], "responses" : { "200" : { - "description" : "", - "schema" : { - "$ref" : "#/definitions/RevRegIssuedResult" - } + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/CredRevIndyRecordsResult" + } + } + }, + "description" : "" } - } + }, + "summary" : "Get details of revoked credentials from ledger", + "tags" : [ "revocation" ] } }, "/revocation/registry/{rev_reg_id}/set-state" : { "patch" : { - "tags" : [ "revocation" ], - "summary" : "Set revocation registry state manually", - "produces" : [ "application/json" ], "parameters" : [ { - "name" : "rev_reg_id", - "in" : "path", "description" : "Revocation Registry identifier", + "in" : "path", + "name" : "rev_reg_id", "required" : true, - "type" : "string", - "pattern" : "^([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}):4:([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}):3:CL:(([1-9][0-9]*)|([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}:2:.+:[0-9.]+))(:.+)?:CL_ACCUM:(.+$)" + "schema" : { + "pattern" : "^([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}):4:([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}):3:CL:(([1-9][0-9]*)|([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}:2:.+:[0-9.]+))(:.+)?:CL_ACCUM:(.+$)", + "type" : "string" + } }, { - "name" : "state", - "in" : "query", "description" : "Revocation registry state to set", + "in" : "query", + "name" : "state", "required" : true, - "type" : "string", - "enum" : [ "init", "generated", "posted", "active", "full" ] + "schema" : { + "enum" : [ "init", "generated", "posted", "active", "full" ], + "type" : "string" + } } ], "responses" : { "200" : { - "description" : "", - "schema" : { - "$ref" : "#/definitions/RevRegResult" - } + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/RevRegResult" + } + } + }, + "description" : "" } - } + }, + "summary" : "Set revocation registry state manually", + "tags" : [ "revocation" ] } }, "/revocation/registry/{rev_reg_id}/tails-file" : { "get" : { - "tags" : [ "revocation" ], - "summary" : "Download tails file", - "produces" : [ "application/octet-stream" ], "parameters" : [ { - "name" : "rev_reg_id", - "in" : "path", "description" : "Revocation Registry identifier", + "in" : "path", + "name" : "rev_reg_id", "required" : true, - "type" : "string", - "pattern" : "^([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}):4:([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}):3:CL:(([1-9][0-9]*)|([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}:2:.+:[0-9.]+))(:.+)?:CL_ACCUM:(.+$)" + "schema" : { + "pattern" : "^([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}):4:([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}):3:CL:(([1-9][0-9]*)|([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}:2:.+:[0-9.]+))(:.+)?:CL_ACCUM:(.+$)", + "type" : "string" + } } ], "responses" : { "200" : { - "description" : "tails file", - "schema" : { - "type" : "string", - "format" : "binary" - } + "content" : { + "application/octet-stream" : { + "schema" : { + "format" : "binary", + "type" : "string" + } + } + }, + "description" : "tails file" } - } + }, + "summary" : "Download tails file", + "tags" : [ "revocation" ] }, "put" : { - "tags" : [ "revocation" ], - "summary" : "Upload local tails file to server", - "produces" : [ "application/json" ], "parameters" : [ { - "name" : "rev_reg_id", - "in" : "path", "description" : "Revocation Registry identifier", + "in" : "path", + "name" : "rev_reg_id", "required" : true, - "type" : "string", - "pattern" : "^([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}):4:([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}):3:CL:(([1-9][0-9]*)|([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}:2:.+:[0-9.]+))(:.+)?:CL_ACCUM:(.+$)" + "schema" : { + "pattern" : "^([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}):4:([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}):3:CL:(([1-9][0-9]*)|([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}:2:.+:[0-9.]+))(:.+)?:CL_ACCUM:(.+$)", + "type" : "string" + } } ], "responses" : { "200" : { - "description" : "", - "schema" : { - "$ref" : "#/definitions/RevocationModuleResponse" - } + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/RevocationModuleResponse" + } + } + }, + "description" : "" } - } + }, + "summary" : "Upload local tails file to server", + "tags" : [ "revocation" ] } }, "/revocation/revoke" : { "post" : { - "tags" : [ "revocation" ], - "summary" : "Revoke an issued credential", - "produces" : [ "application/json" ], - "parameters" : [ { - "in" : "body", - "name" : "body", - "required" : false, - "schema" : { - "$ref" : "#/definitions/RevokeRequest" - } - } ], + "requestBody" : { + "content" : { + "*/*" : { + "schema" : { + "$ref" : "#/components/schemas/RevokeRequest" + } + } + }, + "required" : false + }, "responses" : { "200" : { - "description" : "", - "schema" : { - "$ref" : "#/definitions/RevocationModuleResponse" - } + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/RevocationModuleResponse" + } + } + }, + "description" : "" } - } + }, + "summary" : "Revoke an issued credential", + "tags" : [ "revocation" ], + "x-codegen-request-body-name" : "body" } }, "/schemas" : { "post" : { - "tags" : [ "schema" ], - "summary" : "Sends a schema to the ledger", - "produces" : [ "application/json" ], "parameters" : [ { - "in" : "body", - "name" : "body", - "required" : false, + "description" : "Connection identifier", + "in" : "query", + "name" : "conn_id", "schema" : { - "$ref" : "#/definitions/SchemaSendRequest" + "type" : "string" } }, { - "name" : "conn_id", + "description" : "Create Transaction For Endorser's signature", "in" : "query", - "description" : "Connection identifier", - "required" : false, - "type" : "string" - }, { "name" : "create_transaction_for_endorser", - "in" : "query", - "description" : "Create Transaction For Endorser's signature", - "required" : false, - "type" : "boolean" + "schema" : { + "type" : "boolean" + } } ], + "requestBody" : { + "content" : { + "*/*" : { + "schema" : { + "$ref" : "#/components/schemas/SchemaSendRequest" + } + } + }, + "required" : false + }, "responses" : { "200" : { - "description" : "", - "schema" : { - "$ref" : "#/definitions/TxnOrSchemaSendResult" - } + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/TxnOrSchemaSendResult" + } + } + }, + "description" : "" } - } + }, + "summary" : "Sends a schema to the ledger", + "tags" : [ "schema" ], + "x-codegen-request-body-name" : "body" } }, "/schemas/created" : { "get" : { - "tags" : [ "schema" ], - "summary" : "Search for matching schema that agent originated", - "produces" : [ "application/json" ], "parameters" : [ { - "name" : "schema_id", - "in" : "query", "description" : "Schema identifier", - "required" : false, - "type" : "string", - "pattern" : "^[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}:2:.+:[0-9.]+$" - }, { - "name" : "schema_issuer_did", "in" : "query", - "description" : "Schema issuer DID", - "required" : false, - "type" : "string", - "pattern" : "^(did:sov:)?[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}$" + "name" : "schema_id", + "schema" : { + "pattern" : "^[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}:2:.+:[0-9.]+$", + "type" : "string" + } }, { - "name" : "schema_name", + "description" : "Schema issuer DID", "in" : "query", - "description" : "Schema name", - "required" : false, - "type" : "string" + "name" : "schema_issuer_did", + "schema" : { + "pattern" : "^(did:sov:)?[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}$", + "type" : "string" + } }, { - "name" : "schema_version", + "description" : "Schema name", "in" : "query", + "name" : "schema_name", + "schema" : { + "type" : "string" + } + }, { "description" : "Schema version", - "required" : false, - "type" : "string", - "pattern" : "^[0-9.]+$" + "in" : "query", + "name" : "schema_version", + "schema" : { + "pattern" : "^[0-9.]+$", + "type" : "string" + } } ], "responses" : { "200" : { - "description" : "", - "schema" : { - "$ref" : "#/definitions/SchemasCreatedResult" - } + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/SchemasCreatedResult" + } + } + }, + "description" : "" } - } + }, + "summary" : "Search for matching schema that agent originated", + "tags" : [ "schema" ] } }, "/schemas/{schema_id}" : { "get" : { - "tags" : [ "schema" ], - "summary" : "Gets a schema from the ledger", - "produces" : [ "application/json" ], "parameters" : [ { - "name" : "schema_id", - "in" : "path", "description" : "Schema identifier", + "in" : "path", + "name" : "schema_id", "required" : true, - "type" : "string", - "pattern" : "^[1-9][0-9]*|[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}:2:.+:[0-9.]+$" + "schema" : { + "pattern" : "^[1-9][0-9]*|[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}:2:.+:[0-9.]+$", + "type" : "string" + } } ], "responses" : { "200" : { - "description" : "", - "schema" : { - "$ref" : "#/definitions/SchemaGetResult" - } + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/SchemaGetResult" + } + } + }, + "description" : "" } - } + }, + "summary" : "Gets a schema from the ledger", + "tags" : [ "schema" ] } }, "/schemas/{schema_id}/write_record" : { "post" : { - "tags" : [ "schema" ], - "summary" : "Writes a schema non-secret record to the wallet", - "produces" : [ "application/json" ], "parameters" : [ { - "name" : "schema_id", - "in" : "path", "description" : "Schema identifier", + "in" : "path", + "name" : "schema_id", "required" : true, - "type" : "string", - "pattern" : "^[1-9][0-9]*|[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}:2:.+:[0-9.]+$" + "schema" : { + "pattern" : "^[1-9][0-9]*|[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}:2:.+:[0-9.]+$", + "type" : "string" + } } ], "responses" : { "200" : { - "description" : "", - "schema" : { - "$ref" : "#/definitions/SchemaGetResult" - } + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/SchemaGetResult" + } + } + }, + "description" : "" } - } + }, + "summary" : "Writes a schema non-secret record to the wallet", + "tags" : [ "schema" ] } }, - "/shutdown" : { + "/settings" : { "get" : { - "tags" : [ "server" ], - "summary" : "Shut down server", + "tags" : [ "settings" ], + "summary" : "Get profile settings or config", "produces" : [ "application/json" ], "parameters" : [ ], "responses" : { "200" : { - "description" : "", - "schema" : { - "$ref" : "#/definitions/AdminShutdown" - } + "type" : "object", + "description" : "Settings for this wallet.", + "properties" : { } + } + } + }, + "put" : { + "tags" : [ "settings" ], + "summary" : "Update profile settings or config", + "produces" : [ "application/json" ], + "parameters" : [ { + "in" : "body", + "name" : "body", + "required" : false, + "schema" : { + "$ref" : "#/definitions/UpdateProfileSettingsRequest" + } + } ], + "responses" : { + "200" : { + "type" : "object", + "description" : "Settings for this wallet.", + "properties" : { } } } } }, + "/shutdown" : { + "get" : { + "responses" : { + "200" : { + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/AdminShutdown" + } + } + }, + "description" : "" + } + }, + "summary" : "Shut down server", + "tags" : [ "server" ] + } + }, "/status" : { "get" : { - "tags" : [ "server" ], - "summary" : "Fetch the server status", - "produces" : [ "application/json" ], - "parameters" : [ ], "responses" : { "200" : { - "description" : "", - "schema" : { - "$ref" : "#/definitions/AdminStatus" - } + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/AdminStatus" + } + } + }, + "description" : "" } - } + }, + "summary" : "Fetch the server status", + "tags" : [ "server" ] } }, "/status/config" : { "get" : { - "tags" : [ "server" ], - "summary" : "Fetch the server configuration", - "produces" : [ "application/json" ], - "parameters" : [ ], "responses" : { "200" : { - "description" : "", - "schema" : { - "$ref" : "#/definitions/AdminConfig" - } + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/AdminConfig" + } + } + }, + "description" : "" } - } + }, + "summary" : "Fetch the server configuration", + "tags" : [ "server" ] } }, "/status/live" : { "get" : { - "tags" : [ "server" ], - "summary" : "Liveliness check", - "produces" : [ "application/json" ], - "parameters" : [ ], "responses" : { "200" : { - "description" : "", - "schema" : { - "$ref" : "#/definitions/AdminStatusLiveliness" - } + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/AdminStatusLiveliness" + } + } + }, + "description" : "" } - } + }, + "summary" : "Liveliness check", + "tags" : [ "server" ] } }, "/status/ready" : { "get" : { - "tags" : [ "server" ], - "summary" : "Readiness check", - "produces" : [ "application/json" ], - "parameters" : [ ], "responses" : { "200" : { - "description" : "", - "schema" : { - "$ref" : "#/definitions/AdminStatusReadiness" - } + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/AdminStatusReadiness" + } + } + }, + "description" : "" } - } + }, + "summary" : "Readiness check", + "tags" : [ "server" ] } }, "/status/reset" : { "post" : { - "tags" : [ "server" ], - "summary" : "Reset statistics", - "produces" : [ "application/json" ], - "parameters" : [ ], "responses" : { "200" : { - "description" : "", - "schema" : { - "$ref" : "#/definitions/AdminReset" - } + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/AdminReset" + } + } + }, + "description" : "" } - } + }, + "summary" : "Reset statistics", + "tags" : [ "server" ] } }, "/transaction/{tran_id}/resend" : { "post" : { - "tags" : [ "endorse-transaction" ], - "summary" : "For Author to resend a particular transaction request", - "produces" : [ "application/json" ], "parameters" : [ { - "name" : "tran_id", - "in" : "path", "description" : "Transaction identifier", + "in" : "path", + "name" : "tran_id", "required" : true, - "type" : "string" + "schema" : { + "type" : "string" + } } ], "responses" : { "200" : { - "description" : "null", - "schema" : { - "$ref" : "#/definitions/TransactionRecord" - } + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/TransactionRecord" + } + } + }, + "description" : "" } - } + }, + "summary" : "For Author to resend a particular transaction request", + "tags" : [ "endorse-transaction" ] } }, "/transactions" : { "get" : { - "tags" : [ "endorse-transaction" ], - "summary" : "Query transactions", - "produces" : [ "application/json" ], - "parameters" : [ ], "responses" : { "200" : { - "description" : "null", - "schema" : { - "$ref" : "#/definitions/TransactionList" - } + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/TransactionList" + } + } + }, + "description" : "" } - } + }, + "summary" : "Query transactions", + "tags" : [ "endorse-transaction" ] } }, "/transactions/create-request" : { "post" : { - "tags" : [ "endorse-transaction" ], - "summary" : "For author to send a transaction request", - "produces" : [ "application/json" ], "parameters" : [ { - "in" : "body", - "name" : "body", - "required" : false, + "description" : "Transaction identifier", + "in" : "query", + "name" : "tran_id", + "required" : true, "schema" : { - "$ref" : "#/definitions/Date" + "type" : "string" } }, { - "name" : "tran_id", + "description" : "Endorser will write the transaction after endorsing it", "in" : "query", - "description" : "Transaction identifier", - "required" : true, - "type" : "string" - }, { "name" : "endorser_write_txn", - "in" : "query", - "description" : "Endorser will write the transaction after endorsing it", - "required" : false, - "type" : "boolean" + "schema" : { + "type" : "boolean" + } } ], + "requestBody" : { + "content" : { + "*/*" : { + "schema" : { + "$ref" : "#/components/schemas/Date" + } + } + }, + "required" : false + }, "responses" : { "200" : { - "description" : "null", - "schema" : { - "$ref" : "#/definitions/TransactionRecord" - } + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/TransactionRecord" + } + } + }, + "description" : "" } - } + }, + "summary" : "For author to send a transaction request", + "tags" : [ "endorse-transaction" ], + "x-codegen-request-body-name" : "body" } }, "/transactions/{conn_id}/set-endorser-info" : { "post" : { - "tags" : [ "endorse-transaction" ], - "summary" : "Set Endorser Info", - "produces" : [ "application/json" ], "parameters" : [ { - "name" : "conn_id", - "in" : "path", "description" : "Connection identifier", + "in" : "path", + "name" : "conn_id", "required" : true, - "type" : "string" + "schema" : { + "type" : "string" + } }, { - "name" : "endorser_did", - "in" : "query", "description" : "Endorser DID", + "in" : "query", + "name" : "endorser_did", "required" : true, - "type" : "string" + "schema" : { + "type" : "string" + } }, { - "name" : "endorser_name", - "in" : "query", "description" : "Endorser Name", - "required" : false, - "type" : "string" + "in" : "query", + "name" : "endorser_name", + "schema" : { + "type" : "string" + } } ], "responses" : { "200" : { - "description" : "null", - "schema" : { - "$ref" : "#/definitions/EndorserInfo" - } + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/EndorserInfo" + } + } + }, + "description" : "" } - } + }, + "summary" : "Set Endorser Info", + "tags" : [ "endorse-transaction" ] } }, "/transactions/{conn_id}/set-endorser-role" : { "post" : { - "tags" : [ "endorse-transaction" ], - "summary" : "Set transaction jobs", - "produces" : [ "application/json" ], "parameters" : [ { - "name" : "conn_id", - "in" : "path", "description" : "Connection identifier", + "in" : "path", + "name" : "conn_id", "required" : true, - "type" : "string" + "schema" : { + "type" : "string" + } }, { - "name" : "transaction_my_job", - "in" : "query", "description" : "Transaction related jobs", - "required" : false, - "type" : "string", - "enum" : [ "TRANSACTION_AUTHOR", "TRANSACTION_ENDORSER", "reset" ] + "in" : "query", + "name" : "transaction_my_job", + "schema" : { + "enum" : [ "TRANSACTION_AUTHOR", "TRANSACTION_ENDORSER", "reset" ], + "type" : "string" + } } ], "responses" : { "200" : { - "description" : "null", - "schema" : { - "$ref" : "#/definitions/TransactionJobs" - } + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/TransactionJobs" + } + } + }, + "description" : "" } - } + }, + "summary" : "Set transaction jobs", + "tags" : [ "endorse-transaction" ] } }, "/transactions/{tran_id}" : { "get" : { - "tags" : [ "endorse-transaction" ], - "summary" : "Fetch a single transaction record", - "produces" : [ "application/json" ], "parameters" : [ { - "name" : "tran_id", - "in" : "path", "description" : "Transaction identifier", + "in" : "path", + "name" : "tran_id", "required" : true, - "type" : "string" + "schema" : { + "type" : "string" + } } ], "responses" : { "200" : { - "description" : "null", - "schema" : { - "$ref" : "#/definitions/TransactionRecord" - } + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/TransactionRecord" + } + } + }, + "description" : "" } - } + }, + "summary" : "Fetch a single transaction record", + "tags" : [ "endorse-transaction" ] } }, "/transactions/{tran_id}/cancel" : { "post" : { - "tags" : [ "endorse-transaction" ], - "summary" : "For Author to cancel a particular transaction request", - "produces" : [ "application/json" ], "parameters" : [ { - "name" : "tran_id", - "in" : "path", "description" : "Transaction identifier", + "in" : "path", + "name" : "tran_id", "required" : true, - "type" : "string" + "schema" : { + "type" : "string" + } } ], "responses" : { "200" : { - "description" : "null", - "schema" : { - "$ref" : "#/definitions/TransactionRecord" - } + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/TransactionRecord" + } + } + }, + "description" : "" } - } + }, + "summary" : "For Author to cancel a particular transaction request", + "tags" : [ "endorse-transaction" ] } }, "/transactions/{tran_id}/endorse" : { "post" : { - "tags" : [ "endorse-transaction" ], - "summary" : "For Endorser to endorse a particular transaction record", - "produces" : [ "application/json" ], "parameters" : [ { - "name" : "tran_id", - "in" : "path", "description" : "Transaction identifier", + "in" : "path", + "name" : "tran_id", "required" : true, - "type" : "string" + "schema" : { + "type" : "string" + } + }, { + "description" : "Endorser DID", + "in" : "query", + "name" : "endorser_did", + "schema" : { + "type" : "string" + } } ], "responses" : { "200" : { - "description" : "null", - "schema" : { - "$ref" : "#/definitions/TransactionRecord" - } + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/TransactionRecord" + } + } + }, + "description" : "" } - } + }, + "summary" : "For Endorser to endorse a particular transaction record", + "tags" : [ "endorse-transaction" ] } }, "/transactions/{tran_id}/refuse" : { "post" : { - "tags" : [ "endorse-transaction" ], - "summary" : "For Endorser to refuse a particular transaction record", - "produces" : [ "application/json" ], "parameters" : [ { - "name" : "tran_id", - "in" : "path", "description" : "Transaction identifier", + "in" : "path", + "name" : "tran_id", "required" : true, - "type" : "string" + "schema" : { + "type" : "string" + } } ], "responses" : { "200" : { - "description" : "null", - "schema" : { - "$ref" : "#/definitions/TransactionRecord" - } + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/TransactionRecord" + } + } + }, + "description" : "" } - } + }, + "summary" : "For Endorser to refuse a particular transaction record", + "tags" : [ "endorse-transaction" ] } }, "/transactions/{tran_id}/write" : { "post" : { - "tags" : [ "endorse-transaction" ], - "summary" : "For Author / Endorser to write an endorsed transaction to the ledger", - "produces" : [ "application/json" ], "parameters" : [ { - "name" : "tran_id", - "in" : "path", "description" : "Transaction identifier", + "in" : "path", + "name" : "tran_id", "required" : true, - "type" : "string" + "schema" : { + "type" : "string" + } } ], "responses" : { "200" : { - "description" : "null", - "schema" : { - "$ref" : "#/definitions/TransactionRecord" - } + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/TransactionRecord" + } + } + }, + "description" : "" } - } + }, + "summary" : "For Author / Endorser to write an endorsed transaction to the ledger", + "tags" : [ "endorse-transaction" ] } }, "/wallet/did" : { "get" : { - "tags" : [ "wallet" ], - "summary" : "List wallet DIDs", - "produces" : [ "application/json" ], "parameters" : [ { - "name" : "did", - "in" : "query", "description" : "DID of interest", - "required" : false, - "type" : "string", - "pattern" : "^did:key:z[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]+$|^(did:sov:)?[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}$" - }, { - "name" : "key_type", "in" : "query", - "description" : "Key type to query for.", - "required" : false, - "type" : "string", - "enum" : [ "ed25519", "bls12381g2" ] + "name" : "did", + "schema" : { + "pattern" : "^(did:sov:)?[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}$|^did:([a-zA-Z0-9_]+):([a-zA-Z0-9_.%-]+(:[a-zA-Z0-9_.%-]+)*)((;[a-zA-Z0-9_.:%-]+=[a-zA-Z0-9_.:%-]*)*)(\\/[^#?]*)?([?][^#]*)?(\\#.*)?$$", + "type" : "string" + } }, { - "name" : "method", + "description" : "Key type to query for.", "in" : "query", - "description" : "DID method to query for. e.g. sov to only fetch indy/sov DIDs", - "required" : false, - "type" : "string", - "enum" : [ "key", "sov" ] + "name" : "key_type", + "schema" : { + "enum" : [ "ed25519", "bls12381g2" ], + "type" : "string" + } }, { - "name" : "posture", + "description" : "DID method to query for. e.g. sov to only fetch indy/sov DIDs", "in" : "query", - "description" : "Whether DID is current public DID, posted to ledger but current public DID, or local to the wallet", - "required" : false, - "type" : "string", - "enum" : [ "public", "posted", "wallet_only" ] + "name" : "method", + "schema" : { + "enum" : [ "key", "sov" ], + "type" : "string" + } }, { - "name" : "verkey", + "description" : "Whether DID is current public DID, posted to ledger but current public DID, or local to the wallet", "in" : "query", + "name" : "posture", + "schema" : { + "enum" : [ "public", "posted", "wallet_only" ], + "type" : "string" + } + }, { "description" : "Verification key of interest", - "required" : false, - "type" : "string", - "pattern" : "^[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{43,44}$" + "in" : "query", + "name" : "verkey", + "schema" : { + "pattern" : "^[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{43,44}$", + "type" : "string" + } } ], "responses" : { "200" : { - "description" : "", - "schema" : { - "$ref" : "#/definitions/DIDList" - } + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/DIDList" + } + } + }, + "description" : "" } - } + }, + "summary" : "List wallet DIDs", + "tags" : [ "wallet" ] } }, "/wallet/did/create" : { "post" : { - "tags" : [ "wallet" ], - "summary" : "Create a local DID", - "produces" : [ "application/json" ], - "parameters" : [ { - "in" : "body", - "name" : "body", - "required" : false, - "schema" : { - "$ref" : "#/definitions/DIDCreate" - } - } ], + "requestBody" : { + "content" : { + "*/*" : { + "schema" : { + "$ref" : "#/components/schemas/DIDCreate" + } + } + }, + "required" : false + }, "responses" : { "200" : { - "description" : "", - "schema" : { - "$ref" : "#/definitions/DIDResult" - } + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/DIDResult" + } + } + }, + "description" : "" } - } + }, + "summary" : "Create a local DID", + "tags" : [ "wallet" ], + "x-codegen-request-body-name" : "body" } }, "/wallet/did/local/rotate-keypair" : { "patch" : { - "tags" : [ "wallet" ], - "summary" : "Rotate keypair for a DID not posted to the ledger", - "produces" : [ "application/json" ], "parameters" : [ { - "name" : "did", - "in" : "query", "description" : "DID of interest", + "in" : "query", + "name" : "did", "required" : true, - "type" : "string", - "pattern" : "^(did:sov:)?[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}$" + "schema" : { + "pattern" : "^(did:sov:)?[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}$", + "type" : "string" + } } ], "responses" : { "200" : { - "description" : "", - "schema" : { - "$ref" : "#/definitions/WalletModuleResponse" - } + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/WalletModuleResponse" + } + } + }, + "description" : "" } - } + }, + "summary" : "Rotate keypair for a DID not posted to the ledger", + "tags" : [ "wallet" ] } }, "/wallet/did/public" : { "get" : { - "tags" : [ "wallet" ], - "summary" : "Fetch the current public DID", - "produces" : [ "application/json" ], - "parameters" : [ ], "responses" : { "200" : { - "description" : "", - "schema" : { - "$ref" : "#/definitions/DIDResult" - } + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/DIDResult" + } + } + }, + "description" : "" } - } + }, + "summary" : "Fetch the current public DID", + "tags" : [ "wallet" ] }, "post" : { - "tags" : [ "wallet" ], - "summary" : "Assign the current public DID", - "produces" : [ "application/json" ], "parameters" : [ { - "name" : "did", - "in" : "query", "description" : "DID of interest", + "in" : "query", + "name" : "did", "required" : true, - "type" : "string", - "pattern" : "^(did:sov:)?[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}$" + "schema" : { + "pattern" : "^(did:sov:)?[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}$", + "type" : "string" + } + }, { + "description" : "Connection identifier", + "in" : "query", + "name" : "conn_id", + "schema" : { + "type" : "string" + } + }, { + "description" : "Create Transaction For Endorser's signature", + "in" : "query", + "name" : "create_transaction_for_endorser", + "schema" : { + "type" : "boolean" + } + }, { + "description" : "Mediation identifier", + "in" : "query", + "name" : "mediation_id", + "schema" : { + "type" : "string" + } } ], "responses" : { "200" : { - "description" : "", - "schema" : { - "$ref" : "#/definitions/DIDResult" - } + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/DIDResult" + } + } + }, + "description" : "" } - } + }, + "summary" : "Assign the current public DID", + "tags" : [ "wallet" ] } }, "/wallet/get-did-endpoint" : { "get" : { - "tags" : [ "wallet" ], - "summary" : "Query DID endpoint in wallet", - "produces" : [ "application/json" ], "parameters" : [ { - "name" : "did", - "in" : "query", "description" : "DID of interest", + "in" : "query", + "name" : "did", "required" : true, - "type" : "string", - "pattern" : "^(did:sov:)?[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}$" + "schema" : { + "pattern" : "^(did:sov:)?[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}$", + "type" : "string" + } } ], "responses" : { "200" : { - "description" : "", - "schema" : { - "$ref" : "#/definitions/DIDEndpoint" - } + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/DIDEndpoint" + } + } + }, + "description" : "" } - } + }, + "summary" : "Query DID endpoint in wallet", + "tags" : [ "wallet" ] } }, "/wallet/set-did-endpoint" : { "post" : { - "tags" : [ "wallet" ], - "summary" : "Update endpoint in wallet and on ledger if posted to it", - "produces" : [ "application/json" ], "parameters" : [ { - "in" : "body", - "name" : "body", - "required" : false, + "description" : "Connection identifier", + "in" : "query", + "name" : "conn_id", + "schema" : { + "type" : "string" + } + }, { + "description" : "Create Transaction For Endorser's signature", + "in" : "query", + "name" : "create_transaction_for_endorser", "schema" : { - "$ref" : "#/definitions/DIDEndpointWithType" + "type" : "boolean" } } ], + "requestBody" : { + "content" : { + "*/*" : { + "schema" : { + "$ref" : "#/components/schemas/DIDEndpointWithType" + } + } + }, + "required" : false + }, "responses" : { "200" : { - "description" : "", - "schema" : { - "$ref" : "#/definitions/WalletModuleResponse" - } - } - } + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/WalletModuleResponse" + } + } + }, + "description" : "" + } + }, + "summary" : "Update endpoint in wallet and on ledger if posted to it", + "tags" : [ "wallet" ], + "x-codegen-request-body-name" : "body" } } }, - "securityDefinitions" : { - "AuthorizationHeader" : { - "description" : "Bearer token. Be sure to preprend token with 'Bearer '", - "type" : "apiKey", - "name" : "Authorization", - "in" : "header" - } - }, - "definitions" : { - "AMLRecord" : { - "type" : "object", - "properties" : { - "aml" : { - "type" : "object", - "additionalProperties" : { + "components" : { + "schemas" : { + "AMLRecord" : { + "properties" : { + "aml" : { + "additionalProperties" : { + "type" : "string" + }, + "type" : "object" + }, + "amlContext" : { + "type" : "string" + }, + "version" : { "type" : "string" } }, - "amlContext" : { - "type" : "string" - }, - "version" : { - "type" : "string" - } - } - }, - "ActionMenuFetchResult" : { - "type" : "object", - "properties" : { - "result" : { - "$ref" : "#/definitions/ActionMenuFetchResult_result" - } - } - }, - "ActionMenuModulesResult" : { - "type" : "object" - }, - "AdminAPIMessageTracing" : { - "type" : "object", - "properties" : { - "trace" : { - "type" : "boolean", - "description" : "Record trace information, based on agent configuration" - } - } - }, - "AdminConfig" : { - "type" : "object", - "properties" : { - "config" : { - "type" : "object", - "description" : "Configuration settings", - "properties" : { } - } - } - }, - "AdminMediationDeny" : { - "type" : "object", - "properties" : { - "mediator_terms" : { - "type" : "array", - "description" : "List of mediator rules for recipient", - "items" : { - "type" : "string", - "description" : "Indicate terms to which the mediator requires the recipient to agree" + "type" : "object" + }, + "ActionMenuFetchResult" : { + "properties" : { + "result" : { + "$ref" : "#/components/schemas/ActionMenuFetchResult_result" } }, - "recipient_terms" : { - "type" : "array", - "description" : "List of recipient rules for mediation", - "items" : { - "type" : "string", - "description" : "Indicate terms to which the recipient requires the mediator to agree" - } - } - } - }, - "AdminModules" : { - "type" : "object", - "properties" : { - "result" : { - "type" : "array", - "description" : "List of admin modules", - "items" : { - "type" : "string", - "description" : "admin module" + "type" : "object" + }, + "ActionMenuModulesResult" : { + "type" : "object" + }, + "AdminConfig" : { + "properties" : { + "config" : { + "description" : "Configuration settings", + "properties" : { }, + "type" : "object" } - } - } - }, - "AdminReset" : { - "type" : "object" - }, - "AdminShutdown" : { - "type" : "object" - }, - "AdminStatus" : { - "type" : "object", - "properties" : { - "conductor" : { - "type" : "object", - "description" : "Conductor statistics", - "properties" : { } - }, - "label" : { - "type" : "string", - "description" : "Default label", - "x-nullable" : true - }, - "timing" : { - "type" : "object", - "description" : "Timing results", - "properties" : { } }, - "version" : { - "type" : "string", - "description" : "Version code" - } - } - }, - "AdminStatusLiveliness" : { - "type" : "object", - "properties" : { - "alive" : { - "type" : "boolean", - "example" : true, - "description" : "Liveliness status" - } - } - }, - "AdminStatusReadiness" : { - "type" : "object", - "properties" : { - "ready" : { - "type" : "boolean", - "example" : true, - "description" : "Readiness status" - } - } - }, - "AttachDecorator" : { - "type" : "object", - "required" : [ "data" ], - "properties" : { - "@id" : { - "type" : "string", - "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", - "description" : "Attachment identifier" - }, - "byte_count" : { - "type" : "integer", - "format" : "int32", - "example" : 1234, - "description" : "Byte count of data included by reference" - }, - "data" : { - "$ref" : "#/definitions/AttachDecoratorData" - }, - "description" : { - "type" : "string", - "example" : "view from doorway, facing east, with lights off", - "description" : "Human-readable description of content" - }, - "filename" : { - "type" : "string", - "example" : "IMG1092348.png", - "description" : "File name" - }, - "lastmod_time" : { - "type" : "string", - "example" : "2021-12-31 23:59:59Z", - "description" : "Hint regarding last modification datetime, in ISO-8601 format", - "pattern" : "^\\d{4}-\\d\\d-\\d\\d[T ]\\d\\d:\\d\\d(?:\\:(?:\\d\\d(?:\\.\\d{1,6})?))?(?:[+-]\\d\\d:?\\d\\d|Z|)$" - }, - "mime-type" : { - "type" : "string", - "example" : "image/png", - "description" : "MIME type" - } - } - }, - "AttachDecoratorData" : { - "type" : "object", - "properties" : { - "base64" : { - "type" : "string", - "example" : "ey4uLn0=", - "description" : "Base64-encoded data", - "pattern" : "^[a-zA-Z0-9+/]*={0,2}$" - }, - "json" : { - "type" : "object", - "example" : "{\"sample\": \"content\"}", - "description" : "JSON-serialized data", - "properties" : { } - }, - "jws" : { - "$ref" : "#/definitions/AttachDecoratorData_jws" - }, - "links" : { - "type" : "array", - "description" : "List of hypertext links to data", - "items" : { - "type" : "string", - "example" : "https://link.to/data" + "type" : "object" + }, + "AdminMediationDeny" : { + "properties" : { + "mediator_terms" : { + "description" : "List of mediator rules for recipient", + "items" : { + "description" : "Indicate terms to which the mediator requires the recipient to agree", + "type" : "string" + }, + "type" : "array" + }, + "recipient_terms" : { + "description" : "List of recipient rules for mediation", + "items" : { + "description" : "Indicate terms to which the recipient requires the mediator to agree", + "type" : "string" + }, + "type" : "array" } }, - "sha256" : { - "type" : "string", - "example" : "617a48c7c8afe0521efdc03e5bb0ad9e655893e6b4b51f0e794d70fba132aacb", - "description" : "SHA256 hash (binhex encoded) of content", - "pattern" : "^[a-fA-F0-9+/]{64}$" - } - } - }, - "AttachDecoratorData1JWS" : { - "type" : "object", - "required" : [ "header", "signature" ], - "properties" : { - "header" : { - "$ref" : "#/definitions/AttachDecoratorDataJWSHeader" - }, - "protected" : { - "type" : "string", - "example" : "ey4uLn0", - "description" : "protected JWS header", - "pattern" : "^[-_a-zA-Z0-9]*$" - }, - "signature" : { - "type" : "string", - "example" : "ey4uLn0", - "description" : "signature", - "pattern" : "^[-_a-zA-Z0-9]*$" - } - } - }, - "AttachDecoratorDataJWS" : { - "type" : "object", - "properties" : { - "header" : { - "$ref" : "#/definitions/AttachDecoratorDataJWSHeader" - }, - "protected" : { - "type" : "string", - "example" : "ey4uLn0", - "description" : "protected JWS header", - "pattern" : "^[-_a-zA-Z0-9]*$" - }, - "signature" : { - "type" : "string", - "example" : "ey4uLn0", - "description" : "signature", - "pattern" : "^[-_a-zA-Z0-9]*$" - }, - "signatures" : { - "type" : "array", - "description" : "List of signatures", - "items" : { - "$ref" : "#/definitions/AttachDecoratorData1JWS" + "type" : "object" + }, + "AdminModules" : { + "properties" : { + "result" : { + "description" : "List of admin modules", + "items" : { + "description" : "admin module", + "type" : "string" + }, + "type" : "array" } - } - } - }, - "AttachDecoratorDataJWSHeader" : { - "type" : "object", - "required" : [ "kid" ], - "properties" : { - "kid" : { - "type" : "string", - "example" : "did:sov:LjgpST2rjsoxYegQDRm7EL#keys-4", - "description" : "Key identifier, in W3C did:key or DID URL format", - "pattern" : "^did:(?:key:z[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]+|sov:[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}(;.*)?(\\?.*)?#.+)$" - } - } - }, - "AttachmentDef" : { - "type" : "object", - "properties" : { - "id" : { - "type" : "string", - "example" : "attachment-0", - "description" : "Attachment identifier" }, - "type" : { - "type" : "string", - "example" : "present-proof", - "description" : "Attachment type", - "enum" : [ "credential-offer", "present-proof" ] - } - } - }, - "AttributeMimeTypesResult" : { - "type" : "object", - "properties" : { - "results" : { - "type" : "object", - "additionalProperties" : { - "type" : "string", - "description" : "MIME type" + "type" : "object" + }, + "AdminReset" : { + "type" : "object" + }, + "AdminShutdown" : { + "type" : "object" + }, + "AdminStatus" : { + "properties" : { + "conductor" : { + "description" : "Conductor statistics", + "properties" : { }, + "type" : "object" }, - "x-nullable" : true - } - } - }, - "BasicMessageModuleResponse" : { - "type" : "object" - }, - "ClaimFormat" : { - "type" : "object", - "properties" : { - "jwt" : { - "type" : "object", - "properties" : { } - }, - "jwt_vc" : { - "type" : "object", - "properties" : { } + "label" : { + "description" : "Default label", + "nullable" : true, + "type" : "string" + }, + "timing" : { + "description" : "Timing results", + "properties" : { }, + "type" : "object" + }, + "version" : { + "description" : "Version code", + "type" : "string" + } }, - "jwt_vp" : { - "type" : "object", - "properties" : { } + "type" : "object" + }, + "AdminStatusLiveliness" : { + "properties" : { + "alive" : { + "description" : "Liveliness status", + "example" : true, + "type" : "boolean" + } }, - "ldp" : { - "type" : "object", - "properties" : { } + "type" : "object" + }, + "AdminStatusReadiness" : { + "properties" : { + "ready" : { + "description" : "Readiness status", + "example" : true, + "type" : "boolean" + } }, - "ldp_vc" : { - "type" : "object", - "properties" : { } + "type" : "object" + }, + "AttachDecorator" : { + "properties" : { + "@id" : { + "description" : "Attachment identifier", + "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "type" : "string" + }, + "byte_count" : { + "description" : "Byte count of data included by reference", + "example" : 1234, + "format" : "int32", + "type" : "integer" + }, + "data" : { + "$ref" : "#/components/schemas/AttachDecoratorData" + }, + "description" : { + "description" : "Human-readable description of content", + "example" : "view from doorway, facing east, with lights off", + "type" : "string" + }, + "filename" : { + "description" : "File name", + "example" : "IMG1092348.png", + "type" : "string" + }, + "lastmod_time" : { + "description" : "Hint regarding last modification datetime, in ISO-8601 format", + "example" : "2021-12-31T23:59:59Z", + "pattern" : "^\\d{4}-\\d\\d-\\d\\d[T ]\\d\\d:\\d\\d(?:\\:(?:\\d\\d(?:\\.\\d{1,6})?))?(?:[+-]\\d\\d:?\\d\\d|Z|)$", + "type" : "string" + }, + "mime-type" : { + "description" : "MIME type", + "example" : "image/png", + "type" : "string" + } }, - "ldp_vp" : { - "type" : "object", - "properties" : { } - } - } - }, - "ClearPendingRevocationsRequest" : { - "type" : "object", - "properties" : { - "purge" : { - "type" : "object", - "description" : "Credential revocation ids by revocation registry id: omit for all, specify null or empty list for all pending per revocation registry", - "additionalProperties" : { - "type" : "array", + "required" : [ "data" ], + "type" : "object" + }, + "AttachDecoratorData" : { + "properties" : { + "base64" : { + "description" : "Base64-encoded data", + "example" : "ey4uLn0=", + "pattern" : "^[a-zA-Z0-9+/]*={0,2}$", + "type" : "string" + }, + "json" : { + "description" : "JSON-serialized data", + "example" : "{\"sample\": \"content\"}", + "type" : "object" + }, + "jws" : { + "$ref" : "#/components/schemas/AttachDecoratorData_jws" + }, + "links" : { + "description" : "List of hypertext links to data", "items" : { - "type" : "string", - "example" : "12345", - "description" : "Credential revocation identifier", - "pattern" : "^[1-9][0-9]*$" - } + "example" : "https://link.to/data", + "type" : "string" + }, + "type" : "array" + }, + "sha256" : { + "description" : "SHA256 hash (binhex encoded) of content", + "example" : "617a48c7c8afe0521efdc03e5bb0ad9e655893e6b4b51f0e794d70fba132aacb", + "pattern" : "^[a-fA-F0-9+/]{64}$", + "type" : "string" } - } - } - }, - "ConnRecord" : { - "type" : "object", - "properties" : { - "accept" : { - "type" : "string", - "example" : "auto", - "description" : "Connection acceptance: manual or auto", - "enum" : [ "manual", "auto" ] - }, - "alias" : { - "type" : "string", - "example" : "Bob, providing quotes", - "description" : "Optional alias to apply to connection for later use" - }, - "connection_id" : { - "type" : "string", - "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", - "description" : "Connection identifier" - }, - "connection_protocol" : { - "type" : "string", - "example" : "connections/1.0", - "description" : "Connection protocol used", - "enum" : [ "connections/1.0", "didexchange/1.0" ] - }, - "created_at" : { - "type" : "string", - "example" : "2021-12-31 23:59:59Z", - "description" : "Time of record creation", - "pattern" : "^\\d{4}-\\d\\d-\\d\\d[T ]\\d\\d:\\d\\d(?:\\:(?:\\d\\d(?:\\.\\d{1,6})?))?(?:[+-]\\d\\d:?\\d\\d|Z|)$" - }, - "error_msg" : { - "type" : "string", - "example" : "No DIDDoc provided; cannot connect to public DID", - "description" : "Error message" - }, - "inbound_connection_id" : { - "type" : "string", - "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", - "description" : "Inbound routing connection id to use" - }, - "invitation_key" : { - "type" : "string", - "example" : "H3C2AVvLMv6gmMNam3uVAjZpfkcJCwDwnZn6z3wXmqPV", - "description" : "Public key for connection", - "pattern" : "^[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{43,44}$" - }, - "invitation_mode" : { - "type" : "string", - "example" : "once", - "description" : "Invitation mode", - "enum" : [ "once", "multi", "static" ] - }, - "invitation_msg_id" : { - "type" : "string", - "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", - "description" : "ID of out-of-band invitation message" - }, - "my_did" : { - "type" : "string", - "example" : "WgWxqztrNooG92RXvxSTWv", - "description" : "Our DID for connection", - "pattern" : "^(did:sov:)?[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}$" - }, - "request_id" : { - "type" : "string", - "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", - "description" : "Connection request identifier" - }, - "rfc23_state" : { - "type" : "string", - "example" : "invitation-sent", - "description" : "State per RFC 23", - "readOnly" : true - }, - "routing_state" : { - "type" : "string", - "example" : "active", - "description" : "Routing state of connection", - "enum" : [ "none", "request", "active", "error" ] - }, - "state" : { - "type" : "string", - "example" : "active", - "description" : "Current record state" - }, - "their_did" : { - "type" : "string", - "example" : "WgWxqztrNooG92RXvxSTWv", - "description" : "Their DID for connection", - "pattern" : "^(did:sov:)?[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}$" - }, - "their_label" : { - "type" : "string", - "example" : "Bob", - "description" : "Their label for connection" - }, - "their_public_did" : { - "type" : "string", - "example" : "2cpBmR3FqGKWi5EyUbpRY8", - "description" : "Other agent's public DID for connection" - }, - "their_role" : { - "type" : "string", - "example" : "requester", - "description" : "Their role in the connection protocol", - "enum" : [ "invitee", "requester", "inviter", "responder" ] }, - "updated_at" : { - "type" : "string", - "example" : "2021-12-31 23:59:59Z", - "description" : "Time of last record update", - "pattern" : "^\\d{4}-\\d\\d-\\d\\d[T ]\\d\\d:\\d\\d(?:\\:(?:\\d\\d(?:\\.\\d{1,6})?))?(?:[+-]\\d\\d:?\\d\\d|Z|)$" - } - } - }, - "ConnectionInvitation" : { - "type" : "object", - "properties" : { - "@id" : { - "type" : "string", - "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", - "description" : "Message identifier" - }, - "@type" : { - "type" : "string", - "example" : "https://didcomm.org/my-family/1.0/my-message-type", - "description" : "Message type", - "readOnly" : true - }, - "did" : { - "type" : "string", - "example" : "WgWxqztrNooG92RXvxSTWv", - "description" : "DID for connection invitation", - "pattern" : "^(did:sov:)?[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}$" - }, - "imageUrl" : { - "type" : "string", - "format" : "url", - "example" : "http://192.168.56.101/img/logo.jpg", - "description" : "Optional image URL for connection invitation", - "x-nullable" : true - }, - "label" : { - "type" : "string", - "example" : "Bob", - "description" : "Optional label for connection invitation" - }, - "recipientKeys" : { - "type" : "array", - "description" : "List of recipient keys", - "items" : { - "type" : "string", - "example" : "H3C2AVvLMv6gmMNam3uVAjZpfkcJCwDwnZn6z3wXmqPV", - "description" : "Recipient public key", - "pattern" : "^[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{43,44}$" + "type" : "object" + }, + "AttachDecoratorData1JWS" : { + "properties" : { + "header" : { + "$ref" : "#/components/schemas/AttachDecoratorDataJWSHeader" + }, + "protected" : { + "description" : "protected JWS header", + "example" : "ey4uLn0", + "pattern" : "^[-_a-zA-Z0-9]*$", + "type" : "string" + }, + "signature" : { + "description" : "signature", + "example" : "ey4uLn0", + "pattern" : "^[-_a-zA-Z0-9]*$", + "type" : "string" } }, - "routingKeys" : { - "type" : "array", - "description" : "List of routing keys", - "items" : { - "type" : "string", - "example" : "H3C2AVvLMv6gmMNam3uVAjZpfkcJCwDwnZn6z3wXmqPV", - "description" : "Routing key", - "pattern" : "^[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{43,44}$" + "required" : [ "header", "signature" ], + "type" : "object" + }, + "AttachDecoratorDataJWS" : { + "properties" : { + "header" : { + "$ref" : "#/components/schemas/AttachDecoratorDataJWSHeader" + }, + "protected" : { + "description" : "protected JWS header", + "example" : "ey4uLn0", + "pattern" : "^[-_a-zA-Z0-9]*$", + "type" : "string" + }, + "signature" : { + "description" : "signature", + "example" : "ey4uLn0", + "pattern" : "^[-_a-zA-Z0-9]*$", + "type" : "string" + }, + "signatures" : { + "description" : "List of signatures", + "items" : { + "$ref" : "#/components/schemas/AttachDecoratorData1JWS" + }, + "type" : "array" } }, - "serviceEndpoint" : { - "type" : "string", - "example" : "http://192.168.56.101:8020", - "description" : "Service endpoint at which to reach this agent" - } - } - }, - "ConnectionList" : { - "type" : "object", - "properties" : { - "results" : { - "type" : "array", - "description" : "List of connection records", - "items" : { - "$ref" : "#/definitions/ConnRecord" - } - } - } - }, - "ConnectionMetadata" : { - "type" : "object", - "properties" : { - "results" : { - "type" : "object", - "description" : "Dictionary of metadata associated with connection.", - "properties" : { } - } - } - }, - "ConnectionMetadataSetRequest" : { - "type" : "object", - "required" : [ "metadata" ], - "properties" : { - "metadata" : { - "type" : "object", - "description" : "Dictionary of metadata to set for connection.", - "properties" : { } - } - } - }, - "ConnectionModuleResponse" : { - "type" : "object" - }, - "ConnectionStaticRequest" : { - "type" : "object", - "properties" : { - "alias" : { - "type" : "string", - "description" : "Alias to assign to this connection" - }, - "my_did" : { - "type" : "string", - "example" : "WgWxqztrNooG92RXvxSTWv", - "description" : "Local DID", - "pattern" : "^(did:sov:)?[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}$" - }, - "my_seed" : { - "type" : "string", - "description" : "Seed to use for the local DID" - }, - "their_did" : { - "type" : "string", - "example" : "WgWxqztrNooG92RXvxSTWv", - "description" : "Remote DID", - "pattern" : "^(did:sov:)?[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}$" - }, - "their_endpoint" : { - "type" : "string", - "example" : "https://myhost:8021", - "description" : "URL endpoint for other party", - "pattern" : "^[A-Za-z0-9\\.\\-\\+]+://([A-Za-z0-9][.A-Za-z0-9-_]+[A-Za-z0-9])+(:[1-9][0-9]*)?(/[^?&#]+)?$" - }, - "their_label" : { - "type" : "string", - "description" : "Other party's label for this connection" - }, - "their_seed" : { - "type" : "string", - "description" : "Seed to use for the remote DID" - }, - "their_verkey" : { - "type" : "string", - "description" : "Remote verification key" - } - } - }, - "ConnectionStaticResult" : { - "type" : "object", - "required" : [ "mv_verkey", "my_did", "my_endpoint", "record", "their_did", "their_verkey" ], - "properties" : { - "mv_verkey" : { - "type" : "string", - "example" : "H3C2AVvLMv6gmMNam3uVAjZpfkcJCwDwnZn6z3wXmqPV", - "description" : "My verification key", - "pattern" : "^[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{43,44}$" - }, - "my_did" : { - "type" : "string", - "example" : "WgWxqztrNooG92RXvxSTWv", - "description" : "Local DID", - "pattern" : "^(did:sov:)?[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}$" - }, - "my_endpoint" : { - "type" : "string", - "example" : "https://myhost:8021", - "description" : "My URL endpoint", - "pattern" : "^[A-Za-z0-9\\.\\-\\+]+://([A-Za-z0-9][.A-Za-z0-9-_]+[A-Za-z0-9])+(:[1-9][0-9]*)?(/[^?&#]+)?$" - }, - "record" : { - "$ref" : "#/definitions/ConnRecord" - }, - "their_did" : { - "type" : "string", - "example" : "WgWxqztrNooG92RXvxSTWv", - "description" : "Remote DID", - "pattern" : "^(did:sov:)?[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}$" - }, - "their_verkey" : { - "type" : "string", - "example" : "H3C2AVvLMv6gmMNam3uVAjZpfkcJCwDwnZn6z3wXmqPV", - "description" : "Remote verification key", - "pattern" : "^[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{43,44}$" - } - } - }, - "Constraints" : { - "type" : "object", - "properties" : { - "fields" : { - "type" : "array", - "items" : { - "$ref" : "#/definitions/DIFField" + "type" : "object" + }, + "AttachDecoratorDataJWSHeader" : { + "properties" : { + "kid" : { + "description" : "Key identifier, in W3C did:key or DID URL format", + "example" : "did:sov:LjgpST2rjsoxYegQDRm7EL#keys-4", + "pattern" : "^did:(?:key:z[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]+|sov:[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}(;.*)?(\\?.*)?#.+)$", + "type" : "string" } }, - "is_holder" : { - "type" : "array", - "items" : { - "$ref" : "#/definitions/DIFHolder" + "required" : [ "kid" ], + "type" : "object" + }, + "AttachmentDef" : { + "properties" : { + "id" : { + "description" : "Attachment identifier", + "example" : "attachment-0", + "type" : "string" + }, + "type" : { + "description" : "Attachment type", + "enum" : [ "credential-offer", "present-proof" ], + "example" : "present-proof", + "type" : "string" } }, - "limit_disclosure" : { - "type" : "string", - "description" : "LimitDisclosure" - }, - "status_active" : { - "type" : "string", - "enum" : [ "required", "allowed", "disallowed" ] - }, - "status_revoked" : { - "type" : "string", - "enum" : [ "required", "allowed", "disallowed" ] - }, - "status_suspended" : { - "type" : "string", - "enum" : [ "required", "allowed", "disallowed" ] - }, - "subject_is_issuer" : { - "type" : "string", - "description" : "SubjectIsIssuer", - "enum" : [ "required", "preferred" ] - } - } - }, - "CreateInvitationRequest" : { - "type" : "object", - "properties" : { - "mediation_id" : { - "type" : "string", - "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", - "description" : "Identifier for active mediation record to be used", - "pattern" : "[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-4[a-fA-F0-9]{3}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}" - }, - "metadata" : { - "type" : "object", - "description" : "Optional metadata to attach to the connection created with the invitation", - "properties" : { } - }, - "my_label" : { - "type" : "string", - "example" : "Bob", - "description" : "Optional label for connection invitation" - }, - "recipient_keys" : { - "type" : "array", - "description" : "List of recipient keys", - "items" : { - "type" : "string", - "example" : "H3C2AVvLMv6gmMNam3uVAjZpfkcJCwDwnZn6z3wXmqPV", - "description" : "Recipient public key", - "pattern" : "^[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{43,44}$" + "type" : "object" + }, + "AttributeMimeTypesResult" : { + "properties" : { + "results" : { + "additionalProperties" : { + "description" : "MIME type", + "type" : "string" + }, + "nullable" : true, + "type" : "object" } }, - "routing_keys" : { - "type" : "array", - "description" : "List of routing keys", - "items" : { - "type" : "string", - "example" : "H3C2AVvLMv6gmMNam3uVAjZpfkcJCwDwnZn6z3wXmqPV", - "description" : "Routing key", - "pattern" : "^[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{43,44}$" + "type" : "object" + }, + "BasicMessageModuleResponse" : { + "type" : "object" + }, + "ClaimFormat" : { + "properties" : { + "jwt" : { + "properties" : { }, + "type" : "object" + }, + "jwt_vc" : { + "properties" : { }, + "type" : "object" + }, + "jwt_vp" : { + "properties" : { }, + "type" : "object" + }, + "ldp" : { + "properties" : { }, + "type" : "object" + }, + "ldp_vc" : { + "properties" : { }, + "type" : "object" + }, + "ldp_vp" : { + "properties" : { }, + "type" : "object" } }, - "service_endpoint" : { - "type" : "string", - "example" : "http://192.168.56.102:8020", - "description" : "Connection endpoint" - } - } - }, - "CreateWalletRequest" : { - "type" : "object", - "properties" : { - "image_url" : { - "type" : "string", - "example" : "https://aries.ca/images/sample.png", - "description" : "Image url for this wallet. This image url is publicized (self-attested) to other agents as part of forming a connection." - }, - "key_management_mode" : { - "type" : "string", - "example" : "managed", - "description" : "Key management method to use for this wallet.", - "enum" : [ "managed" ] - }, - "label" : { - "type" : "string", - "example" : "Alice", - "description" : "Label for this wallet. This label is publicized (self-attested) to other agents as part of forming a connection." - }, - "wallet_dispatch_type" : { - "type" : "string", - "example" : "default", - "description" : "Webhook target dispatch type for this wallet. default - Dispatch only to webhooks associated with this wallet. base - Dispatch only to webhooks associated with the base wallet. both - Dispatch to both webhook targets.", - "enum" : [ "default", "both", "base" ] - }, - "wallet_key" : { - "type" : "string", - "example" : "MySecretKey123", - "description" : "Master key used for key derivation." - }, - "wallet_name" : { - "type" : "string", - "example" : "MyNewWallet", - "description" : "Wallet name" - }, - "wallet_type" : { - "type" : "string", - "example" : "indy", - "description" : "Type of the wallet to create", - "enum" : [ "askar", "in_memory", "indy" ] - }, - "wallet_webhook_urls" : { - "type" : "array", - "description" : "List of Webhook URLs associated with this subwallet", - "items" : { - "type" : "string", - "example" : "http://localhost:8022/webhooks", - "description" : "Optional webhook URL to receive webhook messages" + "type" : "object" + }, + "ClearPendingRevocationsRequest" : { + "properties" : { + "purge" : { + "additionalProperties" : { + "items" : { + "description" : "Credential revocation identifier", + "example" : "12345", + "pattern" : "^[1-9][0-9]*$", + "type" : "string" + }, + "type" : "array" + }, + "description" : "Credential revocation ids by revocation registry id: omit for all, specify null or empty list for all pending per revocation registry", + "type" : "object" } - } - } - }, - "CreateWalletResponse" : { - "type" : "object", - "required" : [ "key_management_mode", "wallet_id" ], - "properties" : { - "created_at" : { - "type" : "string", - "example" : "2021-12-31 23:59:59Z", - "description" : "Time of record creation", - "pattern" : "^\\d{4}-\\d\\d-\\d\\d[T ]\\d\\d:\\d\\d(?:\\:(?:\\d\\d(?:\\.\\d{1,6})?))?(?:[+-]\\d\\d:?\\d\\d|Z|)$" - }, - "key_management_mode" : { - "type" : "string", - "description" : "Mode regarding management of wallet key", - "enum" : [ "managed", "unmanaged" ] - }, - "settings" : { - "type" : "object", - "description" : "Settings for this wallet.", - "properties" : { } - }, - "state" : { - "type" : "string", - "example" : "active", - "description" : "Current record state" - }, - "token" : { - "type" : "string", - "example" : "eyJhbGciOiJFZERTQSJ9.eyJhIjogIjAifQ.dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk", - "description" : "Authorization token to authenticate wallet requests" - }, - "updated_at" : { - "type" : "string", - "example" : "2021-12-31 23:59:59Z", - "description" : "Time of last record update", - "pattern" : "^\\d{4}-\\d\\d-\\d\\d[T ]\\d\\d:\\d\\d(?:\\:(?:\\d\\d(?:\\.\\d{1,6})?))?(?:[+-]\\d\\d:?\\d\\d|Z|)$" - }, - "wallet_id" : { - "type" : "string", - "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", - "description" : "Wallet record ID" - } - } - }, - "CreateWalletTokenRequest" : { - "type" : "object", - "properties" : { - "wallet_key" : { - "type" : "string", - "example" : "MySecretKey123", - "description" : "Master key used for key derivation. Only required for unamanged wallets." - } - } - }, - "CreateWalletTokenResponse" : { - "type" : "object", - "properties" : { - "token" : { - "type" : "string", - "example" : "eyJhbGciOiJFZERTQSJ9.eyJhIjogIjAifQ.dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk", - "description" : "Authorization token to authenticate wallet requests" - } - } - }, - "CredAttrSpec" : { - "type" : "object", - "required" : [ "name", "value" ], - "properties" : { - "mime-type" : { - "type" : "string", - "example" : "image/jpeg", - "description" : "MIME type: omit for (null) default", - "x-nullable" : true - }, - "name" : { - "type" : "string", - "example" : "favourite_drink", - "description" : "Attribute name" - }, - "value" : { - "type" : "string", - "example" : "martini", - "description" : "Attribute value: base64-encode if MIME type is present" - } - } - }, - "CredDefValue" : { - "type" : "object", - "properties" : { - "primary" : { - "$ref" : "#/definitions/CredDefValue_primary" - }, - "revocation" : { - "$ref" : "#/definitions/CredDefValue_revocation" - } - } - }, - "CredDefValuePrimary" : { - "type" : "object", - "properties" : { - "n" : { - "type" : "string", - "example" : "0", - "pattern" : "^[0-9]*$" - }, - "r" : { - "$ref" : "#/definitions/Generated" - }, - "rctxt" : { - "type" : "string", - "example" : "0", - "pattern" : "^[0-9]*$" }, - "s" : { - "type" : "string", - "example" : "0", - "pattern" : "^[0-9]*$" - }, - "z" : { - "type" : "string", - "example" : "0", - "pattern" : "^[0-9]*$" - } - } - }, - "CredDefValueRevocation" : { - "type" : "object", - "properties" : { - "g" : { - "type" : "string", - "example" : "1 1F14F&ECB578F 2 095E45DDF417D" - }, - "g_dash" : { - "type" : "string", - "example" : "1 1D64716fCDC00C 1 0C781960FA66E3D3 2 095E45DDF417D" - }, - "h" : { - "type" : "string", - "example" : "1 16675DAE54BFAE8 2 095E45DD417D" - }, - "h0" : { - "type" : "string", - "example" : "1 21E5EF9476EAF18 2 095E45DDF417D" - }, - "h1" : { - "type" : "string", - "example" : "1 236D1D99236090 2 095E45DDF417D" - }, - "h2" : { - "type" : "string", - "example" : "1 1C3AE8D1F1E277 2 095E45DDF417D" - }, - "h_cap" : { - "type" : "string", - "example" : "1 1B2A32CF3167 1 2490FEBF6EE55 1 0000000000000000" - }, - "htilde" : { - "type" : "string", - "example" : "1 1D8549E8C0F8 2 095E45DDF417D" - }, - "pk" : { - "type" : "string", - "example" : "1 142CD5E5A7DC 1 153885BD903312 2 095E45DDF417D" - }, - "u" : { - "type" : "string", - "example" : "1 0C430AAB2B4710 1 1CB3A0932EE7E 1 0000000000000000" - }, - "y" : { - "type" : "string", - "example" : "1 153558BD903312 2 095E45DDF417D 1 0000000000000000" - } - } - }, - "CredInfoList" : { - "type" : "object", - "properties" : { - "results" : { - "type" : "array", - "items" : { - "$ref" : "#/definitions/IndyCredInfo" + "type" : "object" + }, + "ConnRecord" : { + "properties" : { + "accept" : { + "description" : "Connection acceptance: manual or auto", + "enum" : [ "manual", "auto" ], + "example" : "auto", + "type" : "string" + }, + "alias" : { + "description" : "Optional alias to apply to connection for later use", + "example" : "Bob, providing quotes", + "type" : "string" + }, + "connection_id" : { + "description" : "Connection identifier", + "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "type" : "string" + }, + "connection_protocol" : { + "description" : "Connection protocol used", + "enum" : [ "connections/1.0", "didexchange/1.0" ], + "example" : "connections/1.0", + "type" : "string" + }, + "created_at" : { + "description" : "Time of record creation", + "example" : "2021-12-31T23:59:59Z", + "pattern" : "^\\d{4}-\\d\\d-\\d\\d[T ]\\d\\d:\\d\\d(?:\\:(?:\\d\\d(?:\\.\\d{1,6})?))?(?:[+-]\\d\\d:?\\d\\d|Z|)$", + "type" : "string" + }, + "error_msg" : { + "description" : "Error message", + "example" : "No DIDDoc provided; cannot connect to public DID", + "type" : "string" + }, + "inbound_connection_id" : { + "description" : "Inbound routing connection id to use", + "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "type" : "string" + }, + "invitation_key" : { + "description" : "Public key for connection", + "example" : "H3C2AVvLMv6gmMNam3uVAjZpfkcJCwDwnZn6z3wXmqPV", + "pattern" : "^[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{43,44}$", + "type" : "string" + }, + "invitation_mode" : { + "description" : "Invitation mode", + "enum" : [ "once", "multi", "static" ], + "example" : "once", + "type" : "string" + }, + "invitation_msg_id" : { + "description" : "ID of out-of-band invitation message", + "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "type" : "string" + }, + "my_did" : { + "description" : "Our DID for connection", + "example" : "WgWxqztrNooG92RXvxSTWv", + "pattern" : "^(did:sov:)?[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}$", + "type" : "string" + }, + "request_id" : { + "description" : "Connection request identifier", + "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "type" : "string" + }, + "rfc23_state" : { + "description" : "State per RFC 23", + "example" : "invitation-sent", + "readOnly" : true, + "type" : "string" + }, + "routing_state" : { + "description" : "Routing state of connection", + "enum" : [ "none", "request", "active", "error" ], + "example" : "active", + "type" : "string" + }, + "state" : { + "description" : "Current record state", + "example" : "active", + "type" : "string" + }, + "their_did" : { + "description" : "Their DID for connection", + "example" : "WgWxqztrNooG92RXvxSTWv", + "pattern" : "^(did:sov:)?[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}$", + "type" : "string" + }, + "their_label" : { + "description" : "Their label for connection", + "example" : "Bob", + "type" : "string" + }, + "their_public_did" : { + "description" : "Other agent's public DID for connection", + "example" : "2cpBmR3FqGKWi5EyUbpRY8", + "type" : "string" + }, + "their_role" : { + "description" : "Their role in the connection protocol", + "enum" : [ "invitee", "requester", "inviter", "responder" ], + "example" : "requester", + "type" : "string" + }, + "updated_at" : { + "description" : "Time of last record update", + "example" : "2021-12-31T23:59:59Z", + "pattern" : "^\\d{4}-\\d\\d-\\d\\d[T ]\\d\\d:\\d\\d(?:\\:(?:\\d\\d(?:\\.\\d{1,6})?))?(?:[+-]\\d\\d:?\\d\\d|Z|)$", + "type" : "string" } - } - } - }, - "CredRevRecordResult" : { - "type" : "object", - "properties" : { - "result" : { - "$ref" : "#/definitions/IssuerCredRevRecord" - } - } - }, - "CredRevokedResult" : { - "type" : "object", - "properties" : { - "revoked" : { - "type" : "boolean", - "description" : "Whether credential is revoked on the ledger" - } - } - }, - "Credential" : { - "type" : "object", - "required" : [ "@context", "credentialSubject", "issuanceDate", "issuer", "type" ], - "properties" : { - "@context" : { - "type" : "array", - "example" : [ "https://www.w3.org/2018/credentials/v1", "https://www.w3.org/2018/credentials/examples/v1" ], - "description" : "The JSON-LD context of the credential", - "items" : { } - }, - "credentialSubject" : { - "example" : "" }, - "expirationDate" : { - "type" : "string", - "example" : "2010-01-01T19:23:24Z", - "description" : "The expiration date", - "pattern" : "^([0-9]{4})-([0-9]{2})-([0-9]{2})([Tt ]([0-9]{2}):([0-9]{2}):([0-9]{2})(\\.[0-9]+)?)?(([Zz]|([+-])([0-9]{2}):([0-9]{2})))?$" - }, - "id" : { - "type" : "string", - "example" : "http://example.edu/credentials/1872", - "pattern" : "\\w+:(\\/?\\/?)[^\\s]+" - }, - "issuanceDate" : { - "type" : "string", - "example" : "2010-01-01T19:23:24Z", - "description" : "The issuance date", - "pattern" : "^([0-9]{4})-([0-9]{2})-([0-9]{2})([Tt ]([0-9]{2}):([0-9]{2}):([0-9]{2})(\\.[0-9]+)?)?(([Zz]|([+-])([0-9]{2}):([0-9]{2})))?$" - }, - "issuer" : { - "example" : "did:key:z6MkpTHR8VNsBxYAAWHut2Geadd9jSwuBV8xRoAnwWsdvktH", - "description" : "The JSON-LD Verifiable Credential Issuer. Either string of object with id field." - }, - "proof" : { - "$ref" : "#/definitions/Credential_proof" - }, - "type" : { - "type" : "array", - "example" : [ "VerifiableCredential", "AlumniCredential" ], - "description" : "The JSON-LD type of the credential", - "items" : { + "type" : "object" + }, + "ConnectionInvitation" : { + "properties" : { + "@id" : { + "description" : "Message identifier", + "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "type" : "string" + }, + "@type" : { + "description" : "Message type", + "example" : "https://didcomm.org/my-family/1.0/my-message-type", + "readOnly" : true, + "type" : "string" + }, + "did" : { + "description" : "DID for connection invitation", + "example" : "WgWxqztrNooG92RXvxSTWv", + "pattern" : "^(did:sov:)?[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}$", + "type" : "string" + }, + "imageUrl" : { + "description" : "Optional image URL for connection invitation", + "example" : "http://192.168.56.101/img/logo.jpg", + "format" : "url", + "nullable" : true, + "type" : "string" + }, + "label" : { + "description" : "Optional label for connection invitation", + "example" : "Bob", + "type" : "string" + }, + "recipientKeys" : { + "description" : "List of recipient keys", + "items" : { + "description" : "Recipient public key", + "example" : "H3C2AVvLMv6gmMNam3uVAjZpfkcJCwDwnZn6z3wXmqPV", + "pattern" : "^[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{43,44}$", + "type" : "string" + }, + "type" : "array" + }, + "routingKeys" : { + "description" : "List of routing keys", + "items" : { + "description" : "Routing key", + "example" : "H3C2AVvLMv6gmMNam3uVAjZpfkcJCwDwnZn6z3wXmqPV", + "pattern" : "^[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{43,44}$", + "type" : "string" + }, + "type" : "array" + }, + "serviceEndpoint" : { + "description" : "Service endpoint at which to reach this agent", + "example" : "http://192.168.56.101:8020", "type" : "string" } - } - } - }, - "CredentialDefinition" : { - "type" : "object", - "properties" : { - "id" : { - "type" : "string", - "example" : "WgWxqztrNooG92RXvxSTWv:3:CL:20:tag", - "description" : "Credential definition identifier", - "pattern" : "^([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}):3:CL:(([1-9][0-9]*)|([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}:2:.+:[0-9.]+)):(.+)?$" - }, - "schemaId" : { - "type" : "string", - "example" : "20", - "description" : "Schema identifier within credential definition identifier" - }, - "tag" : { - "type" : "string", - "example" : "tag", - "description" : "Tag within credential definition identifier" - }, - "type" : { - "example" : "CL", - "description" : "Signature type: CL for Camenisch-Lysyanskaya" - }, - "value" : { - "$ref" : "#/definitions/CredentialDefinition_value" - }, - "ver" : { - "type" : "string", - "example" : "1.0", - "description" : "Node protocol version", - "pattern" : "^[0-9.]+$" - } - } - }, - "CredentialDefinitionGetResult" : { - "type" : "object", - "properties" : { - "credential_definition" : { - "$ref" : "#/definitions/CredentialDefinition" - } - } - }, - "CredentialDefinitionSendRequest" : { - "type" : "object", - "properties" : { - "revocation_registry_size" : { - "type" : "integer", - "format" : "int32", - "example" : 1000, - "description" : "Revocation registry size", - "minimum" : 4, - "maximum" : 32768 - }, - "schema_id" : { - "type" : "string", - "example" : "WgWxqztrNooG92RXvxSTWv:2:schema_name:1.0", - "description" : "Schema identifier", - "pattern" : "^[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}:2:.+:[0-9.]+$" }, - "support_revocation" : { - "type" : "boolean", - "description" : "Revocation supported flag" - }, - "tag" : { - "type" : "string", - "example" : "default", - "description" : "Credential definition identifier tag" - } - } - }, - "CredentialDefinitionSendResult" : { - "type" : "object", - "properties" : { - "credential_definition_id" : { - "type" : "string", - "example" : "WgWxqztrNooG92RXvxSTWv:3:CL:20:tag", - "description" : "Credential definition identifier", - "pattern" : "^([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}):3:CL:(([1-9][0-9]*)|([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}:2:.+:[0-9.]+)):(.+)?$" - } - } - }, - "CredentialDefinitionsCreatedResult" : { - "type" : "object", - "properties" : { - "credential_definition_ids" : { - "type" : "array", - "items" : { - "type" : "string", - "example" : "WgWxqztrNooG92RXvxSTWv:3:CL:20:tag", - "description" : "Credential definition identifiers", - "pattern" : "^([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}):3:CL:(([1-9][0-9]*)|([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}:2:.+:[0-9.]+)):(.+)?$" + "type" : "object" + }, + "ConnectionList" : { + "properties" : { + "results" : { + "description" : "List of connection records", + "items" : { + "$ref" : "#/components/schemas/ConnRecord" + }, + "type" : "array" } - } - } - }, - "CredentialOffer" : { - "type" : "object", - "required" : [ "offers~attach" ], - "properties" : { - "@id" : { - "type" : "string", - "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", - "description" : "Message identifier" }, - "@type" : { - "type" : "string", - "example" : "https://didcomm.org/my-family/1.0/my-message-type", - "description" : "Message type", - "readOnly" : true - }, - "comment" : { - "type" : "string", - "description" : "Human-readable comment", - "x-nullable" : true - }, - "credential_preview" : { - "$ref" : "#/definitions/CredentialPreview" - }, - "offers~attach" : { - "type" : "array", - "items" : { - "$ref" : "#/definitions/AttachDecorator" + "type" : "object" + }, + "ConnectionMetadata" : { + "properties" : { + "results" : { + "description" : "Dictionary of metadata associated with connection.", + "properties" : { }, + "type" : "object" } - } - } - }, - "CredentialPreview" : { - "type" : "object", - "required" : [ "attributes" ], - "properties" : { - "@type" : { - "type" : "string", - "example" : "issue-credential/1.0/credential-preview", - "description" : "Message type identifier" }, - "attributes" : { - "type" : "array", - "items" : { - "$ref" : "#/definitions/CredAttrSpec" + "type" : "object" + }, + "ConnectionMetadataSetRequest" : { + "properties" : { + "metadata" : { + "description" : "Dictionary of metadata to set for connection.", + "properties" : { }, + "type" : "object" } - } - } - }, - "CredentialProposal" : { - "type" : "object", - "properties" : { - "@id" : { - "type" : "string", - "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", - "description" : "Message identifier" - }, - "@type" : { - "type" : "string", - "example" : "https://didcomm.org/my-family/1.0/my-message-type", - "description" : "Message type", - "readOnly" : true - }, - "comment" : { - "type" : "string", - "description" : "Human-readable comment", - "x-nullable" : true - }, - "cred_def_id" : { - "type" : "string", - "example" : "WgWxqztrNooG92RXvxSTWv:3:CL:20:tag", - "pattern" : "^([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}):3:CL:(([1-9][0-9]*)|([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}:2:.+:[0-9.]+)):(.+)?$" - }, - "credential_proposal" : { - "$ref" : "#/definitions/CredentialPreview" - }, - "issuer_did" : { - "type" : "string", - "example" : "WgWxqztrNooG92RXvxSTWv", - "pattern" : "^(did:sov:)?[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}$" - }, - "schema_id" : { - "type" : "string", - "example" : "WgWxqztrNooG92RXvxSTWv:2:schema_name:1.0", - "pattern" : "^[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}:2:.+:[0-9.]+$" - }, - "schema_issuer_did" : { - "type" : "string", - "example" : "WgWxqztrNooG92RXvxSTWv", - "pattern" : "^(did:sov:)?[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}$" - }, - "schema_name" : { - "type" : "string" - }, - "schema_version" : { - "type" : "string", - "example" : "1.0", - "pattern" : "^[0-9.]+$" - } - } - }, - "CredentialStatusOptions" : { - "type" : "object", - "required" : [ "type" ], - "properties" : { - "type" : { - "type" : "string", - "example" : "CredentialStatusList2017", - "description" : "Credential status method type to use for the credential. Should match status method registered in the Verifiable Credential Extension Registry" - } - } - }, - "DID" : { - "type" : "object", - "properties" : { - "did" : { - "type" : "string", - "example" : "WgWxqztrNooG92RXvxSTWv", - "description" : "DID of interest", - "pattern" : "^did:key:z[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]+$|^(did:sov:)?[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}$" - }, - "key_type" : { - "type" : "string", - "example" : "ed25519", - "description" : "Key type associated with the DID", - "enum" : [ "ed25519", "bls12381g2" ] - }, - "method" : { - "type" : "string", - "example" : "sov", - "description" : "Did method associated with the DID", - "enum" : [ "sov", "key" ] - }, - "posture" : { - "type" : "string", - "example" : "wallet_only", - "description" : "Whether DID is current public DID, posted to ledger but not current public DID, or local to the wallet", - "enum" : [ "public", "posted", "wallet_only" ] - }, - "verkey" : { - "type" : "string", - "example" : "H3C2AVvLMv6gmMNam3uVAjZpfkcJCwDwnZn6z3wXmqPV", - "description" : "Public verification key", - "pattern" : "^[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{43,44}$" - } - } - }, - "DIDCreate" : { - "type" : "object", - "properties" : { - "method" : { - "type" : "string", - "example" : "sov", - "enum" : [ "key", "sov" ] - }, - "options" : { - "$ref" : "#/definitions/DIDCreate_options" - } - } - }, - "DIDCreateOptions" : { - "type" : "object", - "required" : [ "key_type" ], - "properties" : { - "key_type" : { - "type" : "string", - "example" : "ed25519", - "enum" : [ "ed25519", "bls12381g2" ] - } - } - }, - "DIDEndpoint" : { - "type" : "object", - "required" : [ "did" ], - "properties" : { - "did" : { - "type" : "string", - "example" : "WgWxqztrNooG92RXvxSTWv", - "description" : "DID of interest", - "pattern" : "^(did:sov:)?[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}$" }, - "endpoint" : { - "type" : "string", - "example" : "https://myhost:8021", - "description" : "Endpoint to set (omit to delete)", - "pattern" : "^[A-Za-z0-9\\.\\-\\+]+://([A-Za-z0-9][.A-Za-z0-9-_]+[A-Za-z0-9])+(:[1-9][0-9]*)?(/[^?&#]+)?$" - } - } - }, - "DIDEndpointWithType" : { - "type" : "object", - "required" : [ "did" ], - "properties" : { - "did" : { - "type" : "string", - "example" : "WgWxqztrNooG92RXvxSTWv", - "description" : "DID of interest", - "pattern" : "^(did:sov:)?[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}$" - }, - "endpoint" : { - "type" : "string", - "example" : "https://myhost:8021", - "description" : "Endpoint to set (omit to delete)", - "pattern" : "^[A-Za-z0-9\\.\\-\\+]+://([A-Za-z0-9][.A-Za-z0-9-_]+[A-Za-z0-9])+(:[1-9][0-9]*)?(/[^?&#]+)?$" - }, - "endpoint_type" : { - "type" : "string", - "example" : "Endpoint", - "description" : "Endpoint type to set (default 'Endpoint'); affects only public or posted DIDs", - "enum" : [ "Endpoint", "Profile", "LinkedDomains" ] - } - } - }, - "DIDList" : { - "type" : "object", - "properties" : { - "results" : { - "type" : "array", - "description" : "DID list", - "items" : { - "$ref" : "#/definitions/DID" + "required" : [ "metadata" ], + "type" : "object" + }, + "ConnectionModuleResponse" : { + "type" : "object" + }, + "ConnectionStaticRequest" : { + "properties" : { + "alias" : { + "description" : "Alias to assign to this connection", + "type" : "string" + }, + "my_did" : { + "description" : "Local DID", + "example" : "WgWxqztrNooG92RXvxSTWv", + "pattern" : "^(did:sov:)?[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}$", + "type" : "string" + }, + "my_seed" : { + "description" : "Seed to use for the local DID", + "type" : "string" + }, + "their_did" : { + "description" : "Remote DID", + "example" : "WgWxqztrNooG92RXvxSTWv", + "pattern" : "^(did:sov:)?[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}$", + "type" : "string" + }, + "their_endpoint" : { + "description" : "URL endpoint for other party", + "example" : "https://myhost:8021", + "pattern" : "^[A-Za-z0-9\\.\\-\\+]+://([A-Za-z0-9][.A-Za-z0-9-_]+[A-Za-z0-9])+(:[1-9][0-9]*)?(/[^?&#]+)?$", + "type" : "string" + }, + "their_label" : { + "description" : "Other party's label for this connection", + "type" : "string" + }, + "their_seed" : { + "description" : "Seed to use for the remote DID", + "type" : "string" + }, + "their_verkey" : { + "description" : "Remote verification key", + "type" : "string" } - } - } - }, - "DIDResult" : { - "type" : "object", - "properties" : { - "result" : { - "$ref" : "#/definitions/DID" - } - } - }, - "DIDXRequest" : { - "type" : "object", - "required" : [ "label" ], - "properties" : { - "@id" : { - "type" : "string", - "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", - "description" : "Message identifier" - }, - "@type" : { - "type" : "string", - "example" : "https://didcomm.org/my-family/1.0/my-message-type", - "description" : "Message type", - "readOnly" : true - }, - "did" : { - "type" : "string", - "example" : "WgWxqztrNooG92RXvxSTWv", - "description" : "DID of exchange", - "pattern" : "^(did:sov:)?[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}$" - }, - "did_doc~attach" : { - "$ref" : "#/definitions/DIDXRequest_did_docattach" - }, - "label" : { - "type" : "string", - "example" : "Request to connect with Bob", - "description" : "Label for DID exchange request" - } - } - }, - "DIFField" : { - "type" : "object", - "properties" : { - "filter" : { - "$ref" : "#/definitions/Filter" - }, - "id" : { - "type" : "string", - "description" : "ID" }, - "path" : { - "type" : "array", - "items" : { - "type" : "string", - "description" : "Path" + "type" : "object" + }, + "ConnectionStaticResult" : { + "properties" : { + "my_did" : { + "description" : "Local DID", + "example" : "WgWxqztrNooG92RXvxSTWv", + "pattern" : "^(did:sov:)?[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}$", + "type" : "string" + }, + "my_endpoint" : { + "description" : "My URL endpoint", + "example" : "https://myhost:8021", + "pattern" : "^[A-Za-z0-9\\.\\-\\+]+://([A-Za-z0-9][.A-Za-z0-9-_]+[A-Za-z0-9])+(:[1-9][0-9]*)?(/[^?&#]+)?$", + "type" : "string" + }, + "my_verkey" : { + "description" : "My verification key", + "example" : "H3C2AVvLMv6gmMNam3uVAjZpfkcJCwDwnZn6z3wXmqPV", + "pattern" : "^[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{43,44}$", + "type" : "string" + }, + "record" : { + "$ref" : "#/components/schemas/ConnRecord" + }, + "their_did" : { + "description" : "Remote DID", + "example" : "WgWxqztrNooG92RXvxSTWv", + "pattern" : "^(did:sov:)?[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}$", + "type" : "string" + }, + "their_verkey" : { + "description" : "Remote verification key", + "example" : "H3C2AVvLMv6gmMNam3uVAjZpfkcJCwDwnZn6z3wXmqPV", + "pattern" : "^[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{43,44}$", + "type" : "string" } }, - "predicate" : { - "type" : "string", - "description" : "Preference", - "enum" : [ "required", "preferred" ] - }, - "purpose" : { - "type" : "string", - "description" : "Purpose" - } - } - }, - "DIFHolder" : { - "type" : "object", - "properties" : { - "directive" : { - "type" : "string", - "description" : "Preference", - "enum" : [ "required", "preferred" ] + "required" : [ "my_did", "my_endpoint", "my_verkey", "record", "their_did", "their_verkey" ], + "type" : "object" + }, + "Constraints" : { + "properties" : { + "fields" : { + "items" : { + "$ref" : "#/components/schemas/DIFField" + }, + "type" : "array" + }, + "is_holder" : { + "items" : { + "$ref" : "#/components/schemas/DIFHolder" + }, + "type" : "array" + }, + "limit_disclosure" : { + "description" : "LimitDisclosure", + "type" : "string" + }, + "status_active" : { + "enum" : [ "required", "allowed", "disallowed" ], + "type" : "string" + }, + "status_revoked" : { + "enum" : [ "required", "allowed", "disallowed" ], + "type" : "string" + }, + "status_suspended" : { + "enum" : [ "required", "allowed", "disallowed" ], + "type" : "string" + }, + "subject_is_issuer" : { + "description" : "SubjectIsIssuer", + "enum" : [ "required", "preferred" ], + "type" : "string" + } }, - "field_id" : { - "type" : "array", - "items" : { - "type" : "string", + "type" : "object" + }, + "CreateInvitationRequest" : { + "properties" : { + "mediation_id" : { + "description" : "Identifier for active mediation record to be used", "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", - "description" : "FieldID", - "pattern" : "[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-4[a-fA-F0-9]{3}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}" + "pattern" : "[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-4[a-fA-F0-9]{3}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}", + "type" : "string" + }, + "metadata" : { + "description" : "Optional metadata to attach to the connection created with the invitation", + "properties" : { }, + "type" : "object" + }, + "my_label" : { + "description" : "Optional label for connection invitation", + "example" : "Bob", + "type" : "string" + }, + "recipient_keys" : { + "description" : "List of recipient keys", + "items" : { + "description" : "Recipient public key", + "example" : "H3C2AVvLMv6gmMNam3uVAjZpfkcJCwDwnZn6z3wXmqPV", + "pattern" : "^[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{43,44}$", + "type" : "string" + }, + "type" : "array" + }, + "routing_keys" : { + "description" : "List of routing keys", + "items" : { + "description" : "Routing key", + "example" : "H3C2AVvLMv6gmMNam3uVAjZpfkcJCwDwnZn6z3wXmqPV", + "pattern" : "^[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{43,44}$", + "type" : "string" + }, + "type" : "array" + }, + "service_endpoint" : { + "description" : "Connection endpoint", + "example" : "http://192.168.56.102:8020", + "type" : "string" } - } - } - }, - "DIFOptions" : { - "type" : "object", - "properties" : { - "challenge" : { - "type" : "string", - "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", - "description" : "Challenge protect against replay attack", - "pattern" : "[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-4[a-fA-F0-9]{3}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}" }, - "domain" : { - "type" : "string", - "example" : "4jt78h47fh47", - "description" : "Domain protect against replay attack" - } - } - }, - "DIFPresSpec" : { - "type" : "object", - "properties" : { - "issuer_id" : { - "type" : "string", - "description" : "Issuer identifier to sign the presentation, if different from current public DID" - }, - "presentation_definition" : { - "$ref" : "#/definitions/PresentationDefinition" - }, - "record_ids" : { - "type" : "object", - "example" : { - "" : [ "", "" ], - "" : [ "" ] - }, - "description" : "Mapping of input_descriptor id to list of stored W3C credential record_id", - "properties" : { } - }, - "reveal_doc" : { - "type" : "object", - "example" : { - "@context": [ - "https://www.w3.org/2018/credentials/v1", - "https://w3id.org/security/bbs/v1" - ], - "type": ["VerifiableCredential", "LabReport"], - "@explicit": true, - "@requireAll": true, - "issuanceDate": {}, - "issuer": {}, - "credentialSubject": { - "Observation": [ - {"effectiveDateTime": {}, "@explicit": true, "@requireAll": true} - ], - "@explicit": true, - "@requireAll": true - } + "type" : "object" + }, + "CreateWalletRequest" : { + "properties" : { + "image_url" : { + "description" : "Image url for this wallet. This image url is publicized (self-attested) to other agents as part of forming a connection.", + "example" : "https://aries.ca/images/sample.png", + "type" : "string" }, - "description" : "reveal doc [JSON-LD frame] dict used to derive the credential when selective disclosure is required", - "properties" : { } - } - } - }, - "DIFProofProposal" : { - "type" : "object", - "properties" : { - "input_descriptors" : { - "type" : "array", - "items" : { - "$ref" : "#/definitions/InputDescriptors" + "key_management_mode" : { + "description" : "Key management method to use for this wallet.", + "enum" : [ "managed" ], + "example" : "managed", + "type" : "string" + }, + "label" : { + "description" : "Label for this wallet. This label is publicized (self-attested) to other agents as part of forming a connection.", + "example" : "Alice", + "type" : "string" + }, + "wallet_dispatch_type" : { + "description" : "Webhook target dispatch type for this wallet. default - Dispatch only to webhooks associated with this wallet. base - Dispatch only to webhooks associated with the base wallet. both - Dispatch to both webhook targets.", + "enum" : [ "default", "both", "base" ], + "example" : "default", + "type" : "string" + }, + "wallet_key" : { + "description" : "Master key used for key derivation.", + "example" : "MySecretKey123", + "type" : "string" + }, + "wallet_key_derivation" : { + "description" : "Key derivation", + "enum" : [ "ARGON2I_MOD", "ARGON2I_INT", "RAW" ], + "example" : "RAW", + "type" : "string" + }, + "wallet_name" : { + "description" : "Wallet name", + "example" : "MyNewWallet", + "type" : "string" + }, + "wallet_type" : { + "description" : "Type of the wallet to create", + "enum" : [ "askar", "in_memory", "indy" ], + "example" : "indy", + "type" : "string" + }, + "wallet_webhook_urls" : { + "description" : "List of Webhook URLs associated with this subwallet", + "items" : { + "description" : "Optional webhook URL to receive webhook messages", + "example" : "http://localhost:8022/webhooks", + "type" : "string" + }, + "type" : "array" } - } - } - }, - "DIFProofRequest" : { - "type" : "object", - "required" : [ "presentation_definition" ], - "properties" : { - "options" : { - "$ref" : "#/definitions/DIFOptions" }, - "presentation_definition" : { - "$ref" : "#/definitions/PresentationDefinition" - } - } - }, - "Date" : { - "type" : "object", - "required" : [ "expires_time" ], - "properties" : { - "expires_time" : { - "type" : "string", - "format" : "date-time", - "example" : "2021-03-29T05:22:19Z", - "description" : "Expiry Date" - } - } - }, - "Doc" : { - "type" : "object", - "required" : [ "credential", "options" ], - "properties" : { - "credential" : { - "type" : "object", - "description" : "Credential to sign", - "properties" : { } - }, - "options" : { - "$ref" : "#/definitions/Doc_options" - } - } - }, - "EndorserInfo" : { - "type" : "object", - "required" : [ "endorser_did" ], - "properties" : { - "endorser_did" : { - "type" : "string", - "description" : "Endorser DID" - }, - "endorser_name" : { - "type" : "string", - "description" : "Endorser Name" - } - } - }, - "EndpointsResult" : { - "type" : "object", - "properties" : { - "my_endpoint" : { - "type" : "string", - "example" : "https://myhost:8021", - "description" : "My endpoint", - "pattern" : "^[A-Za-z0-9\\.\\-\\+]+://([A-Za-z0-9][.A-Za-z0-9-_]+[A-Za-z0-9])+(:[1-9][0-9]*)?(/[^?&#]+)?$" - }, - "their_endpoint" : { - "type" : "string", - "example" : "https://myhost:8021", - "description" : "Their endpoint", - "pattern" : "^[A-Za-z0-9\\.\\-\\+]+://([A-Za-z0-9][.A-Za-z0-9-_]+[A-Za-z0-9])+(:[1-9][0-9]*)?(/[^?&#]+)?$" - } - } - }, - "Filter" : { - "type" : "object", - "properties" : { - "const" : { - "description" : "Const" - }, - "enum" : { - "type" : "array", - "items" : { - "description" : "Enum" + "type" : "object" + }, + "CreateWalletResponse" : { + "properties" : { + "created_at" : { + "description" : "Time of record creation", + "example" : "2021-12-31T23:59:59Z", + "pattern" : "^\\d{4}-\\d\\d-\\d\\d[T ]\\d\\d:\\d\\d(?:\\:(?:\\d\\d(?:\\.\\d{1,6})?))?(?:[+-]\\d\\d:?\\d\\d|Z|)$", + "type" : "string" + }, + "key_management_mode" : { + "description" : "Mode regarding management of wallet key", + "enum" : [ "managed", "unmanaged" ], + "type" : "string" + }, + "settings" : { + "description" : "Settings for this wallet.", + "properties" : { }, + "type" : "object" + }, + "state" : { + "description" : "Current record state", + "example" : "active", + "type" : "string" + }, + "token" : { + "description" : "Authorization token to authenticate wallet requests", + "example" : "eyJhbGciOiJFZERTQSJ9.eyJhIjogIjAifQ.dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk", + "type" : "string" + }, + "updated_at" : { + "description" : "Time of last record update", + "example" : "2021-12-31T23:59:59Z", + "pattern" : "^\\d{4}-\\d\\d-\\d\\d[T ]\\d\\d:\\d\\d(?:\\:(?:\\d\\d(?:\\.\\d{1,6})?))?(?:[+-]\\d\\d:?\\d\\d|Z|)$", + "type" : "string" + }, + "wallet_id" : { + "description" : "Wallet record ID", + "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "type" : "string" } }, - "exclusiveMaximum" : { - "description" : "ExclusiveMaximum" - }, - "exclusiveMinimum" : { - "description" : "ExclusiveMinimum" - }, - "format" : { - "type" : "string", - "description" : "Format" - }, - "maxLength" : { - "type" : "integer", - "format" : "int32", - "example" : 1234, - "description" : "Max Length" - }, - "maximum" : { - "description" : "Maximum" - }, - "minLength" : { - "type" : "integer", - "format" : "int32", - "example" : 1234, - "description" : "Min Length" - }, - "minimum" : { - "description" : "Minimum" - }, - "not" : { - "type" : "boolean", - "example" : false, - "description" : "Not" - }, - "pattern" : { - "type" : "string", - "description" : "Pattern" - }, - "type" : { - "type" : "string", - "description" : "Type" - } - } - }, - "Generated" : { - "type" : "object", - "properties" : { - "master_secret" : { - "type" : "string", - "example" : "0", - "pattern" : "^[0-9]*$" - }, - "number" : { - "type" : "string", - "example" : "0", - "pattern" : "^[0-9]*$" - }, - "remainder" : { - "type" : "string", - "example" : "0", - "pattern" : "^[0-9]*$" - } - } - }, - "GetDIDEndpointResponse" : { - "type" : "object", - "properties" : { - "endpoint" : { - "type" : "string", - "example" : "https://myhost:8021", - "description" : "Full verification key", - "pattern" : "^[A-Za-z0-9\\.\\-\\+]+://([A-Za-z0-9][.A-Za-z0-9-_]+[A-Za-z0-9])+(:[1-9][0-9]*)?(/[^?&#]+)?$", - "x-nullable" : true - } - } - }, - "GetDIDVerkeyResponse" : { - "type" : "object", - "properties" : { - "verkey" : { - "type" : "string", - "example" : "H3C2AVvLMv6gmMNam3uVAjZpfkcJCwDwnZn6z3wXmqPV", - "description" : "Full verification key", - "pattern" : "^[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{43,44}$", - "x-nullable" : true - } - } - }, - "GetNymRoleResponse" : { - "type" : "object", - "properties" : { - "role" : { - "type" : "string", - "example" : "ENDORSER", - "description" : "Ledger role", - "enum" : [ "STEWARD", "TRUSTEE", "ENDORSER", "NETWORK_MONITOR", "USER", "ROLE_REMOVE" ] - } - } - }, - "HolderModuleResponse" : { - "type" : "object" - }, - "IndyAttrValue" : { - "type" : "object", - "required" : [ "encoded", "raw" ], - "properties" : { - "encoded" : { - "type" : "string", - "example" : "0", - "description" : "Attribute encoded value", - "pattern" : "^[0-9]*$" - }, - "raw" : { - "type" : "string", - "description" : "Attribute raw value" - } - } - }, - "IndyCredAbstract" : { - "type" : "object", - "required" : [ "cred_def_id", "key_correctness_proof", "nonce", "schema_id" ], - "properties" : { - "cred_def_id" : { - "type" : "string", - "example" : "WgWxqztrNooG92RXvxSTWv:3:CL:20:tag", - "description" : "Credential definition identifier", - "pattern" : "^([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}):3:CL:(([1-9][0-9]*)|([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}:2:.+:[0-9.]+)):(.+)?$" - }, - "key_correctness_proof" : { - "$ref" : "#/definitions/IndyCredAbstract_key_correctness_proof" - }, - "nonce" : { - "type" : "string", - "example" : "0", - "description" : "Nonce in credential abstract", - "pattern" : "^[0-9]*$" - }, - "schema_id" : { - "type" : "string", - "example" : "WgWxqztrNooG92RXvxSTWv:2:schema_name:1.0", - "description" : "Schema identifier", - "pattern" : "^[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}:2:.+:[0-9.]+$" - } - } - }, - "IndyCredInfo" : { - "type" : "object", - "properties" : { - "attrs" : { - "type" : "object", - "description" : "Attribute names and value", - "additionalProperties" : { - "type" : "string", - "example" : "alice" + "required" : [ "key_management_mode", "wallet_id" ], + "type" : "object" + }, + "CreateWalletTokenRequest" : { + "properties" : { + "wallet_key" : { + "description" : "Master key used for key derivation. Only required for unamanged wallets.", + "example" : "MySecretKey123", + "type" : "string" } }, - "cred_def_id" : { - "type" : "string", - "example" : "WgWxqztrNooG92RXvxSTWv:3:CL:20:tag", - "description" : "Credential definition identifier", - "pattern" : "^([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}):3:CL:(([1-9][0-9]*)|([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}:2:.+:[0-9.]+)):(.+)?$" - }, - "cred_rev_id" : { - "type" : "string", - "example" : "12345", - "description" : "Credential revocation identifier", - "pattern" : "^[1-9][0-9]*$", - "x-nullable" : true - }, - "referent" : { - "type" : "string", - "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", - "description" : "Wallet referent" - }, - "rev_reg_id" : { - "type" : "string", - "example" : "WgWxqztrNooG92RXvxSTWv:4:WgWxqztrNooG92RXvxSTWv:3:CL:20:tag:CL_ACCUM:0", - "description" : "Revocation registry identifier", - "pattern" : "^([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}):4:([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}):3:CL:(([1-9][0-9]*)|([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}:2:.+:[0-9.]+))(:.+)?:CL_ACCUM:(.+$)", - "x-nullable" : true - }, - "schema_id" : { - "type" : "string", - "example" : "WgWxqztrNooG92RXvxSTWv:2:schema_name:1.0", - "description" : "Schema identifier", - "pattern" : "^[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}:2:.+:[0-9.]+$" - } - } - }, - "IndyCredPrecis" : { - "type" : "object", - "properties" : { - "cred_info" : { - "$ref" : "#/definitions/IndyCredPrecis_cred_info" - }, - "interval" : { - "$ref" : "#/definitions/IndyCredPrecis_interval" - }, - "presentation_referents" : { - "type" : "array", - "items" : { - "type" : "string", - "example" : "1_age_uuid", - "description" : "presentation referent" + "type" : "object" + }, + "CreateWalletTokenResponse" : { + "properties" : { + "token" : { + "description" : "Authorization token to authenticate wallet requests", + "example" : "eyJhbGciOiJFZERTQSJ9.eyJhIjogIjAifQ.dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk", + "type" : "string" } - } - } - }, - "IndyCredRequest" : { - "type" : "object", - "required" : [ "blinded_ms", "blinded_ms_correctness_proof", "cred_def_id", "nonce" ], - "properties" : { - "blinded_ms" : { - "type" : "object", - "description" : "Blinded master secret", - "properties" : { } - }, - "blinded_ms_correctness_proof" : { - "type" : "object", - "description" : "Blinded master secret correctness proof", - "properties" : { } - }, - "cred_def_id" : { - "type" : "string", - "example" : "WgWxqztrNooG92RXvxSTWv:3:CL:20:tag", - "description" : "Credential definition identifier", - "pattern" : "^([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}):3:CL:(([1-9][0-9]*)|([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}:2:.+:[0-9.]+)):(.+)?$" - }, - "nonce" : { - "type" : "string", - "example" : "0", - "description" : "Nonce in credential request", - "pattern" : "^[0-9]*$" - }, - "prover_did" : { - "type" : "string", - "example" : "WgWxqztrNooG92RXvxSTWv", - "description" : "Prover DID", - "pattern" : "^(did:sov:)?[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}$" - } - } - }, - "IndyCredential" : { - "type" : "object", - "required" : [ "cred_def_id", "schema_id", "signature", "signature_correctness_proof", "values" ], - "properties" : { - "cred_def_id" : { - "type" : "string", - "example" : "WgWxqztrNooG92RXvxSTWv:3:CL:20:tag", - "description" : "Credential definition identifier", - "pattern" : "^([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}):3:CL:(([1-9][0-9]*)|([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}:2:.+:[0-9.]+)):(.+)?$" - }, - "rev_reg" : { - "type" : "object", - "description" : "Revocation registry state", - "properties" : { }, - "x-nullable" : true - }, - "rev_reg_id" : { - "type" : "string", - "example" : "WgWxqztrNooG92RXvxSTWv:4:WgWxqztrNooG92RXvxSTWv:3:CL:20:tag:CL_ACCUM:0", - "description" : "Revocation registry identifier", - "pattern" : "^([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}):4:([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}):3:CL:(([1-9][0-9]*)|([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}:2:.+:[0-9.]+))(:.+)?:CL_ACCUM:(.+$)", - "x-nullable" : true }, - "schema_id" : { - "type" : "string", - "example" : "WgWxqztrNooG92RXvxSTWv:2:schema_name:1.0", - "description" : "Schema identifier", - "pattern" : "^[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}:2:.+:[0-9.]+$" - }, - "signature" : { - "type" : "object", - "description" : "Credential signature", - "properties" : { } - }, - "signature_correctness_proof" : { - "type" : "object", - "description" : "Credential signature correctness proof", - "properties" : { } - }, - "values" : { - "type" : "object", - "description" : "Credential attributes", - "additionalProperties" : { - "type" : "object", - "description" : "Attribute value", - "allOf" : [ { - "$ref" : "#/definitions/IndyAttrValue" - } ] + "type" : "object" + }, + "CredAttrSpec" : { + "properties" : { + "mime-type" : { + "description" : "MIME type: omit for (null) default", + "example" : "image/jpeg", + "nullable" : true, + "type" : "string" + }, + "name" : { + "description" : "Attribute name", + "example" : "favourite_drink", + "type" : "string" + }, + "value" : { + "description" : "Attribute value: base64-encode if MIME type is present", + "example" : "martini", + "type" : "string" } }, - "witness" : { - "type" : "object", - "description" : "Witness for revocation proof", - "properties" : { }, - "x-nullable" : true - } - } - }, - "IndyEQProof" : { - "type" : "object", - "properties" : { - "a_prime" : { - "type" : "string", - "example" : "0", - "pattern" : "^[0-9]*$" - }, - "e" : { - "type" : "string", - "example" : "0", - "pattern" : "^[0-9]*$" - }, - "m" : { - "type" : "object", - "additionalProperties" : { - "type" : "string", - "example" : "0", - "pattern" : "^[0-9]*$" + "required" : [ "name", "value" ], + "type" : "object" + }, + "CredDefValue" : { + "properties" : { + "primary" : { + "$ref" : "#/components/schemas/CredDefValue_primary" + }, + "revocation" : { + "$ref" : "#/components/schemas/CredDefValue_revocation" } }, - "m2" : { - "type" : "string", - "example" : "0", - "pattern" : "^[0-9]*$" - }, - "revealed_attrs" : { - "type" : "object", - "additionalProperties" : { - "type" : "string", + "type" : "object" + }, + "CredDefValuePrimary" : { + "properties" : { + "n" : { + "example" : "0", + "pattern" : "^[0-9]*$", + "type" : "string" + }, + "r" : { + "$ref" : "#/components/schemas/Generated" + }, + "rctxt" : { + "example" : "0", + "pattern" : "^[0-9]*$", + "type" : "string" + }, + "s" : { "example" : "0", - "pattern" : "^[0-9]*$" + "pattern" : "^[0-9]*$", + "type" : "string" + }, + "z" : { + "example" : "0", + "pattern" : "^[0-9]*$", + "type" : "string" } }, - "v" : { - "type" : "string", - "example" : "0", - "pattern" : "^[0-9]*$" - } - } - }, - "IndyGEProof" : { - "type" : "object", - "properties" : { - "alpha" : { - "type" : "string", - "example" : "0", - "pattern" : "^[0-9]*$" - }, - "mj" : { - "type" : "string", - "example" : "0", - "pattern" : "^[0-9]*$" - }, - "predicate" : { - "$ref" : "#/definitions/IndyGEProofPred" + "type" : "object" + }, + "CredDefValueRevocation" : { + "properties" : { + "g" : { + "example" : "1 1F14F&ECB578F 2 095E45DDF417D", + "type" : "string" + }, + "g_dash" : { + "example" : "1 1D64716fCDC00C 1 0C781960FA66E3D3 2 095E45DDF417D", + "type" : "string" + }, + "h" : { + "example" : "1 16675DAE54BFAE8 2 095E45DD417D", + "type" : "string" + }, + "h0" : { + "example" : "1 21E5EF9476EAF18 2 095E45DDF417D", + "type" : "string" + }, + "h1" : { + "example" : "1 236D1D99236090 2 095E45DDF417D", + "type" : "string" + }, + "h2" : { + "example" : "1 1C3AE8D1F1E277 2 095E45DDF417D", + "type" : "string" + }, + "h_cap" : { + "example" : "1 1B2A32CF3167 1 2490FEBF6EE55 1 0000000000000000", + "type" : "string" + }, + "htilde" : { + "example" : "1 1D8549E8C0F8 2 095E45DDF417D", + "type" : "string" + }, + "pk" : { + "example" : "1 142CD5E5A7DC 1 153885BD903312 2 095E45DDF417D", + "type" : "string" + }, + "u" : { + "example" : "1 0C430AAB2B4710 1 1CB3A0932EE7E 1 0000000000000000", + "type" : "string" + }, + "y" : { + "example" : "1 153558BD903312 2 095E45DDF417D 1 0000000000000000", + "type" : "string" + } }, - "r" : { - "type" : "object", - "additionalProperties" : { - "type" : "string", - "example" : "0", - "pattern" : "^[0-9]*$" + "type" : "object" + }, + "CredInfoList" : { + "properties" : { + "results" : { + "items" : { + "$ref" : "#/components/schemas/IndyCredInfo" + }, + "type" : "array" } }, - "t" : { - "type" : "object", - "additionalProperties" : { - "type" : "string", - "example" : "0", - "pattern" : "^[0-9]*$" + "type" : "object" + }, + "CredRevIndyRecordsResult" : { + "properties" : { + "rev_reg_delta" : { + "description" : "Indy revocation registry delta", + "properties" : { }, + "type" : "object" } }, - "u" : { - "type" : "object", - "additionalProperties" : { - "type" : "string", - "example" : "0", - "pattern" : "^[0-9]*$" + "type" : "object" + }, + "CredRevRecordDetailsResult" : { + "properties" : { + "results" : { + "items" : { + "$ref" : "#/components/schemas/IssuerCredRevRecord" + }, + "type" : "array" } - } - } - }, - "IndyGEProofPred" : { - "type" : "object", - "properties" : { - "attr_name" : { - "type" : "string", - "description" : "Attribute name, indy-canonicalized" }, - "p_type" : { - "type" : "string", - "description" : "Predicate type", - "enum" : [ "LT", "LE", "GE", "GT" ] + "type" : "object" + }, + "CredRevRecordResult" : { + "properties" : { + "result" : { + "$ref" : "#/components/schemas/IssuerCredRevRecord" + } }, - "value" : { - "type" : "integer", - "format" : "int32", - "description" : "Predicate threshold value" - } - } - }, - "IndyKeyCorrectnessProof" : { - "type" : "object", - "required" : [ "c", "xr_cap", "xz_cap" ], - "properties" : { - "c" : { - "type" : "string", - "example" : "0", - "description" : "c in key correctness proof", - "pattern" : "^[0-9]*$" - }, - "xr_cap" : { - "type" : "array", - "description" : "xr_cap in key correctness proof", - "items" : { - "type" : "array", - "description" : "xr_cap components in key correctness proof", - "items" : { - "type" : "string", - "description" : "xr_cap component values in key correctness proof" - } + "type" : "object" + }, + "CredRevokedResult" : { + "properties" : { + "revoked" : { + "description" : "Whether credential is revoked on the ledger", + "type" : "boolean" } }, - "xz_cap" : { - "type" : "string", - "example" : "0", - "description" : "xz_cap in key correctness proof", - "pattern" : "^[0-9]*$" - } - } - }, - "IndyNonRevocProof" : { - "type" : "object", - "properties" : { - "c_list" : { - "type" : "object", - "additionalProperties" : { + "type" : "object" + }, + "Credential" : { + "properties" : { + "@context" : { + "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" : "object" + }, + "type" : "array" + }, + "credentialSubject" : { + "example" : "", + "type" : "object" + }, + "expirationDate" : { + "description" : "The expiration date", + "example" : "2010-01-01T19:23:24Z", + "pattern" : "^([0-9]{4})-([0-9]{2})-([0-9]{2})([Tt ]([0-9]{2}):([0-9]{2}):([0-9]{2})(\\.[0-9]+)?)?(([Zz]|([+-])([0-9]{2}):([0-9]{2})))?$", + "type" : "string" + }, + "id" : { + "example" : "http://example.edu/credentials/1872", + "pattern" : "\\w+:(\\/?\\/?)[^\\s]+", + "type" : "string" + }, + "issuanceDate" : { + "description" : "The issuance date", + "example" : "2010-01-01T19:23:24Z", + "pattern" : "^([0-9]{4})-([0-9]{2})-([0-9]{2})([Tt ]([0-9]{2}):([0-9]{2}):([0-9]{2})(\\.[0-9]+)?)?(([Zz]|([+-])([0-9]{2}):([0-9]{2})))?$", "type" : "string" + }, + "issuer" : { + "description" : "The JSON-LD Verifiable Credential Issuer. Either string of object with id field.", + "example" : "did:key:z6MkpTHR8VNsBxYAAWHut2Geadd9jSwuBV8xRoAnwWsdvktH", + "type" : "object" + }, + "proof" : { + "$ref" : "#/components/schemas/Credential_proof" + }, + "type" : { + "description" : "The JSON-LD type of the credential", + "example" : [ "VerifiableCredential", "AlumniCredential" ], + "items" : { + "type" : "string" + }, + "type" : "array" } }, - "x_list" : { - "type" : "object", - "additionalProperties" : { + "required" : [ "@context", "credentialSubject", "issuanceDate", "issuer", "type" ], + "type" : "object" + }, + "CredentialDefinition" : { + "properties" : { + "id" : { + "description" : "Credential definition identifier", + "example" : "WgWxqztrNooG92RXvxSTWv:3:CL:20:tag", + "pattern" : "^([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}):3:CL:(([1-9][0-9]*)|([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}:2:.+:[0-9.]+)):(.+)?$", + "type" : "string" + }, + "schemaId" : { + "description" : "Schema identifier within credential definition identifier", + "example" : "20", + "type" : "string" + }, + "tag" : { + "description" : "Tag within credential definition identifier", + "example" : "tag", + "type" : "string" + }, + "type" : { + "description" : "Signature type: CL for Camenisch-Lysyanskaya", + "example" : "CL", + "type" : "object" + }, + "value" : { + "$ref" : "#/components/schemas/CredentialDefinition_value" + }, + "ver" : { + "description" : "Node protocol version", + "example" : "1.0", + "pattern" : "^[0-9.]+$", "type" : "string" } - } - } - }, - "IndyNonRevocationInterval" : { - "type" : "object", - "properties" : { - "from" : { - "type" : "integer", - "format" : "int32", - "example" : 1640995199, - "description" : "Earliest time of interest in non-revocation interval", - "minimum" : 0, - "maximum" : 18446744073709551615 - }, - "to" : { - "type" : "integer", - "format" : "int32", - "example" : 1640995199, - "description" : "Latest time of interest in non-revocation interval", - "minimum" : 0, - "maximum" : 18446744073709551615 - } - } - }, - "IndyPresAttrSpec" : { - "type" : "object", - "required" : [ "name" ], - "properties" : { - "cred_def_id" : { - "type" : "string", - "example" : "WgWxqztrNooG92RXvxSTWv:3:CL:20:tag", - "pattern" : "^([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}):3:CL:(([1-9][0-9]*)|([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}:2:.+:[0-9.]+)):(.+)?$" - }, - "mime-type" : { - "type" : "string", - "example" : "image/jpeg", - "description" : "MIME type (default null)" - }, - "name" : { - "type" : "string", - "example" : "favourite_drink", - "description" : "Attribute name" - }, - "referent" : { - "type" : "string", - "example" : "0", - "description" : "Credential referent" - }, - "value" : { - "type" : "string", - "example" : "martini", - "description" : "Attribute value" - } - } - }, - "IndyPresPredSpec" : { - "type" : "object", - "required" : [ "name", "predicate", "threshold" ], - "properties" : { - "cred_def_id" : { - "type" : "string", - "example" : "WgWxqztrNooG92RXvxSTWv:3:CL:20:tag", - "description" : "Credential definition identifier", - "pattern" : "^([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}):3:CL:(([1-9][0-9]*)|([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}:2:.+:[0-9.]+)):(.+)?$" - }, - "name" : { - "type" : "string", - "example" : "high_score", - "description" : "Attribute name" - }, - "predicate" : { - "type" : "string", - "example" : ">=", - "description" : "Predicate type ('<', '<=', '>=', or '>')", - "enum" : [ "<", "<=", ">=", ">" ] - }, - "threshold" : { - "type" : "integer", - "format" : "int32", - "description" : "Threshold value" - } - } - }, - "IndyPresPreview" : { - "type" : "object", - "required" : [ "attributes", "predicates" ], - "properties" : { - "@type" : { - "type" : "string", - "example" : "did:sov:BzCbsNYhMrjHiqZDTUASHg;spec/present-proof/1.0/presentation-preview", - "description" : "Message type identifier" }, - "attributes" : { - "type" : "array", - "items" : { - "$ref" : "#/definitions/IndyPresAttrSpec" + "type" : "object" + }, + "CredentialDefinitionGetResult" : { + "properties" : { + "credential_definition" : { + "$ref" : "#/components/schemas/CredentialDefinition" } }, - "predicates" : { - "type" : "array", - "items" : { - "$ref" : "#/definitions/IndyPresPredSpec" - } - } - } - }, - "IndyPresSpec" : { - "type" : "object", - "required" : [ "requested_attributes", "requested_predicates", "self_attested_attributes" ], - "properties" : { - "requested_attributes" : { - "type" : "object", - "description" : "Nested object mapping proof request attribute referents to requested-attribute specifiers", - "additionalProperties" : { - "$ref" : "#/definitions/IndyRequestedCredsRequestedAttr" + "type" : "object" + }, + "CredentialDefinitionSendRequest" : { + "properties" : { + "revocation_registry_size" : { + "description" : "Revocation registry size", + "example" : 1000, + "format" : "int32", + "maximum" : 32768, + "minimum" : 4, + "type" : "integer" + }, + "schema_id" : { + "description" : "Schema identifier", + "example" : "WgWxqztrNooG92RXvxSTWv:2:schema_name:1.0", + "pattern" : "^[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}:2:.+:[0-9.]+$", + "type" : "string" + }, + "support_revocation" : { + "description" : "Revocation supported flag", + "type" : "boolean" + }, + "tag" : { + "description" : "Credential definition identifier tag", + "example" : "default", + "type" : "string" } }, - "requested_predicates" : { - "type" : "object", - "description" : "Nested object mapping proof request predicate referents to requested-predicate specifiers", - "additionalProperties" : { - "$ref" : "#/definitions/IndyRequestedCredsRequestedPred" + "type" : "object" + }, + "CredentialDefinitionSendResult" : { + "properties" : { + "credential_definition_id" : { + "description" : "Credential definition identifier", + "example" : "WgWxqztrNooG92RXvxSTWv:3:CL:20:tag", + "pattern" : "^([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}):3:CL:(([1-9][0-9]*)|([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}:2:.+:[0-9.]+)):(.+)?$", + "type" : "string" } }, - "self_attested_attributes" : { - "type" : "object", - "description" : "Self-attested attributes to build into proof", - "additionalProperties" : { - "type" : "string", - "example" : "self_attested_value", - "description" : "Self-attested attribute values to use in requested-credentials structure for proof construction" + "type" : "object" + }, + "CredentialDefinitionsCreatedResult" : { + "properties" : { + "credential_definition_ids" : { + "items" : { + "description" : "Credential definition identifiers", + "example" : "WgWxqztrNooG92RXvxSTWv:3:CL:20:tag", + "pattern" : "^([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}):3:CL:(([1-9][0-9]*)|([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}:2:.+:[0-9.]+)):(.+)?$", + "type" : "string" + }, + "type" : "array" } }, - "trace" : { - "type" : "boolean", - "example" : false, - "description" : "Whether to trace event (default false)" - } - } - }, - "IndyPrimaryProof" : { - "type" : "object", - "properties" : { - "eq_proof" : { - "$ref" : "#/definitions/IndyPrimaryProof_eq_proof" - }, - "ge_proofs" : { - "type" : "array", - "description" : "Indy GE proofs", - "items" : { - "$ref" : "#/definitions/IndyGEProof" + "type" : "object" + }, + "CredentialOffer" : { + "properties" : { + "@id" : { + "description" : "Message identifier", + "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "type" : "string" }, - "x-nullable" : true - } - } - }, - "IndyProof" : { - "type" : "object", - "properties" : { - "identifiers" : { - "type" : "array", - "description" : "Indy proof.identifiers content", - "items" : { - "$ref" : "#/definitions/IndyProofIdentifier" - } - }, - "proof" : { - "$ref" : "#/definitions/IndyProof_proof" - }, - "requested_proof" : { - "$ref" : "#/definitions/IndyProof_requested_proof" - } - } - }, - "IndyProofIdentifier" : { - "type" : "object", - "properties" : { - "cred_def_id" : { - "type" : "string", - "example" : "WgWxqztrNooG92RXvxSTWv:3:CL:20:tag", - "description" : "Credential definition identifier", - "pattern" : "^([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}):3:CL:(([1-9][0-9]*)|([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}:2:.+:[0-9.]+)):(.+)?$" - }, - "rev_reg_id" : { - "type" : "string", - "example" : "WgWxqztrNooG92RXvxSTWv:4:WgWxqztrNooG92RXvxSTWv:3:CL:20:tag:CL_ACCUM:0", - "description" : "Revocation registry identifier", - "pattern" : "^([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}):4:([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}):3:CL:(([1-9][0-9]*)|([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}:2:.+:[0-9.]+))(:.+)?:CL_ACCUM:(.+$)", - "x-nullable" : true - }, - "schema_id" : { - "type" : "string", - "example" : "WgWxqztrNooG92RXvxSTWv:2:schema_name:1.0", - "description" : "Schema identifier", - "pattern" : "^[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}:2:.+:[0-9.]+$" - }, - "timestamp" : { - "type" : "integer", - "format" : "int32", - "example" : 1640995199, - "description" : "Timestamp epoch", - "minimum" : 0, - "maximum" : 18446744073709551615, - "x-nullable" : true - } - } - }, - "IndyProofProof" : { - "type" : "object", - "properties" : { - "aggregated_proof" : { - "$ref" : "#/definitions/IndyProofProof_aggregated_proof" - }, - "proofs" : { - "type" : "array", - "description" : "Indy proof proofs", - "items" : { - "$ref" : "#/definitions/IndyProofProofProofsProof" + "@type" : { + "description" : "Message type", + "example" : "https://didcomm.org/my-family/1.0/my-message-type", + "readOnly" : true, + "type" : "string" + }, + "comment" : { + "description" : "Human-readable comment", + "nullable" : true, + "type" : "string" + }, + "credential_preview" : { + "$ref" : "#/components/schemas/CredentialPreview" + }, + "offers~attach" : { + "items" : { + "$ref" : "#/components/schemas/AttachDecorator" + }, + "type" : "array" } - } - } - }, - "IndyProofProofAggregatedProof" : { - "type" : "object", - "properties" : { - "c_hash" : { - "type" : "string", - "description" : "c_hash value" }, - "c_list" : { - "type" : "array", - "description" : "c_list value", - "items" : { - "type" : "array", + "required" : [ "offers~attach" ], + "type" : "object" + }, + "CredentialPreview" : { + "properties" : { + "@type" : { + "description" : "Message type identifier", + "example" : "issue-credential/1.0/credential-preview", + "type" : "string" + }, + "attributes" : { "items" : { - "type" : "integer", - "format" : "int32" - } + "$ref" : "#/components/schemas/CredAttrSpec" + }, + "type" : "array" } - } - } - }, - "IndyProofProofProofsProof" : { - "type" : "object", - "properties" : { - "non_revoc_proof" : { - "$ref" : "#/definitions/IndyProofProofProofsProof_non_revoc_proof" - }, - "primary_proof" : { - "$ref" : "#/definitions/IndyProofProofProofsProof_primary_proof" - } - } - }, - "IndyProofReqAttrSpec" : { - "type" : "object", - "properties" : { - "name" : { - "type" : "string", - "example" : "favouriteDrink", - "description" : "Attribute name" }, - "names" : { - "type" : "array", - "description" : "Attribute name group", - "items" : { - "type" : "string", - "example" : "age" + "required" : [ "attributes" ], + "type" : "object" + }, + "CredentialProposal" : { + "properties" : { + "@id" : { + "description" : "Message identifier", + "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "type" : "string" + }, + "@type" : { + "description" : "Message type", + "example" : "https://didcomm.org/my-family/1.0/my-message-type", + "readOnly" : true, + "type" : "string" + }, + "comment" : { + "description" : "Human-readable comment", + "nullable" : true, + "type" : "string" + }, + "cred_def_id" : { + "example" : "WgWxqztrNooG92RXvxSTWv:3:CL:20:tag", + "pattern" : "^([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}):3:CL:(([1-9][0-9]*)|([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}:2:.+:[0-9.]+)):(.+)?$", + "type" : "string" + }, + "credential_proposal" : { + "$ref" : "#/components/schemas/CredentialPreview" + }, + "issuer_did" : { + "example" : "WgWxqztrNooG92RXvxSTWv", + "pattern" : "^(did:sov:)?[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}$", + "type" : "string" + }, + "schema_id" : { + "example" : "WgWxqztrNooG92RXvxSTWv:2:schema_name:1.0", + "pattern" : "^[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}:2:.+:[0-9.]+$", + "type" : "string" + }, + "schema_issuer_did" : { + "example" : "WgWxqztrNooG92RXvxSTWv", + "pattern" : "^(did:sov:)?[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}$", + "type" : "string" + }, + "schema_name" : { + "type" : "string" + }, + "schema_version" : { + "example" : "1.0", + "pattern" : "^[0-9.]+$", + "type" : "string" } }, - "non_revoked" : { - "$ref" : "#/definitions/IndyProofReqAttrSpec_non_revoked" - }, - "restrictions" : { - "type" : "array", - "description" : "If present, credential must satisfy one of given restrictions: specify schema_id, schema_issuer_did, schema_name, schema_version, issuer_did, cred_def_id, and/or attr::::value where represents a credential attribute name", - "items" : { - "type" : "object", - "additionalProperties" : { - "type" : "string", - "example" : "WgWxqztrNooG92RXvxSTWv:3:CL:20:tag" - } - } - } - } - }, - "IndyProofReqAttrSpecNonRevoked" : { - "type" : "object", - "properties" : { - "from" : { - "type" : "integer", - "format" : "int32", - "example" : 1640995199, - "description" : "Earliest time of interest in non-revocation interval", - "minimum" : 0, - "maximum" : 18446744073709551615 - }, - "to" : { - "type" : "integer", - "format" : "int32", - "example" : 1640995199, - "description" : "Latest time of interest in non-revocation interval", - "minimum" : 0, - "maximum" : 18446744073709551615 - } - } - }, - "IndyProofReqPredSpec" : { - "type" : "object", - "required" : [ "name", "p_type", "p_value" ], - "properties" : { - "name" : { - "type" : "string", - "example" : "index", - "description" : "Attribute name" - }, - "non_revoked" : { - "$ref" : "#/definitions/IndyProofReqAttrSpec_non_revoked" - }, - "p_type" : { - "type" : "string", - "example" : ">=", - "description" : "Predicate type ('<', '<=', '>=', or '>')", - "enum" : [ "<", "<=", ">=", ">" ] - }, - "p_value" : { - "type" : "integer", - "format" : "int32", - "description" : "Threshold value" - }, - "restrictions" : { - "type" : "array", - "description" : "If present, credential must satisfy one of given restrictions: specify schema_id, schema_issuer_did, schema_name, schema_version, issuer_did, cred_def_id, and/or attr::::value where represents a credential attribute name", - "items" : { - "type" : "object", - "additionalProperties" : { - "type" : "string", - "example" : "WgWxqztrNooG92RXvxSTWv:3:CL:20:tag" - } + "type" : "object" + }, + "CredentialStatusOptions" : { + "properties" : { + "type" : { + "description" : "Credential status method type to use for the credential. Should match status method registered in the Verifiable Credential Extension Registry", + "example" : "CredentialStatusList2017", + "type" : "string" } - } - } - }, - "IndyProofReqPredSpecNonRevoked" : { - "type" : "object", - "properties" : { - "from" : { - "type" : "integer", - "format" : "int32", - "example" : 1640995199, - "description" : "Earliest time of interest in non-revocation interval", - "minimum" : 0, - "maximum" : 18446744073709551615 - }, - "to" : { - "type" : "integer", - "format" : "int32", - "example" : 1640995199, - "description" : "Latest time of interest in non-revocation interval", - "minimum" : 0, - "maximum" : 18446744073709551615 - } - } - }, - "IndyProofRequest" : { - "type" : "object", - "properties" : { - "name" : { - "type" : "string", - "example" : "Proof request", - "description" : "Proof request name" - }, - "non_revoked" : { - "$ref" : "#/definitions/IndyProofReqAttrSpec_non_revoked" }, - "nonce" : { - "type" : "string", - "example" : "1", - "description" : "Nonce", - "pattern" : "^[1-9][0-9]*$" - }, - "requested_attributes" : { - "type" : "object", - "description" : "Requested attribute specifications of proof request", - "additionalProperties" : { - "$ref" : "#/definitions/IndyProofReqAttrSpec" + "required" : [ "type" ], + "type" : "object" + }, + "DID" : { + "properties" : { + "did" : { + "description" : "DID of interest", + "example" : "did:peer:WgWxqztrNooG92RXvxSTWv", + "pattern" : "^(did:sov:)?[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}$|^did:([a-zA-Z0-9_]+):([a-zA-Z0-9_.%-]+(:[a-zA-Z0-9_.%-]+)*)((;[a-zA-Z0-9_.:%-]+=[a-zA-Z0-9_.:%-]*)*)(\\/[^#?]*)?([?][^#]*)?(\\#.*)?$$", + "type" : "string" + }, + "key_type" : { + "description" : "Key type associated with the DID", + "enum" : [ "ed25519", "bls12381g2" ], + "example" : "ed25519", + "type" : "string" + }, + "method" : { + "description" : "Did method associated with the DID", + "example" : "sov", + "type" : "string" + }, + "posture" : { + "description" : "Whether DID is current public DID, posted to ledger but not current public DID, or local to the wallet", + "enum" : [ "public", "posted", "wallet_only" ], + "example" : "wallet_only", + "type" : "string" + }, + "verkey" : { + "description" : "Public verification key", + "example" : "H3C2AVvLMv6gmMNam3uVAjZpfkcJCwDwnZn6z3wXmqPV", + "pattern" : "^[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{43,44}$", + "type" : "string" } }, - "requested_predicates" : { - "type" : "object", - "description" : "Requested predicate specifications of proof request", - "additionalProperties" : { - "$ref" : "#/definitions/IndyProofReqPredSpec" + "type" : "object" + }, + "DIDCreate" : { + "properties" : { + "method" : { + "description" : "Method for the requested DID.Supported methods are 'key', 'sov', and any other registered method.", + "example" : "sov", + "type" : "string" + }, + "options" : { + "$ref" : "#/components/schemas/DIDCreate_options" + }, + "seed" : { + "description" : "Optional seed to use for DID, Must beenabled in configuration before use.", + "example" : "000000000000000000000000Trustee1", + "type" : "string" } }, - "version" : { - "type" : "string", - "example" : "1.0", - "description" : "Proof request version", - "pattern" : "^[0-9.]+$" - } - } - }, - "IndyProofRequestNonRevoked" : { - "type" : "object", - "properties" : { - "from" : { - "type" : "integer", - "format" : "int32", - "example" : 1640995199, - "description" : "Earliest time of interest in non-revocation interval", - "minimum" : 0, - "maximum" : 18446744073709551615 - }, - "to" : { - "type" : "integer", - "format" : "int32", - "example" : 1640995199, - "description" : "Latest time of interest in non-revocation interval", - "minimum" : 0, - "maximum" : 18446744073709551615 - } - } - }, - "IndyProofRequestedProof" : { - "type" : "object", - "properties" : { - "predicates" : { - "type" : "object", - "description" : "Proof requested proof predicates.", - "additionalProperties" : { - "$ref" : "#/definitions/IndyProofRequestedProofPredicate" + "type" : "object" + }, + "DIDCreateOptions" : { + "properties" : { + "did" : { + "description" : "Specify final value of the did (including did:: prefix)if the method supports or requires so.", + "example" : "did:peer:WgWxqztrNooG92RXvxSTWv", + "pattern" : "^(did:sov:)?[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}$|^did:([a-zA-Z0-9_]+):([a-zA-Z0-9_.%-]+(:[a-zA-Z0-9_.%-]+)*)((;[a-zA-Z0-9_.:%-]+=[a-zA-Z0-9_.:%-]*)*)(\\/[^#?]*)?([?][^#]*)?(\\#.*)?$$", + "type" : "string" + }, + "key_type" : { + "description" : "Key type to use for the DID keypair. Validated with the chosen DID method's supported key types.", + "enum" : [ "ed25519", "bls12381g2" ], + "example" : "ed25519", + "type" : "string" } }, - "revealed_attr_groups" : { - "type" : "object", - "description" : "Proof requested proof revealed attribute groups", - "additionalProperties" : { - "$ref" : "#/definitions/IndyProofRequestedProofRevealedAttrGroup" + "required" : [ "key_type" ], + "type" : "object" + }, + "DIDEndpoint" : { + "properties" : { + "did" : { + "description" : "DID of interest", + "example" : "WgWxqztrNooG92RXvxSTWv", + "pattern" : "^(did:sov:)?[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}$", + "type" : "string" }, - "x-nullable" : true + "endpoint" : { + "description" : "Endpoint to set (omit to delete)", + "example" : "https://myhost:8021", + "pattern" : "^[A-Za-z0-9\\.\\-\\+]+://([A-Za-z0-9][.A-Za-z0-9-_]+[A-Za-z0-9])+(:[1-9][0-9]*)?(/[^?&#]+)?$", + "type" : "string" + } }, - "revealed_attrs" : { - "type" : "object", - "description" : "Proof requested proof revealed attributes", - "additionalProperties" : { - "$ref" : "#/definitions/IndyProofRequestedProofRevealedAttr" + "required" : [ "did" ], + "type" : "object" + }, + "DIDEndpointWithType" : { + "properties" : { + "did" : { + "description" : "DID of interest", + "example" : "WgWxqztrNooG92RXvxSTWv", + "pattern" : "^(did:sov:)?[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}$", + "type" : "string" }, - "x-nullable" : true - }, - "self_attested_attrs" : { - "type" : "object", - "description" : "Proof requested proof self-attested attributes", - "properties" : { } - }, - "unrevealed_attrs" : { - "type" : "object", - "description" : "Unrevealed attributes", - "properties" : { } - } - } - }, - "IndyProofRequestedProofPredicate" : { - "type" : "object", - "properties" : { - "sub_proof_index" : { - "type" : "integer", - "format" : "int32", - "description" : "Sub-proof index" - } - } - }, - "IndyProofRequestedProofRevealedAttr" : { - "type" : "object", - "properties" : { - "encoded" : { - "type" : "string", - "example" : "0", - "description" : "Encoded value", - "pattern" : "^[0-9]*$" - }, - "raw" : { - "type" : "string", - "description" : "Raw value" - }, - "sub_proof_index" : { - "type" : "integer", - "format" : "int32", - "description" : "Sub-proof index" - } - } - }, - "IndyProofRequestedProofRevealedAttrGroup" : { - "type" : "object", - "properties" : { - "sub_proof_index" : { - "type" : "integer", - "format" : "int32", - "description" : "Sub-proof index" - }, - "values" : { - "type" : "object", - "description" : "Indy proof requested proof revealed attr groups group value", - "additionalProperties" : { - "$ref" : "#/definitions/RawEncoded" + "endpoint" : { + "description" : "Endpoint to set (omit to delete)", + "example" : "https://myhost:8021", + "pattern" : "^[A-Za-z0-9\\.\\-\\+]+://([A-Za-z0-9][.A-Za-z0-9-_]+[A-Za-z0-9])+(:[1-9][0-9]*)?(/[^?&#]+)?$", + "type" : "string" + }, + "endpoint_type" : { + "description" : "Endpoint type to set (default 'Endpoint'); affects only public or posted DIDs", + "enum" : [ "Endpoint", "Profile", "LinkedDomains" ], + "example" : "Endpoint", + "type" : "string" } - } - } - }, - "IndyRequestedCredsRequestedAttr" : { - "type" : "object", - "required" : [ "cred_id" ], - "properties" : { - "cred_id" : { - "type" : "string", - "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", - "description" : "Wallet credential identifier (typically but not necessarily a UUID)" - }, - "revealed" : { - "type" : "boolean", - "description" : "Whether to reveal attribute in proof (default true)" - } - } - }, - "IndyRequestedCredsRequestedPred" : { - "type" : "object", - "required" : [ "cred_id" ], - "properties" : { - "cred_id" : { - "type" : "string", - "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", - "description" : "Wallet credential identifier (typically but not necessarily a UUID)" - }, - "timestamp" : { - "type" : "integer", - "format" : "int32", - "example" : 1640995199, - "description" : "Epoch timestamp of interest for non-revocation proof", - "minimum" : 0, - "maximum" : 18446744073709551615 - } - } - }, - "IndyRevRegDef" : { - "type" : "object", - "properties" : { - "credDefId" : { - "type" : "string", - "example" : "WgWxqztrNooG92RXvxSTWv:3:CL:20:tag", - "description" : "Credential definition identifier", - "pattern" : "^([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}):3:CL:(([1-9][0-9]*)|([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}:2:.+:[0-9.]+)):(.+)?$" - }, - "id" : { - "type" : "string", - "example" : "WgWxqztrNooG92RXvxSTWv:4:WgWxqztrNooG92RXvxSTWv:3:CL:20:tag:CL_ACCUM:0", - "description" : "Indy revocation registry identifier", - "pattern" : "^([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}):4:([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}):3:CL:(([1-9][0-9]*)|([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}:2:.+:[0-9.]+))(:.+)?:CL_ACCUM:(.+$)" - }, - "revocDefType" : { - "type" : "string", - "example" : "CL_ACCUM", - "description" : "Revocation registry type (specify CL_ACCUM)", - "enum" : [ "CL_ACCUM" ] - }, - "tag" : { - "type" : "string", - "description" : "Revocation registry tag" - }, - "value" : { - "$ref" : "#/definitions/IndyRevRegDef_value" - }, - "ver" : { - "type" : "string", - "example" : "1.0", - "description" : "Version of revocation registry definition", - "pattern" : "^[0-9.]+$" - } - } - }, - "IndyRevRegDefValue" : { - "type" : "object", - "properties" : { - "issuanceType" : { - "type" : "string", - "description" : "Issuance type", - "enum" : [ "ISSUANCE_ON_DEMAND", "ISSUANCE_BY_DEFAULT" ] - }, - "maxCredNum" : { - "type" : "integer", - "format" : "int32", - "example" : 10, - "description" : "Maximum number of credentials; registry size", - "minimum" : 1 - }, - "publicKeys" : { - "$ref" : "#/definitions/IndyRevRegDefValue_publicKeys" - }, - "tailsHash" : { - "type" : "string", - "example" : "H3C2AVvLMv6gmMNam3uVAjZpfkcJCwDwnZn6z3wXmqPV", - "description" : "Tails hash value", - "pattern" : "^[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{43,44}$" - }, - "tailsLocation" : { - "type" : "string", - "description" : "Tails file location" - } - } - }, - "IndyRevRegDefValuePublicKeys" : { - "type" : "object", - "properties" : { - "accumKey" : { - "$ref" : "#/definitions/IndyRevRegDefValuePublicKeysAccumKey" - } - } - }, - "IndyRevRegDefValuePublicKeysAccumKey" : { - "type" : "object", - "properties" : { - "z" : { - "type" : "string", - "example" : "1 120F522F81E6B7 1 09F7A59005C4939854", - "description" : "Value for z" - } - } - }, - "IndyRevRegEntry" : { - "type" : "object", - "properties" : { - "value" : { - "$ref" : "#/definitions/IndyRevRegEntry_value" - }, - "ver" : { - "type" : "string", - "example" : "1.0", - "description" : "Version of revocation registry entry", - "pattern" : "^[0-9.]+$" - } - } - }, - "IndyRevRegEntryValue" : { - "type" : "object", - "properties" : { - "accum" : { - "type" : "string", - "example" : "21 11792B036AED0AAA12A4 4 298B2571FFC63A737", - "description" : "Accumulator value" - }, - "prevAccum" : { - "type" : "string", - "example" : "21 137AC810975E4 6 76F0384B6F23", - "description" : "Previous accumulator value" }, - "revoked" : { - "type" : "array", - "description" : "Revoked credential revocation identifiers", - "items" : { - "type" : "integer", - "format" : "int32" + "required" : [ "did" ], + "type" : "object" + }, + "DIDList" : { + "properties" : { + "results" : { + "description" : "DID list", + "items" : { + "$ref" : "#/components/schemas/DID" + }, + "type" : "array" } - } - } - }, - "InputDescriptors" : { - "type" : "object", - "properties" : { - "constraints" : { - "$ref" : "#/definitions/Constraints" }, - "group" : { - "type" : "array", - "items" : { - "type" : "string", - "description" : "Group" + "type" : "object" + }, + "DIDResult" : { + "properties" : { + "result" : { + "$ref" : "#/components/schemas/DID" } }, - "id" : { - "type" : "string", - "description" : "ID" - }, - "metadata" : { - "type" : "object", - "description" : "Metadata dictionary", - "properties" : { } - }, - "name" : { - "type" : "string", - "description" : "Name" - }, - "purpose" : { - "type" : "string", - "description" : "Purpose" - } - } - }, - "IntroModuleResponse" : { - "type" : "object" - }, - "InvitationCreateRequest" : { - "type" : "object", - "properties" : { - "alias" : { - "type" : "string", - "example" : "Barry", - "description" : "Alias for connection" - }, - "attachments" : { - "type" : "array", - "description" : "Optional invitation attachments", - "items" : { - "$ref" : "#/definitions/AttachmentDef" + "type" : "object" + }, + "DIDXRequest" : { + "properties" : { + "@id" : { + "description" : "Message identifier", + "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "type" : "string" + }, + "@type" : { + "description" : "Message type", + "example" : "https://didcomm.org/my-family/1.0/my-message-type", + "readOnly" : true, + "type" : "string" + }, + "did" : { + "description" : "DID of exchange", + "example" : "WgWxqztrNooG92RXvxSTWv", + "pattern" : "^(did:sov:)?[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}$", + "type" : "string" + }, + "did_doc~attach" : { + "$ref" : "#/components/schemas/DIDXRequest_did_doc_attach" + }, + "label" : { + "description" : "Label for DID exchange request", + "example" : "Request to connect with Bob", + "type" : "string" } }, - "handshake_protocols" : { - "type" : "array", - "items" : { - "type" : "string", - "example" : "did:sov:BzCbsNYhMrjHiqZDTUASHg;spec/didexchange/1.0", - "description" : "Handshake protocol to specify in invitation" + "required" : [ "label" ], + "type" : "object" + }, + "DIFField" : { + "properties" : { + "filter" : { + "$ref" : "#/components/schemas/Filter" + }, + "id" : { + "description" : "ID", + "type" : "string" + }, + "path" : { + "items" : { + "description" : "Path", + "type" : "string" + }, + "type" : "array" + }, + "predicate" : { + "description" : "Preference", + "enum" : [ "required", "preferred" ], + "type" : "string" + }, + "purpose" : { + "description" : "Purpose", + "type" : "string" } }, - "mediation_id" : { - "type" : "string", - "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", - "description" : "Identifier for active mediation record to be used", - "pattern" : "[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-4[a-fA-F0-9]{3}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}" - }, - "metadata" : { - "type" : "object", - "description" : "Optional metadata to attach to the connection created with the invitation", - "properties" : { } - }, - "my_label" : { - "type" : "string", - "example" : "Invitation to Barry", - "description" : "Label for connection invitation" - }, - "use_public_did" : { - "type" : "boolean", - "example" : false, - "description" : "Whether to use public DID in invitation" - } - } - }, - "InvitationMessage" : { - "type" : "object", - "properties" : { - "@id" : { - "type" : "string", - "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", - "description" : "Message identifier" + "type" : "object" + }, + "DIFHolder" : { + "properties" : { + "directive" : { + "description" : "Preference", + "enum" : [ "required", "preferred" ], + "type" : "string" + }, + "field_id" : { + "items" : { + "description" : "FieldID", + "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "pattern" : "[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-4[a-fA-F0-9]{3}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}", + "type" : "string" + }, + "type" : "array" + } }, - "@type" : { - "type" : "string", - "example" : "https://didcomm.org/my-family/1.0/my-message-type", - "description" : "Message type", - "readOnly" : true + "type" : "object" + }, + "DIFOptions" : { + "properties" : { + "challenge" : { + "description" : "Challenge protect against replay attack", + "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "pattern" : "[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-4[a-fA-F0-9]{3}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}", + "type" : "string" + }, + "domain" : { + "description" : "Domain protect against replay attack", + "example" : "4jt78h47fh47", + "type" : "string" + } }, - "handshake_protocols" : { - "type" : "array", - "items" : { - "type" : "string", - "example" : "did:sov:BzCbsNYhMrjHiqZDTUASHg;spec/didexchange/1.0", - "description" : "Handshake protocol" + "type" : "object" + }, + "DIFPresSpec" : { + "properties" : { + "issuer_id" : { + "description" : "Issuer identifier to sign the presentation, if different from current public DID", + "type" : "string" + }, + "presentation_definition" : { + "$ref" : "#/components/schemas/PresentationDefinition" + }, + "record_ids" : { + "description" : "Mapping of input_descriptor id to list of stored W3C credential record_id", + "example" : { + "" : [ "", "" ], + "" : [ "" ] + }, + "properties" : { }, + "type" : "object" + }, + "reveal_doc" : { + "description" : "reveal doc [JSON-LD frame] dict used to derive the credential when selective disclosure is required", + "example" : { + "@context" : [ "https://www.w3.org/2018/credentials/v1", "https://w3id.org/security/bbs/v1" ], + "@explicit" : true, + "@requireAll" : true, + "credentialSubject" : { + "@explicit" : true, + "@requireAll" : true, + "Observation" : [ { + "effectiveDateTime" : { }, + "@explicit" : true, + "@requireAll" : true + } ] + }, + "issuanceDate" : { }, + "issuer" : { }, + "type" : [ "VerifiableCredential", "LabReport" ] + }, + "properties" : { }, + "type" : "object" } }, - "label" : { - "type" : "string", - "example" : "Bob", - "description" : "Optional label" + "type" : "object" + }, + "DIFProofProposal" : { + "properties" : { + "input_descriptors" : { + "items" : { + "$ref" : "#/components/schemas/InputDescriptors" + }, + "type" : "array" + }, + "options" : { + "$ref" : "#/components/schemas/DIFOptions" + } }, - "requests~attach" : { - "type" : "array", - "description" : "Optional request attachment", - "items" : { - "$ref" : "#/definitions/AttachDecorator" + "type" : "object" + }, + "DIFProofRequest" : { + "properties" : { + "options" : { + "$ref" : "#/components/schemas/DIFOptions" + }, + "presentation_definition" : { + "$ref" : "#/components/schemas/PresentationDefinition" } }, - "services" : { - "type" : "array", - "example" : [ { - "did" : "WgWxqztrNooG92RXvxSTWv", - "id" : "string", - "recipientKeys" : [ "did:key:z6MkpTHR8VNsBxYAAWHut2Geadd9jSwuBV8xRoAnwWsdvktH" ], - "routingKeys" : [ "did:key:z6MkpTHR8VNsBxYAAWHut2Geadd9jSwuBV8xRoAnwWsdvktH" ], - "serviceEndpoint" : "http://192.168.56.101:8020", + "required" : [ "presentation_definition" ], + "type" : "object" + }, + "Date" : { + "properties" : { + "expires_time" : { + "description" : "Expiry Date", + "example" : "2021-03-29T05:22:19Z", + "format" : "date-time", "type" : "string" - }, "did:sov:WgWxqztrNooG92RXvxSTWv" ], - "items" : { - "description" : "Either a DIDComm service object (as per RFC0067) or a DID string." } - } - } - }, - "InvitationRecord" : { - "type" : "object", - "properties" : { - "created_at" : { - "type" : "string", - "example" : "2021-12-31 23:59:59Z", - "description" : "Time of record creation", - "pattern" : "^\\d{4}-\\d\\d-\\d\\d[T ]\\d\\d:\\d\\d(?:\\:(?:\\d\\d(?:\\.\\d{1,6})?))?(?:[+-]\\d\\d:?\\d\\d|Z|)$" - }, - "invi_msg_id" : { - "type" : "string", - "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", - "description" : "Invitation message identifier" - }, - "invitation" : { - "$ref" : "#/definitions/InvitationRecord_invitation" - }, - "invitation_id" : { - "type" : "string", - "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", - "description" : "Invitation record identifier" - }, - "invitation_url" : { - "type" : "string", - "example" : "https://example.com/endpoint?c_i=eyJAdHlwZSI6ICIuLi4iLCAiLi4uIjogIi4uLiJ9XX0=", - "description" : "Invitation message URL" - }, - "state" : { - "type" : "string", - "example" : "await_response", - "description" : "Out of band message exchange state" - }, - "trace" : { - "type" : "boolean", - "description" : "Record trace information, based on agent configuration" - }, - "updated_at" : { - "type" : "string", - "example" : "2021-12-31 23:59:59Z", - "description" : "Time of last record update", - "pattern" : "^\\d{4}-\\d\\d-\\d\\d[T ]\\d\\d:\\d\\d(?:\\:(?:\\d\\d(?:\\.\\d{1,6})?))?(?:[+-]\\d\\d:?\\d\\d|Z|)$" - } - } - }, - "InvitationResult" : { - "type" : "object", - "properties" : { - "connection_id" : { - "type" : "string", - "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", - "description" : "Connection identifier" }, - "invitation" : { - "$ref" : "#/definitions/ConnectionInvitation" + "required" : [ "expires_time" ], + "type" : "object" + }, + "Disclose" : { + "properties" : { + "@id" : { + "description" : "Message identifier", + "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "type" : "string" + }, + "@type" : { + "description" : "Message type", + "example" : "https://didcomm.org/my-family/1.0/my-message-type", + "readOnly" : true, + "type" : "string" + }, + "protocols" : { + "description" : "List of protocol descriptors", + "items" : { + "$ref" : "#/components/schemas/ProtocolDescriptor" + }, + "type" : "array" + } }, - "invitation_url" : { - "type" : "string", - "example" : "http://192.168.56.101:8020/invite?c_i=eyJAdHlwZSI6Li4ufQ==", - "description" : "Invitation URL" - } - } - }, - "IssueCredentialModuleResponse" : { - "type" : "object" - }, - "IssuerCredRevRecord" : { - "type" : "object", - "properties" : { - "created_at" : { - "type" : "string", - "example" : "2021-12-31 23:59:59Z", - "description" : "Time of record creation", - "pattern" : "^\\d{4}-\\d\\d-\\d\\d[T ]\\d\\d:\\d\\d(?:\\:(?:\\d\\d(?:\\.\\d{1,6})?))?(?:[+-]\\d\\d:?\\d\\d|Z|)$" - }, - "cred_def_id" : { - "type" : "string", - "example" : "WgWxqztrNooG92RXvxSTWv:3:CL:20:tag", - "description" : "Credential definition identifier", - "pattern" : "^([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}):3:CL:(([1-9][0-9]*)|([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}:2:.+:[0-9.]+)):(.+)?$" - }, - "cred_ex_id" : { - "type" : "string", - "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", - "description" : "Credential exchange record identifier at credential issue" - }, - "cred_rev_id" : { - "type" : "string", - "example" : "12345", - "description" : "Credential revocation identifier", - "pattern" : "^[1-9][0-9]*$" - }, - "record_id" : { - "type" : "string", - "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", - "description" : "Issuer credential revocation record identifier" - }, - "rev_reg_id" : { - "type" : "string", - "example" : "WgWxqztrNooG92RXvxSTWv:4:WgWxqztrNooG92RXvxSTWv:3:CL:20:tag:CL_ACCUM:0", - "description" : "Revocation registry identifier", - "pattern" : "^([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}):4:([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}):3:CL:(([1-9][0-9]*)|([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}:2:.+:[0-9.]+))(:.+)?:CL_ACCUM:(.+$)" - }, - "state" : { - "type" : "string", - "example" : "issued", - "description" : "Issue credential revocation record state" - }, - "updated_at" : { - "type" : "string", - "example" : "2021-12-31 23:59:59Z", - "description" : "Time of last record update", - "pattern" : "^\\d{4}-\\d\\d-\\d\\d[T ]\\d\\d:\\d\\d(?:\\:(?:\\d\\d(?:\\.\\d{1,6})?))?(?:[+-]\\d\\d:?\\d\\d|Z|)$" - } - } - }, - "IssuerRevRegRecord" : { - "type" : "object", - "properties" : { - "created_at" : { - "type" : "string", - "example" : "2021-12-31 23:59:59Z", - "description" : "Time of record creation", - "pattern" : "^\\d{4}-\\d\\d-\\d\\d[T ]\\d\\d:\\d\\d(?:\\:(?:\\d\\d(?:\\.\\d{1,6})?))?(?:[+-]\\d\\d:?\\d\\d|Z|)$" - }, - "cred_def_id" : { - "type" : "string", - "example" : "WgWxqztrNooG92RXvxSTWv:3:CL:20:tag", - "description" : "Credential definition identifier", - "pattern" : "^([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}):3:CL:(([1-9][0-9]*)|([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}:2:.+:[0-9.]+)):(.+)?$" - }, - "error_msg" : { - "type" : "string", - "example" : "Revocation registry undefined", - "description" : "Error message" - }, - "issuer_did" : { - "type" : "string", - "example" : "WgWxqztrNooG92RXvxSTWv", - "description" : "Issuer DID", - "pattern" : "^(did:sov:)?[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}$" - }, - "max_cred_num" : { - "type" : "integer", - "format" : "int32", - "example" : 1000, - "description" : "Maximum number of credentials for revocation registry" - }, - "pending_pub" : { - "type" : "array", - "description" : "Credential revocation identifier for credential revoked and pending publication to ledger", - "items" : { - "type" : "string", - "example" : "23" + "required" : [ "protocols" ], + "type" : "object" + }, + "Disclosures" : { + "properties" : { + "@id" : { + "description" : "Message identifier", + "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "type" : "string" + }, + "@type" : { + "description" : "Message type", + "example" : "https://didcomm.org/my-family/1.0/my-message-type", + "readOnly" : true, + "type" : "string" + }, + "disclosures" : { + "description" : "List of protocol or goal_code descriptors", + "items" : { + "type" : "object" + }, + "type" : "array" } }, - "record_id" : { - "type" : "string", - "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", - "description" : "Issuer revocation registry record identifier" - }, - "revoc_def_type" : { - "type" : "string", - "example" : "CL_ACCUM", - "description" : "Revocation registry type (specify CL_ACCUM)", - "enum" : [ "CL_ACCUM" ] - }, - "revoc_reg_def" : { - "$ref" : "#/definitions/IssuerRevRegRecord_revoc_reg_def" - }, - "revoc_reg_entry" : { - "$ref" : "#/definitions/IssuerRevRegRecord_revoc_reg_entry" - }, - "revoc_reg_id" : { - "type" : "string", - "example" : "WgWxqztrNooG92RXvxSTWv:4:WgWxqztrNooG92RXvxSTWv:3:CL:20:tag:CL_ACCUM:0", - "description" : "Revocation registry identifier", - "pattern" : "^([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}):4:([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}):3:CL:(([1-9][0-9]*)|([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}:2:.+:[0-9.]+))(:.+)?:CL_ACCUM:(.+$)" - }, - "state" : { - "type" : "string", - "example" : "active", - "description" : "Issue revocation registry record state" - }, - "tag" : { - "type" : "string", - "description" : "Tag within issuer revocation registry identifier" - }, - "tails_hash" : { - "type" : "string", - "example" : "H3C2AVvLMv6gmMNam3uVAjZpfkcJCwDwnZn6z3wXmqPV", - "description" : "Tails hash", - "pattern" : "^[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{43,44}$" - }, - "tails_local_path" : { - "type" : "string", - "description" : "Local path to tails file" - }, - "tails_public_uri" : { - "type" : "string", - "description" : "Public URI for tails file" - }, - "updated_at" : { - "type" : "string", - "example" : "2021-12-31 23:59:59Z", - "description" : "Time of last record update", - "pattern" : "^\\d{4}-\\d\\d-\\d\\d[T ]\\d\\d:\\d\\d(?:\\:(?:\\d\\d(?:\\.\\d{1,6})?))?(?:[+-]\\d\\d:?\\d\\d|Z|)$" - } - } - }, - "Keylist" : { - "type" : "object", - "properties" : { - "results" : { - "type" : "array", - "description" : "List of keylist records", - "items" : { - "$ref" : "#/definitions/RouteRecord" + "required" : [ "disclosures" ], + "type" : "object" + }, + "Doc" : { + "properties" : { + "credential" : { + "description" : "Credential to sign", + "properties" : { }, + "type" : "object" + }, + "options" : { + "$ref" : "#/components/schemas/Doc_options" } - } - } - }, - "KeylistQuery" : { - "type" : "object", - "properties" : { - "@id" : { - "type" : "string", - "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", - "description" : "Message identifier" - }, - "@type" : { - "type" : "string", - "example" : "https://didcomm.org/my-family/1.0/my-message-type", - "description" : "Message type", - "readOnly" : true }, - "filter" : { - "type" : "object", - "example" : { - "filter" : { } + "required" : [ "credential", "options" ], + "type" : "object" + }, + "EndorserInfo" : { + "properties" : { + "endorser_did" : { + "description" : "Endorser DID", + "type" : "string" }, - "description" : "Query dictionary object", - "properties" : { } - }, - "paginate" : { - "$ref" : "#/definitions/KeylistQuery_paginate" - } - } - }, - "KeylistQueryFilterRequest" : { - "type" : "object", - "properties" : { - "filter" : { - "type" : "object", - "description" : "Filter for keylist query", - "properties" : { } - } - } - }, - "KeylistQueryPaginate" : { - "type" : "object", - "properties" : { - "limit" : { - "type" : "integer", - "format" : "int32", - "example" : 30, - "description" : "Limit for keylist query" - }, - "offset" : { - "type" : "integer", - "format" : "int32", - "example" : 0, - "description" : "Offset value for query" - } - } - }, - "KeylistUpdate" : { - "type" : "object", - "properties" : { - "@id" : { - "type" : "string", - "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", - "description" : "Message identifier" - }, - "@type" : { - "type" : "string", - "example" : "https://didcomm.org/my-family/1.0/my-message-type", - "description" : "Message type", - "readOnly" : true - }, - "updates" : { - "type" : "array", - "description" : "List of update rules", - "items" : { - "$ref" : "#/definitions/KeylistUpdateRule" - } - } - } - }, - "KeylistUpdateRequest" : { - "type" : "object", - "properties" : { - "updates" : { - "type" : "array", - "items" : { - "$ref" : "#/definitions/KeylistUpdateRule" + "endorser_name" : { + "description" : "Endorser Name", + "type" : "string" } - } - } - }, - "KeylistUpdateRule" : { - "type" : "object", - "required" : [ "action", "recipient_key" ], - "properties" : { - "action" : { - "type" : "string", - "example" : "add", - "description" : "Action for specific key", - "enum" : [ "add", "remove" ] - }, - "recipient_key" : { - "type" : "string", - "example" : "H3C2AVvLMv6gmMNam3uVAjZpfkcJCwDwnZn6z3wXmqPV", - "description" : "Key to remove or add", - "pattern" : "^[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{43,44}$" - } - } - }, - "LDProofVCDetail" : { - "type" : "object", - "required" : [ "credential", "options" ], - "properties" : { - "credential" : { - "$ref" : "#/definitions/LDProofVCDetail_credential" }, - "options" : { - "$ref" : "#/definitions/LDProofVCDetail_options" - } - } - }, - "LDProofVCDetailOptions" : { - "type" : "object", - "required" : [ "proofType" ], - "properties" : { - "challenge" : { - "type" : "string", - "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", - "description" : "A challenge to include in the proof. SHOULD be provided by the requesting party of the credential (=holder)" - }, - "created" : { - "type" : "string", - "example" : "2021-12-31 23:59:59Z", - "description" : "The date and time of the proof (with a maximum accuracy in seconds). Defaults to current system time", - "pattern" : "^\\d{4}-\\d\\d-\\d\\d[T ]\\d\\d:\\d\\d(?:\\:(?:\\d\\d(?:\\.\\d{1,6})?))?(?:[+-]\\d\\d:?\\d\\d|Z|)$" - }, - "credentialStatus" : { - "$ref" : "#/definitions/LDProofVCDetailOptions_credentialStatus" - }, - "domain" : { - "type" : "string", - "example" : "example.com", - "description" : "The intended domain of validity for the proof" - }, - "proofPurpose" : { - "type" : "string", - "example" : "assertionMethod", - "description" : "The proof purpose used for the proof. Should match proof purposes registered in the Linked Data Proofs Specification" - }, - "proofType" : { - "type" : "string", - "example" : "Ed25519Signature2018", - "description" : "The proof type used for the proof. Should match suites registered in the Linked Data Cryptographic Suite Registry" - } - } - }, - "LedgerModulesResult" : { - "type" : "object" - }, - "LinkedDataProof" : { - "type" : "object", - "required" : [ "created", "proofPurpose", "type", "verificationMethod" ], - "properties" : { - "challenge" : { - "type" : "string", - "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", - "description" : "Associates a challenge with a proof, for use with a proofPurpose such as authentication" - }, - "created" : { - "type" : "string", - "example" : "2021-12-31 23:59:59Z", - "description" : "The string value of an ISO8601 combined date and time string generated by the Signature Algorithm", - "pattern" : "^\\d{4}-\\d\\d-\\d\\d[T ]\\d\\d:\\d\\d(?:\\:(?:\\d\\d(?:\\.\\d{1,6})?))?(?:[+-]\\d\\d:?\\d\\d|Z|)$" - }, - "domain" : { - "type" : "string", - "example" : "example.com", - "description" : "A string value specifying the restricted domain of the signature.", - "pattern" : "\\w+:(\\/?\\/?)[^\\s]+" - }, - "jws" : { - "type" : "string", - "example" : "eyJhbGciOiAiRWREUc2UsICJjcml0IjogWyJiNjQiXX0..lKJU0Df_keblRKhZAS9Qq6zybm-HqUXNVZ8vgEPNTAjQ1Ch6YBKY7UBAjg6iBX5qBQ", - "description" : "Associates a Detached Json Web Signature with a proof" - }, - "nonce" : { - "type" : "string", - "example" : "CF69iO3nfvqRsRBNElE8b4wO39SyJHPM7Gg1nExltW5vSfQA1lvDCR/zXX1To0/4NLo==", - "description" : "The nonce" - }, - "proofPurpose" : { - "type" : "string", - "example" : "assertionMethod", - "description" : "Proof purpose" - }, - "proofValue" : { - "type" : "string", - "example" : "sy1AahqbzJQ63n9RtekmwzqZeVj494VppdAVJBnMYrTwft6cLJJGeTSSxCCJ6HKnRtwE7jjDh6sB2z2AAiZY9BBnCD8wUVgwqH3qchGRCuC2RugA4eQ9fUrR4Yuycac3caiaaay", - "description" : "The proof value of a proof" - }, - "type" : { - "type" : "string", - "example" : "Ed25519Signature2018", - "description" : "Identifies the digital signature suite that was used to create the signature" - }, - "verificationMethod" : { - "type" : "string", - "example" : "did:key:z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL#z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL", - "description" : "Information used for proof verification", - "pattern" : "\\w+:(\\/?\\/?)[^\\s]+" - } - } - }, - "MediationCreateRequest" : { - "type" : "object", - "properties" : { - "mediator_terms" : { - "type" : "array", - "description" : "List of mediator rules for recipient", - "items" : { - "type" : "string", - "description" : "Indicate terms to which the mediator requires the recipient to agree" + "required" : [ "endorser_did" ], + "type" : "object" + }, + "EndpointsResult" : { + "properties" : { + "my_endpoint" : { + "description" : "My endpoint", + "example" : "https://myhost:8021", + "pattern" : "^[A-Za-z0-9\\.\\-\\+]+://([A-Za-z0-9][.A-Za-z0-9-_]+[A-Za-z0-9])+(:[1-9][0-9]*)?(/[^?&#]+)?$", + "type" : "string" + }, + "their_endpoint" : { + "description" : "Their endpoint", + "example" : "https://myhost:8021", + "pattern" : "^[A-Za-z0-9\\.\\-\\+]+://([A-Za-z0-9][.A-Za-z0-9-_]+[A-Za-z0-9])+(:[1-9][0-9]*)?(/[^?&#]+)?$", + "type" : "string" } }, - "recipient_terms" : { - "type" : "array", - "description" : "List of recipient rules for mediation", - "items" : { - "type" : "string", - "description" : "Indicate terms to which the recipient requires the mediator to agree" + "type" : "object" + }, + "Filter" : { + "properties" : { + "const" : { + "description" : "Const", + "type" : "object" + }, + "enum" : { + "items" : { + "description" : "Enum", + "type" : "object" + }, + "type" : "array" + }, + "exclusiveMaximum" : { + "description" : "ExclusiveMaximum", + "type" : "object" + }, + "exclusiveMinimum" : { + "description" : "ExclusiveMinimum", + "type" : "object" + }, + "format" : { + "description" : "Format", + "type" : "string" + }, + "maxLength" : { + "description" : "Max Length", + "example" : 1234, + "format" : "int32", + "type" : "integer" + }, + "maximum" : { + "description" : "Maximum", + "type" : "object" + }, + "minLength" : { + "description" : "Min Length", + "example" : 1234, + "format" : "int32", + "type" : "integer" + }, + "minimum" : { + "description" : "Minimum", + "type" : "object" + }, + "not" : { + "description" : "Not", + "example" : false, + "type" : "boolean" + }, + "pattern" : { + "description" : "Pattern", + "type" : "string" + }, + "type" : { + "description" : "Type", + "type" : "string" } - } - } - }, - "MediationDeny" : { - "type" : "object", - "properties" : { - "@id" : { - "type" : "string", - "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", - "description" : "Message identifier" }, - "@type" : { - "type" : "string", - "example" : "https://didcomm.org/my-family/1.0/my-message-type", - "description" : "Message type", - "readOnly" : true - }, - "mediator_terms" : { - "type" : "array", - "items" : { - "type" : "string", - "description" : "Terms for mediator to agree" + "type" : "object" + }, + "Generated" : { + "properties" : { + "master_secret" : { + "example" : "0", + "pattern" : "^[0-9]*$", + "type" : "string" + }, + "number" : { + "example" : "0", + "pattern" : "^[0-9]*$", + "type" : "string" + }, + "remainder" : { + "example" : "0", + "pattern" : "^[0-9]*$", + "type" : "string" } }, - "recipient_terms" : { - "type" : "array", - "items" : { - "type" : "string", - "description" : "Terms for recipient to agree" - } - } - } - }, - "MediationGrant" : { - "type" : "object", - "properties" : { - "@id" : { - "type" : "string", - "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", - "description" : "Message identifier" - }, - "@type" : { - "type" : "string", - "example" : "https://didcomm.org/my-family/1.0/my-message-type", - "description" : "Message type", - "readOnly" : true - }, - "endpoint" : { - "type" : "string", - "example" : "http://192.168.56.102:8020/", - "description" : "endpoint on which messages destined for the recipient are received." - }, - "routing_keys" : { - "type" : "array", - "items" : { - "type" : "string", - "description" : "Keys to use for forward message packaging" - } - } - } - }, - "MediationList" : { - "type" : "object", - "properties" : { - "results" : { - "type" : "array", - "description" : "List of mediation records", - "items" : { - "$ref" : "#/definitions/MediationRecord" + "type" : "object" + }, + "GetDIDEndpointResponse" : { + "properties" : { + "endpoint" : { + "description" : "Full verification key", + "example" : "https://myhost:8021", + "nullable" : true, + "pattern" : "^[A-Za-z0-9\\.\\-\\+]+://([A-Za-z0-9][.A-Za-z0-9-_]+[A-Za-z0-9])+(:[1-9][0-9]*)?(/[^?&#]+)?$", + "type" : "string" } - } - } - }, - "MediationRecord" : { - "type" : "object", - "required" : [ "connection_id", "role" ], - "properties" : { - "connection_id" : { - "type" : "string" - }, - "created_at" : { - "type" : "string", - "example" : "2021-12-31 23:59:59Z", - "description" : "Time of record creation", - "pattern" : "^\\d{4}-\\d\\d-\\d\\d[T ]\\d\\d:\\d\\d(?:\\:(?:\\d\\d(?:\\.\\d{1,6})?))?(?:[+-]\\d\\d:?\\d\\d|Z|)$" - }, - "endpoint" : { - "type" : "string" - }, - "mediation_id" : { - "type" : "string" }, - "mediator_terms" : { - "type" : "array", - "items" : { + "type" : "object" + }, + "GetDIDVerkeyResponse" : { + "properties" : { + "verkey" : { + "description" : "Full verification key", + "example" : "H3C2AVvLMv6gmMNam3uVAjZpfkcJCwDwnZn6z3wXmqPV", + "nullable" : true, + "pattern" : "^[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{43,44}$", "type" : "string" } }, - "recipient_terms" : { - "type" : "array", - "items" : { + "type" : "object" + }, + "GetNymRoleResponse" : { + "properties" : { + "role" : { + "description" : "Ledger role", + "enum" : [ "STEWARD", "TRUSTEE", "ENDORSER", "NETWORK_MONITOR", "USER", "ROLE_REMOVE" ], + "example" : "ENDORSER", "type" : "string" } }, - "role" : { - "type" : "string" - }, - "routing_keys" : { - "type" : "array", - "items" : { - "type" : "string", - "example" : "H3C2AVvLMv6gmMNam3uVAjZpfkcJCwDwnZn6z3wXmqPV", - "pattern" : "^[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{43,44}$" + "type" : "object" + }, + "HolderModuleResponse" : { + "type" : "object" + }, + "IndyAttrValue" : { + "properties" : { + "encoded" : { + "description" : "Attribute encoded value", + "example" : "-1", + "pattern" : "^-?[0-9]*$", + "type" : "string" + }, + "raw" : { + "description" : "Attribute raw value", + "type" : "string" } }, - "state" : { - "type" : "string", - "example" : "active", - "description" : "Current record state" - }, - "updated_at" : { - "type" : "string", - "example" : "2021-12-31 23:59:59Z", - "description" : "Time of last record update", - "pattern" : "^\\d{4}-\\d\\d-\\d\\d[T ]\\d\\d:\\d\\d(?:\\:(?:\\d\\d(?:\\.\\d{1,6})?))?(?:[+-]\\d\\d:?\\d\\d|Z|)$" - } - } - }, - "Menu" : { - "type" : "object", - "required" : [ "options" ], - "properties" : { - "@id" : { - "type" : "string", - "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", - "description" : "Message identifier" - }, - "@type" : { - "type" : "string", - "example" : "https://didcomm.org/my-family/1.0/my-message-type", - "description" : "Message type", - "readOnly" : true - }, - "description" : { - "type" : "string", - "example" : "This menu presents options", - "description" : "Introductory text for the menu" - }, - "errormsg" : { - "type" : "string", - "example" : "Error: item not found", - "description" : "An optional error message to display in menu header" - }, - "options" : { - "type" : "array", - "description" : "List of menu options", - "items" : { - "$ref" : "#/definitions/MenuOption" - } - }, - "title" : { - "type" : "string", - "example" : "My Menu", - "description" : "Menu title" - } - } - }, - "MenuForm" : { - "type" : "object", - "properties" : { - "description" : { - "type" : "string", - "example" : "Window preference settings", - "description" : "Additional descriptive text for menu form" - }, - "params" : { - "type" : "array", - "description" : "List of form parameters", - "items" : { - "$ref" : "#/definitions/MenuFormParam" + "required" : [ "encoded", "raw" ], + "type" : "object" + }, + "IndyCredAbstract" : { + "properties" : { + "cred_def_id" : { + "description" : "Credential definition identifier", + "example" : "WgWxqztrNooG92RXvxSTWv:3:CL:20:tag", + "pattern" : "^([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}):3:CL:(([1-9][0-9]*)|([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}:2:.+:[0-9.]+)):(.+)?$", + "type" : "string" + }, + "key_correctness_proof" : { + "$ref" : "#/components/schemas/IndyCredAbstract_key_correctness_proof" + }, + "nonce" : { + "description" : "Nonce in credential abstract", + "example" : "0", + "pattern" : "^[0-9]*$", + "type" : "string" + }, + "schema_id" : { + "description" : "Schema identifier", + "example" : "WgWxqztrNooG92RXvxSTWv:2:schema_name:1.0", + "pattern" : "^[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}:2:.+:[0-9.]+$", + "type" : "string" } }, - "submit-label" : { - "type" : "string", - "example" : "Send", - "description" : "Alternative label for form submit button" - }, - "title" : { - "type" : "string", - "example" : "Preferences", - "description" : "Menu form title" - } - } - }, - "MenuFormParam" : { - "type" : "object", - "required" : [ "name", "title" ], - "properties" : { - "default" : { - "type" : "string", - "example" : "0", - "description" : "Default parameter value" - }, - "description" : { - "type" : "string", - "example" : "Delay in seconds before starting", - "description" : "Additional descriptive text for menu form parameter" - }, - "name" : { - "type" : "string", - "example" : "delay", - "description" : "Menu parameter name" - }, - "required" : { - "type" : "boolean", - "example" : false, - "description" : "Whether parameter is required" - }, - "title" : { - "type" : "string", - "example" : "Delay in seconds", - "description" : "Menu parameter title" - }, - "type" : { - "type" : "string", - "example" : "int", - "description" : "Menu form parameter input type" - } - } - }, - "MenuJson" : { - "type" : "object", - "required" : [ "options" ], - "properties" : { - "description" : { - "type" : "string", - "example" : "User preferences for window settings", - "description" : "Introductory text for the menu" - }, - "errormsg" : { - "type" : "string", - "example" : "Error: item not present", - "description" : "Optional error message to display in menu header" - }, - "options" : { - "type" : "array", - "description" : "List of menu options", - "items" : { - "$ref" : "#/definitions/MenuOption" + "required" : [ "cred_def_id", "key_correctness_proof", "nonce", "schema_id" ], + "type" : "object" + }, + "IndyCredInfo" : { + "properties" : { + "attrs" : { + "additionalProperties" : { + "example" : "alice", + "type" : "string" + }, + "description" : "Attribute names and value", + "type" : "object" + }, + "cred_def_id" : { + "description" : "Credential definition identifier", + "example" : "WgWxqztrNooG92RXvxSTWv:3:CL:20:tag", + "pattern" : "^([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}):3:CL:(([1-9][0-9]*)|([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}:2:.+:[0-9.]+)):(.+)?$", + "type" : "string" + }, + "cred_rev_id" : { + "description" : "Credential revocation identifier", + "example" : "12345", + "nullable" : true, + "pattern" : "^[1-9][0-9]*$", + "type" : "string" + }, + "referent" : { + "description" : "Wallet referent", + "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "type" : "string" + }, + "rev_reg_id" : { + "description" : "Revocation registry identifier", + "example" : "WgWxqztrNooG92RXvxSTWv:4:WgWxqztrNooG92RXvxSTWv:3:CL:20:tag:CL_ACCUM:0", + "nullable" : true, + "pattern" : "^([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}):4:([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}):3:CL:(([1-9][0-9]*)|([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}:2:.+:[0-9.]+))(:.+)?:CL_ACCUM:(.+$)", + "type" : "string" + }, + "schema_id" : { + "description" : "Schema identifier", + "example" : "WgWxqztrNooG92RXvxSTWv:2:schema_name:1.0", + "pattern" : "^[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}:2:.+:[0-9.]+$", + "type" : "string" } }, - "title" : { - "type" : "string", - "example" : "My Menu", - "description" : "Menu title" - } - } - }, - "MenuOption" : { - "type" : "object", - "required" : [ "name", "title" ], - "properties" : { - "description" : { - "type" : "string", - "example" : "Window display preferences", - "description" : "Additional descriptive text for menu option" - }, - "disabled" : { - "type" : "boolean", - "example" : false, - "description" : "Whether to show option as disabled" - }, - "form" : { - "$ref" : "#/definitions/MenuForm" - }, - "name" : { - "type" : "string", - "example" : "window_prefs", - "description" : "Menu option name (unique identifier)" - }, - "title" : { - "type" : "string", - "example" : "Window Preferences", - "description" : "Menu option title" - } - } - }, - "MultitenantModuleResponse" : { - "type" : "object" - }, - "PerformRequest" : { - "type" : "object", - "properties" : { - "name" : { - "type" : "string", - "example" : "Query", - "description" : "Menu option name" - }, - "params" : { - "type" : "object", - "description" : "Input parameter values", - "additionalProperties" : { - "type" : "string", - "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6" + "type" : "object" + }, + "IndyCredPrecis" : { + "properties" : { + "cred_info" : { + "$ref" : "#/components/schemas/IndyCredPrecis_cred_info" + }, + "interval" : { + "$ref" : "#/components/schemas/IndyCredPrecis_interval" + }, + "presentation_referents" : { + "items" : { + "description" : "presentation referent", + "example" : "1_age_uuid", + "type" : "string" + }, + "type" : "array" } - } - } - }, - "PingRequest" : { - "type" : "object", - "properties" : { - "comment" : { - "type" : "string", - "description" : "Comment for the ping message", - "x-nullable" : true - } - } - }, - "PingRequestResponse" : { - "type" : "object", - "properties" : { - "thread_id" : { - "type" : "string", - "description" : "Thread ID of the ping message" - } - } - }, - "PresentationDefinition" : { - "type" : "object", - "properties" : { - "format" : { - "$ref" : "#/definitions/ClaimFormat" }, - "id" : { - "type" : "string", - "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", - "description" : "Unique Resource Identifier", - "pattern" : "[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-4[a-fA-F0-9]{3}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}" - }, - "input_descriptors" : { - "type" : "array", - "items" : { - "$ref" : "#/definitions/InputDescriptors" + "type" : "object" + }, + "IndyCredRequest" : { + "properties" : { + "blinded_ms" : { + "description" : "Blinded master secret", + "properties" : { }, + "type" : "object" + }, + "blinded_ms_correctness_proof" : { + "description" : "Blinded master secret correctness proof", + "properties" : { }, + "type" : "object" + }, + "cred_def_id" : { + "description" : "Credential definition identifier", + "example" : "WgWxqztrNooG92RXvxSTWv:3:CL:20:tag", + "pattern" : "^([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}):3:CL:(([1-9][0-9]*)|([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}:2:.+:[0-9.]+)):(.+)?$", + "type" : "string" + }, + "nonce" : { + "description" : "Nonce in credential request", + "example" : "0", + "pattern" : "^[0-9]*$", + "type" : "string" + }, + "prover_did" : { + "description" : "Prover DID", + "example" : "WgWxqztrNooG92RXvxSTWv", + "pattern" : "^(did:sov:)?[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}$", + "type" : "string" } }, - "name" : { - "type" : "string", - "description" : "Human-friendly name that describes what the presentation definition pertains to" - }, - "purpose" : { - "type" : "string", - "description" : "Describes the purpose for which the Presentation Definition's inputs are being requested" - }, - "submission_requirements" : { - "type" : "array", - "items" : { - "$ref" : "#/definitions/SubmissionRequirements" + "required" : [ "blinded_ms", "blinded_ms_correctness_proof", "cred_def_id", "nonce", "prover_did" ], + "type" : "object" + }, + "IndyCredential" : { + "properties" : { + "cred_def_id" : { + "description" : "Credential definition identifier", + "example" : "WgWxqztrNooG92RXvxSTWv:3:CL:20:tag", + "pattern" : "^([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}):3:CL:(([1-9][0-9]*)|([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}:2:.+:[0-9.]+)):(.+)?$", + "type" : "string" + }, + "rev_reg" : { + "description" : "Revocation registry state", + "nullable" : true, + "properties" : { }, + "type" : "object" + }, + "rev_reg_id" : { + "description" : "Revocation registry identifier", + "example" : "WgWxqztrNooG92RXvxSTWv:4:WgWxqztrNooG92RXvxSTWv:3:CL:20:tag:CL_ACCUM:0", + "nullable" : true, + "pattern" : "^([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}):4:([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}):3:CL:(([1-9][0-9]*)|([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}:2:.+:[0-9.]+))(:.+)?:CL_ACCUM:(.+$)", + "type" : "string" + }, + "schema_id" : { + "description" : "Schema identifier", + "example" : "WgWxqztrNooG92RXvxSTWv:2:schema_name:1.0", + "pattern" : "^[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}:2:.+:[0-9.]+$", + "type" : "string" + }, + "signature" : { + "description" : "Credential signature", + "properties" : { }, + "type" : "object" + }, + "signature_correctness_proof" : { + "description" : "Credential signature correctness proof", + "properties" : { }, + "type" : "object" + }, + "values" : { + "additionalProperties" : { + "$ref" : "#/components/schemas/IndyCredential_values_value" + }, + "description" : "Credential attributes", + "type" : "object" + }, + "witness" : { + "description" : "Witness for revocation proof", + "nullable" : true, + "properties" : { }, + "type" : "object" } - } - } - }, - "PresentationProposal" : { - "type" : "object", - "required" : [ "presentation_proposal" ], - "properties" : { - "@id" : { - "type" : "string", - "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", - "description" : "Message identifier" - }, - "@type" : { - "type" : "string", - "example" : "https://didcomm.org/my-family/1.0/my-message-type", - "description" : "Message type", - "readOnly" : true - }, - "comment" : { - "type" : "string", - "description" : "Human-readable comment", - "x-nullable" : true - }, - "presentation_proposal" : { - "$ref" : "#/definitions/IndyPresPreview" - } - } - }, - "PresentationRequest" : { - "type" : "object", - "required" : [ "request_presentations~attach" ], - "properties" : { - "@id" : { - "type" : "string", - "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", - "description" : "Message identifier" - }, - "@type" : { - "type" : "string", - "example" : "https://didcomm.org/my-family/1.0/my-message-type", - "description" : "Message type", - "readOnly" : true }, - "comment" : { - "type" : "string", - "description" : "Human-readable comment", - "x-nullable" : true - }, - "request_presentations~attach" : { - "type" : "array", - "items" : { - "$ref" : "#/definitions/AttachDecorator" - } - } - } - }, - "PublishRevocations" : { - "type" : "object", - "properties" : { - "rrid2crid" : { - "type" : "object", - "description" : "Credential revocation ids by revocation registry id", - "additionalProperties" : { - "type" : "array", - "items" : { - "type" : "string", - "example" : "12345", - "description" : "Credential revocation identifier", - "pattern" : "^[1-9][0-9]*$" - } + "required" : [ "cred_def_id", "schema_id", "signature", "signature_correctness_proof", "values" ], + "type" : "object" + }, + "IndyEQProof" : { + "properties" : { + "a_prime" : { + "example" : "0", + "pattern" : "^[0-9]*$", + "type" : "string" + }, + "e" : { + "example" : "0", + "pattern" : "^[0-9]*$", + "type" : "string" + }, + "m" : { + "additionalProperties" : { + "example" : "0", + "pattern" : "^[0-9]*$", + "type" : "string" + }, + "type" : "object" + }, + "m2" : { + "example" : "0", + "pattern" : "^[0-9]*$", + "type" : "string" + }, + "revealed_attrs" : { + "additionalProperties" : { + "example" : "-1", + "pattern" : "^-?[0-9]*$", + "type" : "string" + }, + "type" : "object" + }, + "v" : { + "example" : "0", + "pattern" : "^[0-9]*$", + "type" : "string" } - } - } - }, - "QueryResult" : { - "type" : "object", - "properties" : { - "results" : { - "type" : "object", - "description" : "Query results keyed by protocol", - "additionalProperties" : { - "type" : "object", - "description" : "Protocol descriptor", - "properties" : { } + }, + "type" : "object" + }, + "IndyGEProof" : { + "properties" : { + "alpha" : { + "example" : "0", + "pattern" : "^[0-9]*$", + "type" : "string" + }, + "mj" : { + "example" : "0", + "pattern" : "^[0-9]*$", + "type" : "string" + }, + "predicate" : { + "$ref" : "#/components/schemas/IndyGEProofPred" + }, + "r" : { + "additionalProperties" : { + "example" : "0", + "pattern" : "^[0-9]*$", + "type" : "string" + }, + "type" : "object" + }, + "t" : { + "additionalProperties" : { + "example" : "0", + "pattern" : "^[0-9]*$", + "type" : "string" + }, + "type" : "object" + }, + "u" : { + "additionalProperties" : { + "example" : "0", + "pattern" : "^[0-9]*$", + "type" : "string" + }, + "type" : "object" } - } - } - }, - "RawEncoded" : { - "type" : "object", - "properties" : { - "encoded" : { - "type" : "string", - "example" : "0", - "description" : "Encoded value", - "pattern" : "^[0-9]*$" }, - "raw" : { - "type" : "string", - "description" : "Raw value" - } - } - }, - "ReceiveInvitationRequest" : { - "type" : "object", - "properties" : { - "@id" : { - "type" : "string", - "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", - "description" : "Message identifier" - }, - "@type" : { - "type" : "string", - "example" : "https://didcomm.org/my-family/1.0/my-message-type", - "description" : "Message type", - "readOnly" : true - }, - "did" : { - "type" : "string", - "example" : "WgWxqztrNooG92RXvxSTWv", - "description" : "DID for connection invitation", - "pattern" : "^(did:sov:)?[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}$" - }, - "imageUrl" : { - "type" : "string", - "format" : "url", - "example" : "http://192.168.56.101/img/logo.jpg", - "description" : "Optional image URL for connection invitation", - "x-nullable" : true - }, - "label" : { - "type" : "string", - "example" : "Bob", - "description" : "Optional label for connection invitation" - }, - "recipientKeys" : { - "type" : "array", - "description" : "List of recipient keys", - "items" : { - "type" : "string", - "example" : "H3C2AVvLMv6gmMNam3uVAjZpfkcJCwDwnZn6z3wXmqPV", - "description" : "Recipient public key", - "pattern" : "^[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{43,44}$" + "type" : "object" + }, + "IndyGEProofPred" : { + "properties" : { + "attr_name" : { + "description" : "Attribute name, indy-canonicalized", + "type" : "string" + }, + "p_type" : { + "description" : "Predicate type", + "enum" : [ "LT", "LE", "GE", "GT" ], + "type" : "string" + }, + "value" : { + "description" : "Predicate threshold value", + "format" : "int32", + "type" : "integer" } }, - "routingKeys" : { - "type" : "array", - "description" : "List of routing keys", - "items" : { - "type" : "string", - "example" : "H3C2AVvLMv6gmMNam3uVAjZpfkcJCwDwnZn6z3wXmqPV", - "description" : "Routing key", - "pattern" : "^[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{43,44}$" + "type" : "object" + }, + "IndyKeyCorrectnessProof" : { + "properties" : { + "c" : { + "description" : "c in key correctness proof", + "example" : "0", + "pattern" : "^[0-9]*$", + "type" : "string" + }, + "xr_cap" : { + "description" : "xr_cap in key correctness proof", + "items" : { + "description" : "xr_cap components in key correctness proof", + "items" : { + "description" : "xr_cap component values in key correctness proof", + "type" : "string" + }, + "type" : "array" + }, + "type" : "array" + }, + "xz_cap" : { + "description" : "xz_cap in key correctness proof", + "example" : "0", + "pattern" : "^[0-9]*$", + "type" : "string" } }, - "serviceEndpoint" : { - "type" : "string", - "example" : "http://192.168.56.101:8020", - "description" : "Service endpoint at which to reach this agent" - } - } - }, - "RegisterLedgerNymResponse" : { - "type" : "object", - "properties" : { - "success" : { - "type" : "boolean", - "example" : true, - "description" : "Success of nym registration operation" - } - } - }, - "RemoveWalletRequest" : { - "type" : "object", - "properties" : { - "wallet_key" : { - "type" : "string", - "example" : "MySecretKey123", - "description" : "Master key used for key derivation. Only required for unmanaged wallets." - } - } - }, - "ResolutionResult" : { - "type" : "object", - "required" : [ "did_doc", "metadata" ], - "properties" : { - "did_doc" : { - "type" : "object", - "description" : "DID Document", - "properties" : { } + "required" : [ "c", "xr_cap", "xz_cap" ], + "type" : "object" + }, + "IndyNonRevocProof" : { + "properties" : { + "c_list" : { + "additionalProperties" : { + "type" : "string" + }, + "type" : "object" + }, + "x_list" : { + "additionalProperties" : { + "type" : "string" + }, + "type" : "object" + } }, - "metadata" : { - "type" : "object", - "description" : "Resolution metadata", - "properties" : { } - } - } - }, - "RevRegCreateRequest" : { - "type" : "object", - "properties" : { - "credential_definition_id" : { - "type" : "string", - "example" : "WgWxqztrNooG92RXvxSTWv:3:CL:20:tag", - "description" : "Credential definition identifier", - "pattern" : "^([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}):3:CL:(([1-9][0-9]*)|([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}:2:.+:[0-9.]+)):(.+)?$" - }, - "max_cred_num" : { - "type" : "integer", - "format" : "int32", - "example" : 1000, - "description" : "Revocation registry size", - "minimum" : 4, - "maximum" : 32768 - } - } - }, - "RevRegIssuedResult" : { - "type" : "object", - "properties" : { - "result" : { - "type" : "integer", - "format" : "int32", - "example" : 0, - "description" : "Number of credentials issued against revocation registry", - "minimum" : 0 - } - } - }, - "RevRegResult" : { - "type" : "object", - "properties" : { - "result" : { - "$ref" : "#/definitions/IssuerRevRegRecord" - } - } - }, - "RevRegUpdateTailsFileUri" : { - "type" : "object", - "required" : [ "tails_public_uri" ], - "properties" : { - "tails_public_uri" : { - "type" : "string", - "format" : "url", - "example" : "http://192.168.56.133:6543/revocation/registry/WgWxqztrNooG92RXvxSTWv:4:WgWxqztrNooG92RXvxSTWv:3:CL:20:tag:CL_ACCUM:0/tails-file", - "description" : "Public URI to the tails file" - } - } - }, - "RevRegsCreated" : { - "type" : "object", - "properties" : { - "rev_reg_ids" : { - "type" : "array", - "items" : { - "type" : "string", - "example" : "WgWxqztrNooG92RXvxSTWv:4:WgWxqztrNooG92RXvxSTWv:3:CL:20:tag:CL_ACCUM:0", - "description" : "Revocation registry identifiers", - "pattern" : "^([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}):4:([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}):3:CL:(([1-9][0-9]*)|([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}:2:.+:[0-9.]+))(:.+)?:CL_ACCUM:(.+$)" + "type" : "object" + }, + "IndyNonRevocationInterval" : { + "properties" : { + "from" : { + "description" : "Earliest time of interest in non-revocation interval", + "example" : 1640995199, + "format" : "int32", + "maximum" : 18446744073709551615, + "minimum" : 0, + "type" : "integer" + }, + "to" : { + "description" : "Latest time of interest in non-revocation interval", + "example" : 1640995199, + "format" : "int32", + "maximum" : 18446744073709551615, + "minimum" : 0, + "type" : "integer" } - } - } - }, - "RevocationModuleResponse" : { - "type" : "object" - }, - "RevokeRequest" : { - "type" : "object", - "properties" : { - "cred_ex_id" : { - "type" : "string", - "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", - "description" : "Credential exchange identifier", - "pattern" : "[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-4[a-fA-F0-9]{3}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}" }, - "cred_rev_id" : { - "type" : "string", - "example" : "12345", - "description" : "Credential revocation identifier", - "pattern" : "^[1-9][0-9]*$" + "type" : "object" + }, + "IndyPresAttrSpec" : { + "properties" : { + "cred_def_id" : { + "example" : "WgWxqztrNooG92RXvxSTWv:3:CL:20:tag", + "pattern" : "^([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}):3:CL:(([1-9][0-9]*)|([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}:2:.+:[0-9.]+)):(.+)?$", + "type" : "string" + }, + "mime-type" : { + "description" : "MIME type (default null)", + "example" : "image/jpeg", + "type" : "string" + }, + "name" : { + "description" : "Attribute name", + "example" : "favourite_drink", + "type" : "string" + }, + "referent" : { + "description" : "Credential referent", + "example" : "0", + "type" : "string" + }, + "value" : { + "description" : "Attribute value", + "example" : "martini", + "type" : "string" + } }, - "publish" : { - "type" : "boolean", - "description" : "(True) publish revocation to ledger immediately, or (default, False) mark it pending" + "required" : [ "name" ], + "type" : "object" + }, + "IndyPresPredSpec" : { + "properties" : { + "cred_def_id" : { + "description" : "Credential definition identifier", + "example" : "WgWxqztrNooG92RXvxSTWv:3:CL:20:tag", + "pattern" : "^([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}):3:CL:(([1-9][0-9]*)|([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}:2:.+:[0-9.]+)):(.+)?$", + "type" : "string" + }, + "name" : { + "description" : "Attribute name", + "example" : "high_score", + "type" : "string" + }, + "predicate" : { + "description" : "Predicate type ('<', '<=', '>=', or '>')", + "enum" : [ "<", "<=", ">=", ">" ], + "example" : ">=", + "type" : "string" + }, + "threshold" : { + "description" : "Threshold value", + "format" : "int32", + "type" : "integer" + } }, - "rev_reg_id" : { - "type" : "string", - "example" : "WgWxqztrNooG92RXvxSTWv:4:WgWxqztrNooG92RXvxSTWv:3:CL:20:tag:CL_ACCUM:0", - "description" : "Revocation registry identifier", - "pattern" : "^([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}):4:([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}):3:CL:(([1-9][0-9]*)|([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}:2:.+:[0-9.]+))(:.+)?:CL_ACCUM:(.+$)" - } - } - }, - "RouteRecord" : { - "type" : "object", - "required" : [ "recipient_key" ], - "properties" : { - "connection_id" : { - "type" : "string" + "required" : [ "name", "predicate", "threshold" ], + "type" : "object" + }, + "IndyPresPreview" : { + "properties" : { + "@type" : { + "description" : "Message type identifier", + "example" : "did:sov:BzCbsNYhMrjHiqZDTUASHg;spec/present-proof/1.0/presentation-preview", + "type" : "string" + }, + "attributes" : { + "items" : { + "$ref" : "#/components/schemas/IndyPresAttrSpec" + }, + "type" : "array" + }, + "predicates" : { + "items" : { + "$ref" : "#/components/schemas/IndyPresPredSpec" + }, + "type" : "array" + } }, - "created_at" : { - "type" : "string", - "example" : "2021-12-31 23:59:59Z", - "description" : "Time of record creation", - "pattern" : "^\\d{4}-\\d\\d-\\d\\d[T ]\\d\\d:\\d\\d(?:\\:(?:\\d\\d(?:\\.\\d{1,6})?))?(?:[+-]\\d\\d:?\\d\\d|Z|)$" + "required" : [ "attributes", "predicates" ], + "type" : "object" + }, + "IndyPresSpec" : { + "properties" : { + "requested_attributes" : { + "additionalProperties" : { + "$ref" : "#/components/schemas/IndyRequestedCredsRequestedAttr" + }, + "description" : "Nested object mapping proof request attribute referents to requested-attribute specifiers", + "type" : "object" + }, + "requested_predicates" : { + "additionalProperties" : { + "$ref" : "#/components/schemas/IndyRequestedCredsRequestedPred" + }, + "description" : "Nested object mapping proof request predicate referents to requested-predicate specifiers", + "type" : "object" + }, + "self_attested_attributes" : { + "additionalProperties" : { + "description" : "Self-attested attribute values to use in requested-credentials structure for proof construction", + "example" : "self_attested_value", + "type" : "string" + }, + "description" : "Self-attested attributes to build into proof", + "type" : "object" + }, + "trace" : { + "description" : "Whether to trace event (default false)", + "example" : false, + "type" : "boolean" + } }, - "recipient_key" : { - "type" : "string" + "required" : [ "requested_attributes", "requested_predicates", "self_attested_attributes" ], + "type" : "object" + }, + "IndyPrimaryProof" : { + "properties" : { + "eq_proof" : { + "$ref" : "#/components/schemas/IndyPrimaryProof_eq_proof" + }, + "ge_proofs" : { + "description" : "Indy GE proofs", + "items" : { + "$ref" : "#/components/schemas/IndyGEProof" + }, + "nullable" : true, + "type" : "array" + } }, - "record_id" : { - "type" : "string" + "type" : "object" + }, + "IndyProof" : { + "properties" : { + "identifiers" : { + "description" : "Indy proof.identifiers content", + "items" : { + "$ref" : "#/components/schemas/IndyProofIdentifier" + }, + "type" : "array" + }, + "proof" : { + "$ref" : "#/components/schemas/IndyProof_proof" + }, + "requested_proof" : { + "$ref" : "#/components/schemas/IndyProof_requested_proof" + } }, - "role" : { - "type" : "string" + "type" : "object" + }, + "IndyProofIdentifier" : { + "properties" : { + "cred_def_id" : { + "description" : "Credential definition identifier", + "example" : "WgWxqztrNooG92RXvxSTWv:3:CL:20:tag", + "pattern" : "^([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}):3:CL:(([1-9][0-9]*)|([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}:2:.+:[0-9.]+)):(.+)?$", + "type" : "string" + }, + "rev_reg_id" : { + "description" : "Revocation registry identifier", + "example" : "WgWxqztrNooG92RXvxSTWv:4:WgWxqztrNooG92RXvxSTWv:3:CL:20:tag:CL_ACCUM:0", + "nullable" : true, + "pattern" : "^([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}):4:([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}):3:CL:(([1-9][0-9]*)|([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}:2:.+:[0-9.]+))(:.+)?:CL_ACCUM:(.+$)", + "type" : "string" + }, + "schema_id" : { + "description" : "Schema identifier", + "example" : "WgWxqztrNooG92RXvxSTWv:2:schema_name:1.0", + "pattern" : "^[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}:2:.+:[0-9.]+$", + "type" : "string" + }, + "timestamp" : { + "description" : "Timestamp epoch", + "example" : 1640995199, + "format" : "int32", + "maximum" : 18446744073709551615, + "minimum" : 0, + "nullable" : true, + "type" : "integer" + } }, - "state" : { - "type" : "string", - "example" : "active", - "description" : "Current record state" + "type" : "object" + }, + "IndyProofProof" : { + "properties" : { + "aggregated_proof" : { + "$ref" : "#/components/schemas/IndyProofProof_aggregated_proof" + }, + "proofs" : { + "description" : "Indy proof proofs", + "items" : { + "$ref" : "#/components/schemas/IndyProofProofProofsProof" + }, + "type" : "array" + } }, - "updated_at" : { - "type" : "string", - "example" : "2021-12-31 23:59:59Z", - "description" : "Time of last record update", - "pattern" : "^\\d{4}-\\d\\d-\\d\\d[T ]\\d\\d:\\d\\d(?:\\:(?:\\d\\d(?:\\.\\d{1,6})?))?(?:[+-]\\d\\d:?\\d\\d|Z|)$" + "type" : "object" + }, + "IndyProofProofAggregatedProof" : { + "properties" : { + "c_hash" : { + "description" : "c_hash value", + "type" : "string" + }, + "c_list" : { + "description" : "c_list value", + "items" : { + "items" : { + "format" : "int32", + "type" : "integer" + }, + "type" : "array" + }, + "type" : "array" + } }, - "wallet_id" : { - "type" : "string" - } - } - }, - "Schema" : { - "type" : "object", - "properties" : { - "attrNames" : { - "type" : "array", - "description" : "Schema attribute names", - "items" : { - "type" : "string", - "example" : "score", - "description" : "Attribute name" + "type" : "object" + }, + "IndyProofProofProofsProof" : { + "properties" : { + "non_revoc_proof" : { + "$ref" : "#/components/schemas/IndyProofProofProofsProof_non_revoc_proof" + }, + "primary_proof" : { + "$ref" : "#/components/schemas/IndyProofProofProofsProof_primary_proof" } }, - "id" : { - "type" : "string", - "example" : "WgWxqztrNooG92RXvxSTWv:2:schema_name:1.0", - "description" : "Schema identifier", - "pattern" : "^[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}:2:.+:[0-9.]+$" - }, - "name" : { - "type" : "string", - "example" : "schema_name", - "description" : "Schema name" - }, - "seqNo" : { - "type" : "integer", - "format" : "int32", - "example" : 10, - "description" : "Schema sequence number", - "minimum" : 1 - }, - "ver" : { - "type" : "string", - "example" : "1.0", - "description" : "Node protocol version", - "pattern" : "^[0-9.]+$" - }, - "version" : { - "type" : "string", - "example" : "1.0", - "description" : "Schema version", - "pattern" : "^[0-9.]+$" - } - } - }, - "SchemaGetResult" : { - "type" : "object", - "properties" : { - "schema" : { - "$ref" : "#/definitions/Schema" - } - } - }, - "SchemaInputDescriptor" : { - "type" : "object", - "properties" : { - "required" : { - "type" : "boolean", - "description" : "Required" + "type" : "object" + }, + "IndyProofReqAttrSpec" : { + "properties" : { + "name" : { + "description" : "Attribute name", + "example" : "favouriteDrink", + "type" : "string" + }, + "names" : { + "description" : "Attribute name group", + "items" : { + "example" : "age", + "type" : "string" + }, + "type" : "array" + }, + "non_revoked" : { + "$ref" : "#/components/schemas/IndyProofReqAttrSpec_non_revoked" + }, + "restrictions" : { + "description" : "If present, credential must satisfy one of given restrictions: specify schema_id, schema_issuer_did, schema_name, schema_version, issuer_did, cred_def_id, and/or attr::::value where represents a credential attribute name", + "items" : { + "additionalProperties" : { + "example" : "WgWxqztrNooG92RXvxSTWv:3:CL:20:tag", + "type" : "string" + }, + "type" : "object" + }, + "type" : "array" + } }, - "uri" : { - "type" : "string", - "description" : "URI" - } - } - }, - "SchemaSendRequest" : { - "type" : "object", - "required" : [ "attributes", "schema_name", "schema_version" ], - "properties" : { - "attributes" : { - "type" : "array", - "description" : "List of schema attributes", - "items" : { - "type" : "string", - "example" : "score", - "description" : "attribute name" + "type" : "object" + }, + "IndyProofReqAttrSpecNonRevoked" : { + "properties" : { + "from" : { + "description" : "Earliest time of interest in non-revocation interval", + "example" : 1640995199, + "format" : "int32", + "maximum" : 18446744073709551615, + "minimum" : 0, + "type" : "integer" + }, + "to" : { + "description" : "Latest time of interest in non-revocation interval", + "example" : 1640995199, + "format" : "int32", + "maximum" : 18446744073709551615, + "minimum" : 0, + "type" : "integer" } }, - "schema_name" : { - "type" : "string", - "example" : "prefs", - "description" : "Schema name" + "type" : "object" + }, + "IndyProofReqPredSpec" : { + "properties" : { + "name" : { + "description" : "Attribute name", + "example" : "index", + "type" : "string" + }, + "non_revoked" : { + "$ref" : "#/components/schemas/IndyProofReqPredSpec_non_revoked" + }, + "p_type" : { + "description" : "Predicate type ('<', '<=', '>=', or '>')", + "enum" : [ "<", "<=", ">=", ">" ], + "example" : ">=", + "type" : "string" + }, + "p_value" : { + "description" : "Threshold value", + "format" : "int32", + "type" : "integer" + }, + "restrictions" : { + "description" : "If present, credential must satisfy one of given restrictions: specify schema_id, schema_issuer_did, schema_name, schema_version, issuer_did, cred_def_id, and/or attr::::value where represents a credential attribute name", + "items" : { + "additionalProperties" : { + "example" : "WgWxqztrNooG92RXvxSTWv:3:CL:20:tag", + "type" : "string" + }, + "type" : "object" + }, + "type" : "array" + } }, - "schema_version" : { - "type" : "string", - "example" : "1.0", - "description" : "Schema version", - "pattern" : "^[0-9.]+$" - } - } - }, - "SchemaSendResult" : { - "type" : "object", - "required" : [ "schema_id" ], - "properties" : { - "schema" : { - "$ref" : "#/definitions/SchemaSendResult_schema" + "required" : [ "name", "p_type", "p_value" ], + "type" : "object" + }, + "IndyProofReqPredSpecNonRevoked" : { + "properties" : { + "from" : { + "description" : "Earliest time of interest in non-revocation interval", + "example" : 1640995199, + "format" : "int32", + "maximum" : 18446744073709551615, + "minimum" : 0, + "type" : "integer" + }, + "to" : { + "description" : "Latest time of interest in non-revocation interval", + "example" : 1640995199, + "format" : "int32", + "maximum" : 18446744073709551615, + "minimum" : 0, + "type" : "integer" + } }, - "schema_id" : { - "type" : "string", - "example" : "WgWxqztrNooG92RXvxSTWv:2:schema_name:1.0", - "description" : "Schema identifier", - "pattern" : "^[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}:2:.+:[0-9.]+$" - } - } - }, - "SchemasCreatedResult" : { - "type" : "object", - "properties" : { - "schema_ids" : { - "type" : "array", - "items" : { - "type" : "string", - "example" : "WgWxqztrNooG92RXvxSTWv:2:schema_name:1.0", - "description" : "Schema identifiers", - "pattern" : "^[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}:2:.+:[0-9.]+$" + "type" : "object" + }, + "IndyProofRequest" : { + "properties" : { + "name" : { + "description" : "Proof request name", + "example" : "Proof request", + "type" : "string" + }, + "non_revoked" : { + "$ref" : "#/components/schemas/IndyProofRequest_non_revoked" + }, + "nonce" : { + "description" : "Nonce", + "example" : "1", + "pattern" : "^[1-9][0-9]*$", + "type" : "string" + }, + "requested_attributes" : { + "additionalProperties" : { + "$ref" : "#/components/schemas/IndyProofReqAttrSpec" + }, + "description" : "Requested attribute specifications of proof request", + "type" : "object" + }, + "requested_predicates" : { + "additionalProperties" : { + "$ref" : "#/components/schemas/IndyProofReqPredSpec" + }, + "description" : "Requested predicate specifications of proof request", + "type" : "object" + }, + "version" : { + "description" : "Proof request version", + "example" : "1.0", + "pattern" : "^[0-9.]+$", + "type" : "string" } - } - } - }, - "SendMenu" : { - "type" : "object", - "required" : [ "menu" ], - "properties" : { - "menu" : { - "$ref" : "#/definitions/SendMenu_menu" - } - } - }, - "SendMessage" : { - "type" : "object", - "properties" : { - "content" : { - "type" : "string", - "example" : "Hello", - "description" : "Message content" - } - } - }, - "SignRequest" : { - "type" : "object", - "required" : [ "doc", "verkey" ], - "properties" : { - "doc" : { - "$ref" : "#/definitions/Doc" - }, - "verkey" : { - "type" : "string", - "description" : "Verkey to use for signing" - } - } - }, - "SignResponse" : { - "type" : "object", - "properties" : { - "error" : { - "type" : "string", - "description" : "Error text" - }, - "signed_doc" : { - "type" : "object", - "description" : "Signed document", - "properties" : { } - } - } - }, - "SignatureOptions" : { - "type" : "object", - "required" : [ "proofPurpose", "verificationMethod" ], - "properties" : { - "challenge" : { - "type" : "string" - }, - "domain" : { - "type" : "string" }, - "proofPurpose" : { - "type" : "string" + "required" : [ "requested_attributes", "requested_predicates" ], + "type" : "object" + }, + "IndyProofRequestNonRevoked" : { + "properties" : { + "from" : { + "description" : "Earliest time of interest in non-revocation interval", + "example" : 1640995199, + "format" : "int32", + "maximum" : 18446744073709551615, + "minimum" : 0, + "type" : "integer" + }, + "to" : { + "description" : "Latest time of interest in non-revocation interval", + "example" : 1640995199, + "format" : "int32", + "maximum" : 18446744073709551615, + "minimum" : 0, + "type" : "integer" + } }, - "type" : { - "type" : "string" + "type" : "object" + }, + "IndyProofRequestedProof" : { + "properties" : { + "predicates" : { + "additionalProperties" : { + "$ref" : "#/components/schemas/IndyProofRequestedProofPredicate" + }, + "description" : "Proof requested proof predicates.", + "type" : "object" + }, + "revealed_attr_groups" : { + "additionalProperties" : { + "$ref" : "#/components/schemas/IndyProofRequestedProofRevealedAttrGroup" + }, + "description" : "Proof requested proof revealed attribute groups", + "nullable" : true, + "type" : "object" + }, + "revealed_attrs" : { + "additionalProperties" : { + "$ref" : "#/components/schemas/IndyProofRequestedProofRevealedAttr" + }, + "description" : "Proof requested proof revealed attributes", + "nullable" : true, + "type" : "object" + }, + "self_attested_attrs" : { + "description" : "Proof requested proof self-attested attributes", + "properties" : { }, + "type" : "object" + }, + "unrevealed_attrs" : { + "description" : "Unrevealed attributes", + "properties" : { }, + "type" : "object" + } }, - "verificationMethod" : { - "type" : "string" - } - } - }, - "SignedDoc" : { - "type" : "object", - "required" : [ "proof" ], - "properties" : { - "proof" : { - "$ref" : "#/definitions/SignedDoc_proof" - } - } - }, - "SubmissionRequirements" : { - "type" : "object", - "properties" : { - "count" : { - "type" : "integer", - "format" : "int32", - "example" : 1234, - "description" : "Count Value" - }, - "from" : { - "type" : "string", - "description" : "From" - }, - "from_nested" : { - "type" : "array", - "items" : { - "$ref" : "#/definitions/SubmissionRequirements" - } - }, - "max" : { - "type" : "integer", - "format" : "int32", - "example" : 1234, - "description" : "Max Value" - }, - "min" : { - "type" : "integer", - "format" : "int32", - "example" : 1234, - "description" : "Min Value" - }, - "name" : { - "type" : "string", - "description" : "Name" - }, - "purpose" : { - "type" : "string", - "description" : "Purpose" - }, - "rule" : { - "type" : "string", - "description" : "Selection", - "enum" : [ "all", "pick" ] - } - } - }, - "TAAAccept" : { - "type" : "object", - "properties" : { - "mechanism" : { - "type" : "string" + "type" : "object" + }, + "IndyProofRequestedProofPredicate" : { + "properties" : { + "sub_proof_index" : { + "description" : "Sub-proof index", + "format" : "int32", + "type" : "integer" + } }, - "text" : { - "type" : "string" + "type" : "object" + }, + "IndyProofRequestedProofRevealedAttr" : { + "properties" : { + "encoded" : { + "description" : "Encoded value", + "example" : "-1", + "pattern" : "^-?[0-9]*$", + "type" : "string" + }, + "raw" : { + "description" : "Raw value", + "type" : "string" + }, + "sub_proof_index" : { + "description" : "Sub-proof index", + "format" : "int32", + "type" : "integer" + } }, - "version" : { - "type" : "string" - } - } - }, - "TAAAcceptance" : { - "type" : "object", - "properties" : { - "mechanism" : { - "type" : "string" + "type" : "object" + }, + "IndyProofRequestedProofRevealedAttrGroup" : { + "properties" : { + "sub_proof_index" : { + "description" : "Sub-proof index", + "format" : "int32", + "type" : "integer" + }, + "values" : { + "additionalProperties" : { + "$ref" : "#/components/schemas/RawEncoded" + }, + "description" : "Indy proof requested proof revealed attr groups group value", + "type" : "object" + } }, - "time" : { - "type" : "integer", - "format" : "int32", - "example" : 1640995199, - "minimum" : 0, - "maximum" : 18446744073709551615 - } - } - }, - "TAAInfo" : { - "type" : "object", - "properties" : { - "aml_record" : { - "$ref" : "#/definitions/AMLRecord" + "type" : "object" + }, + "IndyRequestedCredsRequestedAttr" : { + "properties" : { + "cred_id" : { + "description" : "Wallet credential identifier (typically but not necessarily a UUID)", + "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "type" : "string" + }, + "revealed" : { + "description" : "Whether to reveal attribute in proof (default true)", + "type" : "boolean" + } }, - "taa_accepted" : { - "$ref" : "#/definitions/TAAAcceptance" + "required" : [ "cred_id" ], + "type" : "object" + }, + "IndyRequestedCredsRequestedPred" : { + "properties" : { + "cred_id" : { + "description" : "Wallet credential identifier (typically but not necessarily a UUID)", + "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "type" : "string" + }, + "timestamp" : { + "description" : "Epoch timestamp of interest for non-revocation proof", + "example" : 1640995199, + "format" : "int32", + "maximum" : 18446744073709551615, + "minimum" : 0, + "type" : "integer" + } }, - "taa_record" : { - "$ref" : "#/definitions/TAARecord" + "required" : [ "cred_id" ], + "type" : "object" + }, + "IndyRevRegDef" : { + "properties" : { + "credDefId" : { + "description" : "Credential definition identifier", + "example" : "WgWxqztrNooG92RXvxSTWv:3:CL:20:tag", + "pattern" : "^([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}):3:CL:(([1-9][0-9]*)|([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}:2:.+:[0-9.]+)):(.+)?$", + "type" : "string" + }, + "id" : { + "description" : "Indy revocation registry identifier", + "example" : "WgWxqztrNooG92RXvxSTWv:4:WgWxqztrNooG92RXvxSTWv:3:CL:20:tag:CL_ACCUM:0", + "pattern" : "^([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}):4:([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}):3:CL:(([1-9][0-9]*)|([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}:2:.+:[0-9.]+))(:.+)?:CL_ACCUM:(.+$)", + "type" : "string" + }, + "revocDefType" : { + "description" : "Revocation registry type (specify CL_ACCUM)", + "enum" : [ "CL_ACCUM" ], + "example" : "CL_ACCUM", + "type" : "string" + }, + "tag" : { + "description" : "Revocation registry tag", + "type" : "string" + }, + "value" : { + "$ref" : "#/components/schemas/IndyRevRegDef_value" + }, + "ver" : { + "description" : "Version of revocation registry definition", + "example" : "1.0", + "pattern" : "^[0-9.]+$", + "type" : "string" + } }, - "taa_required" : { - "type" : "boolean" - } - } - }, - "TAARecord" : { - "type" : "object", - "properties" : { - "digest" : { - "type" : "string" + "type" : "object" + }, + "IndyRevRegDefValue" : { + "properties" : { + "issuanceType" : { + "description" : "Issuance type", + "enum" : [ "ISSUANCE_ON_DEMAND", "ISSUANCE_BY_DEFAULT" ], + "type" : "string" + }, + "maxCredNum" : { + "description" : "Maximum number of credentials; registry size", + "example" : 10, + "format" : "int32", + "minimum" : 1, + "type" : "integer" + }, + "publicKeys" : { + "$ref" : "#/components/schemas/IndyRevRegDefValue_publicKeys" + }, + "tailsHash" : { + "description" : "Tails hash value", + "example" : "H3C2AVvLMv6gmMNam3uVAjZpfkcJCwDwnZn6z3wXmqPV", + "pattern" : "^[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{43,44}$", + "type" : "string" + }, + "tailsLocation" : { + "description" : "Tails file location", + "type" : "string" + } }, - "text" : { - "type" : "string" + "type" : "object" + }, + "IndyRevRegDefValuePublicKeys" : { + "properties" : { + "accumKey" : { + "$ref" : "#/components/schemas/IndyRevRegDefValuePublicKeysAccumKey" + } }, - "version" : { - "type" : "string" - } - } - }, - "TAAResult" : { - "type" : "object", - "properties" : { - "result" : { - "$ref" : "#/definitions/TAAInfo" - } - } - }, - "TransactionJobs" : { - "type" : "object", - "properties" : { - "transaction_my_job" : { - "type" : "string", - "description" : "My transaction related job", - "enum" : [ "TRANSACTION_AUTHOR", "TRANSACTION_ENDORSER", "reset" ] + "type" : "object" + }, + "IndyRevRegDefValuePublicKeysAccumKey" : { + "properties" : { + "z" : { + "description" : "Value for z", + "example" : "1 120F522F81E6B7 1 09F7A59005C4939854", + "type" : "string" + } }, - "transaction_their_job" : { - "type" : "string", - "description" : "Their transaction related job", - "enum" : [ "TRANSACTION_AUTHOR", "TRANSACTION_ENDORSER", "reset" ] - } - } - }, - "TransactionList" : { - "type" : "object", - "properties" : { - "results" : { - "type" : "array", - "description" : "List of transaction records", - "items" : { - "$ref" : "#/definitions/TransactionRecord" + "type" : "object" + }, + "IndyRevRegEntry" : { + "properties" : { + "value" : { + "$ref" : "#/components/schemas/IndyRevRegEntry_value" + }, + "ver" : { + "description" : "Version of revocation registry entry", + "example" : "1.0", + "pattern" : "^[0-9.]+$", + "type" : "string" } - } - } - }, - "TransactionRecord" : { - "type" : "object", - "properties" : { - "_type" : { - "type" : "string", - "example" : "101", - "description" : "Transaction type" - }, - "connection_id" : { - "type" : "string", - "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", - "description" : "The connection identifier for thie particular transaction record" - }, - "created_at" : { - "type" : "string", - "example" : "2021-12-31 23:59:59Z", - "description" : "Time of record creation", - "pattern" : "^\\d{4}-\\d\\d-\\d\\d[T ]\\d\\d:\\d\\d(?:\\:(?:\\d\\d(?:\\.\\d{1,6})?))?(?:[+-]\\d\\d:?\\d\\d|Z|)$" - }, - "endorser_write_txn" : { - "type" : "boolean", - "example" : true, - "description" : "If True, Endorser will write the transaction after endorsing it" - }, - "formats" : { - "type" : "array", - "items" : { - "type" : "object", - "example" : { - "attach_id" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", - "format" : "dif/endorse-transaction/request@v1.0" + }, + "type" : "object" + }, + "IndyRevRegEntryValue" : { + "properties" : { + "accum" : { + "description" : "Accumulator value", + "example" : "21 11792B036AED0AAA12A4 4 298B2571FFC63A737", + "type" : "string" + }, + "prevAccum" : { + "description" : "Previous accumulator value", + "example" : "21 137AC810975E4 6 76F0384B6F23", + "type" : "string" + }, + "revoked" : { + "description" : "Revoked credential revocation identifiers", + "items" : { + "format" : "int32", + "type" : "integer" }, - "additionalProperties" : { - "type" : "string" - } + "type" : "array" } }, - "messages_attach" : { - "type" : "array", - "items" : { - "type" : "object", - "example" : { - "@id" : "143c458d-1b1c-40c7-ab85-4d16808ddf0a", - "data" : { - "json" : "{\"endorser\": \"V4SGRU86Z58d6TV7PBUe6f\",\"identifier\": \"LjgpST2rjsoxYegQDRm7EL\",\"operation\": {\"data\": {\"attr_names\": [\"first_name\", \"last_name\"],\"name\": \"test_schema\",\"version\": \"2.1\",},\"type\": \"101\",},\"protocolVersion\": 2,\"reqId\": 1597766666168851000,\"signatures\": {\"LjgpST2rjsox\": \"4ATKMn6Y9sTgwqaGTm7py2c2M8x1EVDTWKZArwyuPgjU\"},\"taaAcceptance\": {\"mechanism\": \"manual\",\"taaDigest\": \"f50fe2c2ab977006761d36bd6f23e4c6a7e0fc2feb9f62\",\"time\": 1597708800,}}" - }, - "mime-type" : "application/json" + "type" : "object" + }, + "InputDescriptors" : { + "properties" : { + "constraints" : { + "$ref" : "#/components/schemas/Constraints" + }, + "group" : { + "items" : { + "description" : "Group", + "type" : "string" }, - "properties" : { } + "type" : "array" + }, + "id" : { + "description" : "ID", + "type" : "string" + }, + "metadata" : { + "description" : "Metadata dictionary", + "properties" : { }, + "type" : "object" + }, + "name" : { + "description" : "Name", + "type" : "string" + }, + "purpose" : { + "description" : "Purpose", + "type" : "string" + }, + "schema" : { + "$ref" : "#/components/schemas/InputDescriptors_schema" } }, - "signature_request" : { - "type" : "array", - "items" : { - "type" : "object", - "example" : { - "author_goal_code" : "transaction.ledger.write", - "context" : "did:sov", - "method" : "add-signature", - "signature_type" : "", - "signer_goal_code" : "transaction.endorse" + "type" : "object" + }, + "IntroModuleResponse" : { + "type" : "object" + }, + "InvitationCreateRequest" : { + "properties" : { + "accept" : { + "description" : "List of mime type in order of preference that should be use in responding to the message", + "example" : [ "didcomm/aip1", "didcomm/aip2;env=rfc19" ], + "items" : { + "type" : "string" }, - "properties" : { } + "type" : "array" + }, + "alias" : { + "description" : "Alias for connection", + "example" : "Barry", + "type" : "string" + }, + "attachments" : { + "description" : "Optional invitation attachments", + "items" : { + "$ref" : "#/components/schemas/AttachmentDef" + }, + "type" : "array" + }, + "handshake_protocols" : { + "items" : { + "description" : "Handshake protocol to specify in invitation", + "example" : "did:sov:BzCbsNYhMrjHiqZDTUASHg;spec/didexchange/1.0", + "type" : "string" + }, + "type" : "array" + }, + "mediation_id" : { + "description" : "Identifier for active mediation record to be used", + "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "pattern" : "[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-4[a-fA-F0-9]{3}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}", + "type" : "string" + }, + "metadata" : { + "description" : "Optional metadata to attach to the connection created with the invitation", + "properties" : { }, + "type" : "object" + }, + "my_label" : { + "description" : "Label for connection invitation", + "example" : "Invitation to Barry", + "type" : "string" + }, + "protocol_version" : { + "description" : "OOB protocol version", + "example" : "1.1", + "type" : "string" + }, + "use_public_did" : { + "description" : "Whether to use public DID in invitation", + "example" : false, + "type" : "boolean" } }, - "signature_response" : { - "type" : "array", - "items" : { - "type" : "object", - "example" : { - "context" : "did:sov", - "message_id" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", - "method" : "add-signature", - "signer_goal_code" : "transaction.refuse" + "type" : "object" + }, + "InvitationMessage" : { + "properties" : { + "@id" : { + "description" : "Message identifier", + "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "type" : "string" + }, + "@type" : { + "description" : "Message type", + "example" : "https://didcomm.org/my-family/1.0/my-message-type", + "type" : "string" + }, + "accept" : { + "description" : "List of mime type in order of preference", + "example" : [ "didcomm/aip1", "didcomm/aip2;env=rfc19" ], + "items" : { + "type" : "string" }, - "properties" : { } + "type" : "array" + }, + "handshake_protocols" : { + "items" : { + "description" : "Handshake protocol", + "example" : "did:sov:BzCbsNYhMrjHiqZDTUASHg;spec/didexchange/1.0", + "type" : "string" + }, + "type" : "array" + }, + "imageUrl" : { + "description" : "Optional image URL for out-of-band invitation", + "example" : "http://192.168.56.101/img/logo.jpg", + "format" : "url", + "nullable" : true, + "type" : "string" + }, + "label" : { + "description" : "Optional label", + "example" : "Bob", + "type" : "string" + }, + "requests~attach" : { + "description" : "Optional request attachment", + "items" : { + "$ref" : "#/components/schemas/AttachDecorator" + }, + "type" : "array" + }, + "services" : { + "example" : [ { + "did" : "WgWxqztrNooG92RXvxSTWv", + "id" : "string", + "recipientKeys" : [ "did:key:z6MkpTHR8VNsBxYAAWHut2Geadd9jSwuBV8xRoAnwWsdvktH" ], + "routingKeys" : [ "did:key:z6MkpTHR8VNsBxYAAWHut2Geadd9jSwuBV8xRoAnwWsdvktH" ], + "serviceEndpoint" : "http://192.168.56.101:8020", + "type" : "string" + }, "did:sov:WgWxqztrNooG92RXvxSTWv" ], + "items" : { + "description" : "Either a DIDComm service object (as per RFC0067) or a DID string.", + "type" : "object" + }, + "type" : "array" } }, - "state" : { - "type" : "string", - "example" : "active", - "description" : "Current record state" - }, - "thread_id" : { - "type" : "string", - "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", - "description" : "Thread Identifier" - }, - "timing" : { - "type" : "object", - "example" : { - "expires_time" : "2020-12-13T17:29:06+0000" + "type" : "object" + }, + "InvitationRecord" : { + "properties" : { + "created_at" : { + "description" : "Time of record creation", + "example" : "2021-12-31T23:59:59Z", + "pattern" : "^\\d{4}-\\d\\d-\\d\\d[T ]\\d\\d:\\d\\d(?:\\:(?:\\d\\d(?:\\.\\d{1,6})?))?(?:[+-]\\d\\d:?\\d\\d|Z|)$", + "type" : "string" }, - "properties" : { } - }, - "trace" : { - "type" : "boolean", - "description" : "Record trace information, based on agent configuration" - }, - "transaction_id" : { - "type" : "string", - "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", - "description" : "Transaction identifier" - }, - "updated_at" : { - "type" : "string", - "example" : "2021-12-31 23:59:59Z", - "description" : "Time of last record update", - "pattern" : "^\\d{4}-\\d\\d-\\d\\d[T ]\\d\\d:\\d\\d(?:\\:(?:\\d\\d(?:\\.\\d{1,6})?))?(?:[+-]\\d\\d:?\\d\\d|Z|)$" - } - } - }, - "TxnOrCredentialDefinitionSendResult" : { - "type" : "object", - "properties" : { - "sent" : { - "$ref" : "#/definitions/CredentialDefinitionSendResult" - }, - "txn" : { - "$ref" : "#/definitions/TxnOrCredentialDefinitionSendResult_txn" - } - } - }, - "TxnOrPublishRevocationsResult" : { - "type" : "object", - "properties" : { - "sent" : { - "$ref" : "#/definitions/PublishRevocations" + "invi_msg_id" : { + "description" : "Invitation message identifier", + "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "type" : "string" + }, + "invitation" : { + "$ref" : "#/components/schemas/InvitationRecord_invitation" + }, + "invitation_id" : { + "description" : "Invitation record identifier", + "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "type" : "string" + }, + "invitation_url" : { + "description" : "Invitation message URL", + "example" : "https://example.com/endpoint?c_i=eyJAdHlwZSI6ICIuLi4iLCAiLi4uIjogIi4uLiJ9XX0=", + "type" : "string" + }, + "oob_id" : { + "description" : "Out of band record identifier", + "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "type" : "string" + }, + "state" : { + "description" : "Out of band message exchange state", + "example" : "await_response", + "type" : "string" + }, + "trace" : { + "description" : "Record trace information, based on agent configuration", + "type" : "boolean" + }, + "updated_at" : { + "description" : "Time of last record update", + "example" : "2021-12-31T23:59:59Z", + "pattern" : "^\\d{4}-\\d\\d-\\d\\d[T ]\\d\\d:\\d\\d(?:\\:(?:\\d\\d(?:\\.\\d{1,6})?))?(?:[+-]\\d\\d:?\\d\\d|Z|)$", + "type" : "string" + } }, - "txn" : { - "$ref" : "#/definitions/TxnOrPublishRevocationsResult_txn" - } - } - }, - "TxnOrRevRegResult" : { - "type" : "object", - "properties" : { - "sent" : { - "$ref" : "#/definitions/RevRegResult" + "type" : "object" + }, + "InvitationResult" : { + "properties" : { + "connection_id" : { + "description" : "Connection identifier", + "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "type" : "string" + }, + "invitation" : { + "$ref" : "#/components/schemas/ConnectionInvitation" + }, + "invitation_url" : { + "description" : "Invitation URL", + "example" : "http://192.168.56.101:8020/invite?c_i=eyJAdHlwZSI6Li4ufQ==", + "type" : "string" + } }, - "txn" : { - "$ref" : "#/definitions/TxnOrRevRegResult_txn" - } - } - }, - "TxnOrSchemaSendResult" : { - "type" : "object", - "properties" : { - "sent" : { - "$ref" : "#/definitions/TxnOrSchemaSendResult_sent" - }, - "txn" : { - "$ref" : "#/definitions/TxnOrSchemaSendResult_txn" - } - } - }, - "UpdateWalletRequest" : { - "type" : "object", - "properties" : { - "image_url" : { - "type" : "string", - "example" : "https://aries.ca/images/sample.png", - "description" : "Image url for this wallet. This image url is publicized (self-attested) to other agents as part of forming a connection." - }, - "label" : { - "type" : "string", - "example" : "Alice", - "description" : "Label for this wallet. This label is publicized (self-attested) to other agents as part of forming a connection." - }, - "wallet_dispatch_type" : { - "type" : "string", - "example" : "default", - "description" : "Webhook target dispatch type for this wallet. default - Dispatch only to webhooks associated with this wallet. base - Dispatch only to webhooks associated with the base wallet. both - Dispatch to both webhook targets.", - "enum" : [ "default", "both", "base" ] - }, - "wallet_webhook_urls" : { - "type" : "array", - "description" : "List of Webhook URLs associated with this subwallet", - "items" : { - "type" : "string", - "example" : "http://localhost:8022/webhooks", - "description" : "Optional webhook URL to receive webhook messages" + "type" : "object" + }, + "IssueCredentialModuleResponse" : { + "type" : "object" + }, + "IssuerCredRevRecord" : { + "properties" : { + "created_at" : { + "description" : "Time of record creation", + "example" : "2021-12-31T23:59:59Z", + "pattern" : "^\\d{4}-\\d\\d-\\d\\d[T ]\\d\\d:\\d\\d(?:\\:(?:\\d\\d(?:\\.\\d{1,6})?))?(?:[+-]\\d\\d:?\\d\\d|Z|)$", + "type" : "string" + }, + "cred_def_id" : { + "description" : "Credential definition identifier", + "example" : "WgWxqztrNooG92RXvxSTWv:3:CL:20:tag", + "pattern" : "^([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}):3:CL:(([1-9][0-9]*)|([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}:2:.+:[0-9.]+)):(.+)?$", + "type" : "string" + }, + "cred_ex_id" : { + "description" : "Credential exchange record identifier at credential issue", + "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "type" : "string" + }, + "cred_ex_version" : { + "description" : "Credential exchange version", + "type" : "string" + }, + "cred_rev_id" : { + "description" : "Credential revocation identifier", + "example" : "12345", + "pattern" : "^[1-9][0-9]*$", + "type" : "string" + }, + "record_id" : { + "description" : "Issuer credential revocation record identifier", + "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "type" : "string" + }, + "rev_reg_id" : { + "description" : "Revocation registry identifier", + "example" : "WgWxqztrNooG92RXvxSTWv:4:WgWxqztrNooG92RXvxSTWv:3:CL:20:tag:CL_ACCUM:0", + "pattern" : "^([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}):4:([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}):3:CL:(([1-9][0-9]*)|([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}:2:.+:[0-9.]+))(:.+)?:CL_ACCUM:(.+$)", + "type" : "string" + }, + "state" : { + "description" : "Issue credential revocation record state", + "example" : "issued", + "type" : "string" + }, + "updated_at" : { + "description" : "Time of last record update", + "example" : "2021-12-31T23:59:59Z", + "pattern" : "^\\d{4}-\\d\\d-\\d\\d[T ]\\d\\d:\\d\\d(?:\\:(?:\\d\\d(?:\\.\\d{1,6})?))?(?:[+-]\\d\\d:?\\d\\d|Z|)$", + "type" : "string" } - } - } - }, - "V10CredentialBoundOfferRequest" : { - "type" : "object", - "properties" : { - "counter_proposal" : { - "$ref" : "#/definitions/V10CredentialBoundOfferRequest_counter_proposal" - } - } - }, - "V10CredentialConnFreeOfferRequest" : { - "type" : "object", - "required" : [ "cred_def_id", "credential_preview" ], - "properties" : { - "auto_issue" : { - "type" : "boolean", - "description" : "Whether to respond automatically to credential requests, creating and issuing requested credentials" - }, - "auto_remove" : { - "type" : "boolean", - "description" : "Whether to remove the credential exchange record on completion (overrides --preserve-exchange-records configuration setting)" - }, - "comment" : { - "type" : "string", - "description" : "Human-readable comment", - "x-nullable" : true - }, - "cred_def_id" : { - "type" : "string", - "example" : "WgWxqztrNooG92RXvxSTWv:3:CL:20:tag", - "description" : "Credential definition identifier", - "pattern" : "^([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}):3:CL:(([1-9][0-9]*)|([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}:2:.+:[0-9.]+)):(.+)?$" - }, - "credential_preview" : { - "$ref" : "#/definitions/CredentialPreview" - }, - "trace" : { - "type" : "boolean", - "description" : "Record trace information, based on agent configuration" - } - } - }, - "V10CredentialCreate" : { - "type" : "object", - "required" : [ "credential_proposal" ], - "properties" : { - "auto_remove" : { - "type" : "boolean", - "description" : "Whether to remove the credential exchange record on completion (overrides --preserve-exchange-records configuration setting)" - }, - "comment" : { - "type" : "string", - "description" : "Human-readable comment", - "x-nullable" : true - }, - "cred_def_id" : { - "type" : "string", - "example" : "WgWxqztrNooG92RXvxSTWv:3:CL:20:tag", - "description" : "Credential definition identifier", - "pattern" : "^([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}):3:CL:(([1-9][0-9]*)|([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}:2:.+:[0-9.]+)):(.+)?$" - }, - "credential_proposal" : { - "$ref" : "#/definitions/CredentialPreview" - }, - "issuer_did" : { - "type" : "string", - "example" : "WgWxqztrNooG92RXvxSTWv", - "description" : "Credential issuer DID", - "pattern" : "^(did:sov:)?[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}$" }, - "schema_id" : { - "type" : "string", - "example" : "WgWxqztrNooG92RXvxSTWv:2:schema_name:1.0", - "description" : "Schema identifier", - "pattern" : "^[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}:2:.+:[0-9.]+$" - }, - "schema_issuer_did" : { - "type" : "string", - "example" : "WgWxqztrNooG92RXvxSTWv", - "description" : "Schema issuer DID", - "pattern" : "^(did:sov:)?[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}$" - }, - "schema_name" : { - "type" : "string", - "example" : "preferences", - "description" : "Schema name" + "type" : "object" + }, + "IssuerRevRegRecord" : { + "properties" : { + "created_at" : { + "description" : "Time of record creation", + "example" : "2021-12-31T23:59:59Z", + "pattern" : "^\\d{4}-\\d\\d-\\d\\d[T ]\\d\\d:\\d\\d(?:\\:(?:\\d\\d(?:\\.\\d{1,6})?))?(?:[+-]\\d\\d:?\\d\\d|Z|)$", + "type" : "string" + }, + "cred_def_id" : { + "description" : "Credential definition identifier", + "example" : "WgWxqztrNooG92RXvxSTWv:3:CL:20:tag", + "pattern" : "^([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}):3:CL:(([1-9][0-9]*)|([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}:2:.+:[0-9.]+)):(.+)?$", + "type" : "string" + }, + "error_msg" : { + "description" : "Error message", + "example" : "Revocation registry undefined", + "type" : "string" + }, + "issuer_did" : { + "description" : "Issuer DID", + "example" : "WgWxqztrNooG92RXvxSTWv", + "pattern" : "^(did:sov:)?[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}$", + "type" : "string" + }, + "max_cred_num" : { + "description" : "Maximum number of credentials for revocation registry", + "example" : 1000, + "format" : "int32", + "type" : "integer" + }, + "pending_pub" : { + "description" : "Credential revocation identifier for credential revoked and pending publication to ledger", + "items" : { + "example" : "23", + "type" : "string" + }, + "type" : "array" + }, + "record_id" : { + "description" : "Issuer revocation registry record identifier", + "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "type" : "string" + }, + "revoc_def_type" : { + "description" : "Revocation registry type (specify CL_ACCUM)", + "enum" : [ "CL_ACCUM" ], + "example" : "CL_ACCUM", + "type" : "string" + }, + "revoc_reg_def" : { + "$ref" : "#/components/schemas/IssuerRevRegRecord_revoc_reg_def" + }, + "revoc_reg_entry" : { + "$ref" : "#/components/schemas/IssuerRevRegRecord_revoc_reg_entry" + }, + "revoc_reg_id" : { + "description" : "Revocation registry identifier", + "example" : "WgWxqztrNooG92RXvxSTWv:4:WgWxqztrNooG92RXvxSTWv:3:CL:20:tag:CL_ACCUM:0", + "pattern" : "^([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}):4:([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}):3:CL:(([1-9][0-9]*)|([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}:2:.+:[0-9.]+))(:.+)?:CL_ACCUM:(.+$)", + "type" : "string" + }, + "state" : { + "description" : "Issue revocation registry record state", + "example" : "active", + "type" : "string" + }, + "tag" : { + "description" : "Tag within issuer revocation registry identifier", + "type" : "string" + }, + "tails_hash" : { + "description" : "Tails hash", + "example" : "H3C2AVvLMv6gmMNam3uVAjZpfkcJCwDwnZn6z3wXmqPV", + "pattern" : "^[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{43,44}$", + "type" : "string" + }, + "tails_local_path" : { + "description" : "Local path to tails file", + "type" : "string" + }, + "tails_public_uri" : { + "description" : "Public URI for tails file", + "type" : "string" + }, + "updated_at" : { + "description" : "Time of last record update", + "example" : "2021-12-31T23:59:59Z", + "pattern" : "^\\d{4}-\\d\\d-\\d\\d[T ]\\d\\d:\\d\\d(?:\\:(?:\\d\\d(?:\\.\\d{1,6})?))?(?:[+-]\\d\\d:?\\d\\d|Z|)$", + "type" : "string" + } }, - "schema_version" : { - "type" : "string", - "example" : "1.0", - "description" : "Schema version", - "pattern" : "^[0-9.]+$" + "type" : "object" + }, + "Keylist" : { + "properties" : { + "results" : { + "description" : "List of keylist records", + "items" : { + "$ref" : "#/components/schemas/RouteRecord" + }, + "type" : "array" + } }, - "trace" : { - "type" : "boolean", - "description" : "Record trace information, based on agent configuration" - } - } - }, - "V10CredentialExchange" : { - "type" : "object", - "properties" : { - "auto_issue" : { - "type" : "boolean", - "example" : false, - "description" : "Issuer choice to issue to request in this credential exchange" - }, - "auto_offer" : { - "type" : "boolean", - "example" : false, - "description" : "Holder choice to accept offer in this credential exchange" - }, - "auto_remove" : { - "type" : "boolean", - "example" : false, - "description" : "Issuer choice to remove this credential exchange record when complete" - }, - "connection_id" : { - "type" : "string", - "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", - "description" : "Connection identifier" - }, - "created_at" : { - "type" : "string", - "example" : "2021-12-31 23:59:59Z", - "description" : "Time of record creation", - "pattern" : "^\\d{4}-\\d\\d-\\d\\d[T ]\\d\\d:\\d\\d(?:\\:(?:\\d\\d(?:\\.\\d{1,6})?))?(?:[+-]\\d\\d:?\\d\\d|Z|)$" - }, - "credential" : { - "$ref" : "#/definitions/V10CredentialExchange_credential" - }, - "credential_definition_id" : { - "type" : "string", - "example" : "WgWxqztrNooG92RXvxSTWv:3:CL:20:tag", - "description" : "Credential definition identifier", - "pattern" : "^([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}):3:CL:(([1-9][0-9]*)|([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}:2:.+:[0-9.]+)):(.+)?$" - }, - "credential_exchange_id" : { - "type" : "string", - "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", - "description" : "Credential exchange identifier" - }, - "credential_id" : { - "type" : "string", - "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", - "description" : "Credential identifier" - }, - "credential_offer" : { - "$ref" : "#/definitions/V10CredentialExchange_credential_offer" - }, - "credential_offer_dict" : { - "$ref" : "#/definitions/V10CredentialExchange_credential_offer_dict" - }, - "credential_proposal_dict" : { - "$ref" : "#/definitions/V10CredentialExchange_credential_proposal_dict" - }, - "credential_request" : { - "$ref" : "#/definitions/V10CredentialExchange_credential_request" - }, - "credential_request_metadata" : { - "type" : "object", - "description" : "(Indy) credential request metadata", - "properties" : { } - }, - "error_msg" : { - "type" : "string", - "example" : "Credential definition identifier is not set in proposal", - "description" : "Error message" - }, - "initiator" : { - "type" : "string", - "example" : "self", - "description" : "Issue-credential exchange initiator: self or external", - "enum" : [ "self", "external" ] - }, - "parent_thread_id" : { - "type" : "string", - "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", - "description" : "Parent thread identifier" - }, - "raw_credential" : { - "$ref" : "#/definitions/V10CredentialExchange_raw_credential" - }, - "revoc_reg_id" : { - "type" : "string", - "description" : "Revocation registry identifier" - }, - "revocation_id" : { - "type" : "string", - "description" : "Credential identifier within revocation registry" - }, - "role" : { - "type" : "string", - "example" : "issuer", - "description" : "Issue-credential exchange role: holder or issuer", - "enum" : [ "holder", "issuer" ] - }, - "schema_id" : { - "type" : "string", - "example" : "WgWxqztrNooG92RXvxSTWv:2:schema_name:1.0", - "description" : "Schema identifier", - "pattern" : "^[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}:2:.+:[0-9.]+$" - }, - "state" : { - "type" : "string", - "example" : "credential_acked", - "description" : "Issue-credential exchange state" - }, - "thread_id" : { - "type" : "string", - "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", - "description" : "Thread identifier" - }, - "trace" : { - "type" : "boolean", - "description" : "Record trace information, based on agent configuration" - }, - "updated_at" : { - "type" : "string", - "example" : "2021-12-31 23:59:59Z", - "description" : "Time of last record update", - "pattern" : "^\\d{4}-\\d\\d-\\d\\d[T ]\\d\\d:\\d\\d(?:\\:(?:\\d\\d(?:\\.\\d{1,6})?))?(?:[+-]\\d\\d:?\\d\\d|Z|)$" - } - } - }, - "V10CredentialExchangeListResult" : { - "type" : "object", - "properties" : { - "results" : { - "type" : "array", - "description" : "Aries#0036 v1.0 credential exchange records", - "items" : { - "$ref" : "#/definitions/V10CredentialExchange" + "type" : "object" + }, + "KeylistQuery" : { + "properties" : { + "@id" : { + "description" : "Message identifier", + "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "type" : "string" + }, + "@type" : { + "description" : "Message type", + "example" : "https://didcomm.org/my-family/1.0/my-message-type", + "readOnly" : true, + "type" : "string" + }, + "filter" : { + "description" : "Query dictionary object", + "example" : { + "filter" : { } + }, + "properties" : { }, + "type" : "object" + }, + "paginate" : { + "$ref" : "#/components/schemas/KeylistQuery_paginate" } - } - } - }, - "V10CredentialFreeOfferRequest" : { - "type" : "object", - "required" : [ "connection_id", "cred_def_id", "credential_preview" ], - "properties" : { - "auto_issue" : { - "type" : "boolean", - "description" : "Whether to respond automatically to credential requests, creating and issuing requested credentials" - }, - "auto_remove" : { - "type" : "boolean", - "description" : "Whether to remove the credential exchange record on completion (overrides --preserve-exchange-records configuration setting)" - }, - "comment" : { - "type" : "string", - "description" : "Human-readable comment", - "x-nullable" : true - }, - "connection_id" : { - "type" : "string", - "format" : "uuid", - "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", - "description" : "Connection identifier" - }, - "cred_def_id" : { - "type" : "string", - "example" : "WgWxqztrNooG92RXvxSTWv:3:CL:20:tag", - "description" : "Credential definition identifier", - "pattern" : "^([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}):3:CL:(([1-9][0-9]*)|([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}:2:.+:[0-9.]+)):(.+)?$" }, - "credential_preview" : { - "$ref" : "#/definitions/CredentialPreview" + "type" : "object" + }, + "KeylistQueryFilterRequest" : { + "properties" : { + "filter" : { + "description" : "Filter for keylist query", + "properties" : { }, + "type" : "object" + } }, - "trace" : { - "type" : "boolean", - "description" : "Record trace information, based on agent configuration" - } - } - }, - "V10CredentialIssueRequest" : { - "type" : "object", - "properties" : { - "comment" : { - "type" : "string", - "description" : "Human-readable comment", - "x-nullable" : true - } - } - }, - "V10CredentialProblemReportRequest" : { - "type" : "object", - "required" : [ "description" ], - "properties" : { - "description" : { - "type" : "string" - } - } - }, - "V10CredentialProposalRequestMand" : { - "type" : "object", - "required" : [ "connection_id", "credential_proposal" ], - "properties" : { - "auto_remove" : { - "type" : "boolean", - "description" : "Whether to remove the credential exchange record on completion (overrides --preserve-exchange-records configuration setting)" - }, - "comment" : { - "type" : "string", - "description" : "Human-readable comment", - "x-nullable" : true - }, - "connection_id" : { - "type" : "string", - "format" : "uuid", - "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", - "description" : "Connection identifier" - }, - "cred_def_id" : { - "type" : "string", - "example" : "WgWxqztrNooG92RXvxSTWv:3:CL:20:tag", - "description" : "Credential definition identifier", - "pattern" : "^([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}):3:CL:(([1-9][0-9]*)|([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}:2:.+:[0-9.]+)):(.+)?$" + "type" : "object" + }, + "KeylistQueryPaginate" : { + "properties" : { + "limit" : { + "description" : "Limit for keylist query", + "example" : 30, + "format" : "int32", + "type" : "integer" + }, + "offset" : { + "description" : "Offset value for query", + "example" : 0, + "format" : "int32", + "type" : "integer" + } }, - "credential_proposal" : { - "$ref" : "#/definitions/CredentialPreview" + "type" : "object" + }, + "KeylistUpdate" : { + "properties" : { + "@id" : { + "description" : "Message identifier", + "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "type" : "string" + }, + "@type" : { + "description" : "Message type", + "example" : "https://didcomm.org/my-family/1.0/my-message-type", + "readOnly" : true, + "type" : "string" + }, + "updates" : { + "description" : "List of update rules", + "items" : { + "$ref" : "#/components/schemas/KeylistUpdateRule" + }, + "type" : "array" + } }, - "issuer_did" : { - "type" : "string", - "example" : "WgWxqztrNooG92RXvxSTWv", - "description" : "Credential issuer DID", - "pattern" : "^(did:sov:)?[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}$" + "type" : "object" + }, + "KeylistUpdateRequest" : { + "properties" : { + "updates" : { + "items" : { + "$ref" : "#/components/schemas/KeylistUpdateRule" + }, + "type" : "array" + } }, - "schema_id" : { - "type" : "string", - "example" : "WgWxqztrNooG92RXvxSTWv:2:schema_name:1.0", - "description" : "Schema identifier", - "pattern" : "^[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}:2:.+:[0-9.]+$" + "type" : "object" + }, + "KeylistUpdateRule" : { + "properties" : { + "action" : { + "description" : "Action for specific key", + "enum" : [ "add", "remove" ], + "example" : "add", + "type" : "string" + }, + "recipient_key" : { + "description" : "Key to remove or add", + "example" : "did:key:z6MkpTHR8VNsBxYAAWHut2Geadd9jSwuBV8xRoAnwWsdvktH", + "pattern" : "^did:key:z[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]+$|^[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{43,44}$", + "type" : "string" + } }, - "schema_issuer_did" : { - "type" : "string", - "example" : "WgWxqztrNooG92RXvxSTWv", - "description" : "Schema issuer DID", - "pattern" : "^(did:sov:)?[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}$" + "required" : [ "action", "recipient_key" ], + "type" : "object" + }, + "LDProofVCDetail" : { + "properties" : { + "credential" : { + "$ref" : "#/components/schemas/LDProofVCDetail_credential" + }, + "options" : { + "$ref" : "#/components/schemas/LDProofVCDetail_options" + } }, - "schema_name" : { - "type" : "string", - "example" : "preferences", - "description" : "Schema name" + "required" : [ "credential", "options" ], + "type" : "object" + }, + "LDProofVCDetailOptions" : { + "properties" : { + "challenge" : { + "description" : "A challenge to include in the proof. SHOULD be provided by the requesting party of the credential (=holder)", + "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "type" : "string" + }, + "created" : { + "description" : "The date and time of the proof (with a maximum accuracy in seconds). Defaults to current system time", + "example" : "2021-12-31T23:59:59Z", + "pattern" : "^\\d{4}-\\d\\d-\\d\\d[T ]\\d\\d:\\d\\d(?:\\:(?:\\d\\d(?:\\.\\d{1,6})?))?(?:[+-]\\d\\d:?\\d\\d|Z|)$", + "type" : "string" + }, + "credentialStatus" : { + "$ref" : "#/components/schemas/LDProofVCDetailOptions_credentialStatus" + }, + "domain" : { + "description" : "The intended domain of validity for the proof", + "example" : "example.com", + "type" : "string" + }, + "proofPurpose" : { + "description" : "The proof purpose used for the proof. Should match proof purposes registered in the Linked Data Proofs Specification", + "example" : "assertionMethod", + "type" : "string" + }, + "proofType" : { + "description" : "The proof type used for the proof. Should match suites registered in the Linked Data Cryptographic Suite Registry", + "example" : "Ed25519Signature2018", + "type" : "string" + } }, - "schema_version" : { - "type" : "string", - "example" : "1.0", - "description" : "Schema version", - "pattern" : "^[0-9.]+$" + "required" : [ "proofType" ], + "type" : "object" + }, + "LedgerConfigInstance" : { + "properties" : { + "genesis_file" : { + "description" : "genesis_file", + "type" : "string" + }, + "genesis_transactions" : { + "description" : "genesis_transactions", + "type" : "string" + }, + "genesis_url" : { + "description" : "genesis_url", + "type" : "string" + }, + "id" : { + "description" : "ledger_id", + "type" : "string" + }, + "is_production" : { + "description" : "is_production", + "type" : "boolean" + } }, - "trace" : { - "type" : "boolean", - "description" : "Record trace information, based on agent configuration" - } - } - }, - "V10CredentialProposalRequestOpt" : { - "type" : "object", - "required" : [ "connection_id" ], - "properties" : { - "auto_remove" : { - "type" : "boolean", - "description" : "Whether to remove the credential exchange record on completion (overrides --preserve-exchange-records configuration setting)" - }, - "comment" : { - "type" : "string", - "description" : "Human-readable comment", - "x-nullable" : true - }, - "connection_id" : { - "type" : "string", - "format" : "uuid", - "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", - "description" : "Connection identifier" - }, - "cred_def_id" : { - "type" : "string", - "example" : "WgWxqztrNooG92RXvxSTWv:3:CL:20:tag", - "description" : "Credential definition identifier", - "pattern" : "^([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}):3:CL:(([1-9][0-9]*)|([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}:2:.+:[0-9.]+)):(.+)?$" + "type" : "object" + }, + "LedgerConfigList" : { + "properties" : { + "ledger_config_list" : { + "items" : { + "$ref" : "#/components/schemas/LedgerConfigInstance" + }, + "type" : "array" + } }, - "credential_proposal" : { - "$ref" : "#/definitions/CredentialPreview" + "required" : [ "ledger_config_list" ], + "type" : "object" + }, + "LedgerModulesResult" : { + "type" : "object" + }, + "LinkedDataProof" : { + "properties" : { + "challenge" : { + "description" : "Associates a challenge with a proof, for use with a proofPurpose such as authentication", + "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "type" : "string" + }, + "created" : { + "description" : "The string value of an ISO8601 combined date and time string generated by the Signature Algorithm", + "example" : "2021-12-31T23:59:59Z", + "pattern" : "^\\d{4}-\\d\\d-\\d\\d[T ]\\d\\d:\\d\\d(?:\\:(?:\\d\\d(?:\\.\\d{1,6})?))?(?:[+-]\\d\\d:?\\d\\d|Z|)$", + "type" : "string" + }, + "domain" : { + "description" : "A string value specifying the restricted domain of the signature.", + "example" : "example.com", + "pattern" : "\\w+:(\\/?\\/?)[^\\s]+", + "type" : "string" + }, + "jws" : { + "description" : "Associates a Detached Json Web Signature with a proof", + "example" : "eyJhbGciOiAiRWREUc2UsICJjcml0IjogWyJiNjQiXX0..lKJU0Df_keblRKhZAS9Qq6zybm-HqUXNVZ8vgEPNTAjQ1Ch6YBKY7UBAjg6iBX5qBQ", + "type" : "string" + }, + "nonce" : { + "description" : "The nonce", + "example" : "CF69iO3nfvqRsRBNElE8b4wO39SyJHPM7Gg1nExltW5vSfQA1lvDCR/zXX1To0/4NLo==", + "type" : "string" + }, + "proofPurpose" : { + "description" : "Proof purpose", + "example" : "assertionMethod", + "type" : "string" + }, + "proofValue" : { + "description" : "The proof value of a proof", + "example" : "sy1AahqbzJQ63n9RtekmwzqZeVj494VppdAVJBnMYrTwft6cLJJGeTSSxCCJ6HKnRtwE7jjDh6sB2z2AAiZY9BBnCD8wUVgwqH3qchGRCuC2RugA4eQ9fUrR4Yuycac3caiaaay", + "type" : "string" + }, + "type" : { + "description" : "Identifies the digital signature suite that was used to create the signature", + "example" : "Ed25519Signature2018", + "type" : "string" + }, + "verificationMethod" : { + "description" : "Information used for proof verification", + "example" : "did:key:z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL#z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL", + "pattern" : "\\w+:(\\/?\\/?)[^\\s]+", + "type" : "string" + } }, - "issuer_did" : { - "type" : "string", - "example" : "WgWxqztrNooG92RXvxSTWv", - "description" : "Credential issuer DID", - "pattern" : "^(did:sov:)?[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}$" + "required" : [ "created", "proofPurpose", "type", "verificationMethod" ], + "type" : "object" + }, + "MediationCreateRequest" : { + "properties" : { + "mediator_terms" : { + "description" : "List of mediator rules for recipient", + "items" : { + "description" : "Indicate terms to which the mediator requires the recipient to agree", + "type" : "string" + }, + "type" : "array" + }, + "recipient_terms" : { + "description" : "List of recipient rules for mediation", + "items" : { + "description" : "Indicate terms to which the recipient requires the mediator to agree", + "type" : "string" + }, + "type" : "array" + } }, - "schema_id" : { - "type" : "string", - "example" : "WgWxqztrNooG92RXvxSTWv:2:schema_name:1.0", - "description" : "Schema identifier", - "pattern" : "^[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}:2:.+:[0-9.]+$" + "type" : "object" + }, + "MediationDeny" : { + "properties" : { + "@id" : { + "description" : "Message identifier", + "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "type" : "string" + }, + "@type" : { + "description" : "Message type", + "example" : "https://didcomm.org/my-family/1.0/my-message-type", + "readOnly" : true, + "type" : "string" + }, + "mediator_terms" : { + "items" : { + "description" : "Terms for mediator to agree", + "type" : "string" + }, + "type" : "array" + }, + "recipient_terms" : { + "items" : { + "description" : "Terms for recipient to agree", + "type" : "string" + }, + "type" : "array" + } }, - "schema_issuer_did" : { - "type" : "string", - "example" : "WgWxqztrNooG92RXvxSTWv", - "description" : "Schema issuer DID", - "pattern" : "^(did:sov:)?[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}$" + "type" : "object" + }, + "MediationGrant" : { + "properties" : { + "@id" : { + "description" : "Message identifier", + "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "type" : "string" + }, + "@type" : { + "description" : "Message type", + "example" : "https://didcomm.org/my-family/1.0/my-message-type", + "readOnly" : true, + "type" : "string" + }, + "endpoint" : { + "description" : "endpoint on which messages destined for the recipient are received.", + "example" : "http://192.168.56.102:8020/", + "type" : "string" + }, + "routing_keys" : { + "items" : { + "description" : "Keys to use for forward message packaging", + "type" : "string" + }, + "type" : "array" + } }, - "schema_name" : { - "type" : "string", - "example" : "preferences", - "description" : "Schema name" + "type" : "object" + }, + "MediationIdMatchInfo" : { + "properties" : { + "mediation_id" : { + "description" : "Mediation record identifier", + "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "format" : "uuid", + "type" : "string" + } }, - "schema_version" : { - "type" : "string", - "example" : "1.0", - "description" : "Schema version", - "pattern" : "^[0-9.]+$" + "type" : "object" + }, + "MediationList" : { + "properties" : { + "results" : { + "description" : "List of mediation records", + "items" : { + "$ref" : "#/components/schemas/MediationRecord" + }, + "type" : "array" + } }, - "trace" : { - "type" : "boolean", - "description" : "Record trace information, based on agent configuration" - } - } - }, - "V10CredentialStoreRequest" : { - "type" : "object", - "properties" : { - "credential_id" : { - "type" : "string" - } - } - }, - "V10PresentProofModuleResponse" : { - "type" : "object" - }, - "V10PresentationCreateRequestRequest" : { - "type" : "object", - "required" : [ "proof_request" ], - "properties" : { - "comment" : { - "type" : "string", - "x-nullable" : true + "type" : "object" + }, + "MediationRecord" : { + "properties" : { + "connection_id" : { + "type" : "string" + }, + "created_at" : { + "description" : "Time of record creation", + "example" : "2021-12-31T23:59:59Z", + "pattern" : "^\\d{4}-\\d\\d-\\d\\d[T ]\\d\\d:\\d\\d(?:\\:(?:\\d\\d(?:\\.\\d{1,6})?))?(?:[+-]\\d\\d:?\\d\\d|Z|)$", + "type" : "string" + }, + "endpoint" : { + "type" : "string" + }, + "mediation_id" : { + "type" : "string" + }, + "mediator_terms" : { + "items" : { + "type" : "string" + }, + "type" : "array" + }, + "recipient_terms" : { + "items" : { + "type" : "string" + }, + "type" : "array" + }, + "role" : { + "type" : "string" + }, + "routing_keys" : { + "items" : { + "example" : "did:key:z6MkpTHR8VNsBxYAAWHut2Geadd9jSwuBV8xRoAnwWsdvktH", + "pattern" : "^did:key:z[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]+$", + "type" : "string" + }, + "type" : "array" + }, + "state" : { + "description" : "Current record state", + "example" : "active", + "type" : "string" + }, + "updated_at" : { + "description" : "Time of last record update", + "example" : "2021-12-31T23:59:59Z", + "pattern" : "^\\d{4}-\\d\\d-\\d\\d[T ]\\d\\d:\\d\\d(?:\\:(?:\\d\\d(?:\\.\\d{1,6})?))?(?:[+-]\\d\\d:?\\d\\d|Z|)$", + "type" : "string" + } }, - "proof_request" : { - "$ref" : "#/definitions/IndyProofRequest" + "required" : [ "connection_id", "role" ], + "type" : "object" + }, + "Menu" : { + "properties" : { + "@id" : { + "description" : "Message identifier", + "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "type" : "string" + }, + "@type" : { + "description" : "Message type", + "example" : "https://didcomm.org/my-family/1.0/my-message-type", + "readOnly" : true, + "type" : "string" + }, + "description" : { + "description" : "Introductory text for the menu", + "example" : "This menu presents options", + "type" : "string" + }, + "errormsg" : { + "description" : "An optional error message to display in menu header", + "example" : "Error: item not found", + "type" : "string" + }, + "options" : { + "description" : "List of menu options", + "items" : { + "$ref" : "#/components/schemas/MenuOption" + }, + "type" : "array" + }, + "title" : { + "description" : "Menu title", + "example" : "My Menu", + "type" : "string" + } }, - "trace" : { - "type" : "boolean", - "example" : false, - "description" : "Whether to trace event (default false)" - } - } - }, - "V10PresentationExchange" : { - "type" : "object", - "properties" : { - "auto_present" : { - "type" : "boolean", - "example" : false, - "description" : "Prover choice to auto-present proof as verifier requests" - }, - "connection_id" : { - "type" : "string", - "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", - "description" : "Connection identifier" - }, - "created_at" : { - "type" : "string", - "example" : "2021-12-31 23:59:59Z", - "description" : "Time of record creation", - "pattern" : "^\\d{4}-\\d\\d-\\d\\d[T ]\\d\\d:\\d\\d(?:\\:(?:\\d\\d(?:\\.\\d{1,6})?))?(?:[+-]\\d\\d:?\\d\\d|Z|)$" - }, - "error_msg" : { - "type" : "string", - "example" : "Invalid structure", - "description" : "Error message" - }, - "initiator" : { - "type" : "string", - "example" : "self", - "description" : "Present-proof exchange initiator: self or external", - "enum" : [ "self", "external" ] - }, - "presentation" : { - "$ref" : "#/definitions/V10PresentationExchange_presentation" - }, - "presentation_exchange_id" : { - "type" : "string", - "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", - "description" : "Presentation exchange identifier" - }, - "presentation_proposal_dict" : { - "$ref" : "#/definitions/V10PresentationExchange_presentation_proposal_dict" - }, - "presentation_request" : { - "$ref" : "#/definitions/V10PresentationExchange_presentation_request" - }, - "presentation_request_dict" : { - "$ref" : "#/definitions/V10PresentationExchange_presentation_request_dict" - }, - "role" : { - "type" : "string", - "example" : "prover", - "description" : "Present-proof exchange role: prover or verifier", - "enum" : [ "prover", "verifier" ] - }, - "state" : { - "type" : "string", - "example" : "verified", - "description" : "Present-proof exchange state" - }, - "thread_id" : { - "type" : "string", - "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", - "description" : "Thread identifier" - }, - "trace" : { - "type" : "boolean", - "description" : "Record trace information, based on agent configuration" - }, - "updated_at" : { - "type" : "string", - "example" : "2021-12-31 23:59:59Z", - "description" : "Time of last record update", - "pattern" : "^\\d{4}-\\d\\d-\\d\\d[T ]\\d\\d:\\d\\d(?:\\:(?:\\d\\d(?:\\.\\d{1,6})?))?(?:[+-]\\d\\d:?\\d\\d|Z|)$" - }, - "verified" : { - "type" : "string", - "example" : "true", - "description" : "Whether presentation is verified: true or false", - "enum" : [ "true", "false" ] - } - } - }, - "V10PresentationExchangeList" : { - "type" : "object", - "properties" : { - "results" : { - "type" : "array", - "description" : "Aries RFC 37 v1.0 presentation exchange records", - "items" : { - "$ref" : "#/definitions/V10PresentationExchange" + "required" : [ "options" ], + "type" : "object" + }, + "MenuForm" : { + "properties" : { + "description" : { + "description" : "Additional descriptive text for menu form", + "example" : "Window preference settings", + "type" : "string" + }, + "params" : { + "description" : "List of form parameters", + "items" : { + "$ref" : "#/components/schemas/MenuFormParam" + }, + "type" : "array" + }, + "submit-label" : { + "description" : "Alternative label for form submit button", + "example" : "Send", + "type" : "string" + }, + "title" : { + "description" : "Menu form title", + "example" : "Preferences", + "type" : "string" } - } - } - }, - "V10PresentationProblemReportRequest" : { - "type" : "object", - "required" : [ "description" ], - "properties" : { - "description" : { - "type" : "string" - } - } - }, - "V10PresentationProposalRequest" : { - "type" : "object", - "required" : [ "connection_id", "presentation_proposal" ], - "properties" : { - "auto_present" : { - "type" : "boolean", - "description" : "Whether to respond automatically to presentation requests, building and presenting requested proof" - }, - "comment" : { - "type" : "string", - "description" : "Human-readable comment", - "x-nullable" : true - }, - "connection_id" : { - "type" : "string", - "format" : "uuid", - "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", - "description" : "Connection identifier" - }, - "presentation_proposal" : { - "$ref" : "#/definitions/IndyPresPreview" - }, - "trace" : { - "type" : "boolean", - "example" : false, - "description" : "Whether to trace event (default false)" - } - } - }, - "V10PresentationSendRequestRequest" : { - "type" : "object", - "required" : [ "connection_id", "proof_request" ], - "properties" : { - "comment" : { - "type" : "string", - "x-nullable" : true }, - "connection_id" : { - "type" : "string", - "format" : "uuid", - "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", - "description" : "Connection identifier" + "type" : "object" + }, + "MenuFormParam" : { + "properties" : { + "default" : { + "description" : "Default parameter value", + "example" : "0", + "type" : "string" + }, + "description" : { + "description" : "Additional descriptive text for menu form parameter", + "example" : "Delay in seconds before starting", + "type" : "string" + }, + "name" : { + "description" : "Menu parameter name", + "example" : "delay", + "type" : "string" + }, + "required" : { + "description" : "Whether parameter is required", + "example" : false, + "type" : "boolean" + }, + "title" : { + "description" : "Menu parameter title", + "example" : "Delay in seconds", + "type" : "string" + }, + "type" : { + "description" : "Menu form parameter input type", + "example" : "int", + "type" : "string" + } }, - "proof_request" : { - "$ref" : "#/definitions/IndyProofRequest" + "required" : [ "name", "title" ], + "type" : "object" + }, + "MenuJson" : { + "properties" : { + "description" : { + "description" : "Introductory text for the menu", + "example" : "User preferences for window settings", + "type" : "string" + }, + "errormsg" : { + "description" : "Optional error message to display in menu header", + "example" : "Error: item not present", + "type" : "string" + }, + "options" : { + "description" : "List of menu options", + "items" : { + "$ref" : "#/components/schemas/MenuOption" + }, + "type" : "array" + }, + "title" : { + "description" : "Menu title", + "example" : "My Menu", + "type" : "string" + } }, - "trace" : { - "type" : "boolean", - "example" : false, - "description" : "Whether to trace event (default false)" - } - } - }, - "V20CredAttrSpec" : { - "type" : "object", - "required" : [ "name", "value" ], - "properties" : { - "mime-type" : { - "type" : "string", - "example" : "image/jpeg", - "description" : "MIME type: omit for (null) default", - "x-nullable" : true - }, - "name" : { - "type" : "string", - "example" : "favourite_drink", - "description" : "Attribute name" - }, - "value" : { - "type" : "string", - "example" : "martini", - "description" : "Attribute value: base64-encode if MIME type is present" - } - } - }, - "V20CredBoundOfferRequest" : { - "type" : "object", - "properties" : { - "counter_preview" : { - "$ref" : "#/definitions/V20CredBoundOfferRequest_counter_preview" + "required" : [ "options" ], + "type" : "object" + }, + "MenuOption" : { + "properties" : { + "description" : { + "description" : "Additional descriptive text for menu option", + "example" : "Window display preferences", + "type" : "string" + }, + "disabled" : { + "description" : "Whether to show option as disabled", + "example" : false, + "type" : "boolean" + }, + "form" : { + "$ref" : "#/components/schemas/MenuForm" + }, + "name" : { + "description" : "Menu option name (unique identifier)", + "example" : "window_prefs", + "type" : "string" + }, + "title" : { + "description" : "Menu option title", + "example" : "Window Preferences", + "type" : "string" + } }, - "filter" : { - "$ref" : "#/definitions/V20CredBoundOfferRequest_filter" - } - } - }, - "V20CredExFree" : { - "type" : "object", - "required" : [ "connection_id", "filter" ], - "properties" : { - "auto_remove" : { - "type" : "boolean", - "description" : "Whether to remove the credential exchange record on completion (overrides --preserve-exchange-records configuration setting)" + "required" : [ "name", "title" ], + "type" : "object" + }, + "MultitenantModuleResponse" : { + "type" : "object" + }, + "OobRecord" : { + "properties" : { + "attach_thread_id" : { + "description" : "Connection record identifier", + "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "type" : "string" + }, + "connection_id" : { + "description" : "Connection record identifier", + "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "type" : "string" + }, + "created_at" : { + "description" : "Time of record creation", + "example" : "2021-12-31T23:59:59Z", + "pattern" : "^\\d{4}-\\d\\d-\\d\\d[T ]\\d\\d:\\d\\d(?:\\:(?:\\d\\d(?:\\.\\d{1,6})?))?(?:[+-]\\d\\d:?\\d\\d|Z|)$", + "type" : "string" + }, + "invi_msg_id" : { + "description" : "Invitation message identifier", + "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "type" : "string" + }, + "invitation" : { + "$ref" : "#/components/schemas/InvitationRecord_invitation" + }, + "oob_id" : { + "description" : "Oob record identifier", + "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "type" : "string" + }, + "our_recipient_key" : { + "description" : "Recipient key used for oob invitation", + "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "type" : "string" + }, + "role" : { + "description" : "OOB Role", + "enum" : [ "sender", "receiver" ], + "example" : "receiver", + "type" : "string" + }, + "state" : { + "description" : "Out of band message exchange state", + "enum" : [ "initial", "prepare-response", "await-response", "reuse-not-accepted", "reuse-accepted", "done", "deleted" ], + "example" : "await-response", + "type" : "string" + }, + "their_service" : { + "$ref" : "#/components/schemas/ServiceDecorator" + }, + "trace" : { + "description" : "Record trace information, based on agent configuration", + "type" : "boolean" + }, + "updated_at" : { + "description" : "Time of last record update", + "example" : "2021-12-31T23:59:59Z", + "pattern" : "^\\d{4}-\\d\\d-\\d\\d[T ]\\d\\d:\\d\\d(?:\\:(?:\\d\\d(?:\\.\\d{1,6})?))?(?:[+-]\\d\\d:?\\d\\d|Z|)$", + "type" : "string" + } }, - "comment" : { - "type" : "string", - "description" : "Human-readable comment", - "x-nullable" : true + "required" : [ "invi_msg_id", "invitation", "oob_id", "state" ], + "type" : "object" + }, + "PerformRequest" : { + "properties" : { + "name" : { + "description" : "Menu option name", + "example" : "Query", + "type" : "string" + }, + "params" : { + "additionalProperties" : { + "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "type" : "string" + }, + "description" : "Input parameter values", + "type" : "object" + } }, - "connection_id" : { - "type" : "string", - "format" : "uuid", - "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", - "description" : "Connection identifier" + "type" : "object" + }, + "PingRequest" : { + "properties" : { + "comment" : { + "description" : "Comment for the ping message", + "nullable" : true, + "type" : "string" + } }, - "credential_preview" : { - "$ref" : "#/definitions/V20CredPreview" + "type" : "object" + }, + "PingRequestResponse" : { + "properties" : { + "thread_id" : { + "description" : "Thread ID of the ping message", + "type" : "string" + } }, - "filter" : { - "$ref" : "#/definitions/V20CredBoundOfferRequest_filter" + "type" : "object" + }, + "PresentationDefinition" : { + "properties" : { + "format" : { + "$ref" : "#/components/schemas/ClaimFormat" + }, + "id" : { + "description" : "Unique Resource Identifier", + "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "pattern" : "[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-4[a-fA-F0-9]{3}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}", + "type" : "string" + }, + "input_descriptors" : { + "items" : { + "$ref" : "#/components/schemas/InputDescriptors" + }, + "type" : "array" + }, + "name" : { + "description" : "Human-friendly name that describes what the presentation definition pertains to", + "type" : "string" + }, + "purpose" : { + "description" : "Describes the purpose for which the Presentation Definition's inputs are being requested", + "type" : "string" + }, + "submission_requirements" : { + "items" : { + "$ref" : "#/components/schemas/SubmissionRequirements" + }, + "type" : "array" + } }, - "trace" : { - "type" : "boolean", - "description" : "Record trace information, based on agent configuration" - } - } - }, - "V20CredExRecord" : { - "type" : "object", - "properties" : { - "auto_issue" : { - "type" : "boolean", - "example" : false, - "description" : "Issuer choice to issue to request in this credential exchange" - }, - "auto_offer" : { - "type" : "boolean", - "example" : false, - "description" : "Holder choice to accept offer in this credential exchange" - }, - "auto_remove" : { - "type" : "boolean", - "example" : false, - "description" : "Issuer choice to remove this credential exchange record when complete" - }, - "by_format" : { - "$ref" : "#/definitions/V20CredExRecord_by_format" - }, - "connection_id" : { - "type" : "string", - "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", - "description" : "Connection identifier" - }, - "created_at" : { - "type" : "string", - "example" : "2021-12-31 23:59:59Z", - "description" : "Time of record creation", - "pattern" : "^\\d{4}-\\d\\d-\\d\\d[T ]\\d\\d:\\d\\d(?:\\:(?:\\d\\d(?:\\.\\d{1,6})?))?(?:[+-]\\d\\d:?\\d\\d|Z|)$" - }, - "cred_ex_id" : { - "type" : "string", - "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", - "description" : "Credential exchange identifier" - }, - "cred_issue" : { - "$ref" : "#/definitions/V20CredExRecord_cred_issue" - }, - "cred_offer" : { - "$ref" : "#/definitions/V10CredentialExchange_credential_offer_dict" - }, - "cred_preview" : { - "$ref" : "#/definitions/V20CredExRecord_cred_preview" - }, - "cred_proposal" : { - "$ref" : "#/definitions/V10CredentialExchange_credential_proposal_dict" - }, - "cred_request" : { - "$ref" : "#/definitions/V20CredExRecord_cred_request" - }, - "error_msg" : { - "type" : "string", - "example" : "The front fell off", - "description" : "Error message" - }, - "initiator" : { - "type" : "string", - "example" : "self", - "description" : "Issue-credential exchange initiator: self or external", - "enum" : [ "self", "external" ] - }, - "parent_thread_id" : { - "type" : "string", - "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", - "description" : "Parent thread identifier" - }, - "role" : { - "type" : "string", - "example" : "issuer", - "description" : "Issue-credential exchange role: holder or issuer", - "enum" : [ "issuer", "holder" ] - }, - "state" : { - "type" : "string", - "example" : "done", - "description" : "Issue-credential exchange state", - "enum" : [ "proposal-sent", "proposal-received", "offer-sent", "offer-received", "request-sent", "request-received", "credential-issued", "credential-received", "done" ] - }, - "thread_id" : { - "type" : "string", - "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", - "description" : "Thread identifier" - }, - "trace" : { - "type" : "boolean", - "description" : "Record trace information, based on agent configuration" - }, - "updated_at" : { - "type" : "string", - "example" : "2021-12-31 23:59:59Z", - "description" : "Time of last record update", - "pattern" : "^\\d{4}-\\d\\d-\\d\\d[T ]\\d\\d:\\d\\d(?:\\:(?:\\d\\d(?:\\.\\d{1,6})?))?(?:[+-]\\d\\d:?\\d\\d|Z|)$" - } - } - }, - "V20CredExRecordByFormat" : { - "type" : "object", - "properties" : { - "cred_issue" : { - "type" : "object", - "properties" : { } + "type" : "object" + }, + "PresentationProposal" : { + "properties" : { + "@id" : { + "description" : "Message identifier", + "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "type" : "string" + }, + "@type" : { + "description" : "Message type", + "example" : "https://didcomm.org/my-family/1.0/my-message-type", + "readOnly" : true, + "type" : "string" + }, + "comment" : { + "description" : "Human-readable comment", + "nullable" : true, + "type" : "string" + }, + "presentation_proposal" : { + "$ref" : "#/components/schemas/IndyPresPreview" + } }, - "cred_offer" : { - "type" : "object", - "properties" : { } + "required" : [ "presentation_proposal" ], + "type" : "object" + }, + "PresentationRequest" : { + "properties" : { + "@id" : { + "description" : "Message identifier", + "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "type" : "string" + }, + "@type" : { + "description" : "Message type", + "example" : "https://didcomm.org/my-family/1.0/my-message-type", + "readOnly" : true, + "type" : "string" + }, + "comment" : { + "description" : "Human-readable comment", + "nullable" : true, + "type" : "string" + }, + "request_presentations~attach" : { + "items" : { + "$ref" : "#/components/schemas/AttachDecorator" + }, + "type" : "array" + } }, - "cred_proposal" : { - "type" : "object", - "properties" : { } + "required" : [ "request_presentations~attach" ], + "type" : "object" + }, + "ProtocolDescriptor" : { + "properties" : { + "pid" : { + "type" : "string" + }, + "roles" : { + "description" : "List of roles", + "items" : { + "description" : "Role: requester or responder", + "example" : "requester", + "type" : "string" + }, + "nullable" : true, + "type" : "array" + } }, - "cred_request" : { - "type" : "object", - "properties" : { } - } - } - }, - "V20CredExRecordDetail" : { - "type" : "object", - "properties" : { - "cred_ex_record" : { - "$ref" : "#/definitions/V20CredExRecordDetail_cred_ex_record" + "required" : [ "pid" ], + "type" : "object" + }, + "PublishRevocations" : { + "properties" : { + "rrid2crid" : { + "additionalProperties" : { + "items" : { + "description" : "Credential revocation identifier", + "example" : "12345", + "pattern" : "^[1-9][0-9]*$", + "type" : "string" + }, + "type" : "array" + }, + "description" : "Credential revocation ids by revocation registry id", + "type" : "object" + } }, - "indy" : { - "$ref" : "#/definitions/V20CredExRecordIndy" + "type" : "object" + }, + "Queries" : { + "properties" : { + "@id" : { + "description" : "Message identifier", + "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "type" : "string" + }, + "@type" : { + "description" : "Message type", + "example" : "https://didcomm.org/my-family/1.0/my-message-type", + "readOnly" : true, + "type" : "string" + }, + "queries" : { + "items" : { + "$ref" : "#/components/schemas/QueryItem" + }, + "type" : "array" + } }, - "ld_proof" : { - "$ref" : "#/definitions/V20CredExRecordLDProof" - } - } - }, - "V20CredExRecordIndy" : { - "type" : "object", - "properties" : { - "created_at" : { - "type" : "string", - "example" : "2021-12-31 23:59:59Z", - "description" : "Time of record creation", - "pattern" : "^\\d{4}-\\d\\d-\\d\\d[T ]\\d\\d:\\d\\d(?:\\:(?:\\d\\d(?:\\.\\d{1,6})?))?(?:[+-]\\d\\d:?\\d\\d|Z|)$" - }, - "cred_ex_id" : { - "type" : "string", - "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", - "description" : "Corresponding v2.0 credential exchange record identifier" - }, - "cred_ex_indy_id" : { - "type" : "string", - "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", - "description" : "Record identifier" - }, - "cred_id_stored" : { - "type" : "string", - "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", - "description" : "Credential identifier stored in wallet" - }, - "cred_request_metadata" : { - "type" : "object", - "description" : "Credential request metadata for indy holder", - "properties" : { } - }, - "cred_rev_id" : { - "type" : "string", - "example" : "12345", - "description" : "Credential revocation identifier within revocation registry", - "pattern" : "^[1-9][0-9]*$" - }, - "rev_reg_id" : { - "type" : "string", - "example" : "WgWxqztrNooG92RXvxSTWv:4:WgWxqztrNooG92RXvxSTWv:3:CL:20:tag:CL_ACCUM:0", - "description" : "Revocation registry identifier", - "pattern" : "^([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}):4:([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}):3:CL:(([1-9][0-9]*)|([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}:2:.+:[0-9.]+))(:.+)?:CL_ACCUM:(.+$)" - }, - "state" : { - "type" : "string", - "example" : "active", - "description" : "Current record state" - }, - "updated_at" : { - "type" : "string", - "example" : "2021-12-31 23:59:59Z", - "description" : "Time of last record update", - "pattern" : "^\\d{4}-\\d\\d-\\d\\d[T ]\\d\\d:\\d\\d(?:\\:(?:\\d\\d(?:\\.\\d{1,6})?))?(?:[+-]\\d\\d:?\\d\\d|Z|)$" - } - } - }, - "V20CredExRecordLDProof" : { - "type" : "object", - "properties" : { - "created_at" : { - "type" : "string", - "example" : "2021-12-31 23:59:59Z", - "description" : "Time of record creation", - "pattern" : "^\\d{4}-\\d\\d-\\d\\d[T ]\\d\\d:\\d\\d(?:\\:(?:\\d\\d(?:\\.\\d{1,6})?))?(?:[+-]\\d\\d:?\\d\\d|Z|)$" - }, - "cred_ex_id" : { - "type" : "string", - "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", - "description" : "Corresponding v2.0 credential exchange record identifier" - }, - "cred_ex_ld_proof_id" : { - "type" : "string", - "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", - "description" : "Record identifier" - }, - "cred_id_stored" : { - "type" : "string", - "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", - "description" : "Credential identifier stored in wallet" - }, - "state" : { - "type" : "string", - "example" : "active", - "description" : "Current record state" - }, - "updated_at" : { - "type" : "string", - "example" : "2021-12-31 23:59:59Z", - "description" : "Time of last record update", - "pattern" : "^\\d{4}-\\d\\d-\\d\\d[T ]\\d\\d:\\d\\d(?:\\:(?:\\d\\d(?:\\.\\d{1,6})?))?(?:[+-]\\d\\d:?\\d\\d|Z|)$" - } - } - }, - "V20CredExRecordListResult" : { - "type" : "object", - "properties" : { - "results" : { - "type" : "array", - "description" : "Credential exchange records and corresponding detail records", - "items" : { - "$ref" : "#/definitions/V20CredExRecordDetail" + "type" : "object" + }, + "Query" : { + "properties" : { + "@id" : { + "description" : "Message identifier", + "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "type" : "string" + }, + "@type" : { + "description" : "Message type", + "example" : "https://didcomm.org/my-family/1.0/my-message-type", + "readOnly" : true, + "type" : "string" + }, + "comment" : { + "nullable" : true, + "type" : "string" + }, + "query" : { + "type" : "string" } - } - } - }, - "V20CredFilter" : { - "type" : "object", - "properties" : { - "indy" : { - "$ref" : "#/definitions/V20CredFilter_indy" }, - "ld_proof" : { - "$ref" : "#/definitions/V20CredFilter_ld_proof" - } - } - }, - "V20CredFilterIndy" : { - "type" : "object", - "properties" : { - "cred_def_id" : { - "type" : "string", - "example" : "WgWxqztrNooG92RXvxSTWv:3:CL:20:tag", - "description" : "Credential definition identifier", - "pattern" : "^([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}):3:CL:(([1-9][0-9]*)|([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}:2:.+:[0-9.]+)):(.+)?$" + "required" : [ "query" ], + "type" : "object" + }, + "QueryItem" : { + "properties" : { + "feature-type" : { + "description" : "feature type", + "enum" : [ "protocol", "goal-code" ], + "type" : "string" + }, + "match" : { + "description" : "match", + "type" : "string" + } }, - "issuer_did" : { - "type" : "string", - "example" : "WgWxqztrNooG92RXvxSTWv", - "description" : "Credential issuer DID", - "pattern" : "^(did:sov:)?[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}$" + "required" : [ "feature-type", "match" ], + "type" : "object" + }, + "RawEncoded" : { + "properties" : { + "encoded" : { + "description" : "Encoded value", + "example" : "-1", + "pattern" : "^-?[0-9]*$", + "type" : "string" + }, + "raw" : { + "description" : "Raw value", + "type" : "string" + } }, - "schema_id" : { - "type" : "string", - "example" : "WgWxqztrNooG92RXvxSTWv:2:schema_name:1.0", - "description" : "Schema identifier", - "pattern" : "^[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}:2:.+:[0-9.]+$" - }, - "schema_issuer_did" : { - "type" : "string", - "example" : "WgWxqztrNooG92RXvxSTWv", - "description" : "Schema issuer DID", - "pattern" : "^(did:sov:)?[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}$" - }, - "schema_name" : { - "type" : "string", - "example" : "preferences", - "description" : "Schema name" + "type" : "object" + }, + "ReceiveInvitationRequest" : { + "properties" : { + "@id" : { + "description" : "Message identifier", + "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "type" : "string" + }, + "@type" : { + "description" : "Message type", + "example" : "https://didcomm.org/my-family/1.0/my-message-type", + "readOnly" : true, + "type" : "string" + }, + "did" : { + "description" : "DID for connection invitation", + "example" : "WgWxqztrNooG92RXvxSTWv", + "pattern" : "^(did:sov:)?[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}$", + "type" : "string" + }, + "imageUrl" : { + "description" : "Optional image URL for connection invitation", + "example" : "http://192.168.56.101/img/logo.jpg", + "format" : "url", + "nullable" : true, + "type" : "string" + }, + "label" : { + "description" : "Optional label for connection invitation", + "example" : "Bob", + "type" : "string" + }, + "recipientKeys" : { + "description" : "List of recipient keys", + "items" : { + "description" : "Recipient public key", + "example" : "H3C2AVvLMv6gmMNam3uVAjZpfkcJCwDwnZn6z3wXmqPV", + "pattern" : "^[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{43,44}$", + "type" : "string" + }, + "type" : "array" + }, + "routingKeys" : { + "description" : "List of routing keys", + "items" : { + "description" : "Routing key", + "example" : "H3C2AVvLMv6gmMNam3uVAjZpfkcJCwDwnZn6z3wXmqPV", + "pattern" : "^[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{43,44}$", + "type" : "string" + }, + "type" : "array" + }, + "serviceEndpoint" : { + "description" : "Service endpoint at which to reach this agent", + "example" : "http://192.168.56.101:8020", + "type" : "string" + } }, - "schema_version" : { - "type" : "string", - "example" : "1.0", - "description" : "Schema version", - "pattern" : "^[0-9.]+$" - } - } - }, - "V20CredFilterLDProof" : { - "type" : "object", - "required" : [ "ld_proof" ], - "properties" : { - "ld_proof" : { - "$ref" : "#/definitions/V20CredFilter_ld_proof" - } - } - }, - "V20CredFormat" : { - "type" : "object", - "required" : [ "attach_id", "format" ], - "properties" : { - "attach_id" : { - "type" : "string", - "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", - "description" : "Attachment identifier" + "type" : "object" + }, + "RemoveWalletRequest" : { + "properties" : { + "wallet_key" : { + "description" : "Master key used for key derivation. Only required for unmanaged wallets.", + "example" : "MySecretKey123", + "type" : "string" + } }, - "format" : { - "type" : "string", - "example" : "aries/ld-proof-vc-detail@v1.0", - "description" : "Attachment format specifier" - } - } - }, - "V20CredIssue" : { - "type" : "object", - "required" : [ "credentials~attach", "formats" ], - "properties" : { - "@id" : { - "type" : "string", - "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", - "description" : "Message identifier" - }, - "@type" : { - "type" : "string", - "example" : "https://didcomm.org/my-family/1.0/my-message-type", - "description" : "Message type", - "readOnly" : true - }, - "comment" : { - "type" : "string", - "description" : "Human-readable comment", - "x-nullable" : true - }, - "credentials~attach" : { - "type" : "array", - "description" : "Credential attachments", - "items" : { - "$ref" : "#/definitions/AttachDecorator" - } - }, - "formats" : { - "type" : "array", - "description" : "Acceptable attachment formats", - "items" : { - "$ref" : "#/definitions/V20CredFormat" - } - }, - "replacement_id" : { - "type" : "string", - "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", - "description" : "Issuer-unique identifier to coordinate credential replacement" - } - } - }, - "V20CredIssueProblemReportRequest" : { - "type" : "object", - "required" : [ "description" ], - "properties" : { - "description" : { - "type" : "string" - } - } - }, - "V20CredIssueRequest" : { - "type" : "object", - "properties" : { - "comment" : { - "type" : "string", - "description" : "Human-readable comment", - "x-nullable" : true - } - } - }, - "V20CredOffer" : { - "type" : "object", - "required" : [ "formats", "offers~attach" ], - "properties" : { - "@id" : { - "type" : "string", - "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", - "description" : "Message identifier" + "type" : "object" + }, + "ResolutionResult" : { + "properties" : { + "did_document" : { + "description" : "DID Document", + "properties" : { }, + "type" : "object" + }, + "metadata" : { + "description" : "Resolution metadata", + "properties" : { }, + "type" : "object" + } }, - "@type" : { - "type" : "string", - "example" : "https://didcomm.org/my-family/1.0/my-message-type", - "description" : "Message type", - "readOnly" : true + "required" : [ "did_document", "metadata" ], + "type" : "object" + }, + "RevRegCreateRequest" : { + "properties" : { + "credential_definition_id" : { + "description" : "Credential definition identifier", + "example" : "WgWxqztrNooG92RXvxSTWv:3:CL:20:tag", + "pattern" : "^([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}):3:CL:(([1-9][0-9]*)|([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}:2:.+:[0-9.]+)):(.+)?$", + "type" : "string" + }, + "max_cred_num" : { + "description" : "Revocation registry size", + "example" : 1000, + "format" : "int32", + "maximum" : 32768, + "minimum" : 4, + "type" : "integer" + } }, - "comment" : { - "type" : "string", - "description" : "Human-readable comment", - "x-nullable" : true + "type" : "object" + }, + "RevRegIssuedResult" : { + "properties" : { + "result" : { + "description" : "Number of credentials issued against revocation registry", + "example" : 0, + "format" : "int32", + "minimum" : 0, + "type" : "integer" + } }, - "credential_preview" : { - "$ref" : "#/definitions/V20CredPreview" + "type" : "object" + }, + "RevRegResult" : { + "properties" : { + "result" : { + "$ref" : "#/components/schemas/IssuerRevRegRecord" + } }, - "formats" : { - "type" : "array", - "description" : "Acceptable credential formats", - "items" : { - "$ref" : "#/definitions/V20CredFormat" + "type" : "object" + }, + "RevRegUpdateTailsFileUri" : { + "properties" : { + "tails_public_uri" : { + "description" : "Public URI to the tails file", + "example" : "http://192.168.56.133:6543/revocation/registry/WgWxqztrNooG92RXvxSTWv:4:WgWxqztrNooG92RXvxSTWv:3:CL:20:tag:CL_ACCUM:0/tails-file", + "format" : "url", + "type" : "string" } }, - "offers~attach" : { - "type" : "array", - "description" : "Offer attachments", - "items" : { - "$ref" : "#/definitions/AttachDecorator" + "required" : [ "tails_public_uri" ], + "type" : "object" + }, + "RevRegWalletUpdatedResult" : { + "properties" : { + "accum_calculated" : { + "description" : "Calculated accumulator for phantom revocations", + "properties" : { }, + "type" : "object" + }, + "accum_fixed" : { + "description" : "Applied ledger transaction to fix revocations", + "properties" : { }, + "type" : "object" + }, + "rev_reg_delta" : { + "description" : "Indy revocation registry delta", + "properties" : { }, + "type" : "object" } }, - "replacement_id" : { - "type" : "string", - "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", - "description" : "Issuer-unique identifier to coordinate credential replacement" - } - } - }, - "V20CredOfferConnFreeRequest" : { - "type" : "object", - "required" : [ "filter" ], - "properties" : { - "auto_issue" : { - "type" : "boolean", - "description" : "Whether to respond automatically to credential requests, creating and issuing requested credentials" + "type" : "object" + }, + "RevRegsCreated" : { + "properties" : { + "rev_reg_ids" : { + "items" : { + "description" : "Revocation registry identifiers", + "example" : "WgWxqztrNooG92RXvxSTWv:4:WgWxqztrNooG92RXvxSTWv:3:CL:20:tag:CL_ACCUM:0", + "pattern" : "^([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}):4:([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}):3:CL:(([1-9][0-9]*)|([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}:2:.+:[0-9.]+))(:.+)?:CL_ACCUM:(.+$)", + "type" : "string" + }, + "type" : "array" + } }, - "auto_remove" : { - "type" : "boolean", - "description" : "Whether to remove the credential exchange record on completion (overrides --preserve-exchange-records configuration setting)" + "type" : "object" + }, + "RevocationModuleResponse" : { + "type" : "object" + }, + "RevokeRequest" : { + "properties" : { + "comment" : { + "description" : "Optional comment to include in revocation notification", + "type" : "string" + }, + "connection_id" : { + "description" : "Connection ID to which the revocation notification will be sent; required if notify is true", + "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "pattern" : "[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-4[a-fA-F0-9]{3}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}", + "type" : "string" + }, + "cred_ex_id" : { + "description" : "Credential exchange identifier", + "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "pattern" : "[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-4[a-fA-F0-9]{3}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}", + "type" : "string" + }, + "cred_rev_id" : { + "description" : "Credential revocation identifier", + "example" : "12345", + "pattern" : "^[1-9][0-9]*$", + "type" : "string" + }, + "notify" : { + "description" : "Send a notification to the credential recipient", + "type" : "boolean" + }, + "notify_version" : { + "description" : "Specify which version of the revocation notification should be sent", + "enum" : [ "v1_0", "v2_0" ], + "type" : "string" + }, + "publish" : { + "description" : "(True) publish revocation to ledger immediately, or (default, False) mark it pending", + "type" : "boolean" + }, + "rev_reg_id" : { + "description" : "Revocation registry identifier", + "example" : "WgWxqztrNooG92RXvxSTWv:4:WgWxqztrNooG92RXvxSTWv:3:CL:20:tag:CL_ACCUM:0", + "pattern" : "^([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}):4:([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}):3:CL:(([1-9][0-9]*)|([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}:2:.+:[0-9.]+))(:.+)?:CL_ACCUM:(.+$)", + "type" : "string" + }, + "thread_id" : { + "description" : "Thread ID of the credential exchange message thread resulting in the credential now being revoked; required if notify is true", + "type" : "string" + } }, - "comment" : { - "type" : "string", - "description" : "Human-readable comment", - "x-nullable" : true + "type" : "object" + }, + "RouteRecord" : { + "properties" : { + "connection_id" : { + "type" : "string" + }, + "created_at" : { + "description" : "Time of record creation", + "example" : "2021-12-31T23:59:59Z", + "pattern" : "^\\d{4}-\\d\\d-\\d\\d[T ]\\d\\d:\\d\\d(?:\\:(?:\\d\\d(?:\\.\\d{1,6})?))?(?:[+-]\\d\\d:?\\d\\d|Z|)$", + "type" : "string" + }, + "recipient_key" : { + "type" : "string" + }, + "record_id" : { + "type" : "string" + }, + "role" : { + "type" : "string" + }, + "state" : { + "description" : "Current record state", + "example" : "active", + "type" : "string" + }, + "updated_at" : { + "description" : "Time of last record update", + "example" : "2021-12-31T23:59:59Z", + "pattern" : "^\\d{4}-\\d\\d-\\d\\d[T ]\\d\\d:\\d\\d(?:\\:(?:\\d\\d(?:\\.\\d{1,6})?))?(?:[+-]\\d\\d:?\\d\\d|Z|)$", + "type" : "string" + }, + "wallet_id" : { + "type" : "string" + } }, - "credential_preview" : { - "$ref" : "#/definitions/V20CredPreview" + "required" : [ "recipient_key" ], + "type" : "object" + }, + "Schema" : { + "properties" : { + "attrNames" : { + "description" : "Schema attribute names", + "items" : { + "description" : "Attribute name", + "example" : "score", + "type" : "string" + }, + "type" : "array" + }, + "id" : { + "description" : "Schema identifier", + "example" : "WgWxqztrNooG92RXvxSTWv:2:schema_name:1.0", + "pattern" : "^[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}:2:.+:[0-9.]+$", + "type" : "string" + }, + "name" : { + "description" : "Schema name", + "example" : "schema_name", + "type" : "string" + }, + "seqNo" : { + "description" : "Schema sequence number", + "example" : 10, + "format" : "int32", + "minimum" : 1, + "type" : "integer" + }, + "ver" : { + "description" : "Node protocol version", + "example" : "1.0", + "pattern" : "^[0-9.]+$", + "type" : "string" + }, + "version" : { + "description" : "Schema version", + "example" : "1.0", + "pattern" : "^[0-9.]+$", + "type" : "string" + } }, - "filter" : { - "$ref" : "#/definitions/V20CredBoundOfferRequest_filter" + "type" : "object" + }, + "SchemaGetResult" : { + "properties" : { + "schema" : { + "$ref" : "#/components/schemas/Schema" + } }, - "trace" : { - "type" : "boolean", - "description" : "Record trace information, based on agent configuration" - } - } - }, - "V20CredOfferRequest" : { - "type" : "object", - "required" : [ "connection_id", "filter" ], - "properties" : { - "auto_issue" : { - "type" : "boolean", - "description" : "Whether to respond automatically to credential requests, creating and issuing requested credentials" + "type" : "object" + }, + "SchemaInputDescriptor" : { + "properties" : { + "required" : { + "description" : "Required", + "type" : "boolean" + }, + "uri" : { + "description" : "URI", + "type" : "string" + } }, - "auto_remove" : { - "type" : "boolean", - "description" : "Whether to remove the credential exchange record on completion (overrides --preserve-exchange-records configuration setting)" + "type" : "object" + }, + "SchemaSendRequest" : { + "properties" : { + "attributes" : { + "description" : "List of schema attributes", + "items" : { + "description" : "attribute name", + "example" : "score", + "type" : "string" + }, + "type" : "array" + }, + "schema_name" : { + "description" : "Schema name", + "example" : "prefs", + "type" : "string" + }, + "schema_version" : { + "description" : "Schema version", + "example" : "1.0", + "pattern" : "^[0-9.]+$", + "type" : "string" + } }, - "comment" : { - "type" : "string", - "description" : "Human-readable comment", - "x-nullable" : true + "required" : [ "attributes", "schema_name", "schema_version" ], + "type" : "object" + }, + "SchemaSendResult" : { + "properties" : { + "schema" : { + "$ref" : "#/components/schemas/SchemaSendResult_schema" + }, + "schema_id" : { + "description" : "Schema identifier", + "example" : "WgWxqztrNooG92RXvxSTWv:2:schema_name:1.0", + "pattern" : "^[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}:2:.+:[0-9.]+$", + "type" : "string" + } }, - "connection_id" : { - "type" : "string", - "format" : "uuid", - "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", - "description" : "Connection identifier" + "required" : [ "schema_id" ], + "type" : "object" + }, + "SchemasCreatedResult" : { + "properties" : { + "schema_ids" : { + "items" : { + "description" : "Schema identifiers", + "example" : "WgWxqztrNooG92RXvxSTWv:2:schema_name:1.0", + "pattern" : "^[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}:2:.+:[0-9.]+$", + "type" : "string" + }, + "type" : "array" + } }, - "credential_preview" : { - "$ref" : "#/definitions/V20CredPreview" + "type" : "object" + }, + "SchemasInputDescriptorFilter" : { + "properties" : { + "oneof_filter" : { + "description" : "oneOf", + "type" : "boolean" + }, + "uri_groups" : { + "items" : { + "items" : { + "$ref" : "#/components/schemas/SchemaInputDescriptor" + }, + "type" : "array" + }, + "type" : "array" + } }, - "filter" : { - "$ref" : "#/definitions/V20CredBoundOfferRequest_filter" + "type" : "object" + }, + "SendMenu" : { + "properties" : { + "menu" : { + "$ref" : "#/components/schemas/SendMenu_menu" + } }, - "trace" : { - "type" : "boolean", - "description" : "Record trace information, based on agent configuration" - } - } - }, - "V20CredPreview" : { - "type" : "object", - "required" : [ "attributes" ], - "properties" : { - "@type" : { - "type" : "string", - "example" : "issue-credential/2.0/credential-preview", - "description" : "Message type identifier" + "required" : [ "menu" ], + "type" : "object" + }, + "SendMessage" : { + "properties" : { + "content" : { + "description" : "Message content", + "example" : "Hello", + "type" : "string" + } }, - "attributes" : { - "type" : "array", - "items" : { - "$ref" : "#/definitions/V20CredAttrSpec" + "type" : "object" + }, + "ServiceDecorator" : { + "properties" : { + "recipientKeys" : { + "description" : "List of recipient keys", + "items" : { + "description" : "Recipient public key", + "example" : "H3C2AVvLMv6gmMNam3uVAjZpfkcJCwDwnZn6z3wXmqPV", + "pattern" : "^[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{43,44}$", + "type" : "string" + }, + "type" : "array" + }, + "routingKeys" : { + "description" : "List of routing keys", + "items" : { + "description" : "Routing key", + "example" : "H3C2AVvLMv6gmMNam3uVAjZpfkcJCwDwnZn6z3wXmqPV", + "pattern" : "^[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{43,44}$", + "type" : "string" + }, + "type" : "array" + }, + "serviceEndpoint" : { + "description" : "Service endpoint at which to reach this agent", + "example" : "http://192.168.56.101:8020", + "type" : "string" } - } - } - }, - "V20CredProposal" : { - "type" : "object", - "required" : [ "filters~attach", "formats" ], - "properties" : { - "@id" : { - "type" : "string", - "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", - "description" : "Message identifier" }, - "@type" : { - "type" : "string", - "example" : "https://didcomm.org/my-family/1.0/my-message-type", - "description" : "Message type", - "readOnly" : true + "required" : [ "recipientKeys", "serviceEndpoint" ], + "type" : "object" + }, + "SignRequest" : { + "properties" : { + "doc" : { + "$ref" : "#/components/schemas/Doc" + }, + "verkey" : { + "description" : "Verkey to use for signing", + "type" : "string" + } }, - "comment" : { - "type" : "string", - "description" : "Human-readable comment", - "x-nullable" : true + "required" : [ "doc", "verkey" ], + "type" : "object" + }, + "SignResponse" : { + "properties" : { + "error" : { + "description" : "Error text", + "type" : "string" + }, + "signed_doc" : { + "description" : "Signed document", + "properties" : { }, + "type" : "object" + } }, - "credential_preview" : { - "$ref" : "#/definitions/V20CredProposal_credential_preview" + "type" : "object" + }, + "SignatureOptions" : { + "properties" : { + "challenge" : { + "type" : "string" + }, + "domain" : { + "type" : "string" + }, + "proofPurpose" : { + "type" : "string" + }, + "type" : { + "type" : "string" + }, + "verificationMethod" : { + "type" : "string" + } }, - "filters~attach" : { - "type" : "array", - "description" : "Credential filter per acceptable format on corresponding identifier", - "items" : { - "$ref" : "#/definitions/AttachDecorator" + "required" : [ "proofPurpose", "verificationMethod" ], + "type" : "object" + }, + "SignedDoc" : { + "properties" : { + "proof" : { + "$ref" : "#/components/schemas/SignedDoc_proof" } }, - "formats" : { - "type" : "array", - "description" : "Attachment formats", - "items" : { - "$ref" : "#/definitions/V20CredFormat" + "required" : [ "proof" ], + "type" : "object" + }, + "SubmissionRequirements" : { + "properties" : { + "count" : { + "description" : "Count Value", + "example" : 1234, + "format" : "int32", + "type" : "integer" + }, + "from" : { + "description" : "From", + "type" : "string" + }, + "from_nested" : { + "items" : { + "$ref" : "#/components/schemas/SubmissionRequirements" + }, + "type" : "array" + }, + "max" : { + "description" : "Max Value", + "example" : 1234, + "format" : "int32", + "type" : "integer" + }, + "min" : { + "description" : "Min Value", + "example" : 1234, + "format" : "int32", + "type" : "integer" + }, + "name" : { + "description" : "Name", + "type" : "string" + }, + "purpose" : { + "description" : "Purpose", + "type" : "string" + }, + "rule" : { + "description" : "Selection", + "enum" : [ "all", "pick" ], + "type" : "string" } - } - } - }, - "V20CredRequest" : { - "type" : "object", - "required" : [ "formats", "requests~attach" ], - "properties" : { - "@id" : { - "type" : "string", - "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", - "description" : "Message identifier" }, - "@type" : { - "type" : "string", - "example" : "https://didcomm.org/my-family/1.0/my-message-type", - "description" : "Message type", - "readOnly" : true + "type" : "object" + }, + "TAAAccept" : { + "properties" : { + "mechanism" : { + "type" : "string" + }, + "text" : { + "type" : "string" + }, + "version" : { + "type" : "string" + } }, - "comment" : { - "type" : "string", - "description" : "Human-readable comment", - "x-nullable" : true + "type" : "object" + }, + "TAAAcceptance" : { + "properties" : { + "mechanism" : { + "type" : "string" + }, + "time" : { + "example" : 1640995199, + "format" : "int32", + "maximum" : 18446744073709551615, + "minimum" : 0, + "type" : "integer" + } }, - "formats" : { - "type" : "array", - "description" : "Acceptable attachment formats", - "items" : { - "$ref" : "#/definitions/V20CredFormat" + "type" : "object" + }, + "TAAInfo" : { + "properties" : { + "aml_record" : { + "$ref" : "#/components/schemas/AMLRecord" + }, + "taa_accepted" : { + "$ref" : "#/components/schemas/TAAAcceptance" + }, + "taa_record" : { + "$ref" : "#/components/schemas/TAARecord" + }, + "taa_required" : { + "type" : "boolean" } }, - "requests~attach" : { - "type" : "array", - "description" : "Request attachments", - "items" : { - "$ref" : "#/definitions/AttachDecorator" + "type" : "object" + }, + "TAARecord" : { + "properties" : { + "digest" : { + "type" : "string" + }, + "text" : { + "type" : "string" + }, + "version" : { + "type" : "string" } - } - } - }, - "V20CredRequestFree" : { - "type" : "object", - "required" : [ "connection_id", "filter" ], - "properties" : { - "auto_remove" : { - "type" : "boolean", - "description" : "Whether to remove the credential exchange record on completion (overrides --preserve-exchange-records configuration setting)" - }, - "comment" : { - "type" : "string", - "description" : "Human-readable comment", - "x-nullable" : true - }, - "connection_id" : { - "type" : "string", - "format" : "uuid", - "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", - "description" : "Connection identifier" - }, - "filter" : { - "$ref" : "#/definitions/V20CredBoundOfferRequest_filter" - }, - "holder_did" : { - "type" : "string", - "example" : "did:key:ahsdkjahsdkjhaskjdhakjshdkajhsdkjahs", - "description" : "Holder DID to substitute for the credentialSubject.id", - "x-nullable" : true - }, - "trace" : { - "type" : "boolean", - "example" : false, - "description" : "Whether to trace event (default false)" - } - } - }, - "V20CredRequestRequest" : { - "type" : "object", - "properties" : { - "holder_did" : { - "type" : "string", - "example" : "did:key:ahsdkjahsdkjhaskjdhakjshdkajhsdkjahs", - "description" : "Holder DID to substitute for the credentialSubject.id", - "x-nullable" : true - } - } - }, - "V20CredStoreRequest" : { - "type" : "object", - "properties" : { - "credential_id" : { - "type" : "string" - } - } - }, - "V20IssueCredSchemaCore" : { - "type" : "object", - "required" : [ "filter" ], - "properties" : { - "auto_remove" : { - "type" : "boolean", - "description" : "Whether to remove the credential exchange record on completion (overrides --preserve-exchange-records configuration setting)" }, - "comment" : { - "type" : "string", - "description" : "Human-readable comment", - "x-nullable" : true + "type" : "object" + }, + "TAAResult" : { + "properties" : { + "result" : { + "$ref" : "#/components/schemas/TAAInfo" + } }, - "credential_preview" : { - "$ref" : "#/definitions/V20CredPreview" - }, - "filter" : { - "$ref" : "#/definitions/V20CredBoundOfferRequest_filter" - }, - "trace" : { - "type" : "boolean", - "description" : "Record trace information, based on agent configuration" - } - } - }, - "V20IssueCredentialModuleResponse" : { - "type" : "object" - }, - "V20Pres" : { - "type" : "object", - "required" : [ "formats", "presentations~attach" ], - "properties" : { - "@id" : { - "type" : "string", - "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", - "description" : "Message identifier" - }, - "@type" : { - "type" : "string", - "example" : "https://didcomm.org/my-family/1.0/my-message-type", - "description" : "Message type", - "readOnly" : true - }, - "comment" : { - "type" : "string", - "description" : "Human-readable comment", - "x-nullable" : true - }, - "formats" : { - "type" : "array", - "description" : "Acceptable attachment formats", - "items" : { - "$ref" : "#/definitions/V20PresFormat" + "type" : "object" + }, + "TailsDeleteResponse" : { + "properties" : { + "message" : { + "type" : "string" } }, - "presentations~attach" : { - "type" : "array", - "items" : { - "$ref" : "#/definitions/AttachDecorator" + "type" : "object" + }, + "TransactionJobs" : { + "properties" : { + "transaction_my_job" : { + "description" : "My transaction related job", + "enum" : [ "TRANSACTION_AUTHOR", "TRANSACTION_ENDORSER", "reset" ], + "type" : "string" + }, + "transaction_their_job" : { + "description" : "Their transaction related job", + "enum" : [ "TRANSACTION_AUTHOR", "TRANSACTION_ENDORSER", "reset" ], + "type" : "string" } - } - } - }, - "V20PresCreateRequestRequest" : { - "type" : "object", - "required" : [ "presentation_request" ], - "properties" : { - "comment" : { - "type" : "string", - "x-nullable" : true - }, - "presentation_request" : { - "$ref" : "#/definitions/V20PresRequestByFormat" - }, - "trace" : { - "type" : "boolean", - "example" : false, - "description" : "Whether to trace event (default false)" - } - } - }, - "V20PresExRecord" : { - "type" : "object", - "properties" : { - "auto_present" : { - "type" : "boolean", - "example" : false, - "description" : "Prover choice to auto-present proof as verifier requests" - }, - "by_format" : { - "$ref" : "#/definitions/V20PresExRecord_by_format" - }, - "connection_id" : { - "type" : "string", - "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", - "description" : "Connection identifier" - }, - "created_at" : { - "type" : "string", - "example" : "2021-12-31 23:59:59Z", - "description" : "Time of record creation", - "pattern" : "^\\d{4}-\\d\\d-\\d\\d[T ]\\d\\d:\\d\\d(?:\\:(?:\\d\\d(?:\\.\\d{1,6})?))?(?:[+-]\\d\\d:?\\d\\d|Z|)$" - }, - "error_msg" : { - "type" : "string", - "example" : "Invalid structure", - "description" : "Error message" - }, - "initiator" : { - "type" : "string", - "example" : "self", - "description" : "Present-proof exchange initiator: self or external", - "enum" : [ "self", "external" ] - }, - "pres" : { - "$ref" : "#/definitions/V20PresExRecord_pres" - }, - "pres_ex_id" : { - "type" : "string", - "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", - "description" : "Presentation exchange identifier" - }, - "pres_proposal" : { - "$ref" : "#/definitions/V10PresentationExchange_presentation_proposal_dict" - }, - "pres_request" : { - "$ref" : "#/definitions/V10PresentationExchange_presentation_request_dict" - }, - "role" : { - "type" : "string", - "example" : "prover", - "description" : "Present-proof exchange role: prover or verifier", - "enum" : [ "prover", "verifier" ] - }, - "state" : { - "type" : "string", - "description" : "Present-proof exchange state", - "enum" : [ "proposal-sent", "proposal-received", "request-sent", "request-received", "presentation-sent", "presentation-received", "done", "abandoned" ] - }, - "thread_id" : { - "type" : "string", - "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", - "description" : "Thread identifier" - }, - "trace" : { - "type" : "boolean", - "description" : "Record trace information, based on agent configuration" - }, - "updated_at" : { - "type" : "string", - "example" : "2021-12-31 23:59:59Z", - "description" : "Time of last record update", - "pattern" : "^\\d{4}-\\d\\d-\\d\\d[T ]\\d\\d:\\d\\d(?:\\:(?:\\d\\d(?:\\.\\d{1,6})?))?(?:[+-]\\d\\d:?\\d\\d|Z|)$" - }, - "verified" : { - "type" : "string", - "example" : "true", - "description" : "Whether presentation is verified: 'true' or 'false'", - "enum" : [ "true", "false" ] - } - } - }, - "V20PresExRecordByFormat" : { - "type" : "object", - "properties" : { - "pres" : { - "type" : "object", - "properties" : { } }, - "pres_proposal" : { - "type" : "object", - "properties" : { } - }, - "pres_request" : { - "type" : "object", - "properties" : { } - } - } - }, - "V20PresExRecordList" : { - "type" : "object", - "properties" : { - "results" : { - "type" : "array", - "description" : "Presentation exchange records", - "items" : { - "$ref" : "#/definitions/V20PresExRecord" + "type" : "object" + }, + "TransactionList" : { + "properties" : { + "results" : { + "description" : "List of transaction records", + "items" : { + "$ref" : "#/components/schemas/TransactionRecord" + }, + "type" : "array" } - } - } - }, - "V20PresFormat" : { - "type" : "object", - "required" : [ "attach_id", "format" ], - "properties" : { - "attach_id" : { - "type" : "string", - "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", - "description" : "Attachment identifier" - }, - "format" : { - "type" : "string", - "example" : "dif/presentation-exchange/submission@v1.0", - "description" : "Attachment format specifier" - } - } - }, - "V20PresProblemReportRequest" : { - "type" : "object", - "required" : [ "description" ], - "properties" : { - "description" : { - "type" : "string" - } - } - }, - "V20PresProposal" : { - "type" : "object", - "required" : [ "formats", "proposals~attach" ], - "properties" : { - "@id" : { - "type" : "string", - "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", - "description" : "Message identifier" - }, - "@type" : { - "type" : "string", - "example" : "https://didcomm.org/my-family/1.0/my-message-type", - "description" : "Message type", - "readOnly" : true }, - "comment" : { - "type" : "string", - "description" : "Human-readable comment" - }, - "formats" : { - "type" : "array", - "items" : { - "$ref" : "#/definitions/V20PresFormat" + "type" : "object" + }, + "TransactionRecord" : { + "properties" : { + "_type" : { + "description" : "Transaction type", + "example" : "101", + "type" : "string" + }, + "connection_id" : { + "description" : "The connection identifier for thie particular transaction record", + "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "type" : "string" + }, + "created_at" : { + "description" : "Time of record creation", + "example" : "2021-12-31T23:59:59Z", + "pattern" : "^\\d{4}-\\d\\d-\\d\\d[T ]\\d\\d:\\d\\d(?:\\:(?:\\d\\d(?:\\.\\d{1,6})?))?(?:[+-]\\d\\d:?\\d\\d|Z|)$", + "type" : "string" + }, + "endorser_write_txn" : { + "description" : "If True, Endorser will write the transaction after endorsing it", + "example" : true, + "type" : "boolean" + }, + "formats" : { + "items" : { + "additionalProperties" : { + "type" : "string" + }, + "example" : { + "attach_id" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "format" : "dif/endorse-transaction/request@v1.0" + }, + "type" : "object" + }, + "type" : "array" + }, + "messages_attach" : { + "items" : { + "example" : { + "@id" : "143c458d-1b1c-40c7-ab85-4d16808ddf0a", + "data" : { + "json" : "{\"endorser\": \"V4SGRU86Z58d6TV7PBUe6f\",\"identifier\": \"LjgpST2rjsoxYegQDRm7EL\",\"operation\": {\"data\": {\"attr_names\": [\"first_name\", \"last_name\"],\"name\": \"test_schema\",\"version\": \"2.1\",},\"type\": \"101\",},\"protocolVersion\": 2,\"reqId\": 1597766666168851000,\"signatures\": {\"LjgpST2rjsox\": \"4ATKMn6Y9sTgwqaGTm7py2c2M8x1EVDTWKZArwyuPgjU\"},\"taaAcceptance\": {\"mechanism\": \"manual\",\"taaDigest\": \"f50fe2c2ab977006761d36bd6f23e4c6a7e0fc2feb9f62\",\"time\": 1597708800,}}" + }, + "mime-type" : "application/json" + }, + "properties" : { }, + "type" : "object" + }, + "type" : "array" + }, + "meta_data" : { + "example" : { + "context" : { + "param1" : "param1_value", + "param2" : "param2_value" + }, + "post_process" : [ { + "topic" : "topic_value", + "other" : "other_value" + } ] + }, + "properties" : { }, + "type" : "object" + }, + "signature_request" : { + "items" : { + "example" : { + "author_goal_code" : "aries.transaction.ledger.write", + "context" : "did:sov", + "method" : "add-signature", + "signature_type" : "", + "signer_goal_code" : "aries.transaction.endorse" + }, + "properties" : { }, + "type" : "object" + }, + "type" : "array" + }, + "signature_response" : { + "items" : { + "example" : { + "context" : "did:sov", + "message_id" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "method" : "add-signature", + "signer_goal_code" : "aries.transaction.refuse" + }, + "properties" : { }, + "type" : "object" + }, + "type" : "array" + }, + "state" : { + "description" : "Current record state", + "example" : "active", + "type" : "string" + }, + "thread_id" : { + "description" : "Thread Identifier", + "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "type" : "string" + }, + "timing" : { + "example" : { + "expires_time" : "2020-12-13T17:29:06+0000" + }, + "properties" : { }, + "type" : "object" + }, + "trace" : { + "description" : "Record trace information, based on agent configuration", + "type" : "boolean" + }, + "transaction_id" : { + "description" : "Transaction identifier", + "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "type" : "string" + }, + "updated_at" : { + "description" : "Time of last record update", + "example" : "2021-12-31T23:59:59Z", + "pattern" : "^\\d{4}-\\d\\d-\\d\\d[T ]\\d\\d:\\d\\d(?:\\:(?:\\d\\d(?:\\.\\d{1,6})?))?(?:[+-]\\d\\d:?\\d\\d|Z|)$", + "type" : "string" } }, - "proposals~attach" : { - "type" : "array", - "description" : "Attachment per acceptable format on corresponding identifier", - "items" : { - "$ref" : "#/definitions/AttachDecorator" + "type" : "object" + }, + "TxnOrCredentialDefinitionSendResult" : { + "properties" : { + "sent" : { + "$ref" : "#/components/schemas/CredentialDefinitionSendResult" + }, + "txn" : { + "$ref" : "#/components/schemas/TxnOrCredentialDefinitionSendResult_txn" } - } - } - }, - "V20PresProposalByFormat" : { - "type" : "object", - "properties" : { - "dif" : { - "$ref" : "#/definitions/V20PresProposalByFormat_dif" - }, - "indy" : { - "$ref" : "#/definitions/V20PresProposalByFormat_indy" - } - } - }, - "V20PresProposalRequest" : { - "type" : "object", - "required" : [ "connection_id", "presentation_proposal" ], - "properties" : { - "auto_present" : { - "type" : "boolean", - "description" : "Whether to respond automatically to presentation requests, building and presenting requested proof" - }, - "comment" : { - "type" : "string", - "description" : "Human-readable comment", - "x-nullable" : true - }, - "connection_id" : { - "type" : "string", - "format" : "uuid", - "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", - "description" : "Connection identifier" - }, - "presentation_proposal" : { - "$ref" : "#/definitions/V20PresProposalByFormat" - }, - "trace" : { - "type" : "boolean", - "example" : false, - "description" : "Whether to trace event (default false)" - } - } - }, - "V20PresRequest" : { - "type" : "object", - "required" : [ "formats", "request_presentations~attach" ], - "properties" : { - "@id" : { - "type" : "string", - "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", - "description" : "Message identifier" }, - "@type" : { - "type" : "string", - "example" : "https://didcomm.org/my-family/1.0/my-message-type", - "description" : "Message type", - "readOnly" : true - }, - "comment" : { - "type" : "string", - "description" : "Human-readable comment" - }, - "formats" : { - "type" : "array", - "items" : { - "$ref" : "#/definitions/V20PresFormat" + "type" : "object" + }, + "TxnOrPublishRevocationsResult" : { + "properties" : { + "sent" : { + "$ref" : "#/components/schemas/PublishRevocations" + }, + "txn" : { + "$ref" : "#/components/schemas/TxnOrPublishRevocationsResult_txn" } }, - "request_presentations~attach" : { - "type" : "array", - "description" : "Attachment per acceptable format on corresponding identifier", - "items" : { - "$ref" : "#/definitions/AttachDecorator" + "type" : "object" + }, + "TxnOrRegisterLedgerNymResponse" : { + "properties" : { + "success" : { + "description" : "Success of nym registration operation", + "example" : true, + "type" : "boolean" + }, + "txn" : { + "$ref" : "#/components/schemas/TxnOrRegisterLedgerNymResponse_txn" } }, - "will_confirm" : { - "type" : "boolean", - "description" : "Whether verifier will send confirmation ack" - } - } - }, - "V20PresRequestByFormat" : { - "type" : "object", - "properties" : { - "dif" : { - "$ref" : "#/definitions/V20PresRequestByFormat_dif" - }, - "indy" : { - "$ref" : "#/definitions/V20PresRequestByFormat_indy" - } - } - }, - "V20PresSendRequestRequest" : { - "type" : "object", - "required" : [ "connection_id", "presentation_request" ], - "properties" : { - "comment" : { - "type" : "string", - "x-nullable" : true - }, - "connection_id" : { - "type" : "string", - "format" : "uuid", - "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", - "description" : "Connection identifier" - }, - "presentation_request" : { - "$ref" : "#/definitions/V20PresRequestByFormat" - }, - "trace" : { - "type" : "boolean", - "example" : false, - "description" : "Whether to trace event (default false)" - } - } - }, - "V20PresSpecByFormatRequest" : { - "type" : "object", - "properties" : { - "dif" : { - "$ref" : "#/definitions/V20PresSpecByFormatRequest_dif" - }, - "indy" : { - "$ref" : "#/definitions/V20PresSpecByFormatRequest_indy" - }, - "trace" : { - "type" : "boolean", - "description" : "Record trace information, based on agent configuration" - } - } - }, - "V20PresentProofModuleResponse" : { - "type" : "object" - }, - "VCRecord" : { - "type" : "object", - "properties" : { - "contexts" : { - "type" : "array", - "items" : { - "type" : "string", - "example" : "https://myhost:8021", - "description" : "Context", - "pattern" : "^[A-Za-z0-9\\.\\-\\+]+://([A-Za-z0-9][.A-Za-z0-9-_]+[A-Za-z0-9])+(:[1-9][0-9]*)?(/[^?&#]+)?$" + "type" : "object" + }, + "TxnOrRevRegResult" : { + "properties" : { + "sent" : { + "$ref" : "#/components/schemas/RevRegResult" + }, + "txn" : { + "$ref" : "#/components/schemas/TxnOrRevRegResult_txn" } }, - "cred_tags" : { - "type" : "object", - "additionalProperties" : { - "type" : "string", - "description" : "Retrieval tag value" + "type" : "object" + }, + "TxnOrSchemaSendResult" : { + "properties" : { + "sent" : { + "$ref" : "#/components/schemas/TxnOrSchemaSendResult_sent" + }, + "txn" : { + "$ref" : "#/components/schemas/TxnOrSchemaSendResult_txn" } }, - "cred_value" : { - "type" : "object", - "description" : "(JSON-serializable) credential value", - "properties" : { } + "type" : "object" + }, + "UpdateWalletRequest" : { + "properties" : { + "image_url" : { + "description" : "Image url for this wallet. This image url is publicized (self-attested) to other agents as part of forming a connection.", + "example" : "https://aries.ca/images/sample.png", + "type" : "string" + }, + "label" : { + "description" : "Label for this wallet. This label is publicized (self-attested) to other agents as part of forming a connection.", + "example" : "Alice", + "type" : "string" + }, + "wallet_dispatch_type" : { + "description" : "Webhook target dispatch type for this wallet. default - Dispatch only to webhooks associated with this wallet. base - Dispatch only to webhooks associated with the base wallet. both - Dispatch to both webhook targets.", + "enum" : [ "default", "both", "base" ], + "example" : "default", + "type" : "string" + }, + "wallet_webhook_urls" : { + "description" : "List of Webhook URLs associated with this subwallet", + "items" : { + "description" : "Optional webhook URL to receive webhook messages", + "example" : "http://localhost:8022/webhooks", + "type" : "string" + }, + "type" : "array" + } }, - "expanded_types" : { - "type" : "array", - "items" : { - "type" : "string", - "example" : "https://w3id.org/citizenship#PermanentResidentCard", - "description" : "JSON-LD expanded type extracted from type and context" + "type" : "object" + }, + "V10CredentialBoundOfferRequest" : { + "properties" : { + "counter_proposal" : { + "$ref" : "#/components/schemas/V10CredentialBoundOfferRequest_counter_proposal" } }, - "given_id" : { - "type" : "string", - "example" : "http://example.edu/credentials/3732", - "description" : "Credential identifier" + "type" : "object" + }, + "V10CredentialConnFreeOfferRequest" : { + "properties" : { + "auto_issue" : { + "description" : "Whether to respond automatically to credential requests, creating and issuing requested credentials", + "type" : "boolean" + }, + "auto_remove" : { + "description" : "Whether to remove the credential exchange record on completion (overrides --preserve-exchange-records configuration setting)", + "type" : "boolean" + }, + "comment" : { + "description" : "Human-readable comment", + "nullable" : true, + "type" : "string" + }, + "cred_def_id" : { + "description" : "Credential definition identifier", + "example" : "WgWxqztrNooG92RXvxSTWv:3:CL:20:tag", + "pattern" : "^([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}):3:CL:(([1-9][0-9]*)|([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}:2:.+:[0-9.]+)):(.+)?$", + "type" : "string" + }, + "credential_preview" : { + "$ref" : "#/components/schemas/CredentialPreview" + }, + "trace" : { + "description" : "Record trace information, based on agent configuration", + "type" : "boolean" + } }, - "issuer_id" : { - "type" : "string", - "example" : "https://example.edu/issuers/14", - "description" : "Issuer identifier" + "required" : [ "cred_def_id", "credential_preview" ], + "type" : "object" + }, + "V10CredentialCreate" : { + "properties" : { + "auto_remove" : { + "description" : "Whether to remove the credential exchange record on completion (overrides --preserve-exchange-records configuration setting)", + "type" : "boolean" + }, + "comment" : { + "description" : "Human-readable comment", + "nullable" : true, + "type" : "string" + }, + "cred_def_id" : { + "description" : "Credential definition identifier", + "example" : "WgWxqztrNooG92RXvxSTWv:3:CL:20:tag", + "pattern" : "^([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}):3:CL:(([1-9][0-9]*)|([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}:2:.+:[0-9.]+)):(.+)?$", + "type" : "string" + }, + "credential_proposal" : { + "$ref" : "#/components/schemas/CredentialPreview" + }, + "issuer_did" : { + "description" : "Credential issuer DID", + "example" : "WgWxqztrNooG92RXvxSTWv", + "pattern" : "^(did:sov:)?[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}$", + "type" : "string" + }, + "schema_id" : { + "description" : "Schema identifier", + "example" : "WgWxqztrNooG92RXvxSTWv:2:schema_name:1.0", + "pattern" : "^[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}:2:.+:[0-9.]+$", + "type" : "string" + }, + "schema_issuer_did" : { + "description" : "Schema issuer DID", + "example" : "WgWxqztrNooG92RXvxSTWv", + "pattern" : "^(did:sov:)?[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}$", + "type" : "string" + }, + "schema_name" : { + "description" : "Schema name", + "example" : "preferences", + "type" : "string" + }, + "schema_version" : { + "description" : "Schema version", + "example" : "1.0", + "pattern" : "^[0-9.]+$", + "type" : "string" + }, + "trace" : { + "description" : "Record trace information, based on agent configuration", + "type" : "boolean" + } }, - "proof_types" : { - "type" : "array", - "items" : { - "type" : "string", - "example" : "Ed25519Signature2018", - "description" : "Signature suite used for proof" + "required" : [ "credential_proposal" ], + "type" : "object" + }, + "V10CredentialExchange" : { + "properties" : { + "auto_issue" : { + "description" : "Issuer choice to issue to request in this credential exchange", + "example" : false, + "type" : "boolean" + }, + "auto_offer" : { + "description" : "Holder choice to accept offer in this credential exchange", + "example" : false, + "type" : "boolean" + }, + "auto_remove" : { + "description" : "Issuer choice to remove this credential exchange record when complete", + "example" : false, + "type" : "boolean" + }, + "connection_id" : { + "description" : "Connection identifier", + "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "type" : "string" + }, + "created_at" : { + "description" : "Time of record creation", + "example" : "2021-12-31T23:59:59Z", + "pattern" : "^\\d{4}-\\d\\d-\\d\\d[T ]\\d\\d:\\d\\d(?:\\:(?:\\d\\d(?:\\.\\d{1,6})?))?(?:[+-]\\d\\d:?\\d\\d|Z|)$", + "type" : "string" + }, + "credential" : { + "$ref" : "#/components/schemas/V10CredentialExchange_credential" + }, + "credential_definition_id" : { + "description" : "Credential definition identifier", + "example" : "WgWxqztrNooG92RXvxSTWv:3:CL:20:tag", + "pattern" : "^([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}):3:CL:(([1-9][0-9]*)|([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}:2:.+:[0-9.]+)):(.+)?$", + "type" : "string" + }, + "credential_exchange_id" : { + "description" : "Credential exchange identifier", + "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "type" : "string" + }, + "credential_id" : { + "description" : "Credential identifier", + "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "type" : "string" + }, + "credential_offer" : { + "$ref" : "#/components/schemas/V10CredentialExchange_credential_offer" + }, + "credential_offer_dict" : { + "$ref" : "#/components/schemas/V10CredentialExchange_credential_offer_dict" + }, + "credential_proposal_dict" : { + "$ref" : "#/components/schemas/V10CredentialExchange_credential_proposal_dict" + }, + "credential_request" : { + "$ref" : "#/components/schemas/V10CredentialExchange_credential_request" + }, + "credential_request_metadata" : { + "description" : "(Indy) credential request metadata", + "properties" : { }, + "type" : "object" + }, + "error_msg" : { + "description" : "Error message", + "example" : "Credential definition identifier is not set in proposal", + "type" : "string" + }, + "initiator" : { + "description" : "Issue-credential exchange initiator: self or external", + "enum" : [ "self", "external" ], + "example" : "self", + "type" : "string" + }, + "parent_thread_id" : { + "description" : "Parent thread identifier", + "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "type" : "string" + }, + "raw_credential" : { + "$ref" : "#/components/schemas/V10CredentialExchange_raw_credential" + }, + "revoc_reg_id" : { + "description" : "Revocation registry identifier", + "type" : "string" + }, + "revocation_id" : { + "description" : "Credential identifier within revocation registry", + "type" : "string" + }, + "role" : { + "description" : "Issue-credential exchange role: holder or issuer", + "enum" : [ "holder", "issuer" ], + "example" : "issuer", + "type" : "string" + }, + "schema_id" : { + "description" : "Schema identifier", + "example" : "WgWxqztrNooG92RXvxSTWv:2:schema_name:1.0", + "pattern" : "^[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}:2:.+:[0-9.]+$", + "type" : "string" + }, + "state" : { + "description" : "Issue-credential exchange state", + "example" : "credential_acked", + "type" : "string" + }, + "thread_id" : { + "description" : "Thread identifier", + "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "type" : "string" + }, + "trace" : { + "description" : "Record trace information, based on agent configuration", + "type" : "boolean" + }, + "updated_at" : { + "description" : "Time of last record update", + "example" : "2021-12-31T23:59:59Z", + "pattern" : "^\\d{4}-\\d\\d-\\d\\d[T ]\\d\\d:\\d\\d(?:\\:(?:\\d\\d(?:\\.\\d{1,6})?))?(?:[+-]\\d\\d:?\\d\\d|Z|)$", + "type" : "string" } }, - "record_id" : { - "type" : "string", - "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", - "description" : "Record identifier" + "type" : "object" + }, + "V10CredentialExchangeListResult" : { + "properties" : { + "results" : { + "description" : "Aries#0036 v1.0 credential exchange records", + "items" : { + "$ref" : "#/components/schemas/V10CredentialExchange" + }, + "type" : "array" + } }, - "schema_ids" : { - "type" : "array", - "items" : { - "type" : "string", - "example" : "https://example.org/examples/degree.json", - "description" : "Schema identifier" + "type" : "object" + }, + "V10CredentialFreeOfferRequest" : { + "properties" : { + "auto_issue" : { + "description" : "Whether to respond automatically to credential requests, creating and issuing requested credentials", + "type" : "boolean" + }, + "auto_remove" : { + "description" : "Whether to remove the credential exchange record on completion (overrides --preserve-exchange-records configuration setting)", + "type" : "boolean" + }, + "comment" : { + "description" : "Human-readable comment", + "nullable" : true, + "type" : "string" + }, + "connection_id" : { + "description" : "Connection identifier", + "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "format" : "uuid", + "type" : "string" + }, + "cred_def_id" : { + "description" : "Credential definition identifier", + "example" : "WgWxqztrNooG92RXvxSTWv:3:CL:20:tag", + "pattern" : "^([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}):3:CL:(([1-9][0-9]*)|([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}:2:.+:[0-9.]+)):(.+)?$", + "type" : "string" + }, + "credential_preview" : { + "$ref" : "#/components/schemas/CredentialPreview" + }, + "trace" : { + "description" : "Record trace information, based on agent configuration", + "type" : "boolean" } }, - "subject_ids" : { - "type" : "array", - "items" : { - "type" : "string", - "example" : "did:example:ebfeb1f712ebc6f1c276e12ec21", - "description" : "Subject identifier" + "required" : [ "connection_id", "cred_def_id", "credential_preview" ], + "type" : "object" + }, + "V10CredentialIssueRequest" : { + "properties" : { + "comment" : { + "description" : "Human-readable comment", + "nullable" : true, + "type" : "string" } - } - } - }, - "VCRecordList" : { - "type" : "object", - "properties" : { - "results" : { - "type" : "array", - "items" : { - "$ref" : "#/definitions/VCRecord" + }, + "type" : "object" + }, + "V10CredentialProblemReportRequest" : { + "properties" : { + "description" : { + "type" : "string" } - } - } - }, - "VerifyRequest" : { - "type" : "object", - "required" : [ "doc" ], - "properties" : { - "doc" : { - "$ref" : "#/definitions/VerifyRequest_doc" }, - "verkey" : { - "type" : "string", - "description" : "Verkey to use for doc verification" - } - } - }, - "VerifyResponse" : { - "type" : "object", - "required" : [ "valid" ], - "properties" : { - "error" : { - "type" : "string", - "description" : "Error text" + "required" : [ "description" ], + "type" : "object" + }, + "V10CredentialProposalRequestMand" : { + "properties" : { + "auto_remove" : { + "description" : "Whether to remove the credential exchange record on completion (overrides --preserve-exchange-records configuration setting)", + "type" : "boolean" + }, + "comment" : { + "description" : "Human-readable comment", + "nullable" : true, + "type" : "string" + }, + "connection_id" : { + "description" : "Connection identifier", + "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "format" : "uuid", + "type" : "string" + }, + "cred_def_id" : { + "description" : "Credential definition identifier", + "example" : "WgWxqztrNooG92RXvxSTWv:3:CL:20:tag", + "pattern" : "^([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}):3:CL:(([1-9][0-9]*)|([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}:2:.+:[0-9.]+)):(.+)?$", + "type" : "string" + }, + "credential_proposal" : { + "$ref" : "#/components/schemas/CredentialPreview" + }, + "issuer_did" : { + "description" : "Credential issuer DID", + "example" : "WgWxqztrNooG92RXvxSTWv", + "pattern" : "^(did:sov:)?[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}$", + "type" : "string" + }, + "schema_id" : { + "description" : "Schema identifier", + "example" : "WgWxqztrNooG92RXvxSTWv:2:schema_name:1.0", + "pattern" : "^[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}:2:.+:[0-9.]+$", + "type" : "string" + }, + "schema_issuer_did" : { + "description" : "Schema issuer DID", + "example" : "WgWxqztrNooG92RXvxSTWv", + "pattern" : "^(did:sov:)?[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}$", + "type" : "string" + }, + "schema_name" : { + "description" : "Schema name", + "example" : "preferences", + "type" : "string" + }, + "schema_version" : { + "description" : "Schema version", + "example" : "1.0", + "pattern" : "^[0-9.]+$", + "type" : "string" + }, + "trace" : { + "description" : "Record trace information, based on agent configuration", + "type" : "boolean" + } }, - "valid" : { - "type" : "boolean" - } - } - }, - "W3CCredentialsListRequest" : { - "type" : "object", - "properties" : { - "contexts" : { - "type" : "array", - "items" : { - "type" : "string", - "example" : "https://myhost:8021", - "description" : "Credential context to match", - "pattern" : "^[A-Za-z0-9\\.\\-\\+]+://([A-Za-z0-9][.A-Za-z0-9-_]+[A-Za-z0-9])+(:[1-9][0-9]*)?(/[^?&#]+)?$" + "required" : [ "connection_id", "credential_proposal" ], + "type" : "object" + }, + "V10CredentialProposalRequestOpt" : { + "properties" : { + "auto_remove" : { + "description" : "Whether to remove the credential exchange record on completion (overrides --preserve-exchange-records configuration setting)", + "type" : "boolean" + }, + "comment" : { + "description" : "Human-readable comment", + "nullable" : true, + "type" : "string" + }, + "connection_id" : { + "description" : "Connection identifier", + "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "format" : "uuid", + "type" : "string" + }, + "cred_def_id" : { + "description" : "Credential definition identifier", + "example" : "WgWxqztrNooG92RXvxSTWv:3:CL:20:tag", + "pattern" : "^([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}):3:CL:(([1-9][0-9]*)|([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}:2:.+:[0-9.]+)):(.+)?$", + "type" : "string" + }, + "credential_proposal" : { + "$ref" : "#/components/schemas/CredentialPreview" + }, + "issuer_did" : { + "description" : "Credential issuer DID", + "example" : "WgWxqztrNooG92RXvxSTWv", + "pattern" : "^(did:sov:)?[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}$", + "type" : "string" + }, + "schema_id" : { + "description" : "Schema identifier", + "example" : "WgWxqztrNooG92RXvxSTWv:2:schema_name:1.0", + "pattern" : "^[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}:2:.+:[0-9.]+$", + "type" : "string" + }, + "schema_issuer_did" : { + "description" : "Schema issuer DID", + "example" : "WgWxqztrNooG92RXvxSTWv", + "pattern" : "^(did:sov:)?[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}$", + "type" : "string" + }, + "schema_name" : { + "description" : "Schema name", + "example" : "preferences", + "type" : "string" + }, + "schema_version" : { + "description" : "Schema version", + "example" : "1.0", + "pattern" : "^[0-9.]+$", + "type" : "string" + }, + "trace" : { + "description" : "Record trace information, based on agent configuration", + "type" : "boolean" } }, - "given_id" : { - "type" : "string", - "description" : "Given credential id to match" + "required" : [ "connection_id" ], + "type" : "object" + }, + "V10CredentialStoreRequest" : { + "properties" : { + "credential_id" : { + "type" : "string" + } }, - "issuer_id" : { - "type" : "string", - "description" : "Credential issuer identifier to match" + "type" : "object" + }, + "V10DiscoveryExchangeListResult" : { + "properties" : { + "results" : { + "items" : { + "$ref" : "#/components/schemas/V10DiscoveryExchangeListResult_results_inner" + }, + "type" : "array" + } }, - "max_results" : { - "type" : "integer", - "format" : "int32", - "description" : "Maximum number of results to return" + "type" : "object" + }, + "V10DiscoveryRecord" : { + "properties" : { + "connection_id" : { + "description" : "Connection identifier", + "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "type" : "string" + }, + "created_at" : { + "description" : "Time of record creation", + "example" : "2021-12-31T23:59:59Z", + "pattern" : "^\\d{4}-\\d\\d-\\d\\d[T ]\\d\\d:\\d\\d(?:\\:(?:\\d\\d(?:\\.\\d{1,6})?))?(?:[+-]\\d\\d:?\\d\\d|Z|)$", + "type" : "string" + }, + "disclose" : { + "$ref" : "#/components/schemas/V10DiscoveryRecord_disclose" + }, + "discovery_exchange_id" : { + "description" : "Credential exchange identifier", + "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "type" : "string" + }, + "query_msg" : { + "$ref" : "#/components/schemas/V10DiscoveryRecord_query_msg" + }, + "state" : { + "description" : "Current record state", + "example" : "active", + "type" : "string" + }, + "thread_id" : { + "description" : "Thread identifier", + "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "type" : "string" + }, + "trace" : { + "description" : "Record trace information, based on agent configuration", + "type" : "boolean" + }, + "updated_at" : { + "description" : "Time of last record update", + "example" : "2021-12-31T23:59:59Z", + "pattern" : "^\\d{4}-\\d\\d-\\d\\d[T ]\\d\\d:\\d\\d(?:\\:(?:\\d\\d(?:\\.\\d{1,6})?))?(?:[+-]\\d\\d:?\\d\\d|Z|)$", + "type" : "string" + } }, - "proof_types" : { - "type" : "array", - "items" : { - "type" : "string", - "example" : "Ed25519Signature2018", - "description" : "Signature suite used for proof" + "type" : "object" + }, + "V10PresentProofModuleResponse" : { + "type" : "object" + }, + "V10PresentationCreateRequestRequest" : { + "properties" : { + "auto_verify" : { + "description" : "Verifier choice to auto-verify proof presentation", + "example" : false, + "type" : "boolean" + }, + "comment" : { + "nullable" : true, + "type" : "string" + }, + "proof_request" : { + "$ref" : "#/components/schemas/IndyProofRequest" + }, + "trace" : { + "description" : "Whether to trace event (default false)", + "example" : false, + "type" : "boolean" } }, - "schema_ids" : { - "type" : "array", - "description" : "Schema identifiers, all of which to match", - "items" : { - "type" : "string", - "example" : "https://myhost:8021", - "description" : "Credential schema identifier", - "pattern" : "^[A-Za-z0-9\\.\\-\\+]+://([A-Za-z0-9][.A-Za-z0-9-_]+[A-Za-z0-9])+(:[1-9][0-9]*)?(/[^?&#]+)?$" + "required" : [ "proof_request" ], + "type" : "object" + }, + "V10PresentationExchange" : { + "properties" : { + "auto_present" : { + "description" : "Prover choice to auto-present proof as verifier requests", + "example" : false, + "type" : "boolean" + }, + "auto_verify" : { + "description" : "Verifier choice to auto-verify proof presentation", + "type" : "boolean" + }, + "connection_id" : { + "description" : "Connection identifier", + "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "type" : "string" + }, + "created_at" : { + "description" : "Time of record creation", + "example" : "2021-12-31T23:59:59Z", + "pattern" : "^\\d{4}-\\d\\d-\\d\\d[T ]\\d\\d:\\d\\d(?:\\:(?:\\d\\d(?:\\.\\d{1,6})?))?(?:[+-]\\d\\d:?\\d\\d|Z|)$", + "type" : "string" + }, + "error_msg" : { + "description" : "Error message", + "example" : "Invalid structure", + "type" : "string" + }, + "initiator" : { + "description" : "Present-proof exchange initiator: self or external", + "enum" : [ "self", "external" ], + "example" : "self", + "type" : "string" + }, + "presentation" : { + "$ref" : "#/components/schemas/V10PresentationExchange_presentation" + }, + "presentation_exchange_id" : { + "description" : "Presentation exchange identifier", + "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "type" : "string" + }, + "presentation_proposal_dict" : { + "$ref" : "#/components/schemas/V10PresentationExchange_presentation_proposal_dict" + }, + "presentation_request" : { + "$ref" : "#/components/schemas/V10PresentationExchange_presentation_request" + }, + "presentation_request_dict" : { + "$ref" : "#/components/schemas/V10PresentationExchange_presentation_request_dict" + }, + "role" : { + "description" : "Present-proof exchange role: prover or verifier", + "enum" : [ "prover", "verifier" ], + "example" : "prover", + "type" : "string" + }, + "state" : { + "description" : "Present-proof exchange state", + "example" : "verified", + "type" : "string" + }, + "thread_id" : { + "description" : "Thread identifier", + "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "type" : "string" + }, + "trace" : { + "description" : "Record trace information, based on agent configuration", + "type" : "boolean" + }, + "updated_at" : { + "description" : "Time of last record update", + "example" : "2021-12-31T23:59:59Z", + "pattern" : "^\\d{4}-\\d\\d-\\d\\d[T ]\\d\\d:\\d\\d(?:\\:(?:\\d\\d(?:\\.\\d{1,6})?))?(?:[+-]\\d\\d:?\\d\\d|Z|)$", + "type" : "string" + }, + "verified" : { + "description" : "Whether presentation is verified: true or false", + "enum" : [ "true", "false" ], + "example" : "true", + "type" : "string" + }, + "verified_msgs" : { + "items" : { + "description" : "Proof verification warning or error information", + "type" : "string" + }, + "type" : "array" } }, - "subject_ids" : { - "type" : "array", - "description" : "Subject identifiers, all of which to match", - "items" : { - "type" : "string", - "description" : "Subject identifier" + "type" : "object" + }, + "V10PresentationExchangeList" : { + "properties" : { + "results" : { + "description" : "Aries RFC 37 v1.0 presentation exchange records", + "items" : { + "$ref" : "#/components/schemas/V10PresentationExchange" + }, + "type" : "array" } }, - "tag_query" : { - "type" : "object", - "description" : "Tag filter", - "additionalProperties" : { - "type" : "string", - "description" : "Tag value" + "type" : "object" + }, + "V10PresentationProblemReportRequest" : { + "properties" : { + "description" : { + "type" : "string" } }, - "types" : { - "type" : "array", - "items" : { - "type" : "string", - "example" : "https://myhost:8021", - "description" : "Credential type to match", - "pattern" : "^[A-Za-z0-9\\.\\-\\+]+://([A-Za-z0-9][.A-Za-z0-9-_]+[A-Za-z0-9])+(:[1-9][0-9]*)?(/[^?&#]+)?$" + "required" : [ "description" ], + "type" : "object" + }, + "V10PresentationProposalRequest" : { + "properties" : { + "auto_present" : { + "description" : "Whether to respond automatically to presentation requests, building and presenting requested proof", + "type" : "boolean" + }, + "comment" : { + "description" : "Human-readable comment", + "nullable" : true, + "type" : "string" + }, + "connection_id" : { + "description" : "Connection identifier", + "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "format" : "uuid", + "type" : "string" + }, + "presentation_proposal" : { + "$ref" : "#/components/schemas/IndyPresPreview" + }, + "trace" : { + "description" : "Whether to trace event (default false)", + "example" : false, + "type" : "boolean" } - } - } - }, - "WalletList" : { - "type" : "object", - "properties" : { - "results" : { - "type" : "array", - "description" : "List of wallet records", - "items" : { - "$ref" : "#/definitions/WalletRecord" + }, + "required" : [ "connection_id", "presentation_proposal" ], + "type" : "object" + }, + "V10PresentationSendRequestRequest" : { + "properties" : { + "auto_verify" : { + "description" : "Verifier choice to auto-verify proof presentation", + "example" : false, + "type" : "boolean" + }, + "comment" : { + "nullable" : true, + "type" : "string" + }, + "connection_id" : { + "description" : "Connection identifier", + "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "format" : "uuid", + "type" : "string" + }, + "proof_request" : { + "$ref" : "#/components/schemas/IndyProofRequest" + }, + "trace" : { + "description" : "Whether to trace event (default false)", + "example" : false, + "type" : "boolean" } - } - } - }, - "WalletModuleResponse" : { - "type" : "object" - }, - "WalletRecord" : { - "type" : "object", - "required" : [ "key_management_mode", "wallet_id" ], - "properties" : { - "created_at" : { - "type" : "string", - "example" : "2021-12-31 23:59:59Z", - "description" : "Time of record creation", - "pattern" : "^\\d{4}-\\d\\d-\\d\\d[T ]\\d\\d:\\d\\d(?:\\:(?:\\d\\d(?:\\.\\d{1,6})?))?(?:[+-]\\d\\d:?\\d\\d|Z|)$" - }, - "key_management_mode" : { - "type" : "string", - "description" : "Mode regarding management of wallet key", - "enum" : [ "managed", "unmanaged" ] - }, - "settings" : { - "type" : "object", - "description" : "Settings for this wallet.", - "properties" : { } - }, - "state" : { - "type" : "string", - "example" : "active", - "description" : "Current record state" - }, - "updated_at" : { - "type" : "string", - "example" : "2021-12-31 23:59:59Z", - "description" : "Time of last record update", - "pattern" : "^\\d{4}-\\d\\d-\\d\\d[T ]\\d\\d:\\d\\d(?:\\:(?:\\d\\d(?:\\.\\d{1,6})?))?(?:[+-]\\d\\d:?\\d\\d|Z|)$" - }, - "wallet_id" : { - "type" : "string", - "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", - "description" : "Wallet record ID" - } - } - }, - "ActionMenuFetchResult_result" : { - "type" : "object", - "description" : "Action menu" - }, - "AttachDecoratorData_jws" : { - "type" : "object", - "description" : "Detached Java Web Signature" - }, - "CredDefValue_primary" : { - "type" : "object", - "description" : "Primary value for credential definition" - }, - "CredDefValue_revocation" : { - "type" : "object", - "description" : "Revocation value for credential definition" - }, - "Credential_proof" : { - "type" : "object", - "description" : "The proof of the credential", - "example" : "{\"created\":\"2019-12-11T03:50:55\",\"jws\":\"eyJhbGciOiAiRWREU0EiLCAiYjY0IjogZmFsc2UsICJjcml0JiNjQiXX0..lKJU0Df_keblRKhZAS9Qq6zybm-HqUXNVZ8vgEPNTAjQKBhQDxvXNo7nvtUBb_Eq1Ch6YBKY5qBQ\",\"proofPurpose\":\"assertionMethod\",\"type\":\"Ed25519Signature2018\",\"verificationMethod\":\"did:key:z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL#z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL\"}" - }, - "CredentialDefinition_value" : { - "type" : "object", - "description" : "Credential definition primary and revocation values" - }, - "DIDCreate_options" : { - "type" : "object", - "description" : "To define a key type for a did:key" - }, - "DIDXRequest_did_docattach" : { - "type" : "object", - "description" : "As signed attachment, DID Doc associated with DID" - }, - "Doc_options" : { - "type" : "object", - "description" : "Signature options" - }, - "IndyCredAbstract_key_correctness_proof" : { - "type" : "object", - "description" : "Key correctness proof" - }, - "IndyCredPrecis_cred_info" : { - "type" : "object", - "description" : "Credential info" - }, - "IndyCredPrecis_interval" : { - "type" : "object", - "description" : "Non-revocation interval from presentation request" - }, - "IndyPrimaryProof_eq_proof" : { - "type" : "object", - "description" : "Indy equality proof", - "x-nullable" : true - }, - "IndyProof_proof" : { - "type" : "object", - "description" : "Indy proof.proof content" - }, - "IndyProof_requested_proof" : { - "type" : "object", - "description" : "Indy proof.requested_proof content" - }, - "IndyProofProof_aggregated_proof" : { - "type" : "object", - "description" : "Indy proof aggregated proof" - }, - "IndyProofProofProofsProof_non_revoc_proof" : { - "type" : "object", - "description" : "Indy non-revocation proof", - "x-nullable" : true - }, - "IndyProofProofProofsProof_primary_proof" : { - "type" : "object", - "description" : "Indy primary proof" - }, - "IndyProofReqAttrSpec_non_revoked" : { - "type" : "object", - "x-nullable" : true - }, - "IndyRevRegDef_value" : { - "type" : "object", - "description" : "Revocation registry definition value" - }, - "IndyRevRegDefValue_publicKeys" : { - "type" : "object", - "description" : "Public keys" - }, - "IndyRevRegEntry_value" : { - "type" : "object", - "description" : "Revocation registry entry value" - }, - "InvitationRecord_invitation" : { - "type" : "object", - "description" : "Out of band invitation message" - }, - "IssuerRevRegRecord_revoc_reg_def" : { - "type" : "object", - "description" : "Revocation registry definition" - }, - "IssuerRevRegRecord_revoc_reg_entry" : { - "type" : "object", - "description" : "Revocation registry entry" - }, - "KeylistQuery_paginate" : { - "type" : "object", - "description" : "Pagination info" - }, - "LDProofVCDetail_credential" : { - "type" : "object", - "description" : "Detail of the JSON-LD Credential to be issued", - "example" : "{\"@context\":[\"https://www.w3.org/2018/credentials/v1\",\"https://w3id.org/citizenship/v1\"],\"credentialSubject\":{\"familyName\":\"SMITH\",\"gender\":\"Male\",\"givenName\":\"JOHN\",\"type\":[\"PermanentResident\",\"Person\"]},\"description\":\"Government of Example Permanent Resident Card.\",\"identifier\":\"83627465\",\"issuanceDate\":\"2019-12-03T12:19:52Z\",\"issuer\":\"did:key:z6MkmjY8GnV5i9YTDtPETC2uUAW6ejw3nk5mXF5yci5ab7th\",\"name\":\"Permanent Resident Card\",\"type\":[\"VerifiableCredential\",\"PermanentResidentCard\"]}" - }, - "LDProofVCDetail_options" : { - "type" : "object", - "description" : "Options for specifying how the linked data proof is created.", - "example" : "{\"proofType\":\"Ed25519Signature2018\"}" - }, - "LDProofVCDetailOptions_credentialStatus" : { - "type" : "object", - "description" : "The credential status mechanism to use for the credential. Omitting the property indicates the issued credential will not include a credential status" - }, - "SchemaSendResult_schema" : { - "type" : "object", - "description" : "Schema definition" - }, - "SendMenu_menu" : { - "type" : "object", - "description" : "Menu to send to connection" - }, - "SignedDoc_proof" : { - "type" : "object", - "description" : "Linked data proof" - }, - "TxnOrCredentialDefinitionSendResult_txn" : { - "type" : "object", - "description" : "Credential definition transaction to endorse" - }, - "TxnOrPublishRevocationsResult_txn" : { - "type" : "object", - "description" : "Revocation registry revocations transaction to endorse" - }, - "TxnOrRevRegResult_txn" : { - "type" : "object", - "description" : "Revocation registry definition transaction to endorse" - }, - "TxnOrSchemaSendResult_sent" : { - "type" : "object", - "description" : "Content sent" - }, - "TxnOrSchemaSendResult_txn" : { - "type" : "object", - "description" : "Schema transaction to endorse" - }, - "V10CredentialBoundOfferRequest_counter_proposal" : { - "type" : "object", - "description" : "Optional counter-proposal" - }, - "V10CredentialExchange_credential" : { - "type" : "object", - "description" : "Credential as stored" - }, - "V10CredentialExchange_credential_offer" : { - "type" : "object", - "description" : "(Indy) credential offer" - }, - "V10CredentialExchange_credential_offer_dict" : { - "type" : "object", - "description" : "Credential offer message" - }, - "V10CredentialExchange_credential_proposal_dict" : { - "type" : "object", - "description" : "Credential proposal message" - }, - "V10CredentialExchange_credential_request" : { - "type" : "object", - "description" : "(Indy) credential request" - }, - "V10CredentialExchange_raw_credential" : { - "type" : "object", - "description" : "Credential as received, prior to storage in holder wallet" - }, - "V10PresentationExchange_presentation" : { - "type" : "object", - "description" : "(Indy) presentation (also known as proof)" - }, - "V10PresentationExchange_presentation_proposal_dict" : { - "type" : "object", - "description" : "Presentation proposal message" - }, - "V10PresentationExchange_presentation_request" : { - "type" : "object", - "description" : "(Indy) presentation request (also known as proof request)" - }, - "V10PresentationExchange_presentation_request_dict" : { - "type" : "object", - "description" : "Presentation request message" - }, - "V20CredBoundOfferRequest_counter_preview" : { - "type" : "object", - "description" : "Optional content for counter-proposal" - }, - "V20CredBoundOfferRequest_filter" : { - "type" : "object", - "description" : "Credential specification criteria by format" - }, - "V20CredExRecord_by_format" : { - "type" : "object", - "description" : "Attachment content by format for proposal, offer, request, and issue" - }, - "V20CredExRecord_cred_issue" : { - "type" : "object", - "description" : "Serialized credential issue message" - }, - "V20CredExRecord_cred_preview" : { - "type" : "object", - "description" : "Credential preview from credential proposal" - }, - "V20CredExRecord_cred_request" : { - "type" : "object", - "description" : "Serialized credential request message" - }, - "V20CredExRecordDetail_cred_ex_record" : { - "type" : "object", - "description" : "Credential exchange record" - }, - "V20CredFilter_indy" : { - "type" : "object", - "description" : "Credential filter for indy" - }, - "V20CredFilter_ld_proof" : { - "type" : "object", - "description" : "Credential filter for linked data proof" - }, - "V20CredProposal_credential_preview" : { - "type" : "object", - "description" : "Credential preview" - }, - "V20PresExRecord_by_format" : { - "type" : "object", - "description" : "Attachment content by format for proposal, request, and presentation" - }, - "V20PresExRecord_pres" : { - "type" : "object", - "description" : "Presentation message" - }, - "V20PresProposalByFormat_dif" : { - "type" : "object", - "description" : "Presentation proposal for DIF" - }, - "V20PresProposalByFormat_indy" : { - "type" : "object", - "description" : "Presentation proposal for indy" - }, - "V20PresRequestByFormat_dif" : { - "type" : "object", - "description" : "Presentation request for DIF" - }, - "V20PresRequestByFormat_indy" : { - "type" : "object", - "description" : "Presentation request for indy" - }, - "V20PresSpecByFormatRequest_dif" : { - "type" : "object", - "description" : "Optional Presentation specification for DIF, overrides the PresentionExchange record's PresRequest" - }, - "V20PresSpecByFormatRequest_indy" : { - "type" : "object", - "description" : "Presentation specification for indy" - }, - "VerifyRequest_doc" : { - "type" : "object", - "description" : "Signed document" - } - } + }, + "required" : [ "connection_id", "proof_request" ], + "type" : "object" + }, + "V10PresentationSendRequestToProposal" : { + "properties" : { + "auto_verify" : { + "description" : "Verifier choice to auto-verify proof presentation", + "example" : false, + "type" : "boolean" + }, + "trace" : { + "description" : "Whether to trace event (default false)", + "example" : false, + "type" : "boolean" + } + }, + "type" : "object" + }, + "V20CredAttrSpec" : { + "properties" : { + "mime-type" : { + "description" : "MIME type: omit for (null) default", + "example" : "image/jpeg", + "nullable" : true, + "type" : "string" + }, + "name" : { + "description" : "Attribute name", + "example" : "favourite_drink", + "type" : "string" + }, + "value" : { + "description" : "Attribute value: base64-encode if MIME type is present", + "example" : "martini", + "type" : "string" + } + }, + "required" : [ "name", "value" ], + "type" : "object" + }, + "V20CredBoundOfferRequest" : { + "properties" : { + "counter_preview" : { + "$ref" : "#/components/schemas/V20CredBoundOfferRequest_counter_preview" + }, + "filter" : { + "$ref" : "#/components/schemas/V20CredBoundOfferRequest_filter" + } + }, + "type" : "object" + }, + "V20CredExFree" : { + "properties" : { + "auto_remove" : { + "description" : "Whether to remove the credential exchange record on completion (overrides --preserve-exchange-records configuration setting)", + "type" : "boolean" + }, + "comment" : { + "description" : "Human-readable comment", + "nullable" : true, + "type" : "string" + }, + "connection_id" : { + "description" : "Connection identifier", + "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "format" : "uuid", + "type" : "string" + }, + "credential_preview" : { + "$ref" : "#/components/schemas/V20CredPreview" + }, + "filter" : { + "$ref" : "#/components/schemas/V20CredBoundOfferRequest_filter" + }, + "trace" : { + "description" : "Record trace information, based on agent configuration", + "type" : "boolean" + }, + "verification_method" : { + "description" : "For ld-proofs. Verification method for signing.", + "nullable" : true, + "type" : "string" + } + }, + "required" : [ "connection_id", "filter" ], + "type" : "object" + }, + "V20CredExRecord" : { + "properties" : { + "auto_issue" : { + "description" : "Issuer choice to issue to request in this credential exchange", + "example" : false, + "type" : "boolean" + }, + "auto_offer" : { + "description" : "Holder choice to accept offer in this credential exchange", + "example" : false, + "type" : "boolean" + }, + "auto_remove" : { + "description" : "Issuer choice to remove this credential exchange record when complete", + "example" : false, + "type" : "boolean" + }, + "by_format" : { + "$ref" : "#/components/schemas/V20CredExRecord_by_format" + }, + "connection_id" : { + "description" : "Connection identifier", + "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "type" : "string" + }, + "created_at" : { + "description" : "Time of record creation", + "example" : "2021-12-31T23:59:59Z", + "pattern" : "^\\d{4}-\\d\\d-\\d\\d[T ]\\d\\d:\\d\\d(?:\\:(?:\\d\\d(?:\\.\\d{1,6})?))?(?:[+-]\\d\\d:?\\d\\d|Z|)$", + "type" : "string" + }, + "cred_ex_id" : { + "description" : "Credential exchange identifier", + "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "type" : "string" + }, + "cred_issue" : { + "$ref" : "#/components/schemas/V20CredExRecord_cred_issue" + }, + "cred_offer" : { + "$ref" : "#/components/schemas/V20CredExRecord_cred_offer" + }, + "cred_preview" : { + "$ref" : "#/components/schemas/V20CredExRecord_cred_preview" + }, + "cred_proposal" : { + "$ref" : "#/components/schemas/V20CredExRecord_cred_proposal" + }, + "cred_request" : { + "$ref" : "#/components/schemas/V20CredExRecord_cred_request" + }, + "error_msg" : { + "description" : "Error message", + "example" : "The front fell off", + "type" : "string" + }, + "initiator" : { + "description" : "Issue-credential exchange initiator: self or external", + "enum" : [ "self", "external" ], + "example" : "self", + "type" : "string" + }, + "parent_thread_id" : { + "description" : "Parent thread identifier", + "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "type" : "string" + }, + "role" : { + "description" : "Issue-credential exchange role: holder or issuer", + "enum" : [ "issuer", "holder" ], + "example" : "issuer", + "type" : "string" + }, + "state" : { + "description" : "Issue-credential exchange state", + "enum" : [ "proposal-sent", "proposal-received", "offer-sent", "offer-received", "request-sent", "request-received", "credential-issued", "credential-received", "done", "credential-revoked", "abandoned", "deleted" ], + "example" : "done", + "type" : "string" + }, + "thread_id" : { + "description" : "Thread identifier", + "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "type" : "string" + }, + "trace" : { + "description" : "Record trace information, based on agent configuration", + "type" : "boolean" + }, + "updated_at" : { + "description" : "Time of last record update", + "example" : "2021-12-31T23:59:59Z", + "pattern" : "^\\d{4}-\\d\\d-\\d\\d[T ]\\d\\d:\\d\\d(?:\\:(?:\\d\\d(?:\\.\\d{1,6})?))?(?:[+-]\\d\\d:?\\d\\d|Z|)$", + "type" : "string" + } + }, + "type" : "object" + }, + "V20CredExRecordByFormat" : { + "properties" : { + "cred_issue" : { + "properties" : { }, + "type" : "object" + }, + "cred_offer" : { + "properties" : { }, + "type" : "object" + }, + "cred_proposal" : { + "properties" : { }, + "type" : "object" + }, + "cred_request" : { + "properties" : { }, + "type" : "object" + } + }, + "type" : "object" + }, + "V20CredExRecordDetail" : { + "properties" : { + "cred_ex_record" : { + "$ref" : "#/components/schemas/V20CredExRecordDetail_cred_ex_record" + }, + "indy" : { + "$ref" : "#/components/schemas/V20CredExRecordIndy" + }, + "ld_proof" : { + "$ref" : "#/components/schemas/V20CredExRecordLDProof" + } + }, + "type" : "object" + }, + "V20CredExRecordIndy" : { + "properties" : { + "created_at" : { + "description" : "Time of record creation", + "example" : "2021-12-31T23:59:59Z", + "pattern" : "^\\d{4}-\\d\\d-\\d\\d[T ]\\d\\d:\\d\\d(?:\\:(?:\\d\\d(?:\\.\\d{1,6})?))?(?:[+-]\\d\\d:?\\d\\d|Z|)$", + "type" : "string" + }, + "cred_ex_id" : { + "description" : "Corresponding v2.0 credential exchange record identifier", + "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "type" : "string" + }, + "cred_ex_indy_id" : { + "description" : "Record identifier", + "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "type" : "string" + }, + "cred_id_stored" : { + "description" : "Credential identifier stored in wallet", + "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "type" : "string" + }, + "cred_request_metadata" : { + "description" : "Credential request metadata for indy holder", + "properties" : { }, + "type" : "object" + }, + "cred_rev_id" : { + "description" : "Credential revocation identifier within revocation registry", + "example" : "12345", + "pattern" : "^[1-9][0-9]*$", + "type" : "string" + }, + "rev_reg_id" : { + "description" : "Revocation registry identifier", + "example" : "WgWxqztrNooG92RXvxSTWv:4:WgWxqztrNooG92RXvxSTWv:3:CL:20:tag:CL_ACCUM:0", + "pattern" : "^([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}):4:([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}):3:CL:(([1-9][0-9]*)|([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}:2:.+:[0-9.]+))(:.+)?:CL_ACCUM:(.+$)", + "type" : "string" + }, + "state" : { + "description" : "Current record state", + "example" : "active", + "type" : "string" + }, + "updated_at" : { + "description" : "Time of last record update", + "example" : "2021-12-31T23:59:59Z", + "pattern" : "^\\d{4}-\\d\\d-\\d\\d[T ]\\d\\d:\\d\\d(?:\\:(?:\\d\\d(?:\\.\\d{1,6})?))?(?:[+-]\\d\\d:?\\d\\d|Z|)$", + "type" : "string" + } + }, + "type" : "object" + }, + "V20CredExRecordLDProof" : { + "properties" : { + "created_at" : { + "description" : "Time of record creation", + "example" : "2021-12-31T23:59:59Z", + "pattern" : "^\\d{4}-\\d\\d-\\d\\d[T ]\\d\\d:\\d\\d(?:\\:(?:\\d\\d(?:\\.\\d{1,6})?))?(?:[+-]\\d\\d:?\\d\\d|Z|)$", + "type" : "string" + }, + "cred_ex_id" : { + "description" : "Corresponding v2.0 credential exchange record identifier", + "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "type" : "string" + }, + "cred_ex_ld_proof_id" : { + "description" : "Record identifier", + "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "type" : "string" + }, + "cred_id_stored" : { + "description" : "Credential identifier stored in wallet", + "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "type" : "string" + }, + "state" : { + "description" : "Current record state", + "example" : "active", + "type" : "string" + }, + "updated_at" : { + "description" : "Time of last record update", + "example" : "2021-12-31T23:59:59Z", + "pattern" : "^\\d{4}-\\d\\d-\\d\\d[T ]\\d\\d:\\d\\d(?:\\:(?:\\d\\d(?:\\.\\d{1,6})?))?(?:[+-]\\d\\d:?\\d\\d|Z|)$", + "type" : "string" + } + }, + "type" : "object" + }, + "V20CredExRecordListResult" : { + "properties" : { + "results" : { + "description" : "Credential exchange records and corresponding detail records", + "items" : { + "$ref" : "#/components/schemas/V20CredExRecordDetail" + }, + "type" : "array" + } + }, + "type" : "object" + }, + "V20CredFilter" : { + "properties" : { + "indy" : { + "$ref" : "#/components/schemas/V20CredFilter_indy" + }, + "ld_proof" : { + "$ref" : "#/components/schemas/V20CredFilter_ld_proof" + } + }, + "type" : "object" + }, + "V20CredFilterIndy" : { + "properties" : { + "cred_def_id" : { + "description" : "Credential definition identifier", + "example" : "WgWxqztrNooG92RXvxSTWv:3:CL:20:tag", + "pattern" : "^([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}):3:CL:(([1-9][0-9]*)|([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}:2:.+:[0-9.]+)):(.+)?$", + "type" : "string" + }, + "issuer_did" : { + "description" : "Credential issuer DID", + "example" : "WgWxqztrNooG92RXvxSTWv", + "pattern" : "^(did:sov:)?[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}$", + "type" : "string" + }, + "schema_id" : { + "description" : "Schema identifier", + "example" : "WgWxqztrNooG92RXvxSTWv:2:schema_name:1.0", + "pattern" : "^[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}:2:.+:[0-9.]+$", + "type" : "string" + }, + "schema_issuer_did" : { + "description" : "Schema issuer DID", + "example" : "WgWxqztrNooG92RXvxSTWv", + "pattern" : "^(did:sov:)?[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}$", + "type" : "string" + }, + "schema_name" : { + "description" : "Schema name", + "example" : "preferences", + "type" : "string" + }, + "schema_version" : { + "description" : "Schema version", + "example" : "1.0", + "pattern" : "^[0-9.]+$", + "type" : "string" + } + }, + "type" : "object" + }, + "V20CredFilterLDProof" : { + "properties" : { + "ld_proof" : { + "$ref" : "#/components/schemas/V20CredFilter_ld_proof" + } + }, + "required" : [ "ld_proof" ], + "type" : "object" + }, + "V20CredFormat" : { + "properties" : { + "attach_id" : { + "description" : "Attachment identifier", + "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "type" : "string" + }, + "format" : { + "description" : "Attachment format specifier", + "example" : "aries/ld-proof-vc-detail@v1.0", + "type" : "string" + } + }, + "required" : [ "attach_id", "format" ], + "type" : "object" + }, + "V20CredIssue" : { + "properties" : { + "@id" : { + "description" : "Message identifier", + "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "type" : "string" + }, + "@type" : { + "description" : "Message type", + "example" : "https://didcomm.org/my-family/1.0/my-message-type", + "readOnly" : true, + "type" : "string" + }, + "comment" : { + "description" : "Human-readable comment", + "nullable" : true, + "type" : "string" + }, + "credentials~attach" : { + "description" : "Credential attachments", + "items" : { + "$ref" : "#/components/schemas/AttachDecorator" + }, + "type" : "array" + }, + "formats" : { + "description" : "Acceptable attachment formats", + "items" : { + "$ref" : "#/components/schemas/V20CredFormat" + }, + "type" : "array" + }, + "replacement_id" : { + "description" : "Issuer-unique identifier to coordinate credential replacement", + "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "type" : "string" + } + }, + "required" : [ "credentials~attach", "formats" ], + "type" : "object" + }, + "V20CredIssueProblemReportRequest" : { + "properties" : { + "description" : { + "type" : "string" + } + }, + "required" : [ "description" ], + "type" : "object" + }, + "V20CredIssueRequest" : { + "properties" : { + "comment" : { + "description" : "Human-readable comment", + "nullable" : true, + "type" : "string" + } + }, + "type" : "object" + }, + "V20CredOffer" : { + "properties" : { + "@id" : { + "description" : "Message identifier", + "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "type" : "string" + }, + "@type" : { + "description" : "Message type", + "example" : "https://didcomm.org/my-family/1.0/my-message-type", + "readOnly" : true, + "type" : "string" + }, + "comment" : { + "description" : "Human-readable comment", + "nullable" : true, + "type" : "string" + }, + "credential_preview" : { + "$ref" : "#/components/schemas/V20CredPreview" + }, + "formats" : { + "description" : "Acceptable credential formats", + "items" : { + "$ref" : "#/components/schemas/V20CredFormat" + }, + "type" : "array" + }, + "offers~attach" : { + "description" : "Offer attachments", + "items" : { + "$ref" : "#/components/schemas/AttachDecorator" + }, + "type" : "array" + }, + "replacement_id" : { + "description" : "Issuer-unique identifier to coordinate credential replacement", + "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "type" : "string" + } + }, + "required" : [ "formats", "offers~attach" ], + "type" : "object" + }, + "V20CredOfferConnFreeRequest" : { + "properties" : { + "auto_issue" : { + "description" : "Whether to respond automatically to credential requests, creating and issuing requested credentials", + "type" : "boolean" + }, + "auto_remove" : { + "description" : "Whether to remove the credential exchange record on completion (overrides --preserve-exchange-records configuration setting)", + "type" : "boolean" + }, + "comment" : { + "description" : "Human-readable comment", + "nullable" : true, + "type" : "string" + }, + "credential_preview" : { + "$ref" : "#/components/schemas/V20CredPreview" + }, + "filter" : { + "$ref" : "#/components/schemas/V20CredBoundOfferRequest_filter" + }, + "trace" : { + "description" : "Record trace information, based on agent configuration", + "type" : "boolean" + } + }, + "required" : [ "filter" ], + "type" : "object" + }, + "V20CredOfferRequest" : { + "properties" : { + "auto_issue" : { + "description" : "Whether to respond automatically to credential requests, creating and issuing requested credentials", + "type" : "boolean" + }, + "auto_remove" : { + "description" : "Whether to remove the credential exchange record on completion (overrides --preserve-exchange-records configuration setting)", + "type" : "boolean" + }, + "comment" : { + "description" : "Human-readable comment", + "nullable" : true, + "type" : "string" + }, + "connection_id" : { + "description" : "Connection identifier", + "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "format" : "uuid", + "type" : "string" + }, + "credential_preview" : { + "$ref" : "#/components/schemas/V20CredPreview" + }, + "filter" : { + "$ref" : "#/components/schemas/V20CredBoundOfferRequest_filter" + }, + "trace" : { + "description" : "Record trace information, based on agent configuration", + "type" : "boolean" + }, + "thread_id" : { + "type" : "string", + "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "description" : "thread identifier" + } + }, + "required" : [ "connection_id", "filter" ], + "type" : "object" + }, + "V20CredPreview" : { + "properties" : { + "@type" : { + "description" : "Message type identifier", + "example" : "issue-credential/2.0/credential-preview", + "type" : "string" + }, + "attributes" : { + "items" : { + "$ref" : "#/components/schemas/V20CredAttrSpec" + }, + "type" : "array" + } + }, + "required" : [ "attributes" ], + "type" : "object" + }, + "V20CredProposal" : { + "properties" : { + "@id" : { + "description" : "Message identifier", + "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "type" : "string" + }, + "@type" : { + "description" : "Message type", + "example" : "https://didcomm.org/my-family/1.0/my-message-type", + "readOnly" : true, + "type" : "string" + }, + "comment" : { + "description" : "Human-readable comment", + "nullable" : true, + "type" : "string" + }, + "credential_preview" : { + "$ref" : "#/components/schemas/V20CredProposal_credential_preview" + }, + "filters~attach" : { + "description" : "Credential filter per acceptable format on corresponding identifier", + "items" : { + "$ref" : "#/components/schemas/AttachDecorator" + }, + "type" : "array" + }, + "formats" : { + "description" : "Attachment formats", + "items" : { + "$ref" : "#/components/schemas/V20CredFormat" + }, + "type" : "array" + } + }, + "required" : [ "filters~attach", "formats" ], + "type" : "object" + }, + "V20CredRequest" : { + "properties" : { + "@id" : { + "description" : "Message identifier", + "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "type" : "string" + }, + "@type" : { + "description" : "Message type", + "example" : "https://didcomm.org/my-family/1.0/my-message-type", + "readOnly" : true, + "type" : "string" + }, + "comment" : { + "description" : "Human-readable comment", + "nullable" : true, + "type" : "string" + }, + "formats" : { + "description" : "Acceptable attachment formats", + "items" : { + "$ref" : "#/components/schemas/V20CredFormat" + }, + "type" : "array" + }, + "requests~attach" : { + "description" : "Request attachments", + "items" : { + "$ref" : "#/components/schemas/AttachDecorator" + }, + "type" : "array" + } + }, + "required" : [ "formats", "requests~attach" ], + "type" : "object" + }, + "V20CredRequestFree" : { + "properties" : { + "auto_remove" : { + "description" : "Whether to remove the credential exchange record on completion (overrides --preserve-exchange-records configuration setting)", + "type" : "boolean" + }, + "comment" : { + "description" : "Human-readable comment", + "nullable" : true, + "type" : "string" + }, + "connection_id" : { + "description" : "Connection identifier", + "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "format" : "uuid", + "type" : "string" + }, + "filter" : { + "$ref" : "#/components/schemas/V20CredRequestFree_filter" + }, + "holder_did" : { + "description" : "Holder DID to substitute for the credentialSubject.id", + "example" : "did:key:ahsdkjahsdkjhaskjdhakjshdkajhsdkjahs", + "nullable" : true, + "type" : "string" + }, + "trace" : { + "description" : "Whether to trace event (default false)", + "example" : false, + "type" : "boolean" + } + }, + "required" : [ "connection_id", "filter" ], + "type" : "object" + }, + "V20CredRequestRequest" : { + "properties" : { + "holder_did" : { + "description" : "Holder DID to substitute for the credentialSubject.id", + "example" : "did:key:ahsdkjahsdkjhaskjdhakjshdkajhsdkjahs", + "nullable" : true, + "type" : "string" + } + }, + "type" : "object" + }, + "V20CredStoreRequest" : { + "properties" : { + "credential_id" : { + "type" : "string" + } + }, + "type" : "object" + }, + "V20DiscoveryExchangeListResult" : { + "properties" : { + "results" : { + "items" : { + "$ref" : "#/components/schemas/V20DiscoveryExchangeListResult_results_inner" + }, + "type" : "array" + } + }, + "type" : "object" + }, + "V20DiscoveryExchangeResult" : { + "properties" : { + "results" : { + "$ref" : "#/components/schemas/V20DiscoveryExchangeListResult_results_inner" + } + }, + "type" : "object" + }, + "V20DiscoveryRecord" : { + "properties" : { + "connection_id" : { + "description" : "Connection identifier", + "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "type" : "string" + }, + "created_at" : { + "description" : "Time of record creation", + "example" : "2021-12-31T23:59:59Z", + "pattern" : "^\\d{4}-\\d\\d-\\d\\d[T ]\\d\\d:\\d\\d(?:\\:(?:\\d\\d(?:\\.\\d{1,6})?))?(?:[+-]\\d\\d:?\\d\\d|Z|)$", + "type" : "string" + }, + "disclosures" : { + "$ref" : "#/components/schemas/V20DiscoveryRecord_disclosures" + }, + "discovery_exchange_id" : { + "description" : "Credential exchange identifier", + "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "type" : "string" + }, + "queries_msg" : { + "$ref" : "#/components/schemas/V20DiscoveryRecord_queries_msg" + }, + "state" : { + "description" : "Current record state", + "example" : "active", + "type" : "string" + }, + "thread_id" : { + "description" : "Thread identifier", + "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "type" : "string" + }, + "trace" : { + "description" : "Record trace information, based on agent configuration", + "type" : "boolean" + }, + "updated_at" : { + "description" : "Time of last record update", + "example" : "2021-12-31T23:59:59Z", + "pattern" : "^\\d{4}-\\d\\d-\\d\\d[T ]\\d\\d:\\d\\d(?:\\:(?:\\d\\d(?:\\.\\d{1,6})?))?(?:[+-]\\d\\d:?\\d\\d|Z|)$", + "type" : "string" + } + }, + "type" : "object" + }, + "V20IssueCredSchemaCore" : { + "properties" : { + "auto_remove" : { + "description" : "Whether to remove the credential exchange record on completion (overrides --preserve-exchange-records configuration setting)", + "type" : "boolean" + }, + "comment" : { + "description" : "Human-readable comment", + "nullable" : true, + "type" : "string" + }, + "credential_preview" : { + "$ref" : "#/components/schemas/V20CredPreview" + }, + "filter" : { + "$ref" : "#/components/schemas/V20CredBoundOfferRequest_filter" + }, + "trace" : { + "description" : "Record trace information, based on agent configuration", + "type" : "boolean" + } + }, + "required" : [ "filter" ], + "type" : "object" + }, + "V20IssueCredentialModuleResponse" : { + "type" : "object" + }, + "V20Pres" : { + "properties" : { + "@id" : { + "description" : "Message identifier", + "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "type" : "string" + }, + "@type" : { + "description" : "Message type", + "example" : "https://didcomm.org/my-family/1.0/my-message-type", + "readOnly" : true, + "type" : "string" + }, + "comment" : { + "description" : "Human-readable comment", + "nullable" : true, + "type" : "string" + }, + "formats" : { + "description" : "Acceptable attachment formats", + "items" : { + "$ref" : "#/components/schemas/V20PresFormat" + }, + "type" : "array" + }, + "presentations~attach" : { + "items" : { + "$ref" : "#/components/schemas/AttachDecorator" + }, + "type" : "array" + } + }, + "required" : [ "formats", "presentations~attach" ], + "type" : "object" + }, + "V20PresCreateRequestRequest" : { + "properties" : { + "auto_verify" : { + "description" : "Verifier choice to auto-verify proof presentation", + "example" : false, + "type" : "boolean" + }, + "comment" : { + "nullable" : true, + "type" : "string" + }, + "presentation_request" : { + "$ref" : "#/components/schemas/V20PresRequestByFormat" + }, + "trace" : { + "description" : "Whether to trace event (default false)", + "example" : false, + "type" : "boolean" + } + }, + "required" : [ "presentation_request" ], + "type" : "object" + }, + "V20PresExRecord" : { + "properties" : { + "auto_present" : { + "description" : "Prover choice to auto-present proof as verifier requests", + "example" : false, + "type" : "boolean" + }, + "auto_verify" : { + "description" : "Verifier choice to auto-verify proof presentation", + "type" : "boolean" + }, + "by_format" : { + "$ref" : "#/components/schemas/V20PresExRecord_by_format" + }, + "connection_id" : { + "description" : "Connection identifier", + "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "type" : "string" + }, + "created_at" : { + "description" : "Time of record creation", + "example" : "2021-12-31T23:59:59Z", + "pattern" : "^\\d{4}-\\d\\d-\\d\\d[T ]\\d\\d:\\d\\d(?:\\:(?:\\d\\d(?:\\.\\d{1,6})?))?(?:[+-]\\d\\d:?\\d\\d|Z|)$", + "type" : "string" + }, + "error_msg" : { + "description" : "Error message", + "example" : "Invalid structure", + "type" : "string" + }, + "initiator" : { + "description" : "Present-proof exchange initiator: self or external", + "enum" : [ "self", "external" ], + "example" : "self", + "type" : "string" + }, + "pres" : { + "$ref" : "#/components/schemas/V20PresExRecord_pres" + }, + "pres_ex_id" : { + "description" : "Presentation exchange identifier", + "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "type" : "string" + }, + "pres_proposal" : { + "$ref" : "#/components/schemas/V20PresExRecord_pres_proposal" + }, + "pres_request" : { + "$ref" : "#/components/schemas/V20PresExRecord_pres_request" + }, + "role" : { + "description" : "Present-proof exchange role: prover or verifier", + "enum" : [ "prover", "verifier" ], + "example" : "prover", + "type" : "string" + }, + "state" : { + "description" : "Present-proof exchange state", + "enum" : [ "proposal-sent", "proposal-received", "request-sent", "request-received", "presentation-sent", "presentation-received", "done", "abandoned", "deleted" ], + "type" : "string" + }, + "thread_id" : { + "description" : "Thread identifier", + "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "type" : "string" + }, + "trace" : { + "description" : "Record trace information, based on agent configuration", + "type" : "boolean" + }, + "updated_at" : { + "description" : "Time of last record update", + "example" : "2021-12-31T23:59:59Z", + "pattern" : "^\\d{4}-\\d\\d-\\d\\d[T ]\\d\\d:\\d\\d(?:\\:(?:\\d\\d(?:\\.\\d{1,6})?))?(?:[+-]\\d\\d:?\\d\\d|Z|)$", + "type" : "string" + }, + "verified" : { + "description" : "Whether presentation is verified: 'true' or 'false'", + "enum" : [ "true", "false" ], + "example" : "true", + "type" : "string" + }, + "verified_msgs" : { + "items" : { + "description" : "Proof verification warning or error information", + "type" : "string" + }, + "type" : "array" + } + }, + "type" : "object" + }, + "V20PresExRecordByFormat" : { + "properties" : { + "pres" : { + "properties" : { }, + "type" : "object" + }, + "pres_proposal" : { + "properties" : { }, + "type" : "object" + }, + "pres_request" : { + "properties" : { }, + "type" : "object" + } + }, + "type" : "object" + }, + "V20PresExRecordList" : { + "properties" : { + "results" : { + "description" : "Presentation exchange records", + "items" : { + "$ref" : "#/components/schemas/V20PresExRecord" + }, + "type" : "array" + } + }, + "type" : "object" + }, + "V20PresFormat" : { + "properties" : { + "attach_id" : { + "description" : "Attachment identifier", + "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "type" : "string" + }, + "format" : { + "description" : "Attachment format specifier", + "example" : "dif/presentation-exchange/submission@v1.0", + "type" : "string" + } + }, + "required" : [ "attach_id", "format" ], + "type" : "object" + }, + "V20PresProblemReportRequest" : { + "properties" : { + "description" : { + "type" : "string" + } + }, + "required" : [ "description" ], + "type" : "object" + }, + "V20PresProposal" : { + "properties" : { + "@id" : { + "description" : "Message identifier", + "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "type" : "string" + }, + "@type" : { + "description" : "Message type", + "example" : "https://didcomm.org/my-family/1.0/my-message-type", + "readOnly" : true, + "type" : "string" + }, + "comment" : { + "description" : "Human-readable comment", + "type" : "string" + }, + "formats" : { + "items" : { + "$ref" : "#/components/schemas/V20PresFormat" + }, + "type" : "array" + }, + "proposals~attach" : { + "description" : "Attachment per acceptable format on corresponding identifier", + "items" : { + "$ref" : "#/components/schemas/AttachDecorator" + }, + "type" : "array" + } + }, + "required" : [ "formats", "proposals~attach" ], + "type" : "object" + }, + "V20PresProposalByFormat" : { + "properties" : { + "dif" : { + "$ref" : "#/components/schemas/V20PresProposalByFormat_dif" + }, + "indy" : { + "$ref" : "#/components/schemas/V20PresProposalByFormat_indy" + } + }, + "type" : "object" + }, + "V20PresProposalRequest" : { + "properties" : { + "auto_present" : { + "description" : "Whether to respond automatically to presentation requests, building and presenting requested proof", + "type" : "boolean" + }, + "comment" : { + "description" : "Human-readable comment", + "nullable" : true, + "type" : "string" + }, + "connection_id" : { + "description" : "Connection identifier", + "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "format" : "uuid", + "type" : "string" + }, + "presentation_proposal" : { + "$ref" : "#/components/schemas/V20PresProposalByFormat" + }, + "trace" : { + "description" : "Whether to trace event (default false)", + "example" : false, + "type" : "boolean" + } + }, + "required" : [ "connection_id", "presentation_proposal" ], + "type" : "object" + }, + "V20PresRequest" : { + "properties" : { + "@id" : { + "description" : "Message identifier", + "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "type" : "string" + }, + "@type" : { + "description" : "Message type", + "example" : "https://didcomm.org/my-family/1.0/my-message-type", + "readOnly" : true, + "type" : "string" + }, + "comment" : { + "description" : "Human-readable comment", + "type" : "string" + }, + "formats" : { + "items" : { + "$ref" : "#/components/schemas/V20PresFormat" + }, + "type" : "array" + }, + "request_presentations~attach" : { + "description" : "Attachment per acceptable format on corresponding identifier", + "items" : { + "$ref" : "#/components/schemas/AttachDecorator" + }, + "type" : "array" + }, + "will_confirm" : { + "description" : "Whether verifier will send confirmation ack", + "type" : "boolean" + } + }, + "required" : [ "formats", "request_presentations~attach" ], + "type" : "object" + }, + "V20PresRequestByFormat" : { + "properties" : { + "dif" : { + "$ref" : "#/components/schemas/V20PresRequestByFormat_dif" + }, + "indy" : { + "$ref" : "#/components/schemas/V20PresRequestByFormat_indy" + } + }, + "type" : "object" + }, + "V20PresSendRequestRequest" : { + "properties" : { + "auto_verify" : { + "description" : "Verifier choice to auto-verify proof presentation", + "example" : false, + "type" : "boolean" + }, + "comment" : { + "nullable" : true, + "type" : "string" + }, + "connection_id" : { + "description" : "Connection identifier", + "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "format" : "uuid", + "type" : "string" + }, + "presentation_request" : { + "$ref" : "#/components/schemas/V20PresRequestByFormat" + }, + "trace" : { + "description" : "Whether to trace event (default false)", + "example" : false, + "type" : "boolean" + } + }, + "required" : [ "connection_id", "presentation_request" ], + "type" : "object" + }, + "V20PresSpecByFormatRequest" : { + "properties" : { + "dif" : { + "$ref" : "#/components/schemas/V20PresSpecByFormatRequest_dif" + }, + "indy" : { + "$ref" : "#/components/schemas/V20PresSpecByFormatRequest_indy" + }, + "trace" : { + "description" : "Record trace information, based on agent configuration", + "type" : "boolean" + } + }, + "type" : "object" + }, + "V20PresentProofModuleResponse" : { + "type" : "object" + }, + "V20PresentationSendRequestToProposal" : { + "properties" : { + "auto_verify" : { + "description" : "Verifier choice to auto-verify proof presentation", + "example" : false, + "type" : "boolean" + }, + "trace" : { + "description" : "Whether to trace event (default false)", + "example" : false, + "type" : "boolean" + } + }, + "type" : "object" + }, + "VCRecord" : { + "properties" : { + "contexts" : { + "items" : { + "description" : "Context", + "example" : "https://myhost:8021", + "pattern" : "^[A-Za-z0-9\\.\\-\\+]+://([A-Za-z0-9][.A-Za-z0-9-_]+[A-Za-z0-9])+(:[1-9][0-9]*)?(/[^?&#]+)?$", + "type" : "string" + }, + "type" : "array" + }, + "cred_tags" : { + "additionalProperties" : { + "description" : "Retrieval tag value", + "type" : "string" + }, + "type" : "object" + }, + "cred_value" : { + "description" : "(JSON-serializable) credential value", + "properties" : { }, + "type" : "object" + }, + "expanded_types" : { + "items" : { + "description" : "JSON-LD expanded type extracted from type and context", + "example" : "https://w3id.org/citizenship#PermanentResidentCard", + "type" : "string" + }, + "type" : "array" + }, + "given_id" : { + "description" : "Credential identifier", + "example" : "http://example.edu/credentials/3732", + "type" : "string" + }, + "issuer_id" : { + "description" : "Issuer identifier", + "example" : "https://example.edu/issuers/14", + "type" : "string" + }, + "proof_types" : { + "items" : { + "description" : "Signature suite used for proof", + "example" : "Ed25519Signature2018", + "type" : "string" + }, + "type" : "array" + }, + "record_id" : { + "description" : "Record identifier", + "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "type" : "string" + }, + "schema_ids" : { + "items" : { + "description" : "Schema identifier", + "example" : "https://example.org/examples/degree.json", + "type" : "string" + }, + "type" : "array" + }, + "subject_ids" : { + "items" : { + "description" : "Subject identifier", + "example" : "did:example:ebfeb1f712ebc6f1c276e12ec21", + "type" : "string" + }, + "type" : "array" + } + }, + "type" : "object" + }, + "VCRecordList" : { + "properties" : { + "results" : { + "items" : { + "$ref" : "#/components/schemas/VCRecord" + }, + "type" : "array" + } + }, + "type" : "object" + }, + "VerifyRequest" : { + "properties" : { + "doc" : { + "$ref" : "#/components/schemas/VerifyRequest_doc" + }, + "verkey" : { + "description" : "Verkey to use for doc verification", + "type" : "string" + } + }, + "required" : [ "doc" ], + "type" : "object" + }, + "VerifyResponse" : { + "properties" : { + "error" : { + "description" : "Error text", + "type" : "string" + }, + "valid" : { + "type" : "boolean" + } + }, + "required" : [ "valid" ], + "type" : "object" + }, + "W3CCredentialsListRequest" : { + "properties" : { + "contexts" : { + "items" : { + "description" : "Credential context to match", + "example" : "https://myhost:8021", + "pattern" : "^[A-Za-z0-9\\.\\-\\+]+://([A-Za-z0-9][.A-Za-z0-9-_]+[A-Za-z0-9])+(:[1-9][0-9]*)?(/[^?&#]+)?$", + "type" : "string" + }, + "type" : "array" + }, + "given_id" : { + "description" : "Given credential id to match", + "type" : "string" + }, + "issuer_id" : { + "description" : "Credential issuer identifier to match", + "type" : "string" + }, + "max_results" : { + "description" : "Maximum number of results to return", + "format" : "int32", + "type" : "integer" + }, + "proof_types" : { + "items" : { + "description" : "Signature suite used for proof", + "example" : "Ed25519Signature2018", + "type" : "string" + }, + "type" : "array" + }, + "schema_ids" : { + "description" : "Schema identifiers, all of which to match", + "items" : { + "description" : "Credential schema identifier", + "example" : "https://myhost:8021", + "pattern" : "^[A-Za-z0-9\\.\\-\\+]+://([A-Za-z0-9][.A-Za-z0-9-_]+[A-Za-z0-9])+(:[1-9][0-9]*)?(/[^?&#]+)?$", + "type" : "string" + }, + "type" : "array" + }, + "subject_ids" : { + "description" : "Subject identifiers, all of which to match", + "items" : { + "description" : "Subject identifier", + "type" : "string" + }, + "type" : "array" + }, + "tag_query" : { + "additionalProperties" : { + "description" : "Tag value", + "type" : "string" + }, + "description" : "Tag filter", + "type" : "object" + }, + "types" : { + "items" : { + "description" : "Credential type to match", + "example" : "https://myhost:8021", + "pattern" : "^[A-Za-z0-9\\.\\-\\+]+://([A-Za-z0-9][.A-Za-z0-9-_]+[A-Za-z0-9])+(:[1-9][0-9]*)?(/[^?&#]+)?$", + "type" : "string" + }, + "type" : "array" + } + }, + "type" : "object" + }, + "WalletList" : { + "properties" : { + "results" : { + "description" : "List of wallet records", + "items" : { + "$ref" : "#/components/schemas/WalletRecord" + }, + "type" : "array" + } + }, + "type" : "object" + }, + "WalletModuleResponse" : { + "type" : "object" + }, + "WalletRecord" : { + "properties" : { + "created_at" : { + "description" : "Time of record creation", + "example" : "2021-12-31T23:59:59Z", + "pattern" : "^\\d{4}-\\d\\d-\\d\\d[T ]\\d\\d:\\d\\d(?:\\:(?:\\d\\d(?:\\.\\d{1,6})?))?(?:[+-]\\d\\d:?\\d\\d|Z|)$", + "type" : "string" + }, + "key_management_mode" : { + "description" : "Mode regarding management of wallet key", + "enum" : [ "managed", "unmanaged" ], + "type" : "string" + }, + "settings" : { + "description" : "Settings for this wallet.", + "properties" : { }, + "type" : "object" + }, + "state" : { + "description" : "Current record state", + "example" : "active", + "type" : "string" + }, + "updated_at" : { + "description" : "Time of last record update", + "example" : "2021-12-31T23:59:59Z", + "pattern" : "^\\d{4}-\\d\\d-\\d\\d[T ]\\d\\d:\\d\\d(?:\\:(?:\\d\\d(?:\\.\\d{1,6})?))?(?:[+-]\\d\\d:?\\d\\d|Z|)$", + "type" : "string" + }, + "wallet_id" : { + "description" : "Wallet record ID", + "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "type" : "string" + } + }, + "required" : [ "key_management_mode", "wallet_id" ], + "type" : "object" + }, + "WriteLedgerRequest" : { + "properties" : { + "ledger_id" : { + "type" : "string" + } + }, + "type" : "object" + }, + "UpdateProfileSettingsRequest" : { + "type" : "object", + "properties" : { + "extra_settings" : { + "type" : "object", + "description" : "Settings or config to update.", + "properties" : { } + } + } + }, + "ActionMenuFetchResult_result" : { + "allOf" : [ { + "$ref" : "#/components/schemas/Menu" + } ], + "description" : "Action menu", + "type" : "object" + }, + "AttachDecoratorData_jws" : { + "allOf" : [ { + "$ref" : "#/components/schemas/AttachDecoratorDataJWS" + } ], + "description" : "Detached Java Web Signature", + "type" : "object" + }, + "CredDefValue_primary" : { + "allOf" : [ { + "$ref" : "#/components/schemas/CredDefValuePrimary" + } ], + "description" : "Primary value for credential definition", + "type" : "object" + }, + "CredDefValue_revocation" : { + "allOf" : [ { + "$ref" : "#/components/schemas/CredDefValueRevocation" + } ], + "description" : "Revocation value for credential definition", + "type" : "object" + }, + "Credential_proof" : { + "allOf" : [ { + "$ref" : "#/components/schemas/LinkedDataProof" + } ], + "description" : "The proof of the credential", + "example" : { + "created" : "2019-12-11T03:50:55", + "jws" : "eyJhbGciOiAiRWREU0EiLCAiYjY0IjogZmFsc2UsICJjcml0JiNjQiXX0..lKJU0Df_keblRKhZAS9Qq6zybm-HqUXNVZ8vgEPNTAjQKBhQDxvXNo7nvtUBb_Eq1Ch6YBKY5qBQ", + "proofPurpose" : "assertionMethod", + "type" : "Ed25519Signature2018", + "verificationMethod" : "did:key:z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL#z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL" + }, + "type" : "object" + }, + "CredentialDefinition_value" : { + "allOf" : [ { + "$ref" : "#/components/schemas/CredDefValue" + } ], + "description" : "Credential definition primary and revocation values", + "type" : "object" + }, + "DIDCreate_options" : { + "allOf" : [ { + "$ref" : "#/components/schemas/DIDCreateOptions" + } ], + "description" : "To define a key type and/or a did depending on chosen DID method.", + "type" : "object" + }, + "DIDXRequest_did_doc_attach" : { + "allOf" : [ { + "$ref" : "#/components/schemas/AttachDecorator" + } ], + "description" : "As signed attachment, DID Doc associated with DID", + "type" : "object" + }, + "Doc_options" : { + "allOf" : [ { + "$ref" : "#/components/schemas/SignatureOptions" + } ], + "description" : "Signature options", + "type" : "object" + }, + "IndyCredAbstract_key_correctness_proof" : { + "allOf" : [ { + "$ref" : "#/components/schemas/IndyKeyCorrectnessProof" + } ], + "description" : "Key correctness proof", + "type" : "object" + }, + "IndyCredPrecis_cred_info" : { + "allOf" : [ { + "$ref" : "#/components/schemas/IndyCredInfo" + } ], + "description" : "Credential info", + "type" : "object" + }, + "IndyCredPrecis_interval" : { + "allOf" : [ { + "$ref" : "#/components/schemas/IndyNonRevocationInterval" + } ], + "description" : "Non-revocation interval from presentation request", + "type" : "object" + }, + "IndyCredential_values_value" : { + "allOf" : [ { + "$ref" : "#/definitions/IndyAttrValue" + } ], + "description" : "Attribute value", + "type" : "object" + }, + "IndyPrimaryProof_eq_proof" : { + "allOf" : [ { + "$ref" : "#/components/schemas/IndyEQProof" + } ], + "description" : "Indy equality proof", + "nullable" : true, + "type" : "object" + }, + "IndyProof_proof" : { + "allOf" : [ { + "$ref" : "#/components/schemas/IndyProofProof" + } ], + "description" : "Indy proof.proof content", + "type" : "object" + }, + "IndyProof_requested_proof" : { + "allOf" : [ { + "$ref" : "#/components/schemas/IndyProofRequestedProof" + } ], + "description" : "Indy proof.requested_proof content", + "type" : "object" + }, + "IndyProofProof_aggregated_proof" : { + "allOf" : [ { + "$ref" : "#/components/schemas/IndyProofProofAggregatedProof" + } ], + "description" : "Indy proof aggregated proof", + "type" : "object" + }, + "IndyProofProofProofsProof_non_revoc_proof" : { + "allOf" : [ { + "$ref" : "#/components/schemas/IndyNonRevocProof" + } ], + "description" : "Indy non-revocation proof", + "nullable" : true, + "type" : "object" + }, + "IndyProofProofProofsProof_primary_proof" : { + "allOf" : [ { + "$ref" : "#/components/schemas/IndyPrimaryProof" + } ], + "description" : "Indy primary proof", + "type" : "object" + }, + "IndyProofReqAttrSpec_non_revoked" : { + "allOf" : [ { + "$ref" : "#/components/schemas/IndyProofReqAttrSpecNonRevoked" + } ], + "nullable" : true, + "type" : "object" + }, + "IndyProofReqPredSpec_non_revoked" : { + "allOf" : [ { + "$ref" : "#/components/schemas/IndyProofReqPredSpecNonRevoked" + } ], + "nullable" : true, + "type" : "object" + }, + "IndyProofRequest_non_revoked" : { + "allOf" : [ { + "$ref" : "#/components/schemas/IndyProofRequestNonRevoked" + } ], + "nullable" : true, + "type" : "object" + }, + "IndyRevRegDef_value" : { + "allOf" : [ { + "$ref" : "#/components/schemas/IndyRevRegDefValue" + } ], + "description" : "Revocation registry definition value", + "type" : "object" + }, + "IndyRevRegDefValue_publicKeys" : { + "allOf" : [ { + "$ref" : "#/components/schemas/IndyRevRegDefValuePublicKeys" + } ], + "description" : "Public keys", + "type" : "object" + }, + "IndyRevRegEntry_value" : { + "allOf" : [ { + "$ref" : "#/components/schemas/IndyRevRegEntryValue" + } ], + "description" : "Revocation registry entry value", + "type" : "object" + }, + "InputDescriptors_schema" : { + "allOf" : [ { + "$ref" : "#/components/schemas/SchemasInputDescriptorFilter" + } ], + "description" : "Accepts a list of schema or a dict containing filters like oneof_filter.", + "example" : { + "oneof_filter" : [ [ { + "uri" : "https://www.w3.org/Test1#Test1" + }, { + "uri" : "https://www.w3.org/Test2#Test2" + } ], { + "oneof_filter" : [ [ { + "uri" : "https://www.w3.org/Test1#Test1" + } ], [ { + "uri" : "https://www.w3.org/Test2#Test2" + } ] ] + } ] + }, + "type" : "object" + }, + "InvitationRecord_invitation" : { + "allOf" : [ { + "$ref" : "#/components/schemas/InvitationMessage" + } ], + "description" : "Out of band invitation message", + "type" : "object" + }, + "IssuerRevRegRecord_revoc_reg_def" : { + "allOf" : [ { + "$ref" : "#/components/schemas/IndyRevRegDef" + } ], + "description" : "Revocation registry definition", + "type" : "object" + }, + "IssuerRevRegRecord_revoc_reg_entry" : { + "allOf" : [ { + "$ref" : "#/components/schemas/IndyRevRegEntry" + } ], + "description" : "Revocation registry entry", + "type" : "object" + }, + "KeylistQuery_paginate" : { + "allOf" : [ { + "$ref" : "#/components/schemas/KeylistQueryPaginate" + } ], + "description" : "Pagination info", + "type" : "object" + }, + "LDProofVCDetail_credential" : { + "allOf" : [ { + "$ref" : "#/components/schemas/Credential" + } ], + "description" : "Detail of the JSON-LD Credential to be issued", + "example" : { + "@context" : [ "https://www.w3.org/2018/credentials/v1", "https://w3id.org/citizenship/v1" ], + "credentialSubject" : { + "familyName" : "SMITH", + "gender" : "Male", + "givenName" : "JOHN", + "type" : [ "PermanentResident", "Person" ] + }, + "description" : "Government of Example Permanent Resident Card.", + "identifier" : "83627465", + "issuanceDate" : "2019-12-03T12:19:52Z", + "issuer" : "did:key:z6MkmjY8GnV5i9YTDtPETC2uUAW6ejw3nk5mXF5yci5ab7th", + "name" : "Permanent Resident Card", + "type" : [ "VerifiableCredential", "PermanentResidentCard" ] + }, + "type" : "object" + }, + "LDProofVCDetail_options" : { + "allOf" : [ { + "$ref" : "#/components/schemas/LDProofVCDetailOptions" + } ], + "description" : "Options for specifying how the linked data proof is created.", + "example" : { + "proofType" : "Ed25519Signature2018" + }, + "type" : "object" + }, + "LDProofVCDetailOptions_credentialStatus" : { + "allOf" : [ { + "$ref" : "#/components/schemas/CredentialStatusOptions" + } ], + "description" : "The credential status mechanism to use for the credential. Omitting the property indicates the issued credential will not include a credential status", + "type" : "object" + }, + "SchemaSendResult_schema" : { + "allOf" : [ { + "$ref" : "#/components/schemas/Schema" + } ], + "description" : "Schema definition", + "type" : "object" + }, + "SendMenu_menu" : { + "allOf" : [ { + "$ref" : "#/components/schemas/MenuJson" + } ], + "description" : "Menu to send to connection", + "type" : "object" + }, + "SignedDoc_proof" : { + "allOf" : [ { + "$ref" : "#/components/schemas/SignatureOptions" + } ], + "description" : "Linked data proof", + "type" : "object" + }, + "TxnOrCredentialDefinitionSendResult_txn" : { + "allOf" : [ { + "$ref" : "#/components/schemas/TransactionRecord" + } ], + "description" : "Credential definition transaction to endorse", + "type" : "object" + }, + "TxnOrPublishRevocationsResult_txn" : { + "allOf" : [ { + "$ref" : "#/components/schemas/TransactionRecord" + } ], + "description" : "Revocation registry revocations transaction to endorse", + "type" : "object" + }, + "TxnOrRegisterLedgerNymResponse_txn" : { + "allOf" : [ { + "$ref" : "#/components/schemas/TransactionRecord" + } ], + "description" : "DID transaction to endorse", + "type" : "object" + }, + "TxnOrRevRegResult_txn" : { + "allOf" : [ { + "$ref" : "#/components/schemas/TransactionRecord" + } ], + "description" : "Revocation registry definition transaction to endorse", + "type" : "object" + }, + "TxnOrSchemaSendResult_sent" : { + "allOf" : [ { + "$ref" : "#/components/schemas/SchemaSendResult" + } ], + "description" : "Content sent", + "type" : "object" + }, + "TxnOrSchemaSendResult_txn" : { + "allOf" : [ { + "$ref" : "#/components/schemas/TransactionRecord" + } ], + "description" : "Schema transaction to endorse", + "type" : "object" + }, + "V10CredentialBoundOfferRequest_counter_proposal" : { + "allOf" : [ { + "$ref" : "#/components/schemas/CredentialProposal" + } ], + "description" : "Optional counter-proposal", + "type" : "object" + }, + "V10CredentialExchange_credential" : { + "allOf" : [ { + "$ref" : "#/components/schemas/IndyCredInfo" + } ], + "description" : "Credential as stored", + "type" : "object" + }, + "V10CredentialExchange_credential_offer" : { + "allOf" : [ { + "$ref" : "#/components/schemas/IndyCredAbstract" + } ], + "description" : "(Indy) credential offer", + "type" : "object" + }, + "V10CredentialExchange_credential_offer_dict" : { + "allOf" : [ { + "$ref" : "#/components/schemas/CredentialOffer" + } ], + "description" : "Credential offer message", + "type" : "object" + }, + "V10CredentialExchange_credential_proposal_dict" : { + "allOf" : [ { + "$ref" : "#/components/schemas/CredentialProposal" + } ], + "description" : "Credential proposal message", + "type" : "object" + }, + "V10CredentialExchange_credential_request" : { + "allOf" : [ { + "$ref" : "#/components/schemas/IndyCredRequest" + } ], + "description" : "(Indy) credential request", + "type" : "object" + }, + "V10CredentialExchange_raw_credential" : { + "allOf" : [ { + "$ref" : "#/components/schemas/IndyCredential" + } ], + "description" : "Credential as received, prior to storage in holder wallet", + "type" : "object" + }, + "V10DiscoveryExchangeListResult_results_inner" : { + "allOf" : [ { + "$ref" : "#/components/schemas/V10DiscoveryRecord" + } ], + "description" : "Discover Features v1.0 exchange record", + "type" : "object" + }, + "V10DiscoveryRecord_disclose" : { + "allOf" : [ { + "$ref" : "#/components/schemas/Disclose" + } ], + "description" : "Disclose message", + "type" : "object" + }, + "V10DiscoveryRecord_query_msg" : { + "allOf" : [ { + "$ref" : "#/components/schemas/Query" + } ], + "description" : "Query message", + "type" : "object" + }, + "V10PresentationExchange_presentation" : { + "allOf" : [ { + "$ref" : "#/components/schemas/IndyProof" + } ], + "description" : "(Indy) presentation (also known as proof)", + "type" : "object" + }, + "V10PresentationExchange_presentation_proposal_dict" : { + "allOf" : [ { + "$ref" : "#/components/schemas/PresentationProposal" + } ], + "description" : "Presentation proposal message", + "type" : "object" + }, + "V10PresentationExchange_presentation_request" : { + "allOf" : [ { + "$ref" : "#/components/schemas/IndyProofRequest" + } ], + "description" : "(Indy) presentation request (also known as proof request)", + "type" : "object" + }, + "V10PresentationExchange_presentation_request_dict" : { + "allOf" : [ { + "$ref" : "#/components/schemas/PresentationRequest" + } ], + "description" : "Presentation request message", + "type" : "object" + }, + "V20CredBoundOfferRequest_counter_preview" : { + "allOf" : [ { + "$ref" : "#/components/schemas/V20CredPreview" + } ], + "description" : "Optional content for counter-proposal", + "type" : "object" + }, + "V20CredBoundOfferRequest_filter" : { + "allOf" : [ { + "$ref" : "#/components/schemas/V20CredFilter" + } ], + "description" : "Credential specification criteria by format", + "type" : "object" + }, + "V20CredExRecord_by_format" : { + "allOf" : [ { + "$ref" : "#/components/schemas/V20CredExRecordByFormat" + } ], + "description" : "Attachment content by format for proposal, offer, request, and issue", + "type" : "object" + }, + "V20CredExRecord_cred_issue" : { + "allOf" : [ { + "$ref" : "#/components/schemas/V20CredIssue" + } ], + "description" : "Serialized credential issue message", + "type" : "object" + }, + "V20CredExRecord_cred_offer" : { + "allOf" : [ { + "$ref" : "#/components/schemas/V20CredOffer" + } ], + "description" : "Credential offer message", + "type" : "object" + }, + "V20CredExRecord_cred_preview" : { + "allOf" : [ { + "$ref" : "#/components/schemas/V20CredPreview" + } ], + "description" : "Credential preview from credential proposal", + "type" : "object" + }, + "V20CredExRecord_cred_proposal" : { + "allOf" : [ { + "$ref" : "#/components/schemas/V20CredProposal" + } ], + "description" : "Credential proposal message", + "type" : "object" + }, + "V20CredExRecord_cred_request" : { + "allOf" : [ { + "$ref" : "#/components/schemas/V20CredRequest" + } ], + "description" : "Serialized credential request message", + "type" : "object" + }, + "V20CredExRecordDetail_cred_ex_record" : { + "allOf" : [ { + "$ref" : "#/components/schemas/V20CredExRecord" + } ], + "description" : "Credential exchange record", + "type" : "object" + }, + "V20CredFilter_indy" : { + "allOf" : [ { + "$ref" : "#/components/schemas/V20CredFilterIndy" + } ], + "description" : "Credential filter for indy", + "type" : "object" + }, + "V20CredFilter_ld_proof" : { + "allOf" : [ { + "$ref" : "#/components/schemas/LDProofVCDetail" + } ], + "description" : "Credential filter for linked data proof", + "type" : "object" + }, + "V20CredProposal_credential_preview" : { + "allOf" : [ { + "$ref" : "#/components/schemas/V20CredPreview" + } ], + "description" : "Credential preview", + "type" : "object" + }, + "V20CredRequestFree_filter" : { + "allOf" : [ { + "$ref" : "#/components/schemas/V20CredFilterLDProof" + } ], + "description" : "Credential specification criteria by format", + "type" : "object" + }, + "V20DiscoveryExchangeListResult_results_inner" : { + "allOf" : [ { + "$ref" : "#/components/schemas/V20DiscoveryRecord" + } ], + "description" : "Discover Features v2.0 exchange record", + "type" : "object" + }, + "V20DiscoveryRecord_disclosures" : { + "allOf" : [ { + "$ref" : "#/components/schemas/Disclosures" + } ], + "description" : "Disclosures message", + "type" : "object" + }, + "V20DiscoveryRecord_queries_msg" : { + "allOf" : [ { + "$ref" : "#/components/schemas/Queries" + } ], + "description" : "Queries message", + "type" : "object" + }, + "V20PresExRecord_by_format" : { + "allOf" : [ { + "$ref" : "#/components/schemas/V20PresExRecordByFormat" + } ], + "description" : "Attachment content by format for proposal, request, and presentation", + "type" : "object" + }, + "V20PresExRecord_pres" : { + "allOf" : [ { + "$ref" : "#/components/schemas/V20Pres" + } ], + "description" : "Presentation message", + "type" : "object" + }, + "V20PresExRecord_pres_proposal" : { + "allOf" : [ { + "$ref" : "#/components/schemas/V20PresProposal" + } ], + "description" : "Presentation proposal message", + "type" : "object" + }, + "V20PresExRecord_pres_request" : { + "allOf" : [ { + "$ref" : "#/components/schemas/V20PresRequest" + } ], + "description" : "Presentation request message", + "type" : "object" + }, + "V20PresProposalByFormat_dif" : { + "allOf" : [ { + "$ref" : "#/components/schemas/DIFProofProposal" + } ], + "description" : "Presentation proposal for DIF", + "type" : "object" + }, + "V20PresProposalByFormat_indy" : { + "allOf" : [ { + "$ref" : "#/components/schemas/IndyProofRequest" + } ], + "description" : "Presentation proposal for indy", + "type" : "object" + }, + "V20PresRequestByFormat_dif" : { + "allOf" : [ { + "$ref" : "#/components/schemas/DIFProofRequest" + } ], + "description" : "Presentation request for DIF", + "type" : "object" + }, + "V20PresRequestByFormat_indy" : { + "allOf" : [ { + "$ref" : "#/components/schemas/IndyProofRequest" + } ], + "description" : "Presentation request for indy", + "type" : "object" + }, + "V20PresSpecByFormatRequest_dif" : { + "allOf" : [ { + "$ref" : "#/components/schemas/DIFPresSpec" + } ], + "description" : "Optional Presentation specification for DIF, overrides the PresentionExchange record's PresRequest", + "type" : "object" + }, + "V20PresSpecByFormatRequest_indy" : { + "allOf" : [ { + "$ref" : "#/components/schemas/IndyPresSpec" + } ], + "description" : "Presentation specification for indy", + "type" : "object" + }, + "VerifyRequest_doc" : { + "allOf" : [ { + "$ref" : "#/components/schemas/SignedDoc" + } ], + "description" : "Signed document", + "type" : "object" + } + }, + "securitySchemes" : { + "AuthorizationHeader" : { + "description" : "Bearer token. Be sure to preprend token with 'Bearer '", + "in" : "header", + "name" : "Authorization", + "type" : "apiKey" + } + } + }, + "x-original-swagger-version" : "2.0" } \ No newline at end of file diff --git a/open-api/swagger.json b/open-api/swagger.json new file mode 100644 index 0000000000..71860db595 --- /dev/null +++ b/open-api/swagger.json @@ -0,0 +1,12270 @@ +{ + "swagger" : "2.0", + "info" : { + "version" : "v0.8.2", + "title" : "Aries Cloud Agent" + }, + "tags" : [ { + "name" : "action-menu", + "description" : "Menu interaction over connection" + }, { + "name" : "basicmessage", + "description" : "Simple messaging", + "externalDocs" : { + "description" : "Specification", + "url" : "https://github.com/hyperledger/aries-rfcs/tree/527849ec3aa2a8fd47a7bb6c57f918ff8bcb5e8c/features/0095-basic-message" + } + }, { + "name" : "connection", + "description" : "Connection management", + "externalDocs" : { + "description" : "Specification", + "url" : "https://github.com/hyperledger/aries-rfcs/tree/9b0aaa39df7e8bd434126c4b33c097aae78d65bf/features/0160-connection-protocol" + } + }, { + "name" : "credential-definition", + "description" : "Credential definition operations", + "externalDocs" : { + "description" : "Specification", + "url" : "https://github.com/hyperledger/indy-node/blob/master/design/anoncreds.md#cred_def" + } + }, { + "name" : "credentials", + "description" : "Holder credential management", + "externalDocs" : { + "description" : "Overview", + "url" : "https://w3c.github.io/vc-data-model/#credentials" + } + }, { + "name" : "did-exchange", + "description" : "Connection management via DID exchange", + "externalDocs" : { + "description" : "Specification", + "url" : "https://github.com/hyperledger/aries-rfcs/tree/25464a5c8f8a17b14edaa4310393df6094ace7b0/features/0023-did-exchange" + } + }, { + "name" : "discover-features", + "description" : "Feature discovery", + "externalDocs" : { + "description" : "Specification", + "url" : "https://github.com/hyperledger/aries-rfcs/tree/b3a3942ef052039e73cd23d847f42947f8287da2/features/0031-discover-features" + } + }, { + "name" : "discover-features v2.0", + "description" : "Feature discovery v2", + "externalDocs" : { + "description" : "Specification", + "url" : "https://github.com/hyperledger/aries-rfcs/tree/b3a3942ef052039e73cd23d847f42947f8287da2/features/0557-discover-features-v2" + } + }, { + "name" : "endorse-transaction", + "description" : "Endorse a Transaction" + }, { + "name" : "introduction", + "description" : "Introduction of known parties" + }, { + "name" : "issue-credential v1.0", + "description" : "Credential issue v1.0", + "externalDocs" : { + "description" : "Specification", + "url" : "https://github.com/hyperledger/aries-rfcs/tree/bb42a6c35e0d5543718fb36dd099551ab192f7b0/features/0036-issue-credential" + } + }, { + "name" : "issue-credential v2.0", + "description" : "Credential issue v2.0", + "externalDocs" : { + "description" : "Specification", + "url" : "https://github.com/hyperledger/aries-rfcs/tree/cd27fc64aa2805f756a118043d7c880354353047/features/0453-issue-credential-v2" + } + }, { + "name" : "jsonld", + "description" : "Sign and verify json-ld data", + "externalDocs" : { + "description" : "Specification", + "url" : "https://tools.ietf.org/html/rfc7515" + } + }, { + "name" : "ledger", + "description" : "Interaction with ledger", + "externalDocs" : { + "description" : "Overview", + "url" : "https://hyperledger-indy.readthedocs.io/projects/plenum/en/latest/storage.html#ledger" + } + }, { + "name" : "mediation", + "description" : "Mediation management", + "externalDocs" : { + "description" : "Specification", + "url" : "https://github.com/hyperledger/aries-rfcs/tree/fa8dc4ea1e667eb07db8f9ffeaf074a4455697c0/features/0211-route-coordination" + } + }, { + "name" : "multitenancy", + "description" : "Multitenant wallet management" + }, { + "name" : "out-of-band", + "description" : "Out-of-band connections", + "externalDocs" : { + "description" : "Design", + "url" : "https://github.com/hyperledger/aries-rfcs/tree/2da7fc4ee043effa3a9960150e7ba8c9a4628b68/features/0434-outofband" + } + }, { + "name" : "present-proof v1.0", + "description" : "Proof presentation v1.0", + "externalDocs" : { + "description" : "Specification", + "url" : "https://github.com/hyperledger/aries-rfcs/tree/4fae574c03f9f1013db30bf2c0c676b1122f7149/features/0037-present-proof" + } + }, { + "name" : "present-proof v2.0", + "description" : "Proof presentation v2.0", + "externalDocs" : { + "description" : "Specification", + "url" : "https://github.com/hyperledger/aries-rfcs/tree/eace815c3e8598d4a8dd7881d8c731fdb2bcc0aa/features/0454-present-proof-v2" + } + }, { + "name" : "resolver", + "description" : "did resolver interface.", + "externalDocs" : { + "description" : "Specification" + } + }, { + "name" : "revocation", + "description" : "Revocation registry management", + "externalDocs" : { + "description" : "Overview", + "url" : "https://github.com/hyperledger/indy-hipe/tree/master/text/0011-cred-revocation" + } + }, { + "name" : "schema", + "description" : "Schema operations", + "externalDocs" : { + "description" : "Specification", + "url" : "https://github.com/hyperledger/indy-node/blob/master/design/anoncreds.md#schema" + } + }, { + "name" : "trustping", + "description" : "Trust-ping over connection", + "externalDocs" : { + "description" : "Specification", + "url" : "https://github.com/hyperledger/aries-rfcs/tree/527849ec3aa2a8fd47a7bb6c57f918ff8bcb5e8c/features/0048-trust-ping" + } + }, { + "name" : "wallet", + "description" : "DID and tag policy management", + "externalDocs" : { + "description" : "Design", + "url" : "https://github.com/hyperledger/indy-sdk/tree/master/docs/design/003-wallet-storage" + } + } ], + "security" : [ { + "AuthorizationHeader" : [ ] + } ], + "paths" : { + "/action-menu/{conn_id}/close" : { + "post" : { + "tags" : [ "action-menu" ], + "summary" : "Close the active menu associated with a connection", + "produces" : [ "application/json" ], + "parameters" : [ { + "name" : "conn_id", + "in" : "path", + "description" : "Connection identifier", + "required" : true, + "type" : "string" + } ], + "responses" : { + "200" : { + "description" : "", + "schema" : { + "$ref" : "#/definitions/ActionMenuModulesResult" + } + } + } + } + }, + "/action-menu/{conn_id}/fetch" : { + "post" : { + "tags" : [ "action-menu" ], + "summary" : "Fetch the active menu", + "produces" : [ "application/json" ], + "parameters" : [ { + "name" : "conn_id", + "in" : "path", + "description" : "Connection identifier", + "required" : true, + "type" : "string" + } ], + "responses" : { + "200" : { + "description" : "", + "schema" : { + "$ref" : "#/definitions/ActionMenuFetchResult" + } + } + } + } + }, + "/action-menu/{conn_id}/perform" : { + "post" : { + "tags" : [ "action-menu" ], + "summary" : "Perform an action associated with the active menu", + "produces" : [ "application/json" ], + "parameters" : [ { + "in" : "body", + "name" : "body", + "required" : false, + "schema" : { + "$ref" : "#/definitions/PerformRequest" + } + }, { + "name" : "conn_id", + "in" : "path", + "description" : "Connection identifier", + "required" : true, + "type" : "string" + } ], + "responses" : { + "200" : { + "description" : "", + "schema" : { + "$ref" : "#/definitions/ActionMenuModulesResult" + } + } + } + } + }, + "/action-menu/{conn_id}/request" : { + "post" : { + "tags" : [ "action-menu" ], + "summary" : "Request the active menu", + "produces" : [ "application/json" ], + "parameters" : [ { + "name" : "conn_id", + "in" : "path", + "description" : "Connection identifier", + "required" : true, + "type" : "string" + } ], + "responses" : { + "200" : { + "description" : "", + "schema" : { + "$ref" : "#/definitions/ActionMenuModulesResult" + } + } + } + } + }, + "/action-menu/{conn_id}/send-menu" : { + "post" : { + "tags" : [ "action-menu" ], + "summary" : "Send an action menu to a connection", + "produces" : [ "application/json" ], + "parameters" : [ { + "in" : "body", + "name" : "body", + "required" : false, + "schema" : { + "$ref" : "#/definitions/SendMenu" + } + }, { + "name" : "conn_id", + "in" : "path", + "description" : "Connection identifier", + "required" : true, + "type" : "string" + } ], + "responses" : { + "200" : { + "description" : "", + "schema" : { + "$ref" : "#/definitions/ActionMenuModulesResult" + } + } + } + } + }, + "/connections" : { + "get" : { + "tags" : [ "connection" ], + "summary" : "Query agent-to-agent connections", + "produces" : [ "application/json" ], + "parameters" : [ { + "name" : "alias", + "in" : "query", + "description" : "Alias", + "required" : false, + "type" : "string" + }, { + "name" : "connection_protocol", + "in" : "query", + "description" : "Connection protocol used", + "required" : false, + "type" : "string", + "enum" : [ "connections/1.0", "didexchange/1.0" ] + }, { + "name" : "invitation_key", + "in" : "query", + "description" : "invitation key", + "required" : false, + "type" : "string", + "pattern" : "^[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{43,44}$" + }, { + "name" : "invitation_msg_id", + "in" : "query", + "description" : "Identifier of the associated Invitation Mesage", + "required" : false, + "type" : "string", + "format" : "uuid" + }, { + "name" : "my_did", + "in" : "query", + "description" : "My DID", + "required" : false, + "type" : "string", + "pattern" : "^(did:sov:)?[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}$" + }, { + "name" : "state", + "in" : "query", + "description" : "Connection state", + "required" : false, + "type" : "string", + "enum" : [ "start", "invitation", "request", "abandoned", "error", "init", "response", "active", "completed" ] + }, { + "name" : "their_did", + "in" : "query", + "description" : "Their DID", + "required" : false, + "type" : "string", + "pattern" : "^(did:sov:)?[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}$" + }, { + "name" : "their_public_did", + "in" : "query", + "description" : "Their Public DID", + "required" : false, + "type" : "string", + "pattern" : "^(did:sov:)?[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}$" + }, { + "name" : "their_role", + "in" : "query", + "description" : "Their role in the connection protocol", + "required" : false, + "type" : "string", + "enum" : [ "invitee", "requester", "inviter", "responder" ] + } ], + "responses" : { + "200" : { + "description" : "", + "schema" : { + "$ref" : "#/definitions/ConnectionList" + } + } + } + } + }, + "/connections/create-invitation" : { + "post" : { + "tags" : [ "connection" ], + "summary" : "Create a new connection invitation", + "produces" : [ "application/json" ], + "parameters" : [ { + "in" : "body", + "name" : "body", + "required" : false, + "schema" : { + "$ref" : "#/definitions/CreateInvitationRequest" + } + }, { + "name" : "alias", + "in" : "query", + "description" : "Alias", + "required" : false, + "type" : "string" + }, { + "name" : "auto_accept", + "in" : "query", + "description" : "Auto-accept connection (defaults to configuration)", + "required" : false, + "type" : "boolean" + }, { + "name" : "multi_use", + "in" : "query", + "description" : "Create invitation for multiple use (default false)", + "required" : false, + "type" : "boolean" + }, { + "name" : "public", + "in" : "query", + "description" : "Create invitation from public DID (default false)", + "required" : false, + "type" : "boolean" + } ], + "responses" : { + "200" : { + "description" : "", + "schema" : { + "$ref" : "#/definitions/InvitationResult" + } + } + } + } + }, + "/connections/create-static" : { + "post" : { + "tags" : [ "connection" ], + "summary" : "Create a new static connection", + "produces" : [ "application/json" ], + "parameters" : [ { + "in" : "body", + "name" : "body", + "required" : false, + "schema" : { + "$ref" : "#/definitions/ConnectionStaticRequest" + } + } ], + "responses" : { + "200" : { + "description" : "", + "schema" : { + "$ref" : "#/definitions/ConnectionStaticResult" + } + } + } + } + }, + "/connections/receive-invitation" : { + "post" : { + "tags" : [ "connection" ], + "summary" : "Receive a new connection invitation", + "produces" : [ "application/json" ], + "parameters" : [ { + "in" : "body", + "name" : "body", + "required" : false, + "schema" : { + "$ref" : "#/definitions/ReceiveInvitationRequest" + } + }, { + "name" : "alias", + "in" : "query", + "description" : "Alias", + "required" : false, + "type" : "string" + }, { + "name" : "auto_accept", + "in" : "query", + "description" : "Auto-accept connection (defaults to configuration)", + "required" : false, + "type" : "boolean" + }, { + "name" : "mediation_id", + "in" : "query", + "description" : "Identifier for active mediation record to be used", + "required" : false, + "type" : "string", + "pattern" : "[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-4[a-fA-F0-9]{3}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}" + } ], + "responses" : { + "200" : { + "description" : "", + "schema" : { + "$ref" : "#/definitions/ConnRecord" + } + } + } + } + }, + "/connections/{conn_id}" : { + "get" : { + "tags" : [ "connection" ], + "summary" : "Fetch a single connection record", + "produces" : [ "application/json" ], + "parameters" : [ { + "name" : "conn_id", + "in" : "path", + "description" : "Connection identifier", + "required" : true, + "type" : "string" + } ], + "responses" : { + "200" : { + "description" : "", + "schema" : { + "$ref" : "#/definitions/ConnRecord" + } + } + } + }, + "delete" : { + "tags" : [ "connection" ], + "summary" : "Remove an existing connection record", + "produces" : [ "application/json" ], + "parameters" : [ { + "name" : "conn_id", + "in" : "path", + "description" : "Connection identifier", + "required" : true, + "type" : "string" + } ], + "responses" : { + "200" : { + "description" : "", + "schema" : { + "$ref" : "#/definitions/ConnectionModuleResponse" + } + } + } + } + }, + "/connections/{conn_id}/accept-invitation" : { + "post" : { + "tags" : [ "connection" ], + "summary" : "Accept a stored connection invitation", + "produces" : [ "application/json" ], + "parameters" : [ { + "name" : "conn_id", + "in" : "path", + "description" : "Connection identifier", + "required" : true, + "type" : "string" + }, { + "name" : "mediation_id", + "in" : "query", + "description" : "Identifier for active mediation record to be used", + "required" : false, + "type" : "string", + "pattern" : "[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-4[a-fA-F0-9]{3}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}" + }, { + "name" : "my_endpoint", + "in" : "query", + "description" : "My URL endpoint", + "required" : false, + "type" : "string", + "pattern" : "^[A-Za-z0-9\\.\\-\\+]+://([A-Za-z0-9][.A-Za-z0-9-_]+[A-Za-z0-9])+(:[1-9][0-9]*)?(/[^?&#]+)?$" + }, { + "name" : "my_label", + "in" : "query", + "description" : "Label for connection", + "required" : false, + "type" : "string" + } ], + "responses" : { + "200" : { + "description" : "", + "schema" : { + "$ref" : "#/definitions/ConnRecord" + } + } + } + } + }, + "/connections/{conn_id}/accept-request" : { + "post" : { + "tags" : [ "connection" ], + "summary" : "Accept a stored connection request", + "produces" : [ "application/json" ], + "parameters" : [ { + "name" : "conn_id", + "in" : "path", + "description" : "Connection identifier", + "required" : true, + "type" : "string" + }, { + "name" : "my_endpoint", + "in" : "query", + "description" : "My URL endpoint", + "required" : false, + "type" : "string", + "pattern" : "^[A-Za-z0-9\\.\\-\\+]+://([A-Za-z0-9][.A-Za-z0-9-_]+[A-Za-z0-9])+(:[1-9][0-9]*)?(/[^?&#]+)?$" + } ], + "responses" : { + "200" : { + "description" : "", + "schema" : { + "$ref" : "#/definitions/ConnRecord" + } + } + } + } + }, + "/connections/{conn_id}/endpoints" : { + "get" : { + "tags" : [ "connection" ], + "summary" : "Fetch connection remote endpoint", + "produces" : [ "application/json" ], + "parameters" : [ { + "name" : "conn_id", + "in" : "path", + "description" : "Connection identifier", + "required" : true, + "type" : "string" + } ], + "responses" : { + "200" : { + "description" : "", + "schema" : { + "$ref" : "#/definitions/EndpointsResult" + } + } + } + } + }, + "/connections/{conn_id}/establish-inbound/{ref_id}" : { + "post" : { + "tags" : [ "connection" ], + "summary" : "Assign another connection as the inbound connection", + "produces" : [ "application/json" ], + "parameters" : [ { + "name" : "conn_id", + "in" : "path", + "description" : "Connection identifier", + "required" : true, + "type" : "string" + }, { + "name" : "ref_id", + "in" : "path", + "description" : "Inbound connection identifier", + "required" : true, + "type" : "string" + } ], + "responses" : { + "200" : { + "description" : "", + "schema" : { + "$ref" : "#/definitions/ConnectionModuleResponse" + } + } + } + } + }, + "/connections/{conn_id}/metadata" : { + "get" : { + "tags" : [ "connection" ], + "summary" : "Fetch connection metadata", + "produces" : [ "application/json" ], + "parameters" : [ { + "name" : "conn_id", + "in" : "path", + "description" : "Connection identifier", + "required" : true, + "type" : "string" + }, { + "name" : "key", + "in" : "query", + "description" : "Key to retrieve.", + "required" : false, + "type" : "string" + } ], + "responses" : { + "200" : { + "description" : "", + "schema" : { + "$ref" : "#/definitions/ConnectionMetadata" + } + } + } + }, + "post" : { + "tags" : [ "connection" ], + "summary" : "Set connection metadata", + "produces" : [ "application/json" ], + "parameters" : [ { + "in" : "body", + "name" : "body", + "required" : false, + "schema" : { + "$ref" : "#/definitions/ConnectionMetadataSetRequest" + } + }, { + "name" : "conn_id", + "in" : "path", + "description" : "Connection identifier", + "required" : true, + "type" : "string" + } ], + "responses" : { + "200" : { + "description" : "", + "schema" : { + "$ref" : "#/definitions/ConnectionMetadata" + } + } + } + } + }, + "/connections/{conn_id}/send-message" : { + "post" : { + "tags" : [ "basicmessage" ], + "summary" : "Send a basic message to a connection", + "produces" : [ "application/json" ], + "parameters" : [ { + "in" : "body", + "name" : "body", + "required" : false, + "schema" : { + "$ref" : "#/definitions/SendMessage" + } + }, { + "name" : "conn_id", + "in" : "path", + "description" : "Connection identifier", + "required" : true, + "type" : "string" + } ], + "responses" : { + "200" : { + "description" : "", + "schema" : { + "$ref" : "#/definitions/BasicMessageModuleResponse" + } + } + } + } + }, + "/connections/{conn_id}/send-ping" : { + "post" : { + "tags" : [ "trustping" ], + "summary" : "Send a trust ping to a connection", + "produces" : [ "application/json" ], + "parameters" : [ { + "in" : "body", + "name" : "body", + "required" : false, + "schema" : { + "$ref" : "#/definitions/PingRequest" + } + }, { + "name" : "conn_id", + "in" : "path", + "description" : "Connection identifier", + "required" : true, + "type" : "string" + } ], + "responses" : { + "200" : { + "description" : "", + "schema" : { + "$ref" : "#/definitions/PingRequestResponse" + } + } + } + } + }, + "/connections/{conn_id}/start-introduction" : { + "post" : { + "tags" : [ "introduction" ], + "summary" : "Start an introduction between two connections", + "produces" : [ "application/json" ], + "parameters" : [ { + "name" : "conn_id", + "in" : "path", + "description" : "Connection identifier", + "required" : true, + "type" : "string" + }, { + "name" : "target_connection_id", + "in" : "query", + "description" : "Target connection identifier", + "required" : true, + "type" : "string" + }, { + "name" : "message", + "in" : "query", + "description" : "Message", + "required" : false, + "type" : "string" + } ], + "responses" : { + "200" : { + "description" : "", + "schema" : { + "$ref" : "#/definitions/IntroModuleResponse" + } + } + } + } + }, + "/credential-definitions" : { + "post" : { + "tags" : [ "credential-definition" ], + "summary" : "Sends a credential definition to the ledger", + "produces" : [ "application/json" ], + "parameters" : [ { + "in" : "body", + "name" : "body", + "required" : false, + "schema" : { + "$ref" : "#/definitions/CredentialDefinitionSendRequest" + } + }, { + "name" : "conn_id", + "in" : "query", + "description" : "Connection identifier", + "required" : false, + "type" : "string" + }, { + "name" : "create_transaction_for_endorser", + "in" : "query", + "description" : "Create Transaction For Endorser's signature", + "required" : false, + "type" : "boolean" + } ], + "responses" : { + "200" : { + "description" : "", + "schema" : { + "$ref" : "#/definitions/TxnOrCredentialDefinitionSendResult" + } + } + } + } + }, + "/credential-definitions/created" : { + "get" : { + "tags" : [ "credential-definition" ], + "summary" : "Search for matching credential definitions that agent originated", + "produces" : [ "application/json" ], + "parameters" : [ { + "name" : "cred_def_id", + "in" : "query", + "description" : "Credential definition id", + "required" : false, + "type" : "string", + "pattern" : "^([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}):3:CL:(([1-9][0-9]*)|([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}:2:.+:[0-9.]+)):(.+)?$" + }, { + "name" : "issuer_did", + "in" : "query", + "description" : "Issuer DID", + "required" : false, + "type" : "string", + "pattern" : "^(did:sov:)?[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}$" + }, { + "name" : "schema_id", + "in" : "query", + "description" : "Schema identifier", + "required" : false, + "type" : "string", + "pattern" : "^[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}:2:.+:[0-9.]+$" + }, { + "name" : "schema_issuer_did", + "in" : "query", + "description" : "Schema issuer DID", + "required" : false, + "type" : "string", + "pattern" : "^(did:sov:)?[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}$" + }, { + "name" : "schema_name", + "in" : "query", + "description" : "Schema name", + "required" : false, + "type" : "string" + }, { + "name" : "schema_version", + "in" : "query", + "description" : "Schema version", + "required" : false, + "type" : "string", + "pattern" : "^[0-9.]+$" + } ], + "responses" : { + "200" : { + "description" : "", + "schema" : { + "$ref" : "#/definitions/CredentialDefinitionsCreatedResult" + } + } + } + } + }, + "/credential-definitions/{cred_def_id}" : { + "get" : { + "tags" : [ "credential-definition" ], + "summary" : "Gets a credential definition from the ledger", + "produces" : [ "application/json" ], + "parameters" : [ { + "name" : "cred_def_id", + "in" : "path", + "description" : "Credential definition identifier", + "required" : true, + "type" : "string", + "pattern" : "^([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}):3:CL:(([1-9][0-9]*)|([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}:2:.+:[0-9.]+)):(.+)?$" + } ], + "responses" : { + "200" : { + "description" : "", + "schema" : { + "$ref" : "#/definitions/CredentialDefinitionGetResult" + } + } + } + } + }, + "/credential-definitions/{cred_def_id}/write_record" : { + "post" : { + "tags" : [ "credential-definition" ], + "summary" : "Writes a credential definition non-secret record to the wallet", + "produces" : [ "application/json" ], + "parameters" : [ { + "name" : "cred_def_id", + "in" : "path", + "description" : "Credential definition identifier", + "required" : true, + "type" : "string", + "pattern" : "^([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}):3:CL:(([1-9][0-9]*)|([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}:2:.+:[0-9.]+)):(.+)?$" + } ], + "responses" : { + "200" : { + "description" : "", + "schema" : { + "$ref" : "#/definitions/CredentialDefinitionGetResult" + } + } + } + } + }, + "/credential/mime-types/{credential_id}" : { + "get" : { + "tags" : [ "credentials" ], + "summary" : "Get attribute MIME types from wallet", + "produces" : [ "application/json" ], + "parameters" : [ { + "name" : "credential_id", + "in" : "path", + "description" : "Credential identifier", + "required" : true, + "type" : "string" + } ], + "responses" : { + "200" : { + "description" : "", + "schema" : { + "$ref" : "#/definitions/AttributeMimeTypesResult" + } + } + } + } + }, + "/credential/revoked/{credential_id}" : { + "get" : { + "tags" : [ "credentials" ], + "summary" : "Query credential revocation status by id", + "produces" : [ "application/json" ], + "parameters" : [ { + "name" : "credential_id", + "in" : "path", + "description" : "Credential identifier", + "required" : true, + "type" : "string" + }, { + "name" : "from", + "in" : "query", + "description" : "Earliest epoch of revocation status interval of interest", + "required" : false, + "type" : "string", + "pattern" : "^[0-9]*$" + }, { + "name" : "to", + "in" : "query", + "description" : "Latest epoch of revocation status interval of interest", + "required" : false, + "type" : "string", + "pattern" : "^[0-9]*$" + } ], + "responses" : { + "200" : { + "description" : "", + "schema" : { + "$ref" : "#/definitions/CredRevokedResult" + } + } + } + } + }, + "/credential/w3c/{credential_id}" : { + "get" : { + "tags" : [ "credentials" ], + "summary" : "Fetch W3C credential from wallet by id", + "produces" : [ "application/json" ], + "parameters" : [ { + "name" : "credential_id", + "in" : "path", + "description" : "Credential identifier", + "required" : true, + "type" : "string" + } ], + "responses" : { + "200" : { + "description" : "", + "schema" : { + "$ref" : "#/definitions/VCRecord" + } + } + } + }, + "delete" : { + "tags" : [ "credentials" ], + "summary" : "Remove W3C credential from wallet by id", + "produces" : [ "application/json" ], + "parameters" : [ { + "name" : "credential_id", + "in" : "path", + "description" : "Credential identifier", + "required" : true, + "type" : "string" + } ], + "responses" : { + "200" : { + "description" : "", + "schema" : { + "$ref" : "#/definitions/HolderModuleResponse" + } + } + } + } + }, + "/credential/{credential_id}" : { + "get" : { + "tags" : [ "credentials" ], + "summary" : "Fetch credential from wallet by id", + "produces" : [ "application/json" ], + "parameters" : [ { + "name" : "credential_id", + "in" : "path", + "description" : "Credential identifier", + "required" : true, + "type" : "string" + } ], + "responses" : { + "200" : { + "description" : "", + "schema" : { + "$ref" : "#/definitions/IndyCredInfo" + } + } + } + }, + "delete" : { + "tags" : [ "credentials" ], + "summary" : "Remove credential from wallet by id", + "produces" : [ "application/json" ], + "parameters" : [ { + "name" : "credential_id", + "in" : "path", + "description" : "Credential identifier", + "required" : true, + "type" : "string" + } ], + "responses" : { + "200" : { + "description" : "", + "schema" : { + "$ref" : "#/definitions/HolderModuleResponse" + } + } + } + } + }, + "/credentials" : { + "get" : { + "tags" : [ "credentials" ], + "summary" : "Fetch credentials from wallet", + "produces" : [ "application/json" ], + "parameters" : [ { + "name" : "count", + "in" : "query", + "description" : "Maximum number to retrieve", + "required" : false, + "type" : "string", + "pattern" : "^[1-9][0-9]*$" + }, { + "name" : "start", + "in" : "query", + "description" : "Start index", + "required" : false, + "type" : "string", + "pattern" : "^[0-9]*$" + }, { + "name" : "wql", + "in" : "query", + "description" : "(JSON) WQL query", + "required" : false, + "type" : "string", + "pattern" : "^{.*}$" + } ], + "responses" : { + "200" : { + "description" : "", + "schema" : { + "$ref" : "#/definitions/CredInfoList" + } + } + } + } + }, + "/credentials/w3c" : { + "post" : { + "tags" : [ "credentials" ], + "summary" : "Fetch W3C credentials from wallet", + "produces" : [ "application/json" ], + "parameters" : [ { + "in" : "body", + "name" : "body", + "required" : false, + "schema" : { + "$ref" : "#/definitions/W3CCredentialsListRequest" + } + }, { + "name" : "count", + "in" : "query", + "description" : "Maximum number to retrieve", + "required" : false, + "type" : "string", + "pattern" : "^[1-9][0-9]*$" + }, { + "name" : "start", + "in" : "query", + "description" : "Start index", + "required" : false, + "type" : "string", + "pattern" : "^[0-9]*$" + }, { + "name" : "wql", + "in" : "query", + "description" : "(JSON) WQL query", + "required" : false, + "type" : "string", + "pattern" : "^{.*}$" + } ], + "responses" : { + "200" : { + "description" : "", + "schema" : { + "$ref" : "#/definitions/VCRecordList" + } + } + } + } + }, + "/didexchange/create-request" : { + "post" : { + "tags" : [ "did-exchange" ], + "summary" : "Create and send a request against public DID's implicit invitation", + "produces" : [ "application/json" ], + "parameters" : [ { + "name" : "their_public_did", + "in" : "query", + "description" : "Qualified public DID to which to request connection", + "required" : true, + "type" : "string", + "pattern" : "^(did:sov:)?[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}$|^did:([a-zA-Z0-9_]+):([a-zA-Z0-9_.%-]+(:[a-zA-Z0-9_.%-]+)*)((;[a-zA-Z0-9_.:%-]+=[a-zA-Z0-9_.:%-]*)*)(\\/[^#?]*)?([?][^#]*)?(\\#.*)?$$" + }, { + "name" : "alias", + "in" : "query", + "description" : "Alias for connection", + "required" : false, + "type" : "string" + }, { + "name" : "mediation_id", + "in" : "query", + "description" : "Identifier for active mediation record to be used", + "required" : false, + "type" : "string", + "pattern" : "[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-4[a-fA-F0-9]{3}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}" + }, { + "name" : "my_endpoint", + "in" : "query", + "description" : "My URL endpoint", + "required" : false, + "type" : "string", + "pattern" : "^[A-Za-z0-9\\.\\-\\+]+://([A-Za-z0-9][.A-Za-z0-9-_]+[A-Za-z0-9])+(:[1-9][0-9]*)?(/[^?&#]+)?$" + }, { + "name" : "my_label", + "in" : "query", + "description" : "Label for connection request", + "required" : false, + "type" : "string" + }, { + "name" : "use_public_did", + "in" : "query", + "description" : "Use public DID for this connection", + "required" : false, + "type" : "boolean" + } ], + "responses" : { + "200" : { + "description" : "", + "schema" : { + "$ref" : "#/definitions/ConnRecord" + } + } + } + } + }, + "/didexchange/receive-request" : { + "post" : { + "tags" : [ "did-exchange" ], + "summary" : "Receive request against public DID's implicit invitation", + "produces" : [ "application/json" ], + "parameters" : [ { + "in" : "body", + "name" : "body", + "required" : false, + "schema" : { + "$ref" : "#/definitions/DIDXRequest" + } + }, { + "name" : "alias", + "in" : "query", + "description" : "Alias for connection", + "required" : false, + "type" : "string" + }, { + "name" : "auto_accept", + "in" : "query", + "description" : "Auto-accept connection (defaults to configuration)", + "required" : false, + "type" : "boolean" + }, { + "name" : "mediation_id", + "in" : "query", + "description" : "Identifier for active mediation record to be used", + "required" : false, + "type" : "string", + "pattern" : "[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-4[a-fA-F0-9]{3}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}" + }, { + "name" : "my_endpoint", + "in" : "query", + "description" : "My URL endpoint", + "required" : false, + "type" : "string", + "pattern" : "^[A-Za-z0-9\\.\\-\\+]+://([A-Za-z0-9][.A-Za-z0-9-_]+[A-Za-z0-9])+(:[1-9][0-9]*)?(/[^?&#]+)?$" + } ], + "responses" : { + "200" : { + "description" : "", + "schema" : { + "$ref" : "#/definitions/ConnRecord" + } + } + } + } + }, + "/didexchange/{conn_id}/accept-invitation" : { + "post" : { + "tags" : [ "did-exchange" ], + "summary" : "Accept a stored connection invitation", + "produces" : [ "application/json" ], + "parameters" : [ { + "name" : "conn_id", + "in" : "path", + "description" : "Connection identifier", + "required" : true, + "type" : "string" + }, { + "name" : "my_endpoint", + "in" : "query", + "description" : "My URL endpoint", + "required" : false, + "type" : "string", + "pattern" : "^[A-Za-z0-9\\.\\-\\+]+://([A-Za-z0-9][.A-Za-z0-9-_]+[A-Za-z0-9])+(:[1-9][0-9]*)?(/[^?&#]+)?$" + }, { + "name" : "my_label", + "in" : "query", + "description" : "Label for connection request", + "required" : false, + "type" : "string" + } ], + "responses" : { + "200" : { + "description" : "", + "schema" : { + "$ref" : "#/definitions/ConnRecord" + } + } + } + } + }, + "/didexchange/{conn_id}/accept-request" : { + "post" : { + "tags" : [ "did-exchange" ], + "summary" : "Accept a stored connection request", + "produces" : [ "application/json" ], + "parameters" : [ { + "name" : "conn_id", + "in" : "path", + "description" : "Connection identifier", + "required" : true, + "type" : "string" + }, { + "name" : "mediation_id", + "in" : "query", + "description" : "Identifier for active mediation record to be used", + "required" : false, + "type" : "string", + "pattern" : "[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-4[a-fA-F0-9]{3}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}" + }, { + "name" : "my_endpoint", + "in" : "query", + "description" : "My URL endpoint", + "required" : false, + "type" : "string", + "pattern" : "^[A-Za-z0-9\\.\\-\\+]+://([A-Za-z0-9][.A-Za-z0-9-_]+[A-Za-z0-9])+(:[1-9][0-9]*)?(/[^?&#]+)?$" + } ], + "responses" : { + "200" : { + "description" : "", + "schema" : { + "$ref" : "#/definitions/ConnRecord" + } + } + } + } + }, + "/discover-features-2.0/queries" : { + "get" : { + "tags" : [ "discover-features v2.0" ], + "summary" : "Query supported features", + "produces" : [ "application/json" ], + "parameters" : [ { + "name" : "connection_id", + "in" : "query", + "description" : "Connection identifier, if none specified, then the query will provide features for this agent.", + "required" : false, + "type" : "string" + }, { + "name" : "query_goal_code", + "in" : "query", + "description" : "Goal-code feature-type query", + "required" : false, + "type" : "string" + }, { + "name" : "query_protocol", + "in" : "query", + "description" : "Protocol feature-type query", + "required" : false, + "type" : "string" + } ], + "responses" : { + "200" : { + "description" : "", + "schema" : { + "$ref" : "#/definitions/V20DiscoveryExchangeResult" + } + } + } + } + }, + "/discover-features-2.0/records" : { + "get" : { + "tags" : [ "discover-features v2.0" ], + "summary" : "Discover Features v2.0 records", + "produces" : [ "application/json" ], + "parameters" : [ { + "name" : "connection_id", + "in" : "query", + "description" : "Connection identifier", + "required" : false, + "type" : "string" + } ], + "responses" : { + "200" : { + "description" : "", + "schema" : { + "$ref" : "#/definitions/V20DiscoveryExchangeListResult" + } + } + } + } + }, + "/discover-features/query" : { + "get" : { + "tags" : [ "discover-features" ], + "summary" : "Query supported features", + "produces" : [ "application/json" ], + "parameters" : [ { + "name" : "comment", + "in" : "query", + "description" : "Comment", + "required" : false, + "type" : "string" + }, { + "name" : "connection_id", + "in" : "query", + "description" : "Connection identifier, if none specified, then the query will provide features for this agent.", + "required" : false, + "type" : "string" + }, { + "name" : "query", + "in" : "query", + "description" : "Protocol feature query", + "required" : false, + "type" : "string" + } ], + "responses" : { + "200" : { + "description" : "", + "schema" : { + "$ref" : "#/definitions/V10DiscoveryRecord" + } + } + } + } + }, + "/discover-features/records" : { + "get" : { + "tags" : [ "discover-features" ], + "summary" : "Discover Features records", + "produces" : [ "application/json" ], + "parameters" : [ { + "name" : "connection_id", + "in" : "query", + "description" : "Connection identifier", + "required" : false, + "type" : "string" + } ], + "responses" : { + "200" : { + "description" : "", + "schema" : { + "$ref" : "#/definitions/V10DiscoveryExchangeListResult" + } + } + } + } + }, + "/issue-credential-2.0/create" : { + "post" : { + "tags" : [ "issue-credential v2.0" ], + "summary" : "Create a credential record without sending (generally for use with Out-Of-Band)", + "produces" : [ "application/json" ], + "parameters" : [ { + "in" : "body", + "name" : "body", + "required" : false, + "schema" : { + "$ref" : "#/definitions/V20IssueCredSchemaCore" + } + } ], + "responses" : { + "200" : { + "description" : "", + "schema" : { + "$ref" : "#/definitions/V20CredExRecord" + } + } + } + } + }, + "/issue-credential-2.0/create-offer" : { + "post" : { + "tags" : [ "issue-credential v2.0" ], + "summary" : "Create a credential offer, independent of any proposal or connection", + "produces" : [ "application/json" ], + "parameters" : [ { + "in" : "body", + "name" : "body", + "required" : false, + "schema" : { + "$ref" : "#/definitions/V20CredOfferConnFreeRequest" + } + } ], + "responses" : { + "200" : { + "description" : "", + "schema" : { + "$ref" : "#/definitions/V20CredExRecord" + } + } + } + } + }, + "/issue-credential-2.0/records" : { + "get" : { + "tags" : [ "issue-credential v2.0" ], + "summary" : "Fetch all credential exchange records", + "produces" : [ "application/json" ], + "parameters" : [ { + "name" : "connection_id", + "in" : "query", + "description" : "Connection identifier", + "required" : false, + "type" : "string", + "format" : "uuid" + }, { + "name" : "role", + "in" : "query", + "description" : "Role assigned in credential exchange", + "required" : false, + "type" : "string", + "enum" : [ "issuer", "holder" ] + }, { + "name" : "state", + "in" : "query", + "description" : "Credential exchange state", + "required" : false, + "type" : "string", + "enum" : [ "proposal-sent", "proposal-received", "offer-sent", "offer-received", "request-sent", "request-received", "credential-issued", "credential-received", "done", "credential-revoked", "abandoned" ] + }, { + "name" : "thread_id", + "in" : "query", + "description" : "Thread identifier", + "required" : false, + "type" : "string", + "format" : "uuid" + } ], + "responses" : { + "200" : { + "description" : "", + "schema" : { + "$ref" : "#/definitions/V20CredExRecordListResult" + } + } + } + } + }, + "/issue-credential-2.0/records/{cred_ex_id}" : { + "get" : { + "tags" : [ "issue-credential v2.0" ], + "summary" : "Fetch a single credential exchange record", + "produces" : [ "application/json" ], + "parameters" : [ { + "name" : "cred_ex_id", + "in" : "path", + "description" : "Credential exchange identifier", + "required" : true, + "type" : "string", + "pattern" : "[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-4[a-fA-F0-9]{3}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}" + } ], + "responses" : { + "200" : { + "description" : "", + "schema" : { + "$ref" : "#/definitions/V20CredExRecordDetail" + } + } + } + }, + "delete" : { + "tags" : [ "issue-credential v2.0" ], + "summary" : "Remove an existing credential exchange record", + "produces" : [ "application/json" ], + "parameters" : [ { + "name" : "cred_ex_id", + "in" : "path", + "description" : "Credential exchange identifier", + "required" : true, + "type" : "string", + "pattern" : "[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-4[a-fA-F0-9]{3}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}" + } ], + "responses" : { + "200" : { + "description" : "", + "schema" : { + "$ref" : "#/definitions/V20IssueCredentialModuleResponse" + } + } + } + } + }, + "/issue-credential-2.0/records/{cred_ex_id}/issue" : { + "post" : { + "tags" : [ "issue-credential v2.0" ], + "summary" : "Send holder a credential", + "produces" : [ "application/json" ], + "parameters" : [ { + "in" : "body", + "name" : "body", + "required" : false, + "schema" : { + "$ref" : "#/definitions/V20CredIssueRequest" + } + }, { + "name" : "cred_ex_id", + "in" : "path", + "description" : "Credential exchange identifier", + "required" : true, + "type" : "string", + "pattern" : "[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-4[a-fA-F0-9]{3}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}" + } ], + "responses" : { + "200" : { + "description" : "", + "schema" : { + "$ref" : "#/definitions/V20CredExRecordDetail" + } + } + } + } + }, + "/issue-credential-2.0/records/{cred_ex_id}/problem-report" : { + "post" : { + "tags" : [ "issue-credential v2.0" ], + "summary" : "Send a problem report for credential exchange", + "produces" : [ "application/json" ], + "parameters" : [ { + "in" : "body", + "name" : "body", + "required" : false, + "schema" : { + "$ref" : "#/definitions/V20CredIssueProblemReportRequest" + } + }, { + "name" : "cred_ex_id", + "in" : "path", + "description" : "Credential exchange identifier", + "required" : true, + "type" : "string", + "pattern" : "[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-4[a-fA-F0-9]{3}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}" + } ], + "responses" : { + "200" : { + "description" : "", + "schema" : { + "$ref" : "#/definitions/V20IssueCredentialModuleResponse" + } + } + } + } + }, + "/issue-credential-2.0/records/{cred_ex_id}/send-offer" : { + "post" : { + "tags" : [ "issue-credential v2.0" ], + "summary" : "Send holder a credential offer in reference to a proposal with preview", + "produces" : [ "application/json" ], + "parameters" : [ { + "in" : "body", + "name" : "body", + "required" : false, + "schema" : { + "$ref" : "#/definitions/V20CredBoundOfferRequest" + } + }, { + "name" : "cred_ex_id", + "in" : "path", + "description" : "Credential exchange identifier", + "required" : true, + "type" : "string", + "pattern" : "[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-4[a-fA-F0-9]{3}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}" + } ], + "responses" : { + "200" : { + "description" : "", + "schema" : { + "$ref" : "#/definitions/V20CredExRecord" + } + } + } + } + }, + "/issue-credential-2.0/records/{cred_ex_id}/send-request" : { + "post" : { + "tags" : [ "issue-credential v2.0" ], + "summary" : "Send issuer a credential request", + "produces" : [ "application/json" ], + "parameters" : [ { + "in" : "body", + "name" : "body", + "required" : false, + "schema" : { + "$ref" : "#/definitions/V20CredRequestRequest" + } + }, { + "name" : "cred_ex_id", + "in" : "path", + "description" : "Credential exchange identifier", + "required" : true, + "type" : "string", + "pattern" : "[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-4[a-fA-F0-9]{3}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}" + } ], + "responses" : { + "200" : { + "description" : "", + "schema" : { + "$ref" : "#/definitions/V20CredExRecord" + } + } + } + } + }, + "/issue-credential-2.0/records/{cred_ex_id}/store" : { + "post" : { + "tags" : [ "issue-credential v2.0" ], + "summary" : "Store a received credential", + "produces" : [ "application/json" ], + "parameters" : [ { + "in" : "body", + "name" : "body", + "required" : false, + "schema" : { + "$ref" : "#/definitions/V20CredStoreRequest" + } + }, { + "name" : "cred_ex_id", + "in" : "path", + "description" : "Credential exchange identifier", + "required" : true, + "type" : "string", + "pattern" : "[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-4[a-fA-F0-9]{3}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}" + } ], + "responses" : { + "200" : { + "description" : "", + "schema" : { + "$ref" : "#/definitions/V20CredExRecordDetail" + } + } + } + } + }, + "/issue-credential-2.0/send" : { + "post" : { + "tags" : [ "issue-credential v2.0" ], + "summary" : "Send holder a credential, automating entire flow", + "produces" : [ "application/json" ], + "parameters" : [ { + "in" : "body", + "name" : "body", + "required" : false, + "schema" : { + "$ref" : "#/definitions/V20CredExFree" + } + } ], + "responses" : { + "200" : { + "description" : "", + "schema" : { + "$ref" : "#/definitions/V20CredExRecord" + } + } + } + } + }, + "/issue-credential-2.0/send-offer" : { + "post" : { + "tags" : [ "issue-credential v2.0" ], + "summary" : "Send holder a credential offer, independent of any proposal", + "produces" : [ "application/json" ], + "parameters" : [ { + "in" : "body", + "name" : "body", + "required" : false, + "schema" : { + "$ref" : "#/definitions/V20CredOfferRequest" + } + } ], + "responses" : { + "200" : { + "description" : "", + "schema" : { + "$ref" : "#/definitions/V20CredExRecord" + } + } + } + } + }, + "/issue-credential-2.0/send-proposal" : { + "post" : { + "tags" : [ "issue-credential v2.0" ], + "summary" : "Send issuer a credential proposal", + "produces" : [ "application/json" ], + "parameters" : [ { + "in" : "body", + "name" : "body", + "required" : false, + "schema" : { + "$ref" : "#/definitions/V20CredExFree" + } + } ], + "responses" : { + "200" : { + "description" : "", + "schema" : { + "$ref" : "#/definitions/V20CredExRecord" + } + } + } + } + }, + "/issue-credential-2.0/send-request" : { + "post" : { + "tags" : [ "issue-credential v2.0" ], + "summary" : "Send issuer a credential request not bound to an existing thread. Indy credentials cannot start at a request", + "produces" : [ "application/json" ], + "parameters" : [ { + "in" : "body", + "name" : "body", + "required" : false, + "schema" : { + "$ref" : "#/definitions/V20CredRequestFree" + } + } ], + "responses" : { + "200" : { + "description" : "", + "schema" : { + "$ref" : "#/definitions/V20CredExRecord" + } + } + } + } + }, + "/issue-credential/create" : { + "post" : { + "tags" : [ "issue-credential v1.0" ], + "summary" : "Create a credential record without sending (generally for use with Out-Of-Band)", + "produces" : [ "application/json" ], + "parameters" : [ { + "in" : "body", + "name" : "body", + "required" : false, + "schema" : { + "$ref" : "#/definitions/V10CredentialCreate" + } + } ], + "responses" : { + "200" : { + "description" : "", + "schema" : { + "$ref" : "#/definitions/V10CredentialExchange" + } + } + } + } + }, + "/issue-credential/create-offer" : { + "post" : { + "tags" : [ "issue-credential v1.0" ], + "summary" : "Create a credential offer, independent of any proposal or connection", + "produces" : [ "application/json" ], + "parameters" : [ { + "in" : "body", + "name" : "body", + "required" : false, + "schema" : { + "$ref" : "#/definitions/V10CredentialConnFreeOfferRequest" + } + } ], + "responses" : { + "200" : { + "description" : "", + "schema" : { + "$ref" : "#/definitions/V10CredentialExchange" + } + } + } + } + }, + "/issue-credential/records" : { + "get" : { + "tags" : [ "issue-credential v1.0" ], + "summary" : "Fetch all credential exchange records", + "produces" : [ "application/json" ], + "parameters" : [ { + "name" : "connection_id", + "in" : "query", + "description" : "Connection identifier", + "required" : false, + "type" : "string", + "format" : "uuid" + }, { + "name" : "role", + "in" : "query", + "description" : "Role assigned in credential exchange", + "required" : false, + "type" : "string", + "enum" : [ "issuer", "holder" ] + }, { + "name" : "state", + "in" : "query", + "description" : "Credential exchange state", + "required" : false, + "type" : "string", + "enum" : [ "proposal_sent", "proposal_received", "offer_sent", "offer_received", "request_sent", "request_received", "credential_issued", "credential_received", "credential_acked", "credential_revoked", "abandoned" ] + }, { + "name" : "thread_id", + "in" : "query", + "description" : "Thread identifier", + "required" : false, + "type" : "string", + "format" : "uuid" + } ], + "responses" : { + "200" : { + "description" : "", + "schema" : { + "$ref" : "#/definitions/V10CredentialExchangeListResult" + } + } + } + } + }, + "/issue-credential/records/{cred_ex_id}" : { + "get" : { + "tags" : [ "issue-credential v1.0" ], + "summary" : "Fetch a single credential exchange record", + "produces" : [ "application/json" ], + "parameters" : [ { + "name" : "cred_ex_id", + "in" : "path", + "description" : "Credential exchange identifier", + "required" : true, + "type" : "string", + "pattern" : "[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-4[a-fA-F0-9]{3}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}" + } ], + "responses" : { + "200" : { + "description" : "", + "schema" : { + "$ref" : "#/definitions/V10CredentialExchange" + } + } + } + }, + "delete" : { + "tags" : [ "issue-credential v1.0" ], + "summary" : "Remove an existing credential exchange record", + "produces" : [ "application/json" ], + "parameters" : [ { + "name" : "cred_ex_id", + "in" : "path", + "description" : "Credential exchange identifier", + "required" : true, + "type" : "string", + "pattern" : "[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-4[a-fA-F0-9]{3}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}" + } ], + "responses" : { + "200" : { + "description" : "", + "schema" : { + "$ref" : "#/definitions/IssueCredentialModuleResponse" + } + } + } + } + }, + "/issue-credential/records/{cred_ex_id}/issue" : { + "post" : { + "tags" : [ "issue-credential v1.0" ], + "summary" : "Send holder a credential", + "produces" : [ "application/json" ], + "parameters" : [ { + "in" : "body", + "name" : "body", + "required" : false, + "schema" : { + "$ref" : "#/definitions/V10CredentialIssueRequest" + } + }, { + "name" : "cred_ex_id", + "in" : "path", + "description" : "Credential exchange identifier", + "required" : true, + "type" : "string", + "pattern" : "[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-4[a-fA-F0-9]{3}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}" + } ], + "responses" : { + "200" : { + "description" : "", + "schema" : { + "$ref" : "#/definitions/V10CredentialExchange" + } + } + } + } + }, + "/issue-credential/records/{cred_ex_id}/problem-report" : { + "post" : { + "tags" : [ "issue-credential v1.0" ], + "summary" : "Send a problem report for credential exchange", + "produces" : [ "application/json" ], + "parameters" : [ { + "in" : "body", + "name" : "body", + "required" : false, + "schema" : { + "$ref" : "#/definitions/V10CredentialProblemReportRequest" + } + }, { + "name" : "cred_ex_id", + "in" : "path", + "description" : "Credential exchange identifier", + "required" : true, + "type" : "string", + "pattern" : "[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-4[a-fA-F0-9]{3}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}" + } ], + "responses" : { + "200" : { + "description" : "", + "schema" : { + "$ref" : "#/definitions/IssueCredentialModuleResponse" + } + } + } + } + }, + "/issue-credential/records/{cred_ex_id}/send-offer" : { + "post" : { + "tags" : [ "issue-credential v1.0" ], + "summary" : "Send holder a credential offer in reference to a proposal with preview", + "produces" : [ "application/json" ], + "parameters" : [ { + "in" : "body", + "name" : "body", + "required" : false, + "schema" : { + "$ref" : "#/definitions/V10CredentialBoundOfferRequest" + } + }, { + "name" : "cred_ex_id", + "in" : "path", + "description" : "Credential exchange identifier", + "required" : true, + "type" : "string", + "pattern" : "[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-4[a-fA-F0-9]{3}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}" + } ], + "responses" : { + "200" : { + "description" : "", + "schema" : { + "$ref" : "#/definitions/V10CredentialExchange" + } + } + } + } + }, + "/issue-credential/records/{cred_ex_id}/send-request" : { + "post" : { + "tags" : [ "issue-credential v1.0" ], + "summary" : "Send issuer a credential request", + "produces" : [ "application/json" ], + "parameters" : [ { + "name" : "cred_ex_id", + "in" : "path", + "description" : "Credential exchange identifier", + "required" : true, + "type" : "string", + "pattern" : "[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-4[a-fA-F0-9]{3}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}" + } ], + "responses" : { + "200" : { + "description" : "", + "schema" : { + "$ref" : "#/definitions/V10CredentialExchange" + } + } + } + } + }, + "/issue-credential/records/{cred_ex_id}/store" : { + "post" : { + "tags" : [ "issue-credential v1.0" ], + "summary" : "Store a received credential", + "produces" : [ "application/json" ], + "parameters" : [ { + "in" : "body", + "name" : "body", + "required" : false, + "schema" : { + "$ref" : "#/definitions/V10CredentialStoreRequest" + } + }, { + "name" : "cred_ex_id", + "in" : "path", + "description" : "Credential exchange identifier", + "required" : true, + "type" : "string", + "pattern" : "[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-4[a-fA-F0-9]{3}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}" + } ], + "responses" : { + "200" : { + "description" : "", + "schema" : { + "$ref" : "#/definitions/V10CredentialExchange" + } + } + } + } + }, + "/issue-credential/send" : { + "post" : { + "tags" : [ "issue-credential v1.0" ], + "summary" : "Send holder a credential, automating entire flow", + "produces" : [ "application/json" ], + "parameters" : [ { + "in" : "body", + "name" : "body", + "required" : false, + "schema" : { + "$ref" : "#/definitions/V10CredentialProposalRequestMand" + } + } ], + "responses" : { + "200" : { + "description" : "", + "schema" : { + "$ref" : "#/definitions/V10CredentialExchange" + } + } + } + } + }, + "/issue-credential/send-offer" : { + "post" : { + "tags" : [ "issue-credential v1.0" ], + "summary" : "Send holder a credential offer, independent of any proposal", + "produces" : [ "application/json" ], + "parameters" : [ { + "in" : "body", + "name" : "body", + "required" : false, + "schema" : { + "$ref" : "#/definitions/V10CredentialFreeOfferRequest" + } + } ], + "responses" : { + "200" : { + "description" : "", + "schema" : { + "$ref" : "#/definitions/V10CredentialExchange" + } + } + } + } + }, + "/issue-credential/send-proposal" : { + "post" : { + "tags" : [ "issue-credential v1.0" ], + "summary" : "Send issuer a credential proposal", + "produces" : [ "application/json" ], + "parameters" : [ { + "in" : "body", + "name" : "body", + "required" : false, + "schema" : { + "$ref" : "#/definitions/V10CredentialProposalRequestOpt" + } + } ], + "responses" : { + "200" : { + "description" : "", + "schema" : { + "$ref" : "#/definitions/V10CredentialExchange" + } + } + } + } + }, + "/jsonld/sign" : { + "post" : { + "tags" : [ "jsonld" ], + "summary" : "Sign a JSON-LD structure and return it", + "produces" : [ "application/json" ], + "parameters" : [ { + "in" : "body", + "name" : "body", + "required" : false, + "schema" : { + "$ref" : "#/definitions/SignRequest" + } + } ], + "responses" : { + "200" : { + "description" : "", + "schema" : { + "$ref" : "#/definitions/SignResponse" + } + } + } + } + }, + "/jsonld/verify" : { + "post" : { + "tags" : [ "jsonld" ], + "summary" : "Verify a JSON-LD structure.", + "produces" : [ "application/json" ], + "parameters" : [ { + "in" : "body", + "name" : "body", + "required" : false, + "schema" : { + "$ref" : "#/definitions/VerifyRequest" + } + } ], + "responses" : { + "200" : { + "description" : "", + "schema" : { + "$ref" : "#/definitions/VerifyResponse" + } + } + } + } + }, + "/ledger/did-endpoint" : { + "get" : { + "tags" : [ "ledger" ], + "summary" : "Get the endpoint for a DID from the ledger.", + "produces" : [ "application/json" ], + "parameters" : [ { + "name" : "did", + "in" : "query", + "description" : "DID of interest", + "required" : true, + "type" : "string", + "pattern" : "^(did:sov:)?[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}$" + }, { + "name" : "endpoint_type", + "in" : "query", + "description" : "Endpoint type of interest (default 'Endpoint')", + "required" : false, + "type" : "string", + "enum" : [ "Endpoint", "Profile", "LinkedDomains" ] + } ], + "responses" : { + "200" : { + "description" : "", + "schema" : { + "$ref" : "#/definitions/GetDIDEndpointResponse" + } + } + } + } + }, + "/ledger/did-verkey" : { + "get" : { + "tags" : [ "ledger" ], + "summary" : "Get the verkey for a DID from the ledger.", + "produces" : [ "application/json" ], + "parameters" : [ { + "name" : "did", + "in" : "query", + "description" : "DID of interest", + "required" : true, + "type" : "string", + "pattern" : "^(did:sov:)?[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}$" + } ], + "responses" : { + "200" : { + "description" : "", + "schema" : { + "$ref" : "#/definitions/GetDIDVerkeyResponse" + } + } + } + } + }, + "/ledger/get-nym-role" : { + "get" : { + "tags" : [ "ledger" ], + "summary" : "Get the role from the NYM registration of a public DID.", + "produces" : [ "application/json" ], + "parameters" : [ { + "name" : "did", + "in" : "query", + "description" : "DID of interest", + "required" : true, + "type" : "string", + "pattern" : "^(did:sov:)?[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}$" + } ], + "responses" : { + "200" : { + "description" : "", + "schema" : { + "$ref" : "#/definitions/GetNymRoleResponse" + } + } + } + } + }, + "/ledger/multiple/config" : { + "get" : { + "tags" : [ "ledger" ], + "summary" : "Fetch the multiple ledger configuration currently in use", + "produces" : [ "application/json" ], + "parameters" : [ ], + "responses" : { + "200" : { + "description" : "", + "schema" : { + "$ref" : "#/definitions/LedgerConfigList" + } + } + } + } + }, + "/ledger/multiple/get-write-ledger" : { + "get" : { + "tags" : [ "ledger" ], + "summary" : "Fetch the current write ledger", + "produces" : [ "application/json" ], + "parameters" : [ ], + "responses" : { + "200" : { + "description" : "", + "schema" : { + "$ref" : "#/definitions/WriteLedgerRequest" + } + } + } + } + }, + "/ledger/register-nym" : { + "post" : { + "tags" : [ "ledger" ], + "summary" : "Send a NYM registration to the ledger.", + "produces" : [ "application/json" ], + "parameters" : [ { + "name" : "did", + "in" : "query", + "description" : "DID to register", + "required" : true, + "type" : "string", + "pattern" : "^(did:sov:)?[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}$" + }, { + "name" : "verkey", + "in" : "query", + "description" : "Verification key", + "required" : true, + "type" : "string", + "pattern" : "^[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{43,44}$" + }, { + "name" : "alias", + "in" : "query", + "description" : "Alias", + "required" : false, + "type" : "string" + }, { + "name" : "conn_id", + "in" : "query", + "description" : "Connection identifier", + "required" : false, + "type" : "string" + }, { + "name" : "create_transaction_for_endorser", + "in" : "query", + "description" : "Create Transaction For Endorser's signature", + "required" : false, + "type" : "boolean" + }, { + "name" : "role", + "in" : "query", + "description" : "Role", + "required" : false, + "type" : "string", + "enum" : [ "STEWARD", "TRUSTEE", "ENDORSER", "NETWORK_MONITOR", "reset" ] + } ], + "responses" : { + "200" : { + "description" : "", + "schema" : { + "$ref" : "#/definitions/TxnOrRegisterLedgerNymResponse" + } + } + } + } + }, + "/ledger/rotate-public-did-keypair" : { + "patch" : { + "tags" : [ "ledger" ], + "summary" : "Rotate key pair for public DID.", + "produces" : [ "application/json" ], + "parameters" : [ ], + "responses" : { + "200" : { + "description" : "", + "schema" : { + "$ref" : "#/definitions/LedgerModulesResult" + } + } + } + } + }, + "/ledger/taa" : { + "get" : { + "tags" : [ "ledger" ], + "summary" : "Fetch the current transaction author agreement, if any", + "produces" : [ "application/json" ], + "parameters" : [ ], + "responses" : { + "200" : { + "description" : "", + "schema" : { + "$ref" : "#/definitions/TAAResult" + } + } + } + } + }, + "/ledger/taa/accept" : { + "post" : { + "tags" : [ "ledger" ], + "summary" : "Accept the transaction author agreement", + "produces" : [ "application/json" ], + "parameters" : [ { + "in" : "body", + "name" : "body", + "required" : false, + "schema" : { + "$ref" : "#/definitions/TAAAccept" + } + } ], + "responses" : { + "200" : { + "description" : "", + "schema" : { + "$ref" : "#/definitions/LedgerModulesResult" + } + } + } + } + }, + "/mediation/default-mediator" : { + "get" : { + "tags" : [ "mediation" ], + "summary" : "Get default mediator", + "produces" : [ "application/json" ], + "parameters" : [ ], + "responses" : { + "200" : { + "description" : "", + "schema" : { + "$ref" : "#/definitions/MediationRecord" + } + } + } + }, + "delete" : { + "tags" : [ "mediation" ], + "summary" : "Clear default mediator", + "produces" : [ "application/json" ], + "parameters" : [ ], + "responses" : { + "201" : { + "description" : "", + "schema" : { + "$ref" : "#/definitions/MediationRecord" + } + } + } + } + }, + "/mediation/keylists" : { + "get" : { + "tags" : [ "mediation" ], + "summary" : "Retrieve keylists by connection or role", + "produces" : [ "application/json" ], + "parameters" : [ { + "name" : "conn_id", + "in" : "query", + "description" : "Connection identifier (optional)", + "required" : false, + "type" : "string", + "format" : "uuid" + }, { + "name" : "role", + "in" : "query", + "description" : "Filer on role, 'client' for keys mediated by other agents, 'server' for keys mediated by this agent", + "required" : false, + "type" : "string", + "default" : "server", + "enum" : [ "client", "server" ] + } ], + "responses" : { + "200" : { + "description" : "", + "schema" : { + "$ref" : "#/definitions/Keylist" + } + } + } + } + }, + "/mediation/keylists/{mediation_id}/send-keylist-query" : { + "post" : { + "tags" : [ "mediation" ], + "summary" : "Send keylist query to mediator", + "produces" : [ "application/json" ], + "parameters" : [ { + "in" : "body", + "name" : "body", + "required" : false, + "schema" : { + "$ref" : "#/definitions/KeylistQueryFilterRequest" + } + }, { + "name" : "mediation_id", + "in" : "path", + "description" : "Mediation record identifier", + "required" : true, + "type" : "string", + "format" : "uuid" + }, { + "name" : "paginate_limit", + "in" : "query", + "description" : "limit number of results", + "required" : false, + "type" : "integer", + "default" : -1, + "format" : "int32" + }, { + "name" : "paginate_offset", + "in" : "query", + "description" : "offset to use in pagination", + "required" : false, + "type" : "integer", + "default" : 0, + "format" : "int32" + } ], + "responses" : { + "201" : { + "description" : "", + "schema" : { + "$ref" : "#/definitions/KeylistQuery" + } + } + } + } + }, + "/mediation/keylists/{mediation_id}/send-keylist-update" : { + "post" : { + "tags" : [ "mediation" ], + "summary" : "Send keylist update to mediator", + "produces" : [ "application/json" ], + "parameters" : [ { + "in" : "body", + "name" : "body", + "required" : false, + "schema" : { + "$ref" : "#/definitions/KeylistUpdateRequest" + } + }, { + "name" : "mediation_id", + "in" : "path", + "description" : "Mediation record identifier", + "required" : true, + "type" : "string", + "format" : "uuid" + } ], + "responses" : { + "201" : { + "description" : "", + "schema" : { + "$ref" : "#/definitions/KeylistUpdate" + } + } + } + } + }, + "/mediation/request/{conn_id}" : { + "post" : { + "tags" : [ "mediation" ], + "summary" : "Request mediation from connection", + "produces" : [ "application/json" ], + "parameters" : [ { + "in" : "body", + "name" : "body", + "required" : false, + "schema" : { + "$ref" : "#/definitions/MediationCreateRequest" + } + }, { + "name" : "conn_id", + "in" : "path", + "description" : "Connection identifier", + "required" : true, + "type" : "string" + } ], + "responses" : { + "201" : { + "description" : "", + "schema" : { + "$ref" : "#/definitions/MediationRecord" + } + } + } + } + }, + "/mediation/requests" : { + "get" : { + "tags" : [ "mediation" ], + "summary" : "Query mediation requests, returns list of all mediation records", + "produces" : [ "application/json" ], + "parameters" : [ { + "name" : "conn_id", + "in" : "query", + "description" : "Connection identifier (optional)", + "required" : false, + "type" : "string", + "format" : "uuid" + }, { + "name" : "mediator_terms", + "in" : "query", + "description" : "List of mediator rules for recipient", + "required" : false, + "type" : "array", + "items" : { + "type" : "string", + "description" : "Indicate terms to which the mediator requires the recipient to agree" + }, + "collectionFormat" : "multi" + }, { + "name" : "recipient_terms", + "in" : "query", + "description" : "List of recipient rules for mediation", + "required" : false, + "type" : "array", + "items" : { + "type" : "string", + "description" : "Indicate terms to which the recipient requires the mediator to agree" + }, + "collectionFormat" : "multi" + }, { + "name" : "state", + "in" : "query", + "description" : "Mediation state (optional)", + "required" : false, + "type" : "string", + "enum" : [ "request", "granted", "denied" ] + } ], + "responses" : { + "200" : { + "description" : "", + "schema" : { + "$ref" : "#/definitions/MediationList" + } + } + } + } + }, + "/mediation/requests/{mediation_id}" : { + "get" : { + "tags" : [ "mediation" ], + "summary" : "Retrieve mediation request record", + "produces" : [ "application/json" ], + "parameters" : [ { + "name" : "mediation_id", + "in" : "path", + "description" : "Mediation record identifier", + "required" : true, + "type" : "string", + "format" : "uuid" + } ], + "responses" : { + "200" : { + "description" : "", + "schema" : { + "$ref" : "#/definitions/MediationRecord" + } + } + } + }, + "delete" : { + "tags" : [ "mediation" ], + "summary" : "Delete mediation request by ID", + "produces" : [ "application/json" ], + "parameters" : [ { + "name" : "mediation_id", + "in" : "path", + "description" : "Mediation record identifier", + "required" : true, + "type" : "string", + "format" : "uuid" + } ], + "responses" : { + "200" : { + "description" : "", + "schema" : { + "$ref" : "#/definitions/MediationRecord" + } + } + } + } + }, + "/mediation/requests/{mediation_id}/deny" : { + "post" : { + "tags" : [ "mediation" ], + "summary" : "Deny a stored mediation request", + "produces" : [ "application/json" ], + "parameters" : [ { + "in" : "body", + "name" : "body", + "required" : false, + "schema" : { + "$ref" : "#/definitions/AdminMediationDeny" + } + }, { + "name" : "mediation_id", + "in" : "path", + "description" : "Mediation record identifier", + "required" : true, + "type" : "string", + "format" : "uuid" + } ], + "responses" : { + "201" : { + "description" : "", + "schema" : { + "$ref" : "#/definitions/MediationDeny" + } + } + } + } + }, + "/mediation/requests/{mediation_id}/grant" : { + "post" : { + "tags" : [ "mediation" ], + "summary" : "Grant received mediation", + "produces" : [ "application/json" ], + "parameters" : [ { + "name" : "mediation_id", + "in" : "path", + "description" : "Mediation record identifier", + "required" : true, + "type" : "string", + "format" : "uuid" + } ], + "responses" : { + "201" : { + "description" : "", + "schema" : { + "$ref" : "#/definitions/MediationGrant" + } + } + } + } + }, + "/mediation/update-keylist/{conn_id}" : { + "post" : { + "tags" : [ "mediation" ], + "summary" : "Update keylist for a connection", + "produces" : [ "application/json" ], + "parameters" : [ { + "in" : "body", + "name" : "body", + "required" : false, + "schema" : { + "$ref" : "#/definitions/MediationIdMatchInfo" + } + }, { + "name" : "conn_id", + "in" : "path", + "description" : "Connection identifier", + "required" : true, + "type" : "string" + } ], + "responses" : { + "200" : { + "description" : "", + "schema" : { + "$ref" : "#/definitions/KeylistUpdate" + } + } + } + } + }, + "/mediation/{mediation_id}/default-mediator" : { + "put" : { + "tags" : [ "mediation" ], + "summary" : "Set default mediator", + "produces" : [ "application/json" ], + "parameters" : [ { + "name" : "mediation_id", + "in" : "path", + "description" : "Mediation record identifier", + "required" : true, + "type" : "string", + "format" : "uuid" + } ], + "responses" : { + "201" : { + "description" : "", + "schema" : { + "$ref" : "#/definitions/MediationRecord" + } + } + } + } + }, + "/multitenancy/wallet" : { + "post" : { + "tags" : [ "multitenancy" ], + "summary" : "Create a subwallet", + "produces" : [ "application/json" ], + "parameters" : [ { + "in" : "body", + "name" : "body", + "required" : false, + "schema" : { + "$ref" : "#/definitions/CreateWalletRequest" + } + } ], + "responses" : { + "200" : { + "description" : "", + "schema" : { + "$ref" : "#/definitions/CreateWalletResponse" + } + } + } + } + }, + "/multitenancy/wallet/{wallet_id}" : { + "get" : { + "tags" : [ "multitenancy" ], + "summary" : "Get a single subwallet", + "produces" : [ "application/json" ], + "parameters" : [ { + "name" : "wallet_id", + "in" : "path", + "description" : "Subwallet identifier", + "required" : true, + "type" : "string" + } ], + "responses" : { + "200" : { + "description" : "", + "schema" : { + "$ref" : "#/definitions/WalletRecord" + } + } + } + }, + "put" : { + "tags" : [ "multitenancy" ], + "summary" : "Update a subwallet", + "produces" : [ "application/json" ], + "parameters" : [ { + "in" : "body", + "name" : "body", + "required" : false, + "schema" : { + "$ref" : "#/definitions/UpdateWalletRequest" + } + }, { + "name" : "wallet_id", + "in" : "path", + "description" : "Subwallet identifier", + "required" : true, + "type" : "string" + } ], + "responses" : { + "200" : { + "description" : "", + "schema" : { + "$ref" : "#/definitions/WalletRecord" + } + } + } + } + }, + "/multitenancy/wallet/{wallet_id}/remove" : { + "post" : { + "tags" : [ "multitenancy" ], + "summary" : "Remove a subwallet", + "produces" : [ "application/json" ], + "parameters" : [ { + "in" : "body", + "name" : "body", + "required" : false, + "schema" : { + "$ref" : "#/definitions/RemoveWalletRequest" + } + }, { + "name" : "wallet_id", + "in" : "path", + "description" : "Subwallet identifier", + "required" : true, + "type" : "string" + } ], + "responses" : { + "200" : { + "description" : "", + "schema" : { + "$ref" : "#/definitions/MultitenantModuleResponse" + } + } + } + } + }, + "/multitenancy/wallet/{wallet_id}/token" : { + "post" : { + "tags" : [ "multitenancy" ], + "summary" : "Get auth token for a subwallet", + "produces" : [ "application/json" ], + "parameters" : [ { + "in" : "body", + "name" : "body", + "required" : false, + "schema" : { + "$ref" : "#/definitions/CreateWalletTokenRequest" + } + }, { + "name" : "wallet_id", + "in" : "path", + "required" : true, + "type" : "string" + } ], + "responses" : { + "200" : { + "description" : "", + "schema" : { + "$ref" : "#/definitions/CreateWalletTokenResponse" + } + } + } + } + }, + "/multitenancy/wallets" : { + "get" : { + "tags" : [ "multitenancy" ], + "summary" : "Query subwallets", + "produces" : [ "application/json" ], + "parameters" : [ { + "name" : "wallet_name", + "in" : "query", + "description" : "Wallet name", + "required" : false, + "type" : "string" + } ], + "responses" : { + "200" : { + "description" : "", + "schema" : { + "$ref" : "#/definitions/WalletList" + } + } + } + } + }, + "/out-of-band/create-invitation" : { + "post" : { + "tags" : [ "out-of-band" ], + "summary" : "Create a new connection invitation", + "produces" : [ "application/json" ], + "parameters" : [ { + "in" : "body", + "name" : "body", + "required" : false, + "schema" : { + "$ref" : "#/definitions/InvitationCreateRequest" + } + }, { + "name" : "auto_accept", + "in" : "query", + "description" : "Auto-accept connection (defaults to configuration)", + "required" : false, + "type" : "boolean" + }, { + "name" : "multi_use", + "in" : "query", + "description" : "Create invitation for multiple use (default false)", + "required" : false, + "type" : "boolean" + } ], + "responses" : { + "200" : { + "description" : "", + "schema" : { + "$ref" : "#/definitions/InvitationRecord" + } + } + } + } + }, + "/out-of-band/receive-invitation" : { + "post" : { + "tags" : [ "out-of-band" ], + "summary" : "Receive a new connection invitation", + "produces" : [ "application/json" ], + "parameters" : [ { + "in" : "body", + "name" : "body", + "required" : false, + "schema" : { + "$ref" : "#/definitions/InvitationMessage" + } + }, { + "name" : "alias", + "in" : "query", + "description" : "Alias for connection", + "required" : false, + "type" : "string" + }, { + "name" : "auto_accept", + "in" : "query", + "description" : "Auto-accept connection (defaults to configuration)", + "required" : false, + "type" : "boolean" + }, { + "name" : "mediation_id", + "in" : "query", + "description" : "Identifier for active mediation record to be used", + "required" : false, + "type" : "string", + "pattern" : "[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-4[a-fA-F0-9]{3}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}" + }, { + "name" : "use_existing_connection", + "in" : "query", + "description" : "Use an existing connection, if possible", + "required" : false, + "type" : "boolean" + } ], + "responses" : { + "200" : { + "description" : "", + "schema" : { + "$ref" : "#/definitions/OobRecord" + } + } + } + } + }, + "/plugins" : { + "get" : { + "tags" : [ "server" ], + "summary" : "Fetch the list of loaded plugins", + "produces" : [ "application/json" ], + "parameters" : [ ], + "responses" : { + "200" : { + "description" : "", + "schema" : { + "$ref" : "#/definitions/AdminModules" + } + } + } + } + }, + "/present-proof-2.0/create-request" : { + "post" : { + "tags" : [ "present-proof v2.0" ], + "summary" : "Creates a presentation request not bound to any proposal or connection", + "produces" : [ "application/json" ], + "parameters" : [ { + "in" : "body", + "name" : "body", + "required" : false, + "schema" : { + "$ref" : "#/definitions/V20PresCreateRequestRequest" + } + } ], + "responses" : { + "200" : { + "description" : "", + "schema" : { + "$ref" : "#/definitions/V20PresExRecord" + } + } + } + } + }, + "/present-proof-2.0/records" : { + "get" : { + "tags" : [ "present-proof v2.0" ], + "summary" : "Fetch all present-proof exchange records", + "produces" : [ "application/json" ], + "parameters" : [ { + "name" : "connection_id", + "in" : "query", + "description" : "Connection identifier", + "required" : false, + "type" : "string", + "format" : "uuid" + }, { + "name" : "role", + "in" : "query", + "description" : "Role assigned in presentation exchange", + "required" : false, + "type" : "string", + "enum" : [ "prover", "verifier" ] + }, { + "name" : "state", + "in" : "query", + "description" : "Presentation exchange state", + "required" : false, + "type" : "string", + "enum" : [ "proposal-sent", "proposal-received", "request-sent", "request-received", "presentation-sent", "presentation-received", "done", "abandoned" ] + }, { + "name" : "thread_id", + "in" : "query", + "description" : "Thread identifier", + "required" : false, + "type" : "string", + "format" : "uuid" + } ], + "responses" : { + "200" : { + "description" : "", + "schema" : { + "$ref" : "#/definitions/V20PresExRecordList" + } + } + } + } + }, + "/present-proof-2.0/records/{pres_ex_id}" : { + "get" : { + "tags" : [ "present-proof v2.0" ], + "summary" : "Fetch a single presentation exchange record", + "produces" : [ "application/json" ], + "parameters" : [ { + "name" : "pres_ex_id", + "in" : "path", + "description" : "Presentation exchange identifier", + "required" : true, + "type" : "string", + "pattern" : "[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-4[a-fA-F0-9]{3}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}" + } ], + "responses" : { + "200" : { + "description" : "", + "schema" : { + "$ref" : "#/definitions/V20PresExRecord" + } + } + } + }, + "delete" : { + "tags" : [ "present-proof v2.0" ], + "summary" : "Remove an existing presentation exchange record", + "produces" : [ "application/json" ], + "parameters" : [ { + "name" : "pres_ex_id", + "in" : "path", + "description" : "Presentation exchange identifier", + "required" : true, + "type" : "string", + "pattern" : "[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-4[a-fA-F0-9]{3}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}" + } ], + "responses" : { + "200" : { + "description" : "", + "schema" : { + "$ref" : "#/definitions/V20PresentProofModuleResponse" + } + } + } + } + }, + "/present-proof-2.0/records/{pres_ex_id}/credentials" : { + "get" : { + "tags" : [ "present-proof v2.0" ], + "summary" : "Fetch credentials from wallet for presentation request", + "produces" : [ "application/json" ], + "parameters" : [ { + "name" : "pres_ex_id", + "in" : "path", + "description" : "Presentation exchange identifier", + "required" : true, + "type" : "string", + "pattern" : "[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-4[a-fA-F0-9]{3}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}" + }, { + "name" : "count", + "in" : "query", + "description" : "Maximum number to retrieve", + "required" : false, + "type" : "string", + "pattern" : "^[1-9][0-9]*$" + }, { + "name" : "extra_query", + "in" : "query", + "description" : "(JSON) object mapping referents to extra WQL queries", + "required" : false, + "type" : "string", + "pattern" : "^{\\s*\".*?\"\\s*:\\s*{.*?}\\s*(,\\s*\".*?\"\\s*:\\s*{.*?}\\s*)*\\s*}$" + }, { + "name" : "referent", + "in" : "query", + "description" : "Proof request referents of interest, comma-separated", + "required" : false, + "type" : "string" + }, { + "name" : "start", + "in" : "query", + "description" : "Start index", + "required" : false, + "type" : "string", + "pattern" : "^[0-9]*$" + } ], + "responses" : { + "200" : { + "description" : "", + "schema" : { + "type" : "array", + "items" : { + "$ref" : "#/definitions/IndyCredPrecis" + } + } + } + } + } + }, + "/present-proof-2.0/records/{pres_ex_id}/problem-report" : { + "post" : { + "tags" : [ "present-proof v2.0" ], + "summary" : "Send a problem report for presentation exchange", + "produces" : [ "application/json" ], + "parameters" : [ { + "in" : "body", + "name" : "body", + "required" : false, + "schema" : { + "$ref" : "#/definitions/V20PresProblemReportRequest" + } + }, { + "name" : "pres_ex_id", + "in" : "path", + "description" : "Presentation exchange identifier", + "required" : true, + "type" : "string", + "pattern" : "[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-4[a-fA-F0-9]{3}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}" + } ], + "responses" : { + "200" : { + "description" : "", + "schema" : { + "$ref" : "#/definitions/V20PresentProofModuleResponse" + } + } + } + } + }, + "/present-proof-2.0/records/{pres_ex_id}/send-presentation" : { + "post" : { + "tags" : [ "present-proof v2.0" ], + "summary" : "Sends a proof presentation", + "produces" : [ "application/json" ], + "parameters" : [ { + "in" : "body", + "name" : "body", + "required" : false, + "schema" : { + "$ref" : "#/definitions/V20PresSpecByFormatRequest" + } + }, { + "name" : "pres_ex_id", + "in" : "path", + "description" : "Presentation exchange identifier", + "required" : true, + "type" : "string", + "pattern" : "[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-4[a-fA-F0-9]{3}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}" + } ], + "responses" : { + "200" : { + "description" : "", + "schema" : { + "$ref" : "#/definitions/V20PresExRecord" + } + } + } + } + }, + "/present-proof-2.0/records/{pres_ex_id}/send-request" : { + "post" : { + "tags" : [ "present-proof v2.0" ], + "summary" : "Sends a presentation request in reference to a proposal", + "produces" : [ "application/json" ], + "parameters" : [ { + "in" : "body", + "name" : "body", + "required" : false, + "schema" : { + "$ref" : "#/definitions/V20PresentationSendRequestToProposal" + } + }, { + "name" : "pres_ex_id", + "in" : "path", + "description" : "Presentation exchange identifier", + "required" : true, + "type" : "string", + "pattern" : "[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-4[a-fA-F0-9]{3}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}" + } ], + "responses" : { + "200" : { + "description" : "", + "schema" : { + "$ref" : "#/definitions/V20PresExRecord" + } + } + } + } + }, + "/present-proof-2.0/records/{pres_ex_id}/verify-presentation" : { + "post" : { + "tags" : [ "present-proof v2.0" ], + "summary" : "Verify a received presentation", + "produces" : [ "application/json" ], + "parameters" : [ { + "name" : "pres_ex_id", + "in" : "path", + "description" : "Presentation exchange identifier", + "required" : true, + "type" : "string", + "pattern" : "[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-4[a-fA-F0-9]{3}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}" + } ], + "responses" : { + "200" : { + "description" : "", + "schema" : { + "$ref" : "#/definitions/V20PresExRecord" + } + } + } + } + }, + "/present-proof-2.0/send-proposal" : { + "post" : { + "tags" : [ "present-proof v2.0" ], + "summary" : "Sends a presentation proposal", + "produces" : [ "application/json" ], + "parameters" : [ { + "in" : "body", + "name" : "body", + "required" : false, + "schema" : { + "$ref" : "#/definitions/V20PresProposalRequest" + } + } ], + "responses" : { + "200" : { + "description" : "", + "schema" : { + "$ref" : "#/definitions/V20PresExRecord" + } + } + } + } + }, + "/present-proof-2.0/send-request" : { + "post" : { + "tags" : [ "present-proof v2.0" ], + "summary" : "Sends a free presentation request not bound to any proposal", + "produces" : [ "application/json" ], + "parameters" : [ { + "in" : "body", + "name" : "body", + "required" : false, + "schema" : { + "$ref" : "#/definitions/V20PresSendRequestRequest" + } + } ], + "responses" : { + "200" : { + "description" : "", + "schema" : { + "$ref" : "#/definitions/V20PresExRecord" + } + } + } + } + }, + "/present-proof/create-request" : { + "post" : { + "tags" : [ "present-proof v1.0" ], + "summary" : "Creates a presentation request not bound to any proposal or connection", + "produces" : [ "application/json" ], + "parameters" : [ { + "in" : "body", + "name" : "body", + "required" : false, + "schema" : { + "$ref" : "#/definitions/V10PresentationCreateRequestRequest" + } + } ], + "responses" : { + "200" : { + "description" : "", + "schema" : { + "$ref" : "#/definitions/V10PresentationExchange" + } + } + } + } + }, + "/present-proof/records" : { + "get" : { + "tags" : [ "present-proof v1.0" ], + "summary" : "Fetch all present-proof exchange records", + "produces" : [ "application/json" ], + "parameters" : [ { + "name" : "connection_id", + "in" : "query", + "description" : "Connection identifier", + "required" : false, + "type" : "string", + "format" : "uuid" + }, { + "name" : "role", + "in" : "query", + "description" : "Role assigned in presentation exchange", + "required" : false, + "type" : "string", + "enum" : [ "prover", "verifier" ] + }, { + "name" : "state", + "in" : "query", + "description" : "Presentation exchange state", + "required" : false, + "type" : "string", + "enum" : [ "proposal_sent", "proposal_received", "request_sent", "request_received", "presentation_sent", "presentation_received", "verified", "presentation_acked", "abandoned" ] + }, { + "name" : "thread_id", + "in" : "query", + "description" : "Thread identifier", + "required" : false, + "type" : "string", + "format" : "uuid" + } ], + "responses" : { + "200" : { + "description" : "", + "schema" : { + "$ref" : "#/definitions/V10PresentationExchangeList" + } + } + } + } + }, + "/present-proof/records/{pres_ex_id}" : { + "get" : { + "tags" : [ "present-proof v1.0" ], + "summary" : "Fetch a single presentation exchange record", + "produces" : [ "application/json" ], + "parameters" : [ { + "name" : "pres_ex_id", + "in" : "path", + "description" : "Presentation exchange identifier", + "required" : true, + "type" : "string", + "pattern" : "[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-4[a-fA-F0-9]{3}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}" + } ], + "responses" : { + "200" : { + "description" : "", + "schema" : { + "$ref" : "#/definitions/V10PresentationExchange" + } + } + } + }, + "delete" : { + "tags" : [ "present-proof v1.0" ], + "summary" : "Remove an existing presentation exchange record", + "produces" : [ "application/json" ], + "parameters" : [ { + "name" : "pres_ex_id", + "in" : "path", + "description" : "Presentation exchange identifier", + "required" : true, + "type" : "string", + "pattern" : "[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-4[a-fA-F0-9]{3}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}" + } ], + "responses" : { + "200" : { + "description" : "", + "schema" : { + "$ref" : "#/definitions/V10PresentProofModuleResponse" + } + } + } + } + }, + "/present-proof/records/{pres_ex_id}/credentials" : { + "get" : { + "tags" : [ "present-proof v1.0" ], + "summary" : "Fetch credentials for a presentation request from wallet", + "produces" : [ "application/json" ], + "parameters" : [ { + "name" : "pres_ex_id", + "in" : "path", + "description" : "Presentation exchange identifier", + "required" : true, + "type" : "string", + "pattern" : "[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-4[a-fA-F0-9]{3}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}" + }, { + "name" : "count", + "in" : "query", + "description" : "Maximum number to retrieve", + "required" : false, + "type" : "string", + "pattern" : "^[1-9][0-9]*$" + }, { + "name" : "extra_query", + "in" : "query", + "description" : "(JSON) object mapping referents to extra WQL queries", + "required" : false, + "type" : "string", + "pattern" : "^{\\s*\".*?\"\\s*:\\s*{.*?}\\s*(,\\s*\".*?\"\\s*:\\s*{.*?}\\s*)*\\s*}$" + }, { + "name" : "referent", + "in" : "query", + "description" : "Proof request referents of interest, comma-separated", + "required" : false, + "type" : "string" + }, { + "name" : "start", + "in" : "query", + "description" : "Start index", + "required" : false, + "type" : "string", + "pattern" : "^[0-9]*$" + } ], + "responses" : { + "200" : { + "description" : "", + "schema" : { + "type" : "array", + "items" : { + "$ref" : "#/definitions/IndyCredPrecis" + } + } + } + } + } + }, + "/present-proof/records/{pres_ex_id}/problem-report" : { + "post" : { + "tags" : [ "present-proof v1.0" ], + "summary" : "Send a problem report for presentation exchange", + "produces" : [ "application/json" ], + "parameters" : [ { + "in" : "body", + "name" : "body", + "required" : false, + "schema" : { + "$ref" : "#/definitions/V10PresentationProblemReportRequest" + } + }, { + "name" : "pres_ex_id", + "in" : "path", + "description" : "Presentation exchange identifier", + "required" : true, + "type" : "string", + "pattern" : "[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-4[a-fA-F0-9]{3}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}" + } ], + "responses" : { + "200" : { + "description" : "", + "schema" : { + "$ref" : "#/definitions/V10PresentProofModuleResponse" + } + } + } + } + }, + "/present-proof/records/{pres_ex_id}/send-presentation" : { + "post" : { + "tags" : [ "present-proof v1.0" ], + "summary" : "Sends a proof presentation", + "produces" : [ "application/json" ], + "parameters" : [ { + "in" : "body", + "name" : "body", + "required" : false, + "schema" : { + "$ref" : "#/definitions/IndyPresSpec" + } + }, { + "name" : "pres_ex_id", + "in" : "path", + "description" : "Presentation exchange identifier", + "required" : true, + "type" : "string", + "pattern" : "[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-4[a-fA-F0-9]{3}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}" + } ], + "responses" : { + "200" : { + "description" : "", + "schema" : { + "$ref" : "#/definitions/V10PresentationExchange" + } + } + } + } + }, + "/present-proof/records/{pres_ex_id}/send-request" : { + "post" : { + "tags" : [ "present-proof v1.0" ], + "summary" : "Sends a presentation request in reference to a proposal", + "produces" : [ "application/json" ], + "parameters" : [ { + "in" : "body", + "name" : "body", + "required" : false, + "schema" : { + "$ref" : "#/definitions/V10PresentationSendRequestToProposal" + } + }, { + "name" : "pres_ex_id", + "in" : "path", + "description" : "Presentation exchange identifier", + "required" : true, + "type" : "string", + "pattern" : "[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-4[a-fA-F0-9]{3}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}" + } ], + "responses" : { + "200" : { + "description" : "", + "schema" : { + "$ref" : "#/definitions/V10PresentationExchange" + } + } + } + } + }, + "/present-proof/records/{pres_ex_id}/verify-presentation" : { + "post" : { + "tags" : [ "present-proof v1.0" ], + "summary" : "Verify a received presentation", + "produces" : [ "application/json" ], + "parameters" : [ { + "name" : "pres_ex_id", + "in" : "path", + "description" : "Presentation exchange identifier", + "required" : true, + "type" : "string", + "pattern" : "[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-4[a-fA-F0-9]{3}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}" + } ], + "responses" : { + "200" : { + "description" : "", + "schema" : { + "$ref" : "#/definitions/V10PresentationExchange" + } + } + } + } + }, + "/present-proof/send-proposal" : { + "post" : { + "tags" : [ "present-proof v1.0" ], + "summary" : "Sends a presentation proposal", + "produces" : [ "application/json" ], + "parameters" : [ { + "in" : "body", + "name" : "body", + "required" : false, + "schema" : { + "$ref" : "#/definitions/V10PresentationProposalRequest" + } + } ], + "responses" : { + "200" : { + "description" : "", + "schema" : { + "$ref" : "#/definitions/V10PresentationExchange" + } + } + } + } + }, + "/present-proof/send-request" : { + "post" : { + "tags" : [ "present-proof v1.0" ], + "summary" : "Sends a free presentation request not bound to any proposal", + "produces" : [ "application/json" ], + "parameters" : [ { + "in" : "body", + "name" : "body", + "required" : false, + "schema" : { + "$ref" : "#/definitions/V10PresentationSendRequestRequest" + } + } ], + "responses" : { + "200" : { + "description" : "", + "schema" : { + "$ref" : "#/definitions/V10PresentationExchange" + } + } + } + } + }, + "/resolver/resolve/{did}" : { + "get" : { + "tags" : [ "resolver" ], + "summary" : "Retrieve doc for requested did", + "produces" : [ "application/json" ], + "parameters" : [ { + "name" : "did", + "in" : "path", + "description" : "DID", + "required" : true, + "type" : "string", + "pattern" : "^did:([a-z0-9]+):((?:[a-zA-Z0-9._%-]*:)*[a-zA-Z0-9._%-]+)$" + } ], + "responses" : { + "200" : { + "description" : "", + "schema" : { + "$ref" : "#/definitions/ResolutionResult" + } + } + } + } + }, + "/revocation/active-registry/{cred_def_id}" : { + "get" : { + "tags" : [ "revocation" ], + "summary" : "Get current active revocation registry by credential definition id", + "produces" : [ "application/json" ], + "parameters" : [ { + "name" : "cred_def_id", + "in" : "path", + "description" : "Credential definition identifier", + "required" : true, + "type" : "string", + "pattern" : "^([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}):3:CL:(([1-9][0-9]*)|([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}:2:.+:[0-9.]+)):(.+)?$" + } ], + "responses" : { + "200" : { + "description" : "", + "schema" : { + "$ref" : "#/definitions/RevRegResult" + } + } + } + } + }, + "/revocation/clear-pending-revocations" : { + "post" : { + "tags" : [ "revocation" ], + "summary" : "Clear pending revocations", + "produces" : [ "application/json" ], + "parameters" : [ { + "in" : "body", + "name" : "body", + "required" : false, + "schema" : { + "$ref" : "#/definitions/ClearPendingRevocationsRequest" + } + } ], + "responses" : { + "200" : { + "description" : "", + "schema" : { + "$ref" : "#/definitions/PublishRevocations" + } + } + } + } + }, + "/revocation/create-registry" : { + "post" : { + "tags" : [ "revocation" ], + "summary" : "Creates a new revocation registry", + "produces" : [ "application/json" ], + "parameters" : [ { + "in" : "body", + "name" : "body", + "required" : false, + "schema" : { + "$ref" : "#/definitions/RevRegCreateRequest" + } + } ], + "responses" : { + "200" : { + "description" : "", + "schema" : { + "$ref" : "#/definitions/RevRegResult" + } + } + } + } + }, + "/revocation/credential-record" : { + "get" : { + "tags" : [ "revocation" ], + "summary" : "Get credential revocation status", + "produces" : [ "application/json" ], + "parameters" : [ { + "name" : "cred_ex_id", + "in" : "query", + "description" : "Credential exchange identifier", + "required" : false, + "type" : "string", + "pattern" : "[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-4[a-fA-F0-9]{3}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}" + }, { + "name" : "cred_rev_id", + "in" : "query", + "description" : "Credential revocation identifier", + "required" : false, + "type" : "string", + "pattern" : "^[1-9][0-9]*$" + }, { + "name" : "rev_reg_id", + "in" : "query", + "description" : "Revocation registry identifier", + "required" : false, + "type" : "string", + "pattern" : "^([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}):4:([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}):3:CL:(([1-9][0-9]*)|([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}:2:.+:[0-9.]+))(:.+)?:CL_ACCUM:(.+$)" + } ], + "responses" : { + "200" : { + "description" : "", + "schema" : { + "$ref" : "#/definitions/CredRevRecordResult" + } + } + } + } + }, + "/revocation/publish-revocations" : { + "post" : { + "tags" : [ "revocation" ], + "summary" : "Publish pending revocations to ledger", + "produces" : [ "application/json" ], + "parameters" : [ { + "in" : "body", + "name" : "body", + "required" : false, + "schema" : { + "$ref" : "#/definitions/PublishRevocations" + } + } ], + "responses" : { + "200" : { + "description" : "", + "schema" : { + "$ref" : "#/definitions/TxnOrPublishRevocationsResult" + } + } + } + } + }, + "/revocation/registries/created" : { + "get" : { + "tags" : [ "revocation" ], + "summary" : "Search for matching revocation registries that current agent created", + "produces" : [ "application/json" ], + "parameters" : [ { + "name" : "cred_def_id", + "in" : "query", + "description" : "Credential definition identifier", + "required" : false, + "type" : "string", + "pattern" : "^([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}):3:CL:(([1-9][0-9]*)|([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}:2:.+:[0-9.]+)):(.+)?$" + }, { + "name" : "state", + "in" : "query", + "description" : "Revocation registry state", + "required" : false, + "type" : "string", + "enum" : [ "init", "generated", "posted", "active", "full" ] + } ], + "responses" : { + "200" : { + "description" : "", + "schema" : { + "$ref" : "#/definitions/RevRegsCreated" + } + } + } + } + }, + "/revocation/registry/delete-tails-file" : { + "delete" : { + "tags" : [ "revocation" ], + "summary" : "Delete the tail files", + "produces" : [ "application/json" ], + "parameters" : [ { + "name" : "cred_def_id", + "in" : "query", + "description" : "Credential definition identifier", + "required" : false, + "type" : "string", + "pattern" : "^([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}):3:CL:(([1-9][0-9]*)|([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}:2:.+:[0-9.]+)):(.+)?$" + }, { + "name" : "rev_reg_id", + "in" : "query", + "description" : "Revocation registry identifier", + "required" : false, + "type" : "string", + "pattern" : "^([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}):4:([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}):3:CL:(([1-9][0-9]*)|([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}:2:.+:[0-9.]+))(:.+)?:CL_ACCUM:(.+$)" + } ], + "responses" : { + "200" : { + "description" : "", + "schema" : { + "$ref" : "#/definitions/TailsDeleteResponse" + } + } + } + } + }, + "/revocation/registry/{rev_reg_id}" : { + "get" : { + "tags" : [ "revocation" ], + "summary" : "Get revocation registry by revocation registry id", + "produces" : [ "application/json" ], + "parameters" : [ { + "name" : "rev_reg_id", + "in" : "path", + "description" : "Revocation Registry identifier", + "required" : true, + "type" : "string", + "pattern" : "^([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}):4:([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}):3:CL:(([1-9][0-9]*)|([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}:2:.+:[0-9.]+))(:.+)?:CL_ACCUM:(.+$)" + } ], + "responses" : { + "200" : { + "description" : "", + "schema" : { + "$ref" : "#/definitions/RevRegResult" + } + } + } + }, + "patch" : { + "tags" : [ "revocation" ], + "summary" : "Update revocation registry with new public URI to its tails file", + "produces" : [ "application/json" ], + "parameters" : [ { + "in" : "body", + "name" : "body", + "required" : false, + "schema" : { + "$ref" : "#/definitions/RevRegUpdateTailsFileUri" + } + }, { + "name" : "rev_reg_id", + "in" : "path", + "description" : "Revocation Registry identifier", + "required" : true, + "type" : "string", + "pattern" : "^([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}):4:([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}):3:CL:(([1-9][0-9]*)|([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}:2:.+:[0-9.]+))(:.+)?:CL_ACCUM:(.+$)" + } ], + "responses" : { + "200" : { + "description" : "", + "schema" : { + "$ref" : "#/definitions/RevRegResult" + } + } + } + } + }, + "/revocation/registry/{rev_reg_id}/definition" : { + "post" : { + "tags" : [ "revocation" ], + "summary" : "Send revocation registry definition to ledger", + "produces" : [ "application/json" ], + "parameters" : [ { + "name" : "rev_reg_id", + "in" : "path", + "description" : "Revocation Registry identifier", + "required" : true, + "type" : "string", + "pattern" : "^([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}):4:([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}):3:CL:(([1-9][0-9]*)|([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}:2:.+:[0-9.]+))(:.+)?:CL_ACCUM:(.+$)" + }, { + "name" : "conn_id", + "in" : "query", + "description" : "Connection identifier", + "required" : false, + "type" : "string" + }, { + "name" : "create_transaction_for_endorser", + "in" : "query", + "description" : "Create Transaction For Endorser's signature", + "required" : false, + "type" : "boolean" + } ], + "responses" : { + "200" : { + "description" : "", + "schema" : { + "$ref" : "#/definitions/TxnOrRevRegResult" + } + } + } + } + }, + "/revocation/registry/{rev_reg_id}/entry" : { + "post" : { + "tags" : [ "revocation" ], + "summary" : "Send revocation registry entry to ledger", + "produces" : [ "application/json" ], + "parameters" : [ { + "name" : "rev_reg_id", + "in" : "path", + "description" : "Revocation Registry identifier", + "required" : true, + "type" : "string", + "pattern" : "^([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}):4:([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}):3:CL:(([1-9][0-9]*)|([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}:2:.+:[0-9.]+))(:.+)?:CL_ACCUM:(.+$)" + }, { + "name" : "conn_id", + "in" : "query", + "description" : "Connection identifier", + "required" : false, + "type" : "string" + }, { + "name" : "create_transaction_for_endorser", + "in" : "query", + "description" : "Create Transaction For Endorser's signature", + "required" : false, + "type" : "boolean" + } ], + "responses" : { + "200" : { + "description" : "", + "schema" : { + "$ref" : "#/definitions/RevRegResult" + } + } + } + } + }, + "/revocation/registry/{rev_reg_id}/fix-revocation-entry-state" : { + "put" : { + "tags" : [ "revocation" ], + "summary" : "Fix revocation state in wallet and return number of updated entries", + "produces" : [ "application/json" ], + "parameters" : [ { + "name" : "rev_reg_id", + "in" : "path", + "description" : "Revocation Registry identifier", + "required" : true, + "type" : "string", + "pattern" : "^([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}):4:([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}):3:CL:(([1-9][0-9]*)|([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}:2:.+:[0-9.]+))(:.+)?:CL_ACCUM:(.+$)" + }, { + "name" : "apply_ledger_update", + "in" : "query", + "description" : "Apply updated accumulator transaction to ledger", + "required" : true, + "type" : "boolean" + } ], + "responses" : { + "200" : { + "description" : "", + "schema" : { + "$ref" : "#/definitions/RevRegWalletUpdatedResult" + } + } + } + } + }, + "/revocation/registry/{rev_reg_id}/issued" : { + "get" : { + "tags" : [ "revocation" ], + "summary" : "Get number of credentials issued against revocation registry", + "produces" : [ "application/json" ], + "parameters" : [ { + "name" : "rev_reg_id", + "in" : "path", + "description" : "Revocation Registry identifier", + "required" : true, + "type" : "string", + "pattern" : "^([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}):4:([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}):3:CL:(([1-9][0-9]*)|([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}:2:.+:[0-9.]+))(:.+)?:CL_ACCUM:(.+$)" + } ], + "responses" : { + "200" : { + "description" : "", + "schema" : { + "$ref" : "#/definitions/RevRegIssuedResult" + } + } + } + } + }, + "/revocation/registry/{rev_reg_id}/issued/details" : { + "get" : { + "tags" : [ "revocation" ], + "summary" : "Get details of credentials issued against revocation registry", + "produces" : [ "application/json" ], + "parameters" : [ { + "name" : "rev_reg_id", + "in" : "path", + "description" : "Revocation Registry identifier", + "required" : true, + "type" : "string", + "pattern" : "^([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}):4:([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}):3:CL:(([1-9][0-9]*)|([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}:2:.+:[0-9.]+))(:.+)?:CL_ACCUM:(.+$)" + } ], + "responses" : { + "200" : { + "description" : "", + "schema" : { + "$ref" : "#/definitions/CredRevRecordDetailsResult" + } + } + } + } + }, + "/revocation/registry/{rev_reg_id}/issued/indy_recs" : { + "get" : { + "tags" : [ "revocation" ], + "summary" : "Get details of revoked credentials from ledger", + "produces" : [ "application/json" ], + "parameters" : [ { + "name" : "rev_reg_id", + "in" : "path", + "description" : "Revocation Registry identifier", + "required" : true, + "type" : "string", + "pattern" : "^([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}):4:([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}):3:CL:(([1-9][0-9]*)|([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}:2:.+:[0-9.]+))(:.+)?:CL_ACCUM:(.+$)" + } ], + "responses" : { + "200" : { + "description" : "", + "schema" : { + "$ref" : "#/definitions/CredRevIndyRecordsResult" + } + } + } + } + }, + "/revocation/registry/{rev_reg_id}/set-state" : { + "patch" : { + "tags" : [ "revocation" ], + "summary" : "Set revocation registry state manually", + "produces" : [ "application/json" ], + "parameters" : [ { + "name" : "rev_reg_id", + "in" : "path", + "description" : "Revocation Registry identifier", + "required" : true, + "type" : "string", + "pattern" : "^([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}):4:([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}):3:CL:(([1-9][0-9]*)|([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}:2:.+:[0-9.]+))(:.+)?:CL_ACCUM:(.+$)" + }, { + "name" : "state", + "in" : "query", + "description" : "Revocation registry state to set", + "required" : true, + "type" : "string", + "enum" : [ "init", "generated", "posted", "active", "full" ] + } ], + "responses" : { + "200" : { + "description" : "", + "schema" : { + "$ref" : "#/definitions/RevRegResult" + } + } + } + } + }, + "/revocation/registry/{rev_reg_id}/tails-file" : { + "get" : { + "tags" : [ "revocation" ], + "summary" : "Download tails file", + "produces" : [ "application/octet-stream" ], + "parameters" : [ { + "name" : "rev_reg_id", + "in" : "path", + "description" : "Revocation Registry identifier", + "required" : true, + "type" : "string", + "pattern" : "^([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}):4:([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}):3:CL:(([1-9][0-9]*)|([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}:2:.+:[0-9.]+))(:.+)?:CL_ACCUM:(.+$)" + } ], + "responses" : { + "200" : { + "description" : "tails file", + "schema" : { + "type" : "string", + "format" : "binary" + } + } + } + }, + "put" : { + "tags" : [ "revocation" ], + "summary" : "Upload local tails file to server", + "produces" : [ "application/json" ], + "parameters" : [ { + "name" : "rev_reg_id", + "in" : "path", + "description" : "Revocation Registry identifier", + "required" : true, + "type" : "string", + "pattern" : "^([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}):4:([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}):3:CL:(([1-9][0-9]*)|([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}:2:.+:[0-9.]+))(:.+)?:CL_ACCUM:(.+$)" + } ], + "responses" : { + "200" : { + "description" : "", + "schema" : { + "$ref" : "#/definitions/RevocationModuleResponse" + } + } + } + } + }, + "/revocation/revoke" : { + "post" : { + "tags" : [ "revocation" ], + "summary" : "Revoke an issued credential", + "produces" : [ "application/json" ], + "parameters" : [ { + "in" : "body", + "name" : "body", + "required" : false, + "schema" : { + "$ref" : "#/definitions/RevokeRequest" + } + } ], + "responses" : { + "200" : { + "description" : "", + "schema" : { + "$ref" : "#/definitions/RevocationModuleResponse" + } + } + } + } + }, + "/schemas" : { + "post" : { + "tags" : [ "schema" ], + "summary" : "Sends a schema to the ledger", + "produces" : [ "application/json" ], + "parameters" : [ { + "in" : "body", + "name" : "body", + "required" : false, + "schema" : { + "$ref" : "#/definitions/SchemaSendRequest" + } + }, { + "name" : "conn_id", + "in" : "query", + "description" : "Connection identifier", + "required" : false, + "type" : "string" + }, { + "name" : "create_transaction_for_endorser", + "in" : "query", + "description" : "Create Transaction For Endorser's signature", + "required" : false, + "type" : "boolean" + } ], + "responses" : { + "200" : { + "description" : "", + "schema" : { + "$ref" : "#/definitions/TxnOrSchemaSendResult" + } + } + } + } + }, + "/schemas/created" : { + "get" : { + "tags" : [ "schema" ], + "summary" : "Search for matching schema that agent originated", + "produces" : [ "application/json" ], + "parameters" : [ { + "name" : "schema_id", + "in" : "query", + "description" : "Schema identifier", + "required" : false, + "type" : "string", + "pattern" : "^[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}:2:.+:[0-9.]+$" + }, { + "name" : "schema_issuer_did", + "in" : "query", + "description" : "Schema issuer DID", + "required" : false, + "type" : "string", + "pattern" : "^(did:sov:)?[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}$" + }, { + "name" : "schema_name", + "in" : "query", + "description" : "Schema name", + "required" : false, + "type" : "string" + }, { + "name" : "schema_version", + "in" : "query", + "description" : "Schema version", + "required" : false, + "type" : "string", + "pattern" : "^[0-9.]+$" + } ], + "responses" : { + "200" : { + "description" : "", + "schema" : { + "$ref" : "#/definitions/SchemasCreatedResult" + } + } + } + } + }, + "/schemas/{schema_id}" : { + "get" : { + "tags" : [ "schema" ], + "summary" : "Gets a schema from the ledger", + "produces" : [ "application/json" ], + "parameters" : [ { + "name" : "schema_id", + "in" : "path", + "description" : "Schema identifier", + "required" : true, + "type" : "string", + "pattern" : "^[1-9][0-9]*|[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}:2:.+:[0-9.]+$" + } ], + "responses" : { + "200" : { + "description" : "", + "schema" : { + "$ref" : "#/definitions/SchemaGetResult" + } + } + } + } + }, + "/schemas/{schema_id}/write_record" : { + "post" : { + "tags" : [ "schema" ], + "summary" : "Writes a schema non-secret record to the wallet", + "produces" : [ "application/json" ], + "parameters" : [ { + "name" : "schema_id", + "in" : "path", + "description" : "Schema identifier", + "required" : true, + "type" : "string", + "pattern" : "^[1-9][0-9]*|[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}:2:.+:[0-9.]+$" + } ], + "responses" : { + "200" : { + "description" : "", + "schema" : { + "$ref" : "#/definitions/SchemaGetResult" + } + } + } + } + }, + "/settings" : { + "get" : { + "tags" : [ "settings" ], + "summary" : "Get profile settings or config", + "produces" : [ "application/json" ], + "parameters" : [ ], + "responses" : { + "200" : { + "type" : "object", + "description" : "Settings for this wallet.", + "properties" : { } + } + } + }, + "put" : { + "tags" : [ "settings" ], + "summary" : "Update profile settings or config", + "produces" : [ "application/json" ], + "parameters" : [ { + "in" : "body", + "name" : "body", + "required" : false, + "schema" : { + "$ref" : "#/definitions/UpdateProfileSettingsRequest" + } + } ], + "responses" : { + "200" : { + "type" : "object", + "description" : "Settings for this wallet.", + "properties" : { } + } + } + } + }, + "/shutdown" : { + "get" : { + "tags" : [ "server" ], + "summary" : "Shut down server", + "produces" : [ "application/json" ], + "parameters" : [ ], + "responses" : { + "200" : { + "description" : "", + "schema" : { + "$ref" : "#/definitions/AdminShutdown" + } + } + } + } + }, + "/status" : { + "get" : { + "tags" : [ "server" ], + "summary" : "Fetch the server status", + "produces" : [ "application/json" ], + "parameters" : [ ], + "responses" : { + "200" : { + "description" : "", + "schema" : { + "$ref" : "#/definitions/AdminStatus" + } + } + } + } + }, + "/status/config" : { + "get" : { + "tags" : [ "server" ], + "summary" : "Fetch the server configuration", + "produces" : [ "application/json" ], + "parameters" : [ ], + "responses" : { + "200" : { + "description" : "", + "schema" : { + "$ref" : "#/definitions/AdminConfig" + } + } + } + } + }, + "/status/live" : { + "get" : { + "tags" : [ "server" ], + "summary" : "Liveliness check", + "produces" : [ "application/json" ], + "parameters" : [ ], + "responses" : { + "200" : { + "description" : "", + "schema" : { + "$ref" : "#/definitions/AdminStatusLiveliness" + } + } + } + } + }, + "/status/ready" : { + "get" : { + "tags" : [ "server" ], + "summary" : "Readiness check", + "produces" : [ "application/json" ], + "parameters" : [ ], + "responses" : { + "200" : { + "description" : "", + "schema" : { + "$ref" : "#/definitions/AdminStatusReadiness" + } + } + } + } + }, + "/status/reset" : { + "post" : { + "tags" : [ "server" ], + "summary" : "Reset statistics", + "produces" : [ "application/json" ], + "parameters" : [ ], + "responses" : { + "200" : { + "description" : "", + "schema" : { + "$ref" : "#/definitions/AdminReset" + } + } + } + } + }, + "/transaction/{tran_id}/resend" : { + "post" : { + "tags" : [ "endorse-transaction" ], + "summary" : "For Author to resend a particular transaction request", + "produces" : [ "application/json" ], + "parameters" : [ { + "name" : "tran_id", + "in" : "path", + "description" : "Transaction identifier", + "required" : true, + "type" : "string" + } ], + "responses" : { + "200" : { + "description" : "", + "schema" : { + "$ref" : "#/definitions/TransactionRecord" + } + } + } + } + }, + "/transactions" : { + "get" : { + "tags" : [ "endorse-transaction" ], + "summary" : "Query transactions", + "produces" : [ "application/json" ], + "parameters" : [ ], + "responses" : { + "200" : { + "description" : "", + "schema" : { + "$ref" : "#/definitions/TransactionList" + } + } + } + } + }, + "/transactions/create-request" : { + "post" : { + "tags" : [ "endorse-transaction" ], + "summary" : "For author to send a transaction request", + "produces" : [ "application/json" ], + "parameters" : [ { + "in" : "body", + "name" : "body", + "required" : false, + "schema" : { + "$ref" : "#/definitions/Date" + } + }, { + "name" : "tran_id", + "in" : "query", + "description" : "Transaction identifier", + "required" : true, + "type" : "string" + }, { + "name" : "endorser_write_txn", + "in" : "query", + "description" : "Endorser will write the transaction after endorsing it", + "required" : false, + "type" : "boolean" + } ], + "responses" : { + "200" : { + "description" : "", + "schema" : { + "$ref" : "#/definitions/TransactionRecord" + } + } + } + } + }, + "/transactions/{conn_id}/set-endorser-info" : { + "post" : { + "tags" : [ "endorse-transaction" ], + "summary" : "Set Endorser Info", + "produces" : [ "application/json" ], + "parameters" : [ { + "name" : "conn_id", + "in" : "path", + "description" : "Connection identifier", + "required" : true, + "type" : "string" + }, { + "name" : "endorser_did", + "in" : "query", + "description" : "Endorser DID", + "required" : true, + "type" : "string" + }, { + "name" : "endorser_name", + "in" : "query", + "description" : "Endorser Name", + "required" : false, + "type" : "string" + } ], + "responses" : { + "200" : { + "description" : "", + "schema" : { + "$ref" : "#/definitions/EndorserInfo" + } + } + } + } + }, + "/transactions/{conn_id}/set-endorser-role" : { + "post" : { + "tags" : [ "endorse-transaction" ], + "summary" : "Set transaction jobs", + "produces" : [ "application/json" ], + "parameters" : [ { + "name" : "conn_id", + "in" : "path", + "description" : "Connection identifier", + "required" : true, + "type" : "string" + }, { + "name" : "transaction_my_job", + "in" : "query", + "description" : "Transaction related jobs", + "required" : false, + "type" : "string", + "enum" : [ "TRANSACTION_AUTHOR", "TRANSACTION_ENDORSER", "reset" ] + } ], + "responses" : { + "200" : { + "description" : "", + "schema" : { + "$ref" : "#/definitions/TransactionJobs" + } + } + } + } + }, + "/transactions/{tran_id}" : { + "get" : { + "tags" : [ "endorse-transaction" ], + "summary" : "Fetch a single transaction record", + "produces" : [ "application/json" ], + "parameters" : [ { + "name" : "tran_id", + "in" : "path", + "description" : "Transaction identifier", + "required" : true, + "type" : "string" + } ], + "responses" : { + "200" : { + "description" : "", + "schema" : { + "$ref" : "#/definitions/TransactionRecord" + } + } + } + } + }, + "/transactions/{tran_id}/cancel" : { + "post" : { + "tags" : [ "endorse-transaction" ], + "summary" : "For Author to cancel a particular transaction request", + "produces" : [ "application/json" ], + "parameters" : [ { + "name" : "tran_id", + "in" : "path", + "description" : "Transaction identifier", + "required" : true, + "type" : "string" + } ], + "responses" : { + "200" : { + "description" : "", + "schema" : { + "$ref" : "#/definitions/TransactionRecord" + } + } + } + } + }, + "/transactions/{tran_id}/endorse" : { + "post" : { + "tags" : [ "endorse-transaction" ], + "summary" : "For Endorser to endorse a particular transaction record", + "produces" : [ "application/json" ], + "parameters" : [ { + "name" : "tran_id", + "in" : "path", + "description" : "Transaction identifier", + "required" : true, + "type" : "string" + }, { + "name" : "endorser_did", + "in" : "query", + "description" : "Endorser DID", + "required" : false, + "type" : "string" + } ], + "responses" : { + "200" : { + "description" : "", + "schema" : { + "$ref" : "#/definitions/TransactionRecord" + } + } + } + } + }, + "/transactions/{tran_id}/refuse" : { + "post" : { + "tags" : [ "endorse-transaction" ], + "summary" : "For Endorser to refuse a particular transaction record", + "produces" : [ "application/json" ], + "parameters" : [ { + "name" : "tran_id", + "in" : "path", + "description" : "Transaction identifier", + "required" : true, + "type" : "string" + } ], + "responses" : { + "200" : { + "description" : "", + "schema" : { + "$ref" : "#/definitions/TransactionRecord" + } + } + } + } + }, + "/transactions/{tran_id}/write" : { + "post" : { + "tags" : [ "endorse-transaction" ], + "summary" : "For Author / Endorser to write an endorsed transaction to the ledger", + "produces" : [ "application/json" ], + "parameters" : [ { + "name" : "tran_id", + "in" : "path", + "description" : "Transaction identifier", + "required" : true, + "type" : "string" + } ], + "responses" : { + "200" : { + "description" : "", + "schema" : { + "$ref" : "#/definitions/TransactionRecord" + } + } + } + } + }, + "/wallet/did" : { + "get" : { + "tags" : [ "wallet" ], + "summary" : "List wallet DIDs", + "produces" : [ "application/json" ], + "parameters" : [ { + "name" : "did", + "in" : "query", + "description" : "DID of interest", + "required" : false, + "type" : "string", + "pattern" : "^(did:sov:)?[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}$|^did:([a-zA-Z0-9_]+):([a-zA-Z0-9_.%-]+(:[a-zA-Z0-9_.%-]+)*)((;[a-zA-Z0-9_.:%-]+=[a-zA-Z0-9_.:%-]*)*)(\\/[^#?]*)?([?][^#]*)?(\\#.*)?$$" + }, { + "name" : "key_type", + "in" : "query", + "description" : "Key type to query for.", + "required" : false, + "type" : "string", + "enum" : [ "ed25519", "bls12381g2" ] + }, { + "name" : "method", + "in" : "query", + "description" : "DID method to query for. e.g. sov to only fetch indy/sov DIDs", + "required" : false, + "type" : "string", + "enum" : [ "key", "sov" ] + }, { + "name" : "posture", + "in" : "query", + "description" : "Whether DID is current public DID, posted to ledger but current public DID, or local to the wallet", + "required" : false, + "type" : "string", + "enum" : [ "public", "posted", "wallet_only" ] + }, { + "name" : "verkey", + "in" : "query", + "description" : "Verification key of interest", + "required" : false, + "type" : "string", + "pattern" : "^[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{43,44}$" + } ], + "responses" : { + "200" : { + "description" : "", + "schema" : { + "$ref" : "#/definitions/DIDList" + } + } + } + } + }, + "/wallet/did/create" : { + "post" : { + "tags" : [ "wallet" ], + "summary" : "Create a local DID", + "produces" : [ "application/json" ], + "parameters" : [ { + "in" : "body", + "name" : "body", + "required" : false, + "schema" : { + "$ref" : "#/definitions/DIDCreate" + } + } ], + "responses" : { + "200" : { + "description" : "", + "schema" : { + "$ref" : "#/definitions/DIDResult" + } + } + } + } + }, + "/wallet/did/local/rotate-keypair" : { + "patch" : { + "tags" : [ "wallet" ], + "summary" : "Rotate keypair for a DID not posted to the ledger", + "produces" : [ "application/json" ], + "parameters" : [ { + "name" : "did", + "in" : "query", + "description" : "DID of interest", + "required" : true, + "type" : "string", + "pattern" : "^(did:sov:)?[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}$" + } ], + "responses" : { + "200" : { + "description" : "", + "schema" : { + "$ref" : "#/definitions/WalletModuleResponse" + } + } + } + } + }, + "/wallet/did/public" : { + "get" : { + "tags" : [ "wallet" ], + "summary" : "Fetch the current public DID", + "produces" : [ "application/json" ], + "parameters" : [ ], + "responses" : { + "200" : { + "description" : "", + "schema" : { + "$ref" : "#/definitions/DIDResult" + } + } + } + }, + "post" : { + "tags" : [ "wallet" ], + "summary" : "Assign the current public DID", + "produces" : [ "application/json" ], + "parameters" : [ { + "name" : "did", + "in" : "query", + "description" : "DID of interest", + "required" : true, + "type" : "string", + "pattern" : "^(did:sov:)?[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}$" + }, { + "name" : "conn_id", + "in" : "query", + "description" : "Connection identifier", + "required" : false, + "type" : "string" + }, { + "name" : "create_transaction_for_endorser", + "in" : "query", + "description" : "Create Transaction For Endorser's signature", + "required" : false, + "type" : "boolean" + }, { + "name" : "mediation_id", + "in" : "query", + "description" : "Mediation identifier", + "required" : false, + "type" : "string" + } ], + "responses" : { + "200" : { + "description" : "", + "schema" : { + "$ref" : "#/definitions/DIDResult" + } + } + } + } + }, + "/wallet/get-did-endpoint" : { + "get" : { + "tags" : [ "wallet" ], + "summary" : "Query DID endpoint in wallet", + "produces" : [ "application/json" ], + "parameters" : [ { + "name" : "did", + "in" : "query", + "description" : "DID of interest", + "required" : true, + "type" : "string", + "pattern" : "^(did:sov:)?[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}$" + } ], + "responses" : { + "200" : { + "description" : "", + "schema" : { + "$ref" : "#/definitions/DIDEndpoint" + } + } + } + } + }, + "/wallet/set-did-endpoint" : { + "post" : { + "tags" : [ "wallet" ], + "summary" : "Update endpoint in wallet and on ledger if posted to it", + "produces" : [ "application/json" ], + "parameters" : [ { + "in" : "body", + "name" : "body", + "required" : false, + "schema" : { + "$ref" : "#/definitions/DIDEndpointWithType" + } + }, { + "name" : "conn_id", + "in" : "query", + "description" : "Connection identifier", + "required" : false, + "type" : "string" + }, { + "name" : "create_transaction_for_endorser", + "in" : "query", + "description" : "Create Transaction For Endorser's signature", + "required" : false, + "type" : "boolean" + } ], + "responses" : { + "200" : { + "description" : "", + "schema" : { + "$ref" : "#/definitions/WalletModuleResponse" + } + } + } + } + } + }, + "securityDefinitions" : { + "AuthorizationHeader" : { + "description" : "Bearer token. Be sure to preprend token with 'Bearer '", + "type" : "apiKey", + "name" : "Authorization", + "in" : "header" + } + }, + "definitions" : { + "AMLRecord" : { + "type" : "object", + "properties" : { + "aml" : { + "type" : "object", + "additionalProperties" : { + "type" : "string" + } + }, + "amlContext" : { + "type" : "string" + }, + "version" : { + "type" : "string" + } + } + }, + "ActionMenuFetchResult" : { + "type" : "object", + "properties" : { + "result" : { + "$ref" : "#/definitions/ActionMenuFetchResult_result" + } + } + }, + "ActionMenuModulesResult" : { + "type" : "object" + }, + "AdminConfig" : { + "type" : "object", + "properties" : { + "config" : { + "type" : "object", + "description" : "Configuration settings", + "properties" : { } + } + } + }, + "AdminMediationDeny" : { + "type" : "object", + "properties" : { + "mediator_terms" : { + "type" : "array", + "description" : "List of mediator rules for recipient", + "items" : { + "type" : "string", + "description" : "Indicate terms to which the mediator requires the recipient to agree" + } + }, + "recipient_terms" : { + "type" : "array", + "description" : "List of recipient rules for mediation", + "items" : { + "type" : "string", + "description" : "Indicate terms to which the recipient requires the mediator to agree" + } + } + } + }, + "AdminModules" : { + "type" : "object", + "properties" : { + "result" : { + "type" : "array", + "description" : "List of admin modules", + "items" : { + "type" : "string", + "description" : "admin module" + } + } + } + }, + "AdminReset" : { + "type" : "object" + }, + "AdminShutdown" : { + "type" : "object" + }, + "AdminStatus" : { + "type" : "object", + "properties" : { + "conductor" : { + "type" : "object", + "description" : "Conductor statistics", + "properties" : { } + }, + "label" : { + "type" : "string", + "description" : "Default label", + "x-nullable" : true + }, + "timing" : { + "type" : "object", + "description" : "Timing results", + "properties" : { } + }, + "version" : { + "type" : "string", + "description" : "Version code" + } + } + }, + "AdminStatusLiveliness" : { + "type" : "object", + "properties" : { + "alive" : { + "type" : "boolean", + "example" : true, + "description" : "Liveliness status" + } + } + }, + "AdminStatusReadiness" : { + "type" : "object", + "properties" : { + "ready" : { + "type" : "boolean", + "example" : true, + "description" : "Readiness status" + } + } + }, + "AttachDecorator" : { + "type" : "object", + "required" : [ "data" ], + "properties" : { + "@id" : { + "type" : "string", + "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "description" : "Attachment identifier" + }, + "byte_count" : { + "type" : "integer", + "format" : "int32", + "example" : 1234, + "description" : "Byte count of data included by reference" + }, + "data" : { + "$ref" : "#/definitions/AttachDecoratorData" + }, + "description" : { + "type" : "string", + "example" : "view from doorway, facing east, with lights off", + "description" : "Human-readable description of content" + }, + "filename" : { + "type" : "string", + "example" : "IMG1092348.png", + "description" : "File name" + }, + "lastmod_time" : { + "type" : "string", + "example" : "2021-12-31T23:59:59Z", + "description" : "Hint regarding last modification datetime, in ISO-8601 format", + "pattern" : "^\\d{4}-\\d\\d-\\d\\d[T ]\\d\\d:\\d\\d(?:\\:(?:\\d\\d(?:\\.\\d{1,6})?))?(?:[+-]\\d\\d:?\\d\\d|Z|)$" + }, + "mime-type" : { + "type" : "string", + "example" : "image/png", + "description" : "MIME type" + } + } + }, + "AttachDecoratorData" : { + "type" : "object", + "properties" : { + "base64" : { + "type" : "string", + "example" : "ey4uLn0=", + "description" : "Base64-encoded data", + "pattern" : "^[a-zA-Z0-9+/]*={0,2}$" + }, + "json" : { + "example" : "{\"sample\": \"content\"}", + "description" : "JSON-serialized data" + }, + "jws" : { + "$ref" : "#/definitions/AttachDecoratorData_jws" + }, + "links" : { + "type" : "array", + "description" : "List of hypertext links to data", + "items" : { + "type" : "string", + "example" : "https://link.to/data" + } + }, + "sha256" : { + "type" : "string", + "example" : "617a48c7c8afe0521efdc03e5bb0ad9e655893e6b4b51f0e794d70fba132aacb", + "description" : "SHA256 hash (binhex encoded) of content", + "pattern" : "^[a-fA-F0-9+/]{64}$" + } + } + }, + "AttachDecoratorData1JWS" : { + "type" : "object", + "required" : [ "header", "signature" ], + "properties" : { + "header" : { + "$ref" : "#/definitions/AttachDecoratorDataJWSHeader" + }, + "protected" : { + "type" : "string", + "example" : "ey4uLn0", + "description" : "protected JWS header", + "pattern" : "^[-_a-zA-Z0-9]*$" + }, + "signature" : { + "type" : "string", + "example" : "ey4uLn0", + "description" : "signature", + "pattern" : "^[-_a-zA-Z0-9]*$" + } + } + }, + "AttachDecoratorDataJWS" : { + "type" : "object", + "properties" : { + "header" : { + "$ref" : "#/definitions/AttachDecoratorDataJWSHeader" + }, + "protected" : { + "type" : "string", + "example" : "ey4uLn0", + "description" : "protected JWS header", + "pattern" : "^[-_a-zA-Z0-9]*$" + }, + "signature" : { + "type" : "string", + "example" : "ey4uLn0", + "description" : "signature", + "pattern" : "^[-_a-zA-Z0-9]*$" + }, + "signatures" : { + "type" : "array", + "description" : "List of signatures", + "items" : { + "$ref" : "#/definitions/AttachDecoratorData1JWS" + } + } + } + }, + "AttachDecoratorDataJWSHeader" : { + "type" : "object", + "required" : [ "kid" ], + "properties" : { + "kid" : { + "type" : "string", + "example" : "did:sov:LjgpST2rjsoxYegQDRm7EL#keys-4", + "description" : "Key identifier, in W3C did:key or DID URL format", + "pattern" : "^did:(?:key:z[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]+|sov:[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}(;.*)?(\\?.*)?#.+)$" + } + } + }, + "AttachmentDef" : { + "type" : "object", + "properties" : { + "id" : { + "type" : "string", + "example" : "attachment-0", + "description" : "Attachment identifier" + }, + "type" : { + "type" : "string", + "example" : "present-proof", + "description" : "Attachment type", + "enum" : [ "credential-offer", "present-proof" ] + } + } + }, + "AttributeMimeTypesResult" : { + "type" : "object", + "properties" : { + "results" : { + "type" : "object", + "additionalProperties" : { + "type" : "string", + "description" : "MIME type" + }, + "x-nullable" : true + } + } + }, + "BasicMessageModuleResponse" : { + "type" : "object" + }, + "ClaimFormat" : { + "type" : "object", + "properties" : { + "jwt" : { + "type" : "object", + "properties" : { } + }, + "jwt_vc" : { + "type" : "object", + "properties" : { } + }, + "jwt_vp" : { + "type" : "object", + "properties" : { } + }, + "ldp" : { + "type" : "object", + "properties" : { } + }, + "ldp_vc" : { + "type" : "object", + "properties" : { } + }, + "ldp_vp" : { + "type" : "object", + "properties" : { } + } + } + }, + "ClearPendingRevocationsRequest" : { + "type" : "object", + "properties" : { + "purge" : { + "type" : "object", + "description" : "Credential revocation ids by revocation registry id: omit for all, specify null or empty list for all pending per revocation registry", + "additionalProperties" : { + "type" : "array", + "items" : { + "type" : "string", + "example" : "12345", + "description" : "Credential revocation identifier", + "pattern" : "^[1-9][0-9]*$" + } + } + } + } + }, + "ConnRecord" : { + "type" : "object", + "properties" : { + "accept" : { + "type" : "string", + "example" : "auto", + "description" : "Connection acceptance: manual or auto", + "enum" : [ "manual", "auto" ] + }, + "alias" : { + "type" : "string", + "example" : "Bob, providing quotes", + "description" : "Optional alias to apply to connection for later use" + }, + "connection_id" : { + "type" : "string", + "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "description" : "Connection identifier" + }, + "connection_protocol" : { + "type" : "string", + "example" : "connections/1.0", + "description" : "Connection protocol used", + "enum" : [ "connections/1.0", "didexchange/1.0" ] + }, + "created_at" : { + "type" : "string", + "example" : "2021-12-31T23:59:59Z", + "description" : "Time of record creation", + "pattern" : "^\\d{4}-\\d\\d-\\d\\d[T ]\\d\\d:\\d\\d(?:\\:(?:\\d\\d(?:\\.\\d{1,6})?))?(?:[+-]\\d\\d:?\\d\\d|Z|)$" + }, + "error_msg" : { + "type" : "string", + "example" : "No DIDDoc provided; cannot connect to public DID", + "description" : "Error message" + }, + "inbound_connection_id" : { + "type" : "string", + "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "description" : "Inbound routing connection id to use" + }, + "invitation_key" : { + "type" : "string", + "example" : "H3C2AVvLMv6gmMNam3uVAjZpfkcJCwDwnZn6z3wXmqPV", + "description" : "Public key for connection", + "pattern" : "^[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{43,44}$" + }, + "invitation_mode" : { + "type" : "string", + "example" : "once", + "description" : "Invitation mode", + "enum" : [ "once", "multi", "static" ] + }, + "invitation_msg_id" : { + "type" : "string", + "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "description" : "ID of out-of-band invitation message" + }, + "my_did" : { + "type" : "string", + "example" : "WgWxqztrNooG92RXvxSTWv", + "description" : "Our DID for connection", + "pattern" : "^(did:sov:)?[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}$" + }, + "request_id" : { + "type" : "string", + "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "description" : "Connection request identifier" + }, + "rfc23_state" : { + "type" : "string", + "example" : "invitation-sent", + "description" : "State per RFC 23", + "readOnly" : true + }, + "routing_state" : { + "type" : "string", + "example" : "active", + "description" : "Routing state of connection", + "enum" : [ "none", "request", "active", "error" ] + }, + "state" : { + "type" : "string", + "example" : "active", + "description" : "Current record state" + }, + "their_did" : { + "type" : "string", + "example" : "WgWxqztrNooG92RXvxSTWv", + "description" : "Their DID for connection", + "pattern" : "^(did:sov:)?[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}$" + }, + "their_label" : { + "type" : "string", + "example" : "Bob", + "description" : "Their label for connection" + }, + "their_public_did" : { + "type" : "string", + "example" : "2cpBmR3FqGKWi5EyUbpRY8", + "description" : "Other agent's public DID for connection" + }, + "their_role" : { + "type" : "string", + "example" : "requester", + "description" : "Their role in the connection protocol", + "enum" : [ "invitee", "requester", "inviter", "responder" ] + }, + "updated_at" : { + "type" : "string", + "example" : "2021-12-31T23:59:59Z", + "description" : "Time of last record update", + "pattern" : "^\\d{4}-\\d\\d-\\d\\d[T ]\\d\\d:\\d\\d(?:\\:(?:\\d\\d(?:\\.\\d{1,6})?))?(?:[+-]\\d\\d:?\\d\\d|Z|)$" + } + } + }, + "ConnectionInvitation" : { + "type" : "object", + "properties" : { + "@id" : { + "type" : "string", + "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "description" : "Message identifier" + }, + "@type" : { + "type" : "string", + "example" : "https://didcomm.org/my-family/1.0/my-message-type", + "description" : "Message type", + "readOnly" : true + }, + "did" : { + "type" : "string", + "example" : "WgWxqztrNooG92RXvxSTWv", + "description" : "DID for connection invitation", + "pattern" : "^(did:sov:)?[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}$" + }, + "imageUrl" : { + "type" : "string", + "format" : "url", + "example" : "http://192.168.56.101/img/logo.jpg", + "description" : "Optional image URL for connection invitation", + "x-nullable" : true + }, + "label" : { + "type" : "string", + "example" : "Bob", + "description" : "Optional label for connection invitation" + }, + "recipientKeys" : { + "type" : "array", + "description" : "List of recipient keys", + "items" : { + "type" : "string", + "example" : "H3C2AVvLMv6gmMNam3uVAjZpfkcJCwDwnZn6z3wXmqPV", + "description" : "Recipient public key", + "pattern" : "^[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{43,44}$" + } + }, + "routingKeys" : { + "type" : "array", + "description" : "List of routing keys", + "items" : { + "type" : "string", + "example" : "H3C2AVvLMv6gmMNam3uVAjZpfkcJCwDwnZn6z3wXmqPV", + "description" : "Routing key", + "pattern" : "^[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{43,44}$" + } + }, + "serviceEndpoint" : { + "type" : "string", + "example" : "http://192.168.56.101:8020", + "description" : "Service endpoint at which to reach this agent" + } + } + }, + "ConnectionList" : { + "type" : "object", + "properties" : { + "results" : { + "type" : "array", + "description" : "List of connection records", + "items" : { + "$ref" : "#/definitions/ConnRecord" + } + } + } + }, + "ConnectionMetadata" : { + "type" : "object", + "properties" : { + "results" : { + "type" : "object", + "description" : "Dictionary of metadata associated with connection.", + "properties" : { } + } + } + }, + "ConnectionMetadataSetRequest" : { + "type" : "object", + "required" : [ "metadata" ], + "properties" : { + "metadata" : { + "type" : "object", + "description" : "Dictionary of metadata to set for connection.", + "properties" : { } + } + } + }, + "ConnectionModuleResponse" : { + "type" : "object" + }, + "ConnectionStaticRequest" : { + "type" : "object", + "properties" : { + "alias" : { + "type" : "string", + "description" : "Alias to assign to this connection" + }, + "my_did" : { + "type" : "string", + "example" : "WgWxqztrNooG92RXvxSTWv", + "description" : "Local DID", + "pattern" : "^(did:sov:)?[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}$" + }, + "my_seed" : { + "type" : "string", + "description" : "Seed to use for the local DID" + }, + "their_did" : { + "type" : "string", + "example" : "WgWxqztrNooG92RXvxSTWv", + "description" : "Remote DID", + "pattern" : "^(did:sov:)?[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}$" + }, + "their_endpoint" : { + "type" : "string", + "example" : "https://myhost:8021", + "description" : "URL endpoint for other party", + "pattern" : "^[A-Za-z0-9\\.\\-\\+]+://([A-Za-z0-9][.A-Za-z0-9-_]+[A-Za-z0-9])+(:[1-9][0-9]*)?(/[^?&#]+)?$" + }, + "their_label" : { + "type" : "string", + "description" : "Other party's label for this connection" + }, + "their_seed" : { + "type" : "string", + "description" : "Seed to use for the remote DID" + }, + "their_verkey" : { + "type" : "string", + "description" : "Remote verification key" + } + } + }, + "ConnectionStaticResult" : { + "type" : "object", + "required" : [ "my_did", "my_endpoint", "my_verkey", "record", "their_did", "their_verkey" ], + "properties" : { + "my_did" : { + "type" : "string", + "example" : "WgWxqztrNooG92RXvxSTWv", + "description" : "Local DID", + "pattern" : "^(did:sov:)?[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}$" + }, + "my_endpoint" : { + "type" : "string", + "example" : "https://myhost:8021", + "description" : "My URL endpoint", + "pattern" : "^[A-Za-z0-9\\.\\-\\+]+://([A-Za-z0-9][.A-Za-z0-9-_]+[A-Za-z0-9])+(:[1-9][0-9]*)?(/[^?&#]+)?$" + }, + "my_verkey" : { + "type" : "string", + "example" : "H3C2AVvLMv6gmMNam3uVAjZpfkcJCwDwnZn6z3wXmqPV", + "description" : "My verification key", + "pattern" : "^[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{43,44}$" + }, + "record" : { + "$ref" : "#/definitions/ConnRecord" + }, + "their_did" : { + "type" : "string", + "example" : "WgWxqztrNooG92RXvxSTWv", + "description" : "Remote DID", + "pattern" : "^(did:sov:)?[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}$" + }, + "their_verkey" : { + "type" : "string", + "example" : "H3C2AVvLMv6gmMNam3uVAjZpfkcJCwDwnZn6z3wXmqPV", + "description" : "Remote verification key", + "pattern" : "^[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{43,44}$" + } + } + }, + "Constraints" : { + "type" : "object", + "properties" : { + "fields" : { + "type" : "array", + "items" : { + "$ref" : "#/definitions/DIFField" + } + }, + "is_holder" : { + "type" : "array", + "items" : { + "$ref" : "#/definitions/DIFHolder" + } + }, + "limit_disclosure" : { + "type" : "string", + "description" : "LimitDisclosure" + }, + "status_active" : { + "type" : "string", + "enum" : [ "required", "allowed", "disallowed" ] + }, + "status_revoked" : { + "type" : "string", + "enum" : [ "required", "allowed", "disallowed" ] + }, + "status_suspended" : { + "type" : "string", + "enum" : [ "required", "allowed", "disallowed" ] + }, + "subject_is_issuer" : { + "type" : "string", + "description" : "SubjectIsIssuer", + "enum" : [ "required", "preferred" ] + } + } + }, + "CreateInvitationRequest" : { + "type" : "object", + "properties" : { + "mediation_id" : { + "type" : "string", + "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "description" : "Identifier for active mediation record to be used", + "pattern" : "[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-4[a-fA-F0-9]{3}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}" + }, + "metadata" : { + "type" : "object", + "description" : "Optional metadata to attach to the connection created with the invitation", + "properties" : { } + }, + "my_label" : { + "type" : "string", + "example" : "Bob", + "description" : "Optional label for connection invitation" + }, + "recipient_keys" : { + "type" : "array", + "description" : "List of recipient keys", + "items" : { + "type" : "string", + "example" : "H3C2AVvLMv6gmMNam3uVAjZpfkcJCwDwnZn6z3wXmqPV", + "description" : "Recipient public key", + "pattern" : "^[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{43,44}$" + } + }, + "routing_keys" : { + "type" : "array", + "description" : "List of routing keys", + "items" : { + "type" : "string", + "example" : "H3C2AVvLMv6gmMNam3uVAjZpfkcJCwDwnZn6z3wXmqPV", + "description" : "Routing key", + "pattern" : "^[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{43,44}$" + } + }, + "service_endpoint" : { + "type" : "string", + "example" : "http://192.168.56.102:8020", + "description" : "Connection endpoint" + } + } + }, + "CreateWalletRequest" : { + "type" : "object", + "properties" : { + "image_url" : { + "type" : "string", + "example" : "https://aries.ca/images/sample.png", + "description" : "Image url for this wallet. This image url is publicized (self-attested) to other agents as part of forming a connection." + }, + "key_management_mode" : { + "type" : "string", + "example" : "managed", + "description" : "Key management method to use for this wallet.", + "enum" : [ "managed" ] + }, + "label" : { + "type" : "string", + "example" : "Alice", + "description" : "Label for this wallet. This label is publicized (self-attested) to other agents as part of forming a connection." + }, + "wallet_dispatch_type" : { + "type" : "string", + "example" : "default", + "description" : "Webhook target dispatch type for this wallet. default - Dispatch only to webhooks associated with this wallet. base - Dispatch only to webhooks associated with the base wallet. both - Dispatch to both webhook targets.", + "enum" : [ "default", "both", "base" ] + }, + "wallet_key" : { + "type" : "string", + "example" : "MySecretKey123", + "description" : "Master key used for key derivation." + }, + "wallet_key_derivation" : { + "type" : "string", + "example" : "RAW", + "description" : "Key derivation", + "enum" : [ "ARGON2I_MOD", "ARGON2I_INT", "RAW" ] + }, + "wallet_name" : { + "type" : "string", + "example" : "MyNewWallet", + "description" : "Wallet name" + }, + "wallet_type" : { + "type" : "string", + "example" : "indy", + "description" : "Type of the wallet to create", + "enum" : [ "askar", "in_memory", "indy" ] + }, + "wallet_webhook_urls" : { + "type" : "array", + "description" : "List of Webhook URLs associated with this subwallet", + "items" : { + "type" : "string", + "example" : "http://localhost:8022/webhooks", + "description" : "Optional webhook URL to receive webhook messages" + } + } + } + }, + "CreateWalletResponse" : { + "type" : "object", + "required" : [ "key_management_mode", "wallet_id" ], + "properties" : { + "created_at" : { + "type" : "string", + "example" : "2021-12-31T23:59:59Z", + "description" : "Time of record creation", + "pattern" : "^\\d{4}-\\d\\d-\\d\\d[T ]\\d\\d:\\d\\d(?:\\:(?:\\d\\d(?:\\.\\d{1,6})?))?(?:[+-]\\d\\d:?\\d\\d|Z|)$" + }, + "key_management_mode" : { + "type" : "string", + "description" : "Mode regarding management of wallet key", + "enum" : [ "managed", "unmanaged" ] + }, + "settings" : { + "type" : "object", + "description" : "Settings for this wallet.", + "properties" : { } + }, + "state" : { + "type" : "string", + "example" : "active", + "description" : "Current record state" + }, + "token" : { + "type" : "string", + "example" : "eyJhbGciOiJFZERTQSJ9.eyJhIjogIjAifQ.dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk", + "description" : "Authorization token to authenticate wallet requests" + }, + "updated_at" : { + "type" : "string", + "example" : "2021-12-31T23:59:59Z", + "description" : "Time of last record update", + "pattern" : "^\\d{4}-\\d\\d-\\d\\d[T ]\\d\\d:\\d\\d(?:\\:(?:\\d\\d(?:\\.\\d{1,6})?))?(?:[+-]\\d\\d:?\\d\\d|Z|)$" + }, + "wallet_id" : { + "type" : "string", + "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "description" : "Wallet record ID" + } + } + }, + "CreateWalletTokenRequest" : { + "type" : "object", + "properties" : { + "wallet_key" : { + "type" : "string", + "example" : "MySecretKey123", + "description" : "Master key used for key derivation. Only required for unamanged wallets." + } + } + }, + "CreateWalletTokenResponse" : { + "type" : "object", + "properties" : { + "token" : { + "type" : "string", + "example" : "eyJhbGciOiJFZERTQSJ9.eyJhIjogIjAifQ.dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk", + "description" : "Authorization token to authenticate wallet requests" + } + } + }, + "CredAttrSpec" : { + "type" : "object", + "required" : [ "name", "value" ], + "properties" : { + "mime-type" : { + "type" : "string", + "example" : "image/jpeg", + "description" : "MIME type: omit for (null) default", + "x-nullable" : true + }, + "name" : { + "type" : "string", + "example" : "favourite_drink", + "description" : "Attribute name" + }, + "value" : { + "type" : "string", + "example" : "martini", + "description" : "Attribute value: base64-encode if MIME type is present" + } + } + }, + "CredDefValue" : { + "type" : "object", + "properties" : { + "primary" : { + "$ref" : "#/definitions/CredDefValue_primary" + }, + "revocation" : { + "$ref" : "#/definitions/CredDefValue_revocation" + } + } + }, + "CredDefValuePrimary" : { + "type" : "object", + "properties" : { + "n" : { + "type" : "string", + "example" : "0", + "pattern" : "^[0-9]*$" + }, + "r" : { + "$ref" : "#/definitions/Generated" + }, + "rctxt" : { + "type" : "string", + "example" : "0", + "pattern" : "^[0-9]*$" + }, + "s" : { + "type" : "string", + "example" : "0", + "pattern" : "^[0-9]*$" + }, + "z" : { + "type" : "string", + "example" : "0", + "pattern" : "^[0-9]*$" + } + } + }, + "CredDefValueRevocation" : { + "type" : "object", + "properties" : { + "g" : { + "type" : "string", + "example" : "1 1F14F&ECB578F 2 095E45DDF417D" + }, + "g_dash" : { + "type" : "string", + "example" : "1 1D64716fCDC00C 1 0C781960FA66E3D3 2 095E45DDF417D" + }, + "h" : { + "type" : "string", + "example" : "1 16675DAE54BFAE8 2 095E45DD417D" + }, + "h0" : { + "type" : "string", + "example" : "1 21E5EF9476EAF18 2 095E45DDF417D" + }, + "h1" : { + "type" : "string", + "example" : "1 236D1D99236090 2 095E45DDF417D" + }, + "h2" : { + "type" : "string", + "example" : "1 1C3AE8D1F1E277 2 095E45DDF417D" + }, + "h_cap" : { + "type" : "string", + "example" : "1 1B2A32CF3167 1 2490FEBF6EE55 1 0000000000000000" + }, + "htilde" : { + "type" : "string", + "example" : "1 1D8549E8C0F8 2 095E45DDF417D" + }, + "pk" : { + "type" : "string", + "example" : "1 142CD5E5A7DC 1 153885BD903312 2 095E45DDF417D" + }, + "u" : { + "type" : "string", + "example" : "1 0C430AAB2B4710 1 1CB3A0932EE7E 1 0000000000000000" + }, + "y" : { + "type" : "string", + "example" : "1 153558BD903312 2 095E45DDF417D 1 0000000000000000" + } + } + }, + "CredInfoList" : { + "type" : "object", + "properties" : { + "results" : { + "type" : "array", + "items" : { + "$ref" : "#/definitions/IndyCredInfo" + } + } + } + }, + "CredRevIndyRecordsResult" : { + "type" : "object", + "properties" : { + "rev_reg_delta" : { + "type" : "object", + "description" : "Indy revocation registry delta", + "properties" : { } + } + } + }, + "CredRevRecordDetailsResult" : { + "type" : "object", + "properties" : { + "results" : { + "type" : "array", + "items" : { + "$ref" : "#/definitions/IssuerCredRevRecord" + } + } + } + }, + "CredRevRecordResult" : { + "type" : "object", + "properties" : { + "result" : { + "$ref" : "#/definitions/IssuerCredRevRecord" + } + } + }, + "CredRevokedResult" : { + "type" : "object", + "properties" : { + "revoked" : { + "type" : "boolean", + "description" : "Whether credential is revoked on the ledger" + } + } + }, + "Credential" : { + "type" : "object", + "required" : [ "@context", "credentialSubject", "issuanceDate", "issuer", "type" ], + "properties" : { + "@context" : { + "type" : "array", + "example" : [ "https://www.w3.org/2018/credentials/v1", "https://www.w3.org/2018/credentials/examples/v1" ], + "description" : "The JSON-LD context of the credential", + "items" : { } + }, + "credentialSubject" : { + "example" : "" + }, + "expirationDate" : { + "type" : "string", + "example" : "2010-01-01T19:23:24Z", + "description" : "The expiration date", + "pattern" : "^([0-9]{4})-([0-9]{2})-([0-9]{2})([Tt ]([0-9]{2}):([0-9]{2}):([0-9]{2})(\\.[0-9]+)?)?(([Zz]|([+-])([0-9]{2}):([0-9]{2})))?$" + }, + "id" : { + "type" : "string", + "example" : "http://example.edu/credentials/1872", + "pattern" : "\\w+:(\\/?\\/?)[^\\s]+" + }, + "issuanceDate" : { + "type" : "string", + "example" : "2010-01-01T19:23:24Z", + "description" : "The issuance date", + "pattern" : "^([0-9]{4})-([0-9]{2})-([0-9]{2})([Tt ]([0-9]{2}):([0-9]{2}):([0-9]{2})(\\.[0-9]+)?)?(([Zz]|([+-])([0-9]{2}):([0-9]{2})))?$" + }, + "issuer" : { + "example" : "did:key:z6MkpTHR8VNsBxYAAWHut2Geadd9jSwuBV8xRoAnwWsdvktH", + "description" : "The JSON-LD Verifiable Credential Issuer. Either string of object with id field." + }, + "proof" : { + "$ref" : "#/definitions/Credential_proof" + }, + "type" : { + "type" : "array", + "example" : [ "VerifiableCredential", "AlumniCredential" ], + "description" : "The JSON-LD type of the credential", + "items" : { + "type" : "string" + } + } + } + }, + "CredentialDefinition" : { + "type" : "object", + "properties" : { + "id" : { + "type" : "string", + "example" : "WgWxqztrNooG92RXvxSTWv:3:CL:20:tag", + "description" : "Credential definition identifier", + "pattern" : "^([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}):3:CL:(([1-9][0-9]*)|([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}:2:.+:[0-9.]+)):(.+)?$" + }, + "schemaId" : { + "type" : "string", + "example" : "20", + "description" : "Schema identifier within credential definition identifier" + }, + "tag" : { + "type" : "string", + "example" : "tag", + "description" : "Tag within credential definition identifier" + }, + "type" : { + "example" : "CL", + "description" : "Signature type: CL for Camenisch-Lysyanskaya" + }, + "value" : { + "$ref" : "#/definitions/CredentialDefinition_value" + }, + "ver" : { + "type" : "string", + "example" : "1.0", + "description" : "Node protocol version", + "pattern" : "^[0-9.]+$" + } + } + }, + "CredentialDefinitionGetResult" : { + "type" : "object", + "properties" : { + "credential_definition" : { + "$ref" : "#/definitions/CredentialDefinition" + } + } + }, + "CredentialDefinitionSendRequest" : { + "type" : "object", + "properties" : { + "revocation_registry_size" : { + "type" : "integer", + "format" : "int32", + "example" : 1000, + "description" : "Revocation registry size", + "minimum" : 4, + "maximum" : 32768 + }, + "schema_id" : { + "type" : "string", + "example" : "WgWxqztrNooG92RXvxSTWv:2:schema_name:1.0", + "description" : "Schema identifier", + "pattern" : "^[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}:2:.+:[0-9.]+$" + }, + "support_revocation" : { + "type" : "boolean", + "description" : "Revocation supported flag" + }, + "tag" : { + "type" : "string", + "example" : "default", + "description" : "Credential definition identifier tag" + } + } + }, + "CredentialDefinitionSendResult" : { + "type" : "object", + "properties" : { + "credential_definition_id" : { + "type" : "string", + "example" : "WgWxqztrNooG92RXvxSTWv:3:CL:20:tag", + "description" : "Credential definition identifier", + "pattern" : "^([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}):3:CL:(([1-9][0-9]*)|([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}:2:.+:[0-9.]+)):(.+)?$" + } + } + }, + "CredentialDefinitionsCreatedResult" : { + "type" : "object", + "properties" : { + "credential_definition_ids" : { + "type" : "array", + "items" : { + "type" : "string", + "example" : "WgWxqztrNooG92RXvxSTWv:3:CL:20:tag", + "description" : "Credential definition identifiers", + "pattern" : "^([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}):3:CL:(([1-9][0-9]*)|([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}:2:.+:[0-9.]+)):(.+)?$" + } + } + } + }, + "CredentialOffer" : { + "type" : "object", + "required" : [ "offers~attach" ], + "properties" : { + "@id" : { + "type" : "string", + "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "description" : "Message identifier" + }, + "@type" : { + "type" : "string", + "example" : "https://didcomm.org/my-family/1.0/my-message-type", + "description" : "Message type", + "readOnly" : true + }, + "comment" : { + "type" : "string", + "description" : "Human-readable comment", + "x-nullable" : true + }, + "credential_preview" : { + "$ref" : "#/definitions/CredentialPreview" + }, + "offers~attach" : { + "type" : "array", + "items" : { + "$ref" : "#/definitions/AttachDecorator" + } + } + } + }, + "CredentialPreview" : { + "type" : "object", + "required" : [ "attributes" ], + "properties" : { + "@type" : { + "type" : "string", + "example" : "issue-credential/1.0/credential-preview", + "description" : "Message type identifier" + }, + "attributes" : { + "type" : "array", + "items" : { + "$ref" : "#/definitions/CredAttrSpec" + } + } + } + }, + "CredentialProposal" : { + "type" : "object", + "properties" : { + "@id" : { + "type" : "string", + "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "description" : "Message identifier" + }, + "@type" : { + "type" : "string", + "example" : "https://didcomm.org/my-family/1.0/my-message-type", + "description" : "Message type", + "readOnly" : true + }, + "comment" : { + "type" : "string", + "description" : "Human-readable comment", + "x-nullable" : true + }, + "cred_def_id" : { + "type" : "string", + "example" : "WgWxqztrNooG92RXvxSTWv:3:CL:20:tag", + "pattern" : "^([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}):3:CL:(([1-9][0-9]*)|([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}:2:.+:[0-9.]+)):(.+)?$" + }, + "credential_proposal" : { + "$ref" : "#/definitions/CredentialPreview" + }, + "issuer_did" : { + "type" : "string", + "example" : "WgWxqztrNooG92RXvxSTWv", + "pattern" : "^(did:sov:)?[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}$" + }, + "schema_id" : { + "type" : "string", + "example" : "WgWxqztrNooG92RXvxSTWv:2:schema_name:1.0", + "pattern" : "^[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}:2:.+:[0-9.]+$" + }, + "schema_issuer_did" : { + "type" : "string", + "example" : "WgWxqztrNooG92RXvxSTWv", + "pattern" : "^(did:sov:)?[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}$" + }, + "schema_name" : { + "type" : "string" + }, + "schema_version" : { + "type" : "string", + "example" : "1.0", + "pattern" : "^[0-9.]+$" + } + } + }, + "CredentialStatusOptions" : { + "type" : "object", + "required" : [ "type" ], + "properties" : { + "type" : { + "type" : "string", + "example" : "CredentialStatusList2017", + "description" : "Credential status method type to use for the credential. Should match status method registered in the Verifiable Credential Extension Registry" + } + } + }, + "DID" : { + "type" : "object", + "properties" : { + "did" : { + "type" : "string", + "example" : "did:peer:WgWxqztrNooG92RXvxSTWv", + "description" : "DID of interest", + "pattern" : "^(did:sov:)?[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}$|^did:([a-zA-Z0-9_]+):([a-zA-Z0-9_.%-]+(:[a-zA-Z0-9_.%-]+)*)((;[a-zA-Z0-9_.:%-]+=[a-zA-Z0-9_.:%-]*)*)(\\/[^#?]*)?([?][^#]*)?(\\#.*)?$$" + }, + "key_type" : { + "type" : "string", + "example" : "ed25519", + "description" : "Key type associated with the DID", + "enum" : [ "ed25519", "bls12381g2" ] + }, + "method" : { + "type" : "string", + "example" : "sov", + "description" : "Did method associated with the DID" + }, + "posture" : { + "type" : "string", + "example" : "wallet_only", + "description" : "Whether DID is current public DID, posted to ledger but not current public DID, or local to the wallet", + "enum" : [ "public", "posted", "wallet_only" ] + }, + "verkey" : { + "type" : "string", + "example" : "H3C2AVvLMv6gmMNam3uVAjZpfkcJCwDwnZn6z3wXmqPV", + "description" : "Public verification key", + "pattern" : "^[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{43,44}$" + } + } + }, + "DIDCreate" : { + "type" : "object", + "properties" : { + "method" : { + "type" : "string", + "example" : "sov", + "description" : "Method for the requested DID.Supported methods are 'key', 'sov', and any other registered method." + }, + "options" : { + "$ref" : "#/definitions/DIDCreate_options" + }, + "seed" : { + "type" : "string", + "example" : "000000000000000000000000Trustee1", + "description" : "Optional seed to use for DID, Must beenabled in configuration before use." + } + } + }, + "DIDCreateOptions" : { + "type" : "object", + "required" : [ "key_type" ], + "properties" : { + "did" : { + "type" : "string", + "example" : "did:peer:WgWxqztrNooG92RXvxSTWv", + "description" : "Specify final value of the did (including did:: prefix)if the method supports or requires so.", + "pattern" : "^(did:sov:)?[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}$|^did:([a-zA-Z0-9_]+):([a-zA-Z0-9_.%-]+(:[a-zA-Z0-9_.%-]+)*)((;[a-zA-Z0-9_.:%-]+=[a-zA-Z0-9_.:%-]*)*)(\\/[^#?]*)?([?][^#]*)?(\\#.*)?$$" + }, + "key_type" : { + "type" : "string", + "example" : "ed25519", + "description" : "Key type to use for the DID keypair. Validated with the chosen DID method's supported key types.", + "enum" : [ "ed25519", "bls12381g2" ] + } + } + }, + "DIDEndpoint" : { + "type" : "object", + "required" : [ "did" ], + "properties" : { + "did" : { + "type" : "string", + "example" : "WgWxqztrNooG92RXvxSTWv", + "description" : "DID of interest", + "pattern" : "^(did:sov:)?[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}$" + }, + "endpoint" : { + "type" : "string", + "example" : "https://myhost:8021", + "description" : "Endpoint to set (omit to delete)", + "pattern" : "^[A-Za-z0-9\\.\\-\\+]+://([A-Za-z0-9][.A-Za-z0-9-_]+[A-Za-z0-9])+(:[1-9][0-9]*)?(/[^?&#]+)?$" + } + } + }, + "DIDEndpointWithType" : { + "type" : "object", + "required" : [ "did" ], + "properties" : { + "did" : { + "type" : "string", + "example" : "WgWxqztrNooG92RXvxSTWv", + "description" : "DID of interest", + "pattern" : "^(did:sov:)?[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}$" + }, + "endpoint" : { + "type" : "string", + "example" : "https://myhost:8021", + "description" : "Endpoint to set (omit to delete)", + "pattern" : "^[A-Za-z0-9\\.\\-\\+]+://([A-Za-z0-9][.A-Za-z0-9-_]+[A-Za-z0-9])+(:[1-9][0-9]*)?(/[^?&#]+)?$" + }, + "endpoint_type" : { + "type" : "string", + "example" : "Endpoint", + "description" : "Endpoint type to set (default 'Endpoint'); affects only public or posted DIDs", + "enum" : [ "Endpoint", "Profile", "LinkedDomains" ] + } + } + }, + "DIDList" : { + "type" : "object", + "properties" : { + "results" : { + "type" : "array", + "description" : "DID list", + "items" : { + "$ref" : "#/definitions/DID" + } + } + } + }, + "DIDResult" : { + "type" : "object", + "properties" : { + "result" : { + "$ref" : "#/definitions/DID" + } + } + }, + "DIDXRequest" : { + "type" : "object", + "required" : [ "label" ], + "properties" : { + "@id" : { + "type" : "string", + "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "description" : "Message identifier" + }, + "@type" : { + "type" : "string", + "example" : "https://didcomm.org/my-family/1.0/my-message-type", + "description" : "Message type", + "readOnly" : true + }, + "did" : { + "type" : "string", + "example" : "WgWxqztrNooG92RXvxSTWv", + "description" : "DID of exchange", + "pattern" : "^(did:sov:)?[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}$" + }, + "did_doc~attach" : { + "$ref" : "#/definitions/DIDXRequest_did_docattach" + }, + "label" : { + "type" : "string", + "example" : "Request to connect with Bob", + "description" : "Label for DID exchange request" + } + } + }, + "DIFField" : { + "type" : "object", + "properties" : { + "filter" : { + "$ref" : "#/definitions/Filter" + }, + "id" : { + "type" : "string", + "description" : "ID" + }, + "path" : { + "type" : "array", + "items" : { + "type" : "string", + "description" : "Path" + } + }, + "predicate" : { + "type" : "string", + "description" : "Preference", + "enum" : [ "required", "preferred" ] + }, + "purpose" : { + "type" : "string", + "description" : "Purpose" + } + } + }, + "DIFHolder" : { + "type" : "object", + "properties" : { + "directive" : { + "type" : "string", + "description" : "Preference", + "enum" : [ "required", "preferred" ] + }, + "field_id" : { + "type" : "array", + "items" : { + "type" : "string", + "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "description" : "FieldID", + "pattern" : "[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-4[a-fA-F0-9]{3}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}" + } + } + } + }, + "DIFOptions" : { + "type" : "object", + "properties" : { + "challenge" : { + "type" : "string", + "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "description" : "Challenge protect against replay attack", + "pattern" : "[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-4[a-fA-F0-9]{3}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}" + }, + "domain" : { + "type" : "string", + "example" : "4jt78h47fh47", + "description" : "Domain protect against replay attack" + } + } + }, + "DIFPresSpec" : { + "type" : "object", + "properties" : { + "issuer_id" : { + "type" : "string", + "description" : "Issuer identifier to sign the presentation, if different from current public DID" + }, + "presentation_definition" : { + "$ref" : "#/definitions/PresentationDefinition" + }, + "record_ids" : { + "type" : "object", + "example" : { + "" : [ "", "" ], + "" : [ "" ] + }, + "description" : "Mapping of input_descriptor id to list of stored W3C credential record_id", + "properties" : { } + }, + "reveal_doc" : { + "type" : "object", + "example" : { + "@context" : [ "https://www.w3.org/2018/credentials/v1", "https://w3id.org/security/bbs/v1" ], + "@explicit" : true, + "@requireAll" : true, + "credentialSubject" : { + "@explicit" : true, + "@requireAll" : true, + "Observation" : [ { + "effectiveDateTime" : { }, + "@explicit" : true, + "@requireAll" : true + } ] + }, + "issuanceDate" : { }, + "issuer" : { }, + "type" : [ "VerifiableCredential", "LabReport" ] + }, + "description" : "reveal doc [JSON-LD frame] dict used to derive the credential when selective disclosure is required", + "properties" : { } + } + } + }, + "DIFProofProposal" : { + "type" : "object", + "properties" : { + "input_descriptors" : { + "type" : "array", + "items" : { + "$ref" : "#/definitions/InputDescriptors" + } + }, + "options" : { + "$ref" : "#/definitions/DIFOptions" + } + } + }, + "DIFProofRequest" : { + "type" : "object", + "required" : [ "presentation_definition" ], + "properties" : { + "options" : { + "$ref" : "#/definitions/DIFOptions" + }, + "presentation_definition" : { + "$ref" : "#/definitions/PresentationDefinition" + } + } + }, + "Date" : { + "type" : "object", + "required" : [ "expires_time" ], + "properties" : { + "expires_time" : { + "type" : "string", + "format" : "date-time", + "example" : "2021-03-29T05:22:19Z", + "description" : "Expiry Date" + } + } + }, + "Disclose" : { + "type" : "object", + "required" : [ "protocols" ], + "properties" : { + "@id" : { + "type" : "string", + "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "description" : "Message identifier" + }, + "@type" : { + "type" : "string", + "example" : "https://didcomm.org/my-family/1.0/my-message-type", + "description" : "Message type", + "readOnly" : true + }, + "protocols" : { + "type" : "array", + "description" : "List of protocol descriptors", + "items" : { + "$ref" : "#/definitions/ProtocolDescriptor" + } + } + } + }, + "Disclosures" : { + "type" : "object", + "required" : [ "disclosures" ], + "properties" : { + "@id" : { + "type" : "string", + "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "description" : "Message identifier" + }, + "@type" : { + "type" : "string", + "example" : "https://didcomm.org/my-family/1.0/my-message-type", + "description" : "Message type", + "readOnly" : true + }, + "disclosures" : { + "type" : "array", + "description" : "List of protocol or goal_code descriptors", + "items" : { } + } + } + }, + "Doc" : { + "type" : "object", + "required" : [ "credential", "options" ], + "properties" : { + "credential" : { + "type" : "object", + "description" : "Credential to sign", + "properties" : { } + }, + "options" : { + "$ref" : "#/definitions/Doc_options" + } + } + }, + "EndorserInfo" : { + "type" : "object", + "required" : [ "endorser_did" ], + "properties" : { + "endorser_did" : { + "type" : "string", + "description" : "Endorser DID" + }, + "endorser_name" : { + "type" : "string", + "description" : "Endorser Name" + } + } + }, + "EndpointsResult" : { + "type" : "object", + "properties" : { + "my_endpoint" : { + "type" : "string", + "example" : "https://myhost:8021", + "description" : "My endpoint", + "pattern" : "^[A-Za-z0-9\\.\\-\\+]+://([A-Za-z0-9][.A-Za-z0-9-_]+[A-Za-z0-9])+(:[1-9][0-9]*)?(/[^?&#]+)?$" + }, + "their_endpoint" : { + "type" : "string", + "example" : "https://myhost:8021", + "description" : "Their endpoint", + "pattern" : "^[A-Za-z0-9\\.\\-\\+]+://([A-Za-z0-9][.A-Za-z0-9-_]+[A-Za-z0-9])+(:[1-9][0-9]*)?(/[^?&#]+)?$" + } + } + }, + "Filter" : { + "type" : "object", + "properties" : { + "const" : { + "description" : "Const" + }, + "enum" : { + "type" : "array", + "items" : { + "description" : "Enum" + } + }, + "exclusiveMaximum" : { + "description" : "ExclusiveMaximum" + }, + "exclusiveMinimum" : { + "description" : "ExclusiveMinimum" + }, + "format" : { + "type" : "string", + "description" : "Format" + }, + "maxLength" : { + "type" : "integer", + "format" : "int32", + "example" : 1234, + "description" : "Max Length" + }, + "maximum" : { + "description" : "Maximum" + }, + "minLength" : { + "type" : "integer", + "format" : "int32", + "example" : 1234, + "description" : "Min Length" + }, + "minimum" : { + "description" : "Minimum" + }, + "not" : { + "type" : "boolean", + "example" : false, + "description" : "Not" + }, + "pattern" : { + "type" : "string", + "description" : "Pattern" + }, + "type" : { + "type" : "string", + "description" : "Type" + } + } + }, + "Generated" : { + "type" : "object", + "properties" : { + "master_secret" : { + "type" : "string", + "example" : "0", + "pattern" : "^[0-9]*$" + }, + "number" : { + "type" : "string", + "example" : "0", + "pattern" : "^[0-9]*$" + }, + "remainder" : { + "type" : "string", + "example" : "0", + "pattern" : "^[0-9]*$" + } + } + }, + "GetDIDEndpointResponse" : { + "type" : "object", + "properties" : { + "endpoint" : { + "type" : "string", + "example" : "https://myhost:8021", + "description" : "Full verification key", + "pattern" : "^[A-Za-z0-9\\.\\-\\+]+://([A-Za-z0-9][.A-Za-z0-9-_]+[A-Za-z0-9])+(:[1-9][0-9]*)?(/[^?&#]+)?$", + "x-nullable" : true + } + } + }, + "GetDIDVerkeyResponse" : { + "type" : "object", + "properties" : { + "verkey" : { + "type" : "string", + "example" : "H3C2AVvLMv6gmMNam3uVAjZpfkcJCwDwnZn6z3wXmqPV", + "description" : "Full verification key", + "pattern" : "^[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{43,44}$", + "x-nullable" : true + } + } + }, + "GetNymRoleResponse" : { + "type" : "object", + "properties" : { + "role" : { + "type" : "string", + "example" : "ENDORSER", + "description" : "Ledger role", + "enum" : [ "STEWARD", "TRUSTEE", "ENDORSER", "NETWORK_MONITOR", "USER", "ROLE_REMOVE" ] + } + } + }, + "HolderModuleResponse" : { + "type" : "object" + }, + "IndyAttrValue" : { + "type" : "object", + "required" : [ "encoded", "raw" ], + "properties" : { + "encoded" : { + "type" : "string", + "example" : "-1", + "description" : "Attribute encoded value", + "pattern" : "^-?[0-9]*$" + }, + "raw" : { + "type" : "string", + "description" : "Attribute raw value" + } + } + }, + "IndyCredAbstract" : { + "type" : "object", + "required" : [ "cred_def_id", "key_correctness_proof", "nonce", "schema_id" ], + "properties" : { + "cred_def_id" : { + "type" : "string", + "example" : "WgWxqztrNooG92RXvxSTWv:3:CL:20:tag", + "description" : "Credential definition identifier", + "pattern" : "^([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}):3:CL:(([1-9][0-9]*)|([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}:2:.+:[0-9.]+)):(.+)?$" + }, + "key_correctness_proof" : { + "$ref" : "#/definitions/IndyCredAbstract_key_correctness_proof" + }, + "nonce" : { + "type" : "string", + "example" : "0", + "description" : "Nonce in credential abstract", + "pattern" : "^[0-9]*$" + }, + "schema_id" : { + "type" : "string", + "example" : "WgWxqztrNooG92RXvxSTWv:2:schema_name:1.0", + "description" : "Schema identifier", + "pattern" : "^[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}:2:.+:[0-9.]+$" + } + } + }, + "IndyCredInfo" : { + "type" : "object", + "properties" : { + "attrs" : { + "type" : "object", + "description" : "Attribute names and value", + "additionalProperties" : { + "type" : "string", + "example" : "alice" + } + }, + "cred_def_id" : { + "type" : "string", + "example" : "WgWxqztrNooG92RXvxSTWv:3:CL:20:tag", + "description" : "Credential definition identifier", + "pattern" : "^([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}):3:CL:(([1-9][0-9]*)|([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}:2:.+:[0-9.]+)):(.+)?$" + }, + "cred_rev_id" : { + "type" : "string", + "example" : "12345", + "description" : "Credential revocation identifier", + "pattern" : "^[1-9][0-9]*$", + "x-nullable" : true + }, + "referent" : { + "type" : "string", + "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "description" : "Wallet referent" + }, + "rev_reg_id" : { + "type" : "string", + "example" : "WgWxqztrNooG92RXvxSTWv:4:WgWxqztrNooG92RXvxSTWv:3:CL:20:tag:CL_ACCUM:0", + "description" : "Revocation registry identifier", + "pattern" : "^([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}):4:([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}):3:CL:(([1-9][0-9]*)|([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}:2:.+:[0-9.]+))(:.+)?:CL_ACCUM:(.+$)", + "x-nullable" : true + }, + "schema_id" : { + "type" : "string", + "example" : "WgWxqztrNooG92RXvxSTWv:2:schema_name:1.0", + "description" : "Schema identifier", + "pattern" : "^[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}:2:.+:[0-9.]+$" + } + } + }, + "IndyCredPrecis" : { + "type" : "object", + "properties" : { + "cred_info" : { + "$ref" : "#/definitions/IndyCredPrecis_cred_info" + }, + "interval" : { + "$ref" : "#/definitions/IndyCredPrecis_interval" + }, + "presentation_referents" : { + "type" : "array", + "items" : { + "type" : "string", + "example" : "1_age_uuid", + "description" : "presentation referent" + } + } + } + }, + "IndyCredRequest" : { + "type" : "object", + "required" : [ "blinded_ms", "blinded_ms_correctness_proof", "cred_def_id", "nonce", "prover_did" ], + "properties" : { + "blinded_ms" : { + "type" : "object", + "description" : "Blinded master secret", + "properties" : { } + }, + "blinded_ms_correctness_proof" : { + "type" : "object", + "description" : "Blinded master secret correctness proof", + "properties" : { } + }, + "cred_def_id" : { + "type" : "string", + "example" : "WgWxqztrNooG92RXvxSTWv:3:CL:20:tag", + "description" : "Credential definition identifier", + "pattern" : "^([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}):3:CL:(([1-9][0-9]*)|([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}:2:.+:[0-9.]+)):(.+)?$" + }, + "nonce" : { + "type" : "string", + "example" : "0", + "description" : "Nonce in credential request", + "pattern" : "^[0-9]*$" + }, + "prover_did" : { + "type" : "string", + "example" : "WgWxqztrNooG92RXvxSTWv", + "description" : "Prover DID", + "pattern" : "^(did:sov:)?[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}$" + } + } + }, + "IndyCredential" : { + "type" : "object", + "required" : [ "cred_def_id", "schema_id", "signature", "signature_correctness_proof", "values" ], + "properties" : { + "cred_def_id" : { + "type" : "string", + "example" : "WgWxqztrNooG92RXvxSTWv:3:CL:20:tag", + "description" : "Credential definition identifier", + "pattern" : "^([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}):3:CL:(([1-9][0-9]*)|([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}:2:.+:[0-9.]+)):(.+)?$" + }, + "rev_reg" : { + "type" : "object", + "description" : "Revocation registry state", + "properties" : { }, + "x-nullable" : true + }, + "rev_reg_id" : { + "type" : "string", + "example" : "WgWxqztrNooG92RXvxSTWv:4:WgWxqztrNooG92RXvxSTWv:3:CL:20:tag:CL_ACCUM:0", + "description" : "Revocation registry identifier", + "pattern" : "^([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}):4:([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}):3:CL:(([1-9][0-9]*)|([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}:2:.+:[0-9.]+))(:.+)?:CL_ACCUM:(.+$)", + "x-nullable" : true + }, + "schema_id" : { + "type" : "string", + "example" : "WgWxqztrNooG92RXvxSTWv:2:schema_name:1.0", + "description" : "Schema identifier", + "pattern" : "^[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}:2:.+:[0-9.]+$" + }, + "signature" : { + "type" : "object", + "description" : "Credential signature", + "properties" : { } + }, + "signature_correctness_proof" : { + "type" : "object", + "description" : "Credential signature correctness proof", + "properties" : { } + }, + "values" : { + "type" : "object", + "description" : "Credential attributes", + "additionalProperties" : { + "type" : "object", + "description" : "Attribute value", + "allOf" : [ { + "$ref" : "#/definitions/IndyAttrValue" + } ] + } + }, + "witness" : { + "type" : "object", + "description" : "Witness for revocation proof", + "properties" : { }, + "x-nullable" : true + } + } + }, + "IndyEQProof" : { + "type" : "object", + "properties" : { + "a_prime" : { + "type" : "string", + "example" : "0", + "pattern" : "^[0-9]*$" + }, + "e" : { + "type" : "string", + "example" : "0", + "pattern" : "^[0-9]*$" + }, + "m" : { + "type" : "object", + "additionalProperties" : { + "type" : "string", + "example" : "0", + "pattern" : "^[0-9]*$" + } + }, + "m2" : { + "type" : "string", + "example" : "0", + "pattern" : "^[0-9]*$" + }, + "revealed_attrs" : { + "type" : "object", + "additionalProperties" : { + "type" : "string", + "example" : "-1", + "pattern" : "^-?[0-9]*$" + } + }, + "v" : { + "type" : "string", + "example" : "0", + "pattern" : "^[0-9]*$" + } + } + }, + "IndyGEProof" : { + "type" : "object", + "properties" : { + "alpha" : { + "type" : "string", + "example" : "0", + "pattern" : "^[0-9]*$" + }, + "mj" : { + "type" : "string", + "example" : "0", + "pattern" : "^[0-9]*$" + }, + "predicate" : { + "$ref" : "#/definitions/IndyGEProofPred" + }, + "r" : { + "type" : "object", + "additionalProperties" : { + "type" : "string", + "example" : "0", + "pattern" : "^[0-9]*$" + } + }, + "t" : { + "type" : "object", + "additionalProperties" : { + "type" : "string", + "example" : "0", + "pattern" : "^[0-9]*$" + } + }, + "u" : { + "type" : "object", + "additionalProperties" : { + "type" : "string", + "example" : "0", + "pattern" : "^[0-9]*$" + } + } + } + }, + "IndyGEProofPred" : { + "type" : "object", + "properties" : { + "attr_name" : { + "type" : "string", + "description" : "Attribute name, indy-canonicalized" + }, + "p_type" : { + "type" : "string", + "description" : "Predicate type", + "enum" : [ "LT", "LE", "GE", "GT" ] + }, + "value" : { + "type" : "integer", + "format" : "int32", + "description" : "Predicate threshold value" + } + } + }, + "IndyKeyCorrectnessProof" : { + "type" : "object", + "required" : [ "c", "xr_cap", "xz_cap" ], + "properties" : { + "c" : { + "type" : "string", + "example" : "0", + "description" : "c in key correctness proof", + "pattern" : "^[0-9]*$" + }, + "xr_cap" : { + "type" : "array", + "description" : "xr_cap in key correctness proof", + "items" : { + "type" : "array", + "description" : "xr_cap components in key correctness proof", + "items" : { + "type" : "string", + "description" : "xr_cap component values in key correctness proof" + } + } + }, + "xz_cap" : { + "type" : "string", + "example" : "0", + "description" : "xz_cap in key correctness proof", + "pattern" : "^[0-9]*$" + } + } + }, + "IndyNonRevocProof" : { + "type" : "object", + "properties" : { + "c_list" : { + "type" : "object", + "additionalProperties" : { + "type" : "string" + } + }, + "x_list" : { + "type" : "object", + "additionalProperties" : { + "type" : "string" + } + } + } + }, + "IndyNonRevocationInterval" : { + "type" : "object", + "properties" : { + "from" : { + "type" : "integer", + "format" : "int32", + "example" : 1640995199, + "description" : "Earliest time of interest in non-revocation interval", + "minimum" : 0, + "maximum" : 18446744073709551615 + }, + "to" : { + "type" : "integer", + "format" : "int32", + "example" : 1640995199, + "description" : "Latest time of interest in non-revocation interval", + "minimum" : 0, + "maximum" : 18446744073709551615 + } + } + }, + "IndyPresAttrSpec" : { + "type" : "object", + "required" : [ "name" ], + "properties" : { + "cred_def_id" : { + "type" : "string", + "example" : "WgWxqztrNooG92RXvxSTWv:3:CL:20:tag", + "pattern" : "^([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}):3:CL:(([1-9][0-9]*)|([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}:2:.+:[0-9.]+)):(.+)?$" + }, + "mime-type" : { + "type" : "string", + "example" : "image/jpeg", + "description" : "MIME type (default null)" + }, + "name" : { + "type" : "string", + "example" : "favourite_drink", + "description" : "Attribute name" + }, + "referent" : { + "type" : "string", + "example" : "0", + "description" : "Credential referent" + }, + "value" : { + "type" : "string", + "example" : "martini", + "description" : "Attribute value" + } + } + }, + "IndyPresPredSpec" : { + "type" : "object", + "required" : [ "name", "predicate", "threshold" ], + "properties" : { + "cred_def_id" : { + "type" : "string", + "example" : "WgWxqztrNooG92RXvxSTWv:3:CL:20:tag", + "description" : "Credential definition identifier", + "pattern" : "^([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}):3:CL:(([1-9][0-9]*)|([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}:2:.+:[0-9.]+)):(.+)?$" + }, + "name" : { + "type" : "string", + "example" : "high_score", + "description" : "Attribute name" + }, + "predicate" : { + "type" : "string", + "example" : ">=", + "description" : "Predicate type ('<', '<=', '>=', or '>')", + "enum" : [ "<", "<=", ">=", ">" ] + }, + "threshold" : { + "type" : "integer", + "format" : "int32", + "description" : "Threshold value" + } + } + }, + "IndyPresPreview" : { + "type" : "object", + "required" : [ "attributes", "predicates" ], + "properties" : { + "@type" : { + "type" : "string", + "example" : "did:sov:BzCbsNYhMrjHiqZDTUASHg;spec/present-proof/1.0/presentation-preview", + "description" : "Message type identifier" + }, + "attributes" : { + "type" : "array", + "items" : { + "$ref" : "#/definitions/IndyPresAttrSpec" + } + }, + "predicates" : { + "type" : "array", + "items" : { + "$ref" : "#/definitions/IndyPresPredSpec" + } + } + } + }, + "IndyPresSpec" : { + "type" : "object", + "required" : [ "requested_attributes", "requested_predicates", "self_attested_attributes" ], + "properties" : { + "requested_attributes" : { + "type" : "object", + "description" : "Nested object mapping proof request attribute referents to requested-attribute specifiers", + "additionalProperties" : { + "$ref" : "#/definitions/IndyRequestedCredsRequestedAttr" + } + }, + "requested_predicates" : { + "type" : "object", + "description" : "Nested object mapping proof request predicate referents to requested-predicate specifiers", + "additionalProperties" : { + "$ref" : "#/definitions/IndyRequestedCredsRequestedPred" + } + }, + "self_attested_attributes" : { + "type" : "object", + "description" : "Self-attested attributes to build into proof", + "additionalProperties" : { + "type" : "string", + "example" : "self_attested_value", + "description" : "Self-attested attribute values to use in requested-credentials structure for proof construction" + } + }, + "trace" : { + "type" : "boolean", + "example" : false, + "description" : "Whether to trace event (default false)" + } + } + }, + "IndyPrimaryProof" : { + "type" : "object", + "properties" : { + "eq_proof" : { + "$ref" : "#/definitions/IndyPrimaryProof_eq_proof" + }, + "ge_proofs" : { + "type" : "array", + "description" : "Indy GE proofs", + "items" : { + "$ref" : "#/definitions/IndyGEProof" + }, + "x-nullable" : true + } + } + }, + "IndyProof" : { + "type" : "object", + "properties" : { + "identifiers" : { + "type" : "array", + "description" : "Indy proof.identifiers content", + "items" : { + "$ref" : "#/definitions/IndyProofIdentifier" + } + }, + "proof" : { + "$ref" : "#/definitions/IndyProof_proof" + }, + "requested_proof" : { + "$ref" : "#/definitions/IndyProof_requested_proof" + } + } + }, + "IndyProofIdentifier" : { + "type" : "object", + "properties" : { + "cred_def_id" : { + "type" : "string", + "example" : "WgWxqztrNooG92RXvxSTWv:3:CL:20:tag", + "description" : "Credential definition identifier", + "pattern" : "^([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}):3:CL:(([1-9][0-9]*)|([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}:2:.+:[0-9.]+)):(.+)?$" + }, + "rev_reg_id" : { + "type" : "string", + "example" : "WgWxqztrNooG92RXvxSTWv:4:WgWxqztrNooG92RXvxSTWv:3:CL:20:tag:CL_ACCUM:0", + "description" : "Revocation registry identifier", + "pattern" : "^([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}):4:([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}):3:CL:(([1-9][0-9]*)|([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}:2:.+:[0-9.]+))(:.+)?:CL_ACCUM:(.+$)", + "x-nullable" : true + }, + "schema_id" : { + "type" : "string", + "example" : "WgWxqztrNooG92RXvxSTWv:2:schema_name:1.0", + "description" : "Schema identifier", + "pattern" : "^[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}:2:.+:[0-9.]+$" + }, + "timestamp" : { + "type" : "integer", + "format" : "int32", + "example" : 1640995199, + "description" : "Timestamp epoch", + "minimum" : 0, + "maximum" : 18446744073709551615, + "x-nullable" : true + } + } + }, + "IndyProofProof" : { + "type" : "object", + "properties" : { + "aggregated_proof" : { + "$ref" : "#/definitions/IndyProofProof_aggregated_proof" + }, + "proofs" : { + "type" : "array", + "description" : "Indy proof proofs", + "items" : { + "$ref" : "#/definitions/IndyProofProofProofsProof" + } + } + } + }, + "IndyProofProofAggregatedProof" : { + "type" : "object", + "properties" : { + "c_hash" : { + "type" : "string", + "description" : "c_hash value" + }, + "c_list" : { + "type" : "array", + "description" : "c_list value", + "items" : { + "type" : "array", + "items" : { + "type" : "integer", + "format" : "int32" + } + } + } + } + }, + "IndyProofProofProofsProof" : { + "type" : "object", + "properties" : { + "non_revoc_proof" : { + "$ref" : "#/definitions/IndyProofProofProofsProof_non_revoc_proof" + }, + "primary_proof" : { + "$ref" : "#/definitions/IndyProofProofProofsProof_primary_proof" + } + } + }, + "IndyProofReqAttrSpec" : { + "type" : "object", + "properties" : { + "name" : { + "type" : "string", + "example" : "favouriteDrink", + "description" : "Attribute name" + }, + "names" : { + "type" : "array", + "description" : "Attribute name group", + "items" : { + "type" : "string", + "example" : "age" + } + }, + "non_revoked" : { + "$ref" : "#/definitions/IndyProofReqAttrSpec_non_revoked" + }, + "restrictions" : { + "type" : "array", + "description" : "If present, credential must satisfy one of given restrictions: specify schema_id, schema_issuer_did, schema_name, schema_version, issuer_did, cred_def_id, and/or attr::::value where represents a credential attribute name", + "items" : { + "type" : "object", + "additionalProperties" : { + "type" : "string", + "example" : "WgWxqztrNooG92RXvxSTWv:3:CL:20:tag" + } + } + } + } + }, + "IndyProofReqAttrSpecNonRevoked" : { + "type" : "object", + "properties" : { + "from" : { + "type" : "integer", + "format" : "int32", + "example" : 1640995199, + "description" : "Earliest time of interest in non-revocation interval", + "minimum" : 0, + "maximum" : 18446744073709551615 + }, + "to" : { + "type" : "integer", + "format" : "int32", + "example" : 1640995199, + "description" : "Latest time of interest in non-revocation interval", + "minimum" : 0, + "maximum" : 18446744073709551615 + } + } + }, + "IndyProofReqPredSpec" : { + "type" : "object", + "required" : [ "name", "p_type", "p_value" ], + "properties" : { + "name" : { + "type" : "string", + "example" : "index", + "description" : "Attribute name" + }, + "non_revoked" : { + "$ref" : "#/definitions/IndyProofReqAttrSpec_non_revoked" + }, + "p_type" : { + "type" : "string", + "example" : ">=", + "description" : "Predicate type ('<', '<=', '>=', or '>')", + "enum" : [ "<", "<=", ">=", ">" ] + }, + "p_value" : { + "type" : "integer", + "format" : "int32", + "description" : "Threshold value" + }, + "restrictions" : { + "type" : "array", + "description" : "If present, credential must satisfy one of given restrictions: specify schema_id, schema_issuer_did, schema_name, schema_version, issuer_did, cred_def_id, and/or attr::::value where represents a credential attribute name", + "items" : { + "type" : "object", + "additionalProperties" : { + "type" : "string", + "example" : "WgWxqztrNooG92RXvxSTWv:3:CL:20:tag" + } + } + } + } + }, + "IndyProofReqPredSpecNonRevoked" : { + "type" : "object", + "properties" : { + "from" : { + "type" : "integer", + "format" : "int32", + "example" : 1640995199, + "description" : "Earliest time of interest in non-revocation interval", + "minimum" : 0, + "maximum" : 18446744073709551615 + }, + "to" : { + "type" : "integer", + "format" : "int32", + "example" : 1640995199, + "description" : "Latest time of interest in non-revocation interval", + "minimum" : 0, + "maximum" : 18446744073709551615 + } + } + }, + "IndyProofRequest" : { + "type" : "object", + "required" : [ "requested_attributes", "requested_predicates" ], + "properties" : { + "name" : { + "type" : "string", + "example" : "Proof request", + "description" : "Proof request name" + }, + "non_revoked" : { + "$ref" : "#/definitions/IndyProofReqAttrSpec_non_revoked" + }, + "nonce" : { + "type" : "string", + "example" : "1", + "description" : "Nonce", + "pattern" : "^[1-9][0-9]*$" + }, + "requested_attributes" : { + "type" : "object", + "description" : "Requested attribute specifications of proof request", + "additionalProperties" : { + "$ref" : "#/definitions/IndyProofReqAttrSpec" + } + }, + "requested_predicates" : { + "type" : "object", + "description" : "Requested predicate specifications of proof request", + "additionalProperties" : { + "$ref" : "#/definitions/IndyProofReqPredSpec" + } + }, + "version" : { + "type" : "string", + "example" : "1.0", + "description" : "Proof request version", + "pattern" : "^[0-9.]+$" + } + } + }, + "IndyProofRequestNonRevoked" : { + "type" : "object", + "properties" : { + "from" : { + "type" : "integer", + "format" : "int32", + "example" : 1640995199, + "description" : "Earliest time of interest in non-revocation interval", + "minimum" : 0, + "maximum" : 18446744073709551615 + }, + "to" : { + "type" : "integer", + "format" : "int32", + "example" : 1640995199, + "description" : "Latest time of interest in non-revocation interval", + "minimum" : 0, + "maximum" : 18446744073709551615 + } + } + }, + "IndyProofRequestedProof" : { + "type" : "object", + "properties" : { + "predicates" : { + "type" : "object", + "description" : "Proof requested proof predicates.", + "additionalProperties" : { + "$ref" : "#/definitions/IndyProofRequestedProofPredicate" + } + }, + "revealed_attr_groups" : { + "type" : "object", + "description" : "Proof requested proof revealed attribute groups", + "additionalProperties" : { + "$ref" : "#/definitions/IndyProofRequestedProofRevealedAttrGroup" + }, + "x-nullable" : true + }, + "revealed_attrs" : { + "type" : "object", + "description" : "Proof requested proof revealed attributes", + "additionalProperties" : { + "$ref" : "#/definitions/IndyProofRequestedProofRevealedAttr" + }, + "x-nullable" : true + }, + "self_attested_attrs" : { + "type" : "object", + "description" : "Proof requested proof self-attested attributes", + "properties" : { } + }, + "unrevealed_attrs" : { + "type" : "object", + "description" : "Unrevealed attributes", + "properties" : { } + } + } + }, + "IndyProofRequestedProofPredicate" : { + "type" : "object", + "properties" : { + "sub_proof_index" : { + "type" : "integer", + "format" : "int32", + "description" : "Sub-proof index" + } + } + }, + "IndyProofRequestedProofRevealedAttr" : { + "type" : "object", + "properties" : { + "encoded" : { + "type" : "string", + "example" : "-1", + "description" : "Encoded value", + "pattern" : "^-?[0-9]*$" + }, + "raw" : { + "type" : "string", + "description" : "Raw value" + }, + "sub_proof_index" : { + "type" : "integer", + "format" : "int32", + "description" : "Sub-proof index" + } + } + }, + "IndyProofRequestedProofRevealedAttrGroup" : { + "type" : "object", + "properties" : { + "sub_proof_index" : { + "type" : "integer", + "format" : "int32", + "description" : "Sub-proof index" + }, + "values" : { + "type" : "object", + "description" : "Indy proof requested proof revealed attr groups group value", + "additionalProperties" : { + "$ref" : "#/definitions/RawEncoded" + } + } + } + }, + "IndyRequestedCredsRequestedAttr" : { + "type" : "object", + "required" : [ "cred_id" ], + "properties" : { + "cred_id" : { + "type" : "string", + "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "description" : "Wallet credential identifier (typically but not necessarily a UUID)" + }, + "revealed" : { + "type" : "boolean", + "description" : "Whether to reveal attribute in proof (default true)" + } + } + }, + "IndyRequestedCredsRequestedPred" : { + "type" : "object", + "required" : [ "cred_id" ], + "properties" : { + "cred_id" : { + "type" : "string", + "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "description" : "Wallet credential identifier (typically but not necessarily a UUID)" + }, + "timestamp" : { + "type" : "integer", + "format" : "int32", + "example" : 1640995199, + "description" : "Epoch timestamp of interest for non-revocation proof", + "minimum" : 0, + "maximum" : 18446744073709551615 + } + } + }, + "IndyRevRegDef" : { + "type" : "object", + "properties" : { + "credDefId" : { + "type" : "string", + "example" : "WgWxqztrNooG92RXvxSTWv:3:CL:20:tag", + "description" : "Credential definition identifier", + "pattern" : "^([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}):3:CL:(([1-9][0-9]*)|([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}:2:.+:[0-9.]+)):(.+)?$" + }, + "id" : { + "type" : "string", + "example" : "WgWxqztrNooG92RXvxSTWv:4:WgWxqztrNooG92RXvxSTWv:3:CL:20:tag:CL_ACCUM:0", + "description" : "Indy revocation registry identifier", + "pattern" : "^([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}):4:([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}):3:CL:(([1-9][0-9]*)|([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}:2:.+:[0-9.]+))(:.+)?:CL_ACCUM:(.+$)" + }, + "revocDefType" : { + "type" : "string", + "example" : "CL_ACCUM", + "description" : "Revocation registry type (specify CL_ACCUM)", + "enum" : [ "CL_ACCUM" ] + }, + "tag" : { + "type" : "string", + "description" : "Revocation registry tag" + }, + "value" : { + "$ref" : "#/definitions/IndyRevRegDef_value" + }, + "ver" : { + "type" : "string", + "example" : "1.0", + "description" : "Version of revocation registry definition", + "pattern" : "^[0-9.]+$" + } + } + }, + "IndyRevRegDefValue" : { + "type" : "object", + "properties" : { + "issuanceType" : { + "type" : "string", + "description" : "Issuance type", + "enum" : [ "ISSUANCE_ON_DEMAND", "ISSUANCE_BY_DEFAULT" ] + }, + "maxCredNum" : { + "type" : "integer", + "format" : "int32", + "example" : 10, + "description" : "Maximum number of credentials; registry size", + "minimum" : 1 + }, + "publicKeys" : { + "$ref" : "#/definitions/IndyRevRegDefValue_publicKeys" + }, + "tailsHash" : { + "type" : "string", + "example" : "H3C2AVvLMv6gmMNam3uVAjZpfkcJCwDwnZn6z3wXmqPV", + "description" : "Tails hash value", + "pattern" : "^[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{43,44}$" + }, + "tailsLocation" : { + "type" : "string", + "description" : "Tails file location" + } + } + }, + "IndyRevRegDefValuePublicKeys" : { + "type" : "object", + "properties" : { + "accumKey" : { + "$ref" : "#/definitions/IndyRevRegDefValuePublicKeysAccumKey" + } + } + }, + "IndyRevRegDefValuePublicKeysAccumKey" : { + "type" : "object", + "properties" : { + "z" : { + "type" : "string", + "example" : "1 120F522F81E6B7 1 09F7A59005C4939854", + "description" : "Value for z" + } + } + }, + "IndyRevRegEntry" : { + "type" : "object", + "properties" : { + "value" : { + "$ref" : "#/definitions/IndyRevRegEntry_value" + }, + "ver" : { + "type" : "string", + "example" : "1.0", + "description" : "Version of revocation registry entry", + "pattern" : "^[0-9.]+$" + } + } + }, + "IndyRevRegEntryValue" : { + "type" : "object", + "properties" : { + "accum" : { + "type" : "string", + "example" : "21 11792B036AED0AAA12A4 4 298B2571FFC63A737", + "description" : "Accumulator value" + }, + "prevAccum" : { + "type" : "string", + "example" : "21 137AC810975E4 6 76F0384B6F23", + "description" : "Previous accumulator value" + }, + "revoked" : { + "type" : "array", + "description" : "Revoked credential revocation identifiers", + "items" : { + "type" : "integer", + "format" : "int32" + } + } + } + }, + "InputDescriptors" : { + "type" : "object", + "properties" : { + "constraints" : { + "$ref" : "#/definitions/Constraints" + }, + "group" : { + "type" : "array", + "items" : { + "type" : "string", + "description" : "Group" + } + }, + "id" : { + "type" : "string", + "description" : "ID" + }, + "metadata" : { + "type" : "object", + "description" : "Metadata dictionary", + "properties" : { } + }, + "name" : { + "type" : "string", + "description" : "Name" + }, + "purpose" : { + "type" : "string", + "description" : "Purpose" + }, + "schema" : { + "$ref" : "#/definitions/InputDescriptors_schema" + } + } + }, + "IntroModuleResponse" : { + "type" : "object" + }, + "InvitationCreateRequest" : { + "type" : "object", + "properties" : { + "accept" : { + "type" : "array", + "example" : [ "didcomm/aip1", "didcomm/aip2;env=rfc19" ], + "description" : "List of mime type in order of preference that should be use in responding to the message", + "items" : { + "type" : "string" + } + }, + "alias" : { + "type" : "string", + "example" : "Barry", + "description" : "Alias for connection" + }, + "attachments" : { + "type" : "array", + "description" : "Optional invitation attachments", + "items" : { + "$ref" : "#/definitions/AttachmentDef" + } + }, + "handshake_protocols" : { + "type" : "array", + "items" : { + "type" : "string", + "example" : "did:sov:BzCbsNYhMrjHiqZDTUASHg;spec/didexchange/1.0", + "description" : "Handshake protocol to specify in invitation" + } + }, + "mediation_id" : { + "type" : "string", + "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "description" : "Identifier for active mediation record to be used", + "pattern" : "[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-4[a-fA-F0-9]{3}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}" + }, + "metadata" : { + "type" : "object", + "description" : "Optional metadata to attach to the connection created with the invitation", + "properties" : { } + }, + "my_label" : { + "type" : "string", + "example" : "Invitation to Barry", + "description" : "Label for connection invitation" + }, + "protocol_version" : { + "type" : "string", + "example" : "1.1", + "description" : "OOB protocol version" + }, + "use_public_did" : { + "type" : "boolean", + "example" : false, + "description" : "Whether to use public DID in invitation" + } + } + }, + "InvitationMessage" : { + "type" : "object", + "properties" : { + "@id" : { + "type" : "string", + "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "description" : "Message identifier" + }, + "@type" : { + "type" : "string", + "example" : "https://didcomm.org/my-family/1.0/my-message-type", + "description" : "Message type" + }, + "accept" : { + "type" : "array", + "example" : [ "didcomm/aip1", "didcomm/aip2;env=rfc19" ], + "description" : "List of mime type in order of preference", + "items" : { + "type" : "string" + } + }, + "handshake_protocols" : { + "type" : "array", + "items" : { + "type" : "string", + "example" : "did:sov:BzCbsNYhMrjHiqZDTUASHg;spec/didexchange/1.0", + "description" : "Handshake protocol" + } + }, + "imageUrl" : { + "type" : "string", + "format" : "url", + "example" : "http://192.168.56.101/img/logo.jpg", + "description" : "Optional image URL for out-of-band invitation", + "x-nullable" : true + }, + "label" : { + "type" : "string", + "example" : "Bob", + "description" : "Optional label" + }, + "requests~attach" : { + "type" : "array", + "description" : "Optional request attachment", + "items" : { + "$ref" : "#/definitions/AttachDecorator" + } + }, + "services" : { + "type" : "array", + "example" : [ { + "did" : "WgWxqztrNooG92RXvxSTWv", + "id" : "string", + "recipientKeys" : [ "did:key:z6MkpTHR8VNsBxYAAWHut2Geadd9jSwuBV8xRoAnwWsdvktH" ], + "routingKeys" : [ "did:key:z6MkpTHR8VNsBxYAAWHut2Geadd9jSwuBV8xRoAnwWsdvktH" ], + "serviceEndpoint" : "http://192.168.56.101:8020", + "type" : "string" + }, "did:sov:WgWxqztrNooG92RXvxSTWv" ], + "items" : { + "description" : "Either a DIDComm service object (as per RFC0067) or a DID string." + } + } + } + }, + "InvitationRecord" : { + "type" : "object", + "properties" : { + "created_at" : { + "type" : "string", + "example" : "2021-12-31T23:59:59Z", + "description" : "Time of record creation", + "pattern" : "^\\d{4}-\\d\\d-\\d\\d[T ]\\d\\d:\\d\\d(?:\\:(?:\\d\\d(?:\\.\\d{1,6})?))?(?:[+-]\\d\\d:?\\d\\d|Z|)$" + }, + "invi_msg_id" : { + "type" : "string", + "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "description" : "Invitation message identifier" + }, + "invitation" : { + "$ref" : "#/definitions/InvitationRecord_invitation" + }, + "invitation_id" : { + "type" : "string", + "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "description" : "Invitation record identifier" + }, + "invitation_url" : { + "type" : "string", + "example" : "https://example.com/endpoint?c_i=eyJAdHlwZSI6ICIuLi4iLCAiLi4uIjogIi4uLiJ9XX0=", + "description" : "Invitation message URL" + }, + "oob_id" : { + "type" : "string", + "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "description" : "Out of band record identifier" + }, + "state" : { + "type" : "string", + "example" : "await_response", + "description" : "Out of band message exchange state" + }, + "trace" : { + "type" : "boolean", + "description" : "Record trace information, based on agent configuration" + }, + "updated_at" : { + "type" : "string", + "example" : "2021-12-31T23:59:59Z", + "description" : "Time of last record update", + "pattern" : "^\\d{4}-\\d\\d-\\d\\d[T ]\\d\\d:\\d\\d(?:\\:(?:\\d\\d(?:\\.\\d{1,6})?))?(?:[+-]\\d\\d:?\\d\\d|Z|)$" + } + } + }, + "InvitationResult" : { + "type" : "object", + "properties" : { + "connection_id" : { + "type" : "string", + "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "description" : "Connection identifier" + }, + "invitation" : { + "$ref" : "#/definitions/ConnectionInvitation" + }, + "invitation_url" : { + "type" : "string", + "example" : "http://192.168.56.101:8020/invite?c_i=eyJAdHlwZSI6Li4ufQ==", + "description" : "Invitation URL" + } + } + }, + "IssueCredentialModuleResponse" : { + "type" : "object" + }, + "IssuerCredRevRecord" : { + "type" : "object", + "properties" : { + "created_at" : { + "type" : "string", + "example" : "2021-12-31T23:59:59Z", + "description" : "Time of record creation", + "pattern" : "^\\d{4}-\\d\\d-\\d\\d[T ]\\d\\d:\\d\\d(?:\\:(?:\\d\\d(?:\\.\\d{1,6})?))?(?:[+-]\\d\\d:?\\d\\d|Z|)$" + }, + "cred_def_id" : { + "type" : "string", + "example" : "WgWxqztrNooG92RXvxSTWv:3:CL:20:tag", + "description" : "Credential definition identifier", + "pattern" : "^([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}):3:CL:(([1-9][0-9]*)|([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}:2:.+:[0-9.]+)):(.+)?$" + }, + "cred_ex_id" : { + "type" : "string", + "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "description" : "Credential exchange record identifier at credential issue" + }, + "cred_ex_version" : { + "type" : "string", + "description" : "Credential exchange version" + }, + "cred_rev_id" : { + "type" : "string", + "example" : "12345", + "description" : "Credential revocation identifier", + "pattern" : "^[1-9][0-9]*$" + }, + "record_id" : { + "type" : "string", + "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "description" : "Issuer credential revocation record identifier" + }, + "rev_reg_id" : { + "type" : "string", + "example" : "WgWxqztrNooG92RXvxSTWv:4:WgWxqztrNooG92RXvxSTWv:3:CL:20:tag:CL_ACCUM:0", + "description" : "Revocation registry identifier", + "pattern" : "^([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}):4:([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}):3:CL:(([1-9][0-9]*)|([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}:2:.+:[0-9.]+))(:.+)?:CL_ACCUM:(.+$)" + }, + "state" : { + "type" : "string", + "example" : "issued", + "description" : "Issue credential revocation record state" + }, + "updated_at" : { + "type" : "string", + "example" : "2021-12-31T23:59:59Z", + "description" : "Time of last record update", + "pattern" : "^\\d{4}-\\d\\d-\\d\\d[T ]\\d\\d:\\d\\d(?:\\:(?:\\d\\d(?:\\.\\d{1,6})?))?(?:[+-]\\d\\d:?\\d\\d|Z|)$" + } + } + }, + "IssuerRevRegRecord" : { + "type" : "object", + "properties" : { + "created_at" : { + "type" : "string", + "example" : "2021-12-31T23:59:59Z", + "description" : "Time of record creation", + "pattern" : "^\\d{4}-\\d\\d-\\d\\d[T ]\\d\\d:\\d\\d(?:\\:(?:\\d\\d(?:\\.\\d{1,6})?))?(?:[+-]\\d\\d:?\\d\\d|Z|)$" + }, + "cred_def_id" : { + "type" : "string", + "example" : "WgWxqztrNooG92RXvxSTWv:3:CL:20:tag", + "description" : "Credential definition identifier", + "pattern" : "^([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}):3:CL:(([1-9][0-9]*)|([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}:2:.+:[0-9.]+)):(.+)?$" + }, + "error_msg" : { + "type" : "string", + "example" : "Revocation registry undefined", + "description" : "Error message" + }, + "issuer_did" : { + "type" : "string", + "example" : "WgWxqztrNooG92RXvxSTWv", + "description" : "Issuer DID", + "pattern" : "^(did:sov:)?[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}$" + }, + "max_cred_num" : { + "type" : "integer", + "format" : "int32", + "example" : 1000, + "description" : "Maximum number of credentials for revocation registry" + }, + "pending_pub" : { + "type" : "array", + "description" : "Credential revocation identifier for credential revoked and pending publication to ledger", + "items" : { + "type" : "string", + "example" : "23" + } + }, + "record_id" : { + "type" : "string", + "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "description" : "Issuer revocation registry record identifier" + }, + "revoc_def_type" : { + "type" : "string", + "example" : "CL_ACCUM", + "description" : "Revocation registry type (specify CL_ACCUM)", + "enum" : [ "CL_ACCUM" ] + }, + "revoc_reg_def" : { + "$ref" : "#/definitions/IssuerRevRegRecord_revoc_reg_def" + }, + "revoc_reg_entry" : { + "$ref" : "#/definitions/IssuerRevRegRecord_revoc_reg_entry" + }, + "revoc_reg_id" : { + "type" : "string", + "example" : "WgWxqztrNooG92RXvxSTWv:4:WgWxqztrNooG92RXvxSTWv:3:CL:20:tag:CL_ACCUM:0", + "description" : "Revocation registry identifier", + "pattern" : "^([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}):4:([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}):3:CL:(([1-9][0-9]*)|([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}:2:.+:[0-9.]+))(:.+)?:CL_ACCUM:(.+$)" + }, + "state" : { + "type" : "string", + "example" : "active", + "description" : "Issue revocation registry record state" + }, + "tag" : { + "type" : "string", + "description" : "Tag within issuer revocation registry identifier" + }, + "tails_hash" : { + "type" : "string", + "example" : "H3C2AVvLMv6gmMNam3uVAjZpfkcJCwDwnZn6z3wXmqPV", + "description" : "Tails hash", + "pattern" : "^[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{43,44}$" + }, + "tails_local_path" : { + "type" : "string", + "description" : "Local path to tails file" + }, + "tails_public_uri" : { + "type" : "string", + "description" : "Public URI for tails file" + }, + "updated_at" : { + "type" : "string", + "example" : "2021-12-31T23:59:59Z", + "description" : "Time of last record update", + "pattern" : "^\\d{4}-\\d\\d-\\d\\d[T ]\\d\\d:\\d\\d(?:\\:(?:\\d\\d(?:\\.\\d{1,6})?))?(?:[+-]\\d\\d:?\\d\\d|Z|)$" + } + } + }, + "Keylist" : { + "type" : "object", + "properties" : { + "results" : { + "type" : "array", + "description" : "List of keylist records", + "items" : { + "$ref" : "#/definitions/RouteRecord" + } + } + } + }, + "KeylistQuery" : { + "type" : "object", + "properties" : { + "@id" : { + "type" : "string", + "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "description" : "Message identifier" + }, + "@type" : { + "type" : "string", + "example" : "https://didcomm.org/my-family/1.0/my-message-type", + "description" : "Message type", + "readOnly" : true + }, + "filter" : { + "type" : "object", + "example" : { + "filter" : { } + }, + "description" : "Query dictionary object", + "properties" : { } + }, + "paginate" : { + "$ref" : "#/definitions/KeylistQuery_paginate" + } + } + }, + "KeylistQueryFilterRequest" : { + "type" : "object", + "properties" : { + "filter" : { + "type" : "object", + "description" : "Filter for keylist query", + "properties" : { } + } + } + }, + "KeylistQueryPaginate" : { + "type" : "object", + "properties" : { + "limit" : { + "type" : "integer", + "format" : "int32", + "example" : 30, + "description" : "Limit for keylist query" + }, + "offset" : { + "type" : "integer", + "format" : "int32", + "example" : 0, + "description" : "Offset value for query" + } + } + }, + "KeylistUpdate" : { + "type" : "object", + "properties" : { + "@id" : { + "type" : "string", + "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "description" : "Message identifier" + }, + "@type" : { + "type" : "string", + "example" : "https://didcomm.org/my-family/1.0/my-message-type", + "description" : "Message type", + "readOnly" : true + }, + "updates" : { + "type" : "array", + "description" : "List of update rules", + "items" : { + "$ref" : "#/definitions/KeylistUpdateRule" + } + } + } + }, + "KeylistUpdateRequest" : { + "type" : "object", + "properties" : { + "updates" : { + "type" : "array", + "items" : { + "$ref" : "#/definitions/KeylistUpdateRule" + } + } + } + }, + "KeylistUpdateRule" : { + "type" : "object", + "required" : [ "action", "recipient_key" ], + "properties" : { + "action" : { + "type" : "string", + "example" : "add", + "description" : "Action for specific key", + "enum" : [ "add", "remove" ] + }, + "recipient_key" : { + "type" : "string", + "example" : "did:key:z6MkpTHR8VNsBxYAAWHut2Geadd9jSwuBV8xRoAnwWsdvktH", + "description" : "Key to remove or add", + "pattern" : "^did:key:z[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]+$|^[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{43,44}$" + } + } + }, + "LDProofVCDetail" : { + "type" : "object", + "required" : [ "credential", "options" ], + "properties" : { + "credential" : { + "$ref" : "#/definitions/LDProofVCDetail_credential" + }, + "options" : { + "$ref" : "#/definitions/LDProofVCDetail_options" + } + } + }, + "LDProofVCDetailOptions" : { + "type" : "object", + "required" : [ "proofType" ], + "properties" : { + "challenge" : { + "type" : "string", + "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "description" : "A challenge to include in the proof. SHOULD be provided by the requesting party of the credential (=holder)" + }, + "created" : { + "type" : "string", + "example" : "2021-12-31T23:59:59Z", + "description" : "The date and time of the proof (with a maximum accuracy in seconds). Defaults to current system time", + "pattern" : "^\\d{4}-\\d\\d-\\d\\d[T ]\\d\\d:\\d\\d(?:\\:(?:\\d\\d(?:\\.\\d{1,6})?))?(?:[+-]\\d\\d:?\\d\\d|Z|)$" + }, + "credentialStatus" : { + "$ref" : "#/definitions/LDProofVCDetailOptions_credentialStatus" + }, + "domain" : { + "type" : "string", + "example" : "example.com", + "description" : "The intended domain of validity for the proof" + }, + "proofPurpose" : { + "type" : "string", + "example" : "assertionMethod", + "description" : "The proof purpose used for the proof. Should match proof purposes registered in the Linked Data Proofs Specification" + }, + "proofType" : { + "type" : "string", + "example" : "Ed25519Signature2018", + "description" : "The proof type used for the proof. Should match suites registered in the Linked Data Cryptographic Suite Registry" + } + } + }, + "LedgerConfigInstance" : { + "type" : "object", + "properties" : { + "genesis_file" : { + "type" : "string", + "description" : "genesis_file" + }, + "genesis_transactions" : { + "type" : "string", + "description" : "genesis_transactions" + }, + "genesis_url" : { + "type" : "string", + "description" : "genesis_url" + }, + "id" : { + "type" : "string", + "description" : "ledger_id" + }, + "is_production" : { + "type" : "boolean", + "description" : "is_production" + } + } + }, + "LedgerConfigList" : { + "type" : "object", + "required" : [ "ledger_config_list" ], + "properties" : { + "ledger_config_list" : { + "type" : "array", + "items" : { + "$ref" : "#/definitions/LedgerConfigInstance" + } + } + } + }, + "LedgerModulesResult" : { + "type" : "object" + }, + "LinkedDataProof" : { + "type" : "object", + "required" : [ "created", "proofPurpose", "type", "verificationMethod" ], + "properties" : { + "challenge" : { + "type" : "string", + "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "description" : "Associates a challenge with a proof, for use with a proofPurpose such as authentication" + }, + "created" : { + "type" : "string", + "example" : "2021-12-31T23:59:59Z", + "description" : "The string value of an ISO8601 combined date and time string generated by the Signature Algorithm", + "pattern" : "^\\d{4}-\\d\\d-\\d\\d[T ]\\d\\d:\\d\\d(?:\\:(?:\\d\\d(?:\\.\\d{1,6})?))?(?:[+-]\\d\\d:?\\d\\d|Z|)$" + }, + "domain" : { + "type" : "string", + "example" : "example.com", + "description" : "A string value specifying the restricted domain of the signature.", + "pattern" : "\\w+:(\\/?\\/?)[^\\s]+" + }, + "jws" : { + "type" : "string", + "example" : "eyJhbGciOiAiRWREUc2UsICJjcml0IjogWyJiNjQiXX0..lKJU0Df_keblRKhZAS9Qq6zybm-HqUXNVZ8vgEPNTAjQ1Ch6YBKY7UBAjg6iBX5qBQ", + "description" : "Associates a Detached Json Web Signature with a proof" + }, + "nonce" : { + "type" : "string", + "example" : "CF69iO3nfvqRsRBNElE8b4wO39SyJHPM7Gg1nExltW5vSfQA1lvDCR/zXX1To0/4NLo==", + "description" : "The nonce" + }, + "proofPurpose" : { + "type" : "string", + "example" : "assertionMethod", + "description" : "Proof purpose" + }, + "proofValue" : { + "type" : "string", + "example" : "sy1AahqbzJQ63n9RtekmwzqZeVj494VppdAVJBnMYrTwft6cLJJGeTSSxCCJ6HKnRtwE7jjDh6sB2z2AAiZY9BBnCD8wUVgwqH3qchGRCuC2RugA4eQ9fUrR4Yuycac3caiaaay", + "description" : "The proof value of a proof" + }, + "type" : { + "type" : "string", + "example" : "Ed25519Signature2018", + "description" : "Identifies the digital signature suite that was used to create the signature" + }, + "verificationMethod" : { + "type" : "string", + "example" : "did:key:z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL#z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL", + "description" : "Information used for proof verification", + "pattern" : "\\w+:(\\/?\\/?)[^\\s]+" + } + } + }, + "MediationCreateRequest" : { + "type" : "object", + "properties" : { + "mediator_terms" : { + "type" : "array", + "description" : "List of mediator rules for recipient", + "items" : { + "type" : "string", + "description" : "Indicate terms to which the mediator requires the recipient to agree" + } + }, + "recipient_terms" : { + "type" : "array", + "description" : "List of recipient rules for mediation", + "items" : { + "type" : "string", + "description" : "Indicate terms to which the recipient requires the mediator to agree" + } + } + } + }, + "MediationDeny" : { + "type" : "object", + "properties" : { + "@id" : { + "type" : "string", + "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "description" : "Message identifier" + }, + "@type" : { + "type" : "string", + "example" : "https://didcomm.org/my-family/1.0/my-message-type", + "description" : "Message type", + "readOnly" : true + }, + "mediator_terms" : { + "type" : "array", + "items" : { + "type" : "string", + "description" : "Terms for mediator to agree" + } + }, + "recipient_terms" : { + "type" : "array", + "items" : { + "type" : "string", + "description" : "Terms for recipient to agree" + } + } + } + }, + "MediationGrant" : { + "type" : "object", + "properties" : { + "@id" : { + "type" : "string", + "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "description" : "Message identifier" + }, + "@type" : { + "type" : "string", + "example" : "https://didcomm.org/my-family/1.0/my-message-type", + "description" : "Message type", + "readOnly" : true + }, + "endpoint" : { + "type" : "string", + "example" : "http://192.168.56.102:8020/", + "description" : "endpoint on which messages destined for the recipient are received." + }, + "routing_keys" : { + "type" : "array", + "items" : { + "type" : "string", + "description" : "Keys to use for forward message packaging" + } + } + } + }, + "MediationIdMatchInfo" : { + "type" : "object", + "properties" : { + "mediation_id" : { + "type" : "string", + "format" : "uuid", + "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "description" : "Mediation record identifier" + } + } + }, + "MediationList" : { + "type" : "object", + "properties" : { + "results" : { + "type" : "array", + "description" : "List of mediation records", + "items" : { + "$ref" : "#/definitions/MediationRecord" + } + } + } + }, + "MediationRecord" : { + "type" : "object", + "required" : [ "connection_id", "role" ], + "properties" : { + "connection_id" : { + "type" : "string" + }, + "created_at" : { + "type" : "string", + "example" : "2021-12-31T23:59:59Z", + "description" : "Time of record creation", + "pattern" : "^\\d{4}-\\d\\d-\\d\\d[T ]\\d\\d:\\d\\d(?:\\:(?:\\d\\d(?:\\.\\d{1,6})?))?(?:[+-]\\d\\d:?\\d\\d|Z|)$" + }, + "endpoint" : { + "type" : "string" + }, + "mediation_id" : { + "type" : "string" + }, + "mediator_terms" : { + "type" : "array", + "items" : { + "type" : "string" + } + }, + "recipient_terms" : { + "type" : "array", + "items" : { + "type" : "string" + } + }, + "role" : { + "type" : "string" + }, + "routing_keys" : { + "type" : "array", + "items" : { + "type" : "string", + "example" : "did:key:z6MkpTHR8VNsBxYAAWHut2Geadd9jSwuBV8xRoAnwWsdvktH", + "pattern" : "^did:key:z[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]+$" + } + }, + "state" : { + "type" : "string", + "example" : "active", + "description" : "Current record state" + }, + "updated_at" : { + "type" : "string", + "example" : "2021-12-31T23:59:59Z", + "description" : "Time of last record update", + "pattern" : "^\\d{4}-\\d\\d-\\d\\d[T ]\\d\\d:\\d\\d(?:\\:(?:\\d\\d(?:\\.\\d{1,6})?))?(?:[+-]\\d\\d:?\\d\\d|Z|)$" + } + } + }, + "Menu" : { + "type" : "object", + "required" : [ "options" ], + "properties" : { + "@id" : { + "type" : "string", + "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "description" : "Message identifier" + }, + "@type" : { + "type" : "string", + "example" : "https://didcomm.org/my-family/1.0/my-message-type", + "description" : "Message type", + "readOnly" : true + }, + "description" : { + "type" : "string", + "example" : "This menu presents options", + "description" : "Introductory text for the menu" + }, + "errormsg" : { + "type" : "string", + "example" : "Error: item not found", + "description" : "An optional error message to display in menu header" + }, + "options" : { + "type" : "array", + "description" : "List of menu options", + "items" : { + "$ref" : "#/definitions/MenuOption" + } + }, + "title" : { + "type" : "string", + "example" : "My Menu", + "description" : "Menu title" + } + } + }, + "MenuForm" : { + "type" : "object", + "properties" : { + "description" : { + "type" : "string", + "example" : "Window preference settings", + "description" : "Additional descriptive text for menu form" + }, + "params" : { + "type" : "array", + "description" : "List of form parameters", + "items" : { + "$ref" : "#/definitions/MenuFormParam" + } + }, + "submit-label" : { + "type" : "string", + "example" : "Send", + "description" : "Alternative label for form submit button" + }, + "title" : { + "type" : "string", + "example" : "Preferences", + "description" : "Menu form title" + } + } + }, + "MenuFormParam" : { + "type" : "object", + "required" : [ "name", "title" ], + "properties" : { + "default" : { + "type" : "string", + "example" : "0", + "description" : "Default parameter value" + }, + "description" : { + "type" : "string", + "example" : "Delay in seconds before starting", + "description" : "Additional descriptive text for menu form parameter" + }, + "name" : { + "type" : "string", + "example" : "delay", + "description" : "Menu parameter name" + }, + "required" : { + "type" : "boolean", + "example" : false, + "description" : "Whether parameter is required" + }, + "title" : { + "type" : "string", + "example" : "Delay in seconds", + "description" : "Menu parameter title" + }, + "type" : { + "type" : "string", + "example" : "int", + "description" : "Menu form parameter input type" + } + } + }, + "MenuJson" : { + "type" : "object", + "required" : [ "options" ], + "properties" : { + "description" : { + "type" : "string", + "example" : "User preferences for window settings", + "description" : "Introductory text for the menu" + }, + "errormsg" : { + "type" : "string", + "example" : "Error: item not present", + "description" : "Optional error message to display in menu header" + }, + "options" : { + "type" : "array", + "description" : "List of menu options", + "items" : { + "$ref" : "#/definitions/MenuOption" + } + }, + "title" : { + "type" : "string", + "example" : "My Menu", + "description" : "Menu title" + } + } + }, + "MenuOption" : { + "type" : "object", + "required" : [ "name", "title" ], + "properties" : { + "description" : { + "type" : "string", + "example" : "Window display preferences", + "description" : "Additional descriptive text for menu option" + }, + "disabled" : { + "type" : "boolean", + "example" : false, + "description" : "Whether to show option as disabled" + }, + "form" : { + "$ref" : "#/definitions/MenuForm" + }, + "name" : { + "type" : "string", + "example" : "window_prefs", + "description" : "Menu option name (unique identifier)" + }, + "title" : { + "type" : "string", + "example" : "Window Preferences", + "description" : "Menu option title" + } + } + }, + "MultitenantModuleResponse" : { + "type" : "object" + }, + "OobRecord" : { + "type" : "object", + "required" : [ "invi_msg_id", "invitation", "oob_id", "state" ], + "properties" : { + "attach_thread_id" : { + "type" : "string", + "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "description" : "Connection record identifier" + }, + "connection_id" : { + "type" : "string", + "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "description" : "Connection record identifier" + }, + "created_at" : { + "type" : "string", + "example" : "2021-12-31T23:59:59Z", + "description" : "Time of record creation", + "pattern" : "^\\d{4}-\\d\\d-\\d\\d[T ]\\d\\d:\\d\\d(?:\\:(?:\\d\\d(?:\\.\\d{1,6})?))?(?:[+-]\\d\\d:?\\d\\d|Z|)$" + }, + "invi_msg_id" : { + "type" : "string", + "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "description" : "Invitation message identifier" + }, + "invitation" : { + "$ref" : "#/definitions/InvitationRecord_invitation" + }, + "oob_id" : { + "type" : "string", + "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "description" : "Oob record identifier" + }, + "our_recipient_key" : { + "type" : "string", + "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "description" : "Recipient key used for oob invitation" + }, + "role" : { + "type" : "string", + "example" : "receiver", + "description" : "OOB Role", + "enum" : [ "sender", "receiver" ] + }, + "state" : { + "type" : "string", + "example" : "await-response", + "description" : "Out of band message exchange state", + "enum" : [ "initial", "prepare-response", "await-response", "reuse-not-accepted", "reuse-accepted", "done", "deleted" ] + }, + "their_service" : { + "$ref" : "#/definitions/ServiceDecorator" + }, + "trace" : { + "type" : "boolean", + "description" : "Record trace information, based on agent configuration" + }, + "updated_at" : { + "type" : "string", + "example" : "2021-12-31T23:59:59Z", + "description" : "Time of last record update", + "pattern" : "^\\d{4}-\\d\\d-\\d\\d[T ]\\d\\d:\\d\\d(?:\\:(?:\\d\\d(?:\\.\\d{1,6})?))?(?:[+-]\\d\\d:?\\d\\d|Z|)$" + } + } + }, + "PerformRequest" : { + "type" : "object", + "properties" : { + "name" : { + "type" : "string", + "example" : "Query", + "description" : "Menu option name" + }, + "params" : { + "type" : "object", + "description" : "Input parameter values", + "additionalProperties" : { + "type" : "string", + "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6" + } + } + } + }, + "PingRequest" : { + "type" : "object", + "properties" : { + "comment" : { + "type" : "string", + "description" : "Comment for the ping message", + "x-nullable" : true + } + } + }, + "PingRequestResponse" : { + "type" : "object", + "properties" : { + "thread_id" : { + "type" : "string", + "description" : "Thread ID of the ping message" + } + } + }, + "PresentationDefinition" : { + "type" : "object", + "properties" : { + "format" : { + "$ref" : "#/definitions/ClaimFormat" + }, + "id" : { + "type" : "string", + "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "description" : "Unique Resource Identifier", + "pattern" : "[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-4[a-fA-F0-9]{3}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}" + }, + "input_descriptors" : { + "type" : "array", + "items" : { + "$ref" : "#/definitions/InputDescriptors" + } + }, + "name" : { + "type" : "string", + "description" : "Human-friendly name that describes what the presentation definition pertains to" + }, + "purpose" : { + "type" : "string", + "description" : "Describes the purpose for which the Presentation Definition's inputs are being requested" + }, + "submission_requirements" : { + "type" : "array", + "items" : { + "$ref" : "#/definitions/SubmissionRequirements" + } + } + } + }, + "PresentationProposal" : { + "type" : "object", + "required" : [ "presentation_proposal" ], + "properties" : { + "@id" : { + "type" : "string", + "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "description" : "Message identifier" + }, + "@type" : { + "type" : "string", + "example" : "https://didcomm.org/my-family/1.0/my-message-type", + "description" : "Message type", + "readOnly" : true + }, + "comment" : { + "type" : "string", + "description" : "Human-readable comment", + "x-nullable" : true + }, + "presentation_proposal" : { + "$ref" : "#/definitions/IndyPresPreview" + } + } + }, + "PresentationRequest" : { + "type" : "object", + "required" : [ "request_presentations~attach" ], + "properties" : { + "@id" : { + "type" : "string", + "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "description" : "Message identifier" + }, + "@type" : { + "type" : "string", + "example" : "https://didcomm.org/my-family/1.0/my-message-type", + "description" : "Message type", + "readOnly" : true + }, + "comment" : { + "type" : "string", + "description" : "Human-readable comment", + "x-nullable" : true + }, + "request_presentations~attach" : { + "type" : "array", + "items" : { + "$ref" : "#/definitions/AttachDecorator" + } + } + } + }, + "ProtocolDescriptor" : { + "type" : "object", + "required" : [ "pid" ], + "properties" : { + "pid" : { + "type" : "string" + }, + "roles" : { + "type" : "array", + "description" : "List of roles", + "items" : { + "type" : "string", + "example" : "requester", + "description" : "Role: requester or responder" + }, + "x-nullable" : true + } + } + }, + "PublishRevocations" : { + "type" : "object", + "properties" : { + "rrid2crid" : { + "type" : "object", + "description" : "Credential revocation ids by revocation registry id", + "additionalProperties" : { + "type" : "array", + "items" : { + "type" : "string", + "example" : "12345", + "description" : "Credential revocation identifier", + "pattern" : "^[1-9][0-9]*$" + } + } + } + } + }, + "Queries" : { + "type" : "object", + "properties" : { + "@id" : { + "type" : "string", + "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "description" : "Message identifier" + }, + "@type" : { + "type" : "string", + "example" : "https://didcomm.org/my-family/1.0/my-message-type", + "description" : "Message type", + "readOnly" : true + }, + "queries" : { + "type" : "array", + "items" : { + "$ref" : "#/definitions/QueryItem" + } + } + } + }, + "Query" : { + "type" : "object", + "required" : [ "query" ], + "properties" : { + "@id" : { + "type" : "string", + "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "description" : "Message identifier" + }, + "@type" : { + "type" : "string", + "example" : "https://didcomm.org/my-family/1.0/my-message-type", + "description" : "Message type", + "readOnly" : true + }, + "comment" : { + "type" : "string", + "x-nullable" : true + }, + "query" : { + "type" : "string" + } + } + }, + "QueryItem" : { + "type" : "object", + "required" : [ "feature-type", "match" ], + "properties" : { + "feature-type" : { + "type" : "string", + "description" : "feature type", + "enum" : [ "protocol", "goal-code" ] + }, + "match" : { + "type" : "string", + "description" : "match" + } + } + }, + "RawEncoded" : { + "type" : "object", + "properties" : { + "encoded" : { + "type" : "string", + "example" : "-1", + "description" : "Encoded value", + "pattern" : "^-?[0-9]*$" + }, + "raw" : { + "type" : "string", + "description" : "Raw value" + } + } + }, + "ReceiveInvitationRequest" : { + "type" : "object", + "properties" : { + "@id" : { + "type" : "string", + "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "description" : "Message identifier" + }, + "@type" : { + "type" : "string", + "example" : "https://didcomm.org/my-family/1.0/my-message-type", + "description" : "Message type", + "readOnly" : true + }, + "did" : { + "type" : "string", + "example" : "WgWxqztrNooG92RXvxSTWv", + "description" : "DID for connection invitation", + "pattern" : "^(did:sov:)?[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}$" + }, + "imageUrl" : { + "type" : "string", + "format" : "url", + "example" : "http://192.168.56.101/img/logo.jpg", + "description" : "Optional image URL for connection invitation", + "x-nullable" : true + }, + "label" : { + "type" : "string", + "example" : "Bob", + "description" : "Optional label for connection invitation" + }, + "recipientKeys" : { + "type" : "array", + "description" : "List of recipient keys", + "items" : { + "type" : "string", + "example" : "H3C2AVvLMv6gmMNam3uVAjZpfkcJCwDwnZn6z3wXmqPV", + "description" : "Recipient public key", + "pattern" : "^[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{43,44}$" + } + }, + "routingKeys" : { + "type" : "array", + "description" : "List of routing keys", + "items" : { + "type" : "string", + "example" : "H3C2AVvLMv6gmMNam3uVAjZpfkcJCwDwnZn6z3wXmqPV", + "description" : "Routing key", + "pattern" : "^[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{43,44}$" + } + }, + "serviceEndpoint" : { + "type" : "string", + "example" : "http://192.168.56.101:8020", + "description" : "Service endpoint at which to reach this agent" + } + } + }, + "RemoveWalletRequest" : { + "type" : "object", + "properties" : { + "wallet_key" : { + "type" : "string", + "example" : "MySecretKey123", + "description" : "Master key used for key derivation. Only required for unmanaged wallets." + } + } + }, + "ResolutionResult" : { + "type" : "object", + "required" : [ "did_document", "metadata" ], + "properties" : { + "did_document" : { + "type" : "object", + "description" : "DID Document", + "properties" : { } + }, + "metadata" : { + "type" : "object", + "description" : "Resolution metadata", + "properties" : { } + } + } + }, + "RevRegCreateRequest" : { + "type" : "object", + "properties" : { + "credential_definition_id" : { + "type" : "string", + "example" : "WgWxqztrNooG92RXvxSTWv:3:CL:20:tag", + "description" : "Credential definition identifier", + "pattern" : "^([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}):3:CL:(([1-9][0-9]*)|([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}:2:.+:[0-9.]+)):(.+)?$" + }, + "max_cred_num" : { + "type" : "integer", + "format" : "int32", + "example" : 1000, + "description" : "Revocation registry size", + "minimum" : 4, + "maximum" : 32768 + } + } + }, + "RevRegIssuedResult" : { + "type" : "object", + "properties" : { + "result" : { + "type" : "integer", + "format" : "int32", + "example" : 0, + "description" : "Number of credentials issued against revocation registry", + "minimum" : 0 + } + } + }, + "RevRegResult" : { + "type" : "object", + "properties" : { + "result" : { + "$ref" : "#/definitions/IssuerRevRegRecord" + } + } + }, + "RevRegUpdateTailsFileUri" : { + "type" : "object", + "required" : [ "tails_public_uri" ], + "properties" : { + "tails_public_uri" : { + "type" : "string", + "format" : "url", + "example" : "http://192.168.56.133:6543/revocation/registry/WgWxqztrNooG92RXvxSTWv:4:WgWxqztrNooG92RXvxSTWv:3:CL:20:tag:CL_ACCUM:0/tails-file", + "description" : "Public URI to the tails file" + } + } + }, + "RevRegWalletUpdatedResult" : { + "type" : "object", + "properties" : { + "accum_calculated" : { + "type" : "object", + "description" : "Calculated accumulator for phantom revocations", + "properties" : { } + }, + "accum_fixed" : { + "type" : "object", + "description" : "Applied ledger transaction to fix revocations", + "properties" : { } + }, + "rev_reg_delta" : { + "type" : "object", + "description" : "Indy revocation registry delta", + "properties" : { } + } + } + }, + "RevRegsCreated" : { + "type" : "object", + "properties" : { + "rev_reg_ids" : { + "type" : "array", + "items" : { + "type" : "string", + "example" : "WgWxqztrNooG92RXvxSTWv:4:WgWxqztrNooG92RXvxSTWv:3:CL:20:tag:CL_ACCUM:0", + "description" : "Revocation registry identifiers", + "pattern" : "^([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}):4:([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}):3:CL:(([1-9][0-9]*)|([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}:2:.+:[0-9.]+))(:.+)?:CL_ACCUM:(.+$)" + } + } + } + }, + "RevocationModuleResponse" : { + "type" : "object" + }, + "RevokeRequest" : { + "type" : "object", + "properties" : { + "comment" : { + "type" : "string", + "description" : "Optional comment to include in revocation notification" + }, + "connection_id" : { + "type" : "string", + "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "description" : "Connection ID to which the revocation notification will be sent; required if notify is true", + "pattern" : "[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-4[a-fA-F0-9]{3}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}" + }, + "cred_ex_id" : { + "type" : "string", + "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "description" : "Credential exchange identifier", + "pattern" : "[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-4[a-fA-F0-9]{3}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}" + }, + "cred_rev_id" : { + "type" : "string", + "example" : "12345", + "description" : "Credential revocation identifier", + "pattern" : "^[1-9][0-9]*$" + }, + "notify" : { + "type" : "boolean", + "description" : "Send a notification to the credential recipient" + }, + "notify_version" : { + "type" : "string", + "description" : "Specify which version of the revocation notification should be sent", + "enum" : [ "v1_0", "v2_0" ] + }, + "publish" : { + "type" : "boolean", + "description" : "(True) publish revocation to ledger immediately, or (default, False) mark it pending" + }, + "rev_reg_id" : { + "type" : "string", + "example" : "WgWxqztrNooG92RXvxSTWv:4:WgWxqztrNooG92RXvxSTWv:3:CL:20:tag:CL_ACCUM:0", + "description" : "Revocation registry identifier", + "pattern" : "^([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}):4:([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}):3:CL:(([1-9][0-9]*)|([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}:2:.+:[0-9.]+))(:.+)?:CL_ACCUM:(.+$)" + }, + "thread_id" : { + "type" : "string", + "description" : "Thread ID of the credential exchange message thread resulting in the credential now being revoked; required if notify is true" + } + } + }, + "RouteRecord" : { + "type" : "object", + "required" : [ "recipient_key" ], + "properties" : { + "connection_id" : { + "type" : "string" + }, + "created_at" : { + "type" : "string", + "example" : "2021-12-31T23:59:59Z", + "description" : "Time of record creation", + "pattern" : "^\\d{4}-\\d\\d-\\d\\d[T ]\\d\\d:\\d\\d(?:\\:(?:\\d\\d(?:\\.\\d{1,6})?))?(?:[+-]\\d\\d:?\\d\\d|Z|)$" + }, + "recipient_key" : { + "type" : "string" + }, + "record_id" : { + "type" : "string" + }, + "role" : { + "type" : "string" + }, + "state" : { + "type" : "string", + "example" : "active", + "description" : "Current record state" + }, + "updated_at" : { + "type" : "string", + "example" : "2021-12-31T23:59:59Z", + "description" : "Time of last record update", + "pattern" : "^\\d{4}-\\d\\d-\\d\\d[T ]\\d\\d:\\d\\d(?:\\:(?:\\d\\d(?:\\.\\d{1,6})?))?(?:[+-]\\d\\d:?\\d\\d|Z|)$" + }, + "wallet_id" : { + "type" : "string" + } + } + }, + "Schema" : { + "type" : "object", + "properties" : { + "attrNames" : { + "type" : "array", + "description" : "Schema attribute names", + "items" : { + "type" : "string", + "example" : "score", + "description" : "Attribute name" + } + }, + "id" : { + "type" : "string", + "example" : "WgWxqztrNooG92RXvxSTWv:2:schema_name:1.0", + "description" : "Schema identifier", + "pattern" : "^[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}:2:.+:[0-9.]+$" + }, + "name" : { + "type" : "string", + "example" : "schema_name", + "description" : "Schema name" + }, + "seqNo" : { + "type" : "integer", + "format" : "int32", + "example" : 10, + "description" : "Schema sequence number", + "minimum" : 1 + }, + "ver" : { + "type" : "string", + "example" : "1.0", + "description" : "Node protocol version", + "pattern" : "^[0-9.]+$" + }, + "version" : { + "type" : "string", + "example" : "1.0", + "description" : "Schema version", + "pattern" : "^[0-9.]+$" + } + } + }, + "SchemaGetResult" : { + "type" : "object", + "properties" : { + "schema" : { + "$ref" : "#/definitions/Schema" + } + } + }, + "SchemaInputDescriptor" : { + "type" : "object", + "properties" : { + "required" : { + "type" : "boolean", + "description" : "Required" + }, + "uri" : { + "type" : "string", + "description" : "URI" + } + } + }, + "SchemaSendRequest" : { + "type" : "object", + "required" : [ "attributes", "schema_name", "schema_version" ], + "properties" : { + "attributes" : { + "type" : "array", + "description" : "List of schema attributes", + "items" : { + "type" : "string", + "example" : "score", + "description" : "attribute name" + } + }, + "schema_name" : { + "type" : "string", + "example" : "prefs", + "description" : "Schema name" + }, + "schema_version" : { + "type" : "string", + "example" : "1.0", + "description" : "Schema version", + "pattern" : "^[0-9.]+$" + } + } + }, + "SchemaSendResult" : { + "type" : "object", + "required" : [ "schema_id" ], + "properties" : { + "schema" : { + "$ref" : "#/definitions/SchemaSendResult_schema" + }, + "schema_id" : { + "type" : "string", + "example" : "WgWxqztrNooG92RXvxSTWv:2:schema_name:1.0", + "description" : "Schema identifier", + "pattern" : "^[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}:2:.+:[0-9.]+$" + } + } + }, + "SchemasCreatedResult" : { + "type" : "object", + "properties" : { + "schema_ids" : { + "type" : "array", + "items" : { + "type" : "string", + "example" : "WgWxqztrNooG92RXvxSTWv:2:schema_name:1.0", + "description" : "Schema identifiers", + "pattern" : "^[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}:2:.+:[0-9.]+$" + } + } + } + }, + "SchemasInputDescriptorFilter" : { + "type" : "object", + "properties" : { + "oneof_filter" : { + "type" : "boolean", + "description" : "oneOf" + }, + "uri_groups" : { + "type" : "array", + "items" : { + "type" : "array", + "items" : { + "$ref" : "#/definitions/SchemaInputDescriptor" + } + } + } + } + }, + "SendMenu" : { + "type" : "object", + "required" : [ "menu" ], + "properties" : { + "menu" : { + "$ref" : "#/definitions/SendMenu_menu" + } + } + }, + "SendMessage" : { + "type" : "object", + "properties" : { + "content" : { + "type" : "string", + "example" : "Hello", + "description" : "Message content" + } + } + }, + "ServiceDecorator" : { + "type" : "object", + "required" : [ "recipientKeys", "serviceEndpoint" ], + "properties" : { + "recipientKeys" : { + "type" : "array", + "description" : "List of recipient keys", + "items" : { + "type" : "string", + "example" : "H3C2AVvLMv6gmMNam3uVAjZpfkcJCwDwnZn6z3wXmqPV", + "description" : "Recipient public key", + "pattern" : "^[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{43,44}$" + } + }, + "routingKeys" : { + "type" : "array", + "description" : "List of routing keys", + "items" : { + "type" : "string", + "example" : "H3C2AVvLMv6gmMNam3uVAjZpfkcJCwDwnZn6z3wXmqPV", + "description" : "Routing key", + "pattern" : "^[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{43,44}$" + } + }, + "serviceEndpoint" : { + "type" : "string", + "example" : "http://192.168.56.101:8020", + "description" : "Service endpoint at which to reach this agent" + } + } + }, + "SignRequest" : { + "type" : "object", + "required" : [ "doc", "verkey" ], + "properties" : { + "doc" : { + "$ref" : "#/definitions/Doc" + }, + "verkey" : { + "type" : "string", + "description" : "Verkey to use for signing" + } + } + }, + "SignResponse" : { + "type" : "object", + "properties" : { + "error" : { + "type" : "string", + "description" : "Error text" + }, + "signed_doc" : { + "type" : "object", + "description" : "Signed document", + "properties" : { } + } + } + }, + "SignatureOptions" : { + "type" : "object", + "required" : [ "proofPurpose", "verificationMethod" ], + "properties" : { + "challenge" : { + "type" : "string" + }, + "domain" : { + "type" : "string" + }, + "proofPurpose" : { + "type" : "string" + }, + "type" : { + "type" : "string" + }, + "verificationMethod" : { + "type" : "string" + } + } + }, + "SignedDoc" : { + "type" : "object", + "required" : [ "proof" ], + "properties" : { + "proof" : { + "$ref" : "#/definitions/SignedDoc_proof" + } + } + }, + "SubmissionRequirements" : { + "type" : "object", + "properties" : { + "count" : { + "type" : "integer", + "format" : "int32", + "example" : 1234, + "description" : "Count Value" + }, + "from" : { + "type" : "string", + "description" : "From" + }, + "from_nested" : { + "type" : "array", + "items" : { + "$ref" : "#/definitions/SubmissionRequirements" + } + }, + "max" : { + "type" : "integer", + "format" : "int32", + "example" : 1234, + "description" : "Max Value" + }, + "min" : { + "type" : "integer", + "format" : "int32", + "example" : 1234, + "description" : "Min Value" + }, + "name" : { + "type" : "string", + "description" : "Name" + }, + "purpose" : { + "type" : "string", + "description" : "Purpose" + }, + "rule" : { + "type" : "string", + "description" : "Selection", + "enum" : [ "all", "pick" ] + } + } + }, + "TAAAccept" : { + "type" : "object", + "properties" : { + "mechanism" : { + "type" : "string" + }, + "text" : { + "type" : "string" + }, + "version" : { + "type" : "string" + } + } + }, + "TAAAcceptance" : { + "type" : "object", + "properties" : { + "mechanism" : { + "type" : "string" + }, + "time" : { + "type" : "integer", + "format" : "int32", + "example" : 1640995199, + "minimum" : 0, + "maximum" : 18446744073709551615 + } + } + }, + "TAAInfo" : { + "type" : "object", + "properties" : { + "aml_record" : { + "$ref" : "#/definitions/AMLRecord" + }, + "taa_accepted" : { + "$ref" : "#/definitions/TAAAcceptance" + }, + "taa_record" : { + "$ref" : "#/definitions/TAARecord" + }, + "taa_required" : { + "type" : "boolean" + } + } + }, + "TAARecord" : { + "type" : "object", + "properties" : { + "digest" : { + "type" : "string" + }, + "text" : { + "type" : "string" + }, + "version" : { + "type" : "string" + } + } + }, + "TAAResult" : { + "type" : "object", + "properties" : { + "result" : { + "$ref" : "#/definitions/TAAInfo" + } + } + }, + "TailsDeleteResponse" : { + "type" : "object", + "properties" : { + "message" : { + "type" : "string" + } + } + }, + "TransactionJobs" : { + "type" : "object", + "properties" : { + "transaction_my_job" : { + "type" : "string", + "description" : "My transaction related job", + "enum" : [ "TRANSACTION_AUTHOR", "TRANSACTION_ENDORSER", "reset" ] + }, + "transaction_their_job" : { + "type" : "string", + "description" : "Their transaction related job", + "enum" : [ "TRANSACTION_AUTHOR", "TRANSACTION_ENDORSER", "reset" ] + } + } + }, + "TransactionList" : { + "type" : "object", + "properties" : { + "results" : { + "type" : "array", + "description" : "List of transaction records", + "items" : { + "$ref" : "#/definitions/TransactionRecord" + } + } + } + }, + "TransactionRecord" : { + "type" : "object", + "properties" : { + "_type" : { + "type" : "string", + "example" : "101", + "description" : "Transaction type" + }, + "connection_id" : { + "type" : "string", + "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "description" : "The connection identifier for thie particular transaction record" + }, + "created_at" : { + "type" : "string", + "example" : "2021-12-31T23:59:59Z", + "description" : "Time of record creation", + "pattern" : "^\\d{4}-\\d\\d-\\d\\d[T ]\\d\\d:\\d\\d(?:\\:(?:\\d\\d(?:\\.\\d{1,6})?))?(?:[+-]\\d\\d:?\\d\\d|Z|)$" + }, + "endorser_write_txn" : { + "type" : "boolean", + "example" : true, + "description" : "If True, Endorser will write the transaction after endorsing it" + }, + "formats" : { + "type" : "array", + "items" : { + "type" : "object", + "example" : { + "attach_id" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "format" : "dif/endorse-transaction/request@v1.0" + }, + "additionalProperties" : { + "type" : "string" + } + } + }, + "messages_attach" : { + "type" : "array", + "items" : { + "type" : "object", + "example" : { + "@id" : "143c458d-1b1c-40c7-ab85-4d16808ddf0a", + "data" : { + "json" : "{\"endorser\": \"V4SGRU86Z58d6TV7PBUe6f\",\"identifier\": \"LjgpST2rjsoxYegQDRm7EL\",\"operation\": {\"data\": {\"attr_names\": [\"first_name\", \"last_name\"],\"name\": \"test_schema\",\"version\": \"2.1\",},\"type\": \"101\",},\"protocolVersion\": 2,\"reqId\": 1597766666168851000,\"signatures\": {\"LjgpST2rjsox\": \"4ATKMn6Y9sTgwqaGTm7py2c2M8x1EVDTWKZArwyuPgjU\"},\"taaAcceptance\": {\"mechanism\": \"manual\",\"taaDigest\": \"f50fe2c2ab977006761d36bd6f23e4c6a7e0fc2feb9f62\",\"time\": 1597708800,}}" + }, + "mime-type" : "application/json" + }, + "properties" : { } + } + }, + "meta_data" : { + "type" : "object", + "example" : { + "context" : { + "param1" : "param1_value", + "param2" : "param2_value" + }, + "post_process" : [ { + "topic" : "topic_value", + "other" : "other_value" + } ] + }, + "properties" : { } + }, + "signature_request" : { + "type" : "array", + "items" : { + "type" : "object", + "example" : { + "author_goal_code" : "aries.transaction.ledger.write", + "context" : "did:sov", + "method" : "add-signature", + "signature_type" : "", + "signer_goal_code" : "aries.transaction.endorse" + }, + "properties" : { } + } + }, + "signature_response" : { + "type" : "array", + "items" : { + "type" : "object", + "example" : { + "context" : "did:sov", + "message_id" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "method" : "add-signature", + "signer_goal_code" : "aries.transaction.refuse" + }, + "properties" : { } + } + }, + "state" : { + "type" : "string", + "example" : "active", + "description" : "Current record state" + }, + "thread_id" : { + "type" : "string", + "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "description" : "Thread Identifier" + }, + "timing" : { + "type" : "object", + "example" : { + "expires_time" : "2020-12-13T17:29:06+0000" + }, + "properties" : { } + }, + "trace" : { + "type" : "boolean", + "description" : "Record trace information, based on agent configuration" + }, + "transaction_id" : { + "type" : "string", + "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "description" : "Transaction identifier" + }, + "updated_at" : { + "type" : "string", + "example" : "2021-12-31T23:59:59Z", + "description" : "Time of last record update", + "pattern" : "^\\d{4}-\\d\\d-\\d\\d[T ]\\d\\d:\\d\\d(?:\\:(?:\\d\\d(?:\\.\\d{1,6})?))?(?:[+-]\\d\\d:?\\d\\d|Z|)$" + } + } + }, + "TxnOrCredentialDefinitionSendResult" : { + "type" : "object", + "properties" : { + "sent" : { + "$ref" : "#/definitions/CredentialDefinitionSendResult" + }, + "txn" : { + "$ref" : "#/definitions/TxnOrCredentialDefinitionSendResult_txn" + } + } + }, + "TxnOrPublishRevocationsResult" : { + "type" : "object", + "properties" : { + "sent" : { + "$ref" : "#/definitions/PublishRevocations" + }, + "txn" : { + "$ref" : "#/definitions/TxnOrPublishRevocationsResult_txn" + } + } + }, + "TxnOrRegisterLedgerNymResponse" : { + "type" : "object", + "properties" : { + "success" : { + "type" : "boolean", + "example" : true, + "description" : "Success of nym registration operation" + }, + "txn" : { + "$ref" : "#/definitions/TxnOrRegisterLedgerNymResponse_txn" + } + } + }, + "TxnOrRevRegResult" : { + "type" : "object", + "properties" : { + "sent" : { + "$ref" : "#/definitions/RevRegResult" + }, + "txn" : { + "$ref" : "#/definitions/TxnOrRevRegResult_txn" + } + } + }, + "TxnOrSchemaSendResult" : { + "type" : "object", + "properties" : { + "sent" : { + "$ref" : "#/definitions/TxnOrSchemaSendResult_sent" + }, + "txn" : { + "$ref" : "#/definitions/TxnOrSchemaSendResult_txn" + } + } + }, + "UpdateWalletRequest" : { + "type" : "object", + "properties" : { + "image_url" : { + "type" : "string", + "example" : "https://aries.ca/images/sample.png", + "description" : "Image url for this wallet. This image url is publicized (self-attested) to other agents as part of forming a connection." + }, + "label" : { + "type" : "string", + "example" : "Alice", + "description" : "Label for this wallet. This label is publicized (self-attested) to other agents as part of forming a connection." + }, + "wallet_dispatch_type" : { + "type" : "string", + "example" : "default", + "description" : "Webhook target dispatch type for this wallet. default - Dispatch only to webhooks associated with this wallet. base - Dispatch only to webhooks associated with the base wallet. both - Dispatch to both webhook targets.", + "enum" : [ "default", "both", "base" ] + }, + "wallet_webhook_urls" : { + "type" : "array", + "description" : "List of Webhook URLs associated with this subwallet", + "items" : { + "type" : "string", + "example" : "http://localhost:8022/webhooks", + "description" : "Optional webhook URL to receive webhook messages" + } + } + } + }, + "V10CredentialBoundOfferRequest" : { + "type" : "object", + "properties" : { + "counter_proposal" : { + "$ref" : "#/definitions/V10CredentialBoundOfferRequest_counter_proposal" + } + } + }, + "V10CredentialConnFreeOfferRequest" : { + "type" : "object", + "required" : [ "cred_def_id", "credential_preview" ], + "properties" : { + "auto_issue" : { + "type" : "boolean", + "description" : "Whether to respond automatically to credential requests, creating and issuing requested credentials" + }, + "auto_remove" : { + "type" : "boolean", + "description" : "Whether to remove the credential exchange record on completion (overrides --preserve-exchange-records configuration setting)" + }, + "comment" : { + "type" : "string", + "description" : "Human-readable comment", + "x-nullable" : true + }, + "cred_def_id" : { + "type" : "string", + "example" : "WgWxqztrNooG92RXvxSTWv:3:CL:20:tag", + "description" : "Credential definition identifier", + "pattern" : "^([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}):3:CL:(([1-9][0-9]*)|([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}:2:.+:[0-9.]+)):(.+)?$" + }, + "credential_preview" : { + "$ref" : "#/definitions/CredentialPreview" + }, + "trace" : { + "type" : "boolean", + "description" : "Record trace information, based on agent configuration" + } + } + }, + "V10CredentialCreate" : { + "type" : "object", + "required" : [ "credential_proposal" ], + "properties" : { + "auto_remove" : { + "type" : "boolean", + "description" : "Whether to remove the credential exchange record on completion (overrides --preserve-exchange-records configuration setting)" + }, + "comment" : { + "type" : "string", + "description" : "Human-readable comment", + "x-nullable" : true + }, + "cred_def_id" : { + "type" : "string", + "example" : "WgWxqztrNooG92RXvxSTWv:3:CL:20:tag", + "description" : "Credential definition identifier", + "pattern" : "^([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}):3:CL:(([1-9][0-9]*)|([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}:2:.+:[0-9.]+)):(.+)?$" + }, + "credential_proposal" : { + "$ref" : "#/definitions/CredentialPreview" + }, + "issuer_did" : { + "type" : "string", + "example" : "WgWxqztrNooG92RXvxSTWv", + "description" : "Credential issuer DID", + "pattern" : "^(did:sov:)?[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}$" + }, + "schema_id" : { + "type" : "string", + "example" : "WgWxqztrNooG92RXvxSTWv:2:schema_name:1.0", + "description" : "Schema identifier", + "pattern" : "^[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}:2:.+:[0-9.]+$" + }, + "schema_issuer_did" : { + "type" : "string", + "example" : "WgWxqztrNooG92RXvxSTWv", + "description" : "Schema issuer DID", + "pattern" : "^(did:sov:)?[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}$" + }, + "schema_name" : { + "type" : "string", + "example" : "preferences", + "description" : "Schema name" + }, + "schema_version" : { + "type" : "string", + "example" : "1.0", + "description" : "Schema version", + "pattern" : "^[0-9.]+$" + }, + "trace" : { + "type" : "boolean", + "description" : "Record trace information, based on agent configuration" + } + } + }, + "V10CredentialExchange" : { + "type" : "object", + "properties" : { + "auto_issue" : { + "type" : "boolean", + "example" : false, + "description" : "Issuer choice to issue to request in this credential exchange" + }, + "auto_offer" : { + "type" : "boolean", + "example" : false, + "description" : "Holder choice to accept offer in this credential exchange" + }, + "auto_remove" : { + "type" : "boolean", + "example" : false, + "description" : "Issuer choice to remove this credential exchange record when complete" + }, + "connection_id" : { + "type" : "string", + "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "description" : "Connection identifier" + }, + "created_at" : { + "type" : "string", + "example" : "2021-12-31T23:59:59Z", + "description" : "Time of record creation", + "pattern" : "^\\d{4}-\\d\\d-\\d\\d[T ]\\d\\d:\\d\\d(?:\\:(?:\\d\\d(?:\\.\\d{1,6})?))?(?:[+-]\\d\\d:?\\d\\d|Z|)$" + }, + "credential" : { + "$ref" : "#/definitions/V10CredentialExchange_credential" + }, + "credential_definition_id" : { + "type" : "string", + "example" : "WgWxqztrNooG92RXvxSTWv:3:CL:20:tag", + "description" : "Credential definition identifier", + "pattern" : "^([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}):3:CL:(([1-9][0-9]*)|([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}:2:.+:[0-9.]+)):(.+)?$" + }, + "credential_exchange_id" : { + "type" : "string", + "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "description" : "Credential exchange identifier" + }, + "credential_id" : { + "type" : "string", + "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "description" : "Credential identifier" + }, + "credential_offer" : { + "$ref" : "#/definitions/V10CredentialExchange_credential_offer" + }, + "credential_offer_dict" : { + "$ref" : "#/definitions/V10CredentialExchange_credential_offer_dict" + }, + "credential_proposal_dict" : { + "$ref" : "#/definitions/V10CredentialExchange_credential_proposal_dict" + }, + "credential_request" : { + "$ref" : "#/definitions/V10CredentialExchange_credential_request" + }, + "credential_request_metadata" : { + "type" : "object", + "description" : "(Indy) credential request metadata", + "properties" : { } + }, + "error_msg" : { + "type" : "string", + "example" : "Credential definition identifier is not set in proposal", + "description" : "Error message" + }, + "initiator" : { + "type" : "string", + "example" : "self", + "description" : "Issue-credential exchange initiator: self or external", + "enum" : [ "self", "external" ] + }, + "parent_thread_id" : { + "type" : "string", + "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "description" : "Parent thread identifier" + }, + "raw_credential" : { + "$ref" : "#/definitions/V10CredentialExchange_raw_credential" + }, + "revoc_reg_id" : { + "type" : "string", + "description" : "Revocation registry identifier" + }, + "revocation_id" : { + "type" : "string", + "description" : "Credential identifier within revocation registry" + }, + "role" : { + "type" : "string", + "example" : "issuer", + "description" : "Issue-credential exchange role: holder or issuer", + "enum" : [ "holder", "issuer" ] + }, + "schema_id" : { + "type" : "string", + "example" : "WgWxqztrNooG92RXvxSTWv:2:schema_name:1.0", + "description" : "Schema identifier", + "pattern" : "^[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}:2:.+:[0-9.]+$" + }, + "state" : { + "type" : "string", + "example" : "credential_acked", + "description" : "Issue-credential exchange state" + }, + "thread_id" : { + "type" : "string", + "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "description" : "Thread identifier" + }, + "trace" : { + "type" : "boolean", + "description" : "Record trace information, based on agent configuration" + }, + "updated_at" : { + "type" : "string", + "example" : "2021-12-31T23:59:59Z", + "description" : "Time of last record update", + "pattern" : "^\\d{4}-\\d\\d-\\d\\d[T ]\\d\\d:\\d\\d(?:\\:(?:\\d\\d(?:\\.\\d{1,6})?))?(?:[+-]\\d\\d:?\\d\\d|Z|)$" + } + } + }, + "V10CredentialExchangeListResult" : { + "type" : "object", + "properties" : { + "results" : { + "type" : "array", + "description" : "Aries#0036 v1.0 credential exchange records", + "items" : { + "$ref" : "#/definitions/V10CredentialExchange" + } + } + } + }, + "V10CredentialFreeOfferRequest" : { + "type" : "object", + "required" : [ "connection_id", "cred_def_id", "credential_preview" ], + "properties" : { + "auto_issue" : { + "type" : "boolean", + "description" : "Whether to respond automatically to credential requests, creating and issuing requested credentials" + }, + "auto_remove" : { + "type" : "boolean", + "description" : "Whether to remove the credential exchange record on completion (overrides --preserve-exchange-records configuration setting)" + }, + "comment" : { + "type" : "string", + "description" : "Human-readable comment", + "x-nullable" : true + }, + "connection_id" : { + "type" : "string", + "format" : "uuid", + "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "description" : "Connection identifier" + }, + "cred_def_id" : { + "type" : "string", + "example" : "WgWxqztrNooG92RXvxSTWv:3:CL:20:tag", + "description" : "Credential definition identifier", + "pattern" : "^([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}):3:CL:(([1-9][0-9]*)|([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}:2:.+:[0-9.]+)):(.+)?$" + }, + "credential_preview" : { + "$ref" : "#/definitions/CredentialPreview" + }, + "trace" : { + "type" : "boolean", + "description" : "Record trace information, based on agent configuration" + } + } + }, + "V10CredentialIssueRequest" : { + "type" : "object", + "properties" : { + "comment" : { + "type" : "string", + "description" : "Human-readable comment", + "x-nullable" : true + } + } + }, + "V10CredentialProblemReportRequest" : { + "type" : "object", + "required" : [ "description" ], + "properties" : { + "description" : { + "type" : "string" + } + } + }, + "V10CredentialProposalRequestMand" : { + "type" : "object", + "required" : [ "connection_id", "credential_proposal" ], + "properties" : { + "auto_remove" : { + "type" : "boolean", + "description" : "Whether to remove the credential exchange record on completion (overrides --preserve-exchange-records configuration setting)" + }, + "comment" : { + "type" : "string", + "description" : "Human-readable comment", + "x-nullable" : true + }, + "connection_id" : { + "type" : "string", + "format" : "uuid", + "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "description" : "Connection identifier" + }, + "cred_def_id" : { + "type" : "string", + "example" : "WgWxqztrNooG92RXvxSTWv:3:CL:20:tag", + "description" : "Credential definition identifier", + "pattern" : "^([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}):3:CL:(([1-9][0-9]*)|([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}:2:.+:[0-9.]+)):(.+)?$" + }, + "credential_proposal" : { + "$ref" : "#/definitions/CredentialPreview" + }, + "issuer_did" : { + "type" : "string", + "example" : "WgWxqztrNooG92RXvxSTWv", + "description" : "Credential issuer DID", + "pattern" : "^(did:sov:)?[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}$" + }, + "schema_id" : { + "type" : "string", + "example" : "WgWxqztrNooG92RXvxSTWv:2:schema_name:1.0", + "description" : "Schema identifier", + "pattern" : "^[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}:2:.+:[0-9.]+$" + }, + "schema_issuer_did" : { + "type" : "string", + "example" : "WgWxqztrNooG92RXvxSTWv", + "description" : "Schema issuer DID", + "pattern" : "^(did:sov:)?[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}$" + }, + "schema_name" : { + "type" : "string", + "example" : "preferences", + "description" : "Schema name" + }, + "schema_version" : { + "type" : "string", + "example" : "1.0", + "description" : "Schema version", + "pattern" : "^[0-9.]+$" + }, + "trace" : { + "type" : "boolean", + "description" : "Record trace information, based on agent configuration" + } + } + }, + "V10CredentialProposalRequestOpt" : { + "type" : "object", + "required" : [ "connection_id" ], + "properties" : { + "auto_remove" : { + "type" : "boolean", + "description" : "Whether to remove the credential exchange record on completion (overrides --preserve-exchange-records configuration setting)" + }, + "comment" : { + "type" : "string", + "description" : "Human-readable comment", + "x-nullable" : true + }, + "connection_id" : { + "type" : "string", + "format" : "uuid", + "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "description" : "Connection identifier" + }, + "cred_def_id" : { + "type" : "string", + "example" : "WgWxqztrNooG92RXvxSTWv:3:CL:20:tag", + "description" : "Credential definition identifier", + "pattern" : "^([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}):3:CL:(([1-9][0-9]*)|([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}:2:.+:[0-9.]+)):(.+)?$" + }, + "credential_proposal" : { + "$ref" : "#/definitions/CredentialPreview" + }, + "issuer_did" : { + "type" : "string", + "example" : "WgWxqztrNooG92RXvxSTWv", + "description" : "Credential issuer DID", + "pattern" : "^(did:sov:)?[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}$" + }, + "schema_id" : { + "type" : "string", + "example" : "WgWxqztrNooG92RXvxSTWv:2:schema_name:1.0", + "description" : "Schema identifier", + "pattern" : "^[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}:2:.+:[0-9.]+$" + }, + "schema_issuer_did" : { + "type" : "string", + "example" : "WgWxqztrNooG92RXvxSTWv", + "description" : "Schema issuer DID", + "pattern" : "^(did:sov:)?[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}$" + }, + "schema_name" : { + "type" : "string", + "example" : "preferences", + "description" : "Schema name" + }, + "schema_version" : { + "type" : "string", + "example" : "1.0", + "description" : "Schema version", + "pattern" : "^[0-9.]+$" + }, + "trace" : { + "type" : "boolean", + "description" : "Record trace information, based on agent configuration" + } + } + }, + "V10CredentialStoreRequest" : { + "type" : "object", + "properties" : { + "credential_id" : { + "type" : "string" + } + } + }, + "V10DiscoveryExchangeListResult" : { + "type" : "object", + "properties" : { + "results" : { + "type" : "array", + "items" : { + "type" : "object", + "description" : "Discover Features v1.0 exchange record", + "allOf" : [ { + "$ref" : "#/definitions/V10DiscoveryRecord" + } ] + } + } + } + }, + "V10DiscoveryRecord" : { + "type" : "object", + "properties" : { + "connection_id" : { + "type" : "string", + "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "description" : "Connection identifier" + }, + "created_at" : { + "type" : "string", + "example" : "2021-12-31T23:59:59Z", + "description" : "Time of record creation", + "pattern" : "^\\d{4}-\\d\\d-\\d\\d[T ]\\d\\d:\\d\\d(?:\\:(?:\\d\\d(?:\\.\\d{1,6})?))?(?:[+-]\\d\\d:?\\d\\d|Z|)$" + }, + "disclose" : { + "$ref" : "#/definitions/V10DiscoveryRecord_disclose" + }, + "discovery_exchange_id" : { + "type" : "string", + "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "description" : "Credential exchange identifier" + }, + "query_msg" : { + "$ref" : "#/definitions/V10DiscoveryRecord_query_msg" + }, + "state" : { + "type" : "string", + "example" : "active", + "description" : "Current record state" + }, + "thread_id" : { + "type" : "string", + "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "description" : "Thread identifier" + }, + "trace" : { + "type" : "boolean", + "description" : "Record trace information, based on agent configuration" + }, + "updated_at" : { + "type" : "string", + "example" : "2021-12-31T23:59:59Z", + "description" : "Time of last record update", + "pattern" : "^\\d{4}-\\d\\d-\\d\\d[T ]\\d\\d:\\d\\d(?:\\:(?:\\d\\d(?:\\.\\d{1,6})?))?(?:[+-]\\d\\d:?\\d\\d|Z|)$" + } + } + }, + "V10PresentProofModuleResponse" : { + "type" : "object" + }, + "V10PresentationCreateRequestRequest" : { + "type" : "object", + "required" : [ "proof_request" ], + "properties" : { + "auto_verify" : { + "type" : "boolean", + "example" : false, + "description" : "Verifier choice to auto-verify proof presentation" + }, + "comment" : { + "type" : "string", + "x-nullable" : true + }, + "proof_request" : { + "$ref" : "#/definitions/IndyProofRequest" + }, + "trace" : { + "type" : "boolean", + "example" : false, + "description" : "Whether to trace event (default false)" + } + } + }, + "V10PresentationExchange" : { + "type" : "object", + "properties" : { + "auto_present" : { + "type" : "boolean", + "example" : false, + "description" : "Prover choice to auto-present proof as verifier requests" + }, + "auto_verify" : { + "type" : "boolean", + "description" : "Verifier choice to auto-verify proof presentation" + }, + "connection_id" : { + "type" : "string", + "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "description" : "Connection identifier" + }, + "created_at" : { + "type" : "string", + "example" : "2021-12-31T23:59:59Z", + "description" : "Time of record creation", + "pattern" : "^\\d{4}-\\d\\d-\\d\\d[T ]\\d\\d:\\d\\d(?:\\:(?:\\d\\d(?:\\.\\d{1,6})?))?(?:[+-]\\d\\d:?\\d\\d|Z|)$" + }, + "error_msg" : { + "type" : "string", + "example" : "Invalid structure", + "description" : "Error message" + }, + "initiator" : { + "type" : "string", + "example" : "self", + "description" : "Present-proof exchange initiator: self or external", + "enum" : [ "self", "external" ] + }, + "presentation" : { + "$ref" : "#/definitions/V10PresentationExchange_presentation" + }, + "presentation_exchange_id" : { + "type" : "string", + "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "description" : "Presentation exchange identifier" + }, + "presentation_proposal_dict" : { + "$ref" : "#/definitions/V10PresentationExchange_presentation_proposal_dict" + }, + "presentation_request" : { + "$ref" : "#/definitions/V10PresentationExchange_presentation_request" + }, + "presentation_request_dict" : { + "$ref" : "#/definitions/V10PresentationExchange_presentation_request_dict" + }, + "role" : { + "type" : "string", + "example" : "prover", + "description" : "Present-proof exchange role: prover or verifier", + "enum" : [ "prover", "verifier" ] + }, + "state" : { + "type" : "string", + "example" : "verified", + "description" : "Present-proof exchange state" + }, + "thread_id" : { + "type" : "string", + "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "description" : "Thread identifier" + }, + "trace" : { + "type" : "boolean", + "description" : "Record trace information, based on agent configuration" + }, + "updated_at" : { + "type" : "string", + "example" : "2021-12-31T23:59:59Z", + "description" : "Time of last record update", + "pattern" : "^\\d{4}-\\d\\d-\\d\\d[T ]\\d\\d:\\d\\d(?:\\:(?:\\d\\d(?:\\.\\d{1,6})?))?(?:[+-]\\d\\d:?\\d\\d|Z|)$" + }, + "verified" : { + "type" : "string", + "example" : "true", + "description" : "Whether presentation is verified: true or false", + "enum" : [ "true", "false" ] + }, + "verified_msgs" : { + "type" : "array", + "items" : { + "type" : "string", + "description" : "Proof verification warning or error information" + } + } + } + }, + "V10PresentationExchangeList" : { + "type" : "object", + "properties" : { + "results" : { + "type" : "array", + "description" : "Aries RFC 37 v1.0 presentation exchange records", + "items" : { + "$ref" : "#/definitions/V10PresentationExchange" + } + } + } + }, + "V10PresentationProblemReportRequest" : { + "type" : "object", + "required" : [ "description" ], + "properties" : { + "description" : { + "type" : "string" + } + } + }, + "V10PresentationProposalRequest" : { + "type" : "object", + "required" : [ "connection_id", "presentation_proposal" ], + "properties" : { + "auto_present" : { + "type" : "boolean", + "description" : "Whether to respond automatically to presentation requests, building and presenting requested proof" + }, + "comment" : { + "type" : "string", + "description" : "Human-readable comment", + "x-nullable" : true + }, + "connection_id" : { + "type" : "string", + "format" : "uuid", + "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "description" : "Connection identifier" + }, + "presentation_proposal" : { + "$ref" : "#/definitions/IndyPresPreview" + }, + "trace" : { + "type" : "boolean", + "example" : false, + "description" : "Whether to trace event (default false)" + } + } + }, + "V10PresentationSendRequestRequest" : { + "type" : "object", + "required" : [ "connection_id", "proof_request" ], + "properties" : { + "auto_verify" : { + "type" : "boolean", + "example" : false, + "description" : "Verifier choice to auto-verify proof presentation" + }, + "comment" : { + "type" : "string", + "x-nullable" : true + }, + "connection_id" : { + "type" : "string", + "format" : "uuid", + "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "description" : "Connection identifier" + }, + "proof_request" : { + "$ref" : "#/definitions/IndyProofRequest" + }, + "trace" : { + "type" : "boolean", + "example" : false, + "description" : "Whether to trace event (default false)" + } + } + }, + "V10PresentationSendRequestToProposal" : { + "type" : "object", + "properties" : { + "auto_verify" : { + "type" : "boolean", + "example" : false, + "description" : "Verifier choice to auto-verify proof presentation" + }, + "trace" : { + "type" : "boolean", + "example" : false, + "description" : "Whether to trace event (default false)" + } + } + }, + "V20CredAttrSpec" : { + "type" : "object", + "required" : [ "name", "value" ], + "properties" : { + "mime-type" : { + "type" : "string", + "example" : "image/jpeg", + "description" : "MIME type: omit for (null) default", + "x-nullable" : true + }, + "name" : { + "type" : "string", + "example" : "favourite_drink", + "description" : "Attribute name" + }, + "value" : { + "type" : "string", + "example" : "martini", + "description" : "Attribute value: base64-encode if MIME type is present" + } + } + }, + "V20CredBoundOfferRequest" : { + "type" : "object", + "properties" : { + "counter_preview" : { + "$ref" : "#/definitions/V20CredBoundOfferRequest_counter_preview" + }, + "filter" : { + "$ref" : "#/definitions/V20CredBoundOfferRequest_filter" + } + } + }, + "V20CredExFree" : { + "type" : "object", + "required" : [ "connection_id", "filter" ], + "properties" : { + "auto_remove" : { + "type" : "boolean", + "description" : "Whether to remove the credential exchange record on completion (overrides --preserve-exchange-records configuration setting)" + }, + "comment" : { + "type" : "string", + "description" : "Human-readable comment", + "x-nullable" : true + }, + "connection_id" : { + "type" : "string", + "format" : "uuid", + "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "description" : "Connection identifier" + }, + "credential_preview" : { + "$ref" : "#/definitions/V20CredPreview" + }, + "filter" : { + "$ref" : "#/definitions/V20CredBoundOfferRequest_filter" + }, + "trace" : { + "type" : "boolean", + "description" : "Record trace information, based on agent configuration" + }, + "verification_method" : { + "type" : "string", + "description" : "For ld-proofs. Verification method for signing.", + "x-nullable" : true + } + } + }, + "V20CredExRecord" : { + "type" : "object", + "properties" : { + "auto_issue" : { + "type" : "boolean", + "example" : false, + "description" : "Issuer choice to issue to request in this credential exchange" + }, + "auto_offer" : { + "type" : "boolean", + "example" : false, + "description" : "Holder choice to accept offer in this credential exchange" + }, + "auto_remove" : { + "type" : "boolean", + "example" : false, + "description" : "Issuer choice to remove this credential exchange record when complete" + }, + "by_format" : { + "$ref" : "#/definitions/V20CredExRecord_by_format" + }, + "connection_id" : { + "type" : "string", + "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "description" : "Connection identifier" + }, + "created_at" : { + "type" : "string", + "example" : "2021-12-31T23:59:59Z", + "description" : "Time of record creation", + "pattern" : "^\\d{4}-\\d\\d-\\d\\d[T ]\\d\\d:\\d\\d(?:\\:(?:\\d\\d(?:\\.\\d{1,6})?))?(?:[+-]\\d\\d:?\\d\\d|Z|)$" + }, + "cred_ex_id" : { + "type" : "string", + "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "description" : "Credential exchange identifier" + }, + "cred_issue" : { + "$ref" : "#/definitions/V20CredExRecord_cred_issue" + }, + "cred_offer" : { + "$ref" : "#/definitions/V10CredentialExchange_credential_offer_dict" + }, + "cred_preview" : { + "$ref" : "#/definitions/V20CredExRecord_cred_preview" + }, + "cred_proposal" : { + "$ref" : "#/definitions/V10CredentialExchange_credential_proposal_dict" + }, + "cred_request" : { + "$ref" : "#/definitions/V20CredExRecord_cred_request" + }, + "error_msg" : { + "type" : "string", + "example" : "The front fell off", + "description" : "Error message" + }, + "initiator" : { + "type" : "string", + "example" : "self", + "description" : "Issue-credential exchange initiator: self or external", + "enum" : [ "self", "external" ] + }, + "parent_thread_id" : { + "type" : "string", + "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "description" : "Parent thread identifier" + }, + "role" : { + "type" : "string", + "example" : "issuer", + "description" : "Issue-credential exchange role: holder or issuer", + "enum" : [ "issuer", "holder" ] + }, + "state" : { + "type" : "string", + "example" : "done", + "description" : "Issue-credential exchange state", + "enum" : [ "proposal-sent", "proposal-received", "offer-sent", "offer-received", "request-sent", "request-received", "credential-issued", "credential-received", "done", "credential-revoked", "abandoned", "deleted" ] + }, + "thread_id" : { + "type" : "string", + "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "description" : "Thread identifier" + }, + "trace" : { + "type" : "boolean", + "description" : "Record trace information, based on agent configuration" + }, + "updated_at" : { + "type" : "string", + "example" : "2021-12-31T23:59:59Z", + "description" : "Time of last record update", + "pattern" : "^\\d{4}-\\d\\d-\\d\\d[T ]\\d\\d:\\d\\d(?:\\:(?:\\d\\d(?:\\.\\d{1,6})?))?(?:[+-]\\d\\d:?\\d\\d|Z|)$" + } + } + }, + "V20CredExRecordByFormat" : { + "type" : "object", + "properties" : { + "cred_issue" : { + "type" : "object", + "properties" : { } + }, + "cred_offer" : { + "type" : "object", + "properties" : { } + }, + "cred_proposal" : { + "type" : "object", + "properties" : { } + }, + "cred_request" : { + "type" : "object", + "properties" : { } + } + } + }, + "V20CredExRecordDetail" : { + "type" : "object", + "properties" : { + "cred_ex_record" : { + "$ref" : "#/definitions/V20CredExRecordDetail_cred_ex_record" + }, + "indy" : { + "$ref" : "#/definitions/V20CredExRecordIndy" + }, + "ld_proof" : { + "$ref" : "#/definitions/V20CredExRecordLDProof" + } + } + }, + "V20CredExRecordIndy" : { + "type" : "object", + "properties" : { + "created_at" : { + "type" : "string", + "example" : "2021-12-31T23:59:59Z", + "description" : "Time of record creation", + "pattern" : "^\\d{4}-\\d\\d-\\d\\d[T ]\\d\\d:\\d\\d(?:\\:(?:\\d\\d(?:\\.\\d{1,6})?))?(?:[+-]\\d\\d:?\\d\\d|Z|)$" + }, + "cred_ex_id" : { + "type" : "string", + "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "description" : "Corresponding v2.0 credential exchange record identifier" + }, + "cred_ex_indy_id" : { + "type" : "string", + "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "description" : "Record identifier" + }, + "cred_id_stored" : { + "type" : "string", + "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "description" : "Credential identifier stored in wallet" + }, + "cred_request_metadata" : { + "type" : "object", + "description" : "Credential request metadata for indy holder", + "properties" : { } + }, + "cred_rev_id" : { + "type" : "string", + "example" : "12345", + "description" : "Credential revocation identifier within revocation registry", + "pattern" : "^[1-9][0-9]*$" + }, + "rev_reg_id" : { + "type" : "string", + "example" : "WgWxqztrNooG92RXvxSTWv:4:WgWxqztrNooG92RXvxSTWv:3:CL:20:tag:CL_ACCUM:0", + "description" : "Revocation registry identifier", + "pattern" : "^([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}):4:([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}):3:CL:(([1-9][0-9]*)|([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}:2:.+:[0-9.]+))(:.+)?:CL_ACCUM:(.+$)" + }, + "state" : { + "type" : "string", + "example" : "active", + "description" : "Current record state" + }, + "updated_at" : { + "type" : "string", + "example" : "2021-12-31T23:59:59Z", + "description" : "Time of last record update", + "pattern" : "^\\d{4}-\\d\\d-\\d\\d[T ]\\d\\d:\\d\\d(?:\\:(?:\\d\\d(?:\\.\\d{1,6})?))?(?:[+-]\\d\\d:?\\d\\d|Z|)$" + } + } + }, + "V20CredExRecordLDProof" : { + "type" : "object", + "properties" : { + "created_at" : { + "type" : "string", + "example" : "2021-12-31T23:59:59Z", + "description" : "Time of record creation", + "pattern" : "^\\d{4}-\\d\\d-\\d\\d[T ]\\d\\d:\\d\\d(?:\\:(?:\\d\\d(?:\\.\\d{1,6})?))?(?:[+-]\\d\\d:?\\d\\d|Z|)$" + }, + "cred_ex_id" : { + "type" : "string", + "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "description" : "Corresponding v2.0 credential exchange record identifier" + }, + "cred_ex_ld_proof_id" : { + "type" : "string", + "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "description" : "Record identifier" + }, + "cred_id_stored" : { + "type" : "string", + "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "description" : "Credential identifier stored in wallet" + }, + "state" : { + "type" : "string", + "example" : "active", + "description" : "Current record state" + }, + "updated_at" : { + "type" : "string", + "example" : "2021-12-31T23:59:59Z", + "description" : "Time of last record update", + "pattern" : "^\\d{4}-\\d\\d-\\d\\d[T ]\\d\\d:\\d\\d(?:\\:(?:\\d\\d(?:\\.\\d{1,6})?))?(?:[+-]\\d\\d:?\\d\\d|Z|)$" + } + } + }, + "V20CredExRecordListResult" : { + "type" : "object", + "properties" : { + "results" : { + "type" : "array", + "description" : "Credential exchange records and corresponding detail records", + "items" : { + "$ref" : "#/definitions/V20CredExRecordDetail" + } + } + } + }, + "V20CredFilter" : { + "type" : "object", + "properties" : { + "indy" : { + "$ref" : "#/definitions/V20CredFilter_indy" + }, + "ld_proof" : { + "$ref" : "#/definitions/V20CredFilter_ld_proof" + } + } + }, + "V20CredFilterIndy" : { + "type" : "object", + "properties" : { + "cred_def_id" : { + "type" : "string", + "example" : "WgWxqztrNooG92RXvxSTWv:3:CL:20:tag", + "description" : "Credential definition identifier", + "pattern" : "^([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}):3:CL:(([1-9][0-9]*)|([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}:2:.+:[0-9.]+)):(.+)?$" + }, + "issuer_did" : { + "type" : "string", + "example" : "WgWxqztrNooG92RXvxSTWv", + "description" : "Credential issuer DID", + "pattern" : "^(did:sov:)?[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}$" + }, + "schema_id" : { + "type" : "string", + "example" : "WgWxqztrNooG92RXvxSTWv:2:schema_name:1.0", + "description" : "Schema identifier", + "pattern" : "^[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}:2:.+:[0-9.]+$" + }, + "schema_issuer_did" : { + "type" : "string", + "example" : "WgWxqztrNooG92RXvxSTWv", + "description" : "Schema issuer DID", + "pattern" : "^(did:sov:)?[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}$" + }, + "schema_name" : { + "type" : "string", + "example" : "preferences", + "description" : "Schema name" + }, + "schema_version" : { + "type" : "string", + "example" : "1.0", + "description" : "Schema version", + "pattern" : "^[0-9.]+$" + } + } + }, + "V20CredFilterLDProof" : { + "type" : "object", + "required" : [ "ld_proof" ], + "properties" : { + "ld_proof" : { + "$ref" : "#/definitions/V20CredFilter_ld_proof" + } + } + }, + "V20CredFormat" : { + "type" : "object", + "required" : [ "attach_id", "format" ], + "properties" : { + "attach_id" : { + "type" : "string", + "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "description" : "Attachment identifier" + }, + "format" : { + "type" : "string", + "example" : "aries/ld-proof-vc-detail@v1.0", + "description" : "Attachment format specifier" + } + } + }, + "V20CredIssue" : { + "type" : "object", + "required" : [ "credentials~attach", "formats" ], + "properties" : { + "@id" : { + "type" : "string", + "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "description" : "Message identifier" + }, + "@type" : { + "type" : "string", + "example" : "https://didcomm.org/my-family/1.0/my-message-type", + "description" : "Message type", + "readOnly" : true + }, + "comment" : { + "type" : "string", + "description" : "Human-readable comment", + "x-nullable" : true + }, + "credentials~attach" : { + "type" : "array", + "description" : "Credential attachments", + "items" : { + "$ref" : "#/definitions/AttachDecorator" + } + }, + "formats" : { + "type" : "array", + "description" : "Acceptable attachment formats", + "items" : { + "$ref" : "#/definitions/V20CredFormat" + } + }, + "replacement_id" : { + "type" : "string", + "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "description" : "Issuer-unique identifier to coordinate credential replacement" + } + } + }, + "V20CredIssueProblemReportRequest" : { + "type" : "object", + "required" : [ "description" ], + "properties" : { + "description" : { + "type" : "string" + } + } + }, + "V20CredIssueRequest" : { + "type" : "object", + "properties" : { + "comment" : { + "type" : "string", + "description" : "Human-readable comment", + "x-nullable" : true + } + } + }, + "V20CredOffer" : { + "type" : "object", + "required" : [ "formats", "offers~attach" ], + "properties" : { + "@id" : { + "type" : "string", + "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "description" : "Message identifier" + }, + "@type" : { + "type" : "string", + "example" : "https://didcomm.org/my-family/1.0/my-message-type", + "description" : "Message type", + "readOnly" : true + }, + "comment" : { + "type" : "string", + "description" : "Human-readable comment", + "x-nullable" : true + }, + "credential_preview" : { + "$ref" : "#/definitions/V20CredPreview" + }, + "formats" : { + "type" : "array", + "description" : "Acceptable credential formats", + "items" : { + "$ref" : "#/definitions/V20CredFormat" + } + }, + "offers~attach" : { + "type" : "array", + "description" : "Offer attachments", + "items" : { + "$ref" : "#/definitions/AttachDecorator" + } + }, + "replacement_id" : { + "type" : "string", + "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "description" : "Issuer-unique identifier to coordinate credential replacement" + } + } + }, + "V20CredOfferConnFreeRequest" : { + "type" : "object", + "required" : [ "filter" ], + "properties" : { + "auto_issue" : { + "type" : "boolean", + "description" : "Whether to respond automatically to credential requests, creating and issuing requested credentials" + }, + "auto_remove" : { + "type" : "boolean", + "description" : "Whether to remove the credential exchange record on completion (overrides --preserve-exchange-records configuration setting)" + }, + "comment" : { + "type" : "string", + "description" : "Human-readable comment", + "x-nullable" : true + }, + "credential_preview" : { + "$ref" : "#/definitions/V20CredPreview" + }, + "filter" : { + "$ref" : "#/definitions/V20CredBoundOfferRequest_filter" + }, + "trace" : { + "type" : "boolean", + "description" : "Record trace information, based on agent configuration" + } + } + }, + "V20CredOfferRequest" : { + "type" : "object", + "required" : [ "connection_id", "filter" ], + "properties" : { + "auto_issue" : { + "type" : "boolean", + "description" : "Whether to respond automatically to credential requests, creating and issuing requested credentials" + }, + "auto_remove" : { + "type" : "boolean", + "description" : "Whether to remove the credential exchange record on completion (overrides --preserve-exchange-records configuration setting)" + }, + "comment" : { + "type" : "string", + "description" : "Human-readable comment", + "x-nullable" : true + }, + "connection_id" : { + "type" : "string", + "format" : "uuid", + "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "description" : "Connection identifier" + }, + "credential_preview" : { + "$ref" : "#/definitions/V20CredPreview" + }, + "filter" : { + "$ref" : "#/definitions/V20CredBoundOfferRequest_filter" + }, + "trace" : { + "type" : "boolean", + "description" : "Record trace information, based on agent configuration" + }, + "thread_id" : { + "type" : "string", + "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "description" : "thread identifier" + } + } + }, + "V20CredPreview" : { + "type" : "object", + "required" : [ "attributes" ], + "properties" : { + "@type" : { + "type" : "string", + "example" : "issue-credential/2.0/credential-preview", + "description" : "Message type identifier" + }, + "attributes" : { + "type" : "array", + "items" : { + "$ref" : "#/definitions/V20CredAttrSpec" + } + } + } + }, + "V20CredProposal" : { + "type" : "object", + "required" : [ "filters~attach", "formats" ], + "properties" : { + "@id" : { + "type" : "string", + "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "description" : "Message identifier" + }, + "@type" : { + "type" : "string", + "example" : "https://didcomm.org/my-family/1.0/my-message-type", + "description" : "Message type", + "readOnly" : true + }, + "comment" : { + "type" : "string", + "description" : "Human-readable comment", + "x-nullable" : true + }, + "credential_preview" : { + "$ref" : "#/definitions/V20CredProposal_credential_preview" + }, + "filters~attach" : { + "type" : "array", + "description" : "Credential filter per acceptable format on corresponding identifier", + "items" : { + "$ref" : "#/definitions/AttachDecorator" + } + }, + "formats" : { + "type" : "array", + "description" : "Attachment formats", + "items" : { + "$ref" : "#/definitions/V20CredFormat" + } + } + } + }, + "V20CredRequest" : { + "type" : "object", + "required" : [ "formats", "requests~attach" ], + "properties" : { + "@id" : { + "type" : "string", + "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "description" : "Message identifier" + }, + "@type" : { + "type" : "string", + "example" : "https://didcomm.org/my-family/1.0/my-message-type", + "description" : "Message type", + "readOnly" : true + }, + "comment" : { + "type" : "string", + "description" : "Human-readable comment", + "x-nullable" : true + }, + "formats" : { + "type" : "array", + "description" : "Acceptable attachment formats", + "items" : { + "$ref" : "#/definitions/V20CredFormat" + } + }, + "requests~attach" : { + "type" : "array", + "description" : "Request attachments", + "items" : { + "$ref" : "#/definitions/AttachDecorator" + } + } + } + }, + "V20CredRequestFree" : { + "type" : "object", + "required" : [ "connection_id", "filter" ], + "properties" : { + "auto_remove" : { + "type" : "boolean", + "description" : "Whether to remove the credential exchange record on completion (overrides --preserve-exchange-records configuration setting)" + }, + "comment" : { + "type" : "string", + "description" : "Human-readable comment", + "x-nullable" : true + }, + "connection_id" : { + "type" : "string", + "format" : "uuid", + "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "description" : "Connection identifier" + }, + "filter" : { + "$ref" : "#/definitions/V20CredBoundOfferRequest_filter" + }, + "holder_did" : { + "type" : "string", + "example" : "did:key:ahsdkjahsdkjhaskjdhakjshdkajhsdkjahs", + "description" : "Holder DID to substitute for the credentialSubject.id", + "x-nullable" : true + }, + "trace" : { + "type" : "boolean", + "example" : false, + "description" : "Whether to trace event (default false)" + } + } + }, + "V20CredRequestRequest" : { + "type" : "object", + "properties" : { + "holder_did" : { + "type" : "string", + "example" : "did:key:ahsdkjahsdkjhaskjdhakjshdkajhsdkjahs", + "description" : "Holder DID to substitute for the credentialSubject.id", + "x-nullable" : true + } + } + }, + "V20CredStoreRequest" : { + "type" : "object", + "properties" : { + "credential_id" : { + "type" : "string" + } + } + }, + "V20DiscoveryExchangeListResult" : { + "type" : "object", + "properties" : { + "results" : { + "type" : "array", + "items" : { + "type" : "object", + "description" : "Discover Features v2.0 exchange record", + "allOf" : [ { + "$ref" : "#/definitions/V20DiscoveryRecord" + } ] + } + } + } + }, + "V20DiscoveryExchangeResult" : { + "type" : "object", + "properties" : { + "results" : { + "$ref" : "#/definitions/V20DiscoveryExchangeResult_results" + } + } + }, + "V20DiscoveryRecord" : { + "type" : "object", + "properties" : { + "connection_id" : { + "type" : "string", + "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "description" : "Connection identifier" + }, + "created_at" : { + "type" : "string", + "example" : "2021-12-31T23:59:59Z", + "description" : "Time of record creation", + "pattern" : "^\\d{4}-\\d\\d-\\d\\d[T ]\\d\\d:\\d\\d(?:\\:(?:\\d\\d(?:\\.\\d{1,6})?))?(?:[+-]\\d\\d:?\\d\\d|Z|)$" + }, + "disclosures" : { + "$ref" : "#/definitions/V20DiscoveryRecord_disclosures" + }, + "discovery_exchange_id" : { + "type" : "string", + "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "description" : "Credential exchange identifier" + }, + "queries_msg" : { + "$ref" : "#/definitions/V20DiscoveryRecord_queries_msg" + }, + "state" : { + "type" : "string", + "example" : "active", + "description" : "Current record state" + }, + "thread_id" : { + "type" : "string", + "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "description" : "Thread identifier" + }, + "trace" : { + "type" : "boolean", + "description" : "Record trace information, based on agent configuration" + }, + "updated_at" : { + "type" : "string", + "example" : "2021-12-31T23:59:59Z", + "description" : "Time of last record update", + "pattern" : "^\\d{4}-\\d\\d-\\d\\d[T ]\\d\\d:\\d\\d(?:\\:(?:\\d\\d(?:\\.\\d{1,6})?))?(?:[+-]\\d\\d:?\\d\\d|Z|)$" + } + } + }, + "V20IssueCredSchemaCore" : { + "type" : "object", + "required" : [ "filter" ], + "properties" : { + "auto_remove" : { + "type" : "boolean", + "description" : "Whether to remove the credential exchange record on completion (overrides --preserve-exchange-records configuration setting)" + }, + "comment" : { + "type" : "string", + "description" : "Human-readable comment", + "x-nullable" : true + }, + "credential_preview" : { + "$ref" : "#/definitions/V20CredPreview" + }, + "filter" : { + "$ref" : "#/definitions/V20CredBoundOfferRequest_filter" + }, + "trace" : { + "type" : "boolean", + "description" : "Record trace information, based on agent configuration" + } + } + }, + "V20IssueCredentialModuleResponse" : { + "type" : "object" + }, + "V20Pres" : { + "type" : "object", + "required" : [ "formats", "presentations~attach" ], + "properties" : { + "@id" : { + "type" : "string", + "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "description" : "Message identifier" + }, + "@type" : { + "type" : "string", + "example" : "https://didcomm.org/my-family/1.0/my-message-type", + "description" : "Message type", + "readOnly" : true + }, + "comment" : { + "type" : "string", + "description" : "Human-readable comment", + "x-nullable" : true + }, + "formats" : { + "type" : "array", + "description" : "Acceptable attachment formats", + "items" : { + "$ref" : "#/definitions/V20PresFormat" + } + }, + "presentations~attach" : { + "type" : "array", + "items" : { + "$ref" : "#/definitions/AttachDecorator" + } + } + } + }, + "V20PresCreateRequestRequest" : { + "type" : "object", + "required" : [ "presentation_request" ], + "properties" : { + "auto_verify" : { + "type" : "boolean", + "example" : false, + "description" : "Verifier choice to auto-verify proof presentation" + }, + "comment" : { + "type" : "string", + "x-nullable" : true + }, + "presentation_request" : { + "$ref" : "#/definitions/V20PresRequestByFormat" + }, + "trace" : { + "type" : "boolean", + "example" : false, + "description" : "Whether to trace event (default false)" + } + } + }, + "V20PresExRecord" : { + "type" : "object", + "properties" : { + "auto_present" : { + "type" : "boolean", + "example" : false, + "description" : "Prover choice to auto-present proof as verifier requests" + }, + "auto_verify" : { + "type" : "boolean", + "description" : "Verifier choice to auto-verify proof presentation" + }, + "by_format" : { + "$ref" : "#/definitions/V20PresExRecord_by_format" + }, + "connection_id" : { + "type" : "string", + "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "description" : "Connection identifier" + }, + "created_at" : { + "type" : "string", + "example" : "2021-12-31T23:59:59Z", + "description" : "Time of record creation", + "pattern" : "^\\d{4}-\\d\\d-\\d\\d[T ]\\d\\d:\\d\\d(?:\\:(?:\\d\\d(?:\\.\\d{1,6})?))?(?:[+-]\\d\\d:?\\d\\d|Z|)$" + }, + "error_msg" : { + "type" : "string", + "example" : "Invalid structure", + "description" : "Error message" + }, + "initiator" : { + "type" : "string", + "example" : "self", + "description" : "Present-proof exchange initiator: self or external", + "enum" : [ "self", "external" ] + }, + "pres" : { + "$ref" : "#/definitions/V20PresExRecord_pres" + }, + "pres_ex_id" : { + "type" : "string", + "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "description" : "Presentation exchange identifier" + }, + "pres_proposal" : { + "$ref" : "#/definitions/V10PresentationExchange_presentation_proposal_dict" + }, + "pres_request" : { + "$ref" : "#/definitions/V10PresentationExchange_presentation_request_dict" + }, + "role" : { + "type" : "string", + "example" : "prover", + "description" : "Present-proof exchange role: prover or verifier", + "enum" : [ "prover", "verifier" ] + }, + "state" : { + "type" : "string", + "description" : "Present-proof exchange state", + "enum" : [ "proposal-sent", "proposal-received", "request-sent", "request-received", "presentation-sent", "presentation-received", "done", "abandoned", "deleted" ] + }, + "thread_id" : { + "type" : "string", + "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "description" : "Thread identifier" + }, + "trace" : { + "type" : "boolean", + "description" : "Record trace information, based on agent configuration" + }, + "updated_at" : { + "type" : "string", + "example" : "2021-12-31T23:59:59Z", + "description" : "Time of last record update", + "pattern" : "^\\d{4}-\\d\\d-\\d\\d[T ]\\d\\d:\\d\\d(?:\\:(?:\\d\\d(?:\\.\\d{1,6})?))?(?:[+-]\\d\\d:?\\d\\d|Z|)$" + }, + "verified" : { + "type" : "string", + "example" : "true", + "description" : "Whether presentation is verified: 'true' or 'false'", + "enum" : [ "true", "false" ] + }, + "verified_msgs" : { + "type" : "array", + "items" : { + "type" : "string", + "description" : "Proof verification warning or error information" + } + } + } + }, + "V20PresExRecordByFormat" : { + "type" : "object", + "properties" : { + "pres" : { + "type" : "object", + "properties" : { } + }, + "pres_proposal" : { + "type" : "object", + "properties" : { } + }, + "pres_request" : { + "type" : "object", + "properties" : { } + } + } + }, + "V20PresExRecordList" : { + "type" : "object", + "properties" : { + "results" : { + "type" : "array", + "description" : "Presentation exchange records", + "items" : { + "$ref" : "#/definitions/V20PresExRecord" + } + } + } + }, + "V20PresFormat" : { + "type" : "object", + "required" : [ "attach_id", "format" ], + "properties" : { + "attach_id" : { + "type" : "string", + "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "description" : "Attachment identifier" + }, + "format" : { + "type" : "string", + "example" : "dif/presentation-exchange/submission@v1.0", + "description" : "Attachment format specifier" + } + } + }, + "V20PresProblemReportRequest" : { + "type" : "object", + "required" : [ "description" ], + "properties" : { + "description" : { + "type" : "string" + } + } + }, + "V20PresProposal" : { + "type" : "object", + "required" : [ "formats", "proposals~attach" ], + "properties" : { + "@id" : { + "type" : "string", + "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "description" : "Message identifier" + }, + "@type" : { + "type" : "string", + "example" : "https://didcomm.org/my-family/1.0/my-message-type", + "description" : "Message type", + "readOnly" : true + }, + "comment" : { + "type" : "string", + "description" : "Human-readable comment" + }, + "formats" : { + "type" : "array", + "items" : { + "$ref" : "#/definitions/V20PresFormat" + } + }, + "proposals~attach" : { + "type" : "array", + "description" : "Attachment per acceptable format on corresponding identifier", + "items" : { + "$ref" : "#/definitions/AttachDecorator" + } + } + } + }, + "V20PresProposalByFormat" : { + "type" : "object", + "properties" : { + "dif" : { + "$ref" : "#/definitions/V20PresProposalByFormat_dif" + }, + "indy" : { + "$ref" : "#/definitions/V20PresProposalByFormat_indy" + } + } + }, + "V20PresProposalRequest" : { + "type" : "object", + "required" : [ "connection_id", "presentation_proposal" ], + "properties" : { + "auto_present" : { + "type" : "boolean", + "description" : "Whether to respond automatically to presentation requests, building and presenting requested proof" + }, + "comment" : { + "type" : "string", + "description" : "Human-readable comment", + "x-nullable" : true + }, + "connection_id" : { + "type" : "string", + "format" : "uuid", + "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "description" : "Connection identifier" + }, + "presentation_proposal" : { + "$ref" : "#/definitions/V20PresProposalByFormat" + }, + "trace" : { + "type" : "boolean", + "example" : false, + "description" : "Whether to trace event (default false)" + } + } + }, + "V20PresRequest" : { + "type" : "object", + "required" : [ "formats", "request_presentations~attach" ], + "properties" : { + "@id" : { + "type" : "string", + "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "description" : "Message identifier" + }, + "@type" : { + "type" : "string", + "example" : "https://didcomm.org/my-family/1.0/my-message-type", + "description" : "Message type", + "readOnly" : true + }, + "comment" : { + "type" : "string", + "description" : "Human-readable comment" + }, + "formats" : { + "type" : "array", + "items" : { + "$ref" : "#/definitions/V20PresFormat" + } + }, + "request_presentations~attach" : { + "type" : "array", + "description" : "Attachment per acceptable format on corresponding identifier", + "items" : { + "$ref" : "#/definitions/AttachDecorator" + } + }, + "will_confirm" : { + "type" : "boolean", + "description" : "Whether verifier will send confirmation ack" + } + } + }, + "V20PresRequestByFormat" : { + "type" : "object", + "properties" : { + "dif" : { + "$ref" : "#/definitions/V20PresRequestByFormat_dif" + }, + "indy" : { + "$ref" : "#/definitions/V20PresRequestByFormat_indy" + } + } + }, + "V20PresSendRequestRequest" : { + "type" : "object", + "required" : [ "connection_id", "presentation_request" ], + "properties" : { + "auto_verify" : { + "type" : "boolean", + "example" : false, + "description" : "Verifier choice to auto-verify proof presentation" + }, + "comment" : { + "type" : "string", + "x-nullable" : true + }, + "connection_id" : { + "type" : "string", + "format" : "uuid", + "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "description" : "Connection identifier" + }, + "presentation_request" : { + "$ref" : "#/definitions/V20PresRequestByFormat" + }, + "trace" : { + "type" : "boolean", + "example" : false, + "description" : "Whether to trace event (default false)" + } + } + }, + "V20PresSpecByFormatRequest" : { + "type" : "object", + "properties" : { + "dif" : { + "$ref" : "#/definitions/V20PresSpecByFormatRequest_dif" + }, + "indy" : { + "$ref" : "#/definitions/V20PresSpecByFormatRequest_indy" + }, + "trace" : { + "type" : "boolean", + "description" : "Record trace information, based on agent configuration" + } + } + }, + "V20PresentProofModuleResponse" : { + "type" : "object" + }, + "V20PresentationSendRequestToProposal" : { + "type" : "object", + "properties" : { + "auto_verify" : { + "type" : "boolean", + "example" : false, + "description" : "Verifier choice to auto-verify proof presentation" + }, + "trace" : { + "type" : "boolean", + "example" : false, + "description" : "Whether to trace event (default false)" + } + } + }, + "VCRecord" : { + "type" : "object", + "properties" : { + "contexts" : { + "type" : "array", + "items" : { + "type" : "string", + "example" : "https://myhost:8021", + "description" : "Context", + "pattern" : "^[A-Za-z0-9\\.\\-\\+]+://([A-Za-z0-9][.A-Za-z0-9-_]+[A-Za-z0-9])+(:[1-9][0-9]*)?(/[^?&#]+)?$" + } + }, + "cred_tags" : { + "type" : "object", + "additionalProperties" : { + "type" : "string", + "description" : "Retrieval tag value" + } + }, + "cred_value" : { + "type" : "object", + "description" : "(JSON-serializable) credential value", + "properties" : { } + }, + "expanded_types" : { + "type" : "array", + "items" : { + "type" : "string", + "example" : "https://w3id.org/citizenship#PermanentResidentCard", + "description" : "JSON-LD expanded type extracted from type and context" + } + }, + "given_id" : { + "type" : "string", + "example" : "http://example.edu/credentials/3732", + "description" : "Credential identifier" + }, + "issuer_id" : { + "type" : "string", + "example" : "https://example.edu/issuers/14", + "description" : "Issuer identifier" + }, + "proof_types" : { + "type" : "array", + "items" : { + "type" : "string", + "example" : "Ed25519Signature2018", + "description" : "Signature suite used for proof" + } + }, + "record_id" : { + "type" : "string", + "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "description" : "Record identifier" + }, + "schema_ids" : { + "type" : "array", + "items" : { + "type" : "string", + "example" : "https://example.org/examples/degree.json", + "description" : "Schema identifier" + } + }, + "subject_ids" : { + "type" : "array", + "items" : { + "type" : "string", + "example" : "did:example:ebfeb1f712ebc6f1c276e12ec21", + "description" : "Subject identifier" + } + } + } + }, + "VCRecordList" : { + "type" : "object", + "properties" : { + "results" : { + "type" : "array", + "items" : { + "$ref" : "#/definitions/VCRecord" + } + } + } + }, + "VerifyRequest" : { + "type" : "object", + "required" : [ "doc" ], + "properties" : { + "doc" : { + "$ref" : "#/definitions/VerifyRequest_doc" + }, + "verkey" : { + "type" : "string", + "description" : "Verkey to use for doc verification" + } + } + }, + "VerifyResponse" : { + "type" : "object", + "required" : [ "valid" ], + "properties" : { + "error" : { + "type" : "string", + "description" : "Error text" + }, + "valid" : { + "type" : "boolean" + } + } + }, + "W3CCredentialsListRequest" : { + "type" : "object", + "properties" : { + "contexts" : { + "type" : "array", + "items" : { + "type" : "string", + "example" : "https://myhost:8021", + "description" : "Credential context to match", + "pattern" : "^[A-Za-z0-9\\.\\-\\+]+://([A-Za-z0-9][.A-Za-z0-9-_]+[A-Za-z0-9])+(:[1-9][0-9]*)?(/[^?&#]+)?$" + } + }, + "given_id" : { + "type" : "string", + "description" : "Given credential id to match" + }, + "issuer_id" : { + "type" : "string", + "description" : "Credential issuer identifier to match" + }, + "max_results" : { + "type" : "integer", + "format" : "int32", + "description" : "Maximum number of results to return" + }, + "proof_types" : { + "type" : "array", + "items" : { + "type" : "string", + "example" : "Ed25519Signature2018", + "description" : "Signature suite used for proof" + } + }, + "schema_ids" : { + "type" : "array", + "description" : "Schema identifiers, all of which to match", + "items" : { + "type" : "string", + "example" : "https://myhost:8021", + "description" : "Credential schema identifier", + "pattern" : "^[A-Za-z0-9\\.\\-\\+]+://([A-Za-z0-9][.A-Za-z0-9-_]+[A-Za-z0-9])+(:[1-9][0-9]*)?(/[^?&#]+)?$" + } + }, + "subject_ids" : { + "type" : "array", + "description" : "Subject identifiers, all of which to match", + "items" : { + "type" : "string", + "description" : "Subject identifier" + } + }, + "tag_query" : { + "type" : "object", + "description" : "Tag filter", + "additionalProperties" : { + "type" : "string", + "description" : "Tag value" + } + }, + "types" : { + "type" : "array", + "items" : { + "type" : "string", + "example" : "https://myhost:8021", + "description" : "Credential type to match", + "pattern" : "^[A-Za-z0-9\\.\\-\\+]+://([A-Za-z0-9][.A-Za-z0-9-_]+[A-Za-z0-9])+(:[1-9][0-9]*)?(/[^?&#]+)?$" + } + } + } + }, + "WalletList" : { + "type" : "object", + "properties" : { + "results" : { + "type" : "array", + "description" : "List of wallet records", + "items" : { + "$ref" : "#/definitions/WalletRecord" + } + } + } + }, + "WalletModuleResponse" : { + "type" : "object" + }, + "WalletRecord" : { + "type" : "object", + "required" : [ "key_management_mode", "wallet_id" ], + "properties" : { + "created_at" : { + "type" : "string", + "example" : "2021-12-31T23:59:59Z", + "description" : "Time of record creation", + "pattern" : "^\\d{4}-\\d\\d-\\d\\d[T ]\\d\\d:\\d\\d(?:\\:(?:\\d\\d(?:\\.\\d{1,6})?))?(?:[+-]\\d\\d:?\\d\\d|Z|)$" + }, + "key_management_mode" : { + "type" : "string", + "description" : "Mode regarding management of wallet key", + "enum" : [ "managed", "unmanaged" ] + }, + "settings" : { + "type" : "object", + "description" : "Settings for this wallet.", + "properties" : { } + }, + "state" : { + "type" : "string", + "example" : "active", + "description" : "Current record state" + }, + "updated_at" : { + "type" : "string", + "example" : "2021-12-31T23:59:59Z", + "description" : "Time of last record update", + "pattern" : "^\\d{4}-\\d\\d-\\d\\d[T ]\\d\\d:\\d\\d(?:\\:(?:\\d\\d(?:\\.\\d{1,6})?))?(?:[+-]\\d\\d:?\\d\\d|Z|)$" + }, + "wallet_id" : { + "type" : "string", + "example" : "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "description" : "Wallet record ID" + } + } + }, + "WriteLedgerRequest" : { + "type" : "object", + "properties" : { + "ledger_id" : { + "type" : "string" + } + } + }, + "UpdateProfileSettingsRequest" : { + "type" : "object", + "properties" : { + "extra_settings" : { + "type" : "object", + "description" : "Settings or config to update.", + "properties" : { } + } + } + }, + "ActionMenuFetchResult_result" : { + "type" : "object", + "description" : "Action menu" + }, + "AttachDecoratorData_jws" : { + "type" : "object", + "description" : "Detached Java Web Signature" + }, + "CredDefValue_primary" : { + "type" : "object", + "description" : "Primary value for credential definition" + }, + "CredDefValue_revocation" : { + "type" : "object", + "description" : "Revocation value for credential definition" + }, + "Credential_proof" : { + "type" : "object", + "description" : "The proof of the credential", + "example" : "{\"created\":\"2019-12-11T03:50:55\",\"jws\":\"eyJhbGciOiAiRWREU0EiLCAiYjY0IjogZmFsc2UsICJjcml0JiNjQiXX0..lKJU0Df_keblRKhZAS9Qq6zybm-HqUXNVZ8vgEPNTAjQKBhQDxvXNo7nvtUBb_Eq1Ch6YBKY5qBQ\",\"proofPurpose\":\"assertionMethod\",\"type\":\"Ed25519Signature2018\",\"verificationMethod\":\"did:key:z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL#z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL\"}" + }, + "CredentialDefinition_value" : { + "type" : "object", + "description" : "Credential definition primary and revocation values" + }, + "DIDCreate_options" : { + "type" : "object", + "description" : "To define a key type and/or a did depending on chosen DID method." + }, + "DIDXRequest_did_docattach" : { + "type" : "object", + "description" : "As signed attachment, DID Doc associated with DID" + }, + "Doc_options" : { + "type" : "object", + "description" : "Signature options" + }, + "IndyCredAbstract_key_correctness_proof" : { + "type" : "object", + "description" : "Key correctness proof" + }, + "IndyCredPrecis_cred_info" : { + "type" : "object", + "description" : "Credential info" + }, + "IndyCredPrecis_interval" : { + "type" : "object", + "description" : "Non-revocation interval from presentation request" + }, + "IndyPrimaryProof_eq_proof" : { + "type" : "object", + "description" : "Indy equality proof", + "x-nullable" : true + }, + "IndyProof_proof" : { + "type" : "object", + "description" : "Indy proof.proof content" + }, + "IndyProof_requested_proof" : { + "type" : "object", + "description" : "Indy proof.requested_proof content" + }, + "IndyProofProof_aggregated_proof" : { + "type" : "object", + "description" : "Indy proof aggregated proof" + }, + "IndyProofProofProofsProof_non_revoc_proof" : { + "type" : "object", + "description" : "Indy non-revocation proof", + "x-nullable" : true + }, + "IndyProofProofProofsProof_primary_proof" : { + "type" : "object", + "description" : "Indy primary proof" + }, + "IndyProofReqAttrSpec_non_revoked" : { + "type" : "object", + "x-nullable" : true + }, + "IndyRevRegDef_value" : { + "type" : "object", + "description" : "Revocation registry definition value" + }, + "IndyRevRegDefValue_publicKeys" : { + "type" : "object", + "description" : "Public keys" + }, + "IndyRevRegEntry_value" : { + "type" : "object", + "description" : "Revocation registry entry value" + }, + "InputDescriptors_schema" : { + "type" : "object", + "description" : "Accepts a list of schema or a dict containing filters like oneof_filter.", + "example" : "{\"oneof_filter\":[[{\"uri\":\"https://www.w3.org/Test1#Test1\"},{\"uri\":\"https://www.w3.org/Test2#Test2\"}],{\"oneof_filter\":[[{\"uri\":\"https://www.w3.org/Test1#Test1\"}],[{\"uri\":\"https://www.w3.org/Test2#Test2\"}]]}]}" + }, + "InvitationRecord_invitation" : { + "type" : "object", + "description" : "Out of band invitation message" + }, + "IssuerRevRegRecord_revoc_reg_def" : { + "type" : "object", + "description" : "Revocation registry definition" + }, + "IssuerRevRegRecord_revoc_reg_entry" : { + "type" : "object", + "description" : "Revocation registry entry" + }, + "KeylistQuery_paginate" : { + "type" : "object", + "description" : "Pagination info" + }, + "LDProofVCDetail_credential" : { + "type" : "object", + "description" : "Detail of the JSON-LD Credential to be issued", + "example" : "{\"@context\":[\"https://www.w3.org/2018/credentials/v1\",\"https://w3id.org/citizenship/v1\"],\"credentialSubject\":{\"familyName\":\"SMITH\",\"gender\":\"Male\",\"givenName\":\"JOHN\",\"type\":[\"PermanentResident\",\"Person\"]},\"description\":\"Government of Example Permanent Resident Card.\",\"identifier\":\"83627465\",\"issuanceDate\":\"2019-12-03T12:19:52Z\",\"issuer\":\"did:key:z6MkmjY8GnV5i9YTDtPETC2uUAW6ejw3nk5mXF5yci5ab7th\",\"name\":\"Permanent Resident Card\",\"type\":[\"VerifiableCredential\",\"PermanentResidentCard\"]}" + }, + "LDProofVCDetail_options" : { + "type" : "object", + "description" : "Options for specifying how the linked data proof is created.", + "example" : "{\"proofType\":\"Ed25519Signature2018\"}" + }, + "LDProofVCDetailOptions_credentialStatus" : { + "type" : "object", + "description" : "The credential status mechanism to use for the credential. Omitting the property indicates the issued credential will not include a credential status" + }, + "SchemaSendResult_schema" : { + "type" : "object", + "description" : "Schema definition" + }, + "SendMenu_menu" : { + "type" : "object", + "description" : "Menu to send to connection" + }, + "SignedDoc_proof" : { + "type" : "object", + "description" : "Linked data proof" + }, + "TxnOrCredentialDefinitionSendResult_txn" : { + "type" : "object", + "description" : "Credential definition transaction to endorse" + }, + "TxnOrPublishRevocationsResult_txn" : { + "type" : "object", + "description" : "Revocation registry revocations transaction to endorse" + }, + "TxnOrRegisterLedgerNymResponse_txn" : { + "type" : "object", + "description" : "DID transaction to endorse" + }, + "TxnOrRevRegResult_txn" : { + "type" : "object", + "description" : "Revocation registry definition transaction to endorse" + }, + "TxnOrSchemaSendResult_sent" : { + "type" : "object", + "description" : "Content sent" + }, + "TxnOrSchemaSendResult_txn" : { + "type" : "object", + "description" : "Schema transaction to endorse" + }, + "V10CredentialBoundOfferRequest_counter_proposal" : { + "type" : "object", + "description" : "Optional counter-proposal" + }, + "V10CredentialExchange_credential" : { + "type" : "object", + "description" : "Credential as stored" + }, + "V10CredentialExchange_credential_offer" : { + "type" : "object", + "description" : "(Indy) credential offer" + }, + "V10CredentialExchange_credential_offer_dict" : { + "type" : "object", + "description" : "Credential offer message" + }, + "V10CredentialExchange_credential_proposal_dict" : { + "type" : "object", + "description" : "Credential proposal message" + }, + "V10CredentialExchange_credential_request" : { + "type" : "object", + "description" : "(Indy) credential request" + }, + "V10CredentialExchange_raw_credential" : { + "type" : "object", + "description" : "Credential as received, prior to storage in holder wallet" + }, + "V10DiscoveryRecord_disclose" : { + "type" : "object", + "description" : "Disclose message" + }, + "V10DiscoveryRecord_query_msg" : { + "type" : "object", + "description" : "Query message" + }, + "V10PresentationExchange_presentation" : { + "type" : "object", + "description" : "(Indy) presentation (also known as proof)" + }, + "V10PresentationExchange_presentation_proposal_dict" : { + "type" : "object", + "description" : "Presentation proposal message" + }, + "V10PresentationExchange_presentation_request" : { + "type" : "object", + "description" : "(Indy) presentation request (also known as proof request)" + }, + "V10PresentationExchange_presentation_request_dict" : { + "type" : "object", + "description" : "Presentation request message" + }, + "V20CredBoundOfferRequest_counter_preview" : { + "type" : "object", + "description" : "Optional content for counter-proposal" + }, + "V20CredBoundOfferRequest_filter" : { + "type" : "object", + "description" : "Credential specification criteria by format" + }, + "V20CredExRecord_by_format" : { + "type" : "object", + "description" : "Attachment content by format for proposal, offer, request, and issue" + }, + "V20CredExRecord_cred_issue" : { + "type" : "object", + "description" : "Serialized credential issue message" + }, + "V20CredExRecord_cred_preview" : { + "type" : "object", + "description" : "Credential preview from credential proposal" + }, + "V20CredExRecord_cred_request" : { + "type" : "object", + "description" : "Serialized credential request message" + }, + "V20CredExRecordDetail_cred_ex_record" : { + "type" : "object", + "description" : "Credential exchange record" + }, + "V20CredFilter_indy" : { + "type" : "object", + "description" : "Credential filter for indy" + }, + "V20CredFilter_ld_proof" : { + "type" : "object", + "description" : "Credential filter for linked data proof" + }, + "V20CredProposal_credential_preview" : { + "type" : "object", + "description" : "Credential preview" + }, + "V20DiscoveryExchangeResult_results" : { + "type" : "object", + "description" : "Discover Features v2.0 exchange record" + }, + "V20DiscoveryRecord_disclosures" : { + "type" : "object", + "description" : "Disclosures message" + }, + "V20DiscoveryRecord_queries_msg" : { + "type" : "object", + "description" : "Queries message" + }, + "V20PresExRecord_by_format" : { + "type" : "object", + "description" : "Attachment content by format for proposal, request, and presentation" + }, + "V20PresExRecord_pres" : { + "type" : "object", + "description" : "Presentation message" + }, + "V20PresProposalByFormat_dif" : { + "type" : "object", + "description" : "Presentation proposal for DIF" + }, + "V20PresProposalByFormat_indy" : { + "type" : "object", + "description" : "Presentation proposal for indy" + }, + "V20PresRequestByFormat_dif" : { + "type" : "object", + "description" : "Presentation request for DIF" + }, + "V20PresRequestByFormat_indy" : { + "type" : "object", + "description" : "Presentation request for indy" + }, + "V20PresSpecByFormatRequest_dif" : { + "type" : "object", + "description" : "Optional Presentation specification for DIF, overrides the PresentionExchange record's PresRequest" + }, + "V20PresSpecByFormatRequest_indy" : { + "type" : "object", + "description" : "Presentation specification for indy" + }, + "VerifyRequest_doc" : { + "type" : "object", + "description" : "Signed document" + } + } +} \ No newline at end of file diff --git a/requirements.askar.txt b/requirements.askar.txt index 08d0c2161b..6a44c42e39 100644 --- a/requirements.askar.txt +++ b/requirements.askar.txt @@ -1,3 +1,3 @@ -aries-askar~=0.2.2 +aries-askar~=0.2.5 indy-credx~=0.3 indy-vdr~=0.3.3 diff --git a/requirements.dev.txt b/requirements.dev.txt index 792ac2d13b..0636eba1fa 100644 --- a/requirements.dev.txt +++ b/requirements.dev.txt @@ -1,14 +1,17 @@ asynctest==0.13.0 -pytest~=5.4.0 +async-case~=10.1 +pytest~=6.2 pytest-asyncio==0.14.0 pytest-cov==2.10.1 pytest-flake8==1.0.6 +mock~=4.0 -flake8==3.9.0 +flake8==4.0.1 # flake8-rst-docstrings==0.0.8 flake8-docstrings==1.5.0 flake8-rst==0.7.2 pydocstyle==3.0.0 +pre-commit sphinx==1.8.4 sphinx-rtd-theme>=0.4.3 @@ -16,4 +19,4 @@ sphinx-rtd-theme>=0.4.3 ptvsd==4.3.2 pydevd==1.5.1 -pydevd-pycharm~=193.6015.39 \ No newline at end of file +pydevd-pycharm~=193.6015.39 diff --git a/requirements.indy.txt b/requirements.indy.txt index a364cfe7dd..352d0b22bc 100644 --- a/requirements.indy.txt +++ b/requirements.indy.txt @@ -1 +1 @@ -python3-indy>=1.11.1<2 +python3-indy>=1.11.1,<2 diff --git a/requirements.txt b/requirements.txt index fde3ab2124..c19b79fb4e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,27 +1,31 @@ -aiohttp~=3.7.4 +aiohttp~=3.8.1 aiohttp-apispec~=2.2.1 aiohttp-cors~=0.7.0 apispec~=3.3.0 -async-timeout~=3.0.1 +async-timeout~=4.0.2 +nest_asyncio~=1.5.5 aioredis~=2.0.0 base58~=2.1.0 deepmerge~=0.3.0 ecdsa~=0.16.1 Markdown~=3.1.1 +markupsafe==2.0.1 marshmallow==3.5.1 msgpack~=1.0 prompt_toolkit~=2.0.9 -pynacl~=1.4.0 +pynacl~=1.5.0 requests~=2.25.0 packaging~=20.4 pyld~=2.0.3 pyyaml~=5.4.0 -ConfigArgParse~=1.2.3 -pyjwt~=1.7.1 -pydid~=0.3.3 +ConfigArgParse~=1.5.3 +pyjwt~=2.4.0 +pydid~=0.3.6 +portalocker~=2.7.0 jsonpath_ng==1.5.2 pytz~=2021.1 python-dateutil~=2.8.1 -rlp==0.5.1 +python-json-logger~=2.0.7 +rlp==1.2.0 unflatten~=0.1 qrcode[pil]~=6.1 diff --git a/scripts/generate-open-api-spec b/scripts/generate-open-api-spec index d2299c3290..1ae7c83e20 100755 --- a/scripts/generate-open-api-spec +++ b/scripts/generate-open-api-spec @@ -1,23 +1,26 @@ #!/bin/bash # -# This script will build an ACA-py docker image, -# execute the openapi/swagger codegen tool and create an openapi.json spec file -# for the ACA-py REST API. +# This script will build an ACA-py docker image, +# execute the openapi/swagger codegen tool and create a swagger.json and openapi.json spec file +# for the ACA-py REST API. # ########################################################################################## -# Make sure everything is done starting in our commands home directory +SWAGGER_GEN_CONTAINER="swaggerapi/swagger-codegen-cli:2.4.32" +OPENAPI_GEN_CONTAINER="openapitools/openapi-generator-cli:v6.6.0" + +# Ensure the script is running from the directory containing this script cd $(dirname $0) -# Establish basic context of where things exist +# Define the root and output directories ROOT_DIR="${PWD}/.." OUTPUT_DIR="${ROOT_DIR}/open-api" ########################################################################################## # Global Defaults and Constants ########################################################################################## +# Set some default values for ACA-Py, including the default image name and ports ACA_PY_DOCKER_IMAGE_DEFAULT="aries-cloudagent-run" - ACA_PY_ADMIN_PORT="8305" ACA_PY_INBOUND_PORT="8307" ACA_PY_DOCKER_PORTS="${ACA_PY_INBOUND_PORT}:${ACA_PY_INBOUND_PORT} ${ACA_PY_ADMIN_PORT}:${ACA_PY_ADMIN_PORT}" @@ -37,6 +40,7 @@ ACA_PY_CMD_OPTIONS=" \ --jwt-secret test \ --no-ledger" +# Specify openAPI JSON config file and shared directory OPEN_API_JSON_CONFIG="openAPIJSON.config" OPEN_API_SHARED_DIR="${OUTPUT_DIR}/.build" @@ -51,12 +55,12 @@ function runEval() { returnValue=$? if [ $returnValue != 0 ]; then echo "Command ${1} failed" - exit -1 + exit 1 fi return $returnValue } -# Print an indication of script reaching a processing +# Print an indication of script reaching a processing # milestone in a noticable way # $1 : Message string to print function printMilestone() { @@ -67,13 +71,11 @@ function printMilestone() { echo -e "##########################################################################################\n" } - # Wait for a web server to provide a funcitoning interface we can use # $1 : Url to poll that indicates webserver initialsation complete # $2 : maximum number of seconds to wait function waitActiveWebInterface() { - for (( i=1; i < ${2}; i++)) - do + for ((i = 1; i < ${2}; i++)); do curl -s -f ${1} if [ $? == 0 ]; then return 0 @@ -104,8 +106,8 @@ function buildACAPyDockerImage() { # $2: The port mapping from docker to local host in format "docker1:local1 docker2:local2" # $3: The ACA-py command line arguements # $4: The name of a variable to return the continer ID to -function runACAPy() { - local acaPyImage="${1}" +function runACAPy() { + local acaPyImage="${1}" local ports="${2}" local acaPyArgs="${3}" local result="${4}" @@ -115,12 +117,12 @@ function runACAPy() { args="${args} -p ${port}" done - # Mount the agent logs onto the hosting machine + # Mount the agent logs onto the hosting machine args="${args} -v /$(pwd)/../logs:/home/indy/logs" randName=$(cat /dev/urandom | env LC_ALL=C tr -dc 'a-zA-Z0-9' | fold -w 16 | head -n 1) acaPyCmd="docker run -d --rm --name ${acaPyImage}_${randName} ${args} \ - ${acaPyImage} start ${acaPyArgs}" + ${acaPyImage} start ${acaPyArgs}" printMilestone "Starting ACA-py docker image with command: \n \ \t ${acaPyCmd}" @@ -128,77 +130,79 @@ function runACAPy() { containerId=$(${acaPyCmd}) local returnStatus=$? if [[ ${returnStatus} != 0 ]]; then - echo "**** FAIL - ACA-Py failed to start, exiting. ****" - exit 1 + echo "**** FAIL - ACA-Py failed to start, exiting. ****" + exit 1 fi if [[ "${result}" ]]; then eval ${result}="'${containerId}'" fi } - -OPEN_API_CONTAINER="swaggerapi/swagger-codegen-cli:2.4.15" -# OPEN_API_CONTAINER="openapitools/openapi-generator-cli:v4.3.1" -OPEN_API_OPTIONS=" " OPEN_API_MOUNT="/local" +ACAPY_SPEC_FILE="acapy-raw.json" +CONFIG_LOCATION="${ROOT_DIR}/open-api/${OPEN_API_JSON_CONFIG}" # Pull the open API docker image and run it against the specified web server # or local spec file. # $1 : Web OpenAPI URL or local file to generate API routines from. -# $2 : Language to generate +# $2 : Language to generate (swagger or openapi) # $3 : Language config file location # $4 : The host shared dir for input/output function runOpenAPIGenerate() { local specFile="${1}" - local outputLang="${2}" + local generatorType="${2}" local configLocation="${3}" local hostSharedDir="${4}" runEval "mkdir -p ${hostSharedDir}" - + if [ ! -z ${configLocation} ]; then runEval "cp ${configLocation} ${hostSharedDir}/" configFile="$(basename -- ${configLocation})" fi - openAPICmd="docker run --rm --user $(id -u):$(id -g) -v ${hostSharedDir}:${OPEN_API_MOUNT} ${OPEN_API_CONTAINER} generate \ - --input-spec ${specFile} \ - --output ${OPEN_API_MOUNT}" - - # If using the swagger version of code generator the options are different - # to specify language generator. - if [[ ${OPEN_API_CONTAINER} = *swagger-codegen* ]]; then - if [[ ${outputLang} = "openapi" ]]; then - # Generating the json output is a different language name in swagger - outputLang="swagger" - fi - openAPICmd+=" --lang ${outputLang} ${OPEN_API_OPTIONS} " - else - openAPICmd+=" --generator-name ${outputLang} ${OPEN_API_OPTIONS} " - fi + genSpecCmd="docker run --rm --user $(id -u):$(id -g) -v ${hostSharedDir}:${OPEN_API_MOUNT}" + + # specify language generator. + case ${generatorType} in + swagger) + genSpecCmd+=" ${SWAGGER_GEN_CONTAINER} generate -l swagger" + ;; + openapi) + genSpecCmd+=" ${OPENAPI_GEN_CONTAINER} generate -g openapi" + ;; + *) + echo "Unknown generator type: ${generatorType}" + exit 1 + ;; + esac + + genSpecCmd+=" --input-spec ${specFile} --output ${OPEN_API_MOUNT}" if [ ! -z ${configFile} ]; then - openAPICmd+=" --config ${OPEN_API_MOUNT}/${configFile}" + genSpecCmd+=" --config ${OPEN_API_MOUNT}/${configFile}" fi - printMilestone "Starting Open API code generation with command: \n \ - \t ${openAPICmd}" + printMilestone "Starting ${generatorType} code generation with command: \n \ + \t ${genSpecCmd}" - ${openAPICmd} - - # Copy the swagger output into the normalised output file name - if [[ ${OPEN_API_CONTAINER} = *swagger-codegen* ]] && [[ ${outputLang} = "swagger" ]]; then - runEval "cp ${hostSharedDir}/swagger.json ${hostSharedDir}/openapi.json" - fi + ${genSpecCmd} } ########################################################################################## # Run docker ACA-py image and pull REST API spec file to generate json format -########################################################################################## +########################################################################################## + +# Build the ACA-Py Docker image from the current code buildACAPyDockerImage "${ROOT_DIR}" "${ACA_PY_DOCKER_IMAGE_DEFAULT}" -runACAPy "${ACA_PY_DOCKER_IMAGE_DEFAULT}" "${ACA_PY_DOCKER_PORTS}" "${ACA_PY_CMD_OPTIONS}" ACA_PY_CONTAINER_ID -# Make sure ACA-py container gets terminated when we do + +# Run the ACA-Py Docker image +runACAPy "${ACA_PY_DOCKER_IMAGE_DEFAULT}" "${ACA_PY_DOCKER_PORTS}" "${ACA_PY_CMD_OPTIONS}" ACA_PY_CONTAINER_ID + +# Ensure the ACA-Py container is killed when the script exits trap 'docker kill ${ACA_PY_CONTAINER_ID}' EXIT + +# Wait for the ACA-Py web interface to become active waitActiveWebInterface "http://localhost:${ACA_PY_ADMIN_PORT}" 20 returnValue=$? if [ $returnValue != 0 ]; then @@ -206,18 +210,21 @@ if [ $returnValue != 0 ]; then fi printMilestone "ACA-Py Admin interface active\n\t Docker Id '${ACA_PY_CONTAINER_ID}'" -# Pull the swagger raw format spec file from ACA-py -if [ ! -d ${OPEN_API_SHARED_DIR} ]; then - mkdir -p ${OPEN_API_SHARED_DIR}; +# Create the shared directory if it doesn't already exist +if [ ! -d ${OPEN_API_SHARED_DIR} ]; then + mkdir -p ${OPEN_API_SHARED_DIR} fi -curl --output ${OPEN_API_SHARED_DIR}/acapy-raw.json http://localhost:${ACA_PY_ADMIN_PORT}/api/docs/swagger.json -# Generate the native OpenAPI JSON spec file -runOpenAPIGenerate "${OPEN_API_MOUNT}/acapy-raw.json" openapi "${ROOT_DIR}/open-api/${OPEN_API_JSON_CONFIG}" "${OPEN_API_SHARED_DIR}" +# Download the Swagger specification from the ACA-Py service +curl --output "${OPEN_API_SHARED_DIR}/${ACAPY_SPEC_FILE}" http://localhost:${ACA_PY_ADMIN_PORT}/api/docs/swagger.json -# Force over-write the version controlled openapi.json +# Use the OpenAPI Generator to create both Swagger and OpenAPI spec files from the downloaded Swagger specification +runOpenAPIGenerate "${OPEN_API_MOUNT}/${ACAPY_SPEC_FILE}" swagger "${CONFIG_LOCATION}" "${OPEN_API_SHARED_DIR}" +runOpenAPIGenerate "${OPEN_API_MOUNT}/${ACAPY_SPEC_FILE}" openapi "${CONFIG_LOCATION}" "${OPEN_API_SHARED_DIR}" + +# Overwrite the existing Swagger and OpenAPI spec files +runEval "cp -f ${OPEN_API_SHARED_DIR}/swagger.json ${ROOT_DIR}/open-api/" runEval "cp -f ${OPEN_API_SHARED_DIR}/openapi.json ${ROOT_DIR}/open-api/" # Clean up the working directory. -rm -Rf ${OPEN_API_SHARED_DIR}; - +rm -Rf ${OPEN_API_SHARED_DIR} diff --git a/scripts/run_docker b/scripts/run_docker index e1c99a0b9d..13b65ee2c6 100755 --- a/scripts/run_docker +++ b/scripts/run_docker @@ -14,6 +14,9 @@ for PORT in $PORTS; do ARGS="${ARGS} -p $PORT" done +for ENV_VAR in $ENV_VARS; do + ARGS="${ARGS} -e $ENV_VAR" +done PTVSD_PORT="${PTVSD_PORT-5678}" for arg in "$@"; do @@ -24,12 +27,13 @@ for arg in "$@"; do echo "Backing up database before running aca-py upgrade is highly recommended. Do you wish to proceed" select yn in "Yes" "No"; do case $yn in - Yes) break ;; - No) exit ;; + Yes) break ;; + No) exit ;; esac done fi done + if [ -n "${ENABLE_PTVSD}" ]; then ARGS="${ARGS} -e ENABLE_PTVSD=\"${ENABLE_PTVSD}\" -p $PTVSD_PORT:$PTVSD_PORT" fi @@ -44,6 +48,45 @@ if [ "$OSTYPE" == "msys" ]; then CONTAINER_RUNTIME="winpty docker" fi -RAND_NAME=$(env LC_ALL=C tr -dc 'a-zA-Z0-9' < /dev/urandom | fold -w 16 | head -n 1) -$CONTAINER_RUNTIME run --rm -ti --name "aries-cloudagent-runner_${RAND_NAME}" \ - $ARGS aries-cloudagent-run "$@" +if [ -n "${CONTAINER_NAME}" ]; then + ARGS="${ARGS} --name ${CONTAINER_NAME}" +else + RAND_NAME=$(env LC_ALL=C tr -dc 'a-zA-Z0-9'