diff --git a/container/pod-quote/Dockerfile b/container/pod-quote/Dockerfile new file mode 100644 index 00000000..a7e83afe --- /dev/null +++ b/container/pod-quote/Dockerfile @@ -0,0 +1,30 @@ +FROM rust:1.74.0 as pod-quote-builder + +RUN apt-get update && apt-get install -y \ + build-essential \ + curl \ + make \ + libprotobuf-dev \ + protobuf-compiler \ + musl-dev \ + wget \ + libssl-dev \ + pkg-config + +WORKDIR /app + +COPY . . + +COPY service/pod-quote /pod-quote + +RUN cd /pod-quote && make build + +FROM rust:1.74.0 + +WORKDIR /app +COPY --from=pod-quote-builder /pod-quote/target/release/pod_quote /app/pod_quote + +RUN chmod a+x /app/pod_quote + +# Run the sleep command for demonstration purposes +CMD ["/app/pod_quote"] diff --git a/deployment/manifests/pod-quote-deployment.yaml b/deployment/manifests/pod-quote-deployment.yaml new file mode 100644 index 00000000..e381a628 --- /dev/null +++ b/deployment/manifests/pod-quote-deployment.yaml @@ -0,0 +1,85 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: ccnp +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: pod-reader + namespace: ccnp +rules: +- apiGroups: [""] + resources: ["pods"] + verbs: ["get", "list"] # Adjust the verbs as needed +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: read-pods + namespace: ccnp +subjects: +- kind: ServiceAccount + name: default + namespace: ccnp +roleRef: + kind: Role + name: pod-reader + apiGroup: rbac.authorization.k8s.io +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: sleep-qs-pod + namespace: ccnp + labels: + app: sleep-qs-pod +spec: + replicas: 1 + selector: + matchLabels: + app: sleep-qs-pod + template: + metadata: + labels: + app: sleep-qs-pod + spec: + tolerations: + - key: node-role.kubernetes.io/control-plane + operator: Exists + effect: NoSchedule + - key: node-role.kubernetes.io/master + operator: Exists + effect: NoSchedule + containers: + - name: sleep-container + image: curlimages/curl + imagePullPolicy: IfNotPresent + command: ["/bin/sleep", "infinity"] + - name: pod-quote + image: docker.io/library/ccnp-pod-quote:0.1 + imagePullPolicy: IfNotPresent + env: + - name: POD_NAME + valueFrom: + fieldRef: + fieldPath: metadata.name + - name: POD_NAMESPACE + valueFrom: + fieldRef: + fieldPath: metadata.namespace + resources: + limits: + tdx.intel.com/tdx-guest: 1 + volumeMounts: + - name: podinfo + mountPath: /etc/podinfo + nodeSelector: + intel.feature.node.kubernetes.io/tdx-guest: "enabled" + volumes: + - name: podinfo + downwardAPI: + items: + - path: "label" + fieldRef: + fieldPath: metadata.labels diff --git a/service/pod-quote/Cargo.toml b/service/pod-quote/Cargo.toml new file mode 100644 index 00000000..58bbecd2 --- /dev/null +++ b/service/pod-quote/Cargo.toml @@ -0,0 +1,37 @@ +[package] +name = "pod_quote" +version = "0.1.0" +edition = "2021" + +[[bin]] # Bin to run the quote server +name = "pod_quote" +path = "src/pod_quote.rs" + +[dependencies] +tonic = "0.9" +prost = "0.11" +tokio = { version = "1.0", features = ["macros", "rt-multi-thread"] } +tokio-stream = "0.1.14" +anyhow = "1.0" +async-trait = "0.1.56" +base64 = "0.13.0" +log = "0.4.14" +serde_json = "1.0" +sha2 = "0.10" +clap = { version = "4.0.29", features = ["derive"] } +tonic-reflection = "0.9.2" +tonic-health = "0.9.2" +nix = "0.26.2" +tdx_attest = "0.1.1" +kube = { version = "0.74.0", features = ["runtime", "derive"] } +k8s-openapi = { version = "0.15.0", features = ["v1_24"] } +crypto-hash = "0.3.3" +async-std = "1.8" +hyper = { version ="0.14.27" } + +[dev-dependencies] +tower = { version = "0.4", features = ["util"] } +serial_test = { version ="2.0.0" } + +[build-dependencies] +tonic-build = "0.9" diff --git a/service/pod-quote/Makefile b/service/pod-quote/Makefile new file mode 100644 index 00000000..e6d9aeba --- /dev/null +++ b/service/pod-quote/Makefile @@ -0,0 +1,40 @@ +# Copyright (c) 2023, Intel Corporation. All rights reserved.
+# SPDX-License-Identifier: Apache-2.0 + +PROJDIR := $(shell readlink -f ..) +TOP_DIR := . +CUR_DIR := $(shell pwd) +PREFIX := +DESTDIR ?= $(PREFIX)/bin + +DEBUG ?= + +TARGET_DIR := target +BIN_NAME := pod_quote + +CARGO := /usr/local/cargo/bin/cargo + +ifdef DEBUG + release := + TARGET_DIR := $(TARGET_DIR)/debug +else + release := --release + TARGET_DIR := $(TARGET_DIR)/release +endif + +TARGET := $(TARGET_DIR)/$(BIN_NAME) + +test: + $(CARGO) test + +build: + $(CARGO) build $(release) + +install: + install -D -m0755 $(TARGET) $(DESTDIR) + +uninstall: + rm -f $(DESTDIR)/$(BIN_NAME) + +clean: + $(CARGO) clean diff --git a/service/pod-quote/README.md b/service/pod-quote/README.md new file mode 100644 index 00000000..0f6d42a3 --- /dev/null +++ b/service/pod-quote/README.md @@ -0,0 +1,68 @@ +# Service: CCNP Pod Quote + +This service will provide quote generated by underlying TEE platform for remote attestation service to verify the integrity and confidentiality of the trusted computing environment and required software environment. + +## Introduction + +This server provides functionality to fetch quote of underlying TEE platform with nonce as mandatory input and a base64 encoded user data as optional input.The nonce and user data will be digested and added into quote for remote attestation to verify the freshness of the quote and the user specified data. And it also provides a HTTP REST API for fetching the quote data of current pod which is based on the image IDs of each container in Kubernetes cluster. + +## Installation +The pod quote service can be deployed as a sidecar according to different user scenarios. + +### Prerequisite +User need to have a kubernetes cluster ready to deploy the services. To simplify the deployment process, we provide Helm as one of the options to deploy the service. Please install Helm by following the [Helm official guide](https://helm.sh/docs/intro/install/). However, user can also use the yaml files located in the manifests folder for deployment. +Also, the ccnp device plugin need to installed before the installation of quote server. Please refer to its [deployment guide](../../device-plugin/ccnp-device-plugin/README.md) for installation. + +### Build docker image +The Dockerfile for the service can be found under `container/quote-server` directory. Use the following command to build the image: + +``` +docker build -t ccnp-pod-quote:0.1 -f container/pod-quote/Dockerfile . +``` + +> Note: if you are using containerd as the default runtime for kubernetes, don't forget to use the following commands to import the image into containerd first: +``` +docker save -o ccnp-pod-quote.tar ccnp-pod-quote:0.1 +ctr -n=k8s.io image import ccnp-pod-quote.tar +``` + +### Deploy as DaemonSet in Kubernetes + +#### deploy using manifests yaml file + +please check file `deployment/manifests/pod-quote-deployment.yaml` to confirm the container image to use and run: +``` +kubectl apply -f deployment/manifests/pod-quote-deployment.yaml +``` + +## Testing +You can play with service on host by following the steps below: + +1. Start the pod quote service + +``` +root@tdx-guest:~# ls -l /run/ccnp/ +total 0 +drwxr-xrwx 2 root root 60 Sep 1 05:05 uds +``` + +And then build and run the quote server with binary: +``` +cd service/pod-quote +make build +./target/release/pod-quote +``` +2. Play with the service +Provide a HTTP API for fetching the quote data in `localhost:3000/quote` + +Get quote from the TDX platform: +``` +curl -s http://localhost:3000/quote +``` + +and the output should be as bellow: +``` +{ + "BAACAIEAAAAAAAAAk5pyM/ecTKmUCg2zlX8GB6P8pz1eLkNLuYzlFq7gmQ4AAAAABAAGAAAAAAAAAAAAAAAAAEj6aZSdsIAC7oQlKEf1cpiLHW5WjsE1P2TLbA/ZBTdfaa2VnA6vd0escKOSeJMCoQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAADnAgYAAAAAAKSgAzRsWhmm/SUEcehyvQcdjJLXQxq9pGNBeAihc4OqDUKYeBS8kvX1nGBEtnf1FAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAO8sw2a5sIgf6+MgszlJcgpq4sP0tqpfKTspZny2PWPOYiJbt1aPe1rpeqDtoa+NreW8yD5Jj8ypKTGA+WfwPH77+negDf9ZWNeonsxRtNtsLoUabMeutZ+xSCbs5gUWwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAO+/PqJMpmXxEfoJELnxuPmAbw69VjN9ZQQqe3NjVd72ql13C43JW093ytpa7ipkSR7msIelxHz9nmDTkSucmGPMEAAA0doq0wnfzK1RM8LVMVECwOpiI5ePJkQ7KClggtiBrCrBhE6p3ECY4SUVhWET733tRdTwhkH3JNCkvTIRGeXE3SY1oRIxb7dVmrMVrWdmmfldJoB8RvpN7HOs/g788NOgFoemd5F95mGdNVJ3v0ppPvgJQ5ryby6SOYHAsL25dbkGAEYQAAAGBhMVA/8ABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAVAAAAAAAAAOcAAAAAAAAAOWseNYAkJ5SHxHr5xWG93BUlhjmq0t2vdgCQ70Y/C4QAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAANyeKnxvlI8XR040p/xD7QMPfBVj8bq932NAyC4OVKjFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOYzIRlhSwSQeVYIlVCCAgcDb+K5pY0hOxqAEm1vN5p/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACXrF4+4CGy/IYdLV5k30DF9yMXwa6zYhBz8QproGhnTZPTIEXSIJLkzgEx9OaFrepQWCu+wbggzJVLqv9hg6a5IAAAAQIDBAUGBwgJCgsMDQ4PEBESExQVFhcYGRobHB0eHwUAXg4AAC0tLS0tQkVHSU4gQ0VSVElGSUNBVEUtLS0tLQpNSUlFOERDQ0JKYWdBd0lCQWdJVWR2clZDNnpJTFRvbGI0ZEt5RGhsbFlaUEc1RXdDZ1lJS29aSXpqMEVBd0l3CmNERWlNQ0FHQTFVRUF3d1pTVzUwWld3Z1UwZFlJRkJEU3lCUWJHRjBabTl5YlNCRFFURWFNQmdHQTFVRUNnd1IKU1c1MFpXd2dRMjl5Y0c5eVlYUnBiMjR4RkRBU0JnTlZCQWNNQzFOaGJuUmhJRU5zWVhKaE1Rc3dDUVlEVlFRSQpEQUpEUVRFTE1Ba0dBMVVFQmhNQ1ZWTXdIaGNOTWpNd05URTJNRGd5TXpJd1doY05NekF3TlRFMk1EZ3lNekl3CldqQndNU0l3SUFZRFZRUUREQmxKYm5SbGJDQlRSMWdnVUVOTElFTmxjblJwWm1sallYUmxNUm93R0FZRFZRUUsKREJGSmJuUmxiQ0JEYjNKd2IzSmhkR2x2YmpFVU1CSUdBMVVFQnd3TFUyRnVkR0VnUTJ4aGNtRXhDekFKQmdOVgpCQWdNQWtOQk1Rc3dDUVlEVlFRR0V3SlZVekJaTUJNR0J5cUdTTTQ5QWdFR0NDcUdTTTQ5QXdFSEEwSUFCUHZQClNtKzJtU1R2TzE0RkhpOXd3K05qYUhzazhyVHFQQ0xEMDZ3MmtJVE9yb0RYSmN5NDBMbHRZemFBZ3JXR2FsWFoKTy9GY3cxc0padDZZdFNRVHlyU2pnZ01NTUlJRENEQWZCZ05WSFNNRUdEQVdnQlNWYjEzTnZSdmg2VUJKeWRUMApNODRCVnd2ZVZEQnJCZ05WSFI4RVpEQmlNR0NnWHFCY2hscG9kSFJ3Y3pvdkwyRndhUzUwY25WemRHVmtjMlZ5CmRtbGpaWE11YVc1MFpXd3VZMjl0TDNObmVDOWpaWEowYVdacFkyRjBhVzl1TDNZMEwzQmphMk55YkQ5allUMXcKYkdGMFptOXliU1psYm1OdlpHbHVaejFrWlhJd0hRWURWUjBPQkJZRUZEUGs4eit4L0JtVkw5UDVJTkRaNlhlUwpTOHR4TUE0R0ExVWREd0VCL3dRRUF3SUd3REFNQmdOVkhSTUJBZjhFQWpBQU1JSUNPUVlKS29aSWh2aE5BUTBCCkJJSUNLakNDQWlZd0hnWUtLb1pJaHZoTkFRMEJBUVFRUGYyUXdCNHRTYzhyRmxvZGJJQzlCVENDQVdNR0NpcUcKU0liNFRRRU5BUUl3Z2dGVE1CQUdDeXFHU0liNFRRRU5BUUlCQWdFRk1CQUdDeXFHU0liNFRRRU5BUUlDQWdFRgpNQkFHQ3lxR1NJYjRUUUVOQVFJREFnRUNNQkFHQ3lxR1NJYjRUUUVOQVFJRUFnRUNNQkFHQ3lxR1NJYjRUUUVOCkFRSUZBZ0VETUJBR0N5cUdTSWI0VFFFTkFRSUdBZ0VCTUJBR0N5cUdTSWI0VFFFTkFRSUhBZ0VBTUJBR0N5cUcKU0liNFRRRU5BUUlJQWdFRE1CQUdDeXFHU0liNFRRRU5BUUlKQWdFQU1CQUdDeXFHU0liNFRRRU5BUUlLQWdFQQpNQkFHQ3lxR1NJYjRUUUVOQVFJTEFnRUFNQkFHQ3lxR1NJYjRUUUVOQVFJTUFnRUFNQkFHQ3lxR1NJYjRUUUVOCkFRSU5BZ0VBTUJBR0N5cUdTSWI0VFFFTkFRSU9BZ0VBTUJBR0N5cUdTSWI0VFFFTkFRSVBBZ0VBTUJBR0N5cUcKU0liNFRRRU5BUUlRQWdFQU1CQUdDeXFHU0liNFRRRU5BUUlSQWdFTE1COEdDeXFHU0liNFRRRU5BUUlTQkJBRgpCUUlDQXdFQUF3QUFBQUFBQUFBQU1CQUdDaXFHU0liNFRRRU5BUU1FQWdBQU1CUUdDaXFHU0liNFRRRU5BUVFFCkJnQ0Fid1VBQURBUEJnb3Foa2lHK0UwQkRRRUZDZ0VCTUI0R0NpcUdTSWI0VFFFTkFRWUVFQUxFbzJLdDd4d3QKNmhQZGdZekRNMFl3UkFZS0tvWklodmhOQVEwQkJ6QTJNQkFHQ3lxR1NJYjRUUUVOQVFjQkFRSC9NQkFHQ3lxRwpTSWI0VFFFTkFRY0NBUUVBTUJBR0N5cUdTSWI0VFFFTkFRY0RBUUgvTUFvR0NDcUdTTTQ5QkFNQ0EwZ0FNRVVDCklBTURNUDNSaUJOYVpuM2NLUjducFVxNDFkTm1HNzIzZlFYcWlJVTU0U09KQWlFQSsrZW9Ta1ZOa2NnbERLZncKaDNDbGx6UzNway9hSGhYNjZDUjc1TllJanpnPQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCi0tLS0tQkVHSU4gQ0VSVElGSUNBVEUtLS0tLQpNSUlDbGpDQ0FqMmdBd0lCQWdJVkFKVnZYYzI5RytIcFFFbkoxUFF6emdGWEM5NVVNQW9HQ0NxR1NNNDlCQU1DCk1HZ3hHakFZQmdOVkJBTU1FVWx1ZEdWc0lGTkhXQ0JTYjI5MElFTkJNUm93R0FZRFZRUUtEQkZKYm5SbGJDQkQKYjNKd2IzSmhkR2x2YmpFVU1CSUdBMVVFQnd3TFUyRnVkR0VnUTJ4aGNtRXhDekFKQmdOVkJBZ01Ba05CTVFzdwpDUVlEVlFRR0V3SlZVekFlRncweE9EQTFNakV4TURVd01UQmFGdzB6TXpBMU1qRXhNRFV3TVRCYU1IQXhJakFnCkJnTlZCQU1NR1VsdWRHVnNJRk5IV0NCUVEwc2dVR3hoZEdadmNtMGdRMEV4R2pBWUJnTlZCQW9NRVVsdWRHVnMKSUVOdmNuQnZjbUYwYVc5dU1SUXdFZ1lEVlFRSERBdFRZVzUwWVNCRGJHRnlZVEVMTUFrR0ExVUVDQXdDUTBFeApDekFKQmdOVkJBWVRBbFZUTUZrd0V3WUhLb1pJemowQ0FRWUlLb1pJemowREFRY0RRZ0FFTlNCLzd0MjFsWFNPCjJDdXpweHc3NGVKQjcyRXlER2dXNXJYQ3R4MnRWVExxNmhLazZ6K1VpUlpDbnFSN3BzT3ZncUZlU3hsbVRsSmwKZVRtaTJXWXozcU9CdXpDQnVEQWZCZ05WSFNNRUdEQVdnQlFpWlF6V1dwMDBpZk9EdEpWU3YxQWJPU2NHckRCUwpCZ05WSFI4RVN6QkpNRWVnUmFCRGhrRm9kSFJ3Y3pvdkwyTmxjblJwWm1sallYUmxjeTUwY25WemRHVmtjMlZ5CmRtbGpaWE11YVc1MFpXd3VZMjl0TDBsdWRHVnNVMGRZVW05dmRFTkJMbVJsY2pBZEJnTlZIUTRFRmdRVWxXOWQKemIwYjRlbEFTY25VOURQT0FWY0wzbFF3RGdZRFZSMFBBUUgvQkFRREFnRUdNQklHQTFVZEV3RUIvd1FJTUFZQgpBZjhDQVFBd0NnWUlLb1pJemowRUF3SURSd0F3UkFJZ1hzVmtpMHcraTZWWUdXM1VGLzIydWFYZTBZSkRqMVVlCm5BK1RqRDFhaTVjQ0lDWWIxU0FtRDV4a2ZUVnB2bzRVb3lpU1l4ckRXTG1VUjRDSTlOS3lmUE4rCi0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0KLS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUNqekNDQWpTZ0F3SUJBZ0lVSW1VTTFscWROSW56ZzdTVlVyOVFHemtuQnF3d0NnWUlLb1pJemowRUF3SXcKYURFYU1CZ0dBMVVFQXd3UlNXNTBaV3dnVTBkWUlGSnZiM1FnUTBFeEdqQVlCZ05WQkFvTUVVbHVkR1ZzSUVOdgpjbkJ2Y21GMGFXOXVNUlF3RWdZRFZRUUhEQXRUWVc1MFlTQkRiR0Z5WVRFTE1Ba0dBMVVFQ0F3Q1EwRXhDekFKCkJnTlZCQVlUQWxWVE1CNFhEVEU0TURVeU1URXdORFV4TUZvWERUUTVNVEl6TVRJek5UazFPVm93YURFYU1CZ0cKQTFVRUF3d1JTVzUwWld3Z1UwZFlJRkp2YjNRZ1EwRXhHakFZQmdOVkJBb01FVWx1ZEdWc0lFTnZjbkJ2Y21GMAphVzl1TVJRd0VnWURWUVFIREF0VFlXNTBZU0JEYkdGeVlURUxNQWtHQTFVRUNBd0NRMEV4Q3pBSkJnTlZCQVlUCkFsVlRNRmt3RXdZSEtvWkl6ajBDQVFZSUtvWkl6ajBEQVFjRFFnQUVDNm5Fd01ESVlaT2ovaVBXc0N6YUVLaTcKMU9pT1NMUkZoV0dqYm5CVkpmVm5rWTR1M0lqa0RZWUwwTXhPNG1xc3lZamxCYWxUVll4RlAyc0pCSzV6bEtPQgp1ekNCdURBZkJnTlZIU01FR0RBV2dCUWlaUXpXV3AwMGlmT0R0SlZTdjFBYk9TY0dyREJTQmdOVkhSOEVTekJKCk1FZWdSYUJEaGtGb2RIUndjem92TDJObGNuUnBabWxqWVhSbGN5NTBjblZ6ZEdWa2MyVnlkbWxqWlhNdWFXNTAKWld3dVkyOXRMMGx1ZEdWc1UwZFlVbTl2ZEVOQkxtUmxjakFkQmdOVkhRNEVGZ1FVSW1VTTFscWROSW56ZzdTVgpVcjlRR3prbkJxd3dEZ1lEVlIwUEFRSC9CQVFEQWdFR01CSUdBMVVkRXdFQi93UUlNQVlCQWY4Q0FRRXdDZ1lJCktvWkl6ajBFQXdJRFNRQXdSZ0loQU9XLzVRa1IrUzlDaVNEY05vb3dMdVBSTHNXR2YvWWk3R1NYOTRCZ3dUd2cKQWlFQTRKMGxySG9NcytYbzVvL3NYNk85UVd4SFJBdlpVR09kUlE3Y3ZxUlhhcUk9Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0KAA==" +} +``` diff --git a/service/pod-quote/deny.toml b/service/pod-quote/deny.toml new file mode 100644 index 00000000..6e656f7e --- /dev/null +++ b/service/pod-quote/deny.toml @@ -0,0 +1,35 @@ +[advisories] +vulnerability = "deny" +unmaintained = "warn" +yanked = "warn" +notice = "warn" + +[licenses] +unlicensed = "warn" +allow = [ + "MIT", + "Apache-2.0", + "ISC", + "BSD-3-Clause", + "Unicode-DFS-2016", +] + +copyleft = "warn" +allow-osi-fsf-free = "neither" +default = "deny" +confidence-threshold = 0.8 + +[[licenses.clarify]] +name = "ring" +expression = "MIT AND ISC AND OpenSSL" +license-files = [ + { path = "LICENSE", hash = 0xbd0eed23 } +] + +[bans] +multiple-versions = "warn" +wildcards = "allow" + +[sources] +unknown-registry = "warn" +unknown-git = "warn" \ No newline at end of file diff --git a/service/pod-quote/src/kube.rs b/service/pod-quote/src/kube.rs new file mode 100644 index 00000000..83bc0d10 --- /dev/null +++ b/service/pod-quote/src/kube.rs @@ -0,0 +1,60 @@ +/* +* Copyright (c) 2023, Intel Corporation. All rights reserved.
+* SPDX-License-Identifier: Apache-2.0 +*/ + +extern crate crypto_hash; +extern crate kube; + +use anyhow::{anyhow, Error}; +use k8s_openapi::api::core::v1::Pod; +use kube::api::Api; +use kube::Client; + +use std::env; + +const POD_NAME: &str = "POD_NAME"; +const POD_NAMESPACE: &str = "POD_NAMESPACE"; +const SEPARATOR: &str = "|"; + +pub async fn get_cur_pod_images_info() -> Result { + let mut pod_data_array: Vec = Vec::new(); + let namespace = env::var(POD_NAMESPACE).unwrap_or_default(); + let pod_name = env::var(POD_NAME).unwrap_or_default(); + + let client = Client::try_default().await?; + let pods: Api = Api::namespaced(client.clone(), &namespace); + + let pod_name_str = pod_name.clone(); + let cur_pod = pods.get(&pod_name_str).await?; + // Access the container statuses + if let Some(status) = cur_pod.status { + for container_status in status.container_statuses.unwrap_or_default() { + let image_id = container_status.image_id.clone(); + pod_data_array.push(image_id); + } + println!("pod quote data array:"); + // Print out the quote data of pod. + for item in &pod_data_array { + println!("{}", item); + } + + // Concat all pod quote data into one String. + let pod_image_id_data = pod_data_array.join(SEPARATOR); + return Ok(pod_image_id_data); + } else { + println!("Pod {pod_name} in {namespace} not found."); + let error_message = format!("Pod '{}' in '{}' not found.", pod_name, namespace); + return Err(anyhow!(error_message)); + } +} + +pub fn sha256_hash(input: &str) -> String { + // Convert the input string to bytes + let input_bytes = input.as_bytes(); + + // Calculate the SHA-256 hash + let hash = crypto_hash::hex_digest(crypto_hash::Algorithm::SHA256, input_bytes); + + hash +} diff --git a/service/pod-quote/src/pod_quote.rs b/service/pod-quote/src/pod_quote.rs new file mode 100644 index 00000000..2d0de1a1 --- /dev/null +++ b/service/pod-quote/src/pod_quote.rs @@ -0,0 +1,129 @@ +/* +* Copyright (c) 2023, Intel Corporation. All rights reserved.
+* SPDX-License-Identifier: Apache-2.0 +*/ + +use anyhow::*; +use clap::Parser; +use core::result::Result::Ok; +use hyper::service::{make_service_fn, service_fn}; +use hyper::{Request as HyperRequest, Response as HyperResponse, Body, Server as HyperServer}; +use std::net::SocketAddr; + +pub mod kube; +pub mod tee; +use tee::*; + +// A http server for provide the current pod quote data +#[derive(Copy, Clone)] +pub struct PerPodQuoteServer { + sock_address: SocketAddr, + local_tee: tee::TeeType, +} + +impl PerPodQuoteServer { + pub fn new(sock_address: SocketAddr, local_tee: tee::TeeType) -> Self { + PerPodQuoteServer { + sock_address, + local_tee, + } + } + + pub async fn start(&self) -> Result<(), hyper::Error> { + let local_tee = self.local_tee; + let make_svc = make_service_fn(|_conn| { + let service = service_fn(move |req| { + // Route request to the appropriate handler + Self::handle_request(local_tee, req) + }); + async move { Ok::<_, hyper::Error>(service) } + }); + let http_server = HyperServer::bind(&self.sock_address).serve(make_svc); + println!( + "The Pod Quote HTTP server is listening on: {:?}", + self.sock_address + ); + http_server.await + } + + // generate current pod quote based on its all containers' imageIDs + async fn get_current_pod_quote(local_tee: tee::TeeType) -> Result { + // Handle the "/quote" route + // Create an instance of your custom kube client + let pod_data = kube::get_cur_pod_images_info(); + match pod_data.await { + Ok(report_data) => { + let report_data_clone = report_data.clone(); + let hash_report_data = kube::sha256_hash(&report_data_clone); + let quote_data = get_quote( + local_tee, + hash_report_data.clone(), + hash_report_data.clone(), + ) + .unwrap(); + Ok(quote_data) + } + Err(error) => Err(anyhow!( + "There was a problem when get current pod images information: {:?}", + error + )), + } + } + + async fn handle_request( + local_tee: tee::TeeType, + req: HyperRequest + ) -> Result, hyper::Error> { + match req.uri().path() { + "/quote" => { + match Self::get_current_pod_quote(local_tee).await { + Ok(quote_data) => { + println!("File content: {}", quote_data); + // generate the response from quote file + let response = HyperResponse::new(Body::from(quote_data)); + Ok(response) + } + Err(err) => { + eprintln!("Error: {}", err); + let response = HyperResponse::builder() + .status(404) + .body(Body::from("Not Found Quote File")) + .unwrap(); + Ok(response) + } + } + } + _ => { + // Handle other routes + let response = HyperResponse::builder() + .status(404) + .body(Body::from("Not Found")) + .unwrap(); + Ok(response) + } + } + } +} + +#[derive(Parser)] +struct Cli { + port: String, +} + +#[tokio::main] +async fn main() -> Result<(), Box> { + let http_addr = SocketAddr::from(([127, 0, 0, 1], 3000)); + // Create the http server tokio task for fetching quote with current pod image IDs + let _ = tokio::spawn(async move { + let http_server = PerPodQuoteServer::new(http_addr, { + match tee::get_tee_type() { + tee::TeeType::PLAIN => panic!("Not found any TEE device!"), + t => t, + } + }); + if let Err(err) = http_server.start().await { + eprintln!("HTTP server error: {}", err); + } + }); + Ok(()) +} diff --git a/service/pod-quote/src/tee.rs b/service/pod-quote/src/tee.rs new file mode 100644 index 00000000..b05743c0 --- /dev/null +++ b/service/pod-quote/src/tee.rs @@ -0,0 +1,425 @@ +/* +* Copyright (c) 2023, Intel Corporation. All rights reserved.
+* SPDX-License-Identifier: Apache-2.0 +*/ + +use anyhow::*; +use sha2::{Digest, Sha512}; +use std::path::Path; +use std::result::Result::Ok; + +#[derive(Debug, Copy, Clone)] +pub enum TeeType { + TDX, + SEV, + TPM, + PLAIN, +} + +pub fn get_tee_type() -> TeeType { + if Path::new("/dev/tpm0").exists() { + TeeType::TPM + } else if Path::new("/dev/tdx-guest").exists() + || Path::new("/dev/tdx-attest").exists() + || Path::new("/dev/tdx_guest").exists() + { + if Path::new("/dev/tdx-attest").exists() { + panic!("[get_tee_type]: Deprecated device node /dev/tdx-attest, please upgrade to use /dev/tdx-guest or /dev/tdx_guest"); + } + TeeType::TDX + } else if Path::new("/dev/sev-guest").exists() || Path::new("/dev/sev").exists() { + TeeType::SEV + } else { + TeeType::PLAIN + } +} + +fn generate_tdx_report_data( + report_data: Option, + nonce: String, +) -> Result { + let nonce_decoded = match base64::decode(nonce) { + Ok(v) => v, + Err(e) => { + return Err(anyhow!( + "[generate_tdx_report_data] nonce is not base64 encoded: {:?}", + e + )) + } + }; + let mut hasher = Sha512::new(); + hasher.update(nonce_decoded); + let _ret = match report_data { + Some(_encoded_report_data) => { + if _encoded_report_data.is_empty() { + hasher.update("") + } else { + let decoded_report_data = match base64::decode(_encoded_report_data) { + Ok(v) => v, + Err(e) => { + return Err(anyhow!( + "[generate_tdx_report_data] user data is not base64 encoded: {:?}", + e + )) + } + }; + hasher.update(decoded_report_data) + } + } + None => hasher.update(""), + }; + let hash_array: [u8; 64] = hasher + .finalize() + .as_slice() + .try_into() + .expect("[generate_tdx_report_data] Wrong length of report data"); + Ok(base64::encode(hash_array)) +} + +fn get_tdx_quote(report_data: Option, nonce: String) -> Result { + let tdx_report_data = match generate_tdx_report_data(report_data, nonce) { + Ok(v) => v, + Err(e) => { + return Err(anyhow!("[get_tdx_quote]: {:?}", e)); + } + }; + + let quote = match tdx_attest::get_tdx_quote(tdx_report_data) { + Err(e) => panic!("[get_tdx_quote] Fail to get TDX quote: {:?}", e), + Ok(q) => base64::encode(q), + }; + + serde_json::to_string("e).map_err(|e| anyhow!("[get_tdx_quote]: {:?}", e)) +} + +fn get_tpm_quote() -> Result { + Err(anyhow!("TPM to be supported!")) +} + +fn get_sev_quote() -> Result { + Err(anyhow!("SEV to be supported!")) +} + +pub fn get_quote(local_tee: TeeType, user_data: String, nonce: String) -> Result { + match local_tee { + TeeType::TDX => get_tdx_quote(Some(user_data), nonce), + TeeType::TPM => get_tpm_quote(), + TeeType::SEV => get_sev_quote(), + _ => Err(anyhow!("Unexpected case!")), + } +} + +#[cfg(test)] +mod tests { + + use super::*; + + #[test] + //generate_tdx_report allow empty nonce + fn generate_tdx_report_data_empty_nonce() { + let result = generate_tdx_report_data(Some("YWJjZGVmZw==".to_string()), "".to_string()); + assert!(result.is_ok()); + } + + #[test] + //generate_tdx_report allow optional report data + fn tdx_get_quote_report_data_no_report_data() { + let result = generate_tdx_report_data(None, "IXUKoBO1XEFBPwopN4sY".to_string()); + assert!(result.is_ok()); + } + + #[test] + //generate_tdx_report allow empty report data string + fn generate_tdx_report_data_report_data_size_0() { + let result = + generate_tdx_report_data(Some("".to_string()), "IXUKoBO1XEFBPwopN4sY".to_string()); + assert!(result.is_ok()); + } + + #[test] + //generate_tdx_report allow 8 bytes report data string + fn generate_tdx_report_data_report_data_size_8() { + let result = generate_tdx_report_data( + Some("YWJjZGVmZw==".to_string()), + "IXUKoBO1XEFBPwopN4sY".to_string(), + ); + assert!(result.is_ok()); + } + + #[test] + //generate_tdx_report allow 48 bytes report data string + fn generate_tdx_report_data_size_report_data_size_48() { + // this one should be standard 48 bytes base64 encoded report data + // "MTIzNDU2NzgxMjM0NTY3ODEyMzQ1Njc4MTIzNDU2NzgxMjM0NTY3ODEyMzQ1Njc4" is base64 of "123456781234567812345678123456781234567812345678", 48 bytes + let result = generate_tdx_report_data( + Some("MTIzNDU2NzgxMjM0NTY3ODEyMzQ1Njc4MTIzNDU2NzgxMjM0NTY3ODEyMzQ1Njc4".to_string()), + "IXUKoBO1XEFBPwopN4sY".to_string(), + ); + assert!(result.is_ok()); + } + + #[test] + //generate_tdx_report require report data string is base64 encoded + fn generate_tdx_report_data_report_data_not_base64_encoded() { + //coming in report data should always be base64 encoded + let result = generate_tdx_report_data( + Some("XD^%*!x".to_string()), + "IXUKoBO1XEFBPwopN4sY".to_string(), + ); + assert!(result.is_err()); + } + + #[test] + //generate_tdx_report require nonce string is base64 encoded + fn generate_tdx_report_data_nonce_not_base64_encoded() { + //coming in nonce should always be base64 encoded + let result = generate_tdx_report_data( + Some("IXUKoBO1XEFBPwopN4sY".to_string()), + "XD^%*!x".to_string(), + ); + assert!(result.is_err()); + } + + #[test] + //generate_tdx_report require nonce string is base64 encoded + fn generate_tdx_report_data_nonce_short_not_base64_encoded() { + //coming in nonce should always be base64 encoded + let result = + generate_tdx_report_data(Some("IXUKoBO1XEFBPwopN4sY".to_string()), "123".to_string()); + assert!(result.is_err()); + } + + #[test] + //generate_tdx_report require report data string is base64 encoded + fn generate_tdx_report_data_report_data_short_not_base64_encoded() { + //coming in report data should always be base64 encoded + let result = + generate_tdx_report_data(Some("123".to_string()), "IXUKoBO1XEFBPwopN4sY".to_string()); + assert!(result.is_err()); + } + + #[test] + //generate_tdx_report check result as expected + //original report_data = "abcdefgh", orginal nonce = "12345678" + fn generate_tdx_report_data_report_data_nonce_base64_encoded_as_expected() { + let result = + generate_tdx_report_data(Some("YWJjZGVmZw==".to_string()), "MTIzNDU2Nzg=".to_string()) + .unwrap(); + let expected_hash = [ + 93, 71, 28, 83, 115, 189, 166, 130, 87, 137, 126, 119, 140, 209, 163, 215, 13, 175, + 225, 101, 64, 195, 196, 202, 15, 37, 166, 241, 141, 49, 128, 157, 164, 132, 67, 50, 9, + 32, 162, 89, 243, 191, 177, 131, 4, 159, 156, 104, 11, 193, 18, 217, 92, 215, 194, 98, + 145, 191, 211, 85, 187, 118, 39, 80, + ]; + let generated_hash = base64::decode(result).unwrap(); + assert_eq!(generated_hash, expected_hash); + } + + #[test] + //generate_tdx_report allow long report data string + fn generate_tdx_report_data_long_tdx_report_data() { + let result = generate_tdx_report_data( + Some( + "MTIzNDU2NzgxMjM0NTY3ODEyMzQ1Njc4MTIzNDU2NzgxMjM0NTY3ODEyMzQ1Njc4MTIzNDU2Nzgx\ + MjM0NTY3ODEyMzQ1Njc4MTIzNDU2NzgxMjM0NTY3ODEyMzQ1Njc4MTIzNDU2NzgxMjM0NTY3ODEy\ + MzQ1Njc4MTIzNDU2NzgxMjM0NTY3ODEyMzQ1Njc4MTIzNDU2NzgxMjM0NTY3ODEyMzQ1Njc4MTIz\ + NDU2NzgxMjM0NTY3ODEyMzQ1Njc4MTIzNDU2NzgxMjM0NTY3ODEyMzQ1Njc4MTIzNDU2NzgxMjM0\ + NTY3ODEyMzQ1Njc4MTIzNDU2NzgxMjM0NTY3ODEyMzQ1Njc4MTIzNDU2NzgxMjM0NTY3ODEyMzQ1\ + Njc4MTIzNDU2NzgxMjM0NTY3ODEyMzQ1Njc4MTIzNDU2NzgxMjM0NTY3ODEyMzQ1Njc4MTIzNDU2\ + NzgxMjM0NTY3ODEyMzQ1Njc4MTIzNDU2NzgxMjM0NTY3ODEyMzQ1Njc4Cg==" + .to_string(), + ), + "IXUKoBO1XEFBPwopN4sY".to_string(), + ); + assert!(result.is_ok()); + } + + #[test] + //generate_tdx_report allow long nonce string + fn generate_tdx_report_data_long_nonce() { + let result = generate_tdx_report_data( + Some("MTIzNDU2NzgxMjM0NTY3ODEyMzQ1Njc4MTIzNDU2NzgxMjM0NTY3ODEyMzQ1Njc4".to_string()), + "MTIzNDU2NzgxMjM0NTY3ODEyMzQ1Njc4MTIzNDU2NzgxMjM0NTY3ODEyMzQ1Njc4MTIzNDU2Nzgx\ + MjM0NTY3ODEyMzQ1Njc4MTIzNDU2NzgxMjM0NTY3ODEyMzQ1Njc4MTIzNDU2NzgxMjM0NTY3ODEy\ + MzQ1Njc4MTIzNDU2NzgxMjM0NTY3ODEyMzQ1Njc4MTIzNDU2NzgxMjM0NTY3ODEyMzQ1Njc4MTIz\ + NDU2NzgxMjM0NTY3ODEyMzQ1Njc4MTIzNDU2NzgxMjM0NTY3ODEyMzQ1Njc4MTIzNDU2NzgxMjM0\ + NTY3ODEyMzQ1Njc4MTIzNDU2NzgxMjM0NTY3ODEyMzQ1Njc4MTIzNDU2NzgxMjM0NTY3ODEyMzQ1\ + Njc4MTIzNDU2NzgxMjM0NTY3ODEyMzQ1Njc4MTIzNDU2NzgxMjM0NTY3ODEyMzQ1Njc4MTIzNDU2\ + NzgxMjM0NTY3ODEyMzQ1Njc4MTIzNDU2NzgxMjM0NTY3ODEyMzQ1Njc4Cg==" + .to_string(), + ); + assert!(result.is_ok()); + } + + #[test] + //generate_tdx_report_data generated report data is 64 bytes + fn generate_tdx_report_data_report_data_is_64_bytes() { + let report_data_hashed = match generate_tdx_report_data( + Some("MTIzNDU2NzgxMjM0NTY3ODEyMzQ1Njc4MTIzNDU2NzgxMjM0NTY3ODEyMzQ1Njc4".to_string()), + "IXUKoBO1XEFBPwopN4sY".to_string(), + ) { + Ok(r) => r, + Err(_) => todo!(), + }; + let generated_hash_len = base64::decode(report_data_hashed).unwrap().len(); + assert_eq!(generated_hash_len, 64); + } + + #[test] + //TDX ENV required: tdx_get_quote allow empty nonce + fn tdx_get_quote_empty_nonce() { + let result = get_tdx_quote(Some("YWJjZGVmZw==".to_string()), "".to_string()); + assert!(result.is_ok()); + } + + #[test] + //TDX ENV required: tdx_get_quote allow 0 bytes report data string + fn tdx_get_quote_report_data_size_0() { + let result = get_tdx_quote(Some("".to_string()), "IXUKoBO1XEFBPwopN4sY".to_string()); + assert!(result.is_ok()); + } + + #[test] + //TDX ENV required: tdx_get_quote allow 8 bytes report data string + fn tdx_get_quote_report_data_size_8() { + // "YWJjZGVmZw==" is base64 of "abcdefg", 8 bytes + let result = get_tdx_quote( + Some("YWJjZGVmZw==".to_string()), + "IXUKoBO1XEFBPwopN4sY".to_string(), + ); + assert!(result.is_ok()); + } + + #[test] + //TDX ENV required: tdx_get_quote allow 48 bytes report data string + fn tdx_get_quote_report_data_size_48() { + let result = get_tdx_quote( + Some("MTIzNDU2NzgxMjM0NTY3ODEyMzQ1Njc4MTIzNDU2NzgxMjM0NTY3ODEyMzQ1Njc4".to_string()), + "IXUKoBO1XEFBPwopN4sY".to_string(), + ); + assert!(result.is_ok()); + } + + #[test] + //TDX ENV required: tdx_get_quote allow optional report data + fn tdx_get_quote_report_data_null() { + let result = get_tdx_quote(None, "IXUKoBO1XEFBPwopN4sY".to_string()); + assert!(result.is_ok()); + } + + #[test] + //TDX ENV required: tdx_get_quote require report data string is base64 encoded + fn tdx_get_quote_report_data_not_base64_encoded() { + let result = get_tdx_quote( + Some("XD^%*!x".to_string()), + "IXUKoBO1XEFBPwopN4sY".to_string(), + ); + assert!(result.is_err()); + } + + #[test] + //TDX ENV required: tdx_get_quote require nonce string is base64 encoded + fn tdx_get_quote_nonce_not_base64_encoded() { + let result = get_tdx_quote( + Some("IXUKoBO1XEFBPwopN4sY".to_string()), + "XD^%*!x".to_string(), + ); + assert!(result.is_err()); + } + + #[test] + //TDX ENV required: tdx_get_quote allow long report data string + fn tdx_get_quote_long_tdx_report_data() { + let result = get_tdx_quote( + Some( + "MTIzNDU2NzgxMjM0NTY3ODEyMzQ1Njc4MTIzNDU2NzgxMjM0NTY3ODEyMzQ1Njc4MTIzNDU2Nzgx\ + MjM0NTY3ODEyMzQ1Njc4MTIzNDU2NzgxMjM0NTY3ODEyMzQ1Njc4MTIzNDU2NzgxMjM0NTY3ODEy\ + MzQ1Njc4MTIzNDU2NzgxMjM0NTY3ODEyMzQ1Njc4MTIzNDU2NzgxMjM0NTY3ODEyMzQ1Njc4MTIz\ + NDU2NzgxMjM0NTY3ODEyMzQ1Njc4MTIzNDU2NzgxMjM0NTY3ODEyMzQ1Njc4MTIzNDU2NzgxMjM0\ + NTY3ODEyMzQ1Njc4MTIzNDU2NzgxMjM0NTY3ODEyMzQ1Njc4MTIzNDU2NzgxMjM0NTY3ODEyMzQ1\ + Njc4MTIzNDU2NzgxMjM0NTY3ODEyMzQ1Njc4MTIzNDU2NzgxMjM0NTY3ODEyMzQ1Njc4MTIzNDU2\ + NzgxMjM0NTY3ODEyMzQ1Njc4MTIzNDU2NzgxMjM0NTY3ODEyMzQ1Njc4Cg==" + .to_string(), + ), + "IXUKoBO1XEFBPwopN4sY".to_string(), + ); + assert!(result.is_ok()); + } + + #[test] + //TDX ENV required: tdx_get_quote allow long nonce string + fn tdx_get_quote_long_nonce() { + let result = get_tdx_quote( + Some("MTIzNDU2NzgxMjM0NTY3ODEyMzQ1Njc4MTIzNDU2NzgxMjM0NTY3ODEyMzQ1Njc4".to_string()), + "MTIzNDU2NzgxMjM0NTY3ODEyMzQ1Njc4MTIzNDU2NzgxMjM0NTY3ODEyMzQ1Njc4MTIzNDU2Nzgx\ + MjM0NTY3ODEyMzQ1Njc4MTIzNDU2NzgxMjM0NTY3ODEyMzQ1Njc4MTIzNDU2NzgxMjM0NTY3ODEy\ + MzQ1Njc4MTIzNDU2NzgxMjM0NTY3ODEyMzQ1Njc4MTIzNDU2NzgxMjM0NTY3ODEyMzQ1Njc4MTIz\ + NDU2NzgxMjM0NTY3ODEyMzQ1Njc4MTIzNDU2NzgxMjM0NTY3ODEyMzQ1Njc4MTIzNDU2NzgxMjM0\ + NTY3ODEyMzQ1Njc4MTIzNDU2NzgxMjM0NTY3ODEyMzQ1Njc4MTIzNDU2NzgxMjM0NTY3ODEyMzQ1\ + Njc4MTIzNDU2NzgxMjM0NTY3ODEyMzQ1Njc4MTIzNDU2NzgxMjM0NTY3ODEyMzQ1Njc4MTIzNDU2\ + NzgxMjM0NTY3ODEyMzQ1Njc4MTIzNDU2NzgxMjM0NTY3ODEyMzQ1Njc4Cg==" + .to_string(), + ); + assert!(result.is_ok()); + } + + #[test] + //TDX ENV required: get_tdx_quote return non-empty encoded quote string + fn tdx_get_quote_report_data_encoded_quote_is_not_0_bytes() { + let quote = match get_tdx_quote( + Some("MTIzNDU2NzgxMjM0NTY3ODEyMzQ1Njc4MTIzNDU2NzgxMjM0NTY3ODEyMzQ1Njc4".to_string()), + "IXUKoBO1XEFBPwopN4sY".to_string(), + ) { + Ok(r) => r, + Err(_) => todo!(), + }; + assert_ne!(quote.len(), 0); + } + + #[test] + //get_quote does not allow tee type beyond TDX/SEV/TPM + fn get_quote_wrong_tee_type() { + let result = get_quote( + TeeType::PLAIN, + "".to_string(), + "IXUKoBO1XEFBPwopN4sY".to_string(), + ); + assert!(result.is_err()); + } + + #[test] + //get_quote does not support SEV for now + fn get_quote_sev_tee_type() { + //does not allow tee type beyond TDX/SEV/TPM + let result = get_quote( + TeeType::SEV, + "".to_string(), + "IXUKoBO1XEFBPwopN4sY".to_string(), + ); + assert!(result.is_err()); + } + + #[test] + //get_quote does not support TPM for now + fn get_quote_tpm_tee_type() { + //does not allow tee type beyond TDX/SEV/TPM + let result = get_quote( + TeeType::TPM, + "".to_string(), + "IXUKoBO1XEFBPwopN4sY".to_string(), + ); + assert!(result.is_err()); + } + + #[test] + //get_quote support TDX now + fn get_quote_tdx_tee_type() { + //does not allow tee type beyond TDX/SEV/TPM + let result = get_quote( + TeeType::TDX, + "".to_string(), + "IXUKoBO1XEFBPwopN4sY".to_string(), + ); + assert!(result.is_ok()); + } +} diff --git a/service/pod-quote/tdx_attest/Cargo.toml b/service/pod-quote/tdx_attest/Cargo.toml new file mode 100644 index 00000000..d543db94 --- /dev/null +++ b/service/pod-quote/tdx_attest/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "tdx_attest" +version = "0.1.1" +edition = "2021" +authors = ["Hairong Chen "] +description = "A rust crate to retrieve TD Report and TDX quote via ioctl" +readme = "README.md" +license = "Apache-2.0" +repository = "https://github.com/confidential-cloud-native-primitives" + +[lib] +name = "tdx_attest" +path = "src/tdx_attest.rs" + +[dependencies] +nix = "0.26.2" +base64 = "0.13.0" +anyhow = "1.0" \ No newline at end of file diff --git a/service/pod-quote/tdx_attest/README.md b/service/pod-quote/tdx_attest/README.md new file mode 100644 index 00000000..26aba207 --- /dev/null +++ b/service/pod-quote/tdx_attest/README.md @@ -0,0 +1 @@ +A rust crate to retrieve TD Report and TDX quote via ioctl \ No newline at end of file diff --git a/service/pod-quote/tdx_attest/src/tdx_attest.rs b/service/pod-quote/tdx_attest/src/tdx_attest.rs new file mode 100644 index 00000000..3420c714 --- /dev/null +++ b/service/pod-quote/tdx_attest/src/tdx_attest.rs @@ -0,0 +1,453 @@ +/* +* Copyright (c) 2023, Intel Corporation. All rights reserved.
+* SPDX-License-Identifier: Apache-2.0 +*/ + +#![allow(non_camel_case_types)] + +use anyhow::*; +use nix::*; +use std::convert::TryInto; +use std::fs::File; +use std::mem; +use std::os::unix::io::AsRawFd; +use std::path::Path; +use std::ptr; +use std::result::Result; +use std::result::Result::Ok; + +#[repr(C)] +pub struct tdx_1_0_report_req { + subtype: u8, // Subtype of TDREPORT: fixed as 0 by TDX Module specification + reportdata: u64, // User-defined REPORTDATA to be included into TDREPORT + rpd_len: u32, // Length of the REPORTDATA: fixed as 64 bytes by the TDX Module specification + tdreport: u64, // TDREPORT output from TDCALL[TDG.MR.REPORT] + tdr_len: u32, // Length of the TDREPORT: fixed as 1024 bytes by the TDX Module specification +} + +#[repr(C)] +pub struct tdx_1_5_report_req { + reportdata: [u8; REPORT_DATA_LEN as usize], // User buffer with REPORTDATA to be included into TDREPORT + tdreport: [u8; TDX_REPORT_LEN as usize], // User buffer to store TDREPORT output from TDCALL[TDG.MR.REPORT] +} + +#[repr(C)] +pub struct qgs_msg_header { + major_version: u16, // TDX major version + minor_version: u16, // TDX minor version + msg_type: u32, // GET_QUOTE_REQ or GET_QUOTE_RESP + size: u32, // size of the whole message, include this header, in byte + error_code: u32, // used in response only +} + +#[repr(C)] +pub struct qgs_msg_get_quote_req { + header: qgs_msg_header, // header.type = GET_QUOTE_REQ + report_size: u32, // cannot be 0 + id_list_size: u32, // length of id_list, in byte, can be 0 + report_id_list: [u8; TDX_REPORT_LEN as usize], // report followed by id list +} + +#[repr(C)] +pub struct tdx_quote_hdr { + version: u64, // Quote version, filled by TD + status: u64, // Status code of Quote request, filled by VMM + in_len: u32, // Length of TDREPORT, filled by TD + out_len: u32, // Length of Quote, filled by VMM + data_len_be_bytes: [u8; 4], // big-endian 4 bytes indicate the size of data following + data: [u8; TDX_QUOTE_LEN as usize], // Actual Quote data or TDREPORT on input +} + +#[repr(C)] +pub struct tdx_quote_req { + buf: u64, // Pass user data that includes TDREPORT as input. Upon successful completion of IOCTL, output is copied back to the same buffer + len: u64, // Length of the Quote buffer +} + +#[repr(C)] +pub struct qgs_msg_get_quote_resp { + header: qgs_msg_header, // header.type = GET_QUOTE_RESP + selected_id_size: u32, // can be 0 in case only one id is sent in request + quote_size: u32, // length of quote_data, in byte + id_quote: [u8; TDX_QUOTE_LEN], // selected id followed by quote +} + +pub enum TdxVersion { + TDX_1_0, + TDX_1_5, +} + +pub enum TdxOperation { + TDX_GET_TD_REPORT = 1, + TDX_1_0_GET_QUOTE = 2, + TDX_1_5_GET_QUOTE = 4, +} + +const REPORT_DATA_LEN: u32 = 64; +const TDX_REPORT_LEN: u32 = 1024; +const TDX_QUOTE_LEN: usize = 4 * 4096; + +pub struct TdxInfo { + tdx_version: TdxVersion, + device_node: File, +} + +impl TdxInfo { + fn new(_tdx_version: TdxVersion, _device_node: File) -> Self { + TdxInfo { + tdx_version: _tdx_version, + device_node: _device_node, + } + } +} + +fn get_tdx_version() -> TdxVersion { + if Path::new("/dev/tdx-guest").exists() { + TdxVersion::TDX_1_0 + } else if Path::new("/dev/tdx_guest").exists() { + TdxVersion::TDX_1_5 + } else if Path::new("/dev/tdx-attest").exists() { + panic!("get_tdx_version: Deprecated device node /dev/tdx-attest, please upgrade to use /dev/tdx-guest or /dev/tdx_guest"); + } else { + panic!("get_tdx_version: no TDX device found!"); + } +} + +pub fn get_td_report(report_data: String) -> Result, anyhow::Error> { + //detect TDX version + let tdx_info = match get_tdx_version() { + TdxVersion::TDX_1_0 => { + let device_node = match File::options() + .read(true) + .write(true) + .open("/dev/tdx-guest") + { + Err(e) => { + return Err(anyhow!( + "[get_td_report] Fail to open {}: {:?}", + "/dev/tdx-guest", + e + )) + } + Ok(fd) => fd, + }; + TdxInfo::new(TdxVersion::TDX_1_0, device_node) + } + TdxVersion::TDX_1_5 => { + let device_node = match File::options() + .read(true) + .write(true) + .open("/dev/tdx_guest") + { + Err(e) => { + return Err(anyhow!( + "[get_td_report] Fail to open {}: {:?}", + "/dev/tdx_guest", + e + )) + } + Ok(fd) => fd, + }; + TdxInfo::new(TdxVersion::TDX_1_5, device_node) + } + }; + + match tdx_info.tdx_version { + TdxVersion::TDX_1_0 => match get_tdx_1_0_report(tdx_info.device_node, report_data) { + Err(e) => return Err(anyhow!("[get_td_report] Fail to get TDX report: {:?}", e)), + Ok(report) => Ok(report), + }, + TdxVersion::TDX_1_5 => match get_tdx_1_5_report(tdx_info.device_node, report_data) { + Err(e) => return Err(anyhow!("[get_td_report] Fail to get TDX report: {:?}", e)), + Ok(report) => Ok(report), + }, + } +} + +fn get_tdx_1_0_report(device_node: File, report_data: String) -> Result, anyhow::Error> { + let report_data_bytes = match base64::decode(report_data) { + Ok(v) => v, + Err(e) => return Err(anyhow!("report data is not base64 encoded: {:?}", e)), + }; + + //prepare get TDX report request data + let mut report_data_array: [u8; REPORT_DATA_LEN as usize] = [0; REPORT_DATA_LEN as usize]; + report_data_array.copy_from_slice(&report_data_bytes[0..]); + let td_report: [u8; TDX_REPORT_LEN as usize] = [0; TDX_REPORT_LEN as usize]; + + //build the request + let request = tdx_1_0_report_req { + subtype: 0 as u8, + reportdata: ptr::addr_of!(report_data_array) as u64, + rpd_len: REPORT_DATA_LEN, + tdreport: ptr::addr_of!(td_report) as u64, + tdr_len: TDX_REPORT_LEN, + }; + + //build the operator code + ioctl_readwrite!( + get_report_1_0_ioctl, + b'T', + TdxOperation::TDX_GET_TD_REPORT, + u64 + ); + + //apply the ioctl command + match unsafe { + get_report_1_0_ioctl(device_node.as_raw_fd(), ptr::addr_of!(request) as *mut u64) + } { + Err(e) => { + return Err(anyhow!( + "[get_tdx_1_0_report] Fail to get TDX report: {:?}", + e + )) + } + Ok(_) => (), + }; + + Ok(td_report.to_vec()) +} + +fn get_tdx_1_5_report(device_node: File, report_data: String) -> Result, anyhow::Error> { + let report_data_bytes = match base64::decode(report_data) { + Ok(v) => v, + Err(e) => return Err(anyhow!("report data is not base64 encoded: {:?}", e)), + }; + + //prepare get TDX report request data + let mut request = tdx_1_5_report_req { + reportdata: [0; REPORT_DATA_LEN as usize], + tdreport: [0; TDX_REPORT_LEN as usize], + }; + request.reportdata.copy_from_slice(&report_data_bytes[0..]); + + //build the operator code + ioctl_readwrite!( + get_report_1_5_ioctl, + b'T', + TdxOperation::TDX_GET_TD_REPORT, + tdx_1_5_report_req + ); + + //apply the ioctl command + match unsafe { + get_report_1_5_ioctl( + device_node.as_raw_fd(), + ptr::addr_of!(request) as *mut tdx_1_5_report_req, + ) + } { + Err(e) => { + return Err(anyhow!( + "[get_tdx_1_5_report] Fail to get TDX report: {:?}", + e + )) + } + Ok(_) => (), + }; + + Ok(request.tdreport.to_vec()) +} + +fn generate_qgs_quote_msg(report: [u8; TDX_REPORT_LEN as usize]) -> qgs_msg_get_quote_req { + //build quote service message header to be used by QGS + let qgs_header = qgs_msg_header { + major_version: 1, + minor_version: 0, + msg_type: 0, + size: 16 + 8 + TDX_REPORT_LEN, // header + report_size and id_list_size + TDX_REPORT_LEN + error_code: 0, + }; + + //build quote service message body to be used by QGS + let mut qgs_request = qgs_msg_get_quote_req { + header: qgs_header, + report_size: TDX_REPORT_LEN, + id_list_size: 0, + report_id_list: [0; TDX_REPORT_LEN as usize], + }; + + qgs_request.report_id_list.copy_from_slice(&report[0..]); + + qgs_request +} + +pub fn get_tdx_quote(report_data: String) -> Result, anyhow::Error> { + //retrieve TDX report + let report_data_vec = match get_td_report(report_data) { + Err(e) => return Err(anyhow!("[get_tdx_quote] Fail to get TDX report: {:?}", e)), + Ok(report) => report, + }; + let report_data_array: [u8; TDX_REPORT_LEN as usize] = match report_data_vec.try_into() { + Ok(r) => r, + Err(e) => return Err(anyhow!("[get_tdx_quote] Wrong TDX report format: {:?}", e)), + }; + + //build QGS request message + let qgs_msg = generate_qgs_quote_msg(report_data_array); + + let tdx_info = match get_tdx_version() { + TdxVersion::TDX_1_0 => { + let device_node = match File::options() + .read(true) + .write(true) + .open("/dev/tdx-guest") + { + Err(e) => { + return Err(anyhow!( + "[get_tdx_quote] Fail to open {}: {:?}", + "/dev/tdx-guest", + e + )) + } + Ok(fd) => fd, + }; + TdxInfo::new(TdxVersion::TDX_1_0, device_node) + } + TdxVersion::TDX_1_5 => { + let device_node = match File::options() + .read(true) + .write(true) + .open("/dev/tdx_guest") + { + Err(e) => { + return Err(anyhow!( + "[get_tdx_quote] Fail to open {}: {:?}", + "/dev/tdx_guest", + e + )) + } + Ok(fd) => fd, + }; + TdxInfo::new(TdxVersion::TDX_1_5, device_node) + } + }; + + //build quote generation request header + let mut quote_header = tdx_quote_hdr { + version: 1, + status: 0, + in_len: (mem::size_of_val(&qgs_msg) + 4) as u32, + out_len: 0, + data_len_be_bytes: (1048 as u32).to_be_bytes(), + data: [0; TDX_QUOTE_LEN as usize], + }; + + let qgs_msg_bytes = unsafe { + let ptr = &qgs_msg as *const qgs_msg_get_quote_req as *const u8; + std::slice::from_raw_parts(ptr, mem::size_of::()) + }; + quote_header.data[0..(16 + 8 + TDX_REPORT_LEN) as usize] + .copy_from_slice(&qgs_msg_bytes[0..((16 + 8 + TDX_REPORT_LEN) as usize)]); + + let request = tdx_quote_req { + buf: ptr::addr_of!(quote_header) as u64, + len: TDX_QUOTE_LEN as u64, + }; + + //build the operator code and apply the ioctl command + match tdx_info.tdx_version { + TdxVersion::TDX_1_0 => { + ioctl_read!( + get_quote_1_0_ioctl, + b'T', + TdxOperation::TDX_1_0_GET_QUOTE, + u64 + ); + match unsafe { + get_quote_1_0_ioctl( + tdx_info.device_node.as_raw_fd(), + ptr::addr_of!(request) as *mut u64, + ) + } { + Err(e) => return Err(anyhow!("[get_tdx_quote] Fail to get TDX quote: {:?}", e)), + Ok(_r) => _r, + }; + } + TdxVersion::TDX_1_5 => { + ioctl_read!( + get_quote_1_5_ioctl, + b'T', + TdxOperation::TDX_1_5_GET_QUOTE, + tdx_quote_req + ); + match unsafe { + get_quote_1_5_ioctl( + tdx_info.device_node.as_raw_fd(), + ptr::addr_of!(request) as *mut tdx_quote_req, + ) + } { + Err(e) => return Err(anyhow!("[get_tdx_quote] Fail to get TDX quote: {:?}", e)), + Ok(_r) => _r, + }; + } + }; + + //inspect the response and retrive quote data + let out_len = quote_header.out_len; + let qgs_msg_resp_size = + unsafe { std::mem::transmute::<[u8; 4], u32>(quote_header.data_len_be_bytes) }.to_be(); + + let qgs_msg_resp = unsafe { + let raw_ptr = ptr::addr_of!(quote_header.data) as *mut qgs_msg_get_quote_resp; + raw_ptr.as_mut().unwrap() as &mut qgs_msg_get_quote_resp + }; + + if out_len - qgs_msg_resp_size != 4 { + return Err(anyhow!( + "[get_tdx_quote] Fail to get TDX quote: wrong TDX quote size!" + )); + } + + if qgs_msg_resp.header.major_version != 1 + || qgs_msg_resp.header.minor_version != 0 + || qgs_msg_resp.header.msg_type != 1 + || qgs_msg_resp.header.error_code != 0 + { + return Err(anyhow!( + "[get_tdx_quote] Fail to get TDX quote: QGS response error!" + )); + } + + Ok(qgs_msg_resp.id_quote[0..(qgs_msg_resp.quote_size as usize)].to_vec()) +} + +#[cfg(test)] +mod tdx_attest_tests { + use super::*; + + #[test] + //TDX ENV required: call get_td_report and verify report data embedded in quote + fn get_td_report_verify_report_data() { + let report_data = "XUccU3O9poJXiX53jNGj1w2v4WVAw8TKDyWm8Y0xgJ2khEMyCSCiWfO/sYMEn5xoC8ES2VzXwmKRv9NVu3YnUA=="; + let report = get_td_report(report_data.to_string()).unwrap(); + + let expected_report_data = [ + 93, 71, 28, 83, 115, 189, 166, 130, 87, 137, 126, 119, 140, 209, 163, 215, 13, 175, + 225, 101, 64, 195, 196, 202, 15, 37, 166, 241, 141, 49, 128, 157, 164, 132, 67, 50, 9, + 32, 162, 89, 243, 191, 177, 131, 4, 159, 156, 104, 11, 193, 18, 217, 92, 215, 194, 98, + 145, 191, 211, 85, 187, 118, 39, 80, + ]; + + let mut report_data_in_report: [u8; 64 as usize] = [0; 64 as usize]; + report_data_in_report.copy_from_slice(&report[128..192]); + assert_eq!(report_data_in_report, expected_report_data); + } + + #[test] + //TDX ENV required: call tdx_get_quote and verify report data embedded in quote + fn get_tdx_quote_verify_report_data() { + let report_data = "XUccU3O9poJXiX53jNGj1w2v4WVAw8TKDyWm8Y0xgJ2khEMyCSCiWfO/sYMEn5xoC8ES2VzXwmKRv9NVu3YnUA=="; + let quote = get_tdx_quote(report_data.to_string()).unwrap(); + + let expected_report_data = [ + 93, 71, 28, 83, 115, 189, 166, 130, 87, 137, 126, 119, 140, 209, 163, 215, 13, 175, + 225, 101, 64, 195, 196, 202, 15, 37, 166, 241, 141, 49, 128, 157, 164, 132, 67, 50, 9, + 32, 162, 89, 243, 191, 177, 131, 4, 159, 156, 104, 11, 193, 18, 217, 92, 215, 194, 98, + 145, 191, 211, 85, 187, 118, 39, 80, + ]; + + let mut report_data_in_quote: [u8; 64 as usize] = [0; 64 as usize]; + report_data_in_quote.copy_from_slice("e[568..632]); + assert_eq!(report_data_in_quote, expected_report_data); + } +}