diff --git a/.github/workflows/full_test.yml b/.github/workflows/full_test.yml new file mode 100644 index 0000000..575ee51 --- /dev/null +++ b/.github/workflows/full_test.yml @@ -0,0 +1,62 @@ +# +# SPDX-License-Identifier: Apache-2.0 +# + +# This workflow is intended to be used as a validity test for any +# pull request. That is, this is a minimal functionality that must +# be successfully executed prior to merging a pull request. Note +# that this can be overridden by adding '[skip ci]' in the commit +# name. + +name: Run full PDO contract tests +on: [ workflow_dispatch, pull_request ] + +jobs: + pdo_ci: + if: "!contains(github.event.commits[0].message, '[skip ci]')" + name: PDO Contracts Full Test + runs-on: ubuntu-22.04 + + env: + REGISTRY: ghcr.io + OWNER: ${{ github.repository_owner }} + PDO_USER_UID: 55172 + PDO_GROUP_UID: 55172 + PDO_SOURCE_ROOT: private-data-objects + steps: + - name: Check out repo + uses: actions/checkout@v4 + with: + submodules: recursive + fetch-depth: 0 + fetch-tags: true + + - name: Set the version + run: | + echo "PDO_VERSION=$(${PDO_SOURCE_ROOT}/bin/get_version -f ${PDO_SOURCE_ROOT}/VERSION)" >> $GITHUB_ENV + echo "PDO_CONTRACTS_VERSION=$(bin/get_version)" >> $GITHUB_ENV + + - name: Show the version + run: | + echo "PDO_CONTRACTS_VERSION is $PDO_CONTRACTS_VERSION" + echo "PDO_VERSION is $PDO_VERSION" + + - name: Configure PDO and build the base images + run: | + if $(docker/require_pdo_images.sh -r ${REGISTRY}/${OWNER} -v ${PDO_VERSION}) + then + echo Using pre-built PDO images + else + echo No pre-built PDO images available for version ${PDO_VERSION} from ${REGISTRY} + echo Please populate the registry and retry + exit -1 + fi + + - name: Build and run contract tests + run: | + git checkout -b ci-test-branch + sudo chown -R $PDO_USER_UID:$PDO_GROUP_UID docker/xfer + sudo chmod -R g+w docker/xfer + make -C docker test \ + CONTRACTS_USER_UID=$PDO_USER_UID CONTRACTS_GROUP_UID=$PDO_GROUP_UID \ + PDO_VERSION=$PDO_VERSION PDO_REGISTRY=$REGISTRY/$OWNER diff --git a/CMakeLists.txt b/CMakeLists.txt index b378b16..6150967 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -27,7 +27,7 @@ INCLUDE(wawaka_common) LIST(APPEND WASM_LIBRARIES ${WW_COMMON_LIB}) LIST(APPEND WASM_INCLUDES ${WW_COMMON_INCLUDES}) -FILE(GLOB CONTRACT_FAMILIES "*-contract") +FILE(GLOB CONTRACT_FAMILIES RELATIVE ${CMAKE_SOURCE_DIR} "*-contract") # A local cmake file (Local.cmake) allows for local overrides of # variables. In particular, this is useful to set CONTRACT_FAMILIES @@ -43,6 +43,15 @@ INCLUDE(Local.cmake OPTIONAL) ENABLE_TESTING() +# Contract families must contain common-contract and +# it should be the first built; this just makes sure +# it is include + +LIST(REMOVE_ITEM CONTRACT_FAMILIES common-contract) +LIST(PREPEND CONTRACT_FAMILIES common-contract) + +MESSAGE(STATUS "BUILD CONTRACT FAMILIES ${CONTRACT_FAMILIES}") + FOREACH(FAMILY IN ITEMS ${CONTRACT_FAMILIES}) ADD_SUBDIRECTORY(${FAMILY}) ENDFOREACH() diff --git a/Makefile b/Makefile index ecfb91c..ba01c7c 100644 --- a/Makefile +++ b/Makefile @@ -30,6 +30,7 @@ TEST_LOG_LEVEL ?= warn TEST_LOG_FILE ?= __screen__ TEST_SERVICE_HOST ?= $(PDO_HOSTNAME) TEST_LEDGER ?= $(PDO_LEDGER_URL) +TEST_LIST ?= ^system VERSION=${shell ${SOURCE_ROOT}/bin/get_version} @@ -51,7 +52,7 @@ test : install -DTEST_LOG_FILE=$(TEST_LOG_FILE) \ -DTEST_LEDGER=$(TEST_LEDGER) \ -DTEST_SERVICE_HOST=$(TEST_SERVICE_HOST) - @ make -C build test ARGS='-V' + @ make -C build test ARGS='-V -R "$(TEST_LIST)"' clean : @ echo Remove build directory diff --git a/VERSION b/VERSION new file mode 100644 index 0000000..9b10495 --- /dev/null +++ b/VERSION @@ -0,0 +1,2 @@ +v0.2.0 cc37e8428d9564256bbdff63714113eb63795f3e automated testing enabled; pdo 0.4.1 +v0.1.0 b57f345c67b6babb8b386d695b6c7e665b76e8b0 initial commit of exchange and digital asset families; pdo 0.2.63 diff --git a/bin/get_version b/bin/get_version index bfca2dd..d78cf4d 100755 --- a/bin/get_version +++ b/bin/get_version @@ -14,35 +14,42 @@ # See the License for the specific language governing permissions and # limitations under the License. +import argparse +import os +import pathlib import subprocess -import sys -import warnings - -count = 0 -commit = '' -dirty = '' - -try : - output = subprocess.check_output(['git', 'describe', '--dirty']) - (version, *rest) = output.decode('utf-8').strip().split('-') - (major, minor, patch) = version.strip('v').split('.') - - # first case: this is a dirty tagged release, only dirty flag - if len(rest) == 1 : - assert rest[0] == 'dirty' - dirty = 'dirty' - # second case: this is a committed post tag release - elif len(rest) == 2 : - count = rest[0] - commit = rest[1] - # third case: this is a dirty, committed post tag release - elif len(rest) == 3 : - assert rest[2] == 'dirty' - count = rest[0] - commit = rest[1] - dirty = rest[2] - - print('{}.{}.{}'.format(major, minor, count)) -except Exception as e : - warnings.warn('failed to compute version, using default') - print('0.0.0') + +pdo_source_root=pathlib.Path(__file__).parent.parent +version_file = pdo_source_root / 'VERSION' + +parser = argparse.ArgumentParser() + +parser.add_argument( + '--version-file', '-f', + help=f'File where version information is stored (default: {version_file})', + type=str) + +options = parser.parse_args() + +if options.version_file : + version_file = pathlib.Path(options.version_file) + pdo_source_root = version_file.parent + +# the version file is a tab separated list of version numbers and git commit hashes in reverse +# order (newest is at the top of the file) +with open(version_file, 'r') as vf : + (version, commit, *rest) = vf.readline().strip().split('\t') + +# the version is of the form x.y.z, there may be an optional 'v' at the beginning of the version +# string +(major, minor, patch) = version.strip('v').split('.') + +# compute the number of commits since the tagged version was +# committed to the repository +command = ['git', 'rev-list', commit + '...HEAD', '--count'] +output = subprocess.run(command, cwd=pdo_source_root, capture_output=True, text=True) +count = output.stdout.strip() + +# the actual patch version number is the recorded patch number added to the number of commits +# since the version was committed +print('{}.{}.{}'.format(major, minor, int(patch) + int(count))) diff --git a/docker/Makefile b/docker/Makefile index 6c69c21..69ac444 100644 --- a/docker/Makefile +++ b/docker/Makefile @@ -14,6 +14,9 @@ # limitations under the License. # ------------------------------------------------------------------------------ +# Ensure that the default target builds the contracts +all : build_contracts + # Include local customizations if they are available -include make.loc @@ -23,10 +26,10 @@ # that the user/group identifiers need to have write access to the xfer directory CONTRACTS_USER_UID ?= $(shell id -u) CONTRACTS_GROUP_UID ?= $(shell id -g) +CONTRACTS_USER_NAME ?= $(shell whoami) DOCKER_COMMAND ?= docker -DOCKER_COMPOSE_COMMAND ?= docker-compose ifndef DOCKER_COMPOSE_COMMAND DOCKER_COMPOSE_COMMAND := $(shell command -v docker-compose 2> /dev/null) ifndef DOCKER_COMPOSE_COMMAND @@ -66,15 +69,19 @@ PDO_VERSION ?= $$( \ echo "latest"; \ fi ) -PDO_REPSITORY ?= '' +# This variable ensures that the registry ends with a '/'. The additional +# variable ensures that the substitution is applied; conditional assignment +# of the registry appears to prevent early evaluation of the expression. +PDO_REGISTRY_SAFE := $(if $(PDO_REGISTRY),$(patsubst %/,%,$(PDO_REGISTRY))/) # DOCKER BUILD DOCKER_USERNAME = $(LOGNAME) +DOCKER_BUILDARGS += --build-arg UNAME=$(CONTRACTS_USER_NAME) DOCKER_BUILDARGS += --build-arg UID=$(CONTRACTS_USER_UID) DOCKER_BUILDARGS += --build-arg GID=$(CONTRACTS_GROUP_UID) DOCKER_BUILDARGS += --build-arg CONTRACTS_VERSION=$(CONTRACTS_VERSION) DOCKER_BUILDARGS += --build-arg PDO_VERSION=$(PDO_VERSION) -DOCKER_BUILDARGS += --build-arg PDO_REPOSITORY=$(PDO_REPOSITORY) +DOCKER_BUILDARGS += --build-arg PDO_REGISTRY=$(PDO_REGISTRY_SAFE) DOCKER_ARGS = $(DOCKER_BUILDARGS) # The CONTRACT_FAMILIES variable contains a list of contract families @@ -87,8 +94,6 @@ CONTRACT_FAMILIES ?= $(notdir $(wildcard $(CONTRACTS_SOURCE_ROOT)/*-contract)) # PDO contracts has changed TIMESTAMP := $(shell /bin/date "+%Y%m%d%H%M%S") -all : build_contracts - rebuild_contracts : repository models $(DOCKER_COMMAND) build $(DOCKER_ARGS) \ --build-arg REBUILD=$(TIMESTAMP) \ @@ -97,6 +102,7 @@ rebuild_contracts : repository models build_contracts : DOCKER_BUILDARGS+=--build-arg CONTRACT_FAMILIES="$(CONTRACT_FAMILIES)" build_contracts : repository models + - echo "Build contract families: $(CONTRACT_FAMILIES)" $(DOCKER_COMMAND) build $(DOCKER_ARGS) \ --tag pdo_contracts:$(CONTRACTS_VERSION) \ --file $(DOCKER_DIR)/pdo_contracts.dockerfile . @@ -121,7 +127,6 @@ build_pdo_images : repository PDO_SOURCE_ROOT=$(DOCKER_DIR)/repository/private-data-objects \ make -C $(DOCKER_DIR)/repository/private-data-objects/docker clean_repository - # ----------------------------------------------------------------- # ----------------------------------------------------------------- # For the purposes of testing, we just use the resnet model for @@ -219,27 +224,62 @@ endif TEST_COMPOSE_ARGS += -f test.yaml test : clean_config clean_repository build_contracts - - PDO_VERSION=$(PDO_VERSION) CONTRACTS_VERSION=$(CONTRACTS_VERSION) \ + - PDO_REGISTRY=$(PDO_REGISTRY_SAFE) PDO_VERSION=$(PDO_VERSION) CONTRACTS_VERSION=$(CONTRACTS_VERSION) \ $(DOCKER_COMPOSE_COMMAND) $(TEST_COMPOSE_ARGS) $(COMPOSE_PROFILES) up --abort-on-container-exit - PDO_VERSION=$(PDO_VERSION) CONTRACTS_VERSION=$(CONTRACTS_VERSION) \ + PDO_REGISTRY=$(PDO_REGISTRY_SAFE) PDO_VERSION=$(PDO_VERSION) CONTRACTS_VERSION=$(CONTRACTS_VERSION) \ $(DOCKER_COMPOSE_COMMAND) $(TEST_COMPOSE_ARGS) down JUPYTER_COMPOSE_ARGS += -f test_jupyter.yaml test_jupyter : clean_config clean_repository build_contracts - - PDO_VERSION=$(PDO_VERSION) CONTRACTS_VERSION=$(CONTRACTS_VERSION) \ + - PDO_REGISTRY=$(PDO_REGISTRY_SAFE) PDO_VERSION=$(PDO_VERSION) CONTRACTS_VERSION=$(CONTRACTS_VERSION) \ $(DOCKER_COMPOSE_COMMAND) $(JUPYTER_COMPOSE_ARGS) $(COMPOSE_PROFILES) up --abort-on-container-exit - PDO_VERSION=$(PDO_VERSION) CONTRACTS_VERSION=$(CONTRACTS_VERSION) \ + PDO_REGISTRY=$(PDO_REGISTRY_SAFE) PDO_VERSION=$(PDO_VERSION) CONTRACTS_VERSION=$(CONTRACTS_VERSION) \ $(DOCKER_COMPOSE_COMMAND) $(JUPYTER_COMPOSE_ARGS) down JUPYTER_MULTIUSER_COMPOSE_ARGS += -f test_multiuser_jupyter.yaml test_multiuser_jupyter : clean_config clean_repository build_contracts - - PDO_VERSION=$(PDO_VERSION) CONTRACTS_VERSION=$(CONTRACTS_VERSION) \ + - PDO_REGISTRY=$(PDO_REGISTRY_SAFE) PDO_VERSION=$(PDO_VERSION) CONTRACTS_VERSION=$(CONTRACTS_VERSION) \ $(DOCKER_COMPOSE_COMMAND) $(JUPYTER_MULTIUSER_COMPOSE_ARGS) $(COMPOSE_PROFILES) up --abort-on-container-exit - PDO_VERSION=$(PDO_VERSION) CONTRACTS_VERSION=$(CONTRACTS_VERSION) \ + PDO_REGISTRY=$(PDO_REGISTRY_SAFE) PDO_VERSION=$(PDO_VERSION) CONTRACTS_VERSION=$(CONTRACTS_VERSION) \ $(DOCKER_COMPOSE_COMMAND) $(JUPYTER_MULTIUSER_COMPOSE_ARGS) down +# The example target builds and runs only the example contract family. This +# is intended to be a very simple way to test drive PDO contracts as a user +# and as a contract developer. + +example : CONTRACT_FAMILIES=example-contract +example : clean_config clean_repository build_contracts + - PDO_REGISTRY=$(PDO_REGISTRY_SAFE) PDO_VERSION=$(PDO_VERSION) CONTRACTS_VERSION=$(CONTRACTS_VERSION) \ + CONTRACTS_SOURCE_ROOT=$(CONTRACTS_SOURCE_ROOT) \ + $(DOCKER_COMPOSE_COMMAND) $(TEST_COMPOSE_ARGS) up --abort-on-container-exit + PDO_REGISTRY=$(PDO_REGISTRY_SAFE) PDO_VERSION=$(PDO_VERSION) CONTRACTS_VERSION=$(CONTRACTS_VERSION) \ + $(DOCKER_COMPOSE_COMMAND) $(TEST_COMPOSE_ARGS) down + +example_jupyter : CONTRACT_FAMILIES=exchange-contract example-contract +example_jupyter : clean_config clean_repository build_contracts + - PDO_REGISTRY=$(PDO_REGISTRY_SAFE) PDO_VERSION=$(PDO_VERSION) CONTRACTS_VERSION=$(CONTRACTS_VERSION) \ + CONTRACTS_SOURCE_ROOT=$(CONTRACTS_SOURCE_ROOT) \ + $(DOCKER_COMPOSE_COMMAND) $(JUPYTER_COMPOSE_ARGS) up --abort-on-container-exit + PDO_REGISTRY=$(PDO_REGISTRY_SAFE) PDO_VERSION=$(PDO_VERSION) CONTRACTS_VERSION=$(CONTRACTS_VERSION) \ + $(DOCKER_COMPOSE_COMMAND) $(JUPYTER_COMPOSE_ARGS) down + +# The developer target creates a container with no contracts built or +# installed. When the container starts, the current contract source +# root is mounted at /project/pdo/dev. In addition, a Jupyter server +# is started. The Jupyter launcher provides a shell window that can be +# used to interact with the PDO installation to build and install +# contracts. +DEVELOP_COMPOSE_ARGS += -f test_dev.yaml +developer : CONTRACT_FAMILIES= +developer : clean_config build_contracts + - PDO_REGISTRY=$(PDO_REGISTRY_SAFE) PDO_VERSION=$(PDO_VERSION) CONTRACTS_VERSION=$(CONTRACTS_VERSION) \ + CONTRACTS_SOURCE_ROOT=$(CONTRACTS_SOURCE_ROOT) \ + $(DOCKER_COMPOSE_COMMAND) $(DEVELOP_COMPOSE_ARGS) up --abort-on-container-exit + PDO_REGISTRY=$(PDO_REGISTRY_SAFE) PDO_VERSION=$(PDO_VERSION) CONTRACTS_VERSION=$(CONTRACTS_VERSION) \ + $(DOCKER_COMPOSE_COMMAND) $(JUPYTER_COMPOSE_ARGS) down + # ----------------------------------------------------------------- # Cleaning is a bit interesting because the containers don't go away # unless they are told to very nicely. Until they go away they hold onto @@ -257,7 +297,7 @@ clean_config : @ rm -f $(DOCKER_DIR)/xfer/services/keys/*.pem $(DOCKER_DIR)/xfer/services/etc/*.toml @ rm -f $(DOCKER_DIR)/xfer/services/etc/site.psh -clean : clean_images clean_config clean_repository clean_models +clean : clean_images clean_config clean_repository .PHONY: clean clean_images clean_config clean_repository clean_models .PHONY: test diff --git a/docker/README.md b/docker/README.md index c9daaea..07f3bea 100644 --- a/docker/README.md +++ b/docker/README.md @@ -59,7 +59,7 @@ Four configuration variables should be set as necessary: user. * `PDO_VERSION` -- the version identifier for PDO images; this defaults to `latest`. -* `PDO_REPOSITORY` -- the docker repository from which PDO docker images +* `PDO_REGISTRY` -- the docker repository from which PDO docker images may be retrieved; this defaults to `""` so that local images will be used by default. * `CONTRACTS_VERSION` -- the version that will be used to tag the PDO diff --git a/docker/pdo_contracts.dockerfile b/docker/pdo_contracts.dockerfile index 859755a..2cff9af 100644 --- a/docker/pdo_contracts.dockerfile +++ b/docker/pdo_contracts.dockerfile @@ -34,29 +34,60 @@ # PURPOSE: develop and test contracts with clean -ARG PDO_REPOSITORY -ARG PDO_VERSION -FROM ${PDO_REPOSITORY}pdo_client:${PDO_VERSION} +ARG PDO_REGISTRY= +ARG PDO_VERSION=latest +FROM ${PDO_REGISTRY}pdo_client:${PDO_VERSION} # ----------------------------------------------------------------- -# Install the necessary dependencies +# Install the necessary system dependencies # ----------------------------------------------------------------- -USER 0:0 -ENV DEBIAN_FRONTEND "noninteractive" -RUN apt-get update \ +USER root +ENV DEBIAN_FRONTEND="noninteractive" +RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \ + --mount=type=cache,target=/var/lib/apt,sharing=locked \ + apt-get update \ && apt-get install -y -q \ - libgl1 + libgl1 + +# ----------------------------------------------------------------- +# Create a user account from the specification +# ----------------------------------------------------------------- +ARG UNAME=pdo_contract +ENV UNAME=${UNAME} + +ARG UID=1000 +ARG GID=${UID} + +# Create a group for the UID/GID if it does not already exist, rename +# it to the correct group if it does exist +RUN if getent group ${GID} > /dev/null 2>&1 ; then \ + groupmod -n ${UNAME} $(getent group ${GID} | cut -d: -f1) ; \ + else \ + groupadd -f -g ${GID} -o ${UNAME} ; \ + fi + +# Create a user for the UID/GID if it does not already exist, rename +# it to the correct login if it does exist +RUN if getent passwd ${UID} > /dev/null 2>&1 ; then \ + usermod -l ${UNAME} $(getent passwd ${UID} | cut -d: -f1) ; \ + else \ + useradd -m -u ${UID} -g ${GID} -d /project/pdo -o -s /bin/bash $UNAME ; \ + fi + +# Reassign ownership of the PDO directory tree to the user:group +RUN chown --recursive $UNAME:$UNAME /project/pdo + +# Use the user account for the rest of the installation process +USER $UNAME # Many of the dependencies should be addressed with the installation # of the PDO client including the common contracts, the wasi toolkit # and all of the WAMR toolchain # This should set up all we need for the jupyter server -ARG UID=1000 -ARG GID=${UID} -USER ${UID}:${GID} RUN --mount=type=cache,uid=${UID},gid=${GID},target=/project/pdo/.cache/pip \ - /project/pdo/run/bin/pip install notebook papermill ipywidgets jupytext + /project/pdo/run/bin/pip install notebook papermill ipywidgets jupytext bash_kernel +RUN /project/pdo/run/bin/python -m bash_kernel.install # ----------------------------------------------------------------- # Set up the contract source and configure for specified tests @@ -81,8 +112,6 @@ WORKDIR /project/pdo/tools COPY --chown=${UNAME}:${UNAME} tools/*.sh ./ # build it!!! -ARG UID=1000 -ARG GID=${UID} RUN --mount=type=cache,uid=${UID},gid=${GID},target=/project/pdo/.cache/pip \ /project/pdo/tools/build_contracts.sh diff --git a/docker/require_pdo_images.sh b/docker/require_pdo_images.sh new file mode 100755 index 0000000..cfe60a9 --- /dev/null +++ b/docker/require_pdo_images.sh @@ -0,0 +1,64 @@ +#!/bin/bash + +# Copyright 2023 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +SCRIPT_NAME=$(basename ${BASH_SOURCE[-1]} ) +source ${PDO_SOURCE_ROOT}/bin/lib/common.sh + +# ----------------------------------------------------------------- +# Process command line arguments +# ----------------------------------------------------------------- +F_REGISTRY= +F_VERSION=latest + +F_USAGE='-r|--registry [registry] -v|--version [pdo_version]' +SHORT_OPTS='r:v:' +LONG_OPTS='registry:,version:' + +TEMP=$(getopt -o ${SHORT_OPTS} --long ${LONG_OPTS} -n "${SCRIPT_NAME}" -- "$@") +if [ $? != 0 ] ; then echo "Usage: ${SCRIPT_NAME} ${F_USAGE}" >&2 ; exit 1 ; fi + +eval set -- "$TEMP" +while true ; do + case "$1" in + -r|--registry) F_REGISTRY="$2" ; shift 2 ;; + -v|--version) F_VERSION="$2" ; shift 2 ;; + --help) echo "Usage: ${SCRIPT_NAME} ${F_USAGE}"; exit 0 ;; + --) shift ; break ;; + *) echo "Internal error!" ; exit 1 ;; + esac +done + +# if a registry is specified then make sure it has a trailing '/' +if [ -n "${F_REGISTRY}" ]; then + if [[ "${F_REGISTRY}" != */ ]]; then + F_REGISTRY=${F_REGISTRY}/ + fi +fi + +# check for each of the PDO images +for image in pdo_ccf pdo_services pdo_client ; do + # First check to see if there is a local version that matches + if docker inspect ${F_REGISTRY}$image:${F_VERSION} > /dev/null 2>&1 ; then + continue + fi + + # Pull the image if there is a registry and fail if not + yell Attempt to pull ${F_REGISTRY}$image:${F_VERSION} + docker pull ${F_REGISTRY}$image:${F_VERSION} > /dev/null 2>&1 || \ + die Unable to find ${F_REGISTRY}$image:${F_VERSION} +done + +exit 0 diff --git a/docker/test.yaml b/docker/test.yaml index ae6c25e..c1df690 100644 --- a/docker/test.yaml +++ b/docker/test.yaml @@ -12,7 +12,6 @@ # See the License for the specific language governing permissions and # limitations under the License. # ------------------------------------------------------------------------------ -version: "3.4" # Note that we do not need to specify PDO_HOSTNAME or PDO_LEDGER_URL # (or the corresponding --inteface or --ledger switches) for the test @@ -21,7 +20,7 @@ version: "3.4" services: ccf_container: - image: ${PDO_REPOSITORY:-}pdo_ccf:${PDO_VERSION:-latest} + image: ${PDO_REGISTRY:-}pdo_ccf:${PDO_VERSION:-latest} container_name: ccf_container network_mode: "host" volumes: @@ -29,7 +28,7 @@ services: entrypoint: /project/pdo/tools/start_ccf.sh -m build -i localhost --start services_container: - image: ${PDO_REPOSITORY:-}pdo_services:${PDO_VERSION:-latest} + image: ${PDO_REGISTRY:-}pdo_services:${PDO_VERSION:-latest} container_name: services_container depends_on: [ ccf_container ] network_mode: "host" diff --git a/docker/test_dev.yaml b/docker/test_dev.yaml index 56354cd..c1006b8 100644 --- a/docker/test_dev.yaml +++ b/docker/test_dev.yaml @@ -12,7 +12,6 @@ # See the License for the specific language governing permissions and # limitations under the License. # ------------------------------------------------------------------------------ -version: "3.4" # This compose file provides a means of starting the basic services # necessary for developing the contracts image. These can be started @@ -22,7 +21,7 @@ version: "3.4" services: ccf_container: - image: ${PDO_REPOSITORY:-}pdo_ccf:${PDO_VERSION:-latest} + image: ${PDO_REGISTRY:-}pdo_ccf:${PDO_VERSION:-latest} container_name: ccf_container network_mode: "host" volumes: @@ -30,7 +29,7 @@ services: entrypoint: /project/pdo/tools/start_ccf.sh -m build -i localhost --start services_container: - image: ${PDO_REPOSITORY:-}pdo_services:${PDO_VERSION:-latest} + image: ${PDO_REGISTRY:-}pdo_services:${PDO_VERSION:-latest} container_name: services_container depends_on: [ ccf_container ] network_mode: "host" @@ -45,3 +44,16 @@ services: volumes: - ./models/:/models/ command: --model_path /models/ --model_name resnet --port 9000 --shape auto --grpc_bind_address 127.0.0.1 + profiles: + - inference + + contracts_container: + image: pdo_contracts:${CONTRACTS_VERSION} + container_name: contracts_container + depends_on: + - services_container + network_mode: "host" + volumes: + - ./xfer/:/project/pdo/xfer/ + - ${CONTRACTS_SOURCE_ROOT:-./repository}:/project/pdo/dev/ + entrypoint: /project/pdo/tools/run_jupyter_tests.sh -i localhost -l http://127.0.0.1:6600 -s localhost diff --git a/docker/test_jupyter.yaml b/docker/test_jupyter.yaml index a29332a..fb4a261 100644 --- a/docker/test_jupyter.yaml +++ b/docker/test_jupyter.yaml @@ -12,7 +12,6 @@ # See the License for the specific language governing permissions and # limitations under the License. # ------------------------------------------------------------------------------ -version: "3.4" # This file creates a complete environment for testing the contracts # through the jupyter notebook interface. @@ -24,7 +23,7 @@ version: "3.4" services: ccf_container: - image: ${PDO_REPOSITORY:-}pdo_ccf:${PDO_VERSION:-latest} + image: ${PDO_REGISTRY:-}pdo_ccf:${PDO_VERSION:-latest} container_name: ccf_container network_mode: "host" volumes: @@ -32,7 +31,7 @@ services: entrypoint: /project/pdo/tools/start_ccf.sh -m build -i localhost --start services_container: - image: ${PDO_REPOSITORY:-}pdo_services:${PDO_VERSION:-latest} + image: ${PDO_REGISTRY:-}pdo_services:${PDO_VERSION:-latest} container_name: services_container depends_on: [ ccf_container ] network_mode: "host" diff --git a/docker/test_multiuser_jupyter.yaml b/docker/test_multiuser_jupyter.yaml index 05d45c4..bb789e2 100644 --- a/docker/test_multiuser_jupyter.yaml +++ b/docker/test_multiuser_jupyter.yaml @@ -12,7 +12,6 @@ # See the License for the specific language governing permissions and # limitations under the License. # ------------------------------------------------------------------------------ -version: "3.4" # This file creates a complete environment for testing the contracts # through the jupyter notebook interface. @@ -24,7 +23,7 @@ version: "3.4" services: ccf_container: - image: ${PDO_REPOSITORY:-}pdo_ccf:${PDO_VERSION:-latest} + image: ${PDO_REGISTRY:-}pdo_ccf:${PDO_VERSION:-latest} container_name: ccf_container network_mode: "host" volumes: @@ -32,7 +31,7 @@ services: entrypoint: /project/pdo/tools/start_ccf.sh -m build -i localhost --start services_container: - image: ${PDO_REPOSITORY:-}pdo_services:${PDO_VERSION:-latest} + image: ${PDO_REGISTRY:-}pdo_services:${PDO_VERSION:-latest} container_name: services_container depends_on: [ ccf_container ] network_mode: "host" diff --git a/docker/tools/run_jupyter_tests.sh b/docker/tools/run_jupyter_tests.sh index 48d4a3d..e0824fd 100755 --- a/docker/tools/run_jupyter_tests.sh +++ b/docker/tools/run_jupyter_tests.sh @@ -113,6 +113,7 @@ try ${PDO_INSTALL_ROOT}/bin/pdo-configure-users -t ${PDO_SOURCE_ROOT}/build/temp # ----------------------------------------------------------------- yell start the jupyter server # ----------------------------------------------------------------- +export SHELL=/bin/bash # make bash the default shell for jupyter launcher export PDO_JUPYTER_ROOT=${PDO_INSTALL_ROOT}/opt/pdo/notebooks cd ${PDO_JUPYTER_ROOT} diff --git a/example-contract/CMakeLists.txt b/example-contract/CMakeLists.txt new file mode 100644 index 0000000..21b4b39 --- /dev/null +++ b/example-contract/CMakeLists.txt @@ -0,0 +1,74 @@ +# Copyright 2019 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# ----------------------------------------------------------------- +# If you are building an independent contract family using the +# standard paths, you should not need to change anything below. +# Information about the contract family should be set in family.cmake +# ----------------------------------------------------------------- +INCLUDE(family.cmake) +INCLUDE(methods.cmake) + +# ----------------------------------------------------------------- +# Build the library of methods that are defined by the contract +# family. This simplifies sharing the methods with other contract +# families. +# ----------------------------------------------------------------- +ADD_LIBRARY(${${CF_HANDLE}_LIB} STATIC ${${CF_HANDLE}_SOURCES}) +TARGET_INCLUDE_DIRECTORIES(${${CF_HANDLE}_LIB} PUBLIC ${${CF_HANDLE}_INCLUDES}) +TARGET_INCLUDE_DIRECTORIES(${${CF_HANDLE}_LIB} PUBLIC ${WASM_INCLUDES}) + +SET_PROPERTY(TARGET ${${CF_HANDLE}_LIB} APPEND_STRING PROPERTY COMPILE_OPTIONS "${WASM_BUILD_OPTIONS}") +SET_PROPERTY(TARGET ${${CF_HANDLE}_LIB} APPEND_STRING PROPERTY LINK_OPTIONS "${WASM_LINK_OPTIONS}") +SET_TARGET_PROPERTIES(${${CF_HANDLE}_LIB} PROPERTIES EXCLUDE_FROM_ALL TRUE) + +# ----------------------------------------------------------------- +# Add a build target for each of the contracts; to ensure uniqueness +# prepend the name of the contract family to the name of the contract +# ----------------------------------------------------------------- +FOREACH(contract ${CF_CONTRACTS}) + BUILD_CONTRACT(${CF_NAME}_${contract} src/contracts/${contract}.cpp + HEADERS ${${CF_HANDLE}_INCLUDES} + LIBRARIES ${${CF_HANDLE}_LIB} + ) +ENDFOREACH() + +# ----------------------------------------------------------------- +# Build the python wheel that will install all of the contracts in the +# contract family; the contract targets must match the targets used +# to create the contracts above. +# ----------------------------------------------------------------- +INCLUDE(Python) +LIST(TRANSFORM CF_CONTRACTS PREPEND ${CF_NAME}_ OUTPUT_VARIABLE expanded_contracts) +BUILD_WHEEL(${CF_NAME} ${expanded_contracts}) + +# ----------------------------------------------------------------- +INCLUDE(Test) +ADD_SHELL_TEST(${CF_NAME} script SCRIPT test/script_test.sh) +ADD_SHELL_TEST(${CF_NAME} functional SCRIPT test/run_tests.sh) + +# ----------------------------------------------------------------- +# install the jupyter notebooks, note that the trailing slash here +# is significant and should not be removed; it prevents the notebooks +# directory being prepended to the copied name +# ----------------------------------------------------------------- +INCLUDE(Jupyter) +FILE(GLOB_RECURSE NOTEBOOK_SOURCES docs/notebooks/*.py docs/notebooks/*.md) +CONVERT_JUPYTEXT(EX_NOTEBOOKS ${NOTEBOOK_SOURCES}) + +ADD_CUSTOM_TARGET(${CF_NAME}-notebooks ALL DEPENDS ${EX_NOTEBOOKS}) + +INSTALL(DIRECTORY docs/notebooks/ + DESTINATION "${PDO_JUPYTER_ROOT}/${CF_NAME}" + FILES_MATCHING PATTERN "*.ipynb" PATTERN "files/*" ) diff --git a/example-contract/MANIFEST b/example-contract/MANIFEST new file mode 100644 index 0000000..847af1f --- /dev/null +++ b/example-contract/MANIFEST @@ -0,0 +1,14 @@ +MANIFEST.in +setup.py +context/counter.toml +etc/example.toml +pdo/__init__.py +pdo/example/__init__.py +pdo/example/jupyter/__init__.py +pdo/example/jupyter/context.py +pdo/example/plugins/__init__.py +pdo/example/plugins/counter.py +pdo/example/resources/__init__.py +pdo/example/resources/resources.py +pdo/example/scripts/__init__.py +pdo/example/scripts/scripts.py diff --git a/example-contract/MANIFEST.in b/example-contract/MANIFEST.in new file mode 100644 index 0000000..dfe4b55 --- /dev/null +++ b/example-contract/MANIFEST.in @@ -0,0 +1,4 @@ +recursive-include ../build/example-contract *.b64 +recursive-include etc *.toml +recursive-include context *.toml +recursive-include scripts *.psh \ No newline at end of file diff --git a/example-contract/README.md b/example-contract/README.md new file mode 100644 index 0000000..94b3369 --- /dev/null +++ b/example-contract/README.md @@ -0,0 +1,97 @@ + + +**The protocols and software are for reference purposes only and not intended for production usage.** + +# Example Contract Family # + +This directory contains an example contract family that can be used as +the basis for implementing your own contract families. This contract +family defines a single contract called `counter` that implements a +confidential integer counter. The family includes shell plugins, test +scripts, and a simple Jupyter notebook interface for creating and +interacting with counters. + +## Starting the Tutorial ## + +The `docs/notebooks/documents` directory contains a +[tutorial](docs/notebooks/documents/index.md) for creating a new +contract family. While the material may be used through an IDE, the +tutorial is intended to be viewed through the PDO Contracts Jupyter +server. The following steps will guide you through the process of +creating a development environment and starting a Jupyter server using +the [PDO contracts docker images](../docker/README.md). + +The following commands should be executed from the PDO contracts root +directory. + +1. Build the PDO docker images + +```bash +make -C docker build_pdo_images +``` + +2. Build the PDO contracts images and start the containers + +The `developer` target in the `docker` directory will create a +`pdo_contracts` image and will start local instances of all of the +necessary containers to create a complete PDO environment. + +*NOTE*: The `developer` target mounts the current PDO contracts source +directory into the `pdo_contracts` container in the directory +`/project/pdo/dev`. Any changes that you make to the source in the +container will be reflected in the source on the host. Likewise, any +changes you make to the host source will be reflected in the +container. + +```bash +make -C docker developer +``` + +3. Connect to the Jupyter server + +The `pdo_contracts` container created in the previous step starts a +Jupyter Lab server on port 8888. In the console for the container, you +should see instructions for how to connect to the server. It will look +something like: + +``` +contracts_container | To access the server, open this file in a browser: +contracts_container | file:///project/pdo/.local/share/jupyter/runtime/jpserver-43-open.html +contracts_container | Or copy and paste one of these URLs: +contracts_container | http://localhost:8888/lab?token= +contracts_container | http://127.0.0.1:8888/lab?token= +``` + +Open one of the `http` URLs (including the token) in your browser. + +4. Build and install the example contract family + +When you connect to the Jupyter Lab server, you will start on the +Launcher window. In the `Other` section of the launcher, click on +`Terminal` to open an interactive shell on the `pdo_contracts` +container. + +The `developer` docker target does not pre-build any of the PDO +contract families. The first thing to do is build the exchange and +example contract families. + +A brief comment about `Local.cmake`: this file allows for personalized +configuration of the PDO contracts build process. It is most often +used to specify the contract families that will be built by default +(the `CONTRACT_FAMILIES` list). The statement below will overwrite +any existing `Local.cmake`. + + +```bash +cd /project/pdo/dev +echo 'SET(CONTRACT_FAMILIES exchange-contract example-contract)' >| Local.cmake +make install +``` + + +5. Open the tutorial + +From the Jupyter launcher, navigate to `example/documents/index.ipynb`. diff --git a/example-contract/context/counter.toml b/example-contract/context/counter.toml new file mode 100644 index 0000000..848755a --- /dev/null +++ b/example-contract/context/counter.toml @@ -0,0 +1,8 @@ +# ----------------------------------------------------------------- +# counter ${counter} +# ----------------------------------------------------------------- +[counter.${counter}] +module = "pdo.example.plugins.counter" +identity = "user1" +source = "${ContractFamily.Example.counter.source}" +name = "${counter}" diff --git a/example-contract/docs/notebooks/.gitignore b/example-contract/docs/notebooks/.gitignore new file mode 100644 index 0000000..fa65608 --- /dev/null +++ b/example-contract/docs/notebooks/.gitignore @@ -0,0 +1 @@ +*.ipynb diff --git a/example-contract/docs/notebooks/documents/tutorial.md b/example-contract/docs/notebooks/documents/tutorial.md new file mode 100644 index 0000000..b30dc8c --- /dev/null +++ b/example-contract/docs/notebooks/documents/tutorial.md @@ -0,0 +1,403 @@ +--- +jupyter: + jupytext: + text_representation: + extension: .md + format_name: markdown + format_version: '1.3' + jupytext_version: 1.16.1 + kernelspec: + display_name: Bash + language: bash + name: bash +--- + + + +**The protocols and software are for reference purposes only and not intended for production usage.** + +> This tutorial is intended to be viewed through the PDO contract +> Jupyter server. For information on setting up the environment for +> the tutorial, please view the `README.md` in the `example-contract` +> directory. + + +# Contract Developer Tutorial # + +This page is intended to guide a contract developer through the +process of creating a new contract family based on the `Example +Contract Family`. The material here is intended to provide one method +for creating a new contract family; feel free to adjust your process +to your personal preferences. + + +# What is a Contract Family? # + +A contract family is a collection of PDO contracts that work together +to create an "application". For example, the contracts in the Exchange +contract family, provide contracts to create digital assets (like "red +marbles") with a verifiable trust chain, and contracts to use those +assets to purchase or exchange goods, services, or other assets. It is +this collection of contracts intentionally designed to work together +that we call a "contract family". + +Operationally, a contract family consists of a set of PDO contracts +and instructions for how to build them, configuration files that help +to specify the relationship between contracts or contract objects, and +plugins that makes it easier to use the contracts through a PDO shell, +a `bash` shell, or a Jupyter notebook. + + +# Contract Family Directory Layout ## + +* The root directory of the contract family generally contains + information useful for building and deploying contracts in the + family. + +* The `context` directory contains context files that describe the + relationship between contract objects and help to coordinate the + configuration of dependent contract objects. + +* The `src` directory contains the source files used to build the + contracts in the contract family. The source directory contains + several subdirectories: + + * The `common` directory contains modules that will be shared + amongst all of the contracts. + + * The `packages` directory may contain external dependencies + required for the contract. + + * The `methods` directory contains modules that implement different + groups of methods. For simple contracts there is generally one + method module per contract. More complex contracts may mix and + match methods from different modules. + + * The `contracts` directory contains the definition of the contracts + themselves. These are generally specified as a collection of + methods defined in the `methods` directory. + + * Finally, there is often a directory that contains interface + definitions that may be used by other contract families to re-use + methods. For example, this contract family defines an interface + for the counter contract in the `example` directory. + +* The `docs` directory generally contains interface specifications for + the methods in the contract. It may also contain the templates for + Jupyter notebooks used to interact with the contracts. + +* The `etc` directory contains basic configuration information. When + you customize your own contract family, you will need to update + information about the mapping between contract types and compiled + contract code. + +* The `pdo` directory is the root of the Python modules associated + with the contract family. The Python modules contain plugins for the + PDO and bash shells, and utility functions that can be invoked from + a Jupyter notebook. + +* The `scripts` directory may optionally contain PDO shell scripts + that will be installed with the deployed contracts. + +* The `test` directory defines system tests for the contract family + that will be automatically run by the contracts CI system. Tests are + broken into two parts, one for running a set of commands in a single + PDO shell invocation and one for running a series of bash shell + commands. Note that the two are similar but not the same. The PDO + shell will process transaction commits asynchronously while the bash + shell tests commit synchronously. + + +# Set up the Environment for Jupyter # + +Several of the shell commands below may be executed directly from the +notebook. The command blocks assume that the document is being viewed +through the Jupyter interface with the current source directory +mounted at `/project/pdo/dev` (the default if you started the Jupyter +Lab server using the `developer` target in the docker Makefile). This +command block should be run before any of the following statements is +run. + +```bash + export CONTRACT_SOURCE=/project/pdo/dev + cd ${CONTRACT_SOURCE} +``` + + +# Create a New Contract Family Using the Example Template # + +The following instructions use the `example` contract family as a +template for building a new contract family that we'll call the +`myfirst` contract family. While each contract family can be +structured to your personal preferences, the `example` contract family +provides an easy way to get started. + + +## Clone the Example Contract Family ## + +1. Copy the `example-contract` directory to a new directory. To be + picked up by the build system automatically, the new directory + should be named with `myfirst-contract`. + +```bash + cp -R example-contract myfirst-contract +``` + +2. Edit `myfirst-contract/family.cmake`. Replace `example` with + `myfirst` as the value of `CF_NAME`. `CF_NAME` is used to identify + the contract family. The other macro in that file is a list of + contracts that belong to the family. This will be addressed below + when we add a new method to the contract. + +```bash + sed -i s/example/myfirst/ myfirst-contract/family.cmake +``` + +3. Edit `myfirst-contract/setup.py`. Replace `example` with `myfirst` + as the value of the `contract_family` variable. Replace + `example_counter` with `myfirst_counter` as the value of the + `contract_scripts` variable. This variable provides a list of shell + scripts that will be installed for the contract family. Finally, + update the author information for the Python package. + +```bash + sed -i s/example/myfirst/ myfirst-contract/setup.py +``` + +4. Rename the `example` contract context file, the Python module + directory and the directory that contains exported header + files. The context file helps to specify the relationshihp between + contract objects (like an asset account and the issuer of the + assets). The other two directories are important for adding methods + to the contract. + +```bash + mv myfirst-contract/etc/example.toml myfirst-contract/etc/myfirst.toml + mv myfirst-contract/src/example myfirst-contract/src/myfirst + mv myfirst-contract/pdo/example myfirst-contract/pdo/myfirst +``` + +5. Replace references to `example` with `myfirst`. This will update + contract family references in several locations including the + contract source namespace in the `src` directory, the contract + references in the tests, and the plugin and resource references in + the Python modules. + +```bash +find myfirst-contract -type f -exec sed -i s/example/myfirst/g {} \; +find myfirst-contract -type f -exec sed -i s/Example/MyFirst/g {} \; +``` + + +## Test the New Contract Family ## + +At this point, assuming you have a complete PDO client installation, +you should be able build and test your new contract family. To do +this, add or edit the following line to `Local.cmake` in the PDO +contracts root directory: + +```bash +echo 'LIST(APPEND CONTRACT_FAMILIES myfirst-contract)' >> Local.cmake +``` + +This will limit the build process to the new contract family to +simplify testing. + +Assuming that you have installed and configured a PDO client +environment (and have activated the PDO Python virtual environment), +then you can build and run the tests: + +```bash +make clean +make install +make test +``` + + +# Add a New Contract to the MyFirst Contract Family # + +A PDO contract consists of a set of methods that operate on a +persistent state. Methods are generally grouped together in a +namespace to make them easier to re-use for multiple contracts. + +The `counter` methods module copied from the `example` contract family +initially defines two methods, one for incrementing the counter and +one for returning the current value of the counter (the methods module +is implemented in the file `src/methods/counter.cpp`). The methods +module also includes a function that can be used to initialize the +contract state prior to using either method. These methods can be +imported into any contract so long as the contract invokes the +initialization function prior to invoking either of the counter +methods. + +The `counter` contract is defined in `src/contracts/counter.cpp`. +Effectively, the contract consists of an initialization function that +will be called when the contract object is first created and a +dispatch table for methods on the contract object. The dispatch table +maps an external name of the method (all methods are invoked through a +JSON RPC protocol defined in PDO) to the code that implements the +method. + +Note that in the `counter` contract, the `initialize_contract` +function calls the `counter` method module initialization function so +that the `counter` methods will work correctly. + + +## Add a Method to Decrement the Counter ## + +This section contains instructions for adding a new method to the +`counter` method module. The new method, `dec_value` will decrement +the value of the counter and return the value as a result. Only the +creator of the contract object may invoke the method. The +implementation of `dec_value` method is provided the file +[dec_value.cpp](../files/dec_value.cpp). + +1. Add the method implementation to `src/methods/counter.cpp`: copy + the contents of [dec_value.cpp](../files/dec_value.cpp) to the end of + `src/methods/counter.cpp`. + +2. Add the interface to the method to the public header file + `src/myfirst/counter.h` in the `counter` namespace along with the + other contract methods. The interface can be found in the file + [dec_value.h](../files/dec_value.h). The public header file exports + the method to `counter` contracts and any other contract family + that would like to use the method. + +3. Add the new method to the dispatch table in the contract definition + file `src/contracts/counter.cpp`. The table uses the PDO macro + `CONTRACT_METHOD2` for specifying the external name of the method + and the contract function that will be invoked through that + name. The new method can be added to the contract with the line: + +```c++ + CONTRACT_METHOD2(dec_value, ww::myfirst_contract::counter::dec_value), +``` + + +## How to Define a New Contract ## + +As discussed in the previous section, the definition of a contract +comes from the dispatch table used to map method names to contract +functions implemented in the method module. In the `myfirst` contract +family, each file in the directory `src/contracts` represents a type +of contract. + +The previous section describes the process for adding a new method to +an existing contract. This section describes the steps to create a +new type of contract using the methods in the `counter` method module. + +The following steps are taken to create a new type of contract: + +1. *OPTIONAL*: Define and implement any additional methods that are + necessary in a method module. Methods may also be imported from + other contract families. See the `CMakeLists.txt` file in the + digital-asset contract family for an example of how methods are + imported from the exchange contract family. + +2. Create a new contract definition file in `src/contracts`. The file + must contain a contract initialization function and a dispatch + table for the methods exported by the contract. For example, copy + `src/contracts/counter.cpp` to `src/contracts/new_counter.cpp`. + +3. *OPTIONAL*: Edit the dispatch table in + `src/contracts/new_counter.cpp` to include any additional methods + you would like to include in the contract. Note that if you include + methods from method modules other than the `counter` method module, + you may need to invoke the initialization function in + `initialize_contract`. + +3. Add the new contract to the list of contracts in + `family.cmake`. That is, add the name of the new contract to the + `CF_CONTRACTS` macro. This might look like: + +``` +SET(CF_CONTRACTS counter new_counter) +``` + +4. Add the contract mapping to the context configuration file in the + `etc` directory. The name of the context file for the `myfirst` + contract family is `etc/myfirst.toml`. This mapping helps the + shells to identify the location of the compiled contract when a new + contract object is created. + +``` + new_counter = { source = "${home}/contracts/myfirst/_myfirst_new_counter.b64" } +``` + +Assuming that you have installed and configured a PDO client +environment (and have activated the PDO Python virtual environment), +then you can build and install the new contract: + +```bash +make install +``` + +The new contract should be available in the `${PDO_HOME}/contracts` +directory. To test the contract effectively will require a new Python +plugin for the contract. + + +# How to Define a New Shell Plugin # + +Shell plugins provide access to PDO contract methods through the PDO +shell, Bash shell, and other Python scripts using a single +implementation. + +Plugins come in two flavors. The first are *operations* derived from +the `pdo.client.builder.contract.contract_op_base` class. Operations +generally have a one-to-one correspondance to methods on the contract +object. The second are *commands* derived from the +`pdo.client.builder.command.contract_command_base` class. Commands are +generally used for complex interactions that involve invocation of +multiple operations potentially across multiple contract objects. + +For example, in the file `pdo/example/plugins/counter.py` the plugin +for `counter` contract objects defines one *operation* for each of the +methods (`inc_value` and `get_value`) and a *command* to create the +counter (which creates the contract object and initializes it). + +More information about plugins is available in the client +documentations from PDO. + + +## Add an Operation for a New Method ## + +In a previous tutorial, a `dec_value` method was added to the +`counter` contract object in the `myfirst` contract family. In order +to make that method available, it is recommended that you provide a +shell plugin *operation*. The new *operation* will process command +line parameters and invoke the `dec_value` method on a contract +object. An implementation of the plugin operation can be found in the +file [dec_plugin.py](../files/dec_plugin.py). + +1. Copy the contents of [dec_plugin.py](../files/dec_plugin.py) into the + `counter` plugins file at `pdo/myfirst/plugins/counter.py`. + +2. Add the name of the plugin operation, `op_dec_value`, to the + `__operations__` list in `pdo/myfirst/plugins/counter.py`. + +3. *OPTIONAL*: Although it is not necessary, you can add a *command* + plugin that will simplify scripted access to the `dec_value` + method. Copy the code from `cmd_inc_value` in the `counter` plugins + file, change the operation that is invoked to `op_dec_value`, and + add the new command to the `__commands__` list. + + +## Add a Plugin Module for a New Contract ## + +The easiest way to define a plugin for a new type of contract object +is to simply copy the basic framework for the `counter` plugin and +implement an operation for each method on your new contract. If your +new contract imports methods from another contract family, it is +relatively straightforward to import the plugin operations as well. + +Examples for invoking the operations and commands is provided by the +scripts in the `test` directory. + + +# Add a Juptyer Notebooks for Accessing a Contract # + +**This section is under construction.** diff --git a/example-contract/docs/notebooks/factories/counter.py b/example-contract/docs/notebooks/factories/counter.py new file mode 100644 index 0000000..5062993 --- /dev/null +++ b/example-contract/docs/notebooks/factories/counter.py @@ -0,0 +1,49 @@ +# --- +# jupyter: +# jupytext: +# text_representation: +# extension: .py +# format_name: percent +# format_version: '1.3' +# jupytext_version: 1.16.1 +# kernelspec: +# display_name: Python 3 (ipykernel) +# language: python +# name: python3 +# --- + +# %% [markdown] +# # Counter Factory +# +# This notebook simplifies the creation of counter. + +# %% +import os +import pdo.contracts.jupyter as pc_jupyter +import IPython.display as ip_display + +pc_jupyter.load_contract_families(exchange='ex', example='example') +pc_jupyter.load_ipython_extension(get_ipython()) + +try : state +except NameError: + (state, bindings) = pc_jupyter.initialize_environment('unknown') + +# %% [markdown] +# ## Configure Counter Information +# + +# %% tags=["parameters"] +counter_name = input('Name of the counter: ') +identity = input('Identity to use to create the counter [user1]: ') or "user1" +service_group = input('Service group [default]: ') or "default" + +# %% +instance_parameters = { + 'counter_owner' : identity, + 'counter_name' : counter_name, + 'service_group' : service_group, +} + +instance_file = pc_jupyter.instantiate_notebook_from_template(counter_name, 'counter', instance_parameters) +ip_display.display(ip_display.Markdown('[Newly created counter]({})'.format(instance_file))) diff --git a/example-contract/docs/notebooks/files/dec_plugin.py b/example-contract/docs/notebooks/files/dec_plugin.py new file mode 100644 index 0000000..45f2cfa --- /dev/null +++ b/example-contract/docs/notebooks/files/dec_plugin.py @@ -0,0 +1,10 @@ +## ----------------------------------------------------------------- +class op_dec_value(pcontract.contract_op_base) : + name = "dec_value" + help = "Decrement the value of the counter by 1" + + @classmethod + def invoke(cls, state, session_params, **kwargs) : + message = invocation_request('dec_value') + result = pcontract_cmd.send_to_contract(state, message, **session_params) + return result diff --git a/example-contract/docs/notebooks/files/dec_value.cpp b/example-contract/docs/notebooks/files/dec_value.cpp new file mode 100644 index 0000000..4e68758 --- /dev/null +++ b/example-contract/docs/notebooks/files/dec_value.cpp @@ -0,0 +1,25 @@ + +// ----------------------------------------------------------------- +// NAME: dec_value +// ----------------------------------------------------------------- +bool ww::myfirst_contract::counter::dec_value(const Message& msg, const Environment& env, Response& rsp) +{ + // + ASSERT_SENDER_IS_CREATOR(env, rsp); + + // get the current value of the counter from the contract state + uint32_t value; + if (! value_store.get(counter_key, value)) + return rsp.error("no such key"); + + // decrement the value + value -= 1; + + // store the new value in the contract state + if (! value_store.set(counter_key, value)) + return rsp.error("failed to save the new value"); + + // and return the new value + ww::value::Number v((double)value); + return rsp.value(v, true); +} diff --git a/example-contract/docs/notebooks/files/dec_value.h b/example-contract/docs/notebooks/files/dec_value.h new file mode 100644 index 0000000..a840502 --- /dev/null +++ b/example-contract/docs/notebooks/files/dec_value.h @@ -0,0 +1 @@ +bool dec_value(const Message& msg, const Environment& env, Response& rsp); diff --git a/example-contract/docs/notebooks/templates/counter.py b/example-contract/docs/notebooks/templates/counter.py new file mode 100644 index 0000000..06e381f --- /dev/null +++ b/example-contract/docs/notebooks/templates/counter.py @@ -0,0 +1,112 @@ +# --- +# jupyter: +# jupytext: +# text_representation: +# extension: .py +# format_name: percent +# format_version: '1.3' +# jupytext_version: 1.16.1 +# kernelspec: +# display_name: Python 3 (ipykernel) +# language: python +# name: python3 +# --- + +# %% [markdown] +# # Notebook to Invoke Counter Operations + +# %% [markdown] +# ## Configure Counter Information +# +# %% tags=["parameters"] +counter_owner = 'user1' +counter_name = 'counter_1' +service_group = 'default' +instance_identifier = '' + +# %% [markdown] +#
+# +# ## Initialize + +# %% +import os +import pdo.contracts.jupyter as pc_jupyter + +pc_jupyter.load_contract_families(exchange='ex', example='example') +pc_jupyter.load_ipython_extension(get_ipython()) + +# %% [markdown] +# ### Initialize the PDO Environment +# +# Initialize the PDO environment. This assumes that a functional PDO configuration is in place and +# that the PDO virtual environment has been activated. +# +# For the most part, no modifications should be required below. +# %% +common_bindings = { + 'counter_owner' : counter_owner, + 'counter_name' : counter_name, + 'instance' : instance_identifier, +} + +try : state +except NameError : + (state, bindings) = pc_jupyter.initialize_environment(counter_owner, **common_bindings) + +print('environment initialized') + +# %% [markdown] +# ### Initialize the Contract Context +# +# The contract context defines the configuration for a collection of contract objects that interact +# with one another. In the case of the counter contract, there are no interactions and the context +# is very simple. + +# %% +context_file = bindings.expand('${etc}/context/${counter_name}_${instance}.toml') +print("using context file {}".format(context_file)) + +context_bindings = { + 'identity' : counter_owner, + 'service_group' : service_group, +} + +counter_path = 'counter.' + counter_name +context = pc_jupyter.example_jupyter.initialize_counter_context( + state, bindings, context_file, counter_path, **context_bindings) +print('context initialized') + +counter_context = pc_jupyter.pbuilder.Context(state, counter_path + '.counter') +counter_save_file = pc_jupyter.pcommand.invoke_contract_cmd( + pc_jupyter.example_counter.cmd_create_counter, state, counter_context) +pc_jupyter.pbuilder.Context.SaveContextFile(state, context_file, prefix=counter_path) +print('saved counter contract in {}'.format(counter_save_file)) + +# %% [markdown] +#
+# ## Operate on the Counter +# + +# %% [markdown] +# ### Get Counter Value +# +# %% +def get_counter_value() : + return pc_jupyter.pcommand.invoke_contract_cmd( + pc_jupyter.example_counter.cmd_get_value, state, counter_context) +# %% +# %%skip True +print("The current value of the counter is {}".format(get_counter_value())) + +# %% [markdown] +# ### Increment the Counter Value +# +# %% +def inc_counter_value() : + return pc_jupyter.pcommand.invoke_contract_cmd( + pc_jupyter.example_counter.cmd_inc_value, state, counter_context) +# %% +# %%skip True +inc_counter_value() +print("The current value of the counter is {}".format(get_counter_value())) diff --git a/example-contract/etc/example.toml b/example-contract/etc/example.toml new file mode 100644 index 0000000..58c842a --- /dev/null +++ b/example-contract/etc/example.toml @@ -0,0 +1,5 @@ +# ----------------------------------------------------------------- +# Example contract family contract source +# ----------------------------------------------------------------- +[ContractFamily.Example] +counter = { source = "${home}/contracts/example/_example_counter.b64" } diff --git a/example-contract/family.cmake b/example-contract/family.cmake new file mode 100644 index 0000000..99beae4 --- /dev/null +++ b/example-contract/family.cmake @@ -0,0 +1,18 @@ +# Copyright 2019 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +SET(CF_NAME example) +SET(CF_CONTRACTS counter) + +STRING(TOUPPER ${CF_NAME} CF_HANDLE) diff --git a/example-contract/methods.cmake b/example-contract/methods.cmake new file mode 100644 index 0000000..eb111ee --- /dev/null +++ b/example-contract/methods.cmake @@ -0,0 +1,36 @@ +# Copyright 2022 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +INCLUDE(family.cmake) + +# --------------------------------------------- +# Set up the include list +# --------------------------------------------- +SET (${CF_HANDLE}_INCLUDES ${WASM_INCLUDES}) +LIST(APPEND ${CF_HANDLE}_INCLUDES ${CMAKE_CURRENT_LIST_DIR}/src) + +# --------------------------------------------- +# Set up the default source list +# --------------------------------------------- +FILE(GLOB ${CF_HANDLE}_COMMON_SOURCE ${CMAKE_CURRENT_LIST_DIR}/src/common/*.cpp) +FILE(GLOB ${CF_HANDLE}_CONTRACT_SOURCE ${CMAKE_CURRENT_LIST_DIR}/src/methods/*.cpp) + +SET (${CF_HANDLE}_SOURCES PARENT_SCOPE) +LIST(APPEND ${CF_HANDLE}_SOURCES ${${CF_HANDLE}_COMMON_SOURCE}) +LIST(APPEND ${CF_HANDLE}_SOURCES ${${CF_HANDLE}_CONTRACT_SOURCE}) + +# --------------------------------------------- +# Build the wawaka contract common library +# --------------------------------------------- +SET(${CF_HANDLE}_LIB ww_${CF_NAME}) diff --git a/example-contract/pdo/__init__.py b/example-contract/pdo/__init__.py new file mode 100644 index 0000000..25bb107 --- /dev/null +++ b/example-contract/pdo/__init__.py @@ -0,0 +1,15 @@ +# Copyright 2018 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +__import__('pkg_resources').declare_namespace('pdo') diff --git a/example-contract/pdo/example/__init__.py b/example-contract/pdo/example/__init__.py new file mode 100644 index 0000000..67128f4 --- /dev/null +++ b/example-contract/pdo/example/__init__.py @@ -0,0 +1,15 @@ +# Copyright 2018 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +__all__ = [ 'jupyter', 'plugins', 'resources', 'scripts' ] diff --git a/example-contract/pdo/example/jupyter/__init__.py b/example-contract/pdo/example/jupyter/__init__.py new file mode 100644 index 0000000..4185650 --- /dev/null +++ b/example-contract/pdo/example/jupyter/__init__.py @@ -0,0 +1,18 @@ + +# Copyright 2024 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +__all__ = [ 'context' ] + +from .context import * diff --git a/example-contract/pdo/example/jupyter/context.py b/example-contract/pdo/example/jupyter/context.py new file mode 100644 index 0000000..25e8c1c --- /dev/null +++ b/example-contract/pdo/example/jupyter/context.py @@ -0,0 +1,57 @@ +# Copyright 2024 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import logging + +import pdo.contracts.common as jp_common +import pdo.exchange.jupyter as ex_jupyter + +_logger = logging.getLogger(__name__) + +__all__ = [ + 'counter_context', + 'initialize_counter_context', +] + +# ----------------------------------------------------------------- +# set up the context +# ----------------------------------------------------------------- +counter_context = jp_common.ContextTemplate('counter', { + 'module' : 'pdo.example.plugins.counter', + 'identity' : '${..identity}', + 'source' : '${ContractFamily.Example.counter.source}', + 'eservice_group' : '${..eservice_group}', + 'pservice_group' : '${..pservice_group}', + 'sservice_group' : '${..sservice_group}', +}) + +# ----------------------------------------------------------------- +# ----------------------------------------------------------------- +def initialize_counter_context(state, bindings, context_file : str, prefix : str, **kwargs) : + """Initialize a context for counters + + @type state: pdo.client.builder.state.State + @param state: client state + @type bindings: pdo.client.builder.bindings.Bindings + @param bindings: current interpreter bindings + @param context_file: name of the context file + @param prefix: prefix for paths in the context + @keyword kwargs: dictionary of string (paths) to values that override context + @rtype: pdo.client.builder.context.Context + @return: the initialized context + """ + contexts = [ + counter_context, + ] + return jp_common.initialize_context(state, bindings, context_file, prefix, contexts, **kwargs) diff --git a/example-contract/pdo/example/plugins/__init__.py b/example-contract/pdo/example/plugins/__init__.py new file mode 100644 index 0000000..e93a2d9 --- /dev/null +++ b/example-contract/pdo/example/plugins/__init__.py @@ -0,0 +1,15 @@ +# Copyright 2022 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +__all__ = [ 'counter' ] diff --git a/example-contract/pdo/example/plugins/counter.py b/example-contract/pdo/example/plugins/counter.py new file mode 100644 index 0000000..17214e6 --- /dev/null +++ b/example-contract/pdo/example/plugins/counter.py @@ -0,0 +1,168 @@ +# Copyright 2018 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import logging + +from pdo.contract import invocation_request + +import pdo.client.builder as pbuilder +import pdo.client.builder.command as pcommand +import pdo.client.builder.contract as pcontract +import pdo.client.builder.shell as pshell +import pdo.client.commands.contract as pcontract_cmd + +logger = logging.getLogger(__name__) + +__all__ = [ + 'op_inc_value', + 'op_get_value', + 'cmd_create_counter', + 'cmd_inc_value', + 'cmd_get_value', + 'do_example_counter', + 'do_example_counter_contract', + 'load_commands', +] + +logger = logging.getLogger(__name__) + +## ----------------------------------------------------------------- +class op_inc_value(pcontract.contract_op_base) : + name = "inc_value" + help = "Increment the value of the counter by 1" + + @classmethod + def invoke(cls, state, session_params, **kwargs) : + message = invocation_request('inc_value') + result = pcontract_cmd.send_to_contract(state, message, **session_params) + return result + +## ----------------------------------------------------------------- +class op_get_value(pcontract.contract_op_base) : + name = "get_value" + help = "Get the current value of the counter" + + @classmethod + def invoke(cls, state, session_params, **kwargs) : + message = invocation_request('get_value') + result = pcontract_cmd.send_to_contract(state, message, **session_params) + return result + +# ----------------------------------------------------------------- +# create a counter +# ----------------------------------------------------------------- +class cmd_create_counter(pcommand.contract_command_base) : + """Create a counter + """ + name = "create" + help = "create a counter" + + @classmethod + def add_arguments(cls, parser) : + parser.add_argument('-c', '--contract-class', help='Name of the contract class', type=str) + parser.add_argument('-e', '--eservice-group', help='Name of the enclave service group to use', type=str) + parser.add_argument('-f', '--save-file', help='File where contract data is stored', type=str) + parser.add_argument('-p', '--pservice-group', help='Name of the provisioning service group to use', type=str) + parser.add_argument('-r', '--sservice-group', help='Name of the storage service group to use', type=str) + parser.add_argument('--source', help='File that contains contract source code', type=str) + parser.add_argument('--extra', help='Extra data associated with the contract file', nargs=2, action='append') + + @classmethod + def invoke(cls, state, context, **kwargs) : + if pcontract_cmd.get_contract_from_context(state, context) : + return + + # create the counter contract + save_file = pcontract_cmd.create_contract_from_context(state, context, 'example_counter', **kwargs) + context['save_file'] = save_file + + return save_file + +# ----------------------------------------------------------------- +# increment a counter +# ----------------------------------------------------------------- +class cmd_inc_value(pcommand.contract_command_base) : + """Increment the value of a counter + """ + + name = "inc_value" + help = "increment the counter" + + @classmethod + def invoke(cls, state, context, **kwargs) : + + save_file = pcontract_cmd.get_contract_from_context(state, context) + if not save_file : + raise ValueError('counter contract must be created and initialized') + + session = pbuilder.SessionParameters(save_file=save_file) + result = pcontract.invoke_contract_op( + op_inc_value, + state, context, session, + **kwargs) + + cls.display('current value of the counter is {}'.format(result)) + return result + +# ----------------------------------------------------------------- +# get the value of a counter +# ----------------------------------------------------------------- +class cmd_get_value(pcommand.contract_command_base) : + """Get the value of a counter + """ + + name = "get_value" + help = "get the current value of the counter" + + @classmethod + def invoke(cls, state, context, **kwargs) : + + save_file = pcontract_cmd.get_contract_from_context(state, context) + if not save_file : + raise ValueError('counter contract must be created and initialized') + + session = pbuilder.SessionParameters(save_file=save_file) + result = pcontract.invoke_contract_op( + op_get_value, + state, context, session, + **kwargs) + + cls.display('current value of the counter is {}'.format(result)) + return result + + +## ----------------------------------------------------------------- +## Create the generic, shell independent version of the aggregate command +## ----------------------------------------------------------------- +__operations__ = [ + op_inc_value, + op_get_value, +] + +do_example_counter_contract = pcontract.create_shell_command('example_counter_contract', __operations__) + +__commands__ = [ + cmd_create_counter, + cmd_inc_value, + cmd_get_value, +] + +do_example_counter = pcommand.create_shell_command('example_counter', __commands__) + +## ----------------------------------------------------------------- +## Enable binding of the shell independent version to a pdo-shell command +## ----------------------------------------------------------------- +def load_commands(cmdclass) : + pshell.bind_shell_command(cmdclass, 'example_counter', do_example_counter) + pshell.bind_shell_command(cmdclass, 'example_counter_contract', do_example_counter_contract) diff --git a/example-contract/pdo/example/resources/__init__.py b/example-contract/pdo/example/resources/__init__.py new file mode 100644 index 0000000..7cf1870 --- /dev/null +++ b/example-contract/pdo/example/resources/__init__.py @@ -0,0 +1,15 @@ +# Copyright 2023 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +__all__ = ['resources'] diff --git a/example-contract/pdo/example/resources/resources.py b/example-contract/pdo/example/resources/resources.py new file mode 100644 index 0000000..8a432c2 --- /dev/null +++ b/example-contract/pdo/example/resources/resources.py @@ -0,0 +1,18 @@ +# Copyright 2023 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import pdo.client.builder.installer as pinstaller + +def install_example_resources() : + pinstaller.install_plugin_resources('pdo.example.resources', 'example') diff --git a/example-contract/pdo/example/scripts/__init__.py b/example-contract/pdo/example/scripts/__init__.py new file mode 100644 index 0000000..e850a69 --- /dev/null +++ b/example-contract/pdo/example/scripts/__init__.py @@ -0,0 +1,15 @@ +# Copyright 2022 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +__all__ = [ 'scripts' ] diff --git a/example-contract/pdo/example/scripts/scripts.py b/example-contract/pdo/example/scripts/scripts.py new file mode 100644 index 0000000..37028ab --- /dev/null +++ b/example-contract/pdo/example/scripts/scripts.py @@ -0,0 +1,24 @@ +# Copyright 2022 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from pdo.client.builder.shell import run_shell_command + +import warnings +warnings.catch_warnings() +warnings.simplefilter("ignore") + +# ----------------------------------------------------------------- +# ----------------------------------------------------------------- +def example_counter() : + run_shell_command('do_example_counter', 'pdo.example.plugins.counter') diff --git a/example-contract/scripts/.keep b/example-contract/scripts/.keep new file mode 100644 index 0000000..e69de29 diff --git a/example-contract/setup.py b/example-contract/setup.py new file mode 100644 index 0000000..d6c022b --- /dev/null +++ b/example-contract/setup.py @@ -0,0 +1,103 @@ +#!/usr/bin/env python + +# Copyright 2022 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os +import sys +import subprocess +import warnings + +## ----------------------------------------------------------------- +# Change values of the following variables to customize your +# contract family; the contract_scripts variable is a list of +# bash commands that will be created in the python wheel. See +# ./pdo/{contract_family}/scripts/scripts.py for more information +# on building command line scripts. +## ----------------------------------------------------------------- +contract_family = 'example' +contract_scripts = [ 'example_counter' ] + +author = 'Mic Bowman, Intel Labs' +author_email = 'mic.bowman@intel.com' +author_url = 'http://www.intel.com' + +## ----------------------------------------------------------------- +# Unless you change the standard behavior or layout of the contract +# there should be no changes required below +## ----------------------------------------------------------------- + +# this should only be run with python3 +if sys.version_info[0] < 3: + print('ERROR: must run with python3') + sys.exit(1) + +from setuptools import setup + +## ----------------------------------------------------------------- +# Versions are tied to tags on the repository; to compute correctly +# it is necessary to be within the repository itself hence the need +# to set the cwd for the bin/get_version command. +## ----------------------------------------------------------------- +root_dir = os.path.dirname(os.path.realpath(__file__)) +try : + pdo_contracts_version = subprocess.check_output( + 'bin/get_version', cwd=os.path.join(root_dir, os.pardir)).decode('ascii').strip() +except Exception as e : + warnings.warn(f'Failed to get pdo_contracts version, using the default; {e}') + pdo_contracts_version = '0.0.0' + +try : + pdo_client_version = subprocess.check_output( + 'bin/get_version', cwd=os.path.join(root_dir, os.pardir, 'private-data-objects')).decode('ascii').strip() +except Exception as e : + warnings.warn(f'Failed to get pdo_client version, using the default; {e}') + pdo_client_version = '0.0.0' + +## ----------------------------------------------------------------- +## ----------------------------------------------------------------- +setup( + name=f'pdo_{contract_family}', + version=pdo_contracts_version, + description='Contract and support scripts for a PDO contract', + author=author, + author_email=author_email, + url=author_url, + package_dir = { + 'pdo' : 'pdo', + f'pdo.{contract_family}.resources.etc' : 'etc', + f'pdo.{contract_family}.resources.context' : 'context', + f'pdo.{contract_family}.resources.contracts' : f'../build/{contract_family}-contract', + }, + packages = [ + 'pdo', + f'pdo.{contract_family}', + f'pdo.{contract_family}.jupyter', + f'pdo.{contract_family}.plugins', + f'pdo.{contract_family}.scripts', + f'pdo.{contract_family}.resources', + f'pdo.{contract_family}.resources.etc', + f'pdo.{contract_family}.resources.context', + f'pdo.{contract_family}.resources.contracts', + ], + include_package_data=True, + install_requires = [ + 'colorama', + 'pdo-client>=' + pdo_client_version, + 'pdo-common-library>=' + pdo_client_version, + ], + entry_points = { + 'console_scripts' : list(map(lambda s : f'{s}=pdo.{contract_family}.scripts.scripts:{s}', contract_scripts)), + } +) diff --git a/example-contract/src/common/.keep b/example-contract/src/common/.keep new file mode 100644 index 0000000..e69de29 diff --git a/example-contract/src/contracts/counter.cpp b/example-contract/src/contracts/counter.cpp new file mode 100644 index 0000000..03d7a6f --- /dev/null +++ b/example-contract/src/contracts/counter.cpp @@ -0,0 +1,35 @@ +/* Copyright 2019 Intel Corporation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "Dispatch.h" + +#include "Environment.h" +#include "Response.h" + +#include "example/counter.h" + +// ----------------------------------------------------------------- +// NAME: initialize_contract +// ----------------------------------------------------------------- +bool initialize_contract(const Environment& env, Response& rsp) +{ + return ww::example_contract::counter::initialize_contract(env, rsp); +} + +contract_method_reference_t contract_method_dispatch_table[] = { + CONTRACT_METHOD2(inc_value, ww::example_contract::counter::inc_value), + CONTRACT_METHOD2(get_value, ww::example_contract::counter::get_value), + { NULL, NULL } +}; diff --git a/example-contract/src/contracts/counter.h b/example-contract/src/contracts/counter.h new file mode 100644 index 0000000..6379b75 --- /dev/null +++ b/example-contract/src/contracts/counter.h @@ -0,0 +1,40 @@ +/* Copyright 2019 Intel Corporation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include + +#include "Environment.h" +#include "Message.h" +#include "Response.h" +#include "Util.h" + +namespace ww +{ +namespace example +{ +namespace counter +{ + bool initialize_contract(const Environment& env, Response& rsp); + + // capability handlers, these must be executed through the process + // capability interface + bool inc_value(const Message& msg, const Environment& env, Response& rsp); + bool get_value(const Message& msg, const Environment& env, Response& rsp); +} /* counter */ +} /* example */ +} /* ww + */ diff --git a/example-contract/src/example/counter.h b/example-contract/src/example/counter.h new file mode 100644 index 0000000..e1f1a70 --- /dev/null +++ b/example-contract/src/example/counter.h @@ -0,0 +1,37 @@ +/* Copyright 2019 Intel Corporation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include + +#include "Environment.h" +#include "Message.h" +#include "Response.h" + +namespace ww +{ +namespace example_contract +{ +namespace counter +{ + bool initialize_contract(const Environment& env, Response& rsp); + + bool inc_value(const Message& msg, const Environment& env, Response& rsp); + bool get_value(const Message& msg, const Environment& env, Response& rsp); +} /* counter */ +} /* example_contract */ +} /* ww + */ diff --git a/example-contract/src/methods/counter.cpp b/example-contract/src/methods/counter.cpp new file mode 100644 index 0000000..c1e6964 --- /dev/null +++ b/example-contract/src/methods/counter.cpp @@ -0,0 +1,85 @@ +/* Copyright 2019 Intel Corporation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include +#include + +#include "Dispatch.h" + +#include "KeyValue.h" +#include "Environment.h" +#include "Message.h" +#include "Response.h" +#include "Util.h" +#include "Value.h" +#include "WasmExtensions.h" + +#include "example/counter.h" + +static KeyValueStore meta_store("meta"); +static KeyValueStore value_store("values"); + +const std::string counter_key("counter"); + +// ----------------------------------------------------------------- +// NAME: initialize_contract +// ----------------------------------------------------------------- +bool ww::example_contract::counter::initialize_contract(const Environment& env, Response& rsp) +{ + // create the value and save it to state + const uint32_t value = 0; + + if (! value_store.set(counter_key, value)) + return rsp.error("failed to create the test key"); + + return rsp.success(true); +} + +// ----------------------------------------------------------------- +// NAME: inc_value +// ----------------------------------------------------------------- +bool ww::example_contract::counter::inc_value(const Message& msg, const Environment& env, Response& rsp) +{ + ASSERT_SENDER_IS_CREATOR(env, rsp); + + // get the value and increment it + uint32_t value; + if (! value_store.get(counter_key, value)) + return rsp.error("no such key"); + + value += 1; + if (! value_store.set(counter_key, value)) + return rsp.error("failed to save the new value"); + + ww::value::Number v((double)value); + return rsp.value(v, true); +} + +// ----------------------------------------------------------------- +// NAME: get_value +// ----------------------------------------------------------------- +bool ww::example_contract::counter::get_value(const Message& msg, const Environment& env, Response& rsp) +{ + ASSERT_SENDER_IS_CREATOR(env, rsp); + + // get the value + uint32_t value; + if (! value_store.get(counter_key, value)) + return rsp.error("no such key"); + + ww::value::Number v((double)value); + return rsp.value(v, false); +} diff --git a/example-contract/src/packages/.keep b/example-contract/src/packages/.keep new file mode 100644 index 0000000..e69de29 diff --git a/example-contract/test/.gitignore b/example-contract/test/.gitignore new file mode 100644 index 0000000..eb0abc5 --- /dev/null +++ b/example-contract/test/.gitignore @@ -0,0 +1 @@ +test_context.toml diff --git a/example-contract/test/README.md b/example-contract/test/README.md new file mode 100644 index 0000000..92ee672 --- /dev/null +++ b/example-contract/test/README.md @@ -0,0 +1,35 @@ + +# Example Test Scripts + +This directory contains a number of pdo-shell scripts to test the +digital asset contract family. The scripts assume that a complete +installation of the PDO client is complete. + +## Test Scripts + +Test scripts take several parameters that can override the standard +PDO environment variables: + +* `--host` : Specify the host where the PDO services operate, override `PDO_HOSTNAME` +* `--ledger` : Specify the URL for the PDO ledger, override `PDO_LEDGER_URL` +* `--loglevel` : Specify logging verbosity +* `--logfile` : Specify the logging output file + +### `run_tests.sh` + +This script sets up and runs the functional test suite for the digital asset +contract family. The actual tests will be found in the pdo-shell script +`functional_test.psh` + +### `script_test.sh` + +This script tests the `bash` entry points for digital asset plugins. + +### `functional_test.psh` + +This script provides a functional test of the various contract +types in the digital asset contract family. In general, this should +not be invoked directly but should be called through `run-tests.sh`. diff --git a/example-contract/test/functional_test.psh b/example-contract/test/functional_test.psh new file mode 100755 index 0000000..6f12c1e --- /dev/null +++ b/example-contract/test/functional_test.psh @@ -0,0 +1,92 @@ +#! /usr/bin/env pdo-shell + +## Copyright 2022 Intel Corporation +## +## Licensed under the Apache License, Version 2.0 (the "License"); +## you may not use this file except in compliance with the License. +## You may obtain a copy of the License at +## +## http://www.apache.org/licenses/LICENSE-2.0 +## +## Unless required by applicable law or agreed to in writing, software +## distributed under the License is distributed on an "AS IS" BASIS, +## WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +## See the License for the specific language governing permissions and +## limitations under the License. + +## Two shell variables are used: +## data -- the directory where the contract objects are stored +## path -- the directory where the PSH scripts are stored +## +## $ pdo-shell -s create.psh -m path + +set --conditional -s home -v . +set --conditional -s data -v . +set --conditional -s save -v . +set --conditional -s contracts -v ${home}/contracts/example +set --conditional -s plugins -v ${home}/contracts/plugins/example + +if -e "${_identity_}" "__unknown__" + identity -n user1 +fi + +set --conditional -s _counter1_ -v ${save}/counter1.pdo +set --conditional -s _counter2_ -v ${save}/counter2.pdo + +## some definitions to make it easier to display text +set -s ENDC -v "\033[0m" +set -s BOLD -v '\033[1m' +set -s HEADER -v "\033[95m" +set -s ERROR -v "\033[91m" +set -s WARN -v "\033[93m" +set -s INFO -v "\033[92m" + +load_plugin -m pdo.example.plugins.counter + +## ----------------------------------------------------------------- +echo ${HEADER} create the counter ${ENDC} +## ----------------------------------------------------------------- +trap_error + +contract create -c example_counter --source ${contracts}/_example_counter -f ${_counter1_} +if -o ${_error_code_} 0 + echo ${ERROR} [ERROR ${_error_code_}] failed to create the counter; ${_error_message_} + exit -v ${_error_code_} +fi + +## ----------------------------------------------------------------- +echo ${HEADER} increment the counter ${ENDC} +## ----------------------------------------------------------------- +example_counter_contract inc_value -f ${_counter1_} -s _value_ +if -o ${_error_code_} 0 + echo ${ERROR} [ERROR ${_error_code_}] failed to increment the counter; ${_error_message_} + exit -v ${_error_code_} +fi + +if --not -e ${_value_} 1 + echo ${ERROR} [ERROR ${_error_code_}] invalid initial value; ${_value_} + exit -v -1 +fi + +## repeat the increment +example_counter_contract inc_value -f ${_counter1_} -s _value_ +example_counter_contract inc_value -f ${_counter1_} -s _value_ +example_counter_contract inc_value -f ${_counter1_} -s _value_ +example_counter_contract inc_value -f ${_counter1_} -s _value_ + +## ----------------------------------------------------------------- +echo ${HEADER} get the counter value ${ENDC} +## ----------------------------------------------------------------- +example_counter_contract get_value -f ${_counter1_} -s _value_ +if -o ${_error_code_} 0 + echo ${ERROR} [ERROR ${_error_code_}] failed to get the counter value; ${_error_message_} + exit -v ${_error_code_} +fi + +if --not -e ${_value_} 5 + echo ${ERROR} [ERROR ${_error_code_}] invalid counter value; ${_value_} instead of 5 + exit -v -1 +fi + +echo ${HEADER} all counter tests passed ${ENDC} +exit diff --git a/example-contract/test/run_tests.sh b/example-contract/test/run_tests.sh new file mode 100755 index 0000000..a0e51bf --- /dev/null +++ b/example-contract/test/run_tests.sh @@ -0,0 +1,159 @@ +#!/bin/bash + +# Copyright 2018 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# ----------------------------------------------------------------- +# ----------------------------------------------------------------- +: "${PDO_LEDGER_URL?Missing environment variable PDO_LEDGER_URL}" +: "${PDO_HOME?Missing environment variable PDO_HOME}" +: "${PDO_SOURCE_ROOT?Missing environment variable PDO_SOURCE_ROOT}" + +# ----------------------------------------------------------------- +# ----------------------------------------------------------------- +source ${PDO_HOME}/bin/lib/common.sh +check_python_version + +if ! command -v pdo-shell &> /dev/null ; then + die unable to locate pdo-shell +fi + +# ----------------------------------------------------------------- +# ----------------------------------------------------------------- +if [ "${PDO_LEDGER_TYPE}" == "ccf" ]; then + if [ ! -f "${PDO_LEDGER_KEY_ROOT}/networkcert.pem" ]; then + die "CCF ledger keys are missing, please copy and try again" + fi +fi + +# ----------------------------------------------------------------- +# Process command line arguments +# ----------------------------------------------------------------- +SCRIPTDIR="$(dirname $(readlink --canonicalize ${BASH_SOURCE}))" +SOURCE_ROOT="$(realpath ${SCRIPTDIR}/..)" + +F_SCRIPT=$(basename ${BASH_SOURCE[-1]} ) +F_SERVICE_HOST=${PDO_HOSTNAME} +F_LEDGER_URL=${PDO_LEDGER_URL} +F_LOGLEVEL=${PDO_LOG_LEVEL:-info} +F_LOGFILE=${PDO_LOG_FILE:-__screen__} +F_CONTEXT_FILE=${SOURCE_ROOT}/test/test_context.toml +F_CONTEXT_TEMPLATES=${PDO_HOME}/contracts/example/context + +F_USAGE='--host service-host | --ledger url | --loglevel [debug|info|warn] | --logfile file' +SHORT_OPTS='h:l:' +LONG_OPTS='host:,ledger:,loglevel:,logfile:' + +TEMP=$(getopt -o ${SHORT_OPTS} --long ${LONG_OPTS} -n "${F_SCRIPT}" -- "$@") +if [ $? != 0 ] ; then echo "Usage: ${F_SCRIPT} ${F_USAGE}" >&2 ; exit 1 ; fi + +eval set -- "$TEMP" +while true ; do + case "$1" in + -h|--host) F_SERVICE_HOST="$2" ; shift 2 ;; + -1|--ledger) F_LEDGER_URL="$2" ; shift 2 ;; + --loglevel) F_LOGLEVEL="$2" ; shift 2 ;; + --logfile) F_LOGFILE="$2" ; shift 2 ;; + --help) echo "Usage: ${SCRIPT_NAME} ${F_USAGE}"; exit 0 ;; + --) shift ; break ;; + *) echo "Internal error!" ; exit 1 ;; + esac +done + +F_SERVICE_SITE_FILE=${PDO_HOME}/etc/sites/${F_SERVICE_HOST}.toml +if [ ! -f ${F_SERVICE_SITE_FILE} ] ; then + die unable to locate the service information file ${F_SERVICE_SITE_FILE}; \ + please copy the site.toml file from the service host +fi + +F_SERVICE_GROUPS_DB_FILE=${SOURCE_ROOT}/test/${F_SERVICE_HOST}_groups_db +F_SERVICE_DB_FILE=${SOURCE_ROOT}/test/${F_SERVICE_HOST}_db + +_COMMON_=("--logfile ${F_LOGFILE}" "--loglevel ${F_LOGLEVEL}") +_COMMON_+=("--ledger ${F_LEDGER_URL}") +_COMMON_+=("--groups-db ${F_SERVICE_GROUPS_DB_FILE}") +_COMMON_+=("--service-db ${F_SERVICE_DB_FILE}") +SHORT_OPTS=${_COMMON_[@]} + +_COMMON_+=("--context-file ${F_CONTEXT_FILE}") +OPTS=${_COMMON_[@]} + +# ----------------------------------------------------------------- +# Make sure the keys and eservice database are created and up to date +# ----------------------------------------------------------------- +F_KEY_FILES=() +KEYGEN=${PDO_SOURCE_ROOT}/build/__tools__/make-keys +if [ ! -f ${PDO_HOME}/keys/red_type_private.pem ]; then + yell create keys for the contracts + for color in red green blue orange purple white ; do + ${KEYGEN} --keyfile ${PDO_HOME}/keys/${color}_type --format pem + ${KEYGEN} --keyfile ${PDO_HOME}/keys/${color}_vetting --format pem + ${KEYGEN} --keyfile ${PDO_HOME}/keys/${color}_issuer --format pem + F_KEY_FILES+=(${PDO_HOME}/keys/${color}_{type,vetting,issuer}_{private,public}.pem) + done + + for color in green1 green2 green3; do + ${KEYGEN} --keyfile ${PDO_HOME}/keys/${color}_issuer --format pem + F_KEY_FILES+=(${PDO_HOME}/keys/${color}_issuer_{private,public}.pem) + done + + ${KEYGEN} --keyfile ${PDO_HOME}/keys/token_type --format pem + ${KEYGEN} --keyfile ${PDO_HOME}/keys/token_vetting --format pem + ${KEYGEN} --keyfile ${PDO_HOME}/keys/token_issuer --format pem + F_KEY_FILES+=(${PDO_HOME}/keys/token_{type,vetting,issuer}_{private,public}.pem) + for count in 1 2 3 4 5 ; do + ${KEYGEN} --keyfile ${PDO_HOME}/keys/token_holder${count} --format pem + F_KEY_FILES+=(${PDO_HOME}/keys/token_holder${count}_{private,public}.pem) + done +fi + +# ----------------------------------------------------------------- +function cleanup { + rm -f ${F_SERVICE_GROUPS_DB_FILE} ${F_SERVICE_GROUPS_DB_FILE}-lock + rm -f ${F_SERVICE_DB_FILE} ${F_SERVICE_DB_FILE}-lock + rm -f ${F_CONTEXT_FILE} + for key_file in ${F_KEY_FILES[@]} ; do + rm -f ${key_file} + done +} + +trap cleanup EXIT + +# ----------------------------------------------------------------- +# create the service and groups databases from a site file; the site +# file is assumed to exist in ${PDO_HOME}/etc/sites/${SERVICE_HOST}.toml +# +# by default, the groups will include all available services from the +# service host +# ----------------------------------------------------------------- +yell create the service and groups database for host ${F_SERVICE_HOST} +try pdo-service-db import ${SHORT_OPTS} --file ${F_SERVICE_SITE_FILE} +try pdo-eservice create_from_site ${SHORT_OPTS} --file ${F_SERVICE_SITE_FILE} --group default +try pdo-pservice create_from_site ${SHORT_OPTS} --file ${F_SERVICE_SITE_FILE} --group default +try pdo-sservice create_from_site ${SHORT_OPTS} --file ${F_SERVICE_SITE_FILE} --group default \ + --replicas 1 --duration 60 + +# ----------------------------------------------------------------- +# ----------------------------------------------------------------- +cd "${SOURCE_ROOT}" + +rm -f ${F_CONTEXT_FILE} + +yell create the context for the counter object +try pdo-context load ${OPTS} --import-file ${F_CONTEXT_TEMPLATES}/counter.toml --bind counter mycounter + +# ----------------------------------------------------------------- +# ----------------------------------------------------------------- +cd "${SOURCE_ROOT}" +try ${SCRIPTDIR}/functional_test.psh ${OPTS} diff --git a/example-contract/test/script_test.sh b/example-contract/test/script_test.sh new file mode 100755 index 0000000..7943293 --- /dev/null +++ b/example-contract/test/script_test.sh @@ -0,0 +1,171 @@ +#!/bin/bash + +# Copyright 2022 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# ----------------------------------------------------------------- +# ----------------------------------------------------------------- +: "${PDO_LEDGER_URL?Missing environment variable PDO_LEDGER_URL}" +: "${PDO_HOME?Missing environment variable PDO_HOME}" +: "${PDO_SOURCE_ROOT?Missing environment variable PDO_SOURCE_ROOT}" + +# ----------------------------------------------------------------- +# ----------------------------------------------------------------- +source ${PDO_HOME}/bin/lib/common.sh +check_python_version + +if ! command -v pdo-shell &> /dev/null ; then + die unable to locate pdo-shell +fi + +# ----------------------------------------------------------------- +# ----------------------------------------------------------------- +if [ "${PDO_LEDGER_TYPE}" == "ccf" ]; then + if [ ! -f "${PDO_LEDGER_KEY_ROOT}/networkcert.pem" ]; then + die "CCF ledger keys are missing, please copy and try again" + fi +fi + +# ----------------------------------------------------------------- +# Process command line arguments +# ----------------------------------------------------------------- +SCRIPTDIR="$(dirname $(readlink --canonicalize ${BASH_SOURCE}))" +SOURCE_ROOT="$(realpath ${SCRIPTDIR}/..)" + +F_SCRIPT=$(basename ${BASH_SOURCE[-1]} ) +F_SERVICE_HOST=${PDO_HOSTNAME} +F_LEDGER_URL=${PDO_LEDGER_URL} +F_LOGLEVEL=${PDO_LOG_LEVEL:-info} +F_LOGFILE=${PDO_LOG_FILE:-__screen__} +F_CONTEXT_FILE=${SOURCE_ROOT}/test/test_context.toml +F_CONTEXT_TEMPLATES=${PDO_HOME}/contracts/example/context + +F_USAGE='--host service-host | --ledger url | --loglevel [debug|info|warn] | --logfile file' +SHORT_OPTS='h:l:' +LONG_OPTS='host:,ledger:,loglevel:,logfile:' + +TEMP=$(getopt -o ${SHORT_OPTS} --long ${LONG_OPTS} -n "${F_SCRIPT}" -- "$@") +if [ $? != 0 ] ; then echo "Usage: ${F_SCRIPT} ${F_USAGE}" >&2 ; exit 1 ; fi + +eval set -- "$TEMP" +while true ; do + case "$1" in + -h|--host) F_SERVICE_HOST="$2" ; shift 2 ;; + -1|--ledger) F_LEDGER_URL="$2" ; shift 2 ;; + --loglevel) F_LOGLEVEL="$2" ; shift 2 ;; + --logfile) F_LOGFILE="$2" ; shift 2 ;; + --help) echo "Usage: ${SCRIPT_NAME} ${F_USAGE}"; exit 0 ;; + --) shift ; break ;; + *) echo "Internal error!" ; exit 1 ;; + esac +done + +F_SERVICE_SITE_FILE=${PDO_HOME}/etc/sites/${F_SERVICE_HOST}.toml +if [ ! -f ${F_SERVICE_SITE_FILE} ] ; then + die unable to locate the service information file ${F_SERVICE_SITE_FILE}; \ + please copy the site.toml file from the service host +fi + +F_SERVICE_GROUPS_DB_FILE=${SOURCE_ROOT}/test/${F_SERVICE_HOST}_groups_db +F_SERVICE_DB_FILE=${SOURCE_ROOT}/test/${F_SERVICE_HOST}_db + +_COMMON_=("--logfile ${F_LOGFILE}" "--loglevel ${F_LOGLEVEL}") +_COMMON_+=("--ledger ${F_LEDGER_URL}") +_COMMON_+=("--groups-db ${F_SERVICE_GROUPS_DB_FILE}") +_COMMON_+=("--service-db ${F_SERVICE_DB_FILE}") +SHORT_OPTS=${_COMMON_[@]} + +_COMMON_+=("--context-file ${F_CONTEXT_FILE}") +OPTS=${_COMMON_[@]} + +# ----------------------------------------------------------------- +# Make sure the keys and eservice database are created and up to date +# ----------------------------------------------------------------- +F_KEY_FILES=() +KEYGEN=${PDO_SOURCE_ROOT}/build/__tools__/make-keys +if [ ! -f ${PDO_HOME}/keys/red_type_private.pem ]; then + yell create keys for the contracts + for color in red green blue orange purple white ; do + ${KEYGEN} --keyfile ${PDO_HOME}/keys/${color}_type --format pem + ${KEYGEN} --keyfile ${PDO_HOME}/keys/${color}_vetting --format pem + ${KEYGEN} --keyfile ${PDO_HOME}/keys/${color}_issuer --format pem + F_KEY_FILES+=(${PDO_HOME}/keys/${color}_{type,vetting,issuer}_{private,public}.pem) + done + + for color in green1 green2 green3; do + ${KEYGEN} --keyfile ${PDO_HOME}/keys/${color}_issuer --format pem + F_KEY_FILES+=(${PDO_HOME}/keys/${color}_issuer_{private,public}.pem) + done + + ${KEYGEN} --keyfile ${PDO_HOME}/keys/token_type --format pem + ${KEYGEN} --keyfile ${PDO_HOME}/keys/token_vetting --format pem + ${KEYGEN} --keyfile ${PDO_HOME}/keys/token_issuer --format pem + F_KEY_FILES+=(${PDO_HOME}/keys/token_{type,vetting,issuer}_{private,public}.pem) + for count in 1 2 3 4 5 ; do + ${KEYGEN} --keyfile ${PDO_HOME}/keys/token_holder${count} --format pem + F_KEY_FILES+=(${PDO_HOME}/keys/token_holder${count}_{private,public}.pem) + done +fi + +# ----------------------------------------------------------------- +function cleanup { + rm -f ${F_SERVICE_GROUPS_DB_FILE} ${F_SERVICE_GROUPS_DB_FILE}-lock + rm -f ${F_SERVICE_DB_FILE} ${F_SERVICE_DB_FILE}-lock + rm -f ${F_CONTEXT_FILE} + for key_file in ${F_KEY_FILES[@]} ; do + rm -f ${key_file} + done +} + +trap cleanup EXIT + +# ----------------------------------------------------------------- +# create the service and groups databases from a site file; the site +# file is assumed to exist in ${PDO_HOME}/etc/sites/${SERVICE_HOST}.toml +# +# by default, the groups will include all available services from the +# service host +# ----------------------------------------------------------------- +yell create the service and groups database for host ${F_SERVICE_HOST} +try pdo-service-db import ${SHORT_OPTS} --file ${F_SERVICE_SITE_FILE} +try pdo-eservice create_from_site ${SHORT_OPTS} --file ${F_SERVICE_SITE_FILE} --group default +try pdo-pservice create_from_site ${SHORT_OPTS} --file ${F_SERVICE_SITE_FILE} --group default +try pdo-sservice create_from_site ${SHORT_OPTS} --file ${F_SERVICE_SITE_FILE} --group default \ + --replicas 1 --duration 60 + +# ----------------------------------------------------------------- +# setup the contexts that will be used later for the tests +# ----------------------------------------------------------------- +cd "${SOURCE_ROOT}" + +rm -f ${F_CONTEXT_FILE} + +yell create the context for the counter object +try pdo-context load ${OPTS} --import-file ${F_CONTEXT_TEMPLATES}/counter.toml --bind counter mycounter + +# ----------------------------------------------------------------- +# start the tests +# ----------------------------------------------------------------- +yell create a counter +try example_counter create ${OPTS} --contract counter.mycounter + +yell increment the counter +try example_counter inc_value ${OPTS} --contract counter.mycounter +try example_counter inc_value ${OPTS} --contract counter.mycounter +try example_counter inc_value ${OPTS} --contract counter.mycounter +try example_counter inc_value ${OPTS} --contract counter.mycounter +try example_counter inc_value ${OPTS} --contract counter.mycounter + +yell get the value of the counter +try example_counter get_value ${OPTS} --contract counter.mycounter diff --git a/identity-contract/CMakeLists.txt b/identity-contract/CMakeLists.txt new file mode 100644 index 0000000..80c7bb7 --- /dev/null +++ b/identity-contract/CMakeLists.txt @@ -0,0 +1,44 @@ +# Copyright 2023 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +INCLUDE(exchange-contract/methods) +INCLUDE(methods.cmake) + +# ----------------------------------------------------------------- +ADD_LIBRARY(${IDENTITY_LIB} STATIC ${IDENTITY_SOURCES}) +TARGET_INCLUDE_DIRECTORIES(${IDENTITY_LIB} PUBLIC ${IDENTITY_INCLUDES}) +TARGET_INCLUDE_DIRECTORIES(${IDENTITY_LIB} PUBLIC ${EXCHANGE_INCLUDES}) +TARGET_INCLUDE_DIRECTORIES(${IDENTITY_LIB} PUBLIC ${WASM_INCLUDES}) + +SET_PROPERTY(TARGET ${IDENTITY_LIB} APPEND_STRING PROPERTY COMPILE_OPTIONS "${WASM_BUILD_OPTIONS}") +SET_PROPERTY(TARGET ${IDENTITY_LIB} APPEND_STRING PROPERTY LINK_OPTIONS "${WASM_LINK_OPTIONS}") +SET_TARGET_PROPERTIES(${IDENTITY_LIB} PROPERTIES EXCLUDE_FROM_ALL TRUE) + +# ----------------------------------------------------------------- +BUILD_CONTRACT(signature_authority contracts/signature_authority.cpp + HEADERS ${EXCHANGE_INCLUDES} ${IDENTITY_INCLUDES} + LIBRARIES ${EXCHANGE_LIB} ${IDENTITY_LIB}) + +BUILD_CONTRACT(identity contracts/identity.cpp + HEADERS ${EXCHANGE_INCLUDES} ${IDENTITY_INCLUDES} + LIBRARIES ${EXCHANGE_LIB} ${IDENTITY_LIB}) + +# ----------------------------------------------------------------- +INCLUDE(Python) +BUILD_WHEEL(identity) + +# ----------------------------------------------------------------- +INCLUDE(Test) +# ADD_SHELL_TEST(identity script SCRIPT test/script_test.sh) +ADD_SHELL_TEST(identity functional SCRIPT test/run_tests.sh) diff --git a/identity-contract/MANIFEST b/identity-contract/MANIFEST new file mode 100644 index 0000000..ff7d12d --- /dev/null +++ b/identity-contract/MANIFEST @@ -0,0 +1,11 @@ +MANIFEST.in +setup.py +pdo/__init__.py +pdo/identity/__init__.py +pdo/identity/plugins/__init__.py +pdo/identity/plugins/signature_authority.py +pdo/identity/plugins/identity.py +pdo/identity/resources/__init__.py +pdo/identity/resources/resources.py +pdo/identity/scripts/__init__.py +pdo/identity/scripts/scripts.py diff --git a/identity-contract/MANIFEST.in b/identity-contract/MANIFEST.in new file mode 100644 index 0000000..8b0ba15 --- /dev/null +++ b/identity-contract/MANIFEST.in @@ -0,0 +1,3 @@ +recursive-include ../build/identity-contract *.b64 +recursive-include etc *.toml +recursive-include scripts *.psh \ No newline at end of file diff --git a/identity-contract/README.md b/identity-contract/README.md new file mode 100644 index 0000000..3276c21 --- /dev/null +++ b/identity-contract/README.md @@ -0,0 +1,116 @@ + + +**The protocols and software are for reference purposes only and not intended for production usage.** + +# Identity Contract Family + +## Usages + +### Proof of Group Membership + +*SIMPLE EXAMPLE* + +### Proof of Majority + +The Oregon DMV issues a standard driver's license credential for Oregon +residents. The credential includes the driver's name, address, birth date, and +several biometric identifiers (height, weight, eye color, picture). A local +restaurant requires proof that a patron is over the age of 21 before serving +alcohol. The patron would like to present a credential to the restaurant that +proves age without disclosing birth date, address, or name. + +Proposed Solution: The Oregon DMV issues a license credential to Alice. This +credential is a JSON blob adhering to the W3C VC standards with the Oregon DMV +signature in the proof block. Alice stores the credential in her identity +contract object. Alice enters a restaurant and orders a drink. The restaurant +requests proof that Alice is over 21. Alice retrieves the license credential +from her identity contract object and submits it to the Age Verification trust +authority (AVTA) contract object. The AVTA contract object identifies the +credential as a license credential (it also accepts passport credentials) and +verifies the integrity of the credential using the signature in the proof +block. If the integrity check is successful, the AVTA contract object checks the +birth date in the credential against the current date and issues a new +credential with the assertion that the bearer is older than 21. The new +credential contains biometric identifiers to associate the credential with a +specific bearer. Alice stores the AVTA credential in her identity contract and +presents it to the restaurant for verification. The restaurant recognizes the +AVTA contract object as a legitimate source of age credentials (that is, it is +part of the restaurant's trust network), verifies the integrity of the +credential, and uses the biometric identifiers to confirm that the credential +belongs to Alice. + +### Prescription + +This example is based on threats identified in the [Web Of Trust document](https://github.com/WebOfTrustInfo/rwot9-prague/blob/master/final-documents/alice-attempts-abuse-verifiable-credential.md). + +A doctor issues prescription to Alice in the form of a credential. The +prescription credential contains information about the drug Alice may use and +also biometric identifiers derived from an identity credential (along the lines +of the driver's lience credential in the previous example) that Alice keeps +stored in her identity contract object. The pharmacy that Alice selects requires +proof that the prescription was written by a legitimate, licensed doctor; that +it was written for Alice; that it has not been filled by another pharmacy; and +that Alice's insurance will cover the cost of the drug identified by the +prescription. *Note that the proposed solution does not address the uniqueness +or insurance aspects of this usage.* + +Proposed Solution: The doctor issues a prescription credential to Alice. Alice +selects a pharmacy and finds which trust authorities are required for +verification. She picks a trust authority that represents her local doctors (the +trust authority maintains a list of locally licensed physicians, the LLPTA) and +presents the prescription credential to the trust authority. The LLPTA verifies +the physician's identity and issues a new credential to Alice. Alice presents +the LLPTA credential to the pharmacy who verifies that it was issued by one of +the LLPTA's that it trusts. Along with the credential, Alice presents her +biometric identifiers to confirm that she is, in fact, the subject of the +prescription. + +There are several alternatives to verify the doctor's identity. For example, the +pharmacy could retrieve the doctor's credentials explicitly. The approach +described here has two advantages. The first is that the doctor's identity is +hidden from the pharmacy. The second is that the LLPTA may have several ways to +validate the doctor's license to prescribe medicine. The policy for what may be +prescribed may differ from person to person. + +### Provenance + +### Endorsement + +### Reputation (?) + + + +## Contract Objects + +### Identity + +Store credentials. +List stored credentials. +Create a presentation from the stored credentials. + +### Trust Agent + +A trust agent contract implements, at a minimum, an operation that takes a list +of credentials as input and outputs a new credential. The configuration of a +trust agent may include specification of schemas for the incoming credentials, +configuration of trusted credential issuers, or other operations to configure +the context of the trust agent. + +The primary purpose of the trust agent is to handle delegation of policy. + +The primary operation is to take a list of credentials, apply a policy to the +credentials, and then issue a new credential. The policy may be as simple as +validating the schema of the incoming credential. It may also perform a +computation on the credential (for example, computing majority from a +birthdate). + +Pre-configured with some policy. +Provided a list of credentials. +Issues a new credential. diff --git a/identity-contract/contracts/identity.cpp b/identity-contract/contracts/identity.cpp new file mode 100644 index 0000000..6f48b66 --- /dev/null +++ b/identity-contract/contracts/identity.cpp @@ -0,0 +1,65 @@ +/* Copyright 2023 Intel Corporation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include +#include + +#include "Dispatch.h" + +#include "KeyValue.h" +#include "Environment.h" +#include "Message.h" +#include "Response.h" +#include "Types.h" +#include "Util.h" +#include "Value.h" +#include "WasmExtensions.h" + +#include "contract/base.h" +#include "identity/identity.h" + +// ----------------------------------------------------------------- +// METHOD: initialize_contract +// contract initialization method +// +// JSON PARAMETERS: +// none +// +// RETURNS: +// true if successfully initialized +// ----------------------------------------------------------------- +bool initialize_contract(const Environment& env, Response& rsp) +{ + // ---------- initialize the base contract ---------- + ASSERT_SUCCESS(rsp, ww::identity::identity::initialize_contract(env), + "unexpected error: failed to initialize the contract"); + + return rsp.success(true); +} + +// ----------------------------------------------------------------- +// ----------------------------------------------------------------- +contract_method_reference_t contract_method_dispatch_table[] = { + CONTRACT_METHOD2(initialize, ww::identity::identity::initialize), + + CONTRACT_METHOD2(get_verifying_key, ww::identity::identity::get_verifying_key), + CONTRACT_METHOD2(register_signing_context, ww::identity::identity::register_signing_context), + CONTRACT_METHOD2(describe_signing_context, ww::identity::identity::describe_signing_context), + CONTRACT_METHOD2(sign, ww::identity::identity::sign), + CONTRACT_METHOD2(verify, ww::identity::identity::verify), + + { NULL, NULL } +}; diff --git a/identity-contract/contracts/signature_authority.cpp b/identity-contract/contracts/signature_authority.cpp new file mode 100644 index 0000000..559ed3a --- /dev/null +++ b/identity-contract/contracts/signature_authority.cpp @@ -0,0 +1,69 @@ +/* Copyright 2023 Intel Corporation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include +#include + +#include "Dispatch.h" + +#include "KeyValue.h" +#include "Environment.h" +#include "Message.h" +#include "Response.h" +#include "Types.h" +#include "Util.h" +#include "Value.h" +#include "WasmExtensions.h" + +#include "contract/base.h" +#include "identity/identity.h" +#include "identity/signature_authority.h" + +// ----------------------------------------------------------------- +// METHOD: initialize_contract +// contract initialization method +// +// JSON PARAMETERS: +// none +// +// RETURNS: +// true if successfully initialized +// ----------------------------------------------------------------- +bool initialize_contract(const Environment& env, Response& rsp) +{ + // ---------- initialize the base contract ---------- + ASSERT_SUCCESS(rsp, ww::identity::identity::initialize_contract(env), + "unexpected error: failed to initialize the contract"); + + return rsp.success(true); +} + +// ----------------------------------------------------------------- +// ----------------------------------------------------------------- +contract_method_reference_t contract_method_dispatch_table[] = { + CONTRACT_METHOD2(initialize, ww::identity::identity::initialize), + + CONTRACT_METHOD2(get_verifying_key, ww::identity::identity::get_verifying_key), + CONTRACT_METHOD2(register_signing_context, ww::identity::identity::register_signing_context), + CONTRACT_METHOD2(describe_signing_context, ww::identity::identity::describe_signing_context), + CONTRACT_METHOD2(sign, ww::identity::identity::sign), + CONTRACT_METHOD2(verify, ww::identity::identity::verify), + + CONTRACT_METHOD2(sign_credential, ww::identity::signature_authority::sign_credential), + CONTRACT_METHOD2(verify_credential, ww::identity::signature_authority::verify_credential), + + { NULL, NULL } +}; diff --git a/identity-contract/docs/examples/vc_scai_attributes.json b/identity-contract/docs/examples/vc_scai_attributes.json new file mode 100644 index 0000000..1a467a7 --- /dev/null +++ b/identity-contract/docs/examples/vc_scai_attributes.json @@ -0,0 +1,59 @@ +{ + "//": "this is a verifiable credential", + "nonce": "p86OvShgiWuODUdABK6Ong==", + "issuer": { + "description": "The build manager is the issuer, the id is for the build manager org", + "id": "PDO://ledgerco.org:8001/A6QBclbcAayAvw7BggM7iMz_Xa6NEn_YGT94mpkQmEk=", + } + "issuanceDate": "04/17/2023", + "expirationData": "12/31/2024", + "type": [ "VerifiableCredential" ], + "credentialSchema": "http://example.com/scai_schema.json", + "credentialSubject": + { + "subject": { + "id": "PDO://ledgerco.org:8001/SMeYjWc5IOdvI3KJtPbx4WHlRvkdL5A__xcHayDj9+0=", + "identified_by": { + "sha256": "0b222de8bcb1ea30807f1a4d733108e96de4512b689da8c5f371ac8a572e9271" + } + }, + "claims": { + "HasStackProtection": { + "conditions": { + "flags": "-fstack-protector*" + }, + }, + "NoKnownVulnerabilities": { + "evidence": { + "mediaType":"text/plain" + "digest": { + "sha256":"6db3ee1d8bdf7ea064281ecfaf999f9eb5749e4647d2a59e62eaa1ea8fcf6837" + }, + "downloadLocation":"https://scans.example.com/results/snyk", + } + }, + "HasSBOM": { + "evidence": { + "mediaType": "application/spdx+json", + "digest": { + "sha256": "911d4365b61ba7ace55f7333b2c638caca4b811ee73da5beb28b9ecbbd22ca78" + }, + "downloadLocation": "https://github.com/hyperledger-labs/private-data-objects/artifacts/808758122" + } + }, + "HasSLSA": { + "evidence": { + "mediaType": "application/x.dsse+jsonl", + "digest": { + "sha256": "ea4d1e56e739f26a451c095b9fb40a353b3e73ea1778fdddafe13562e81bd745" + }, + "downloadLocation": "https://github.com/hyperledger-labs/private-data-objects/artifacts/808758121" + } + }, + "ProducedBy": { + "id": "PDO://ledgerco.org:8001/contract_identifer_for_gcc_binary", + "command_line": "gcc -fstack-protector -o hello-world hello-world.c" + } + } + } +} diff --git a/identity-contract/docs/examples/vc_softball_coach.json b/identity-contract/docs/examples/vc_softball_coach.json new file mode 100644 index 0000000..ed73ce9 --- /dev/null +++ b/identity-contract/docs/examples/vc_softball_coach.json @@ -0,0 +1,36 @@ +{ + "//": "this is a verifiable credential", + "id": "PDO://ledgerco.org:8001/SMeYjWc5IOdvI3KJtPbx4WHlRvkdL5A__xcHayDj9-0=/XT3LAe_ERcsP2HGdzEA9gw==" + "issuer": { + "description": "Issuer is Washington County ASA Commissioner, contract object is ASA national office", + "id": "PDO://ledgerco.org:8001/A6QBclbcAayAvw7BggM7iMz_Xa6NEn_YGT94mpkQmEk=", + "context_path": [ "Oregon", "Washington County", "GeorgeSmiley" ] + } + "issuanceDate": "03/21/2023", + "expirationData": "12/31/2024", + "credentialSubject": { + "//": "Local coach is the subject of the credential", + "id": "PDO://ledgerco.org:8001/SMeYjWc5IOdvI3KJtPbx4WHlRvkdL5A__xcHayDj9-0=" + "identified_by": { + "type": "http://registerusasoftball.com/schema/identifying_information.json", + "picture": { + "url": "http://photos.google.com/", + "hash": "" + } + "date_of_birth": "06/19/1972", + }, + "certified_coach": { + "type": "http://registerusasoftball.com/schema/asa_coach_certification.json", + "certification_level": 5, + "background_check": true, + "concussion_certification": true + }, + "insured_by": { + "type": "http://registerusasoftball.com/schema/asa_insurance.json", + "member_id": "1375777", + "policy_number": "NAS1A21", + "maximum_liability_claim": 1000000, + "deductible": 250 + } + } +} diff --git a/identity-contract/docs/examples/vc_softball_player.json b/identity-contract/docs/examples/vc_softball_player.json new file mode 100644 index 0000000..6340928 --- /dev/null +++ b/identity-contract/docs/examples/vc_softball_player.json @@ -0,0 +1,35 @@ +{ + "//": "this is a verifiable credential", + "id": "PDO://ledgerco.org:8001/SMeYjWc5IOdvI3KJtPbx4WHlRvkdL5A__xcHayDj9-0=/p86OvShgiWuODUdABK6Ong==" + "issuer": { + "description": "Issuer is Washington County ASA Commissioner, contract object is USA softball national office", + "id": "PDO://ledgerco.org:8001/A6QBclbcAayAvw7BggM7iMz_Xa6NEn_YGT94mpkQmEk=", + "context_path": [ "Oregon", "Washington County", "GeorgeSmiley" ] + } + "issuanceDate": "04/17/2023", + "expirationData": "12/31/2024", + "credentialSubject": + { + "id": "PDO://ledgerco.org:8001/SMeYjWc5IOdvI3KJtPbx4WHlRvkdL5A__xcHayDj9+0=", + "identified_by": { + "type": "http://registerusasoftball.com/schema/identifying_information.json", + "picture": { + "url": "http://photos.google.com/", + "hash": "" + } + "date_of_birth": "07/23/2005", + }, + "certified_player": { + "type": "http://registerusasoftball.com/schema/asa_player_certification.json", + "usa_softball_age": 18, + "rostered_by": "PDO://ledgerco.org:8001/", + "member_id": "1375777" + }, + "insured_by": { + "type": "http://registerusasoftball.com/schema/asa_insurance.json", + "policy_number": "NAS1A21", + "maximum_liability_claim": 1000000, + "deductible": 250 + } + } +} diff --git a/identity-contract/docs/specs/basetypes.json b/identity-contract/docs/specs/basetypes.json new file mode 100644 index 0000000..e264f84 --- /dev/null +++ b/identity-contract/docs/specs/basetypes.json @@ -0,0 +1,94 @@ +{ + "$schema": "http://json-schema.org/schema#", + "title": "PDO Credential Interface", + "id": "http://tradenet.org/pdo-contracts/identity/credential#", + + "definitions": { + "identity": { + "//": "-----------------------------------------------------------------", + "description": [ + "Provides a reference to a specific identity contract object" + ], + "type": "object", + "properties": { + "id": { + "description": [ + "Identity of the subject of the credential, since all subjects", + "are contracts this will be assumed to be a contract id" + ], + "$ref": "#/pdo/basetypes/contract-id", + "required": true + }, + "name": { + "description": [ + "A human readable name for the object", + "It need not be unique nor meaningful" + ], + "type": "string", + "required": false + }, + "description": { + "description": [ + "A human readable description of the object" + ], + "type": "string", + "required": false + }, + "identified_by": { + "description": [ + "A list of attributes that can be used to establish the binding", + "between an entity and the identity contract object." + ], + "type": "object", + "required": false + } + } + }, + "identity_key": { + "//": "-----------------------------------------------------------------", + "description": [ + "Provides information required to identify a specific key in the", + "context of an identity object" + ], + "type": "object", + "properties": { + "id": { + "description": [ + "Identity of the subject of the credential, since all subjects", + "are contracts this will be assumed to be a contract id" + ], + "$ref": "#/pdo/basetypes/contract-id", + "required": true + }, + "context_path": { + "description": [ + "A list of strings that contextualizes the identity" + ], + "type": "array", + "items": { + "type" : "string", + "minItems": 0, + "uniqueItems": false + } + "required": true + } + "name": { + "description": [ + "A human readable name for the object", + "It need not be unique nor meaningful" + ], + "type": "string", + "required": false + }, + "description": { + "description": [ + "A human readable description of the object" + ], + "type": "string", + "required": false + } + } + }, + + } +} diff --git a/identity-contract/docs/specs/credential.json b/identity-contract/docs/specs/credential.json new file mode 100644 index 0000000..d25733d --- /dev/null +++ b/identity-contract/docs/specs/credential.json @@ -0,0 +1,274 @@ +{ + "$schema": "http://json-schema.org/schema#", + "title": "PDO Credential Interface", + "id": "http://tradenet.org/pdo-contracts/identity/credential#", + + "definitions": { + "claim": { + "//": "-----------------------------------------------------------------", + "description": [ + ], + "type": "object", + "properties": { + "subject": { + "description": [ + "Identity of the subject of the credential, since all subjects", + "are contracts this will be assumed to be a contract id" + ], + "$ref": "#/pdo/identity/basetypes/identity", + "required": true + }, + "claims": { + "description": [ + "A list of objects that describe issued properties of the subject.", + "Each credential type will define and enforce a schema on the claims", + "specified in the credential" + ], + "type": "object", + "required": true + } + } + }, + + + "proof": { + "//": "-----------------------------------------------------------------", + "description": [ + ], + "type": "object", + "properties": { + "type": { + "description": [ + "Verification method used in the proof.", + "Specifically intended to be the cryptographic verification system." + ], + "type": "string", + "enum": [ + "ecdsa_secp384r1" + ], + "required": true + }, + "created": { + "description": [ + "This is the time when the proof was created.", + "TIME FIELD" + ], + "type": "datetime", + "required": false + }, + "verificationMethod": { + "description": [ + "Identity of the entity generating the proof and a contextualizing context path.", + ], + "$ref": "#/pdo/identity/basetypes/identity_key", + "required": true + }, + "proofPurpose": { + "description": [ + "Identifies what the proof claims.", + "For example, the proof may be that the claims match a schema." + ], + "type": "string", + "enum": [ + "assertion", + "authentication", + "type-validation" + ], + "required": false + }, + "proofValue": { + "description": [ + "The cryptographic signature generated by the verification method." + ], + "$ref": "#/pdo/basetypes/ecdsa-signature", + "required": true + } + } + }, + + + "credential": { + "//": "-----------------------------------------------------------------", + "description": [ + ], + "type": "object", + "properties": { + "nonce": { + "description": [ + "Base64 encoded random number used to mitigate replay attacks" + ], + "type": "string", + "length": 32, + "required": false + }, + "type": { + "description": [ + "List of identifiers for type objects" + ], + "type": "array", + "items": { + "type": "string", + "minItems": 0, + "uniqueItems": true + "enum": [ + "VerifiableCredential", + "verifiablePresentation" + ] + }, + "required": false + }, + "issuer": { + "description": [ + "Identity of the issuer, since all issuers are contracts", + "this will be assumed to be a contract id" + ], + "$ref": "#/pdo/identity/basetypes/identity", + "required": true + }, + "issuanceDate": { + "description": [ + "This is the time when the credential was issued", + "TIME FIELD" + ], + "type": "datetime", + "required": false + }, + "validFrom": { + "description": [ + "This is the time when the credential begins to be valid", + "TIME FIELD" + ], + "type": "datetime", + "required": false + }, + "expirationDate": { + "description": [ + "This is the time after which the credential is no longe valid", + "TIME FIELD" + ], + "type": "datetime", + "required": false + }, + "credentialSchema": { + "description": [ + "URL identifier for the schema used by the claims in the credential", + ], + "type": "string", + "required": false + }, + "credentialSubject": { + "description": [ + "Specifies the claims", + "Strictly speaking, W3C allows claims about multiple subjects", + "but we are restricting the claims to a single subject." + ], + "$ref": "#/pdo/identity/claim", + "required": true + } + }, + }, + + + "verifiable-credential": { + "//": "-----------------------------------------------------------------", + "description": [ + "To avoid the need for common serialization of JSON objects,", + "necessary for verifiable signatures, we are serializing the credential", + "and then signing." + ], + "type": "object", + "properties": { + "serializedCredential": { + "description": [ + "Serialized credential, when deserialized it will have", + "the #/pdo/identity/credential schema" + ], + "type": "string", + "format": "base64 urlsafe encoded, no padding" + "required": true + }, + "proof": { + "description": [ + "Proof of authenticity", + ], + "$ref": "#/pdo/identity/proof", + "required": true + } + }, + }, + + + "presentation": { + "//": "-----------------------------------------------------------------", + "description": [ + "A presentation is a collection of verifiable credentials that are", + "presented to a verifier.", + "The bundle of credentials is counter-signed by the holder/presenter." + ], + "type": "object", + "properties": { + "nonce": { + "description": [ + "Base64 encoded random number used to mitigate replay attacks" + ], + "type": "string", + "length": 32, + "required": false + }, + "holder": { + "description": [ + "Identity of the holder, since all holders are contracts", + "this will be assumed to be a contract id" + ], + "$ref": "#/pdo/basetypes/identity", + "required": true + }, + "verifiableCredential": { + "description": [ + "A list of verifiable credentials.", + "Note that each element of the list is in serialized form." + ], + "type": "array", + "items": { + "$ref": "#/pdo/identity/verifiable-credential", + "minItems": 1, + "uniqueItems": true + } + "required": true + } + } + }, + + "verifiable-presentation": { + "//": "-----------------------------------------------------------------", + "description": [ + "To avoid the need for common serialization of JSON objects,", + "necessary for verifiable signatures, we are serializing the presentation", + "and then signing." + ], + "type": "object", + "properties": { + "serializedPresentation": { + "description": [ + "Serialized presentation, when deserialized it will have", + "the #/pdo/identity/presentation schema" + ], + "type": "string", + "format": "base64 urlsafe encoded, no padding" + "required": true + }, + "proof": { + "description": [ + "Proof of authenticity", + ], + "$ref": "#/pdo/identity/basetypes/proof", + "required": true + } + } + } + } +} + +// Local Variables: +// mode: hs-minor +// End: diff --git a/identity-contract/etc/identity.toml b/identity-contract/etc/identity.toml new file mode 100644 index 0000000..d3a910c --- /dev/null +++ b/identity-contract/etc/identity.toml @@ -0,0 +1,6 @@ +# ----------------------------------------------------------------- +# Exchange family contract source +# ----------------------------------------------------------------- +[ContractFamily.Identity] +signature_authority = { source = "${home}/contracts/identity/_signature_authority.b64" } +identity = { source = "${home}/contracts/identity/_identity.b64" } diff --git a/identity-contract/identity/common/.keep b/identity-contract/identity/common/.keep new file mode 100644 index 0000000..e69de29 diff --git a/identity-contract/identity/common/BigNum.cpp b/identity-contract/identity/common/BigNum.cpp new file mode 100644 index 0000000..b62d8a1 --- /dev/null +++ b/identity-contract/identity/common/BigNum.cpp @@ -0,0 +1,256 @@ +/* Copyright 2023 Intel Corporation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +#include +#include +#include +#include +#include + +#include "BigNum.h" + +#define MAXIMUM_WORD_VALUE 256 + +// ----------------------------------------------------------------- +// Bit operations +// ----------------------------------------------------------------- +uint8_t ww::types::get_bit( + const ww::types::ByteArray& v, + const size_t index) +{ + uint8_t byte = v[index / 8]; + return ((byte >> (8 - (index % 8) - 1)) & 0x01); +} + +void ww::types::set_bit( + const size_t index, + const uint8_t value, + ww::types::ByteArray& v) +{ + uint8_t byte = v[index / 8]; + uint8_t mask = (0x01 << (8 - (index % 8) - 1)); + byte = value ? (byte | mask) : (byte & (~ mask)); + v[index / 8] = byte; +} + +// ----------------------------------------------------------------- +// cmp_big_numbers +// Compare two big numbers with return similar to other comparison +// operators: <0 if num1 < num2, 0 if num1 == num2, 1 if num1 > num2; +// -255 if something has gone wrong. +// ----------------------------------------------------------------- +int ww::types::cmp_big_numbers( + const ww::types::ByteArray& num1, + const ww::types::ByteArray& num2) +{ + // we don't really have an error and cant throw an + // exception + if (num1.size() != num2.size()) + return -255; + + const size_t SIZE = num1.size(); + + for (size_t i = 0; i < SIZE; i++) + { + if (num1[i] < num2[i]) + return -1; + else if (num1[i] > num2[i]) + return 1; + } + + return 0; +} + +// ----------------------------------------------------------------- +// add_big_numbers +// +// Add two equally sized big numbers, put the result in another +// big number, numbers are encoded in ByteArrays, numbers are all +// assumed to be positive and there is no overflow. +// ----------------------------------------------------------------- +bool ww::types::add_big_numbers( + const ww::types::ByteArray& num1, + const ww::types::ByteArray& num2, + ww::types::ByteArray& result) +{ + if (num1.size() != num2.size()) + return false; + + const size_t SIZE = num1.size(); + + result.resize(SIZE); + result.assign(SIZE, 0); + + uint8_t carry = 0; + + // least significant digit to most significant + for (size_t i = SIZE; i > 0; i--) + { + uint32_t sum = carry + num1[i-1] + num2[i-1]; + carry = sum / MAXIMUM_WORD_VALUE; + result[i-1] = sum % MAXIMUM_WORD_VALUE; + } + + return true; +} + +// ----------------------------------------------------------------- +// sub_big_numbers +// +// Subtract one big number from another and put the result into a +// third. Big numbers are assumed to be positive and the result must +// be positive (e.g. num1 > num2). +// ----------------------------------------------------------------- +bool ww::types::sub_big_numbers( + const ww::types::ByteArray& num1, + const ww::types::ByteArray& num2, + ww::types::ByteArray& result) +{ + if (num1.size() != num2.size()) + return false; + + const size_t SIZE = num1.size(); + + // num1 must be greater than num2 + if (cmp_big_numbers(num1, num2) < 0) + return false; + + result.resize(SIZE); + result.assign(SIZE, 0); + + uint8_t borrow = 0; + + // least significant digit to most significant + for (size_t i = SIZE; i > 0; i--) + { + if (num1[i-1] - borrow < num2[i-1]) + { + result[i-1] = MAXIMUM_WORD_VALUE + num1[i-1] - borrow - num2[i-1]; + borrow = 1; + } + else + { + result[i-1] = num1[i-1] - borrow - num2[i-1]; + borrow = 0; + } + } + + return true; +} + +// ----------------------------------------------------------------- +// mod_big_numbers +// +// Compute the remainder after dividing one big number from another. +// Put the result into a third. All numbers are assumed to be positive. +// ----------------------------------------------------------------- +bool ww::types::mod_big_numbers( + const ww::types::ByteArray& num, + const ww::types::ByteArray& mod, + ww::types::ByteArray& result) +{ + if (num.size() != mod.size()) + return false; + + const size_t SIZE = num.size(); + + result.resize(SIZE); + result.assign(SIZE, 0); + + // if num < mod, then the result is num + if (cmp_big_numbers(num, mod) < 0) + { + result.assign(num.begin(), num.end()); + return true; + } + + size_t msb_num = find_most_significant_bit(num); + size_t msb_mod = find_most_significant_bit(mod); + ww::types::ByteArray temp_num = num; + + // Multiply mod so that it is almost as big as num, basically + // this code fixes one binary digit at a time in the subtraction + for (size_t i = msb_mod - msb_num + 1; i > 0; i--) + { + ww::types::ByteArray temp_mod; + + shift_left_big_numbers(i-1, mod, temp_mod); + if (cmp_big_numbers(temp_mod,temp_num) < 0) + { + if (! sub_big_numbers(temp_num, temp_mod, result)) + return false; + + temp_num = result; + } + } + + return true; +} + +// ----------------------------------------------------------------- +// shift_left_big_numbers +// +// Shift bits from least significant to most significant, dropping +// any carry bits. Copy the result to a new array. +// ----------------------------------------------------------------- +bool ww::types::shift_left_big_numbers( + const size_t shift, + const ww::types::ByteArray& num, + ww::types::ByteArray& result) +{ + const size_t SIZE = num.size(); + + if (8 * SIZE <= shift) + return false; + + result.resize(SIZE); + result.assign(SIZE, 0); + + for (size_t i = 0; i < (8 * SIZE) - shift; i++) + { + uint8_t b = ww::types::get_bit(num, i+shift); + ww::types::set_bit(i, b, result); + } + + return true; +} + +// ----------------------------------------------------------------- +// find_most_significant_bit +// +// Utility function that helps to compute modulus +// ----------------------------------------------------------------- +size_t ww::types::find_most_significant_bit( + const ww::types::ByteArray& num) +{ + const size_t SIZE = num.size(); + + size_t w, b; + for (w = 0; w < SIZE; w++) + { + if (num[w] > 0) break; + } + + uint8_t word = num[w]; + for (b = 0; b < 8; b++) + { + if (word & 0x80) + break; + word = word << 1; + } + + return w * 8 + b; +} diff --git a/identity-contract/identity/common/BigNum.h b/identity-contract/identity/common/BigNum.h new file mode 100644 index 0000000..b221608 --- /dev/null +++ b/identity-contract/identity/common/BigNum.h @@ -0,0 +1,171 @@ +/* Copyright 2023 Intel Corporation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include +#include +#include +#include + +#include "Cryptography.h" +#include "Types.h" + +/* + This file contains a very simple implementation of big numbers. Specifically, + it defines addition, subtraction and modulus operations for positive integers + with fixed byte size. The numbers are encoded most significant digits at the + low index in the array. +*/ +namespace ww +{ +namespace types +{ + // Big number operations + int cmp_big_numbers(const std::vector& num1, const std::vector& num2); + bool add_big_numbers(const std::vector& num1, const std::vector& num2, std::vector& result); + bool sub_big_numbers(const std::vector& num1, const std::vector& num2, std::vector& result); + bool mod_big_numbers(const std::vector& num, const std::vector& mod, std::vector& result); + bool shift_left_big_numbers(const size_t shift, const std::vector& num, std::vector& result); + size_t find_most_significant_bit(const std::vector& num); + + // Bit operations + uint8_t get_bit(const std::vector& v, const size_t index); + void set_bit(const size_t index, const uint8_t value, std::vector& v); + + template + class BigNum : protected ww::types::ByteArray + { + private: + + public: + // Constructors + BigNum(void) { + resize(SIZE); + }; + + BigNum(std::initializer_list n) : BigNum() { + if (n.size() != SIZE) + return; // throw std::runtime_error("bad length"); + + std::copy_n(n.begin(), SIZE, this->begin()); + }; + + BigNum(const ww::types::ByteArray& n) : BigNum() + { + this->decode(n); + }; + + BigNum(const std::string& encoded) : BigNum() + { + this->decode(encoded); + } + + // assign from a raw byte array + bool decode(const ww::types::ByteArray& n) + { + if (n.size() != SIZE) + return false; + std::copy_n(n.begin(), SIZE, this->begin()); + return true; + } + + // copy to a raw byte array + bool encode(ww::types::ByteArray& n) const + { + n.resize(SIZE); + std::copy_n(this->begin(), SIZE, n.begin()); + return true; + } + + // assign from a base64 encoded string + bool decode(const std::string& encoded) + { + ww::types::ByteArray decoded; + if (! ww::crypto::b64_decode(encoded, decoded)) + return false; + return this->decode(decoded); + } + + // copy to a base64 encoded string + bool encode(std::string& encoded) const + { + ww::types::ByteArray n; + if (! this->encode(n)) + return false; + return ww::crypto::b64_encode(n, encoded); + } + + void reset(void) + { + std::fill(this->begin(), this->end(), 0); + }; + + // Arithmetic operators + BigNum operator+(const BigNum &b) const + { + BigNum result; + ww::types::add_big_numbers(*this, b, result); + return result; + }; + + BigNum operator-(const BigNum &b) const + { + BigNum result; + ww::types::sub_big_numbers(*this, b, result); + return result; + }; + + BigNum operator%(const BigNum &b) const + { + BigNum result; + ww::types::mod_big_numbers(*this, b, result); + return result; + }; + + // Comparison operators + bool operator<(const BigNum &b) const + { + return ww::types::cmp_big_numbers(*this, b) < 0; + }; + + bool operator<=(const BigNum &b) const + { + return ww::types::cmp_big_numbers(*this, b) <= 0; + }; + + bool operator>(const BigNum &b) const + { + return ww::types::cmp_big_numbers(*this, b) > 0; + }; + + bool operator>=(const BigNum &b) const + { + return ww::types::cmp_big_numbers(*this, b) >= 0; + }; + + bool operator==(const BigNum &b) const + { + return ww::types::cmp_big_numbers(*this, b) == 0; + }; + }; + + typedef BigNum<4> BigNum32; + typedef BigNum<32> BigNum256; + typedef BigNum<48> BigNum384; + typedef BigNum<64> BigNum512; +}; // types +} // ww diff --git a/identity-contract/identity/common/Credential.cpp b/identity-contract/identity/common/Credential.cpp new file mode 100644 index 0000000..e399cc1 --- /dev/null +++ b/identity-contract/identity/common/Credential.cpp @@ -0,0 +1,610 @@ +/* Copyright 2024 Intel Corporation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include + +#include "Types.h" +#include "Value.h" +#include "WasmExtensions.h" + +#include "Cryptography.h" + +#include "exchange/common/Common.h" +#include "identity/common/Credential.h" +#include "identity/common/SigningContext.h" +#include "identity/common/SigningContextManager.h" + +// XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX +// Class: ww::identity::SigningContext +// XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX + +// ----------------------------------------------------------------- +// ----------------------------------------------------------------- +static bool deserialize_timestamp( + const char* input_timestamp, + std::string& output_timestamp) +{ + if (input_timestamp == NULL) + return false; + + // this is just a placeholder, will check format in the future + output_timestamp.assign(input_timestamp); + return true; +} + +// ----------------------------------------------------------------- +// ----------------------------------------------------------------- +static bool deserialize_context_path( + const ww::value::Array& context_array, + std::vector& context_path) +{ + context_path.resize(0); + + const size_t count = context_array.get_count(); + for (size_t index = 0; index < count; index++) + { + const std::string c = context_array.get_string(index); + context_path.push_back(c); + } + + return true; +} + +// ----------------------------------------------------------------- +// ----------------------------------------------------------------- +static bool serialize_context_path( + const std::vector& context_path, + ww::value::Array& context_array) +{ + for (size_t index = 0; index < context_path.size(); index++) + { + if (! context_array.append_string(context_path[index].c_str())) + return false; + } + + return true; +} + +// ----------------------------------------------------------------- +// ----------------------------------------------------------------- +bool ww::identity::Identity::deserialize(const ww::value::Object& serialized_object) +{ + if (! ww::identity::Identity::verify_schema(serialized_object)) + return false; + + // Required fields + const char *id = serialized_object.get_string("id"); + if (id == NULL) + return false; + id_.assign(id); + + // Optional fields + const char *name = serialized_object.get_string("name"); + if (name != NULL) + name_.assign(name); + + const char *description = serialized_object.get_string("description"); + if (description != NULL) + description_.assign(description); + + return true; +} + +// ----------------------------------------------------------------- +// ----------------------------------------------------------------- +bool ww::identity::Identity::serialize(ww::value::Value& serialized_object) const +{ + ww::value::Structure serializer(IDENTITY_SCHEMA); + + // Required fields + if (! serializer.set_string("id", id_.c_str())) + return false; + + // Optional fields + if (! name_.empty()) + if (! serializer.set_string("name", name_.c_str())) + return false; + + if (! description_.empty()) + if (! serializer.set_string("description", description_.c_str())) + return false; + + serialized_object.set(serializer); + return true; +} + +// ----------------------------------------------------------------- +// ----------------------------------------------------------------- +bool ww::identity::IdentityKey::deserialize(const ww::value::Object& serialized_object) +{ + if (! ww::identity::IdentityKey::verify_schema(serialized_object)) + return false; + + // Required fields + const char *id = serialized_object.get_string("id"); + if (id == NULL) + return false; + id_.assign(id); + + ww::value::Array context_array; + if (! serialized_object.get_value("context_path", context_array)) + return false; + + if (! deserialize_context_path(context_array, context_path_)) + return false; + + // Optional fields + const char *name = serialized_object.get_string("name"); + if (name != NULL) + name_.assign(name); + + const char *description = serialized_object.get_string("description"); + if (description != NULL) + description_.assign(description); + + return true; +} + +// ----------------------------------------------------------------- +// ----------------------------------------------------------------- +bool ww::identity::IdentityKey::serialize(ww::value::Value& serialized_object) const +{ + ww::value::Structure serializer(IDENTITY_KEY_SCHEMA); + if (! serializer.set_string("id", id_.c_str())) + return false; + + // Required fields + ww::value::Array context_array; + if (! serialize_context_path(context_path_, context_array)) + return false; + + if (! serializer.set_value("context_path", context_array)) + return false; + + // Optional fields + if (! name_.empty()) + if (! serializer.set_string("name", name_.c_str())) + return false; + + if (! description_.empty()) + if (! serializer.set_string("description", description_.c_str())) + return false; + + serialized_object.set(serializer); + return true; +} + +// ----------------------------------------------------------------- +// ----------------------------------------------------------------- +bool ww::identity::Claims::deserialize(const ww::value::Object& serialized_object) +{ + if (! ww::identity::Claims::verify_schema(serialized_object)) + return false; + + ww::value::Object subject; + if (! serialized_object.get_value("subject", subject)) + return false; + if (! subject_.deserialize(subject)) + return false; + + if (! serialized_object.get_value("claims", claims_)) + return false; + + return true; +} + +// ----------------------------------------------------------------- +// ----------------------------------------------------------------- +bool ww::identity::Claims::serialize(ww::value::Value& serialized_object) const +{ + ww::value::Structure serializer(CLAIMS_SCHEMA); + + ww::value::Object serialized_subject; + if (! subject_.serialize(serialized_subject)) + return false; + if (! serializer.set_value("subject", serialized_subject)) + return false; + + if (! serializer.set_value("claims", claims_)) + return false; + + serialized_object.set(serializer); + return true; +} + +// ----------------------------------------------------------------- +// ----------------------------------------------------------------- +bool ww::identity::Proof::deserialize(const ww::value::Object& serialized_object) +{ + if (! ww::identity::Proof::verify_schema(serialized_object)) + return false; + + // Required fields + const char *type = serialized_object.get_string("type"); + if (type == NULL) + return false; + type_.assign(type); + + ww::value::Structure serialized_identity_key(IDENTITY_KEY_SCHEMA); + if (! serialized_object.get_value("verificationMethod", serialized_identity_key)) + return false; + if (! verificationMethod_.deserialize(serialized_identity_key)) + return false; + + const char *proofValue = serialized_object.get_string("proofValue"); + if (proofValue == NULL) + return false; + proofValue_.assign(proofValue); + + // Optional fields + const char *created = serialized_object.get_string("created"); + if (created != NULL) + if (! deserialize_timestamp(created, created_)) + return false; + + const char *proofPurpose = serialized_object.get_string("proofPurpose"); + if (proofPurpose != NULL) + proofPurpose_.assign(proofPurpose); // TBD: verify that this is one of the enums + + return true; +} + +// ----------------------------------------------------------------- +// ----------------------------------------------------------------- +bool ww::identity::Proof::serialize(ww::value::Value& serialized_object) const +{ + ww::value::Structure serializer(PROOF_SCHEMA); + + // Required fields + if (! serializer.set_string("type", type_.c_str())) + return false; + + ww::value::Structure serialized_identity_key(IDENTITY_KEY_SCHEMA); + if (! verificationMethod_.serialize(serialized_identity_key)) + return false; + if (! serializer.set_value("verificationMethod", serialized_identity_key)) + return false; + + if (! serializer.set_string("proofValue", proofValue_.c_str())) + return false; + + // Optional fields + if (! created_.empty()) + if (! serializer.set_string("created", created_.c_str())) + return false; + + if (! proofPurpose_.empty()) + if (! serializer.set_string("proofPurpose", proofPurpose_.c_str())) + return false; + + serialized_object.set(serializer); + return true; +} + +// ----------------------------------------------------------------- +// ----------------------------------------------------------------- +bool ww::identity::Credential::deserialize(const ww::value::Object& serialized_object) +{ + if (! ww::identity::Credential::verify_schema(serialized_object)) + return false; + + ww::value::Object serialized_issuer; + if (! serialized_object.get_value("issuer", serialized_issuer)) + return false; + if (! issuer_.deserialize(serialized_issuer)) + return false; + + ww::value::Object serialized_claims; + if (! serialized_object.get_value("credentialSubject", serialized_claims)) + return false; + if (! credentialSubject_.deserialize(serialized_claims)) + return false; + + // Optional fields + const char *name = serialized_object.get_string("name"); + if (name != NULL) + name_.assign(name); + + const char *description = serialized_object.get_string("description"); + if (description != NULL) + description_.assign(description); + + const char *nonce = serialized_object.get_string("nonce"); + if (nonce != NULL) + nonce_.assign(nonce); + + const char *issuanceDate = serialized_object.get_string("issuanceDate"); + if (issuanceDate != NULL) + if (! deserialize_timestamp(issuanceDate, issuanceDate_)) + return false; + + const char *expirationDate = serialized_object.get_string("expirationDate"); + if (expirationDate != NULL) + if (! deserialize_timestamp(expirationDate, expirationDate_)) + return false; + + return true; +} + +// ----------------------------------------------------------------- +// ----------------------------------------------------------------- +bool ww::identity::Credential::serialize(ww::value::Value& serialized_object) const +{ + ww::value::Structure serializer(CREDENTIAL_SCHEMA); + + ww::value::Value serialized_issuer; + if (! issuer_.serialize(serialized_issuer)) + return false; + if (! serializer.set_value("issuer", serialized_issuer)) + return false; + + ww::value::Value serialized_claims; + if (! credentialSubject_.serialize(serialized_claims)) + return false; + if (! serializer.set_value("credentialSubject", serialized_claims)) + return false; + + // Optional fields + if (! name_.empty()) + if (! serializer.set_string("name", name_.c_str())) + return false; + + if (! description_.empty()) + if (! serializer.set_string("description", description_.c_str())) + return false; + + if (! nonce_.empty()) + if (! serializer.set_string("nonce", nonce_.c_str())) + return false; + + if (! issuanceDate_.empty()) + if (! serializer.set_string("issuanceDate", issuanceDate_.c_str())) + return false; + + if (! expirationDate_.empty()) + if (! serializer.set_string("expirationDate", expirationDate_.c_str())) + return false; + + serialized_object.set(serializer); + return true; +} + +// ----------------------------------------------------------------- +// ----------------------------------------------------------------- +bool ww::identity::VerifiableCredential::deserialize(const ww::value::Object& serialized_object) +{ + if (! ww::identity::VerifiableCredential::verify_schema(serialized_object)) + return false; + + const char *serializedCredential = serialized_object.get_string("serializedCredential"); + if (serializedCredential == NULL) + return false; + serializedCredential_.assign(serializedCredential); + + // the serialized credential is b64 encoded and must be converted + // to a string for deserialization + { + ww::types::ByteArray decoded_credential_ba; + if (! ww::crypto::b64_decode(serializedCredential_, decoded_credential_ba)) + return false; + const std::string decoded_credential = ww::types::ByteArrayToString(decoded_credential_ba); + if (! credential_.deserialize_string(decoded_credential)) + return false; + } + + ww::value::Object serialized_proof; + if (! serialized_object.get_value("proof", serialized_proof)) + return false; + if (! proof_.deserialize(serialized_proof)) + return false; + + return true; +} + +// ----------------------------------------------------------------- +// ----------------------------------------------------------------- +bool ww::identity::VerifiableCredential::serialize(ww::value::Value& serialized_object) const +{ + ww::value::Structure serializer(VERIFIABLE_CREDENTIAL_SCHEMA); + + serialized_object.set(serializer); + if (! serializer.set_string("serializedCredential", serializedCredential_.c_str())) + return false; + + ww::value::Value serialized_proof; + if (! proof_.serialize(serialized_proof)) + return false; + if (! serializer.set_value("proof", serialized_proof)) + return false; + + serialized_object.set(serializer); + return true; +} + +// ----------------------------------------------------------------- +// ----------------------------------------------------------------- +bool ww::identity::VerifiableCredential::build( + const ww::value::Object& credential, + const ww::identity::IdentityKey& identity, + const ww::types::ByteArray& extended_key_seed) +{ + // De-serializing the input will check for a schema match and + // will unpack the expected fields + if (! credential_.deserialize(credential)) + return false; + + // Re-serializing the credential will ensure that the format contains + // no additional information beyond the credential fields + std::string serialized_credential; + if (! credential_.serialize_string(serialized_credential)) + return false; + + // Base64 encode the serialized credential and save it in the + // serializedCredential_ field + ww::types::ByteArray serialized_credential_ba(serialized_credential.begin(), serialized_credential.end()); + if (! ww::crypto::b64_encode(serialized_credential_ba, serializedCredential_)) + return false; + + // Sign the serialized credential; in this case we are signing the base64 encoding + // of the serialized credential, this is not the only approach that would be valid + // but it does represent a fairly standard way of signing JSON + // see https://datatracker.ietf.org/doc/rfc7515/ + ww::types::ByteArray signature_ba; + ww::types::ByteArray message_ba(serializedCredential_.begin(), serializedCredential_.end()); + if (! ww::identity::SigningContext::sign_message(extended_key_seed, identity.context_path_, message_ba, signature_ba)) + return false; + + // Convert the signature to base64 + std::string b64_signature; + if (! ww::crypto::b64_encode(signature_ba, b64_signature)) + return false; + + // Finally save all the information into the verifiable credential + proof_.type_ = "ecdsa_secp384r1"; + proof_.verificationMethod_ = identity; + proof_.proofValue_ = b64_signature; + proof_.proofPurpose_ = "assertion"; + + return true; +} + +// ----------------------------------------------------------------- +// ----------------------------------------------------------------- +bool ww::identity::VerifiableCredential::check(const ww::types::ByteArray& extended_key_seed) const +{ + // The signature was computed over the base64 encoded credential so we + // do not need to decode the credential before checking the signature + ww::types::ByteArray message(serializedCredential_.begin(), serializedCredential_.end()); + + ww::types::ByteArray signature; + if (! ww::crypto::b64_decode(proof_.proofValue_, signature)) + return false; + + return ww::identity::SigningContext::verify_signature( + extended_key_seed, proof_.verificationMethod_.context_path_, message, signature); +} + +// ----------------------------------------------------------------- +// ----------------------------------------------------------------- +bool ww::identity::Presentation::deserialize(const ww::value::Object& serialized_object) +{ + if (! ww::identity::Presentation::verify_schema(serialized_object)) + return false; + + ww::value::Object serialized_holder; + if (! serialized_object.get_value("holder", serialized_holder)) + return false; + if (! holder_.deserialize(serialized_holder)) + return false; + + ww::value::Array serialized_credential_list; + if (! serialized_object.get_value("verifiableCredential", serialized_credential_list)) + return false; + + const size_t count = serialized_credential_list.get_count(); + for (size_t index = 0; index < count; index++) + { + ww::value::Object serialized_credential; + if (! serialized_credential_list.get_value(index, serialized_credential)) + return false; + + ww::identity::VerifiableCredential credential; + if (! credential.deserialize(serialized_credential)) + return false; + + verifiableCredential_.push_back(credential); + } + + return true; +} + +// ----------------------------------------------------------------- +// ----------------------------------------------------------------- +bool ww::identity::Presentation::serialize(ww::value::Value& serialized_object) const +{ + ww::value::Structure serializer(PRESENTATION_SCHEMA); + + ww::value::Value serialized_holder; + if (! holder_.serialize(serialized_holder)) + return false; + serializer.set_value("holder", serialized_holder); + + ww::value::Array serialized_credential_list; + for (size_t index = 0; index < verifiableCredential_.size(); index++) + { + ww::value::Object serialized_credential; + if (! verifiableCredential_[index].serialize(serialized_credential)) + return false; + if (! serialized_credential_list.append_value(serialized_credential)) + return false; + } + if (! serializer.set_value("verifiableCredential", serialized_credential_list)) + return false; + + serialized_object.set(serializer); + return true; +} + +// ----------------------------------------------------------------- +// ----------------------------------------------------------------- +bool ww::identity::VerifiablePresentation::deserialize(const ww::value::Object& serialized_object) +{ + if (! ww::identity::VerifiablePresentation::verify_schema(serialized_object)) + return false; + + const char* serializedPresentation = serialized_object.get_string("serializedPresentation"); + if (serializedPresentation == NULL) + return false; + serializedPresentation_.assign(serializedPresentation); + + // the serialized presentation is b64 encoded and must be converted + // to a string for deserialization + { + ww::types::ByteArray decoded_presentation_ba; + if (! ww::crypto::b64_decode(serializedPresentation_, decoded_presentation_ba)) + return false; + const std::string decoded_presentation = ww::types::ByteArrayToString(decoded_presentation_ba); + if (! presentation_.deserialize_string(decoded_presentation)) + return false; + } + + ww::value::Object serialized_proof; + if (! serialized_object.get_value("proof", serialized_proof)) + return false; + if (! proof_.deserialize(serialized_proof)) + return false; + + return true; +} + +// ----------------------------------------------------------------- +// ----------------------------------------------------------------- +bool ww::identity::VerifiablePresentation::serialize(ww::value::Value& serialized_object) const +{ + ww::value::Structure serializer(VERIFIABLE_PRESENTATION_SCHEMA); + + serialized_object.set(serializer); + if (! serializer.set_string("serializedPresentation", serializedPresentation_.c_str())) + return false; + + ww::value::Value serialized_proof; + if (! proof_.serialize(serialized_proof)) + return false; + if (! serializer.set_value("proof", serialized_proof)) + return false; + + return true; +} diff --git a/identity-contract/identity/common/Credential.h b/identity-contract/identity/common/Credential.h new file mode 100644 index 0000000..8d7f114 --- /dev/null +++ b/identity-contract/identity/common/Credential.h @@ -0,0 +1,358 @@ +/* Copyright 2023 Intel Corporation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once +#include +#include + +#include "Types.h" +#include "Util.h" +#include "Value.h" +#include "exchange/common/Common.h" + +// All PDO contract identifiers are assumed to be of the form: +// PDO:/// +// This identifier uniques identifies a PDO contract object and +// the ledger where its authenticity can be established. + +// Identity Schema +// +// Provides a reference to a specific identity contract object +// +// Required fields include: +// id -- a PDO contract identifier +// Optional fields include: +// name -- a human readable name of the object, need not be unique or meaningful +// description -- a human readable description of the object +// identified_by -- a JSON object with properties to identify holder, definition TBD +#define IDENTITY_SCHEMA \ + "{" \ + SCHEMA_KW(id, "") \ + "}" + +namespace ww +{ +namespace identity +{ + class Identity : public ww::exchange::SerializeableObject + { + private: + public: + std::string id_; + std::string name_; + std::string description_; + + static bool verify_schema(const ww::value::Object& deserialized_object) + { + return ww::exchange::SerializeableObject::verify_schema_actual( + deserialized_object, IDENTITY_SCHEMA); + } + + bool deserialize(const ww::value::Object& credential); + bool serialize(ww::value::Value& serialized_credential) const; + }; +} +} + +// Identity Key Schema +// +// Provides information to identify a specific key in the context of +// an identity object. +// +// Required fields include: +// id -- a PDO contract identifier +// context_path -- a list of strings that contextualizes the identity +// Optional fields include: +// name -- a human readable name of the object, need not be unique or meaningful +// description -- a human readable description of the object +#define IDENTITY_KEY_SCHEMA \ + "{" \ + SCHEMA_KW(id, "") "," \ + SCHEMA_KW(context_path, [ "" ]) \ + "}" + +namespace ww +{ +namespace identity +{ + class IdentityKey : public ww::exchange::SerializeableObject + { + private: + public: + std::string id_; + std::vector context_path_; + std::string name_; + std::string description_; + + static bool verify_schema(const ww::value::Object& deserialized_object) + { + return ww::exchange::SerializeableObject::verify_schema_actual( + deserialized_object, IDENTITY_KEY_SCHEMA); + } + + bool deserialize(const ww::value::Object& credential); + bool serialize(ww::value::Value& serialized_credential) const; + + IdentityKey(void) { } + IdentityKey(const std::string id, const std::vector& context_path) : + id_(id), context_path_(context_path) { } + }; +} +} + +// Claims Schema +// +// Provides a set of assertions about a subject +// +// Required fields include: +// id -- the PDO contract identifier for the subject of the claim, +// claims -- an object whose properties are attributes of the subject; +// specific details of claims about the subject will be provided +// at contract configuration time +#define CLAIMS_SCHEMA \ + "{" \ + SCHEMA_KWS(subject, IDENTITY_SCHEMA) "," \ + SCHEMA_KW(claims, {}) \ + "}" + +namespace ww +{ +namespace identity +{ + class Claims : public ww::exchange::SerializeableObject + { + private: + public: + ww::identity::Identity subject_; + ww::value::Object claims_; + + static bool verify_schema(const ww::value::Object& deserialized_object) + { + return ww::exchange::SerializeableObject::verify_schema_actual( + deserialized_object, CLAIMS_SCHEMA); + } + + bool deserialize(const ww::value::Object& credential); + bool serialize(ww::value::Value& serialized_credential) const; + }; +} +} + +// Proof Schema +// +// There are many forms of proof that can accompany the credential, the +// proof is intended to verify the authenticity of the credential (that +// is, that the issuer really issued this credential). + +// Required fields include: +// type -- identify the signature method, for now always "ecdsa_secp384r1" +// verificationMethod -- identifier plus an optional context [#context] +// proofValue -- base64 encoded signature +// Optional fields include: +// proofPurpose -- keyword that identifies what the proof claims +// created -- time when the proof was createdxc +#define PROOF_SCHEMA \ + "{" \ + SCHEMA_KW(type, "") "," \ + SCHEMA_KWS(verificationMethod, IDENTITY_KEY_SCHEMA) "," \ + SCHEMA_KW(proofValue, "") \ + "}" + +namespace ww +{ +namespace identity +{ + class Proof : public ww::exchange::SerializeableObject + { + private: + public: + std::string type_; + ww::identity::IdentityKey verificationMethod_; + std::string proofValue_; + std::string proofPurpose_; + std::string created_; + + static bool verify_schema(const ww::value::Object& deserialized_object) + { + return ww::exchange::SerializeableObject::verify_schema_actual( + deserialized_object, PROOF_SCHEMA); + } + + bool deserialize(const ww::value::Object& credential); + bool serialize(ww::value::Value& serialized_credential) const; + }; +} +} + +// Credential Schema +// +// Required fields include: +// issuer -- identifier for the issuer of the credential, should be +// verifiable by the signature in the proof section +// credentialSubject -- specifies a claim, while strictly speaking a +// verifiable credential may contain claims about a list of subjects, +// we are limiting the number of subjects to one, though there may be +// multiple claims about that subject + +// Optional fields include: +// name -- a human readable name of the credential, need not be unique or meaningful +// description -- a human readable description of the credential +// nonce -- base64 encoded number that serves to protect against replay attacks +// issuanceDate -- data-time string, earliest time when claims are valid +// expirationDate -- date-time string when the credential will expire +// type -- list of type identifiers for type objects, VerifiableCredential is assumed +#define CREDENTIAL_SCHEMA \ + "{" \ + SCHEMA_KWS(issuer, IDENTITY_SCHEMA) "," \ + SCHEMA_KWS(credentialSubject, CLAIMS_SCHEMA) \ + "}" + +namespace ww +{ +namespace identity +{ + class Credential : public ww::exchange::SerializeableObject + { + private: + public: + ww::identity::Identity issuer_; + ww::identity::Claims credentialSubject_; + + std::string name_; + std::string description_; + std::string nonce_; + std::string issuanceDate_; + std::string expirationDate_; + + static bool verify_schema(const ww::value::Object& deserialized_object) + { + return ww::exchange::SerializeableObject::verify_schema_actual( + deserialized_object, CREDENTIAL_SCHEMA); + } + + bool deserialize(const ww::value::Object& credential); + bool serialize(ww::value::Value& serialized_credential) const; + }; +} +} + +// Verifiable Credential Schema +// +// To avoid the need for common JSON serialization schemes necessary for +// verifiable signatures, we are just passing a base64 encoding of the +// serialized credential that will be signed. This ensures that inconsistent +// serialization will not be an issue. +#define VERIFIABLE_CREDENTIAL_SCHEMA \ + "{" \ + SCHEMA_KW(serializedCredential, "") "," \ + SCHEMA_KWS(proof, PROOF_SCHEMA) \ + "}" + +namespace ww +{ +namespace identity +{ + class VerifiableCredential : public ww::exchange::SerializeableObject + { + private: + std::string serializedCredential_; // Base64 encoding of serialized credential + + public: + ww::identity::Credential credential_; + ww::identity::Proof proof_; + + static bool verify_schema(const ww::value::Object& deserialized_object) + { + return ww::exchange::SerializeableObject::verify_schema_actual( + deserialized_object, VERIFIABLE_CREDENTIAL_SCHEMA); + } + + bool deserialize(const ww::value::Object& verifiable_credential); + bool serialize(ww::value::Value& serialized_verifiable_credential) const; + + bool build( + const ww::value::Object& credential, + const ww::identity::IdentityKey& identity, + const ww::types::ByteArray& extended_key_seed); + bool check( + const ww::types::ByteArray& extended_key_seed) const; + }; +} +} + +// Presentation Schema +// +// +#define PRESENTATION_SCHEMA \ + "{" \ + SCHEMA_KWS(holder, IDENTITY_SCHEMA) "," \ + SCHEMA_KWS(verifiableCredential, "[" VERIFIABLE_CREDENTIAL_SCHEMA "]") \ + "}" + +namespace ww +{ +namespace identity +{ + class Presentation : public ww::exchange::SerializeableObject + { + private: + public: + ww::identity::Identity holder_; + std::vector verifiableCredential_; + + static bool verify_schema(const ww::value::Object& deserialized_object) + { + return ww::exchange::SerializeableObject::verify_schema_actual( + deserialized_object, PRESENTATION_SCHEMA); + } + + bool deserialize(const ww::value::Object& verifiable_credential); + bool serialize(ww::value::Value& serialized_verifiable_credential) const; + }; +} +} + +// Verifiable Presentation Schema +// +// +#define VERIFIABLE_PRESENTATION_SCHEMA \ + "{" \ + SCHEMA_KW(serializedPresentation, "") "," \ + SCHEMA_KWS(proof, PROOF_SCHEMA) \ + "}" + +namespace ww +{ +namespace identity +{ + class VerifiablePresentation : public ww::exchange::SerializeableObject + { + private: + std::string serializedPresentation_; + public: + ww::identity::Presentation presentation_; + ww::identity::Proof proof_; + + static bool verify_schema(const ww::value::Object& deserialized_object) + { + return ww::exchange::SerializeableObject::verify_schema_actual( + deserialized_object, VERIFIABLE_PRESENTATION_SCHEMA); + } + + bool deserialize(const ww::value::Object& verifiable_credential); + bool serialize(ww::value::Value& serialized_verifiable_credential) const; + }; +} +} diff --git a/identity-contract/identity/common/SigningContext.cpp b/identity-contract/identity/common/SigningContext.cpp new file mode 100644 index 0000000..62752fe --- /dev/null +++ b/identity-contract/identity/common/SigningContext.cpp @@ -0,0 +1,236 @@ +/* Copyright 2023 Intel Corporation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include +#include + +#include "Types.h" +#include "Value.h" +#include "WasmExtensions.h" + +#include "Cryptography.h" + +#include "exchange/common/Common.h" +#include "identity/common/BigNum.h" +#include "identity/common/SigningContext.h" + +// XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX +// Class: ww::identity::SigningContext +// XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX +// ----------------------------------------------------------------- +// ----------------------------------------------------------------- +bool ww::identity::SigningContext::contains(const std::string& name) const +{ + for (auto i = subcontexts_.begin(); i != subcontexts_.end(); i++) + { + if ((*i) == name) + return true; + } + + return false; +} + +// ----------------------------------------------------------------- +bool ww::identity::SigningContext::deserialize(const ww::value::Object& context) +{ + if (! ww::identity::SigningContext::verify_schema(context)) + return false; + + extensible_ = context.get_boolean("extensible"); + description_ = context.get_string("description"); + subcontexts_.resize(0); + + ww::value::Array subcontexts; + if (! context.get_value("subcontexts", subcontexts)) + return false; + + size_t count = subcontexts.get_count(); + for (size_t index = 0; index < count; index++) + { + const std::string context_name(subcontexts.get_string(index)); + subcontexts_.push_back(context_name); + } + + return true; +} + +// ----------------------------------------------------------------- +bool ww::identity::SigningContext::serialize(ww::value::Value& serialized_context) const +{ + ww::value::Structure context(SIGNING_CONTEXT_SCHEMA); + if (! context.set_boolean("extensible", extensible_)) + return false; + + if (! context.set_string("description", description_.c_str())) + return false; + + ww::value::Array subcontexts; + for (auto c = subcontexts_.begin(); c < subcontexts_.end(); c++) + { + ww::value::String s(c->c_str()); + if (! subcontexts.append_value(s)) + return false; + } + context.set_value("subcontexts", subcontexts); + + serialized_context.set(context); + return true; +} + +// ----------------------------------------------------------------- +// sign +// +// Generate an extended key from the context path and use that to sign +// the buffer. The assumption is that the context path has been validated. +// ----------------------------------------------------------------- +bool ww::identity::SigningContext::sign_message( + const ww::types::ByteArray& root_key, + const std::vector& context_path, + const ww::types::ByteArray& message, + ww::types::ByteArray& signature) +{ + std::string private_key, public_key; + if (! ww::identity::SigningContext::generate_keys(root_key, context_path, private_key, public_key)) + return false; + + return ww::crypto::ecdsa::sign_message(message, private_key, signature); +} + +// ----------------------------------------------------------------- +// verify +// +// Verify a signature using an extended key generated from the context +// path. The assumption is that the context path has been validated. +// ----------------------------------------------------------------- +bool ww::identity::SigningContext::verify_signature( + const ww::types::ByteArray& root_key, + const std::vector& context_path, + const ww::types::ByteArray& message, + const ww::types::ByteArray& signature) +{ + std::string private_key, public_key; + if (! ww::identity::SigningContext::generate_keys(root_key, context_path, private_key, public_key)) + return false; + + return ww::crypto::ecdsa::verify_signature(message, public_key, signature); +} + +// ----------------------------------------------------------------- +// ----------------------------------------------------------------- +// generate_key implements a version of the bip32 protocol for generating +// extended keys. more information about bip32 is available here: +// https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki +// the main difference is that this implementation currently only performs +// hardened derivations (private key) and it focuses on the secp384r1 +// curve rather than the bitcoin focused secp256k1 curve. + +#define CHUNK_HASH_FUNCTION ww::crypto::hash::sha256_hmac +#define EXTENDED_CHUNK_SIZE 16 + +const std::string index_base("PDO SigningContext:"); + +#ifdef DEBUG_BIGNUM +void DumpByteArray(const char* msg, const ww::types::ByteArray ba) +{ + std::string s; + ww::crypto::b64_encode(ba, s); + CONTRACT_SAFE_LOG(3, "[BA] %s: %s", msg, s.c_str()); +} + +void DumpBigNum(const char* msg, BIGNUM_TYPE bn) +{ + std::string s; + bn.encode(s); + CONTRACT_SAFE_LOG(3, "[BN] %s: %s", msg, s.c_str()); +} +#endif + +// ----------------------------------------------------------------- +bool ww::identity::SigningContext::generate_keys( + const ww::types::ByteArray& root_key, // base64 encoded representation of 48 byte random array + const std::vector& context_path, + std::string& private_key, // PEM encoded ECDSA private and public keys + std::string& public_key) +{ + // Root key must contain EXTENDED_KEY_SIZE bytes + if (root_key.size() != EXTENDED_KEY_SIZE) + return false; + + // Create the initial extended key, this is a fixed value based on the + // index_base strings + ww::types::ByteArray base(index_base.begin(), index_base.end()); + ww::types::ByteArray hashed_base; + if (! HASH_FUNCTION(base, hashed_base)) + return false; + + // CURVE_ORDER is a base64 encoded number + const BIGNUM_TYPE curve_order(CURVE_ORDER); + BIGNUM_TYPE extended_key; + if (! extended_key.decode(hashed_base)) + return false; + + // Decode the root key, this is the chain code for the first iteration + ww::types::ByteArray extended_chain_code = root_key; + std::vector::iterator path_element; + for ( path_element = context_path.begin(); path_element < context_path.end(); path_element++) + { + // For the purpose of the hashing, we are concatenating the index and the parent private key + size_t path_hash = std::hash{}(*path_element); + + ww::types::ByteArray ba_extended_key; + extended_key.encode(ba_extended_key); + + ww::types::ByteArray index; + index.push_back(0x00); // this is part of the BIP32 specification for extended keys + index.insert(index.end(), ba_extended_key.begin(), ba_extended_key.end()); + auto ptr = reinterpret_cast(&path_hash); + index.insert(index.end(), ptr, ptr + sizeof(size_t)); + + ww::types::ByteArray child_key_ba; + ww::types::ByteArray child_chain_code; + + for (int i = 0; i < EXTENDED_KEY_SIZE / EXTENDED_CHUNK_SIZE; i++) { + const size_t seg_start = i * EXTENDED_CHUNK_SIZE; + const size_t seg_end = (i + 1) * EXTENDED_CHUNK_SIZE; + ww::types::ByteArray chain_code_segment( + extended_chain_code.begin() + seg_start, extended_chain_code.begin() + seg_end); + + ww::types::ByteArray hmac; + CHUNK_HASH_FUNCTION(index, chain_code_segment, hmac); + if (hmac.size() != 2 * EXTENDED_CHUNK_SIZE) + return false; + + child_key_ba.insert(child_key_ba.end(), hmac.begin(), hmac.begin() + EXTENDED_CHUNK_SIZE); + child_chain_code.insert(child_chain_code.end(), hmac.begin() + EXTENDED_CHUNK_SIZE, hmac.end()); + } + + // Add the extended key to the value created... + BIGNUM_TYPE child_key; + if (! child_key.decode(child_key_ba)) + return false; + + extended_key = (extended_key + child_key) % curve_order; + extended_chain_code = child_chain_code; + } + + // now convert the extended_key into an ECDSA key, for the moment the key + // generation function only understands byte arrays + ww::types::ByteArray extended_key_ba; + if (! extended_key.encode(extended_key_ba)) + return false; + + return ww::crypto::ecdsa::generate_keys(extended_key_ba, private_key, public_key); +} diff --git a/identity-contract/identity/common/SigningContext.h b/identity-contract/identity/common/SigningContext.h new file mode 100644 index 0000000..31feed3 --- /dev/null +++ b/identity-contract/identity/common/SigningContext.h @@ -0,0 +1,133 @@ +/* Copyright 2023 Intel Corporation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once +#include +#include + +#include "Util.h" +#include "Value.h" + +#include "exchange/common/Common.h" + +#define SIGNING_CONTEXT_SCHEMA \ + "{" \ + SCHEMA_KW(extensible, true) "," \ + SCHEMA_KW(description, "") "," \ + SCHEMA_KW(subcontexts, [ "" ]) \ + "}" + +// when the extensible flag is true: +// * all subcontexts are valid +// * no subcontext registration is allowed +// * subcontexts field is empty + +// when the extensible flag is false: +// * only registered subcontexts are valid +// * subcontext registration is allowed +// * subcontext field contains a list of registered subcontexts + +// algorithm to determine if path [p1, p2, .. pn] is valid: +// context = root context +// foreach p in path : +// if context is extensible : +// return true +// if p is not in context.subcontexts : +// return false +// context is context.subcontext[p] +// return true + +// we need to find a signing context based on the longest prefix. the easiest way +// to do that is to have a signing context for each element of the list. + +// subcontexts is necessary because the extensible flag must be inherited correctly +// by any new context that is created below this one + +// several ways to do this... +// * build the key when the context is created and store it here +// * store the full context path here and reconstruct the key from this context +// * + +#define USE_SECP384R1 + +// https://en.wikipedia.org/wiki/P-384 +#ifdef USE_SECP384R1 +#define EXTENDED_KEY_SIZE 48 +#define BIGNUM_TYPE ww::types::BigNum384 +#define HASH_FUNCTION ww::crypto::hash::sha384_hash +#define CURVE_ORDER "//////////////////////////////////////////7/////AAAAAAAAAAD/////" +#endif + +namespace ww +{ +namespace identity +{ + class SigningContext : public ww::exchange::SerializeableObject + { + friend class SigningContextManager; + + protected: + bool contains(const std::string& name) const; + + bool extensible_; // extensible implies no subcontexts + std::string description_; // human readable description + std::vector subcontexts_; // registered subcontexts + + public: + static bool sign_message( + const ww::types::ByteArray& root_key, + const std::vector& context_path, + const ww::types::ByteArray& message, + ww::types::ByteArray& signature); + + static bool verify_signature( + const ww::types::ByteArray& root_key, + const std::vector& context_path, + const ww::types::ByteArray& message, + const ww::types::ByteArray& signature); + + static bool generate_keys( + const ww::types::ByteArray& root_key, // base64 encoded representation of 48 byte random array + const std::vector& context_path, + std::string& private_key, // PEM encoded ECDSA private and public keys + std::string& public_key); + + // SerializeableObject virtual methods + static bool verify_schema(const ww::value::Object& deserialized_object) + { + return ww::exchange::SerializeableObject::verify_schema_actual( + deserialized_object, SIGNING_CONTEXT_SCHEMA); + } + + bool deserialize(const ww::value::Object& request); + bool serialize(ww::value::Value& serialized_request) const; + + SigningContext( + const std::vector& subcontexts, + const bool extensible, + const std::string& description) + : subcontexts_(subcontexts), extensible_(extensible), description_(description) { }; + + SigningContext( + const bool extensible, + const std::string& description) + : extensible_(extensible), description_(description) + { subcontexts_.resize(0); }; + + SigningContext(void) : SigningContext(false, "") { }; + }; + +}; // identity +} // ww diff --git a/identity-contract/identity/common/SigningContextManager.cpp b/identity-contract/identity/common/SigningContextManager.cpp new file mode 100644 index 0000000..0e76e86 --- /dev/null +++ b/identity-contract/identity/common/SigningContextManager.cpp @@ -0,0 +1,161 @@ +/* Copyright 2023 Intel Corporation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include +#include +#include + +#include "KeyValue.h" +#include "Types.h" +#include "Value.h" +#include "WasmExtensions.h" + +#include "identity/common/SigningContext.h" +#include "identity/common/SigningContextManager.h" + +// XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX +// Class: ww::identity::SigningContextManager +// XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX +const std::string ww::identity::SigningContextManager::root_key_("__ROOT__"); + +// ----------------------------------------------------------------- +bool ww::identity::SigningContextManager::initialize(void) +{ + ww::identity::SigningContext root_context; + + std::vector root_path; + return root_context.save_to_datastore(store_, make_key(root_path)); +} + +// ----------------------------------------------------------------- +std::string ww::identity::SigningContextManager::make_key( + const std::vector& context_path) +{ + std::string p(root_key_); + + std::vector::iterator path_element; + for ( path_element = context_path.begin(); path_element < context_path.end(); path_element++) + p += "." + (*path_element); + + return p; +} + +// ----------------------------------------------------------------- +bool ww::identity::SigningContextManager::add_context( + const std::vector& context_path, + const ww::identity::SigningContext& new_context) +{ + // must contain at least the new context name + if (context_path.size() < 1) + return false; + + const std::string new_context_name = context_path.back(); + const std::vector parent_path(context_path.begin(), context_path.end() - 1); + + ww::identity::SigningContext parent; + std::vector extended_path; + if (! find_context(parent_path, extended_path, parent)) + return false; + + // make sure the context path terminates in a signing context + if (extended_path.size() > 0) + return false; + + // make sure the parent context is not extensible; if it is extensible + // then all paths are legitimate and none may have signing contexts + if (parent.extensible_) + return false; + + // make sure the new context does not already exist + if (parent.contains(new_context_name)) + return false; + + // looks like all tests pass, now update the parent + parent.subcontexts_.push_back(new_context_name); + const std::string pkey = make_key(parent_path); + if (! parent.save_to_datastore(store_, pkey)) + return false; + + // and now save the new context; note that we + // assume that the new context has been correctly + // initialized; specifically, the subcontexts vector + // should be empty on the initial save + const std::string ckey = make_key(context_path); + if (! new_context.save_to_datastore(store_, ckey)) + return false; + + return true; +} + +// ----------------------------------------------------------------- +bool ww::identity::SigningContextManager::remove_context( + const std::vector& context_path) +{ + return true; +} + +// ----------------------------------------------------------------- +bool ww::identity::SigningContextManager::find_context( + const std::vector& context_path, + std::vector& extended_path, + ww::identity::SigningContext& context) const +{ + extended_path.resize(0); + + // empty path should point to the root context + std::vector path; + if (! context.get_from_datastore(store_, make_key(path))) + { + // this shouldn't happen unless the data store has been corrupted + CONTRACT_SAFE_LOG(3, "failed to locate the root context\n"); + return false; + } + + // walk the tree of contexts and verify that they exist and are not extensible + for (auto path_element = context_path.begin(); path_element < context_path.end(); path_element++) + { + // make sure that the path element is in the current context, that is, verify + // that the context path is valid + if (! context.contains(*path_element)) + { + CONTRACT_SAFE_LOG(3, "failed to find a path element %s\n", path_element->c_str()); + return false; + } + + // create the path to retrieve the next context in the path + path.push_back(*path_element); + + const std::string key = make_key(path); + if (! context.get_from_datastore(store_, key)) + { + // this shouldn't happen unless the data store has been corrupted + CONTRACT_SAFE_LOG(3, "failed to find the context from the path\n"); + return false; + } + + // if the current context is extensible then we copy whatever is left in the context path + // into the extended path + if (context.extensible_) + { + for (path_element++; path_element < context_path.end(); path_element++) + extended_path.push_back(*path_element); + + return true; + } + } + + return true; +} diff --git a/identity-contract/identity/common/SigningContextManager.h b/identity-contract/identity/common/SigningContextManager.h new file mode 100644 index 0000000..751679e --- /dev/null +++ b/identity-contract/identity/common/SigningContextManager.h @@ -0,0 +1,58 @@ +/* Copyright 2023 Intel Corporation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once +#include +#include + +#include "KeyValue.h" +#include "Types.h" + +#include "SigningContext.h" + +namespace ww +{ +namespace identity +{ + class SigningContextManager + { + private: + KeyValueStore store_; + + static std::string make_key( + const std::vector& context_path); + + static const std::string root_key_; + + public: + bool add_context( + const std::vector& context_path, + const ww::identity::SigningContext& new_context); + + bool remove_context( + const std::vector& context_path); + + bool find_context( + const std::vector& context_path, + std::vector& extended_path, + ww::identity::SigningContext& context) const; + + bool initialize(void); + + SigningContextManager(const KeyValueStore& store) : store_(store) {}; + }; + +} /* identity */ +} /* ww */ diff --git a/identity-contract/identity/contracts/identity.cpp b/identity-contract/identity/contracts/identity.cpp new file mode 100644 index 0000000..48071e3 --- /dev/null +++ b/identity-contract/identity/contracts/identity.cpp @@ -0,0 +1,362 @@ +/* Copyright 2023 Intel Corporation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include +#include + +#include "Dispatch.h" + +#include "Cryptography.h" +#include "KeyValue.h" +#include "Environment.h" +#include "Message.h" +#include "Response.h" +#include "Types.h" +#include "Util.h" +#include "Value.h" +#include "WasmExtensions.h" + +#include "contract/base.h" +#include "identity/identity.h" +#include "identity/common/SigningContext.h" +#include "identity/common/SigningContextManager.h" + +static KeyValueStore identity_metadata_store("key_store"); +static KeyValueStore signing_context_store("signing_context"); + +static const std::string md_extended_key_seed("extend_key_seed"); +static const std::string md_description("description"); + +// ----------------------------------------------------------------- +// FUNCTION: get_context_path +// create a context path from a message parameter +// RETURNS: +// true if path successfully created +// ----------------------------------------------------------------- +bool ww::identity::identity::get_context_path(const Message& msg, std::vector& context_path) +{ + ww::value::Array context_path_array; + if (! msg.get_value("context_path", context_path_array)) + return false; + + // context path must have at least one element + if (context_path_array.get_count() < 1) + return false; + + context_path.resize(0); + for (size_t i = 0; i < context_path_array.get_count(); i++) + { + const std::string s(context_path_array.get_string(i)); + context_path.push_back(s); + } + + return true; +} + +// ----------------------------------------------------------------- +// FUNCTION: validate_context_path +// ----------------------------------------------------------------- +bool ww::identity::identity::validate_context_path(std::vector& context_path) +{ + // Attempt to find the context, this will tell us whether + // the path is valid... that is, it meets the extensibility + // criteria for all registered contexts + ww::identity::SigningContextManager manager(signing_context_store); + ww::identity::SigningContext context; + std::vector extended_path; + + return manager.find_context(context_path, extended_path, context); +} + +// ----------------------------------------------------------------- +// FUNCTION: get_extended_key_seed +// ----------------------------------------------------------------- +bool ww::identity::identity::get_extended_key_seed(ww::types::ByteArray& extended_key_seed) +{ + return identity_metadata_store.get(md_extended_key_seed, extended_key_seed); +} + +// ----------------------------------------------------------------- +// METHOD: initialize_contract +// contract initialization method +// +// JSON PARAMETERS: +// none +// +// RETURNS: +// true if successfully initialized +// ----------------------------------------------------------------- +bool ww::identity::identity::initialize_contract(const Environment& env) +{ + // ---------- initialize the base contract ---------- + if (! ww::contract::base::initialize_contract(env)) + return false; + + // ---------- create the extended key seed ---------- + ww::types::ByteArray extended_key_seed(EXTENDED_KEY_SIZE); + if (! ww::crypto::random_identifier(extended_key_seed)) + return false; + + if (! identity_metadata_store.set(md_extended_key_seed, extended_key_seed)) + return false; + + // ---------- prime signing context store ---------- + ww::identity::SigningContextManager manager(signing_context_store); + if (! manager.initialize()) + return false; + + // ---------- prime the credential store ---------- + + // ---------- other metadata ---------- + if (! identity_metadata_store.set(md_description, "identity object")) + return false; + + return true; +} + +// ----------------------------------------------------------------- +// METHOD: initialize +// set the basic information for the asset type +// +// JSON PARAMETERS: +// +// RETURNS: +// true if successfully initialized +// ----------------------------------------------------------------- +bool ww::identity::identity::initialize(const Message& msg, const Environment& env, Response& rsp) +{ + ASSERT_SENDER_IS_OWNER(env, rsp); + ASSERT_UNINITIALIZED(rsp); + + ASSERT_SUCCESS(rsp, msg.validate_schema(IDENTITY_INITIALIZE_PARAM_SCHEMA), + "invalid request, missing required parameters"); + + const std::string description(msg.get_string("description")); + ASSERT_SUCCESS(rsp, identity_metadata_store.set(md_description, description), + "unexpected error, failed to save description"); + + // Mark as initialized + ASSERT_SUCCESS(rsp, ww::contract::base::mark_initialized(), "initialization failed"); + + // ---------- RETURN ---------- + return rsp.success(true); +} + +// ----------------------------------------------------------------- +// METHOD: +// register_signing_context +// +// Register a signing context. If the context already exists, it will be overridden +// with the new context. +// JSON PARAMETERS: +// IDENTITY_REGISTER_SIGNING_CONTEXT_PARAM_SCHEMA +// RETURNS: +// boolean +// ----------------------------------------------------------------- +bool ww::identity::identity::register_signing_context( + const Message& msg, const Environment& env, Response& rsp) +{ + ASSERT_SENDER_IS_OWNER(env, rsp); + ASSERT_INITIALIZED(rsp); + + ASSERT_SUCCESS(rsp, msg.validate_schema(IDENTITY_REGISTER_SIGNING_CONTEXT_PARAM_SCHEMA), + "invalid request, missing required parameters"); + + std::vector context_path; + ASSERT_SUCCESS(rsp, get_context_path(msg, context_path), + "invalid request, ill-formed context path"); + + const std::string description(msg.get_string("description")); + const bool extensible(msg.get_boolean("extensible")); + + ww::identity::SigningContextManager manager(signing_context_store); + ww::identity::SigningContext context(extensible, description); + ASSERT_SUCCESS(rsp, manager.add_context(context_path, context), + "failed to register the new context"); + + // ---------- RETURN ---------- + return rsp.success(true); +} + +// ----------------------------------------------------------------- +// METHOD: +// describe_signing_context +// +// JSON PARAMETERS: +// IDENTITY_DESCRIBE_SIGNING_CONTEXT_PARAM_SCHEMA +// RETURNS: +// IDENTITY_DESCRIBE_SIGNING_CONTEXT_RESULT_SCHEMA +// ----------------------------------------------------------------- +bool ww::identity::identity::describe_signing_context( + const Message& msg, const Environment& env, Response& rsp) +{ + ASSERT_SENDER_IS_OWNER(env, rsp); + ASSERT_INITIALIZED(rsp); + + ASSERT_SUCCESS(rsp, msg.validate_schema(IDENTITY_DESCRIBE_SIGNING_CONTEXT_PARAM_SCHEMA), + "invalid request, missing required parameters"); + + std::vector context_path; + ASSERT_SUCCESS(rsp, get_context_path(msg, context_path), + "invalid request, ill-formed context path"); + + ww::identity::SigningContextManager manager(signing_context_store); + ww::identity::SigningContext context; + std::vector extended_path; + + ASSERT_SUCCESS(rsp, manager.find_context(context_path, extended_path, context), + "invalid request, unable to locate context"); + ASSERT_SUCCESS(rsp, extended_path.size() == 0, + "invalid request, extensible paths not allowed"); + + // ---------- RETURN ---------- + ww::value::Value v; + ASSERT_SUCCESS(rsp, context.serialize(v), + "unexpected error, failed to serialize signing context"); + + return rsp.value(v, false); +} + +// ----------------------------------------------------------------- +// METHOD: +// sign +// +// JSON PARAMETERS: +// IDENTITY_SIGN_PARAM_SCHEMA +// RETURNS: +// IDENTITY_SIGN_RESULT_SCHEMA +// ----------------------------------------------------------------- +bool ww::identity::identity::sign(const Message& msg, const Environment& env, Response& rsp) +{ + ASSERT_SENDER_IS_OWNER(env, rsp); + ASSERT_INITIALIZED(rsp); + + ASSERT_SUCCESS(rsp, msg.validate_schema(IDENTITY_SIGN_PARAM_SCHEMA), + "invalid request, missing required parameters"); + + std::vector context_path; + ASSERT_SUCCESS(rsp, get_context_path(msg, context_path), + "invalid request, ill-formed context path"); + ASSERT_SUCCESS(rsp, validate_context_path(context_path), + "invalid request, ill-formed context path"); + + ww::types::ByteArray extended_key_seed; + ASSERT_SUCCESS(rsp, get_extended_key_seed(extended_key_seed), + "unexpected error, failed to retrieve extended key seed"); + + const std::string b64_message(msg.get_string("message")); + ww::types::ByteArray message; + ASSERT_SUCCESS(rsp, ww::crypto::b64_decode(b64_message, message), + "invalid request, failed to decode message"); + + ww::types::ByteArray signature; + ASSERT_SUCCESS(rsp, ww::identity::SigningContext::sign_message(extended_key_seed, context_path, message, signature), + "unexpected error, signature failed"); + + std::string b64_signature; + ASSERT_SUCCESS(rsp, ww::crypto::b64_encode(signature, b64_signature), + "unexpected error: failed to encode signature"); + + // ---------- RETURN ---------- + ww::value::String s(b64_signature.c_str()); + return rsp.value(s, false); +} + +// ----------------------------------------------------------------- +// METHOD: +// verify +// +// JSON PARAMETERS: +// IDENTITY_VERIFY_PARAM_SCHEMA +// RETURNS: +// +// ----------------------------------------------------------------- +bool ww::identity::identity::verify(const Message& msg, const Environment& env, Response& rsp) +{ + ASSERT_INITIALIZED(rsp); + + ASSERT_SUCCESS(rsp, msg.validate_schema(IDENTITY_VERIFY_PARAM_SCHEMA), + "invalid request, missing required parameters"); + + std::vector context_path; + ASSERT_SUCCESS(rsp, get_context_path(msg, context_path), + "invalid request, ill-formed context path"); + ASSERT_SUCCESS(rsp, validate_context_path(context_path), + "invalid request, ill-formed context path"); + + ww::types::ByteArray extended_key_seed; + ASSERT_SUCCESS(rsp, get_extended_key_seed(extended_key_seed), + "unexpected error, failed to retrieve extended key seed"); + + const std::string b64_message(msg.get_string("message")); + ww::types::ByteArray message; + ASSERT_SUCCESS(rsp, ww::crypto::b64_decode(b64_message, message), + "invalid request, failed to decode message"); + + const std::string b64_signature(msg.get_string("signature")); + ww::types::ByteArray signature; + ASSERT_SUCCESS(rsp, ww::crypto::b64_decode(b64_signature, signature), + "invalid request, failed to decode signature"); + + bool success = ww::identity::SigningContext::verify_signature( + extended_key_seed, context_path, message, signature); + + // ---------- RETURN ---------- + ww::value::Boolean b(success); + return rsp.value(b, false); +} + +// ----------------------------------------------------------------- +// METHOD: +// get_verifying_key +// +// Note that this method will override the get_verifying_key method +// from the common library. That method returned the verifying key +// for the contract. This is a more semantically rich variant. The +// contract verifying key should still be available from the ledger. +// +// JSON PARAMETERS: +// IDENTITY_GET_VERIFYING_KEY_PARAM_SCHEMA +// RETURNS: +// PEM encoded public key +// ----------------------------------------------------------------- +bool ww::identity::identity::get_verifying_key(const Message& msg, const Environment& env, Response& rsp) +{ + ASSERT_INITIALIZED(rsp); + + ASSERT_SUCCESS(rsp, msg.validate_schema(IDENTITY_GET_VERIFYING_KEY_PARAM_SCHEMA), + "invalid request, missing required parameters"); + + // Get the context path parameter + std::vector context_path; + ASSERT_SUCCESS(rsp, get_context_path(msg, context_path), + "invalid request, ill-formed context path"); + ASSERT_SUCCESS(rsp, validate_context_path(context_path), + "invalid request, ill-formed context path"); + + // Get the keys associated with the context path + ww::types::ByteArray root_key; + ASSERT_SUCCESS(rsp, get_extended_key_seed(root_key), + "unexpected error, failed to retrieve extended key seed"); + + std::string private_key, public_key; + ASSERT_SUCCESS(rsp, ww::identity::SigningContext::generate_keys(root_key, context_path, private_key, public_key), + "unexpected error, failed to generate public key"); + + // ---------- RETURN ---------- + ww::value::String result(public_key.c_str()); + return rsp.value(result, false); +} diff --git a/identity-contract/identity/contracts/signature_authority.cpp b/identity-contract/identity/contracts/signature_authority.cpp new file mode 100644 index 0000000..7b38959 --- /dev/null +++ b/identity-contract/identity/contracts/signature_authority.cpp @@ -0,0 +1,130 @@ +/* Copyright 2023 Intel Corporation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include +#include + +#include "Dispatch.h" + +#include "Cryptography.h" +#include "KeyValue.h" +#include "Environment.h" +#include "Message.h" +#include "Response.h" +#include "Types.h" +#include "Util.h" +#include "Value.h" +#include "WasmExtensions.h" + +#include "contract/base.h" +#include "identity/identity.h" +#include "identity/signature_authority.h" +#include "identity/common/Credential.h" +#include "identity/common/SigningContext.h" +#include "identity/common/SigningContextManager.h" + +// ----------------------------------------------------------------- +// METHOD: sign_credential +// Sign a credential generating the appropriate proof data +// +// JSON PARAMETERS: +// SIGNATURE_AUTHORITY_SIGN_CREDENTIAL_PARAM_SCHEMA +// RETURNS: +// VERIFIABLE_CREDENTIAL_SCHEMA +// ----------------------------------------------------------------- +bool ww::identity::signature_authority::sign_credential(const Message& msg, const Environment& env, Response& rsp) +{ + ASSERT_INITIALIZED(rsp); + + ASSERT_SUCCESS(rsp, msg.validate_schema(SIGNATURE_AUTHORITY_SIGN_CREDENTIAL_PARAM_SCHEMA), + "invalid request, missing required parameters"); + + // Get and validate the context path parameter + std::vector context_path; + ASSERT_SUCCESS(rsp, ww::identity::identity::get_context_path(msg, context_path), + "invalid request, ill-formed context path"); + ASSERT_SUCCESS(rsp, ww::identity::identity::validate_context_path(context_path), + "invalid request, ill-formed context path"); + + // Get and validate the credential parameter + ww::value::Object credential; + ASSERT_SUCCESS(rsp, msg.get_value("credential", credential), + "missing required parameter; credential"); + + // Pull together the information needed to build the vc + ww::types::ByteArray extended_key_seed; + ASSERT_SUCCESS(rsp, ww::identity::identity::get_extended_key_seed(extended_key_seed), + "unexpected error, failed to retrieve extended key seed"); + + const ww::identity::IdentityKey identity(env.contract_id_, context_path); + + // And build the veriable credential; just wanted to note that it would be + // completely appropriate to make a constructor for VC's that took the + // information for build; however, there are no exceptions with our current + // WASM interpreter so failure in the constructor would be a catastrophic + // failure for the contract + ww::identity::VerifiableCredential vc; + ASSERT_SUCCESS(rsp, vc.build(credential, identity, extended_key_seed), + "invalid request, ill-formed credential"); + + // Finally pull the serialized verifiable credential and send it back + ww::value::Object serialized_vc; + ASSERT_SUCCESS(rsp, vc.serialize(serialized_vc), + "unexpected error, failed to serialized the credential"); + + return rsp.value(serialized_vc, false); +} + +// ----------------------------------------------------------------- +// METHOD: verify_credential +// Verify the signature on a credential +// +// JSON PARAMETERS: +// SIGNATURE_AUTHORITY_VERIFY_CREDENTIAL_PARAM_SCHEMA +// RETURNS: +// true signature is verified +// ----------------------------------------------------------------- +bool ww::identity::signature_authority::verify_credential(const Message& msg, const Environment& env, Response& rsp) +{ + ASSERT_INITIALIZED(rsp); + + ASSERT_SUCCESS(rsp, msg.validate_schema(SIGNATURE_AUTHORITY_VERIFY_CREDENTIAL_PARAM_SCHEMA), + "invalid request, missing required parameters"); + + // Get the credential parameter + ww::value::Object credential; + ASSERT_SUCCESS(rsp, msg.get_value("credential", credential), + "missing required parameter; credential"); + + ww::identity::VerifiableCredential vc; + ASSERT_SUCCESS(rsp, vc.deserialize(credential), + "invalid request, ill-formed credential"); + ASSERT_SUCCESS(rsp, vc.proof_.verificationMethod_.id_ == env.contract_id_, + "invalid request, wrong verifier"); + ASSERT_SUCCESS(rsp, ww::identity::identity::validate_context_path(vc.proof_.verificationMethod_.context_path_), + "invalid request, unknown context path"); + + // Pull together the information needed to build the vc + ww::types::ByteArray extended_key_seed; + ASSERT_SUCCESS(rsp, ww::identity::identity::get_extended_key_seed(extended_key_seed), + "unexpected error, failed to retrieve extended key seed"); + + bool verified = vc.check(extended_key_seed); + + // ---------- RETURN ---------- + ww::value::Boolean result(verified); + return rsp.value(result, false); +} diff --git a/identity-contract/identity/identity.h b/identity-contract/identity/identity.h new file mode 100644 index 0000000..57b5a27 --- /dev/null +++ b/identity-contract/identity/identity.h @@ -0,0 +1,103 @@ +/* Copyright 2023 Intel Corporation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include + +#include "Environment.h" +#include "Message.h" +#include "Response.h" + +#define IDENTITY_INITIALIZE_PARAM_SCHEMA \ + "{" \ + SCHEMA_KW(description, "") \ + "}" + +// At some point, we would like to add a context administrator to the +// registration. The administrator of the context or of any parent +// context would be the only ones allowed to register additional +// contexts in the tree below this one or to sign objects with the +// keys in the subcontexts. The basic idea is that we can create some +// level of accountability for how subcontexts are used. + +#define IDENTITY_REGISTER_SIGNING_CONTEXT_PARAM_SCHEMA \ + "{" \ + SCHEMA_KW(context_path, [ "" ]) "," \ + SCHEMA_KW(description, "") "," \ + SCHEMA_KW(extensible, true) \ + "}" + +// At some point, we need to add an operation to unregister a signing +// context. While it may sound straightforward (and might actually be +// so), there may be issues to address with usefulness of keys that +// have been used previously + +#define IDENTITY_DESCRIBE_SIGNING_CONTEXT_PARAM_SCHEMA \ + "{" \ + SCHEMA_KW(context_path, [ "" ]) \ + "}" + +#define IDENTITY_DESCRIBE_SIGNING_CONTEXT_RESULT_SCHEMA \ + "{" \ + SCHEMA_KW(subcontexts, [ "" ]) "," \ + SCHEMA_KW(description, "") "," \ + SCHEMA_KW(extensible, true) \ + "}" + +#define IDENTITY_SIGN_PARAM_SCHEMA \ + "{" \ + SCHEMA_KW(context_path, [ "" ]) "," \ + SCHEMA_KW(message, "") \ + "}" + +#define IDENTITY_SIGN_RESULT_SCHEMA \ + "{" \ + SCHEMA_KW(signature, "") \ + "}" + +#define IDENTITY_VERIFY_PARAM_SCHEMA \ + "{" \ + SCHEMA_KW(context_path, [ "" ]) "," \ + SCHEMA_KW(message, "") "," \ + SCHEMA_KW(signature, "") \ + "}" + +#define IDENTITY_GET_VERIFYING_KEY_PARAM_SCHEMA \ + "{" \ + SCHEMA_KW(context_path, [ "" ]) \ + "}" + +namespace ww +{ +namespace identity +{ +namespace identity +{ + bool initialize_contract(const Environment& env); + bool initialize(const Message& msg, const Environment& env, Response& rsp); + bool register_signing_context(const Message& msg, const Environment& env, Response& rsp); + bool describe_signing_context(const Message& msg, const Environment& env, Response& rsp); + bool sign(const Message& msg, const Environment& env, Response& rsp); + bool verify(const Message& msg, const Environment& env, Response& rsp); + bool get_verifying_key(const Message& msg, const Environment& env, Response& rsp); + + bool get_context_path(const Message& msg, std::vector& context_path); + bool validate_context_path(std::vector& context_path); + bool get_extended_key_seed(ww::types::ByteArray& extended_key_seed); + +}; // identity +}; // identity +}; // ww diff --git a/identity-contract/identity/signature_authority.h b/identity-contract/identity/signature_authority.h new file mode 100644 index 0000000..25ba3f9 --- /dev/null +++ b/identity-contract/identity/signature_authority.h @@ -0,0 +1,51 @@ +/* Copyright 2023 Intel Corporation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include + +#include "Environment.h" +#include "Message.h" +#include "Response.h" +#include "Util.h" + +#include "common/Credential.h" + +#define SIGNATURE_AUTHORITY_SIGN_CREDENTIAL_PARAM_SCHEMA \ + "{" \ + SCHEMA_KW(context_path, [ "" ]) "," \ + SCHEMA_KWS(credential, CREDENTIAL_SCHEMA) \ + "}" + +#define SIGNATURE_AUTHORITY_SIGN_CREDENTIAL_RESULT_SCHEMA \ + VERIFIABLE_CREDENTIAL_SCHEMA + +#define SIGNATURE_AUTHORITY_VERIFY_CREDENTIAL_PARAM_SCHEMA \ + "{" \ + SCHEMA_KWS(credential, VERIFIABLE_CREDENTIAL_SCHEMA) \ + "}" + +namespace ww +{ +namespace identity +{ +namespace signature_authority +{ + bool sign_credential(const Message& msg, const Environment& env, Response& rsp); + bool verify_credential(const Message& msg, const Environment& env, Response& rsp); +}; // signature_authority +}; // identity +}; // ww diff --git a/identity-contract/methods.cmake b/identity-contract/methods.cmake new file mode 100644 index 0000000..72f72e5 --- /dev/null +++ b/identity-contract/methods.cmake @@ -0,0 +1,39 @@ +# Copyright 2023 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +IF(NOT DEFINED EXCHANGE_INCLUDES) + MESSAGE(FATAL_ERROR "EXCHANGE_INCLUDES is not defined") +ENDIF() + +# --------------------------------------------- +# Set up the include list +# --------------------------------------------- +SET (IDENTITY_INCLUDES ${WASM_INCLUDES}) +LIST(APPEND IDENTITY_INCLUDES ${EXCHANGE_INCLUDES}) +LIST(APPEND IDENTITY_INCLUDES ${CMAKE_CURRENT_LIST_DIR}) + +# --------------------------------------------- +# Set up the default source list +# --------------------------------------------- +FILE(GLOB IDENTITY_COMMON_SOURCE ${CMAKE_CURRENT_LIST_DIR}/identity/common/*.cpp) +FILE(GLOB IDENTITY_CONTRACT_SOURCE ${CMAKE_CURRENT_LIST_DIR}/identity/contracts/*.cpp) + +SET (IDENTITY_SOURCES) +LIST(APPEND IDENTITY_SOURCES ${IDENTITY_COMMON_SOURCE}) +LIST(APPEND IDENTITY_SOURCES ${IDENTITY_CONTRACT_SOURCE}) + +# --------------------------------------------- +# Build the wawaka contract common library +# --------------------------------------------- +SET(IDENTITY_LIB ww_identity) diff --git a/identity-contract/pdo/__init__.py b/identity-contract/pdo/__init__.py new file mode 100644 index 0000000..6b6af53 --- /dev/null +++ b/identity-contract/pdo/__init__.py @@ -0,0 +1,15 @@ +# Copyright 2023 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +__import__('pkg_resources').declare_namespace('pdo') diff --git a/identity-contract/pdo/identity/__init__.py b/identity-contract/pdo/identity/__init__.py new file mode 100644 index 0000000..3c34c82 --- /dev/null +++ b/identity-contract/pdo/identity/__init__.py @@ -0,0 +1,15 @@ +# Copyright 2023 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +__all__ = [ 'plugins', 'resources', 'scripts', ] diff --git a/identity-contract/pdo/identity/plugins/__init__.py b/identity-contract/pdo/identity/plugins/__init__.py new file mode 100644 index 0000000..8d63b25 --- /dev/null +++ b/identity-contract/pdo/identity/plugins/__init__.py @@ -0,0 +1,15 @@ +# Copyright 2023 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +__all__ = [ 'identity', 'signature_authority' ] diff --git a/identity-contract/pdo/identity/plugins/identity.py b/identity-contract/pdo/identity/plugins/identity.py new file mode 100644 index 0000000..6a0b41e --- /dev/null +++ b/identity-contract/pdo/identity/plugins/identity.py @@ -0,0 +1,312 @@ +# Copyright 2023 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import logging + +from pdo.contract import invocation_request + +import pdo.client.builder as pbuilder +import pdo.client.builder.command as pcommand +import pdo.client.builder.contract as pcontract +import pdo.client.builder.shell as pshell +import pdo.client.commands.contract as pcontract_cmd + +import pdo.common.crypto as pcrypto + +__all__ = [ + 'op_initialize', + 'op_get_verifying_key', + 'op_register_signing_context', + 'op_describe_signing_context', + 'op_sign', + 'op_verify', + 'cmd_create_identity', + 'do_identity', + 'do_identity_contract', + 'load_commands', +] + +logger = logging.getLogger(__name__) + +# ----------------------------------------------------------------- +# ----------------------------------------------------------------- +class op_initialize(pcontract.contract_op_base) : + + name = "initialize" + help = "initialize an identity contract object" + + @classmethod + def add_arguments(cls, subparser) : + subparser.add_argument( + '-d', '--description', + help='Description of the asset described by the identity', + type=str, + required=True) + + @classmethod + def invoke(cls, state, session_params, description, **kwargs) : + session_params['commit'] = True + + message = invocation_request('initialize', description=description) + result = pcontract_cmd.send_to_contract(state, message, **session_params) + cls.log_invocation(message, result) + + return result + +# ----------------------------------------------------------------- +# ----------------------------------------------------------------- +class op_get_verifying_key(pcontract.contract_op_base) : + + name = "get_verifying_key" + help = "Get the verifying key for a context" + + @classmethod + def add_arguments(cls, subparser) : + subparser.add_argument( + '-p', '--path', + help='Path to the signing context', + type=str, + nargs='+', + required=True) + + @classmethod + def invoke(cls, state, session_params, path, **kwargs) : + session_params['commit'] = True + + params = { + 'context_path' : path, + } + message = invocation_request('get_verifying_key', **params) + result = pcontract_cmd.send_to_contract(state, message, **session_params) + cls.log_invocation(message, result) + + return result + +# ----------------------------------------------------------------- +# ----------------------------------------------------------------- +class op_register_signing_context(pcontract.contract_op_base) : + + name = "register_signing_context" + help = "Register a signing context" + + @classmethod + def add_arguments(cls, subparser) : + subparser.add_argument( + '-d', '--description', + help='Description of the asset described by the identity', + type=str, + required=True) + subparser.add_argument( + '--extensible', + help='Allow unregistered contexts to be used from this context', + action='store_true') + subparser.add_argument( + '--fixed', + help='Only registered contexts may be used from this context', + action='store_false', + dest='extensible') + subparser.add_argument( + '-p', '--path', + help='Path to the signing context', + type=str, + nargs='+', + required=True) + + @classmethod + def invoke(cls, state, session_params, path, description, extensible, **kwargs) : + session_params['commit'] = True + + params = { + 'context_path' : path, + 'description' : description, + 'extensible' : extensible, + } + message = invocation_request('register_signing_context', **params) + result = pcontract_cmd.send_to_contract(state, message, **session_params) + cls.log_invocation(message, result) + + return result + +# ----------------------------------------------------------------- +# ----------------------------------------------------------------- +class op_describe_signing_context(pcontract.contract_op_base) : + + name = "describe_signing_context" + help = "Describe a signing context" + + @classmethod + def add_arguments(cls, subparser) : + subparser.add_argument( + '-p', '--path', + help='Path to the signing context', + type=str, + nargs='+', + required=True) + pass + + @classmethod + def invoke(cls, state, session_params, path, **kwargs) : + session_params['commit'] = True + + message = invocation_request('describe_signing_context', context_path=path) + result = pcontract_cmd.send_to_contract(state, message, **session_params) + cls.log_invocation(message, result) + + return result + +# ----------------------------------------------------------------- +# ----------------------------------------------------------------- +class op_sign(pcontract.contract_op_base) : + + name = "sign" + help = "Sign message using specified signing context" + + @classmethod + def add_arguments(cls, subparser) : + subparser.add_argument( + '-m', '--message', + help='Base64 encoded string to sign', + type=str, + required=True) + subparser.add_argument( + '-p', '--path', + help='Path to the signing context', + type=str, + nargs='+', + required=True) + + @classmethod + def invoke(cls, state, session_params, path, message, **kwargs) : + session_params['commit'] = True + + bytes_message = pcrypto.string_to_byte_array(message) + b64_message = pcrypto.byte_array_to_base64(bytes_message) + message = invocation_request('sign', context_path=path, message=b64_message) + result = pcontract_cmd.send_to_contract(state, message, **session_params) + cls.log_invocation(message, result) + + return result + +# ----------------------------------------------------------------- +# ----------------------------------------------------------------- +class op_verify(pcontract.contract_op_base) : + + name = "verify" + help = "Verify a signature using the specified signing context" + + @classmethod + def add_arguments(cls, subparser) : + subparser.add_argument( + '-m', '--message', + help='Base64 encoded string to sign', + type=str, + required=True) + subparser.add_argument( + '-p', '--path', + help='Path to the signing context', + type=str, + nargs='+', + required=True) + subparser.add_argument( + '--signature', + help='Base64 encoded signature to verify', + type=pbuilder.invocation_parameter, + required=True) + + @classmethod + def invoke(cls, state, session_params, path, message, signature, **kwargs) : + session_params['commit'] = True + + bytes_message = pcrypto.string_to_byte_array(message) + b64_message = pcrypto.byte_array_to_base64(bytes_message) + + params = { + 'message' : b64_message, + 'context_path' : path, + 'signature' : signature, + } + message = invocation_request('verify', **params) + result = pcontract_cmd.send_to_contract(state, message, **session_params) + cls.log_invocation(message, result) + + return result + +# ----------------------------------------------------------------- +# ----------------------------------------------------------------- +class cmd_create_identity(pcommand.contract_command_base) : + name = "create" + help = "script to create an identity" + + @classmethod + def add_arguments(cls, subparser) : + subparser.add_argument('-c', '--contract-class', help='Name of the contract class', type=str) + subparser.add_argument('-e', '--eservice-group', help='Name of the enclave service group to use', type=str) + subparser.add_argument('-f', '--save-file', help='File where contract data is stored', type=str) + subparser.add_argument('-p', '--pservice-group', help='Name of the provisioning service group to use', type=str) + subparser.add_argument('-r', '--sservice-group', help='Name of the storage service group to use', type=str) + subparser.add_argument('--source', help='File that contains contract source code', type=str) + subparser.add_argument('--extra', help='Extra data associated with the contract file', nargs=2, action='append') + + subparser.add_argument( + '-d', '--description', + help='Description of the asset described by the identity', + type=str, + required=True) + + @classmethod + def invoke(cls, state, context, description, **kwargs) : + save_file = pcontract_cmd.get_contract_from_context(state, context) + if save_file : + return save_file + + # create the vetting organization type + save_file = pcontract_cmd.create_contract_from_context(state, context, 'identity', **kwargs) + context['save_file'] = save_file + + session = pbuilder.SessionParameters(save_file=save_file) + pcontract.invoke_contract_op( + op_initialize, + state, context, session, + description, + **kwargs) + + cls.display('created identity in {}'.format(save_file)) + return save_file + +# ----------------------------------------------------------------- +# Create the generic, shell independent version of the aggregate command +# ----------------------------------------------------------------- +__operations__ = [ + op_initialize, + op_get_verifying_key, + op_register_signing_context, + op_describe_signing_context, + op_sign, + op_verify, +] + +do_identity_contract = pcontract.create_shell_command('identity_contract', __operations__) + +__commands__ = [ + cmd_create_identity, +] + +do_identity = pcommand.create_shell_command('identity', __commands__) + +# ----------------------------------------------------------------- +# Enable binding of the shell independent version to a pdo-shell command +# ----------------------------------------------------------------- +def load_commands(cmdclass) : + pshell.bind_shell_command(cmdclass, 'identity_wallet', do_identity) + pshell.bind_shell_command(cmdclass, 'identity_wallet_contract', do_identity_contract) diff --git a/identity-contract/pdo/identity/plugins/signature_authority.py b/identity-contract/pdo/identity/plugins/signature_authority.py new file mode 100644 index 0000000..f0c15be --- /dev/null +++ b/identity-contract/pdo/identity/plugins/signature_authority.py @@ -0,0 +1,192 @@ +# Copyright 2023 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import json +import logging + +from pdo.contract import invocation_request + +import pdo.client.builder as pbuilder +import pdo.client.builder.command as pcommand +import pdo.client.builder.contract as pcontract +import pdo.client.builder.shell as pshell +import pdo.client.commands.contract as pcontract_cmd + +import pdo.client.plugins.common as common +import pdo.identity.plugins.identity as identity + +__all__ = [ + 'op_initialize', + 'op_get_verifying_key', + 'op_register_signing_context', + 'op_describe_signing_context', + 'op_sign', + 'op_verify', + 'op_sign_credential', + 'op_verify_credential', + 'do_signature_authority', + 'do_signature_authority_contract', + 'load_commands', +] + +op_initialize = identity.op_initialize +op_get_verifying_key = identity.op_get_verifying_key +op_register_signing_context = identity.op_register_signing_context +op_describe_signing_context = identity.op_describe_signing_context +op_sign = identity.op_sign +op_verify = identity.op_verify + +logger = logging.getLogger(__name__) + +# ----------------------------------------------------------------- +# ----------------------------------------------------------------- +class op_sign_credential(pcontract.contract_op_base) : + + name = "sign_credential" + help = "Sign a credential and generate the associated verifiable credential" + + @classmethod + def add_arguments(cls, subparser) : + subparser.add_argument( + '-c', '--credential', + help='The credential to sign (JSON)', + type=pbuilder.invocation_parameter, + required=True) + subparser.add_argument( + '-p', '--path', + help='Path to the signing context', + type=str, + nargs='+', + required=True) + + @classmethod + def invoke(cls, state, session_params, credential, path, **kwargs) : + session_params['commit'] = False + + params = { + 'credential' : credential, + 'context_path' : path, + } + + message = invocation_request('sign_credential', **params) + result = pcontract_cmd.send_to_contract(state, message, **session_params) + cls.log_invocation(message, result) + + return result + +# ----------------------------------------------------------------- +# ----------------------------------------------------------------- +class op_verify_credential(pcontract.contract_op_base) : + + name = "verify_credential" + help = "Verify the signature on a signed (verifiable) credential" + + @classmethod + def add_arguments(cls, subparser) : + subparser.add_argument( + '-c', '--credential', + help='The signed credential to verify (JSON)', + type=pbuilder.invocation_parameter, + required=True) + + @classmethod + def invoke(cls, state, session_params, credential, **kwargs) : + session_params['commit'] = False + + params = { + 'credential' : credential + } + + message = invocation_request('verify_credential', **params) + result = pcontract_cmd.send_to_contract(state, message, **session_params) + cls.log_invocation(message, result) + + return result + +# ----------------------------------------------------------------- +# ----------------------------------------------------------------- +class cmd_sign_credential(pcommand.contract_command_base) : + name = "sign_credential" + help = "Sign a credential and generate the associated verifiable credential" + + @classmethod + def add_arguments(cls, subparser) : + subparser.add_argument( + '-c', '--credential', + help='The name of the file containing the credential to sign', + type=str, + required=True) + subparser.add_argument( + '-p', '--path', + help='Path to the signing context', + type=str, + nargs='+', + required=True) + subparser.add_argument( + '-s', '--signed-credential', + help='Name of the file where the signed credential will be saved', + type=str, + required=True) + + @classmethod + def invoke(cls, state, context, credential, path, signed_credential, **kwargs) : + with open(credential, "r") as fp : + credential_data = json.load(fp) + + save_file = pcontract_cmd.get_contract_from_context(state, context) + if not save_file : + raise ValueError('signature authority contract must be created and initialized') + + session = pbuilder.SessionParameters(save_file=save_file) + signed_credential_data = pcontract.invoke_contract_op( + op_sign_credential, + state, context, session, + credential=credential_data, + path=path, + **kwargs) + + with open(signed_credential, "w") as fp : + json.dump(signed_credential_data, fp) + + cls.display('saved credential to {}'.format(signed_credential)) + return True + +# ----------------------------------------------------------------- +# Create the generic, shell independent version of the aggregate command +# ----------------------------------------------------------------- +__operations__ = [ + op_initialize, + op_get_verifying_key, + op_register_signing_context, + op_describe_signing_context, + op_sign, + op_verify, + op_sign_credential, + op_verify_credential, +] + +do_signature_authority_contract = pcontract.create_shell_command('signature_authority_contract', __operations__) + +__commands__ = [ + cmd_sign_credential, +] + +do_signature_authority = pcommand.create_shell_command('signature_authority', __commands__) + +# ----------------------------------------------------------------- +# Enable binding of the shell independent version to a pdo-shell command +# ----------------------------------------------------------------- +def load_commands(cmdclass) : + pshell.bind_shell_command(cmdclass, 'signature_authority', do_signature_authority) + pshell.bind_shell_command(cmdclass, 'signature_authority_contract', do_signature_authority_contract) diff --git a/identity-contract/pdo/identity/resources/__init__.py b/identity-contract/pdo/identity/resources/__init__.py new file mode 100644 index 0000000..ecc0b2b --- /dev/null +++ b/identity-contract/pdo/identity/resources/__init__.py @@ -0,0 +1,15 @@ +# Copyright 2023 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +__all__ = [ 'resources' ] diff --git a/identity-contract/pdo/identity/resources/resources.py b/identity-contract/pdo/identity/resources/resources.py new file mode 100644 index 0000000..f485ef8 --- /dev/null +++ b/identity-contract/pdo/identity/resources/resources.py @@ -0,0 +1,18 @@ +# Copyright 2023 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import pdo.client.builder.installer as pinstaller + +def install_identity_resources() : + pinstaller.install_plugin_resources('pdo.identity.resources', 'identity') diff --git a/identity-contract/pdo/identity/scripts/__init__.py b/identity-contract/pdo/identity/scripts/__init__.py new file mode 100644 index 0000000..6a186e9 --- /dev/null +++ b/identity-contract/pdo/identity/scripts/__init__.py @@ -0,0 +1,15 @@ +# Copyright 2023 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +__all__ = [ 'scripts' ] diff --git a/identity-contract/pdo/identity/scripts/scripts.py b/identity-contract/pdo/identity/scripts/scripts.py new file mode 100644 index 0000000..1c12e9b --- /dev/null +++ b/identity-contract/pdo/identity/scripts/scripts.py @@ -0,0 +1,29 @@ +# Copyright 2023 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from pdo.client.builder.shell import run_shell_command + +import warnings +warnings.catch_warnings() +warnings.simplefilter("ignore") + +# ----------------------------------------------------------------- +# ----------------------------------------------------------------- +def signature_authority() : + run_shell_command('do_signature_authority', 'pdo.identity.plugins.signature_authority') + +# ----------------------------------------------------------------- +# ----------------------------------------------------------------- +def identity() : + run_shell_command('do_identity', 'pdo.identity.plugins.identity') diff --git a/identity-contract/scripts/init.psh b/identity-contract/scripts/init.psh new file mode 100755 index 0000000..9c68689 --- /dev/null +++ b/identity-contract/scripts/init.psh @@ -0,0 +1,31 @@ +#! /usr/bin/env pdo-shell + +## Copyright 2023 Intel Corporation +## +## Licensed under the Apache License, Version 2.0 (the "License"); +## you may not use this file except in compliance with the License. +## You may obtain a copy of the License at +## +## http://www.apache.org/licenses/LICENSE-2.0 +## +## Unless required by applicable law or agreed to in writing, software +## distributed under the License is distributed on an "AS IS" BASIS, +## WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +## See the License for the specific language governing permissions and +## limitations under the License. + +## load the identity plugins +set --conditional -s contracts -v ${home}/contracts/identity +set --conditional -s plugins -v ${home}/contracts/plugins/identity + +## load_plugin -m pdo.identity.plugins.assertion +load_plugin -m pdo.identity.plugins.identity +load_plugin -m pdo.identity.plugins.signature_authority + +## some definitions to make it easier to display text +set -s ENDC -v "\033[0m" +set -s BOLD -v '\033[1m' +set -s HEADER -v "\033[95m" +set -s ERROR -v "\033[91m" +set -s WARN -v "\033[93m" +set -s INFO -v "\033[92m" diff --git a/identity-contract/setup.py b/identity-contract/setup.py new file mode 100644 index 0000000..29f7cd6 --- /dev/null +++ b/identity-contract/setup.py @@ -0,0 +1,88 @@ +#!/usr/bin/env python + +# Copyright 2023 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os +import sys +import subprocess +import warnings + +# this should only be run with python3 +import sys +if sys.version_info[0] < 3: + print('ERROR: must run with python3') + sys.exit(1) + +from setuptools import setup, find_packages, find_namespace_packages + +# ----------------------------------------------------------------- +# Versions are tied to tags on the repository; to compute correctly +# it is necessary to be within the repository itself hence the need +# to set the cwd for the bin/get_version command. +# ----------------------------------------------------------------- +root_dir = os.path.dirname(os.path.realpath(__file__)) +try : + pdo_contracts_version = subprocess.check_output( + 'bin/get_version', cwd=os.path.join(root_dir, os.pardir)).decode('ascii').strip() +except Exception as e : + warnings.warn('Failed to get pdo_contracts version, using the default') + pdo_contracts_version = '0.0.0' + +try : + pdo_client_version = subprocess.check_output( + 'bin/get_version', cwd=os.path.join(root_dir, os.pardir, 'private-data-objects')).decode('ascii').strip() +except Exception as e : + warnings.warn('Failed to get pdo_client version, using the default') + pdo_client_version = '0.0.0' + +## ----------------------------------------------------------------- +## ----------------------------------------------------------------- +setup( + name='pdo_identity', + version=pdo_contracts_version, + description='Support functions PDO identity contracts', + author='Mic Bowman, Intel Labs', + author_email='mic.bowman@intel.com', + url='http://www.intel.com', + package_dir = { + 'pdo' : 'pdo', + 'pdo.identity.resources.etc' : 'etc', + 'pdo.identity.resources.contracts' : '../build/identity-contract', + 'pdo.identity.resources.scripts' : 'scripts', + }, + packages = [ + 'pdo', + 'pdo.identity', + 'pdo.identity.plugins', + 'pdo.identity.scripts', + 'pdo.identity.resources', + 'pdo.identity.resources.etc', + 'pdo.identity.resources.contracts', + 'pdo.identity.resources.scripts', + ], + include_package_data=True, + install_requires = [ + 'colorama', + 'pdo-client>=' + pdo_client_version, + 'pdo-common-library>=' + pdo_client_version, + 'pdo-sservice>=' + pdo_client_version, + ], + entry_points = { + 'console_scripts' : [ + 'ex_signature_authority=pdo.identity.scripts.scripts:signature_authority', + 'ex_identity=pdo.identity.scripts.scripts:identity', + ] + } +) diff --git a/identity-contract/test/credential1.json b/identity-contract/test/credential1.json new file mode 100644 index 0000000..89d3ecb --- /dev/null +++ b/identity-contract/test/credential1.json @@ -0,0 +1,14 @@ +{ + "issuer": { + "id": "A6QBclbcAayAvw7BggM7iMz_Xa6NEn_YGT94mpkQmEk=" + }, + "credentialSubject": + { + "subject": { + "id": "SMeYjWc5IOdvI3KJtPbx4WHlRvkdL5A__xcHayDj9+0=" + }, + "claims": { + "property": "5" + } + } +} diff --git a/identity-contract/test/credential2.json b/identity-contract/test/credential2.json new file mode 100644 index 0000000..9fde8b0 --- /dev/null +++ b/identity-contract/test/credential2.json @@ -0,0 +1,59 @@ +{ + "//": "this is a verifiable credential", + "nonce": "p86OvShgiWuODUdABK6Ong==", + "issuer": { + "description": "The build manager is the issuer, the id is for the build manager org", + "id": "A6QBclbcAayAvw7BggM7iMz_Xa6NEn_YGT94mpkQmEk=" + }, + "issuanceDate": "04/17/2023", + "expirationDate": "12/31/2024", + "type": [ "VerifiableCredential" ], + "credentialSchema": "http://example.com/scai_schema.json", + "credentialSubject": + { + "subject": { + "id": "SMeYjWc5IOdvI3KJtPbx4WHlRvkdL5A__xcHayDj9+0=", + "identified_by": { + "sha256": "0b222de8bcb1ea30807f1a4d733108e96de4512b689da8c5f371ac8a572e9271" + } + }, + "claims": { + "HasStackProtection": { + "conditions": { + "flags": "-fstack-protector*" + } + }, + "NoKnownVulnerabilities": { + "evidence": { + "mediaType":"text/plain", + "digest": { + "sha256":"6db3ee1d8bdf7ea064281ecfaf999f9eb5749e4647d2a59e62eaa1ea8fcf6837" + }, + "downloadLocation":"https://scans.example.com/results/snyk" + } + }, + "HasSBOM": { + "evidence": { + "mediaType": "application/spdx+json", + "digest": { + "sha256": "911d4365b61ba7ace55f7333b2c638caca4b811ee73da5beb28b9ecbbd22ca78" + }, + "downloadLocation": "https://github.com/hyperledger-labs/private-data-objects/artifacts/808758122" + } + }, + "HasSLSA": { + "evidence": { + "mediaType": "application/x.dsse+jsonl", + "digest": { + "sha256": "ea4d1e56e739f26a451c095b9fb40a353b3e73ea1778fdddafe13562e81bd745" + }, + "downloadLocation": "https://github.com/hyperledger-labs/private-data-objects/artifacts/808758121" + } + }, + "ProducedBy": { + "id": "Q06QXF2Ymmbb+L6X3cbWOJMpBGYQigWv+GnArm6+Q5I=", + "command_line": "gcc -fstack-protector -o hello-world hello-world.c" + } + } + } +} diff --git a/identity-contract/test/functional_test.psh b/identity-contract/test/functional_test.psh new file mode 100755 index 0000000..237a7ad --- /dev/null +++ b/identity-contract/test/functional_test.psh @@ -0,0 +1,42 @@ +#! /usr/bin/env pdo-shell + +## Copyright 2023 Intel Corporation +## +## Licensed under the Apache License, Version 2.0 (the "License"); +## you may not use this file except in compliance with the License. +## You may obtain a copy of the License at +## +## http://www.apache.org/licenses/LICENSE-2.0 +## +## Unless required by applicable law or agreed to in writing, software +## distributed under the License is distributed on an "AS IS" BASIS, +## WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +## See the License for the specific language governing permissions and +## limitations under the License. + +## Two shell variables are used: +## data -- the directory where the contract objects are stored +## path -- the directory where the PSH scripts are stored +## +## $ pdo-shell -s create.psh -m path + +set --conditional -s home -v . +set --conditional -s data -v . +set --conditional -s save -v . +set --conditional -s _bin_ -v ${home}/contracts/identity/scripts + +script -f ${_bin_}/init.psh + +trap_error + +script -f ${_path_}/tests/identity.psh +if -o ${_error_code_} 0 + echo ${ERROR} [ERROR ${_error_code_}] identity test failed; ${_error_message_} + exit -v ${_error_code_} +fi + +script -f ${_path_}/tests/signature_authority.psh +if -o ${_error_code_} 0 + echo ${ERROR} [ERROR ${_error_code_}] signature_authority test failed; ${_error_message_} + exit -v ${_error_code_} +fi diff --git a/identity-contract/test/run_tests.sh b/identity-contract/test/run_tests.sh new file mode 100755 index 0000000..56ae1a2 --- /dev/null +++ b/identity-contract/test/run_tests.sh @@ -0,0 +1,150 @@ +#!/bin/bash + +# Copyright 2023 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# ----------------------------------------------------------------- +# ----------------------------------------------------------------- +: "${PDO_LEDGER_URL?Missing environment variable PDO_LEDGER_URL}" +: "${PDO_HOME?Missing environment variable PDO_HOME}" +: "${PDO_SOURCE_ROOT?Missing environment variable PDO_SOURCE_ROOT}" + +# ----------------------------------------------------------------- +# ----------------------------------------------------------------- +source ${PDO_HOME}/bin/lib/common.sh +check_python_version + +if ! command -v pdo-shell &> /dev/null ; then + yell unable to locate pdo-shell + exit 1 +fi + +# ----------------------------------------------------------------- +# ----------------------------------------------------------------- +if [ "${PDO_LEDGER_TYPE}" == "ccf" ]; then + if [ ! -f "${PDO_LEDGER_KEY_ROOT}/networkcert.pem" ]; then + die "CCF ledger keys are missing, please copy and try again" + fi +fi + +# ----------------------------------------------------------------- +# Process command line arguments +# ----------------------------------------------------------------- +SCRIPTDIR="$(dirname $(readlink --canonicalize ${BASH_SOURCE}))" +SOURCE_ROOT="$(realpath ${SCRIPTDIR}/..)" + +F_SCRIPT=$(basename ${BASH_SOURCE[-1]} ) +F_SERVICE_HOST=${PDO_HOSTNAME} +F_LEDGER_URL=${PDO_LEDGER_URL} +F_LOGLEVEL=${PDO_LOG_LEVEL:-info} +F_LOGFILE=${PDO_LOG_FILE:-__screen__} +F_CONTEXT_FILE=${SOURCE_ROOT}/test/test_context.toml +F_CONTEXT_TEMPLATES=${PDO_HOME}/contracts/identity/context + +F_USAGE='--host service-host | --ledger url | --loglevel [debug|info|warn] | --logfile file' +SHORT_OPTS='h:l:' +LONG_OPTS='host:,ledger:,loglevel:,logfile:' + +TEMP=$(getopt -o ${SHORT_OPTS} --long ${LONG_OPTS} -n "${F_SCRIPT}" -- "$@") +if [ $? != 0 ] ; then echo "Usage: ${F_SCRIPT} ${F_USAGE}" >&2 ; exit 1 ; fi + +eval set -- "$TEMP" +while true ; do + case "$1" in + -h|--host) F_SERVICE_HOST="$2" ; shift 2 ;; + -1|--ledger) F_LEDGER_URL="$2" ; shift 2 ;; + --loglevel) F_LOGLEVEL="$2" ; shift 2 ;; + --logfile) F_LOGFILE="$2" ; shift 2 ;; + --help) echo "Usage: ${SCRIPT_NAME} ${F_USAGE}"; exit 0 ;; + --) shift ; break ;; + *) echo "Internal error!" ; exit 1 ;; + esac +done + +F_SERVICE_SITE_FILE=${PDO_HOME}/etc/sites/${F_SERVICE_HOST}.toml +if [ ! -f ${F_SERVICE_SITE_FILE} ] ; then + die unable to locate the service information file ${F_SERVICE_SITE_FILE}; \ + please copy the site.toml file from the service host +fi + +F_SERVICE_GROUPS_DB_FILE=${SOURCE_ROOT}/test/${F_SERVICE_HOST}_groups_db +F_SERVICE_DB_FILE=${SOURCE_ROOT}/test/${F_SERVICE_HOST}_db + +_COMMON_=("--logfile ${F_LOGFILE}" "--loglevel ${F_LOGLEVEL}") +_COMMON_+=("--ledger ${F_LEDGER_URL}") +_COMMON_+=("--groups-db ${F_SERVICE_GROUPS_DB_FILE}") +_COMMON_+=("--service-db ${F_SERVICE_DB_FILE}") +SHORT_OPTS=${_COMMON_[@]} + +_COMMON_+=("--context-file ${F_CONTEXT_FILE}") +OPTS=${_COMMON_[@]} + +# ----------------------------------------------------------------- +# Make sure the keys and eservice database are created and up to date +# ----------------------------------------------------------------- +F_KEY_FILES=() +KEYGEN=${PDO_SOURCE_ROOT}/build/__tools__/make-keys + +yell create keys for the contracts +for i in 1 2 3 4 5 ; do + if [ ! -f ${PDO_HOME}/keys/user${i}_private.pem ] ; then + ${KEYGEN} --keyfile ${PDO_HOME}/keys/user${i} --format pem + F_KEY_FILES+=(${PDO_HOME}/keys/user${i}_{private,public}.pem) + fi +done + +# ----------------------------------------------------------------- +function cleanup { + rm -f ${F_SERVICE_GROUPS_DB_FILE} ${F_SERVICE_GROUPS_DB_FILE}-lock + rm -f ${F_SERVICE_DB_FILE} ${F_SERVICE_DB_FILE}-lock + rm -f ${F_CONTEXT_FILE} + for key_file in ${F_KEY_FILES[@]} ; do + rm -f ${key_file} + done +} + +trap cleanup EXIT + +# ----------------------------------------------------------------- +# create the service and groups databases from a site file; the site +# file is assumed to exist in ${PDO_HOME}/etc/sites/${SERVICE_HOST}.toml +# +# by default, the groups will include all available services from the +# service host +# ----------------------------------------------------------------- +yell create the service and groups database for host ${F_SERVICE_HOST} +try pdo-service-db import ${SHORT_OPTS} --file ${F_SERVICE_SITE_FILE} +try pdo-eservice create_from_site ${SHORT_OPTS} --file ${F_SERVICE_SITE_FILE} --group default +try pdo-pservice create_from_site ${SHORT_OPTS} --file ${F_SERVICE_SITE_FILE} --group default +try pdo-sservice create_from_site ${SHORT_OPTS} --file ${F_SERVICE_SITE_FILE} --group default \ + --replicas 1 --duration 60 + +# ----------------------------------------------------------------- +# setup the contexts that will be used later for the tests +# ----------------------------------------------------------------- +yell create the contexts +if [ -f ${F_CONTEXT_FILE} ]; then + rm ${F_CONTEXT_FILE} +fi + +# create any necessary contexts here + +# ----------------------------------------------------------------- +# ----------------------------------------------------------------- +yell run the functional test +if [ -f ${SCRIPTDIR}/functional_test.psh ]; then + pushd "${SOURCE_ROOT}" + try ${SCRIPTDIR}/functional_test.psh ${OPTS} + popd +fi diff --git a/identity-contract/test/script_test.sh b/identity-contract/test/script_test.sh new file mode 100755 index 0000000..c895e7f --- /dev/null +++ b/identity-contract/test/script_test.sh @@ -0,0 +1,144 @@ +#!/bin/bash + +# Copyright 2023 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# ----------------------------------------------------------------- +# ----------------------------------------------------------------- +: "${PDO_LEDGER_URL?Missing environment variable PDO_LEDGER_URL}" +: "${PDO_HOME?Missing environment variable PDO_HOME}" +: "${PDO_SOURCE_ROOT?Missing environment variable PDO_SOURCE_ROOT}" + +# ----------------------------------------------------------------- +# ----------------------------------------------------------------- +source ${PDO_HOME}/bin/lib/common.sh +check_python_version + +if ! command -v pdo-shell &> /dev/null ; then + yell unable to locate pdo-shell + exit 1 +fi + +# ----------------------------------------------------------------- +# ----------------------------------------------------------------- +if [ "${PDO_LEDGER_TYPE}" == "ccf" ]; then + if [ ! -f "${PDO_LEDGER_KEY_ROOT}/networkcert.pem" ]; then + die "CCF ledger keys are missing, please copy and try again" + fi +fi + +# ----------------------------------------------------------------- +# Process command line arguments +# ----------------------------------------------------------------- +SCRIPTDIR="$(dirname $(readlink --canonicalize ${BASH_SOURCE}))" +SOURCE_ROOT="$(realpath ${SCRIPTDIR}/..)" + +F_SCRIPT=$(basename ${BASH_SOURCE[-1]} ) +F_SERVICE_HOST=${PDO_HOSTNAME} +F_LEDGER_URL=${PDO_LEDGER_URL} +F_LOGLEVEL=${PDO_LOG_LEVEL:-info} +F_LOGFILE=${PDO_LOG_FILE:-__screen__} +F_CONTEXT_FILE=${SOURCE_ROOT}/test/test_context.toml +F_CONTEXT_TEMPLATES=${PDO_HOME}/contracts/exchange/context + +F_USAGE='--host service-host | --ledger url | --loglevel [debug|info|warn] | --logfile file' +SHORT_OPTS='h:l:' +LONG_OPTS='host:,ledger:,loglevel:,logfile:' + +TEMP=$(getopt -o ${SHORT_OPTS} --long ${LONG_OPTS} -n "${F_SCRIPT}" -- "$@") +if [ $? != 0 ] ; then echo "Usage: ${F_SCRIPT} ${F_USAGE}" >&2 ; exit 1 ; fi + +eval set -- "$TEMP" +while true ; do + case "$1" in + -h|--host) F_SERVICE_HOST="$2" ; shift 2 ;; + -1|--ledger) F_LEDGER_URL="$2" ; shift 2 ;; + --loglevel) F_LOGLEVEL="$2" ; shift 2 ;; + --logfile) F_LOGFILE="$2" ; shift 2 ;; + --help) echo "Usage: ${SCRIPT_NAME} ${F_USAGE}"; exit 0 ;; + --) shift ; break ;; + *) echo "Internal error!" ; exit 1 ;; + esac +done + +F_SERVICE_SITE_FILE=${PDO_HOME}/etc/sites/${F_SERVICE_HOST}.toml +if [ ! -f ${F_SERVICE_SITE_FILE} ] ; then + die unable to locate the service information file ${F_SERVICE_SITE_FILE}; \ + please copy the site.toml file from the service host +fi + +F_SERVICE_GROUPS_DB_FILE=${SOURCE_ROOT}/test/${F_SERVICE_HOST}_groups_db +F_SERVICE_DB_FILE=${SOURCE_ROOT}/test/${F_SERVICE_HOST}_db + +_COMMON_=("--logfile ${F_LOGFILE}" "--loglevel ${F_LOGLEVEL}") +_COMMON_+=("--ledger ${F_LEDGER_URL}") +_COMMON_+=("--groups-db ${F_SERVICE_GROUPS_DB_FILE}") +_COMMON_+=("--service-db ${F_SERVICE_DB_FILE}") +SHORT_OPTS=${_COMMON_[@]} + +_COMMON_+=("--context-file ${F_CONTEXT_FILE}") +OPTS=${_COMMON_[@]} + +# ----------------------------------------------------------------- +# Make sure the keys and eservice database are created and up to date +# ----------------------------------------------------------------- +F_KEY_FILES=() +KEYGEN=${PDO_SOURCE_ROOT}/build/__tools__/make-keys + +yell create keys for the contracts +for i in 1 2 3 4 5 ; do + if [ ! -f ${PDO_HOME}/keys/user${i}_private.pem ] ; then + ${KEYGEN} --keyfile ${PDO_HOME}/keys/user${i} --format pem + F_KEY_FILES+=(${PDO_HOME}/keys/user${i}_{private,public}.pem) + fi +done + +# ----------------------------------------------------------------- +function cleanup { + rm -f ${F_SERVICE_GROUPS_DB_FILE} ${F_SERVICE_GROUPS_DB_FILE}-lock + rm -f ${F_SERVICE_DB_FILE} ${F_SERVICE_DB_FILE}-lock + rm -f ${F_CONTEXT_FILE} + for key_file in ${F_KEY_FILES[@]} ; do + rm -f ${key_file} + done +} + +trap cleanup EXIT + +# ----------------------------------------------------------------- +# create the service and groups databases from a site file; the site +# file is assumed to exist in ${PDO_HOME}/etc/sites/${SERVICE_HOST}.toml +# +# by default, the groups will include all available services from the +# service host +# ----------------------------------------------------------------- +yell create the service and groups database for host ${F_SERVICE_HOST} +try pdo-service-db import ${SHORT_OPTS} --file ${F_SERVICE_SITE_FILE} +try pdo-eservice create_from_site ${SHORT_OPTS} --file ${F_SERVICE_SITE_FILE} --group default +try pdo-pservice create_from_site ${SHORT_OPTS} --file ${F_SERVICE_SITE_FILE} --group default +try pdo-sservice create_from_site ${SHORT_OPTS} --file ${F_SERVICE_SITE_FILE} --group default \ + --replicas 1 --duration 60 + +# ----------------------------------------------------------------- +# setup the contexts that will be used later for the tests +# ----------------------------------------------------------------- +cd "${SOURCE_ROOT}" + +rm -f ${F_CONTEXT_FILE} + +# create any necessary contexts here + +# ----------------------------------------------------------------- +# start the tests +# ----------------------------------------------------------------- diff --git a/identity-contract/test/tests/identity.psh b/identity-contract/test/tests/identity.psh new file mode 100755 index 0000000..814d4e7 --- /dev/null +++ b/identity-contract/test/tests/identity.psh @@ -0,0 +1,245 @@ +#! /usr/bin/env pdo-shell + +## Copyright 2023 Intel Corporation +## +## Licensed under the Apache License, Version 2.0 (the "License"); +## you may not use this file except in compliance with the License. +## You may obtain a copy of the License at +## +## http://www.apache.org/licenses/LICENSE-2.0 +## +## Unless required by applicable law or agreed to in writing, software +## distributed under the License is distributed on an "AS IS" BASIS, +## WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +## See the License for the specific language governing permissions and +## limitations under the License. + +## Two shell variables are used: +## data -- the directory where the contract objects are stored +## path -- the directory where the PSH scripts are stored +## +## $ pdo-shell -s create.psh -m path + +set --conditional -s home -v . +set --conditional -s data -v . +set --conditional -s save -v . +set --conditional -s _bin_ -v ${home}/contracts/identity/scripts + +script -f ${_bin_}/init.psh + +set -s _contract_ -v ${save}/identity.pdo +set -s _owner_ -v user1 + +trap_error + +## ================================================================= +echo ${HEADER} create the identity contract ${ENDC} +## ================================================================= +identity -n ${_owner_} + +contract create -c identity --source ${contracts}/_identity -f ${_contract_} +if -o ${_error_code_} 0 + echo ${ERROR} [Error ${_error_code_}] failed to create the identity contract ${ENDC} + exit -v ${_error_code_} +fi + +identity_wallet_contract initialize -w -f ${_contract_} \ + --description "this is a test identity" +if -o ${_error_code_} 0 + echo ${ERROR} [Error ${_error_code_}] failed to initialize the identity contract ${ENDC} + exit -v ${_error_code_} +fi + +## ================================================================= +echo ${HEADER} test key creation, these should succeed ${ENDC} +## ================================================================= +identity_wallet_contract register_signing_context -w -f ${_contract_} \ + -d "test1 context" --fixed -p "test1" +if -o ${_error_code_} 0 + echo ${ERROR} [Error ${_error_code_}] failed to save signing context test1 ${ENDC} + exit -v ${_error_code_} +fi + +identity_wallet_contract register_signing_context -w -f ${_contract_} \ + -d "test2 context" --fixed -p "test2" +if -o ${_error_code_} 0 + echo ${ERROR} [Error ${_error_code_}] failed to save signing context test2 ${ENDC} + exit -v ${_error_code_} +fi + +identity_wallet_contract register_signing_context -w -f ${_contract_} \ + -d "test1.1 context" --fixed -p "test1" "test1.1" +if -o ${_error_code_} 0 + echo ${ERROR} [Error ${_error_code_}] failed to save signing context test1 test1.1 ${ENDC} + exit -v ${_error_code_} +fi + +identity_wallet_contract register_signing_context -w -f ${_contract_} \ + -d "test2.1 context" --fixed -p "test2" "test2.1" +if -o ${_error_code_} 0 + echo ${ERROR} [Error ${_error_code_}] failed to save signing context test2 test2.1 ${ENDC} + exit -v ${_error_code_} +fi + +identity_wallet_contract register_signing_context -w -f ${_contract_} \ + -d "test3 extensible context" --extensible -p "test3" +if -o ${_error_code_} 0 + echo ${ERROR} [Error ${_error_code_}] failed to save extensible signing context test3 ${ENDC} + exit -v ${_error_code_} +fi + +## ================================================================= +echo ${HEADER} test key description, these should succeed ${ENDC} +## ================================================================= +identity_wallet_contract describe_signing_context -f ${_contract_} -s _description_ -p "test1" +if -o ${_error_code_} 0 + echo ${ERROR} [Error ${_error_code_}] failed to fetch signing context test1 ${ENDC} + exit -v ${_error_code_} +fi +echo TEST1 DESCRIPTION : ${_description_} + +identity_wallet_contract get_verifying_key -f ${_contract_} -s _key_ -p "test1" +if -o ${_error_code_} 0 + echo ${ERROR} [Error ${_error_code_}] failed to fetch verifying key test1 ${ENDC} + exit -v ${_error_code_} +fi +echo TEST1 KEY : ${_key_} + +identity_wallet_contract describe_signing_context -f ${_contract_} -s _description_ -p "test1" "test1.1" +if -o ${_error_code_} 0 + echo ${ERROR} [Error ${_error_code_}] failed to fetch signing context test1 test1.1 ${ENDC} + exit -v ${_error_code_} +fi +echo TEST1 TEST1.1 DESCRIPTION : ${_description_} + +identity_wallet_contract get_verifying_key -f ${_contract_} -s _key_ -p "test1" "test1.1" +if -o ${_error_code_} 0 + echo ${ERROR} [Error ${_error_code_}] failed to fetch verifying key test1 test1.1 ${ENDC} + exit -v ${_error_code_} +fi +echo TEST1 TEST1.1 KEY : ${_key_} + +identity_wallet_contract describe_signing_context -f ${_contract_} -s _description_ -p "test3" +if -o ${_error_code_} 0 + echo ${ERROR} [Error ${_error_code_}] failed to fetch extensible signing context test3 ${ENDC} + exit -v ${_error_code_} +fi +echo TEST3 DESCRIPTION : ${_description_} + +identity_wallet_contract get_verifying_key -f ${_contract_} -s _key_ -p "test3" +if -o ${_error_code_} 0 + echo ${ERROR} [Error ${_error_code_}] failed to fetch verifying key test3 ${ENDC} + exit -v ${_error_code_} +fi +echo TEST3 KEY : ${_key_} + +## ================================================================= +echo ${HEADER} test key signing/verification, these should succeed ${ENDC} +## ================================================================= +echo Test simple key verification +identity_wallet_contract sign -f ${_contract_} -s _sig_ -p test1 -m 'this is a test' +if -o ${_error_code_} 0 + echo ${ERROR} [Error ${_error_code_}] failed to sign message using signing context test1 ${ENDC} + exit -v ${_error_code_} +fi + +identity_wallet_contract verify -f ${_contract_} -s _v_ -p test1 -m 'this is a test' --signature ${_sig_} +if -o ${_error_code_} 0 + echo ${ERROR} [Error ${_error_code_}] failed to verify signature ${ENDC} + exit -v ${_error_code_} +fi +echo verified: ${_v_} + +echo Test second level key verification +identity_wallet_contract sign -f ${_contract_} -s _sig_ -p test1 test1.1 -m 'this is a test' +if -o ${_error_code_} 0 + echo ${ERROR} [Error ${_error_code_}] failed to sign message using signing context test1 test1.1 ${ENDC} + exit -v ${_error_code_} +fi + +identity_wallet_contract verify -f ${_contract_} -s _v_ -p test1 test1.1 -m 'this is a test' --signature ${_sig_} +if -o ${_error_code_} 0 + echo ${ERROR} [Error ${_error_code_}] failed to verify signature ${ENDC} + exit -v ${_error_code_} +fi +echo verified: ${_v_} + +echo Test extensible key verification +identity_wallet_contract sign -f ${_contract_} -s _sig_ -p test3 test3.1 test3.1.1 -m 'this is a test' +if -o ${_error_code_} 0 + echo ${ERROR} [Error ${_error_code_}] failed to sign message using extensible context test3 test3.1 test3.1.1. ${ENDC} + exit -v ${_error_code_} +fi + +identity_wallet_contract verify -f ${_contract_} -s _v_ -p test3 test3.1 test3.1.1 -m 'this is a test' --signature ${_sig_} +if -o ${_error_code_} 0 + echo ${ERROR} [Error ${_error_code_}] failed to verify signature ${ENDC} + exit -v ${_error_code_} +fi +echo verified: ${_v_} + +## ================================================================= +echo ${HEADER} test key generate, these should generate errors ${ENDC} +## ================================================================= +echo test registration below extensible path +identity_wallet_contract register_signing_context -w -f ${_contract_} \ + -d "test bad context" --fixed -p "test3" "test3.1" +if -o ${_error_code_} 0 + echo error successfully caught +else + echo ${ERROR} [Error ${_error_code_}] failed to catch invalid signing context registration ${ENDC} + exit -v -1 +fi + +echo test registration below non-existant context +identity_wallet_contract register_signing_context -w -f ${_contract_} \ + -d "test bad context" --fixed -p "test4" "test4.1" +if -o ${_error_code_} 0 + echo error successfully caught +else + echo ${ERROR} [Error ${_error_code_}] failed to catch invalid signing context registration ${ENDC} + exit -v -1 +fi + +## ================================================================= +echo ${HEADER} test invalid signatures, these should generate errors ${ENDC} +## ================================================================= +echo Test signature generation from non-existant signing context +identity_wallet_contract sign -f ${_contract_} -s _sig_ -p test4 -m 'this is a test' +if -o ${_error_code_} 0 + echo error successfully caught +else + echo ${ERROR} [Error ${_error_code_}] failed to catch invalid signing context ${ENDC} + exit -v -1 +fi + +echo Test verification from non-existant signing context +identity_wallet_contract verify -f ${_contract_} -s _v_ -p test4 -m 'this is a test' --signature ${_sig_} +if -o ${_error_code_} 0 + echo error successfully caught +else + echo ${ERROR} [Error ${_error_code_}] failed to catch invalid signing context ${ENDC} + exit -v -1 +fi + +echo Test bad message verfication with valid key +identity_wallet_contract sign -f ${_contract_} -s _sig_ -p test1 -m 'this is a test' +identity_wallet_contract verify -f ${_contract_} -s _v_ -p test1 -m 'this is a not test' --signature ${_sig_} +if -o ${_error_code_} 0 + echo error successfully caught +else + echo ${ERROR} [Error ${_error_code_}] failed to catch invalid message ${ENDC} + exit -v -1 +fi + +echo Test bad signature verfication with valid key +identity_wallet_contract sign -f ${_contract_} -s _sig_ -p test1 -m 'this is a test' +identity_wallet_contract verify -f ${_contract_} -s _v_ -p test2 -m 'this is a test' --signature ${_sig_} +if -o ${_error_code_} 0 + echo error successfully caught +else + echo ${ERROR} [Error ${_error_code_}] failed to catch invalid signature ${ENDC} + exit -v -1 +fi + +exit diff --git a/identity-contract/test/tests/signature_authority.psh b/identity-contract/test/tests/signature_authority.psh new file mode 100755 index 0000000..7b7576d --- /dev/null +++ b/identity-contract/test/tests/signature_authority.psh @@ -0,0 +1,119 @@ +#! /usr/bin/env pdo-shell + +## Copyright 2023 Intel Corporation +## +## Licensed under the Apache License, Version 2.0 (the "License"); +## you may not use this file except in compliance with the License. +## You may obtain a copy of the License at +## +## http://www.apache.org/licenses/LICENSE-2.0 +## +## Unless required by applicable law or agreed to in writing, software +## distributed under the License is distributed on an "AS IS" BASIS, +## WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +## See the License for the specific language governing permissions and +## limitations under the License. + +## Two shell variables are used: +## data -- the directory where the contract objects are stored +## path -- the directory where the PSH scripts are stored +## +## $ pdo-shell -s create.psh -m path + +set --conditional -s home -v . +set --conditional -s data -v . +set --conditional -s save -v . +set --conditional -s _bin_ -v ${home}/contracts/identity/scripts + +script -f ${_bin_}/init.psh + +set -s _contract_ -v ${save}/identity.pdo +set -s _owner_ -v user1 + +trap_error + +## ================================================================= +echo ${HEADER} create a signature authority ${ENDC} +## ================================================================= +set -s _contract_ -v /tmp/signature_authority.pdo +set -s _owner_ -v user1 + +identity -n ${_owner_} + +contract create -c signature_authority --source ${contracts}/_signature_authority -f ${_contract_} +if -o ${_error_code_} 0 + echo ${ERROR} [Error ${_error_code_}] failed to create the signature authority contract ${ENDC} + exit -v ${_error_code_} +fi + +signature_authority_contract initialize -w -f ${_contract_} \ + --description "this is a test identity" +if -o ${_error_code_} 0 + echo ${ERROR} [Error ${_error_code_}] failed to initialize the signature authority contract ${ENDC} + exit -v ${_error_code_} +fi + +signature_authority_contract register_signing_context -w -f ${_contract_} \ + -d "test1 context" --fixed -p "test1" +if -o ${_error_code_} 0 + echo ${ERROR} [Error ${_error_code_}] failed to save signing context test1 ${ENDC} + exit -v ${_error_code_} +fi + +signature_authority_contract register_signing_context -w -f ${_contract_} \ + -d "test1 context" --fixed -p "test1" "test2" +if -o ${_error_code_} 0 + echo ${ERROR} [Error ${_error_code_}] failed to save signing context test1 ${ENDC} + exit -v ${_error_code_} +fi + +## ================================================================= +echo ${HEADER} simple credential test ${ENDC} +## ================================================================= +set -s _credential1_ -f ${_path_}/../credential1.json + +signature_authority_contract sign_credential -f ${_contract_} -c ${_credential1_} -p test1 test2 -s _signed_credential1_ +if -o ${_error_code_} 0 + echo ${ERROR} [Error ${_error_code_}] failed to sign credential ${ENDC} + exit -v ${_error_code_} +fi + +echo ${INFO} Signed Credential: ${ENDC} +echo ${_signed_credential1_} + +signature_authority_contract verify_credential -f ${_contract_} -c ${_signed_credential1_} -s _result_ +if -o ${_error_code_} 0 + echo ${ERROR} [Error ${_error_code_}] failed to verify the credential; ${_error_message_} ${ENDC} + exit -v ${_error_code_} +fi +if --not -e ${_result_} "true" + echo ${ERROR} [Error] credential verification failed ${ENDC} + exit -v 1 +fi + +## ================================================================= +echo ${HEADER} complex credential test ${ENDC} +## ================================================================= +set -s _credential2_ -f ${_path_}/../credential2.json + +signature_authority_contract sign_credential -f ${_contract_} -c ${_credential2_} -p test1 -s _signed_credential2_ +if -o ${_error_code_} 0 + echo ${ERROR} [Error ${_error_code_}] failed to sign credential; ${_error_message_} ${ENDC} + exit -v ${_error_code_} +fi + +echo ${INFO} Signed Credential: ${ENDC} +echo ${_signed_credential2_} + +signature_authority_contract verify_credential -f ${_contract_} -c ${_signed_credential2_} -s _result_ +if -o ${_error_code_} 0 + echo ${ERROR} [Error ${_error_code_}] failed to verify the credential; ${_error_message_} ${ENDC} + exit -v ${_error_code_} +fi +if --not -e ${_result_} "true" + echo ${ERROR} [Error] credential verification failed ${ENDC} + exit -v 1 +fi + + +exit