├── .dockerignore ├── .github ├── dependabot.yml └── workflows │ ├── basic.yml │ ├── dockerbuild.yml │ └── release.yml ├── .gitignore ├── CODEOWNERS ├── Cargo.lock ├── Cargo.toml ├── Dockerfile.as ├── Dockerfile.rvps ├── LICENSE ├── Makefile ├── README.md ├── as-types ├── Cargo.toml └── src │ └── lib.rs ├── attestation-service ├── Cargo.toml ├── build.rs └── src │ ├── cgo │ ├── go.mod │ ├── go.sum │ ├── intoto.go │ └── opa.go │ ├── config.rs │ ├── lib.rs │ ├── policy_engine │ ├── mod.rs │ └── opa │ │ ├── default_policy.rego │ │ └── mod.rs │ ├── rvps │ ├── extractors │ │ ├── extractor_modules │ │ │ ├── in_toto │ │ │ │ ├── README.md │ │ │ │ ├── mod.rs │ │ │ │ └── shim │ │ │ │ │ ├── README.md │ │ │ │ │ └── mod.rs │ │ │ ├── mod.rs │ │ │ └── sample │ │ │ │ ├── README.md │ │ │ │ └── mod.rs │ │ └── mod.rs │ ├── grpc.rs │ ├── mod.rs │ ├── native.rs │ ├── pre_processor │ │ └── mod.rs │ ├── reference_value.rs │ └── store │ │ ├── local_fs │ │ ├── README.md │ │ └── mod.rs │ │ └── mod.rs │ ├── token │ ├── mod.rs │ └── simple.rs │ ├── utils.rs │ └── verifier │ ├── az_snp_vtpm │ ├── milan_ask_ark.pem │ └── mod.rs │ ├── cca │ └── mod.rs │ ├── csv │ ├── hrk.cert │ └── mod.rs │ ├── mod.rs │ ├── sample │ └── mod.rs │ ├── sgx │ ├── mod.rs │ └── types.rs │ ├── snp │ ├── milan_ask_ark.pem │ ├── mod.rs │ ├── test-vcek-invalid-legacy.der │ ├── test-vcek-invalid-new.der │ └── test-vcek.der │ └── tdx │ ├── claims.rs │ ├── eventlog.rs │ ├── mod.rs │ └── quote.rs ├── bin ├── grpc-as │ ├── Cargo.toml │ ├── README.md │ ├── build.rs │ └── src │ │ ├── main.rs │ │ └── server.rs ├── rvps-client │ ├── Cargo.toml │ ├── build.rs │ └── src │ │ └── main.rs └── rvps │ ├── Cargo.toml │ ├── build.rs │ └── src │ ├── main.rs │ └── server.rs ├── docs ├── rvps-grpc.svg ├── rvps-native.svg ├── rvps.md └── rvps.svg ├── protos ├── attestation.proto └── reference.proto ├── test_data ├── CCEL_data ├── az-hcl-data.bin ├── az-vcek.pem ├── az-vtpm-quote-msg.bin ├── az-vtpm-quote-sig.bin ├── cca-claims.json ├── occlum_quote.dat └── tdx_quote_4.dat └── tests └── in-toto ├── alice.pub ├── demo.layout ├── foo.tar.gz ├── package.d3ffd108.link └── write-code.b7d643de.link /.dockerignore: -------------------------------------------------------------------------------- 1 | target 2 | 3 | Dockerfile.as 4 | Dockerfile.rvps -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "cargo" 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | open-pull-requests-limit: 1 8 | allow: 9 | - dependency-type: direct 10 | - dependency-type: indirect 11 | 12 | - package-ecosystem: "gomod" 13 | directory: "/attestation-service/src/cgo" # Location of shim's go.mod 14 | schedule: 15 | interval: "daily" 16 | open-pull-requests-limit: 1 17 | 18 | - package-ecosystem: "github-actions" 19 | directory: "/" 20 | schedule: 21 | interval: "weekly" 22 | 23 | 24 | -------------------------------------------------------------------------------- /.github/workflows/basic.yml: -------------------------------------------------------------------------------- 1 | name: attestation-service basic build and unit tests 2 | on: [push, pull_request, create] 3 | 4 | jobs: 5 | basic_ci: 6 | if: github.event_name == 'pull_request' || github.event_name == 'push' 7 | name: Check 8 | runs-on: ubuntu-latest 9 | strategy: 10 | fail-fast: false 11 | matrix: 12 | rust: 13 | - stable 14 | steps: 15 | - name: Code checkout 16 | uses: actions/checkout@v4 17 | with: 18 | fetch-depth: 1 19 | 20 | - name: Install OPA command line tool 21 | run: | 22 | curl -L -o opa https://openpolicyagent.org/downloads/v0.42.2/opa_linux_amd64_static 23 | chmod 755 ./opa && cp opa /usr/local/bin 24 | 25 | - name: OPA policy.rego fmt and check 26 | run: | 27 | opa fmt -d ./attestation-service/src/policy_engine/opa/default_policy.rego | awk '{ print } END { if (NR!=0) { print "run `opa fmt -w ` to fix this"; exit 1 } }' 28 | opa check ./attestation-service/src/policy_engine/opa/default_policy.rego 29 | 30 | - name: Install protoc 31 | run: | 32 | sudo apt-get update && sudo apt-get install -y protobuf-compiler libprotobuf-dev 33 | 34 | - name: Install TPM build dependencies 35 | run: | 36 | sudo apt-get update 37 | sudo apt-get install -y libtss2-dev 38 | 39 | - name: Install TDX build dependencies 40 | run: | 41 | sudo curl -L https://download.01.org/intel-sgx/sgx_repo/ubuntu/intel-sgx-deb.key | sudo apt-key add - 42 | sudo echo 'deb [arch=amd64] https://download.01.org/intel-sgx/sgx_repo/ubuntu focal main' | sudo tee /etc/apt/sources.list.d/intel-sgx.list 43 | sudo apt-get update 44 | sudo apt-get install -y libsgx-dcap-quote-verify-dev 45 | 46 | - name: Install Rust toolchain (${{ matrix.rust }}) 47 | uses: actions-rs/toolchain@v1 48 | with: 49 | profile: minimal 50 | toolchain: ${{ matrix.rust }} 51 | override: true 52 | components: rustfmt, clippy 53 | 54 | - name: Build 55 | run: | 56 | make 57 | 58 | - name: Run cargo test 59 | uses: actions-rs/cargo@v1 60 | with: 61 | command: test 62 | 63 | - name: Run cargo fmt check 64 | uses: actions-rs/cargo@v1 65 | with: 66 | command: fmt 67 | args: --all -- --check 68 | 69 | - name: Run rust lint check 70 | uses: actions-rs/cargo@v1 71 | with: 72 | command: clippy 73 | # We are getting error in generated code due to derive_partial_eq_without_eq check, so ignore it for now 74 | args: -- -D warnings -A clippy::derive_partial_eq_without_eq 75 | -------------------------------------------------------------------------------- /.github/workflows/dockerbuild.yml: -------------------------------------------------------------------------------- 1 | name: Container image build test 2 | on: [push, pull_request, create] 3 | 4 | jobs: 5 | basic_ci: 6 | if: github.event_name == 'pull_request' || github.event_name == 'push' 7 | name: Check 8 | runs-on: ubuntu-latest 9 | strategy: 10 | fail-fast: false 11 | 12 | steps: 13 | - name: Code checkout 14 | uses: actions/checkout@v4 15 | 16 | - name: Build gRPC AS Container Image 17 | run: | 18 | DOCKER_BUILDKIT=1 docker build -t attestation-service:latest . -f Dockerfile.as 19 | 20 | - name: Build RVPS Container Image 21 | run: | 22 | Docker_BUILDKIT=1 docker build -t rvps:latest . -f Dockerfile.rvps -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Cut Release 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | jobs: 8 | build-and-push-images: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - 12 | name: Checkout 13 | uses: actions/checkout@v4 14 | - 15 | name: Login to Docker Hub 16 | uses: docker/login-action@v2 17 | with: 18 | registry: ghcr.io 19 | username: ${{github.actor}} 20 | password: ${{secrets.GITHUB_TOKEN}} 21 | - 22 | name: Build and push attestation-service 23 | uses: docker/build-push-action@v4 24 | with: 25 | context: . 26 | file: ./Dockerfile.as 27 | platforms: linux/amd64 28 | push: true 29 | tags: ghcr.io/confidential-containers/attestation-service:latest, ghcr.io/confidential-containers/attestation-service:${{ github.ref_name }} 30 | - 31 | name: Build and push reference-value-provider-service 32 | uses: docker/build-push-action@v4 33 | with: 34 | context: . 35 | file: ./Dockerfile.rvps 36 | platforms: linux/amd64 37 | push: true 38 | tags: ghcr.io/confidential-containers/reference-value-provider-service:latest, ghcr.io/confidential-containers/reference-value-provider-service:${{ github.ref_name }} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # build cache 2 | target 3 | 4 | # Local Fs tempfile 5 | reference_values 6 | 7 | test_data/*_output.txt 8 | test_data/opa/ 9 | -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | @attestation-service-maintainers 2 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [ 3 | "as-types", 4 | "attestation-service", 5 | "bin/rvps", 6 | "bin/grpc-as", 7 | "bin/rvps-client", 8 | ] 9 | 10 | resolver = "2" 11 | 12 | [workspace.package] 13 | version = "0.1.0" 14 | authors = ["The Confidential Container Authors"] 15 | description = "Attestation Service" 16 | documentation = "https://github.com/confidential-containers/attestation-service" 17 | edition = "2021" 18 | 19 | [workspace.dependencies] 20 | anyhow = "1.0" 21 | assert-json-diff = "2.0.2" 22 | async-trait = "0.1.31" 23 | clap = { version = "3.2.6", features = ["derive"] } 24 | env_logger = "0.9.1" 25 | log = "0.4.17" 26 | prost = "0.11.0" 27 | rstest = "0.17.0" 28 | serde = { version = "1.0", features = ["derive"] } 29 | serde_json = "*" 30 | serial_test = "0.9.0" 31 | sha2 = "0.10" 32 | shadow-rs = "0.19.0" 33 | tokio = { version = "1.0", features = ["rt-multi-thread", "macros", "fs"] } 34 | tonic = "0.8.1" 35 | tonic-build = "0.8.0" 36 | -------------------------------------------------------------------------------- /Dockerfile.as: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2023 by Alibaba. 2 | # Licensed under the Apache License, Version 2.0, see LICENSE for details. 3 | # SPDX-License-Identifier: Apache-2.0 4 | 5 | FROM rust:latest as builder 6 | 7 | WORKDIR /usr/src/attestation-service 8 | COPY . . 9 | 10 | # Install golang 11 | RUN wget https://go.dev/dl/go1.20.1.linux-amd64.tar.gz && \ 12 | tar -C /usr/local -xzf go1.20.1.linux-amd64.tar.gz 13 | 14 | ENV PATH="/usr/local/go/bin:${PATH}" 15 | 16 | # Install TPM Build Dependencies 17 | RUN apt-get update && apt-get install -y protobuf-compiler clang libtss2-dev 18 | 19 | # Install TDX Build Dependencies 20 | RUN curl -L https://download.01.org/intel-sgx/sgx_repo/ubuntu/intel-sgx-deb.key | tee intel-sgx-deb.key | apt-key add - && \ 21 | echo 'deb [arch=amd64] https://download.01.org/intel-sgx/sgx_repo/ubuntu focal main' | tee /etc/apt/sources.list.d/intel-sgx.list && \ 22 | apt-get update && apt-get install -y libtdx-attest-dev libsgx-dcap-quote-verify-dev 23 | 24 | # Build and Install gRPC attestation-service 25 | RUN cargo install --path bin/grpc-as 26 | 27 | 28 | FROM ubuntu:22.04 29 | 30 | LABEL org.opencontainers.image.source="https://github.com/confidential-containers/attestation-service" 31 | 32 | # Install TDX Runtime Dependencies 33 | RUN apt-get update && apt-get install curl gnupg openssl -y && \ 34 | rm -rf /var/lib/apt/lists/{apt,dpkg,cache,log} /tmp/* /var/tmp/* 35 | 36 | RUN curl -L https://download.01.org/intel-sgx/sgx_repo/ubuntu/intel-sgx-deb.key | tee intel-sgx-deb.key | apt-key add - && \ 37 | echo 'deb [arch=amd64] https://download.01.org/intel-sgx/sgx_repo/ubuntu focal main' | tee /etc/apt/sources.list.d/intel-sgx.list && \ 38 | apt-get update && \ 39 | apt-get install -y libsgx-dcap-default-qpl libsgx-dcap-quote-verify && \ 40 | rm -rf /var/lib/apt/lists/{apt,dpkg,cache,log} /tmp/* /var/tmp/* 41 | 42 | # Copy TPM Runtime Dependencies 43 | COPY --from=builder /usr/lib/x86_64-linux-gnu/libtss* /usr/lib/x86_64-linux-gnu 44 | 45 | COPY --from=builder /usr/local/cargo/bin/grpc-as /usr/local/bin/grpc-as 46 | 47 | VOLUME /opt/confidential-containers/attestation-service 48 | 49 | CMD ["grpc-as", "--socket", "0.0.0.0:50004"] 50 | 51 | EXPOSE 50004 -------------------------------------------------------------------------------- /Dockerfile.rvps: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2023 by Alibaba. 2 | # Licensed under the Apache License, Version 2.0, see LICENSE for details. 3 | # SPDX-License-Identifier: Apache-2.0 4 | 5 | FROM rust:latest as builder 6 | 7 | WORKDIR /usr/src/rvps 8 | 9 | RUN wget https://go.dev/dl/go1.20.2.linux-amd64.tar.gz && \ 10 | tar -C /usr/local -xzf go1.20.2.linux-amd64.tar.gz 11 | 12 | ENV PATH="/usr/local/go/bin:${PATH}" 13 | 14 | COPY . . 15 | 16 | RUN apt-get update && apt-get install protobuf-compiler -y 17 | 18 | RUN cargo install --path bin/rvps 19 | 20 | FROM debian:bookworm-slim 21 | 22 | LABEL org.opencontainers.image.source="https://github.com/confidential-containers/attestation-service" 23 | 24 | COPY --from=builder /usr/local/cargo/bin/rvps /usr/local/bin/rvps 25 | 26 | CMD ["rvps", "--socket", "0.0.0.0:50003"] 27 | 28 | VOLUME /opt/confidential-containers/attestation-service/reference_values/ 29 | 30 | EXPOSE 50003 -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | PROJDIR := $(shell readlink -f ..) 2 | TOP_DIR := . 3 | CUR_DIR := $(shell pwd) 4 | PREFIX := /usr/local 5 | TARGET_DIR := target 6 | BIN_NAMES := grpc-as 7 | 8 | DEBUG ?= 9 | DESTDIR ?= $(PREFIX)/bin 10 | 11 | ifdef DEBUG 12 | release := 13 | TARGET_DIR := $(TARGET_DIR)/debug 14 | else 15 | release := --release 16 | TARGET_DIR := $(TARGET_DIR)/release 17 | endif 18 | 19 | build: grpc-as 20 | 21 | grpc-as: 22 | cargo build --bin grpc-as $(release) 23 | 24 | install: 25 | for bin_name in $(BIN_NAMES); do \ 26 | install -D -m0755 $(TARGET_DIR)/$$bin_name $(DESTDIR); \ 27 | done 28 | 29 | clean: 30 | cargo clean 31 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Attestation Service 2 | 3 | **This project has been consolidated with the Key Broker Service and now lives [here](https://github.com/confidential-containers/kbs/tree/main/attestation-service).** 4 | 5 | Attestation Service (AS for short) is a general function set that can verify TEE evidence. 6 | With Confidential Containers, the attestation service must run in an secure environment, outside of the guest node. 7 | 8 | With remote attestation, Attesters (e.g. the [Attestation Agent](https://github.com/confidential-containers/attestation-agent)) running on the guest node will request a resource (e.g. a container image decryption key) from the [Key Broker Service (KBS)](https://github.com/confidential-containers/kbs)). 9 | The KBS receives the attestation evidence from the client in TEE and forwards it to the Attestation Service (AS). The AS role is to verify the attestation evidence and provide Attestation Results token back to the KBS. Verifying the evidence is a two steps process: 10 | 11 | 1. Verify the evidence signature, and assess that it's been signed with a trusted key of TEE. 12 | 2. Verify that the TCB described by that evidence (including hardware status and software measurements) meets the guest owner expectations. 13 | 14 | Those two steps are accomplished by respectively one of the [Verifier Drivers](#verifier-drivers) and the AS [Policy Engine](#policy-engine). The policy engine can be customized with different policy configurations. 15 | 16 | The AS can be built as a library (i.e. a Rust crate) by other confidential computing resources providers, like for example the KBS. 17 | It can also run as a standalone binary, in which case its API is exposed through a set of [gRPC](https://grpc.io/) endpoints. 18 | 19 | # Components 20 | 21 | ## Library 22 | 23 | The AS can be built and imported as a Rust crate into any project providing attestation services. 24 | 25 | As the AS API is not yet fully stable, the AS crate needs to be imported from GitHub directly: 26 | 27 | ```toml 28 | attestation-service = { git = "https://github.com/confidential-containers/attestation-service", branch = "main" } 29 | ``` 30 | 31 | ## Server 32 | 33 | This project provides the Attestation Service binary program that can be run as an independent server: 34 | 35 | - [`grpc-as`](bin/grpc-as/): Provide AS APIs based on gRPC protocol. 36 | 37 | # Usage 38 | 39 | Build and install AS components: 40 | 41 | ```shell 42 | git clone https://github.com/confidential-containers/attestation-service 43 | cd attestation-service 44 | make && make install 45 | ``` 46 | 47 | `grpc-as` will be installed into `/usr/local/bin`. 48 | 49 | # Architecture 50 | 51 | The main architecture of the Attestation Service is shown in the figure below: 52 | ``` 53 | ┌───────────────────────┐ 54 | ┌───────────────────────┐ Evidence │ Attestation Service │ 55 | │ ├────────────►│ │ 56 | │ Verification Demander │ │ ┌────────┐ ┌──────────┴───────┐ 57 | │ (Such as KBS) │ │ │ Policy │ │ Reference Value │◄───Reference Value 58 | │ │◄────────────┤ │ Engine │ │ Provider Service │ 59 | └───────────────────────┘ Attestation │ └────────┘ └──────────┬───────┘ 60 | Results Token │ │ 61 | │ ┌───────────────────┐ │ 62 | │ │ Verifier Drivers │ │ 63 | │ └───────────────────┘ │ 64 | │ │ 65 | └───────────────────────┘ 66 | ``` 67 | 68 | The Reference Value Provider Service supports different deploy mode, 69 | please refer to [the doc](./docs/rvps.md#run-mode) for more details. 70 | 71 | ### Evidence format: 72 | 73 | The attestation evidence is included in a [KBS defined Attestation payload](https://github.com/confidential-containers/kbs/blob/main/docs/kbs_attestation_protocol.md#attestation): 74 | 75 | ```json 76 | { 77 | "tee-pubkey": $pubkey, 78 | "tee-evidence": {} 79 | } 80 | ``` 81 | 82 | - `tee-pubkey`: A JWK-formatted public key, generated by the client running in the HW-TEE. 83 | For more details on the `tee-pubkey` format, see the [KBS protocol](https://github.com/confidential-containers/kbs/blob/main/docs/kbs_attestation_protocol.md#key-format). 84 | 85 | - `tee-evidence`: The attestation evidence generated by the HW-TEE platform software and hardware in the AA's execution environment. 86 | The tee-evidence formats depend on the TEE and are typically defined by the each TEE verifier driver of AS. 87 | 88 | **Note**: Verification Demander needs to specify the TEE type and pass `nonce` to Attestation-Service together with Evidence, 89 | Hash of `nonce` and `tee-pubkey` should be embedded in report/quote in `tee-evidence`, so they can be signed by HW-TEE. 90 | This mechanism ensures the freshness of Evidence and the authenticity of `tee-pubkey`. 91 | 92 | ### Attestation Results Token: 93 | 94 | If the verification of TEE evidence is successful, AS will return an Attestation Results Token. 95 | Otherwise, AS will return an Error which contain verifier output or policy engine output. 96 | 97 | Attestation results token is a [JSON Web Token](https://datatracker.ietf.org/doc/html/rfc7519) which contains the parsed evidence claims such as TCB status. 98 | 99 | Claims format of the attestation results token is: 100 | 101 | ```json 102 | { 103 | "iss": $issuer_name, 104 | "jwk": $public_key, 105 | "exp": $expire_timestamp, 106 | "nbf": $notbefore_timestamp, 107 | "tee-pubkey": $pubkey, 108 | "tcb-status": $parsed_evidence, 109 | "evaluation-report": $report 110 | } 111 | ``` 112 | 113 | * `iss`: Token issuer name, default is `CoCo-Attestation-Service`. 114 | * `jwk`: Public key to verify token signature. Must be in format of [JSON Web Key](https://datatracker.ietf.org/doc/html/rfc7517). 115 | * `exp`: Token expire time in Unix timestamp format. 116 | * `nbf`: Token effective time in Unix timestamp format. 117 | * `tee-pubkey`: A JWK-formatted public key, generated by the client running in the HW-TEE. 118 | For more details on the `tee-pubkey` format, see the [KBS protocol](https://github.com/confidential-containers/kbs/blob/main/docs/kbs_attestation_protocol.md#key-format). 119 | * `tcb_status`: Contains HW-TEE informations and software measurements of AA's execution environment. 120 | * `evaluation-report` : The output of the policy engine, it is AS policy's evaluation opinion on TEE evidence. 121 | 122 | ## Verifier Drivers 123 | 124 | A verifier driver parse the HW-TEE specific `tee-evidence` data from the received attestation evidence, and performs the following tasks: 125 | 126 | 1. Verify HW-TEE hardware signature of the TEE quote/report in `tee-evidence`. 127 | 128 | 2. Resolve `tee-evidence`, and organize the TCB status into JSON claims to return. 129 | 130 | Supported Verifier Drivers: 131 | 132 | - `sample`: A dummy TEE verifier driver which is used to test/demo the AS's functionalities. 133 | - `tdx`: Verifier Driver for Intel Trust Domain Extention (Intel TDX). 134 | - `amd-sev-snp`: TODO. 135 | 136 | ## Policy Engine 137 | 138 | The AS supports modular policy engine, which can be specified through the AS configuration. The currently supported policy engines are: 139 | 140 | ### [Open Policy Agent (OPA)](https://www.openpolicyagent.org/docs/latest/) 141 | 142 | OPA is a very flexible and powerful policy engine, AS allows users to define and upload their own policy when performing evidence verification. 143 | 144 | **Note**: Please refer to the [Policy Language](https://www.openpolicyagent.org/docs/latest/policy-language/) documentation for more information about the `.rego`. 145 | 146 | If the user does not need to customize his own policy, AS will use the [default policy](src/policy_engine/opa/default_policy.rego). 147 | 148 | ## Reference Value Provider 149 | 150 | [Reference Value Provider Service](docs/rvps.md) (RVPS for short) is a module integrated in the AS to verify, 151 | store and provide reference values. RVPS receives and verifies the provenance input from the software supply chain, 152 | stores the measurement values, and generates reference value claims for the AS according to the evidence content when the AS verifies the evidence. 153 | -------------------------------------------------------------------------------- /as-types/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "as-types" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | # TODO: change it to "0.5", once released. 8 | kbs-types = { git = "https://github.com/virtee/kbs-types", rev = "c90df0e" } 9 | serde.workspace = true 10 | serde_json.workspace = true 11 | -------------------------------------------------------------------------------- /as-types/src/lib.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | pub type TeeEvidenceParsedClaim = serde_json::Value; 4 | 5 | #[derive(Serialize, Deserialize, Debug, Clone)] 6 | pub struct SetPolicyInput { 7 | pub r#type: String, 8 | pub policy_id: String, 9 | pub policy: String, 10 | } 11 | -------------------------------------------------------------------------------- /attestation-service/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "attestation-service" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [features] 7 | default = [ "rvps-native", "all-verifier" ] 8 | all-verifier = [ "tdx-verifier", "sgx-verifier", "snp-verifier", "az-snp-vtpm-verifier", "csv-verifier", "cca-verifier" ] 9 | tdx-verifier = [ "eventlog-rs", "scroll", "sgx-dcap-quoteverify-rs" ] 10 | sgx-verifier = [ "scroll", "sgx-dcap-quoteverify-rs" ] 11 | az-snp-vtpm-verifier = [ "az-snp-vtpm", "sev" ] 12 | snp-verifier = [ "asn1-rs", "openssl", "sev", "x509-parser" ] 13 | csv-verifier = [ "openssl", "csv-rs", "codicon" ] 14 | cca-verifier = [ "cbor-diag", "veraison-apiclient" ] 15 | 16 | rvps-native = [] 17 | rvps-grpc = [ "tonic" ] 18 | 19 | [dependencies] 20 | anyhow.workspace = true 21 | asn1-rs = { version = "0.5.1", optional = true } 22 | async-trait.workspace = true 23 | as-types = { path = "../as-types" } 24 | az-snp-vtpm = { version = "0.3.0", default-features = false, features = ["verifier"], optional = true } 25 | base64 = "0.21" 26 | bincode = "1.3.3" 27 | byteorder = "1" 28 | cbor-diag = { version = "0.1.11", optional = true } 29 | cfg-if = "1.0.0" 30 | chrono = { version = "0.4.19", features = [ "serde" ] } 31 | codicon = { version = "3.0", optional = true } 32 | # TODO: change it to "0.1", once released. 33 | csv-rs = { git = "https://gitee.com/anolis/csv-rs", rev = "9d8882e", optional = true } 34 | eventlog-rs = { version = "0.1.3", optional = true } 35 | futures = "0.3.17" 36 | hex = "0.4.3" 37 | jsonwebtoken = "8" 38 | jwt = { version = "0.16.0", features = ["openssl"]} 39 | # TODO: change it to "0.5", once released. 40 | kbs-types = { git = "https://github.com/virtee/kbs-types", rev = "c90df0e" } 41 | lazy_static = "1.4.0" 42 | log.workspace = true 43 | openssl = { version = "0.10.55", optional = true } 44 | path-clean = "1.0.1" 45 | prost.workspace = true 46 | rand = "0.8.5" 47 | rsa = { version = "0.9.2", features = ["sha2"] } 48 | scroll = { version = "0.11.0", default-features = false, features = ["derive"], optional = true } 49 | serde.workspace = true 50 | serde_json.workspace = true 51 | serde_variant = "0.1.2" 52 | sev = { version = "1.2.0", features = ["openssl", "snp"], optional = true } 53 | sgx-dcap-quoteverify-rs = { git = "https://github.com/intel/SGXDataCenterAttestationPrimitives", tag = "DCAP_1.16", optional = true } 54 | sha2.workspace = true 55 | shadow-rs.workspace = true 56 | sled = "0.34.7" 57 | strum = "0.24.0" 58 | strum_macros = "0.24.0" 59 | tempfile = "3.3.0" 60 | time = { version = "0.3.23", features = ["std"] } 61 | tokio.workspace = true 62 | tonic = { workspace = true, optional = true } 63 | uuid = { version = "1.1.2", features = ["v4"] } 64 | veraison-apiclient = { git = "https://github.com/chendave/rust-apiclient", branch = "token", optional = true } 65 | ear = { git = "https://github.com/veraison/rust-ear", rev = "cc6ea53" } 66 | x509-parser = { version = "0.14.0", optional = true } 67 | 68 | [build-dependencies] 69 | shadow-rs.workspace = true 70 | tonic-build.workspace = true 71 | 72 | [dev-dependencies] 73 | assert-json-diff.workspace = true 74 | rstest.workspace = true 75 | serial_test.workspace = true 76 | sha2.workspace = true 77 | testing_logger = "0.1.1" 78 | walkdir = "2.3.2" 79 | -------------------------------------------------------------------------------- /attestation-service/build.rs: -------------------------------------------------------------------------------- 1 | use std::process::{exit, Command}; 2 | 3 | fn real_main() -> Result<(), String> { 4 | let out_dir = std::env::var("OUT_DIR").unwrap(); 5 | println!("cargo:rerun-if-changed={out_dir}"); 6 | println!("cargo:rustc-link-search=native={out_dir}"); 7 | println!("cargo:rustc-link-lib=static=cgo"); 8 | let cgo_dir = "./src/cgo".to_string(); 9 | let cgo = Command::new("go") 10 | .args([ 11 | "build", 12 | "-o", 13 | &format!("{out_dir}/libcgo.a"), 14 | "-buildmode=c-archive", 15 | "opa.go", 16 | "intoto.go", 17 | ]) 18 | .current_dir(cgo_dir) 19 | .output() 20 | .expect("failed to launch opa compile process"); 21 | if !cgo.status.success() { 22 | return Err(std::str::from_utf8(&cgo.stderr.to_vec()) 23 | .unwrap() 24 | .to_string()); 25 | } 26 | 27 | tonic_build::compile_protos("../protos/attestation.proto").map_err(|e| format!("{e}"))?; 28 | 29 | tonic_build::compile_protos("../protos/reference.proto").map_err(|e| format!("{e}"))?; 30 | 31 | Ok(()) 32 | } 33 | 34 | fn main() -> shadow_rs::SdResult<()> { 35 | if let Err(e) = real_main() { 36 | eprintln!("ERROR: {e}"); 37 | exit(1); 38 | } 39 | 40 | shadow_rs::new() 41 | } 42 | -------------------------------------------------------------------------------- /attestation-service/src/cgo/go.mod: -------------------------------------------------------------------------------- 1 | module cgo 2 | 3 | go 1.20 4 | 5 | require ( 6 | github.com/in-toto/in-toto-golang v0.9.0 7 | github.com/open-policy-agent/opa v0.56.0 8 | ) 9 | 10 | require ( 11 | github.com/OneOfOne/xxhash v1.2.8 // indirect 12 | github.com/agnivade/levenshtein v1.1.1 // indirect 13 | github.com/beorn7/perks v1.0.1 // indirect 14 | github.com/cespare/xxhash/v2 v2.2.0 // indirect 15 | github.com/go-ini/ini v1.67.0 // indirect 16 | github.com/go-logr/logr v1.2.4 // indirect 17 | github.com/go-logr/stdr v1.2.2 // indirect 18 | github.com/gobwas/glob v0.2.3 // indirect 19 | github.com/golang/protobuf v1.5.3 // indirect 20 | github.com/gorilla/mux v1.8.0 // indirect 21 | github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect 22 | github.com/prometheus/client_golang v1.16.0 // indirect 23 | github.com/prometheus/client_model v0.3.0 // indirect 24 | github.com/prometheus/common v0.42.0 // indirect 25 | github.com/prometheus/procfs v0.10.1 // indirect 26 | github.com/rcrowley/go-metrics v0.0.0-20200313005456-10cdbea86bc0 // indirect 27 | github.com/secure-systems-lab/go-securesystemslib v0.6.0 // indirect 28 | github.com/shibumi/go-pathspec v1.3.0 // indirect 29 | github.com/sirupsen/logrus v1.9.3 // indirect 30 | github.com/tchap/go-patricia/v2 v2.3.1 // indirect 31 | github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect 32 | github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect 33 | github.com/yashtewari/glob-intersection v0.2.0 // indirect 34 | go.opentelemetry.io/otel v1.16.0 // indirect 35 | go.opentelemetry.io/otel/metric v1.16.0 // indirect 36 | go.opentelemetry.io/otel/sdk v1.16.0 // indirect 37 | go.opentelemetry.io/otel/trace v1.16.0 // indirect 38 | golang.org/x/crypto v0.12.0 // indirect 39 | golang.org/x/sys v0.11.0 // indirect 40 | google.golang.org/protobuf v1.30.0 // indirect 41 | gopkg.in/yaml.v2 v2.4.0 // indirect 42 | sigs.k8s.io/yaml v1.3.0 // indirect 43 | ) 44 | -------------------------------------------------------------------------------- /attestation-service/src/cgo/intoto.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "C" 4 | 5 | import ( 6 | "encoding/json" 7 | "fmt" 8 | "io" 9 | "os" 10 | 11 | intoto "github.com/in-toto/in-toto-golang/in_toto" 12 | ) 13 | 14 | //export verifyGo 15 | func verifyGo( 16 | layoutPath string, 17 | pubKeyPaths []string, 18 | intermediatePaths []string, 19 | linkDir string, 20 | lineNormalizationInt int) *C.char { 21 | layoutMb, err := intoto.LoadMetadata(layoutPath) 22 | if err != nil { 23 | e := fmt.Errorf("failed to load layout at %s: %w", layoutPath, err) 24 | return C.CString("Error:: " + e.Error()) 25 | } 26 | 27 | pubKeyCount := len(pubKeyPaths) 28 | layoutKeys := make(map[string]intoto.Key, pubKeyCount) 29 | 30 | for _, pubKeyPath := range pubKeyPaths { 31 | var pubKey intoto.Key 32 | if err := pubKey.LoadKeyDefaults(pubKeyPath); err != nil { 33 | e := fmt.Errorf("invalid key at %s: %w", pubKeyPath, err) 34 | return C.CString("Error:: " + e.Error()) 35 | } 36 | 37 | layoutKeys[pubKey.KeyID] = pubKey 38 | } 39 | 40 | intermediatePathCount := len(intermediatePaths) 41 | intermediatePems := make([][]byte, 0, int(intermediatePathCount)) 42 | 43 | for _, intermediate := range intermediatePaths { 44 | f, err := os.Open(intermediate) 45 | if err != nil { 46 | e := fmt.Errorf("failed to open intermediate %s: %w", intermediate, err) 47 | return C.CString("Error:: " + e.Error()) 48 | } 49 | defer f.Close() 50 | 51 | pemBytes, err := io.ReadAll(f) 52 | if err != nil { 53 | e := fmt.Errorf("failed to read intermediate %s: %w", intermediate, err) 54 | return C.CString("Error:: " + e.Error()) 55 | } 56 | 57 | intermediatePems = append(intermediatePems, pemBytes) 58 | 59 | if err := f.Close(); err != nil { 60 | e := fmt.Errorf("could not close intermediate cert: %w", err) 61 | return C.CString("Error:: " + e.Error()) 62 | } 63 | } 64 | 65 | var lineNormalization bool 66 | if lineNormalizationInt == 0 { 67 | lineNormalization = false 68 | } else { 69 | lineNormalization = true 70 | } 71 | 72 | summaryLink, err := intoto.InTotoVerify(layoutMb, layoutKeys, linkDir, "", make(map[string]string), intermediatePems, lineNormalization) 73 | if err != nil { 74 | e := fmt.Errorf("inspection failed: %w", err) 75 | return C.CString("Error:: " + e.Error()) 76 | } 77 | 78 | jsonBytes, err := json.Marshal(summaryLink) 79 | if err != nil { 80 | e := fmt.Errorf("json failed: %w", err) 81 | return C.CString("Error:: " + e.Error()) 82 | } 83 | 84 | return C.CString(string(jsonBytes)) 85 | } 86 | -------------------------------------------------------------------------------- /attestation-service/src/cgo/opa.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "C" 4 | 5 | import ( 6 | "context" 7 | "encoding/json" 8 | 9 | "github.com/open-policy-agent/opa/rego" 10 | "github.com/open-policy-agent/opa/storage/inmem" 11 | ) 12 | 13 | //export evaluateGo 14 | func evaluateGo(policy string, data string, input string) *C.char { 15 | // Deserialize the message in json format 16 | input_map := make(map[string]interface{}) 17 | err := json.Unmarshal([]byte(input), &input_map) 18 | if err != nil { 19 | return C.CString("Error:: " + err.Error()) 20 | } 21 | 22 | data_map := make(map[string]interface{}) 23 | err2 := json.Unmarshal([]byte(data), &data_map) 24 | if err2 != nil { 25 | return C.CString("Error:: " + err.Error()) 26 | } 27 | // Manually create the storage layer. inmem.NewFromObject returns an 28 | // in-memory store containing the supplied data. 29 | store := inmem.NewFromObject(data_map) 30 | 31 | // Construct a Rego object that can be prepared or evaluated. 32 | r := rego.New( 33 | rego.Query("input;data.policy"), 34 | rego.Module("policy.rego", policy), 35 | rego.Store(store), 36 | ) 37 | 38 | // Create a prepared query that can be evaluated. 39 | ctx := context.Background() 40 | query, err := r.PrepareForEval(ctx) 41 | if err != nil { 42 | return C.CString("Error:: " + err.Error()) 43 | } 44 | 45 | // Make opa query 46 | rs, err := query.Eval(ctx, rego.EvalInput(input_map)) 47 | if err != nil { 48 | return C.CString("Error:: " + err.Error()) 49 | } 50 | 51 | dataOPA, ok := rs[0].Expressions[1].Value.(map[string]interface{}) 52 | if !ok { 53 | return C.CString("Error:: unexpected type in second expression") 54 | } 55 | 56 | decisionMap := make(map[string]interface{}) 57 | // OPA's evaluation results. 58 | for k, v := range dataOPA { 59 | decisionMap[k] = v 60 | } 61 | 62 | decision, err := json.Marshal(decisionMap) 63 | if err != nil { 64 | return C.CString("Error:: " + err.Error()) 65 | } 66 | 67 | return C.CString(string(decision)) 68 | } 69 | 70 | func main() {} 71 | -------------------------------------------------------------------------------- /attestation-service/src/config.rs: -------------------------------------------------------------------------------- 1 | use crate::token::{AttestationTokenBrokerType, AttestationTokenConfig}; 2 | 3 | use anyhow::{anyhow, Result}; 4 | use serde::Deserialize; 5 | use std::convert::TryFrom; 6 | use std::fs::File; 7 | use std::path::{Path, PathBuf}; 8 | 9 | use crate::rvps::store::StoreType; 10 | 11 | /// Environment macro for Attestation Service work dir. 12 | const AS_WORK_DIR: &str = "AS_WORK_DIR"; 13 | const DEFAULT_WORK_DIR: &str = "/opt/confidential-containers/attestation-service"; 14 | 15 | #[derive(Clone, Debug, Deserialize)] 16 | pub struct Config { 17 | /// The location for Attestation Service to store data. 18 | pub work_dir: PathBuf, 19 | 20 | /// Policy Engine type. 21 | pub policy_engine: String, 22 | 23 | pub rvps_store_type: StoreType, 24 | 25 | /// The Attestation Result Token Broker type. 26 | /// 27 | /// Possible values: 28 | /// * `Simple` 29 | pub attestation_token_broker: AttestationTokenBrokerType, 30 | 31 | /// The Attestation Result Token Broker Config 32 | pub attestation_token_config: AttestationTokenConfig, 33 | } 34 | 35 | impl Default for Config { 36 | // Construct a default instance of `Config` 37 | fn default() -> Config { 38 | let work_dir = PathBuf::from( 39 | std::env::var(AS_WORK_DIR).unwrap_or_else(|_| DEFAULT_WORK_DIR.to_string()), 40 | ); 41 | 42 | Config { 43 | work_dir, 44 | policy_engine: "opa".to_string(), 45 | rvps_store_type: StoreType::LocalFs, 46 | attestation_token_broker: AttestationTokenBrokerType::Simple, 47 | attestation_token_config: AttestationTokenConfig::default(), 48 | } 49 | } 50 | } 51 | 52 | impl TryFrom<&Path> for Config { 53 | /// Load `Config` from a configuration file like: 54 | /// { 55 | /// "work_dir": "/var/lib/attestation-service/", 56 | /// "policy_engine": "opa", 57 | /// "rvps_store_type": "LocalFs", 58 | /// "attestation_token_broker": "Simple", 59 | /// "attestation_token_config": { 60 | /// "duration_min": 5 61 | /// } 62 | /// } 63 | type Error = anyhow::Error; 64 | fn try_from(config_path: &Path) -> Result { 65 | let file = File::open(config_path) 66 | .map_err(|e| anyhow!("failed to open AS config file {}", e.to_string()))?; 67 | 68 | serde_json::from_reader::(file) 69 | .map_err(|e| anyhow!("failed to parse AS config file {}", e.to_string())) 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /attestation-service/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Attestation Service 2 | //! 3 | //! # Features 4 | //! - `rvps-grpc`: The AS will connect a remote RVPS. 5 | //! - `rvps-native`: The AS will integrate RVPS functionalities itself. 6 | 7 | extern crate serde; 8 | 9 | #[macro_use] 10 | extern crate log; 11 | 12 | extern crate strum; 13 | 14 | #[macro_use] 15 | extern crate strum_macros; 16 | 17 | pub mod config; 18 | pub mod policy_engine; 19 | pub mod rvps; 20 | mod token; 21 | mod utils; 22 | pub mod verifier; 23 | 24 | use crate::token::AttestationTokenBroker; 25 | 26 | use anyhow::{anyhow, Context, Result}; 27 | use as_types::SetPolicyInput; 28 | use config::Config; 29 | pub use kbs_types::{Attestation, Tee}; 30 | use policy_engine::PolicyEngine; 31 | use rvps::{Message, RVPSAPI}; 32 | use serde_json::json; 33 | use std::collections::HashMap; 34 | 35 | #[cfg(any(feature = "rvps-grpc", feature = "rvps-native"))] 36 | use std::{fs, str::FromStr}; 37 | 38 | #[cfg(any(feature = "rvps-grpc", feature = "rvps-native"))] 39 | use policy_engine::PolicyEngineType; 40 | 41 | use crate::utils::flatten_claims; 42 | 43 | pub struct AttestationService { 44 | _config: Config, 45 | policy_engine: Box, 46 | rvps: Box, 47 | token_broker: Box, 48 | } 49 | 50 | impl AttestationService { 51 | /// Create a new Attestation Service instance. 52 | #[cfg(feature = "rvps-native")] 53 | pub fn new(config: Config) -> Result { 54 | if !config.work_dir.as_path().exists() { 55 | fs::create_dir_all(&config.work_dir) 56 | .map_err(|e| anyhow!("Create AS work dir failed: {:?}", e))?; 57 | } 58 | 59 | let policy_engine = PolicyEngineType::from_str(&config.policy_engine) 60 | .map_err(|_| anyhow!("Policy Engine {} is not supported", &config.policy_engine))? 61 | .to_policy_engine(config.work_dir.as_path())?; 62 | 63 | let rvps_store = config.rvps_store_type.to_store()?; 64 | let rvps = Box::new(rvps::Core::new(rvps_store)); 65 | 66 | let token_broker = config 67 | .attestation_token_broker 68 | .to_token_broker(config.attestation_token_config.clone())?; 69 | 70 | Ok(Self { 71 | _config: config, 72 | policy_engine, 73 | rvps, 74 | token_broker, 75 | }) 76 | } 77 | 78 | /// Create a new Attestation Service, and connect to a remote rvps. 79 | #[cfg(feature = "rvps-grpc")] 80 | pub async fn new_with_rvps_grpc(rvps_addr: &str, config: Config) -> Result { 81 | if !config.work_dir.as_path().exists() { 82 | fs::create_dir_all(&config.work_dir) 83 | .map_err(|e| anyhow!("Create AS work dir failed: {:?}", e))?; 84 | } 85 | 86 | let policy_engine = PolicyEngineType::from_str(&config.policy_engine) 87 | .map_err(|_| anyhow!("Policy Engine {} is not supported", &config.policy_engine))? 88 | .to_policy_engine(config.work_dir.as_path())?; 89 | 90 | let rvps = Box::new(rvps::Agent::new(rvps_addr).await?); 91 | 92 | let token_broker = config 93 | .attestation_token_broker 94 | .to_token_broker(config.attestation_token_config.clone())?; 95 | 96 | Ok(Self { 97 | _config: config, 98 | policy_engine, 99 | rvps, 100 | token_broker, 101 | }) 102 | } 103 | 104 | /// Set Attestation Verification Policy. 105 | pub async fn set_policy(&mut self, input: SetPolicyInput) -> Result<()> { 106 | self.policy_engine 107 | .set_policy(input) 108 | .await 109 | .map_err(|e| anyhow!("Cannot Set Policy: {:?}", e)) 110 | } 111 | 112 | /// Evaluate Attestation Evidence. 113 | /// Issue an attestation results token which contain TCB status and TEE public key. 114 | pub async fn evaluate(&self, tee: Tee, nonce: &str, attestation: &str) -> Result { 115 | let attestation = serde_json::from_str::(attestation) 116 | .context("Failed to deserialize Attestation")?; 117 | let verifier = crate::verifier::to_verifier(&tee)?; 118 | 119 | let claims_from_tee_evidence = verifier 120 | .evaluate(nonce.to_string(), &attestation) 121 | .await 122 | .map_err(|e| anyhow!("Verifier evaluate failed: {e:?}"))?; 123 | 124 | let flattened_claims = flatten_claims(tee.clone(), &claims_from_tee_evidence)?; 125 | let tcb = serde_json::to_string(&flattened_claims)?; 126 | let reference_data_map = self 127 | .get_reference_data(&tcb) 128 | .await 129 | .map_err(|e| anyhow!("Generate reference data failed{:?}", e))?; 130 | 131 | // Now only support using default policy to evaluate 132 | let evaluation_report = self 133 | .policy_engine 134 | .evaluate(reference_data_map, tcb.clone(), None) 135 | .await 136 | .map_err(|e| anyhow!("Policy Engine evaluation failed: {e}"))?; 137 | 138 | let token_claims = json!({ 139 | "tee-pubkey": attestation.tee_pubkey.clone(), 140 | "tcb-status": flattened_claims, 141 | "evaluation-report": evaluation_report, 142 | }); 143 | let attestation_results_token = self.token_broker.issue(token_claims)?; 144 | 145 | Ok(attestation_results_token) 146 | } 147 | 148 | async fn get_reference_data(&self, tcb_claims: &str) -> Result>> { 149 | let mut data = HashMap::new(); 150 | let tcb_claims_map: HashMap = serde_json::from_str(tcb_claims)?; 151 | for key in tcb_claims_map.keys() { 152 | data.insert( 153 | key.to_string(), 154 | self.rvps 155 | .get_digests(key) 156 | .await? 157 | .unwrap_or_default() 158 | .hash_values 159 | .clone(), 160 | ); 161 | } 162 | Ok(data) 163 | } 164 | 165 | /// Registry a new reference value 166 | pub async fn registry_reference_value(&mut self, message: Message) -> Result<()> { 167 | self.rvps.verify_and_extract(message).await 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /attestation-service/src/policy_engine/mod.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use as_types::SetPolicyInput; 3 | use async_trait::async_trait; 4 | use serde::Deserialize; 5 | use std::collections::HashMap; 6 | use std::path::Path; 7 | 8 | pub mod opa; 9 | 10 | #[derive(Debug, EnumString, Deserialize)] 11 | #[strum(ascii_case_insensitive)] 12 | pub enum PolicyEngineType { 13 | OPA, 14 | } 15 | 16 | #[derive(Debug, EnumString, Deserialize, PartialEq)] 17 | #[strum(ascii_case_insensitive)] 18 | pub enum PolicyType { 19 | Rego, 20 | } 21 | 22 | impl PolicyEngineType { 23 | #[allow(dead_code)] 24 | pub fn to_policy_engine(&self, work_dir: &Path) -> Result> { 25 | match self { 26 | PolicyEngineType::OPA => Ok(Box::new(opa::OPA::new(work_dir.to_path_buf())?) 27 | as Box), 28 | } 29 | } 30 | } 31 | 32 | #[async_trait] 33 | pub trait PolicyEngine { 34 | async fn evaluate( 35 | &self, 36 | reference_data_map: HashMap>, 37 | input: String, 38 | policy_id: Option, 39 | ) -> Result; 40 | 41 | async fn set_policy(&mut self, input: SetPolicyInput) -> Result<()>; 42 | } 43 | -------------------------------------------------------------------------------- /attestation-service/src/policy_engine/opa/default_policy.rego: -------------------------------------------------------------------------------- 1 | # Attestation Service Default Policy 2 | # 3 | # The function of this policy is to adopt the default policy when no custom policy 4 | # is provided in the attestation request of Attestation Service. 5 | # 6 | # - The input data required by this default policy is a set of key value pairs: 7 | # 8 | # { 9 | # "sample1": "112233", 10 | # "sample2": "332211", 11 | # ... 12 | # } 13 | # 14 | # - The format of reference data required by this default policy is defined as follows: 15 | # 16 | # { 17 | # "reference": { 18 | # "sample1": ["112233", "223311"], 19 | # "sample2": "332211", 20 | # "sample3": [], 21 | # ... 22 | # } 23 | # } 24 | # 25 | # If the default policy is used for verification, the reference meeting the above format 26 | # needs to be provided in the attestation request, otherwise the Attestation Service will 27 | # automatically generate a reference data meeting the above format. 28 | package policy 29 | 30 | import future.keywords.every 31 | 32 | default allow = false 33 | 34 | allow { 35 | every k, v in input { 36 | # `judge_field`: Traverse each key value pair in the input and make policy judgments on it. 37 | # 38 | # For each key value pair: 39 | # * If there isn't a corresponding key in the reference: 40 | # It is considered that the current key value pair has passed the verification. 41 | # * If there is a corresponding key in the reference: 42 | # Call `match_value` to further judge the value in input with the value in reference. 43 | judge_field(k, v) 44 | } 45 | } 46 | 47 | judge_field(input_key, input_value) { 48 | has_key(data.reference, input_key) 49 | reference_value := data.reference[input_key] 50 | 51 | # `match_value`: judge the value in input with the value in reference. 52 | # 53 | # * If the type of reference value is not array: 54 | # Judge whether input value and reference value are equal。 55 | # * If the type of reference value is array: 56 | # Call `array_include` to further judge the input value with the values in the array. 57 | match_value(reference_value, input_value) 58 | } 59 | 60 | judge_field(input_key, input_value) { 61 | not has_key(data.reference, input_key) 62 | } 63 | 64 | match_value(reference_value, input_value) { 65 | not is_array(reference_value) 66 | input_value == reference_value 67 | } 68 | 69 | match_value(reference_value, input_value) { 70 | is_array(reference_value) 71 | 72 | # `array_include`: judge the input value with the values in the array. 73 | # 74 | # * If the reference value array is empty: 75 | # It is considered that the current input value has passed the verification. 76 | # * If the reference value array is not empty: 77 | # Judge whether there is a value equal to input value in the reference value array. 78 | array_include(reference_value, input_value) 79 | } 80 | 81 | array_include(reference_value_array, input_value) { 82 | reference_value_array == [] 83 | } 84 | 85 | array_include(reference_value_array, input_value) { 86 | reference_value_array != [] 87 | some i 88 | reference_value_array[i] == input_value 89 | } 90 | 91 | has_key(m, k) { 92 | _ = m[k] 93 | } 94 | -------------------------------------------------------------------------------- /attestation-service/src/policy_engine/opa/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::policy_engine::{PolicyEngine, PolicyType}; 2 | use anyhow::{anyhow, bail, Result}; 3 | use as_types::SetPolicyInput; 4 | use async_trait::async_trait; 5 | use base64::Engine; 6 | use serde_json::Value; 7 | use std::collections::HashMap; 8 | use std::ffi::CStr; 9 | use std::fs; 10 | use std::os::raw::c_char; 11 | use std::path::PathBuf; 12 | use std::str::FromStr; 13 | 14 | // Link import cgo function 15 | #[link(name = "cgo")] 16 | extern "C" { 17 | pub fn evaluateGo(policy: GoString, data: GoString, input: GoString) -> *mut c_char; 18 | } 19 | 20 | /// String structure passed into cgo 21 | #[derive(Debug)] 22 | #[repr(C)] 23 | pub struct GoString { 24 | pub p: *const c_char, 25 | pub n: isize, 26 | } 27 | 28 | #[derive(Debug)] 29 | pub struct OPA { 30 | policy_dir_path: PathBuf, 31 | } 32 | 33 | impl OPA { 34 | pub fn new(work_dir: PathBuf) -> Result { 35 | let mut policy_dir_path = work_dir; 36 | 37 | policy_dir_path.push("opa"); 38 | if !policy_dir_path.as_path().exists() { 39 | fs::create_dir_all(&policy_dir_path) 40 | .map_err(|e| anyhow!("Create policy dir failed: {:?}", e))?; 41 | } 42 | 43 | let mut default_policy_path = PathBuf::from( 44 | &policy_dir_path 45 | .to_str() 46 | .ok_or_else(|| anyhow!("Policy DirPath to string failed"))?, 47 | ); 48 | default_policy_path.push("default.rego"); 49 | if !default_policy_path.as_path().exists() { 50 | let policy = std::include_str!("default_policy.rego").to_string(); 51 | fs::write(&default_policy_path, policy)?; 52 | } 53 | 54 | Ok(Self { policy_dir_path }) 55 | } 56 | } 57 | 58 | #[async_trait] 59 | impl PolicyEngine for OPA { 60 | async fn evaluate( 61 | &self, 62 | reference_data_map: HashMap>, 63 | input: String, 64 | policy_id: Option, 65 | ) -> Result { 66 | let policy_file_path = format!( 67 | "{}/{}.rego", 68 | self.policy_dir_path 69 | .to_str() 70 | .ok_or_else(|| anyhow!("Miss Policy DirPath"))?, 71 | policy_id.unwrap_or("default".to_string()) 72 | ); 73 | let policy = tokio::fs::read_to_string(policy_file_path) 74 | .await 75 | .map_err(|e| anyhow!("Read OPA policy file failed: {:?}", e))?; 76 | 77 | let policy_go = GoString { 78 | p: policy.as_ptr() as *const c_char, 79 | n: policy.len() as isize, 80 | }; 81 | 82 | let reference = serde_json::json!({ "reference": reference_data_map }).to_string(); 83 | 84 | let reference_go = GoString { 85 | p: reference.as_ptr() as *const c_char, 86 | n: reference.len() as isize, 87 | }; 88 | 89 | let input_go = GoString { 90 | p: input.as_ptr() as *const c_char, 91 | n: input.len() as isize, 92 | }; 93 | 94 | // Call the function exported by cgo and process the returned decision 95 | let decision_buf: *mut c_char = unsafe { evaluateGo(policy_go, reference_go, input_go) }; 96 | let decision_str: &CStr = unsafe { CStr::from_ptr(decision_buf) }; 97 | let res = decision_str.to_str()?.to_string(); 98 | debug!("Evaluated: {}", res); 99 | if res.starts_with("Error::") { 100 | return Err(anyhow!(res)); 101 | } 102 | 103 | // If a clear approval opinion is given in the evaluation report, 104 | // the rejection information will be reflected in the evaluation failure return value. 105 | let res_kv: Value = serde_json::from_str(&res)?; 106 | if let Some(allow) = res_kv["allow"].as_bool() { 107 | if !allow { 108 | bail!("Untrusted TEE evidence") 109 | } 110 | } 111 | 112 | Ok(res) 113 | } 114 | 115 | async fn set_policy(&mut self, input: SetPolicyInput) -> Result<()> { 116 | let policy_type = PolicyType::from_str(&input.r#type) 117 | .map_err(|_| anyhow!("{} is not support by AS", &input.r#type))?; 118 | if policy_type != PolicyType::Rego { 119 | bail!("OPA Policy Engine only support .rego policy"); 120 | } 121 | 122 | let policy_bytes = base64::engine::general_purpose::URL_SAFE_NO_PAD 123 | .decode(input.policy) 124 | .map_err(|e| anyhow!("Base64 decode OPA policy string failed: {:?}", e))?; 125 | let mut policy_file_path = PathBuf::from( 126 | &self 127 | .policy_dir_path 128 | .to_str() 129 | .ok_or_else(|| anyhow!("Policy DirPath to string failed"))?, 130 | ); 131 | policy_file_path.push(format!("{}.rego", input.policy_id)); 132 | 133 | tokio::fs::write(&policy_file_path, policy_bytes) 134 | .await 135 | .map_err(|e| anyhow!("Write OPA policy to file failed: {:?}", e)) 136 | } 137 | } 138 | 139 | #[cfg(test)] 140 | mod tests { 141 | use super::*; 142 | use serde_json::json; 143 | 144 | fn dummy_reference(ver: u64) -> String { 145 | json!({ 146 | "productId": [ver.to_string()], 147 | "svn": [ver.to_string()] 148 | }) 149 | .to_string() 150 | } 151 | 152 | fn dummy_input(product_id: u64, svn: u64) -> String { 153 | json!({ 154 | "productId": product_id.to_string(), 155 | "svn": svn.to_string() 156 | }) 157 | .to_string() 158 | } 159 | 160 | #[tokio::test] 161 | async fn test_evaluate() { 162 | let opa = OPA { 163 | policy_dir_path: PathBuf::from("./src/policy_engine/opa"), 164 | }; 165 | let default_policy_id = "default_policy".to_string(); 166 | 167 | let reference_data: HashMap> = 168 | serde_json::from_str(&dummy_reference(5)).unwrap(); 169 | 170 | let res = opa 171 | .evaluate( 172 | reference_data.clone(), 173 | dummy_input(5, 5), 174 | Some(default_policy_id.clone()), 175 | ) 176 | .await; 177 | assert!(res.is_ok(), "OPA execution() should be success"); 178 | 179 | let res = opa 180 | .evaluate(reference_data, dummy_input(0, 0), Some(default_policy_id)) 181 | .await; 182 | assert!(res.is_err(), "OPA execution() should be failed"); 183 | } 184 | 185 | #[tokio::test] 186 | async fn test_set_policy() { 187 | let mut opa = OPA::new(PathBuf::from("../test_data")).unwrap(); 188 | let policy = "package policy 189 | default allow = true" 190 | .to_string(); 191 | 192 | let input = SetPolicyInput { 193 | r#type: "rego".to_string(), 194 | policy_id: "test".to_string(), 195 | policy: base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(policy), 196 | }; 197 | 198 | assert!(opa.set_policy(input).await.is_ok()); 199 | } 200 | } 201 | -------------------------------------------------------------------------------- /attestation-service/src/rvps/extractors/extractor_modules/in_toto/README.md: -------------------------------------------------------------------------------- 1 | # In-toto Extractor 2 | 3 | This Extractor verifies all `.link` files using in-toto 4 | [verify-lib](https://github.com/in-toto/in-toto-golang/blob/master/in_toto/verifylib.go). 5 | 6 | ## Format of Provenance 7 | 8 | The format of in-toto provenance in a `Message` is as the following 9 | ```json 10 | { 11 | "version" : "VERSION OF IN-TOTO", 12 | "line_normalization" : true/false, 13 | "files" : { 14 | "FILE_PATH" : "BASE64 ENCODED CONTENT", 15 | ... 16 | } 17 | } 18 | ``` 19 | 20 | Here, 21 | * `files` includes all `.link`, `.pub` and `.layout` files, with relative 22 | file path set as `"FILE_PATH"` (e.g., `keys/key1.pub` indicates `key1.pub` is in the 23 | directory `keys/`), and content encoded in base64 `"BASE64 ENCODED CONTENT"`. 24 | * `line_normalization` indicates whether line separators like CRLF in Windows 25 | should be all converted to LF, to avoid cross-platform compatibility when 26 | calculating digest. 27 | * `version` indicates the version of this in-toto provenance. By default, 28 | the `version` will be `0.9`. 29 | 30 | ## Process Logic 31 | 32 | All the given metadata of in-toto will be verified using golang-in-toto's verifylib. 33 | The verification process includes the following: 34 | * Verify the signature of the layout, together with all the linkfiles 35 | * Verify whether the materials and products follows what the layout defines. 36 | * Check all other operations the layout defines (s.t. Inspection) 37 | * Return a summary link, which contains information about the products of the whole 38 | supply chain together with its digests. 39 | 40 | We take the summary link and gather all the related information of the products to format 41 | into a `ReferenceValue`. 42 | 43 | ## More about In-toto 44 | 45 | In-toto is a framework to secure software supply chain, also a CNCF project. Related links 46 | * Main page: https://in-toto.io/ 47 | * Slides about RVPS & verifible build using in-toto: https://docs.google.com/presentation/d/1mBthljo6-UZcZrEkRrOnOdp31cp1O-gT/edit?usp=sharing&ouid=107855505470969153275&rtpof=true&sd=true 48 | * Slides about VBDA: https://docs.google.com/presentation/d/1sdicILTowOxH7jL_701fnU8kHgSePM1L/edit?usp=sharing&ouid=107855505470969153275&rtpof=true&sd=true -------------------------------------------------------------------------------- /attestation-service/src/rvps/extractors/extractor_modules/in_toto/shim/README.md: -------------------------------------------------------------------------------- 1 | # In-toto shim 2 | 3 | This is a wrapper of in-toto-golang for rust 4 | 5 | ## Usage 6 | 7 | The function `verify()` is the rust wrapper of golang func `verifyGo` in [intoto.go](../../../../cgo/intoto.go). 8 | 9 | The interface looks like the following 10 | 11 | ```rust 12 | pub fn verify( 13 | layout_path: String, 14 | pub_key_paths: Vec, 15 | intermediate_paths: Vec, 16 | link_dir: String, 17 | line_normalization: bool, 18 | ) -> Result 19 | ``` 20 | 21 | Here 22 | - `layout_path`: Path to the layout file 23 | - `pub_key_paths`: Paths of public keys of the software supply chain owners to verify the layout. 24 | - `intermediate_paths`: Paths to PEM formatted certificates, used as intermediaries to verify the chain of trust to the layout's trusted root. 25 | - `link_dir`: Directory where the link metadata files are stored. 26 | - `line_normalization`: a flag indicating whether Windows-style line separators (CRLF) are normalized to Unix-style line separators (LF) for cross-platform consistency. -------------------------------------------------------------------------------- /attestation-service/src/rvps/extractors/extractor_modules/in_toto/shim/mod.rs: -------------------------------------------------------------------------------- 1 | use anyhow::*; 2 | use std::ffi::CStr; 3 | use std::os::raw::c_char; 4 | 5 | #[repr(C)] 6 | #[derive(Debug, Copy, Clone)] 7 | pub struct GoString { 8 | pub p: *const c_char, 9 | pub n: isize, 10 | } 11 | 12 | #[repr(C)] 13 | #[derive(Debug, Copy, Clone)] 14 | pub struct GoSlice { 15 | pub data: *const c_char, 16 | pub len: i64, 17 | pub cap: i64, 18 | } 19 | 20 | // Link import cgo function 21 | #[link(name = "cgo")] 22 | extern "C" { 23 | pub fn verifyGo( 24 | layoutPath: GoString, 25 | pubKeyPaths: GoSlice, 26 | intermediatePaths: GoSlice, 27 | linkDir: GoString, 28 | lineNormalizationc: i32, 29 | ) -> *mut c_char; 30 | } 31 | 32 | pub fn verify( 33 | layout_path: String, 34 | pub_key_paths: Vec, 35 | intermediate_paths: Vec, 36 | link_dir: String, 37 | line_normalization: bool, 38 | ) -> Result<()> { 39 | // Convert Rust String to GoString 40 | let layout_path = GoString { 41 | p: layout_path.as_ptr() as *const c_char, 42 | n: layout_path.len() as isize, 43 | }; 44 | 45 | // Convert Rust Vec to GoSlice of GoString 46 | let pub_key_paths_vec: Vec<_> = pub_key_paths 47 | .iter() 48 | .map(|arg| GoString { 49 | p: arg.as_ptr() as *const c_char, 50 | n: arg.len() as isize, 51 | }) 52 | .collect(); 53 | 54 | let pub_key_paths_goslice = GoSlice { 55 | data: pub_key_paths_vec.as_ptr() as *const c_char, 56 | len: pub_key_paths_vec.len() as i64, 57 | cap: pub_key_paths_vec.len() as i64, 58 | }; 59 | 60 | // Convert Rust Vec to GoSlice of GoString 61 | let intermediate_paths_vec: Vec<_> = intermediate_paths 62 | .iter() 63 | .map(|arg| GoString { 64 | p: arg.as_ptr() as *const c_char, 65 | n: arg.len() as isize, 66 | }) 67 | .collect(); 68 | 69 | let intermediate_paths_goslice = GoSlice { 70 | data: intermediate_paths_vec.as_ptr() as *const c_char, 71 | len: intermediate_paths_vec.len() as i64, 72 | cap: intermediate_paths_vec.len() as i64, 73 | }; 74 | 75 | // Convert Rust String to C char* 76 | let link_dir = GoString { 77 | p: link_dir.as_ptr() as *const c_char, 78 | n: link_dir.len() as isize, 79 | }; 80 | 81 | // Convert Rust bool to C int 82 | let line_normalization_c = line_normalization as i32; 83 | 84 | // Call the function exported by cgo and process the returned string 85 | let result_buf: *mut c_char = unsafe { 86 | verifyGo( 87 | layout_path, 88 | pub_key_paths_goslice, 89 | intermediate_paths_goslice, 90 | link_dir, 91 | line_normalization_c, 92 | ) 93 | }; 94 | 95 | let result_str: &CStr = unsafe { CStr::from_ptr(result_buf) }; 96 | let res = result_str.to_str()?.to_string(); 97 | 98 | if res.starts_with("Error::") { 99 | bail!(res); 100 | } 101 | 102 | Ok(()) 103 | } 104 | -------------------------------------------------------------------------------- /attestation-service/src/rvps/extractors/extractor_modules/mod.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022 Alibaba Cloud 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | // 5 | 6 | // Add your specific provenance declaration here. 7 | 8 | use anyhow::*; 9 | use std::collections::HashMap; 10 | 11 | use crate::rvps::ReferenceValue; 12 | 13 | #[cfg(feature = "in-toto")] 14 | pub mod in_toto; 15 | 16 | pub mod sample; 17 | 18 | /// Extractor is a standard interface that all provenance extractors 19 | /// need to implement. Here reference_value can be modified in the 20 | /// handler, added any field if needed. 21 | pub trait Extractor { 22 | fn verify_and_extract(&self, provenance: &str) -> Result>; 23 | } 24 | 25 | pub type ExtractorInstance = Box; 26 | type ExtractorInstantiateFunc = Box ExtractorInstance + Send + Sync>; 27 | 28 | pub struct ExtractorModuleList { 29 | mod_list: HashMap, 30 | } 31 | 32 | impl Default for ExtractorModuleList { 33 | fn default() -> ExtractorModuleList { 34 | // TODO: when new extractor is added, change mod_list 35 | // to mutable. 36 | let mut mod_list = HashMap::new(); 37 | 38 | { 39 | let instantiate_func: ExtractorInstantiateFunc = 40 | Box::new(|| -> ExtractorInstance { Box::::default() }); 41 | mod_list.insert("sample".to_string(), instantiate_func); 42 | } 43 | 44 | #[cfg(feature = "in-toto")] 45 | { 46 | let instantiate_func: ExtractorInstantiateFunc = 47 | Box::new(|| -> ExtractorInstance { Box::new(in_toto::InTotoExtractor::new()) }); 48 | mod_list.insert("in-toto".to_string(), instantiate_func); 49 | } 50 | 51 | ExtractorModuleList { mod_list } 52 | } 53 | } 54 | 55 | impl ExtractorModuleList { 56 | pub fn get_func(&self, extractor_name: &str) -> Result<&ExtractorInstantiateFunc> { 57 | let instantiate_func: &ExtractorInstantiateFunc = 58 | self.mod_list.get(extractor_name).ok_or_else(|| { 59 | anyhow!( 60 | "RVPS Extractors does not support the given extractor: {}!", 61 | extractor_name 62 | ) 63 | })?; 64 | Ok(instantiate_func) 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /attestation-service/src/rvps/extractors/extractor_modules/sample/README.md: -------------------------------------------------------------------------------- 1 | # Sample Extractor 2 | 3 | This Extractor will directly extract the reference value from the input **WITHOUT** verifying any signatures. 4 | 5 | This format is only for test and demo. It should be replaced with a signed provenance which contains the trust relationship for a software supply chain. 6 | 7 | ## Format of Provenance 8 | 9 | The format of sample provenance in a `Message` is as the following 10 | ```json 11 | { 12 | "": [ 13 | "", 14 | "", 15 | ... 16 | ], 17 | "": [ 18 | "", 19 | "", 20 | ], 21 | ... 22 | } 23 | ``` 24 | 25 | The expire time will be 12 months and the hash algorithm `sha384` by default. -------------------------------------------------------------------------------- /attestation-service/src/rvps/extractors/extractor_modules/sample/mod.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023 Alibaba Cloud 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | // 5 | 6 | //! This is a very simple format of provenance 7 | 8 | use std::collections::HashMap; 9 | 10 | use anyhow::*; 11 | use base64::Engine; 12 | use chrono::{Months, Timelike, Utc}; 13 | use serde::{Deserialize, Serialize}; 14 | 15 | use crate::rvps::{ 16 | reference_value::{HashValuePair, REFERENCE_VALUE_VERSION}, 17 | ReferenceValue, 18 | }; 19 | 20 | use super::Extractor; 21 | 22 | #[derive(Serialize, Deserialize)] 23 | pub struct Provenance { 24 | #[serde(flatten)] 25 | rvs: HashMap>, 26 | } 27 | 28 | #[derive(Default)] 29 | pub struct SampleExtractor; 30 | 31 | /// Default reference value hash algorithm 32 | const DEFAULT_ALG: &str = "sha384"; 33 | 34 | /// The reference value will be expired in the default time (months) 35 | const DEFAULT_EXPIRED_TIME: u32 = 12; 36 | 37 | impl Extractor for SampleExtractor { 38 | fn verify_and_extract(&self, provenance_base64: &str) -> Result> { 39 | let provenance = base64::engine::general_purpose::STANDARD 40 | .decode(provenance_base64) 41 | .context("base64 decode")?; 42 | let payload: Provenance = 43 | serde_json::from_slice(&provenance).context("deseralize sample provenance")?; 44 | 45 | let res = payload 46 | .rvs 47 | .iter() 48 | .filter_map(|(name, rvalues)| { 49 | let rvs = rvalues 50 | .iter() 51 | .map(|rv| HashValuePair::new(DEFAULT_ALG.into(), rv.to_string())) 52 | .collect(); 53 | 54 | let time = Utc::now() 55 | .with_nanosecond(0) 56 | .and_then(|t| t.checked_add_months(Months::new(DEFAULT_EXPIRED_TIME))); 57 | match time { 58 | Some(expired) => Some(ReferenceValue { 59 | version: REFERENCE_VALUE_VERSION.into(), 60 | name: name.to_string(), 61 | expired, 62 | hash_value: rvs, 63 | }), 64 | None => { 65 | warn!("Expired time calculated overflowed for reference value of {name}."); 66 | None 67 | } 68 | } 69 | }) 70 | .collect(); 71 | 72 | Ok(res) 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /attestation-service/src/rvps/extractors/mod.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022 Alibaba Cloud 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | // 5 | 6 | //! Extractors for RVPS. 7 | 8 | pub mod extractor_modules; 9 | 10 | use anyhow::*; 11 | 12 | use std::collections::HashMap; 13 | 14 | use self::extractor_modules::{ExtractorInstance, ExtractorModuleList}; 15 | 16 | use super::{Message, ReferenceValue}; 17 | 18 | /// `Extractors` provides different kinds of `Extractor`s due to 19 | /// different provenance types, e.g. in-toto, etc. 20 | /// Each `Extractor` will process the input provenance, verify the 21 | /// authenticity of the provenance, and then extract the formatted 22 | /// reference value (degest, s.t. hash value and name of the artifact) 23 | /// from the provenance. If the verification fails, no reference value 24 | /// will be extracted. 25 | 26 | /// `Extractors` defines the interfaces of Extractors. 27 | pub trait Extractors { 28 | /// Process the message, e.g. verifying 29 | /// and extracting the provenance inside the message due to 30 | /// type also inside the same message. If verification 31 | /// succeeds, return the generated ReferenceValue. 32 | /// All the digests value inside a ReferenceValue must be 33 | /// base64 encoded. 34 | fn process(&mut self, message: Message) -> Result>; 35 | } 36 | 37 | /// The struct `ExtractorsImpl` is responsible for implementing 38 | /// trait `Extractors`. 39 | /// `extractors_module_list` is a map that maps provenance type 40 | /// (in String) to its Extractor's instancializer. 41 | /// `extractors_instance_map` is another map that maps provenance type 42 | /// to the instancialized Extractor. The two map implement a 43 | /// "instantialization-on-demand" mechanism. 44 | #[derive(Default)] 45 | pub struct ExtractorsImpl { 46 | extractors_module_list: ExtractorModuleList, 47 | extractors_instance_map: HashMap, 48 | } 49 | 50 | impl ExtractorsImpl { 51 | /// Register an `Extractor` instance to `Extractors`. The `Extractor` is responsible for 52 | /// handling specific kind of provenance (as `extractor_name` indicates). 53 | fn register_instance(&mut self, extractor_name: String, extractor_instance: ExtractorInstance) { 54 | self.extractors_instance_map 55 | .insert(extractor_name, extractor_instance); 56 | } 57 | 58 | /// Instantiate an `Extractor` of given type `extractor_name`. This method will 59 | /// instantiate an `Extractor` instance and then register it. 60 | fn instantiate_extractor(&mut self, extractor_name: String) -> Result<()> { 61 | let instantiate_func = self.extractors_module_list.get_func(&extractor_name)?; 62 | let extractor_instance = (instantiate_func)(); 63 | self.register_instance(extractor_name, extractor_instance); 64 | Ok(()) 65 | } 66 | } 67 | 68 | impl Extractors for ExtractorsImpl { 69 | fn process(&mut self, message: Message) -> Result> { 70 | let typ = message.r#type; 71 | 72 | if self.extractors_instance_map.get_mut(&typ).is_none() { 73 | self.instantiate_extractor(typ.clone())?; 74 | } 75 | let extractor_instance = self 76 | .extractors_instance_map 77 | .get_mut(&typ) 78 | .ok_or_else(|| anyhow!("The Extractor instance does not existing!"))?; 79 | 80 | extractor_instance.verify_and_extract(&message.payload) 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /attestation-service/src/rvps/grpc.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022 Alibaba Cloud 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | // 5 | 6 | use anyhow::*; 7 | use tokio::sync::Mutex; 8 | 9 | use self::rvps_api::{ 10 | reference_value_provider_service_client::ReferenceValueProviderServiceClient, 11 | ReferenceValueQueryRequest, ReferenceValueRegisterRequest, 12 | }; 13 | 14 | use super::{Message, TrustedDigest, RVPSAPI}; 15 | 16 | pub mod rvps_api { 17 | tonic::include_proto!("reference"); 18 | } 19 | 20 | /// An agent for rvps, uses grpc to connect 21 | pub struct Agent { 22 | client: Mutex>, 23 | } 24 | 25 | impl Agent { 26 | pub async fn new(addr: &str) -> Result { 27 | Ok(Self { 28 | client: Mutex::new( 29 | ReferenceValueProviderServiceClient::connect(addr.to_string()).await?, 30 | ), 31 | }) 32 | } 33 | } 34 | 35 | #[async_trait::async_trait] 36 | impl RVPSAPI for Agent { 37 | async fn verify_and_extract(&mut self, message: Message) -> Result<()> { 38 | let message = serde_json::to_string(&message)?; 39 | let req = tonic::Request::new(ReferenceValueRegisterRequest { message }); 40 | let _ = self 41 | .client 42 | .lock() 43 | .await 44 | .register_reference_value(req) 45 | .await 46 | .context("register failed")?; 47 | Ok(()) 48 | } 49 | 50 | async fn get_digests(&self, name: &str) -> Result> { 51 | let req = tonic::Request::new(ReferenceValueQueryRequest { 52 | name: name.to_string(), 53 | }); 54 | let res = self 55 | .client 56 | .lock() 57 | .await 58 | .query_reference_value(req) 59 | .await? 60 | .into_inner(); 61 | let trust_digest = serde_json::from_str(&res.reference_value_results)?; 62 | Ok(trust_digest) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /attestation-service/src/rvps/mod.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022 Alibaba Cloud 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | // 5 | 6 | extern crate strum; 7 | 8 | #[allow(clippy::new_without_default)] 9 | pub mod extractors; 10 | pub mod pre_processor; 11 | pub mod reference_value; 12 | pub mod store; 13 | 14 | #[cfg(feature = "rvps-grpc")] 15 | pub mod grpc; 16 | #[cfg(feature = "rvps-grpc")] 17 | pub use grpc::Agent; 18 | 19 | #[cfg(feature = "rvps-native")] 20 | pub mod native; 21 | #[cfg(feature = "rvps-native")] 22 | pub use native::Core; 23 | 24 | use anyhow::*; 25 | use serde::{Deserialize, Serialize}; 26 | 27 | pub use reference_value::{ReferenceValue, TrustedDigest}; 28 | pub use store::Store; 29 | 30 | /// Default version of Message 31 | static MESSAGE_VERSION: &str = "0.1.0"; 32 | 33 | /// Message is an overall packet that Reference Value Provider Service 34 | /// receives. It will contain payload (content of different provenance, 35 | /// JSON format), provenance type (indicates the type of the payload) 36 | /// and a version number (use to distinguish different version of 37 | /// message, for extendability). 38 | /// * `version`: version of this message. 39 | /// * `payload`: content of the provenance, JSON encoded. 40 | /// * `type`: provenance type of the payload. 41 | #[derive(Serialize, Deserialize, Debug)] 42 | pub struct Message { 43 | #[serde(default = "default_version")] 44 | version: String, 45 | payload: String, 46 | r#type: String, 47 | } 48 | 49 | /// Set the default version for Message 50 | fn default_version() -> String { 51 | MESSAGE_VERSION.into() 52 | } 53 | 54 | /// The interfaces of Reference Value Provider Service 55 | /// * `verify_and_extract` is responsible for verify a message and 56 | /// store reference values from it. 57 | /// * `get_digests` gets trusted digests by the artifact's name. 58 | #[async_trait::async_trait] 59 | pub trait RVPSAPI { 60 | async fn verify_and_extract(&mut self, message: Message) -> Result<()>; 61 | async fn get_digests(&self, name: &str) -> Result>; 62 | } 63 | -------------------------------------------------------------------------------- /attestation-service/src/rvps/native.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022 Alibaba Cloud 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | // 5 | 6 | use anyhow::{bail, Result}; 7 | use chrono::{DateTime, Utc}; 8 | use log::{info, warn}; 9 | use std::time::SystemTime; 10 | 11 | use super::{ 12 | extractors::{Extractors, ExtractorsImpl}, 13 | pre_processor::{PreProcessor, PreProcessorAPI, Ware}, 14 | Message, Store, TrustedDigest, MESSAGE_VERSION, RVPSAPI, 15 | }; 16 | 17 | /// The core of the RVPS, s.t. componants except communication componants. 18 | pub struct Core { 19 | pre_processor: PreProcessor, 20 | extractors: ExtractorsImpl, 21 | store: Box, 22 | } 23 | 24 | impl Core { 25 | /// Instantiate a new RVPS Core 26 | pub fn new(store: Box) -> Self { 27 | let pre_processor = PreProcessor::default(); 28 | 29 | let extractors = ExtractorsImpl::default(); 30 | 31 | Core { 32 | pre_processor, 33 | extractors, 34 | store, 35 | } 36 | } 37 | 38 | /// Add Ware to the Core's Pre-Processor 39 | pub fn with_ware(&mut self, ware: Box) -> &Self { 40 | self.pre_processor.add_ware(ware); 41 | self 42 | } 43 | } 44 | 45 | #[async_trait::async_trait] 46 | impl RVPSAPI for Core { 47 | async fn verify_and_extract(&mut self, mut message: Message) -> Result<()> { 48 | // Judge the version field 49 | if message.version != MESSAGE_VERSION { 50 | bail!( 51 | "Version unmatched! Need {}, given {}.", 52 | MESSAGE_VERSION, 53 | message.version 54 | ); 55 | } 56 | 57 | self.pre_processor.process(&mut message)?; 58 | 59 | let rv = self.extractors.process(message)?; 60 | for v in rv.iter() { 61 | let old = self.store.set(v.name().to_string(), v.clone())?; 62 | if let Some(old) = old { 63 | info!("Old Reference value of {} is replaced.", old.name()); 64 | } 65 | } 66 | 67 | Ok(()) 68 | } 69 | 70 | async fn get_digests(&self, name: &str) -> Result> { 71 | let rv = self.store.get(name)?; 72 | match rv { 73 | None => Ok(None), 74 | Some(rv) => { 75 | let now: DateTime = DateTime::from(SystemTime::now()); 76 | if now > *rv.expired() { 77 | warn!("Reference value of {} is expired.", name); 78 | return Ok(None); 79 | } 80 | 81 | let hash_values = rv 82 | .hash_values() 83 | .iter() 84 | .map(|pair| pair.value().to_owned()) 85 | .collect(); 86 | 87 | Ok(Some(TrustedDigest { 88 | name: name.to_owned(), 89 | hash_values, 90 | })) 91 | } 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /attestation-service/src/rvps/pre_processor/mod.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022 Alibaba Cloud 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | // 5 | 6 | //! Pre-Processor of RVPS 7 | 8 | use std::collections::HashMap; 9 | 10 | use anyhow::*; 11 | 12 | use super::Message; 13 | 14 | /// A Ware loaded in Pre-Processor will process all the messages passing 15 | /// through the Pre-Processor. A series of Wares organized in order can 16 | /// process all the messages in need before they are consumed by the 17 | /// Extractors. 18 | pub trait Ware { 19 | fn handle( 20 | &self, 21 | message: &mut Message, 22 | context: &mut HashMap, 23 | next: Next<'_>, 24 | ) -> Result<()>; 25 | } 26 | 27 | /// Next encapsulates the remaining ware chain to run in [`Ware::handle`]. You can 28 | /// forward the task down the chain with [`run`]. 29 | /// 30 | /// [`Ware::handle`]: Ware::handle 31 | /// [`run`]: Self::run 32 | #[derive(Clone)] 33 | pub struct Next<'a> { 34 | wares: &'a [Box], 35 | } 36 | 37 | impl<'a> Next<'a> { 38 | pub(crate) fn new(wares: &'a [Box]) -> Self { 39 | Next { wares } 40 | } 41 | 42 | pub fn run( 43 | mut self, 44 | message: &mut Message, 45 | context: &'a mut HashMap, 46 | ) -> Result<()> { 47 | if let Some((current, rest)) = self.wares.split_first() { 48 | self.wares = rest; 49 | current.handle(message, context, self) 50 | } else { 51 | Ok(()) 52 | } 53 | } 54 | } 55 | 56 | /// PreProcessor's interfaces 57 | /// `process` processes the given [`Message`], which contains 58 | /// the provenance information and its type. The process 59 | /// can modify the given [`Message`]. 60 | pub trait PreProcessorAPI { 61 | fn process(&self, message: &mut Message) -> Result<()>; 62 | fn add_ware(&mut self, ware: Box) -> &Self; 63 | } 64 | 65 | #[derive(Default)] 66 | pub struct PreProcessor { 67 | wares: Vec>, 68 | } 69 | 70 | impl PreProcessorAPI for PreProcessor { 71 | fn process(&self, message: &mut Message) -> Result<()> { 72 | let mut context = HashMap::new(); 73 | let next = Next::new(&self.wares); 74 | next.run(message, &mut context) 75 | } 76 | 77 | fn add_ware(&mut self, ware: Box) -> &Self { 78 | self.wares.push(ware); 79 | self 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /attestation-service/src/rvps/reference_value.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022 Alibaba Cloud 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | // 5 | 6 | //! reference value for RVPS 7 | 8 | use anyhow::{anyhow, Result}; 9 | use chrono::{DateTime, NaiveDateTime, Timelike, Utc}; 10 | use serde::{Deserialize, Deserializer, Serialize}; 11 | 12 | /// Default version of ReferenceValue 13 | pub const REFERENCE_VALUE_VERSION: &str = "0.1.0"; 14 | 15 | /// A HashValuePair stores a hash algorithm name 16 | /// and relative artifact's hash value due to 17 | /// the algorithm. 18 | #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] 19 | pub struct HashValuePair { 20 | alg: String, 21 | value: String, 22 | } 23 | 24 | impl HashValuePair { 25 | pub fn new(alg: String, value: String) -> Self { 26 | Self { alg, value } 27 | } 28 | 29 | pub fn alg(&self) -> &String { 30 | &self.alg 31 | } 32 | 33 | pub fn value(&self) -> &String { 34 | &self.value 35 | } 36 | } 37 | 38 | /// Helper to deserialize an expired time 39 | fn primitive_date_time_from_str<'de, D: Deserializer<'de>>( 40 | d: D, 41 | ) -> Result, D::Error> { 42 | let s = >::deserialize(d)? 43 | .ok_or_else(|| serde::de::Error::invalid_length(0, &"