mirror of
https://github.com/matter-labs/teepot.git
synced 2025-07-22 07:24:48 +02:00
Compare commits
No commits in common. "main" and "tdx_google-v0.1.0" have entirely different histories.
main
...
tdx_google
211 changed files with 4778 additions and 17015 deletions
4
.github/workflows/lint.yml
vendored
4
.github/workflows/lint.yml
vendored
|
@ -16,7 +16,7 @@ jobs:
|
|||
steps:
|
||||
- name: checkout
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
- uses: enarx/spdx@d4020ee98e3101dd487c5184f27c6a6fb4f88709
|
||||
- uses: enarx/spdx@b5bfdd4410071bf058c8333d0e70020001524b6b
|
||||
with:
|
||||
licenses: |-
|
||||
Apache-2.0
|
||||
|
@ -28,5 +28,5 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
- uses: cachix/install-nix-action@v31
|
||||
- uses: cachix/install-nix-action@v30
|
||||
- run: nix run nixpkgs#taplo -- fmt --check
|
||||
|
|
38
.github/workflows/nix-non-x86.yml
vendored
38
.github/workflows/nix-non-x86.yml
vendored
|
@ -1,38 +0,0 @@
|
|||
name: nix-non-x86
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: read
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: ["main"]
|
||||
push:
|
||||
branches: ["main"]
|
||||
tags: ["*"]
|
||||
|
||||
jobs:
|
||||
macos-latest:
|
||||
runs-on: macos-latest
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
- uses: cachix/install-nix-action@v31
|
||||
with:
|
||||
extra_nix_config: |
|
||||
access-tokens = github.com=${{ github.token }}
|
||||
trusted-public-keys = cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY= tee-pot:SS6HcrpG87S1M6HZGPsfo7d1xJccCGev7/tXc5+I4jg=
|
||||
substituters = https://cache.nixos.org/ https://attic.teepot.org/tee-pot
|
||||
sandbox = true
|
||||
- name: Setup Attic cache
|
||||
uses: ryanccn/attic-action@v0
|
||||
with:
|
||||
endpoint: https://attic.teepot.org/
|
||||
cache: tee-pot
|
||||
token: ${{ secrets.ATTIC_TOKEN }}
|
||||
|
||||
- name: nixci
|
||||
# FIXME: this prevents it from running on macos
|
||||
# https://github.com/NixOS/nix/pull/12570
|
||||
# run: nix run github:nixos/nixpkgs/nixos-24.11#nixci -- build
|
||||
run: nix build -L .#teepot --no-sandbox
|
||||
|
42
.github/workflows/nix.yml
vendored
42
.github/workflows/nix.yml
vendored
|
@ -2,10 +2,10 @@ name: nix
|
|||
|
||||
on:
|
||||
pull_request:
|
||||
branches: ["main"]
|
||||
branches: [ "main" ]
|
||||
push:
|
||||
branches: ["main"]
|
||||
tags: ["*"]
|
||||
branches: [ "main" ]
|
||||
tags: [ "*" ]
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
|
@ -16,9 +16,8 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
- uses: cachix/install-nix-action@v31
|
||||
- uses: cachix/install-nix-action@v30
|
||||
with:
|
||||
install_url: https://releases.nixos.org/nix/nix-2.28.3/install
|
||||
extra_nix_config: |
|
||||
access-tokens = github.com=${{ github.token }}
|
||||
trusted-public-keys = cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY= tee-pot:SS6HcrpG87S1M6HZGPsfo7d1xJccCGev7/tXc5+I4jg=
|
||||
|
@ -35,12 +34,11 @@ jobs:
|
|||
|
||||
build:
|
||||
needs: check
|
||||
runs-on: [matterlabs-default-infra-runners]
|
||||
runs-on: [ matterlabs-default-infra-runners ]
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
- uses: cachix/install-nix-action@v31
|
||||
- uses: cachix/install-nix-action@v30
|
||||
with:
|
||||
install_url: https://releases.nixos.org/nix/nix-2.28.3/install
|
||||
extra_nix_config: |
|
||||
access-tokens = github.com=${{ github.token }}
|
||||
trusted-public-keys = cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY= tee-pot:SS6HcrpG87S1M6HZGPsfo7d1xJccCGev7/tXc5+I4jg=
|
||||
|
@ -58,7 +56,7 @@ jobs:
|
|||
|
||||
push_to_docker:
|
||||
needs: build
|
||||
runs-on: [matterlabs-default-infra-runners]
|
||||
runs-on: [ matterlabs-default-infra-runners ]
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}-${{ matrix.config.nixpackage }}
|
||||
cancel-in-progress: true
|
||||
|
@ -78,9 +76,8 @@ jobs:
|
|||
- { nixpackage: 'container-tdx-test' }
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: cachix/install-nix-action@v31
|
||||
- uses: cachix/install-nix-action@v30
|
||||
with:
|
||||
install_url: https://releases.nixos.org/nix/nix-2.28.3/install
|
||||
extra_nix_config: |
|
||||
access-tokens = github.com=${{ github.token }}
|
||||
trusted-public-keys = cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY= tee-pot:SS6HcrpG87S1M6HZGPsfo7d1xJccCGev7/tXc5+I4jg=
|
||||
|
@ -93,12 +90,11 @@ jobs:
|
|||
cache: tee-pot
|
||||
token: ${{ secrets.ATTIC_TOKEN }}
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0
|
||||
- name: Log in to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
username: ${{ secrets.DOCKERHUB_USER }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Load container
|
||||
id: build
|
||||
|
@ -110,21 +106,21 @@ jobs:
|
|||
|
||||
- name: Push container
|
||||
run: |
|
||||
echo "Pushing image ${{ steps.build.outputs.IMAGE_TAG }} to GitHub Container Registry"
|
||||
docker tag "${{ steps.build.outputs.IMAGE_TAG }}" "ghcr.io/${{ github.repository_owner }}"/"${{ steps.build.outputs.IMAGE_TAG }}"
|
||||
docker push "ghcr.io/${{ github.repository_owner }}"/"${{ steps.build.outputs.IMAGE_TAG }}"
|
||||
echo "Pushing image ${{ steps.build.outputs.IMAGE_TAG }} to Docker Hub"
|
||||
docker tag "${{ steps.build.outputs.IMAGE_TAG }}" matterlabsrobot/"${{ steps.build.outputs.IMAGE_TAG }}"
|
||||
docker push matterlabsrobot/"${{ steps.build.outputs.IMAGE_TAG }}"
|
||||
|
||||
- name: Tag container as latest
|
||||
if: ${{ github.event_name == 'push' }}
|
||||
run: |
|
||||
docker tag "${{ steps.build.outputs.IMAGE_TAG }}" "ghcr.io/${{ github.repository_owner }}"/"${{ steps.build.outputs.IMAGE_NAME }}:latest"
|
||||
docker push "ghcr.io/${{ github.repository_owner }}"/"${{ steps.build.outputs.IMAGE_NAME }}:latest"
|
||||
docker tag "${{ steps.build.outputs.IMAGE_TAG }}" matterlabsrobot/"${{ steps.build.outputs.IMAGE_NAME }}:latest"
|
||||
docker push matterlabsrobot/"${{ steps.build.outputs.IMAGE_NAME }}:latest"
|
||||
|
||||
- name: Tag container with tag
|
||||
if: ${{ github.event_name == 'push' && github.ref_type == 'tag' }}
|
||||
run: |
|
||||
docker tag "${{ steps.build.outputs.IMAGE_TAG }}" "ghcr.io/${{ github.repository_owner }}"/"${{ steps.build.outputs.IMAGE_NAME }}:$GITHUB_REF_NAME"
|
||||
docker push "ghcr.io/${{ github.repository_owner }}"/"${{ steps.build.outputs.IMAGE_NAME }}:$GITHUB_REF_NAME"
|
||||
docker tag "${{ steps.build.outputs.IMAGE_TAG }}" matterlabsrobot/"${{ steps.build.outputs.IMAGE_NAME }}:$GITHUB_REF_NAME"
|
||||
docker push matterlabsrobot/"${{ steps.build.outputs.IMAGE_NAME }}:$GITHUB_REF_NAME"
|
||||
|
||||
- name: Generate build ID for Flux Image Automation
|
||||
id: flux
|
||||
|
|
2
.github/workflows/secrets_scanner.yaml
vendored
2
.github/workflows/secrets_scanner.yaml
vendored
|
@ -9,7 +9,7 @@ jobs:
|
|||
with:
|
||||
fetch-depth: 0
|
||||
- name: TruffleHog OSS
|
||||
uses: trufflesecurity/trufflehog@c8921694a53d95ce424af6ae76dbebf3b6a83aef # v3.88.30
|
||||
uses: trufflesecurity/trufflehog@943daae06ba9cc80437a748155c818e9e3177a30 # v3.88.6
|
||||
with:
|
||||
path: ./
|
||||
base: ${{ github.event.repository.default_branch }}
|
||||
|
|
4167
Cargo.lock
generated
4167
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
62
Cargo.toml
62
Cargo.toml
|
@ -1,19 +1,12 @@
|
|||
[workspace]
|
||||
members = ["crates/*", "bin/*", "crates/teepot-vault/bin/*"]
|
||||
members = ["crates/*", "bin/*"]
|
||||
resolver = "2"
|
||||
|
||||
# exclude x86_64 only crates
|
||||
exclude = [
|
||||
"crates/teepot-tee-quote-verification-rs",
|
||||
"crates/teepot-tdx-attest-rs",
|
||||
"crates/teepot-tdx-attest-sys",
|
||||
]
|
||||
|
||||
[profile.release]
|
||||
strip = true
|
||||
|
||||
[workspace.package]
|
||||
version = "0.6.0"
|
||||
version = "0.3.0"
|
||||
edition = "2021"
|
||||
authors = ["Harald Hoyer <hh@matterlabs.dev>"]
|
||||
# rest of the workspace, if not specified in the package section
|
||||
|
@ -26,56 +19,49 @@ homepage = "https://github.com/matter-labs/teepot"
|
|||
actix-http = "3"
|
||||
actix-web = { version = "4.5", features = ["rustls-0_23"] }
|
||||
anyhow = "1.0.82"
|
||||
asn1_der = { version = "0.7", default-features = false, features = ["native_types"] }
|
||||
async-trait = "0.1.86"
|
||||
awc = { version = "3.5", features = ["rustls-0_23-webpki-roots"] }
|
||||
awc = { version = "3.4", features = ["rustls-0_23-webpki-roots"] }
|
||||
base64 = "0.22.0"
|
||||
bytemuck = { version = "1.15.0", features = ["derive", "min_const_generics", "extern_crate_std"] }
|
||||
bytes = "1"
|
||||
chrono = "0.4.40"
|
||||
clap = { version = "4.5", features = ["std", "derive", "env", "error-context", "help", "usage", "wrap_help"], default-features = false }
|
||||
config = { version = "0.15.8", default-features = false, features = ["yaml", "json", "toml", "async"] }
|
||||
const-oid = { version = "0.9.6", default-features = false }
|
||||
dcap-qvl = "0.2.3"
|
||||
const-oid = { version = "0.9", default-features = false }
|
||||
enumset = { version = "1.1", features = ["serde"] }
|
||||
futures = "0.3.31"
|
||||
futures-core = { version = "0.3.30", default-features = false }
|
||||
futures-core = { version = "0.3.30", features = ["alloc"], default-features = false }
|
||||
getrandom = { version = "0.3.1", features = ["std"] }
|
||||
gpt = "4.0.0"
|
||||
hex = { version = "0.4.3", features = ["std"], default-features = false }
|
||||
intel-dcap-api = { path = "crates/intel-dcap-api" }
|
||||
jsonrpsee-types = "0.25.1"
|
||||
mockito = "1.4"
|
||||
intel-tee-quote-verification-rs = { package = "teepot-tee-quote-verification-rs", path = "crates/teepot-tee-quote-verification-rs", version = "0.3.0" }
|
||||
intel-tee-quote-verification-sys = { version = "0.2.1" }
|
||||
jsonrpsee-types = { version = "0.24", default-features = false }
|
||||
num-integer = "0.1.46"
|
||||
num-traits = "0.2.18"
|
||||
opentelemetry = { version = "0.30", features = ["default", "logs"] }
|
||||
opentelemetry-appender-tracing = { version = "0.30", features = ["experimental_metadata_attributes", "log"] }
|
||||
opentelemetry-otlp = { version = "0.30", features = ["grpc-tonic", "logs"] }
|
||||
opentelemetry-semantic-conventions = { version = "0.30", features = ["semconv_experimental"] }
|
||||
opentelemetry_sdk = { version = "0.30", features = ["tokio", "rt-tokio"] }
|
||||
opentelemetry = { version = "0.27.0", features = ["default", "logs"] }
|
||||
opentelemetry-appender-tracing = { version = "0.27.0", features = ["experimental_metadata_attributes", "log"] }
|
||||
opentelemetry-otlp = { version = "0.27.0", features = ["grpc-tonic", "logs"] }
|
||||
opentelemetry-semantic-conventions = { version = "0.28.0", features = ["semconv_experimental"] }
|
||||
opentelemetry_sdk = { version = "0.27.1", features = ["tokio", "rt-tokio"] }
|
||||
p256 = "0.13.2"
|
||||
pe-sign = "0.1.10"
|
||||
percent-encoding = "2.3.1"
|
||||
pgp = { version = "0.16", default-features = false }
|
||||
pgp = "0.15"
|
||||
pkcs8 = { version = "0.10" }
|
||||
rand = { version = "0.8", features = ["std", "std_rng"] }
|
||||
reqwest = { version = "0.12", features = ["json"] }
|
||||
rsa = { version = "0.9.6", features = ["sha2", "pem"] }
|
||||
rustls = { version = "0.23.20", default-features = false, features = ["std", "logging", "tls12", "ring"] }
|
||||
secp256k1 = { version = "0.31", features = ["rand", "global-context"] }
|
||||
rustls = { version = "0.23.20" }
|
||||
secp256k1 = { version = "0.30", features = ["rand", "global-context"] }
|
||||
serde = { version = "1", features = ["derive", "rc"] }
|
||||
serde_json = "1"
|
||||
serde_with = { version = "3.8", features = ["base64", "hex"] }
|
||||
serde_yaml = "0.9.33"
|
||||
sha2 = "0.10.8"
|
||||
sha3 = "0.10.8"
|
||||
signature = "2.2.0"
|
||||
teepot = { version = "0.6.0", path = "crates/teepot" }
|
||||
teepot-tee-quote-verification-rs = { version = "0.6.0", path = "crates/teepot-tee-quote-verification-rs" }
|
||||
teepot-vault = { version = "0.6.0", path = "crates/teepot-vault" }
|
||||
tdx-attest-rs = { version = "0.1.2", git = "https://github.com/intel/SGXDataCenterAttestationPrimitives.git", rev = "aa239d25a437a28f3f4de92c38f5b6809faac842" }
|
||||
teepot = { path = "crates/teepot" }
|
||||
testaso = "0.1.0"
|
||||
thiserror = "2.0.11"
|
||||
tokio = { version = "1", features = ["sync", "macros", "rt-multi-thread", "fs", "time", "signal"] }
|
||||
tokio-util = "0.7.14"
|
||||
tracing = "0.1"
|
||||
tracing-actix-web = "0.7"
|
||||
tracing-futures = { version = "0.2.5", features = ["std"] }
|
||||
|
@ -83,9 +69,9 @@ tracing-log = "0.2"
|
|||
tracing-subscriber = { version = "0.3", features = ["env-filter", "json", "ansi"] }
|
||||
tracing-test = { version = "0.2.5", features = ["no-env-filter"] }
|
||||
url = "2.5.2"
|
||||
webpki-roots = "1.0.0"
|
||||
x509-cert = { version = "0.2", features = ["builder", "signature", "default"] }
|
||||
webpki-roots = "0.26.1"
|
||||
x509-cert = { version = "0.2", features = ["builder", "signature"] }
|
||||
zeroize = { version = "1.7.0", features = ["serde"] }
|
||||
zksync_basic_types = "28.6.0-non-semver-compat"
|
||||
zksync_types = "28.6.0-non-semver-compat"
|
||||
zksync_web3_decl = "28.6.0-non-semver-compat"
|
||||
zksync_basic_types = "=0.1.0"
|
||||
zksync_types = "=0.1.0"
|
||||
zksync_web3_decl = "=0.1.0"
|
||||
|
|
55
README.md
55
README.md
|
@ -2,37 +2,31 @@
|
|||
|
||||
## Parts of this project
|
||||
|
||||
### teepot - lib
|
||||
- `teepot`: The main rust crate that abstracts TEEs and key-value stores.
|
||||
- `tee-vault-unseal`: An enclave that uses the Vault API to unseal a vault as a proxy.
|
||||
- `vault-unseal`: A client utility, that talks to `tee-vault-unseal` to unseal a vault.
|
||||
- `tee-vault-admin`: An enclave that uses the Vault API to administer a vault as a proxy.
|
||||
- `vault-admin`: A client utility, that talks to `tee-vault-admin` to administer a vault.
|
||||
- `teepot-read` : A pre-exec utility that reads from the key-value store and passes the key-value pairs as environment
|
||||
variables to the enclave.
|
||||
- `teepot-write` : A pre-exec utility that reads key-values from the environment variables and writes them to the
|
||||
key-value store.
|
||||
- `verify-attestation`: A client utility that verifies the attestation of an enclave.
|
||||
- `tee-key-preexec`: A pre-exec utility that generates a p256 secret key and passes it as an environment variable to the
|
||||
enclave along with the attestation quote containing the hash of the public key.
|
||||
- `tdx_google`: A base VM running on Google Cloud TDX. It receives a container URL via the instance metadata,
|
||||
measures the sha384 of the URL to RTMR3 and launches the container.
|
||||
- `tdx-extend`: A utility to extend an RTMR register with a hash value.
|
||||
- `rtmr-calc`: A utility to calculate RTMR1 and RTMR2 from a GPT disk, the linux kernel, the linux initrd
|
||||
and a UKI (unified kernel image).
|
||||
- `sha384-extend`: A utility to calculate RTMR registers after extending them with a digest.
|
||||
|
||||
- `teepot`: The main rust crate that abstracts TEEs.
|
||||
- `verify-attestation`: A client utility that verifies the attestation of an enclave.
|
||||
- `tee-key-preexec`: A pre-exec utility that generates a p256 secret key and passes it as an environment variable to
|
||||
the
|
||||
enclave along with the attestation quote containing the hash of the public key.
|
||||
- `tdx_google`: A base VM running on Google Cloud TDX. It receives a container URL via the instance metadata,
|
||||
measures the sha384 of the URL to RTMR3 and launches the container.
|
||||
- `tdx-extend`: A utility to extend an RTMR register with a hash value.
|
||||
- `rtmr-calc`: A utility to calculate RTMR1 and RTMR2 from a GPT disk, the linux kernel, the linux initrd
|
||||
and a UKI (unified kernel image).
|
||||
- `sha384-extend`: A utility to calculate RTMR registers after extending them with a digest.
|
||||
|
||||
### Vault
|
||||
## Vault
|
||||
|
||||
Part of this project is a key-value store that runs in a Trusted Execution Environment (TEE) and uses Remote Attestation
|
||||
for Authentication. The key-value store is implemented using Hashicorp Vault running in an Intel SGX enclave via the
|
||||
Gramine runtime.
|
||||
|
||||
- `teepot-vault`: A crate lib with for the TEE key-value store components:
|
||||
- `tee-vault-unseal`: An enclave that uses the Vault API to unseal a vault as a proxy.
|
||||
- `vault-unseal`: A client utility, that talks to `tee-vault-unseal` to unseal a vault.
|
||||
- `tee-vault-admin`: An enclave that uses the Vault API to administer a vault as a proxy.
|
||||
- `vault-admin`: A client utility, that talks to `tee-vault-admin` to administer a vault.
|
||||
- `teepot-read` : A pre-exec utility that reads from the key-value store and passes the key-value pairs as
|
||||
environment
|
||||
variables to the enclave.
|
||||
- `teepot-write` : A pre-exec utility that reads key-values from the environment variables and writes them to the
|
||||
key-value store.
|
||||
|
||||
## Development
|
||||
|
||||
### Prerequisites
|
||||
|
@ -83,7 +77,7 @@ $ nix run .#fmt
|
|||
### Build as the CI would
|
||||
|
||||
```shell
|
||||
$ nix run github:nixos/nixpkgs/nixos-24.11#nixci -- build
|
||||
$ nix run github:nixos/nixpkgs/nixos-23.11#nixci
|
||||
```
|
||||
|
||||
### Build and test individual container
|
||||
|
@ -112,12 +106,3 @@ Attributes:
|
|||
```shell
|
||||
nixos-rebuild -L --flake .#tdxtest build-vm && ./result/bin/run-tdxtest-vm
|
||||
```
|
||||
|
||||
## Release
|
||||
|
||||
```shell
|
||||
$ cargo release 0.1.0 --manifest-path crates/teepot-tdx-attest-sys/Cargo.toml --sign
|
||||
$ cargo release 0.1.2 --manifest-path crates/teepot-tdx-attest-rs/Cargo.toml --sign
|
||||
$ cargo release 0.6.0 --manifest-path crates/teepot-tee-quote-verification-rs/Cargo.toml --sign
|
||||
$ cargo release 0.6.0 --sign
|
||||
```
|
||||
|
|
|
@ -45,6 +45,6 @@ gcloud compute instances create tdx-pilot \
|
|||
--maintenance-policy=TERMINATE \
|
||||
--image-project=tdx-pilot \
|
||||
--project tdx-pilot \
|
||||
--metadata=container_hub="docker.io",container_image="ghcr.io/matter-labs/test-tdx:117p5y281limw0w7b03v802ij00c5gzw" \
|
||||
--metadata=container_hub="docker.io",container_image="matterlabsrobot/test-tdx:117p5y281limw0w7b03v802ij00c5gzw" \
|
||||
--metadata-from-file=container_config=$BASE_DIR/config.json \
|
||||
--image tdx-img-f-"${NO}"
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
[package]
|
||||
name = "rtmr-calc"
|
||||
publish = false
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
authors.workspace = true
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (c) 2024-2025 Matter Labs
|
||||
// Copyright (c) 2024 Matter Labs
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
use clap::Parser;
|
||||
|
@ -7,13 +7,10 @@ use pesign::PE;
|
|||
use sha2::{Digest, Sha384};
|
||||
use std::{
|
||||
fmt::{Display, Formatter},
|
||||
io::{Error, Read, Seek, SeekFrom},
|
||||
io::{Error, ErrorKind, Read, Seek, SeekFrom},
|
||||
path::PathBuf,
|
||||
};
|
||||
use teepot::{
|
||||
log::{setup_logging, LogLevelParser},
|
||||
tdx::UEFI_MARKER_DIGEST_BYTES,
|
||||
};
|
||||
use teepot::log::{setup_logging, LogLevelParser};
|
||||
use tracing::{debug, info, level_filters::LevelFilter};
|
||||
|
||||
/// Precalculate rtmr1 and rtmr2 values.
|
||||
|
@ -65,8 +62,6 @@ impl Display for Rtmr {
|
|||
}
|
||||
}
|
||||
|
||||
const CHUNK_SIZE: u64 = 1024 * 128;
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let args = Arguments::parse();
|
||||
tracing::subscriber::set_global_default(setup_logging(
|
||||
|
@ -103,7 +98,7 @@ fn main() -> Result<()> {
|
|||
Ok:
|
||||
validseparator: UEFI
|
||||
*/
|
||||
rtmr1.extend(&UEFI_MARKER_DIGEST_BYTES);
|
||||
rtmr1.extend(&hex::decode("394341b7182cd227c5c6b07ef8000cdfd86136c4292b8e576573ad7ed9ae41019f5818b4b971c9effc60e1ad9f1289f0")?);
|
||||
|
||||
// Open disk image.
|
||||
let cfg = gpt::GptConfig::new().writable(false);
|
||||
|
@ -125,11 +120,11 @@ fn main() -> Result<()> {
|
|||
let pstart = header
|
||||
.part_start
|
||||
.checked_mul(lb_size.as_u64())
|
||||
.ok_or_else(|| Error::other("partition overflow - start offset"))?;
|
||||
.ok_or_else(|| Error::new(ErrorKind::Other, "partition overflow - start offset"))?;
|
||||
let _ = device.seek(SeekFrom::Start(pstart))?;
|
||||
|
||||
assert_eq!(header.part_size, 128);
|
||||
assert!(header.num_parts < u32::from(u8::MAX));
|
||||
assert!(header.num_parts < u8::MAX as _);
|
||||
|
||||
let empty_bytes = [0u8; 128];
|
||||
|
||||
|
@ -160,7 +155,7 @@ fn main() -> Result<()> {
|
|||
|
||||
let section_table = pe.get_section_table()?;
|
||||
|
||||
for section in §ion_table {
|
||||
for section in section_table.iter() {
|
||||
debug!(section_name = ?section.name()?);
|
||||
}
|
||||
|
||||
|
@ -177,13 +172,14 @@ fn main() -> Result<()> {
|
|||
.find(|s| s.name().unwrap().eq(sect))
|
||||
.ok_or(anyhow!("Failed to find section `{sect}`"))?;
|
||||
|
||||
let mut start = u64::from(s.pointer_to_raw_data);
|
||||
let end = start + u64::from(s.virtual_size);
|
||||
let mut start = s.pointer_to_raw_data as u64;
|
||||
let end = start + s.virtual_size as u64;
|
||||
|
||||
debug!(sect, start, end, len = (s.virtual_size));
|
||||
|
||||
let mut hasher = Sha384::new();
|
||||
|
||||
const CHUNK_SIZE: u64 = 1024 * 128;
|
||||
loop {
|
||||
if start >= end {
|
||||
break;
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
[package]
|
||||
name = "sha384-extend"
|
||||
publish = false
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
authors.workspace = true
|
||||
|
@ -13,4 +12,3 @@ anyhow.workspace = true
|
|||
clap.workspace = true
|
||||
hex.workspace = true
|
||||
sha2.workspace = true
|
||||
teepot.workspace = true
|
||||
|
|
|
@ -1,26 +1,7 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (c) 2024-2025 Matter Labs
|
||||
// Copyright (c) 2024 Matter Labs
|
||||
|
||||
//! A tool for extending SHA384 digests, commonly used in TPM and TDX operations
|
||||
//!
|
||||
//! # Overview
|
||||
//! This utility implements the extend operation used in Trusted Platform Module (TPM)
|
||||
//! Platform Configuration Registers (PCRs) and Intel Trust Domain Extensions (TDX)
|
||||
//! Runtime Measurement Registers (RTMRs). The extend operation combines two SHA384
|
||||
//! digests by concatenating and then hashing them.
|
||||
//!
|
||||
//! # Usage
|
||||
//! ```shell
|
||||
//! sha384-extend <extend-value> [--base <initial-value>]
|
||||
//! ```
|
||||
//! Where:
|
||||
//! - `extend-value`: SHA384 digest in hex format to extend with
|
||||
//! - `initial-value`: Optional initial SHA384 digest in hex format (defaults to "00")
|
||||
//!
|
||||
//! # Example
|
||||
//! ```shell
|
||||
//! sha384-extend --base 01 26bb0c
|
||||
//! ```
|
||||
//! Extend the TDX measurement
|
||||
|
||||
#![deny(missing_docs)]
|
||||
#![deny(clippy::all)]
|
||||
|
@ -28,137 +9,31 @@
|
|||
use anyhow::{Context, Result};
|
||||
use clap::Parser;
|
||||
use sha2::Digest;
|
||||
use teepot::util::pad;
|
||||
|
||||
/// Calculate e.g. a TDX RTMR or TPM PCR SHA384 digest by extending it with another
|
||||
/// Calculate a TDX rtmr or TPM pcr sha384 value by extending it
|
||||
#[derive(Parser, Debug)]
|
||||
#[command(author, version, about, long_about = None)]
|
||||
struct Arguments {
|
||||
/// The SHA384 digest (in hex format) to extend the base value with.
|
||||
/// Must be a valid hex string that can be padded to 48 bytes (384 bits).
|
||||
/// digest in hex to extend with
|
||||
#[arg(long)]
|
||||
extend: String,
|
||||
|
||||
/// The initial SHA384 digest (in hex format) to extend from.
|
||||
/// Must be a valid hex string that can be padded to 48 bytes (384 bits).
|
||||
#[arg(long, default_value = "00", required = false)]
|
||||
base: String,
|
||||
}
|
||||
|
||||
/// Extends a base SHA384 digest with another digest
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `base` - Base hex string to extend from
|
||||
/// * `extend` - Hex string to extend with
|
||||
///
|
||||
/// # Returns
|
||||
/// * `Result<String>` - The resulting SHA384 digest as a hex string
|
||||
///
|
||||
/// # Examples
|
||||
/// ```
|
||||
/// let result = extend_sha384("00", "aa").unwrap();
|
||||
/// ```
|
||||
pub fn extend_sha384(base: &str, extend: &str) -> Result<String> {
|
||||
let mut hasher = sha2::Sha384::new();
|
||||
|
||||
hasher.update(pad::<48>(&hex::decode(base).context(format!(
|
||||
"Failed to decode base digest '{base}' - expected hex string",
|
||||
))?)?);
|
||||
|
||||
hasher.update(pad::<48>(&hex::decode(extend).context(format!(
|
||||
"Failed to decode extend digest '{extend}' - expected hex string",
|
||||
))?)?);
|
||||
|
||||
Ok(hex::encode(hasher.finalize()))
|
||||
/// initial digest in hex
|
||||
#[arg(long)]
|
||||
digest: String,
|
||||
}
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let args = Arguments::parse();
|
||||
let hex = extend_sha384(&args.base, &args.extend)?;
|
||||
|
||||
// Parse the digest string as a hex array
|
||||
let extend_bytes = hex::decode(&args.extend).context("Invalid digest format")?;
|
||||
let mut digest_bytes = hex::decode(&args.digest).context("Invalid digest format")?;
|
||||
|
||||
digest_bytes.extend(extend_bytes);
|
||||
|
||||
let bytes = sha2::Sha384::digest(&digest_bytes);
|
||||
let hex = hex::encode(bytes);
|
||||
|
||||
println!("{hex}");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
const TEST_BASE: &str = "00";
|
||||
const TEST_EXTEND: &str = "d3a665eb2bf8a6c4e6cee0ccfa663ee4098fc4903725b1823d8d0316126bb0cb";
|
||||
const EXPECTED_RESULT: &str = "971fb52f90ec98a234301ca9b8fc30b613c33e3dd9c0cc42dcb8003d4a95d8fb218b75baf028b70a3cabcb947e1ca453";
|
||||
|
||||
const EXPECTED_RESULT_00: &str = "f57bb7ed82c6ae4a29e6c9879338c592c7d42a39135583e8ccbe3940f2344b0eb6eb8503db0ffd6a39ddd00cd07d8317";
|
||||
|
||||
#[test]
|
||||
fn test_extend_sha384_with_test_vectors() {
|
||||
let result = extend_sha384(TEST_BASE, TEST_EXTEND).unwrap();
|
||||
assert_eq!(
|
||||
result, EXPECTED_RESULT,
|
||||
"SHA384 extend result didn't match expected value"
|
||||
);
|
||||
|
||||
// Test with empty base
|
||||
let result = extend_sha384("", TEST_EXTEND).unwrap();
|
||||
assert_eq!(
|
||||
result, EXPECTED_RESULT,
|
||||
"SHA384 extend result didn't match expected value"
|
||||
);
|
||||
|
||||
// Test with empty base
|
||||
let result = extend_sha384("", "").unwrap();
|
||||
assert_eq!(
|
||||
result, EXPECTED_RESULT_00,
|
||||
"SHA384 extend result didn't match expected value"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extend_sha384_with_invalid_base() {
|
||||
// Test with invalid hex in base
|
||||
let result = extend_sha384("not_hex", TEST_EXTEND);
|
||||
assert!(result.is_err(), "Should fail with invalid base hex");
|
||||
|
||||
// Test with odd length hex string
|
||||
let result = extend_sha384("0", TEST_EXTEND);
|
||||
assert!(result.is_err(), "Should fail with odd-length hex string");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extend_sha384_with_invalid_extend() {
|
||||
// Test with invalid hex in extend
|
||||
let result = extend_sha384(TEST_BASE, "not_hex");
|
||||
assert!(result.is_err(), "Should fail with invalid extend hex");
|
||||
|
||||
// Test with odd length hex string
|
||||
let result = extend_sha384(TEST_BASE, "0");
|
||||
assert!(result.is_err(), "Should fail with odd-length hex string");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extend_sha384_with_oversized_input() {
|
||||
// Create a hex string that's too long (more than 48 bytes when decoded)
|
||||
let oversized = "00".repeat(49); // 49 bytes when decoded
|
||||
|
||||
let result = extend_sha384(TEST_BASE, &oversized);
|
||||
assert!(result.is_err(), "Should fail with oversized extend value");
|
||||
|
||||
let result = extend_sha384(&oversized, TEST_EXTEND);
|
||||
assert!(result.is_err(), "Should fail with oversized base value");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extend_sha384_idempotent() {
|
||||
// Test that extending with the same values produces the same result
|
||||
let result1 = extend_sha384(TEST_BASE, TEST_EXTEND).unwrap();
|
||||
let result2 = extend_sha384(TEST_BASE, TEST_EXTEND).unwrap();
|
||||
assert_eq!(result1, result2, "Same inputs should produce same output");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extend_sha384_case_sensitivity() {
|
||||
// Test that upper and lower case hex strings produce the same result
|
||||
let upper_extend = TEST_EXTEND.to_uppercase();
|
||||
let result1 = extend_sha384(TEST_BASE, TEST_EXTEND).unwrap();
|
||||
let result2 = extend_sha384(TEST_BASE, &upper_extend).unwrap();
|
||||
assert_eq!(result1, result2, "Case should not affect the result");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,69 +1,58 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (c) 2024-2025 Matter Labs
|
||||
// Copyright (c) 2024 Matter Labs
|
||||
|
||||
//! Extend the TDX measurement
|
||||
|
||||
#![deny(missing_docs)]
|
||||
#![deny(clippy::all)]
|
||||
|
||||
use tracing::error;
|
||||
use anyhow::{Context, Result};
|
||||
use clap::Parser;
|
||||
use teepot::{
|
||||
log::{setup_logging, LogLevelParser},
|
||||
pad,
|
||||
tdx::rtmr::TdxRtmrEvent,
|
||||
};
|
||||
use tracing::{error, level_filters::LevelFilter};
|
||||
|
||||
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
|
||||
mod os {
|
||||
use anyhow::{Context as _, Result};
|
||||
use clap::Parser;
|
||||
use teepot::{
|
||||
log::{setup_logging, LogLevelParser},
|
||||
tdx::rtmr::TdxRtmrEvent,
|
||||
util::pad,
|
||||
};
|
||||
use tracing::level_filters::LevelFilter;
|
||||
|
||||
/// Extend a TDX rtmr with a hash digest for measured boot.
|
||||
#[derive(Parser, Debug)]
|
||||
#[command(author, version, about, long_about = None)]
|
||||
struct Arguments {
|
||||
/// digest in hex to extend the rtmr with
|
||||
#[arg(long)]
|
||||
digest: String,
|
||||
/// the number or the rtmr
|
||||
#[arg(long, default_value = "2")]
|
||||
rtmr: u64,
|
||||
/// Log level for the log output.
|
||||
/// Valid values are: `off`, `error`, `warn`, `info`, `debug`, `trace`
|
||||
#[clap(long, default_value_t = LevelFilter::WARN, value_parser = LogLevelParser)]
|
||||
pub log_level: LevelFilter,
|
||||
}
|
||||
|
||||
pub fn main_with_error() -> Result<()> {
|
||||
let args = Arguments::parse();
|
||||
tracing::subscriber::set_global_default(setup_logging(
|
||||
env!("CARGO_CRATE_NAME"),
|
||||
&args.log_level,
|
||||
)?)?;
|
||||
|
||||
// Parse the digest string as a hex array
|
||||
let digest_bytes = hex::decode(&args.digest).context("Invalid digest format")?;
|
||||
let extend_data: [u8; 48] = pad(&digest_bytes).context("Invalid digest length")?;
|
||||
|
||||
// Extend the TDX measurement with the extend data
|
||||
TdxRtmrEvent::default()
|
||||
.with_extend_data(extend_data)
|
||||
.with_rtmr_index(args.rtmr)
|
||||
.extend()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
/// Extend a TDX rtmr with a hash digest for measured boot.
|
||||
#[derive(Parser, Debug)]
|
||||
#[command(author, version, about, long_about = None)]
|
||||
struct Arguments {
|
||||
/// digest in hex to extend the rtmr with
|
||||
#[arg(long)]
|
||||
digest: String,
|
||||
/// the number or the rtmr
|
||||
#[arg(long, default_value = "2")]
|
||||
rtmr: u64,
|
||||
/// Log level for the log output.
|
||||
/// Valid values are: `off`, `error`, `warn`, `info`, `debug`, `trace`
|
||||
#[clap(long, default_value_t = LevelFilter::WARN, value_parser = LogLevelParser)]
|
||||
pub log_level: LevelFilter,
|
||||
}
|
||||
|
||||
#[cfg(not(all(target_os = "linux", target_arch = "x86_64")))]
|
||||
mod os {
|
||||
pub fn main_with_error() -> anyhow::Result<()> {
|
||||
anyhow::bail!("OS or architecture not supported");
|
||||
}
|
||||
fn main_with_error() -> Result<()> {
|
||||
let args = Arguments::parse();
|
||||
tracing::subscriber::set_global_default(setup_logging(
|
||||
env!("CARGO_CRATE_NAME"),
|
||||
&args.log_level,
|
||||
)?)?;
|
||||
|
||||
// Parse the digest string as a hex array
|
||||
let digest_bytes = hex::decode(&args.digest).context("Invalid digest format")?;
|
||||
let extend_data: [u8; 48] = pad(&digest_bytes);
|
||||
|
||||
// Extend the TDX measurement with the extend data
|
||||
TdxRtmrEvent::default()
|
||||
.with_extend_data(extend_data)
|
||||
.with_rtmr_index(args.rtmr)
|
||||
.extend()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
fn main() -> anyhow::Result<()> {
|
||||
let ret = os::main_with_error();
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let ret = main_with_error();
|
||||
if let Err(e) = &ret {
|
||||
error!(error = %e, "Execution failed");
|
||||
}
|
||||
|
|
|
@ -6,7 +6,6 @@ authors.workspace = true
|
|||
license.workspace = true
|
||||
repository.workspace = true
|
||||
homepage.workspace = true
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
|
|
|
@ -12,6 +12,7 @@ repository.workspace = true
|
|||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
clap.workspace = true
|
||||
rand.workspace = true
|
||||
secp256k1.workspace = true
|
||||
teepot.workspace = true
|
||||
tracing.workspace = true
|
||||
|
|
|
@ -1,15 +1,23 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (c) 2024-2025 Matter Labs
|
||||
// Copyright (c) 2024 Matter Labs
|
||||
|
||||
//! Pre-exec for binary running in a TEE needing attestation of a secret signing key
|
||||
|
||||
#![deny(missing_docs)]
|
||||
#![deny(clippy::all)]
|
||||
|
||||
use anyhow::Result;
|
||||
use anyhow::{Context, Result};
|
||||
use clap::Parser;
|
||||
use std::ffi::OsString;
|
||||
use secp256k1::{rand, Secp256k1};
|
||||
use std::{ffi::OsString, os::unix::process::CommandExt, process::Command};
|
||||
use teepot::{
|
||||
ethereum::public_key_to_ethereum_address, prover::reportdata::ReportDataV1, quote::get_quote,
|
||||
};
|
||||
use tracing::error;
|
||||
use tracing_log::LogTracer;
|
||||
use tracing_subscriber::{fmt, prelude::*, EnvFilter, Registry};
|
||||
|
||||
const TEE_QUOTE_FILE: &str = "/tmp/tee_quote";
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
#[command(author, version, about, long_about = None)]
|
||||
|
@ -22,20 +30,7 @@ struct Args {
|
|||
cmd_args: Vec<OsString>,
|
||||
}
|
||||
|
||||
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
|
||||
fn main_with_error() -> Result<()> {
|
||||
use anyhow::Context;
|
||||
use secp256k1::{rand, Secp256k1};
|
||||
use std::{os::unix::process::CommandExt, process::Command};
|
||||
use teepot::{
|
||||
ethereum::public_key_to_ethereum_address, prover::reportdata::ReportDataV1,
|
||||
quote::get_quote, tdx::rtmr::TdxRtmrEvent,
|
||||
};
|
||||
use tracing_log::LogTracer;
|
||||
use tracing_subscriber::{fmt, prelude::*, EnvFilter, Registry};
|
||||
|
||||
const TEE_QUOTE_FILE: &str = "/tmp/tee_quote";
|
||||
|
||||
LogTracer::init().context("Failed to set logger")?;
|
||||
|
||||
let subscriber = Registry::default()
|
||||
|
@ -44,33 +39,21 @@ fn main_with_error() -> Result<()> {
|
|||
tracing::subscriber::set_global_default(subscriber).context("Failed to set logger")?;
|
||||
|
||||
let args = Args::parse();
|
||||
let mut rng = rand::rng();
|
||||
let mut rng = rand::thread_rng();
|
||||
let secp = Secp256k1::new();
|
||||
let (signing_key, verifying_key) = secp.generate_keypair(&mut rng);
|
||||
let ethereum_address = public_key_to_ethereum_address(&verifying_key);
|
||||
let report_data = ReportDataV1 { ethereum_address };
|
||||
let report_data_bytes: [u8; 64] = report_data.into();
|
||||
let tee_type = match get_quote(&report_data_bytes) {
|
||||
Ok((teepot::quote::TEEType::TDX, quote)) => {
|
||||
// In the case of TDX, we want to advance RTMR 3 after getting the quote,
|
||||
// so that any breach can't generate a new attestation with the expected RTMRs
|
||||
TdxRtmrEvent::default()
|
||||
.with_rtmr_index(3)
|
||||
.with_extend_data(teepot::tdx::UEFI_MARKER_DIGEST_BYTES)
|
||||
.extend()?;
|
||||
|
||||
// save quote to file
|
||||
std::fs::write(TEE_QUOTE_FILE, quote).context(TEE_QUOTE_FILE)?;
|
||||
teepot::quote::TEEType::TDX.to_string()
|
||||
}
|
||||
Ok((tee_type, quote)) => {
|
||||
// save quote to file
|
||||
std::fs::write(TEE_QUOTE_FILE, quote).context(TEE_QUOTE_FILE)?;
|
||||
std::fs::write(TEE_QUOTE_FILE, quote)?;
|
||||
tee_type.to_string()
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Failed to get quote: {}", e);
|
||||
std::fs::write(TEE_QUOTE_FILE, []).context(TEE_QUOTE_FILE)?;
|
||||
std::fs::write(TEE_QUOTE_FILE, [])?;
|
||||
"none".to_string()
|
||||
}
|
||||
};
|
||||
|
@ -96,11 +79,6 @@ fn main_with_error() -> Result<()> {
|
|||
})
|
||||
}
|
||||
|
||||
#[cfg(not(all(target_os = "linux", target_arch = "x86_64")))]
|
||||
fn main_with_error() -> Result<()> {
|
||||
anyhow::bail!("OS or architecture not supported");
|
||||
}
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let ret = main_with_error();
|
||||
if let Err(e) = &ret {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (c) 2024-2025 Matter Labs
|
||||
// Copyright (c) 2024 Matter Labs
|
||||
|
||||
//! Pre-exec for binary running in a TEE needing attestation of a secret signing key
|
||||
|
||||
|
@ -17,7 +17,7 @@ use std::io::Write;
|
|||
use std::os::unix::process::CommandExt;
|
||||
use std::path::PathBuf;
|
||||
use std::process::Command;
|
||||
use teepot::pki::make_signed_cert;
|
||||
use teepot::server::pki::make_signed_cert;
|
||||
use tracing::error;
|
||||
use tracing_log::LogTracer;
|
||||
use tracing_subscriber::{fmt, prelude::*, EnvFilter, Registry};
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
# self-attestation-test
|
||||
|
||||
Optionally build and load the containers (remove the `ghcr.io/matter-labs/` repo from the commands below then)
|
||||
Optionally build and load the containers (remove the `matterlabsrobot/` repo from the commands below then)
|
||||
|
||||
```bash
|
||||
$ nix build -L .#container-verify-attestation-sgx && docker load -i result
|
||||
|
@ -12,9 +12,9 @@ $ nix build -L .#container-self-attestation-test-sgx-azure && docker load -i res
|
|||
|
||||
```bash
|
||||
❯ docker run -i --init --rm --privileged --device /dev/sgx_enclave \
|
||||
ghcr.io/matter-labs/teepot-self-attestation-test-sgx-azure:latest \
|
||||
matterlabsrobot/teepot-self-attestation-test-sgx-azure:latest \
|
||||
| base64 -d --ignore-garbage \
|
||||
| docker run -i --rm ghcr.io/matter-labs/verify-attestation-sgx:latest -
|
||||
| docker run -i --rm matterlabsrobot/verify-attestation-sgx:latest -
|
||||
|
||||
aesm_service: warning: Turn to daemon. Use "--no-daemon" option to execute in foreground.
|
||||
Gramine is starting. Parsing TOML manifest file, this may take some time...
|
||||
|
@ -31,9 +31,9 @@ reportdata: 00000000000000000000000000000000000000000000000000000000000000000000
|
|||
|
||||
```bash
|
||||
❯ docker run -i --init --rm --privileged --device /dev/sgx_enclave \
|
||||
ghcr.io/matter-labs/teepot-self-attestation-test-sgx-dcap:latest \
|
||||
matterlabsrobot/teepot-self-attestation-test-sgx-dcap:latest \
|
||||
| base64 -d --ignore-garbage \
|
||||
| docker run -i --rm ghcr.io/matter-labs/verify-attestation-sgx:latest -
|
||||
| docker run -i --rm matterlabsrobot/verify-attestation-sgx:latest -
|
||||
|
||||
aesm_service: warning: Turn to daemon. Use "--no-daemon" option to execute in foreground.
|
||||
Gramine is starting. Parsing TOML manifest file, this may take some time...
|
||||
|
@ -48,9 +48,9 @@ On an outdated machine, this might look like this:
|
|||
|
||||
```bash
|
||||
❯ docker run -i --init --rm --privileged --device /dev/sgx_enclave \
|
||||
ghcr.io/matter-labs/teepot-self-attestation-test-sgx-dcap:latest \
|
||||
matterlabsrobot/teepot-self-attestation-test-sgx-dcap:latest \
|
||||
| base64 -d --ignore-garbage \
|
||||
| docker run -i --rm ghcr.io/matter-labs/verify-attestation-sgx:latest -
|
||||
| docker run -i --rm matterlabsrobot/verify-attestation-sgx:latest -
|
||||
|
||||
aesm_service: warning: Turn to daemon. Use "--no-daemon" option to execute in foreground.
|
||||
Gramine is starting. Parsing TOML manifest file, this may take some time...
|
||||
|
@ -68,14 +68,3 @@ mrsigner: c5591a72b8b86e0d8814d6e8750e3efe66aea2d102b8ba2405365559b858697d
|
|||
mrenclave: 7ffe70789261a51769f50e129bfafb2aafe91a4e17c3f0d52839006777c652f6
|
||||
reportdata: 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||
```
|
||||
|
||||
## podman
|
||||
|
||||
```bash
|
||||
❯ podman run -i --rm --group-add=keep-groups -v /var/run/aesmd:/var/run/aesmd -v /dev/sgx_enclave:/dev/sgx_enclave \
|
||||
ghcr.io/matter-labs/teepot-self-attestation-test-sgx-dcap:latest \
|
||||
| base64 -d --ignore-garbage \
|
||||
| podman run -i --rm --net host \
|
||||
-v /etc/sgx_default_qcnl.conf:/etc/sgx_default_qcnl.conf \
|
||||
ghcr.io/matter-labs/verify-attestation-sgx-dcap:latest
|
||||
```
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (c) 2023-2025 Matter Labs
|
||||
// Copyright (c) 2023-2024 Matter Labs
|
||||
|
||||
//! Simple TEE self-attestation test
|
||||
|
||||
|
@ -8,7 +8,7 @@
|
|||
|
||||
use anyhow::{Context, Result};
|
||||
use base64::{engine::general_purpose, Engine as _};
|
||||
use teepot::quote::attestation::get_quote_and_collateral;
|
||||
use teepot::server::attestation::get_quote_and_collateral;
|
||||
use tracing_log::LogTracer;
|
||||
use tracing_subscriber::{fmt, prelude::*, EnvFilter, Registry};
|
||||
|
||||
|
@ -26,7 +26,7 @@ async fn main() -> Result<()> {
|
|||
.context("failed to get quote and collateral")?;
|
||||
|
||||
let base64_string = general_purpose::STANDARD.encode(report.quote.as_ref());
|
||||
print!("{base64_string}");
|
||||
print!("{}", base64_string);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -13,7 +13,6 @@ anyhow.workspace = true
|
|||
clap.workspace = true
|
||||
serde.workspace = true
|
||||
teepot.workspace = true
|
||||
teepot-vault.workspace = true
|
||||
tracing.workspace = true
|
||||
tracing-log.workspace = true
|
||||
tracing-subscriber.workspace = true
|
|
@ -1,5 +1,5 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (c) 2024-2025 Matter Labs
|
||||
// Copyright (c) 2024 Matter Labs
|
||||
|
||||
//! Server to handle requests to the Vault TEE
|
||||
|
||||
|
@ -11,17 +11,14 @@ use anyhow::{Context, Result};
|
|||
use clap::Parser;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::time::Duration;
|
||||
use teepot::quote::tcblevel::{parse_tcb_levels, EnumSet, TcbLevel};
|
||||
use teepot_vault::{
|
||||
client::vault::VaultConnection,
|
||||
server::{
|
||||
attestation::{get_quote_and_collateral, VaultAttestationArgs},
|
||||
pki::make_self_signed_cert,
|
||||
},
|
||||
};
|
||||
use teepot::client::vault::VaultConnection;
|
||||
use teepot::server::attestation::{get_quote_and_collateral, VaultAttestationArgs};
|
||||
use teepot::server::pki::make_self_signed_cert;
|
||||
use teepot::sgx::{parse_tcb_levels, EnumSet, TcbLevel};
|
||||
use tracing::{error, trace};
|
||||
use tracing_log::LogTracer;
|
||||
use tracing_subscriber::{fmt, prelude::*, EnvFilter, Registry};
|
||||
use tracing_subscriber::Registry;
|
||||
use tracing_subscriber::{fmt, prelude::*, EnvFilter};
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
#[command(author, version, about, long_about = None)]
|
|
@ -18,7 +18,6 @@ rustls.workspace = true
|
|||
serde_json.workspace = true
|
||||
sha2.workspace = true
|
||||
teepot.workspace = true
|
||||
teepot-vault.workspace = true
|
||||
tracing.workspace = true
|
||||
tracing-actix-web.workspace = true
|
||||
tracing-log.workspace = true
|
|
@ -1,5 +1,5 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (c) 2023-2025 Matter Labs
|
||||
// Copyright (c) 2023 Matter Labs
|
||||
|
||||
//! post commands
|
||||
|
||||
|
@ -9,12 +9,12 @@ use anyhow::{anyhow, Context, Result};
|
|||
use awc::http::StatusCode;
|
||||
use sha2::{Digest, Sha256};
|
||||
use std::sync::Arc;
|
||||
use teepot_vault::client::vault::VaultConnection;
|
||||
use teepot_vault::json::http::{
|
||||
use teepot::client::vault::VaultConnection;
|
||||
use teepot::json::http::{
|
||||
VaultCommandRequest, VaultCommandResponse, VaultCommands, VaultCommandsResponse,
|
||||
};
|
||||
use teepot_vault::json::secrets::{AdminConfig, AdminState};
|
||||
use teepot_vault::server::{signatures::VerifySig, HttpResponseError, Status};
|
||||
use teepot::json::secrets::{AdminConfig, AdminState};
|
||||
use teepot::server::{signatures::VerifySig, HttpResponseError, Status};
|
||||
use tracing::instrument;
|
||||
|
||||
/// Post command
|
|
@ -1,5 +1,5 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (c) 2023-2025 Matter Labs
|
||||
// Copyright (c) 2023 Matter Labs
|
||||
|
||||
//! digest
|
||||
|
||||
|
@ -9,9 +9,9 @@ use anyhow::{Context, Result};
|
|||
use awc::http::StatusCode;
|
||||
use serde_json::json;
|
||||
use std::sync::Arc;
|
||||
use teepot_vault::client::vault::VaultConnection;
|
||||
use teepot_vault::json::secrets::AdminState;
|
||||
use teepot_vault::server::{HttpResponseError, Status};
|
||||
use teepot::client::vault::VaultConnection;
|
||||
use teepot::json::secrets::AdminState;
|
||||
use teepot::server::{HttpResponseError, Status};
|
||||
use tracing::instrument;
|
||||
|
||||
/// Get last digest
|
|
@ -1,5 +1,5 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (c) 2023-2025 Matter Labs
|
||||
// Copyright (c) 2023-2024 Matter Labs
|
||||
|
||||
//! Server to handle requests to the Vault TEE
|
||||
|
||||
|
@ -9,27 +9,26 @@ mod command;
|
|||
mod digest;
|
||||
mod sign;
|
||||
|
||||
use actix_web::{web, web::Data, App, HttpServer};
|
||||
use actix_web::web::Data;
|
||||
use actix_web::{web, App, HttpServer};
|
||||
use anyhow::{Context, Result};
|
||||
use clap::Parser;
|
||||
use command::post_command;
|
||||
use digest::get_digest;
|
||||
use rustls::ServerConfig;
|
||||
use sign::post_sign;
|
||||
use std::{net::Ipv6Addr, sync::Arc};
|
||||
use teepot::quote::tcblevel::{parse_tcb_levels, EnumSet, TcbLevel};
|
||||
use teepot_vault::{
|
||||
json::http::{SignRequest, VaultCommandRequest, DIGEST_URL},
|
||||
server::{
|
||||
attestation::{get_quote_and_collateral, VaultAttestationArgs},
|
||||
new_json_cfg,
|
||||
pki::make_self_signed_cert,
|
||||
},
|
||||
};
|
||||
use std::net::Ipv6Addr;
|
||||
use std::sync::Arc;
|
||||
use teepot::json::http::{SignRequest, VaultCommandRequest, DIGEST_URL};
|
||||
use teepot::server::attestation::{get_quote_and_collateral, VaultAttestationArgs};
|
||||
use teepot::server::new_json_cfg;
|
||||
use teepot::server::pki::make_self_signed_cert;
|
||||
use teepot::sgx::{parse_tcb_levels, EnumSet, TcbLevel};
|
||||
use tracing::{error, info};
|
||||
use tracing_actix_web::TracingLogger;
|
||||
use tracing_log::LogTracer;
|
||||
use tracing_subscriber::{fmt, prelude::*, EnvFilter, Registry};
|
||||
use tracing_subscriber::Registry;
|
||||
use tracing_subscriber::{fmt, prelude::*, EnvFilter};
|
||||
|
||||
/// Server state
|
||||
pub struct ServerState {
|
||||
|
@ -71,8 +70,6 @@ async fn main() -> Result<()> {
|
|||
// don't return for now, we can still serve requests but we won't be able to attest
|
||||
}
|
||||
|
||||
let _ = rustls::crypto::ring::default_provider().install_default();
|
||||
|
||||
// init server config builder with safe defaults
|
||||
let config = ServerConfig::builder()
|
||||
.with_no_client_auth()
|
||||
|
@ -118,9 +115,9 @@ async fn main() -> Result<()> {
|
|||
#[cfg(test)]
|
||||
mod tests {
|
||||
use serde_json::json;
|
||||
use teepot_vault::json::http::{VaultCommand, VaultCommands};
|
||||
use teepot::json::http::{VaultCommand, VaultCommands};
|
||||
|
||||
const TEST_DATA: &str = include_str!("../../../tests/data/test.json");
|
||||
const TEST_DATA: &str = include_str!("../../../crates/teepot/tests/data/test.json");
|
||||
|
||||
#[test]
|
||||
fn test_vault_commands() {
|
|
@ -1,5 +1,5 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (c) 2023-2025 Matter Labs
|
||||
// Copyright (c) 2023 Matter Labs
|
||||
|
||||
//! post signing request
|
||||
|
||||
|
@ -9,14 +9,14 @@ use actix_web::web;
|
|||
use anyhow::{anyhow, Context, Result};
|
||||
use sha2::{Digest, Sha256};
|
||||
use std::sync::Arc;
|
||||
use teepot::client::vault::VaultConnection;
|
||||
use teepot::json::http::{SignRequest, SignRequestData, SignResponse};
|
||||
use teepot::json::secrets::{AdminConfig, AdminState, SGXSigningKey};
|
||||
use teepot::server::signatures::VerifySig as _;
|
||||
use teepot::server::{HttpResponseError, Status};
|
||||
use teepot::sgx::sign::PrivateKey as _;
|
||||
use teepot::sgx::sign::{Author, Signature};
|
||||
use teepot::sgx::sign::{Body, RS256PrivateKey};
|
||||
use teepot_vault::client::vault::VaultConnection;
|
||||
use teepot_vault::json::http::{SignRequest, SignRequestData, SignResponse};
|
||||
use teepot_vault::json::secrets::{AdminConfig, AdminState, SGXSigningKey};
|
||||
use teepot_vault::server::signatures::VerifySig as _;
|
||||
use teepot_vault::server::{HttpResponseError, Status};
|
||||
use tracing::instrument;
|
||||
|
||||
/// Sign command
|
|
@ -15,7 +15,6 @@ clap.workspace = true
|
|||
rustls.workspace = true
|
||||
serde_json.workspace = true
|
||||
teepot.workspace = true
|
||||
teepot-vault.workspace = true
|
||||
tracing.workspace = true
|
||||
tracing-log.workspace = true
|
||||
tracing-subscriber.workspace = true
|
|
@ -1,5 +1,5 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (c) 2023-2025 Matter Labs
|
||||
// Copyright (c) 2023-2024 Matter Labs
|
||||
|
||||
use crate::{get_vault_status, UnsealServerState, Worker};
|
||||
use actix_web::error::ErrorBadRequest;
|
||||
|
@ -7,10 +7,10 @@ use actix_web::{web, HttpResponse};
|
|||
use anyhow::{anyhow, Context, Result};
|
||||
use awc::http::StatusCode;
|
||||
use serde_json::json;
|
||||
use teepot_vault::client::TeeConnection;
|
||||
use teepot_vault::json::http::{Init, InitResponse, VaultInitRequest};
|
||||
use teepot_vault::json::secrets::AdminConfig;
|
||||
use teepot_vault::server::{HttpResponseError, Status};
|
||||
use teepot::client::TeeConnection;
|
||||
use teepot::json::http::{Init, InitResponse, VaultInitRequest};
|
||||
use teepot::json::secrets::AdminConfig;
|
||||
use teepot::server::{HttpResponseError, Status};
|
||||
use tracing::{debug, error, info, instrument, trace};
|
||||
|
||||
#[instrument(level = "info", name = "/v1/sys/init", skip_all)]
|
|
@ -1,5 +1,5 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (c) 2023-2025 Matter Labs
|
||||
// Copyright (c) 2023-2024 Matter Labs
|
||||
|
||||
//! Server to initialize and unseal the Vault TEE.
|
||||
|
||||
|
@ -9,7 +9,9 @@
|
|||
mod init;
|
||||
mod unseal;
|
||||
|
||||
use actix_web::{rt::time::sleep, web, web::Data, App, HttpServer};
|
||||
use actix_web::rt::time::sleep;
|
||||
use actix_web::web::Data;
|
||||
use actix_web::{web, App, HttpServer};
|
||||
use anyhow::{bail, Context, Result};
|
||||
use awc::Client;
|
||||
use clap::Parser;
|
||||
|
@ -21,13 +23,13 @@ use std::net::Ipv6Addr;
|
|||
use std::path::PathBuf;
|
||||
use std::sync::{Arc, RwLock};
|
||||
use std::time::Duration;
|
||||
use teepot::pki::make_self_signed_cert;
|
||||
use teepot::quote::tcblevel::{parse_tcb_levels, EnumSet, TcbLevel};
|
||||
use teepot_vault::client::{AttestationArgs, TeeConnection};
|
||||
use teepot_vault::json::http::{Init, Unseal};
|
||||
use teepot_vault::json::secrets::AdminConfig;
|
||||
use teepot_vault::server::attestation::{get_quote_and_collateral, VaultAttestationArgs};
|
||||
use teepot_vault::server::new_json_cfg;
|
||||
use teepot::client::{AttestationArgs, TeeConnection};
|
||||
use teepot::json::http::{Init, Unseal};
|
||||
use teepot::json::secrets::AdminConfig;
|
||||
use teepot::server::attestation::{get_quote_and_collateral, VaultAttestationArgs};
|
||||
use teepot::server::new_json_cfg;
|
||||
use teepot::server::pki::make_self_signed_cert;
|
||||
use teepot::sgx::{parse_tcb_levels, EnumSet, TcbLevel};
|
||||
use tracing::{error, info};
|
||||
use tracing_log::LogTracer;
|
||||
use tracing_subscriber::{fmt, prelude::*, EnvFilter, Registry};
|
||||
|
@ -134,8 +136,6 @@ async fn main() -> Result<()> {
|
|||
|
||||
let (report_data, cert_chain, priv_key) = make_self_signed_cert("CN=localhost", None)?;
|
||||
|
||||
let _ = rustls::crypto::ring::default_provider().install_default();
|
||||
|
||||
// init server config builder with safe defaults
|
||||
let config = ServerConfig::builder()
|
||||
.with_no_client_auth()
|
|
@ -1,5 +1,5 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (c) 2023-2025 Matter Labs
|
||||
// Copyright (c) 2023-2024 Matter Labs
|
||||
|
||||
use crate::{get_vault_status, UnsealServerConfig, UnsealServerState, Worker, VAULT_TOKEN_HEADER};
|
||||
use actix_web::http::StatusCode;
|
||||
|
@ -12,11 +12,11 @@ use std::fs::File;
|
|||
use std::future::Future;
|
||||
use std::io::Read;
|
||||
use std::time::Duration;
|
||||
use teepot_vault::client::vault::VaultConnection;
|
||||
use teepot_vault::client::TeeConnection;
|
||||
use teepot_vault::json::http::Unseal;
|
||||
use teepot_vault::json::secrets::{AdminConfig, AdminState};
|
||||
use teepot_vault::server::{HttpResponseError, Status};
|
||||
use teepot::client::vault::VaultConnection;
|
||||
use teepot::client::TeeConnection;
|
||||
use teepot::json::http::Unseal;
|
||||
use teepot::json::secrets::{AdminConfig, AdminState};
|
||||
use teepot::server::{HttpResponseError, Status};
|
||||
use tracing::{debug, error, info, instrument, trace};
|
||||
|
||||
#[instrument(level = "info", name = "/v1/sys/unseal", skip_all)]
|
|
@ -15,7 +15,7 @@ anyhow.workspace = true
|
|||
awc.workspace = true
|
||||
clap.workspace = true
|
||||
serde_json.workspace = true
|
||||
teepot-vault.workspace = true
|
||||
teepot.workspace = true
|
||||
tracing.workspace = true
|
||||
tracing-log.workspace = true
|
||||
tracing-subscriber.workspace = true
|
|
@ -1,5 +1,5 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (c) 2023-2025 Matter Labs
|
||||
// Copyright (c) 2023 Matter Labs
|
||||
|
||||
//! Get the secrets from a Vault TEE and pass them as environment variables to a command
|
||||
|
||||
|
@ -12,8 +12,8 @@ use serde_json::Value;
|
|||
use std::collections::HashMap;
|
||||
use std::os::unix::process::CommandExt;
|
||||
use std::process::Command;
|
||||
use teepot_vault::client::vault::VaultConnection;
|
||||
use teepot_vault::server::attestation::VaultAttestationArgs;
|
||||
use teepot::client::vault::VaultConnection;
|
||||
use teepot::server::attestation::VaultAttestationArgs;
|
||||
use tracing::{debug, info, warn};
|
||||
use tracing_log::LogTracer;
|
||||
use tracing_subscriber::{fmt, prelude::*, EnvFilter, Registry};
|
|
@ -15,7 +15,7 @@ anyhow.workspace = true
|
|||
awc.workspace = true
|
||||
clap.workspace = true
|
||||
serde_json.workspace = true
|
||||
teepot-vault.workspace = true
|
||||
teepot.workspace = true
|
||||
tracing.workspace = true
|
||||
tracing-log.workspace = true
|
||||
tracing-subscriber.workspace = true
|
|
@ -1,5 +1,5 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (c) 2023-2025 Matter Labs
|
||||
// Copyright (c) 2023 Matter Labs
|
||||
|
||||
//! Write secrets to a Vault TEE from environment variables
|
||||
|
||||
|
@ -9,8 +9,10 @@
|
|||
use anyhow::{Context, Result};
|
||||
use clap::Parser;
|
||||
use serde_json::Value;
|
||||
use std::{collections::HashMap, env};
|
||||
use teepot_vault::{client::vault::VaultConnection, server::attestation::VaultAttestationArgs};
|
||||
use std::collections::HashMap;
|
||||
use std::env;
|
||||
use teepot::client::vault::VaultConnection;
|
||||
use teepot::server::attestation::VaultAttestationArgs;
|
||||
use tracing::{debug, info, warn};
|
||||
use tracing_log::LogTracer;
|
||||
use tracing_subscriber::{fmt, prelude::*, EnvFilter, Registry};
|
|
@ -16,5 +16,4 @@ hex.workspace = true
|
|||
pgp.workspace = true
|
||||
serde_json.workspace = true
|
||||
teepot.workspace = true
|
||||
teepot-vault.workspace = true
|
||||
tracing.workspace = true
|
|
@ -9,12 +9,12 @@
|
|||
Verified signature for `81A312C59D679D930FA9E8B06D728F29A2DBABF8`
|
||||
|
||||
❯ RUST_LOG=info cargo run -p vault-admin -- \
|
||||
command \
|
||||
send \
|
||||
--sgx-mrsigner c5591a72b8b86e0d8814d6e8750e3efe66aea2d102b8ba2405365559b858697d \
|
||||
--sgx-allowed-tcb-levels SwHardeningNeeded \
|
||||
--server https://127.0.0.1:8444 \
|
||||
crates/teepot-vault/tests/data/test.json \
|
||||
crates/teepot-vault/tests/data/test.json.asc
|
||||
bin/tee-vault-admin/tests/data/test.json \
|
||||
bin/tee-vault-admin/tests/data/test.json.asc
|
||||
|
||||
2023-08-04T10:51:14.919941Z INFO vault_admin: Quote verified! Connection secure!
|
||||
2023-08-04T10:51:14.920430Z INFO tee_client: Getting attestation report
|
|
@ -1,12 +1,9 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (c) 2023-2025 Matter Labs
|
||||
// Copyright (c) 2023-2024 Matter Labs
|
||||
|
||||
use anyhow::{anyhow, bail, Context, Result};
|
||||
use clap::{Args, Parser, Subcommand};
|
||||
use pgp::{
|
||||
composed::{Deserializable, SignedPublicKey},
|
||||
types::KeyDetails,
|
||||
};
|
||||
use pgp::{types::PublicKeyTrait, Deserializable, SignedPublicKey};
|
||||
use serde_json::Value;
|
||||
use std::{
|
||||
default::Default,
|
||||
|
@ -15,18 +12,16 @@ use std::{
|
|||
path::{Path, PathBuf},
|
||||
};
|
||||
use teepot::{
|
||||
log::{setup_logging, LogLevelParser},
|
||||
sgx::sign::Signature,
|
||||
};
|
||||
use teepot_vault::{
|
||||
client::{AttestationArgs, TeeConnection},
|
||||
json::http::{
|
||||
SignRequest, SignRequestData, SignResponse, VaultCommandRequest, VaultCommands,
|
||||
VaultCommandsResponse, DIGEST_URL,
|
||||
},
|
||||
log::{setup_logging, LogLevelParser},
|
||||
server::signatures::verify_sig,
|
||||
sgx::sign::Signature,
|
||||
};
|
||||
use tracing::{error, level_filters::LevelFilter};
|
||||
use tracing::{error, info, level_filters::LevelFilter};
|
||||
|
||||
#[derive(Args, Debug)]
|
||||
struct SendArgs {
|
||||
|
@ -122,6 +117,8 @@ async fn main() -> Result<()> {
|
|||
&args.log_level,
|
||||
)?)?;
|
||||
|
||||
info!("Quote verified! Connection secure!");
|
||||
|
||||
match args.cmd {
|
||||
SubCommands::Command(args) => send_commands(args).await?,
|
||||
SubCommands::SignTee(args) => send_sig_request(args).await?,
|
|
@ -13,7 +13,7 @@ anyhow.workspace = true
|
|||
base64.workspace = true
|
||||
clap.workspace = true
|
||||
serde_json.workspace = true
|
||||
teepot-vault.workspace = true
|
||||
teepot.workspace = true
|
||||
tracing.workspace = true
|
||||
tracing-log.workspace = true
|
||||
tracing-subscriber.workspace = true
|
|
@ -1,18 +1,18 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (c) 2023-2025 Matter Labs
|
||||
// Copyright (c) 2023-2024 Matter Labs
|
||||
|
||||
use anyhow::{anyhow, bail, Context, Result};
|
||||
use base64::{engine::general_purpose, Engine as _};
|
||||
use clap::{Args, Parser, Subcommand};
|
||||
use serde_json::Value;
|
||||
use std::{fs::File, io::Read};
|
||||
use teepot_vault::{
|
||||
client::{AttestationArgs, TeeConnection},
|
||||
json::http::{Init, InitResponse, Unseal},
|
||||
};
|
||||
use std::fs::File;
|
||||
use std::io::Read;
|
||||
use teepot::client::{AttestationArgs, TeeConnection};
|
||||
use teepot::json::http::{Init, InitResponse, Unseal};
|
||||
use tracing::{error, info, trace, warn};
|
||||
use tracing_log::LogTracer;
|
||||
use tracing_subscriber::{fmt, prelude::*, EnvFilter, Registry};
|
||||
use tracing_subscriber::Registry;
|
||||
use tracing_subscriber::{fmt, prelude::*, EnvFilter};
|
||||
|
||||
#[derive(Args, Debug)]
|
||||
pub struct InitArgs {
|
|
@ -10,4 +10,7 @@ repository.workspace = true
|
|||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
clap.workspace = true
|
||||
hex.workspace = true
|
||||
secp256k1.workspace = true
|
||||
teepot.workspace = true
|
||||
zksync_basic_types.workspace = true
|
||||
|
|
|
@ -1,13 +1,21 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (c) 2023-2025 Matter Labs
|
||||
// Copyright (c) 2023-2024 Matter Labs
|
||||
|
||||
//! Tool for SGX attestation and batch signature verification
|
||||
|
||||
use anyhow::{bail, Context, Result};
|
||||
use clap::Parser;
|
||||
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use clap::{Args, Parser, Subcommand};
|
||||
use core::convert::TryInto;
|
||||
use hex::encode;
|
||||
use secp256k1::Message;
|
||||
use std::{fs, io::Read, path::PathBuf, str::FromStr, time::UNIX_EPOCH};
|
||||
use teepot::quote::{get_collateral, verify_quote_with_collateral, QuoteVerificationResult};
|
||||
use teepot::{
|
||||
client::TcbLevel,
|
||||
ethereum::recover_signer,
|
||||
prover::reportdata::ReportData,
|
||||
quote::{error, tee_qv_get_collateral, verify_quote_with_collateral, QuoteVerificationResult},
|
||||
};
|
||||
use zksync_basic_types::H256;
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
#[command(author = "Matter Labs", version, about = "SGX attestation and batch signature verifier", long_about = None)]
|
||||
|
@ -15,6 +23,9 @@ struct Arguments {
|
|||
/// Attestation quote proving the signature originated from a TEE enclave.
|
||||
#[clap(name = "attestation_file", value_parser)]
|
||||
attestation: ArgSource,
|
||||
/// An optional subcommand, for instance, for optional signature verification.
|
||||
#[clap(subcommand)]
|
||||
command: Option<SubCommands>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
|
@ -34,6 +45,22 @@ impl FromStr for ArgSource {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Args, Debug)]
|
||||
struct SignatureArgs {
|
||||
/// File containing a batch signature signed within a TEE enclave.
|
||||
#[arg(long)]
|
||||
signature_file: PathBuf,
|
||||
/// Batch root hash for signature verification.
|
||||
#[arg(long)]
|
||||
root_hash: H256,
|
||||
}
|
||||
|
||||
#[derive(Subcommand, Debug)]
|
||||
enum SubCommands {
|
||||
/// Verify a batch signature signed within a TEE enclave.
|
||||
SignVerify(SignatureArgs),
|
||||
}
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let args = Arguments::parse();
|
||||
let attestation_quote_bytes = match args.attestation {
|
||||
|
@ -48,18 +75,52 @@ fn main() -> Result<()> {
|
|||
};
|
||||
let quote_verification_result = verify_attestation_quote(&attestation_quote_bytes)?;
|
||||
print_quote_verification_summary("e_verification_result);
|
||||
match &args.command {
|
||||
Some(SubCommands::SignVerify(signature_args)) => {
|
||||
verify_signature("e_verification_result, signature_args)?;
|
||||
}
|
||||
None => {}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn verify_signature(
|
||||
quote_verification_result: &QuoteVerificationResult,
|
||||
signature_args: &SignatureArgs,
|
||||
) -> Result<()> {
|
||||
let report_data = ReportData::try_from(quote_verification_result.quote.get_report_data())?;
|
||||
let ethereum_address_from_quote = match report_data {
|
||||
ReportData::V1(report_data_v1) => report_data_v1.ethereum_address,
|
||||
_ => return Err(anyhow!("Unsupported report data version")),
|
||||
};
|
||||
let signature_bytes: &[u8] = &fs::read(&signature_args.signature_file)?;
|
||||
let root_hash = Message::from_digest_slice(signature_args.root_hash.as_bytes())?;
|
||||
let ethereum_address_from_signature = recover_signer(&signature_bytes.try_into()?, &root_hash)?;
|
||||
let verification_successful = ethereum_address_from_signature == ethereum_address_from_quote;
|
||||
|
||||
println!(
|
||||
"Signature '{}' {}. Ethereum address from attestation quote: {}. Ethereum address from signature: {}.",
|
||||
encode(signature_bytes),
|
||||
if verification_successful {
|
||||
"verified successfully"
|
||||
} else {
|
||||
"verification failed"
|
||||
},
|
||||
encode(ethereum_address_from_quote),
|
||||
encode(ethereum_address_from_signature)
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn verify_attestation_quote(attestation_quote_bytes: &[u8]) -> Result<QuoteVerificationResult> {
|
||||
if attestation_quote_bytes.is_empty() {
|
||||
bail!("Empty quote provided!");
|
||||
}
|
||||
println!(
|
||||
"Verifying quote ({} bytes)...",
|
||||
attestation_quote_bytes.len()
|
||||
);
|
||||
let collateral = get_collateral(attestation_quote_bytes)?;
|
||||
let collateral = error::QuoteContext::context(
|
||||
tee_qv_get_collateral(attestation_quote_bytes),
|
||||
"Failed to get collateral",
|
||||
)?;
|
||||
let unix_time: i64 = std::time::SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)?
|
||||
.as_secs() as _;
|
||||
|
@ -70,7 +131,7 @@ fn verify_attestation_quote(attestation_quote_bytes: &[u8]) -> Result<QuoteVerif
|
|||
fn print_quote_verification_summary(quote_verification_result: &QuoteVerificationResult) {
|
||||
let QuoteVerificationResult {
|
||||
collateral_expired,
|
||||
result: tcblevel,
|
||||
result,
|
||||
quote,
|
||||
advisories,
|
||||
..
|
||||
|
@ -78,10 +139,11 @@ fn print_quote_verification_summary(quote_verification_result: &QuoteVerificatio
|
|||
if *collateral_expired {
|
||||
println!("Freshly fetched collateral expired");
|
||||
}
|
||||
let tcblevel = TcbLevel::from(*result);
|
||||
for advisory in advisories {
|
||||
println!("\tInfo: Advisory ID: {advisory}");
|
||||
}
|
||||
println!("Quote verification result: {tcblevel}");
|
||||
println!("Quote verification result: {}", tcblevel);
|
||||
|
||||
println!("{:#}", "e.report);
|
||||
}
|
||||
|
|
|
@ -8,21 +8,16 @@ repository.workspace = true
|
|||
version.workspace = true
|
||||
|
||||
[dependencies]
|
||||
bytes.workspace = true
|
||||
anyhow.workspace = true
|
||||
clap.workspace = true
|
||||
enumset.workspace = true
|
||||
hex.workspace = true
|
||||
jsonrpsee-types.workspace = true
|
||||
reqwest.workspace = true
|
||||
secp256k1.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
serde_with = { workspace = true, features = ["hex"] }
|
||||
serde_yaml.workspace = true
|
||||
teepot.workspace = true
|
||||
thiserror.workspace = true
|
||||
tokio.workspace = true
|
||||
tokio-util.workspace = true
|
||||
tracing.workspace = true
|
||||
tracing-subscriber.workspace = true
|
||||
url.workspace = true
|
||||
|
|
|
@ -1,76 +0,0 @@
|
|||
# Era Proof Attestation Verifier
|
||||
|
||||
This tool verifies the SGX/TDX attestations and signatures for zkSync Era L1 batches.
|
||||
|
||||
## Usage
|
||||
|
||||
Basic usage with attestation policy provided from a YAML file:
|
||||
|
||||
```bash
|
||||
verify-era-proof-attestation --rpc https://mainnet.era.zksync.io \
|
||||
--continuous 493220 \
|
||||
--attestation-policy-file examples/attestation_policy.yaml \
|
||||
--log-level info
|
||||
```
|
||||
|
||||
## Attestation Policy Configuration
|
||||
|
||||
You can specify the attestation policy either through command-line arguments or by providing a YAML configuration file.
|
||||
|
||||
### Command-line Arguments
|
||||
|
||||
The following command-line arguments are available:
|
||||
|
||||
- `--batch`, `-n <BATCH>`: The batch number or range of batch numbers to verify the attestation and signature (e.g., "
|
||||
42" or "42-45"). Mutually exclusive with `--continuous`.
|
||||
- `--continuous <FIRST_BATCH>`: Continuous mode: keep verifying new batches starting from the specified batch number
|
||||
until interrupted. Mutually exclusive with `--batch`.
|
||||
- `--rpc <URL>`: URL of the RPC server to query for the batch attestation and signature.
|
||||
- `--chain <CHAIN_ID>`: Chain ID of the network to query (default: L2ChainId::default()).
|
||||
- `--rate-limit <MILLISECONDS>`: Rate limit between requests in milliseconds (default: 0).
|
||||
- `--log-level <LEVEL>`: Log level for the log output. Valid values are: `off`, `error`, `warn`, `info`, `debug`,
|
||||
`trace` (default: `warn`).
|
||||
- `--attestation-policy-file <PATH>`: Path to a YAML file containing attestation policy configuration. This overrides
|
||||
any attestation policy settings provided via command line options.
|
||||
|
||||
Either `--batch` or `--continuous` mode must be specified.
|
||||
|
||||
### YAML Configuration File
|
||||
|
||||
The attestation policy is loaded from a YAML file using the `--attestation-policy-file` option.
|
||||
|
||||
Example YAML configuration file:
|
||||
|
||||
```yaml
|
||||
sgx:
|
||||
mrenclaves:
|
||||
- a2caa7055e333f69c3e46ca7ba65b135a86c90adfde2afb356e05075b7818b3c
|
||||
- 36eeb64cc816f80a1cf5818b26710f360714b987d3799e757cbefba7697b9589
|
||||
- 4a8b79e5123f4dbf23453d583cb8e5dcf4d19a6191a0be6dd85b7b3052c32faf
|
||||
- 1498845b3f23667356cc49c38cae7b4ac234621a5b85fdd5c52b5f5d12703ec9
|
||||
- 1b2374631bb2572a0e05b3be8b5cdd23c42e9d7551e1ef200351cae67c515a65
|
||||
- 6fb19e47d72a381a9f3235c450f8c40f01428ce19a941f689389be3eac24f42a
|
||||
- b610fd1d749775cc3de88beb84afe8bb79f55a19100db12d76f6a62ac576e35d
|
||||
- a0b1b069b01bdcf3c1517ef8d4543794a27ed4103e464be7c4afdc6136b42d66
|
||||
- 71e2a11a74b705082a7286b2008f812f340c0e4de19f8b151baa347eda32d057
|
||||
- d5a0bf8932d9a3d7af6d9405d4c6de7dcb7b720bb5510666b4396fc58ee58bb2
|
||||
allowed_tcb_levels:
|
||||
- Ok
|
||||
- SwHardeningNeeded
|
||||
allowed_advisory_ids:
|
||||
- INTEL-SA-00615
|
||||
tdx:
|
||||
mrs:
|
||||
- - 2a90c8fa38672cafd791d994beb6836b99383b2563736858632284f0f760a6446efd1e7ec457cf08b629ea630f7b4525
|
||||
- 3300980705adf09d28b707b79699d9874892164280832be2c386a715b6e204e0897fb564a064f810659207ba862b304f
|
||||
- c08ab64725566bcc8a6fb1c79e2e64744fcff1594b8f1f02d716fb66592ecd5de94933b2bc54ffbbc43a52aab7eb1146
|
||||
- 092a4866a9e6a1672d7439a5d106fbc6eb57b738d5bfea5276d41afa2551824365fdd66700c1ce9c0b20542b9f9d5945
|
||||
- 971fb52f90ec98a234301ca9b8fc30b613c33e3dd9c0cc42dcb8003d4a95d8fb218b75baf028b70a3cabcb947e1ca453
|
||||
- - 2a90c8fa38672cafd791d994beb6836b99383b2563736858632284f0f760a6446efd1e7ec457cf08b629ea630f7b4525
|
||||
- 3300980705adf09d28b707b79699d9874892164280832be2c386a715b6e204e0897fb564a064f810659207ba862b304f
|
||||
- c08ab64725566bcc8a6fb1c79e2e64744fcff1594b8f1f02d716fb66592ecd5de94933b2bc54ffbbc43a52aab7eb1146
|
||||
- 092a4866a9e6a1672d7439a5d106fbc6eb57b738d5bfea5276d41afa2551824365fdd66700c1ce9c0b20542b9f9d5945
|
||||
- f57bb7ed82c6ae4a29e6c9879338c592c7d42a39135583e8ccbe3940f2344b0eb6eb8503db0ffd6a39ddd00cd07d8317
|
||||
allowed_tcb_levels:
|
||||
- Ok
|
||||
```
|
|
@ -1,31 +0,0 @@
|
|||
sgx:
|
||||
mrenclaves:
|
||||
- a2caa7055e333f69c3e46ca7ba65b135a86c90adfde2afb356e05075b7818b3c
|
||||
- 36eeb64cc816f80a1cf5818b26710f360714b987d3799e757cbefba7697b9589
|
||||
- 4a8b79e5123f4dbf23453d583cb8e5dcf4d19a6191a0be6dd85b7b3052c32faf
|
||||
- 1498845b3f23667356cc49c38cae7b4ac234621a5b85fdd5c52b5f5d12703ec9
|
||||
- 1b2374631bb2572a0e05b3be8b5cdd23c42e9d7551e1ef200351cae67c515a65
|
||||
- 6fb19e47d72a381a9f3235c450f8c40f01428ce19a941f689389be3eac24f42a
|
||||
- b610fd1d749775cc3de88beb84afe8bb79f55a19100db12d76f6a62ac576e35d
|
||||
- a0b1b069b01bdcf3c1517ef8d4543794a27ed4103e464be7c4afdc6136b42d66
|
||||
- 71e2a11a74b705082a7286b2008f812f340c0e4de19f8b151baa347eda32d057
|
||||
- d5a0bf8932d9a3d7af6d9405d4c6de7dcb7b720bb5510666b4396fc58ee58bb2
|
||||
allowed_tcb_levels:
|
||||
- Ok
|
||||
- SwHardeningNeeded
|
||||
allowed_advisory_ids:
|
||||
- INTEL-SA-00615
|
||||
tdx:
|
||||
mrs:
|
||||
- - 2a90c8fa38672cafd791d994beb6836b99383b2563736858632284f0f760a6446efd1e7ec457cf08b629ea630f7b4525
|
||||
- 3300980705adf09d28b707b79699d9874892164280832be2c386a715b6e204e0897fb564a064f810659207ba862b304f
|
||||
- c08ab64725566bcc8a6fb1c79e2e64744fcff1594b8f1f02d716fb66592ecd5de94933b2bc54ffbbc43a52aab7eb1146
|
||||
- 092a4866a9e6a1672d7439a5d106fbc6eb57b738d5bfea5276d41afa2551824365fdd66700c1ce9c0b20542b9f9d5945
|
||||
- 971fb52f90ec98a234301ca9b8fc30b613c33e3dd9c0cc42dcb8003d4a95d8fb218b75baf028b70a3cabcb947e1ca453
|
||||
- - 2a90c8fa38672cafd791d994beb6836b99383b2563736858632284f0f760a6446efd1e7ec457cf08b629ea630f7b4525
|
||||
- 3300980705adf09d28b707b79699d9874892164280832be2c386a715b6e204e0897fb564a064f810659207ba862b304f
|
||||
- c08ab64725566bcc8a6fb1c79e2e64744fcff1594b8f1f02d716fb66592ecd5de94933b2bc54ffbbc43a52aab7eb1146
|
||||
- 092a4866a9e6a1672d7439a5d106fbc6eb57b738d5bfea5276d41afa2551824365fdd66700c1ce9c0b20542b9f9d5945
|
||||
- f57bb7ed82c6ae4a29e6c9879338c592c7d42a39135583e8ccbe3940f2344b0eb6eb8503db0ffd6a39ddd00cd07d8317
|
||||
allowed_tcb_levels:
|
||||
- Ok
|
95
bin/verify-era-proof-attestation/src/args.rs
Normal file
95
bin/verify-era-proof-attestation/src/args.rs
Normal file
|
@ -0,0 +1,95 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (c) 2023-2024 Matter Labs
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
use clap::{ArgGroup, Args, Parser};
|
||||
use std::time::Duration;
|
||||
use teepot::log::LogLevelParser;
|
||||
use teepot::sgx::{parse_tcb_levels, EnumSet, TcbLevel};
|
||||
use tracing_subscriber::filter::LevelFilter;
|
||||
use url::Url;
|
||||
use zksync_basic_types::L1BatchNumber;
|
||||
use zksync_types::L2ChainId;
|
||||
|
||||
#[derive(Parser, Debug, Clone)]
|
||||
#[command(author = "Matter Labs", version, about = "SGX attestation and batch signature verifier", long_about = None)]
|
||||
#[clap(group(
|
||||
ArgGroup::new("mode")
|
||||
.required(true)
|
||||
.args(&["batch_range", "continuous"]),
|
||||
))]
|
||||
pub struct Arguments {
|
||||
/// Log level for the log output.
|
||||
/// Valid values are: `off`, `error`, `warn`, `info`, `debug`, `trace`
|
||||
#[clap(long, default_value_t = LevelFilter::WARN, value_parser = LogLevelParser)]
|
||||
pub log_level: LevelFilter,
|
||||
/// The batch number or range of batch numbers to verify the attestation and signature (e.g.,
|
||||
/// "42" or "42-45"). This option is mutually exclusive with the `--continuous` mode.
|
||||
#[clap(short = 'n', long = "batch", value_parser = parse_batch_range)]
|
||||
pub batch_range: Option<(L1BatchNumber, L1BatchNumber)>,
|
||||
/// Continuous mode: keep verifying new batches until interrupted. This option is mutually
|
||||
/// exclusive with the `--batch` option.
|
||||
#[clap(long, value_name = "FIRST_BATCH")]
|
||||
pub continuous: Option<L1BatchNumber>,
|
||||
/// URL of the RPC server to query for the batch attestation and signature.
|
||||
#[clap(long = "rpc")]
|
||||
pub rpc_url: Url,
|
||||
/// Chain ID of the network to query.
|
||||
#[clap(long = "chain", default_value_t = L2ChainId::default().as_u64())]
|
||||
pub chain_id: u64,
|
||||
/// Rate limit between requests in milliseconds.
|
||||
#[clap(long, default_value = "0", value_parser = parse_duration)]
|
||||
pub rate_limit: Duration,
|
||||
/// Criteria for valid attestation policy. Invalid proofs will be rejected.
|
||||
#[clap(flatten)]
|
||||
pub attestation_policy: AttestationPolicyArgs,
|
||||
}
|
||||
|
||||
/// Attestation policy implemented as a set of criteria that must be met by SGX attestation.
|
||||
#[derive(Args, Debug, Clone)]
|
||||
pub struct AttestationPolicyArgs {
|
||||
/// Comma-separated list of allowed hex-encoded SGX mrsigners. Batch attestation must consist of
|
||||
/// one of these mrsigners. If the list is empty, the mrsigner check is skipped.
|
||||
#[arg(long = "mrsigners")]
|
||||
pub sgx_mrsigners: Option<String>,
|
||||
/// Comma-separated list of allowed hex-encoded SGX mrenclaves. Batch attestation must consist
|
||||
/// of one of these mrenclaves. If the list is empty, the mrenclave check is skipped.
|
||||
#[arg(long = "mrenclaves")]
|
||||
pub sgx_mrenclaves: Option<String>,
|
||||
/// Comma-separated list of allowed TCB levels. If the list is empty, the TCB level check is
|
||||
/// skipped. Allowed values: Ok, ConfigNeeded, ConfigAndSwHardeningNeeded, SwHardeningNeeded,
|
||||
/// OutOfDate, OutOfDateConfigNeeded.
|
||||
#[arg(long, value_parser = parse_tcb_levels, default_value = "Ok")]
|
||||
pub sgx_allowed_tcb_levels: EnumSet<TcbLevel>,
|
||||
}
|
||||
|
||||
fn parse_batch_range(s: &str) -> Result<(L1BatchNumber, L1BatchNumber)> {
|
||||
let parse = |s: &str| {
|
||||
s.parse::<u32>()
|
||||
.map(L1BatchNumber::from)
|
||||
.map_err(|e| anyhow!(e))
|
||||
};
|
||||
match s.split_once('-') {
|
||||
Some((start, end)) => {
|
||||
let (start, end) = (parse(start)?, parse(end)?);
|
||||
if start > end {
|
||||
Err(anyhow!(
|
||||
"Start batch number ({}) must be less than or equal to end batch number ({})",
|
||||
start,
|
||||
end
|
||||
))
|
||||
} else {
|
||||
Ok((start, end))
|
||||
}
|
||||
}
|
||||
None => {
|
||||
let batch_number = parse(s)?;
|
||||
Ok((batch_number, batch_number))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_duration(s: &str) -> Result<Duration> {
|
||||
let millis = s.parse()?;
|
||||
Ok(Duration::from_millis(millis))
|
||||
}
|
45
bin/verify-era-proof-attestation/src/client.rs
Normal file
45
bin/verify-era-proof-attestation/src/client.rs
Normal file
|
@ -0,0 +1,45 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (c) 2023-2024 Matter Labs
|
||||
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use url::Url;
|
||||
use zksync_basic_types::{L1BatchNumber, H256};
|
||||
use zksync_types::L2ChainId;
|
||||
use zksync_web3_decl::{
|
||||
client::{Client as NodeClient, L2},
|
||||
error::ClientRpcContext,
|
||||
namespaces::ZksNamespaceClient,
|
||||
};
|
||||
|
||||
pub trait JsonRpcClient {
|
||||
async fn get_root_hash(&self, batch_number: L1BatchNumber) -> Result<H256>;
|
||||
// TODO implement get_tee_proofs(batch_number, tee_type) once https://crates.io/crates/zksync_web3_decl crate is updated
|
||||
}
|
||||
|
||||
pub struct MainNodeClient(NodeClient<L2>);
|
||||
|
||||
impl MainNodeClient {
|
||||
pub fn new(rpc_url: Url, chain_id: u64) -> Result<Self> {
|
||||
let node_client = NodeClient::http(rpc_url.into())
|
||||
.context("failed creating JSON-RPC client for main node")?
|
||||
.for_network(
|
||||
L2ChainId::try_from(chain_id)
|
||||
.map_err(anyhow::Error::msg)?
|
||||
.into(),
|
||||
)
|
||||
.build();
|
||||
|
||||
Ok(MainNodeClient(node_client))
|
||||
}
|
||||
}
|
||||
|
||||
impl JsonRpcClient for MainNodeClient {
|
||||
async fn get_root_hash(&self, batch_number: L1BatchNumber) -> Result<H256> {
|
||||
self.0
|
||||
.get_l1_batch_details(batch_number)
|
||||
.rpc_context("get_l1_batch_details")
|
||||
.await?
|
||||
.and_then(|res| res.base.root_hash)
|
||||
.ok_or_else(|| anyhow!("No root hash found for batch #{}", batch_number))
|
||||
}
|
||||
}
|
|
@ -1,66 +0,0 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (c) 2023-2025 Matter Labs
|
||||
|
||||
//! HTTP client for making requests to external services
|
||||
|
||||
use reqwest::Client;
|
||||
use serde::{de::DeserializeOwned, Serialize};
|
||||
use std::time::Duration;
|
||||
use url::Url;
|
||||
|
||||
use crate::{
|
||||
core::DEFAULT_HTTP_REQUEST_TIMEOUT,
|
||||
error::{Error, Result},
|
||||
};
|
||||
|
||||
/// Client for making HTTP requests
|
||||
#[derive(Clone)]
|
||||
pub struct HttpClient {
|
||||
client: Client,
|
||||
}
|
||||
|
||||
impl HttpClient {
|
||||
/// Create a new HTTP client with default configuration
|
||||
pub fn new() -> Self {
|
||||
let client = Client::builder()
|
||||
.timeout(Duration::from_secs(DEFAULT_HTTP_REQUEST_TIMEOUT))
|
||||
.build()
|
||||
.expect("Failed to create HTTP client");
|
||||
|
||||
Self { client }
|
||||
}
|
||||
|
||||
/// Make a POST request to the specified URL with the provided body
|
||||
pub async fn post<T: Serialize>(&self, url: &Url, body: T) -> Result<String> {
|
||||
let response = self.client.post(url.clone()).json(&body).send().await?;
|
||||
self.handle_response(response).await
|
||||
}
|
||||
|
||||
/// Send a JSON request and parse the response
|
||||
pub async fn send_json<T: Serialize, R: DeserializeOwned>(
|
||||
&self,
|
||||
url: &Url,
|
||||
body: T,
|
||||
) -> Result<R> {
|
||||
let response_text = self.post(url, body).await?;
|
||||
let response: R = serde_json::from_str(&response_text)
|
||||
.map_err(|e| Error::JsonRpcInvalidResponse(e.to_string()))?;
|
||||
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
/// Handle the HTTP response
|
||||
async fn handle_response(&self, response: reqwest::Response) -> Result<String> {
|
||||
let status = response.status();
|
||||
let body = response.text().await?;
|
||||
|
||||
if status.is_success() {
|
||||
Ok(body)
|
||||
} else {
|
||||
Err(Error::Http {
|
||||
status_code: status.as_u16(),
|
||||
message: body,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,56 +0,0 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (c) 2023-2025 Matter Labs
|
||||
|
||||
use url::Url;
|
||||
use zksync_basic_types::{L1BatchNumber, H256};
|
||||
use zksync_types::L2ChainId;
|
||||
use zksync_web3_decl::{
|
||||
client::{Client as NodeClient, L2},
|
||||
error::ClientRpcContext,
|
||||
namespaces::ZksNamespaceClient,
|
||||
};
|
||||
|
||||
use crate::error;
|
||||
|
||||
/// Trait for interacting with the JSON-RPC API
|
||||
pub trait JsonRpcClient {
|
||||
/// Get the root hash for a specific batch
|
||||
async fn get_root_hash(&self, batch_number: L1BatchNumber) -> error::Result<H256>;
|
||||
// TODO implement get_tee_proofs(batch_number, tee_type) once https://crates.io/crates/zksync_web3_decl crate is updated
|
||||
}
|
||||
|
||||
/// Client for interacting with the main node
|
||||
pub struct MainNodeClient(NodeClient<L2>);
|
||||
|
||||
impl MainNodeClient {
|
||||
/// Create a new client for the main node
|
||||
pub fn new(rpc_url: Url, chain_id: u64) -> error::Result<Self> {
|
||||
let chain_id = L2ChainId::try_from(chain_id)
|
||||
.map_err(|e| error::Error::Internal(format!("Invalid chain ID: {e}")))?;
|
||||
|
||||
let node_client = NodeClient::http(rpc_url.into())
|
||||
.map_err(|e| error::Error::Internal(format!("Failed to create JSON-RPC client: {e}")))?
|
||||
.for_network(chain_id.into())
|
||||
.build();
|
||||
|
||||
Ok(MainNodeClient(node_client))
|
||||
}
|
||||
}
|
||||
|
||||
impl JsonRpcClient for MainNodeClient {
|
||||
async fn get_root_hash(&self, batch_number: L1BatchNumber) -> error::Result<H256> {
|
||||
let batch_details = self
|
||||
.0
|
||||
.get_l1_batch_details(batch_number)
|
||||
.rpc_context("get_l1_batch_details")
|
||||
.await
|
||||
.map_err(|e| error::Error::JsonRpc(format!("Failed to get batch details: {e}")))?
|
||||
.ok_or_else(|| {
|
||||
error::Error::JsonRpc(format!("No details found for batch #{batch_number}"))
|
||||
})?;
|
||||
|
||||
batch_details.base.root_hash.ok_or_else(|| {
|
||||
error::Error::JsonRpc(format!("No root hash found for batch #{batch_number}"))
|
||||
})
|
||||
}
|
||||
}
|
|
@ -1,12 +0,0 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (c) 2023-2025 Matter Labs
|
||||
|
||||
//! Client modules for external API communication
|
||||
|
||||
mod http;
|
||||
mod json_rpc;
|
||||
mod retry;
|
||||
|
||||
pub use http::HttpClient;
|
||||
pub use json_rpc::{JsonRpcClient, MainNodeClient};
|
||||
pub use retry::{RetryConfig, RetryHelper};
|
|
@ -1,107 +0,0 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (c) 2023-2025 Matter Labs
|
||||
|
||||
//! Retry mechanism for handling transient failures
|
||||
|
||||
use std::time::Duration;
|
||||
use tokio::time::sleep;
|
||||
|
||||
use crate::{
|
||||
core::{DEFAULT_RETRY_DELAY_MS, MAX_PROOF_FETCH_RETRIES},
|
||||
error::{Error, Result},
|
||||
};
|
||||
|
||||
/// Configuration for retry behavior
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct RetryConfig {
|
||||
/// Maximum number of retry attempts
|
||||
pub max_attempts: u32,
|
||||
/// Delay between retry attempts
|
||||
pub delay: Duration,
|
||||
/// Whether to use exponential backoff
|
||||
pub use_exponential_backoff: bool,
|
||||
}
|
||||
|
||||
impl Default for RetryConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
max_attempts: MAX_PROOF_FETCH_RETRIES,
|
||||
delay: Duration::from_millis(DEFAULT_RETRY_DELAY_MS),
|
||||
use_exponential_backoff: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper for executing operations with retries
|
||||
pub struct RetryHelper {
|
||||
config: RetryConfig,
|
||||
}
|
||||
|
||||
impl RetryHelper {
|
||||
/// Create a new retry helper with the given configuration
|
||||
pub fn new(config: RetryConfig) -> Self {
|
||||
Self { config }
|
||||
}
|
||||
|
||||
/// Execute an operation with retries
|
||||
pub async fn execute<T, F, Fut>(&self, operation_name: &str, operation: F) -> Result<T>
|
||||
where
|
||||
F: Fn() -> Fut,
|
||||
Fut: std::future::Future<Output = Result<T>>,
|
||||
{
|
||||
let mut attempt = 0;
|
||||
let mut last_error;
|
||||
|
||||
loop {
|
||||
attempt += 1;
|
||||
tracing::debug!(
|
||||
"Executing operation '{}' (attempt {}/{})",
|
||||
operation_name,
|
||||
attempt,
|
||||
self.config.max_attempts
|
||||
);
|
||||
|
||||
match operation().await {
|
||||
Ok(result) => {
|
||||
tracing::debug!(
|
||||
"Operation '{}' succeeded on attempt {}",
|
||||
operation_name,
|
||||
attempt
|
||||
);
|
||||
return Ok(result);
|
||||
}
|
||||
Err(Error::Interrupted) => return Err(Error::Interrupted),
|
||||
Err(e) => {
|
||||
last_error = e;
|
||||
|
||||
if attempt >= self.config.max_attempts {
|
||||
tracing::warn!(
|
||||
"Operation '{}' failed after {} attempts. Giving up.",
|
||||
operation_name,
|
||||
attempt
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
let delay = if self.config.use_exponential_backoff {
|
||||
self.config.delay.mul_f32(2.0_f32.powi(attempt as i32 - 1))
|
||||
} else {
|
||||
self.config.delay
|
||||
};
|
||||
|
||||
tracing::warn!(
|
||||
"Operation '{}' failed on attempt {}: {}. Retrying in {:?}...",
|
||||
operation_name,
|
||||
attempt,
|
||||
last_error,
|
||||
delay
|
||||
);
|
||||
|
||||
sleep(delay).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Err(last_error)
|
||||
}
|
||||
}
|
|
@ -1,449 +0,0 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (c) 2023-2025 Matter Labs
|
||||
|
||||
//! Configuration settings for the verification process
|
||||
|
||||
use crate::{
|
||||
core::{SGX_HASH_SIZE, TDX_HASH_SIZE},
|
||||
error,
|
||||
};
|
||||
use bytes::{Bytes, BytesMut};
|
||||
use clap::{ArgGroup, Parser};
|
||||
use enumset::EnumSet;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{collections::HashSet, fs, ops::Deref, path::PathBuf, str::FromStr, time::Duration};
|
||||
use teepot::{log::LogLevelParser, quote::tcblevel::TcbLevel};
|
||||
use tracing_subscriber::filter::LevelFilter;
|
||||
use url::Url;
|
||||
use zksync_basic_types::{tee_types::TeeType, L1BatchNumber};
|
||||
use zksync_types::L2ChainId;
|
||||
|
||||
/// Primary configuration for the verification process
|
||||
#[derive(Parser, Debug, Clone)]
|
||||
#[command(author = "Matter Labs", version, about = "SGX attestation and batch signature verifier", long_about = None
|
||||
)]
|
||||
#[clap(group(
|
||||
ArgGroup::new("mode")
|
||||
.required(true)
|
||||
.args(&["batch_range", "continuous"]),
|
||||
))]
|
||||
pub struct VerifierConfigArgs {
|
||||
/// Log level for the log output.
|
||||
/// Valid values are: `off`, `error`, `warn`, `info`, `debug`, `trace`
|
||||
#[clap(long, default_value_t = LevelFilter::WARN, value_parser = LogLevelParser)]
|
||||
pub log_level: LevelFilter,
|
||||
|
||||
/// The batch number or range of batch numbers to verify the attestation and signature (e.g.,
|
||||
/// "42" or "42-45"). This option is mutually exclusive with the `--continuous` mode.
|
||||
#[clap(short = 'n', long = "batch", value_parser = parse_batch_range)]
|
||||
pub batch_range: Option<(L1BatchNumber, L1BatchNumber)>,
|
||||
|
||||
/// Continuous mode: keep verifying new batches until interrupted. This option is mutually
|
||||
/// exclusive with the `--batch` option.
|
||||
#[clap(long, value_name = "FIRST_BATCH")]
|
||||
pub continuous: Option<L1BatchNumber>,
|
||||
|
||||
/// URL of the RPC server to query for the batch attestation and signature.
|
||||
#[clap(long = "rpc")]
|
||||
pub rpc_url: Url,
|
||||
|
||||
/// Chain ID of the network to query.
|
||||
#[clap(long = "chain", default_value_t = L2ChainId::default().as_u64())]
|
||||
pub chain_id: u64,
|
||||
|
||||
/// Rate limit between requests in milliseconds.
|
||||
#[clap(long, default_value = "0", value_parser = parse_duration)]
|
||||
pub rate_limit: Duration,
|
||||
|
||||
/// Path to a YAML file containing attestation policy configuration.
|
||||
/// This overrides any attestation policy settings provided via command line options.
|
||||
#[clap(long = "attestation-policy-file")]
|
||||
pub attestation_policy_file: Option<PathBuf>,
|
||||
|
||||
/// Comma separated list of Tee types to process
|
||||
#[clap(long)]
|
||||
pub tee_types: TeeTypes,
|
||||
}
|
||||
|
||||
/// Attestation policy implemented as a set of criteria that must be met by SGX attestation.
|
||||
#[derive(Default, Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct SgxAttestationPolicyConfig {
|
||||
/// List of allowed hex-encoded SGX mrsigners. Batch attestation must consist of
|
||||
/// one of these mrsigners. If the list is empty, the mrsigner check is skipped.
|
||||
#[serde(default)]
|
||||
pub mrsigners: Option<Vec<String>>,
|
||||
|
||||
/// List of allowed hex-encoded SGX mrenclaves. Batch attestation must consist
|
||||
/// of one of these mrenclaves. If the list is empty, the mrenclave check is skipped.
|
||||
#[serde(default)]
|
||||
pub mrenclaves: Option<Vec<String>>,
|
||||
|
||||
/// List of allowed SGX TCB levels. If the list is empty, the TCB level check is
|
||||
/// skipped. Allowed values: Ok, ConfigNeeded, ConfigAndSwHardeningNeeded, SwHardeningNeeded,
|
||||
/// OutOfDate, OutOfDateConfigNeeded.
|
||||
#[serde(default = "default_tcb_levels")]
|
||||
pub allowed_tcb_levels: EnumSet<TcbLevel>,
|
||||
|
||||
/// List of allowed SGX Advisories. If the list is empty, theAdvisories check is skipped.
|
||||
#[serde(default)]
|
||||
pub allowed_advisory_ids: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
/// Attestation policy implemented as a set of criteria that must be met by TDX attestation.
|
||||
#[derive(Default, Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct TdxAttestationPolicyConfig {
|
||||
/// List of allowed hex-encoded TDX mrs. Batch attestation must consist
|
||||
/// of one of these mrs. If the list is empty, the mrs check is skipped.
|
||||
#[serde(default)]
|
||||
pub mrs: Option<Vec<[String; 5]>>,
|
||||
|
||||
/// List of allowed SGX TCB levels. If the list is empty, the TCB level check is
|
||||
/// skipped. Allowed values: Ok, ConfigNeeded, ConfigAndSwHardeningNeeded, SwHardeningNeeded,
|
||||
/// OutOfDate, OutOfDateConfigNeeded.
|
||||
#[serde(default = "default_tcb_levels")]
|
||||
pub allowed_tcb_levels: EnumSet<TcbLevel>,
|
||||
|
||||
/// List of allowed TDX Advisories. If the list is empty, theAdvisories check is skipped.
|
||||
#[serde(default)]
|
||||
pub allowed_advisory_ids: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
/// Attestation policy implemented as a set of criteria that must be met by SGX or TDX attestation.
|
||||
#[derive(Default, Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct AttestationPolicyConfig {
|
||||
/// SGX attestation policy
|
||||
pub sgx: SgxAttestationPolicyConfig,
|
||||
/// TDX attestation policy
|
||||
pub tdx: TdxAttestationPolicyConfig,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct AttestationPolicy {
|
||||
pub sgx_mrsigners: Option<Vec<Bytes>>,
|
||||
pub sgx_mrenclaves: Option<Vec<Bytes>>,
|
||||
pub sgx_allowed_tcb_levels: EnumSet<TcbLevel>,
|
||||
pub sgx_allowed_advisory_ids: Option<Vec<String>>,
|
||||
pub tdx_allowed_tcb_levels: EnumSet<TcbLevel>,
|
||||
pub tdx_mrs: Option<Vec<Bytes>>,
|
||||
pub tdx_allowed_advisory_ids: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
/// Default TCB levels used for Serde deserialization
|
||||
fn default_tcb_levels() -> EnumSet<TcbLevel> {
|
||||
let mut set = EnumSet::new();
|
||||
set.insert(TcbLevel::Ok);
|
||||
set
|
||||
}
|
||||
|
||||
// TODO:
|
||||
// When moving this binary to the `zksync-era` repo, we
|
||||
// should be using `EnumSet<TeeType>` but this requires
|
||||
// #[derive(EnumSetType, Debug, Serialize, Deserialize)]
|
||||
// #[enumset(serialize_repr = "list")]
|
||||
// for `TeeType`
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct TeeTypes(HashSet<TeeType>);
|
||||
|
||||
impl FromStr for TeeTypes {
|
||||
type Err = error::Error;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
let mut hs = HashSet::new();
|
||||
let tee_strs: Vec<&str> = s.split(',').collect();
|
||||
for tee_str in tee_strs {
|
||||
match tee_str.to_ascii_lowercase().as_str() {
|
||||
"sgx" => {
|
||||
hs.insert(TeeType::Sgx);
|
||||
}
|
||||
"tdx" => {
|
||||
hs.insert(TeeType::Tdx);
|
||||
}
|
||||
_ => {
|
||||
return Err(error::Error::internal("Unknown TEE type"));
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(Self(hs))
|
||||
}
|
||||
}
|
||||
impl Default for TeeTypes {
|
||||
fn default() -> Self {
|
||||
Self(HashSet::from([TeeType::Sgx, TeeType::Tdx]))
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for TeeTypes {
|
||||
type Target = HashSet<TeeType>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct VerifierConfig {
|
||||
pub args: VerifierConfigArgs,
|
||||
pub policy: AttestationPolicy,
|
||||
}
|
||||
|
||||
impl VerifierConfig {
|
||||
pub fn new(args: VerifierConfigArgs) -> error::Result<Self> {
|
||||
let policy = if let Some(path) = &args.attestation_policy_file {
|
||||
let policy_content = fs::read_to_string(path).map_err(|e| {
|
||||
error::Error::internal(format!("Failed to read attestation policy file: {e}"))
|
||||
})?;
|
||||
|
||||
let policy_config: AttestationPolicyConfig = serde_yaml::from_str(&policy_content)
|
||||
.map_err(|e| {
|
||||
error::Error::internal(format!("Failed to parse attestation policy file: {e}"))
|
||||
})?;
|
||||
|
||||
tracing::info!("Loaded attestation policy from file: {:?}", path);
|
||||
policy_config
|
||||
} else {
|
||||
AttestationPolicyConfig::default()
|
||||
};
|
||||
|
||||
let policy = AttestationPolicy {
|
||||
sgx_mrsigners: decode_hex_vec_option(policy.sgx.mrsigners, SGX_HASH_SIZE)?,
|
||||
sgx_mrenclaves: decode_hex_vec_option(policy.sgx.mrenclaves, SGX_HASH_SIZE)?,
|
||||
sgx_allowed_tcb_levels: policy.sgx.allowed_tcb_levels,
|
||||
sgx_allowed_advisory_ids: policy.sgx.allowed_advisory_ids,
|
||||
tdx_allowed_tcb_levels: policy.tdx.allowed_tcb_levels,
|
||||
tdx_mrs: decode_tdx_mrs(policy.tdx.mrs, TDX_HASH_SIZE)?,
|
||||
tdx_allowed_advisory_ids: policy.tdx.allowed_advisory_ids,
|
||||
};
|
||||
|
||||
if policy.sgx_mrsigners.is_none() && policy.sgx_mrenclaves.is_none() {
|
||||
tracing::error!(
|
||||
"Neither `--sgx-mrenclaves` nor `--sgx-mrsigners` specified. Any code could have produced the SGX proof."
|
||||
);
|
||||
}
|
||||
|
||||
if policy.tdx_mrs.is_none() {
|
||||
tracing::error!(
|
||||
"`--tdxmrs` not specified. Any code could have produced the TDX proof."
|
||||
);
|
||||
}
|
||||
|
||||
Ok(Self { args, policy })
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to decode a vector of hex strings
|
||||
fn decode_hex_vec_option(
|
||||
hex_strings: Option<Vec<String>>,
|
||||
bytes_length: usize,
|
||||
) -> Result<Option<Vec<Bytes>>, hex::FromHexError> {
|
||||
hex_strings
|
||||
.map(|strings| {
|
||||
strings
|
||||
.into_iter()
|
||||
.map(|s| {
|
||||
if s.len() > (bytes_length * 2) {
|
||||
return Err(hex::FromHexError::InvalidStringLength);
|
||||
}
|
||||
hex::decode(s).map(Bytes::from)
|
||||
})
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
})
|
||||
.transpose()
|
||||
}
|
||||
|
||||
// Improved decode_tdx_mrs function
|
||||
fn decode_tdx_mrs(
|
||||
tdx_mrs_opt: Option<Vec<[String; 5]>>,
|
||||
bytes_length: usize,
|
||||
) -> Result<Option<Vec<Bytes>>, hex::FromHexError> {
|
||||
match tdx_mrs_opt {
|
||||
None => Ok(None),
|
||||
Some(mrs_array) => {
|
||||
let result = mrs_array
|
||||
.into_iter()
|
||||
.map(|strings| decode_and_combine_mrs(&strings, bytes_length))
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
Ok(Some(result))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to decode and combine MRs
|
||||
fn decode_and_combine_mrs(
|
||||
strings: &[String; 5],
|
||||
bytes_length: usize,
|
||||
) -> Result<Bytes, hex::FromHexError> {
|
||||
let mut buffer = BytesMut::with_capacity(bytes_length * 5);
|
||||
|
||||
for s in strings {
|
||||
if s.len() > (bytes_length * 2) {
|
||||
return Err(hex::FromHexError::InvalidStringLength);
|
||||
}
|
||||
let decoded = hex::decode(s)?;
|
||||
buffer.extend(decoded);
|
||||
}
|
||||
|
||||
Ok(buffer.freeze())
|
||||
}
|
||||
|
||||
/// Parse a batch range from a string like "42" or "42-45"
|
||||
fn parse_batch_range(s: &str) -> error::Result<(L1BatchNumber, L1BatchNumber)> {
|
||||
let parse = |s: &str| {
|
||||
s.parse::<u32>()
|
||||
.map(L1BatchNumber::from)
|
||||
.map_err(|e| error::Error::internal(format!("Can't convert batch {s} to number: {e}")))
|
||||
};
|
||||
if let Some((start, end)) = s.split_once('-') {
|
||||
let (start, end) = (parse(start)?, parse(end)?);
|
||||
if start > end {
|
||||
Err(error::Error::InvalidBatchRange(s.into()))
|
||||
} else {
|
||||
Ok((start, end))
|
||||
}
|
||||
} else {
|
||||
let batch_number = parse(s)?;
|
||||
Ok((batch_number, batch_number))
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse a duration from a millisecond string
|
||||
fn parse_duration(s: &str) -> error::Result<Duration> {
|
||||
let millis = s
|
||||
.parse()
|
||||
.map_err(|e| error::Error::internal(format!("Can't convert {s} to duration: {e}")))?;
|
||||
Ok(Duration::from_millis(millis))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use std::{env, fs, path::PathBuf};
|
||||
use teepot::quote::tcblevel::TcbLevel;
|
||||
|
||||
#[test]
|
||||
fn test_load_attestation_policy_from_yaml() {
|
||||
// Create a temporary directory for the test
|
||||
let temp_dir = env::temp_dir().join("test_attestation_policy");
|
||||
fs::create_dir_all(&temp_dir).expect("Failed to create temp directory");
|
||||
|
||||
// Create a temporary YAML file
|
||||
let yaml_path = temp_dir.join("policy.yaml");
|
||||
let yaml_content = r#"
|
||||
sgx:
|
||||
mrenclaves:
|
||||
- a2caa7055e333f69c3e46ca7ba65b135a86c90adfde2afb356e05075b7818b3c
|
||||
- 36eeb64cc816f80a1cf5818b26710f360714b987d3799e757cbefba7697b9589
|
||||
allowed_tcb_levels:
|
||||
- Ok
|
||||
- SwHardeningNeeded
|
||||
tdx:
|
||||
mrs:
|
||||
- - 2a90c8fa38672cafd791d994beb6836b99383b2563736858632284f0f760a6446efd1e7ec457cf08b629ea630f7b4525
|
||||
- 3300980705adf09d28b707b79699d9874892164280832be2c386a715b6e204e0897fb564a064f810659207ba862b304f
|
||||
- c08ab64725566bcc8a6fb1c79e2e64744fcff1594b8f1f02d716fb66592ecd5de94933b2bc54ffbbc43a52aab7eb1146
|
||||
- 092a4866a9e6a1672d7439a5d106fbc6eb57b738d5bfea5276d41afa2551824365fdd66700c1ce9c0b20542b9f9d5945
|
||||
- 971fb52f90ec98a234301ca9b8fc30b613c33e3dd9c0cc42dcb8003d4a95d8fb218b75baf028b70a3cabcb947e1ca453
|
||||
"#;
|
||||
fs::write(&yaml_path, yaml_content).expect("Failed to write YAML file");
|
||||
|
||||
// Create a minimal config
|
||||
let config = VerifierConfig::new(VerifierConfigArgs {
|
||||
log_level: LevelFilter::INFO,
|
||||
batch_range: Some((L1BatchNumber(1), L1BatchNumber(10))),
|
||||
continuous: None,
|
||||
rpc_url: Url::parse("http://localhost:8545").unwrap(),
|
||||
chain_id: 270,
|
||||
rate_limit: Duration::from_millis(0),
|
||||
attestation_policy_file: Some(yaml_path.clone()),
|
||||
tee_types: Default::default(),
|
||||
})
|
||||
.expect("Failed to load attestation policy");
|
||||
|
||||
// Verify that the attestation policy was loaded correctly
|
||||
assert_eq!(config.policy.sgx_mrsigners, None);
|
||||
assert_eq!(
|
||||
config.policy.sgx_mrenclaves,
|
||||
Some(vec![
|
||||
Bytes::from(
|
||||
hex::decode("a2caa7055e333f69c3e46ca7ba65b135a86c90adfde2afb356e05075b7818b3c")
|
||||
.unwrap(),
|
||||
),
|
||||
Bytes::from(
|
||||
hex::decode("36eeb64cc816f80a1cf5818b26710f360714b987d3799e757cbefba7697b9589")
|
||||
.unwrap(),
|
||||
),
|
||||
])
|
||||
);
|
||||
assert!(config.policy.sgx_allowed_tcb_levels.contains(TcbLevel::Ok));
|
||||
assert!(config
|
||||
.policy
|
||||
.sgx_allowed_tcb_levels
|
||||
.contains(TcbLevel::SwHardeningNeeded));
|
||||
assert_eq!(
|
||||
config.policy.tdx_mrs,
|
||||
Some(vec![Bytes::from(
|
||||
hex::decode(concat!(
|
||||
"2a90c8fa38672cafd791d994beb6836b99383b2563736858632284f0f760a6446efd1e7ec457cf08b629ea630f7b4525",
|
||||
"3300980705adf09d28b707b79699d9874892164280832be2c386a715b6e204e0897fb564a064f810659207ba862b304f",
|
||||
"c08ab64725566bcc8a6fb1c79e2e64744fcff1594b8f1f02d716fb66592ecd5de94933b2bc54ffbbc43a52aab7eb1146",
|
||||
"092a4866a9e6a1672d7439a5d106fbc6eb57b738d5bfea5276d41afa2551824365fdd66700c1ce9c0b20542b9f9d5945",
|
||||
"971fb52f90ec98a234301ca9b8fc30b613c33e3dd9c0cc42dcb8003d4a95d8fb218b75baf028b70a3cabcb947e1ca453"
|
||||
)).unwrap()),
|
||||
])
|
||||
);
|
||||
|
||||
// Clean up
|
||||
fs::remove_file(yaml_path).expect("Failed to remove temp YAML file");
|
||||
fs::remove_dir_all(temp_dir).expect("Failed to remove temp directory");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_invalid_yaml_file_path() {
|
||||
// Create a minimal config with a non-existent YAML file path
|
||||
let result = VerifierConfig::new(VerifierConfigArgs {
|
||||
log_level: LevelFilter::INFO,
|
||||
batch_range: Some((L1BatchNumber(1), L1BatchNumber(10))),
|
||||
continuous: None,
|
||||
rpc_url: Url::parse("http://localhost:8545").unwrap(),
|
||||
chain_id: 270,
|
||||
rate_limit: Duration::from_millis(0),
|
||||
attestation_policy_file: Some(PathBuf::from("/non/existent/path.yaml")),
|
||||
tee_types: Default::default(),
|
||||
});
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_invalid_yaml_content() {
|
||||
// Create a temporary directory for the test
|
||||
let temp_dir = env::temp_dir().join("test_invalid_yaml");
|
||||
fs::create_dir_all(&temp_dir).expect("Failed to create temp directory");
|
||||
|
||||
// Create a temporary YAML file with invalid content
|
||||
let yaml_path = temp_dir.join("invalid_policy.yaml");
|
||||
let yaml_content = r#"
|
||||
sgx_mrsigners: 1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef
|
||||
invalid_key: "some value"
|
||||
allowed_tcb_levels:
|
||||
- Invalid
|
||||
- ConfigNeeded
|
||||
"#;
|
||||
fs::write(&yaml_path, yaml_content).expect("Failed to write YAML file");
|
||||
|
||||
// Create a minimal config
|
||||
let result = VerifierConfig::new(VerifierConfigArgs {
|
||||
log_level: LevelFilter::INFO,
|
||||
batch_range: Some((L1BatchNumber(1), L1BatchNumber(10))),
|
||||
continuous: None,
|
||||
rpc_url: Url::parse("http://localhost:8545").unwrap(),
|
||||
chain_id: 270,
|
||||
rate_limit: Duration::from_millis(0),
|
||||
attestation_policy_file: Some(yaml_path.clone()),
|
||||
tee_types: Default::default(),
|
||||
});
|
||||
assert!(result.is_err());
|
||||
|
||||
// Clean up
|
||||
fs::remove_file(yaml_path).expect("Failed to remove temp YAML file");
|
||||
fs::remove_dir_all(temp_dir).expect("Failed to remove temp directory");
|
||||
}
|
||||
}
|
|
@ -1,19 +0,0 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (c) 2023-2025 Matter Labs
|
||||
|
||||
//! Constants used throughout the application
|
||||
|
||||
/// Maximum number of retry attempts for fetching proofs
|
||||
pub const MAX_PROOF_FETCH_RETRIES: u32 = 3;
|
||||
|
||||
/// Default delay between retries (in milliseconds)
|
||||
pub const DEFAULT_RETRY_DELAY_MS: u64 = 1000;
|
||||
|
||||
/// Default timeout for HTTP requests (in seconds)
|
||||
pub const DEFAULT_HTTP_REQUEST_TIMEOUT: u64 = 30;
|
||||
|
||||
/// SGX hash size in bytes
|
||||
pub const SGX_HASH_SIZE: usize = 32;
|
||||
|
||||
/// TDX hash size in bytes
|
||||
pub const TDX_HASH_SIZE: usize = 48;
|
|
@ -1,12 +0,0 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (c) 2023-2025 Matter Labs
|
||||
|
||||
//! Core components for Era proof attestation verification
|
||||
|
||||
mod config;
|
||||
mod constants;
|
||||
mod types;
|
||||
|
||||
pub use config::*;
|
||||
pub use constants::*;
|
||||
pub use types::*;
|
|
@ -1,100 +0,0 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (c) 2023-2025 Matter Labs
|
||||
|
||||
//! Common type definitions used throughout the application
|
||||
|
||||
use std::fmt;
|
||||
use zksync_basic_types::L1BatchNumber;
|
||||
|
||||
/// Represents the operating mode of the verifier
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum VerifierMode {
|
||||
/// Run on a single batch or range of batches and then exit
|
||||
OneShot {
|
||||
/// Starting batch number
|
||||
start_batch: L1BatchNumber,
|
||||
/// Ending batch number
|
||||
end_batch: L1BatchNumber,
|
||||
},
|
||||
/// Run continuously starting from a specific batch, until interrupted
|
||||
Continuous {
|
||||
/// Starting batch number
|
||||
start_batch: L1BatchNumber,
|
||||
},
|
||||
}
|
||||
|
||||
impl fmt::Display for VerifierMode {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
VerifierMode::OneShot {
|
||||
start_batch,
|
||||
end_batch,
|
||||
} => {
|
||||
if start_batch == end_batch {
|
||||
write!(f, "one-shot mode (batch {start_batch})")
|
||||
} else {
|
||||
write!(f, "one-shot mode (batches {start_batch}-{end_batch})")
|
||||
}
|
||||
}
|
||||
VerifierMode::Continuous { start_batch } => {
|
||||
write!(f, "continuous mode (starting from batch {start_batch})")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Result of proof verification for a single batch
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum VerificationResult {
|
||||
/// All proofs for the batch were verified successfully
|
||||
Success,
|
||||
/// Some proofs for the batch failed verification
|
||||
PartialSuccess {
|
||||
/// Number of successfully verified proofs
|
||||
verified_count: u32,
|
||||
/// Number of proofs that failed verification
|
||||
unverified_count: u32,
|
||||
},
|
||||
/// No proofs for the batch were verified successfully
|
||||
Failure,
|
||||
/// Verification was interrupted before completion
|
||||
Interrupted,
|
||||
/// No proofs were found for the batch
|
||||
NoProofsFound,
|
||||
}
|
||||
|
||||
impl VerificationResult {
|
||||
/// Check if the majority of the proofs was verified successfully
|
||||
pub fn is_successful(&self) -> bool {
|
||||
match self {
|
||||
VerificationResult::Success => true,
|
||||
VerificationResult::PartialSuccess {
|
||||
verified_count,
|
||||
unverified_count,
|
||||
} => verified_count > unverified_count,
|
||||
VerificationResult::Failure
|
||||
| VerificationResult::Interrupted
|
||||
| VerificationResult::NoProofsFound => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for VerificationResult {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
VerificationResult::Success => write!(f, "Success"),
|
||||
VerificationResult::PartialSuccess {
|
||||
verified_count,
|
||||
unverified_count,
|
||||
} => {
|
||||
write!(
|
||||
f,
|
||||
"Partial Success ({verified_count} verified, {unverified_count} failed)"
|
||||
)
|
||||
}
|
||||
VerificationResult::Failure => write!(f, "Failure"),
|
||||
VerificationResult::Interrupted => write!(f, "Interrupted"),
|
||||
VerificationResult::NoProofsFound => write!(f, "No Proofs Found"),
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,103 +0,0 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (c) 2023-2025 Matter Labs
|
||||
|
||||
//! Error types for the verification process
|
||||
|
||||
use teepot::sgx::QuoteError;
|
||||
use thiserror::Error;
|
||||
use zksync_basic_types::L1BatchNumber;
|
||||
|
||||
/// Result type used throughout the application
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
/// Error types that can occur during verification
|
||||
#[derive(Error, Debug)]
|
||||
pub enum Error {
|
||||
/// Error fetching proof
|
||||
#[error("Failed to fetch proof for batch {batch_number}: {reason}")]
|
||||
ProofFetch {
|
||||
/// Batch number that caused the error
|
||||
batch_number: L1BatchNumber,
|
||||
/// Reason for the error
|
||||
reason: String,
|
||||
},
|
||||
|
||||
/// Error communicating with the HTTP server
|
||||
#[error("HTTP request failed with status {status_code}: {message}")]
|
||||
Http {
|
||||
/// HTTP status code
|
||||
status_code: u16,
|
||||
/// Error message
|
||||
message: String,
|
||||
},
|
||||
|
||||
/// Error communicating with the JSON-RPC server
|
||||
#[error("JSON-RPC error: {0}")]
|
||||
JsonRpc(String),
|
||||
|
||||
/// JSON-RPC response has an invalid format
|
||||
#[error("JSON-RPC response has an invalid format")]
|
||||
JsonRpcInvalidResponse(String),
|
||||
|
||||
/// Invalid batch range
|
||||
#[error("Invalid batch range: {0}")]
|
||||
InvalidBatchRange(String),
|
||||
|
||||
/// Error verifying attestation
|
||||
#[error(transparent)]
|
||||
AttestationVerification(#[from] QuoteError),
|
||||
|
||||
/// Error verifying signature
|
||||
#[error("Signature verification failed: {0}")]
|
||||
SignatureVerification(String),
|
||||
|
||||
/// Attestation policy violation
|
||||
#[error("Attestation policy violation: {0}")]
|
||||
PolicyViolation(String),
|
||||
|
||||
/// Operation interrupted
|
||||
#[error("Operation interrupted")]
|
||||
Interrupted,
|
||||
|
||||
#[error(transparent)]
|
||||
FromHex(#[from] hex::FromHexError),
|
||||
|
||||
/// Internal error
|
||||
#[error("Internal error: {0}")]
|
||||
Internal(String),
|
||||
}
|
||||
|
||||
/// Utility functions for working with errors
|
||||
impl Error {
|
||||
/// Create a new proof fetch error
|
||||
pub fn proof_fetch(batch_number: L1BatchNumber, reason: impl Into<String>) -> Self {
|
||||
Self::ProofFetch {
|
||||
batch_number,
|
||||
reason: reason.into(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new policy violation error
|
||||
pub fn policy_violation(reason: impl Into<String>) -> Self {
|
||||
Self::PolicyViolation(reason.into())
|
||||
}
|
||||
|
||||
/// Create a new signature verification error
|
||||
pub fn signature_verification(reason: impl Into<String>) -> Self {
|
||||
Self::SignatureVerification(reason.into())
|
||||
}
|
||||
|
||||
/// Create a new internal error
|
||||
pub fn internal(reason: impl Into<String>) -> Self {
|
||||
Self::Internal(reason.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<reqwest::Error> for Error {
|
||||
fn from(value: reqwest::Error) -> Self {
|
||||
Self::Http {
|
||||
status_code: value.status().map_or(0, |v| v.as_u16()),
|
||||
message: value.to_string(),
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,94 +1,221 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (c) 2023-2025 Matter Labs
|
||||
// Copyright (c) 2023-2024 Matter Labs
|
||||
|
||||
//! Tool for SGX attestation and batch signature verification, both continuous and one-shot
|
||||
|
||||
mod args;
|
||||
mod client;
|
||||
mod core;
|
||||
mod error;
|
||||
mod processor;
|
||||
mod proof;
|
||||
mod verification;
|
||||
|
||||
use crate::{
|
||||
core::{VerifierConfig, VerifierConfigArgs},
|
||||
error::Error,
|
||||
processor::ProcessorFactory,
|
||||
use crate::verification::{
|
||||
log_quote_verification_summary, verify_attestation_quote, verify_batch_proof,
|
||||
};
|
||||
use anyhow::Result;
|
||||
use args::{Arguments, AttestationPolicyArgs};
|
||||
use clap::Parser;
|
||||
use error::Result;
|
||||
use tokio::signal;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
use client::MainNodeClient;
|
||||
use proof::get_proofs;
|
||||
use reqwest::Client;
|
||||
use teepot::log::setup_logging;
|
||||
use tokio::{signal, sync::watch};
|
||||
use tracing::{debug, error, info, trace, warn};
|
||||
use url::Url;
|
||||
use zksync_basic_types::L1BatchNumber;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
// Parse command-line arguments
|
||||
let config = VerifierConfig::new(VerifierConfigArgs::parse())?;
|
||||
let args = Arguments::parse();
|
||||
tracing::subscriber::set_global_default(setup_logging(
|
||||
env!("CARGO_CRATE_NAME"),
|
||||
&args.log_level,
|
||||
)?)?;
|
||||
|
||||
// Initialize logging
|
||||
tracing::subscriber::set_global_default(
|
||||
teepot::log::setup_logging(env!("CARGO_CRATE_NAME"), &config.args.log_level)
|
||||
.map_err(|e| Error::internal(e.to_string()))?,
|
||||
)
|
||||
.map_err(|e| Error::internal(e.to_string()))?;
|
||||
|
||||
// Create processor based on config
|
||||
let (processor, mode) = ProcessorFactory::create(config.clone())?;
|
||||
|
||||
// Set up a cancellation Token
|
||||
let token = CancellationToken::new();
|
||||
|
||||
// Log startup information
|
||||
tracing::info!("Starting verification in {}", mode);
|
||||
|
||||
// Spawn processing task
|
||||
let mut process_handle = {
|
||||
let token = token.clone();
|
||||
tokio::spawn(async move { processor.run(token).await })
|
||||
};
|
||||
|
||||
// Wait for processing to complete or for stop signal
|
||||
validate_arguments(&args)?;
|
||||
let (stop_sender, stop_receiver) = watch::channel(false);
|
||||
let mut process_handle = tokio::spawn(verify_batches_proofs(stop_receiver, args));
|
||||
tokio::select! {
|
||||
result = &mut process_handle => {
|
||||
match result {
|
||||
Ok(Ok(verification_results)) => {
|
||||
tracing::info!("Verification completed successfully");
|
||||
|
||||
let total_batches = verification_results.len();
|
||||
let successful_batches = verification_results.iter()
|
||||
.filter(|(_, result)| result.is_successful())
|
||||
.count();
|
||||
|
||||
tracing::info!(
|
||||
"Verified {} batches: {} succeeded, {} failed",
|
||||
total_batches,
|
||||
successful_batches,
|
||||
total_batches - successful_batches
|
||||
);
|
||||
|
||||
Ok(())
|
||||
},
|
||||
Ok(Err(e)) => {
|
||||
tracing::error!("Verification failed: {}", e);
|
||||
Err(e)
|
||||
},
|
||||
Err(e) => {
|
||||
tracing::error!("Task panicked: {}", e);
|
||||
Err(Error::internal(format!("Task panicked: {e}")))
|
||||
}
|
||||
}
|
||||
},
|
||||
ret = &mut process_handle => { return ret?; },
|
||||
_ = signal::ctrl_c() => {
|
||||
tracing::info!("Stop signal received, shutting down gracefully...");
|
||||
token.cancel();
|
||||
|
||||
// Wait for processor to complete gracefully
|
||||
match process_handle.await {
|
||||
Ok(_) => tracing::info!("Processor stopped gracefully"),
|
||||
Err(e) => tracing::error!("Error stopping processor: {}", e),
|
||||
}
|
||||
|
||||
Ok(())
|
||||
tracing::info!("Stop signal received, shutting down");
|
||||
stop_sender.send(true).ok();
|
||||
// Wait for process_batches to complete gracefully
|
||||
process_handle.await??;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn validate_arguments(args: &Arguments) -> Result<()> {
|
||||
if args.attestation_policy.sgx_mrsigners.is_none()
|
||||
&& args.attestation_policy.sgx_mrenclaves.is_none()
|
||||
{
|
||||
error!("Neither `--sgx-mrenclaves` nor `--sgx-mrsigners` specified. Any code could have produced the proof.");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Verify all TEE proofs for all batches starting from the given batch number up to the specified
|
||||
/// batch number, if a range is provided. Otherwise, continue verifying batches until the stop
|
||||
/// signal is received.
|
||||
async fn verify_batches_proofs(
|
||||
mut stop_receiver: watch::Receiver<bool>,
|
||||
args: Arguments,
|
||||
) -> Result<()> {
|
||||
let node_client = MainNodeClient::new(args.rpc_url.clone(), args.chain_id)?;
|
||||
let http_client = Client::new();
|
||||
let first_batch_number = match args.batch_range {
|
||||
Some((first_batch_number, _)) => first_batch_number,
|
||||
None => args
|
||||
.continuous
|
||||
.expect("clap::ArgGroup should guarantee batch range or continuous option is set"),
|
||||
};
|
||||
let end_batch_number = args
|
||||
.batch_range
|
||||
.map_or(u32::MAX, |(_, end_batch_number)| end_batch_number.0);
|
||||
let mut unverified_batches_count: u32 = 0;
|
||||
let mut last_processed_batch_number = first_batch_number.0;
|
||||
|
||||
for current_batch_number in first_batch_number.0..=end_batch_number {
|
||||
if *stop_receiver.borrow() {
|
||||
tracing::warn!("Stop signal received, shutting down");
|
||||
break;
|
||||
}
|
||||
|
||||
trace!("Verifying TEE proofs for batch #{}", current_batch_number);
|
||||
|
||||
let all_verified = verify_batch_proofs(
|
||||
&mut stop_receiver,
|
||||
current_batch_number.into(),
|
||||
&args.rpc_url,
|
||||
&http_client,
|
||||
&node_client,
|
||||
&args.attestation_policy,
|
||||
)
|
||||
.await?;
|
||||
|
||||
if !all_verified {
|
||||
unverified_batches_count += 1;
|
||||
}
|
||||
|
||||
if current_batch_number < end_batch_number {
|
||||
tokio::time::timeout(args.rate_limit, stop_receiver.changed())
|
||||
.await
|
||||
.ok();
|
||||
}
|
||||
|
||||
last_processed_batch_number = current_batch_number;
|
||||
}
|
||||
|
||||
let verified_batches_count =
|
||||
last_processed_batch_number + 1 - first_batch_number.0 - unverified_batches_count;
|
||||
|
||||
if unverified_batches_count > 0 {
|
||||
if verified_batches_count == 0 {
|
||||
error!(
|
||||
"All {} batches failed verification!",
|
||||
unverified_batches_count
|
||||
);
|
||||
} else {
|
||||
error!(
|
||||
"Some batches failed verification! Unverified batches: {}. Verified batches: {}.",
|
||||
unverified_batches_count, verified_batches_count
|
||||
);
|
||||
}
|
||||
} else {
|
||||
info!(
|
||||
"All {} batches verified successfully!",
|
||||
verified_batches_count
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Verify all TEE proofs for the given batch number. Note that each batch number can potentially
|
||||
/// have multiple proofs of the same TEE type.
|
||||
async fn verify_batch_proofs(
|
||||
stop_receiver: &mut watch::Receiver<bool>,
|
||||
batch_number: L1BatchNumber,
|
||||
rpc_url: &Url,
|
||||
http_client: &Client,
|
||||
node_client: &MainNodeClient,
|
||||
attestation_policy: &AttestationPolicyArgs,
|
||||
) -> Result<bool> {
|
||||
let proofs = get_proofs(stop_receiver, batch_number, http_client, rpc_url).await?;
|
||||
let batch_no = batch_number.0;
|
||||
let mut total_proofs_count: u32 = 0;
|
||||
let mut unverified_proofs_count: u32 = 0;
|
||||
|
||||
for proof in proofs
|
||||
.into_iter()
|
||||
// only support SGX proofs for now
|
||||
.filter(|proof| proof.tee_type.eq_ignore_ascii_case("sgx"))
|
||||
{
|
||||
let batch_no = proof.l1_batch_number;
|
||||
|
||||
total_proofs_count += 1;
|
||||
let tee_type = proof.tee_type.to_uppercase();
|
||||
|
||||
if proof
|
||||
.status
|
||||
.map_or(false, |s| s.eq_ignore_ascii_case("permanently_ignored"))
|
||||
{
|
||||
trace!(
|
||||
batch_no,
|
||||
tee_type,
|
||||
"Proof is marked as permanently ignored. Skipping."
|
||||
);
|
||||
continue;
|
||||
}
|
||||
trace!(batch_no, tee_type, proof.proved_at, "Verifying proof.");
|
||||
|
||||
let attestation = proof.attestation.unwrap_or_default();
|
||||
debug!(batch_no, "Verifying quote ({} bytes)...", attestation.len());
|
||||
let quote_verification_result = verify_attestation_quote(&attestation)?;
|
||||
let verified_successfully = verify_batch_proof(
|
||||
"e_verification_result,
|
||||
attestation_policy,
|
||||
node_client,
|
||||
&proof.signature.unwrap_or_default(),
|
||||
L1BatchNumber(proof.l1_batch_number),
|
||||
)
|
||||
.await?;
|
||||
|
||||
log_quote_verification_summary("e_verification_result);
|
||||
|
||||
if verified_successfully {
|
||||
info!(
|
||||
batch_no,
|
||||
proof.proved_at, tee_type, "Verification succeeded.",
|
||||
);
|
||||
} else {
|
||||
unverified_proofs_count += 1;
|
||||
warn!(batch_no, proof.proved_at, tee_type, "Verification failed!",);
|
||||
}
|
||||
}
|
||||
|
||||
let verified_proofs_count = total_proofs_count - unverified_proofs_count;
|
||||
if unverified_proofs_count > 0 {
|
||||
if verified_proofs_count == 0 {
|
||||
error!(
|
||||
batch_no,
|
||||
"All {} proofs failed verification!", unverified_proofs_count
|
||||
);
|
||||
} else {
|
||||
warn!(
|
||||
batch_no,
|
||||
"Some proofs failed verification. Unverified proofs: {}. Verified proofs: {}.",
|
||||
unverified_proofs_count,
|
||||
verified_proofs_count
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// if at least one proof is verified, consider the batch verified
|
||||
let is_batch_verified = verified_proofs_count > 0;
|
||||
|
||||
Ok(is_batch_verified)
|
||||
}
|
||||
|
|
|
@ -1,117 +0,0 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (c) 2023-2025 Matter Labs
|
||||
|
||||
//! Core functionality for processing individual batches
|
||||
|
||||
use crate::error;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
use zksync_basic_types::L1BatchNumber;
|
||||
|
||||
use crate::{
|
||||
client::{HttpClient, MainNodeClient, RetryConfig},
|
||||
core::{VerificationResult, VerifierConfig},
|
||||
proof::ProofFetcher,
|
||||
verification::{BatchVerifier, VerificationReporter},
|
||||
};
|
||||
|
||||
/// Responsible for processing individual batches
|
||||
pub struct BatchProcessor {
|
||||
config: VerifierConfig,
|
||||
proof_fetcher: ProofFetcher,
|
||||
batch_verifier: BatchVerifier<MainNodeClient>,
|
||||
}
|
||||
|
||||
impl BatchProcessor {
|
||||
/// Create a new batch processor with the given configuration
|
||||
pub fn new(config: VerifierConfig) -> error::Result<Self> {
|
||||
// Initialize clients and fetchers
|
||||
let node_client = MainNodeClient::new(config.args.rpc_url.clone(), config.args.chain_id)?;
|
||||
let http_client = HttpClient::new();
|
||||
let retry_config = RetryConfig::default();
|
||||
let proof_fetcher =
|
||||
ProofFetcher::new(http_client, config.args.rpc_url.clone(), retry_config);
|
||||
let batch_verifier = BatchVerifier::new(node_client, config.policy.clone());
|
||||
Ok(Self {
|
||||
config,
|
||||
proof_fetcher,
|
||||
batch_verifier,
|
||||
})
|
||||
}
|
||||
|
||||
/// Process a single batch and return the verification result
|
||||
pub async fn process_batch(
|
||||
&self,
|
||||
token: &CancellationToken,
|
||||
batch_number: L1BatchNumber,
|
||||
) -> error::Result<VerificationResult> {
|
||||
if token.is_cancelled() {
|
||||
tracing::info!("Stop signal received, shutting down");
|
||||
return Ok(VerificationResult::Interrupted);
|
||||
}
|
||||
|
||||
tracing::trace!("Verifying TEE proofs for batch #{}", batch_number.0);
|
||||
|
||||
// Fetch proofs for the current batch across different TEE types
|
||||
let mut proofs = Vec::new();
|
||||
for tee_type in self.config.args.tee_types.iter().copied() {
|
||||
match self
|
||||
.proof_fetcher
|
||||
.get_proofs(token, batch_number, tee_type)
|
||||
.await
|
||||
{
|
||||
Ok(batch_proofs) => proofs.extend(batch_proofs),
|
||||
Err(error::Error::Interrupted) => return Err(error::Error::Interrupted),
|
||||
Err(e) => {
|
||||
tracing::error!(
|
||||
"Failed to fetch proofs for TEE type {:?} at batch {}: {:#}",
|
||||
tee_type,
|
||||
batch_number.0,
|
||||
e
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if proofs.is_empty() {
|
||||
tracing::warn!("No proofs found for batch #{}", batch_number.0);
|
||||
return Ok(VerificationResult::NoProofsFound);
|
||||
}
|
||||
|
||||
// Verify proofs for the current batch
|
||||
let verification_result = self
|
||||
.batch_verifier
|
||||
.verify_batch_proofs(token, batch_number, proofs)
|
||||
.await?;
|
||||
|
||||
let result = if verification_result.total_count == 0 {
|
||||
VerificationResult::NoProofsFound
|
||||
} else if verification_result.verified_count == verification_result.total_count {
|
||||
VerificationResult::Success
|
||||
} else if verification_result.verified_count > 0 {
|
||||
VerificationResult::PartialSuccess {
|
||||
verified_count: verification_result.verified_count,
|
||||
unverified_count: verification_result.unverified_count,
|
||||
}
|
||||
} else {
|
||||
VerificationResult::Failure
|
||||
};
|
||||
|
||||
tracing::debug!("Batch #{} verification result: {}", batch_number.0, result);
|
||||
|
||||
// Apply rate limiting between batches if needed
|
||||
if !matches!(result, VerificationResult::Interrupted)
|
||||
&& self.config.args.rate_limit.as_millis() > 0
|
||||
{
|
||||
tokio::time::timeout(self.config.args.rate_limit, token.cancelled())
|
||||
.await
|
||||
.ok();
|
||||
}
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// Log the overall verification results
|
||||
pub fn log_overall_results(success_count: u32, failure_count: u32) {
|
||||
VerificationReporter::log_overall_verification_results(success_count, failure_count);
|
||||
}
|
||||
}
|
|
@ -1,92 +0,0 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (c) 2023-2025 Matter Labs
|
||||
|
||||
//! Continuous batch processor for ongoing verification of new batches
|
||||
|
||||
use tokio_util::sync::CancellationToken;
|
||||
use zksync_basic_types::L1BatchNumber;
|
||||
|
||||
use crate::{
|
||||
core::{VerificationResult, VerifierConfig},
|
||||
error,
|
||||
processor::BatchProcessor,
|
||||
};
|
||||
|
||||
/// Processes batches continuously until stopped
|
||||
pub struct ContinuousProcessor {
|
||||
batch_processor: BatchProcessor,
|
||||
start_batch: L1BatchNumber,
|
||||
}
|
||||
|
||||
impl ContinuousProcessor {
|
||||
/// Create a new continuous processor that starts from the given batch
|
||||
pub fn new(config: VerifierConfig, start_batch: L1BatchNumber) -> error::Result<Self> {
|
||||
let batch_processor = BatchProcessor::new(config)?;
|
||||
|
||||
Ok(Self {
|
||||
batch_processor,
|
||||
start_batch,
|
||||
})
|
||||
}
|
||||
|
||||
/// Run the processor until stopped
|
||||
pub async fn run(
|
||||
&self,
|
||||
token: &CancellationToken,
|
||||
) -> error::Result<Vec<(u32, VerificationResult)>> {
|
||||
tracing::info!(
|
||||
"Starting continuous verification from batch {}",
|
||||
self.start_batch.0
|
||||
);
|
||||
|
||||
let mut results = Vec::new();
|
||||
let mut success_count = 0;
|
||||
let mut failure_count = 0;
|
||||
let mut current_batch = self.start_batch.0;
|
||||
|
||||
// Continue processing batches until stopped or reaching maximum batch number
|
||||
while !token.is_cancelled() {
|
||||
let batch = L1BatchNumber(current_batch);
|
||||
match self.batch_processor.process_batch(token, batch).await {
|
||||
Ok(result) => {
|
||||
match result {
|
||||
VerificationResult::Success | VerificationResult::PartialSuccess { .. } => {
|
||||
success_count += 1;
|
||||
}
|
||||
VerificationResult::Failure => failure_count += 1,
|
||||
VerificationResult::Interrupted => {
|
||||
results.push((current_batch, result));
|
||||
break;
|
||||
}
|
||||
VerificationResult::NoProofsFound => {
|
||||
// In continuous mode, we might hit batches that don't have proofs yet
|
||||
// Wait a bit longer before retrying
|
||||
if !token.is_cancelled() {
|
||||
tokio::time::sleep(tokio::time::Duration::from_secs(5)).await;
|
||||
// Don't increment batch number, try again
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
results.push((current_batch, result));
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!("Error processing batch {}: {}", current_batch, e);
|
||||
results.push((current_batch, VerificationResult::Failure));
|
||||
failure_count += 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Move to the next batch
|
||||
current_batch = current_batch
|
||||
.checked_add(1)
|
||||
.ok_or(error::Error::internal("Maximum batch number reached"))?;
|
||||
}
|
||||
|
||||
// Log overall results
|
||||
BatchProcessor::log_overall_results(success_count, failure_count);
|
||||
|
||||
Ok(results)
|
||||
}
|
||||
}
|
|
@ -1,62 +0,0 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (c) 2023-2025 Matter Labs
|
||||
|
||||
//! Processing logic for batch verification
|
||||
|
||||
mod batch_processor;
|
||||
mod continuous_processor;
|
||||
mod one_shot_processor;
|
||||
|
||||
pub use batch_processor::BatchProcessor;
|
||||
pub use continuous_processor::ContinuousProcessor;
|
||||
pub use one_shot_processor::OneShotProcessor;
|
||||
|
||||
use crate::{
|
||||
core::{VerificationResult, VerifierConfig, VerifierMode},
|
||||
error::Result,
|
||||
};
|
||||
use tokio_util::sync::CancellationToken;
|
||||
|
||||
// Using an enum instead of a trait because async functions in traits can't be used in trait objects
|
||||
/// Processor variants for different verification modes
|
||||
pub enum ProcessorType {
|
||||
/// One-shot processor for processing a specific range of batches
|
||||
OneShot(OneShotProcessor),
|
||||
/// Continuous processor for monitoring new batches
|
||||
Continuous(ContinuousProcessor),
|
||||
}
|
||||
|
||||
impl ProcessorType {
|
||||
/// Run the processor until completion or interruption
|
||||
pub async fn run(&self, token: CancellationToken) -> Result<Vec<(u32, VerificationResult)>> {
|
||||
match self {
|
||||
ProcessorType::OneShot(processor) => processor.run(&token).await,
|
||||
ProcessorType::Continuous(processor) => processor.run(&token).await,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Factory for creating the appropriate processor based on configuration
|
||||
pub struct ProcessorFactory;
|
||||
|
||||
impl ProcessorFactory {
|
||||
/// Create a new processor based on the provided configuration
|
||||
pub fn create(config: VerifierConfig) -> Result<(ProcessorType, VerifierMode)> {
|
||||
let mode = if let Some((start, end)) = config.args.batch_range {
|
||||
let processor = OneShotProcessor::new(config, start, end)?;
|
||||
let mode = VerifierMode::OneShot {
|
||||
start_batch: start,
|
||||
end_batch: end,
|
||||
};
|
||||
(ProcessorType::OneShot(processor), mode)
|
||||
} else if let Some(start) = config.args.continuous {
|
||||
let processor = ContinuousProcessor::new(config, start)?;
|
||||
let mode = VerifierMode::Continuous { start_batch: start };
|
||||
(ProcessorType::Continuous(processor), mode)
|
||||
} else {
|
||||
unreachable!("Clap ArgGroup should ensure either batch_range or continuous is set")
|
||||
};
|
||||
|
||||
Ok(mode)
|
||||
}
|
||||
}
|
|
@ -1,77 +0,0 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (c) 2023-2025 Matter Labs
|
||||
|
||||
//! One-shot batch processor for verifying a single batch or a range of batches
|
||||
|
||||
use crate::error;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
use zksync_basic_types::L1BatchNumber;
|
||||
|
||||
use crate::{
|
||||
core::{VerificationResult, VerifierConfig},
|
||||
processor::BatchProcessor,
|
||||
};
|
||||
|
||||
/// Processes a specific range of batches and then exits
|
||||
pub struct OneShotProcessor {
|
||||
batch_processor: BatchProcessor,
|
||||
start_batch: L1BatchNumber,
|
||||
end_batch: L1BatchNumber,
|
||||
}
|
||||
|
||||
impl OneShotProcessor {
|
||||
/// Create a new one-shot processor for the given batch range
|
||||
pub fn new(
|
||||
config: VerifierConfig,
|
||||
start_batch: L1BatchNumber,
|
||||
end_batch: L1BatchNumber,
|
||||
) -> error::Result<Self> {
|
||||
let batch_processor = BatchProcessor::new(config)?;
|
||||
|
||||
Ok(Self {
|
||||
batch_processor,
|
||||
start_batch,
|
||||
end_batch,
|
||||
})
|
||||
}
|
||||
|
||||
/// Run the processor until completion or interruption
|
||||
pub async fn run(
|
||||
&self,
|
||||
token: &CancellationToken,
|
||||
) -> error::Result<Vec<(u32, VerificationResult)>> {
|
||||
tracing::info!(
|
||||
"Starting one-shot verification of batches {} to {}",
|
||||
self.start_batch.0,
|
||||
self.end_batch.0
|
||||
);
|
||||
|
||||
let mut results = Vec::new();
|
||||
let mut success_count = 0;
|
||||
let mut failure_count = 0;
|
||||
|
||||
for batch_number in self.start_batch.0..=self.end_batch.0 {
|
||||
let batch = L1BatchNumber(batch_number);
|
||||
let result = self.batch_processor.process_batch(token, batch).await?;
|
||||
|
||||
match result {
|
||||
VerificationResult::Success | VerificationResult::PartialSuccess { .. } => {
|
||||
success_count += 1;
|
||||
}
|
||||
VerificationResult::Failure => failure_count += 1,
|
||||
VerificationResult::Interrupted => {
|
||||
results.push((batch_number, result));
|
||||
break;
|
||||
}
|
||||
VerificationResult::NoProofsFound => {}
|
||||
}
|
||||
|
||||
results.push((batch_number, result));
|
||||
}
|
||||
|
||||
// Log overall results
|
||||
BatchProcessor::log_overall_results(success_count, failure_count);
|
||||
|
||||
Ok(results)
|
||||
}
|
||||
}
|
172
bin/verify-era-proof-attestation/src/proof.rs
Normal file
172
bin/verify-era-proof-attestation/src/proof.rs
Normal file
|
@ -0,0 +1,172 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (c) 2023-2024 Matter Labs
|
||||
|
||||
use anyhow::{bail, Result};
|
||||
use jsonrpsee_types::error::ErrorObject;
|
||||
use reqwest::Client;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_with::{hex::Hex, serde_as};
|
||||
use std::time::Duration;
|
||||
use tokio::sync::watch;
|
||||
use tracing::{error, warn};
|
||||
use url::Url;
|
||||
use zksync_basic_types::L1BatchNumber;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct GetProofsRequest {
|
||||
pub jsonrpc: String,
|
||||
pub id: u32,
|
||||
pub method: String,
|
||||
pub params: (L1BatchNumber, String),
|
||||
}
|
||||
|
||||
pub async fn get_proofs(
|
||||
stop_receiver: &mut watch::Receiver<bool>,
|
||||
batch_number: L1BatchNumber,
|
||||
http_client: &Client,
|
||||
rpc_url: &Url,
|
||||
) -> Result<Vec<Proof>> {
|
||||
let mut proofs_request = GetProofsRequest::new(batch_number);
|
||||
let mut retries = 0;
|
||||
let mut backoff = Duration::from_secs(1);
|
||||
let max_backoff = Duration::from_secs(128);
|
||||
let retry_backoff_multiplier: f32 = 2.0;
|
||||
|
||||
while !*stop_receiver.borrow() {
|
||||
let proofs = proofs_request
|
||||
.send(stop_receiver, http_client, rpc_url)
|
||||
.await?;
|
||||
|
||||
if !proofs.is_empty()
|
||||
&& proofs.iter().all(|proof| {
|
||||
!proof.status.as_ref().map_or(false, |s| {
|
||||
s.eq_ignore_ascii_case("failed") | s.eq_ignore_ascii_case("picked_by_prover")
|
||||
})
|
||||
})
|
||||
{
|
||||
return Ok(proofs);
|
||||
}
|
||||
|
||||
retries += 1;
|
||||
warn!(
|
||||
batch_no = batch_number.0, retries,
|
||||
"No TEE proofs found for batch #{}. They may not be ready yet. Retrying in {} milliseconds.",
|
||||
batch_number, backoff.as_millis(),
|
||||
);
|
||||
|
||||
tokio::time::timeout(backoff, stop_receiver.changed())
|
||||
.await
|
||||
.ok();
|
||||
|
||||
backoff = std::cmp::min(backoff.mul_f32(retry_backoff_multiplier), max_backoff);
|
||||
}
|
||||
|
||||
Ok(vec![])
|
||||
}
|
||||
|
||||
impl GetProofsRequest {
|
||||
pub fn new(batch_number: L1BatchNumber) -> Self {
|
||||
GetProofsRequest {
|
||||
jsonrpc: "2.0".to_string(),
|
||||
id: 1,
|
||||
method: "unstable_getTeeProofs".to_string(),
|
||||
params: (batch_number, "sgx".to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn send(
|
||||
&mut self,
|
||||
stop_receiver: &mut watch::Receiver<bool>,
|
||||
http_client: &Client,
|
||||
rpc_url: &Url,
|
||||
) -> Result<Vec<Proof>> {
|
||||
let mut retries = 0;
|
||||
let max_retries = 5;
|
||||
let mut backoff = Duration::from_secs(1);
|
||||
let max_backoff = Duration::from_secs(128);
|
||||
let retry_backoff_multiplier: f32 = 2.0;
|
||||
let mut response = None;
|
||||
|
||||
while !*stop_receiver.borrow() {
|
||||
let result = http_client
|
||||
.post(rpc_url.clone())
|
||||
.json(self)
|
||||
.send()
|
||||
.await?
|
||||
.error_for_status()?
|
||||
.json::<GetProofsResponse>()
|
||||
.await;
|
||||
|
||||
match result {
|
||||
Ok(res) => match res.error {
|
||||
None => {
|
||||
response = Some(res);
|
||||
break;
|
||||
}
|
||||
Some(error) => {
|
||||
// Handle corner case, where the old RPC interface expects 'Sgx'
|
||||
if let Some(data) = error.data() {
|
||||
if data.get().contains("unknown variant `sgx`, expected `Sgx`") {
|
||||
self.params.1 = "Sgx".to_string();
|
||||
continue;
|
||||
}
|
||||
}
|
||||
error!(?error, "received JSONRPC error {error:?}");
|
||||
bail!("JSONRPC error {error:?}");
|
||||
}
|
||||
},
|
||||
Err(err) => {
|
||||
retries += 1;
|
||||
if retries >= max_retries {
|
||||
return Err(anyhow::anyhow!(
|
||||
"Failed to send request to {} after {} retries: {}. Request details: {:?}",
|
||||
rpc_url,
|
||||
max_retries,
|
||||
err,
|
||||
self
|
||||
));
|
||||
}
|
||||
warn!(
|
||||
%err,
|
||||
"Failed to send request to {rpc_url}. {retries}/{max_retries}, retrying in {} milliseconds. Request details: {:?}",
|
||||
backoff.as_millis(),
|
||||
self
|
||||
);
|
||||
tokio::time::timeout(backoff, stop_receiver.changed())
|
||||
.await
|
||||
.ok();
|
||||
backoff = std::cmp::min(backoff.mul_f32(retry_backoff_multiplier), max_backoff);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
Ok(response.map_or_else(Vec::new, |res| res.result.unwrap_or_default()))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct GetProofsResponse {
|
||||
pub jsonrpc: String,
|
||||
pub result: Option<Vec<Proof>>,
|
||||
pub id: u32,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub error: Option<ErrorObject<'static>>,
|
||||
}
|
||||
|
||||
#[serde_as]
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Proof {
|
||||
pub l1_batch_number: u32,
|
||||
pub tee_type: String,
|
||||
#[serde_as(as = "Option<Hex>")]
|
||||
pub pubkey: Option<Vec<u8>>,
|
||||
#[serde_as(as = "Option<Hex>")]
|
||||
pub signature: Option<Vec<u8>>,
|
||||
#[serde_as(as = "Option<Hex>")]
|
||||
pub proof: Option<Vec<u8>>,
|
||||
pub proved_at: String,
|
||||
pub status: Option<String>,
|
||||
#[serde_as(as = "Option<Hex>")]
|
||||
pub attestation: Option<Vec<u8>>,
|
||||
}
|
|
@ -1,137 +0,0 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (c) 2023-2025 Matter Labs
|
||||
|
||||
use crate::{
|
||||
client::{HttpClient, RetryConfig, RetryHelper},
|
||||
error::{Error, Result},
|
||||
proof::{
|
||||
parsing::ProofResponseParser,
|
||||
types::{GetProofsRequest, GetProofsResponse, Proof},
|
||||
},
|
||||
};
|
||||
use std::time::Duration;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
use url::Url;
|
||||
use zksync_basic_types::{tee_types::TeeType, L1BatchNumber};
|
||||
|
||||
/// Handles fetching proofs from the server with retry logic
|
||||
pub struct ProofFetcher {
|
||||
http_client: HttpClient,
|
||||
rpc_url: Url,
|
||||
retry_config: RetryConfig,
|
||||
}
|
||||
|
||||
impl ProofFetcher {
|
||||
/// Create a new proof fetcher
|
||||
pub fn new(http_client: HttpClient, rpc_url: Url, retry_config: RetryConfig) -> Self {
|
||||
Self {
|
||||
http_client,
|
||||
rpc_url,
|
||||
retry_config,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get proofs for a batch number with retry logic
|
||||
pub async fn get_proofs(
|
||||
&self,
|
||||
token: &CancellationToken,
|
||||
batch_number: L1BatchNumber,
|
||||
tee_type: TeeType,
|
||||
) -> Result<Vec<Proof>> {
|
||||
let mut proofs_request = GetProofsRequest::new(batch_number, tee_type);
|
||||
let mut backoff = Duration::from_secs(1);
|
||||
let max_backoff = Duration::from_secs(128);
|
||||
let retry_backoff_multiplier: f32 = 2.0;
|
||||
|
||||
while !token.is_cancelled() {
|
||||
match self.send_request(&proofs_request, token).await {
|
||||
Ok(response) => {
|
||||
// Parse the response using the ProofResponseParser
|
||||
match ProofResponseParser::parse_response(response) {
|
||||
Ok(proofs) => {
|
||||
// Filter valid proofs
|
||||
let valid_proofs = ProofResponseParser::filter_valid_proofs(&proofs);
|
||||
|
||||
if !valid_proofs.is_empty() {
|
||||
return Ok(valid_proofs);
|
||||
}
|
||||
|
||||
// No valid proofs found, retry
|
||||
let error_msg = format!(
|
||||
"No valid TEE proofs found for batch #{}. They may not be ready yet. Retrying in {} milliseconds.",
|
||||
batch_number.0,
|
||||
backoff.as_millis()
|
||||
);
|
||||
tracing::warn!(batch_no = batch_number.0, "{}", error_msg);
|
||||
// Here we could use the ProofFetching error if we needed to return immediately
|
||||
// return Err(Error::ProofFetching(error_msg));
|
||||
}
|
||||
Err(e) => {
|
||||
// Handle specific error for Sgx variant
|
||||
if let Error::JsonRpc(msg) = &e {
|
||||
if msg.contains("RPC requires 'Sgx' variant") {
|
||||
tracing::debug!("Switching to 'Sgx' variant for RPC");
|
||||
proofs_request.params.1 = "Sgx".to_string();
|
||||
continue;
|
||||
}
|
||||
}
|
||||
return Err(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
return Err(e);
|
||||
}
|
||||
}
|
||||
|
||||
tokio::time::timeout(backoff, token.cancelled()).await.ok();
|
||||
|
||||
backoff = std::cmp::min(
|
||||
Duration::from_millis(
|
||||
(backoff.as_millis() as f32 * retry_backoff_multiplier) as u64,
|
||||
),
|
||||
max_backoff,
|
||||
);
|
||||
|
||||
if token.is_cancelled() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// If we've reached this point, we've either been stopped or exhausted retries
|
||||
if token.is_cancelled() {
|
||||
// Return empty vector if stopped
|
||||
Ok(vec![])
|
||||
} else {
|
||||
// Use the ProofFetching error variant if we've exhausted retries
|
||||
Err(Error::proof_fetch(batch_number, "exhausted retries"))
|
||||
}
|
||||
}
|
||||
|
||||
/// Send a request to the server with retry logic
|
||||
async fn send_request(
|
||||
&self,
|
||||
request: &GetProofsRequest,
|
||||
token: &CancellationToken,
|
||||
) -> Result<GetProofsResponse> {
|
||||
let retry_helper = RetryHelper::new(self.retry_config.clone());
|
||||
let request_clone = request.clone();
|
||||
let http_client = self.http_client.clone();
|
||||
let rpc_url = self.rpc_url.clone();
|
||||
|
||||
retry_helper
|
||||
.execute(&format!("get_proofs_{}", request.params.0), || async {
|
||||
let result = http_client
|
||||
.send_json::<_, GetProofsResponse>(&rpc_url, &request_clone)
|
||||
.await;
|
||||
|
||||
// Check if we need to abort due to stop signal
|
||||
if token.is_cancelled() {
|
||||
return Err(Error::Interrupted);
|
||||
}
|
||||
|
||||
result
|
||||
})
|
||||
.await
|
||||
}
|
||||
}
|
|
@ -1,9 +0,0 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (c) 2023-2025 Matter Labs
|
||||
|
||||
mod fetcher;
|
||||
mod parsing;
|
||||
mod types;
|
||||
|
||||
pub use fetcher::ProofFetcher;
|
||||
pub use types::Proof;
|
|
@ -1,277 +0,0 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (c) 2023-2025 Matter Labs
|
||||
|
||||
use super::types::{GetProofsResponse, Proof};
|
||||
use crate::error;
|
||||
|
||||
/// Handles parsing of proof responses and error handling
|
||||
pub struct ProofResponseParser;
|
||||
|
||||
impl ProofResponseParser {
|
||||
/// Parse a response and extract the proofs
|
||||
pub fn parse_response(response: GetProofsResponse) -> error::Result<Vec<Proof>> {
|
||||
// Handle JSON-RPC errors
|
||||
if let Some(error) = response.error {
|
||||
// Special case for handling the old RPC interface
|
||||
if let Some(data) = error.data() {
|
||||
if data.get().contains("unknown variant `sgx`, expected `Sgx`") {
|
||||
return Err(error::Error::JsonRpc(
|
||||
"RPC requires 'Sgx' variant instead of 'sgx'".to_string(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
return Err(error::Error::JsonRpc(format!("JSONRPC error: {error:?}")));
|
||||
}
|
||||
|
||||
// Extract proofs from the result
|
||||
Ok(response.result.unwrap_or_default())
|
||||
}
|
||||
|
||||
/// Filter proofs to find valid ones
|
||||
pub fn filter_valid_proofs(proofs: &[Proof]) -> Vec<Proof> {
|
||||
proofs
|
||||
.iter()
|
||||
.filter(|proof| !proof.is_failed_or_picked())
|
||||
.cloned()
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use jsonrpsee_types::error::ErrorObject;
|
||||
|
||||
#[test]
|
||||
fn test_proof_is_permanently_ignored() {
|
||||
let proof = Proof {
|
||||
l1_batch_number: 123,
|
||||
tee_type: "TDX".to_string(),
|
||||
pubkey: None,
|
||||
signature: None,
|
||||
proof: None,
|
||||
proved_at: "2023-01-01T00:00:00Z".to_string(),
|
||||
status: Some("permanently_ignored".to_string()),
|
||||
attestation: None,
|
||||
};
|
||||
|
||||
assert!(proof.is_permanently_ignored());
|
||||
|
||||
let proof = Proof {
|
||||
l1_batch_number: 123,
|
||||
tee_type: "TDX".to_string(),
|
||||
pubkey: None,
|
||||
signature: None,
|
||||
proof: None,
|
||||
proved_at: "2023-01-01T00:00:00Z".to_string(),
|
||||
status: Some("PERMANENTLY_IGNORED".to_string()),
|
||||
attestation: None,
|
||||
};
|
||||
|
||||
assert!(proof.is_permanently_ignored());
|
||||
|
||||
let proof = Proof {
|
||||
l1_batch_number: 123,
|
||||
tee_type: "TDX".to_string(),
|
||||
pubkey: None,
|
||||
signature: None,
|
||||
proof: None,
|
||||
proved_at: "2023-01-01T00:00:00Z".to_string(),
|
||||
status: Some("other".to_string()),
|
||||
attestation: None,
|
||||
};
|
||||
|
||||
assert!(!proof.is_permanently_ignored());
|
||||
|
||||
let proof = Proof {
|
||||
l1_batch_number: 123,
|
||||
tee_type: "TDX".to_string(),
|
||||
pubkey: None,
|
||||
signature: None,
|
||||
proof: None,
|
||||
proved_at: "2023-01-01T00:00:00Z".to_string(),
|
||||
status: None,
|
||||
attestation: None,
|
||||
};
|
||||
|
||||
assert!(!proof.is_permanently_ignored());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_proof_is_failed_or_picked() {
|
||||
let proof = Proof {
|
||||
l1_batch_number: 123,
|
||||
tee_type: "TDX".to_string(),
|
||||
pubkey: None,
|
||||
signature: None,
|
||||
proof: None,
|
||||
proved_at: "2023-01-01T00:00:00Z".to_string(),
|
||||
status: Some("failed".to_string()),
|
||||
attestation: None,
|
||||
};
|
||||
|
||||
assert!(proof.is_failed_or_picked());
|
||||
|
||||
let proof = Proof {
|
||||
l1_batch_number: 123,
|
||||
tee_type: "TDX".to_string(),
|
||||
pubkey: None,
|
||||
signature: None,
|
||||
proof: None,
|
||||
proved_at: "2023-01-01T00:00:00Z".to_string(),
|
||||
status: Some("picked_by_prover".to_string()),
|
||||
attestation: None,
|
||||
};
|
||||
|
||||
assert!(proof.is_failed_or_picked());
|
||||
|
||||
let proof = Proof {
|
||||
l1_batch_number: 123,
|
||||
tee_type: "TDX".to_string(),
|
||||
pubkey: None,
|
||||
signature: None,
|
||||
proof: None,
|
||||
proved_at: "2023-01-01T00:00:00Z".to_string(),
|
||||
status: Some("FAILED".to_string()),
|
||||
attestation: None,
|
||||
};
|
||||
|
||||
assert!(proof.is_failed_or_picked());
|
||||
|
||||
let proof = Proof {
|
||||
l1_batch_number: 123,
|
||||
tee_type: "TDX".to_string(),
|
||||
pubkey: None,
|
||||
signature: None,
|
||||
proof: None,
|
||||
proved_at: "2023-01-01T00:00:00Z".to_string(),
|
||||
status: Some("other".to_string()),
|
||||
attestation: None,
|
||||
};
|
||||
|
||||
assert!(!proof.is_failed_or_picked());
|
||||
|
||||
let proof = Proof {
|
||||
l1_batch_number: 123,
|
||||
tee_type: "TDX".to_string(),
|
||||
pubkey: None,
|
||||
signature: None,
|
||||
proof: None,
|
||||
proved_at: "2023-01-01T00:00:00Z".to_string(),
|
||||
status: None,
|
||||
attestation: None,
|
||||
};
|
||||
|
||||
assert!(!proof.is_failed_or_picked());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_response_success() {
|
||||
let response = GetProofsResponse {
|
||||
jsonrpc: "2.0".to_string(),
|
||||
result: Some(vec![Proof {
|
||||
l1_batch_number: 123,
|
||||
tee_type: "TDX".to_string(),
|
||||
pubkey: None,
|
||||
signature: None,
|
||||
proof: None,
|
||||
proved_at: "2023-01-01T00:00:00Z".to_string(),
|
||||
status: None,
|
||||
attestation: None,
|
||||
}]),
|
||||
id: 1,
|
||||
error: None,
|
||||
};
|
||||
|
||||
let proofs = ProofResponseParser::parse_response(response).unwrap();
|
||||
assert_eq!(proofs.len(), 1);
|
||||
assert_eq!(proofs[0].l1_batch_number, 123);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_response_error() {
|
||||
let response = GetProofsResponse {
|
||||
jsonrpc: "2.0".to_string(),
|
||||
result: None,
|
||||
id: 1,
|
||||
error: Some(ErrorObject::owned(1, "Error", None::<()>)),
|
||||
};
|
||||
|
||||
let error = ProofResponseParser::parse_response(response).unwrap_err();
|
||||
match error {
|
||||
error::Error::JsonRpc(msg) => {
|
||||
assert!(msg.contains("JSONRPC error"));
|
||||
}
|
||||
_ => panic!("Expected JsonRpc error"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_response_sgx_variant_error() {
|
||||
let error_obj = ErrorObject::owned(
|
||||
1,
|
||||
"Error",
|
||||
Some(
|
||||
serde_json::to_value("unknown variant `sgx`, expected `Sgx`")
|
||||
.unwrap()
|
||||
.to_string(),
|
||||
),
|
||||
);
|
||||
|
||||
let response = GetProofsResponse {
|
||||
jsonrpc: "2.0".to_string(),
|
||||
result: None,
|
||||
id: 1,
|
||||
error: Some(error_obj),
|
||||
};
|
||||
|
||||
let error = ProofResponseParser::parse_response(response).unwrap_err();
|
||||
match error {
|
||||
error::Error::JsonRpc(msg) => {
|
||||
assert!(msg.contains("RPC requires 'Sgx' variant"));
|
||||
}
|
||||
_ => panic!("Expected JsonRpc error about Sgx variant"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_filter_valid_proofs() {
|
||||
let proofs = vec![
|
||||
Proof {
|
||||
l1_batch_number: 123,
|
||||
tee_type: "TDX".to_string(),
|
||||
pubkey: None,
|
||||
signature: None,
|
||||
proof: None,
|
||||
proved_at: "2023-01-01T00:00:00Z".to_string(),
|
||||
status: None,
|
||||
attestation: None,
|
||||
},
|
||||
Proof {
|
||||
l1_batch_number: 124,
|
||||
tee_type: "TDX".to_string(),
|
||||
pubkey: None,
|
||||
signature: None,
|
||||
proof: None,
|
||||
proved_at: "2023-01-01T00:00:00Z".to_string(),
|
||||
status: Some("failed".to_string()),
|
||||
attestation: None,
|
||||
},
|
||||
Proof {
|
||||
l1_batch_number: 125,
|
||||
tee_type: "TDX".to_string(),
|
||||
pubkey: None,
|
||||
signature: None,
|
||||
proof: None,
|
||||
proved_at: "2023-01-01T00:00:00Z".to_string(),
|
||||
status: Some("picked_by_prover".to_string()),
|
||||
attestation: None,
|
||||
},
|
||||
];
|
||||
|
||||
let valid_proofs = ProofResponseParser::filter_valid_proofs(&proofs);
|
||||
assert_eq!(valid_proofs.len(), 1);
|
||||
assert_eq!(valid_proofs[0].l1_batch_number, 123);
|
||||
}
|
||||
}
|
|
@ -1,83 +0,0 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (c) 2023-2025 Matter Labs
|
||||
|
||||
use jsonrpsee_types::error::ErrorObject;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_with::{hex::Hex, serde_as};
|
||||
use zksync_basic_types::{tee_types::TeeType, L1BatchNumber};
|
||||
|
||||
/// Request structure for fetching proofs
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct GetProofsRequest {
|
||||
pub jsonrpc: String,
|
||||
pub id: u32,
|
||||
pub method: String,
|
||||
pub params: (L1BatchNumber, String),
|
||||
}
|
||||
|
||||
impl GetProofsRequest {
|
||||
/// Create a new request for the given batch number
|
||||
pub fn new(batch_number: L1BatchNumber, tee_type: TeeType) -> Self {
|
||||
GetProofsRequest {
|
||||
jsonrpc: "2.0".to_string(),
|
||||
id: 1,
|
||||
method: "unstable_getTeeProofs".to_string(),
|
||||
params: (batch_number, tee_type.to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Response structure for proof requests
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct GetProofsResponse {
|
||||
pub jsonrpc: String,
|
||||
pub result: Option<Vec<Proof>>,
|
||||
pub id: u32,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub error: Option<ErrorObject<'static>>,
|
||||
}
|
||||
|
||||
/// Proof structure containing attestation and signature data
|
||||
#[serde_as]
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Proof {
|
||||
pub l1_batch_number: u32,
|
||||
pub tee_type: String,
|
||||
#[serde_as(as = "Option<Hex>")]
|
||||
pub pubkey: Option<Vec<u8>>,
|
||||
#[serde_as(as = "Option<Hex>")]
|
||||
pub signature: Option<Vec<u8>>,
|
||||
#[serde_as(as = "Option<Hex>")]
|
||||
pub proof: Option<Vec<u8>>,
|
||||
pub proved_at: String,
|
||||
pub status: Option<String>,
|
||||
#[serde_as(as = "Option<Hex>")]
|
||||
pub attestation: Option<Vec<u8>>,
|
||||
}
|
||||
|
||||
impl Proof {
|
||||
/// Check if the proof is marked as permanently ignored
|
||||
pub fn is_permanently_ignored(&self) -> bool {
|
||||
self.status
|
||||
.as_ref()
|
||||
.is_some_and(|s| s.eq_ignore_ascii_case("permanently_ignored"))
|
||||
}
|
||||
|
||||
/// Check if the proof is failed or picked by a prover
|
||||
pub fn is_failed_or_picked(&self) -> bool {
|
||||
self.status.as_ref().is_some_and(|s| {
|
||||
s.eq_ignore_ascii_case("failed") || s.eq_ignore_ascii_case("picked_by_prover")
|
||||
})
|
||||
}
|
||||
|
||||
/// Get the attestation bytes or an empty vector if not present
|
||||
pub fn attestation_bytes(&self) -> Vec<u8> {
|
||||
self.attestation.clone().unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Get the signature bytes or an empty vector if not present
|
||||
pub fn signature_bytes(&self) -> Vec<u8> {
|
||||
self.signature.clone().unwrap_or_default()
|
||||
}
|
||||
}
|
168
bin/verify-era-proof-attestation/src/verification.rs
Normal file
168
bin/verify-era-proof-attestation/src/verification.rs
Normal file
|
@ -0,0 +1,168 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (c) 2023-2024 Matter Labs
|
||||
|
||||
use crate::{args::AttestationPolicyArgs, client::JsonRpcClient};
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use hex::encode;
|
||||
use secp256k1::{ecdsa::Signature, Message};
|
||||
use teepot::{
|
||||
client::TcbLevel,
|
||||
ethereum::recover_signer,
|
||||
prover::reportdata::ReportData,
|
||||
quote::{
|
||||
error::QuoteContext, tee_qv_get_collateral, verify_quote_with_collateral,
|
||||
QuoteVerificationResult, Report,
|
||||
},
|
||||
};
|
||||
use tracing::{debug, info, warn};
|
||||
use zksync_basic_types::{L1BatchNumber, H256};
|
||||
|
||||
struct TeeProof {
|
||||
report: ReportData,
|
||||
root_hash: H256,
|
||||
signature: Vec<u8>,
|
||||
}
|
||||
|
||||
impl TeeProof {
|
||||
pub fn new(report: ReportData, root_hash: H256, signature: Vec<u8>) -> Self {
|
||||
Self {
|
||||
report,
|
||||
root_hash,
|
||||
signature,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn verify(&self) -> Result<bool> {
|
||||
match &self.report {
|
||||
ReportData::V0(report) => {
|
||||
let signature = Signature::from_compact(&self.signature)?;
|
||||
let root_hash_msg = Message::from_digest_slice(&self.root_hash.0)?;
|
||||
Ok(signature.verify(&root_hash_msg, &report.pubkey).is_ok())
|
||||
}
|
||||
ReportData::V1(report) => {
|
||||
let ethereum_address_from_report = report.ethereum_address;
|
||||
let root_hash_msg = Message::from_digest_slice(self.root_hash.as_bytes())?;
|
||||
let signature_bytes: [u8; 65] = self
|
||||
.signature
|
||||
.clone()
|
||||
.try_into()
|
||||
.map_err(|e| anyhow!("{:?}", e))?;
|
||||
let ethereum_address_from_signature =
|
||||
recover_signer(&signature_bytes, &root_hash_msg)?;
|
||||
debug!(
|
||||
"Root hash: {}. Ethereum address from the attestation quote: {}. Ethereum address from the signature: {}.",
|
||||
self.root_hash,
|
||||
encode(ethereum_address_from_report),
|
||||
encode(ethereum_address_from_signature),
|
||||
);
|
||||
Ok(ethereum_address_from_signature == ethereum_address_from_report)
|
||||
}
|
||||
ReportData::Unknown(_) => Ok(false),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn verify_batch_proof(
|
||||
quote_verification_result: &QuoteVerificationResult,
|
||||
attestation_policy: &AttestationPolicyArgs,
|
||||
node_client: &impl JsonRpcClient,
|
||||
signature: &[u8],
|
||||
batch_number: L1BatchNumber,
|
||||
) -> Result<bool> {
|
||||
if !is_quote_matching_policy(attestation_policy, quote_verification_result) {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
let root_hash = node_client.get_root_hash(batch_number).await?;
|
||||
let report_data_bytes = quote_verification_result.quote.get_report_data();
|
||||
let report_data = ReportData::try_from(report_data_bytes)?;
|
||||
let tee_proof = TeeProof::new(report_data, root_hash, signature.to_vec());
|
||||
let verification_successful = tee_proof.verify().is_ok();
|
||||
Ok(verification_successful)
|
||||
}
|
||||
|
||||
pub fn verify_attestation_quote(attestation_quote_bytes: &[u8]) -> Result<QuoteVerificationResult> {
|
||||
let collateral = QuoteContext::context(
|
||||
tee_qv_get_collateral(attestation_quote_bytes),
|
||||
"Failed to get collateral!",
|
||||
)?;
|
||||
let unix_time: i64 = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)?
|
||||
.as_secs() as _;
|
||||
verify_quote_with_collateral(attestation_quote_bytes, Some(&collateral), unix_time)
|
||||
.context("Failed to verify quote with collateral!")
|
||||
}
|
||||
|
||||
pub fn log_quote_verification_summary(quote_verification_result: &QuoteVerificationResult) {
|
||||
let QuoteVerificationResult {
|
||||
collateral_expired,
|
||||
result,
|
||||
quote,
|
||||
advisories,
|
||||
..
|
||||
} = quote_verification_result;
|
||||
if *collateral_expired {
|
||||
warn!("Freshly fetched collateral expired!");
|
||||
}
|
||||
let tcblevel = TcbLevel::from(*result);
|
||||
let advisories = if advisories.is_empty() {
|
||||
"None".to_string()
|
||||
} else {
|
||||
advisories
|
||||
.iter()
|
||||
.map(ToString::to_string)
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ")
|
||||
};
|
||||
|
||||
info!(
|
||||
"Quote verification result: {tcblevel}. {report}. Advisory IDs: {advisories}.",
|
||||
report = "e.report
|
||||
);
|
||||
}
|
||||
|
||||
fn is_quote_matching_policy(
|
||||
attestation_policy: &AttestationPolicyArgs,
|
||||
quote_verification_result: &QuoteVerificationResult,
|
||||
) -> bool {
|
||||
let quote = "e_verification_result.quote;
|
||||
let tcblevel = TcbLevel::from(quote_verification_result.result);
|
||||
|
||||
if !attestation_policy.sgx_allowed_tcb_levels.contains(tcblevel) {
|
||||
warn!(
|
||||
"Quote verification failed: TCB level mismatch (expected one of: {:?}, actual: {})",
|
||||
attestation_policy.sgx_allowed_tcb_levels, tcblevel
|
||||
);
|
||||
return false;
|
||||
}
|
||||
match "e.report {
|
||||
Report::SgxEnclave(report_body) => {
|
||||
check_policy(
|
||||
attestation_policy.sgx_mrsigners.as_deref(),
|
||||
&report_body.mr_signer,
|
||||
"mrsigner",
|
||||
) && check_policy(
|
||||
attestation_policy.sgx_mrenclaves.as_deref(),
|
||||
&report_body.mr_enclave,
|
||||
"mrenclave",
|
||||
)
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn check_policy(policy: Option<&str>, actual_value: &[u8], field_name: &str) -> bool {
|
||||
if let Some(valid_values) = policy {
|
||||
let valid_values: Vec<&str> = valid_values.split(',').collect();
|
||||
let actual_value = hex::encode(actual_value);
|
||||
if !valid_values.contains(&actual_value.as_str()) {
|
||||
warn!(
|
||||
"Quote verification failed: {} mismatch (expected one of: {:?}, actual: {})",
|
||||
field_name, valid_values, actual_value
|
||||
);
|
||||
return false;
|
||||
}
|
||||
debug!(field_name, actual_value, "Attestation policy check passed");
|
||||
}
|
||||
true
|
||||
}
|
|
@ -1,29 +0,0 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (c) 2023-2025 Matter Labs
|
||||
|
||||
use teepot::quote::{get_collateral, verify_quote_with_collateral, QuoteVerificationResult};
|
||||
|
||||
use crate::error;
|
||||
|
||||
/// Handles verification of attestation quotes
|
||||
pub struct AttestationVerifier;
|
||||
|
||||
impl AttestationVerifier {
|
||||
/// Verify an attestation quote
|
||||
pub fn verify_quote(attestation_quote_bytes: &[u8]) -> error::Result<QuoteVerificationResult> {
|
||||
// Get collateral for the quote
|
||||
let collateral = get_collateral(attestation_quote_bytes)?;
|
||||
|
||||
// Get current time for verification
|
||||
let unix_time: i64 = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.map_err(|e| error::Error::internal(format!("Failed to get system time: {e}")))?
|
||||
.as_secs() as _;
|
||||
|
||||
// Verify the quote with the collateral
|
||||
let res =
|
||||
verify_quote_with_collateral(attestation_quote_bytes, Some(&collateral), unix_time)?;
|
||||
|
||||
Ok(res)
|
||||
}
|
||||
}
|
|
@ -1,141 +0,0 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (c) 2025 Matter Labs
|
||||
|
||||
use crate::{
|
||||
client::JsonRpcClient,
|
||||
core::AttestationPolicy,
|
||||
error,
|
||||
proof::Proof,
|
||||
verification::{AttestationVerifier, PolicyEnforcer, SignatureVerifier, VerificationReporter},
|
||||
};
|
||||
use tokio_util::sync::CancellationToken;
|
||||
use zksync_basic_types::L1BatchNumber;
|
||||
|
||||
/// Result of a batch verification
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct BatchVerificationResult {
|
||||
/// Total number of proofs processed
|
||||
pub total_count: u32,
|
||||
/// Number of proofs that were verified successfully
|
||||
pub verified_count: u32,
|
||||
/// Number of proofs that failed verification
|
||||
pub unverified_count: u32,
|
||||
}
|
||||
|
||||
/// Handles the batch verification process
|
||||
pub struct BatchVerifier<C: JsonRpcClient> {
|
||||
node_client: C,
|
||||
attestation_policy: AttestationPolicy,
|
||||
}
|
||||
|
||||
impl<C: JsonRpcClient> BatchVerifier<C> {
|
||||
/// Create a new batch verifier
|
||||
pub fn new(node_client: C, attestation_policy: AttestationPolicy) -> Self {
|
||||
Self {
|
||||
node_client,
|
||||
attestation_policy,
|
||||
}
|
||||
}
|
||||
|
||||
/// Verify proofs for a batch
|
||||
pub async fn verify_batch_proofs(
|
||||
&self,
|
||||
token: &CancellationToken,
|
||||
batch_number: L1BatchNumber,
|
||||
proofs: Vec<Proof>,
|
||||
) -> error::Result<BatchVerificationResult> {
|
||||
let batch_no = batch_number.0;
|
||||
let mut total_proofs_count: u32 = 0;
|
||||
let mut verified_proofs_count: u32 = 0;
|
||||
|
||||
for proof in proofs {
|
||||
if token.is_cancelled() {
|
||||
tracing::warn!("Stop signal received during batch verification");
|
||||
return Ok(BatchVerificationResult {
|
||||
total_count: total_proofs_count,
|
||||
verified_count: verified_proofs_count,
|
||||
unverified_count: total_proofs_count - verified_proofs_count,
|
||||
});
|
||||
}
|
||||
|
||||
total_proofs_count += 1;
|
||||
let tee_type = proof.tee_type.to_uppercase();
|
||||
|
||||
if proof.is_permanently_ignored() {
|
||||
tracing::debug!(
|
||||
batch_no,
|
||||
tee_type,
|
||||
"Proof is marked as permanently ignored. Skipping."
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
tracing::debug!(batch_no, tee_type, proof.proved_at, "Verifying proof.");
|
||||
|
||||
let attestation_bytes = proof.attestation_bytes();
|
||||
let signature_bytes = proof.signature_bytes();
|
||||
|
||||
tracing::debug!(
|
||||
batch_no,
|
||||
"Verifying quote ({} bytes)...",
|
||||
attestation_bytes.len()
|
||||
);
|
||||
|
||||
// Verify attestation
|
||||
let quote_verification_result = AttestationVerifier::verify_quote(&attestation_bytes)?;
|
||||
|
||||
// Log verification results
|
||||
VerificationReporter::log_quote_verification_summary("e_verification_result);
|
||||
|
||||
// Check if attestation matches policy
|
||||
let policy_matches = PolicyEnforcer::validate_policy(
|
||||
&self.attestation_policy,
|
||||
"e_verification_result,
|
||||
);
|
||||
|
||||
if let Err(e) = policy_matches {
|
||||
tracing::error!(batch_no, tee_type, "Attestation policy check failed: {e}");
|
||||
continue;
|
||||
}
|
||||
|
||||
// Verify signature
|
||||
let root_hash = self
|
||||
.node_client
|
||||
.get_root_hash(L1BatchNumber(proof.l1_batch_number))
|
||||
.await?;
|
||||
|
||||
let signature_verified = SignatureVerifier::verify_batch_proof(
|
||||
"e_verification_result,
|
||||
root_hash,
|
||||
&signature_bytes,
|
||||
)?;
|
||||
|
||||
if signature_verified {
|
||||
tracing::info!(
|
||||
batch_no,
|
||||
proof.proved_at,
|
||||
tee_type,
|
||||
"Verification succeeded.",
|
||||
);
|
||||
verified_proofs_count += 1;
|
||||
} else {
|
||||
tracing::warn!(batch_no, proof.proved_at, tee_type, "Verification failed!");
|
||||
}
|
||||
}
|
||||
|
||||
let unverified_proofs_count = total_proofs_count.saturating_sub(verified_proofs_count);
|
||||
|
||||
// Log batch verification results
|
||||
VerificationReporter::log_batch_verification_results(
|
||||
batch_no,
|
||||
verified_proofs_count,
|
||||
unverified_proofs_count,
|
||||
);
|
||||
|
||||
Ok(BatchVerificationResult {
|
||||
total_count: total_proofs_count,
|
||||
verified_count: verified_proofs_count,
|
||||
unverified_count: unverified_proofs_count,
|
||||
})
|
||||
}
|
||||
}
|
|
@ -1,14 +0,0 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (c) 2023-2025 Matter Labs
|
||||
|
||||
mod attestation;
|
||||
mod batch;
|
||||
mod policy;
|
||||
mod reporting;
|
||||
mod signature;
|
||||
|
||||
pub use attestation::AttestationVerifier;
|
||||
pub use batch::BatchVerifier;
|
||||
pub use policy::PolicyEnforcer;
|
||||
pub use reporting::VerificationReporter;
|
||||
pub use signature::SignatureVerifier;
|
|
@ -1,207 +0,0 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (c) 2023-2025 Matter Labs
|
||||
|
||||
use crate::{
|
||||
core::AttestationPolicy,
|
||||
error::{Error, Result},
|
||||
};
|
||||
use bytes::Bytes;
|
||||
use enumset::EnumSet;
|
||||
use teepot::quote::{tcblevel::TcbLevel, QuoteVerificationResult, Report};
|
||||
|
||||
/// Enforces policy requirements on attestation quotes
|
||||
pub struct PolicyEnforcer;
|
||||
|
||||
impl PolicyEnforcer {
|
||||
/// Check if a quote matches the attestation policy
|
||||
pub fn validate_policy(
|
||||
attestation_policy: &AttestationPolicy,
|
||||
quote_verification_result: &QuoteVerificationResult,
|
||||
) -> Result<()> {
|
||||
let quote = "e_verification_result.quote;
|
||||
let tcblevel = quote_verification_result.result;
|
||||
|
||||
match "e.report {
|
||||
Report::SgxEnclave(report_body) => {
|
||||
// Validate TCB level
|
||||
Self::validate_tcb_level(attestation_policy.sgx_allowed_tcb_levels, tcblevel)?;
|
||||
|
||||
// Validate SGX Advisories
|
||||
for advisory in "e_verification_result.advisories {
|
||||
Self::check_policy(
|
||||
attestation_policy.sgx_allowed_advisory_ids.as_deref(),
|
||||
advisory,
|
||||
"advisories",
|
||||
)?;
|
||||
}
|
||||
|
||||
// Validate SGX policies
|
||||
Self::check_policy_hash(
|
||||
attestation_policy.sgx_mrsigners.as_deref(),
|
||||
&report_body.mr_signer,
|
||||
"mrsigner",
|
||||
)?;
|
||||
|
||||
Self::check_policy_hash(
|
||||
attestation_policy.sgx_mrenclaves.as_deref(),
|
||||
&report_body.mr_enclave,
|
||||
"mrenclave",
|
||||
)
|
||||
}
|
||||
Report::TD10(report_body) => {
|
||||
// Validate TCB level
|
||||
Self::validate_tcb_level(attestation_policy.tdx_allowed_tcb_levels, tcblevel)?;
|
||||
|
||||
// Validate TDX Advisories
|
||||
for advisory in "e_verification_result.advisories {
|
||||
Self::check_policy(
|
||||
attestation_policy.tdx_allowed_advisory_ids.as_deref(),
|
||||
advisory,
|
||||
"mrsigner",
|
||||
)?;
|
||||
}
|
||||
|
||||
// Build combined TDX MR and validate
|
||||
let tdx_mr = Self::build_tdx_mr([
|
||||
&report_body.mr_td,
|
||||
&report_body.rt_mr0,
|
||||
&report_body.rt_mr1,
|
||||
&report_body.rt_mr2,
|
||||
&report_body.rt_mr3,
|
||||
]);
|
||||
|
||||
Self::check_policy_hash(attestation_policy.tdx_mrs.as_deref(), &tdx_mr, "tdxmr")
|
||||
}
|
||||
Report::TD15(report_body) => {
|
||||
// Validate TCB level
|
||||
Self::validate_tcb_level(attestation_policy.tdx_allowed_tcb_levels, tcblevel)?;
|
||||
|
||||
// Validate TDX Advisories
|
||||
for advisory in "e_verification_result.advisories {
|
||||
Self::check_policy(
|
||||
attestation_policy.tdx_allowed_advisory_ids.as_deref(),
|
||||
advisory,
|
||||
"advisories",
|
||||
)?;
|
||||
}
|
||||
|
||||
// Build combined TDX MR and validate
|
||||
let tdx_mr = Self::build_tdx_mr([
|
||||
&report_body.base.mr_td,
|
||||
&report_body.base.rt_mr0,
|
||||
&report_body.base.rt_mr1,
|
||||
&report_body.base.rt_mr2,
|
||||
&report_body.base.rt_mr3,
|
||||
]);
|
||||
|
||||
Self::check_policy_hash(attestation_policy.tdx_mrs.as_deref(), &tdx_mr, "tdxmr")
|
||||
}
|
||||
_ => Err(Error::policy_violation("Unknown quote report format")),
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper method to validate TCB levels
|
||||
fn validate_tcb_level(allowed_levels: EnumSet<TcbLevel>, actual_level: TcbLevel) -> Result<()> {
|
||||
if !allowed_levels.contains(actual_level) {
|
||||
let error_msg = format!(
|
||||
"Quote verification failed: TCB level mismatch (expected one of: {allowed_levels:?}, actual: {actual_level})",
|
||||
);
|
||||
return Err(Error::policy_violation(error_msg));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Helper method to build combined TDX measurement register
|
||||
fn build_tdx_mr<const N: usize>(parts: [&[u8]; N]) -> Vec<u8> {
|
||||
parts.into_iter().flatten().copied().collect()
|
||||
}
|
||||
|
||||
/// Check if a policy value matches the actual value
|
||||
fn check_policy(policy: Option<&[String]>, actual_value: &str, field_name: &str) -> Result<()> {
|
||||
if let Some(valid_values) = policy {
|
||||
if !valid_values.iter().any(|value| value == actual_value) {
|
||||
let error_msg =
|
||||
format!(
|
||||
"Quote verification failed: {} mismatch (expected one of: [ {} ], actual: {})",
|
||||
field_name, valid_values.join(", "), actual_value
|
||||
);
|
||||
return Err(Error::policy_violation(error_msg));
|
||||
}
|
||||
|
||||
tracing::debug!(field_name, actual_value, "Attestation policy check passed");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn check_policy_hash(
|
||||
policy: Option<&[Bytes]>,
|
||||
actual_value: &[u8],
|
||||
field_name: &str,
|
||||
) -> Result<()> {
|
||||
if let Some(valid_values) = policy {
|
||||
let actual_value = Bytes::copy_from_slice(actual_value);
|
||||
if !valid_values.contains(&actual_value) {
|
||||
let valid_values = valid_values
|
||||
.iter()
|
||||
.map(hex::encode)
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ");
|
||||
let error_msg = format!(
|
||||
"Quote verification failed: {field_name} mismatch (expected one of: [ {valid_values} ], actual: {actual_value:x})"
|
||||
);
|
||||
return Err(Error::policy_violation(error_msg));
|
||||
}
|
||||
|
||||
tracing::debug!(
|
||||
field_name,
|
||||
actual_value = format!("{actual_value:x}"),
|
||||
"Attestation policy check passed"
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_check_policy() {
|
||||
// Test with no policy (should pass)
|
||||
PolicyEnforcer::check_policy_hash(None, &[1, 2, 3], "test").unwrap();
|
||||
|
||||
// Test with matching policy
|
||||
let actual_value: Bytes = hex::decode("01020304").unwrap().into();
|
||||
PolicyEnforcer::check_policy_hash(
|
||||
Some(vec![actual_value.clone()]).as_deref(),
|
||||
&actual_value,
|
||||
"test",
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
//.clone() Test with matching policy (multiple values)
|
||||
PolicyEnforcer::check_policy_hash(
|
||||
Some(vec![
|
||||
"aabbcc".into(),
|
||||
"01020304".into(),
|
||||
"ddeeff".into(),
|
||||
actual_value.clone(),
|
||||
])
|
||||
.as_deref(),
|
||||
&actual_value,
|
||||
"test",
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// Test with non-matching policy
|
||||
PolicyEnforcer::check_policy_hash(
|
||||
Some(vec!["aabbcc".into(), "ddeeff".into()]).as_deref(),
|
||||
&actual_value,
|
||||
"test",
|
||||
)
|
||||
.unwrap_err();
|
||||
}
|
||||
}
|
|
@ -1,92 +0,0 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (c) 2023-2025 Matter Labs
|
||||
|
||||
use teepot::quote::QuoteVerificationResult;
|
||||
|
||||
/// Handles reporting and logging of verification results
|
||||
pub struct VerificationReporter;
|
||||
|
||||
impl VerificationReporter {
|
||||
/// Log summary of a quote verification
|
||||
pub fn log_quote_verification_summary(quote_verification_result: &QuoteVerificationResult) {
|
||||
let QuoteVerificationResult {
|
||||
collateral_expired,
|
||||
result: tcblevel,
|
||||
quote,
|
||||
advisories,
|
||||
..
|
||||
} = quote_verification_result;
|
||||
|
||||
if *collateral_expired {
|
||||
tracing::warn!("Freshly fetched collateral expired!");
|
||||
}
|
||||
|
||||
let advisories = if advisories.is_empty() {
|
||||
"None".to_string()
|
||||
} else {
|
||||
advisories
|
||||
.iter()
|
||||
.map(ToString::to_string)
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ")
|
||||
};
|
||||
|
||||
tracing::debug!(
|
||||
"Quote verification result: {tcblevel}. {report}. Advisory IDs: {advisories}.",
|
||||
report = "e.report
|
||||
);
|
||||
}
|
||||
|
||||
/// Log the results of batch verification
|
||||
pub fn log_batch_verification_results(
|
||||
batch_no: u32,
|
||||
verified_proofs_count: u32,
|
||||
unverified_proofs_count: u32,
|
||||
) {
|
||||
if unverified_proofs_count > 0 {
|
||||
if verified_proofs_count == 0 {
|
||||
tracing::error!(
|
||||
batch_no,
|
||||
"All {} proofs failed verification!",
|
||||
unverified_proofs_count
|
||||
);
|
||||
} else {
|
||||
tracing::warn!(
|
||||
batch_no,
|
||||
"Some proofs failed verification. Unverified proofs: {}. Verified proofs: {}.",
|
||||
unverified_proofs_count,
|
||||
verified_proofs_count
|
||||
);
|
||||
}
|
||||
} else if verified_proofs_count > 0 {
|
||||
tracing::info!(
|
||||
batch_no,
|
||||
"All {} proofs verified successfully!",
|
||||
verified_proofs_count
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Log overall verification results for multiple batches
|
||||
pub fn log_overall_verification_results(
|
||||
verified_batches_count: u32,
|
||||
unverified_batches_count: u32,
|
||||
) {
|
||||
if unverified_batches_count > 0 {
|
||||
if verified_batches_count == 0 {
|
||||
tracing::error!(
|
||||
"All {} batches failed verification!",
|
||||
unverified_batches_count
|
||||
);
|
||||
} else {
|
||||
tracing::error!(
|
||||
"Some batches failed verification! Unverified batches: {}. Verified batches: {}.",
|
||||
unverified_batches_count,
|
||||
verified_batches_count
|
||||
);
|
||||
}
|
||||
} else {
|
||||
tracing::info!("{} batches verified successfully!", verified_batches_count);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,156 +0,0 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (c) 2023-2025 Matter Labs
|
||||
|
||||
use secp256k1::{
|
||||
ecdsa::{RecoverableSignature, RecoveryId, Signature},
|
||||
Message, SECP256K1,
|
||||
};
|
||||
use teepot::{
|
||||
ethereum::{public_key_to_ethereum_address, recover_signer},
|
||||
prover::reportdata::ReportData,
|
||||
quote::QuoteVerificationResult,
|
||||
};
|
||||
use zksync_basic_types::H256;
|
||||
|
||||
use crate::error;
|
||||
|
||||
const SIGNATURE_LENGTH_WITH_RECOVERY_ID: usize = 65;
|
||||
const SIGNATURE_LENGTH_WITHOUT_RECOVERY_ID: usize = 64;
|
||||
|
||||
/// Handles verification of signatures in proofs
|
||||
pub struct SignatureVerifier;
|
||||
|
||||
impl SignatureVerifier {
|
||||
/// Verify a batch proof signature
|
||||
pub fn verify_batch_proof(
|
||||
quote_verification_result: &QuoteVerificationResult,
|
||||
root_hash: H256,
|
||||
signature: &[u8],
|
||||
) -> error::Result<bool> {
|
||||
let report_data_bytes = quote_verification_result.quote.get_report_data();
|
||||
tracing::trace!(?report_data_bytes);
|
||||
|
||||
let report_data = ReportData::try_from(report_data_bytes)
|
||||
.map_err(|e| error::Error::internal(format!("Could not convert to ReportData: {e}")))?;
|
||||
|
||||
Self::verify(&report_data, &root_hash, signature)
|
||||
}
|
||||
|
||||
/// Verify signature against report data and root hash
|
||||
pub fn verify(
|
||||
report_data: &ReportData,
|
||||
root_hash: &H256,
|
||||
signature: &[u8],
|
||||
) -> error::Result<bool> {
|
||||
match report_data {
|
||||
ReportData::V0(report) => Self::verify_v0(report, root_hash, signature),
|
||||
ReportData::V1(report) => Self::verify_v1(report, root_hash, signature),
|
||||
ReportData::Unknown(_) => Ok(false),
|
||||
}
|
||||
}
|
||||
|
||||
/// Verify a V0 report
|
||||
fn verify_v0(
|
||||
report: &teepot::prover::reportdata::ReportDataV0,
|
||||
root_hash: &H256,
|
||||
signature: &[u8],
|
||||
) -> error::Result<bool> {
|
||||
tracing::debug!("ReportData::V0");
|
||||
let signature = Signature::from_compact(signature)
|
||||
.map_err(|e| error::Error::signature_verification(e.to_string()))?;
|
||||
let root_hash_msg = Message::from_digest(root_hash.0);
|
||||
Ok(signature.verify(root_hash_msg, &report.pubkey).is_ok())
|
||||
}
|
||||
|
||||
/// Verify a V1 report
|
||||
fn verify_v1(
|
||||
report: &teepot::prover::reportdata::ReportDataV1,
|
||||
root_hash: &H256,
|
||||
signature: &[u8],
|
||||
) -> error::Result<bool> {
|
||||
tracing::debug!("ReportData::V1");
|
||||
let ethereum_address_from_report = report.ethereum_address;
|
||||
|
||||
let root_hash_msg = Message::from_digest(
|
||||
root_hash
|
||||
.as_bytes()
|
||||
.try_into()
|
||||
.map_err(|_| error::Error::signature_verification("root hash not 32 bytes"))?,
|
||||
);
|
||||
|
||||
tracing::trace!("sig len = {}", signature.len());
|
||||
|
||||
// Try to recover Ethereum address from signature
|
||||
let ethereum_address_from_signature = match signature.len() {
|
||||
// Handle 64-byte signature case (missing recovery ID)
|
||||
SIGNATURE_LENGTH_WITHOUT_RECOVERY_ID => {
|
||||
SignatureVerifier::recover_address_with_missing_recovery_id(
|
||||
signature,
|
||||
&root_hash_msg,
|
||||
)?
|
||||
}
|
||||
// Standard 65-byte signature case
|
||||
SIGNATURE_LENGTH_WITH_RECOVERY_ID => {
|
||||
let signature_bytes: [u8; SIGNATURE_LENGTH_WITH_RECOVERY_ID] =
|
||||
signature.try_into().map_err(|_| {
|
||||
error::Error::signature_verification(
|
||||
"Expected 65-byte signature but got a different length",
|
||||
)
|
||||
})?;
|
||||
|
||||
recover_signer(&signature_bytes, &root_hash_msg).map_err(|e| {
|
||||
error::Error::signature_verification(format!("Failed to recover signer: {e}"))
|
||||
})?
|
||||
}
|
||||
// Any other length is invalid
|
||||
len => {
|
||||
return Err(error::Error::signature_verification(format!(
|
||||
"Invalid signature length: {len} bytes"
|
||||
)))
|
||||
}
|
||||
};
|
||||
|
||||
// Log verification details
|
||||
tracing::debug!(
|
||||
"Root hash: {}. Ethereum address from the attestation quote: {}. Ethereum address from the signature: {}.",
|
||||
root_hash,
|
||||
hex::encode(ethereum_address_from_report),
|
||||
hex::encode(ethereum_address_from_signature),
|
||||
);
|
||||
|
||||
Ok(ethereum_address_from_signature == ethereum_address_from_report)
|
||||
}
|
||||
|
||||
/// Helper function to recover Ethereum address when recovery ID is missing
|
||||
fn recover_address_with_missing_recovery_id(
|
||||
signature: &[u8],
|
||||
message: &Message,
|
||||
) -> error::Result<[u8; 20]> {
|
||||
tracing::info!("Signature is missing RecoveryId!");
|
||||
|
||||
// Try all possible recovery IDs
|
||||
for rec_id in [
|
||||
RecoveryId::Zero,
|
||||
RecoveryId::One,
|
||||
RecoveryId::Two,
|
||||
RecoveryId::Three,
|
||||
] {
|
||||
let Ok(rec_sig) = RecoverableSignature::from_compact(signature, rec_id) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let Ok(public) = SECP256K1.recover_ecdsa(*message, &rec_sig) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let ethereum_address = public_key_to_ethereum_address(&public);
|
||||
tracing::info!("Had to use RecoveryId::{rec_id:?}");
|
||||
return Ok(ethereum_address);
|
||||
}
|
||||
|
||||
// No valid recovery ID found
|
||||
Err(error::Error::signature_verification(
|
||||
"Could not find valid recovery ID",
|
||||
))
|
||||
}
|
||||
}
|
|
@ -1,143 +0,0 @@
|
|||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## Overview
|
||||
|
||||
This crate (`intel-dcap-api`) is a Rust client library for Intel's Data Center Attestation Primitives (DCAP) API. It
|
||||
provides access to Intel's Trusted Services API for SGX and TDX attestation, including TCB info, PCK certificates, CRLs,
|
||||
and enclave identity verification.
|
||||
|
||||
## Features
|
||||
|
||||
- Support for both API v3 and v4
|
||||
- Async/await API using tokio
|
||||
- Comprehensive error handling with Intel-specific error codes
|
||||
- Type-safe request/response structures
|
||||
- Support for SGX and TDX platforms
|
||||
- Real data integration tests
|
||||
- **Automatic rate limit handling with configurable retries**
|
||||
|
||||
## Development Commands
|
||||
|
||||
```bash
|
||||
# Build
|
||||
cargo build
|
||||
cargo build --no-default-features --features rustls # Use rustls instead of default TLS
|
||||
|
||||
# Test
|
||||
cargo test
|
||||
|
||||
# Lint
|
||||
cargo clippy
|
||||
|
||||
# Examples
|
||||
cargo run --example example # Basic usage example
|
||||
cargo run --example get_pck_crl # Fetch certificate revocation lists
|
||||
cargo run --example common_usage # Common attestation verification patterns
|
||||
cargo run --example integration_test # Comprehensive test of most API endpoints
|
||||
cargo run --example fetch_test_data # Fetch real data from Intel API for tests
|
||||
cargo run --example handle_rate_limit # Demonstrate automatic rate limiting handling
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
### Client Structure
|
||||
|
||||
- **ApiClient** (`src/client/mod.rs`): Main entry point supporting API v3/v4
|
||||
- Base URL: https://api.trustedservices.intel.com
|
||||
- Manages HTTP client and API version selection
|
||||
- Automatic retry logic for 429 (Too Many Requests) responses
|
||||
- Default: 3 retries, configurable via `set_max_retries()`
|
||||
|
||||
### Key Modules
|
||||
|
||||
- **client/**: API endpoint implementations
|
||||
- `tcb_info`: SGX/TDX TCB information retrieval
|
||||
- `get_sgx_tcb_info()`, `get_tdx_tcb_info()`
|
||||
- `pck_cert`: PCK certificate operations
|
||||
- `get_pck_certificate_by_ppid()`, `get_pck_certificate_by_manifest()`
|
||||
- `get_pck_certificates_by_ppid()`, `get_pck_certificates_by_manifest()`
|
||||
- `get_pck_certificates_config_by_ppid()`, `get_pck_certificates_config_by_manifest()`
|
||||
- `pck_crl`: Certificate revocation lists
|
||||
- `get_pck_crl()` - supports PEM and DER encoding
|
||||
- `enclave_identity`: SGX QE/QVE/QAE/TDQE identity
|
||||
- `get_sgx_qe_identity()`, `get_sgx_qve_identity()`, `get_sgx_qae_identity()`, `get_tdx_qe_identity()`
|
||||
- `fmspc`: FMSPC-related operations (V4 only)
|
||||
- `get_fmspcs()` - with optional platform filter
|
||||
- `get_sgx_tcb_evaluation_data_numbers()`, `get_tdx_tcb_evaluation_data_numbers()`
|
||||
- `registration`: Platform registration
|
||||
- `register_platform()`, `add_package()`
|
||||
|
||||
### Core Types
|
||||
|
||||
- **error.rs**: `IntelApiError` for comprehensive error handling
|
||||
- Extracts error details from Error-Code and Error-Message headers
|
||||
- **`TooManyRequests` variant for rate limiting (429) after retry exhaustion**
|
||||
- **types.rs**: Enums (CaType, ApiVersion, UpdateType, etc.)
|
||||
- **requests.rs**: Request structures
|
||||
- **responses.rs**: Response structures with JSON and certificate data
|
||||
|
||||
### API Pattern
|
||||
|
||||
All client methods follow this pattern:
|
||||
|
||||
1. Build request with query parameters
|
||||
2. Send HTTP request with proper headers (with automatic retry on 429)
|
||||
3. Parse response (JSON + certificate chains)
|
||||
4. Return typed response or error
|
||||
|
||||
### Rate Limiting & Retry Logic
|
||||
|
||||
- **Automatic Retries**: All HTTP requests automatically retry on 429 (Too Many Requests) responses
|
||||
- **Retry Configuration**: Default 3 retries, configurable via `ApiClient::set_max_retries()`
|
||||
- **Retry-After Handling**: Waits for duration specified in Retry-After header before retrying
|
||||
- **Error Handling**: `IntelApiError::TooManyRequests` returned only after all retries exhausted
|
||||
- **Implementation**: `execute_with_retry()` in `src/client/helpers.rs` handles retry logic
|
||||
|
||||
### Testing Strategy
|
||||
|
||||
- **Mock Tests**: Two test suites using mockito for HTTP mocking
|
||||
- `tests/mock_api_tests.rs`: Basic API functionality tests with simple data (11 tests)
|
||||
- `tests/real_data_mock_tests.rs`: Tests using real Intel API responses (25 tests)
|
||||
- **Test Data**: Real responses stored in `tests/test_data/` (JSON format)
|
||||
- Fetched using `cargo run --example fetch_test_data`
|
||||
- Includes TCB info, CRLs, enclave identities for both SGX and TDX
|
||||
- Covers V3 and V4 API variations, different update types, and evaluation data numbers
|
||||
- **Key Testing Considerations**:
|
||||
- Headers with newlines must be URL-encoded for mockito (use `percent_encode` with `NON_ALPHANUMERIC`)
|
||||
- V3 vs V4 API use different header names:
|
||||
- V3: `SGX-TCB-Info-Issuer-Chain`
|
||||
- V4: `TCB-Info-Issuer-Chain`
|
||||
- Error responses include Error-Code and Error-Message headers
|
||||
- Examples use real Intel API endpoints
|
||||
- Test data (FMSPC, PPID) from Intel documentation
|
||||
- Async tests require tokio runtime
|
||||
|
||||
## API Version Differences
|
||||
|
||||
### V4-Only Features
|
||||
|
||||
- FMSPC listing (`get_fmspcs()`)
|
||||
- TCB Evaluation Data Numbers endpoints
|
||||
- PPID encryption key type parameter
|
||||
- TDX QE identity endpoint
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
1. **Mockito Header Encoding**: Always URL-encode headers containing newlines/special characters
|
||||
2. **API Version Selection**: Some endpoints are V4-only and will return errors on V3
|
||||
3. **Rate Limiting**: Client automatically retries 429 responses; disable with `set_max_retries(0)` if manual handling
|
||||
needed
|
||||
4. **Platform Filters**: Only certain values are valid (All, Client, E3, E5)
|
||||
5. **Test Data**: PCK certificate endpoints require valid platform data and often need subscription keys
|
||||
6. **Issuer Chain Validation**: Always check that `issuer_chain` is non-empty - it's critical for signature verification
|
||||
|
||||
## Security Considerations
|
||||
|
||||
- **Certificate Chain Verification**: The `issuer_chain` field contains the certificates needed to verify the signature
|
||||
of the response data
|
||||
- **Signature Validation**: All JSON responses (TCB info, enclave identities) should have their signatures verified
|
||||
using the issuer chain
|
||||
- **CRL Verification**: PCK CRLs must be signature-verified before being used for certificate revocation checking
|
||||
- **Empty Issuer Chains**: Always validate that issuer chains are present and non-empty before trusting response data
|
|
@ -1,35 +0,0 @@
|
|||
[package]
|
||||
name = "intel-dcap-api"
|
||||
description = "Intel DCAP API Client"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
authors.workspace = true
|
||||
license.workspace = true
|
||||
repository.workspace = true
|
||||
homepage.workspace = true
|
||||
keywords = ["sgx", "tdx", "intel", "attestation", "confidential"]
|
||||
categories = ["api-bindings", "cryptography", "authentication"]
|
||||
|
||||
[dependencies]
|
||||
base64.workspace = true
|
||||
percent-encoding.workspace = true
|
||||
reqwest = { workspace = true, features = ["json"] }
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
thiserror.workspace = true
|
||||
tokio.workspace = true
|
||||
url.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
base64.workspace = true
|
||||
hex.workspace = true
|
||||
mockito.workspace = true
|
||||
x509-cert.workspace = true
|
||||
|
||||
[[example]]
|
||||
name = "integration_test"
|
||||
required-features = ["default"]
|
||||
|
||||
[features]
|
||||
default = ["reqwest/default-tls"]
|
||||
rustls = ["reqwest/rustls-tls"]
|
|
@ -1,182 +0,0 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (c) 2025 Matter Labs
|
||||
|
||||
use intel_dcap_api::{ApiClient, CaType, IntelApiError, UpdateType};
|
||||
|
||||
/// Common usage patterns for the Intel DCAP API client
|
||||
///
|
||||
/// This example demonstrates typical use cases for attestation verification.
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
// Create a client (defaults to V4 API)
|
||||
let client = ApiClient::new()?;
|
||||
|
||||
// Example 1: Get TCB info for quote verification
|
||||
println!("Example 1: Getting TCB info for SGX quote verification");
|
||||
println!("======================================================");
|
||||
|
||||
let fmspc = "00906ED50000"; // From SGX quote
|
||||
|
||||
match client.get_sgx_tcb_info(fmspc, None, None).await {
|
||||
Ok(response) => {
|
||||
// Critical: Check that issuer chain is present for signature verification
|
||||
if response.issuer_chain.is_empty() {
|
||||
println!("✗ Error: Empty issuer chain - cannot verify TCB info signature!");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
println!("✓ Retrieved TCB info for FMSPC: {}", fmspc);
|
||||
|
||||
// Parse the TCB info
|
||||
let tcb_info: serde_json::Value = serde_json::from_str(&response.tcb_info_json)?;
|
||||
|
||||
// Extract useful information
|
||||
if let Some(tcb_levels) = tcb_info["tcbInfo"]["tcbLevels"].as_array() {
|
||||
println!(" Found {} TCB levels", tcb_levels.len());
|
||||
|
||||
// Show the latest TCB level
|
||||
if let Some(latest) = tcb_levels.first() {
|
||||
println!(" Latest TCB level:");
|
||||
if let Some(status) = latest["tcbStatus"].as_str() {
|
||||
println!(" Status: {}", status);
|
||||
}
|
||||
if let Some(date) = latest["tcbDate"].as_str() {
|
||||
println!(" Date: {}", date);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The issuer chain is needed to verify the signature
|
||||
println!(
|
||||
" Issuer chain length: {} bytes",
|
||||
response.issuer_chain.len()
|
||||
);
|
||||
// Verify we have certificate chain for signature verification
|
||||
let cert_count = response.issuer_chain.matches("BEGIN CERTIFICATE").count();
|
||||
println!(" Certificate chain contains {} certificates", cert_count);
|
||||
}
|
||||
Err(IntelApiError::ApiError {
|
||||
status,
|
||||
error_message,
|
||||
..
|
||||
}) => {
|
||||
println!(
|
||||
"✗ API Error {}: {}",
|
||||
status,
|
||||
error_message.unwrap_or_default()
|
||||
);
|
||||
}
|
||||
Err(e) => {
|
||||
println!("✗ Error: {:?}", e);
|
||||
}
|
||||
}
|
||||
|
||||
println!();
|
||||
|
||||
// Example 2: Get QE identity for enclave verification
|
||||
println!("Example 2: Getting QE identity for enclave verification");
|
||||
println!("======================================================");
|
||||
|
||||
match client.get_sgx_qe_identity(None, None).await {
|
||||
Ok(response) => {
|
||||
// Critical: Check that issuer chain is present for signature verification
|
||||
if response.issuer_chain.is_empty() {
|
||||
println!("✗ Error: Empty issuer chain - cannot verify QE identity signature!");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
println!("✓ Retrieved QE identity");
|
||||
println!(
|
||||
" Issuer chain length: {} bytes",
|
||||
response.issuer_chain.len()
|
||||
);
|
||||
|
||||
let identity: serde_json::Value =
|
||||
serde_json::from_str(&response.enclave_identity_json)?;
|
||||
|
||||
if let Some(enclave_id) = identity["enclaveIdentity"]["id"].as_str() {
|
||||
println!(" Enclave ID: {}", enclave_id);
|
||||
}
|
||||
|
||||
if let Some(version) = identity["enclaveIdentity"]["version"].as_u64() {
|
||||
println!(" Version: {}", version);
|
||||
}
|
||||
|
||||
if let Some(mrsigner) = identity["enclaveIdentity"]["mrsigner"].as_str() {
|
||||
println!(" MRSIGNER: {}...", &mrsigner[..16]);
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
println!("✗ Failed to get QE identity: {:?}", e);
|
||||
}
|
||||
}
|
||||
|
||||
println!();
|
||||
|
||||
// Example 3: Check certificate revocation
|
||||
println!("Example 3: Checking certificate revocation status");
|
||||
println!("================================================");
|
||||
|
||||
match client.get_pck_crl(CaType::Processor, None).await {
|
||||
Ok(response) => {
|
||||
// Critical: Check that issuer chain is present for CRL verification
|
||||
if response.issuer_chain.is_empty() {
|
||||
println!("✗ Error: Empty issuer chain - cannot verify CRL signature!");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
println!("✓ Retrieved PCK CRL");
|
||||
println!(
|
||||
" Issuer chain length: {} bytes",
|
||||
response.issuer_chain.len()
|
||||
);
|
||||
|
||||
let crl_pem = String::from_utf8_lossy(&response.crl_data);
|
||||
|
||||
// In real usage, you would parse this CRL and check if a certificate is revoked
|
||||
if crl_pem.contains("BEGIN X509 CRL") {
|
||||
println!(" CRL format: PEM");
|
||||
println!(" CRL size: {} bytes", crl_pem.len());
|
||||
|
||||
// Count the revoked certificates (naive approach)
|
||||
let revoked_count = crl_pem.matches("Serial Number:").count();
|
||||
println!(" Approximate revoked certificates: {}", revoked_count);
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
println!("✗ Failed to get CRL: {:?}", e);
|
||||
}
|
||||
}
|
||||
|
||||
println!();
|
||||
|
||||
// Example 4: Early update for testing
|
||||
println!("Example 4: Getting early TCB update (for testing)");
|
||||
println!("================================================");
|
||||
|
||||
match client
|
||||
.get_sgx_tcb_info(fmspc, Some(UpdateType::Early), None)
|
||||
.await
|
||||
{
|
||||
Ok(response) => {
|
||||
println!("✓ Retrieved early TCB update");
|
||||
|
||||
let tcb_info: serde_json::Value = serde_json::from_str(&response.tcb_info_json)?;
|
||||
|
||||
if let Some(next_update) = tcb_info["tcbInfo"]["nextUpdate"].as_str() {
|
||||
println!(" Next update: {}", next_update);
|
||||
}
|
||||
}
|
||||
Err(IntelApiError::ApiError { status, .. }) if status.as_u16() == 404 => {
|
||||
println!(" No early update available (this is normal)");
|
||||
}
|
||||
Err(e) => {
|
||||
println!("✗ Error: {:?}", e);
|
||||
}
|
||||
}
|
||||
|
||||
println!();
|
||||
println!("Done! These examples show common patterns for attestation verification.");
|
||||
|
||||
Ok(())
|
||||
}
|
|
@ -1,78 +0,0 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (c) 2025 Matter Labs
|
||||
|
||||
use intel_dcap_api::{
|
||||
ApiClient, ApiVersion, CaType, CrlEncoding, EnclaveIdentityResponse, IntelApiError,
|
||||
PckCrlResponse, PlatformFilter, TcbInfoResponse,
|
||||
};
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), IntelApiError> {
|
||||
for api_version in [ApiVersion::V3, ApiVersion::V4] {
|
||||
println!("Using API version: {}", api_version);
|
||||
|
||||
let client = ApiClient::new_with_version(api_version)?;
|
||||
|
||||
// Example: Get SGX TCB Info
|
||||
let fmspc_example = "00606A000000"; // Example FMSPC from docs
|
||||
match client.get_sgx_tcb_info(fmspc_example, None, None).await {
|
||||
Ok(TcbInfoResponse {
|
||||
tcb_info_json,
|
||||
issuer_chain,
|
||||
}) => println!(
|
||||
"SGX TCB Info for {}:\n{}\nIssuer Chain: {}",
|
||||
fmspc_example, tcb_info_json, issuer_chain
|
||||
),
|
||||
Err(e) => eprintln!("Error getting SGX TCB info: {}", e),
|
||||
}
|
||||
|
||||
// Example: Get FMSPCs
|
||||
match client.get_fmspcs(Some(PlatformFilter::E3)).await {
|
||||
// Filter for E3 platform type [cite: 230]
|
||||
Ok(fmspc_list) => println!("\nE3 FMSPCs:\n{}", fmspc_list),
|
||||
Err(e) => eprintln!("Error getting FMSPCs: {}", e),
|
||||
}
|
||||
|
||||
// Example: Get SGX QE Identity
|
||||
match client.get_sgx_qe_identity(None, None).await {
|
||||
Ok(EnclaveIdentityResponse {
|
||||
enclave_identity_json,
|
||||
issuer_chain,
|
||||
}) => {
|
||||
println!(
|
||||
"\nSGX QE Identity:\n{}\nIssuer Chain: {}",
|
||||
enclave_identity_json, issuer_chain
|
||||
)
|
||||
}
|
||||
Err(e) => eprintln!("Error getting SGX QE Identity: {}", e),
|
||||
}
|
||||
|
||||
// Example: Get PCK CRL (Platform CA, PEM encoding)
|
||||
match client
|
||||
.get_pck_crl(CaType::Platform, Some(CrlEncoding::Pem))
|
||||
.await
|
||||
{
|
||||
// [cite: 118, 119]
|
||||
Ok(PckCrlResponse {
|
||||
crl_data,
|
||||
issuer_chain,
|
||||
}) => {
|
||||
// Attempt to decode PEM for display, otherwise show byte count
|
||||
match String::from_utf8(crl_data.clone()) {
|
||||
Ok(pem_string) => println!(
|
||||
"\nPlatform PCK CRL (PEM):\n{}\nIssuer Chain: {}",
|
||||
pem_string, issuer_chain
|
||||
),
|
||||
Err(_) => println!(
|
||||
"\nPlatform PCK CRL ({} bytes, likely DER):\nIssuer Chain: {}",
|
||||
crl_data.len(),
|
||||
issuer_chain
|
||||
),
|
||||
}
|
||||
}
|
||||
Err(e) => eprintln!("Error getting PCK CRL: {}", e),
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
|
@ -1,515 +0,0 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (c) 2025 Matter Labs
|
||||
|
||||
use base64::{engine::general_purpose, Engine as _};
|
||||
use intel_dcap_api::{ApiClient, ApiVersion, CaType, CrlEncoding, PlatformFilter, UpdateType};
|
||||
use std::{fs, path::Path};
|
||||
|
||||
/// Fetch real data from Intel API and save it as JSON files
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
// Create test data directory
|
||||
let test_data_dir = Path::new("tests/test_data");
|
||||
fs::create_dir_all(test_data_dir)?;
|
||||
|
||||
let client = ApiClient::new()?;
|
||||
|
||||
println!("Fetching real test data from Intel API...");
|
||||
|
||||
// Keep track of successful fetches
|
||||
let mut successes: Vec<String> = Vec::new();
|
||||
let mut failures: Vec<String> = Vec::new();
|
||||
|
||||
// 1. Fetch SGX TCB info
|
||||
println!("\n1. Fetching SGX TCB info...");
|
||||
match client
|
||||
.get_sgx_tcb_info("00606A6A0000", Some(UpdateType::Standard), None)
|
||||
.await
|
||||
{
|
||||
Ok(response) => {
|
||||
let data = serde_json::json!({
|
||||
"tcb_info_json": response.tcb_info_json,
|
||||
"issuer_chain": response.issuer_chain,
|
||||
});
|
||||
fs::write(
|
||||
test_data_dir.join("sgx_tcb_info.json"),
|
||||
serde_json::to_string_pretty(&data)?,
|
||||
)?;
|
||||
successes.push("SGX TCB info".to_string());
|
||||
}
|
||||
Err(e) => {
|
||||
failures.push(format!("SGX TCB info: {}", e));
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Fetch TDX TCB info
|
||||
println!("\n2. Fetching TDX TCB info...");
|
||||
match client
|
||||
.get_tdx_tcb_info("00806F050000", Some(UpdateType::Standard), None)
|
||||
.await
|
||||
{
|
||||
Ok(response) => {
|
||||
let data = serde_json::json!({
|
||||
"tcb_info_json": response.tcb_info_json,
|
||||
"issuer_chain": response.issuer_chain,
|
||||
});
|
||||
fs::write(
|
||||
test_data_dir.join("tdx_tcb_info.json"),
|
||||
serde_json::to_string_pretty(&data)?,
|
||||
)?;
|
||||
successes.push("TDX TCB info".to_string());
|
||||
}
|
||||
Err(e) => {
|
||||
failures.push(format!("TDX TCB info: {}", e));
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Fetch PCK CRL for processor
|
||||
println!("\n3. Fetching PCK CRL (processor)...");
|
||||
match client.get_pck_crl(CaType::Processor, None).await {
|
||||
Ok(response) => {
|
||||
let crl_string = String::from_utf8_lossy(&response.crl_data);
|
||||
let data = serde_json::json!({
|
||||
"crl_data": crl_string,
|
||||
"issuer_chain": response.issuer_chain,
|
||||
});
|
||||
fs::write(
|
||||
test_data_dir.join("pck_crl_processor.json"),
|
||||
serde_json::to_string_pretty(&data)?,
|
||||
)?;
|
||||
successes.push("PCK CRL (processor)".to_string());
|
||||
}
|
||||
Err(e) => {
|
||||
failures.push(format!("PCK CRL (processor): {}", e));
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Fetch PCK CRL for platform
|
||||
println!("\n4. Fetching PCK CRL (platform)...");
|
||||
match client.get_pck_crl(CaType::Platform, None).await {
|
||||
Ok(response) => {
|
||||
let crl_string = String::from_utf8_lossy(&response.crl_data);
|
||||
let data = serde_json::json!({
|
||||
"crl_data": crl_string,
|
||||
"issuer_chain": response.issuer_chain,
|
||||
});
|
||||
fs::write(
|
||||
test_data_dir.join("pck_crl_platform.json"),
|
||||
serde_json::to_string_pretty(&data)?,
|
||||
)?;
|
||||
successes.push("PCK CRL (platform)".to_string());
|
||||
}
|
||||
Err(e) => {
|
||||
failures.push(format!("PCK CRL (platform): {}", e));
|
||||
}
|
||||
}
|
||||
|
||||
// 5. Fetch SGX QE identity
|
||||
println!("\n5. Fetching SGX QE identity...");
|
||||
match client.get_sgx_qe_identity(None, None).await {
|
||||
Ok(response) => {
|
||||
let data = serde_json::json!({
|
||||
"enclave_identity_json": response.enclave_identity_json,
|
||||
"issuer_chain": response.issuer_chain,
|
||||
});
|
||||
fs::write(
|
||||
test_data_dir.join("sgx_qe_identity.json"),
|
||||
serde_json::to_string_pretty(&data)?,
|
||||
)?;
|
||||
successes.push("SGX QE identity".to_string());
|
||||
}
|
||||
Err(e) => {
|
||||
failures.push(format!("SGX QE identity: {}", e));
|
||||
}
|
||||
}
|
||||
|
||||
// 6. Fetch SGX QVE identity
|
||||
println!("\n6. Fetching SGX QVE identity...");
|
||||
match client.get_sgx_qve_identity(None, None).await {
|
||||
Ok(response) => {
|
||||
let data = serde_json::json!({
|
||||
"enclave_identity_json": response.enclave_identity_json,
|
||||
"issuer_chain": response.issuer_chain,
|
||||
});
|
||||
fs::write(
|
||||
test_data_dir.join("sgx_qve_identity.json"),
|
||||
serde_json::to_string_pretty(&data)?,
|
||||
)?;
|
||||
successes.push("SGX QVE identity".to_string());
|
||||
}
|
||||
Err(e) => {
|
||||
failures.push(format!("SGX QVE identity: {}", e));
|
||||
}
|
||||
}
|
||||
|
||||
// 7. Fetch TDX QE identity
|
||||
println!("\n7. Fetching TDX QE identity...");
|
||||
match client.get_tdx_qe_identity(None, None).await {
|
||||
Ok(response) => {
|
||||
let data = serde_json::json!({
|
||||
"enclave_identity_json": response.enclave_identity_json,
|
||||
"issuer_chain": response.issuer_chain,
|
||||
});
|
||||
fs::write(
|
||||
test_data_dir.join("tdx_qe_identity.json"),
|
||||
serde_json::to_string_pretty(&data)?,
|
||||
)?;
|
||||
successes.push("TDX QE identity".to_string());
|
||||
}
|
||||
Err(e) => {
|
||||
failures.push(format!("TDX QE identity: {}", e));
|
||||
}
|
||||
}
|
||||
|
||||
// 8. Try an alternative FMSPC
|
||||
println!("\n8. Fetching alternative SGX TCB info...");
|
||||
match client.get_sgx_tcb_info("00906ED50000", None, None).await {
|
||||
Ok(response) => {
|
||||
let data = serde_json::json!({
|
||||
"tcb_info_json": response.tcb_info_json,
|
||||
"issuer_chain": response.issuer_chain,
|
||||
});
|
||||
fs::write(
|
||||
test_data_dir.join("sgx_tcb_info_alt.json"),
|
||||
serde_json::to_string_pretty(&data)?,
|
||||
)?;
|
||||
successes.push("Alternative SGX TCB info".to_string());
|
||||
}
|
||||
Err(e) => {
|
||||
failures.push(format!("Alternative SGX TCB info: {}", e));
|
||||
}
|
||||
}
|
||||
|
||||
// 9. Fetch PCK certificate
|
||||
println!("\n9. Attempting to fetch PCK certificate...");
|
||||
let ppid = "3d6dd97e96f84536a2267e727dd860e4fdd3ffa3e319db41e8f69c9a43399e7b7ce97d7eb3bd05b0a58bdb5b90a0e218";
|
||||
let cpusvn = "0606060606060606060606060606060606060606060606060606060606060606";
|
||||
let pcesvn = "0a00";
|
||||
let pceid = "0000";
|
||||
|
||||
match client
|
||||
.get_pck_certificate_by_ppid(ppid, cpusvn, pcesvn, pceid, None, None)
|
||||
.await
|
||||
{
|
||||
Ok(response) => {
|
||||
let data = serde_json::json!({
|
||||
"pck_cert_pem": response.pck_cert_pem,
|
||||
"issuer_chain": response.issuer_chain,
|
||||
"tcbm": response.tcbm,
|
||||
"fmspc": response.fmspc,
|
||||
});
|
||||
fs::write(
|
||||
test_data_dir.join("pck_cert.json"),
|
||||
serde_json::to_string_pretty(&data)?,
|
||||
)?;
|
||||
successes.push("PCK certificate".to_string());
|
||||
}
|
||||
Err(e) => {
|
||||
failures.push(format!("PCK certificate: {}", e));
|
||||
}
|
||||
}
|
||||
|
||||
// 10. Fetch SGX QAE identity
|
||||
println!("\n10. Fetching SGX QAE identity...");
|
||||
match client.get_sgx_qae_identity(None, None).await {
|
||||
Ok(response) => {
|
||||
let data = serde_json::json!({
|
||||
"enclave_identity_json": response.enclave_identity_json,
|
||||
"issuer_chain": response.issuer_chain,
|
||||
});
|
||||
fs::write(
|
||||
test_data_dir.join("sgx_qae_identity.json"),
|
||||
serde_json::to_string_pretty(&data)?,
|
||||
)?;
|
||||
successes.push("SGX QAE identity".to_string());
|
||||
}
|
||||
Err(e) => {
|
||||
failures.push(format!("SGX QAE identity: {}", e));
|
||||
}
|
||||
}
|
||||
|
||||
// 11. Fetch FMSPCs
|
||||
println!("\n11. Fetching FMSPCs...");
|
||||
match client.get_fmspcs(Some(PlatformFilter::All)).await {
|
||||
Ok(fmspcs_json) => {
|
||||
let data = serde_json::json!({
|
||||
"fmspcs_json": fmspcs_json,
|
||||
});
|
||||
fs::write(
|
||||
test_data_dir.join("fmspcs.json"),
|
||||
serde_json::to_string_pretty(&data)?,
|
||||
)?;
|
||||
successes.push("FMSPCs".to_string());
|
||||
}
|
||||
Err(e) => {
|
||||
failures.push(format!("FMSPCs: {}", e));
|
||||
}
|
||||
}
|
||||
|
||||
// 12. Fetch SGX TCB evaluation data numbers
|
||||
println!("\n12. Fetching SGX TCB evaluation data numbers...");
|
||||
match client.get_sgx_tcb_evaluation_data_numbers().await {
|
||||
Ok(response) => {
|
||||
let data = serde_json::json!({
|
||||
"tcb_evaluation_data_numbers_json": response.tcb_evaluation_data_numbers_json,
|
||||
"issuer_chain": response.issuer_chain,
|
||||
});
|
||||
fs::write(
|
||||
test_data_dir.join("sgx_tcb_eval_nums.json"),
|
||||
serde_json::to_string_pretty(&data)?,
|
||||
)?;
|
||||
successes.push("SGX TCB evaluation data numbers".to_string());
|
||||
}
|
||||
Err(e) => {
|
||||
failures.push(format!("SGX TCB evaluation data numbers: {}", e));
|
||||
}
|
||||
}
|
||||
|
||||
// 13. Fetch TDX TCB evaluation data numbers
|
||||
println!("\n13. Fetching TDX TCB evaluation data numbers...");
|
||||
match client.get_tdx_tcb_evaluation_data_numbers().await {
|
||||
Ok(response) => {
|
||||
let data = serde_json::json!({
|
||||
"tcb_evaluation_data_numbers_json": response.tcb_evaluation_data_numbers_json,
|
||||
"issuer_chain": response.issuer_chain,
|
||||
});
|
||||
fs::write(
|
||||
test_data_dir.join("tdx_tcb_eval_nums.json"),
|
||||
serde_json::to_string_pretty(&data)?,
|
||||
)?;
|
||||
successes.push("TDX TCB evaluation data numbers".to_string());
|
||||
}
|
||||
Err(e) => {
|
||||
failures.push(format!("TDX TCB evaluation data numbers: {}", e));
|
||||
}
|
||||
}
|
||||
|
||||
// 14. Fetch PCK CRL with DER encoding
|
||||
println!("\n14. Fetching PCK CRL (processor, DER encoding)...");
|
||||
match client
|
||||
.get_pck_crl(CaType::Processor, Some(CrlEncoding::Der))
|
||||
.await
|
||||
{
|
||||
Ok(response) => {
|
||||
// For DER, save as base64
|
||||
let crl_base64 = general_purpose::STANDARD.encode(&response.crl_data);
|
||||
let data = serde_json::json!({
|
||||
"crl_data_base64": crl_base64,
|
||||
"issuer_chain": response.issuer_chain,
|
||||
});
|
||||
fs::write(
|
||||
test_data_dir.join("pck_crl_processor_der.json"),
|
||||
serde_json::to_string_pretty(&data)?,
|
||||
)?;
|
||||
successes.push("PCK CRL (processor, DER)".to_string());
|
||||
}
|
||||
Err(e) => {
|
||||
failures.push(format!("PCK CRL (processor, DER): {}", e));
|
||||
}
|
||||
}
|
||||
|
||||
// 15. Try different update types
|
||||
println!("\n15. Fetching SGX TCB info with Early update...");
|
||||
match client
|
||||
.get_sgx_tcb_info("00906ED50000", Some(UpdateType::Early), None)
|
||||
.await
|
||||
{
|
||||
Ok(response) => {
|
||||
let data = serde_json::json!({
|
||||
"tcb_info_json": response.tcb_info_json,
|
||||
"issuer_chain": response.issuer_chain,
|
||||
});
|
||||
fs::write(
|
||||
test_data_dir.join("sgx_tcb_info_early.json"),
|
||||
serde_json::to_string_pretty(&data)?,
|
||||
)?;
|
||||
successes.push("SGX TCB info (Early update)".to_string());
|
||||
}
|
||||
Err(e) => {
|
||||
failures.push(format!("SGX TCB info (Early update): {}", e));
|
||||
}
|
||||
}
|
||||
|
||||
// 16. Try with specific TCB evaluation data number
|
||||
println!("\n16. Fetching TDX TCB info with specific evaluation number...");
|
||||
match client
|
||||
.get_tdx_tcb_info("00806F050000", None, Some(17))
|
||||
.await
|
||||
{
|
||||
Ok(response) => {
|
||||
let data = serde_json::json!({
|
||||
"tcb_info_json": response.tcb_info_json,
|
||||
"issuer_chain": response.issuer_chain,
|
||||
});
|
||||
fs::write(
|
||||
test_data_dir.join("tdx_tcb_info_eval17.json"),
|
||||
serde_json::to_string_pretty(&data)?,
|
||||
)?;
|
||||
successes.push("TDX TCB info (eval number 17)".to_string());
|
||||
}
|
||||
Err(e) => {
|
||||
failures.push(format!("TDX TCB info (eval number 17): {}", e));
|
||||
}
|
||||
}
|
||||
|
||||
// 17. Try different FMSPCs
|
||||
println!("\n17. Fetching more SGX TCB info variations...");
|
||||
let test_fmspcs = vec!["00906ED50000", "00906C0F0000", "00A06F050000"];
|
||||
for fmspc in test_fmspcs {
|
||||
match client.get_sgx_tcb_info(fmspc, None, None).await {
|
||||
Ok(response) => {
|
||||
let data = serde_json::json!({
|
||||
"tcb_info_json": response.tcb_info_json,
|
||||
"issuer_chain": response.issuer_chain,
|
||||
});
|
||||
fs::write(
|
||||
test_data_dir.join(format!("sgx_tcb_info_{}.json", fmspc)),
|
||||
serde_json::to_string_pretty(&data)?,
|
||||
)?;
|
||||
successes.push(format!("SGX TCB info (FMSPC: {})", fmspc));
|
||||
}
|
||||
Err(e) => {
|
||||
failures.push(format!("SGX TCB info (FMSPC: {}): {}", fmspc, e));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 18. Try FMSPCs with different platform filters
|
||||
println!("\n18. Fetching FMSPCs with different platform filters...");
|
||||
match client.get_fmspcs(None).await {
|
||||
Ok(fmspcs_json) => {
|
||||
let data = serde_json::json!({
|
||||
"fmspcs_json": fmspcs_json,
|
||||
});
|
||||
fs::write(
|
||||
test_data_dir.join("fmspcs_no_filter.json"),
|
||||
serde_json::to_string_pretty(&data)?,
|
||||
)?;
|
||||
successes.push("FMSPCs (no filter)".to_string());
|
||||
}
|
||||
Err(e) => {
|
||||
failures.push(format!("FMSPCs (no filter): {}", e));
|
||||
}
|
||||
}
|
||||
|
||||
match client.get_fmspcs(Some(PlatformFilter::All)).await {
|
||||
Ok(fmspcs_json) => {
|
||||
let data = serde_json::json!({
|
||||
"fmspcs_json": fmspcs_json,
|
||||
});
|
||||
fs::write(
|
||||
test_data_dir.join("fmspcs_all_platforms.json"),
|
||||
serde_json::to_string_pretty(&data)?,
|
||||
)?;
|
||||
successes.push("FMSPCs (all platforms)".to_string());
|
||||
}
|
||||
Err(e) => {
|
||||
failures.push(format!("FMSPCs (all platforms): {}", e));
|
||||
}
|
||||
}
|
||||
|
||||
// 19. Try PCK certificates with different parameters (encrypted PPID)
|
||||
println!("\n19. Attempting to fetch PCK certificates with different params...");
|
||||
// Try with a different encrypted PPID format
|
||||
let encrypted_ppid = "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000";
|
||||
let pceid = "0000";
|
||||
|
||||
match client
|
||||
.get_pck_certificates_by_ppid(encrypted_ppid, pceid, None, None)
|
||||
.await
|
||||
{
|
||||
Ok(response) => {
|
||||
let data = serde_json::json!({
|
||||
"pck_certificates_json": response.pck_certs_json,
|
||||
"issuer_chain": response.issuer_chain,
|
||||
"fmspc": response.fmspc,
|
||||
});
|
||||
fs::write(
|
||||
test_data_dir.join("pck_certificates.json"),
|
||||
serde_json::to_string_pretty(&data)?,
|
||||
)?;
|
||||
successes.push("PCK certificates (by PPID)".to_string());
|
||||
}
|
||||
Err(e) => {
|
||||
failures.push(format!("PCK certificates (by PPID): {}", e));
|
||||
}
|
||||
}
|
||||
|
||||
// 20. Try TDX TCB info with different FMSPCs
|
||||
println!("\n20. Fetching TDX TCB info variations...");
|
||||
let tdx_fmspcs = vec!["00806F050000", "00A06F050000", "00606A0000000"];
|
||||
for fmspc in tdx_fmspcs {
|
||||
match client.get_tdx_tcb_info(fmspc, None, None).await {
|
||||
Ok(response) => {
|
||||
let data = serde_json::json!({
|
||||
"tcb_info_json": response.tcb_info_json,
|
||||
"issuer_chain": response.issuer_chain,
|
||||
});
|
||||
fs::write(
|
||||
test_data_dir.join(format!("tdx_tcb_info_{}.json", fmspc)),
|
||||
serde_json::to_string_pretty(&data)?,
|
||||
)?;
|
||||
successes.push(format!("TDX TCB info (FMSPC: {})", fmspc));
|
||||
}
|
||||
Err(e) => {
|
||||
failures.push(format!("TDX TCB info (FMSPC: {}): {}", fmspc, e));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 21. Try with V3 API for some endpoints
|
||||
println!("\n21. Testing V3 API endpoints...");
|
||||
let v3_client =
|
||||
ApiClient::new_with_options("https://api.trustedservices.intel.com", ApiVersion::V3)?;
|
||||
|
||||
match v3_client.get_sgx_tcb_info("00906ED50000", None, None).await {
|
||||
Ok(response) => {
|
||||
let data = serde_json::json!({
|
||||
"tcb_info_json": response.tcb_info_json,
|
||||
"issuer_chain": response.issuer_chain,
|
||||
});
|
||||
fs::write(
|
||||
test_data_dir.join("sgx_tcb_info_v3.json"),
|
||||
serde_json::to_string_pretty(&data)?,
|
||||
)?;
|
||||
successes.push("SGX TCB info (V3 API)".to_string());
|
||||
}
|
||||
Err(e) => {
|
||||
failures.push(format!("SGX TCB info (V3 API): {}", e));
|
||||
}
|
||||
}
|
||||
|
||||
match v3_client.get_sgx_qe_identity(None, None).await {
|
||||
Ok(response) => {
|
||||
let data = serde_json::json!({
|
||||
"enclave_identity_json": response.enclave_identity_json,
|
||||
"issuer_chain": response.issuer_chain,
|
||||
});
|
||||
fs::write(
|
||||
test_data_dir.join("sgx_qe_identity_v3.json"),
|
||||
serde_json::to_string_pretty(&data)?,
|
||||
)?;
|
||||
successes.push("SGX QE identity (V3 API)".to_string());
|
||||
}
|
||||
Err(e) => {
|
||||
failures.push(format!("SGX QE identity (V3 API): {}", e));
|
||||
}
|
||||
}
|
||||
|
||||
println!("\n\nTest data fetching complete!");
|
||||
println!("\nSuccessful fetches:");
|
||||
for s in &successes {
|
||||
println!(" ✓ {}", s);
|
||||
}
|
||||
|
||||
if !failures.is_empty() {
|
||||
println!("\nFailed fetches:");
|
||||
for f in &failures {
|
||||
println!(" ✗ {}", f);
|
||||
}
|
||||
}
|
||||
|
||||
println!("\nData saved in: {}", test_data_dir.display());
|
||||
|
||||
Ok(())
|
||||
}
|
|
@ -1,75 +0,0 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (c) 2025 Matter Labs
|
||||
|
||||
use intel_dcap_api::{ApiClient, CaType, CrlEncoding, IntelApiError, PckCrlResponse};
|
||||
use x509_cert::{
|
||||
der::{oid::AssociatedOid, Decode, SliceReader},
|
||||
ext::pkix::{
|
||||
crl::dp::DistributionPoint,
|
||||
name::{DistributionPointName, GeneralName},
|
||||
CrlDistributionPoints,
|
||||
},
|
||||
};
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), IntelApiError> {
|
||||
let client = ApiClient::new()?;
|
||||
|
||||
let PckCrlResponse {
|
||||
crl_data,
|
||||
issuer_chain,
|
||||
} = client
|
||||
.get_pck_crl(CaType::Platform, Some(CrlEncoding::Der))
|
||||
.await?;
|
||||
|
||||
let certs = x509_cert::certificate::CertificateInner::<
|
||||
x509_cert::certificate::Rfc5280
|
||||
>::load_pem_chain(issuer_chain.as_bytes()).map_err(
|
||||
|_| IntelApiError::InvalidParameter("Could not load a PEM chain")
|
||||
)?;
|
||||
|
||||
for cert in certs {
|
||||
println!("Issuer: {}", cert.tbs_certificate.issuer);
|
||||
println!("Subject: {}", cert.tbs_certificate.subject);
|
||||
println!("Serial Number: {}", cert.tbs_certificate.serial_number);
|
||||
println!("Not Before: {}", cert.tbs_certificate.validity.not_before);
|
||||
println!("Not After: {}", cert.tbs_certificate.validity.not_after);
|
||||
|
||||
// Extract and print CRL distribution points
|
||||
if let Some(extensions) = &cert.tbs_certificate.extensions {
|
||||
for ext in extensions.iter() {
|
||||
if ext.extn_id == CrlDistributionPoints::OID {
|
||||
// Create a SliceReader from the byte slice
|
||||
let mut reader = SliceReader::new(ext.extn_value.as_bytes()).map_err(|_| {
|
||||
IntelApiError::InvalidParameter(
|
||||
"Could not create reader from extension value",
|
||||
)
|
||||
})?;
|
||||
|
||||
// Now pass the reader to decode_value
|
||||
if let Ok(dist_points) = Vec::<DistributionPoint>::decode(&mut reader) {
|
||||
for point in dist_points {
|
||||
if let Some(DistributionPointName::FullName(names)) =
|
||||
point.distribution_point
|
||||
{
|
||||
for name in names {
|
||||
if let GeneralName::UniformResourceIdentifier(uri) = name {
|
||||
let uri = uri.as_str();
|
||||
let crl_bytes = reqwest::get(uri).await?.bytes().await?;
|
||||
println!("CRL bytes (hex): {}", hex::encode(&crl_bytes));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
println!("Could not decode CRL distribution points");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
println!("CRL bytes (hex): {}", hex::encode(&crl_data));
|
||||
|
||||
Ok(())
|
||||
}
|
|
@ -1,91 +0,0 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (c) 2025 Matter Labs
|
||||
|
||||
//! Example demonstrating automatic rate limit handling
|
||||
//!
|
||||
//! The Intel DCAP API client now automatically handles 429 Too Many Requests responses
|
||||
//! by retrying up to 3 times by default. This example shows how to configure the retry
|
||||
//! behavior and handle cases where all retries are exhausted.
|
||||
|
||||
use intel_dcap_api::{ApiClient, IntelApiError};
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
// Create API client with default settings (3 retries)
|
||||
let mut client = ApiClient::new()?;
|
||||
|
||||
println!("Example 1: Default behavior (automatic retries)");
|
||||
println!("================================================");
|
||||
|
||||
// Example FMSPC value
|
||||
let fmspc = "00606A000000";
|
||||
|
||||
// The client will automatically retry up to 3 times if rate limited
|
||||
match client.get_sgx_tcb_info(fmspc, None, None).await {
|
||||
Ok(tcb_info) => {
|
||||
println!("✓ Successfully retrieved TCB info");
|
||||
println!(
|
||||
" TCB Info JSON length: {} bytes",
|
||||
tcb_info.tcb_info_json.len()
|
||||
);
|
||||
println!(
|
||||
" Issuer Chain length: {} bytes",
|
||||
tcb_info.issuer_chain.len()
|
||||
);
|
||||
}
|
||||
Err(IntelApiError::TooManyRequests {
|
||||
request_id,
|
||||
retry_after,
|
||||
}) => {
|
||||
println!("✗ Rate limited even after 3 automatic retries");
|
||||
println!(" Request ID: {}", request_id);
|
||||
println!(" Last retry-after was: {} seconds", retry_after);
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("✗ Other error: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
println!("\nExample 2: Custom retry configuration");
|
||||
println!("=====================================");
|
||||
|
||||
// Configure client to retry up to 5 times
|
||||
client.set_max_retries(5);
|
||||
println!("Set max retries to 5");
|
||||
|
||||
match client.get_sgx_tcb_info(fmspc, None, None).await {
|
||||
Ok(_) => println!("✓ Request succeeded"),
|
||||
Err(IntelApiError::TooManyRequests { .. }) => {
|
||||
println!("✗ Still rate limited after 5 retries")
|
||||
}
|
||||
Err(e) => eprintln!("✗ Error: {}", e),
|
||||
}
|
||||
|
||||
println!("\nExample 3: Disable automatic retries");
|
||||
println!("====================================");
|
||||
|
||||
// Disable automatic retries
|
||||
client.set_max_retries(0);
|
||||
println!("Disabled automatic retries");
|
||||
|
||||
match client.get_sgx_tcb_info(fmspc, None, None).await {
|
||||
Ok(_) => println!("✓ Request succeeded on first attempt"),
|
||||
Err(IntelApiError::TooManyRequests {
|
||||
request_id,
|
||||
retry_after,
|
||||
}) => {
|
||||
println!("✗ Rate limited (no automatic retry)");
|
||||
println!(" Request ID: {}", request_id);
|
||||
println!(" Retry after: {} seconds", retry_after);
|
||||
println!(" You would need to implement manual retry logic here");
|
||||
}
|
||||
Err(e) => eprintln!("✗ Error: {}", e),
|
||||
}
|
||||
|
||||
println!("\nNote: The client handles rate limiting automatically!");
|
||||
println!("You only need to handle TooManyRequests errors if:");
|
||||
println!("- You disable automatic retries (set_max_retries(0))");
|
||||
println!("- All automatic retries are exhausted");
|
||||
|
||||
Ok(())
|
||||
}
|
|
@ -1,495 +0,0 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (c) 2025 Matter Labs
|
||||
|
||||
use intel_dcap_api::{
|
||||
ApiClient, ApiVersion, CaType, CrlEncoding, IntelApiError, PlatformFilter, UpdateType,
|
||||
};
|
||||
use std::time::Duration;
|
||||
use tokio::time::sleep;
|
||||
|
||||
/// Comprehensive integration test example demonstrating most Intel DCAP API client functions
|
||||
///
|
||||
/// This example shows how to use various endpoints of the Intel Trusted Services API.
|
||||
/// Note: Some operations may fail with 404 or 400 errors if the data doesn't exist on Intel's servers.
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
println!("=== Intel DCAP API Integration Test Example ===\n");
|
||||
|
||||
// Create clients for both V3 and V4 APIs
|
||||
let v4_client = ApiClient::new()?;
|
||||
let v3_client =
|
||||
ApiClient::new_with_options("https://api.trustedservices.intel.com", ApiVersion::V3)?;
|
||||
|
||||
// Track successes and failures
|
||||
let mut results = Vec::new();
|
||||
|
||||
// Test FMSPC - commonly used for TCB lookups
|
||||
let test_fmspc = "00906ED50000";
|
||||
let test_fmspc_tdx = "00806F050000";
|
||||
|
||||
println!("1. Testing TCB Info Endpoints...");
|
||||
println!("================================");
|
||||
|
||||
// 1.1 SGX TCB Info (V4)
|
||||
print!(" - SGX TCB Info (V4): ");
|
||||
match v4_client.get_sgx_tcb_info(test_fmspc, None, None).await {
|
||||
Ok(response) => {
|
||||
if response.issuer_chain.is_empty() {
|
||||
println!("✗ Failed: Empty issuer chain");
|
||||
results.push(("SGX TCB Info (V4)", false));
|
||||
} else {
|
||||
println!("✓ Success");
|
||||
println!(" FMSPC: {}", test_fmspc);
|
||||
println!(" Issuer chain: {} bytes", response.issuer_chain.len());
|
||||
let tcb_info: serde_json::Value = serde_json::from_str(&response.tcb_info_json)?;
|
||||
if let Some(version) = tcb_info["tcbInfo"]["version"].as_u64() {
|
||||
println!(" TCB Info Version: {}", version);
|
||||
}
|
||||
results.push(("SGX TCB Info (V4)", true));
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
println!("✗ Failed: {:?}", e);
|
||||
results.push(("SGX TCB Info (V4)", false));
|
||||
}
|
||||
}
|
||||
|
||||
// Add small delay between requests to be nice to the API
|
||||
sleep(Duration::from_millis(100)).await;
|
||||
|
||||
// 1.2 SGX TCB Info (V3)
|
||||
print!(" - SGX TCB Info (V3): ");
|
||||
match v3_client.get_sgx_tcb_info(test_fmspc, None, None).await {
|
||||
Ok(response) => {
|
||||
if response.issuer_chain.is_empty() {
|
||||
println!("✗ Failed: Empty issuer chain");
|
||||
results.push(("SGX TCB Info (V3)", false));
|
||||
} else {
|
||||
println!("✓ Success");
|
||||
println!(" Issuer chain: {} bytes", response.issuer_chain.len());
|
||||
results.push(("SGX TCB Info (V3)", true));
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
println!("✗ Failed: {:?}", e);
|
||||
results.push(("SGX TCB Info (V3)", false));
|
||||
}
|
||||
}
|
||||
|
||||
sleep(Duration::from_millis(100)).await;
|
||||
|
||||
// 1.3 TDX TCB Info
|
||||
print!(" - TDX TCB Info: ");
|
||||
match v4_client.get_tdx_tcb_info(test_fmspc_tdx, None, None).await {
|
||||
Ok(response) => {
|
||||
if response.issuer_chain.is_empty() {
|
||||
println!("✗ Failed: Empty issuer chain");
|
||||
results.push(("TDX TCB Info", false));
|
||||
} else {
|
||||
println!("✓ Success");
|
||||
println!(" Issuer chain: {} bytes", response.issuer_chain.len());
|
||||
let tcb_info: serde_json::Value = serde_json::from_str(&response.tcb_info_json)?;
|
||||
if let Some(id) = tcb_info["tcbInfo"]["id"].as_str() {
|
||||
println!(" Platform: {}", id);
|
||||
}
|
||||
results.push(("TDX TCB Info", true));
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
println!("✗ Failed: {:?}", e);
|
||||
results.push(("TDX TCB Info", false));
|
||||
}
|
||||
}
|
||||
|
||||
sleep(Duration::from_millis(100)).await;
|
||||
|
||||
// 1.4 SGX TCB Info with Early Update
|
||||
print!(" - SGX TCB Info (Early Update): ");
|
||||
match v4_client
|
||||
.get_sgx_tcb_info(test_fmspc, Some(UpdateType::Early), None)
|
||||
.await
|
||||
{
|
||||
Ok(response) => {
|
||||
if response.issuer_chain.is_empty() {
|
||||
println!("✗ Failed: Empty issuer chain");
|
||||
results.push(("SGX TCB Info (Early)", false));
|
||||
} else {
|
||||
println!("✓ Success");
|
||||
println!(" Issuer chain: {} bytes", response.issuer_chain.len());
|
||||
results.push(("SGX TCB Info (Early)", true));
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
println!("✗ Failed: {:?}", e);
|
||||
results.push(("SGX TCB Info (Early)", false));
|
||||
}
|
||||
}
|
||||
|
||||
sleep(Duration::from_millis(100)).await;
|
||||
|
||||
println!("\n2. Testing Enclave Identity Endpoints...");
|
||||
println!("========================================");
|
||||
|
||||
// 2.1 SGX QE Identity
|
||||
print!(" - SGX QE Identity: ");
|
||||
match v4_client.get_sgx_qe_identity(None, None).await {
|
||||
Ok(response) => {
|
||||
if response.issuer_chain.is_empty() {
|
||||
println!("✗ Failed: Empty issuer chain");
|
||||
results.push(("SGX QE Identity", false));
|
||||
} else {
|
||||
println!("✓ Success");
|
||||
println!(" Issuer chain: {} bytes", response.issuer_chain.len());
|
||||
let identity: serde_json::Value =
|
||||
serde_json::from_str(&response.enclave_identity_json)?;
|
||||
if let Some(id) = identity["enclaveIdentity"]["id"].as_str() {
|
||||
println!(" Enclave ID: {}", id);
|
||||
}
|
||||
results.push(("SGX QE Identity", true));
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
println!("✗ Failed: {:?}", e);
|
||||
results.push(("SGX QE Identity", false));
|
||||
}
|
||||
}
|
||||
|
||||
sleep(Duration::from_millis(100)).await;
|
||||
|
||||
// 2.2 SGX QVE Identity
|
||||
print!(" - SGX QVE Identity: ");
|
||||
match v4_client.get_sgx_qve_identity(None, None).await {
|
||||
Ok(response) => {
|
||||
if response.issuer_chain.is_empty() {
|
||||
println!("✗ Failed: Empty issuer chain");
|
||||
results.push(("SGX QVE Identity", false));
|
||||
} else {
|
||||
println!("✓ Success");
|
||||
println!(" Issuer chain: {} bytes", response.issuer_chain.len());
|
||||
results.push(("SGX QVE Identity", true));
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
println!("✗ Failed: {:?}", e);
|
||||
results.push(("SGX QVE Identity", false));
|
||||
}
|
||||
}
|
||||
|
||||
sleep(Duration::from_millis(100)).await;
|
||||
|
||||
// 2.3 SGX QAE Identity
|
||||
print!(" - SGX QAE Identity: ");
|
||||
match v4_client.get_sgx_qae_identity(None, None).await {
|
||||
Ok(response) => {
|
||||
if response.issuer_chain.is_empty() {
|
||||
println!("✗ Failed: Empty issuer chain");
|
||||
results.push(("SGX QAE Identity", false));
|
||||
} else {
|
||||
println!("✓ Success");
|
||||
println!(" Issuer chain: {} bytes", response.issuer_chain.len());
|
||||
results.push(("SGX QAE Identity", true));
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
println!("✗ Failed: {:?}", e);
|
||||
results.push(("SGX QAE Identity", false));
|
||||
}
|
||||
}
|
||||
|
||||
sleep(Duration::from_millis(100)).await;
|
||||
|
||||
// 2.4 TDX QE Identity (V4 only)
|
||||
print!(" - TDX QE Identity: ");
|
||||
match v4_client.get_tdx_qe_identity(None, None).await {
|
||||
Ok(response) => {
|
||||
if response.issuer_chain.is_empty() {
|
||||
println!("✗ Failed: Empty issuer chain");
|
||||
results.push(("TDX QE Identity", false));
|
||||
} else {
|
||||
println!("✓ Success");
|
||||
println!(" Issuer chain: {} bytes", response.issuer_chain.len());
|
||||
results.push(("TDX QE Identity", true));
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
println!("✗ Failed: {:?}", e);
|
||||
results.push(("TDX QE Identity", false));
|
||||
}
|
||||
}
|
||||
|
||||
sleep(Duration::from_millis(100)).await;
|
||||
|
||||
println!("\n3. Testing PCK CRL Endpoints...");
|
||||
println!("================================");
|
||||
|
||||
// 3.1 PCK CRL - Processor (PEM)
|
||||
print!(" - PCK CRL (Processor, PEM): ");
|
||||
match v4_client.get_pck_crl(CaType::Processor, None).await {
|
||||
Ok(response) => {
|
||||
if response.issuer_chain.is_empty() {
|
||||
println!("✗ Failed: Empty issuer chain");
|
||||
results.push(("PCK CRL (Processor)", false));
|
||||
} else {
|
||||
println!("✓ Success");
|
||||
println!(" Issuer chain: {} bytes", response.issuer_chain.len());
|
||||
let crl_str = String::from_utf8_lossy(&response.crl_data);
|
||||
if crl_str.contains("BEGIN X509 CRL") {
|
||||
println!(" Format: PEM");
|
||||
}
|
||||
results.push(("PCK CRL (Processor)", true));
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
println!("✗ Failed: {:?}", e);
|
||||
results.push(("PCK CRL (Processor)", false));
|
||||
}
|
||||
}
|
||||
|
||||
sleep(Duration::from_millis(100)).await;
|
||||
|
||||
// 3.2 PCK CRL - Platform (DER)
|
||||
print!(" - PCK CRL (Platform, DER): ");
|
||||
match v4_client
|
||||
.get_pck_crl(CaType::Platform, Some(CrlEncoding::Der))
|
||||
.await
|
||||
{
|
||||
Ok(response) => {
|
||||
if response.issuer_chain.is_empty() {
|
||||
println!("✗ Failed: Empty issuer chain");
|
||||
results.push(("PCK CRL (Platform, DER)", false));
|
||||
} else {
|
||||
println!("✓ Success");
|
||||
println!(" Issuer chain: {} bytes", response.issuer_chain.len());
|
||||
println!(" CRL size: {} bytes", response.crl_data.len());
|
||||
results.push(("PCK CRL (Platform, DER)", true));
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
println!("✗ Failed: {:?}", e);
|
||||
results.push(("PCK CRL (Platform, DER)", false));
|
||||
}
|
||||
}
|
||||
|
||||
sleep(Duration::from_millis(100)).await;
|
||||
|
||||
println!("\n4. Testing FMSPC Endpoints (V4 only)...");
|
||||
println!("=======================================");
|
||||
|
||||
// 4.1 Get FMSPCs (no filter)
|
||||
print!(" - Get FMSPCs (no filter): ");
|
||||
match v4_client.get_fmspcs(None).await {
|
||||
Ok(fmspcs_json) => {
|
||||
println!("✓ Success");
|
||||
let fmspcs: serde_json::Value = serde_json::from_str(&fmspcs_json)?;
|
||||
if let Some(arr) = fmspcs.as_array() {
|
||||
println!(" Total FMSPCs: {}", arr.len());
|
||||
// Show first few FMSPCs
|
||||
for (i, fmspc) in arr.iter().take(3).enumerate() {
|
||||
if let (Some(fmspc_val), Some(platform)) =
|
||||
(fmspc["fmspc"].as_str(), fmspc["platform"].as_str())
|
||||
{
|
||||
println!(" [{}] {} - {}", i + 1, fmspc_val, platform);
|
||||
}
|
||||
}
|
||||
if arr.len() > 3 {
|
||||
println!(" ... and {} more", arr.len() - 3);
|
||||
}
|
||||
}
|
||||
results.push(("Get FMSPCs", true));
|
||||
}
|
||||
Err(e) => {
|
||||
println!("✗ Failed: {:?}", e);
|
||||
results.push(("Get FMSPCs", false));
|
||||
}
|
||||
}
|
||||
|
||||
sleep(Duration::from_millis(100)).await;
|
||||
|
||||
// 4.2 Get FMSPCs with platform filter
|
||||
print!(" - Get FMSPCs (All platforms): ");
|
||||
match v4_client.get_fmspcs(Some(PlatformFilter::All)).await {
|
||||
Ok(_) => {
|
||||
println!("✓ Success");
|
||||
results.push(("Get FMSPCs (filtered)", true));
|
||||
}
|
||||
Err(e) => {
|
||||
println!("✗ Failed: {:?}", e);
|
||||
results.push(("Get FMSPCs (filtered)", false));
|
||||
}
|
||||
}
|
||||
|
||||
sleep(Duration::from_millis(100)).await;
|
||||
|
||||
println!("\n5. Testing TCB Evaluation Data Numbers (V4 only)...");
|
||||
println!("===================================================");
|
||||
|
||||
// 5.1 SGX TCB Evaluation Data Numbers
|
||||
print!(" - SGX TCB Evaluation Data Numbers: ");
|
||||
match v4_client.get_sgx_tcb_evaluation_data_numbers().await {
|
||||
Ok(response) => {
|
||||
if response.issuer_chain.is_empty() {
|
||||
println!("✗ Failed: Empty issuer chain");
|
||||
results.push(("SGX TCB Eval Numbers", false));
|
||||
} else {
|
||||
println!("✓ Success");
|
||||
println!(" Issuer chain: {} bytes", response.issuer_chain.len());
|
||||
let data: serde_json::Value =
|
||||
serde_json::from_str(&response.tcb_evaluation_data_numbers_json)?;
|
||||
if let Some(sgx_data) = data.get("sgx") {
|
||||
println!(
|
||||
" SGX entries: {}",
|
||||
sgx_data.as_array().map(|a| a.len()).unwrap_or(0)
|
||||
);
|
||||
}
|
||||
results.push(("SGX TCB Eval Numbers", true));
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
println!("✗ Failed: {:?}", e);
|
||||
results.push(("SGX TCB Eval Numbers", false));
|
||||
}
|
||||
}
|
||||
|
||||
sleep(Duration::from_millis(100)).await;
|
||||
|
||||
// 5.2 TDX TCB Evaluation Data Numbers
|
||||
print!(" - TDX TCB Evaluation Data Numbers: ");
|
||||
match v4_client.get_tdx_tcb_evaluation_data_numbers().await {
|
||||
Ok(response) => {
|
||||
if response.issuer_chain.is_empty() {
|
||||
println!("✗ Failed: Empty issuer chain");
|
||||
results.push(("TDX TCB Eval Numbers", false));
|
||||
} else {
|
||||
println!("✓ Success");
|
||||
println!(" Issuer chain: {} bytes", response.issuer_chain.len());
|
||||
let data: serde_json::Value =
|
||||
serde_json::from_str(&response.tcb_evaluation_data_numbers_json)?;
|
||||
if let Some(tdx_data) = data.get("tdx") {
|
||||
println!(
|
||||
" TDX entries: {}",
|
||||
tdx_data.as_array().map(|a| a.len()).unwrap_or(0)
|
||||
);
|
||||
}
|
||||
results.push(("TDX TCB Eval Numbers", true));
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
println!("✗ Failed: {:?}", e);
|
||||
results.push(("TDX TCB Eval Numbers", false));
|
||||
}
|
||||
}
|
||||
|
||||
sleep(Duration::from_millis(100)).await;
|
||||
|
||||
println!("\n6. Testing PCK Certificate Endpoints...");
|
||||
println!("=======================================");
|
||||
|
||||
/* // 6.1 PCK Certificate by PPID (usually requires valid data)
|
||||
print!(" - PCK Certificate by PPID: ");
|
||||
let test_ppid = "0000000000000000000000000000000000000000000000000000000000000000";
|
||||
let test_cpusvn = "00000000000000000000000000000000";
|
||||
let test_pcesvn = "0000";
|
||||
let test_pceid = "0000";
|
||||
|
||||
match v4_client
|
||||
.get_pck_certificate_by_ppid(test_ppid, test_cpusvn, test_pcesvn, test_pceid, None, None)
|
||||
.await
|
||||
{
|
||||
Ok(_) => {
|
||||
println!("✓ Success");
|
||||
results.push(("PCK Certificate", true));
|
||||
}
|
||||
Err(e) => {
|
||||
// Expected to fail with test data
|
||||
match &e {
|
||||
IntelApiError::ApiError { status, .. } => {
|
||||
println!("✗ Failed (Expected): HTTP {}", status);
|
||||
}
|
||||
_ => println!("✗ Failed: {:?}", e),
|
||||
}
|
||||
results.push(("PCK Certificate", false));
|
||||
}
|
||||
}
|
||||
|
||||
sleep(Duration::from_millis(100)).await;
|
||||
*/
|
||||
println!("\n7. Testing API Version Compatibility...");
|
||||
println!("=======================================");
|
||||
|
||||
// 7.1 Try V4-only endpoint on V3
|
||||
print!(" - V4-only endpoint on V3 (should fail): ");
|
||||
match v3_client.get_fmspcs(None).await {
|
||||
Ok(_) => {
|
||||
println!("✗ Unexpected success!");
|
||||
results.push(("V3/V4 compatibility check", false));
|
||||
}
|
||||
Err(IntelApiError::UnsupportedApiVersion(_)) => {
|
||||
println!("✓ Correctly rejected");
|
||||
results.push(("V3/V4 compatibility check", true));
|
||||
}
|
||||
Err(e) => {
|
||||
println!("✗ Wrong error: {:?}", e);
|
||||
results.push(("V3/V4 compatibility check", false));
|
||||
}
|
||||
}
|
||||
|
||||
println!("\n8. Testing Error Handling...");
|
||||
println!("============================");
|
||||
|
||||
// 8.1 Invalid FMSPC
|
||||
print!(" - Invalid FMSPC format: ");
|
||||
match v4_client.get_sgx_tcb_info("invalid", None, None).await {
|
||||
Ok(_) => {
|
||||
println!("✗ Unexpected success!");
|
||||
results.push(("Error handling", false));
|
||||
}
|
||||
Err(IntelApiError::ApiError {
|
||||
status,
|
||||
error_code,
|
||||
error_message,
|
||||
..
|
||||
}) => {
|
||||
println!("✓ Correctly handled");
|
||||
println!(" Status: {}", status);
|
||||
if let Some(code) = error_code {
|
||||
println!(" Error Code: {}", code);
|
||||
}
|
||||
if let Some(msg) = error_message {
|
||||
println!(" Error Message: {}", msg);
|
||||
}
|
||||
results.push(("Error handling", true));
|
||||
}
|
||||
Err(e) => {
|
||||
println!("✗ Unexpected error: {:?}", e);
|
||||
results.push(("Error handling", false));
|
||||
}
|
||||
}
|
||||
|
||||
// Summary
|
||||
println!("\n\n=== Summary ===");
|
||||
println!("===============");
|
||||
|
||||
let total = results.len();
|
||||
let successful = results.iter().filter(|(_, success)| *success).count();
|
||||
let failed = total - successful;
|
||||
|
||||
println!("Total tests: {}", total);
|
||||
println!(
|
||||
"Successful: {} ({}%)",
|
||||
successful,
|
||||
(successful * 100) / total
|
||||
);
|
||||
println!("Failed: {} ({}%)", failed, (failed * 100) / total);
|
||||
|
||||
println!("\nDetailed Results:");
|
||||
for (test, success) in &results {
|
||||
println!(" {} {}", if *success { "✓" } else { "✗" }, test);
|
||||
}
|
||||
|
||||
println!("\nNote: Some failures are expected due to:");
|
||||
println!("- Test data not existing on Intel servers");
|
||||
println!("- PCK operations requiring valid platform data");
|
||||
println!("- Subscription key requirements for certain endpoints");
|
||||
|
||||
Ok(())
|
||||
}
|
|
@ -1,694 +0,0 @@
|
|||
# Intel® SGX and Intel® TDX services - V3 API Documentation
|
||||
|
||||
## Intel® SGX and Intel® TDX Registration Service for Scalable Platforms
|
||||
|
||||
The API exposed by the Intel SGX registration service allows registering an Intel® SGX platform with multiple processor
|
||||
packages as a single platform instance, which can be remotely attested as a single entity later on[cite: 1]. The minimum
|
||||
version of the TLS protocol supported by the service is 1.2; any connection attempts with previous versions of TLS/SSL
|
||||
will be dropped by the server[cite: 2].
|
||||
|
||||
### Register Platform
|
||||
|
||||
This API allows registering a multi-package SGX platform, covering initial registration and TCB Recovery[cite: 2].
|
||||
During registration, the platform manifest is authenticated by the Registration Service to verify it originates from a
|
||||
genuine, non-revoked SGX platform[cite: 2]. If the platform configuration is successfully verified, platform
|
||||
provisioning root keys are stored in the backend[cite: 2].
|
||||
|
||||
Stored platform provisioning root keys are later used to derive the public parts of Provisioning Certification Keys (
|
||||
PCKs)[cite: 2]. These PCKs are distributed as x.509 certificates by the Provisioning Certification Service for Intel SGX
|
||||
and are used during the remote attestation of the platform[cite: 3].
|
||||
|
||||
#### POST `https://api.trustedservices.intel.com/sgx/registration/v1/platform`
|
||||
|
||||
**Request**
|
||||
|
||||
**Headers**
|
||||
|
||||
Besides the headers explicitly mentioned below, the HTTP request may contain standard HTTP headers (e.g.,
|
||||
Content-Length)[cite: 3].
|
||||
|
||||
| Name | Required | Value | Description |
|
||||
|:-------------|:---------|:---------------------------|:----------------------------------------|
|
||||
| Content-Type | True | `application/octet-stream` | MIME type of the request body[cite: 4]. |
|
||||
|
||||
**Body**
|
||||
|
||||
The body is a binary representation of the Platform Manifest structure – an opaque blob representing a registration
|
||||
manifest for a multi-package platform[cite: 5]. It contains platform provisioning root keys established by the platform
|
||||
instance and data required to authenticate the platform as genuine and non-revoked[cite: 5].
|
||||
|
||||
**Example Request**
|
||||
|
||||
```bash
|
||||
curl -H "Content-Type: application/octet-stream" --data-binary @platform_manifest POST "[https://api.trustedservices.intel.com/sgx/registration/v1/platform](https://api.trustedservices.intel.com/sgx/registration/v1/platform)"
|
||||
````
|
||||
|
||||
**Response**
|
||||
|
||||
**Model**
|
||||
|
||||
The response is a Hex-encoded representation of the PPID for the registered platform instance (only if the HTTP Status
|
||||
Code is 201; otherwise, the body is empty).
|
||||
|
||||
**Example Response**
|
||||
|
||||
```
|
||||
001122334455667788AABBCCDDEEFF
|
||||
```
|
||||
|
||||
**Status Codes**
|
||||
|
||||
| Code | Headers | Body | Description |
|
||||
|:-----|:--------------------------------------------------------------------------------------------------------------------------------------|:------------------------------------|:-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| 201 | Request-ID: Randomly generated identifier for each request (for troubleshooting purposes). | Hex-encoded representation of PPID. | Operation successful (new platform instance registered). A new platform instance has been registered[cite: 5]. |
|
||||
| 400 | Request-ID: Randomly generated identifier[cite: 6]. \<br\> Error-Code and Error-Message: Additional details about the error[cite: 9]. | | Invalid Platform Manifest[cite: 8]. The request might be malformed[cite: 6], intended for a different server[cite: 7], contain an invalid/revoked package[cite: 7], an unrecognized package[cite: 7], an incompatible package[cite: 7], an invalid manifest[cite: 7], or violate a key caching policy[cite: 8]. The client should not repeat the request without modifications[cite: 9]. |
|
||||
| 415 | Request-ID: Randomly generated identifier[cite: 10]. | | MIME type specified in the request is not supported[cite: 10]. |
|
||||
| 500 | Request-ID: Randomly generated identifier[cite: 10]. | | Internal server error occurred[cite: 10]. |
|
||||
| 503 | Request-ID: Randomly generated identifier[cite: 10]. | | Server is currently unable to process the request. The client should try again later[cite: 11]. |
|
||||
|
||||
-----
|
||||
|
||||
### Add Package
|
||||
|
||||
This API adds new package(s) to an already registered platform instance[cite: 11]. A subscription is required[cite: 11].
|
||||
If successful, a Platform Membership Certificate is generated for each processor package in the Add Request[cite: 12].
|
||||
|
||||
#### POST `https://api.trustedservices.intel.com/sgx/registration/v1/package`
|
||||
|
||||
**Request**
|
||||
|
||||
**Headers**
|
||||
|
||||
| Name | Required | Value | Description |
|
||||
|:--------------------------|:---------|:---------------------------|:--------------------------------------------------------------------------------|
|
||||
| Ocp-Apim-Subscription-Key | True | | Subscription key providing access to this API, found in your Profile[cite: 14]. |
|
||||
| Content-Type | True | `application/octet-stream` | MIME type of the request body[cite: 14]. |
|
||||
|
||||
**Body**
|
||||
|
||||
Binary representation of the Add Request structure – an opaque blob for adding new processor packages to an existing
|
||||
platform instance.
|
||||
|
||||
**Example Request**
|
||||
|
||||
```bash
|
||||
curl -H "Content-Type: application/octet-stream" --data-binary @add_package POST "[https://api.trustedservices.intel.com/sgx/registration/v1/package](https://api.trustedservices.intel.com/sgx/registration/v1/package)" -H "Ocp-Apim-Subscription-Key: {subscription_key}"
|
||||
```
|
||||
|
||||
**Response**
|
||||
|
||||
**Model**
|
||||
|
||||
For a 200 HTTP Status Code, the response is a fixed-size array (8 elements) containing binary representations of
|
||||
Platform Membership Certificate structures[cite: 15]. Certificates are populated sequentially, starting at index 0, with
|
||||
the rest of the elements zeroed[cite: 15].
|
||||
|
||||
**Example Response (hex-encoded)**
|
||||
|
||||
```
|
||||
E4B0E8B80F8B49184488F77273550840984816854488B7CFRP...
|
||||
```
|
||||
|
||||
**Status Codes**
|
||||
|
||||
| Code | Headers | Body | Description |
|
||||
|:-----|:------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:----------------------------------------------------------------|:------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| 200 | Content-Type: `application/octet-stream`[cite: 17]. \<br\> Request-ID: Random identifier[cite: 17]. \<br\> CertificateCount: Number of certificates returned[cite: 17]. | Fixed-size array of Platform Membership Certificates[cite: 17]. | Operation successful. Packages added[cite: 17]. |
|
||||
| 400 | Request-ID: Random identifier[cite: 17]. \<br\> Error-Code and Error-Message: Details on the error[cite: 17]. | | Invalid Add Request Payload[cite: 17]. Can be due to malformed syntax, platform not found, invalid/revoked/unrecognized package, or invalid AddRequest[cite: 17]. |
|
||||
| 401 | Request-ID: Random identifier[cite: 17]. | | Failed to authenticate or authorize the request[cite: 17]. |
|
||||
| 415 | Request-ID: Random identifier[cite: 17]. | | MIME type specified is not supported[cite: 17]. |
|
||||
| 500 | Request-ID: Random identifier[cite: 17]. | | Internal server error occurred[cite: 17]. |
|
||||
| 503 | Request-ID: Random identifier[cite: 17]. | | Server is currently unable to process the request[cite: 17]. |
|
||||
|
||||
-----
|
||||
|
||||
## Intel® SGX Provisioning Certification Service for ECDSA Attestation
|
||||
|
||||
Download the Provisioning Certification Root CA Certificate (API v3) here:
|
||||
|
||||
* [DER](https://www.google.com/search?q=https://certificates.trustedservices.intel.com/Intel_SGX_Provisioning_Certification_RootCA.cer) [cite: 18]
|
||||
* [PEM](https://www.google.com/search?q=https://certificates.trustedservices.intel.com/intel_SGX_Provisioning_Certification_RootCA.perm) [cite: 18]
|
||||
|
||||
### Get PCK Certificate V3
|
||||
|
||||
This API allows requesting a single PCK certificate by specifying PPID and SVNs or Platform Manifest and SVNs[cite: 18].
|
||||
A subscription is required[cite: 18].
|
||||
|
||||
* **Using PPID and SVNs**:
|
||||
* Single-socket platforms: No prerequisites[cite: 18].
|
||||
* Multi-socket platforms: Requires previous registration via `Register Platform` API[cite: 18]. Platform root keys
|
||||
must be persistently stored[cite: 19], and the `Keys Caching Policy` must be set to `true`[cite: 21]. The service
|
||||
uses a PCK public key derived from stored keys[cite: 20].
|
||||
* **Using Platform Manifest and SVNs**:
|
||||
* Multi-socket platforms: Does not require previous registration[cite: 21]. It doesn't require keys to be
|
||||
persistently stored[cite: 22]. The service uses a PCK public key derived from the provided manifest[cite: 23].
|
||||
Depending on the `Keys Caching Policy`, keys might be stored[cite: 24].
|
||||
* **Direct Registration** (`Register Platform` first): Sets policy to always store keys[cite: 25]. Keys are
|
||||
stored when the manifest is sent[cite: 26]. `CachedKeys` flag in PCK Certificates is set to `true`[cite: 27].
|
||||
* **Indirect Registration** (`Get PCK Certificate(s)` first): Sets policy to never store keys[cite: 27]. Keys
|
||||
are discarded after use[cite: 28]. Standard metadata is stored, but `Register Platform` cannot be used
|
||||
anymore[cite: 29]. `CachedKeys` flag is set to `false`[cite: 30].
|
||||
|
||||
The PCS returns the PCK Certificate representing the TCB level with the highest security posture based on CPUSVN and PCE
|
||||
ISVSVN[cite: 30].
|
||||
|
||||
#### GET `https://api.trustedservices.intel.com/sgx/certification/v3/pckcert`
|
||||
|
||||
**Request**
|
||||
|
||||
| Name | Type | Type | Required | Pattern | Description |
|
||||
|:--------------------------|:-------|:-------|:---------|:--------------------|:-----------------------------------------------------------------|
|
||||
| Ocp-Apim-Subscription-Key | String | Header | True | | Subscription key[cite: 32]. |
|
||||
| PPID-Encryption-Key | String | Header | False | | Type of key for PPID encryption (Default: `RSA-3072`)[cite: 32]. |
|
||||
| encrypted\_ppid | String | Query | True | `[0-9a-fA-F]{768}$` | Base16-encoded PPID (encrypted with PPIDEK)[cite: 32]. |
|
||||
| cpusvn | String | Query | True | `[0-9a-fA-F]{32}$` | Base16-encoded CPUSVN (16 bytes)[cite: 32]. |
|
||||
| pcesvn | String | Query | True | `[0-9a-fA-F]{4}$` | Base16-encoded PCESVN (2 bytes, little endian)[cite: 32]. |
|
||||
| pceid | String | Query | True | `[0-9a-fA-F]{4}$` | Base16-encoded PCE-ID (2 bytes, little endian)[cite: 32]. |
|
||||
|
||||
**Example Request**
|
||||
|
||||
```bash
|
||||
curl -X GET "[https://api.trustedservices.intel.com/sgx/certification/v3/pckcert?encrypted_ppid=...&cpusvn=...&pcesvn=...&pceid=](https://api.trustedservices.intel.com/sgx/certification/v3/pckcert?encrypted_ppid=...&cpusvn=...&pcesvn=...&pceid=)..." -H "Ocp-Apim-Subscription-Key: {subscription_key}"
|
||||
```
|
||||
|
||||
**Response**: Response description can be
|
||||
found [here](https://www.google.com/search?q=%23response-get-and-post-1)[cite: 34].
|
||||
|
||||
#### POST `https://api.trustedservices.intel.com/sgx/certification/v3/pckcert`
|
||||
|
||||
**Request**
|
||||
|
||||
| Name | Type | Request Type | Required | Pattern | Description |
|
||||
|:--------------------------|:-------|:-------------|:---------|:-----------------------------|:---------------------------------------------|
|
||||
| Ocp-Apim-Subscription-Key | String | Header | True | | Subscription key[cite: 35]. |
|
||||
| Content-Type | String | Header | True | | Content Type (`application/json`)[cite: 35]. |
|
||||
| platformManifest | String | Body Field | True | `[0-9a-fA-F]{16882,112884}$` | Base16-encoded Platform Manifest[cite: 35]. |
|
||||
| cpusvn | String | Body Field | True | `[0-9a-fA-F]{32}$` | Base16-encoded CPUSVN[cite: 35]. |
|
||||
| pcesvn | String | Body Field | True | `[0-9a-fA-F]{4}$` | Base16-encoded PCESVN[cite: 35]. |
|
||||
| pceid | String | Body Field | True | `[0-9a-fA-F]{4}$` | Base16-encoded PCE-ID[cite: 35]. |
|
||||
|
||||
**Body**
|
||||
|
||||
```json
|
||||
{
|
||||
"platformManifest": "...",
|
||||
"cpusvn": "...",
|
||||
"pcesvn": "...",
|
||||
"pceid": "..."
|
||||
}
|
||||
```
|
||||
|
||||
**Example Request**
|
||||
|
||||
```bash
|
||||
curl -X POST -d '{"platformManifest": "...", "cpusvn": "...", "pcesvn": "...", "pceid": "..."}' -H "Content-Type: application/json" -H "Ocp-Apim-Subscription-Key: {subscription_key}" "[https://api.trustedservices.intel.com/sgx/certification/v3/pckcert](https://api.trustedservices.intel.com/sgx/certification/v3/pckcert)"
|
||||
```
|
||||
|
||||
**Response (GET and POST)**
|
||||
|
||||
**Model**: PckCert (X-PEM-FILE) - PEM-encoded SGX PCK Certificate[cite: 36].
|
||||
|
||||
**Example Response**
|
||||
|
||||
```pem
|
||||
-----BEGIN CERTIFICATE-----
|
||||
...
|
||||
-----END CERTIFICATE-----
|
||||
```
|
||||
|
||||
**Status Codes**
|
||||
|
||||
| Code | Model | Headers | Description |
|
||||
|:-----|:--------|:------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:-------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| 200 | PckCert | Content-Type: `application/x-pem-file`[cite: 36]. \<br\> Request-ID[cite: 36]. \<br\> SGX-PCK-Certificate-Issuer-Chain: URL-encoded issuer chain[cite: 36]. \<br\> SGX-TCBm: Hex-encoded CPUSVN and PCESVN[cite: 37]. \<br\> SGX-FMSPC: Hex-encoded FMSPC[cite: 37]. \<br\> SGX-PCK-Certificate-CA-Type: 'processor' or 'platform'[cite: 39]. \<br\> Warning: Optional message[cite: 39]. | Operation successful[cite: 36]. |
|
||||
| 400 | | Request-ID[cite: 39]. \<br\> Warning[cite: 39]. | Invalid request parameters[cite: 39]. |
|
||||
| 401 | | Request-ID[cite: 40]. \<br\> Warning[cite: 40]. | Failed to authenticate or authorize the request[cite: 40]. |
|
||||
| 404 | | Request-ID[cite: 40]. \<br\> Warning[cite: 40]. | PCK Certificate not found[cite: 40]. Reasons: unsupported PPID/PCE-ID, TCB level too low, or Platform Manifest not registered/updated[cite: 41]. |
|
||||
| 500 | | Request-ID[cite: 41]. \<br\> Warning[cite: 41]. | Internal server error occurred[cite: 41]. |
|
||||
| 503 | | Request-ID[cite: 42]. \<br\> Warning[cite: 42]. | Server is currently unable to process the request[cite: 42]. |
|
||||
|
||||
-----
|
||||
|
||||
### Get PCK Certificates V3
|
||||
|
||||
This API retrieves PCK certificates for all configured TCB levels using PPID or Platform Manifest[cite: 42].
|
||||
Subscription required[cite: 42].
|
||||
|
||||
* **Using PPID**:
|
||||
* Single-socket platforms: No prerequisites[cite: 43].
|
||||
* Multi-socket platforms: Requires prior registration via `Register Platform` API[cite: 44]. Keys must be
|
||||
persistently stored[cite: 45], and `Keys Caching Policy` must be `true`[cite: 47]. PCS uses stored keys[cite: 46].
|
||||
* **Using Platform Manifest**:
|
||||
* Multi-socket platforms: Does not require prior registration[cite: 47]. Does not require persistent
|
||||
storage[cite: 48]. PCS uses manifest keys[cite: 49]. Caching policy determines storage[cite: 50].
|
||||
* **Direct Registration**: Always stores keys; `CachedKeys` is `true`[cite: 51, 52].
|
||||
* **Indirect Registration**: Never stores keys; `CachedKeys` is `false`[cite: 53].
|
||||
|
||||
#### GET `https://api.trustedservices.intel.com/sgx/certification/v3/pckcerts`
|
||||
|
||||
Retrieves certificates based on encrypted PPID and PCE-ID[cite: 53].
|
||||
|
||||
**Request**
|
||||
|
||||
| Name | Type | Type | Required | Pattern | Description |
|
||||
|:--------------------------|:-------|:-------|:---------|:--------------------|:--------------------------------------------------------------|
|
||||
| Ocp-Apim-Subscription-Key | String | Header | True | | Subscription key[cite: 54]. |
|
||||
| PPID-Encryption-Key | String | Header | False | | Key type for PPID encryption (Default: `RSA-3072`)[cite: 54]. |
|
||||
| encrypted\_ppid | String | Query | True | `[0-9a-fA-F]{768}$` | Base16-encoded PPID[cite: 54]. |
|
||||
| pceid | String | Query | True | `[0-9a-fA-F]{4}$` | Base16-encoded PCE-ID[cite: 54]. |
|
||||
|
||||
**Example Request**
|
||||
|
||||
```bash
|
||||
curl -X GET "[https://api.trustedservices.intel.com/sgx/certification/v3/pckcerts?encrypted_ppid=...&pceid=](https://api.trustedservices.intel.com/sgx/certification/v3/pckcerts?encrypted_ppid=...&pceid=)..." -H "Ocp-Apim-Subscription-Key: {subscription_key}"
|
||||
```
|
||||
|
||||
**Response**: Response description can be
|
||||
found [here](https://www.google.com/search?q=%23response-get-and-post-2)[cite: 55].
|
||||
|
||||
#### GET `https://api.trustedservices.intel.com/sgx/certification/v3/pckcerts/config`
|
||||
|
||||
Retrieves certificates for a specific CPUSVN (multi-package only)[cite: 55].
|
||||
|
||||
**Request**
|
||||
|
||||
| Name | Type | Type | Required | Pattern | Description |
|
||||
|:--------------------------|:-------|:-------|:---------|:--------------------|:----------------------------------------|
|
||||
| Ocp-Apim-Subscription-Key | String | Header | True | | Subscription key[cite: 56]. |
|
||||
| PPID-Encryption-Key | String | Header | False | | Key type for PPID encryption[cite: 56]. |
|
||||
| encrypted\_ppid | String | Query | True | `[0-9a-fA-F]{768}$` | Base16-encoded PPID[cite: 56]. |
|
||||
| pceid | String | Query | True | `[0-9a-fA-F]{4}$` | Base16-encoded PCE-ID[cite: 56]. |
|
||||
| cpusvn | String | Query | True | `[0-9a-fA-F]{32}$` | Base16-encoded CPUSVN[cite: 56]. |
|
||||
|
||||
**Example Request**
|
||||
|
||||
```bash
|
||||
curl -X GET "[https://api.trustedservices.intel.com/sgx/certification/v3/pckcerts/config?encrypted_ppid=...&pceid=...&cpusvn=](https://api.trustedservices.intel.com/sgx/certification/v3/pckcerts/config?encrypted_ppid=...&pceid=...&cpusvn=)..." -H "Ocp-Apim-Subscription-Key: {subscription_key}"
|
||||
```
|
||||
|
||||
**Response**: Response description can be
|
||||
found [here](https://www.google.com/search?q=%23response-get-and-post-2)[cite: 57].
|
||||
|
||||
#### POST `https://api.trustedservices.intel.com/sgx/certification/v3/pckcerts`
|
||||
|
||||
Retrieves certificates based on Platform Manifest and PCE-ID (multi-package only)[cite: 57].
|
||||
|
||||
**Request**
|
||||
|
||||
| Name | Type | Request Type | Required | Pattern | Description |
|
||||
|:--------------------------|:-------|:-------------|:---------|:-----------------------------|:--------------------------------------------|
|
||||
| Ocp-Apim-Subscription-Key | String | Header | True | | Subscription key[cite: 58]. |
|
||||
| Content-Type | String | Header | True | `application/json` | Content Type[cite: 58]. |
|
||||
| platformManifest | String | Body Field | True | `[0-9a-fA-F]{16882,112884}$` | Base16-encoded Platform Manifest[cite: 58]. |
|
||||
| pceid | String | Body Field | True | `[0-9a-fA-F]{4}$` | Base16-encoded PCE-ID[cite: 58]. |
|
||||
|
||||
**Body**
|
||||
|
||||
```json
|
||||
{
|
||||
"platformManifest": "...",
|
||||
"pceid": "..."
|
||||
}
|
||||
```
|
||||
|
||||
**Example Request**
|
||||
|
||||
```bash
|
||||
curl -X POST -d '{"platformManifest": "...", "pceid": "..."}' -H "Content-Type: application/json" -H "Ocp-Apim-Subscription-Key: {subscription_key}" "[https://api.trustedservices.intel.com/sgx/certification/v3/pckcerts](https://api.trustedservices.intel.com/sgx/certification/v3/pckcerts)"
|
||||
```
|
||||
|
||||
**Response**: Response description can be
|
||||
found [here](https://www.google.com/search?q=%23response-get-and-post-2)[cite: 59].
|
||||
|
||||
#### POST `https://api.trustedservices.intel.com/sgx/certification/v3/pckcerts/config`
|
||||
|
||||
Retrieves certificates for a specific CPUSVN using Platform Manifest (multi-package only)[cite: 59].
|
||||
|
||||
**Request**
|
||||
|
||||
| Name | Type | Request Type | Required | Pattern | Description |
|
||||
|:--------------------------|:-------|:-------------|:---------|:-----------------------------|:--------------------------------------------|
|
||||
| Ocp-Apim-Subscription-Key | String | Header | True | | Subscription key[cite: 61]. |
|
||||
| Content-Type | String | Header | True | `application/json` | Content Type[cite: 61]. |
|
||||
| platformManifest | String | Body Field | True | `[0-9a-fA-F]{16882,112884}$` | Base16-encoded Platform Manifest[cite: 61]. |
|
||||
| cpusvn | String | Body Field | True | `[0-9a-fA-F]{32}$` | Base16-encoded CPUSVN[cite: 61]. |
|
||||
| pceid | String | Body Field | True | `[0-9a-fA-F]{4}$` | Base16-encoded PCE-ID[cite: 61]. |
|
||||
|
||||
**Body**
|
||||
|
||||
```json
|
||||
{
|
||||
"platformManifest": "...",
|
||||
"cpusvn": "...",
|
||||
"pceid": "..."
|
||||
}
|
||||
```
|
||||
|
||||
**Example Request**
|
||||
|
||||
```bash
|
||||
curl -X POST -d '{"platformManifest": "...", "cpusvn": "...", "pceid": "..."}' -H "Content-Type: application/json" -H "Ocp-Apim-Subscription-Key: {subscription_key}" "[https://api.trustedservices.intel.com/sgx/certification/v3/pckcerts/config](https://api.trustedservices.intel.com/sgx/certification/v3/pckcerts/config)"
|
||||
```
|
||||
|
||||
**Response (GET and POST)**
|
||||
|
||||
**Model**: PckCerts (JSON) - Array of data structures with `tcb`, `tcm`, and `certificate`[cite: 62].
|
||||
|
||||
**PckCerts Structure**
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"tcb": {
|
||||
"sgxtcbcomp01svn": 0,
|
||||
// Integer
|
||||
"sgxtcbcomp02svn": 0,
|
||||
// Integer
|
||||
// ... (03 to 16)
|
||||
"pcesvn": 0
|
||||
// Integer
|
||||
},
|
||||
"tcm": "...",
|
||||
// String, Hex-encoded TCBm [cite: 63, 64]
|
||||
"cert": "..."
|
||||
// String, PEM-encoded certificate or "Not available" [cite: 64]
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
**Example Response**
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"tcb": {
|
||||
"sgxtcbcomp01svn": 0,
|
||||
"sgxtcbcomp02svn": 0,
|
||||
"sgxtcbcomp03svn": 0,
|
||||
"sgxtcbcomp04svn": 0,
|
||||
"sgxtcbcomp05svn": 0,
|
||||
"sgxtcbcomp06svn": 0,
|
||||
"sgxtcbcomp07svn": 0,
|
||||
"sgxtcbcomp08svn": 0,
|
||||
"sgxtcbcomp09svn": 0,
|
||||
"sgxtcbcomp10svn": 0,
|
||||
"sgxtcbcomp11svn": 0,
|
||||
"sgxtcbcomp12svn": 0,
|
||||
"sgxtcbcomp13svn": 0,
|
||||
"sgxtcbcomp14svn": 0,
|
||||
"sgxtcbcomp15svn": 0,
|
||||
"sgxtcbcomp16svn": 0,
|
||||
"pcesvn": 0
|
||||
},
|
||||
"tcm": "...",
|
||||
"cert": "-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
**Status Codes**
|
||||
|
||||
| Code | Model | Headers | Description |
|
||||
|:-----|:---------|:----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:-----------------------------------------------------------------------------------------------------------------------|
|
||||
| 200 | PckCerts | Content-Type: `application/json`[cite: 65]. \<br\> Request-ID[cite: 65]. \<br\> SGX-PCK-Certificate-Issuer-Chain: Issuer chain[cite: 66]. \<br\> SGX-FMSPC[cite: 66]. \<br\> SGX-PCK-Certificate-CA-Type[cite: 66]. \<br\> Warning[cite: 66]. | Operation successful[cite: 65]. |
|
||||
| 400 | | Request-ID[cite: 67]. \<br\> Warning[cite: 67]. | Invalid request parameters[cite: 67]. |
|
||||
| 401 | | Request-ID[cite: 68]. \<br\> Warning[cite: 68]. | Failed to authenticate or authorize the request[cite: 68]. |
|
||||
| 404 | | Request-ID[cite: 69]. \<br\> Warning[cite: 69]. | PCK Certificate not found[cite: 69]. Reasons: PPID/PCE-ID not supported or Platform Manifest not registered[cite: 70]. |
|
||||
| 500 | | Request-ID[cite: 70]. \<br\> Warning[cite: 70]. | Internal server error occurred[cite: 70]. |
|
||||
| 503 | | Request-ID[cite: 70]. \<br\> Warning[cite: 70]. | Server is currently unable to process the request[cite: 70]. |
|
||||
|
||||
-----
|
||||
|
||||
### Get Revocation List V3
|
||||
|
||||
Retrieves the X.509 Certificate Revocation List (CRL) for revoked SGX PCK Certificates[cite: 71]. CRLs are issued by
|
||||
Intel SGX Processor CA or Platform CA[cite: 71].
|
||||
|
||||
#### GET `https://api.trustedservices.intel.com/sgx/certification/v3/pckcrl`
|
||||
|
||||
**Request**
|
||||
|
||||
| Name | Type | Request Type | Required | Pattern | Description |
|
||||
|:---------|:-------|:-------------|:---------|:------------|:------------|
|
||||
| ca | String | Query | True | `(processor | platform)` | CA that issued the CRL[cite: 71]. |
|
||||
| encoding | String | Query | False | `(pem | der)` | Encoding (Default: PEM)[cite: 71]. |
|
||||
|
||||
**Example Request**
|
||||
|
||||
```bash
|
||||
curl -X GET "[https://api.trustedservices.intel.com/sgx/certification/v3/pckcrl?ca=platform&encoding=der](https://api.trustedservices.intel.com/sgx/certification/v3/pckcrl?ca=platform&encoding=der)"
|
||||
```
|
||||
|
||||
**Response**
|
||||
|
||||
**Model**: PckCrl (X-PEM-FILE or PKIX-CRL) - PEM or DER-encoded CRL[cite: 71].
|
||||
|
||||
**Example Response**
|
||||
|
||||
```
|
||||
-----BEGIN X509 CRL-----
|
||||
...
|
||||
-----END X509 CRL-----
|
||||
```
|
||||
|
||||
**Status Codes**
|
||||
|
||||
| Code | Model | Headers | Description |
|
||||
|:-----|:-------|:--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:-------------------------------------------------|
|
||||
| 200 | PckCrl | Content-Type: `application/x-pem-file` (PEM) or `application/pkix-crl` (DER)[cite: 72]. \<br\> Request-ID[cite: 72]. \<br\> SGX-PCK-CRL-Issuer-Chain: Issuer chain[cite: 72]. \<br\> Warning[cite: 72]. | Operation successful[cite: 72]. |
|
||||
| 400 | | Request-ID[cite: 72]. \<br\> Warning[cite: 73]. | Invalid request parameters[cite: 72]. |
|
||||
| 401 | | Request-ID[cite: 73]. \<br\> Warning[cite: 73]. | Failed to authenticate or authorize[cite: 73]. |
|
||||
| 500 | | Request-ID[cite: 73]. \<br\> Warning[cite: 73]. | Internal server error occurred[cite: 73]. |
|
||||
| 503 | | Request-ID[cite: 73]. \<br\> Warning[cite: 73]. | Server is currently unable to process[cite: 73]. |
|
||||
|
||||
-----
|
||||
|
||||
### Get TCB Info V3
|
||||
|
||||
Retrieves SGX TCB information for a given FMSPC[cite: 74].
|
||||
|
||||
**Algorithm for TCB Status:**
|
||||
|
||||
1. Retrieve FMSPC from the SGX PCK Certificate[cite: 74].
|
||||
2. Retrieve TCB Info matching the FMSPC[cite: 75].
|
||||
3. Iterate through the sorted TCB Levels[cite: 75]:
|
||||
* Compare all SGX TCB Comp SVNs (01-16) from the certificate with TCB Level values[cite: 76]. If all are \>=,
|
||||
proceed[cite: 76]. Otherwise, move to the next item[cite: 76].
|
||||
* Compare PCESVN from the certificate with the TCB Level value[cite: 77]. If \>=, read the status[cite: 77].
|
||||
Otherwise, move to the next item[cite: 78].
|
||||
4. If no match is found, the TCB Level is not supported[cite: 78].
|
||||
|
||||
#### GET `https://api.trustedservices.intel.com/sgx/certification/v3/tcb`
|
||||
|
||||
**Request**
|
||||
|
||||
| Name | Type | Request Type | Required | Pattern | Description |
|
||||
|:------------------------|:-------|:-------------|:---------|:-------------------|:---------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| fmspc | String | Query | True | `[0-9a-fA-F]{12}$` | Base16-encoded FMSPC (6 bytes)[cite: 81]. |
|
||||
| update | String | Query | False | `(early | standard)` | Update type (Default: standard). 'early' provides early access, 'standard' provides standard access[cite: 81]. Cannot be used with `tcbEvaluationDataNumber`[cite: 81]. |
|
||||
| tcbEvaluationDataNumber | Number | Query | False | `\d+$` | Specifies a TCB Evaluation Data Number. Allows fetching specific versions; returns 410 if \< M, 404 if \> N[cite: 81]. Cannot be used with `update`[cite: 81]. |
|
||||
|
||||
**Example Requests**
|
||||
|
||||
```bash
|
||||
curl -X GET "[https://api.trustedservices.intel.com/sgx/certification/v3/tcb?fmspc=...&update=early](https://api.trustedservices.intel.com/sgx/certification/v3/tcb?fmspc=...&update=early)"
|
||||
curl -X GET "[https://api.trustedservices.intel.com/sgx/certification/v3/tcb?fmspc=...&tcbEvaluationDataNumber=](https://api.trustedservices.intel.com/sgx/certification/v3/tcb?fmspc=...&tcbEvaluationDataNumber=)..."
|
||||
```
|
||||
|
||||
**Response**
|
||||
|
||||
**Model**: TcbInfoV2 (JSON) - SGX TCB Info[cite: 82].
|
||||
|
||||
**TcbInfoV2 Structure**
|
||||
|
||||
* `version`: Integer[cite: 83].
|
||||
* `issueDate`: String (date-time, ISO 8601 UTC)[cite: 84].
|
||||
* `nextUpdate`: String (date-time, ISO 8601 UTC)[cite: 85].
|
||||
* `fmspc`: String (Base16-encoded FMSPC)[cite: 85].
|
||||
* `pceId`: String (Base16-encoded PCE-ID)[cite: 85].
|
||||
* `tcbType`: Integer[cite: 85].
|
||||
* `tcbEvaluationDataNumber`: Integer, monotonically increasing sequence number for TCB evaluation data set
|
||||
updates[cite: 86]. Synchronized across TCB Info and Identities[cite: 86]. Helps determine which data supersedes
|
||||
another[cite: 87].
|
||||
* `tcbLevels`: Array of TCB level objects[cite: 87].
|
||||
* `tcb`: Object with `sgxtcbcompXXsvn` (Integer) and `pcesvn` (Integer)[cite: 87].
|
||||
* `tcbDate`: String (date-time, ISO 8601 UTC)[cite: 89]. If advisories exist after this date with enforced
|
||||
mitigations, status won't be `UpToDate`[cite: 88].
|
||||
* `tcbStatus`: String (`UpToDate`, `HardeningNeeded`, `ConfigurationNeeded`, `ConfigurationAndHardeningNeeded`,
|
||||
`OutOfDate`, `OutOfDateConfigurationNeeded`, `Revoked`)[cite: 90, 91, 92].
|
||||
* `advisoryIDs`: Array of strings (e.g., `INTEL-SA-XXXXX`, `INTEL-DOC-XXXXX`)[cite: 93, 94].
|
||||
* `signature`: String (Base16 encoded)[cite: 94].
|
||||
|
||||
**Example Response**
|
||||
|
||||
```json
|
||||
{
|
||||
"tcbInfo": {
|
||||
"version": 2,
|
||||
"issueDate": "2018-07-30T12:00:00Z",
|
||||
"nextUpdate": "2018-08-30T12:00:00Z",
|
||||
"fmspc": "...",
|
||||
"pceId": "0000",
|
||||
"tcbType": 1,
|
||||
"tcbEvaluationDataNumber": 7,
|
||||
"tcbLevels": [
|
||||
{
|
||||
"tcb": {
|
||||
"sgxtcbcomp01svn": 0,
|
||||
/* ... */
|
||||
"pcesvn": 0
|
||||
},
|
||||
"tcbDate": "2018-07-11T12:00:00Z",
|
||||
"tcbStatus": "UpToDate",
|
||||
"advisoryIDs": [
|
||||
"INTEL-SA-00070",
|
||||
"INTEL-SA-00076"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"signature": "..."
|
||||
}
|
||||
```
|
||||
|
||||
**Status Codes**
|
||||
|
||||
| Code | Model | Headers | Description |
|
||||
|:-----|:----------|:-------------------------------------------------------------------------------------------------------------------------------------------------------------|:-----------------------------------------------------------------------------------------|
|
||||
| 200 | TcbInfoV2 | Content-Type: `application/json`[cite: 96]. \<br\> Request-ID[cite: 96]. \<br\> SGX-TCB-Info-Issuer-Chain: Issuer chain[cite: 96]. \<br\> Warning[cite: 96]. | Operation successful[cite: 96]. |
|
||||
| 400 | | Request-ID[cite: 96]. \<br\> Warning[cite: 96]. | Invalid request (bad FMSPC or conflicting `update`/`tcbEvaluationDataNumber`)[cite: 96]. |
|
||||
| 401 | | Request-ID[cite: 96]. \<br\> Warning[cite: 96]. | Failed to authenticate or authorize[cite: 96]. |
|
||||
| 404 | | Request-ID[cite: 96]. \<br\> Warning[cite: 96]. | TCB info not found for FMSPC or `tcbEvaluationDataNumber`[cite: 96]. |
|
||||
| 410 | | Request-ID[cite: 98]. \<br\> Warning[cite: 98]. | TCB Information for `tcbEvaluationDataNumber` no longer available[cite: 98]. |
|
||||
| 500 | | Request-ID[cite: 98]. \<br\> Warning[cite: 98]. | Internal server error[cite: 98]. |
|
||||
| 503 | | Request-ID[cite: 98]. \<br\> Warning[cite: 98]. | Server unable to process[cite: 98]. |
|
||||
|
||||
-----
|
||||
|
||||
### Get Quoting Enclave Identity V3
|
||||
|
||||
Verifies if an SGX Enclave Report matches a valid Quoting Enclave (QE) identity[cite: 99].
|
||||
|
||||
**Algorithm:**
|
||||
|
||||
1. Retrieve and validate QE Identity[cite: 99].
|
||||
2. Compare SGX Enclave Report against QE Identity:
|
||||
* Verify `MRSIGNER` equals `mrsigner`[cite: 100].
|
||||
* Verify `ISVPRODID` equals `isvprodid`[cite: 101].
|
||||
* Verify `(miscselectMask & MISCSELECT)` equals `miscselect`[cite: 102].
|
||||
* Verify `(attributesMask & ATTRIBUTES)` equals `attributes`[cite: 103, 104].
|
||||
3. If any check fails, identity doesn't match[cite: 105].
|
||||
4. Determine TCB status:
|
||||
* Retrieve TCB Levels[cite: 106].
|
||||
* Find TCB Level with ISVSVN \<= Enclave Report ISVSVN (descending)[cite: 107].
|
||||
* Read `tcbStatus`; if not found, it's unsupported[cite: 108].
|
||||
|
||||
#### GET `https://api.trustedservices.intel.com/sgx/certification/v3/qe/identity`
|
||||
|
||||
**Request**
|
||||
|
||||
| Name | Type | Type | Required | Pattern | Description |
|
||||
|:------------------------|:-------|:------|:---------|:--------|:------------------------------------------------------------------------------------------|
|
||||
| update | String | Query | False | `(early | standard)` | Update type (Default: standard)[cite: 110]. Cannot be used with `tcbEvaluationDataNumber`[cite: 110]. |
|
||||
| tcbEvaluationDataNumber | Number | Query | False | `\d+` | Specifies TCB Evaluation Data Number[cite: 110]. Cannot be used with `update`[cite: 110]. |
|
||||
|
||||
**Example Requests**
|
||||
|
||||
```bash
|
||||
curl -X GET "[https://api.trustedservices.intel.com/sgx/certification/v3/qe/identity?update=early](https://api.trustedservices.intel.com/sgx/certification/v3/qe/identity?update=early)"
|
||||
curl -X GET "[https://api.trustedservices.intel.com/sgx/certification/v3/qe/identity?tcbEvaluationDataNumber=](https://api.trustedservices.intel.com/sgx/certification/v3/qe/identity?tcbEvaluationDataNumber=)..."
|
||||
```
|
||||
|
||||
**Response**
|
||||
|
||||
**Model**: QEIdentityV2 (JSON) - QE Identity data[cite: 111].
|
||||
|
||||
**QEIdentityV2 Structure**
|
||||
|
||||
* `enclaveIdentity`:
|
||||
* `id`: String (`QE`, `QVE`, or `QAE`)[cite: 113].
|
||||
* `version`: Integer[cite: 113].
|
||||
* `issueDate`, `nextUpdate`: String (date-time, ISO 8601 UTC)[cite: 114].
|
||||
* `tcbEvaluationDataNumber`: Integer[cite: 115].
|
||||
* `miscselect`, `miscselectMask`: String (Base16-encoded)[cite: 115, 116].
|
||||
* `attributes`, `attributesMask`: String (Base16-encoded)[cite: 116].
|
||||
* `mrsigner`: String (Base16-encoded)[cite: 116].
|
||||
* `isvprodid`: Integer[cite: 116].
|
||||
* `tcbLevels`: Array of TCB level objects[cite: 116].
|
||||
* `tcb`: Object with `isvsvn` (Integer)[cite: 117].
|
||||
* `tcbDate`: String (date-time, ISO 8601 UTC)[cite: 117].
|
||||
* `tcbStatus`: String (`UpToDate`, `OutOfDate`, `Revoked`)[cite: 119].
|
||||
* `advisoryIDs`: Array of strings[cite: 119].
|
||||
* `signature`: String (Hex-encoded)[cite: 119].
|
||||
|
||||
**Status Codes**
|
||||
|
||||
| Code | Model | Headers | Description |
|
||||
|:-----|:-------------|:-------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:------------------------------------------------------------------------------------------------|
|
||||
| 200 | QEIdentityV2 | Content-Type: `application/json`[cite: 122]. \<br\> Request-ID[cite: 122]. \<br\> SGX-Enclave-Identity-Issuer-Chain: Issuer chain[cite: 122]. \<br\> Warning[cite: 122]. | Operation successful[cite: 122]. |
|
||||
| 400 | | Request-ID[cite: 122]. \<br\> Warning[cite: 123]. | Invalid request (bad params or conflicting `update`/`tcbEvaluationDataNumber`)[cite: 122, 124]. |
|
||||
| 401 | | Request-ID[cite: 123]. \<br\> Warning[cite: 123]. | Failed to authenticate or authorize[cite: 123]. |
|
||||
| 404 | | Request-ID[cite: 123]. \<br\> Warning[cite: 123]. | QE identity not found for `tcbEvaluationDataNumber`[cite: 124]. |
|
||||
| 410 | | Request-ID[cite: 124]. \<br\> Warning[cite: 124]. | QEIdentity for `tcbEvaluationDataNumber` no longer available[cite: 124]. |
|
||||
| 500 | | Request-ID[cite: 125]. \<br\> Warning[cite: 125]. | Internal server error[cite: 125]. |
|
||||
| 503 | | Request-ID[cite: 125]. \<br\> Warning[cite: 125]. | Server unable to process[cite: 125]. |
|
||||
|
||||
-----
|
||||
|
||||
### Get Quote Verification Enclave Identity V3
|
||||
|
||||
Verifies if an SGX Enclave Report matches a valid QVE identity[cite: 126].
|
||||
|
||||
**Algorithm:**
|
||||
|
||||
1. Retrieve and validate QVE Identity[cite: 126].
|
||||
2. Compare Enclave Report: `MRSIGNER`[cite: 127], `ISVPRODID`[cite: 128], `MISCSELECT` (with mask)[cite: 128],
|
||||
`ATTRIBUTES` (with mask)[cite: 128].
|
||||
3. If any fails, no match[cite: 129].
|
||||
4. Determine TCB status via ISVSVN comparison[cite: 129, 130].
|
||||
|
||||
#### GET `https://api.trustedservices.intel.com/sgx/certification/v3/qve/identity`
|
||||
|
||||
**Request**: Same parameters as `Get Quoting Enclave Identity V3` (`update` and `tcbEvaluationDataNumber`)[cite: 132].
|
||||
|
||||
**Response**: QVEIdentityV2 (JSON) - QVE Identity data[cite: 133]. Structure similar to QE
|
||||
Identity[cite: 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144].
|
||||
|
||||
**Status Codes**: Similar to `Get Quoting Enclave Identity V3`[cite: 145].
|
||||
|
||||
-----
|
||||
|
||||
### Get Quote Appraisal Enclave Identity V3
|
||||
|
||||
Verifies if an SGX Enclave Report matches a valid QAE identity[cite: 149].
|
||||
|
||||
**Algorithm:**
|
||||
|
||||
1. Retrieve and validate QAE Identity[cite: 149].
|
||||
2. Compare Enclave Report: `MRSIGNER`[cite: 151], `ISVPRODID`[cite: 151], `MISCSELECT` (with mask)[cite: 152, 153],
|
||||
`ATTRIBUTES` (with mask)[cite: 154, 155].
|
||||
3. If any fails, no match[cite: 155].
|
||||
4. Determine TCB status via ISVSVN comparison[cite: 157, 158].
|
||||
|
||||
#### GET `https://api.trustedservices.intel.com/sgx/certification/v3/qae/identity`
|
||||
|
||||
**Request**: Same parameters as `Get Quoting Enclave Identity V3` (`update` and `tcbEvaluationDataNumber`)[cite: 160].
|
||||
|
||||
**Response**: QAEIdentityV2 (JSON) - QAE Identity data[cite: 161]. Structure similar to QE
|
||||
Identity[cite: 162, 163, 164, 165, 166, 167, 168, 169, 170].
|
||||
|
||||
**Status Codes**: Similar to `Get Quoting Enclave Identity V3`[cite: 171, 174].
|
||||
|
||||
-----
|
||||
|
||||
### PCK Certificate and CRL Specification
|
||||
|
||||
This document specifies the hierarchy and format of X.509 v3 certificates and v2 CRLs for Provisioning Certification
|
||||
Keys[cite: 175].
|
||||
|
||||
Enforcement of a mitigation means the attestation process can detect its presence and the result will differ[cite: 175].
|
||||
Intel offers `standard` (default) and `early` update parameters, affecting when enforcement occurs[cite: 176]. The
|
||||
attestation result is an objective assessment[cite: 177]. Relying parties can use additional factors [cite: 178] and may
|
||||
choose to trust an 'OutOfDate' platform, accepting risks[cite: 180]. Intel will strive to communicate schedule
|
||||
deviations[cite: 181].
|
||||
|
|
@ -1,664 +0,0 @@
|
|||
This document outlines the API for Intel® SGX and Intel® TDX services, focusing on platform registration and
|
||||
provisioning certification using ECDSA attestation.
|
||||
|
||||
## Intel® SGX and Intel® TDX Registration Service for Scalable Platforms [cite: 1]
|
||||
|
||||
The Intel® SGX and Intel® TDX Registration Service API enables the registration of Intel® SGX platforms with multiple
|
||||
processor packages as a unified platform instance[cite: 2]. This allows these platforms to be remotely attested as a
|
||||
single entity[cite: 2]. It is important to note that the service enforces a minimum TLS protocol version of 1.2; any
|
||||
attempts to connect with older TLS/SSL versions will be rejected[cite: 3].
|
||||
|
||||
### Register Platform
|
||||
|
||||
This API facilitates the registration of multi-package SGX platforms, encompassing both initial registration and TCB (
|
||||
Trusted Computing Base) recovery[cite: 4]. During this process, the Registration Service authenticates the platform
|
||||
manifest to confirm it originates from a genuine, non-revoked SGX platform[cite: 4]. If the platform configuration
|
||||
passes verification, its provisioning root keys are securely stored[cite: 4]. These stored keys are subsequently used to
|
||||
derive the public components of Provisioning Certification Keys (PCKs), which are then distributed as X.509 certificates
|
||||
by the Provisioning Certification Service[cite: 5]. These PCK certificates are integral to the remote attestation
|
||||
process for the platform[cite: 5].
|
||||
|
||||
**POST** `https://api.trustedservices.intel.com/sgx/registration/v1/platform`
|
||||
|
||||
**Request**
|
||||
|
||||
* **Headers**: In addition to standard HTTP headers (like `Content-Length`), the following is required[cite: 1]:
|
||||
|
||||
| Name | Required | Value | Description |
|
||||
|:-------------|:---------|:-------------------------|:----------------------------------------|
|
||||
| Content-Type | True | application/octet-stream | MIME type of the request body[cite: 1]. |
|
||||
|
||||
* **Body**: The request body must be a binary representation of the Platform Manifest structure[cite: 6]. This is an
|
||||
opaque blob containing the registration manifest for a multi-package platform[cite: 6]. It includes the platform
|
||||
provisioning root keys established by the platform instance and the necessary data to authenticate it as a genuine,
|
||||
non-revoked SGX platform[cite: 6].
|
||||
|
||||
* **Example Request**:
|
||||
```bash
|
||||
curl -v -X POST "Content-Type: application/octet-stream" --data-binary @platform_manifest.bin "https://api.trustedservices.intel.com/sgx/registration/v1/platform" [cite: 1]
|
||||
```
|
||||
|
||||
**Response**
|
||||
|
||||
* **Model**: The response body will contain the hex-encoded representation of the PPID (Platform Provisioning ID) for
|
||||
the registered platform instance, but only if the HTTP Status Code is 201[cite: 1]. Otherwise, the body will be
|
||||
empty[cite: 1].
|
||||
|
||||
* **Example Response**:
|
||||
```
|
||||
00112233445566778899AABBCCDDEEFF [cite: 1]
|
||||
```
|
||||
|
||||
* **Status Codes**:
|
||||
|
||||
| Code | Headers | Body | Description |
|
||||
|:-----|:--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:-----------------|:--------------------------------------------------------------------------------------------------|
|
||||
| 201 | `Request-ID`: Randomly generated identifier for troubleshooting[cite: 7]. | Hex-encoded PPID | Operation successful; a new platform instance has been registered[cite: 7]. |
|
||||
| 400 | `Request-ID`: Randomly generated identifier[cite: 8]. `Error-Code` & `Error-Message`: Details on the error (e.g., `InvalidRequestSyntax`, `InvalidRegistrationServer`, `InvalidOrRevokedPackage`, `PackageNotFound`, `IncompatiblePackage`, `InvalidPlatformManifest`, `CachedKeysPolicyViolation`)[cite: 8]. | | Invalid Platform Manifest[cite: 10]. The client should not retry without modifications[cite: 10]. |
|
||||
| 415 | `Request-ID`: Randomly generated identifier[cite: 8]. | | The MIME type specified in the request is not supported[cite: 8]. |
|
||||
| 500 | `Request-ID`: Randomly generated identifier[cite: 8]. | | An internal server error occurred[cite: 8]. |
|
||||
| 503 | `Request-ID`: Randomly generated identifier[cite: 8]. | | The server is currently unable to process the request; try again later[cite: 8]. |
|
||||
|
||||
### Add Package
|
||||
|
||||
This API allows for adding new processor packages to an already registered platform instance[cite: 11]. Upon successful
|
||||
execution, a Platform Membership Certificate is generated for each processor package included in the Add
|
||||
Request[cite: 11]. This requires a subscription for registration[cite: 11].
|
||||
|
||||
**POST** `https://api.trustedservices.intel.com/sgx/registration/v1/package`
|
||||
|
||||
**Request**
|
||||
|
||||
* **Headers**: Besides standard headers like `Content-Length`[cite: 12], the following are needed:
|
||||
|
||||
| Name | Required | Value | Description |
|
||||
|:--------------------------|:---------|:-------------------------|:------------------------------------------------------------------|
|
||||
| Ocp-Apim-Subscription-Key | True | *Your Subscription Key* | Subscription key for API access, found in your profile[cite: 12]. |
|
||||
| Content-Type | True | application/octet-stream | MIME type of the request body[cite: 12]. |
|
||||
|
||||
* **Body**: A binary representation of the Add Request structure, an opaque blob for adding new packages to an existing
|
||||
platform[cite: 13].
|
||||
|
||||
* **Example Request**:
|
||||
```bash
|
||||
curl -v -X POST "Content-Type: application/octet-stream" --data-binary @add_package_request.bin "https://api.trustedservices.intel.com/sgx/registration/v1/package" -H "Ocp-Apim-Subscription-Key: {subscription_key}" [cite: 14]
|
||||
```
|
||||
|
||||
**Response**
|
||||
|
||||
* **Model**: For a 200 HTTP Status Code, the response is a fixed-size array (8 elements) containing binary Platform
|
||||
Membership Certificate structures appended together[cite: 14]. Certificates fill the array sequentially, starting from
|
||||
index 0, with remaining elements zeroed out[cite: 14].
|
||||
|
||||
* **Example Response (hex-encoded)**:
|
||||
```
|
||||
E8BDBECFEF9040184488777267355084...00000000 [cite: 15]
|
||||
```
|
||||
|
||||
* **Status Codes**:
|
||||
|
||||
| Code | Headers | Body | Description |
|
||||
|:-----|:-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:------------------------------------------------------------------------------------------|:-------------------------------------------------------------------------------------|
|
||||
| 200 | `Content-Type`: application/octet-stream[cite: 18]. `Request-ID`: Randomly generated identifier[cite: 18]. `Certificate-Count`: Number of certificates returned[cite: 18]. | Fixed-size array (8 elements) with binary Platform Membership Certificates[cite: 18, 19]. | Operation successful; packages added to the platform[cite: 18]. |
|
||||
| 400 | `Request-ID`: Randomly generated identifier[cite: 18]. `Error-Code` & `Error-Message`: Details on the error (e.g., `InvalidRequestSyntax`, `PlatformNotFound`, `InvalidOrRevokedPackage`, `PackageNotFound`[cite: 17], `InvalidAddRequest`)[cite: 18]. | | Invalid Add Request Payload[cite: 20]. Do not retry without modifications[cite: 20]. |
|
||||
| 401 | `Request-ID`: Randomly generated identifier[cite: 18]. | | Failed to authenticate or authorize the request[cite: 18]. |
|
||||
| 415 | `Request-ID`: Randomly generated identifier[cite: 18]. | | The MIME type specified is not supported[cite: 18]. |
|
||||
| 500 | `Request-ID`: Randomly generated identifier[cite: 18]. | | Internal server error occurred[cite: 18]. |
|
||||
| 503 | `Request-ID`: Randomly generated identifier[cite: 18]. | | Server is currently unable to process the request[cite: 18]. |
|
||||
|
||||
## Intel® SGX and Intel® TDX Provisioning Certification Service for ECDSA Attestation [cite: 21]
|
||||
|
||||
This service provides PCK certificates. You can download the Provisioning Certification Root CA Certificate (v4) in both
|
||||
DER and PEM formats[cite: 21].
|
||||
|
||||
### Get/Post PCK Certificate V4
|
||||
|
||||
This API allows requesting a single PCK certificate. It offers two primary methods:
|
||||
|
||||
1. **Using PPID and SVNs**:
|
||||
* **Single-socket platforms**: No prerequisites[cite: 22].
|
||||
* **Multi-socket platforms**: Requires prior platform registration via the Register Platform API[cite: 22]. This
|
||||
flow necessitates that platform root keys are persistently stored in the backend[cite: 23], and the Keys Caching
|
||||
Policy must be `true`[cite: 23].
|
||||
2. **Using Platform Manifest and SVNs**:
|
||||
* **Multi-socket platforms**: Does *not* require prior registration[cite: 24]. Platform root keys are *not* required
|
||||
to be persistently stored[cite: 24]. The Keys Caching Policy determines whether keys are stored or not[cite: 25].
|
||||
* **Direct Registration (via Register Platform API)**: Keys are always stored; `CachedKeys` flag in PCK
|
||||
certificates is `true`[cite: 26, 27].
|
||||
* **Indirect Registration (via Get PCK Certificate(s) API)**: Keys are never stored; `CachedKeys` flag is
|
||||
`false`[cite: 28, 30]. Register Platform API cannot be used afterward[cite: 29].
|
||||
|
||||
**Note**: The PCS returns the PCK Certificate representing the highest TCB security level based on the CPUSVN and PCE
|
||||
ISVSVN inputs[cite: 31].
|
||||
|
||||
**GET** `https://api.trustedservices.intel.com/sgx/certification/v4/pckcert`
|
||||
|
||||
* **Request**:
|
||||
|
||||
| Name | Type | Request Type | Required | Pattern | Description |
|
||||
|:--------------------------|:-------|:-------------|:---------|:-------------------|:--------------------------------------------------------------|
|
||||
| Ocp-Apim-Subscription-Key | String | Header | False | | Subscription key[cite: 32]. |
|
||||
| PPID-Encryption-Key | String | Header | False | | Key type for PPID encryption (default: "RSA-3072")[cite: 32]. |
|
||||
| encrypted_ppid | String | Query | True | `[0-9a-fA-F]{768}` | Base16-encoded encrypted PPID[cite: 32]. |
|
||||
| cpusvn | String | Query | True | `[0-9a-fA-F]{32}` | Base16-encoded CPUSVN[cite: 32]. |
|
||||
| pcesvn | String | Query | True | `[0-9a-fA-F]{4}` | Base16-encoded PCESVN (little endian)[cite: 32]. |
|
||||
| pceid | String | Query | True | `[0-9a-fA-F]{4}` | Base16-encoded PCE-ID (little endian)[cite: 32]. |
|
||||
|
||||
* **Example Request**:
|
||||
```bash
|
||||
curl -v -X GET "https://api.trustedservices.intel.com/sgx/certification/v4/pckcert?encrypted_ppid={encrypted_ppid}&cpusvn={cpusvn}&pcesvn={pcesvn}&pceid={pceid}" -H "Ocp-Apim-Subscription-Key: {subscription_key}" [cite: 33]
|
||||
```
|
||||
|
||||
**POST** `https://api.trustedservices.intel.com/sgx/certification/v4/pckcert`
|
||||
|
||||
* **Request**:
|
||||
|
||||
| Name | Type | Request Type | Required | Pattern | Description |
|
||||
|:--------------------------|:-------|:-------------|:---------|:----------------------------|:-------------------------------------------------|
|
||||
| Ocp-Apim-Subscription-Key | String | Header | False | | Subscription key[cite: 33]. |
|
||||
| Content-Type | String | Header | True | `application/json` | Content type[cite: 35]. |
|
||||
| platformManifest | String | Body Field | True | `[0-9a-fA-F]{16862,112884}` | Base16-encoded Platform Manifest[cite: 35]. |
|
||||
| cpusvn | String | Body Field | True | `[0-9a-fA-F]{32}` | Base16-encoded CPUSVN[cite: 35]. |
|
||||
| pcesvn | String | Body Field | True | `[0-9a-fA-F]{4}` | Base16-encoded PCESVN (little endian)[cite: 35]. |
|
||||
| pceid | String | Body Field | True | `[0-9a-fA-F]{4}` | Base16-encoded PCE-ID (little endian)[cite: 35]. |
|
||||
|
||||
* **Body**:
|
||||
```json
|
||||
{
|
||||
"platformManifest": "...", [cite: 36]
|
||||
"cpusvn": "...", [cite: 36]
|
||||
"pcesvn": "...", [cite: 36]
|
||||
"pceid": "..." [cite: 36]
|
||||
}
|
||||
```
|
||||
|
||||
* **Example Request**:
|
||||
```bash
|
||||
curl -v -X POST --data '{"platformManifest":"...","cpusvn":"...","pcesvn":"...","pceid":"..."}' "https://api.trustedservices.intel.com/sgx/certification/v4/pckcert" -H "Ocp-Apim-Subscription-Key: {subscription_key}" -H "Content-Type: application/json" [cite: 36]
|
||||
```
|
||||
|
||||
**Response (Both GET & POST)**
|
||||
|
||||
* **Model**: `PckCert (X-PEM-FILE)` - PEM-encoded SGX PCK Certificate[cite: 36].
|
||||
* **Example Response**:
|
||||
```pem
|
||||
-----BEGIN CERTIFICATE-----
|
||||
...
|
||||
-----END CERTIFICATE----- [cite: 36]
|
||||
```
|
||||
* **Status Codes**:
|
||||
|
||||
| Code | Model | Headers | Description |
|
||||
|:-----|:--------|:---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:----------------------------------------------------------------------------------------------------------------------------------|
|
||||
| 200 | PckCert | `Content-Type`: application/x-pem-file[cite: 37]. `Request-ID`: Identifier[cite: 37]. `SGX-PCK-Certificate-Issuer-Chain`: PEM-encoded Issuer Chain[cite: 37]. `SGX-TCBm`: Hex-encoded CPUSVN & PCESVN[cite: 37]. `SGX-FMSPC`: Hex-encoded FMSPC[cite: 37]. `SGX-PCK-Certificate-CA-Type`: "processor" or "platform"[cite: 37]. `Warning` (Optional)[cite: 37]. | Operation successful[cite: 37]. |
|
||||
| 400 | | `Request-ID`: Identifier[cite: 37]. `Warning` (Optional)[cite: 37]. `Error-Code` & `Error-Message` (e.g., `InvalidRequestSyntax`, `InvalidRegistrationServer`, `InvalidOrRevokedPackage`, `PackageNotFound`, `IncompatiblePackage`, `InvalidPlatformManifest`)[cite: 37]. | Invalid request parameters[cite: 37]. |
|
||||
| 401 | | `Request-ID`: Identifier[cite: 37]. `Warning` (Optional)[cite: 37]. | Failed to authenticate or authorize[cite: 37]. |
|
||||
| 404 | | `Request-ID`: Identifier[cite: 37]. `Warning` (Optional)[cite: 37]. | PCK Certificate not found (e.g., unsupported PPID/PCE-ID, TCB below minimum, Platform Manifest not registered/updated)[cite: 37]. |
|
||||
| 429 | | `Retry-After`: Wait time in seconds[cite: 37]. `Warning` (Optional)[cite: 37]. | Too many requests[cite: 37]. |
|
||||
| 500 | | `Request-ID`: Identifier[cite: 37]. `Warning` (Optional)[cite: 37]. | Internal server error[cite: 37]. |
|
||||
| 503 | | `Request-ID`: Identifier[cite: 39]. `Warning` (Optional)[cite: 39]. | Server is currently unable to process[cite: 39]. |
|
||||
|
||||
### Get PCK Certificates V4
|
||||
|
||||
This API retrieves PCK certificates for *all* configured TCB levels for a platform. The usage conditions (single-socket
|
||||
vs. multi-socket, PPID vs. Platform Manifest, key caching) are similar to the single PCK certificate
|
||||
API[cite: 40, 41, 42, 43, 44, 45, 46, 47, 48].
|
||||
|
||||
**GET** `https://api.trustedservices.intel.com/sgx/certification/v4/pckcerts` (Using PPID & PCE-ID)
|
||||
|
||||
* **Request**:
|
||||
|
||||
| Name | Type | Request Type | Required | Pattern | Description |
|
||||
|:--------------------------|:-------|:-------------|:---------|:-------------------|:------------------------------------------|
|
||||
| Ocp-Apim-Subscription-Key | String | Header | False | | Subscription key[cite: 49]. |
|
||||
| PPID-Encryption-Key | String | Header | False | | Key type (default: "RSA-3072")[cite: 49]. |
|
||||
| encrypted_ppid | String | Query | True | `[0-9a-fA-F]{768}` | Encrypted PPID[cite: 49]. |
|
||||
| pceid | String | Query | True | `[0-9a-fA-F]{4}` | PCE-ID[cite: 49]. |
|
||||
|
||||
* **Example Request**:
|
||||
```bash
|
||||
curl -v -X GET "https://api.trustedservices.intel.com/sgx/certification/v4/pckcerts?encrypted_ppid={...}&pceid={...}" -H "Ocp-Apim-Subscription-Key: {subscription_key}" [cite: 50]
|
||||
```
|
||||
|
||||
**GET** `https://api.trustedservices.intel.com/sgx/certification/v4/pckcerts/config` (Using PPID, PCE-ID &
|
||||
CPUSVN) [cite: 51]
|
||||
|
||||
* **Request**:
|
||||
|
||||
| Name | Type | Request Type | Required | Pattern | Description |
|
||||
|:--------------------------|:-------|:-------------|:---------|:-------------------|:------------------------------------------|
|
||||
| Ocp-Apim-Subscription-Key | String | Header | False | | Subscription key[cite: 52]. |
|
||||
| PPID-Encryption-Key | String | Header | False | | Key type (default: "RSA-3072")[cite: 52]. |
|
||||
| encrypted_ppid | String | Query | True | `[0-9a-fA-F]{768}` | Encrypted PPID[cite: 52]. |
|
||||
| pceid | String | Query | True | `[0-9a-fA-F]{4}` | PCE-ID[cite: 52]. |
|
||||
| cpusvn | String | Query | True | `[0-9a-fA-F]{32}` | CPUSVN[cite: 52]. |
|
||||
|
||||
* **Example Request**:
|
||||
```bash
|
||||
curl -v -X GET "https://api.trustedservices.intel.com/sgx/certification/v4/pckcerts/config?encrypted_ppid={...}&pceid={...}&cpusvn={...}" -H "Ocp-Apim-Subscription-Key: {subscription_key}" [cite: 53]
|
||||
```
|
||||
|
||||
**POST** `https://api.trustedservices.intel.com/sgx/certification/v4/pckcerts` (Using Platform Manifest & PCE-ID)
|
||||
|
||||
* **Request**:
|
||||
|
||||
| Name | Type | Request Type | Required | Pattern | Description |
|
||||
|:--------------------------|:-------|:-------------|:---------|:----------------------------|:-----------------------------|
|
||||
| Ocp-Apim-Subscription-Key | String | Header | False | | Subscription key[cite: 54]. |
|
||||
| Content-Type | String | Header | True | `application/json` | Content type[cite: 54]. |
|
||||
| platformManifest | String | Body Field | True | `[0-9a-fA-F]{16862,112884}` | Platform Manifest[cite: 54]. |
|
||||
| pceid | String | Body Field | True | `[0-9a-fA-F]{4}` | PCE-ID[cite: 54]. |
|
||||
|
||||
* **Body**:
|
||||
```json
|
||||
{
|
||||
"platformManifest": "...", [cite: 55]
|
||||
"pceid": "..." [cite: 55]
|
||||
}
|
||||
```
|
||||
* **Example Request**:
|
||||
```bash
|
||||
curl -v -X POST --data '{"platformManifest":"...","pceid":"..."}' "https://api.trustedservices.intel.com/sgx/certification/v4/pckcerts" -H "Ocp-Apim-Subscription-Key: {subscription_key}" -H "Content-Type: application/json" [cite: 55]
|
||||
```
|
||||
|
||||
**POST** `https://api.trustedservices.intel.com/sgx/certification/v4/pckcerts/config` (Using Platform Manifest, PCE-ID &
|
||||
CPUSVN)
|
||||
|
||||
* **Request**:
|
||||
|
||||
| Name | Type | Request Type | Required | Pattern | Description |
|
||||
|:--------------------------|:-------|:-------------|:---------|:----------------------------|:-----------------------------|
|
||||
| Ocp-Apim-Subscription-Key | String | Header | False | | Subscription key[cite: 56]. |
|
||||
| Content-Type | String | Header | True | `application/json` | Content type[cite: 57]. |
|
||||
| platformManifest | String | Body Field | True | `[0-9a-fA-F]{16862,112884}` | Platform Manifest[cite: 56]. |
|
||||
| cpusvn | String | Body Field | True | `[0-9a-fA-F]{32}` | CPUSVN[cite: 56]. |
|
||||
| pceid | String | Body Field | True | `[0-9a-fA-F]{4}` | PCE-ID[cite: 56]. |
|
||||
|
||||
* **Body**:
|
||||
```json
|
||||
{
|
||||
"platformManifest": "...", [cite: 57]
|
||||
"cpusvn": "...", [cite: 57]
|
||||
"pceid": "..." [cite: 57]
|
||||
}
|
||||
```
|
||||
* **Example Request**:
|
||||
```bash
|
||||
curl -v -X POST --data '{"platformManifest":"...","cpusvn":"...","pceid":"..."}' "https://api.trustedservices.intel.com/sgx/certification/v4/pckcerts/config" -H "Ocp-Apim-Subscription-Key: {subscription_key}" -H "Content-Type: application/json" [cite: 57]
|
||||
```
|
||||
|
||||
**Response (All GET & POST for multiple certs)**
|
||||
|
||||
* **Model**: `PckCerts` (JSONArray of objects, each containing `tcb`, `tcbm`, and `cert`)[cite: 56].
|
||||
* `tcb`: Object with 16 `sgxtcbcompXXsvn` fields (integer 0-255) and `pcesvn` (integer 0-65535)[cite: 59].
|
||||
* `tcbm`: Hex-encoded string of CPUSVN (16 bytes) and PCESVN (2 bytes)[cite: 7].
|
||||
* `cert`: URL-encoded PEM PCK Certificate, or "Not available" string[cite: 60].
|
||||
* **Example Response**:
|
||||
```json
|
||||
[
|
||||
{
|
||||
"tcb": {
|
||||
"sgxtcbcomp01svn": 3,
|
||||
"sgxtcbcomp02svn": 1,
|
||||
...
|
||||
"pcesvn": 11
|
||||
},
|
||||
"tcbm": "...",
|
||||
"cert": "-----BEGIN%20CERTIFICATE-----%0A...%0A-----END%20CERTIFICATE-----" [cite: 61]
|
||||
},
|
||||
...
|
||||
]
|
||||
```
|
||||
* **Status Codes**:
|
||||
|
||||
| Code | Model | Headers | Description |
|
||||
|:-----|:---------|:--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:-------------------------------------------------------------------------------------------------------|
|
||||
| 200 | PckCerts | `Content-Type`: application/json[cite: 8]. `Request-ID`: Identifier[cite: 8]. `SGX-PCK-Certificate-Issuer-Chain`: Issuer Chain[cite: 62]. `SGX-FMSPC`: FMSPC[cite: 8]. `SGX-PCK-Certificate-CA-Type`: "processor" or "platform"[cite: 63]. `Warning` (Optional)[cite: 8]. | Operation successful[cite: 8]. |
|
||||
| 400 | | `Request-ID`: Identifier[cite: 8]. `Warning` (Optional)[cite: 8]. `Error-Code` & `Error-Message` (e.g., `InvalidRequestSyntax`[cite: 65], `InvalidRegistrationServer`[cite: 65], `InvalidOrRevokedPackage`[cite: 65], `PackageNotFound`[cite: 65], `IncompatiblePackage`[cite: 65], `InvalidPlatformManifest` [cite: 66]) | Invalid request parameters[cite: 8]. |
|
||||
| 401 | | `Request-ID`: Identifier[cite: 68]. `Warning` (Optional)[cite: 68]. | Failed to authenticate or authorize[cite: 68]. |
|
||||
| 404 | | `Request-ID`: Identifier[cite: 68]. `Warning` (Optional)[cite: 68]. | PCK Certificate not found (e.g., unsupported PPID/PCE-ID, Platform Manifest not registered)[cite: 68]. |
|
||||
| 429 | | `Retry-After`: Wait time[cite: 68]. `Warning` (Optional)[cite: 68]. | Too many requests[cite: 68]. |
|
||||
| 500 | | `Request-ID`: Identifier[cite: 68]. `Warning` (Optional)[cite: 68]. | Internal server error[cite: 68]. |
|
||||
| 503 | | `Request-ID`: Identifier[cite: 68]. `Warning` (Optional)[cite: 68]. | Server is currently unable to process[cite: 68]. |
|
||||
|
||||
### Get Revocation List V4
|
||||
|
||||
This API retrieves the X.509 Certificate Revocation List (CRL) for revoked SGX PCK Certificates, issued by either the
|
||||
Intel SGX Processor CA or Platform CA[cite: 69, 70].
|
||||
|
||||
**GET** `https://api.trustedservices.intel.com/sgx/certification/v4/pckcrl` [cite: 71]
|
||||
|
||||
* **Request**:
|
||||
|
||||
| Name | Type | Request Type | Required | Pattern | Description |
|
||||
|:---------|:-------|:-------------|:---------|:------------|:------------|
|
||||
| ca | String | Query | True | `(processor | platform)` | CA identifier ("processor" or "platform")[cite: 71, 72]. |
|
||||
| encoding | String | Query | False | `(pem | der)` | CRL encoding (default: PEM)[cite: 71]. |
|
||||
|
||||
* **Example Request**:
|
||||
```bash
|
||||
curl -v -X GET "https://api.trustedservices.intel.com/sgx/certification/v4/pckcrl?ca=platform&encoding=pem" [cite: 71]
|
||||
```
|
||||
|
||||
**Response**
|
||||
|
||||
* **Model**: `PckCrl` (X-PEM-FILE or PKIX-CRL) - PEM or DER encoded CRL[cite: 71].
|
||||
* **Example Response**:
|
||||
```pem
|
||||
-----BEGIN X509 CRL-----
|
||||
...
|
||||
-----END X509 CRL----- [cite: 71]
|
||||
```
|
||||
* **Status Codes**:
|
||||
|
||||
| Code | Model | Headers | Description |
|
||||
|:-----|:-------|:------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:-------------------------------------------------|
|
||||
| 200 | PckCrl | `Content-Type`: "application/x-pem-file" or "application/pkix-crl"[cite: 71]. `Request-ID`: Identifier[cite: 71]. `SGX-PCK-CRL-Issuer-Chain`: Issuer Chain[cite: 73]. `Warning` (Optional)[cite: 71]. | Operation successful[cite: 71]. |
|
||||
| 400 | | `Request-ID`: Identifier[cite: 71]. `Warning` (Optional)[cite: 71]. | Invalid request parameters[cite: 71]. |
|
||||
| 401 | | `Request-ID`: Identifier[cite: 74]. `Warning` (Optional)[cite: 74]. | Failed to authenticate or authorize[cite: 74]. |
|
||||
| 500 | | `Request-ID`: Identifier[cite: 74]. `Warning` (Optional)[cite: 74]. | Internal server error[cite: 74]. |
|
||||
| 503 | | `Request-ID`: Identifier[cite: 74]. `Warning` (Optional)[cite: 74]. | Server is currently unable to process[cite: 74]. |
|
||||
|
||||
### Get SGX TCB Info V4
|
||||
|
||||
This API retrieves SGX TCB information for a specific FMSPC, which is crucial for determining the TCB status of a
|
||||
platform[cite: 75]. The process involves:
|
||||
|
||||
1. Retrieving the FMSPC from the SGX PCK Certificate[cite: 75].
|
||||
2. Fetching the corresponding SGX TCB info[cite: 76].
|
||||
3. Iterating through the TCB Levels:
|
||||
* Comparing all 16 SGX TCB Comp SVNs from the certificate against the TCB Level; they must be >=[cite: 77, 78].
|
||||
* Comparing the PCESVN from the certificate against the TCB Level; it must be >=[cite: 79, 80]. If both match, the
|
||||
TCB level's status is found[cite: 80].
|
||||
4. If no match is found, the TCB level is unsupported[cite: 82].
|
||||
|
||||
**GET** `https://api.trustedservices.intel.com/sgx/certification/v4/tcb` [cite: 82]
|
||||
|
||||
* **Request**:
|
||||
|
||||
| Name | Type | Request Type | Required | Pattern | Description |
|
||||
|:------------------------|:-------|:-------------|:---------|:------------------|:-------------------------------------------------------------------------------------------------------|
|
||||
| fmspc | String | Query | True | `[0-9a-fA-F]{12}` | Base16-encoded FMSPC[cite: 83]. |
|
||||
| update | String | Query | False | `(early | standard)` | TCB Info update type (default: standard). `early` provides access sooner than `standard`[cite: 83]. Cannot be used with `tcbEvaluationDataNumber`[cite: 83]. |
|
||||
| tcbEvaluationDataNumber | Number | Query | False | `\d+` | Retrieves TCB info for a specific evaluation number[cite: 83]. Cannot be used with `update`[cite: 83]. |
|
||||
|
||||
* **Example Requests**:
|
||||
```bash
|
||||
curl -v -X GET "https://api.trustedservices.intel.com/sgx/certification/v4/tcb?fmspc={fmspc_value}&update=early" [cite: 84]
|
||||
curl -v -X GET "https://api.trustedservices.intel.com/sgx/certification/v4/tcb?fmspc={fmspc_value}&tcbEvaluationDataNumber={number}" [cite: 84]
|
||||
```
|
||||
|
||||
**Response**
|
||||
|
||||
* **Model**: `Appendix A: TCB info V3`[cite: 86]. (See Appendix A below).
|
||||
* **Example Response**: (JSON structure as shown in the document)[cite: 85].
|
||||
* **Status Codes**:
|
||||
|
||||
| Code | Model | Headers | Description |
|
||||
|:-----|:----------|:-----------------------------------------------------------------------------------------------------------------------------------------------------------------|:----------------------------------------------------------------------------------------------------------------|
|
||||
| 200 | TcbInfoV3 | `Content-Type`: application/json[cite: 86]. `Request-ID`: Identifier[cite: 86]. `TCB-Info-Issuer-Chain`: Issuer Chain[cite: 86]. `Warning` (Optional)[cite: 86]. | Operation successful[cite: 86]. |
|
||||
| 400 | | `Request-ID`: Identifier[cite: 86]. `Warning` (Optional)[cite: 86]. | Invalid request (bad `fmspc`, invalid params, or `update` & `tcbEvaluationDataNumber` used together)[cite: 86]. |
|
||||
| 401 | | `Request-ID`: Identifier[cite: 86]. `Warning` (Optional)[cite: 87]. | Failed to authenticate or authorize[cite: 86]. |
|
||||
| 404 | | `Request-ID`: Identifier[cite: 86]. `Warning` (Optional)[cite: 87]. | TCB info not found for the given `fmspc` or `tcbEvaluationDataNumber`[cite: 86]. |
|
||||
| 410 | | `Request-ID`: Identifier[cite: 88]. `Warning` (Optional)[cite: 88]. | TCB info for the provided `tcbEvaluationDataNumber` is no longer available[cite: 88]. |
|
||||
| 500 | | `Request-ID`: Identifier[cite: 88]. `Warning` (Optional)[cite: 88]. | Internal server error[cite: 88]. |
|
||||
| 503 | | `Request-ID`: Identifier[cite: 88]. `Warning` (Optional)[cite: 88]. | Server currently unable to process[cite: 88]. |
|
||||
|
||||
### Get TDX TCB Info V4
|
||||
|
||||
This API retrieves TDX TCB information[cite: 89]. The TCB status determination follows a similar process to SGX but
|
||||
includes additional steps for TDX TEE TCB SVNs and TDX Module
|
||||
Identity[cite: 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102].
|
||||
|
||||
**GET** `https://api.trustedservices.intel.com/tdx/certification/v4/tcb` [cite: 102]
|
||||
|
||||
* **Request**:
|
||||
|
||||
| Name | Type | Request Type | Required | Pattern | Description |
|
||||
|:------------------------|:-------|:-------------|:---------|:------------------|:---------------------------------------------------------------------------------------------------------|
|
||||
| fmspc | String | Query | True | `[0-9a-fA-F]{12}` | Base16-encoded FMSPC[cite: 103]. |
|
||||
| update | String | Query | False | `(early | standard)` | TCB Info update type (default: standard)[cite: 103]. Cannot be used with `tcbEvaluationDataNumber`[cite: 103]. |
|
||||
| tcbEvaluationDataNumber | Number | Query | False | `\d+` | Retrieves TCB info for a specific evaluation number[cite: 103]. Cannot be used with `update`[cite: 103]. |
|
||||
|
||||
* **Example Requests**:
|
||||
```bash
|
||||
curl -v -X GET "https://api.trustedservices.intel.com/tdx/certification/v4/tcb?fmspc={fmspc_value}&update=early" [cite: 104]
|
||||
curl -v -X GET "https://api.trustedservices.intel.com/tdx/certification/v4/tcb?fmspc={fmspc_value}&tcbEvaluationDataNumber={number}" [cite: 104]
|
||||
```
|
||||
|
||||
**Response**
|
||||
|
||||
* **Model**: `Appendix A: TCB info V3`[cite: 107]. (See Appendix A below).
|
||||
* **Example Response**: (JSON structure including `tdxModule` and `tdxtcbcomponents` as shown in the
|
||||
document)[cite: 105, 106].
|
||||
* **Status Codes**: Similar to Get SGX TCB Info V4[cite: 108].
|
||||
|
||||
### Enclave Identity V4
|
||||
|
||||
This set of APIs allows for determining if an SGX Enclave's identity matches Intel's published identity[cite: 109]. The
|
||||
process involves:
|
||||
|
||||
1. Retrieving the Enclave Identity (SGX QE, TDX QE, QVE, or QAE)[cite: 109].
|
||||
2. Comparing `MRSIGNER` and `ISVPRODID` fields[cite: 109].
|
||||
3. Applying `miscselectMask` and `attributesMask` and comparing the results[cite: 111, 112, 113, 114].
|
||||
4. If checks pass, determining the TCB status by finding the highest TCB Level (sorted by ISVSVN) whose ISVSVN is <= the
|
||||
Enclave Report's ISVSVN[cite: 116, 117].
|
||||
|
||||
**GET** `https://api.trustedservices.intel.com/sgx/certification/v4/qe/identity` [cite: 118]
|
||||
**GET** `https://api.trustedservices.intel.com/tdx/certification/v4/qe/identity` [cite: 128]
|
||||
**GET** `https://api.trustedservices.intel.com/sgx/certification/v4/qve/identity` [cite: 133]
|
||||
**GET** `https://api.trustedservices.intel.com/sgx/certification/v4/qae/identity` [cite: 138]
|
||||
|
||||
* **Request**:
|
||||
|
||||
| Name | Type | Request Type | Required | Pattern | Description |
|
||||
|:------------------------|:-------|:-------------|:---------|:--------|:--------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| update | String | Query | False | `(early | standard)` | Identity update type (default: standard)[cite: 118, 127, 132, 137]. Cannot be used with `tcbEvaluationDataNumber`[cite: 118, 121, 127, 132, 137]. |
|
||||
| tcbEvaluationDataNumber | Number | Query | False | `\d+` | Retrieves Identity for a specific evaluation number[cite: 119, 120, 127, 132, 137]. Cannot be used with `update`[cite: 121, 127, 132, 137]. |
|
||||
|
||||
* **Example Requests** (SGX QE shown):
|
||||
```bash
|
||||
curl -v -X GET "https://api.trustedservices.intel.com/sgx/certification/v4/qe/identity?update=early" [cite: 122]
|
||||
curl -v -X GET "https://api.trustedservices.intel.com/sgx/certification/v4/qe/identity?tcbEvaluationDataNumber={number}" [cite: 122]
|
||||
```
|
||||
|
||||
**Response**
|
||||
|
||||
* **Model**: `Appendix B: Enclave Identity V2`[cite: 122, 128, 134, 139]. (See Appendix B below).
|
||||
* **Example Response**: (JSON structure as shown in the document for QE[cite: 125], TDX QE[cite: 131], QVE[cite: 136],
|
||||
and QAE [cite: 141]).
|
||||
* **Status Codes** (SGX QE shown, others are similar):
|
||||
|
||||
| Code | Model | Headers | Description |
|
||||
|:-----|:------------|:---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:--------------------------------------------------------------------------------------|
|
||||
| 200 | EIdentityV2 | `Content-Type`: application/json[cite: 122]. `Request-ID`: Identifier[cite: 122]. `SGX-Enclave-Identity-Issuer-Chain`: Issuer Chain[cite: 122]. `Warning` (Optional)[cite: 122]. | Operation successful[cite: 122]. |
|
||||
| 400 | | `Request-ID`: Identifier[cite: 122]. `Warning` (Optional)[cite: 122]. | Invalid request (params or `update` & `tcbEvaluationDataNumber` conflict)[cite: 122]. |
|
||||
| 401 | | `Request-ID`: Identifier[cite: 123]. `Warning` (Optional)[cite: 123]. | Failed to authenticate or authorize[cite: 122]. |
|
||||
| 404 | | `Request-ID`: Identifier[cite: 123]. `Warning` (Optional)[cite: 123]. | Identity info not found[cite: 122]. |
|
||||
| 410 | | `Request-ID`: Identifier[cite: 124]. `Warning` (Optional)[cite: 124]. | Identity info no longer available[cite: 124]. |
|
||||
| 500 | | `Request-ID`: Identifier[cite: 124]. `Warning` (Optional)[cite: 124]. | Internal server error[cite: 124]. |
|
||||
| 503 | | `Request-ID`: Identifier[cite: 124]. `Warning` (Optional)[cite: 124]. | Server currently unable to process[cite: 124]. |
|
||||
|
||||
### Retrieve FMSPCs V4
|
||||
|
||||
Retrieves a list of FMSPC values for SGX and TDX platforms that support DCAP attestation[cite: 141].
|
||||
|
||||
**GET** `https://api.trustedservices.intel.com/sgx/certification/v4/fmspcs` [cite: 141]
|
||||
|
||||
* **Request**:
|
||||
|
||||
| Name | Type | Request Type | Required | Description |
|
||||
|:---------|:-------|:-------------|:---------|:----------------------------------------------------------------------------|
|
||||
| platform | String | Query | False | Optional platform filter: `all` (default), `client`, `E3`, `E5`[cite: 141]. |
|
||||
|
||||
* **Example Request**:
|
||||
```bash
|
||||
curl -v -X GET "https://api.trustedservices.intel.com/sgx/certification/v4/fmspcs?platform=E5" [cite: 141]
|
||||
```
|
||||
|
||||
**Response**
|
||||
|
||||
* **Example Response**:
|
||||
```json
|
||||
[
|
||||
{"platform": "E3", "fmspc": "123456789000"}, [cite: 142]
|
||||
{"platform": "E5", "fmspc": "987654321000"}, [cite: 142]
|
||||
{"platform": "client", "fmspc": "ABCDEF123456"} [cite: 142]
|
||||
]
|
||||
```
|
||||
* **Status Codes**:
|
||||
|
||||
| Code | Headers | Description |
|
||||
|:-----|:-------------------------------------------------------------------------------------------------------------------|:-----------------------------------------------|
|
||||
| 200 | `Content-Type`: application/json[cite: 142]. `Request-ID`: Identifier[cite: 142]. `Warning` (Optional)[cite: 142]. | Operation successful[cite: 142]. |
|
||||
| 400 | `Request-ID`: Identifier[cite: 142]. `Warning` (Optional)[cite: 143]. | Invalid request parameters[cite: 142]. |
|
||||
| 500 | `Request-ID`: Identifier[cite: 142]. `Warning` (Optional)[cite: 142]. | Internal server error[cite: 142]. |
|
||||
| 503 | `Request-ID`: Identifier[cite: 142]. `Warning` (Optional)[cite: 142]. | Server currently unable to process[cite: 142]. |
|
||||
|
||||
### Retrieve TCB Evaluation Data Numbers V4
|
||||
|
||||
Retrieves the list of currently supported TCB Evaluation Data Numbers and their associated TCB-R event
|
||||
states[cite: 142].
|
||||
|
||||
**GET** `https://api.trustedservices.intel.com/{sgx|tdx}/certification/v4/tcbevaluationdatanumbers` [cite: 142]
|
||||
|
||||
* **Example Requests**:
|
||||
```bash
|
||||
curl -v -X GET "https://api.trustedservices.intel.com/sgx/certification/v4/tcbevaluationdatanumbers" [cite: 142]
|
||||
curl -v -X GET "https://api.trustedservices.intel.com/tdx/certification/v4/tcbevaluationdatanumbers" [cite: 142]
|
||||
```
|
||||
|
||||
**Response**
|
||||
|
||||
* **Model**: `Appendix C: TCB Evaluation Data Numbers V1`[cite: 144]. (See Appendix C below).
|
||||
* **Example Response**:
|
||||
```json
|
||||
{
|
||||
"tcbEvaluationDataNumbers": {
|
||||
"version": 1,
|
||||
"issueDate": "2023-04-13T09:38:17Z",
|
||||
"nextUpdate": "2023-05-13T09:38:17Z",
|
||||
"tcbNumbers": [
|
||||
{"tcbEvaluationDataNumber": 12, "tcbRecoveryEventDate": "2023-04-13T00:00:00Z", "tcbDate": "2023-04-13T00:00:00Z"},
|
||||
{"tcbEvaluationDataNumber": 11, "tcbRecoveryEventDate": "2023-01-14T00:00:00Z", "tcbDate": "2023-01-14T00:00:00Z"}
|
||||
],
|
||||
"signature": "..." [cite: 142]
|
||||
}
|
||||
}
|
||||
```
|
||||
* **Status Codes**:
|
||||
|
||||
| Code | Headers | Description |
|
||||
|:-----|:----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:-----------------------------------------------|
|
||||
| 200 | `Content-Type`: application/json[cite: 144]. `Request-ID`: Identifier[cite: 144]. `TCB-Evaluation-Data-Numbers-Issuer-Chain`: Issuer Chain[cite: 145]. `Warning` (Optional)[cite: 144]. | Operation successful[cite: 144]. |
|
||||
| 500 | `Request-ID`: Identifier[cite: 144]. `Warning` (Optional)[cite: 144]. | Internal server error[cite: 144]. |
|
||||
| 503 | `Request-ID`: Identifier[cite: 144]. `Warning` (Optional)[cite: 144]. | Server currently unable to process[cite: 146]. |
|
||||
|
||||
---
|
||||
|
||||
## Appendix A: TCB Info V3 [cite: 147]
|
||||
|
||||
This defines the structure of the TCB Info V3 JSON response[cite: 147].
|
||||
|
||||
* `tcbInfo`: (Object)
|
||||
* `id`: (String) Identifier (e.g., "SGX", "TDX")[cite: 148].
|
||||
* `version`: (Integer) Structure version[cite: 148].
|
||||
* `issueDate`: (String - datetime) Creation timestamp (ISO 8601 UTC)[cite: 148].
|
||||
* `nextUpdate`: (String - datetime) Next update timestamp (ISO 8601 UTC)[cite: 149].
|
||||
* `fmspc`: (String) Base16-encoded FMSPC[cite: 149].
|
||||
* `pceId`: (String) Base16-encoded PCE ID[cite: 149].
|
||||
* `tcbType`: (Integer) TCB level composition type[cite: 149].
|
||||
* `tcbEvaluationDataNumber`: (Integer) Monotonically increasing sequence number, synchronized across TCB Info and
|
||||
Enclave Identities, indicating updates[cite: 150, 151, 152].
|
||||
* `tdxModule`: (Object - Optional, only for TDX TCB Info)[cite: 153].
|
||||
* `mrsigner`: (String) Base16-encoded TDX SEAM module's signer measurement[cite: 154].
|
||||
* `attributes`: (String) Base16-encoded "golden" attributes[cite: 154].
|
||||
* `attributesMask`: (String) Base16-encoded attributes mask[cite: 154].
|
||||
* `tdxModuleIdentities`: (Array - Optional, for multiple TDX SEAM Modules)[cite: 154].
|
||||
* `id`: (String) Module identifier[cite: 154].
|
||||
* `mrsigner`: (String) Base16-encoded signer measurement[cite: 155].
|
||||
* `attributes`: (String) Base16-encoded "golden" attributes[cite: 155].
|
||||
* `attributesMask`: (String) Base16-encoded attributes mask[cite: 156].
|
||||
* `tcbLevels`: (Array) Sorted list of TCB levels for this module[cite: 157].
|
||||
* `tcb`: (Object)
|
||||
* `isvsvn`: (Integer) ISV SVN[cite: 157].
|
||||
* `tcbDate`: (String - datetime) TCB date (ISO 8601 UTC)[cite: 158].
|
||||
* `tcbStatus`: (String) "UpToDate", "OutOfDate", or "Revoked"[cite: 158].
|
||||
* `advisoryIDs`: (Array - Optional) List of relevant `INTEL-SA-XXXXX` or `INTEL-DOC-XXXXX`
|
||||
identifiers[cite: 159, 160].
|
||||
* `tcbLevels`: (Array) Sorted list of TCB levels for the FMSPC[cite: 160].
|
||||
* `tcb`: (Object)
|
||||
* `sgxtcbcomponents`: (Array - Optional) 16 SGX TCB Components (SVN, Category, Type)[cite: 161].
|
||||
* `tdxtcbcomponents`: (Array - Optional, only for TDX TCB Info) 16 TDX TCB Components (SVN, Category,
|
||||
Type)[cite: 161, 162, 164].
|
||||
* `pcesvn`: (Integer) PCE SVN[cite: 161].
|
||||
* `tcbDate`: (String - datetime) TCB date (ISO 8601 UTC)[cite: 165].
|
||||
* `tcbStatus`: (String) "UpToDate", "HardeningNeeded", "ConfigurationNeeded", "
|
||||
ConfigurationAndHardeningNeeded", "OutOfDate", "OutOfDateConfigurationNeeded", "Revoked"[cite: 165, 166].
|
||||
* `advisoryIDs`: (Array - Optional) List of relevant `INTEL-SA-XXXXX` or `INTEL-DOC-XXXXX`
|
||||
identifiers[cite: 167, 168].
|
||||
* `signature`: (String) Base16-encoded signature over the `tcbInfo` body[cite: 163].
|
||||
|
||||
---
|
||||
|
||||
## Appendix B: Enclave Identity V2 [cite: 168]
|
||||
|
||||
This defines the structure of the Enclave Identity V2 JSON response[cite: 168].
|
||||
|
||||
* `enclaveIdentity`: (Object)
|
||||
* `id`: (String) Identifier ("QE", "QVE", "QAE", "TD_QE")[cite: 169].
|
||||
* `version`: (Integer) Structure version[cite: 169].
|
||||
* `issueDate`: (String - datetime) Creation timestamp (ISO 8601 UTC)[cite: 170].
|
||||
* `nextUpdate`: (String - datetime) Next update timestamp (ISO 8601 UTC)[cite: 170].
|
||||
* `tcbEvaluationDataNumber`: (Integer) Monotonically increasing sequence number, synchronized across TCB Info and
|
||||
Enclave Identities[cite: 171, 172].
|
||||
* `miscselect`: (String) Base16-encoded "golden" miscselect value[cite: 172].
|
||||
* `miscselectMask`: (String) Base16-encoded miscselect mask[cite: 172].
|
||||
* `attributes`: (String) Base16-encoded "golden" attributes value[cite: 172].
|
||||
* `attributesMask`: (String) Base16-encoded attributes mask[cite: 173].
|
||||
* `mrsigner`: (String) Base16-encoded mrsigner hash[cite: 173].
|
||||
* `isvprodid`: (Integer) Enclave Product ID[cite: 173].
|
||||
* `tcbLevels`: (Array) Sorted list of Enclave TCB levels[cite: 173].
|
||||
* `tcb`: (Object)
|
||||
* `isvsvn`: (Integer) Enclave's ISV SVN[cite: 173].
|
||||
* `tcbDate`: (String - datetime) TCB date (ISO 8601 UTC)[cite: 174].
|
||||
* `tcbStatus`: (String) "UpToDate", "OutOfDate", or "Revoked"[cite: 174, 176].
|
||||
* `advisoryIDs`: (Array - Optional) List of relevant `INTEL-SA-XXXXX` or `INTEL-DOC-XXXXX`
|
||||
identifiers[cite: 177].
|
||||
* `signature`: (String) Base16-encoded signature over the `enclaveIdentity` body[cite: 175].
|
||||
|
||||
---
|
||||
|
||||
## Appendix C: TCB Evaluation Data Numbers V1 [cite: 177]
|
||||
|
||||
This defines the structure of the TCB Evaluation Data Numbers V1 JSON response[cite: 177].
|
||||
|
||||
* `tcbEvaluationDataNumbers`: (Object)
|
||||
* `id`: (String) Identifier ("SGX" or "TDX")[cite: 178].
|
||||
* `version`: (Integer) Structure version[cite: 178].
|
||||
* `issueDate`: (String - datetime) Creation timestamp (ISO 8601 UTC)[cite: 178].
|
||||
* `nextUpdate`: (String - datetime) Suggested next call timestamp (ISO 8601 UTC)[cite: 179].
|
||||
* `tcbNumbers`: (Array) List of TCB Evaluation Data Number objects[cite: 179].
|
||||
* `tcbEvaluationDataNumber`: (Integer) The number itself[cite: 179].
|
||||
* `tcbRecoveryEventDate`: (String - datetime) The date Intel first publishes related collateral (ISO 8601
|
||||
UTC)[cite: 179].
|
||||
* `tcbDate`: (String - datetime) TCB date (ISO 8601 UTC)[cite: 180, 181].
|
||||
* `signature`: (String) Base16-encoded signature over the structure's body[cite: 181].
|
||||
|
||||
---
|
||||
|
||||
## Appendix D: PCK Certificate and CRL Specification
|
||||
|
||||
This section refers to an external document that specifies the hierarchy and format of X.509 v3 certificates and X.509
|
||||
v2 CRLs issued by Intel for Provisioning Certification Keys[cite: 181].
|
||||
|
||||
---
|
||||
|
||||
**Notes on TCB Status and Enforcement:**
|
||||
|
||||
* **Enforcement Grace Periods**: Intel provides "early" and "standard" update parameters, offering different enforcement
|
||||
grace periods[cite: 182]. The attestation result depends on which parameter is used[cite: 182].
|
||||
* **Relying Party Trust Decisions**: Relying parties can use additional factors beyond the attestation result to make
|
||||
trust decisions[cite: 183]. They might accept risks even if a platform is technically "OutOfDate" due to low-severity
|
||||
issues[cite: 184].
|
||||
* **Communication**: Intel aims to communicate planned deviations via email to registered API subscribers[cite: 185].
|
|
@ -1,229 +0,0 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (c) 2025 Matter Labs
|
||||
|
||||
//! Enclave Identity
|
||||
|
||||
use super::ApiClient; // Import from parent module
|
||||
use crate::{
|
||||
error::IntelApiError,
|
||||
responses::EnclaveIdentityResponse,
|
||||
types::{ApiVersion, UpdateType},
|
||||
};
|
||||
|
||||
impl ApiClient {
|
||||
/// Retrieves the SGX QE Identity from the Intel API.
|
||||
///
|
||||
/// Returns Enclave Identity JSON string (Appendix B) and Issuer Chain header.
|
||||
/// Supports both v3 and v4. The `update` and `tcb_evaluation_data_number`
|
||||
/// parameters are only valid in API v4. Returns the enclave identity JSON
|
||||
/// and an issuer chain header.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `update` - Optional [`UpdateType`] (v4 only).
|
||||
/// * `tcb_evaluation_data_number` - Optional TCB Evaluation Data Number (v4 only).
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// An [`EnclaveIdentityResponse`] containing the JSON identity and issuer chain.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an `IntelApiError` if the request fails, if conflicting v4 parameters are used,
|
||||
/// or if the desired identity resource is not found.
|
||||
pub async fn get_sgx_qe_identity(
|
||||
&self,
|
||||
update: Option<UpdateType>,
|
||||
tcb_evaluation_data_number: Option<u64>,
|
||||
) -> Result<EnclaveIdentityResponse, IntelApiError> {
|
||||
self.get_sgx_enclave_identity("qe", update, tcb_evaluation_data_number)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Retrieves the TDX QE Identity from the Intel API (API v4 only).
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `update` - Optional [`UpdateType`] (v4 only).
|
||||
/// * `tcb_evaluation_data_number` - Optional TCB Evaluation Data Number (v4 only).
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// An [`EnclaveIdentityResponse`] containing the JSON identity and issuer chain.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an `IntelApiError` if an unsupported API version is used,
|
||||
/// if conflicting parameters are provided, or if the identity resource is not found.
|
||||
/// GET /tdx/certification/v4/qe/identity - V4 ONLY
|
||||
pub async fn get_tdx_qe_identity(
|
||||
&self,
|
||||
update: Option<UpdateType>,
|
||||
tcb_evaluation_data_number: Option<u64>,
|
||||
) -> Result<EnclaveIdentityResponse, IntelApiError> {
|
||||
// Ensure V4 API
|
||||
self.ensure_v4_api("get_tdx_qe_identity")?;
|
||||
// Check conflicting parameters (only relevant for V4, checked inside helper)
|
||||
self.check_conflicting_update_params(update, tcb_evaluation_data_number)?;
|
||||
|
||||
let path = self.build_api_path("tdx", "qe", "identity")?;
|
||||
let mut url = self.base_url.join(&path)?;
|
||||
|
||||
if let Some(upd) = update {
|
||||
url.query_pairs_mut()
|
||||
.append_pair("update", &upd.to_string());
|
||||
}
|
||||
if let Some(tedn) = tcb_evaluation_data_number {
|
||||
url.query_pairs_mut()
|
||||
.append_pair("tcbEvaluationDataNumber", &tedn.to_string());
|
||||
}
|
||||
|
||||
let request_builder = self.client.get(url);
|
||||
|
||||
// Special handling for 404/410 when tcbEvaluationDataNumber is specified
|
||||
if let Some(tedn_val) = tcb_evaluation_data_number {
|
||||
// Use the helper function to check status before proceeding
|
||||
self.check_tcb_evaluation_status(&request_builder, tedn_val, "TDX QE Identity")
|
||||
.await?;
|
||||
// If the check passes (doesn't return Err), continue to fetch_json_with_issuer_chain
|
||||
}
|
||||
|
||||
// Fetch JSON and header (TDX only exists in V4)
|
||||
let (enclave_identity_json, issuer_chain) = self
|
||||
.fetch_json_with_issuer_chain(
|
||||
request_builder,
|
||||
"SGX-Enclave-Identity-Issuer-Chain",
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(EnclaveIdentityResponse {
|
||||
enclave_identity_json,
|
||||
issuer_chain,
|
||||
})
|
||||
}
|
||||
|
||||
/// Retrieves the SGX QVE Identity from the Intel API.
|
||||
///
|
||||
/// Supports API v3 and v4. The `update` and `tcb_evaluation_data_number` parameters
|
||||
/// are v4 only. Returns the QVE identity JSON and issuer chain.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `update` - Optional [`UpdateType`] (v4 only).
|
||||
/// * `tcb_evaluation_data_number` - Optional TCB Evaluation Data Number (v4 only).
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// An [`EnclaveIdentityResponse`] containing the QVE identity JSON and issuer chain.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an `IntelApiError` if the request fails, if conflicting parameters are used,
|
||||
/// or if the identity resource is not found.
|
||||
/// GET /sgx/certification/{v3,v4}/qve/identity
|
||||
pub async fn get_sgx_qve_identity(
|
||||
&self,
|
||||
update: Option<UpdateType>,
|
||||
tcb_evaluation_data_number: Option<u64>,
|
||||
) -> Result<EnclaveIdentityResponse, IntelApiError> {
|
||||
self.get_sgx_enclave_identity("qve", update, tcb_evaluation_data_number)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Retrieves the SGX QAE Identity from the Intel API (API v4 only).
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `update` - Optional [`UpdateType`] (v4 only).
|
||||
/// * `tcb_evaluation_data_number` - Optional TCB Evaluation Data Number (v4 only).
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// An [`EnclaveIdentityResponse`] containing the QAE identity JSON and issuer chain.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an `IntelApiError` if an unsupported API version is used,
|
||||
/// if conflicting parameters are provided, or if the QAE identity is not found.
|
||||
/// GET /sgx/certification/v4/qae/identity - V4 ONLY
|
||||
pub async fn get_sgx_qae_identity(
|
||||
&self,
|
||||
update: Option<UpdateType>,
|
||||
tcb_evaluation_data_number: Option<u64>,
|
||||
) -> Result<EnclaveIdentityResponse, IntelApiError> {
|
||||
// QAE endpoint requires V4
|
||||
if self.api_version != ApiVersion::V4 {
|
||||
return Err(IntelApiError::UnsupportedApiVersion(
|
||||
"QAE Identity endpoint requires API v4".to_string(),
|
||||
));
|
||||
}
|
||||
// Call the generic helper, it will handle V4 params and 404/410 checks
|
||||
self.get_sgx_enclave_identity("qae", update, tcb_evaluation_data_number)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Retrieves generic SGX enclave identity (QE, QVE, QAE) data.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `identity_path_segment` - String slice representing the identity path segment (e.g., "qe", "qve", "qae").
|
||||
/// * `update` - Optional [`UpdateType`] for API v4.
|
||||
/// * `tcb_evaluation_data_number` - Optional TCB Evaluation Data Number for API v4.
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// An [`EnclaveIdentityResponse`] containing the JSON identity data and issuer chain.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an `IntelApiError` if the request fails or the specified resource
|
||||
/// is unavailable.
|
||||
async fn get_sgx_enclave_identity(
|
||||
&self,
|
||||
identity_path_segment: &str,
|
||||
update: Option<UpdateType>,
|
||||
tcb_evaluation_data_number: Option<u64>,
|
||||
) -> Result<EnclaveIdentityResponse, IntelApiError> {
|
||||
self.check_v4_only_param(update, "update")?;
|
||||
self.check_v4_only_param(tcb_evaluation_data_number, "tcbEvaluationDataNumber")?;
|
||||
self.check_conflicting_update_params(update, tcb_evaluation_data_number)?;
|
||||
|
||||
let path = self.build_api_path("sgx", identity_path_segment, "identity")?;
|
||||
let mut url = self.base_url.join(&path)?;
|
||||
|
||||
if self.api_version == ApiVersion::V4 {
|
||||
if let Some(upd) = update {
|
||||
url.query_pairs_mut()
|
||||
.append_pair("update", &upd.to_string());
|
||||
}
|
||||
if let Some(tedn) = tcb_evaluation_data_number {
|
||||
url.query_pairs_mut()
|
||||
.append_pair("tcbEvaluationDataNumber", &tedn.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
let request_builder = self.client.get(url);
|
||||
|
||||
if self.api_version == ApiVersion::V4 {
|
||||
if let Some(tedn_val) = tcb_evaluation_data_number {
|
||||
let description = format!("SGX {} Identity", identity_path_segment.to_uppercase());
|
||||
self.check_tcb_evaluation_status(&request_builder, tedn_val, &description)
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
|
||||
let (enclave_identity_json, issuer_chain) = self
|
||||
.fetch_json_with_issuer_chain(
|
||||
request_builder,
|
||||
"SGX-Enclave-Identity-Issuer-Chain",
|
||||
Some("SGX-Enclave-Identity-Issuer-Chain"),
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(EnclaveIdentityResponse {
|
||||
enclave_identity_json,
|
||||
issuer_chain,
|
||||
})
|
||||
}
|
||||
}
|
|
@ -1,134 +0,0 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (c) 2025 Matter Labs
|
||||
|
||||
//! FMSPCs & TCB Evaluation Data Numbers
|
||||
|
||||
use super::ApiClient; // Import from parent module
|
||||
use crate::{
|
||||
error::{check_status, IntelApiError},
|
||||
responses::TcbEvaluationDataNumbersResponse,
|
||||
types::{ApiVersion, PlatformFilter},
|
||||
FmspcJsonResponse,
|
||||
};
|
||||
use reqwest::StatusCode;
|
||||
|
||||
impl ApiClient {
|
||||
/// GET /sgx/certification/{v3,v4}/fmspcs
|
||||
/// Retrieves a list of FMSPC values for SGX and TDX platforms (API v4 only).
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `platform_filter` - An optional filter specifying SGX or TDX platforms.
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// Optional 'platform' filter.
|
||||
/// A `String` containing the JSON array of objects, each containing `fmspc` and `platform`.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an `IntelApiError` if an unsupported API version is used or if the request fails.
|
||||
pub async fn get_fmspcs(
|
||||
&self,
|
||||
platform_filter: Option<PlatformFilter>,
|
||||
) -> Result<FmspcJsonResponse, IntelApiError> {
|
||||
if self.api_version == ApiVersion::V3 {
|
||||
return Err(IntelApiError::UnsupportedApiVersion(
|
||||
"API v4 only function".to_string(),
|
||||
));
|
||||
}
|
||||
let path = self.build_api_path("sgx", "", "fmspcs")?;
|
||||
let mut url = self.base_url.join(&path)?;
|
||||
|
||||
if let Some(pf) = platform_filter {
|
||||
url.query_pairs_mut()
|
||||
.append_pair("platform", &pf.to_string());
|
||||
}
|
||||
|
||||
let request_builder = self.client.get(url);
|
||||
let response = self.execute_with_retry(request_builder).await?;
|
||||
let response = check_status(response, &[StatusCode::OK]).await?;
|
||||
|
||||
let fmspcs_json = response.text().await?;
|
||||
|
||||
Ok(fmspcs_json)
|
||||
}
|
||||
|
||||
/// GET /sgx/certification/v4/tcbevaluationdatanumbers - V4 ONLY
|
||||
/// Retrieves the currently supported SGX TCB Evaluation Data Numbers (API v4 only).
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// A [`TcbEvaluationDataNumbersResponse`] containing the JSON structure of TCB Evaluation
|
||||
/// Data Numbers and an issuer chain header.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an `IntelApiError` if an unsupported API version is used or if the request fails.
|
||||
pub async fn get_sgx_tcb_evaluation_data_numbers(
|
||||
&self,
|
||||
) -> Result<TcbEvaluationDataNumbersResponse, IntelApiError> {
|
||||
// Endpoint requires V4
|
||||
if self.api_version != ApiVersion::V4 {
|
||||
return Err(IntelApiError::UnsupportedApiVersion(
|
||||
"SGX TCB Evaluation Data Numbers endpoint requires API v4".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
let path = self.build_api_path("sgx", "", "tcbevaluationdatanumbers")?;
|
||||
let url = self.base_url.join(&path)?;
|
||||
let request_builder = self.client.get(url);
|
||||
|
||||
let (tcb_evaluation_data_numbers_json, issuer_chain) = self
|
||||
.fetch_json_with_issuer_chain(
|
||||
request_builder,
|
||||
"TCB-Evaluation-Data-Numbers-Issuer-Chain",
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(TcbEvaluationDataNumbersResponse {
|
||||
tcb_evaluation_data_numbers_json,
|
||||
issuer_chain,
|
||||
})
|
||||
}
|
||||
|
||||
/// GET /tdx/certification/v4/tcbevaluationdatanumbers - V4 ONLY
|
||||
/// Retrieves the currently supported TDX TCB Evaluation Data Numbers (API v4 only).
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// A [`TcbEvaluationDataNumbersResponse`] containing the JSON structure of TCB Evaluation
|
||||
/// Data Numbers and an issuer chain header.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an `IntelApiError` if an unsupported API version is used or if the request fails.
|
||||
pub async fn get_tdx_tcb_evaluation_data_numbers(
|
||||
&self,
|
||||
) -> Result<TcbEvaluationDataNumbersResponse, IntelApiError> {
|
||||
// Endpoint requires V4
|
||||
if self.api_version != ApiVersion::V4 {
|
||||
return Err(IntelApiError::UnsupportedApiVersion(
|
||||
"TDX TCB Evaluation Data Numbers endpoint requires API v4".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
let path = self.build_api_path("tdx", "", "tcbevaluationdatanumbers")?;
|
||||
let url = self.base_url.join(&path)?;
|
||||
let request_builder = self.client.get(url);
|
||||
|
||||
let (tcb_evaluation_data_numbers_json, issuer_chain) = self
|
||||
.fetch_json_with_issuer_chain(
|
||||
request_builder,
|
||||
"TCB-Evaluation-Data-Numbers-Issuer-Chain",
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(TcbEvaluationDataNumbersResponse {
|
||||
tcb_evaluation_data_numbers_json,
|
||||
issuer_chain,
|
||||
})
|
||||
}
|
||||
}
|
|
@ -1,291 +0,0 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (c) 2025 Matter Labs
|
||||
|
||||
//! Internal helper methods
|
||||
|
||||
use super::ApiClient; // Import from parent module
|
||||
use crate::{
|
||||
error::{check_status, extract_api_error_details, IntelApiError},
|
||||
responses::{PckCertificateResponse, PckCertificatesResponse},
|
||||
types::{ApiVersion, UpdateType},
|
||||
};
|
||||
use percent_encoding::percent_decode_str;
|
||||
use reqwest::{RequestBuilder, Response, StatusCode};
|
||||
use std::{io, time::Duration};
|
||||
use tokio::time::sleep;
|
||||
|
||||
impl ApiClient {
|
||||
/// Helper to construct API paths dynamically based on version and technology (SGX/TDX).
|
||||
pub(super) fn build_api_path(
|
||||
&self,
|
||||
technology: &str,
|
||||
service: &str,
|
||||
endpoint: &str,
|
||||
) -> Result<String, IntelApiError> {
|
||||
let api_segment = self.api_version.path_segment();
|
||||
|
||||
if technology == "tdx" && self.api_version == ApiVersion::V3 {
|
||||
return Err(IntelApiError::UnsupportedApiVersion(format!(
|
||||
"TDX endpoint /{service}/{endpoint}/{technology} requires API v4",
|
||||
)));
|
||||
}
|
||||
if technology == "sgx" && service == "registration" {
|
||||
// Registration paths are fixed at v1 regardless of client's api_version
|
||||
return Ok(format!("/sgx/registration/v1/{endpoint}").replace("//", "/"));
|
||||
}
|
||||
|
||||
Ok(
|
||||
format!("/{technology}/certification/{api_segment}/{service}/{endpoint}")
|
||||
.replace("//", "/"),
|
||||
)
|
||||
}
|
||||
|
||||
/// Helper to add an optional header if the string is non-empty.
|
||||
pub(super) fn maybe_add_header(
|
||||
builder: RequestBuilder,
|
||||
header_name: &'static str,
|
||||
header_value: Option<&str>,
|
||||
) -> RequestBuilder {
|
||||
match header_value {
|
||||
Some(value) if !value.is_empty() => builder.header(header_name, value),
|
||||
_ => builder,
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper to extract a required header string value, handling potential v3/v4 differences.
|
||||
pub(super) fn get_required_header(
|
||||
&self,
|
||||
response: &Response,
|
||||
v4_header_name: &'static str,
|
||||
v3_header_name: Option<&'static str>,
|
||||
) -> Result<String, IntelApiError> {
|
||||
let header_name = match self.api_version {
|
||||
ApiVersion::V4 => v4_header_name,
|
||||
ApiVersion::V3 => v3_header_name.unwrap_or(v4_header_name),
|
||||
};
|
||||
let value = response
|
||||
.headers()
|
||||
.get(header_name)
|
||||
.ok_or(IntelApiError::MissingOrInvalidHeader(header_name))?
|
||||
.to_str()
|
||||
.map_err(|e| IntelApiError::HeaderValueParse(header_name, e.to_string()))?;
|
||||
|
||||
if value.contains('%') {
|
||||
percent_decode_str(value)
|
||||
.decode_utf8()
|
||||
.map_err(|e| IntelApiError::HeaderValueParse(header_name, e.to_string()))
|
||||
.map(|s| s.to_string())
|
||||
} else {
|
||||
Ok(value.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper to execute a request that returns a single PCK certificate and associated headers.
|
||||
pub(super) async fn fetch_pck_certificate(
|
||||
&self,
|
||||
request_builder: RequestBuilder,
|
||||
) -> Result<PckCertificateResponse, IntelApiError> {
|
||||
let response = self.execute_with_retry(request_builder).await?;
|
||||
let response = check_status(response, &[StatusCode::OK]).await?;
|
||||
|
||||
let issuer_chain = self.get_required_header(
|
||||
&response,
|
||||
"SGX-PCK-Certificate-Issuer-Chain",
|
||||
Some("SGX-PCK-Certificate-Issuer-Chain"),
|
||||
)?;
|
||||
let tcbm = self.get_required_header(&response, "SGX-TCBm", Some("SGX-TCBm"))?;
|
||||
let fmspc = self.get_required_header(&response, "SGX-FMSPC", Some("SGX-FMSPC"))?;
|
||||
let pck_cert_pem = response.text().await?;
|
||||
|
||||
Ok(PckCertificateResponse {
|
||||
pck_cert_pem,
|
||||
issuer_chain,
|
||||
tcbm,
|
||||
fmspc,
|
||||
})
|
||||
}
|
||||
|
||||
/// Helper to execute a request that returns a PCK certificates JSON array and associated headers.
|
||||
pub(super) async fn fetch_pck_certificates(
|
||||
&self,
|
||||
request_builder: RequestBuilder,
|
||||
) -> Result<PckCertificatesResponse, IntelApiError> {
|
||||
let response = self.execute_with_retry(request_builder).await?;
|
||||
let response = check_status(response, &[StatusCode::OK]).await?;
|
||||
|
||||
let issuer_chain = self.get_required_header(
|
||||
&response,
|
||||
"SGX-PCK-Certificate-Issuer-Chain",
|
||||
Some("SGX-PCK-Certificate-Issuer-Chain"),
|
||||
)?;
|
||||
let fmspc = self.get_required_header(&response, "SGX-FMSPC", Some("SGX-FMSPC"))?;
|
||||
let pck_certs_json = response.text().await?;
|
||||
|
||||
Ok(PckCertificatesResponse {
|
||||
pck_certs_json,
|
||||
issuer_chain,
|
||||
fmspc,
|
||||
})
|
||||
}
|
||||
|
||||
/// Helper to execute a request expected to return JSON plus an Issuer-Chain header.
|
||||
pub(super) async fn fetch_json_with_issuer_chain(
|
||||
&self,
|
||||
request_builder: RequestBuilder,
|
||||
v4_issuer_chain_header: &'static str,
|
||||
v3_issuer_chain_header: Option<&'static str>,
|
||||
) -> Result<(String, String), IntelApiError> {
|
||||
let response = self.execute_with_retry(request_builder).await?;
|
||||
let response = check_status(response, &[StatusCode::OK]).await?;
|
||||
|
||||
let issuer_chain =
|
||||
self.get_required_header(&response, v4_issuer_chain_header, v3_issuer_chain_header)?;
|
||||
let json_body = response.text().await?;
|
||||
|
||||
Ok((json_body, issuer_chain))
|
||||
}
|
||||
|
||||
/// Checks for HTTP 404 or 410 status when querying TCB Evaluation Data Number based resources.
|
||||
pub(super) async fn check_tcb_evaluation_status(
|
||||
&self,
|
||||
request_builder: &RequestBuilder,
|
||||
tcb_evaluation_data_number_val: u64,
|
||||
resource_description: &str,
|
||||
) -> Result<(), IntelApiError> {
|
||||
let builder_clone = request_builder.try_clone().ok_or_else(|| {
|
||||
IntelApiError::Io(io::Error::other(
|
||||
"Failed to clone request builder for status check",
|
||||
))
|
||||
})?;
|
||||
|
||||
let response = self.execute_with_retry(builder_clone).await?;
|
||||
let status = response.status();
|
||||
|
||||
if status == StatusCode::NOT_FOUND || status == StatusCode::GONE {
|
||||
let (request_id, _, _) = extract_api_error_details(&response);
|
||||
return Err(IntelApiError::ApiError {
|
||||
status,
|
||||
request_id,
|
||||
error_code: None,
|
||||
error_message: Some(format!(
|
||||
"{} for TCB Evaluation Data Number {} {}",
|
||||
resource_description,
|
||||
tcb_evaluation_data_number_val,
|
||||
if status == StatusCode::NOT_FOUND {
|
||||
"not found"
|
||||
} else {
|
||||
"is no longer available"
|
||||
}
|
||||
)),
|
||||
});
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Ensures the client is configured for API v4, otherwise returns an error.
|
||||
pub(super) fn ensure_v4_api(&self, function_name: &str) -> Result<(), IntelApiError> {
|
||||
if self.api_version != ApiVersion::V4 {
|
||||
return Err(IntelApiError::UnsupportedApiVersion(format!(
|
||||
"{function_name} requires API v4",
|
||||
)));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Checks if a V4-only parameter is provided with a V3 API version.
|
||||
pub(super) fn check_v4_only_param<T: Copy>(
|
||||
&self,
|
||||
param_value: Option<T>,
|
||||
param_name: &str,
|
||||
) -> Result<(), IntelApiError> {
|
||||
if self.api_version == ApiVersion::V3 && param_value.is_some() {
|
||||
Err(IntelApiError::UnsupportedApiVersion(format!(
|
||||
"'{param_name}' parameter requires API v4",
|
||||
)))
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Checks for conflicting `update` and `tcb_evaluation_data_number` parameters when using V4.
|
||||
pub(super) fn check_conflicting_update_params(
|
||||
&self,
|
||||
update: Option<UpdateType>,
|
||||
tcb_evaluation_data_number: Option<u64>,
|
||||
) -> Result<(), IntelApiError> {
|
||||
if self.api_version == ApiVersion::V4
|
||||
&& update.is_some()
|
||||
&& tcb_evaluation_data_number.is_some()
|
||||
{
|
||||
Err(IntelApiError::ConflictingParameters(
|
||||
"'update' and 'tcbEvaluationDataNumber'",
|
||||
))
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Executes a request with automatic retry logic for rate limiting (429 responses).
|
||||
///
|
||||
/// This method will automatically retry the request up to `max_retries` times
|
||||
/// when receiving a 429 Too Many Requests response, waiting for the duration
|
||||
/// specified in the Retry-After header.
|
||||
pub(super) async fn execute_with_retry(
|
||||
&self,
|
||||
request_builder: RequestBuilder,
|
||||
) -> Result<Response, IntelApiError> {
|
||||
let mut retries = 0;
|
||||
|
||||
loop {
|
||||
// Clone the request builder for retry attempts
|
||||
let builder = request_builder.try_clone().ok_or_else(|| {
|
||||
IntelApiError::Io(io::Error::other(
|
||||
"Failed to clone request builder for retry",
|
||||
))
|
||||
})?;
|
||||
|
||||
let response = builder.send().await?;
|
||||
let status = response.status();
|
||||
|
||||
if status != StatusCode::TOO_MANY_REQUESTS {
|
||||
// Not a rate limit error, return the response
|
||||
return Ok(response);
|
||||
}
|
||||
|
||||
// Handle 429 Too Many Requests
|
||||
if retries >= self.max_retries {
|
||||
// No more retries, return the error
|
||||
let request_id = response
|
||||
.headers()
|
||||
.get("Request-ID")
|
||||
.and_then(|v| v.to_str().ok())
|
||||
.unwrap_or("Unknown")
|
||||
.to_string();
|
||||
|
||||
let retry_after = response
|
||||
.headers()
|
||||
.get("Retry-After")
|
||||
.and_then(|v| v.to_str().ok())
|
||||
.and_then(|v| v.parse::<u64>().ok())
|
||||
.unwrap_or(60);
|
||||
|
||||
return Err(IntelApiError::TooManyRequests {
|
||||
request_id,
|
||||
retry_after,
|
||||
});
|
||||
}
|
||||
|
||||
// Parse Retry-After header
|
||||
let retry_after_secs = response
|
||||
.headers()
|
||||
.get("Retry-After")
|
||||
.and_then(|v| v.to_str().ok())
|
||||
.and_then(|v| v.parse::<u64>().ok())
|
||||
.unwrap_or(60); // Default to 60 seconds
|
||||
|
||||
// Wait before retrying
|
||||
sleep(Duration::from_secs(retry_after_secs)).await;
|
||||
retries += 1;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,135 +0,0 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (c) 2025 Matter Labs
|
||||
|
||||
mod enclave_identity;
|
||||
mod fmspc;
|
||||
mod helpers;
|
||||
mod pck_cert;
|
||||
mod pck_crl;
|
||||
mod registration;
|
||||
mod tcb_info;
|
||||
|
||||
use crate::{error::IntelApiError, types::ApiVersion};
|
||||
use reqwest::Client;
|
||||
use url::Url;
|
||||
|
||||
// Base URL for the Intel Trusted Services API
|
||||
const BASE_URL: &str = "https://api.trustedservices.intel.com";
|
||||
|
||||
/// Client for interacting with Intel Trusted Services API.
|
||||
///
|
||||
/// Provides methods to access both SGX and TDX certification services,
|
||||
/// supporting API versions V3 and V4. This client offers functionality
|
||||
/// to register platforms, retrieve PCK certificates and CRLs, fetch TCB
|
||||
/// information, enclave identities, as well as TCB evaluation data numbers.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// use intel_dcap_api::ApiClient;
|
||||
///
|
||||
/// #[tokio::main]
|
||||
/// async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
/// // Create a client with default settings (V4 API)
|
||||
/// let client = ApiClient::new()?;
|
||||
///
|
||||
/// // Retrieve TCB info for a specific FMSPC
|
||||
/// let tcb_info = client.get_sgx_tcb_info("00606A000000", None, None).await?;
|
||||
/// println!("TCB Info: {}", tcb_info.tcb_info_json);
|
||||
///
|
||||
/// Ok(())
|
||||
/// }
|
||||
/// ```
|
||||
#[derive(Clone)]
|
||||
pub struct ApiClient {
|
||||
client: Client,
|
||||
base_url: Url,
|
||||
api_version: ApiVersion,
|
||||
/// Maximum number of automatic retries for rate-limited requests (429 responses)
|
||||
max_retries: u32,
|
||||
}
|
||||
|
||||
impl ApiClient {
|
||||
/// Creates a new client targeting the latest supported API version (V4).
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// A result containing the newly created `ApiClient` or an `IntelApiError` if there
|
||||
/// was an issue building the underlying HTTP client.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// This function may fail if the provided TLS version or base URL
|
||||
/// cannot be used to build a `reqwest` client.
|
||||
pub fn new() -> Result<Self, IntelApiError> {
|
||||
// Default to V4
|
||||
Self::new_with_options(BASE_URL, ApiVersion::V4)
|
||||
}
|
||||
|
||||
/// Creates a new client targeting a specific API version.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `api_version` - The desired API version to use (V3 or V4).
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an `IntelApiError` if the `reqwest` client cannot be built
|
||||
/// with the specified options.
|
||||
pub fn new_with_version(api_version: ApiVersion) -> Result<Self, IntelApiError> {
|
||||
Self::new_with_options(BASE_URL, api_version)
|
||||
}
|
||||
|
||||
/// Creates a new client with a custom base URL, targeting the latest supported API version (V4).
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `base_url` - The custom base URL for the Intel Trusted Services API.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an `IntelApiError` if the `reqwest` client cannot be built
|
||||
/// or if the provided base URL is invalid.
|
||||
pub fn new_with_base_url(base_url: impl reqwest::IntoUrl) -> Result<Self, IntelApiError> {
|
||||
// Default to V4
|
||||
Self::new_with_options(base_url, ApiVersion::V4)
|
||||
}
|
||||
|
||||
/// Creates a new client with a custom base URL and specific API version.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `base_url` - The custom base URL for the Intel Trusted Services API.
|
||||
/// * `api_version` - The desired API version (V3 or V4).
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an `IntelApiError` if the `reqwest` client cannot be built
|
||||
/// or if the provided base URL is invalid.
|
||||
pub fn new_with_options(
|
||||
base_url: impl reqwest::IntoUrl,
|
||||
api_version: ApiVersion,
|
||||
) -> Result<Self, IntelApiError> {
|
||||
Ok(ApiClient {
|
||||
client: Client::builder()
|
||||
.min_tls_version(reqwest::tls::Version::TLS_1_2)
|
||||
.build()?,
|
||||
base_url: base_url.into_url()?,
|
||||
api_version,
|
||||
max_retries: 3, // Default to 3 retries
|
||||
})
|
||||
}
|
||||
|
||||
/// Sets the maximum number of automatic retries for rate-limited requests.
|
||||
///
|
||||
/// When the API returns a 429 (Too Many Requests) response, the client will
|
||||
/// automatically wait for the duration specified in the Retry-After header
|
||||
/// and retry the request up to this many times.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `max_retries` - Maximum number of retries (0 disables automatic retries)
|
||||
pub fn set_max_retries(&mut self, max_retries: u32) {
|
||||
self.max_retries = max_retries;
|
||||
}
|
||||
}
|
|
@ -1,353 +0,0 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (c) 2025 Matter Labs
|
||||
|
||||
//! Provisioning Certification Service
|
||||
|
||||
use super::ApiClient; // Import from parent module
|
||||
use crate::{
|
||||
error::IntelApiError,
|
||||
requests::{PckCertRequest, PckCertsConfigRequest, PckCertsRequest},
|
||||
responses::{PckCertificateResponse, PckCertificatesResponse},
|
||||
types::ApiVersion,
|
||||
};
|
||||
use reqwest::header;
|
||||
|
||||
impl ApiClient {
|
||||
/// GET /sgx/certification/{v3,v4}/pckcert
|
||||
/// Retrieves a single SGX PCK certificate using encrypted PPID and SVNs.
|
||||
///
|
||||
/// Optionally requires a subscription key. The `ppid_encryption_key_type` parameter
|
||||
/// is only valid for API v4 and allows specifying the PPID encryption key type (e.g. "RSA-3072").
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `encrypted_ppid` - Hex-encoded encrypted PPID.
|
||||
/// * `cpusvn` - Hex-encoded CPUSVN value.
|
||||
/// * `pcesvn` - Hex-encoded PCESVN value.
|
||||
/// * `pceid` - Hex-encoded PCEID value.
|
||||
/// * `subscription_key` - Optional subscription key if the Intel API requires it.
|
||||
/// * `ppid_encryption_key_type` - Optional PPID encryption key type (V4 only).
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// A [`PckCertificateResponse`] containing the PEM-encoded certificate, issuer chain,
|
||||
/// TCBm, and FMSPC.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an `IntelApiError` if the API call fails or the response contains an invalid status.
|
||||
/// Returns PEM Cert, Issuer Chain, TCBm, FMSPC.
|
||||
pub async fn get_pck_certificate_by_ppid(
|
||||
&self,
|
||||
encrypted_ppid: &str,
|
||||
cpusvn: &str,
|
||||
pcesvn: &str,
|
||||
pceid: &str,
|
||||
subscription_key: Option<&str>,
|
||||
ppid_encryption_key_type: Option<&str>,
|
||||
) -> Result<PckCertificateResponse, IntelApiError> {
|
||||
// Check V4-only parameter
|
||||
self.check_v4_only_param(ppid_encryption_key_type, "PPID-Encryption-Key")?;
|
||||
|
||||
let path = self.build_api_path("sgx", "", "pckcert")?; // service is empty
|
||||
let mut url = self.base_url.join(&path)?;
|
||||
url.query_pairs_mut()
|
||||
.append_pair("encrypted_ppid", encrypted_ppid)
|
||||
.append_pair("cpusvn", cpusvn)
|
||||
.append_pair("pcesvn", pcesvn)
|
||||
.append_pair("pceid", pceid);
|
||||
|
||||
let mut request_builder = self.client.get(url);
|
||||
|
||||
request_builder = Self::maybe_add_header(
|
||||
request_builder,
|
||||
"Ocp-Apim-Subscription-Key",
|
||||
subscription_key,
|
||||
);
|
||||
|
||||
// Only add for V4
|
||||
if self.api_version == ApiVersion::V4 {
|
||||
request_builder = Self::maybe_add_header(
|
||||
request_builder,
|
||||
"PPID-Encryption-Key",
|
||||
ppid_encryption_key_type,
|
||||
);
|
||||
}
|
||||
|
||||
self.fetch_pck_certificate(request_builder).await
|
||||
}
|
||||
|
||||
/// POST /sgx/certification/{v3,v4}/pckcert
|
||||
/// Retrieves a single SGX PCK certificate using a platform manifest and SVNs.
|
||||
///
|
||||
/// Optionally requires a subscription key.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `platform_manifest` - Hex-encoded platform manifest.
|
||||
/// * `cpusvn` - Hex-encoded CPUSVN value.
|
||||
/// * `pcesvn` - Hex-encoded PCESVN value.
|
||||
/// * `pceid` - Hex-encoded PCEID value.
|
||||
/// * `subscription_key` - Optional subscription key if the Intel API requires it.
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// A [`PckCertificateResponse`] containing the PEM-encoded certificate, issuer chain,
|
||||
/// TCBm, and FMSPC.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an `IntelApiError` if the request fails or if the response is invalid.
|
||||
/// Returns PEM Cert, Issuer Chain, TCBm, FMSPC.
|
||||
pub async fn get_pck_certificate_by_manifest(
|
||||
&self,
|
||||
platform_manifest: &str,
|
||||
cpusvn: &str,
|
||||
pcesvn: &str,
|
||||
pceid: &str,
|
||||
subscription_key: Option<&str>,
|
||||
) -> Result<PckCertificateResponse, IntelApiError> {
|
||||
let path = self.build_api_path("sgx", "", "pckcert")?;
|
||||
let url = self.base_url.join(&path)?;
|
||||
let request_body = PckCertRequest {
|
||||
platform_manifest,
|
||||
cpusvn,
|
||||
pcesvn,
|
||||
pceid,
|
||||
};
|
||||
|
||||
let mut request_builder = self
|
||||
.client
|
||||
.post(url)
|
||||
.header(header::CONTENT_TYPE, "application/json")
|
||||
.json(&request_body);
|
||||
|
||||
request_builder = Self::maybe_add_header(
|
||||
request_builder,
|
||||
"Ocp-Apim-Subscription-Key",
|
||||
subscription_key,
|
||||
);
|
||||
|
||||
self.fetch_pck_certificate(request_builder).await
|
||||
}
|
||||
|
||||
/// GET /sgx/certification/{v3,v4}/pckcerts
|
||||
/// Retrieves all SGX PCK certificates for a platform using encrypted PPID.
|
||||
///
|
||||
/// Optionally requires a subscription key. The `ppid_encryption_key_type` parameter
|
||||
/// is only valid for API v4.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `encrypted_ppid` - Hex-encoded encrypted PPID.
|
||||
/// * `pceid` - Hex-encoded PCEID value.
|
||||
/// * `subscription_key` - Optional subscription key if the Intel API requires it.
|
||||
/// * `ppid_encryption_key_type` - Optional PPID encryption key type (V4 only).
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// A [`PckCertificatesResponse`] containing JSON with `{tcb, tcbm, cert}` entries,
|
||||
/// as well as the issuer chain and FMSPC headers.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an `IntelApiError` if the API call fails or the response status is invalid.
|
||||
pub async fn get_pck_certificates_by_ppid(
|
||||
&self,
|
||||
encrypted_ppid: &str,
|
||||
pceid: &str,
|
||||
subscription_key: Option<&str>,
|
||||
ppid_encryption_key_type: Option<&str>,
|
||||
) -> Result<PckCertificatesResponse, IntelApiError> {
|
||||
// Check V4-only parameter
|
||||
self.check_v4_only_param(ppid_encryption_key_type, "PPID-Encryption-Key")?;
|
||||
|
||||
let path = self.build_api_path("sgx", "", "pckcerts")?;
|
||||
let mut url = self.base_url.join(&path)?;
|
||||
url.query_pairs_mut()
|
||||
.append_pair("encrypted_ppid", encrypted_ppid)
|
||||
.append_pair("pceid", pceid);
|
||||
|
||||
let mut request_builder = self.client.get(url);
|
||||
|
||||
request_builder = Self::maybe_add_header(
|
||||
request_builder,
|
||||
"Ocp-Apim-Subscription-Key",
|
||||
subscription_key,
|
||||
);
|
||||
|
||||
// Only add for V4
|
||||
if self.api_version == ApiVersion::V4 {
|
||||
request_builder = Self::maybe_add_header(
|
||||
request_builder,
|
||||
"PPID-Encryption-Key",
|
||||
ppid_encryption_key_type,
|
||||
);
|
||||
}
|
||||
|
||||
self.fetch_pck_certificates(request_builder).await
|
||||
}
|
||||
|
||||
/// POST /sgx/certification/{v3,v4}/pckcerts
|
||||
/// Retrieves all SGX PCK certificates for a platform using a platform manifest.
|
||||
///
|
||||
/// Optionally requires a subscription key.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `platform_manifest` - Hex-encoded platform manifest.
|
||||
/// * `pceid` - Hex-encoded PCEID value.
|
||||
/// * `subscription_key` - Optional subscription key if the Intel API requires it.
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// A [`PckCertificatesResponse`] containing JSON with `{tcb, tcbm, cert}` entries,
|
||||
/// as well as the issuer chain and FMSPC headers.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an `IntelApiError` if the API call fails or the response status is invalid.
|
||||
pub async fn get_pck_certificates_by_manifest(
|
||||
&self,
|
||||
platform_manifest: &str,
|
||||
pceid: &str,
|
||||
subscription_key: Option<&str>,
|
||||
) -> Result<PckCertificatesResponse, IntelApiError> {
|
||||
let path = self.build_api_path("sgx", "", "pckcerts")?;
|
||||
let url = self.base_url.join(&path)?;
|
||||
let request_body = PckCertsRequest {
|
||||
platform_manifest,
|
||||
pceid,
|
||||
};
|
||||
|
||||
let mut request_builder = self
|
||||
.client
|
||||
.post(url)
|
||||
.header(header::CONTENT_TYPE, "application/json")
|
||||
.json(&request_body);
|
||||
|
||||
request_builder = Self::maybe_add_header(
|
||||
request_builder,
|
||||
"Ocp-Apim-Subscription-Key",
|
||||
subscription_key,
|
||||
);
|
||||
|
||||
self.fetch_pck_certificates(request_builder).await
|
||||
}
|
||||
|
||||
/// GET /sgx/certification/{v3,v4}/pckcerts/config (using PPID)
|
||||
/// Retrieves SGX PCK certificates for a specific configuration (CPUSVN) using encrypted PPID.
|
||||
///
|
||||
/// Optionally requires a subscription key. The `ppid_encryption_key_type` parameter
|
||||
/// is only valid for API v4. Returns JSON with `{tcb, tcbm, cert}` entries,
|
||||
/// as well as the issuer chain and FMSPC headers.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `encrypted_ppid` - Hex-encoded encrypted PPID.
|
||||
/// * `pceid` - Hex-encoded PCEID value.
|
||||
/// * `cpusvn` - Hex-encoded CPUSVN value for the requested configuration.
|
||||
/// * `subscription_key` - Optional subscription key if the Intel API requires it.
|
||||
/// * `ppid_encryption_key_type` - Optional PPID encryption key type (V4 only).
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// A [`PckCertificatesResponse`] with the requested config's certificate data.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an `IntelApiError` if the request fails or if the response status
|
||||
/// is not `200 OK`.
|
||||
pub async fn get_pck_certificates_config_by_ppid(
|
||||
&self,
|
||||
encrypted_ppid: &str,
|
||||
pceid: &str,
|
||||
cpusvn: &str,
|
||||
subscription_key: Option<&str>,
|
||||
ppid_encryption_key_type: Option<&str>,
|
||||
) -> Result<PckCertificatesResponse, IntelApiError> {
|
||||
// V3 does not support PPID-Encryption-Key header/type
|
||||
if self.api_version == ApiVersion::V3 && ppid_encryption_key_type.is_some() {
|
||||
return Err(IntelApiError::UnsupportedApiVersion(
|
||||
"PPID-Encryption-Key header is only supported in API v4".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
let path = self.build_api_path("sgx", "", "pckcerts/config")?;
|
||||
let mut url = self.base_url.join(&path)?;
|
||||
url.query_pairs_mut()
|
||||
.append_pair("encrypted_ppid", encrypted_ppid)
|
||||
.append_pair("pceid", pceid)
|
||||
.append_pair("cpusvn", cpusvn);
|
||||
|
||||
let mut request_builder = self.client.get(url);
|
||||
|
||||
request_builder = Self::maybe_add_header(
|
||||
request_builder,
|
||||
"Ocp-Apim-Subscription-Key",
|
||||
subscription_key,
|
||||
);
|
||||
|
||||
// Only add for V4
|
||||
if self.api_version == ApiVersion::V4 {
|
||||
request_builder = Self::maybe_add_header(
|
||||
request_builder,
|
||||
"PPID-Encryption-Key",
|
||||
ppid_encryption_key_type,
|
||||
);
|
||||
}
|
||||
|
||||
self.fetch_pck_certificates(request_builder).await
|
||||
}
|
||||
|
||||
/// POST /sgx/certification/{v3,v4}/pckcerts/config (using Manifest)
|
||||
/// Retrieves SGX PCK certificates for a specific configuration (CPUSVN) using a platform manifest.
|
||||
///
|
||||
/// Optionally requires a subscription key. Returns JSON with `{tcb, tcbm, cert}` entries,
|
||||
/// as well as the issuer chain and FMSPC headers.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `platform_manifest` - Hex-encoded platform manifest.
|
||||
/// * `pceid` - Hex-encoded PCEID value.
|
||||
/// * `cpusvn` - Hex-encoded CPUSVN value for the requested configuration.
|
||||
/// * `subscription_key` - Optional subscription key if needed by the Intel API.
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// A [`PckCertificatesResponse`] with the requested config's certificate data.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an `IntelApiError` if the request fails or if the response status
|
||||
/// is not `200 OK`.
|
||||
pub async fn get_pck_certificates_config_by_manifest(
|
||||
&self,
|
||||
platform_manifest: &str,
|
||||
pceid: &str,
|
||||
cpusvn: &str,
|
||||
subscription_key: Option<&str>,
|
||||
) -> Result<PckCertificatesResponse, IntelApiError> {
|
||||
let path = self.build_api_path("sgx", "", "pckcerts/config")?;
|
||||
let url = self.base_url.join(&path)?;
|
||||
let request_body = PckCertsConfigRequest {
|
||||
platform_manifest,
|
||||
pceid,
|
||||
cpusvn,
|
||||
};
|
||||
|
||||
let mut request_builder = self
|
||||
.client
|
||||
.post(url)
|
||||
.header(header::CONTENT_TYPE, "application/json")
|
||||
.json(&request_body);
|
||||
|
||||
request_builder = Self::maybe_add_header(
|
||||
request_builder,
|
||||
"Ocp-Apim-Subscription-Key",
|
||||
subscription_key,
|
||||
);
|
||||
|
||||
self.fetch_pck_certificates(request_builder).await
|
||||
}
|
||||
}
|
|
@ -1,69 +0,0 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (c) 2025 Matter Labs
|
||||
|
||||
//! PCK Certificate Revocation List
|
||||
|
||||
use super::ApiClient; // Import from parent module
|
||||
use crate::{
|
||||
error::{check_status, IntelApiError},
|
||||
responses::PckCrlResponse,
|
||||
types::{CaType, CrlEncoding},
|
||||
};
|
||||
use reqwest::StatusCode;
|
||||
|
||||
impl ApiClient {
|
||||
/// GET /sgx/certification/{v3,v4}/pckcrl
|
||||
/// Retrieves the PCK Certificate Revocation List (CRL) for a specified CA type.
|
||||
///
|
||||
/// Optionally takes an `encoding` parameter indicating whether the CRL should be
|
||||
/// returned as PEM or DER. Defaults to PEM if not specified.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `ca_type` - The type of CA to retrieve the CRL for (e.g., "processor" or "platform").
|
||||
/// * `encoding` - An optional [`CrlEncoding`] (PEM or DER).
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// A [`PckCrlResponse`] containing the CRL data and the issuer chain.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an `IntelApiError` if the request fails or if the response status
|
||||
/// is not `200 OK`.
|
||||
/// Optional 'encoding' parameter ("pem" or "der").
|
||||
/// Returns CRL data (PEM or DER) and Issuer Chain header.
|
||||
pub async fn get_pck_crl(
|
||||
&self,
|
||||
ca_type: CaType,
|
||||
encoding: Option<CrlEncoding>,
|
||||
) -> Result<PckCrlResponse, IntelApiError> {
|
||||
let path = self.build_api_path("sgx", "", "pckcrl")?;
|
||||
let mut url = self.base_url.join(&path)?;
|
||||
url.query_pairs_mut()
|
||||
.append_pair("ca", &ca_type.to_string());
|
||||
|
||||
if let Some(enc) = encoding {
|
||||
url.query_pairs_mut()
|
||||
.append_pair("encoding", &enc.to_string());
|
||||
}
|
||||
|
||||
let request_builder = self.client.get(url);
|
||||
let response = self.execute_with_retry(request_builder).await?;
|
||||
let response = check_status(response, &[StatusCode::OK]).await?;
|
||||
|
||||
let issuer_chain = self.get_required_header(
|
||||
&response,
|
||||
"SGX-PCK-CRL-Issuer-Chain",
|
||||
Some("SGX-PCK-CRL-Issuer-Chain"),
|
||||
)?;
|
||||
|
||||
// Response body is PEM or DER CRL
|
||||
let crl_data = response.bytes().await?.to_vec();
|
||||
|
||||
Ok(PckCrlResponse {
|
||||
crl_data,
|
||||
issuer_chain,
|
||||
})
|
||||
}
|
||||
}
|
|
@ -1,108 +0,0 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (c) 2025 Matter Labs
|
||||
|
||||
//! Registration
|
||||
|
||||
use super::ApiClient; // Import from parent module
|
||||
use crate::{
|
||||
error::{check_status, IntelApiError},
|
||||
responses::AddPackageResponse,
|
||||
};
|
||||
use reqwest::{header, StatusCode};
|
||||
use std::num::ParseIntError;
|
||||
|
||||
impl ApiClient {
|
||||
/// POST /sgx/registration/v1/platform
|
||||
/// Registers a multi-package SGX platform with the Intel Trusted Services API.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `platform_manifest` - Binary data representing the platform manifest.
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// Request body is binary Platform Manifest
|
||||
/// Returns the hex-encoded PPID as a `String` upon success.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an `IntelApiError` if the request fails or if the response status
|
||||
/// is not HTTP `201 CREATED`.
|
||||
pub async fn register_platform(
|
||||
&self,
|
||||
platform_manifest: Vec<u8>,
|
||||
) -> Result<String, IntelApiError> {
|
||||
// Registration paths are fixed, use the helper with "registration" service
|
||||
let path = self.build_api_path("sgx", "registration", "platform")?;
|
||||
let url = self.base_url.join(&path)?;
|
||||
|
||||
let request_builder = self
|
||||
.client
|
||||
.post(url)
|
||||
.header(header::CONTENT_TYPE, "application/octet-stream")
|
||||
.body(platform_manifest);
|
||||
|
||||
let response = self.execute_with_retry(request_builder).await?;
|
||||
|
||||
let response = check_status(response, &[StatusCode::CREATED]).await?;
|
||||
|
||||
// Response body is hex-encoded PPID
|
||||
let ppid_hex = response.text().await?;
|
||||
Ok(ppid_hex)
|
||||
}
|
||||
|
||||
/// POST /sgx/registration/v1/package
|
||||
/// Adds new package(s) to an already registered SGX platform instance.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `add_package_request` - Binary data for the "Add Package" request body.
|
||||
/// * `subscription_key` - The subscription key required by the Intel API.
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// A [`AddPackageResponse`] containing the Platform Membership Certificates and
|
||||
/// the count of them extracted from the response header.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an `IntelApiError` if the request fails, if the subscription key is invalid,
|
||||
/// or if the response status is not HTTP `200 OK`.
|
||||
pub async fn add_package(
|
||||
&self,
|
||||
add_package_request: Vec<u8>,
|
||||
subscription_key: &str,
|
||||
) -> Result<AddPackageResponse, IntelApiError> {
|
||||
if subscription_key.is_empty() {
|
||||
return Err(IntelApiError::InvalidSubscriptionKey);
|
||||
}
|
||||
|
||||
// Registration paths are fixed
|
||||
let path = self.build_api_path("sgx", "registration", "package")?;
|
||||
let url = self.base_url.join(&path)?;
|
||||
|
||||
let request_builder = self
|
||||
.client
|
||||
.post(url)
|
||||
.header("Ocp-Apim-Subscription-Key", subscription_key)
|
||||
.header(header::CONTENT_TYPE, "application/octet-stream")
|
||||
.body(add_package_request);
|
||||
|
||||
let response = self.execute_with_retry(request_builder).await?;
|
||||
|
||||
let response = check_status(response, &[StatusCode::OK]).await?;
|
||||
|
||||
// Use the generic header helper, assuming header name is stable across reg versions
|
||||
let cert_count_str = self.get_required_header(&response, "Certificate-Count", None)?;
|
||||
let pck_cert_count: usize = cert_count_str.parse().map_err(|e: ParseIntError| {
|
||||
IntelApiError::HeaderValueParse("Certificate-Count", e.to_string())
|
||||
})?;
|
||||
|
||||
// Response body is a binary array of certificates
|
||||
let pck_certs = response.bytes().await?.to_vec();
|
||||
Ok(AddPackageResponse {
|
||||
pck_certs,
|
||||
pck_cert_count,
|
||||
})
|
||||
}
|
||||
}
|
|
@ -1,167 +0,0 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (c) 2025 Matter Labs
|
||||
|
||||
//! TCB Info
|
||||
|
||||
use super::ApiClient; // Import from parent module
|
||||
use crate::{
|
||||
error::IntelApiError,
|
||||
responses::TcbInfoResponse,
|
||||
types::{ApiVersion, UpdateType},
|
||||
};
|
||||
|
||||
impl ApiClient {
|
||||
/// GET /sgx/certification/{v3,v4}/tcb
|
||||
/// Retrieves SGX TCB information for a given FMSPC.
|
||||
///
|
||||
/// Returns TCB Info JSON string (Appendix A) and Issuer Chain header.
|
||||
/// This function supports both API v3 and v4. The `update` and `tcbEvaluationDataNumber`
|
||||
/// parameters are only supported by API v4. If both are provided at the same time (for v4),
|
||||
/// a conflict error is returned.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `fmspc` - Hex-encoded FMSPC value.
|
||||
/// * `update` - Optional [`UpdateType`] for API v4.
|
||||
/// * `tcb_evaluation_data_number` - Optional TCB Evaluation Data Number (v4 only).
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// A [`TcbInfoResponse`] containing the TCB info JSON and the issuer chain.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an `IntelApiError` if the API request fails, if conflicting parameters are used,
|
||||
/// or if the requested TCB data is not found.
|
||||
pub async fn get_sgx_tcb_info(
|
||||
&self,
|
||||
fmspc: &str,
|
||||
update: Option<UpdateType>,
|
||||
tcb_evaluation_data_number: Option<u64>,
|
||||
) -> Result<TcbInfoResponse, IntelApiError> {
|
||||
// V3 does not support 'update' or 'tcbEvaluationDataNumber'
|
||||
if self.api_version == ApiVersion::V3 && update.is_some() {
|
||||
return Err(IntelApiError::UnsupportedApiVersion(
|
||||
"'update' parameter requires API v4".to_string(),
|
||||
));
|
||||
}
|
||||
if self.api_version == ApiVersion::V3 && tcb_evaluation_data_number.is_some() {
|
||||
return Err(IntelApiError::UnsupportedApiVersion(
|
||||
"'tcbEvaluationDataNumber' parameter requires API v4".to_string(),
|
||||
));
|
||||
}
|
||||
if self.api_version == ApiVersion::V4
|
||||
&& update.is_some()
|
||||
&& tcb_evaluation_data_number.is_some()
|
||||
{
|
||||
return Err(IntelApiError::ConflictingParameters(
|
||||
"'update' and 'tcbEvaluationDataNumber'",
|
||||
));
|
||||
}
|
||||
|
||||
let path = self.build_api_path("sgx", "", "tcb")?;
|
||||
let mut url = self.base_url.join(&path)?;
|
||||
url.query_pairs_mut().append_pair("fmspc", fmspc);
|
||||
|
||||
// Add V4-specific parameters
|
||||
if self.api_version == ApiVersion::V4 {
|
||||
if let Some(upd) = update {
|
||||
url.query_pairs_mut()
|
||||
.append_pair("update", &upd.to_string());
|
||||
}
|
||||
if let Some(tedn) = tcb_evaluation_data_number {
|
||||
url.query_pairs_mut()
|
||||
.append_pair("tcbEvaluationDataNumber", &tedn.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
let request_builder = self.client.get(url);
|
||||
|
||||
// Special handling for 404/410 when tcbEvaluationDataNumber is specified (V4 only)
|
||||
if self.api_version == ApiVersion::V4 {
|
||||
if let Some(tedn_val) = tcb_evaluation_data_number {
|
||||
// Use the helper function to check status before proceeding
|
||||
self.check_tcb_evaluation_status(&request_builder, tedn_val, "SGX TCB Info")
|
||||
.await?;
|
||||
// If the check passes (doesn't return Err), continue to fetch_json_with_issuer_chain
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch JSON and header (header name seems same for v3/v4)
|
||||
let (tcb_info_json, issuer_chain) = self
|
||||
.fetch_json_with_issuer_chain(
|
||||
request_builder,
|
||||
"TCB-Info-Issuer-Chain",
|
||||
Some("SGX-TCB-Info-Issuer-Chain"),
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(TcbInfoResponse {
|
||||
tcb_info_json,
|
||||
issuer_chain,
|
||||
})
|
||||
}
|
||||
|
||||
/// GET /tdx/certification/v4/tcb
|
||||
/// Retrieves TDX TCB information for a given FMSPC (API v4 only).
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `fmspc` - Hex-encoded FMSPC value.
|
||||
/// * `update` - An optional [`UpdateType`] (v4 only).
|
||||
/// * `tcb_evaluation_data_number` - An optional TCB Evaluation Data Number (v4 only).
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// A [`TcbInfoResponse`] containing TDX TCB info JSON and the issuer chain.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an `IntelApiError` if an unsupported API version is used,
|
||||
/// if there are conflicting parameters, or if the TDX TCB data is not found.
|
||||
/// Returns TCB Info JSON string (Appendix A) and Issuer Chain header.
|
||||
pub async fn get_tdx_tcb_info(
|
||||
&self,
|
||||
fmspc: &str,
|
||||
update: Option<UpdateType>,
|
||||
tcb_evaluation_data_number: Option<u64>,
|
||||
) -> Result<TcbInfoResponse, IntelApiError> {
|
||||
// Ensure V4 API
|
||||
self.ensure_v4_api("get_tdx_tcb_info")?;
|
||||
// Check conflicting parameters (only relevant for V4, checked inside helper)
|
||||
self.check_conflicting_update_params(update, tcb_evaluation_data_number)?;
|
||||
|
||||
let path = self.build_api_path("tdx", "", "tcb")?;
|
||||
let mut url = self.base_url.join(&path)?;
|
||||
url.query_pairs_mut().append_pair("fmspc", fmspc);
|
||||
|
||||
if let Some(upd) = update {
|
||||
url.query_pairs_mut()
|
||||
.append_pair("update", &upd.to_string());
|
||||
}
|
||||
if let Some(tedn) = tcb_evaluation_data_number {
|
||||
url.query_pairs_mut()
|
||||
.append_pair("tcbEvaluationDataNumber", &tedn.to_string());
|
||||
}
|
||||
|
||||
let request_builder = self.client.get(url);
|
||||
|
||||
// Special handling for 404/410 when tcbEvaluationDataNumber is specified
|
||||
if let Some(tedn_val) = tcb_evaluation_data_number {
|
||||
// Use the helper function to check status before proceeding
|
||||
self.check_tcb_evaluation_status(&request_builder, tedn_val, "TDX TCB Info")
|
||||
.await?;
|
||||
// If the check passes (doesn't return Err), continue to fetch_json_with_issuer_chain
|
||||
}
|
||||
|
||||
// Fetch JSON and header (TDX only exists in V4)
|
||||
let (tcb_info_json, issuer_chain) = self
|
||||
.fetch_json_with_issuer_chain(request_builder, "TCB-Info-Issuer-Chain", None)
|
||||
.await?;
|
||||
|
||||
Ok(TcbInfoResponse {
|
||||
tcb_info_json,
|
||||
issuer_chain,
|
||||
})
|
||||
}
|
||||
}
|
|
@ -1,159 +0,0 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (c) 2025 Matter Labs
|
||||
|
||||
use reqwest::{Response, StatusCode};
|
||||
use thiserror::Error;
|
||||
|
||||
/// Represents all possible errors that can occur when interacting with Intel's DCAP API.
|
||||
#[derive(Error, Debug)]
|
||||
pub enum IntelApiError {
|
||||
/// Indicates that the requested API version or feature is unsupported.
|
||||
#[error("Unsupported API version or feature: {0}")]
|
||||
UnsupportedApiVersion(String),
|
||||
|
||||
/// Wraps an underlying reqwest error.
|
||||
#[error("Reqwest error: {0}")]
|
||||
Reqwest(#[from] reqwest::Error),
|
||||
|
||||
/// Wraps a URL parsing error.
|
||||
#[error("URL parsing error: {0}")]
|
||||
UrlParse(#[from] url::ParseError),
|
||||
|
||||
/// Wraps a Serde JSON error.
|
||||
#[error("Serde JSON error: {0}")]
|
||||
JsonError(#[from] serde_json::Error),
|
||||
|
||||
/// Represents a general API error, capturing the HTTP status and optional error details.
|
||||
#[error("API Error: Status={status}, Request-ID={request_id}, Code={error_code:?}, Message={error_message:?}")]
|
||||
ApiError {
|
||||
/// HTTP status code returned by the API.
|
||||
status: StatusCode,
|
||||
/// The unique request identifier for tracing errors.
|
||||
request_id: String,
|
||||
/// An optional server-provided error code.
|
||||
error_code: Option<String>,
|
||||
/// An optional server-provided error message.
|
||||
error_message: Option<String>,
|
||||
},
|
||||
|
||||
/// Indicates that a header is missing or invalid.
|
||||
#[error("Header missing or invalid: {0}")]
|
||||
MissingOrInvalidHeader(&'static str),
|
||||
|
||||
/// Represents an invalid subscription key.
|
||||
#[error("Invalid Subscription Key format")]
|
||||
InvalidSubscriptionKey,
|
||||
|
||||
/// Indicates that conflicting parameters were supplied.
|
||||
#[error("Cannot provide conflicting parameters: {0}")]
|
||||
ConflictingParameters(&'static str),
|
||||
|
||||
/// Wraps a standard I/O error.
|
||||
#[error("I/O Error: {0}")]
|
||||
Io(#[from] std::io::Error),
|
||||
|
||||
/// Represents an error while parsing a header's value.
|
||||
#[error("Header value parse error for '{0}': {1}")]
|
||||
HeaderValueParse(&'static str, String),
|
||||
|
||||
/// Indicates an invalid parameter was provided.
|
||||
#[error("Invalid parameter value: {0}")]
|
||||
InvalidParameter(&'static str),
|
||||
|
||||
/// Indicates that the API rate limit has been exceeded (HTTP 429).
|
||||
///
|
||||
/// This error is returned after the client has exhausted all automatic retry attempts
|
||||
/// for a rate-limited request. The `retry_after` field contains the number of seconds
|
||||
/// that was specified in the last Retry-After header. By default, the client automatically
|
||||
/// retries rate-limited requests up to 3 times.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// use intel_dcap_api::{ApiClient, IntelApiError};
|
||||
///
|
||||
/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
|
||||
/// let mut client = ApiClient::new()?;
|
||||
/// client.set_max_retries(0); // Disable automatic retries
|
||||
///
|
||||
/// match client.get_sgx_tcb_info("00606A000000", None, None).await {
|
||||
/// Ok(tcb_info) => println!("Success"),
|
||||
/// Err(IntelApiError::TooManyRequests { request_id, retry_after }) => {
|
||||
/// println!("Rate limited after all retries. Last retry-after was {} seconds.", retry_after);
|
||||
/// }
|
||||
/// Err(e) => eprintln!("Other error: {}", e),
|
||||
/// }
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
#[error("Too many requests. Retry after {retry_after} seconds")]
|
||||
TooManyRequests {
|
||||
/// The unique request identifier for tracing.
|
||||
request_id: String,
|
||||
/// Number of seconds to wait before retrying, from Retry-After header.
|
||||
retry_after: u64,
|
||||
},
|
||||
}
|
||||
|
||||
/// Extracts common API error details from response headers.
|
||||
pub(crate) fn extract_api_error_details(
|
||||
response: &Response,
|
||||
) -> (String, Option<String>, Option<String>) {
|
||||
let request_id = response
|
||||
.headers()
|
||||
.get("Request-ID")
|
||||
.and_then(|v| v.to_str().ok())
|
||||
.unwrap_or("Unknown")
|
||||
.to_string();
|
||||
let error_code = response
|
||||
.headers()
|
||||
.get("Error-Code")
|
||||
.and_then(|v| v.to_str().ok())
|
||||
.map(String::from);
|
||||
let error_message = response
|
||||
.headers()
|
||||
.get("Error-Message")
|
||||
.and_then(|v| v.to_str().ok())
|
||||
.map(String::from);
|
||||
(request_id, error_code, error_message)
|
||||
}
|
||||
|
||||
/// Checks the response status and returns an ApiError if it's not one of the expected statuses.
|
||||
pub(crate) async fn check_status(
|
||||
response: Response,
|
||||
expected_statuses: &[StatusCode],
|
||||
) -> Result<Response, IntelApiError> {
|
||||
let status = response.status();
|
||||
if expected_statuses.contains(&status) {
|
||||
Ok(response)
|
||||
} else if status == StatusCode::TOO_MANY_REQUESTS {
|
||||
// Handle 429 Too Many Requests with Retry-After header
|
||||
let request_id = response
|
||||
.headers()
|
||||
.get("Request-ID")
|
||||
.and_then(|v| v.to_str().ok())
|
||||
.unwrap_or("Unknown")
|
||||
.to_string();
|
||||
|
||||
// Parse Retry-After header (can be in seconds or HTTP date format)
|
||||
let retry_after = response
|
||||
.headers()
|
||||
.get("Retry-After")
|
||||
.and_then(|v| v.to_str().ok())
|
||||
.and_then(|v| v.parse::<u64>().ok())
|
||||
.unwrap_or(60); // Default to 60 seconds if header is missing or invalid
|
||||
|
||||
Err(IntelApiError::TooManyRequests {
|
||||
request_id,
|
||||
retry_after,
|
||||
})
|
||||
} else {
|
||||
let (request_id, error_code, error_message) = extract_api_error_details(&response);
|
||||
Err(IntelApiError::ApiError {
|
||||
status,
|
||||
request_id,
|
||||
error_code,
|
||||
error_message,
|
||||
})
|
||||
}
|
||||
}
|
|
@ -1,61 +0,0 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (c) 2025 Matter Labs
|
||||
|
||||
//! Intel API Client
|
||||
//!
|
||||
//! This module provides an API client for interacting with the Intel API for Trusted Services.
|
||||
//! The API follows the documentation found at [Intel API Documentation](https://api.portal.trustedservices.intel.com/content/documentation.html).
|
||||
//!
|
||||
//! Create an [`ApiClient`] to interface with the Intel API.
|
||||
//!
|
||||
//! # Rate Limiting
|
||||
//!
|
||||
//! The Intel API implements rate limiting and may return HTTP 429 (Too Many Requests) responses.
|
||||
//! This client automatically handles rate limiting by retrying requests up to 3 times by default,
|
||||
//! waiting for the duration specified in the `Retry-After` header. You can configure the retry
|
||||
//! behavior using [`ApiClient::set_max_retries`]. If all retries are exhausted, the client
|
||||
//! returns an [`IntelApiError::TooManyRequests`] error.
|
||||
//!
|
||||
//! Example
|
||||
//! ```rust,no_run
|
||||
//! use intel_dcap_api::{ApiClient, IntelApiError, TcbInfoResponse};
|
||||
//!
|
||||
//! #[tokio::main]
|
||||
//! async fn main() -> Result<(), IntelApiError> {
|
||||
//! let client = ApiClient::new()?;
|
||||
//!
|
||||
//! // Example: Get SGX TCB Info
|
||||
//! let fmspc_example = "00606A000000"; // Example FMSPC from docs
|
||||
//! match client.get_sgx_tcb_info(fmspc_example, None, None).await {
|
||||
//! Ok(TcbInfoResponse {
|
||||
//! tcb_info_json,
|
||||
//! issuer_chain,
|
||||
//! }) => println!(
|
||||
//! "SGX TCB Info for {}:\n{}\nIssuer Chain: {}",
|
||||
//! fmspc_example, tcb_info_json, issuer_chain
|
||||
//! ),
|
||||
//! Err(e) => eprintln!("Error getting SGX TCB info: {}", e),
|
||||
//! }
|
||||
//!
|
||||
//! Ok(())
|
||||
//! }
|
||||
//! ```
|
||||
|
||||
#![deny(missing_docs)]
|
||||
#![deny(clippy::all)]
|
||||
|
||||
mod client;
|
||||
mod error;
|
||||
mod requests;
|
||||
mod responses;
|
||||
mod types;
|
||||
|
||||
// Re-export public items
|
||||
pub use client::ApiClient;
|
||||
pub use error::IntelApiError;
|
||||
pub use responses::{
|
||||
AddPackageResponse, EnclaveIdentityJson, EnclaveIdentityResponse, FmspcJsonResponse,
|
||||
PckCertificateResponse, PckCertificatesResponse, PckCrlResponse, TcbEvaluationDataNumbersJson,
|
||||
TcbEvaluationDataNumbersResponse, TcbInfoJson, TcbInfoResponse,
|
||||
};
|
||||
pub use types::{ApiVersion, CaType, CrlEncoding, PlatformFilter, UpdateType};
|
|
@ -1,28 +0,0 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (c) 2025 Matter Labs
|
||||
|
||||
use serde::Serialize;
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub(crate) struct PckCertRequest<'a> {
|
||||
#[serde(rename = "platformManifest")]
|
||||
pub(crate) platform_manifest: &'a str,
|
||||
pub(crate) cpusvn: &'a str,
|
||||
pub(crate) pcesvn: &'a str,
|
||||
pub(crate) pceid: &'a str,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub(crate) struct PckCertsRequest<'a> {
|
||||
#[serde(rename = "platformManifest")]
|
||||
pub(crate) platform_manifest: &'a str,
|
||||
pub(crate) pceid: &'a str,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub(crate) struct PckCertsConfigRequest<'a> {
|
||||
#[serde(rename = "platformManifest")]
|
||||
pub(crate) platform_manifest: &'a str,
|
||||
pub(crate) cpusvn: &'a str,
|
||||
pub(crate) pceid: &'a str,
|
||||
}
|
|
@ -1,111 +0,0 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (c) 2025 Matter Labs
|
||||
|
||||
/// JSON structure as defined in Appendix A of the API spec.
|
||||
/// Content may vary slightly between API v3 and v4.
|
||||
pub type TcbInfoJson = String;
|
||||
|
||||
/// JSON structure as defined in Appendix B of the API spec.
|
||||
/// Content may vary slightly between API v3 and v4.
|
||||
pub type EnclaveIdentityJson = String;
|
||||
|
||||
/// JSON Array of {tcb, tcbm, cert}.
|
||||
/// Content structure expected to be consistent between v3 and v4.
|
||||
pub type PckCertsJsonResponse = String;
|
||||
|
||||
/// JSON Array of {fmspc, platform}.
|
||||
/// Content structure expected to be consistent between v3 and v4.
|
||||
pub type FmspcJsonResponse = String;
|
||||
|
||||
/// JSON structure as defined in Appendix C of the API spec (V4 ONLY).
|
||||
pub type TcbEvaluationDataNumbersJson = String;
|
||||
|
||||
/// Response structure for a PCK (Platform Configuration Key) Certificate.
|
||||
///
|
||||
/// Contains the PCK certificate, its issuer chain, TCB measurement, and FMSPC value.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PckCertificateResponse {
|
||||
/// PEM-encoded PCK certificate.
|
||||
pub pck_cert_pem: String,
|
||||
/// PEM-encoded certificate chain for the PCK certificate issuer.
|
||||
/// Header name differs between v3 ("PCS-Certificate-Issuer-Chain") and v4 ("SGX-PCK-Certificate-Issuer-Chain").
|
||||
pub issuer_chain: String,
|
||||
/// TCBm value associated with the certificate (Hex-encoded).
|
||||
pub tcbm: String,
|
||||
/// FMSPC value associated with the certificate (Hex-encoded).
|
||||
pub fmspc: String,
|
||||
}
|
||||
|
||||
/// Response structure for multiple PCK (Platform Configuration Key) Certificates.
|
||||
///
|
||||
/// Contains a JSON array of PCK certificates, their issuer chain, and the associated FMSPC value.
|
||||
/// This struct represents the response for retrieving multiple PCK certificates from the Intel SGX API.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PckCertificatesResponse {
|
||||
/// JSON array containing PCK certificates and their associated TCB levels.
|
||||
pub pck_certs_json: PckCertsJsonResponse, // String alias for now
|
||||
/// PEM-encoded certificate chain for the PCK certificate issuer.
|
||||
/// Header name differs between v3 ("PCS-Certificate-Issuer-Chain") and v4 ("SGX-PCK-Certificate-Issuer-Chain").
|
||||
pub issuer_chain: String,
|
||||
/// FMSPC value associated with the certificates (Hex-encoded).
|
||||
pub fmspc: String,
|
||||
}
|
||||
|
||||
/// Response structure for TCB (Trusted Computing Base) Information.
|
||||
///
|
||||
/// Contains the JSON representation of TCB information for a specific platform,
|
||||
/// along with the certificate chain of the TCB Info signer.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct TcbInfoResponse {
|
||||
/// JSON containing TCB information for a specific platform (FMSPC).
|
||||
pub tcb_info_json: TcbInfoJson, // String alias for now
|
||||
/// PEM-encoded certificate chain for the TCB Info signer.
|
||||
/// Header name differs slightly between v3 ("SGX-TCB-Info-Issuer-Chain") and v4 ("TCB-Info-Issuer-Chain" - check spec).
|
||||
pub issuer_chain: String,
|
||||
}
|
||||
|
||||
/// Response structure for Enclave Identity Information.
|
||||
///
|
||||
/// Contains the JSON representation of enclave identity details for QE, QvE, or QAE,
|
||||
/// along with its issuer chain.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct EnclaveIdentityResponse {
|
||||
/// JSON containing information about the QE, QvE, or QAE.
|
||||
pub enclave_identity_json: EnclaveIdentityJson, // String alias for now
|
||||
/// PEM-encoded certificate chain for the Enclave Identity signer.
|
||||
/// Header name seems consistent ("SGX-Enclave-Identity-Issuer-Chain").
|
||||
pub issuer_chain: String,
|
||||
}
|
||||
|
||||
/// Response structure for TCB Evaluation Data Numbers (V4 ONLY).
|
||||
///
|
||||
/// Contains the JSON representation of supported TCB Evaluation Data Numbers
|
||||
/// and its corresponding issuer chain.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct TcbEvaluationDataNumbersResponse {
|
||||
/// JSON containing the list of supported TCB Evaluation Data Numbers (V4 ONLY).
|
||||
pub tcb_evaluation_data_numbers_json: TcbEvaluationDataNumbersJson, // String alias for now
|
||||
/// PEM-encoded certificate chain for the TCB Evaluation Data Numbers signer (V4 ONLY).
|
||||
/// Header: "TCB-Evaluation-Data-Numbers-Issuer-Chain".
|
||||
pub issuer_chain: String,
|
||||
}
|
||||
|
||||
/// Response structure for Platform Configuration Key Certificate Revocation List (PCK CRL).
|
||||
///
|
||||
/// Contains the CRL data and its issuer chain for validating platform configuration keys.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PckCrlResponse {
|
||||
/// CRL data (PEM or DER encoded).
|
||||
pub crl_data: Vec<u8>,
|
||||
/// PEM-encoded certificate chain for the CRL issuer.
|
||||
/// Header name differs between v3 ("PCS-CRL-Issuer-Chain") and v4 ("SGX-PCK-CRL-Issuer-Chain").
|
||||
pub issuer_chain: String,
|
||||
}
|
||||
|
||||
/// Response structure for the request to add a package.
|
||||
pub struct AddPackageResponse {
|
||||
/// Platform Membership Certificates
|
||||
pub pck_certs: Vec<u8>,
|
||||
/// The certificate count extracted from the response header.
|
||||
pub pck_cert_count: usize,
|
||||
}
|
|
@ -1,122 +0,0 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (c) 2025 Matter Labs
|
||||
|
||||
use std::fmt;
|
||||
|
||||
/// Represents the type of Certificate Authority (CA) for Intel Trusted Services.
|
||||
///
|
||||
/// This enum defines the different types of Certificate Authorities used in the Intel DCAP API,
|
||||
/// specifically distinguishing between processor and platform CAs.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum CaType {
|
||||
/// Represents a processor-specific Certificate Authority.
|
||||
Processor,
|
||||
/// Represents a platform-wide Certificate Authority.
|
||||
Platform,
|
||||
}
|
||||
|
||||
impl fmt::Display for CaType {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
CaType::Processor => write!(f, "processor"),
|
||||
CaType::Platform => write!(f, "platform"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents the encoding format for Certificate Revocation Lists (CRLs).
|
||||
///
|
||||
/// This enum defines the supported encoding formats for CRLs in the Intel DCAP API,
|
||||
/// distinguishing between PEM (Privacy Enhanced Mail) and DER (Distinguished Encoding Rules) formats.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum CrlEncoding {
|
||||
/// Represents the PEM (Privacy Enhanced Mail) encoding format.
|
||||
Pem,
|
||||
/// Represents the DER (Distinguished Encoding Rules) encoding format.
|
||||
Der,
|
||||
}
|
||||
|
||||
impl fmt::Display for CrlEncoding {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
CrlEncoding::Pem => write!(f, "pem"),
|
||||
CrlEncoding::Der => write!(f, "der"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents the type of update for Intel Trusted Services.
|
||||
///
|
||||
/// This enum defines different update types, distinguishing between early and standard updates
|
||||
/// in the Intel DCAP (Data Center Attestation Primitives) API.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum UpdateType {
|
||||
/// Represents early updates, typically used for preview or beta releases.
|
||||
Early,
|
||||
/// Represents standard updates, which are the regular release cycle.
|
||||
Standard,
|
||||
}
|
||||
|
||||
impl fmt::Display for UpdateType {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
UpdateType::Early => write!(f, "early"),
|
||||
UpdateType::Standard => write!(f, "standard"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents the platform filter options for Intel DCAP (Data Center Attestation Primitives) API.
|
||||
///
|
||||
/// This enum allows filtering platforms based on different criteria,
|
||||
/// such as selecting all platforms, client-specific platforms, or specific Intel processor generations.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum PlatformFilter {
|
||||
/// Represents a selection of all available platforms.
|
||||
All,
|
||||
/// Represents a selection of client-specific platforms.
|
||||
Client,
|
||||
/// Represents platforms with Intel E3 processors.
|
||||
E3,
|
||||
/// Represents platforms with Intel E5 processors.
|
||||
E5,
|
||||
}
|
||||
|
||||
impl fmt::Display for PlatformFilter {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
PlatformFilter::All => write!(f, "all"),
|
||||
PlatformFilter::Client => write!(f, "client"),
|
||||
PlatformFilter::E3 => write!(f, "E3"),
|
||||
PlatformFilter::E5 => write!(f, "E5"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents the version of the Intel Trusted Services API to target.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum ApiVersion {
|
||||
/// Represents version 3 of the Intel Trusted Services API.
|
||||
V3,
|
||||
/// Represents version 4 of the Intel Trusted Services API.
|
||||
V4,
|
||||
}
|
||||
|
||||
impl ApiVersion {
|
||||
/// Returns the string representation of the version for URL paths.
|
||||
pub fn path_segment(&self) -> &'static str {
|
||||
match self {
|
||||
ApiVersion::V3 => "v3",
|
||||
ApiVersion::V4 => "v4",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for ApiVersion {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
ApiVersion::V3 => write!(f, "v3"),
|
||||
ApiVersion::V4 => write!(f, "v4"),
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,312 +0,0 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (c) 2025 Matter Labs
|
||||
|
||||
use intel_dcap_api::{ApiClient, ApiVersion, CaType, IntelApiError, UpdateType};
|
||||
use mockito::Server;
|
||||
use reqwest::Client;
|
||||
|
||||
// Create a test client without TLS requirements
|
||||
async fn create_test_client(base_url: &str) -> ApiClient {
|
||||
// Create a custom client without TLS requirements for testing
|
||||
ApiClient::new_with_base_url(base_url).expect("Failed to create client")
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_simple_request() {
|
||||
let mut server = Server::new_async().await;
|
||||
|
||||
// First, test with plain reqwest to ensure mock works
|
||||
let _m = server
|
||||
.mock("GET", "/test")
|
||||
.with_status(200)
|
||||
.with_body("test")
|
||||
.create_async()
|
||||
.await;
|
||||
|
||||
let client = Client::new();
|
||||
let resp = client
|
||||
.get(format!("{}/test", server.url()))
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(resp.status(), 200);
|
||||
assert_eq!(resp.text().await.unwrap(), "test");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_tdx_tcb_minimal() {
|
||||
let mut server = Server::new_async().await;
|
||||
|
||||
// Use minimal response
|
||||
let _m = server
|
||||
.mock("GET", "/tdx/certification/v4/tcb")
|
||||
.match_query(mockito::Matcher::UrlEncoded(
|
||||
"fmspc".into(),
|
||||
"test123".into(),
|
||||
))
|
||||
.with_status(200)
|
||||
.with_header("TCB-Info-Issuer-Chain", "test-cert")
|
||||
.with_body("{}")
|
||||
.create_async()
|
||||
.await;
|
||||
|
||||
let client = create_test_client(&server.url()).await;
|
||||
let result = client.get_tdx_tcb_info("test123", None, None).await;
|
||||
|
||||
match &result {
|
||||
Ok(resp) => {
|
||||
assert_eq!(resp.tcb_info_json, "{}");
|
||||
assert_eq!(resp.issuer_chain, "test-cert");
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("Error: {:?}", e);
|
||||
panic!("Request failed");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_sgx_qe_identity_minimal() {
|
||||
let mut server = Server::new_async().await;
|
||||
|
||||
let _m = server
|
||||
.mock("GET", "/sgx/certification/v4/qe/identity")
|
||||
.with_status(200)
|
||||
.with_header("SGX-Enclave-Identity-Issuer-Chain", "test-cert")
|
||||
.with_body("{}")
|
||||
.create_async()
|
||||
.await;
|
||||
|
||||
let client = create_test_client(&server.url()).await;
|
||||
let result = client.get_sgx_qe_identity(None, None).await;
|
||||
|
||||
assert!(result.is_ok());
|
||||
let resp = result.unwrap();
|
||||
assert_eq!(resp.enclave_identity_json, "{}");
|
||||
assert_eq!(resp.issuer_chain, "test-cert");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_pck_crl_minimal() {
|
||||
let mut server = Server::new_async().await;
|
||||
|
||||
let _m = server
|
||||
.mock("GET", "/sgx/certification/v4/pckcrl")
|
||||
.match_query(mockito::Matcher::UrlEncoded(
|
||||
"ca".into(),
|
||||
"processor".into(),
|
||||
))
|
||||
.with_status(200)
|
||||
.with_header("SGX-PCK-CRL-Issuer-Chain", "test-cert")
|
||||
.with_body("test-crl")
|
||||
.create_async()
|
||||
.await;
|
||||
|
||||
let client = create_test_client(&server.url()).await;
|
||||
let result = client.get_pck_crl(CaType::Processor, None).await;
|
||||
|
||||
assert!(result.is_ok());
|
||||
let resp = result.unwrap();
|
||||
assert_eq!(String::from_utf8_lossy(&resp.crl_data), "test-crl");
|
||||
assert_eq!(resp.issuer_chain, "test-cert");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_error_handling() {
|
||||
let mut server = Server::new_async().await;
|
||||
|
||||
let _m = server
|
||||
.mock("GET", "/sgx/certification/v4/tcb")
|
||||
.match_query(mockito::Matcher::UrlEncoded("fmspc".into(), "bad".into()))
|
||||
.with_status(404)
|
||||
.with_header("Request-ID", "test-123")
|
||||
.create_async()
|
||||
.await;
|
||||
|
||||
let client = create_test_client(&server.url()).await;
|
||||
let result = client.get_sgx_tcb_info("bad", None, None).await;
|
||||
|
||||
assert!(result.is_err());
|
||||
match result.unwrap_err() {
|
||||
IntelApiError::ApiError {
|
||||
status, request_id, ..
|
||||
} => {
|
||||
assert_eq!(status.as_u16(), 404);
|
||||
assert_eq!(request_id, "test-123");
|
||||
}
|
||||
_ => panic!("Wrong error type"),
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_update_types() {
|
||||
let mut server = Server::new_async().await;
|
||||
|
||||
// Test Early update type
|
||||
let _m1 = server
|
||||
.mock("GET", "/tdx/certification/v4/tcb")
|
||||
.match_query(mockito::Matcher::AllOf(vec![
|
||||
mockito::Matcher::UrlEncoded("fmspc".into(), "test".into()),
|
||||
mockito::Matcher::UrlEncoded("update".into(), "early".into()),
|
||||
]))
|
||||
.with_status(200)
|
||||
.with_header("TCB-Info-Issuer-Chain", "cert")
|
||||
.with_body("{\"early\":true}")
|
||||
.create_async()
|
||||
.await;
|
||||
|
||||
let client = create_test_client(&server.url()).await;
|
||||
let result = client
|
||||
.get_tdx_tcb_info("test", Some(UpdateType::Early), None)
|
||||
.await;
|
||||
assert!(result.is_ok());
|
||||
assert_eq!(result.unwrap().tcb_info_json, "{\"early\":true}");
|
||||
|
||||
// Test Standard update type
|
||||
let _m2 = server
|
||||
.mock("GET", "/tdx/certification/v4/tcb")
|
||||
.match_query(mockito::Matcher::AllOf(vec![
|
||||
mockito::Matcher::UrlEncoded("fmspc".into(), "test".into()),
|
||||
mockito::Matcher::UrlEncoded("update".into(), "standard".into()),
|
||||
]))
|
||||
.with_status(200)
|
||||
.with_header("TCB-Info-Issuer-Chain", "cert")
|
||||
.with_body("{\"standard\":true}")
|
||||
.create_async()
|
||||
.await;
|
||||
|
||||
let result2 = client
|
||||
.get_tdx_tcb_info("test", Some(UpdateType::Standard), None)
|
||||
.await;
|
||||
assert!(result2.is_ok());
|
||||
assert_eq!(result2.unwrap().tcb_info_json, "{\"standard\":true}");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_v3_api_headers() {
|
||||
let mut server = Server::new_async().await;
|
||||
|
||||
// V3 uses different header names for CRL
|
||||
let _m = server
|
||||
.mock("GET", "/sgx/certification/v3/pckcrl")
|
||||
.match_query(mockito::Matcher::UrlEncoded("ca".into(), "platform".into()))
|
||||
.with_status(200)
|
||||
.with_header("SGX-PCK-CRL-Issuer-Chain", "v3-cert")
|
||||
.with_body("v3-crl-data")
|
||||
.create_async()
|
||||
.await;
|
||||
|
||||
let client = ApiClient::new_with_options(server.url(), ApiVersion::V3).unwrap();
|
||||
let result = client.get_pck_crl(CaType::Platform, None).await;
|
||||
|
||||
assert!(result.is_ok());
|
||||
let resp = result.unwrap();
|
||||
assert_eq!(String::from_utf8_lossy(&resp.crl_data), "v3-crl-data");
|
||||
assert_eq!(resp.issuer_chain, "v3-cert");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_sgx_qve_identity() {
|
||||
let mut server = Server::new_async().await;
|
||||
|
||||
let _m = server
|
||||
.mock("GET", "/sgx/certification/v4/qve/identity")
|
||||
.with_status(200)
|
||||
.with_header("SGX-Enclave-Identity-Issuer-Chain", "qve-cert")
|
||||
.with_body("{\"id\":\"QVE\"}")
|
||||
.create_async()
|
||||
.await;
|
||||
|
||||
let client = create_test_client(&server.url()).await;
|
||||
let result = client.get_sgx_qve_identity(None, None).await;
|
||||
|
||||
assert!(result.is_ok());
|
||||
let resp = result.unwrap();
|
||||
assert_eq!(resp.enclave_identity_json, "{\"id\":\"QVE\"}");
|
||||
assert_eq!(resp.issuer_chain, "qve-cert");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_tdx_qe_identity() {
|
||||
let mut server = Server::new_async().await;
|
||||
|
||||
let _m = server
|
||||
.mock("GET", "/tdx/certification/v4/qe/identity")
|
||||
.with_status(200)
|
||||
.with_header("SGX-Enclave-Identity-Issuer-Chain", "tdx-qe-cert")
|
||||
.with_body("{\"id\":\"TDX-QE\"}")
|
||||
.create_async()
|
||||
.await;
|
||||
|
||||
let client = create_test_client(&server.url()).await;
|
||||
let result = client.get_tdx_qe_identity(None, None).await;
|
||||
|
||||
assert!(result.is_ok());
|
||||
let resp = result.unwrap();
|
||||
assert_eq!(resp.enclave_identity_json, "{\"id\":\"TDX-QE\"}");
|
||||
assert_eq!(resp.issuer_chain, "tdx-qe-cert");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_error_with_details() {
|
||||
let mut server = Server::new_async().await;
|
||||
|
||||
let _m = server
|
||||
.mock("GET", "/sgx/certification/v4/pckcert")
|
||||
.match_query(mockito::Matcher::Any)
|
||||
.with_status(400)
|
||||
.with_header("Request-ID", "error-req-123")
|
||||
.with_header("Error-Code", "InvalidParameter")
|
||||
.with_header("Error-Message", "PPID format is invalid")
|
||||
.create_async()
|
||||
.await;
|
||||
|
||||
let client = create_test_client(&server.url()).await;
|
||||
let result = client
|
||||
.get_pck_certificate_by_ppid("bad", "bad", "bad", "bad", None, None)
|
||||
.await;
|
||||
|
||||
assert!(result.is_err());
|
||||
match result.unwrap_err() {
|
||||
IntelApiError::ApiError {
|
||||
status,
|
||||
request_id,
|
||||
error_code,
|
||||
error_message,
|
||||
} => {
|
||||
assert_eq!(status.as_u16(), 400);
|
||||
assert_eq!(request_id, "error-req-123");
|
||||
assert_eq!(error_code.as_deref(), Some("InvalidParameter"));
|
||||
assert_eq!(error_message.as_deref(), Some("PPID format is invalid"));
|
||||
}
|
||||
_ => panic!("Wrong error type"),
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_sgx_tcb_info() {
|
||||
let mut server = Server::new_async().await;
|
||||
|
||||
let _m = server
|
||||
.mock("GET", "/sgx/certification/v4/tcb")
|
||||
.match_query(mockito::Matcher::UrlEncoded(
|
||||
"fmspc".into(),
|
||||
"00606A6A0000".into(),
|
||||
))
|
||||
.with_status(200)
|
||||
.with_header("TCB-Info-Issuer-Chain", "sgx-tcb-cert")
|
||||
.with_body("{\"tcbInfo\":{\"fmspc\":\"00606A6A0000\"}}")
|
||||
.create_async()
|
||||
.await;
|
||||
|
||||
let client = create_test_client(&server.url()).await;
|
||||
let result = client.get_sgx_tcb_info("00606A6A0000", None, None).await;
|
||||
|
||||
assert!(result.is_ok());
|
||||
let resp = result.unwrap();
|
||||
assert_eq!(
|
||||
resp.tcb_info_json,
|
||||
"{\"tcbInfo\":{\"fmspc\":\"00606A6A0000\"}}"
|
||||
);
|
||||
assert_eq!(resp.issuer_chain, "sgx-tcb-cert");
|
||||
}
|
|
@ -1,901 +0,0 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (c) 2025 Matter Labs
|
||||
|
||||
use intel_dcap_api::{
|
||||
ApiClient, ApiVersion, CaType, CrlEncoding, IntelApiError, PlatformFilter, UpdateType,
|
||||
};
|
||||
use mockito::Server;
|
||||
use percent_encoding::{percent_encode, NON_ALPHANUMERIC};
|
||||
use serde_json::Value;
|
||||
|
||||
// Include real test data
|
||||
const TDX_TCB_INFO_DATA: &[u8] = include_bytes!("test_data/tdx_tcb_info.json");
|
||||
const PCK_CRL_PROCESSOR_DATA: &[u8] = include_bytes!("test_data/pck_crl_processor.json");
|
||||
const PCK_CRL_PLATFORM_DATA: &[u8] = include_bytes!("test_data/pck_crl_platform.json");
|
||||
const SGX_QE_IDENTITY_DATA: &[u8] = include_bytes!("test_data/sgx_qe_identity.json");
|
||||
const SGX_QVE_IDENTITY_DATA: &[u8] = include_bytes!("test_data/sgx_qve_identity.json");
|
||||
const TDX_QE_IDENTITY_DATA: &[u8] = include_bytes!("test_data/tdx_qe_identity.json");
|
||||
const SGX_TCB_INFO_ALT_DATA: &[u8] = include_bytes!("test_data/sgx_tcb_info_alt.json");
|
||||
const SGX_QAE_IDENTITY_DATA: &[u8] = include_bytes!("test_data/sgx_qae_identity.json");
|
||||
const FMSPCS_DATA: &[u8] = include_bytes!("test_data/fmspcs.json");
|
||||
const SGX_TCB_EVAL_NUMS_DATA: &[u8] = include_bytes!("test_data/sgx_tcb_eval_nums.json");
|
||||
const TDX_TCB_EVAL_NUMS_DATA: &[u8] = include_bytes!("test_data/tdx_tcb_eval_nums.json");
|
||||
const PCK_CRL_PROCESSOR_DER_DATA: &[u8] = include_bytes!("test_data/pck_crl_processor_der.json");
|
||||
const SGX_TCB_INFO_EARLY_DATA: &[u8] = include_bytes!("test_data/sgx_tcb_info_early.json");
|
||||
const TDX_TCB_INFO_EVAL17_DATA: &[u8] = include_bytes!("test_data/tdx_tcb_info_eval17.json");
|
||||
const FMSPCS_NO_FILTER_DATA: &[u8] = include_bytes!("test_data/fmspcs_no_filter.json");
|
||||
// const FMSPCS_ALL_PLATFORMS_DATA: &[u8] = include_bytes!("test_data/fmspcs_all_platforms.json"); // Reserved for future use
|
||||
const SGX_QE_IDENTITY_V3_DATA: &[u8] = include_bytes!("test_data/sgx_qe_identity_v3.json");
|
||||
const SGX_TCB_INFO_V3_DATA: &[u8] = include_bytes!("test_data/sgx_tcb_info_v3.json");
|
||||
const TDX_TCB_INFO_ALT_DATA: &[u8] = include_bytes!("test_data/tdx_tcb_info_00806F050000.json");
|
||||
|
||||
fn parse_test_data(data: &[u8]) -> Value {
|
||||
serde_json::from_slice(data).expect("Failed to parse test data")
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_tdx_tcb_info_with_real_data() {
|
||||
let mut server = Server::new_async().await;
|
||||
let test_data = parse_test_data(TDX_TCB_INFO_DATA);
|
||||
|
||||
// URL encode the issuer chain header value
|
||||
let issuer_chain = test_data["issuer_chain"].as_str().unwrap();
|
||||
let encoded_issuer_chain =
|
||||
percent_encode(issuer_chain.as_bytes(), NON_ALPHANUMERIC).to_string();
|
||||
|
||||
let _m = server
|
||||
.mock("GET", "/tdx/certification/v4/tcb")
|
||||
.match_query(mockito::Matcher::UrlEncoded(
|
||||
"fmspc".into(),
|
||||
"00806F050000".into(),
|
||||
))
|
||||
.with_status(200)
|
||||
.with_header("Content-Type", "application/json")
|
||||
.with_header("TCB-Info-Issuer-Chain", &encoded_issuer_chain)
|
||||
.with_body(test_data["tcb_info_json"].as_str().unwrap())
|
||||
.create_async()
|
||||
.await;
|
||||
|
||||
let client = ApiClient::new_with_base_url(server.url()).unwrap();
|
||||
let result = client.get_tdx_tcb_info("00806F050000", None, None).await;
|
||||
|
||||
if let Err(e) = &result {
|
||||
eprintln!("Error: {:?}", e);
|
||||
eprintln!("Server URL: {}", server.url());
|
||||
}
|
||||
|
||||
assert!(result.is_ok());
|
||||
let response = result.unwrap();
|
||||
assert_eq!(
|
||||
response.tcb_info_json,
|
||||
test_data["tcb_info_json"].as_str().unwrap()
|
||||
);
|
||||
assert_eq!(
|
||||
response.issuer_chain,
|
||||
test_data["issuer_chain"].as_str().unwrap()
|
||||
);
|
||||
|
||||
// Verify the JSON can be parsed
|
||||
let tcb_info: Value = serde_json::from_str(&response.tcb_info_json).unwrap();
|
||||
assert_eq!(tcb_info["tcbInfo"]["fmspc"], "00806F050000");
|
||||
assert_eq!(tcb_info["tcbInfo"]["id"], "TDX");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_sgx_qe_identity_with_real_data() {
|
||||
let mut server = Server::new_async().await;
|
||||
let test_data = parse_test_data(SGX_QE_IDENTITY_DATA);
|
||||
|
||||
let issuer_chain = test_data["issuer_chain"].as_str().unwrap();
|
||||
let encoded_issuer_chain =
|
||||
percent_encode(issuer_chain.as_bytes(), NON_ALPHANUMERIC).to_string();
|
||||
|
||||
let _m = server
|
||||
.mock("GET", "/sgx/certification/v4/qe/identity")
|
||||
.with_status(200)
|
||||
.with_header("Content-Type", "application/json")
|
||||
.with_header("SGX-Enclave-Identity-Issuer-Chain", &encoded_issuer_chain)
|
||||
.with_body(test_data["enclave_identity_json"].as_str().unwrap())
|
||||
.create_async()
|
||||
.await;
|
||||
|
||||
let client = ApiClient::new_with_base_url(server.url()).unwrap();
|
||||
let result = client.get_sgx_qe_identity(None, None).await;
|
||||
|
||||
assert!(result.is_ok());
|
||||
let response = result.unwrap();
|
||||
assert_eq!(
|
||||
response.enclave_identity_json,
|
||||
test_data["enclave_identity_json"].as_str().unwrap()
|
||||
);
|
||||
assert_eq!(
|
||||
response.issuer_chain,
|
||||
test_data["issuer_chain"].as_str().unwrap()
|
||||
);
|
||||
|
||||
// Verify the JSON structure
|
||||
let identity: Value = serde_json::from_str(&response.enclave_identity_json).unwrap();
|
||||
assert_eq!(identity["enclaveIdentity"]["id"], "QE");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_sgx_qve_identity_with_real_data() {
|
||||
let mut server = Server::new_async().await;
|
||||
let test_data = parse_test_data(SGX_QVE_IDENTITY_DATA);
|
||||
|
||||
let issuer_chain = test_data["issuer_chain"].as_str().unwrap();
|
||||
let encoded_issuer_chain =
|
||||
percent_encode(issuer_chain.as_bytes(), NON_ALPHANUMERIC).to_string();
|
||||
|
||||
let _m = server
|
||||
.mock("GET", "/sgx/certification/v4/qve/identity")
|
||||
.with_status(200)
|
||||
.with_header("Content-Type", "application/json")
|
||||
.with_header("SGX-Enclave-Identity-Issuer-Chain", &encoded_issuer_chain)
|
||||
.with_body(test_data["enclave_identity_json"].as_str().unwrap())
|
||||
.create_async()
|
||||
.await;
|
||||
|
||||
let client = ApiClient::new_with_base_url(server.url()).unwrap();
|
||||
let result = client.get_sgx_qve_identity(None, None).await;
|
||||
|
||||
assert!(result.is_ok());
|
||||
let response = result.unwrap();
|
||||
|
||||
// Verify the JSON structure
|
||||
let identity: Value = serde_json::from_str(&response.enclave_identity_json).unwrap();
|
||||
assert_eq!(identity["enclaveIdentity"]["id"], "QVE");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_tdx_qe_identity_with_real_data() {
|
||||
let mut server = Server::new_async().await;
|
||||
let test_data = parse_test_data(TDX_QE_IDENTITY_DATA);
|
||||
|
||||
let issuer_chain = test_data["issuer_chain"].as_str().unwrap();
|
||||
let encoded_issuer_chain =
|
||||
percent_encode(issuer_chain.as_bytes(), NON_ALPHANUMERIC).to_string();
|
||||
|
||||
let _m = server
|
||||
.mock("GET", "/tdx/certification/v4/qe/identity")
|
||||
.with_status(200)
|
||||
.with_header("Content-Type", "application/json")
|
||||
.with_header("SGX-Enclave-Identity-Issuer-Chain", &encoded_issuer_chain)
|
||||
.with_body(test_data["enclave_identity_json"].as_str().unwrap())
|
||||
.create_async()
|
||||
.await;
|
||||
|
||||
let client = ApiClient::new_with_base_url(server.url()).unwrap();
|
||||
let result = client.get_tdx_qe_identity(None, None).await;
|
||||
|
||||
assert!(result.is_ok());
|
||||
let response = result.unwrap();
|
||||
|
||||
// Verify the JSON structure
|
||||
let identity: Value = serde_json::from_str(&response.enclave_identity_json).unwrap();
|
||||
assert_eq!(identity["enclaveIdentity"]["id"], "TD_QE");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_pck_crl_processor_with_real_data() {
|
||||
let mut server = Server::new_async().await;
|
||||
let test_data = parse_test_data(PCK_CRL_PROCESSOR_DATA);
|
||||
|
||||
let issuer_chain = test_data["issuer_chain"].as_str().unwrap();
|
||||
let encoded_issuer_chain =
|
||||
percent_encode(issuer_chain.as_bytes(), NON_ALPHANUMERIC).to_string();
|
||||
|
||||
let _m = server
|
||||
.mock("GET", "/sgx/certification/v4/pckcrl")
|
||||
.match_query(mockito::Matcher::UrlEncoded(
|
||||
"ca".into(),
|
||||
"processor".into(),
|
||||
))
|
||||
.with_status(200)
|
||||
.with_header("SGX-PCK-CRL-Issuer-Chain", &encoded_issuer_chain)
|
||||
.with_body(test_data["crl_data"].as_str().unwrap())
|
||||
.create_async()
|
||||
.await;
|
||||
|
||||
let client = ApiClient::new_with_base_url(server.url()).unwrap();
|
||||
let result = client.get_pck_crl(CaType::Processor, None).await;
|
||||
|
||||
assert!(result.is_ok());
|
||||
let response = result.unwrap();
|
||||
assert_eq!(
|
||||
String::from_utf8_lossy(&response.crl_data),
|
||||
test_data["crl_data"].as_str().unwrap()
|
||||
);
|
||||
assert_eq!(
|
||||
response.issuer_chain,
|
||||
test_data["issuer_chain"].as_str().unwrap()
|
||||
);
|
||||
|
||||
// Verify it's a valid CRL format
|
||||
let crl_str = String::from_utf8_lossy(&response.crl_data);
|
||||
assert!(crl_str.contains("BEGIN X509 CRL"));
|
||||
assert!(crl_str.contains("END X509 CRL"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_pck_crl_platform_with_real_data() {
|
||||
let mut server = Server::new_async().await;
|
||||
let test_data = parse_test_data(PCK_CRL_PLATFORM_DATA);
|
||||
|
||||
let issuer_chain = test_data["issuer_chain"].as_str().unwrap();
|
||||
let encoded_issuer_chain =
|
||||
percent_encode(issuer_chain.as_bytes(), NON_ALPHANUMERIC).to_string();
|
||||
|
||||
let _m = server
|
||||
.mock("GET", "/sgx/certification/v4/pckcrl")
|
||||
.match_query(mockito::Matcher::UrlEncoded("ca".into(), "platform".into()))
|
||||
.with_status(200)
|
||||
.with_header("SGX-PCK-CRL-Issuer-Chain", &encoded_issuer_chain)
|
||||
.with_body(test_data["crl_data"].as_str().unwrap())
|
||||
.create_async()
|
||||
.await;
|
||||
|
||||
let client = ApiClient::new_with_base_url(server.url()).unwrap();
|
||||
let result = client.get_pck_crl(CaType::Platform, None).await;
|
||||
|
||||
assert!(result.is_ok());
|
||||
let response = result.unwrap();
|
||||
|
||||
// Verify issuer chain contains multiple certificates
|
||||
assert!(response.issuer_chain.contains("BEGIN CERTIFICATE"));
|
||||
assert!(response.issuer_chain.contains("END CERTIFICATE"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_sgx_tcb_info_alt_with_real_data() {
|
||||
let mut server = Server::new_async().await;
|
||||
let test_data = parse_test_data(SGX_TCB_INFO_ALT_DATA);
|
||||
|
||||
let issuer_chain = test_data["issuer_chain"].as_str().unwrap();
|
||||
let encoded_issuer_chain =
|
||||
percent_encode(issuer_chain.as_bytes(), NON_ALPHANUMERIC).to_string();
|
||||
|
||||
let _m = server
|
||||
.mock("GET", "/sgx/certification/v4/tcb")
|
||||
.match_query(mockito::Matcher::UrlEncoded(
|
||||
"fmspc".into(),
|
||||
"00906ED50000".into(),
|
||||
))
|
||||
.with_status(200)
|
||||
.with_header("Content-Type", "application/json")
|
||||
.with_header("TCB-Info-Issuer-Chain", &encoded_issuer_chain)
|
||||
.with_body(test_data["tcb_info_json"].as_str().unwrap())
|
||||
.create_async()
|
||||
.await;
|
||||
|
||||
let client = ApiClient::new_with_base_url(server.url()).unwrap();
|
||||
let result = client.get_sgx_tcb_info("00906ED50000", None, None).await;
|
||||
|
||||
assert!(result.is_ok());
|
||||
let response = result.unwrap();
|
||||
|
||||
// Verify the JSON structure
|
||||
let tcb_info: Value = serde_json::from_str(&response.tcb_info_json).unwrap();
|
||||
assert_eq!(tcb_info["tcbInfo"]["fmspc"], "00906ED50000");
|
||||
assert_eq!(tcb_info["tcbInfo"]["id"], "SGX");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_tdx_tcb_with_update_type() {
|
||||
let mut server = Server::new_async().await;
|
||||
let test_data = parse_test_data(TDX_TCB_INFO_DATA);
|
||||
|
||||
// Test with Early update type
|
||||
let issuer_chain = test_data["issuer_chain"].as_str().unwrap();
|
||||
let encoded_issuer_chain =
|
||||
percent_encode(issuer_chain.as_bytes(), NON_ALPHANUMERIC).to_string();
|
||||
|
||||
let _m1 = server
|
||||
.mock("GET", "/tdx/certification/v4/tcb")
|
||||
.match_query(mockito::Matcher::AllOf(vec![
|
||||
mockito::Matcher::UrlEncoded("fmspc".into(), "00806F050000".into()),
|
||||
mockito::Matcher::UrlEncoded("update".into(), "early".into()),
|
||||
]))
|
||||
.with_status(200)
|
||||
.with_header("TCB-Info-Issuer-Chain", &encoded_issuer_chain)
|
||||
.with_body(test_data["tcb_info_json"].as_str().unwrap())
|
||||
.create_async()
|
||||
.await;
|
||||
|
||||
let client = ApiClient::new_with_base_url(server.url()).unwrap();
|
||||
let result = client
|
||||
.get_tdx_tcb_info("00806F050000", Some(UpdateType::Early), None)
|
||||
.await;
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_error_handling_with_intel_headers() {
|
||||
let mut server = Server::new_async().await;
|
||||
|
||||
// Real error response from Intel API
|
||||
let _m = server
|
||||
.mock("GET", "/sgx/certification/v4/tcb")
|
||||
.match_query(mockito::Matcher::UrlEncoded(
|
||||
"fmspc".into(),
|
||||
"invalid".into(),
|
||||
))
|
||||
.with_status(404)
|
||||
.with_header("Request-ID", "abc123def456")
|
||||
.create_async()
|
||||
.await;
|
||||
|
||||
let client = ApiClient::new_with_base_url(server.url()).unwrap();
|
||||
let result = client.get_sgx_tcb_info("invalid", None, None).await;
|
||||
|
||||
assert!(result.is_err());
|
||||
match result.unwrap_err() {
|
||||
IntelApiError::ApiError {
|
||||
status, request_id, ..
|
||||
} => {
|
||||
assert_eq!(status.as_u16(), 404);
|
||||
assert_eq!(request_id, "abc123def456");
|
||||
}
|
||||
_ => panic!("Expected ApiError"),
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_v3_api_with_real_data() {
|
||||
let mut server = Server::new_async().await;
|
||||
let test_data = parse_test_data(PCK_CRL_PROCESSOR_DATA);
|
||||
|
||||
// V3 uses different header names
|
||||
let issuer_chain = test_data["issuer_chain"].as_str().unwrap();
|
||||
let encoded_issuer_chain =
|
||||
percent_encode(issuer_chain.as_bytes(), NON_ALPHANUMERIC).to_string();
|
||||
|
||||
let _m = server
|
||||
.mock("GET", "/sgx/certification/v3/pckcrl")
|
||||
.match_query(mockito::Matcher::UrlEncoded(
|
||||
"ca".into(),
|
||||
"processor".into(),
|
||||
))
|
||||
.with_status(200)
|
||||
.with_header("SGX-PCK-CRL-Issuer-Chain", &encoded_issuer_chain)
|
||||
.with_body(test_data["crl_data"].as_str().unwrap())
|
||||
.create_async()
|
||||
.await;
|
||||
|
||||
let client = ApiClient::new_with_options(server.url(), ApiVersion::V3).unwrap();
|
||||
let result = client.get_pck_crl(CaType::Processor, None).await;
|
||||
|
||||
assert!(result.is_ok());
|
||||
let response = result.unwrap();
|
||||
assert_eq!(
|
||||
String::from_utf8_lossy(&response.crl_data),
|
||||
test_data["crl_data"].as_str().unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_sgx_qae_identity_with_real_data() {
|
||||
let mut server = Server::new_async().await;
|
||||
let test_data = parse_test_data(SGX_QAE_IDENTITY_DATA);
|
||||
|
||||
let issuer_chain = test_data["issuer_chain"].as_str().unwrap();
|
||||
let encoded_issuer_chain =
|
||||
percent_encode(issuer_chain.as_bytes(), NON_ALPHANUMERIC).to_string();
|
||||
|
||||
let _m = server
|
||||
.mock("GET", "/sgx/certification/v4/qae/identity")
|
||||
.with_status(200)
|
||||
.with_header("Content-Type", "application/json")
|
||||
.with_header("SGX-Enclave-Identity-Issuer-Chain", &encoded_issuer_chain)
|
||||
.with_body(test_data["enclave_identity_json"].as_str().unwrap())
|
||||
.create_async()
|
||||
.await;
|
||||
|
||||
let client = ApiClient::new_with_base_url(server.url()).unwrap();
|
||||
let result = client.get_sgx_qae_identity(None, None).await;
|
||||
|
||||
assert!(result.is_ok());
|
||||
let response = result.unwrap();
|
||||
assert_eq!(
|
||||
response.enclave_identity_json,
|
||||
test_data["enclave_identity_json"].as_str().unwrap()
|
||||
);
|
||||
assert_eq!(
|
||||
response.issuer_chain,
|
||||
test_data["issuer_chain"].as_str().unwrap()
|
||||
);
|
||||
|
||||
// Verify the JSON structure
|
||||
let identity: Value = serde_json::from_str(&response.enclave_identity_json).unwrap();
|
||||
assert_eq!(identity["enclaveIdentity"]["id"], "QAE");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_get_fmspcs_with_real_data() {
|
||||
let mut server = Server::new_async().await;
|
||||
let test_data = parse_test_data(FMSPCS_DATA);
|
||||
|
||||
let _m = server
|
||||
.mock("GET", "/sgx/certification/v4/fmspcs")
|
||||
.match_query(mockito::Matcher::UrlEncoded(
|
||||
"platform".into(),
|
||||
"all".into(),
|
||||
))
|
||||
.with_status(200)
|
||||
.with_header("Content-Type", "application/json")
|
||||
.with_body(test_data["fmspcs_json"].as_str().unwrap())
|
||||
.create_async()
|
||||
.await;
|
||||
|
||||
let client = ApiClient::new_with_base_url(server.url()).unwrap();
|
||||
let result = client.get_fmspcs(Some(PlatformFilter::All)).await;
|
||||
|
||||
assert!(result.is_ok());
|
||||
let fmspcs_json = result.unwrap();
|
||||
assert_eq!(fmspcs_json, test_data["fmspcs_json"].as_str().unwrap());
|
||||
|
||||
// Verify the JSON structure
|
||||
let fmspcs: Value = serde_json::from_str(&fmspcs_json).unwrap();
|
||||
assert!(fmspcs.is_array());
|
||||
assert!(!fmspcs.as_array().unwrap().is_empty());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_sgx_tcb_evaluation_data_numbers_with_real_data() {
|
||||
let mut server = Server::new_async().await;
|
||||
let test_data = parse_test_data(SGX_TCB_EVAL_NUMS_DATA);
|
||||
|
||||
let issuer_chain = test_data["issuer_chain"].as_str().unwrap();
|
||||
let encoded_issuer_chain =
|
||||
percent_encode(issuer_chain.as_bytes(), NON_ALPHANUMERIC).to_string();
|
||||
|
||||
let _m = server
|
||||
.mock("GET", "/sgx/certification/v4/tcbevaluationdatanumbers")
|
||||
.with_status(200)
|
||||
.with_header("Content-Type", "application/json")
|
||||
.with_header(
|
||||
"TCB-Evaluation-Data-Numbers-Issuer-Chain",
|
||||
&encoded_issuer_chain,
|
||||
)
|
||||
.with_body(
|
||||
test_data["tcb_evaluation_data_numbers_json"]
|
||||
.as_str()
|
||||
.unwrap(),
|
||||
)
|
||||
.create_async()
|
||||
.await;
|
||||
|
||||
let client = ApiClient::new_with_base_url(server.url()).unwrap();
|
||||
let result = client.get_sgx_tcb_evaluation_data_numbers().await;
|
||||
|
||||
assert!(result.is_ok());
|
||||
let response = result.unwrap();
|
||||
assert_eq!(
|
||||
response.tcb_evaluation_data_numbers_json,
|
||||
test_data["tcb_evaluation_data_numbers_json"]
|
||||
.as_str()
|
||||
.unwrap()
|
||||
);
|
||||
assert_eq!(
|
||||
response.issuer_chain,
|
||||
test_data["issuer_chain"].as_str().unwrap()
|
||||
);
|
||||
|
||||
// Verify the JSON structure
|
||||
let eval_nums: Value =
|
||||
serde_json::from_str(&response.tcb_evaluation_data_numbers_json).unwrap();
|
||||
assert!(eval_nums.is_object());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_tdx_tcb_evaluation_data_numbers_with_real_data() {
|
||||
let mut server = Server::new_async().await;
|
||||
let test_data = parse_test_data(TDX_TCB_EVAL_NUMS_DATA);
|
||||
|
||||
let issuer_chain = test_data["issuer_chain"].as_str().unwrap();
|
||||
let encoded_issuer_chain =
|
||||
percent_encode(issuer_chain.as_bytes(), NON_ALPHANUMERIC).to_string();
|
||||
|
||||
let _m = server
|
||||
.mock("GET", "/tdx/certification/v4/tcbevaluationdatanumbers")
|
||||
.with_status(200)
|
||||
.with_header("Content-Type", "application/json")
|
||||
.with_header(
|
||||
"TCB-Evaluation-Data-Numbers-Issuer-Chain",
|
||||
&encoded_issuer_chain,
|
||||
)
|
||||
.with_body(
|
||||
test_data["tcb_evaluation_data_numbers_json"]
|
||||
.as_str()
|
||||
.unwrap(),
|
||||
)
|
||||
.create_async()
|
||||
.await;
|
||||
|
||||
let client = ApiClient::new_with_base_url(server.url()).unwrap();
|
||||
let result = client.get_tdx_tcb_evaluation_data_numbers().await;
|
||||
|
||||
assert!(result.is_ok());
|
||||
let response = result.unwrap();
|
||||
assert_eq!(
|
||||
response.tcb_evaluation_data_numbers_json,
|
||||
test_data["tcb_evaluation_data_numbers_json"]
|
||||
.as_str()
|
||||
.unwrap()
|
||||
);
|
||||
assert_eq!(
|
||||
response.issuer_chain,
|
||||
test_data["issuer_chain"].as_str().unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_pck_crl_der_encoding_with_real_data() {
|
||||
let mut server = Server::new_async().await;
|
||||
let test_data = parse_test_data(PCK_CRL_PROCESSOR_DER_DATA);
|
||||
|
||||
let issuer_chain = test_data["issuer_chain"].as_str().unwrap();
|
||||
let encoded_issuer_chain =
|
||||
percent_encode(issuer_chain.as_bytes(), NON_ALPHANUMERIC).to_string();
|
||||
|
||||
// The DER data is stored as base64 in our test data
|
||||
let crl_base64 = test_data["crl_data_base64"].as_str().unwrap();
|
||||
use base64::{engine::general_purpose, Engine as _};
|
||||
let crl_der = general_purpose::STANDARD.decode(crl_base64).unwrap();
|
||||
|
||||
let _m = server
|
||||
.mock("GET", "/sgx/certification/v4/pckcrl")
|
||||
.match_query(mockito::Matcher::AllOf(vec![
|
||||
mockito::Matcher::UrlEncoded("ca".into(), "processor".into()),
|
||||
mockito::Matcher::UrlEncoded("encoding".into(), "der".into()),
|
||||
]))
|
||||
.with_status(200)
|
||||
.with_header("SGX-PCK-CRL-Issuer-Chain", &encoded_issuer_chain)
|
||||
.with_body(crl_der)
|
||||
.create_async()
|
||||
.await;
|
||||
|
||||
let client = ApiClient::new_with_base_url(server.url()).unwrap();
|
||||
let result = client
|
||||
.get_pck_crl(CaType::Processor, Some(CrlEncoding::Der))
|
||||
.await;
|
||||
|
||||
assert!(result.is_ok());
|
||||
let response = result.unwrap();
|
||||
|
||||
// Verify the response data matches
|
||||
let response_base64 = general_purpose::STANDARD.encode(&response.crl_data);
|
||||
assert_eq!(response_base64, crl_base64);
|
||||
assert_eq!(
|
||||
response.issuer_chain,
|
||||
test_data["issuer_chain"].as_str().unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_sgx_tcb_info_early_update_with_real_data() {
|
||||
let mut server = Server::new_async().await;
|
||||
let test_data = parse_test_data(SGX_TCB_INFO_EARLY_DATA);
|
||||
|
||||
let issuer_chain = test_data["issuer_chain"].as_str().unwrap();
|
||||
let encoded_issuer_chain =
|
||||
percent_encode(issuer_chain.as_bytes(), NON_ALPHANUMERIC).to_string();
|
||||
|
||||
let _m = server
|
||||
.mock("GET", "/sgx/certification/v4/tcb")
|
||||
.match_query(mockito::Matcher::AllOf(vec![
|
||||
mockito::Matcher::UrlEncoded("fmspc".into(), "00906ED50000".into()),
|
||||
mockito::Matcher::UrlEncoded("update".into(), "early".into()),
|
||||
]))
|
||||
.with_status(200)
|
||||
.with_header("Content-Type", "application/json")
|
||||
.with_header("TCB-Info-Issuer-Chain", &encoded_issuer_chain)
|
||||
.with_body(test_data["tcb_info_json"].as_str().unwrap())
|
||||
.create_async()
|
||||
.await;
|
||||
|
||||
let client = ApiClient::new_with_base_url(server.url()).unwrap();
|
||||
let result = client
|
||||
.get_sgx_tcb_info("00906ED50000", Some(UpdateType::Early), None)
|
||||
.await;
|
||||
|
||||
assert!(result.is_ok());
|
||||
let response = result.unwrap();
|
||||
assert_eq!(
|
||||
response.tcb_info_json,
|
||||
test_data["tcb_info_json"].as_str().unwrap()
|
||||
);
|
||||
|
||||
// Verify the JSON structure
|
||||
let tcb_info: Value = serde_json::from_str(&response.tcb_info_json).unwrap();
|
||||
assert_eq!(tcb_info["tcbInfo"]["fmspc"], "00906ED50000");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_tdx_tcb_info_with_eval_number_with_real_data() {
|
||||
let mut server = Server::new_async().await;
|
||||
let test_data = parse_test_data(TDX_TCB_INFO_EVAL17_DATA);
|
||||
|
||||
let issuer_chain = test_data["issuer_chain"].as_str().unwrap();
|
||||
let encoded_issuer_chain =
|
||||
percent_encode(issuer_chain.as_bytes(), NON_ALPHANUMERIC).to_string();
|
||||
|
||||
let _m = server
|
||||
.mock("GET", "/tdx/certification/v4/tcb")
|
||||
.match_query(mockito::Matcher::AllOf(vec![
|
||||
mockito::Matcher::UrlEncoded("fmspc".into(), "00806F050000".into()),
|
||||
mockito::Matcher::UrlEncoded("tcbEvaluationDataNumber".into(), "17".into()),
|
||||
]))
|
||||
.with_status(200)
|
||||
.with_header("Content-Type", "application/json")
|
||||
.with_header("TCB-Info-Issuer-Chain", &encoded_issuer_chain)
|
||||
.with_body(test_data["tcb_info_json"].as_str().unwrap())
|
||||
.create_async()
|
||||
.await;
|
||||
|
||||
let client = ApiClient::new_with_base_url(server.url()).unwrap();
|
||||
let result = client
|
||||
.get_tdx_tcb_info("00806F050000", None, Some(17))
|
||||
.await;
|
||||
|
||||
assert!(result.is_ok());
|
||||
let response = result.unwrap();
|
||||
|
||||
// Verify the response
|
||||
let tcb_info: Value = serde_json::from_str(&response.tcb_info_json).unwrap();
|
||||
assert_eq!(tcb_info["tcbInfo"]["fmspc"], "00806F050000");
|
||||
assert_eq!(tcb_info["tcbInfo"]["id"], "TDX");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_get_fmspcs_v3_should_fail() {
|
||||
let server = Server::new_async().await;
|
||||
|
||||
// FMSPCs is V4 only
|
||||
let client = ApiClient::new_with_options(server.url(), ApiVersion::V3).unwrap();
|
||||
let result = client.get_fmspcs(None).await;
|
||||
|
||||
assert!(result.is_err());
|
||||
match result.unwrap_err() {
|
||||
IntelApiError::UnsupportedApiVersion(msg) => {
|
||||
assert!(msg.contains("API v4 only"));
|
||||
}
|
||||
_ => panic!("Expected UnsupportedApiVersion error"),
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_tcb_evaluation_data_numbers_v3_should_fail() {
|
||||
let server = Server::new_async().await;
|
||||
|
||||
// TCB evaluation data numbers is V4 only
|
||||
let client = ApiClient::new_with_options(server.url(), ApiVersion::V3).unwrap();
|
||||
|
||||
let sgx_result = client.get_sgx_tcb_evaluation_data_numbers().await;
|
||||
assert!(sgx_result.is_err());
|
||||
match sgx_result.unwrap_err() {
|
||||
IntelApiError::UnsupportedApiVersion(msg) => {
|
||||
assert!(msg.contains("requires API v4"));
|
||||
}
|
||||
_ => panic!("Expected UnsupportedApiVersion error"),
|
||||
}
|
||||
|
||||
let tdx_result = client.get_tdx_tcb_evaluation_data_numbers().await;
|
||||
assert!(tdx_result.is_err());
|
||||
match tdx_result.unwrap_err() {
|
||||
IntelApiError::UnsupportedApiVersion(msg) => {
|
||||
assert!(msg.contains("requires API v4"));
|
||||
}
|
||||
_ => panic!("Expected UnsupportedApiVersion error"),
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_get_fmspcs_no_filter_with_real_data() {
|
||||
let mut server = Server::new_async().await;
|
||||
let test_data = parse_test_data(FMSPCS_NO_FILTER_DATA);
|
||||
|
||||
let _m = server
|
||||
.mock("GET", "/sgx/certification/v4/fmspcs")
|
||||
.with_status(200)
|
||||
.with_header("Content-Type", "application/json")
|
||||
.with_body(test_data["fmspcs_json"].as_str().unwrap())
|
||||
.create_async()
|
||||
.await;
|
||||
|
||||
let client = ApiClient::new_with_base_url(server.url()).unwrap();
|
||||
let result = client.get_fmspcs(None).await;
|
||||
|
||||
assert!(result.is_ok());
|
||||
let fmspcs_json = result.unwrap();
|
||||
assert_eq!(fmspcs_json, test_data["fmspcs_json"].as_str().unwrap());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_sgx_qe_identity_v3_with_real_data() {
|
||||
let mut server = Server::new_async().await;
|
||||
let test_data = parse_test_data(SGX_QE_IDENTITY_V3_DATA);
|
||||
|
||||
let issuer_chain = test_data["issuer_chain"].as_str().unwrap();
|
||||
let encoded_issuer_chain =
|
||||
percent_encode(issuer_chain.as_bytes(), NON_ALPHANUMERIC).to_string();
|
||||
|
||||
// V3 uses different header names
|
||||
let _m = server
|
||||
.mock("GET", "/sgx/certification/v3/qe/identity")
|
||||
.with_status(200)
|
||||
.with_header("Content-Type", "application/json")
|
||||
.with_header("SGX-Enclave-Identity-Issuer-Chain", &encoded_issuer_chain)
|
||||
.with_body(test_data["enclave_identity_json"].as_str().unwrap())
|
||||
.create_async()
|
||||
.await;
|
||||
|
||||
let client = ApiClient::new_with_options(server.url(), ApiVersion::V3).unwrap();
|
||||
let result = client.get_sgx_qe_identity(None, None).await;
|
||||
|
||||
if let Err(e) = &result {
|
||||
eprintln!("Error in V3 test: {:?}", e);
|
||||
}
|
||||
|
||||
assert!(result.is_ok());
|
||||
let response = result.unwrap();
|
||||
assert_eq!(
|
||||
response.enclave_identity_json,
|
||||
test_data["enclave_identity_json"].as_str().unwrap()
|
||||
);
|
||||
assert_eq!(
|
||||
response.issuer_chain,
|
||||
test_data["issuer_chain"].as_str().unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_sgx_tcb_info_v3_with_real_data() {
|
||||
let mut server = Server::new_async().await;
|
||||
let test_data = parse_test_data(SGX_TCB_INFO_V3_DATA);
|
||||
|
||||
let issuer_chain = test_data["issuer_chain"].as_str().unwrap();
|
||||
let encoded_issuer_chain =
|
||||
percent_encode(issuer_chain.as_bytes(), NON_ALPHANUMERIC).to_string();
|
||||
|
||||
// V3 uses different header names
|
||||
let _m = server
|
||||
.mock("GET", "/sgx/certification/v3/tcb")
|
||||
.match_query(mockito::Matcher::UrlEncoded(
|
||||
"fmspc".into(),
|
||||
"00906ED50000".into(),
|
||||
))
|
||||
.with_status(200)
|
||||
.with_header("Content-Type", "application/json")
|
||||
.with_header("SGX-TCB-Info-Issuer-Chain", &encoded_issuer_chain)
|
||||
.with_body(test_data["tcb_info_json"].as_str().unwrap())
|
||||
.create_async()
|
||||
.await;
|
||||
|
||||
let client = ApiClient::new_with_options(server.url(), ApiVersion::V3).unwrap();
|
||||
let result = client.get_sgx_tcb_info("00906ED50000", None, None).await;
|
||||
|
||||
assert!(result.is_ok());
|
||||
let response = result.unwrap();
|
||||
assert_eq!(
|
||||
response.tcb_info_json,
|
||||
test_data["tcb_info_json"].as_str().unwrap()
|
||||
);
|
||||
|
||||
// Verify the JSON structure
|
||||
let tcb_info: Value = serde_json::from_str(&response.tcb_info_json).unwrap();
|
||||
assert_eq!(tcb_info["tcbInfo"]["fmspc"], "00906ED50000");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_tdx_tcb_info_alternate_fmspc_with_real_data() {
|
||||
let mut server = Server::new_async().await;
|
||||
let test_data = parse_test_data(TDX_TCB_INFO_ALT_DATA);
|
||||
|
||||
let issuer_chain = test_data["issuer_chain"].as_str().unwrap();
|
||||
let encoded_issuer_chain =
|
||||
percent_encode(issuer_chain.as_bytes(), NON_ALPHANUMERIC).to_string();
|
||||
|
||||
let _m = server
|
||||
.mock("GET", "/tdx/certification/v4/tcb")
|
||||
.match_query(mockito::Matcher::UrlEncoded(
|
||||
"fmspc".into(),
|
||||
"00806F050000".into(),
|
||||
))
|
||||
.with_status(200)
|
||||
.with_header("Content-Type", "application/json")
|
||||
.with_header("TCB-Info-Issuer-Chain", &encoded_issuer_chain)
|
||||
.with_body(test_data["tcb_info_json"].as_str().unwrap())
|
||||
.create_async()
|
||||
.await;
|
||||
|
||||
let client = ApiClient::new_with_base_url(server.url()).unwrap();
|
||||
let result = client.get_tdx_tcb_info("00806F050000", None, None).await;
|
||||
|
||||
assert!(result.is_ok());
|
||||
let response = result.unwrap();
|
||||
|
||||
// Verify we got the same data as the first TDX TCB info test
|
||||
let tcb_info: Value = serde_json::from_str(&response.tcb_info_json).unwrap();
|
||||
assert_eq!(tcb_info["tcbInfo"]["fmspc"], "00806F050000");
|
||||
assert_eq!(tcb_info["tcbInfo"]["id"], "TDX");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_platform_filter_combinations() {
|
||||
let mut server = Server::new_async().await;
|
||||
|
||||
// Test with different platform filters
|
||||
let filters = vec![
|
||||
(Some(PlatformFilter::All), "all"),
|
||||
(Some(PlatformFilter::Client), "client"),
|
||||
(Some(PlatformFilter::E3), "E3"),
|
||||
(Some(PlatformFilter::E5), "E5"),
|
||||
(None, ""),
|
||||
];
|
||||
|
||||
for (filter, query_value) in filters {
|
||||
let mock_response = r#"[{"fmspc": "00906ED50000", "platform": "SGX"}]"#;
|
||||
|
||||
let mut mock = server.mock("GET", "/sgx/certification/v4/fmspcs");
|
||||
|
||||
if !query_value.is_empty() {
|
||||
mock = mock.match_query(mockito::Matcher::UrlEncoded(
|
||||
"platform".into(),
|
||||
query_value.into(),
|
||||
));
|
||||
}
|
||||
|
||||
let _m = mock
|
||||
.with_status(200)
|
||||
.with_header("Content-Type", "application/json")
|
||||
.with_body(mock_response)
|
||||
.create_async()
|
||||
.await;
|
||||
|
||||
let client = ApiClient::new_with_base_url(server.url()).unwrap();
|
||||
let result = client.get_fmspcs(filter).await;
|
||||
|
||||
assert!(result.is_ok());
|
||||
let response = result.unwrap();
|
||||
assert!(response.contains("00906ED50000"));
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_error_scenarios() {
|
||||
let mut server = Server::new_async().await;
|
||||
|
||||
// Test 404 with Error headers
|
||||
let _m = server
|
||||
.mock("GET", "/sgx/certification/v4/tcb")
|
||||
.match_query(mockito::Matcher::UrlEncoded(
|
||||
"fmspc".into(),
|
||||
"invalid".into(),
|
||||
))
|
||||
.with_status(404)
|
||||
.with_header("Request-ID", "test123")
|
||||
.with_header("Error-Code", "InvalidParameter")
|
||||
.with_header("Error-Message", "Invalid FMSPC format")
|
||||
.create_async()
|
||||
.await;
|
||||
|
||||
let client = ApiClient::new_with_base_url(server.url()).unwrap();
|
||||
let result = client.get_sgx_tcb_info("invalid", None, None).await;
|
||||
|
||||
assert!(result.is_err());
|
||||
match result.unwrap_err() {
|
||||
IntelApiError::ApiError {
|
||||
status,
|
||||
request_id,
|
||||
error_code,
|
||||
error_message,
|
||||
} => {
|
||||
assert_eq!(status.as_u16(), 404);
|
||||
assert_eq!(request_id, "test123");
|
||||
assert_eq!(error_code.as_deref(), Some("InvalidParameter"));
|
||||
assert_eq!(error_message.as_deref(), Some("Invalid FMSPC format"));
|
||||
}
|
||||
_ => panic!("Expected ApiError"),
|
||||
}
|
||||
}
|
|
@ -1,3 +0,0 @@
|
|||
{
|
||||
"fmspcs_json": "[{\"fmspc\":\"00A06D080000\",\"platform\":\"E5\"},{\"fmspc\":\"10A06D070000\",\"platform\":\"E5\"},{\"fmspc\":\"70A06D070000\",\"platform\":\"E5\"},{\"fmspc\":\"00A06E050000\",\"platform\":\"E5\"},{\"fmspc\":\"00906EA50000\",\"platform\":\"client\"},{\"fmspc\":\"20606C040000\",\"platform\":\"E5\"},{\"fmspc\":\"50806F000000\",\"platform\":\"E5\"},{\"fmspc\":\"00A067110000\",\"platform\":\"E3\"},{\"fmspc\":\"00606C040000\",\"platform\":\"E5\"},{\"fmspc\":\"00706E470000\",\"platform\":\"client\"},{\"fmspc\":\"00806EA60000\",\"platform\":\"client\"},{\"fmspc\":\"00706A800000\",\"platform\":\"client\"},{\"fmspc\":\"00706A100000\",\"platform\":\"client\"},{\"fmspc\":\"F0806F000000\",\"platform\":\"E5\"},{\"fmspc\":\"00806EB70000\",\"platform\":\"client\"},{\"fmspc\":\"00906EC50000\",\"platform\":\"client\"},{\"fmspc\":\"90806F000000\",\"platform\":\"E5\"},{\"fmspc\":\"10A06F010000\",\"platform\":\"E5\"},{\"fmspc\":\"00906EC10000\",\"platform\":\"client\"},{\"fmspc\":\"B0C06F000000\",\"platform\":\"E5\"},{\"fmspc\":\"20A06F000000\",\"platform\":\"E5\"},{\"fmspc\":\"00906ED50000\",\"platform\":\"E3\"},{\"fmspc\":\"40A06F000000\",\"platform\":\"E5\"},{\"fmspc\":\"60A06F000000\",\"platform\":\"E5\"},{\"fmspc\":\"D0806F000000\",\"platform\":\"E5\"},{\"fmspc\":\"00A065510000\",\"platform\":\"client\"},{\"fmspc\":\"10A06D080000\",\"platform\":\"E5\"},{\"fmspc\":\"30606A000000\",\"platform\":\"E5\"},{\"fmspc\":\"20806EB70000\",\"platform\":\"client\"},{\"fmspc\":\"00906EA10000\",\"platform\":\"E3\"},{\"fmspc\":\"30806F040000\",\"platform\":\"E5\"},{\"fmspc\":\"C0806F000000\",\"platform\":\"E5\"},{\"fmspc\":\"30A06D050000\",\"platform\":\"E5\"},{\"fmspc\":\"60C06F000000\",\"platform\":\"E5\"},{\"fmspc\":\"20A06D080000\",\"platform\":\"E5\"},{\"fmspc\":\"10A06D000000\",\"platform\":\"E5\"},{\"fmspc\":\"00806F050000\",\"platform\":\"E5\"},{\"fmspc\":\"60A06D070000\",\"platform\":\"E5\"},{\"fmspc\":\"20906EC10000\",\"platform\":\"client\"},{\"fmspc\":\"90C06F000000\",\"platform\":\"E5\"},{\"fmspc\":\"80C06F000000\",\"platform\":\"E5\"},{\"fmspc\":\"00806F000000\",\"platform\":\"E5\"},{\"fmspc\":\"00906EB10000\",\"platform\":\"client\"},{\"fmspc\":\"00606A000000\",\"platform\":\"E5\"}]"
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue