diff --git a/.github/workflows/kbs-rust.yml b/.github/workflows/kbs-rust.yml index f99c102a2..d468963b7 100644 --- a/.github/workflows/kbs-rust.yml +++ b/.github/workflows/kbs-rust.yml @@ -59,7 +59,7 @@ jobs: - name: KBS Build [Built-in CoCo AS, OpenSSL] working-directory: kbs - run: make HTTPS_CRYPTO=openssl + run: make - name: KBS Build [gRPC CoCo AS, RustTLS] working-directory: kbs diff --git a/.github/workflows/push-kbs-image-to-ghcr.yml b/.github/workflows/push-kbs-image-to-ghcr.yml index 4c4a25e9e..ed30c3b6b 100644 --- a/.github/workflows/push-kbs-image-to-ghcr.yml +++ b/.github/workflows/push-kbs-image-to-ghcr.yml @@ -25,15 +25,12 @@ jobs: include: - tag: kbs docker_file: kbs/docker/Dockerfile - https_crypto: openssl name: build-in AS - tag: kbs-grpc-as docker_file: kbs/docker/coco-as-grpc/Dockerfile - https_crypto: rustls name: gRPC AS - tag: kbs-ita-as docker_file: kbs/docker/intel-trust-authority/Dockerfile - https_crypto: rustls name: Intel Trust Authority AS runs-on: ${{ matrix.instance }} @@ -56,12 +53,10 @@ jobs: run: | commit_sha=${{ github.sha }} arch=$(uname -m) - https_crypto=${{ matrix.https_crypto }} - [ "${arch}" = "s390x" ] && https_crypto=openssl DOCKER_BUILDKIT=1 docker build -f "${{ matrix.docker_file }}" --push \ -t "ghcr.io/confidential-containers/staged-images/${{ matrix.tag }}:${commit_sha}-${arch}" \ -t "ghcr.io/confidential-containers/staged-images/${{ matrix.tag }}:latest-${arch}" \ - --build-arg ARCH="${arch}" --build-arg HTTPS_CRYPTO="${https_crypto}" . + --build-arg ARCH="${arch}" . publish_multi_arch_image: needs: build_and_push diff --git a/Cargo.lock b/Cargo.lock index 6bbce59a2..563f1cc70 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -135,10 +135,8 @@ dependencies = [ "pin-project-lite", "tokio", "tokio-openssl", - "tokio-rustls 0.23.4", "tokio-util", "tracing", - "webpki-roots 0.22.6", ] [[package]] @@ -2580,7 +2578,7 @@ dependencies = [ "base64 0.21.7", "js-sys", "pem", - "ring 0.17.8", + "ring", "serde", "serde_json", "simple_asn1", @@ -2682,8 +2680,6 @@ dependencies = [ "reqwest 0.12.5", "rsa 0.9.6", "rstest", - "rustls 0.20.9", - "rustls-pemfile 1.0.4", "scc", "semver", "serde", @@ -2768,7 +2764,7 @@ dependencies = [ "rand", "reqwest 0.12.5", "resource_uri", - "ring 0.17.8", + "ring", "serde", "serde_json", "sha2", @@ -2794,7 +2790,7 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" dependencies = [ - "spin 0.9.8", + "spin", ] [[package]] @@ -3938,7 +3934,7 @@ checksum = "ba92fb39ec7ad06ca2582c0ca834dfeadcaf06ddfc8e635c80aa7e1c05315fdd" dependencies = [ "bytes", "rand", - "ring 0.17.8", + "ring", "rustc-hash 2.0.0", "rustls 0.23.7", "slab", @@ -4244,21 +4240,6 @@ dependencies = [ "subtle", ] -[[package]] -name = "ring" -version = "0.16.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc" -dependencies = [ - "cc", - "libc", - "once_cell", - "spin 0.5.2", - "untrusted 0.7.1", - "web-sys", - "winapi", -] - [[package]] name = "ring" version = "0.17.8" @@ -4269,8 +4250,8 @@ dependencies = [ "cfg-if", "getrandom", "libc", - "spin 0.9.8", - "untrusted 0.9.0", + "spin", + "untrusted", "windows-sys 0.52.0", ] @@ -4415,18 +4396,6 @@ dependencies = [ "windows-sys 0.52.0", ] -[[package]] -name = "rustls" -version = "0.20.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b80e3dec595989ea8510028f30c408a4630db12c9cbb8de34203b89d6577e99" -dependencies = [ - "log", - "ring 0.16.20", - "sct", - "webpki", -] - [[package]] name = "rustls" version = "0.21.12" @@ -4434,7 +4403,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e" dependencies = [ "log", - "ring 0.17.8", + "ring", "rustls-webpki 0.101.7", "sct", ] @@ -4447,7 +4416,7 @@ checksum = "ebbbdb961df0ad3f2652da8f3fdc4b36122f568f968f45ad3316f26c025c677b" dependencies = [ "log", "once_cell", - "ring 0.17.8", + "ring", "rustls-pki-types", "rustls-webpki 0.102.3", "subtle", @@ -4485,8 +4454,8 @@ version = "0.101.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" dependencies = [ - "ring 0.17.8", - "untrusted 0.9.0", + "ring", + "untrusted", ] [[package]] @@ -4495,9 +4464,9 @@ version = "0.102.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f3bce581c0dd41bce533ce695a1437fa16a7ab5ac3ccfa99fe1a620a7885eabf" dependencies = [ - "ring 0.17.8", + "ring", "rustls-pki-types", - "untrusted 0.9.0", + "untrusted", ] [[package]] @@ -4643,8 +4612,8 @@ version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" dependencies = [ - "ring 0.17.8", - "untrusted 0.9.0", + "ring", + "untrusted", ] [[package]] @@ -5019,12 +4988,6 @@ dependencies = [ "windows-sys 0.52.0", ] -[[package]] -name = "spin" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" - [[package]] name = "spin" version = "0.9.8" @@ -5412,17 +5375,6 @@ dependencies = [ "tokio", ] -[[package]] -name = "tokio-rustls" -version = "0.23.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c43ee83903113e03984cb9e5cebe6c04a5116269e900e3ddba8f068a62adda59" -dependencies = [ - "rustls 0.20.9", - "tokio", - "webpki", -] - [[package]] name = "tokio-rustls" version = "0.24.1" @@ -5817,12 +5769,6 @@ dependencies = [ "subtle", ] -[[package]] -name = "untrusted" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" - [[package]] name = "untrusted" version = "0.9.0" @@ -6067,25 +6013,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "webpki" -version = "0.22.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed63aea5ce73d0ff405984102c42de94fc55a6b75765d621c65262469b3c9b53" -dependencies = [ - "ring 0.17.8", - "untrusted 0.9.0", -] - -[[package]] -name = "webpki-roots" -version = "0.22.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6c71e40d7d2c34a5106301fb632274ca37242cd0c9d3e64dbece371a40a2d87" -dependencies = [ - "webpki", -] - [[package]] name = "webpki-roots" version = "0.25.4" diff --git a/attestation-service/src/token/simple.rs b/attestation-service/src/token/simple.rs index 20c1d9652..23281c323 100644 --- a/attestation-service/src/token/simple.rs +++ b/attestation-service/src/token/simple.rs @@ -92,6 +92,7 @@ impl AttestationTokenBroker for SimpleAttestationTokenBroker { let header_value = json!({ "typ": "JWT", "alg": SIMPLE_TOKEN_ALG, + "jwk": serde_json::from_str::(&self.pubkey_jwks()?)?["keys"][0].clone(), }); let header_string = serde_json::to_string(&header_value)?; let header_b64 = URL_SAFE_NO_PAD.encode(header_string.as_bytes()); @@ -109,7 +110,6 @@ impl AttestationTokenBroker for SimpleAttestationTokenBroker { "iss": self.config.issuer_name.clone(), "iat": now.unix_timestamp(), "jti": id, - "jwk": serde_json::from_str::(&self.pubkey_jwks()?)?["keys"][0].clone(), "nbf": now.unix_timestamp(), "exp": exp.unix_timestamp(), }) diff --git a/deps/verifier/src/se/README.md b/deps/verifier/src/se/README.md index 0ee09c088..d2cfa5a4b 100644 --- a/deps/verifier/src/se/README.md +++ b/deps/verifier/src/se/README.md @@ -67,7 +67,7 @@ openssl pkey -in kbs.key -pubout -out kbs.pem - Build KBS ```bash -cargo install --locked --debug --path kbs/src/kbs --no-default-features --features coco-as-builtin,openssl,resource,opa +cargo install --locked --debug --path kbs/src/kbs --no-default-features --features coco-as-builtin,resource,opa ``` - Prepare the material retrieved above, similar as: @@ -101,7 +101,7 @@ auth_public_key = "/kbs/kbs.pem" insecure_http = true [attestation_token_config] -attestation_token_type = "CoCo" +insecure_key = true [as_config] work_dir = "/opt/confidential-containers/attestation-service" @@ -128,7 +128,7 @@ export SE_SKIP_CERTS_VERIFICATION=true ## (Option 2) Launch KBS via docker-compose - Build the docker image ``` -DOCKER_BUILDKIT=1 docker build --build-arg HTTPS_CRYPTO="openssl" --build-arg ARCH="s390x" -t ghcr.io/confidential-containers/staged-images/kbs:latest . -f kbs/docker/Dockerfile +DOCKER_BUILDKIT=1 docker build --build-arg --build-arg ARCH="s390x" -t ghcr.io/confidential-containers/staged-images/kbs:latest . -f kbs/docker/Dockerfile ``` - Prepare a docker compose file, similar as: diff --git a/kbs/Cargo.toml b/kbs/Cargo.toml index fc0ae8109..6ff78dd42 100644 --- a/kbs/Cargo.toml +++ b/kbs/Cargo.toml @@ -7,10 +7,10 @@ documentation.workspace = true edition.workspace = true [features] -default = ["coco-as-builtin", "resource", "opa", "rustls"] +default = ["coco-as-builtin", "resource", "opa"] # Feature that allows to access resources from KBS -resource = ["rsa", "dep:openssl", "reqwest", "aes-gcm", "jsonwebtoken"] +resource = ["rsa", "reqwest", "aes-gcm", "jsonwebtoken"] # Support a backend attestation service for KBS as = [] @@ -36,17 +36,11 @@ coco-as-grpc = ["coco-as", "mobc", "tonic", "tonic-build", "prost"] # Use Intel TA as backend attestation service intel-trust-authority-as = ["as", "reqwest", "resource", "az-cvm-vtpm"] -# Use pure rust crypto stack for KBS -rustls = ["actix-web/rustls", "dep:rustls", "dep:rustls-pemfile"] - -# Use openssl crypto stack for KBS -openssl = ["actix-web/openssl", "dep:openssl"] - # Use aliyun KMS as KBS backend aliyun = ["kms/aliyun"] [dependencies] -actix-web.workspace = true +actix-web = { workspace = true, features = ["openssl"] } actix-web-httpauth.workspace = true aes-gcm = { version = "0.10.1", optional = true } anyhow.workspace = true @@ -69,8 +63,6 @@ rand = "0.8.5" regorus.workspace = true reqwest = { workspace = true, features = ["json"], optional = true } rsa = { version = "0.9.2", optional = true, features = ["sha2"] } -rustls = { version = "0.20.8", optional = true } -rustls-pemfile = { version = "1.0.4", optional = true } scc = "2" semver = "1.0.16" serde = { workspace = true, features = ["derive"] } @@ -81,7 +73,7 @@ time = { version = "0.3.23", features = ["std"] } tokio.workspace = true tonic = { workspace = true, optional = true } uuid = { version = "1.2.2", features = ["serde", "v4"] } -openssl = { version = "0.10.46", optional = true } +openssl = "0.10.55" az-cvm-vtpm = { version = "0.7.0", default-features = false, optional = true } [dev-dependencies] diff --git a/kbs/Makefile b/kbs/Makefile index 33d76f642..90c6267d3 100644 --- a/kbs/Makefile +++ b/kbs/Makefile @@ -1,5 +1,4 @@ AS_TYPE ?= coco-as -HTTPS_CRYPTO ?= rustls POLICY_ENGINE ?= ALIYUN ?= false @@ -39,16 +38,16 @@ build: background-check-kbs .PHONY: background-check-kbs background-check-kbs: - cargo build -p kbs --locked --release --no-default-features --features $(AS_FEATURE),resource,$(HTTPS_CRYPTO),$(POLICY_ENGINE),$(FEATURES) + cargo build -p kbs --locked --release --no-default-features --features $(AS_FEATURE),resource,$(POLICY_ENGINE),$(FEATURES) .PHONY: passport-issuer-kbs passport-issuer-kbs: - cargo build -p kbs --locked --release --no-default-features --features $(AS_FEATURE),$(HTTPS_CRYPTO),$(FEATURES) + cargo build -p kbs --locked --release --no-default-features --features $(AS_FEATURE),$(FEATURES) mv ../target/release/kbs ../target/release/issuer-kbs .PHONY: passport-resource-kbs passport-resource-kbs: - cargo build -p kbs --locked --release --no-default-features --features $(HTTPS_CRYPTO),resource,$(POLICY_ENGINE),$(FEATURES) + cargo build -p kbs --locked --release --no-default-features --features resource,$(POLICY_ENGINE),$(FEATURES) mv ../target/release/kbs ../target/release/resource-kbs .PHONY: cli diff --git a/kbs/README.md b/kbs/README.md index fd322f3ab..6b6e69c6e 100644 --- a/kbs/README.md +++ b/kbs/README.md @@ -90,11 +90,10 @@ The Makefile supports a number of other configuration parameters. For example, ```shell -make background-check-kbs [HTTPS_CRYPTO=?] [POLICY_ENGINE=?] [AS_TYPES=?] [COCO_AS_INTEGRATION_TYPE=?] [ALIYUN=?] +make background-check-kbs [POLICY_ENGINE=?] [AS_TYPES=?] [COCO_AS_INTEGRATION_TYPE=?] [ALIYUN=?] ``` The parameters -- `HTTPS_CRYPTO`: either `rustls` or `openssl` can be specified. If not provided, `rustls` is default. - `POLICY_ENGINE`: The KBS has a policy engine to facilitate access control. This should not be confused with the policy engine in the AS, which determines whether or not TEE evidence is valid. `POLICY_ENGINE` determines which type of policy engine the KBS will use. Today only `opa` is supported. The KBS can also be built without a policy engine if it is not required. - `AS_TYPES`: The KBS supports multiple backend attestation services. `AS_TYPES` selects which verifier to use. The options are `coco-as` and `intel-trust-authority-as`. @@ -103,8 +102,6 @@ if it is not required. ## HTTPS Support The KBS can use HTTPS. This requires a crypto backend. -`HTTPS_CRYPTO` determines which backend will be used. -The options are `rustls` and `openssl`. The default is `rustls`. If you want a self-signed cert for test cases, please refer to [the document](docs/self-signed-https.md). diff --git a/kbs/config/docker-compose/kbs-config.toml b/kbs/config/docker-compose/kbs-config.toml index b99963989..461d2aa54 100644 --- a/kbs/config/docker-compose/kbs-config.toml +++ b/kbs/config/docker-compose/kbs-config.toml @@ -3,7 +3,7 @@ auth_public_key = "/opt/confidential-containers/kbs/user-keys/public.pub" insecure_http = true [attestation_token_config] -attestation_token_type = "CoCo" +insecure_key = true [grpc_config] as_addr = "http://as:50004" diff --git a/kbs/config/kbs-config-grpc.toml b/kbs/config/kbs-config-grpc.toml index 04bfd1381..4bc596917 100644 --- a/kbs/config/kbs-config-grpc.toml +++ b/kbs/config/kbs-config-grpc.toml @@ -2,7 +2,7 @@ insecure_http = true insecure_api = true [attestation_token_config] -attestation_token_type = "CoCo" +insecure_key = true [grpc_config] as_addr = "http://127.0.0.1:50004" diff --git a/kbs/config/kbs-config-intel-trust-authority.toml b/kbs/config/kbs-config-intel-trust-authority.toml index 48d435b64..070841da6 100644 --- a/kbs/config/kbs-config-intel-trust-authority.toml +++ b/kbs/config/kbs-config-intel-trust-authority.toml @@ -2,7 +2,6 @@ insecure_http = true insecure_api = true [attestation_token_config] -attestation_token_type = "Jwk" trusted_certs_paths = ["https://portal.trustauthority.intel.com"] [intel_trust_authority_config] diff --git a/kbs/config/kbs-config.toml b/kbs/config/kbs-config.toml index d04fd5340..7b99f0359 100644 --- a/kbs/config/kbs-config.toml +++ b/kbs/config/kbs-config.toml @@ -2,7 +2,7 @@ insecure_http = true insecure_api = true [attestation_token_config] -attestation_token_type = "CoCo" +insecure_key = true [repository_config] type = "LocalFs" diff --git a/kbs/config/kubernetes/base/kbs-config.toml b/kbs/config/kubernetes/base/kbs-config.toml index c6544eece..67d01a6ff 100644 --- a/kbs/config/kubernetes/base/kbs-config.toml +++ b/kbs/config/kubernetes/base/kbs-config.toml @@ -5,7 +5,7 @@ auth_public_key = "/kbs/kbs.pem" insecure_http = true [attestation_token_config] -attestation_token_type = "CoCo" +insecure_key = true [as_config] work_dir = "/opt/confidential-containers/attestation-service" diff --git a/kbs/config/kubernetes/ita/kbs-config.toml b/kbs/config/kubernetes/ita/kbs-config.toml index 0bba5e3f2..044864e78 100644 --- a/kbs/config/kubernetes/ita/kbs-config.toml +++ b/kbs/config/kubernetes/ita/kbs-config.toml @@ -5,7 +5,6 @@ auth_public_key = "/kbs/kbs.pem" insecure_http = true [attestation_token_config] -attestation_token_type = "Jwk" trusted_certs_paths = ["https://portal.trustauthority.intel.com"] [intel_trust_authority_config] diff --git a/kbs/docker/Dockerfile b/kbs/docker/Dockerfile index 2831d6a5b..f6bd8294a 100644 --- a/kbs/docker/Dockerfile +++ b/kbs/docker/Dockerfile @@ -1,6 +1,5 @@ FROM rust:slim as builder ARG ARCH=x86_64 -ARG HTTPS_CRYPTO=rustls ARG ALIYUN=false ENV DEBIAN_FRONTEND noninteractive @@ -37,7 +36,7 @@ RUN if [ "${ARCH}" = "x86_64" ]; then curl -fsSL https://download.01.org/intel-s WORKDIR /usr/src/kbs COPY . . -RUN cd kbs && make AS_FEATURE=coco-as-builtin HTTPS_CRYPTO=${HTTPS_CRYPTO} POLICY_ENGINE=opa ALIYUN=${ALIYUN} && \ +RUN cd kbs && make AS_FEATURE=coco-as-builtin POLICY_ENGINE=opa ALIYUN=${ALIYUN} && \ make install-kbs FROM ubuntu:22.04 diff --git a/kbs/docker/coco-as-grpc/Dockerfile b/kbs/docker/coco-as-grpc/Dockerfile index 2a96e9045..67f099e6a 100644 --- a/kbs/docker/coco-as-grpc/Dockerfile +++ b/kbs/docker/coco-as-grpc/Dockerfile @@ -1,6 +1,5 @@ FROM rust:latest as builder ARG ARCH=x86_64 -ARG HTTPS_CRYPTO=rustls ARG ALIYUN=false WORKDIR /usr/src/kbs @@ -9,7 +8,7 @@ COPY . . RUN apt-get update && apt install -y protobuf-compiler git # Build and Install KBS -RUN cd kbs && make AS_FEATURE=coco-as-grpc HTTPS_CRYPTO=${HTTPS_CRYPTO} POLICY_ENGINE=opa ALIYUN=${ALIYUN} && \ +RUN cd kbs && make AS_FEATURE=coco-as-grpc POLICY_ENGINE=opa ALIYUN=${ALIYUN} && \ make install-kbs FROM ubuntu:22.04 diff --git a/kbs/docker/intel-trust-authority/Dockerfile b/kbs/docker/intel-trust-authority/Dockerfile index a2b4f650e..0638b9cf8 100644 --- a/kbs/docker/intel-trust-authority/Dockerfile +++ b/kbs/docker/intel-trust-authority/Dockerfile @@ -1,5 +1,4 @@ FROM rust:latest as builder -ARG HTTPS_CRYPTO=rustls ARG ALIYUN=false WORKDIR /usr/src/kbs @@ -8,7 +7,7 @@ COPY . . RUN apt-get update && apt install -y git # Build and Install KBS -RUN cd kbs && make AS_FEATURE=intel-trust-authority-as HTTPS_CRYPTO=${HTTPS_CRYPTO} POLICY_ENGINE=opa ALIYUN=${ALIYUN} && \ +RUN cd kbs && make AS_FEATURE=intel-trust-authority-as POLICY_ENGINE=opa ALIYUN=${ALIYUN} && \ make install-kbs FROM ubuntu:22.04 diff --git a/kbs/docker/rhel-ubi/Dockerfile b/kbs/docker/rhel-ubi/Dockerfile index 426c9a8d3..a49ee0aae 100644 --- a/kbs/docker/rhel-ubi/Dockerfile +++ b/kbs/docker/rhel-ubi/Dockerfile @@ -15,7 +15,7 @@ dnf -y install --nogpgcheck --repofrompath "sgx,file:///root/sgx_rpm_local_repo" # Build. WORKDIR /usr/src/kbs COPY . . -ARG KBS_FEATURES=coco-as-builtin,rustls,resource,opa +ARG KBS_FEATURES=coco-as-builtin,resource,opa RUN \ cargo install --locked --root /usr/local/ --path kbs --bin kbs --no-default-features --features ${KBS_FEATURES} && \ # Collect linked files necessary for the binary to run. diff --git a/kbs/docs/config.md b/kbs/docs/config.md index 6181e3d98..1d2498bf1 100644 --- a/kbs/docs/config.md +++ b/kbs/docs/config.md @@ -39,12 +39,26 @@ The following properties can be set under the `attestation_token_config` section | Property | Type | Description | Required | Default | |----------------------------|--------------|----------------------------------------------------------------------------------------------------------------------------------------------------------|----------|---------| -| `attestation_token_config` | String | Attestation token broker type. Valid values: `CoCo`, `Jwk` | Yes | - | -| `trusted_certs_paths` | String Array | Trusted Certificates file (PEM format) for `CoCo` or a valid Url (`file://` or `https://`) pointing to a JWKSet certificates (local or OpenID) for `Jwk` | No | - | +| `trusted_jwk_sets` | String Array | Valid Url (`file://` or `https://`) pointing to trusted JWKSets (local or OpenID) for Attestation Tokens trustworthy verification | No | - | +| `trusted_certs_paths` | String Array | Trusted Certificates file (PEM format) for Attestation Tokens trustworthy verification | No | - | +| `extra_teekey_paths` | String Array | User defined paths to the tee public key in the JWT body | No | - | +| `insecure_key` | Boolean | Whether to check the trustworthy of the JWK inside JWT. See comments. | No | `false` | +Each JWT contains a TEE Public Key. Users can use the `extra_teekey_paths` field to additionally specify the path of this Key in the JWT. +Example of `extra_teekey_paths` is `/attester_runtime_data/tee-pubkey` which refers to the key +`attester_runtime_data.tee-pubkey` inside the JWT body claims. By default CoCo AS Token and Intel TA +Token TEE Public Key paths are supported. -If `trusted_certs_paths` is set, KBS will forcibly check the validity of the Attestation Token signature public key certificate, -if not set this field, KBS will skip the verification of the certificate. +For Attestation Services like CoCo-AS, the public key to verify the JWT will be given +in the token's `jwk` field (with or without the public key cert chain `x5c`). + +- If `insecure_key` is set to `true`, KBS will ignore to verify the trustworthy of the `jwk`. +- If `insecure_key` is set to `false`, KBS will look up its `trusted_certs_paths` and the `x5c` +field to verify the trustworthy of the `jwk`. + +For Attestation Services like Intel TA, there will only be a `kid` field inside the JWT. +The `kid` field is used to look up the trusted jwk configured by KBS via `trusted_jwk_sets` to +verify the integrity and trustworthy of the JWT. ### Repository Configuration @@ -207,8 +221,7 @@ insecure_http = true insecure_api = true [attestation_token_config] -attestation_token_type = "Jwk" -trusted_certs_paths = ["https://portal.trustauthority.intel.com"] +trusted_jwk_sets = ["https://portal.trustauthority.intel.com"] [repository_config] type = "LocalFs" diff --git a/kbs/docs/self-signed-https.md b/kbs/docs/self-signed-https.md index 8899f304e..4c36f62f8 100644 --- a/kbs/docs/self-signed-https.md +++ b/kbs/docs/self-signed-https.md @@ -72,7 +72,7 @@ auth_public_key = "/etc/public.pub" insecure_api = true [attestation_token_config] -attestation_token_type = "CoCo" +insecure_key = true [repository_config] type = "LocalFs" diff --git a/kbs/quickstart.md b/kbs/quickstart.md index d793c750a..5466fff13 100644 --- a/kbs/quickstart.md +++ b/kbs/quickstart.md @@ -240,13 +240,27 @@ Adding the following content to JSON config file of gRPC AS: ### Configure trusted root certificate of KBS -Adding the following content to the config file of Resource KBS to specify trusted root certificate (PEM format), -which used to verify the trustworthy of the certificate in Attestation Token: +Attestation Tokens are now all in JWT format. + +Adding the following content to the config file of Resource KBS to specify trusted root certificate (PEM format) +or JWK set which are used to verify the trustworthy of the Attestation Token: ```toml [attestation_token_config] -attestation_token_type = "CoCo" +# Path of root certificate used to verify the trustworthy of `x5c` extension in the JWT trusted_certs_paths = ["/path/to/trusted_cacert.pem"] + +# URL (`path://` or `https://`) of the trusted JWK that can be indexed by `kid` in +# JWT Header. +trusted_jwk_sets = ["/url/to/trusted_jwk_set"] + +# For Attestation Services like CoCo-AS, the public key to verify the JWT will be given +# in the token's `jwk` field (with or without the public key cert chain `x5c`). +# +# - If this flag is set to `true`, KBS will ignore to verify the trustworthy of the `jwk`. +# - If this flag is set to `false`, KBS will look up its `trusted_certs_paths` and the `x5c` +# field to verify the trustworthy of the `jwk`. +insecure_key = false ``` If `trusted_certs_paths` field is not set, KBS will skip the verification of the certificate in Attestation Token. diff --git a/kbs/src/attestation/intel_trust_authority/mod.rs b/kbs/src/attestation/intel_trust_authority/mod.rs index 9f5ae4c4c..7986fd4ec 100644 --- a/kbs/src/attestation/intel_trust_authority/mod.rs +++ b/kbs/src/attestation/intel_trust_authority/mod.rs @@ -4,10 +4,7 @@ use super::Attest; use crate::attestation::{generic_generate_challenge, make_nonce}; -use crate::token::{ - jwk::JwkAttestationTokenVerifier, AttestationTokenVerifier, AttestationTokenVerifierConfig, - AttestationTokenVerifierType, -}; +use crate::token::{jwk::JwkAttestationTokenVerifier, AttestationTokenVerifierConfig}; use anyhow::*; use async_trait::async_trait; use az_cvm_vtpm::hcl::HclReport; @@ -16,8 +13,7 @@ use kbs_types::Challenge; use kbs_types::{Attestation, Tee}; use reqwest::header::{ACCEPT, CONTENT_TYPE, USER_AGENT}; use serde::{Deserialize, Serialize}; -use serde_json::from_value; -use serde_json::json; +use serde_json::{from_value, json}; use strum::{AsRefStr, Display, EnumString}; const SUPPORTED_HASH_ALGORITHMS_JSON_KEY: &str = "supported-hash-algorithms"; @@ -190,7 +186,7 @@ impl Attest for IntelTrustAuthority { .await .context("Failed to verify attestation token")?; - let claims = serde_json::from_str::(&token) + let claims = serde_json::from_value::(token) .context("Failed to deserialize attestation token claims")?; // check unmatched policy @@ -287,8 +283,10 @@ impl Attest for IntelTrustAuthority { impl IntelTrustAuthority { pub async fn new(config: IntelTrustAuthorityConfig) -> Result { let token_verifier = JwkAttestationTokenVerifier::new(&AttestationTokenVerifierConfig { - attestation_token_type: AttestationTokenVerifierType::Jwk, - trusted_certs_paths: vec![config.certs_file.clone()], + extra_teekey_paths: vec![], + trusted_certs_paths: vec![], + trusted_jwk_sets: vec![config.certs_file.clone()], + insecure_key: true, }) .await .context("Failed to initialize token verifier")?; diff --git a/kbs/src/http/mod.rs b/kbs/src/http/mod.rs index 426653a39..4d4c44323 100644 --- a/kbs/src/http/mod.rs +++ b/kbs/src/http/mod.rs @@ -11,8 +11,6 @@ use crate::policy_engine::PolicyEngine; use crate::resource::{set_secret_resource, Repository, ResourceDesc}; #[cfg(feature = "as")] use crate::session::{SessionMap, KBS_SESSION_ID}; -#[cfg(feature = "resource")] -use crate::token::AttestationTokenVerifier; use actix_web::Responder; use actix_web::{body::BoxBody, web, HttpRequest, HttpResponse}; use jwt_simple::prelude::Ed25519PublicKey; diff --git a/kbs/src/http/resource.rs b/kbs/src/http/resource.rs index abf8aed54..ee5e6e66b 100644 --- a/kbs/src/http/resource.rs +++ b/kbs/src/http/resource.rs @@ -15,15 +15,10 @@ use rsa::{BigUint, Pkcs1v15Encrypt, RsaPublicKey}; use serde::Deserialize; use serde_json::{json, Deserializer, Value}; -use crate::raise_error; +use crate::{raise_error, token::TokenVerifier}; use super::*; -#[cfg(feature = "as")] -const TOKEN_TEE_PUBKEY_PATH: &str = AS_TOKEN_TEE_PUBKEY_PATH; -#[cfg(not(feature = "as"))] -const TOKEN_TEE_PUBKEY_PATH: &str = "/customized_claims/runtime_data/tee-pubkey"; - #[allow(unused_assignments)] /// GET /resource/{repository}/{type}/{tag} /// GET /resource/{type}/{tag} @@ -31,7 +26,7 @@ pub(crate) async fn get_resource( request: HttpRequest, repository: web::Data>>, #[cfg(feature = "as")] map: web::Data, - token_verifier: web::Data>>, + token_verifier: web::Data, #[cfg(feature = "policy")] policy_engine: web::Data, ) -> Result { #[allow(unused_mut)] @@ -45,20 +40,16 @@ pub(crate) async fn get_resource( c } else { debug!("Get pkey from auth header"); - get_attest_claims_from_header(&request, token_verifier).await? + get_attest_claims_from_header(&request, &token_verifier).await? }; let claims: Value = serde_json::from_str(&claims_str).map_err(|e| { Error::AttestationClaimsParseFailed(format!("illegal attestation claims: {e}")) })?; - let pkey_value = - claims - .pointer(TOKEN_TEE_PUBKEY_PATH) - .ok_or(Error::AttestationClaimsParseFailed(String::from( - "Failed to find `tee-pubkey` in the attestation claims", - )))?; - let pubkey = TeePubKey::deserialize(pkey_value).map_err(|e| { - Error::AttestationClaimsParseFailed(format!("illegal attestation claims: {e}")) + let pubkey = token_verifier.extract_tee_public_key(claims).map_err(|e| { + Error::AttestationClaimsParseFailed(format!( + "Failed to extract public key in attestation claims: {e:?}" + )) })?; let resource_description = ResourceDesc { @@ -168,7 +159,7 @@ async fn get_attest_claims_from_session( async fn get_attest_claims_from_header( request: &HttpRequest, - token_verifier: web::Data>>, + token_verifier: &web::Data, ) -> Result { let bearer = Authorization::::parse(request) .map_err(|e| Error::InvalidRequest(format!("parse Authorization header failed: {e}")))? @@ -177,11 +168,11 @@ async fn get_attest_claims_from_header( let token = bearer.token().to_string(); let claims = token_verifier - .read() - .await .verify(token) .await - .map_err(|e| Error::TokenParseFailed(format!("verify token failed: {e}")))?; + .map_err(|e| Error::TokenParseFailed(format!("verify token failed: {e:?}")))?; + let claims = serde_json::to_string(&claims) + .map_err(|_| Error::TokenParseFailed("failed to serialize claims".into()))?; Ok(claims) } diff --git a/kbs/src/lib.rs b/kbs/src/lib.rs index b46abf82f..1bc7ed052 100644 --- a/kbs/src/lib.rs +++ b/kbs/src/lib.rs @@ -22,6 +22,7 @@ use anyhow::{anyhow, bail, Context, Result}; #[cfg(feature = "as")] use attestation::AttestationService; use jwt_simple::prelude::Ed25519PublicKey; +use openssl::ssl::SslAcceptorBuilder; #[cfg(feature = "resource")] use resource::RepositoryConfig; #[cfg(feature = "as")] @@ -30,12 +31,6 @@ use std::{net::SocketAddr, path::PathBuf}; #[cfg(feature = "resource")] use token::AttestationTokenVerifierConfig; -#[cfg(feature = "rustls")] -use rustls::ServerConfig; - -#[cfg(feature = "openssl")] -use openssl::ssl::SslAcceptorBuilder; - #[cfg(feature = "as")] use crate::session::SessionMap; @@ -148,45 +143,6 @@ impl ApiServer { }) } - #[cfg(feature = "rustls")] - fn tls_config(&self) -> Result { - use rustls::{Certificate, PrivateKey}; - use rustls_pemfile::{certs, read_one, Item}; - use std::fs::File; - use std::io::BufReader; - - let cert_file = &mut BufReader::new(File::open( - self.certificate - .clone() - .ok_or_else(|| anyhow!("Missing certificate"))?, - )?); - - let key_file = &mut BufReader::new(File::open( - self.private_key - .clone() - .ok_or_else(|| anyhow!("Missing private key"))?, - )?); - - let cert_chain = certs(cert_file)? - .iter() - .map(|c| Certificate(c.clone())) - .collect(); - - let key = match read_one(key_file)? { - Some(Item::RSAKey(key)) | Some(Item::PKCS8Key(key)) | Some(Item::ECKey(key)) => { - Ok(PrivateKey(key)) - } - None | Some(_) => Err(anyhow!("Invalid private key file")), - }?; - - ServerConfig::builder() - .with_safe_defaults() - .with_no_client_auth() - .with_single_cert(cert_chain, key) - .map_err(anyhow::Error::from) - } - - #[cfg(feature = "openssl")] fn tls_config(&self) -> Result { use openssl::ssl::{SslAcceptor, SslFiletype, SslMethod}; @@ -240,7 +196,7 @@ impl ApiServer { #[cfg(feature = "resource")] let token_verifier = - crate::token::create_token_verifier(self.attestation_token_config.clone()).await?; + crate::token::TokenVerifier::from_config(self.attestation_token_config.clone()).await?; #[cfg(feature = "policy")] let policy_engine = PolicyEngine::new(&self.policy_engine_config).await?; @@ -306,15 +262,7 @@ impl ApiServer { }); if !self.insecure { - let tls_server = { - cfg_if::cfg_if! { - if #[cfg(feature = "openssl")] { - http_server.bind_openssl(&self.sockets[..], self.tls_config()?)? - } else { - http_server.bind_rustls(&self.sockets[..], self.tls_config()?)? - } - } - }; + let tls_server = http_server.bind_openssl(&self.sockets[..], self.tls_config()?)?; tls_server.run().await.map_err(anyhow::Error::from) } else { diff --git a/kbs/src/token/coco.rs b/kbs/src/token/coco.rs deleted file mode 100644 index 76e40be0c..000000000 --- a/kbs/src/token/coco.rs +++ /dev/null @@ -1,216 +0,0 @@ -// Copyright (c) 2023 by Alibaba. -// Licensed under the Apache License, Version 2.0, see LICENSE for details. -// SPDX-License-Identifier: Apache-2.0 - -use crate::token::{AttestationTokenVerifier, AttestationTokenVerifierConfig}; -use anyhow::*; -use async_trait::async_trait; -use base64::engine::general_purpose::URL_SAFE_NO_PAD; -use base64::Engine; -use log::warn; -use openssl::hash::MessageDigest; -use openssl::pkey::PKey; -use openssl::rsa::Rsa; -use openssl::sign::Verifier; -use openssl::stack::Stack; -use openssl::x509::store::{X509Store, X509StoreBuilder}; -use openssl::x509::{X509StoreContext, X509}; -use serde_json::Value; - -pub struct CoCoAttestationTokenVerifier { - trusted_certs: X509Store, -} - -impl CoCoAttestationTokenVerifier { - pub fn new(config: &AttestationTokenVerifierConfig) -> Result { - let mut store_builder = X509StoreBuilder::new()?; - - // check all files in trusted_certs_paths but don't exit (only warn). - // the result can be an empty trust store. - for path in &config.trusted_certs_paths { - std::fs::read(path).map_or_else( - |e| warn!("Failed to read trusted certificate: {e}"), - |pem| { - let _ = X509::from_pem(&pem) - .and_then(|certs| store_builder.add_cert(certs.to_owned())) - .map_err(|e| warn!("Failed to add certificate to trust store: {e}")); - }, - ); - } - - Ok(Self { - trusted_certs: store_builder.build(), - }) - } -} - -#[async_trait] -impl AttestationTokenVerifier for CoCoAttestationTokenVerifier { - async fn verify(&self, token: String) -> Result { - let split_token: Vec<&str> = token.split('.').collect(); - if !split_token.len() == 3 { - bail!("Illegal JWT format") - } - - let header = URL_SAFE_NO_PAD.decode(split_token[0])?; - let claims = URL_SAFE_NO_PAD.decode(split_token[1])?; - let signature = URL_SAFE_NO_PAD.decode(split_token[2])?; - - let header_value = serde_json::from_slice::(&header)?; - let claims_value = serde_json::from_slice::(&claims)?; - - let now = time::OffsetDateTime::now_utc().unix_timestamp(); - let Some(exp) = claims_value["exp"].as_i64() else { - bail!("token expiration unset"); - }; - if exp < now { - bail!("token expired"); - } - if let Some(nbf) = claims_value["nbf"].as_i64() { - if nbf > now { - bail!("before validity"); - } - } - - let jwk_value = claims_value["jwk"].as_object().ok_or_else(|| anyhow!("CoCo Attestation Token Claims must contain public key (JWK format) to verify signature"))?; - let jwk = serde_json::to_string(&jwk_value)?; - let rsa_jwk = serde_json::from_str::(&jwk)?; - let payload = format!("{}.{}", &split_token[0], &split_token[1]) - .as_bytes() - .to_vec(); - - match header_value["alg"].as_str() { - Some("RS384") => { - if rsa_jwk.alg != *"RS384" { - bail!("Unmatched RSA JWK alg"); - } - rs384_verify(&payload, &signature, &rsa_jwk)?; - } - None => { - bail!("Miss `alg` in JWT header") - } - _ => { - bail!("Unsupported JWT algrithm") - } - } - - if self.trusted_certs.all_certificates().is_empty() { - warn!("No Trusted Certificate in Config, skip verification of JWK cert of Attestation Token"); - return Ok(serde_json::to_string(&claims_value)?); - }; - - let mut cert_chain: Vec = vec![]; - - // Get certificate chain from 'x5c' or 'x5u' in JWK. - if let Some(x5c) = rsa_jwk.x5c { - for base64_der_cert in x5c { - let der_cert = URL_SAFE_NO_PAD.decode(base64_der_cert)?; - let cert = X509::from_der(&der_cert)?; - cert_chain.push(cert) - } - } else if let Some(x5u) = rsa_jwk.x5u { - download_cert_chain(x5u, &mut cert_chain).await?; - } else { - bail!("Missing certificate in Attestation Token JWK"); - } - - // Check certificate is valid and trustworthy - let mut untrusted_stack = Stack::::new()?; - for cert in cert_chain.iter().skip(1) { - untrusted_stack.push(cert.clone())?; - } - let mut context = X509StoreContext::new()?; - if !context.init( - &self.trusted_certs, - &cert_chain[0], - &untrusted_stack, - |ctx| ctx.verify_cert(), - )? { - bail!("Untrusted certificate in Attestation Token JWK"); - }; - - // Check the public key in JWK is consistent with the public key in certificate - let n = openssl::bn::BigNum::from_slice(&URL_SAFE_NO_PAD.decode(&rsa_jwk.n)?)?; - let e = openssl::bn::BigNum::from_slice(&URL_SAFE_NO_PAD.decode(&rsa_jwk.e)?)?; - let rsa_public_key = Rsa::from_public_components(n, e)?; - let rsa_pkey = PKey::from_rsa(rsa_public_key)?; - let cert_pub_key = cert_chain[0].public_key()?; - if !cert_pub_key.public_eq(&rsa_pkey) { - bail!("Certificate Public Key Mismatched in Attestation Token"); - } - - Ok(serde_json::to_string(&claims_value)?) - } -} - -#[allow(dead_code)] -#[derive(serde::Deserialize, Clone, Debug)] -struct RsaJWK { - kty: String, - alg: String, - n: String, - e: String, - x5u: Option, - x5c: Option>, -} - -fn rs384_verify(payload: &[u8], signature: &[u8], jwk: &RsaJWK) -> Result<()> { - let n = openssl::bn::BigNum::from_slice(&URL_SAFE_NO_PAD.decode(&jwk.n)?)?; - let e = openssl::bn::BigNum::from_slice(&URL_SAFE_NO_PAD.decode(&jwk.e)?)?; - let rsa_public_key = Rsa::from_public_components(n, e)?; - let rsa_pkey = PKey::from_rsa(rsa_public_key)?; - - let mut verifier = Verifier::new(MessageDigest::sha384(), &rsa_pkey)?; - verifier.update(payload)?; - - if !verifier.verify(signature)? { - bail!("RS384 verify failed") - } - - Ok(()) -} - -async fn download_cert_chain(url: String, chain: &mut Vec) -> Result<()> { - let res = reqwest::get(url).await?; - match res.status() { - reqwest::StatusCode::OK => { - let pem_cert_chain = res.text().await?; - parse_pem_cert_chain(pem_cert_chain, chain)?; - } - _ => { - bail!( - "Request x5u in Attestation Token JWK Failed, Response: {:?}", - res.text().await? - ); - } - } - - Ok(()) -} - -fn parse_pem_cert_chain(pem_cert_chain: String, chain: &mut Vec) -> Result<()> { - for pem in pem_cert_chain.split("-----END CERTIFICATE-----") { - let trimmed = format!("{}\n-----END CERTIFICATE-----", pem.trim()); - if !trimmed.starts_with("-----BEGIN CERTIFICATE-----") { - continue; - } - let cert = X509::from_pem(trimmed.as_bytes()) - .map_err(|_| anyhow!("Invalid PEM certificate chain"))?; - chain.push(cert); - } - - Ok(()) -} - -#[allow(unused_imports)] -mod test { - use super::*; - - #[test] - fn test_parse_pem_cert_chain() { - let pem_cert_chain = std::fs::read_to_string("test/data/test_cert_chain.pem").unwrap(); - let mut chain: Vec = Vec::new(); - assert!(parse_pem_cert_chain(pem_cert_chain, &mut chain).is_ok()); - assert_eq!(chain.len(), 2); - } -} diff --git a/kbs/src/token/error.rs b/kbs/src/token/error.rs new file mode 100644 index 000000000..835de1e90 --- /dev/null +++ b/kbs/src/token/error.rs @@ -0,0 +1,30 @@ +// Copyright (c) 2024 by Alibaba. +// Licensed under the Apache License, Version 2.0, see LICENSE for details. +// SPDX-License-Identifier: Apache-2.0 + +use log::error; +use strum::AsRefStr; +use thiserror::Error; + +pub type Result = std::result::Result; + +#[derive(Error, AsRefStr, Debug)] +pub enum Error { + #[error("Failed to verify Attestation Token")] + TokenVerificationFailed { + #[source] + source: anyhow::Error, + }, + + #[error("Failed to initialize Token Verifier")] + TokenVerifierInitialization { + #[source] + source: anyhow::Error, + }, + + #[error("Tee public key is not found inside the claims of token")] + NoTeePubKeyClaimFound, + + #[error("Failed to parse Tee public key")] + TeePubKeyParseFailed, +} diff --git a/kbs/src/token/jwk.rs b/kbs/src/token/jwk.rs index 2bb29eec6..b51b7c9a1 100644 --- a/kbs/src/token/jwk.rs +++ b/kbs/src/token/jwk.rs @@ -2,10 +2,18 @@ // Licensed under the Apache License, Version 2.0, see LICENSE for details. // SPDX-License-Identifier: Apache-2.0 -use crate::token::{AttestationTokenVerifier, AttestationTokenVerifierConfig}; -use anyhow::*; -use async_trait::async_trait; -use jsonwebtoken::{decode, decode_header, jwk, Algorithm, DecodingKey, Validation}; +use crate::token::AttestationTokenVerifierConfig; +use anyhow::{anyhow, bail, Context}; +use base64::engine::general_purpose::URL_SAFE_NO_PAD; +use base64::Engine; +use jsonwebtoken::jwk::{AlgorithmParameters, Jwk}; +use jsonwebtoken::{decode, decode_header, jwk, Algorithm, DecodingKey, Header, Validation}; +use openssl::bn::BigNum; +use openssl::pkey::PKey; +use openssl::stack::Stack; +use openssl::x509::store::X509StoreBuilder; +use openssl::x509::X509StoreContext; +use openssl::{rsa::Rsa, x509::X509}; use reqwest::{get, Url}; use serde::Deserialize; use serde_json::Value; @@ -14,6 +22,7 @@ use std::io::BufReader; use std::result::Result::Ok; use std::str::FromStr; use thiserror::Error; +use tokio::fs; const OPENID_CONFIG_URL_SUFFIX: &str = ".well-known/openid-configuration"; @@ -32,11 +41,14 @@ struct OpenIDConfig { jwks_uri: String, } +#[derive(Clone)] pub struct JwkAttestationTokenVerifier { - trusted_certs: jwk::JwkSet, + trusted_jwk_sets: jwk::JwkSet, + trusted_certs: Vec, + insecure_key: bool, } -pub async fn get_jwks_from_file_or_url(p: &str) -> Result { +async fn get_jwks_from_file_or_url(p: &str) -> Result { let mut url = Url::parse(p).map_err(|e| JwksGetError::InvalidSourcePath(e.to_string()))?; match url.scheme() { "https" => { @@ -73,37 +85,122 @@ pub async fn get_jwks_from_file_or_url(p: &str) -> Result Result { - let mut trusted_certs = jwk::JwkSet { keys: Vec::new() }; + pub async fn new(config: &AttestationTokenVerifierConfig) -> anyhow::Result { + let mut trusted_jwk_sets = jwk::JwkSet { keys: Vec::new() }; - for path in config.trusted_certs_paths.iter() { + for path in config.trusted_jwk_sets.iter() { match get_jwks_from_file_or_url(path).await { - Ok(mut jwkset) => trusted_certs.keys.append(&mut jwkset.keys), + Ok(mut jwkset) => trusted_jwk_sets.keys.append(&mut jwkset.keys), Err(e) => log::warn!("error getting JWKS: {:?}", e), } } - Ok(Self { trusted_certs }) + let mut trusted_certs = Vec::new(); + for path in &config.trusted_certs_paths { + let cert_content = fs::read(path).await.map_err(|_| { + JwksGetError::AccessFailed(format!("failed to read certificate {path}")) + })?; + let cert = X509::from_pem(&cert_content)?; + trusted_certs.push(cert); + } + + Ok(Self { + trusted_jwk_sets, + trusted_certs, + insecure_key: config.insecure_key, + }) } -} -#[async_trait] -impl AttestationTokenVerifier for JwkAttestationTokenVerifier { - async fn verify(&self, token: String) -> Result { - if self.trusted_certs.keys.is_empty() { + fn verify_jwk_endorsement(&self, key: &Jwk) -> anyhow::Result<()> { + let AlgorithmParameters::RSA(rsa) = &key.algorithm else { + bail!("Only supports RSA JWK now"); + }; + + let n = URL_SAFE_NO_PAD + .decode(&rsa.n) + .context("decode RSA public key parameter n")?; + let n = BigNum::from_slice(&n)?; + let e = URL_SAFE_NO_PAD + .decode(&rsa.e) + .context("decode RSA public key parameter e")?; + let e = BigNum::from_slice(&e)?; + + let public_key = Rsa::from_public_components(n, e)?; + let public_key = PKey::from_rsa(public_key)?; + + let Some(x5c) = &key.common.x509_chain else { + bail!("No x5c extension inside JWK. Malwared public key.") + }; + + if x5c.is_empty() { + bail!("No x5c extension inside JWK. Malwared public key.") + } + + let pem = x5c[0].split('\n').collect::(); + let der = URL_SAFE_NO_PAD.decode(pem).context("Illegal x5c cert")?; + + let leaf_cert = X509::from_der(&der).context("malwared x509 in x5c")?; + // verify the public key matches the leaf cert + if !public_key.public_eq(leaf_cert.public_key()?.as_ref()) { + bail!("jwk does not match x5c"); + }; + + let mut cert_chain = Stack::new()?; + for cert in &x5c[1..] { + let pem = cert.split('\n').collect::(); + let der = URL_SAFE_NO_PAD.decode(&pem).context("Illegal x5c cert")?; + + let cert = X509::from_der(&der).context("malwared x509 in x5c")?; + cert_chain.push(cert)?; + } + + let mut trust_store_builder = X509StoreBuilder::new()?; + for cert in &self.trusted_certs { + trust_store_builder.add_cert(cert.clone())?; + } + let trust_store = trust_store_builder.build(); + + // verify the cert chain + let mut ctx = X509StoreContext::new()?; + if !ctx.init(&trust_store, &leaf_cert, &cert_chain, |c| c.verify_cert())? { + bail!("The JWK is malwared because no trust anchor can verify it."); + } + Ok(()) + } + + fn get_verification_jwk<'a>(&'a self, header: &'a Header) -> anyhow::Result<&'a Jwk> { + if let Some(key) = &header.jwk { + if self.insecure_key { + return Ok(key); + } + if self.trusted_certs.is_empty() { + bail!("Cannot verify token since trusted cert is empty"); + }; + self.verify_jwk_endorsement(key)?; + return Ok(key); + } + + if self.trusted_jwk_sets.keys.is_empty() { bail!("Cannot verify token since trusted JWK Set is empty"); }; - let kid = decode_header(&token) - .context("Failed to decode attestation token header")? + let kid = header .kid + .as_ref() .ok_or(anyhow!("Failed to decode kid in the token header"))?; let key = &self - .trusted_certs - .find(&kid) + .trusted_jwk_sets + .find(kid) .ok_or(anyhow!("Failed to find Jwk with kid {kid} in JwkSet"))?; + Ok(key) + } + + pub async fn verify(&self, token: String) -> anyhow::Result { + let header = decode_header(&token).context("Failed to decode attestation token header")?; + + let key = self.get_verification_jwk(&header)?; let key_alg = key .common .key_algorithm @@ -116,7 +213,7 @@ impl AttestationTokenVerifier for JwkAttestationTokenVerifier { let token_data = decode::(&token, &dkey, &Validation::new(alg)) .context("Failed to decode attestation token")?; - Ok(serde_json::to_string(&token_data.claims)?) + Ok(token_data.claims) } } diff --git a/kbs/src/token/mod.rs b/kbs/src/token/mod.rs index 30f509061..6381c9041 100644 --- a/kbs/src/token/mod.rs +++ b/kbs/src/token/mod.rs @@ -2,54 +2,92 @@ // Licensed under the Apache License, Version 2.0, see LICENSE for details. // SPDX-License-Identifier: Apache-2.0 -use anyhow::*; -use async_trait::async_trait; +use jwk::JwkAttestationTokenVerifier; +use kbs_types::TeePubKey; +use log::debug; use serde::Deserialize; -use std::sync::Arc; -use strum::EnumString; -use tokio::sync::RwLock; +use serde_json::Value; -mod coco; +mod error; pub(crate) mod jwk; +pub use error::*; -#[async_trait] -pub trait AttestationTokenVerifier { - /// Verify an signed attestation token. - /// Returns the custom claims JSON string of the token. - async fn verify(&self, token: String) -> Result; -} - -#[derive(Deserialize, Default, Debug, Clone, EnumString)] -pub enum AttestationTokenVerifierType { - #[default] - CoCo, - Jwk, -} +pub const TOKEN_TEE_PUBKEY_PATH_ITA: &str = "/attester_runtime_data/tee-pubkey"; +pub const TOKEN_TEE_PUBKEY_PATH_COCO: &str = "/customized_claims/runtime_data/tee-pubkey"; -#[derive(Deserialize, Debug, Clone)] +#[derive(Deserialize, Debug, Clone, PartialEq, Default)] pub struct AttestationTokenVerifierConfig { #[serde(default)] - pub attestation_token_type: AttestationTokenVerifierType, + /// The paths to the tee public key in the JWT body. For example, + /// `/attester_runtime_data/tee-pubkey` refers to the key + /// `attester_runtime_data.tee-pubkey` inside the JWT body claims. + /// + /// If a JWT is received, the [`TokenVerifier`] will try to extract + /// the tee public key from built-in ones ([`TOKEN_TEE_PUBKEY_PATH_ITA`], + /// [`TOKEN_TEE_PUBKEY_PATH_COCO`]) and the configured `extra_teekey_paths`. + /// + /// This field will default to an empty vector. + pub extra_teekey_paths: Vec, - /// Trusted Certificates file (PEM format) path (for "CoCo") or a valid Url - /// (file:// and https:// schemes accepted) pointing to a local JWKSet file + /// Trusted Certificates file (PEM format) paths use to verify Attestation + /// Token Signature. + #[serde(default)] + pub trusted_certs_paths: Vec, + + /// Urls (file:// and https:// schemes accepted) pointing to a local JWKSet file /// or to an OpenID configuration url giving a pointer to JWKSet certificates /// (for "Jwk") to verify Attestation Token Signature. #[serde(default)] - pub trusted_certs_paths: Vec, + pub trusted_jwk_sets: Vec, + + /// Whether a JWK that directly comes from the JWT token is allowed to verify + /// the signature. This is insecure as it will not check the endorsement of + /// the JWK. If this option is set to false, the JWK will be looked up from + /// the key store configured during launching the KBS with kid field in the JWT, + /// or be checked against the configured trusted CA certs. + #[serde(default = "bool::default")] + pub insecure_key: bool, } -pub async fn create_token_verifier( - config: AttestationTokenVerifierConfig, -) -> Result>> { - match config.attestation_token_type { - AttestationTokenVerifierType::CoCo => Ok(Arc::new(RwLock::new( - coco::CoCoAttestationTokenVerifier::new(&config)?, - )) - as Arc>), - AttestationTokenVerifierType::Jwk => Ok(Arc::new(RwLock::new( - jwk::JwkAttestationTokenVerifier::new(&config).await?, - )) - as Arc>), +#[derive(Clone)] +pub struct TokenVerifier { + verifier: JwkAttestationTokenVerifier, + extra_teekey_paths: Vec, +} + +impl TokenVerifier { + pub async fn verify(&self, token: String) -> Result { + self.verifier + .verify(token) + .await + .map_err(|e| Error::TokenVerificationFailed { source: e }) + } + + pub async fn from_config(config: AttestationTokenVerifierConfig) -> Result { + let verifier = JwkAttestationTokenVerifier::new(&config) + .await + .map_err(|e| Error::TokenVerifierInitialization { source: e })?; + + let mut extra_teekey_paths = config.extra_teekey_paths; + extra_teekey_paths.push(TOKEN_TEE_PUBKEY_PATH_ITA.into()); + extra_teekey_paths.push(TOKEN_TEE_PUBKEY_PATH_COCO.into()); + + Ok(Self { + verifier, + extra_teekey_paths, + }) + } + + /// Different attestation service would embed tee public key + /// in different parts of the claims. + pub fn extract_tee_public_key(&self, claim: Value) -> Result { + for path in &self.extra_teekey_paths { + if let Some(pkey_value) = claim.pointer(path) { + debug!("Extract tee public key from {path}"); + return TeePubKey::deserialize(pkey_value).map_err(|_| Error::TeePubKeyParseFailed); + } + } + + Err(Error::NoTeePubKeyClaimFound) } } diff --git a/kbs/test/config/kbs.toml b/kbs/test/config/kbs.toml index 0f08b733f..7c6314ab5 100644 --- a/kbs/test/config/kbs.toml +++ b/kbs/test/config/kbs.toml @@ -5,7 +5,7 @@ private_key = "./work/https.key" certificate = "./work/https.crt" [attestation_token_config] -attestation_token_type = "CoCo" +trusted_certs_paths = ["./work/token-cert.pem"] [repository_config] type = "LocalFs" diff --git a/kbs/test/config/resource-kbs.toml b/kbs/test/config/resource-kbs.toml index 5c14ab519..8abbec27e 100644 --- a/kbs/test/config/resource-kbs.toml +++ b/kbs/test/config/resource-kbs.toml @@ -3,7 +3,6 @@ auth_public_key = "./work/kbs.pem" insecure_http = true [attestation_token_config] -attestation_token_type = "CoCo" trusted_certs_paths = ["./work/ca-cert.pem"] [repository_config]