├── testing ├── testdata │ ├── ccel │ │ ├── nonce.dat │ │ ├── ccel_data.dat │ │ ├── ccel_table.dat │ │ └── cos-113-tdx-quote.dat │ ├── pckcrl │ ├── report.dat │ ├── rootcrl.der │ ├── tdx_prod_quote_SPR_E4.dat │ ├── sample_qeIdentity_response │ ├── testdata.go │ ├── sample_tcbInfo_response │ └── README.md ├── client │ └── client.go ├── mocks.go └── test_cases.go ├── go.mod ├── proto ├── tdx │ └── doc.go ├── checkconfig │ └── doc.go ├── doc.go ├── checkconfig.proto └── tdx.proto ├── verify ├── trusted_root.pem └── trust │ ├── trust_test.go │ └── trust.go ├── CONTRIBUTING.md ├── .goreleaser.yml ├── .github └── workflows │ ├── release.yml │ └── CI.yml ├── pcs └── pcs_test.go ├── rtmr ├── ccel_test.go ├── extend_test.go ├── ccel.go └── extend.go ├── tools ├── extend │ ├── README.md │ └── extend.go ├── check │ ├── README.md │ ├── check_test.go │ └── check.go └── attest │ ├── README.md │ └── attest.go ├── client ├── client_windows.go ├── client_macos.go ├── client_linux.go ├── client_test.go ├── client.go └── linuxabi │ └── linux_abi.go ├── go.sum ├── README.md ├── abi └── abi_test.go ├── validate ├── validate_test.go └── validate.go └── LICENSE /testing/testdata/ccel/nonce.dat: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /testing/testdata/pckcrl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/go-tdx-guest/HEAD/testing/testdata/pckcrl -------------------------------------------------------------------------------- /testing/testdata/report.dat: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/go-tdx-guest/HEAD/testing/testdata/report.dat -------------------------------------------------------------------------------- /testing/testdata/rootcrl.der: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/go-tdx-guest/HEAD/testing/testdata/rootcrl.der -------------------------------------------------------------------------------- /testing/testdata/ccel/ccel_data.dat: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/go-tdx-guest/HEAD/testing/testdata/ccel/ccel_data.dat -------------------------------------------------------------------------------- /testing/testdata/ccel/ccel_table.dat: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/go-tdx-guest/HEAD/testing/testdata/ccel/ccel_table.dat -------------------------------------------------------------------------------- /testing/testdata/ccel/cos-113-tdx-quote.dat: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/go-tdx-guest/HEAD/testing/testdata/ccel/cos-113-tdx-quote.dat -------------------------------------------------------------------------------- /testing/testdata/tdx_prod_quote_SPR_E4.dat: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/go-tdx-guest/HEAD/testing/testdata/tdx_prod_quote_SPR_E4.dat -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/google/go-tdx-guest 2 | 3 | go 1.20 4 | 5 | require ( 6 | github.com/google/go-cmp v0.6.0 7 | github.com/google/go-configfs-tsm v0.3.2 8 | github.com/google/go-eventlog v0.0.2-0.20241213203620-f921bdc3aeb0 9 | github.com/google/go-sev-guest v0.8.0 10 | github.com/google/logger v1.1.1 11 | go.uber.org/multierr v1.11.0 12 | golang.org/x/crypto v0.17.0 13 | golang.org/x/sys v0.19.0 14 | google.golang.org/protobuf v1.34.2 15 | ) 16 | 17 | require github.com/google/go-tpm v0.9.0 // indirect 18 | -------------------------------------------------------------------------------- /testing/testdata/sample_qeIdentity_response: -------------------------------------------------------------------------------- 1 | {"enclaveIdentity":{"id":"TD_QE","version":2,"issueDate":"2023-06-08T07:24:59Z","nextUpdate":"2023-07-08T07:24:59Z","tcbEvaluationDataNumber":15,"miscselect":"00000000","miscselectMask":"FFFFFFFF","attributes":"11000000000000000000000000000000","attributesMask":"FBFFFFFFFFFFFFFF0000000000000000","mrsigner":"DC9E2A7C6F948F17474E34A7FC43ED030F7C1563F1BABDDF6340C82E0E54A8C5","isvprodid":2,"tcbLevels":[{"tcb":{"isvsvn":4},"tcbDate":"2023-02-15T00:00:00Z","tcbStatus":"UpToDate"}]},"signature":"b6a601f05de27f2ca5105eec24bdd4bf7dd1b8bbfffc76dffe4f4d16b8a395843e4b92d430fd6744b0648bf44302c528412fcb9cbf3cc9ce6922a3057932b6a6"} -------------------------------------------------------------------------------- /proto/tdx/doc.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Package tdx implements a protocol buffer for representing TDX attestations. 16 | package tdx 17 | -------------------------------------------------------------------------------- /proto/checkconfig/doc.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Package checkconfig defines the message type for the check CLI tool's options. 16 | package checkconfig 17 | -------------------------------------------------------------------------------- /verify/trusted_root.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIICjzCCAjSgAwIBAgIUImUM1lqdNInzg7SVUr9QGzknBqwwCgYIKoZIzj0EAwIw 3 | aDEaMBgGA1UEAwwRSW50ZWwgU0dYIFJvb3QgQ0ExGjAYBgNVBAoMEUludGVsIENv 4 | cnBvcmF0aW9uMRQwEgYDVQQHDAtTYW50YSBDbGFyYTELMAkGA1UECAwCQ0ExCzAJ 5 | BgNVBAYTAlVTMB4XDTE4MDUyMTEwNDUxMFoXDTQ5MTIzMTIzNTk1OVowaDEaMBgG 6 | A1UEAwwRSW50ZWwgU0dYIFJvb3QgQ0ExGjAYBgNVBAoMEUludGVsIENvcnBvcmF0 7 | aW9uMRQwEgYDVQQHDAtTYW50YSBDbGFyYTELMAkGA1UECAwCQ0ExCzAJBgNVBAYT 8 | AlVTMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEC6nEwMDIYZOj/iPWsCzaEKi7 9 | 1OiOSLRFhWGjbnBVJfVnkY4u3IjkDYYL0MxO4mqsyYjlBalTVYxFP2sJBK5zlKOB 10 | uzCBuDAfBgNVHSMEGDAWgBQiZQzWWp00ifODtJVSv1AbOScGrDBSBgNVHR8ESzBJ 11 | MEegRaBDhkFodHRwczovL2NlcnRpZmljYXRlcy50cnVzdGVkc2VydmljZXMuaW50 12 | ZWwuY29tL0ludGVsU0dYUm9vdENBLmRlcjAdBgNVHQ4EFgQUImUM1lqdNInzg7SV 13 | Ur9QGzknBqwwDgYDVR0PAQH/BAQDAgEGMBIGA1UdEwEB/wQIMAYBAf8CAQEwCgYI 14 | KoZIzj0EAwIDSQAwRgIhAOW/5QkR+S9CiSDcNoowLuPRLsWGf/Yi7GSX94BgwTwg 15 | AiEA4J0lrHoMs+Xo5o/sX6O9QWxHRAvZUGOdRQ7cvqRXaqI= 16 | -----END CERTIFICATE----- -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to Contribute 2 | 3 | We'd love to accept your patches and contributions to this project. There are 4 | just a few small guidelines you need to follow. 5 | 6 | ## Contributor License Agreement 7 | 8 | Contributions to this project must be accompanied by a Contributor License 9 | Agreement. You (or your employer) retain the copyright to your contribution; 10 | this simply gives us permission to use and redistribute your contributions as 11 | part of the project. Head over to to see 12 | your current agreements on file or to sign a new one. 13 | 14 | You generally only need to submit a CLA once, so if you've already submitted one 15 | (even if it was for a different project), you probably don't need to do it 16 | again. 17 | 18 | ## Code reviews 19 | 20 | All submissions, including submissions by project members, require review. We 21 | use GitHub pull requests for this purpose. Consult 22 | [GitHub Help](https://help.github.com/articles/about-pull-requests/) for more 23 | information on using pull requests. 24 | 25 | ## Community Guidelines 26 | 27 | This project follows [Google's Open Source Community 28 | Guidelines](https://opensource.google.com/conduct/). 29 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | builds: 2 | - env: 3 | - CGO_ENABLED=0 4 | goos: 5 | - linux 6 | # - windows 7 | # - darwin 8 | goarch: 9 | - amd64 10 | id: "attest" 11 | main: ./tools/attest/attest.go 12 | binary: attest 13 | - env: 14 | - CGO_ENABLED=0 15 | goos: 16 | - linux 17 | # - windows 18 | # - darwin 19 | goarch: 20 | - amd64 21 | id: "check" 22 | main: ./tools/check/check.go 23 | binary: check 24 | 25 | archives: 26 | - format: tar.gz 27 | # this name template makes the OS and Arch compatible with the results of uname. 28 | name_template: >- 29 | {{ .ProjectName }}_ 30 | {{- title .Os }}_ 31 | {{- if eq .Arch "amd64" }}x86_64 32 | {{- else if eq .Arch "386" }}i386 33 | {{- else }}{{ .Arch }}{{ end }} 34 | {{- if .Arm }}v{{ .Arm }}{{ end }} 35 | # use zip for windows archives 36 | format_overrides: 37 | - goos: windows 38 | format: zip 39 | checksum: 40 | name_template: 'checksums.txt' 41 | snapshot: 42 | name_template: "{{ incpatch .Version }}-next" 43 | changelog: 44 | sort: asc 45 | filters: 46 | exclude: 47 | - '^docs:' 48 | - '^test:' 49 | 50 | # yaml-language-server: $schema=https://goreleaser.com/static/schema.json 51 | # vim: set ts=2 sw=2 tw=0 fo=cnqoj -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: release 2 | 'on': 3 | push: 4 | branches: 5 | - main 6 | tags: 7 | - v* 8 | pull_request: null 9 | jobs: 10 | release: 11 | strategy: 12 | matrix: 13 | go-version: 14 | - 1.20.x 15 | os: 16 | - ubuntu-latest 17 | name: 'Release for (${{ matrix.os}}, Go ${{ matrix.go-version }})' 18 | runs-on: '${{ matrix.os }}' 19 | steps: 20 | - uses: actions/checkout@v3 21 | with: 22 | fetch-depth: 0 23 | - uses: actions/setup-go@v3 24 | with: 25 | go-version: '${{ matrix.go-version }}' 26 | cache: true 27 | - shell: bash 28 | run: echo "sha_short=$(git rev-parse --short HEAD)" >> $GITHUB_ENV 29 | - id: cache 30 | uses: actions/cache@v3 31 | with: 32 | path: 'dist/${{ matrix.os }}' 33 | key: '${{ matrix.go }}-${{ env.sha_short }}' 34 | - name: Build all packages 35 | run: go build -v ./... 36 | - name: Run GoReleaser 37 | uses: goreleaser/goreleaser-action@v3 38 | if: >- 39 | success() && startsWith(github.ref, 'refs/tags/') && 40 | steps.cache.outputs.cache-hit != 'true' 41 | with: 42 | version: latest 43 | args: release --clean 44 | env: 45 | GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}' 46 | -------------------------------------------------------------------------------- /pcs/pcs_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package pcs 16 | 17 | import ( 18 | "encoding/hex" 19 | "testing" 20 | ) 21 | 22 | func TestPckCrlURL(t *testing.T) { 23 | want := SgxBaseURL + "/pckcrl?ca=platform&encoding=der" 24 | 25 | if got := PckCrlURL("platform"); got != want { 26 | t.Errorf(`PckCrlURL("platform") = %q. Expected %q`, got, want) 27 | } 28 | } 29 | 30 | func TestTcbInfoURL(t *testing.T) { 31 | want := TdxBaseURL + "/tcb?fmspc=50806f000000" 32 | fmspcBytes := []byte{80, 128, 111, 0, 0, 0} 33 | fmspc := hex.EncodeToString(fmspcBytes) 34 | if got := TcbInfoURL(fmspc); got != want { 35 | t.Errorf("TcbInfoURL(%q) = %q. Expected %q", fmspc, got, want) 36 | } 37 | } 38 | 39 | func TestQeIdentityURL(t *testing.T) { 40 | want := TdxBaseURL + "/qe/identity" 41 | if got := QeIdentityURL(); got != want { 42 | t.Errorf("QEIdentityURL() = %q. Expected %q", got, want) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /rtmr/ccel_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); you may not 4 | // use this file except in compliance with the License. You may obtain a copy of 5 | // the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | // License for the specific language governing permissions and limitations under 13 | // the License. 14 | 15 | package rtmr 16 | 17 | import ( 18 | "os" 19 | "testing" 20 | 21 | "github.com/google/go-tdx-guest/abi" 22 | ) 23 | 24 | func TestReplayCcelWithTdQuote(t *testing.T) { 25 | ccelBytes, err := os.ReadFile("../testing/testdata/ccel/ccel_data.dat") 26 | if err != nil { 27 | t.Fatal(err) 28 | } 29 | tableBytes, err := os.ReadFile("../testing/testdata/ccel/ccel_table.dat") 30 | if err != nil { 31 | t.Fatal(err) 32 | } 33 | quoteBytes, err := os.ReadFile("../testing/testdata/ccel/cos-113-tdx-quote.dat") 34 | if err != nil { 35 | t.Fatal(err) 36 | } 37 | nonceBytes, err := os.ReadFile("../testing/testdata/ccel/nonce.dat") 38 | if err != nil { 39 | t.Fatal(err) 40 | } 41 | option := TdxDefaultOpts(nonceBytes) 42 | quote, err := abi.QuoteToProto(quoteBytes) 43 | if err != nil { 44 | t.Fatal(err) 45 | } 46 | ccel, err := ParseCcelWithTdQuote(ccelBytes, tableBytes, quote, &option) 47 | if err != nil { 48 | t.Fatal(err) 49 | } 50 | t.Log(ccel) 51 | } 52 | -------------------------------------------------------------------------------- /testing/testdata/testdata.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Package testdata defines sample responses of the collaterals 16 | package testdata 17 | 18 | import ( 19 | _ "embed" 20 | ) 21 | 22 | // RawQuote contains raw bytes of quote. To be used only for testing 23 | // 24 | //go:embed "tdx_prod_quote_SPR_E4.dat" 25 | var RawQuote []byte 26 | 27 | // RawReport contains raw bytes of report. To be used only for testing 28 | // 29 | //go:embed "report.dat" 30 | var RawReport []byte 31 | 32 | // PckCrlBody contains sample PCK CRL. To be used only for testing 33 | // 34 | //go:embed "pckcrl" 35 | var PckCrlBody []byte 36 | 37 | // RootCrlBody contains sample Root CA CRL. To be used only for testing 38 | // 39 | //go:embed "rootcrl.der" 40 | var RootCrlBody []byte 41 | 42 | // TcbInfoBody contains sample TCBInfo response. To be used only for testing 43 | // 44 | //go:embed "sample_tcbInfo_response" 45 | var TcbInfoBody []byte 46 | 47 | // QeIdentityBody contains sample QeIdentity response. To be used only for testing 48 | // 49 | //go:embed "sample_qeIdentity_response" 50 | var QeIdentityBody []byte 51 | -------------------------------------------------------------------------------- /tools/extend/README.md: -------------------------------------------------------------------------------- 1 | # `check` CLI tool 2 | 3 | This binary is a thin wrapper around the `rtmr` library to 4 | extend the measurement into RTMR registers. 5 | 6 | The tool's input is the event log. 7 | 8 | The tool's output is an error or "Success". 9 | 10 | ## Usage 11 | 12 | ``` 13 | ./extend [options...] 14 | ``` 15 | 16 | ### `-in` 17 | 18 | This flag provides the path to the event log to measure. Stdin is "-". 19 | 20 | ### `-rtmr` 21 | 22 | This flag defines the RTMR index that will be extended. As specified in the Intel TDX specification, user space applications can only extend to RTMR2 or RTMR3. 23 | 24 | ### `-quiet` 25 | 26 | If set, doesn't write exit errors to Stdout. All results are communicated through exit code. 27 | 28 | ### `-verbosity` 29 | 30 | Used to set the verbosity of logger, where higher number means more verbose output. 31 | 32 | Default value is `0`. 33 | 34 | ### `-dev-mode-verify` 35 | 36 | This flag enables developer mode to run the RTMR extension verification test suite. This suite performs multiple rounds of measurement extensions and compares 37 | the hardware results against software-simulated results to ensure the logic is correct. This flag is intended for debugging and development purposes only. When 38 | this flag is used, all other flags are ignored. 39 | **IMPORTANT**: This operation will pollute the existing RTMR values. To obtain a correct TD Quote after running this command, you **MUST** reboot the VM to reset the RTMRs. 40 | 41 | ## Examples 42 | 43 | The following example measures an event log and extends its measurement into the RTMR2 44 | register. 45 | 46 | ```shell 47 | $ ./extend -in log.in -rtmr 2 48 | ``` 49 | 50 | ## Exit code meaning 51 | 52 | * 0: Success 53 | * 1: Failure due to tool misuse 54 | -------------------------------------------------------------------------------- /testing/client/client.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Package client (in testing) allows tests to get a fake or real tdx-guest device. 16 | package client 17 | 18 | import ( 19 | "testing" 20 | 21 | "github.com/google/go-tdx-guest/client" 22 | test "github.com/google/go-tdx-guest/testing" 23 | ) 24 | 25 | // GetTdxGuest is a testing helper function that retrieves the 26 | // appropriate TDX-guest device from the flags passed into "go test". 27 | // 28 | // If using a test guest device, this will also produce a fake Device. 29 | func GetTdxGuest(tcs []test.TestCase, tb testing.TB) client.Device { 30 | tb.Helper() 31 | if client.UseDefaultTdxGuestDevice() { 32 | tdxTestDevice, err := test.TcDevice(tcs) 33 | if err != nil { 34 | tb.Fatalf("failed to create test device: %v", err) 35 | } 36 | return tdxTestDevice 37 | } 38 | client, err := client.OpenDevice() 39 | if err != nil { 40 | tb.Fatalf("Failed to open TDX guest device: %v", err) 41 | } 42 | return client 43 | } 44 | 45 | // GetMockTdxQuoteProvider is a testing helper function that produces a fake TDX quote provider. 46 | func GetMockTdxQuoteProvider(tcs []test.TestCase, tb testing.TB) client.QuoteProvider { 47 | tb.Helper() 48 | tdxTestQuoteProvider, err := test.TcQuoteProvider(tcs) 49 | if err != nil { 50 | tb.Fatalf("Failed to create test quote provider: %v", err) 51 | } 52 | return tdxTestQuoteProvider 53 | } 54 | -------------------------------------------------------------------------------- /client/client_windows.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | //go:build windows 16 | 17 | package client 18 | 19 | import ( 20 | "fmt" 21 | ) 22 | 23 | // WindowsDevice implements the Device interface with Linux ioctls. 24 | type WindowsDevice struct{} 25 | 26 | // Open is not supported on Windows. 27 | func (*WindowsDevice) Open(_ string) error { 28 | return fmt.Errorf("Windows is unsupported") 29 | } 30 | 31 | // OpenDevice fails on Windows. 32 | func OpenDevice() (*WindowsDevice, error) { 33 | return nil, fmt.Errorf("Windows is unsupported") 34 | } 35 | 36 | // Close is not supported on Windows. 37 | func (*WindowsDevice) Close() error { 38 | return fmt.Errorf("Windows is unsupported") 39 | } 40 | 41 | // Ioctl is not supported on Windows. 42 | func (*WindowsDevice) Ioctl(_ uintptr, _ any) (uintptr, error) { 43 | // The GuestAttestation library on Windows is closed source. 44 | return 0, fmt.Errorf("Windows is unsupported") 45 | } 46 | 47 | // WindowsConfigFsQuoteProvider implements the QuoteProvider interface to fetch attestation quote via ConfigFS. 48 | type WindowsConfigFsQuoteProvider struct{} 49 | 50 | // IsSupported is not supported on Windows. 51 | func (p *WindowsConfigFsQuoteProvider) IsSupported() error { 52 | return fmt.Errorf("Windows is unsupported") 53 | } 54 | 55 | // GetRawQuote is not supported on Windows. 56 | func (p *WindowsConfigFsQuoteProvider) GetRawQuote(reportData [64]byte) ([]uint8, error) { 57 | return nil, fmt.Errorf("Windows is unsupported") 58 | } 59 | 60 | // GetQuoteProvider is not supported on Windows. 61 | func GetQuoteProvider() (*WindowsConfigFsQuoteProvider, error) { 62 | return nil, fmt.Errorf("Windows is unsupported") 63 | } 64 | -------------------------------------------------------------------------------- /proto/doc.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Package proto contains protocol buffers that are exchanged between the client 16 | // and server, as well as convenience configuration definitions for tools. 17 | // 18 | // # Generating Protocol Buffer Code 19 | // 20 | // Anytime the Protocol Buffer definitions change, the generated Go code must be 21 | // regenerated. This can be done with "go generate". Just run: 22 | // 23 | // go generate ./... 24 | // 25 | // Upstream documentation: 26 | // https://developers.google.com/protocol-buffers/docs/reference/go-generated 27 | // 28 | // # Code Generation Dependencies 29 | // 30 | // To generate the Go code, your system must have "protoc" installed. See: 31 | // https://github.com/protocolbuffers/protobuf#protocol-compiler-installation 32 | // 33 | // The "protoc-gen-go" tool must also be installed. To install it, run: 34 | // 35 | // go install google.golang.org/protobuf/cmd/protoc-gen-go 36 | // 37 | // If you see a 'protoc-gen-go: program not found or is not executable' error 38 | // for the 'go generate' command, run the following: 39 | // 40 | // echo 'export PATH=$PATH:$GOPATH/bin' >> $HOME/.bashrc 41 | // source $HOME/.bashrc 42 | // 43 | // If you see 'google/protobuf/wrappers.proto not found', then you need to 44 | // similarly set your PROTOC_INSTALL_DIR environment variable to the protoc 45 | // installation directory which should have the "well-known types" in the 46 | // include subdirectory. 47 | package proto 48 | 49 | //go:generate protoc -I$PROTOC_INSTALL_DIR/include -I=. --go_out=. --go_opt=module=github.com/google/go-tdx-guest/proto tdx.proto 50 | //go:generate protoc -I$PROTOC_INSTALL_DIR/include -I=. --go_out=. --go_opt=module=github.com/google/go-tdx-guest/proto checkconfig.proto 51 | -------------------------------------------------------------------------------- /client/client_macos.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | //go:build darwin 16 | 17 | // Package client provides an interface to the Intel TDX guest device commands. 18 | package client 19 | 20 | import ( 21 | "fmt" 22 | ) 23 | 24 | // defaultTdxGuestDevicePath is the platform's usual device path to the TDX guest. 25 | const defaultTdxGuestDevicePath = "unknown" 26 | 27 | // MacOSDevice implements the Device interface with Linux ioctls. 28 | type MacOSDevice struct{} 29 | 30 | // Open is not supported on MacOS. 31 | func (*MacOSDevice) Open(_ string) error { 32 | return fmt.Errorf("MacOS is unsupported") 33 | } 34 | 35 | // OpenDevice fails on MacOS. 36 | func OpenDevice() (*MacOSDevice, error) { 37 | return nil, fmt.Errorf("MacOS is unsupported") 38 | } 39 | 40 | // Close is not supported on MacOS. 41 | func (*MacOSDevice) Close() error { 42 | return fmt.Errorf("MacOS is unsupported") 43 | } 44 | 45 | // Ioctl is not supported on MacOS. 46 | func (*MacOSDevice) Ioctl(_ uintptr, _ any) (uintptr, error) { 47 | return 0, fmt.Errorf("MacOS is unsupported") 48 | } 49 | 50 | // MacOsConfigFsQuoteProvider implements the QuoteProvider interface to fetch attestation quote via ConfigFS. 51 | type MacOsConfigFsQuoteProvider struct{} 52 | 53 | // IsSupported is not supported on MacOS. 54 | func (p *MacOsConfigFsQuoteProvider) IsSupported() error { 55 | return fmt.Errorf("MacOS is unsupported") 56 | } 57 | 58 | // GetRawQuote is not supported on MacOS. 59 | func (p *MacOsConfigFsQuoteProvider) GetRawQuote(reportData [64]byte) ([]uint8, error) { 60 | return nil, fmt.Errorf("MacOS is unsupported") 61 | } 62 | 63 | // GetQuoteProvider is not supported on MacOS. 64 | func GetQuoteProvider() (*MacOsConfigFsQuoteProvider, error) { 65 | return nil, fmt.Errorf("MacOS is unsupported") 66 | } 67 | -------------------------------------------------------------------------------- /testing/testdata/sample_tcbInfo_response: -------------------------------------------------------------------------------- 1 | {"tcbInfo":{"id":"TDX","version":3,"issueDate":"2023-06-18T08:42:58Z","nextUpdate":"2023-07-18T08:42:58Z","fmspc":"50806f000000","pceId":"0000","tcbType":0,"tcbEvaluationDataNumber":15,"tdxModule":{"mrsigner":"000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000","attributes":"0000000000000000","attributesMask":"FFFFFFFFFFFFFFFF"},"tcbLevels":[{"tcb":{"sgxtcbcomponents":[{"svn":5,"category":"BIOS","type":"Early Microcode Update"},{"svn":5,"category":"OS/VMM","type":"SGX Late Microcode Update"},{"svn":2,"category":"OS/VMM","type":"TXT SINIT"},{"svn":2,"category":"BIOS"},{"svn":3,"category":"BIOS"},{"svn":1,"category":"BIOS"},{"svn":0},{"svn":3,"category":"OS/VMM","type":"SEAMLDR ACM"},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0}],"pcesvn":11,"tdxtcbcomponents":[{"svn":3,"category":"OS/VMM","type":"TDX Module"},{"svn":0,"category":"OS/VMM","type":"TDX Module"},{"svn":5,"category":"OS/VMM","type":"TDX Late Microcode Update"},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0}]},"tcbDate":"2023-02-15T00:00:00Z","tcbStatus":"UpToDate"},{"tcb":{"sgxtcbcomponents":[{"svn":5,"category":"BIOS","type":"Early Microcode Update"},{"svn":5,"category":"OS/VMM","type":"SGX Late Microcode Update"},{"svn":2,"category":"OS/VMM","type":"TXT SINIT"},{"svn":2,"category":"BIOS"},{"svn":3,"category":"BIOS"},{"svn":1,"category":"BIOS"},{"svn":0},{"svn":3,"category":"OS/VMM","type":"SEAMLDR ACM"},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0}],"pcesvn":5,"tdxtcbcomponents":[{"svn":3,"category":"OS/VMM","type":"TDX Module"},{"svn":0,"category":"OS/VMM","type":"TDX Module"},{"svn":5,"category":"OS/VMM","type":"TDX Late Microcode Update"},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0}]},"tcbDate":"2018-01-04T00:00:00Z","tcbStatus":"OutOfDate","advisoryIDs":["INTEL-SA-00106","INTEL-SA-00115","INTEL-SA-00135","INTEL-SA-00203","INTEL-SA-00220","INTEL-SA-00233","INTEL-SA-00270","INTEL-SA-00293","INTEL-SA-00320","INTEL-SA-00329","INTEL-SA-00381","INTEL-SA-00389","INTEL-SA-00477"]}]},"signature":"f6502d6fad1e3b7281df2b7eddc773d5b5281187346c12c5647b4f243cea49212be96a7a1a6b5d83e36323fe3fa9dacd61ebfbc38e631ff0fe29ef14ae0db0b4"} 2 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 2 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 3 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 4 | github.com/google/go-configfs-tsm v0.3.2 h1:ZYmHkdQavfsvVGDtX7RRda0gamelUNUhu0A9fbiuLmE= 5 | github.com/google/go-configfs-tsm v0.3.2/go.mod h1:EL1GTDFMb5PZQWDviGfZV9n87WeGTR/JUg13RfwkgRo= 6 | github.com/google/go-eventlog v0.0.1 h1:7lV3gf61LNDhfS9gQplqaJc/j9ztLhKKgZk/lR6vv4Q= 7 | github.com/google/go-eventlog v0.0.1/go.mod h1:7huE5P8w2NTObSwSJjboHmB7ioBNblkijdzoVa2skfQ= 8 | github.com/google/go-eventlog v0.0.2-0.20241213203620-f921bdc3aeb0 h1:270O3tFxca1lAXm3JVWqUU4fHlK3EEIEYIfk4koWMkM= 9 | github.com/google/go-eventlog v0.0.2-0.20241213203620-f921bdc3aeb0/go.mod h1:7huE5P8w2NTObSwSJjboHmB7ioBNblkijdzoVa2skfQ= 10 | github.com/google/go-sev-guest v0.8.0 h1:IIZIqdcMJXgTm1nMvId442OUpYebbWDWa9bi9/lUUwc= 11 | github.com/google/go-sev-guest v0.8.0/go.mod h1:hc1R4R6f8+NcJwITs0L90fYWTsBpd1Ix+Gur15sqHDs= 12 | github.com/google/go-tpm v0.9.0 h1:sQF6YqWMi+SCXpsmS3fd21oPy/vSddwZry4JnmltHVk= 13 | github.com/google/go-tpm v0.9.0/go.mod h1:FkNVkc6C+IsvDI9Jw1OveJmxGZUUaKxtrpOS47QWKfU= 14 | github.com/google/logger v1.1.1 h1:+6Z2geNxc9G+4D4oDO9njjjn2d0wN5d7uOo0vOIW1NQ= 15 | github.com/google/logger v1.1.1/go.mod h1:BkeJZ+1FhQ+/d087r4dzojEg1u2ZX+ZqG1jTUrLM+zQ= 16 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 17 | github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= 18 | go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= 19 | go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= 20 | golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k= 21 | golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= 22 | golang.org/x/sys v0.0.0-20210426230700-d19ff857e887/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 23 | golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= 24 | golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 25 | google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= 26 | google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= 27 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 28 | -------------------------------------------------------------------------------- /tools/check/README.md: -------------------------------------------------------------------------------- 1 | # `check` CLI tool 2 | 3 | This binary is a thin wrapper around the `verify` library to 4 | check Intel TDX quotes against expectations. 5 | 6 | The tool's input is an Intel TDX quote. 7 | 8 | The tool's output is an error or "Success". 9 | 10 | ## Usage 11 | 12 | ``` 13 | ./check [options...] 14 | ``` 15 | 16 | ### `-in` 17 | 18 | This flag provides the path to the quote to check. Stdin is "-". 19 | 20 | ### `-inform` 21 | 22 | The format that input takes. One of 23 | 24 | * `bin`: for a raw binary quote. 25 | * `proto`: A binary serialized `tdx.QuoteV4` message. 26 | * `textproto`: The `tdx.QuoteV4` message in textproto format. 27 | 28 | Default value is `bin`. 29 | 30 | ### `-quiet` 31 | 32 | If set, doesn't write exit errors to Stdout. All results are communicated through exit code. 33 | 34 | ### `-parse_ccel` 35 | 36 | If true, parses a Confidential Computing event log and replays the parsed event log 37 | against the RTMR banks extracted from the verified TD quote. 38 | The parsed results are then saved to a **result.textproto** file in the current directory. 39 | 40 | Default `false`. 41 | 42 | ### `-verbosity` 43 | 44 | Used to set the verbosity of logger, where higher number means more verbose output. 45 | 46 | Default value is `0`. 47 | 48 | ### `-check_crl` 49 | 50 | Checks if the PCK certificate and the intermediate certificate of the PCK 51 | certificate chain has been revoked, and errors if so. Default `false`. Requires 52 | `-get_collateral` to be true so that CRLs are downloaded from the network. 53 | 54 | Note: For more details about PCK CRLs refer [Intel's PCK CRL specification](https://api.trustedservices.intel.com/documents/Intel_SGX_PCK_Certificate_CRL_Spec-1.5.pdf) 55 | 56 | ### `-get_collateral` 57 | 58 | Uses the network to download "collateral" elements: 59 | 60 | * CRLs (if `-check_crl`) 61 | * The Intel quoting enclave (QE) Identity, and 62 | * TCB info from Intel's PCS. 63 | 64 | Default `false`. 65 | 66 | ## Examples 67 | 68 | The following example checks a binary quote, downloads collaterals, checks the 69 | quote against collaterals, and checks certificate revocations. 70 | 71 | ```shell 72 | $ ./check -in quote.dat -inform bin -get_collateral -check_crl 73 | ``` 74 | 75 | ## Exit code meaning 76 | 77 | * 0: Success 78 | * 1: Failure due to tool misuse 79 | * 2: Failure due to quote parsing errors, invalid signatures, certificates or 80 | collateral mismatch 81 | * 3: Failure due to an issue with the network or Intel's PCS 82 | 83 | -------------------------------------------------------------------------------- /tools/attest/README.md: -------------------------------------------------------------------------------- 1 | # `attest` CLI tool 2 | 3 | This binary is a thin wrapper around the `client` library to gather attestation 4 | reports in either binary or textproto formats. 5 | 6 | The tool's input is the intended `REPORT_DATA` contents, which is 64 bytes of 7 | user-provided data to include in the attestation report. This is typically a 8 | nonce. 9 | 10 | The tool's output is the report in any specified format to either standard out 11 | or directly to a file. 12 | 13 | *Note*: For Ubuntu images, the `tdx_guest` module was moved to linux-modules-extra 14 | package in the 1016 and newer kernels. You should be able to install the module, 15 | and either manually load the module or reboot. 16 | 17 | To install the linux-modules-extra package, run: 18 | 19 | ```console 20 | sudo apt-get install linux-modules-extra-$(uname -r) 21 | ``` 22 | 23 | To manually load the module, run: 24 | 25 | ```console 26 | sudo modprobe tdx_guest 27 | ``` 28 | 29 | ## Usage 30 | 31 | ``` 32 | ./attest [options...] 33 | ``` 34 | 35 | ### `-in` 36 | 37 | This flag provides a string of 64 bytes `REPORT_DATA` content directly on the command line to include in the output attestation report. 38 | REPORT_DATA can be either in base64 or hex format. If -inform=auto, first check with base64, hex and last with auto. 39 | 40 | ### `-inform` 41 | 42 | The format that input takes. One of 43 | 44 | * `base64`: for a byte string in base64 encoding. Fewer bytes than expected 45 | will be zero-filled. 46 | * `hex`: for a byte string encoded as a hexadecimal string. Fewer bytes than 47 | expected will be zero-filled. 48 | * `auto`: first check with base64 and last with hex 49 | 50 | Default value is `auto`. 51 | 52 | ### `-outform` 53 | 54 | The format that output takes. This can be `bin` for Intel's specified structures 55 | in binary or `textproto` for this module's protobuf message types in human readable text format. 56 | 57 | Default value is `bin`. 58 | 59 | ### `-out` 60 | 61 | Path to output file to write attestation report to. 62 | 63 | Default is empty, interpreted as stdout. 64 | 65 | 66 | ### `-verbose` 67 | 68 | If set, then the logger can append INFO and WARNING logs to stdout as per the verbosity level. Default logger has verbosity set to `0`, so verbosity option should be set to appropriate value to append INFO and WARN logs at variable verbosity levels to stdout. 69 | 70 | Default value is `false`. 71 | 72 | ### `-verbosity` 73 | 74 | Used to set the verbosity of logger, where higher number means more verbose output. 75 | 76 | Default value is `0`. 77 | -------------------------------------------------------------------------------- /rtmr/extend_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package rtmr 16 | 17 | import ( 18 | "crypto" 19 | "strings" 20 | "testing" 21 | 22 | "github.com/google/go-configfs-tsm/configfs/fakertmr" 23 | ) 24 | 25 | func TestExtendEventLogOk(t *testing.T) { 26 | client := fakertmr.CreateRtmrSubsystem(t.TempDir()) 27 | err := ExtendEventLogClient(client, 2, crypto.SHA384, []byte("event log")) 28 | if err != nil { 29 | t.Errorf("ExtendEventlog failed: %v", err) 30 | } 31 | } 32 | 33 | func TestExtendDigestOk(t *testing.T) { 34 | client := fakertmr.CreateRtmrSubsystem(t.TempDir()) 35 | var sha384Hash [48]byte 36 | err := ExtendDigestClient(client, 2, sha384Hash[:]) 37 | if err != nil { 38 | t.Errorf("ExtendDigest failed: %v", err) 39 | } 40 | } 41 | 42 | func TestExtendEventLogErr(t *testing.T) { 43 | tcs := []struct { 44 | rtmr int 45 | crypto crypto.Hash 46 | eventlog []byte 47 | wantErr string 48 | }{ 49 | {rtmr: 2, crypto: crypto.SHA256, eventlog: []byte("event log"), wantErr: "unsupported hash algorithm SHA-256"}, 50 | {rtmr: 4, crypto: crypto.SHA384, eventlog: []byte("event log"), wantErr: "index can only be 0-3"}, 51 | {rtmr: 3, crypto: crypto.SHA384, eventlog: []byte(""), wantErr: "input event log is empty"}, 52 | } 53 | client := fakertmr.CreateRtmrSubsystem(t.TempDir()) 54 | for _, tc := range tcs { 55 | err := ExtendEventLogClient(client, tc.rtmr, tc.crypto, tc.eventlog) 56 | if err == nil || !strings.Contains(err.Error(), tc.wantErr) { 57 | t.Fatalf("ExtendEventLog(%d, %v, %q) failed: %v, want %q", tc.rtmr, tc.crypto, tc.eventlog, err, tc.wantErr) 58 | } 59 | } 60 | } 61 | 62 | func TestExtendDigestErr(t *testing.T) { 63 | var sha384Hash [48]byte 64 | var sha512Hash [64]byte 65 | tcs := []struct { 66 | rtmr int 67 | digest []byte 68 | wantErr string 69 | }{ 70 | {rtmr: 4, digest: sha384Hash[:], wantErr: "index can only be 0-3"}, 71 | {rtmr: 3, digest: sha512Hash[:], wantErr: "sha384 digest should be 48 bytes"}, 72 | } 73 | client := fakertmr.CreateRtmrSubsystem(t.TempDir()) 74 | for _, tc := range tcs { 75 | err := ExtendDigestClient(client, tc.rtmr, tc.digest) 76 | if err == nil || !strings.Contains(err.Error(), tc.wantErr) { 77 | t.Fatalf("ExtendDigest(%d, %v) failed: %v, want %q", tc.rtmr, tc.digest, err, tc.wantErr) 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /.github/workflows/CI.yml: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2023 Google LLC 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | 17 | name: CI 18 | 'on': 19 | push: 20 | tags: 21 | - v* 22 | branches: 23 | - main 24 | pull_request: null 25 | 26 | jobs: 27 | build: 28 | strategy: 29 | matrix: 30 | go-version: 31 | - 1.21.x 32 | os: 33 | - macos-latest 34 | - ubuntu-latest 35 | name: 'Build/Test (${{ matrix.os}}, Go ${{ matrix.go-version }})' 36 | runs-on: '${{ matrix.os }}' 37 | steps: 38 | - uses: actions/checkout@v3 39 | - name: Setup Go 40 | uses: actions/setup-go@v3 41 | with: 42 | go-version: '${{ matrix.go-version }}' 43 | - name: Install Protoc 44 | uses: arduino/setup-protoc@v3 45 | with: 46 | repo-token: '${{ secrets.GITHUB_TOKEN }}' 47 | - name: Install protoc-gen-go 48 | run: go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.28.1 49 | - name: Check Protobuf Generation 50 | run: | 51 | go generate ./... 52 | git diff -G'^[^/]' --exit-code 53 | - name: Generate all protobufs 54 | run: go generate ./... 55 | - name: Build all packages 56 | run: go build -v ./... 57 | - name: Test all packages 58 | run: go test -v ./... 59 | 60 | lint: 61 | strategy: 62 | matrix: 63 | go-version: 64 | - 1.21.x 65 | os: 66 | - ubuntu-latest 67 | name: 'Lint ${{ matrix.dir }} (${{ matrix.os }}, Go ${{ matrix.go-version }})' 68 | runs-on: '${{ matrix.os }}' 69 | steps: 70 | - uses: actions/checkout@v3 71 | - uses: actions/setup-go@v2 72 | with: 73 | go-version: '${{ matrix.go-version }}' 74 | - name: Run golangci-lint 75 | uses: golangci/golangci-lint-action@v3.3.0 76 | with: 77 | version: latest 78 | working-directory: ./ 79 | args: > 80 | -D errcheck -E stylecheck -E goimports -E misspell -E revive -E 81 | gofmt -E goimports --exclude-use-default=false --max-same-issues=0 82 | --max-issues-per-linter=0 --timeout 2m 83 | 84 | lintc: 85 | strategy: 86 | matrix: 87 | go-version: 88 | - 1.21.x 89 | os: 90 | - ubuntu-latest 91 | name: 'Lint CGO (${{ matrix.os}}, Go ${{ matrix.go-version }})' 92 | runs-on: '${{ matrix.os }}' 93 | steps: 94 | - uses: actions/checkout@v3 95 | - uses: actions/setup-go@v2 96 | with: 97 | go-version: '${{ matrix.go-version }}' 98 | - name: Check for CGO Warnings (gcc) 99 | run: CGO_CFLAGS=-Werror CC=gcc go build ./... 100 | - name: Check for CGO Warnings (clang) 101 | run: CGO_CFLAGS=-Werror CC=clang go build ./... 102 | -------------------------------------------------------------------------------- /proto/checkconfig.proto: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | syntax = "proto3"; 16 | 17 | // Package checkconfig represents an attestation validation policy. 18 | package checkconfig; 19 | 20 | option go_package = "github.com/google/go-tdx-guest/proto/checkconfig"; 21 | 22 | // Policy is a representation of an attestation quote validation policy. 23 | // Each field corresponds to a field on validate.Options. This format 24 | // is useful for providing programmatic inputs to the `check` CLI tool. 25 | message Policy { 26 | // HeaderPolicy is representation of Header of an attestation quote validation 27 | // policy. 28 | HeaderPolicy header_policy = 1; // should be 20 bytes 29 | 30 | // TDQuoteBodyPolicy is representation of TdQuoteBody of an attestation quote 31 | // validation policy. 32 | TDQuoteBodyPolicy td_quote_body_policy = 2; // should be 528 bytes 33 | } 34 | 35 | message HeaderPolicy { 36 | uint32 minimum_qe_svn = 1; // should not exceed uint16 max 37 | uint32 minimum_pce_svn = 2; // should not exceed uint16 max 38 | 39 | // Unique vendor id of QE vendor 40 | bytes qe_vendor_id = 3; // should be 16 bytes 41 | } 42 | 43 | message TDQuoteBodyPolicy { 44 | bytes minimum_tee_tcb_svn = 1; // should be 16 bytes 45 | bytes mr_seam = 2; // should be 48 bytes 46 | bytes td_attributes = 3; // should be 8 bytes 47 | bytes xfam = 4; // should be 8 bytes 48 | bytes mr_td = 5; // should be 48 bytes 49 | bytes mr_config_id = 6; // should be 48 bytes 50 | bytes mr_owner = 7; // should be 48 bytes 51 | bytes mr_owner_config = 8; // should be 48 bytes 52 | repeated bytes rtmrs = 9; // should be 48 * rtmrsCount 53 | bytes report_data = 10; // should be 64 bytes 54 | repeated bytes any_mr_td = 11; // each should be 48 bytes. 55 | bool enable_td_debug_check = 12; // if true, check that the DEBUG bit is 0 in TDAttributes. 56 | } 57 | 58 | // RootOfTrust represents configuration for which hardware root of trust 59 | // certificates to use for verifying attestation quote. 60 | message RootOfTrust { 61 | // Paths to CA bundles for the Intel TDX. 62 | // Must be in PEM format. 63 | // If empty, uses the verification library's embedded certificates from Intel. 64 | repeated string cabundle_paths = 1; 65 | 66 | // PEM format CA bundles for Intel TDX. Combined with contents of 67 | // cabundle_paths. 68 | repeated string cabundles = 2; 69 | 70 | // If true, download and check the CRL for revoked certificates. 71 | bool check_crl = 3; 72 | 73 | // If true, then check is not permitted to download necessary files for 74 | // verification. 75 | bool get_collateral = 4; 76 | } 77 | 78 | // Config is the overall message input for the check tool. This provides all 79 | // the flags that configure the tool, including the validation policy. 80 | message Config { 81 | // The report validation policy. 82 | Policy policy = 1; 83 | 84 | // Configures which hardware keys to trust. Default uses library-embedded 85 | // certificate. 86 | RootOfTrust root_of_trust = 2; 87 | } 88 | -------------------------------------------------------------------------------- /verify/trust/trust_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2025 Edgeless Systems GmbH 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package trust_test 16 | 17 | import ( 18 | "bytes" 19 | "context" 20 | "errors" 21 | "testing" 22 | "time" 23 | 24 | "github.com/google/go-tdx-guest/verify/trust" 25 | ) 26 | 27 | // Ensure that the HTTPSGetters implement the expected interfaces. 28 | var ( 29 | _ = trust.HTTPSGetter(&trust.RetryHTTPSGetter{}) 30 | _ = trust.ContextHTTPSGetter(&trust.RetryHTTPSGetter{}) 31 | _ = trust.HTTPSGetter(&trust.SimpleHTTPSGetter{}) 32 | _ = trust.ContextHTTPSGetter(&trust.SimpleHTTPSGetter{}) 33 | ) 34 | 35 | func TestRetryHTTPSGetterContext(t *testing.T) { 36 | testGetter := &recordingContextGetter{} 37 | r := &trust.RetryHTTPSGetter{ 38 | MaxRetryDelay: 1 * time.Millisecond, 39 | Getter: testGetter, 40 | } 41 | 42 | ctx, cancel := context.WithCancel(context.Background()) 43 | cancel() 44 | headers, body, err := r.GetContext(ctx, "https://fetch.me") 45 | if len(headers) > 0 { 46 | t.Errorf("expected empty headers but got %q", headers) 47 | } 48 | if !bytes.Equal(body, []byte("")) { 49 | t.Errorf("expected empty body but got %q", body) 50 | } 51 | if !errors.Is(err, context.Canceled) { 52 | t.Errorf("expected error %q, but got %q", context.Canceled, err) 53 | } 54 | } 55 | 56 | func TestGetWith(t *testing.T) { 57 | url := "" 58 | t.Run("HTTPSGetter uses Get", func(t *testing.T) { 59 | contextGetter := recordingContextGetter{} 60 | if _, _, err := trust.GetWith(context.Background(), &contextGetter.recordingGetter, url); err != nil { 61 | t.Fatalf("trust.GetWith returned an unexpected error: %v", err) 62 | } 63 | if contextGetter.getContextCalls != 0 { 64 | t.Errorf("wrong number of calls to GetContext: got %d, want 0", contextGetter.getContextCalls) 65 | } 66 | if contextGetter.recordingGetter.getCalls != 1 { 67 | t.Errorf("wrong number of calls to Get: got %d, want 1", contextGetter.getCalls) 68 | } 69 | }) 70 | t.Run("ContextHTTPSGetter uses GetContext", func(t *testing.T) { 71 | contextGetter := recordingContextGetter{} 72 | if _, _, err := trust.GetWith(context.Background(), &contextGetter, url); err != nil { 73 | t.Fatalf("trust.GetWith returned an unexpected error: %v", err) 74 | } 75 | if contextGetter.getContextCalls != 1 { 76 | t.Errorf("wrong number of calls to GetContext: got %d, want 1", contextGetter.getContextCalls) 77 | } 78 | if contextGetter.recordingGetter.getCalls != 0 { 79 | t.Errorf("wrong number of calls to Get: got %d, want 0", contextGetter.getCalls) 80 | } 81 | }) 82 | 83 | } 84 | 85 | type recordingGetter struct { 86 | getCalls int 87 | } 88 | 89 | func (r *recordingGetter) Get(_ string) (map[string][]string, []byte, error) { 90 | r.getCalls++ 91 | return map[string][]string{"Header": {"value"}}, []byte("content"), nil 92 | } 93 | 94 | type recordingContextGetter struct { 95 | recordingGetter 96 | getContextCalls int 97 | } 98 | 99 | func (r *recordingContextGetter) GetContext(ctx context.Context, _ string) (map[string][]string, []byte, error) { 100 | r.getContextCalls++ 101 | select { 102 | case <-ctx.Done(): 103 | return nil, nil, ctx.Err() 104 | default: 105 | return map[string][]string{"Header": {"value"}}, []byte("content"), nil 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /rtmr/ccel.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); you may not 4 | // use this file except in compliance with the License. You may obtain a copy of 5 | // the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | // License for the specific language governing permissions and limitations under 13 | // the License. 14 | 15 | // Package rtmr provides the library functions: 16 | // 1. extend and read TDX rtmr registers and their tcg maps. 17 | // 2. replay the event log with the TDX quote. 18 | package rtmr 19 | 20 | import ( 21 | "fmt" 22 | 23 | "github.com/google/go-eventlog/ccel" 24 | "github.com/google/go-eventlog/extract" 25 | "github.com/google/go-eventlog/proto/state" 26 | "github.com/google/go-eventlog/register" 27 | "github.com/google/go-tdx-guest/abi" 28 | tdxpb "github.com/google/go-tdx-guest/proto/tdx" 29 | "github.com/google/go-tdx-guest/validate" 30 | "github.com/google/go-tdx-guest/verify" 31 | ) 32 | 33 | // ParseTdxCcelOpts allows for customizing the functionality of VerifyAttestation's TDX verification. 34 | type ParseTdxCcelOpts struct { 35 | Validation *validate.Options 36 | Verification *verify.Options 37 | ExtractOpt extract.Opts 38 | } 39 | 40 | func getRtmrsFromTdQuoteV4(quote *tdxpb.QuoteV4) (*register.RTMRBank, error) { 41 | bank := register.RTMRBank{} 42 | rtmrs := quote.TdQuoteBody.Rtmrs 43 | for index, rtmr := range rtmrs { 44 | bank.RTMRs = append(bank.RTMRs, register.RTMR{ 45 | Index: int(index), 46 | Digest: rtmr, 47 | }) 48 | // Tdx Quote V4 has a maximum of 4 RTMRs 49 | if index > 3 { 50 | return nil, fmt.Errorf("too many RTMRs in quote") 51 | } 52 | } 53 | return &bank, nil 54 | } 55 | 56 | // GetRtmrsFromTdQuote extracts the RTMRs from a TDX attestation quote. 57 | // It is the caller's responsibility to verify the quote before calling this function. 58 | func GetRtmrsFromTdQuote(quote interface{}) (*register.RTMRBank, error) { 59 | switch q := quote.(type) { 60 | case *tdxpb.QuoteV4: 61 | rtmrs, err := getRtmrsFromTdQuoteV4(q) 62 | if err != nil { 63 | return nil, err 64 | } 65 | return rtmrs, nil 66 | default: 67 | return nil, fmt.Errorf("unsupported quote type: %T", quote) 68 | } 69 | } 70 | 71 | // TdxDefaultOpts returns a default validation policy and verification options for TDX 72 | // attestation quote on GCE. 73 | func TdxDefaultOpts(tdxNonce []byte) ParseTdxCcelOpts { 74 | policy := &validate.Options{HeaderOptions: validate.HeaderOptions{}, 75 | TdQuoteBodyOptions: validate.TdQuoteBodyOptions{}} 76 | policy.TdQuoteBodyOptions.ReportData = make([]byte, abi.ReportDataSize) 77 | copy(policy.TdQuoteBodyOptions.ReportData, tdxNonce) 78 | return ParseTdxCcelOpts{ 79 | Validation: policy, 80 | Verification: verify.DefaultOptions(), 81 | ExtractOpt: extract.Opts{Loader: extract.GRUB}, 82 | } 83 | } 84 | 85 | // ParseCcelWithTdQuote verify the TD quote, parses the CCEL, and replays the 86 | // events against the RTMR values from the TD quote.. 87 | // It returns the corresponding FirmwareLogState containing the events verified 88 | // by particular RTMR indexes/digests. 89 | // It returns an error on failing to replay the events against the RTMR bank or 90 | // on failing to parse malformed events. 91 | func ParseCcelWithTdQuote(ccelBytes []byte, tableBytes []byte, tdxAttestationQuote any, opts *ParseTdxCcelOpts) (*state.FirmwareLogState, error) { 92 | // Check that the quote contains valid signature and certificates. 93 | if err := verify.TdxQuote(tdxAttestationQuote, opts.Verification); err != nil { 94 | return nil, err 95 | } 96 | // Check that the fields of the quote are acceptable. 97 | if err := validate.TdxQuote(tdxAttestationQuote, opts.Validation); err != nil { 98 | return nil, err 99 | } 100 | // Read the RTMRs from the quote. 101 | rtmrBank, err := GetRtmrsFromTdQuote(tdxAttestationQuote) 102 | if err != nil { 103 | return nil, err 104 | } 105 | // Parse the event log and replay the event log with the RTMR values. 106 | return ccel.ReplayAndExtract(tableBytes, ccelBytes, *rtmrBank, opts.ExtractOpt) 107 | } 108 | -------------------------------------------------------------------------------- /client/client_linux.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | //go:build linux 16 | 17 | // Package client provides an interface to the Intel TDX guest device commands. 18 | package client 19 | 20 | import ( 21 | "fmt" 22 | 23 | "unsafe" 24 | 25 | "github.com/google/go-configfs-tsm/configfs/linuxtsm" 26 | "github.com/google/go-configfs-tsm/report" 27 | labi "github.com/google/go-tdx-guest/client/linuxabi" 28 | "golang.org/x/sys/unix" 29 | ) 30 | 31 | // defaultTdxGuestDevicePath is the platform's usual device path to the TDX guest. 32 | const defaultTdxGuestDevicePath = "/dev/tdx_guest" 33 | 34 | // LinuxDevice implements the Device interface with Linux ioctls. 35 | type LinuxDevice struct { 36 | fd int 37 | } 38 | 39 | // Open opens the TDX guest device from a given path 40 | func (d *LinuxDevice) Open(path string) error { 41 | fd, err := unix.Open(path, unix.O_RDWR|unix.O_SYNC, 0) 42 | if err != nil { 43 | d.fd = -1 44 | return fmt.Errorf("could not open Intel TDX guest device at %q: %v", path, err) 45 | } 46 | d.fd = fd 47 | return nil 48 | } 49 | 50 | // OpenDevice opens the TDX guest device. 51 | func OpenDevice() (*LinuxDevice, error) { 52 | result := &LinuxDevice{} 53 | path := *tdxGuestPath 54 | if UseDefaultTdxGuestDevice() { 55 | path = defaultTdxGuestDevicePath 56 | } 57 | if err := result.Open(path); err != nil { 58 | return nil, err 59 | } 60 | return result, nil 61 | } 62 | 63 | // Close closes the TDX guest device. 64 | func (d *LinuxDevice) Close() error { 65 | if d.fd == -1 { // Not open 66 | return nil 67 | } 68 | if err := unix.Close(d.fd); err != nil { 69 | return err 70 | } 71 | // Prevent double-close. 72 | d.fd = -1 73 | return nil 74 | } 75 | 76 | // Ioctl sends a command with its wrapped request and response values to the Linux device. 77 | func (d *LinuxDevice) Ioctl(command uintptr, req any) (uintptr, error) { 78 | if d.fd == -1 { 79 | return 0, fmt.Errorf("intel TDX Guest Device is not open") 80 | } 81 | switch sreq := req.(type) { 82 | case *labi.TdxQuoteReq: 83 | abi := sreq.ABI() 84 | result, _, errno := unix.Syscall(unix.SYS_IOCTL, uintptr(d.fd), command, uintptr(abi.Pointer())) 85 | if err := abi.Finish(sreq); err != nil { 86 | return 0, fmt.Errorf("failed to finish ABI request: %w", err) 87 | } 88 | if errno == unix.EBUSY { 89 | return 0, errno 90 | } else if errno == unix.ENOTTY { 91 | return 0, fmt.Errorf("invalid ioctl! use QuoteProvider to fetch attestation quote") 92 | } else if errno != 0 { 93 | return 0, errno 94 | } 95 | return result, nil 96 | case *labi.TdxReportReq: 97 | result, _, errno := unix.Syscall(unix.SYS_IOCTL, uintptr(d.fd), command, uintptr(unsafe.Pointer(req.(*labi.TdxReportReq)))) 98 | if errno != 0 { 99 | return 0, errno 100 | } 101 | return result, nil 102 | } 103 | return 0, fmt.Errorf("unexpected request value: %v", req) 104 | } 105 | 106 | // LinuxConfigFsQuoteProvider implements the QuoteProvider interface to fetch 107 | // attestation quote via ConfigFS. 108 | type LinuxConfigFsQuoteProvider struct{} 109 | 110 | // IsSupported checks if TSM client can be created to use ConfigFS system, 111 | // and that ConfigFS concurs that the backing technology is TDX. 112 | func (p *LinuxConfigFsQuoteProvider) IsSupported() error { 113 | c, err := linuxtsm.MakeClient() 114 | if err != nil { 115 | return err 116 | } 117 | r, err := report.Create(c, &report.Request{}) 118 | if err != nil { 119 | return err 120 | } 121 | provider, err := r.ReadOption("provider") 122 | if err != nil { 123 | return err 124 | } 125 | pstr := string(provider) 126 | if pstr != "tdx_guest\n" { 127 | return fmt.Errorf("report provider %q is not 'tdx_guest'", pstr) 128 | } 129 | return nil 130 | } 131 | 132 | // GetRawQuote returns byte format attestation quote via ConfigFS. 133 | func (p *LinuxConfigFsQuoteProvider) GetRawQuote(reportData [64]byte) ([]uint8, error) { 134 | req := &report.Request{ 135 | InBlob: reportData[:], 136 | GetAuxBlob: false, 137 | } 138 | resp, err := linuxtsm.GetReport(req) 139 | if err != nil { 140 | return nil, err 141 | } 142 | tdReport := resp.OutBlob 143 | return tdReport, nil 144 | } 145 | 146 | // GetQuoteProvider returns an instance of LinuxConfigFsQuoteProvider. 147 | func GetQuoteProvider() (*LinuxConfigFsQuoteProvider, error) { 148 | return &LinuxConfigFsQuoteProvider{}, nil 149 | } 150 | -------------------------------------------------------------------------------- /client/client_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | package client 15 | 16 | import ( 17 | "bytes" 18 | "fmt" 19 | "sync" 20 | "testing" 21 | 22 | "github.com/google/go-cmp/cmp" 23 | "github.com/google/go-tdx-guest/abi" 24 | test "github.com/google/go-tdx-guest/testing" 25 | "google.golang.org/protobuf/testing/protocmp" 26 | ) 27 | 28 | var devMu sync.Once 29 | var device Device 30 | var quoteProvider QuoteProvider 31 | var tests []test.TestCase 32 | 33 | const defaultTDXDevicePath = "/dev/tdx-guest" 34 | 35 | func initialize() { 36 | for _, tc := range test.TestCases() { 37 | // Don't test faked errors when running real hardware tests. 38 | if !UseDefaultTdxGuestDevice() && tc.WantErr != "" { 39 | continue 40 | } 41 | tests = append(tests, tc) 42 | } 43 | tdxTestQuoteProvider, err := test.TcQuoteProvider(tests) 44 | if err != nil { 45 | panic(fmt.Sprintf("failed to create test quote provider: %v", err)) 46 | } 47 | quoteProvider = tdxTestQuoteProvider 48 | // Choose a mock device or a real device depending on the --tdx_guest_device_path flag. 49 | if UseDefaultTdxGuestDevice() { 50 | tdxTestDevice, err := test.TcDevice(tests) 51 | if err != nil { 52 | panic(fmt.Sprintf("failed to create test device: %v", err)) 53 | } 54 | if err := tdxTestDevice.Open(defaultTDXDevicePath); err != nil { 55 | panic(err) 56 | } 57 | device = tdxTestDevice 58 | return 59 | } 60 | client, err := OpenDevice() 61 | if err != nil { 62 | panic(err) 63 | } 64 | device = client 65 | } 66 | func TestGetReport(t *testing.T) { 67 | devMu.Do(initialize) 68 | for _, tc := range test.TestCases() { 69 | t.Run(tc.Name, func(t *testing.T) { 70 | got, err := getReport(device, tc.Input) 71 | if err != nil { 72 | t.Errorf("failed to get the report: %v", err) 73 | } 74 | if tc.WantErr == "" { 75 | want := tc.Report 76 | if !bytes.Equal(got[:], want[:]) { 77 | t.Errorf("Got %v want %v", got, want) 78 | } 79 | } 80 | }) 81 | } 82 | } 83 | func TestGetRawQuote(t *testing.T) { 84 | devMu.Do(initialize) 85 | for _, tc := range test.TestCases() { 86 | t.Run(tc.Name, func(t *testing.T) { 87 | got, err := GetRawQuote(device, tc.Input) 88 | if !test.Match(err, tc.WantErr) { 89 | t.Fatalf("GetRawQuote(device, %v) = %v, %v. Want err: %q", tc.Input, got, err, tc.WantErr) 90 | } 91 | if tc.WantErr == "" { 92 | want := tc.Quote 93 | if !bytes.Equal(got, want) { 94 | t.Errorf("GetRawQuote(device, %v) = %v want %v", tc.Input, got, want) 95 | } 96 | } 97 | }) 98 | } 99 | } 100 | func TestGetQuote(t *testing.T) { 101 | devMu.Do(initialize) 102 | for _, tc := range test.TestCases() { 103 | t.Run(tc.Name, func(t *testing.T) { 104 | got, err := GetQuote(device, tc.Input) 105 | if !test.Match(err, tc.WantErr) { 106 | t.Fatalf("Expected %v got err: %v", err, tc.WantErr) 107 | } 108 | if tc.WantErr == "" { 109 | quote, err := abi.QuoteToProto(tc.Quote) 110 | if err != nil { 111 | t.Error(err) 112 | } 113 | if diff := cmp.Diff(got, quote, protocmp.Transform()); diff != "" { 114 | t.Errorf("Difference in quote: %s", diff) 115 | } 116 | } 117 | }) 118 | } 119 | } 120 | func TestGetRawQuoteViaProvider(t *testing.T) { 121 | devMu.Do(initialize) 122 | for _, tc := range test.TestCases() { 123 | t.Run(tc.Name, func(t *testing.T) { 124 | got, err := GetRawQuote(quoteProvider, tc.Input) 125 | if !test.Match(err, tc.WantErr) { 126 | t.Fatalf("GetRawQuoteViaProvider(quoteProvider, %v) = %v, %v. Want err: %q", tc.Input, got, err, tc.WantErr) 127 | } 128 | if tc.WantErr == "" { 129 | want := tc.Quote 130 | if !bytes.Equal(got, want) { 131 | t.Errorf("GetRawQuoteViaProvider(quoteProvider, %v) = %v want %v", tc.Input, got, want) 132 | } 133 | } 134 | }) 135 | } 136 | } 137 | func TestGetQuoteViaProvider(t *testing.T) { 138 | devMu.Do(initialize) 139 | for _, tc := range test.TestCases() { 140 | t.Run(tc.Name, func(t *testing.T) { 141 | got, err := GetQuote(quoteProvider, tc.Input) 142 | if !test.Match(err, tc.WantErr) { 143 | t.Fatalf("Expected %v got err: %v", err, tc.WantErr) 144 | } 145 | if tc.WantErr == "" { 146 | quote, err := abi.QuoteToProto(tc.Quote) 147 | if err != nil { 148 | t.Error(err) 149 | } 150 | if diff := cmp.Diff(got, quote, protocmp.Transform()); diff != "" { 151 | t.Errorf("Difference in quote: %s", diff) 152 | } 153 | } 154 | }) 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /verify/trust/trust.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Package trust defines core trust types and values for attestation verification. 16 | package trust 17 | 18 | import ( 19 | "context" 20 | "fmt" 21 | "io" 22 | "net/http" 23 | "time" 24 | ) 25 | 26 | // HTTPSGetter represents the ability to fetch data from the internet from an HTTP URL. 27 | type HTTPSGetter interface { 28 | Get(url string) (map[string][]string, []byte, error) 29 | } 30 | 31 | // ContextHTTPSGetter is an HTTPSGetter that takes an additional context input. 32 | type ContextHTTPSGetter interface { 33 | GetContext(ctx context.Context, url string) (map[string][]string, []byte, error) 34 | } 35 | 36 | // GetWith fetches a resource from a URL with an HTTPSGetter. If the getter implements 37 | // ContextHTTPSGetter, the GetContext method will be used, otherwise the Get method. 38 | func GetWith(ctx context.Context, getter HTTPSGetter, url string) (map[string][]string, []byte, error) { 39 | if contextGetter, ok := getter.(ContextHTTPSGetter); ok { 40 | return contextGetter.GetContext(ctx, url) 41 | } 42 | return getter.Get(url) 43 | } 44 | 45 | // AttestationRecreationErr represents a problem with fetching or interpreting associated 46 | // API responses for a given API call. This is typically due to network unreliability. 47 | type AttestationRecreationErr struct { 48 | Msg string 49 | } 50 | 51 | func (e *AttestationRecreationErr) Error() string { 52 | return e.Msg 53 | } 54 | 55 | // SimpleHTTPSGetter implements the HTTPSGetter interface with http.Get. 56 | type SimpleHTTPSGetter struct{} 57 | 58 | // Get uses http.Get to return the HTTPS response body as a byte array. 59 | func (n *SimpleHTTPSGetter) Get(url string) (map[string][]string, []byte, error) { 60 | return n.GetContext(context.TODO(), url) 61 | } 62 | 63 | // GetContext behaves like Get but forwards the context to the http package. 64 | func (n *SimpleHTTPSGetter) GetContext(ctx context.Context, url string) (map[string][]string, []byte, error) { 65 | var header map[string][]string 66 | 67 | req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) 68 | if err != nil { 69 | return nil, nil, err 70 | } 71 | resp, err := http.DefaultClient.Do(req) 72 | if err != nil { 73 | return nil, nil, err 74 | } else if resp.StatusCode >= 300 { 75 | return nil, nil, fmt.Errorf("failed to retrieve %s, status code received %d", url, resp.StatusCode) 76 | } 77 | 78 | header = resp.Header 79 | 80 | body, err := io.ReadAll(resp.Body) 81 | if err != nil { 82 | return nil, nil, err 83 | } 84 | resp.Body.Close() 85 | return header, body, nil 86 | } 87 | 88 | // RetryHTTPSGetter is a meta-HTTPS getter that will retry on failure a given number of times. 89 | type RetryHTTPSGetter struct { 90 | // Timeout is how long to retry before failure. If Timeout is zero, Get will retry indefinitely 91 | // and GetContext will retry until the context expires. 92 | Timeout time.Duration 93 | // MaxRetryDelay is the maximum amount of time to wait between retries. 94 | MaxRetryDelay time.Duration 95 | // Getter is the non-retrying way of getting a URL. 96 | Getter HTTPSGetter 97 | } 98 | 99 | // Get fetches the body of the URL, retrying a given amount of times on failure. 100 | func (n *RetryHTTPSGetter) Get(url string) (map[string][]string, []byte, error) { 101 | return n.GetContext(context.TODO(), url) 102 | } 103 | 104 | // GetContext behaves like Get but forwards the context to the embedded Getter. 105 | func (n *RetryHTTPSGetter) GetContext(ctx context.Context, url string) (map[string][]string, []byte, error) { 106 | delay := 2 * time.Second 107 | cancel := func() {} 108 | if n.Timeout > 0 { 109 | ctx, cancel = context.WithTimeout(ctx, n.Timeout) 110 | } 111 | 112 | for { 113 | header, body, err := GetWith(ctx, n.Getter, url) 114 | if err == nil { 115 | cancel() 116 | return header, body, nil 117 | } 118 | delay = delay + delay 119 | if delay > n.MaxRetryDelay { 120 | delay = n.MaxRetryDelay 121 | } 122 | select { 123 | case <-ctx.Done(): 124 | cancel() 125 | return nil, nil, ctx.Err() 126 | case <-time.After(delay): // wait to retry 127 | } 128 | } 129 | } 130 | 131 | // DefaultHTTPSGetter returns the library's default getter implementation. It will 132 | // retry slowly. 133 | func DefaultHTTPSGetter() HTTPSGetter { 134 | return &RetryHTTPSGetter{ 135 | Timeout: 2 * time.Minute, 136 | MaxRetryDelay: 30 * time.Second, 137 | Getter: &SimpleHTTPSGetter{}, 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /testing/mocks.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Package testing defines the mock tdx-guest device 16 | package testing 17 | 18 | import ( 19 | "fmt" 20 | "strings" 21 | 22 | labi "github.com/google/go-tdx-guest/client/linuxabi" 23 | ) 24 | 25 | // GetReportResponse represents a mocked response to a command request. 26 | type GetReportResponse struct { 27 | Resp labi.TdxReportReq 28 | EsResult labi.EsResult 29 | } 30 | 31 | // GetQuoteResponse represents a mocked response to a command request. 32 | type GetQuoteResponse struct { 33 | Resp labi.TdxQuoteHdr 34 | EsResult labi.EsResult 35 | } 36 | 37 | // Device represents a fake tdx-guest device with pre-programmed responses. 38 | type Device struct { 39 | isOpen bool 40 | reportResponse map[[labi.TdReportDataSize]byte]any 41 | quoteResponse map[[labi.TdReportSize]byte]any 42 | } 43 | 44 | // Match returns true if both errors match expectations closely enough 45 | func Match(got error, want string) bool { 46 | if got == nil { 47 | return want == "" 48 | } 49 | return strings.Contains(got.Error(), want) 50 | } 51 | 52 | // Open changes the mock device's state to open. 53 | func (d *Device) Open(_ string) error { 54 | if d.isOpen { 55 | return fmt.Errorf("device is already open") 56 | } 57 | d.isOpen = true 58 | return nil 59 | } 60 | 61 | // Close changes the mock device's state to close. 62 | func (d *Device) Close() error { 63 | if !d.isOpen { 64 | return fmt.Errorf("device is already closed") 65 | } 66 | d.isOpen = false 67 | return nil 68 | } 69 | 70 | func (d *Device) getReport(req *labi.TdxReportReq) (uintptr, error) { 71 | tdReportRespI, ok := d.reportResponse[req.ReportData] 72 | if !ok { 73 | return 0, fmt.Errorf("test error: no response for %v", req.ReportData) 74 | } 75 | tdReportResp, ok := tdReportRespI.(*GetReportResponse) 76 | if !ok { 77 | return 0, fmt.Errorf("test error: incorrect response for %v", tdReportRespI) 78 | } 79 | esResult := uintptr(tdReportResp.EsResult) 80 | req.TdReport = tdReportResp.Resp.TdReport 81 | return esResult, nil 82 | } 83 | 84 | func (d *Device) getQuote(req *labi.TdxQuoteHdr) (uintptr, error) { 85 | var report [labi.TdReportSize]byte 86 | copy(report[:], req.Data[:labi.TdReportSize]) 87 | quoteRespI, ok := d.quoteResponse[report] 88 | if !ok { 89 | return 0, fmt.Errorf("test error: no response for %v", report) 90 | } 91 | 92 | quoteResp, ok := quoteRespI.(*GetQuoteResponse) 93 | if !ok { 94 | return 0, fmt.Errorf("test error: incorrect response for %v", quoteRespI) 95 | } 96 | esResult := uintptr(quoteResp.EsResult) 97 | copy(req.Data[:], quoteResp.Resp.Data[:]) 98 | req.OutLen = quoteResp.Resp.OutLen 99 | return esResult, nil 100 | } 101 | 102 | // Ioctl mocks commands with pre-specified responses for a finite number of requests. 103 | func (d *Device) Ioctl(command uintptr, req any) (uintptr, error) { 104 | switch sreq := req.(type) { 105 | case *labi.TdxQuoteReq: 106 | switch command { 107 | case labi.IocTdxGetQuote: 108 | return d.getQuote(sreq.Buffer.(*labi.TdxQuoteHdr)) 109 | default: 110 | return 0, fmt.Errorf("unexpected request value: %v", req) 111 | } 112 | case *labi.TdxReportReq: 113 | switch command { 114 | case labi.IocTdxGetReport: 115 | return d.getReport(sreq) 116 | default: 117 | return 0, fmt.Errorf("unexpected request value: %v", req) 118 | } 119 | } 120 | return 0, fmt.Errorf("unexpected request value: %v", req) 121 | } 122 | 123 | // HTTPResponse represents structure for containing header and body 124 | type HTTPResponse struct { 125 | Header map[string][]string 126 | Body []byte 127 | } 128 | 129 | // Getter represents a static server for request/respond url -> body contents. 130 | type Getter struct { 131 | Responses map[string]HTTPResponse 132 | } 133 | 134 | // Get returns a registered response for a given URL. 135 | func (g *Getter) Get(url string) (map[string][]string, []byte, error) { 136 | v, ok := g.Responses[url] 137 | if !ok { 138 | return nil, nil, fmt.Errorf("404: %s", url) 139 | } 140 | return v.Header, v.Body, nil 141 | } 142 | 143 | // TdxQuoteProvider represents a fake quote provider with pre-programmed responses. 144 | type TdxQuoteProvider struct { 145 | isSupported bool 146 | rawQuoteResponse map[[labi.TdReportDataSize]byte][]uint8 147 | } 148 | 149 | // IsSupported checks if mock quote provider is supported. 150 | func (p *TdxQuoteProvider) IsSupported() error { 151 | if p.isSupported { 152 | return nil 153 | } 154 | return fmt.Errorf("configfs not supported") 155 | } 156 | 157 | // GetRawQuote returns pre-programmed response as raw quote. 158 | func (p *TdxQuoteProvider) GetRawQuote(reportData [64]byte) ([]uint8, error) { 159 | return p.rawQuoteResponse[reportData], nil 160 | } 161 | -------------------------------------------------------------------------------- /testing/testdata/README.md: -------------------------------------------------------------------------------- 1 | # `testdata` 2 | This folder contains embedded files that serve as sample API responses, 3 | intended for testing purposes. These responses can be used to stimulate the 4 | behavior of Intel PCS APIs without actually making network access. This folder 5 | also contains a sample TDX quote version 4 which is only used for testing purpose. 6 | 7 | 8 | ## Files 9 | 10 | ### `pckcrl` 11 | 12 | This file serves as sample for Intel PCS API response for PCK certificate 13 | Revocation List. This response is specifically designed to check whether a PCK 14 | certificate is revoked or not. 15 | This sample API follows a structure similar to 16 | `https://api.trustedservices.intel.com/sgx/certification/v4/pckcrl?ca=platform&encoding=der` 17 | 18 | 19 | ### `sample_tcbInfo_response` 20 | 21 | This file serves as sample for Intel PCS API response to retrieve TDX TCB 22 | information for given FMSPC. This response helps in determining the status of a 23 | TDX TCB level for a given platform needs to be done using TDX TCB information 24 | according to the following algorithm: 25 | 26 | 1. Retrieve FMSPC value from SGX PCK Certificate assigned to a given platform. 27 | 28 | 2. Retrieve TDX TCB Info matching the FMSPC value. 29 | 30 | 3. Go over the sorted collection of TCB Levels retrieved from TCB Info starting 31 | from the first item on the list: 32 | 33 | a. Compare all of the SGX TCB Comp SVNs retrieved from the SGX PCK Certificate 34 | (from 01 to 16) with the corresponding values of SVNs in sgxtcbcomponents 35 | array of TCB Level. If all SGX TCB Comp SVNs in the certificate are greater 36 | or equal to the corresponding values in TCB Level, go to 3.b, otherwise move 37 | to the next item on TCB Levels list. 38 | 39 | b. Compare PCESVN value retrieved from the SGX PCK certificate with the 40 | corresponding value in the TCB Level. If it is greater or equal to the 41 | value in TCB Level, go to 3.c, otherwise move to the next item on TCB 42 | Levels list. 43 | 44 | c. Compare all of the SVNs in TEE TCB SVN array retrieved from TD Report in 45 | Quote (from index 0 to 15) with the corresponding values of SVNs in 46 | tdxtcbcomponents array of TCB Level. If all TEE TCB SVNs in the TD Report 47 | are greater or equal to the corresponding values in TCB Level, read status 48 | assigned to this TCB level. Otherwise, move to the next item on TCB Levels 49 | list. 50 | 51 | 4. For the selected TCB level verify that SVN at index 1 in tdxtcbcomponents 52 | array matches the value of SVN at index 1 in TEE TCB SVNs array (from TD Report 53 | in Quote). In case of a mismatch the selected TCB level should be rejected as 54 | TCB Info that was used for the comparison is not supported for this platform 55 | configuration. 56 | 57 | 5. If no TCB level matches the SGX PCK Certificate and TD Report, then the TCB 58 | level is not supported. 59 | 60 | This sample API follows a structure similar to 61 | `https://api.trustedservices.intel.com/tdx/certification/v4/tcb?fmspc=50806f000000` 62 | 63 | 64 | ### `sample_qeIdentity_response` 65 | 66 | This file serves as sample for Intel PCS API response to retrieve QE identity 67 | response. This response helps in determining if the identity of a SGX Enclave 68 | (represented by SGX Enclave Report) matches a valid, up-to-date Enclave Identity 69 | issued by Intel requires following steps: 70 | 71 | 1. Retrieve Enclave Identity(TDX QE) from PCS and verify that it is a valid 72 | structure issued by Intel. 73 | 74 | 2. Perform the following comparison of SGX Enclave Report against the retrieved 75 | Enclave Identity: 76 | 77 | a. Verify if MRSIGNER field retrieved from SGX Enclave Report is equal to the 78 | value of mrsigner field in Enclave Identity. 79 | 80 | b. Verify if ISVPRODID field retrieved from SGX Enclave Report is equal to the 81 | value of isvprodid field in Enclave Identity. 82 | 83 | c. Apply miscselectMask (binary mask) from Enclave Identity to MISCSELECT 84 | field retrieved from SGX Enclave Report. Verify if the outcome 85 | (miscselectMask & MISCSELECT) is equal to the value of miscselect field in 86 | Enclave Identity. 87 | 88 | d. Apply attributesMask (binary mask) from Enclave Identity to ATTRIBUTES 89 | field retrieved from SGX Enclave Report. Verify if the outcome (attributesMask 90 | & ATTRIBUTES) is equal to the value of attributes field in Enclave Identity. 91 | 92 | 3. If any of the checks above fail, the identity of the enclave does not match 93 | Enclave Identity published by Intel. 94 | 95 | 4. Determine a TCB status of the Enclave: 96 | 97 | a. Retrieve a collection of TCB Levels (sorted by ISVSVNs) from tcbLevels field 98 | in Enclave Identity structure. 99 | 100 | b. Go over the list of TCB Levels (descending order) and find the one that has 101 | ISVSVN that is lower or equal to the ISVSVN value from SGX Enclave Report. 102 | 103 | c. If a TCB level is found, read its status from tcbStatus field, otherwise 104 | the TCB Level is not supported. 105 | 106 | This sample API follows a structure similar to 107 | `https://api.trustedservices.intel.com/tdx/certification/v4/qe/identity` 108 | 109 | 110 | ### `rootcrl.der` 111 | 112 | This file serves as sample for Intel PCS API response for Root certificate 113 | Revocation List. This response is specifically designed to check whether a Root 114 | certificate is revoked or not. 115 | This sample API follows a structure similar to 116 | `https://certificates.trustedservices.intel.com/IntelSGXRootCA.der` 117 | -------------------------------------------------------------------------------- /tools/attest/attest.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Package main implements a CLI tool for collecting attestation reports. 16 | package main 17 | 18 | import ( 19 | "encoding/base64" 20 | "encoding/hex" 21 | "flag" 22 | "fmt" 23 | "io" 24 | "os" 25 | "strings" 26 | "unicode/utf8" 27 | 28 | "github.com/google/go-tdx-guest/client" 29 | labi "github.com/google/go-tdx-guest/client/linuxabi" 30 | pb "github.com/google/go-tdx-guest/proto/tdx" 31 | "github.com/google/logger" 32 | "google.golang.org/protobuf/encoding/prototext" 33 | ) 34 | 35 | var ( 36 | outform = flag.String("outform", "bin", 37 | "The format of the output attestation report. "+ 38 | "One of \"bin\", \"textproto\".") 39 | reportDataStr = flag.String("in", "", 40 | "A string of 64 bytes REPORT_DATA to include in the output attestation. "+ 41 | "REPORT_DATA can be either in base64 or hex format. If -inform=auto, first check with base64, hex and last with auto.") 42 | inform = flag.String("inform", "auto", "The format of the reportData input. One of base64, hex and auto. "+ 43 | "If -inform=auto, first check with base64 and last with hex.") 44 | out = flag.String("out", "", "Path to output file to write attestation report to. "+ 45 | "If unset, outputs to stdout.") 46 | verbose = flag.Bool("v", false, "Enable verbose logging.") 47 | verbosity = flag.Int("verbosity", 0, "The output verbosity. Higher number means more verbose output") 48 | ) 49 | 50 | func outputReport(data [labi.TdReportDataSize]byte, out io.Writer) error { 51 | tdxQuoteProvider, err := client.GetQuoteProvider() 52 | if err != nil { 53 | return err 54 | } 55 | if *outform == "bin" { 56 | bytes, err := client.GetRawQuote(tdxQuoteProvider, data) 57 | if err != nil { 58 | return err 59 | } 60 | if _, err := out.Write(bytes); err != nil { 61 | return err 62 | } 63 | return nil 64 | } 65 | quote, err := client.GetQuote(tdxQuoteProvider, data) 66 | if err != nil { 67 | return err 68 | } 69 | return marshalAndWriteBytes(quote, out) 70 | } 71 | 72 | func marshalAndWriteBytes(quote any, out io.Writer) error { 73 | switch q := quote.(type) { 74 | case *pb.QuoteV4: 75 | bytes, err := prototext.Marshal(q) 76 | if err != nil { 77 | return err 78 | } 79 | if _, err := out.Write(bytes); err != nil { 80 | return err 81 | } 82 | return nil 83 | default: 84 | return fmt.Errorf("unsupported quote type: %T", quote) 85 | } 86 | } 87 | 88 | func outWriter() (io.Writer, *os.File, error) { 89 | if *out == "" { 90 | return os.Stdout, nil, nil 91 | } 92 | file, err := os.Create(*out) 93 | if err != nil { 94 | return nil, nil, err 95 | } 96 | return file, file, nil 97 | } 98 | func sizedBytes(flag, value string, byteSize int, decode func(string) ([]byte, error)) ([]byte, error) { 99 | bytes, err := decode(value) 100 | if err != nil { 101 | return nil, fmt.Errorf("%s=%s could not be decoded: %v", flag, value, err) 102 | } 103 | if len(bytes) > byteSize { 104 | return nil, fmt.Errorf("%s=%s (%v) is not representable in %d bytes", flag, value, bytes, byteSize) 105 | } 106 | sized := make([]byte, byteSize) 107 | copy(sized, bytes) 108 | return sized, nil 109 | } 110 | func parseBytes(name string, in io.Reader, inform string, byteSize int) ([]byte, error) { 111 | inbytes, err := io.ReadAll(in) 112 | if err != nil { 113 | return nil, err 114 | } 115 | if len(inbytes) == 0 { 116 | return nil, nil 117 | } 118 | inByteStr := strings.TrimSpace(string(inbytes)) 119 | if !utf8.ValidString(inByteStr) { 120 | return nil, fmt.Errorf("could not decode %s contents as a UTF-8 string", name) 121 | } 122 | switch inform { 123 | case "base64": 124 | return sizedBytes(name, inByteStr, byteSize, base64.StdEncoding.DecodeString) 125 | case "hex": 126 | return sizedBytes(name, inByteStr, byteSize, hex.DecodeString) 127 | case "auto": 128 | // "auto" means to try base64 encoding first, then hex. 129 | if b, err := sizedBytes(name, inByteStr, byteSize, base64.StdEncoding.DecodeString); err == nil { 130 | return b, nil 131 | } 132 | return sizedBytes(name, inByteStr, byteSize, hex.DecodeString) 133 | default: 134 | return nil, fmt.Errorf("-inform should be either base64 or hex") 135 | } 136 | } 137 | func main() { 138 | flag.Parse() 139 | logger.Init("", *verbose, false, os.Stderr) 140 | logger.SetLevel(logger.Level(*verbosity)) 141 | reportData, err := parseBytes("-in", strings.NewReader(*reportDataStr), *inform, labi.TdReportDataSize) 142 | if err != nil { 143 | logger.Fatal(err) 144 | } 145 | if !(*outform == "bin" || *outform == "textproto") { 146 | logger.Fatalf("-outform is %s. Expect \"bin\" or \"textproto\"", *outform) 147 | } 148 | outwriter, filetoclose, err := outWriter() 149 | if err != nil { 150 | logger.Fatalf("failed to open output file: %v", err) 151 | } 152 | defer func() { 153 | if filetoclose != nil { 154 | filetoclose.Close() 155 | } 156 | }() 157 | var reportData64 [labi.TdReportDataSize]byte 158 | copy(reportData64[:], reportData) 159 | if err := outputReport(reportData64, outwriter); err != nil { 160 | logger.Fatal(err) 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /client/client.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Package client provides the library functions to get a TDX quote 16 | // from the TDX guest device 17 | package client 18 | 19 | import ( 20 | "flag" 21 | "fmt" 22 | "os" 23 | 24 | "github.com/google/go-tdx-guest/abi" 25 | labi "github.com/google/go-tdx-guest/client/linuxabi" 26 | "github.com/google/logger" 27 | ) 28 | 29 | var tdxGuestPath = flag.String("tdx_guest_device_path", "default", 30 | "Path to TDX guest device. If \"default\", uses platform default or a fake if testing.") 31 | 32 | // Device encapsulates the possible commands to the TDX guest device. 33 | // Deprecated: The Device interface is deprecated, and use of quote provider interface is 34 | // recommended for fetching attestation quote. 35 | type Device interface { 36 | Open(path string) error 37 | Close() error 38 | Ioctl(command uintptr, argument any) (uintptr, error) 39 | } 40 | 41 | // QuoteProvider encapsulates calls to attestation quote. 42 | type QuoteProvider interface { 43 | IsSupported() error 44 | GetRawQuote(reportData [64]byte) ([]uint8, error) 45 | } 46 | 47 | // UseDefaultTdxGuestDevice returns true if tdxGuestPath=default. 48 | func UseDefaultTdxGuestDevice() bool { 49 | return *tdxGuestPath == "default" 50 | } 51 | 52 | // getReport requests for tdx report by making an ioctl call. 53 | func getReport(d Device, reportData [64]byte) ([]uint8, error) { 54 | tdxReportReq := labi.TdxReportReq{} 55 | copy(tdxReportReq.ReportData[:], reportData[:]) 56 | result, err := d.Ioctl(labi.IocTdxGetReport, &tdxReportReq) 57 | if err != nil { 58 | return nil, err 59 | } 60 | if result != uintptr(labi.TdxAttestSuccess) { 61 | return nil, fmt.Errorf("unable to get the report: %d", result) 62 | } 63 | return tdxReportReq.TdReport[:], nil 64 | } 65 | 66 | // GetRawQuote uses Quote provider or Device(deprecated) to get the quote in byte array. 67 | func GetRawQuote(quoteProvider any, reportData [64]byte) ([]uint8, error) { 68 | switch qp := quoteProvider.(type) { 69 | case Device: 70 | return getRawQuoteViaDevice(qp, reportData) 71 | case QuoteProvider: 72 | return getRawQuoteViaProvider(qp, reportData) 73 | default: 74 | return nil, fmt.Errorf("unsupported quote provider type: %T", quoteProvider) 75 | } 76 | } 77 | 78 | // getRawQuoteViaDevice uses TDX device driver to call getReport for report and convert it to 79 | // quote using an ioctl call. 80 | func getRawQuoteViaDevice(d Device, reportData [64]byte) ([]uint8, error) { 81 | logger.V(1).Info("Get raw TDX quote via Device") 82 | tdReport, err := getReport(d, reportData) 83 | if err != nil { 84 | return nil, err 85 | } 86 | tdxHdr := &labi.TdxQuoteHdr{ 87 | Status: 0, 88 | Version: 1, 89 | InLen: labi.TdReportSize, 90 | OutLen: 0, 91 | } 92 | copy(tdxHdr.Data[:], tdReport[:labi.TdReportSize]) 93 | tdxReq := labi.TdxQuoteReq{ 94 | Buffer: tdxHdr, 95 | Length: labi.ReqBufSize, 96 | } 97 | result, err := d.Ioctl(labi.IocTdxGetQuote, &tdxReq) 98 | if err != nil { 99 | return nil, err 100 | } 101 | if result != uintptr(labi.TdxAttestSuccess) { 102 | return nil, fmt.Errorf("unable to get the quote") 103 | } 104 | if tdxHdr.Status != 0 { 105 | if labi.GetQuoteInFlight == tdxHdr.Status { 106 | return nil, fmt.Errorf("the device driver return busy") 107 | } else if labi.GetQuoteServiceUnavailable == tdxHdr.Status { 108 | return nil, fmt.Errorf("request feature is not supported") 109 | } else if tdxHdr.OutLen == 0 || tdxHdr.OutLen > labi.ReqBufSize { 110 | return nil, fmt.Errorf("invalid Quote size: %v. It must be > 0 and <= : %v", tdxHdr.OutLen, labi.ReqBufSize) 111 | } 112 | 113 | return nil, fmt.Errorf("unexpected error: %v", tdxHdr.Status) 114 | } 115 | 116 | return tdxHdr.Data[:tdxHdr.OutLen], nil 117 | } 118 | 119 | // getRawQuoteViaProvider use QuoteProvider to fetch quote in byte array format. 120 | func getRawQuoteViaProvider(qp QuoteProvider, reportData [64]byte) ([]uint8, error) { 121 | if err := qp.IsSupported(); err == nil { 122 | logger.V(1).Info("Get raw TDX quote via QuoteProvider") 123 | quote, err := qp.GetRawQuote(reportData) 124 | return quote, err 125 | } 126 | return fallbackToDeviceForRawQuote(reportData) 127 | } 128 | 129 | // GetQuote uses Quote provider or Device(deprecated) to get the quote in byte array and convert it 130 | // into proto. 131 | // Supported quote formats - QuoteV4. 132 | func GetQuote(quoteProvider any, reportData [64]byte) (any, error) { 133 | quotebytes, err := GetRawQuote(quoteProvider, reportData) 134 | if err != nil { 135 | return nil, err 136 | } 137 | quote, err := abi.QuoteToProto(quotebytes) 138 | if err != nil { 139 | return nil, err 140 | } 141 | return quote, nil 142 | } 143 | 144 | // fallbackToDeviceForRawQuote opens tdx_guest device to fetch raw quote. 145 | func fallbackToDeviceForRawQuote(reportData [64]byte) ([]uint8, error) { 146 | // Fall back to TDX device driver. 147 | device, err := OpenDevice() 148 | if err != nil { 149 | return nil, fmt.Errorf("neither TDX device, nor ConfigFs is available to fetch attestation quote") 150 | } 151 | bytes, err := getRawQuoteViaDevice(device, reportData) 152 | device.Close() 153 | return bytes, err 154 | } 155 | 156 | func init() { 157 | logger.Init("", false, false, os.Stdout) 158 | } 159 | -------------------------------------------------------------------------------- /rtmr/extend.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Package rtmr provides the library functions: 16 | // 1. extend and read TDX rtmr registers and their tcg maps. 17 | // 2. replay the event log with the TDX quote. 18 | package rtmr 19 | 20 | import ( 21 | "crypto" 22 | _ "crypto/sha512" // Register SHA384 and SHA512 23 | "fmt" 24 | "os" 25 | 26 | "github.com/google/go-configfs-tsm/configfs/configfsi" 27 | "github.com/google/go-configfs-tsm/configfs/linuxtsm" 28 | "github.com/google/go-configfs-tsm/rtmr" 29 | ) 30 | 31 | const ( 32 | // Defines the legacy rtmr file path. 33 | legacyRTMRPath = "/sys/kernel/config/tsm/rtmrs" 34 | ) 35 | 36 | // ExtendDigestSysfs extends the measurement to the rtmr through the sysfs interface. 37 | // This is the modern method for extending TDX rtmr. 38 | func ExtendDigestSysfs(rtmrIndex int, digest []byte) error { 39 | if rtmrIndex < 0 || rtmrIndex > 3 { 40 | return fmt.Errorf("invalid rtmr index %d. For TDX, index can only be 0-3", rtmrIndex) 41 | } 42 | if len(digest) != crypto.SHA384.Size() { 43 | return fmt.Errorf("sha384 digest should be %d bytes, the input is %d bytes", crypto.SHA384.Size(), len(digest)) 44 | } 45 | // The sysfs file is an interface for rtmr. Each write operation functions 46 | // as an "extend" operation, not a normal file write. 47 | filePath := fmt.Sprintf("/sys/class/misc/tdx_guest/measurements/rtmr%d:sha384", rtmrIndex) 48 | file, err := os.OpenFile(filePath, os.O_WRONLY, 0) 49 | if err != nil { 50 | return fmt.Errorf("failed to open TDX RTMR file %s: %w", filePath, err) 51 | } 52 | defer file.Close() 53 | 54 | if _, err = file.WriteAt(digest, 0); err != nil { 55 | return fmt.Errorf("failed to write data to %s at offset 0: %w", filePath, err) 56 | } 57 | return nil 58 | } 59 | 60 | // ExtendDigestClient extends the measurement to the rtmr with the given client. 61 | func ExtendDigestClient(client configfsi.Client, rtmrIndex int, digest []byte) error { 62 | if rtmrIndex < 0 || rtmrIndex > 3 { 63 | return fmt.Errorf("invalid rtmr index %d. For TDX, index can only be 0-3", rtmrIndex) 64 | } 65 | if len(digest) != crypto.SHA384.Size() { 66 | return fmt.Errorf("sha384 digest should be %d bytes, the input is %d bytes", crypto.SHA384.Size(), len(digest)) 67 | } 68 | // TODO: check the TCG mapping of the rtmr index 69 | return rtmr.ExtendDigest(client, rtmrIndex, digest) 70 | } 71 | 72 | // ExtendEventLogSysfs extends the event log to the rtmr through the sysfs interface. 73 | func ExtendEventLogSysfs(rtmrIndex int, hashAlgo crypto.Hash, eventLog []byte) error { 74 | if hashAlgo != crypto.SHA384 { 75 | return fmt.Errorf("unsupported hash algorithm %v", hashAlgo) 76 | } 77 | if len(eventLog) == 0 { 78 | return fmt.Errorf("input event log is empty") 79 | } 80 | 81 | sha384 := hashAlgo.New() 82 | sha384.Write(eventLog) 83 | hash := sha384.Sum(nil) 84 | return ExtendDigestSysfs(rtmrIndex, hash) 85 | } 86 | 87 | // ExtendEventLogClient extends the event log to the rtmr with the given client. 88 | func ExtendEventLogClient(client configfsi.Client, rtmrIndex int, hashAlgo crypto.Hash, eventLog []byte) error { 89 | if hashAlgo != crypto.SHA384 { 90 | return fmt.Errorf("unsupported hash algorithm %v", hashAlgo) 91 | } 92 | if len(eventLog) == 0 { 93 | return fmt.Errorf("input event log is empty") 94 | } 95 | sha384 := hashAlgo.New() 96 | sha384.Write(eventLog) 97 | hash := sha384.Sum(nil) 98 | return ExtendDigestClient(client, rtmrIndex, hash) 99 | } 100 | 101 | // ExtendEventLog extends the measurement into the rtmr with the given hash algorithm and event log. 102 | // It checks for the existence of the legacy configfs path to decide whether to use the client or sysfs method. 103 | func ExtendEventLog(rtmrIndex int, hashAlgo crypto.Hash, eventLog []byte) error { 104 | // Check if the legacy configfs path exists to determine the extension method. 105 | _, err := os.Stat(legacyRTMRPath) 106 | if err == nil { 107 | // If the path exists, use the client method. 108 | client, err := linuxtsm.MakeClient() 109 | if err != nil { 110 | return err 111 | } 112 | return ExtendEventLogClient(client, rtmrIndex, hashAlgo, eventLog) 113 | } else if os.IsNotExist(err) { 114 | // If the configfs path does not exist, use the sysfs interface instead. 115 | return ExtendEventLogSysfs(rtmrIndex, hashAlgo, eventLog) 116 | } 117 | // Handle other unexpected errors from os.Stat. 118 | return fmt.Errorf("failed to check for directory %s: %w", legacyRTMRPath, err) 119 | } 120 | 121 | // ExtendDigest extends the measurement into the rtmr with the given digest. 122 | // It checks for the existence of the legacy configfs path to decide whether to use the client or sysfs method. 123 | func ExtendDigest(rtmrIndex int, digest []byte) error { 124 | // Check if the legacy configfs path exists to determine the extension method. 125 | _, err := os.Stat(legacyRTMRPath) 126 | if err == nil { 127 | // If the path exists, use the client method. 128 | client, err := linuxtsm.MakeClient() 129 | if err != nil { 130 | return err 131 | } 132 | return ExtendDigestClient(client, rtmrIndex, digest) 133 | } else if os.IsNotExist(err) { 134 | // If the configs path does not exist, use the sysfs interface instead. 135 | return ExtendDigestSysfs(rtmrIndex, digest) 136 | } 137 | // Handle other unexpected errors from os.Stat. 138 | return fmt.Errorf("failed to check for directory %s: %w", legacyRTMRPath, err) 139 | } 140 | -------------------------------------------------------------------------------- /proto/tdx.proto: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | syntax = "proto3"; 16 | 17 | // Package tdx represents a TDX attestation quote. 18 | package tdx; 19 | 20 | option go_package = "github.com/google/go-tdx-guest/proto/tdx"; 21 | 22 | message QuoteV4 { 23 | // Header of quote structure 24 | Header header = 1; // should be 48 bytes 25 | 26 | // TD report by which quote is generated 27 | TDQuoteBody td_quote_body = 2; // should be 584 bytes 28 | 29 | // Size of the Quote Signature Data structure 30 | uint32 signed_data_size = 3; 31 | 32 | // Quote Signature Data structure. 33 | Ecdsa256BitQuoteV4AuthData signed_data = 34 | 4; // The size should be same as signed_data_size 35 | 36 | // Extra bytes included to fill the quote buffer 37 | bytes extra_bytes = 5; // These trailing bytes can be ignored 38 | } 39 | 40 | message Header { 41 | // Version 4 supported 42 | uint32 version = 1; 43 | 44 | // Type of attestation key used by quoting enclave 45 | // Values : 46 | // 2 (ECDSA-256-with-P-256 curve) 47 | // 3 (ECDSA-384-with-P-384 curve) (Currently not supported) 48 | uint32 attestation_key_type = 2; 49 | 50 | // TEE for this attestation 51 | // TDX : 0x00000081 52 | uint32 tee_type = 3; 53 | 54 | bytes qe_svn = 4; // should be 2 bytes 55 | bytes pce_svn = 5; // should be 2 bytes 56 | 57 | // Unique vendor id of QE vendor 58 | bytes qe_vendor_id = 6; // should be 16 bytes 59 | 60 | // Custom user defined data 61 | bytes user_data = 7; // should be 20 bytes 62 | } 63 | 64 | message TDQuoteBody { 65 | bytes tee_tcb_svn = 1; // should be 16 bytes 66 | bytes mr_seam = 2; // should be 48 bytes 67 | bytes mr_signer_seam = 3; // should be 48 bytes 68 | bytes seam_attributes = 4; // should be 8 bytes 69 | bytes td_attributes = 5; // should be 8 bytes 70 | bytes xfam = 6; // should be 8 bytes 71 | bytes mr_td = 7; // should be 48 bytes 72 | bytes mr_config_id = 8; // should be 48 bytes 73 | bytes mr_owner = 9; // should be 48 bytes 74 | bytes mr_owner_config = 10; // should be 48 bytes 75 | repeated bytes rtmrs = 11; // should be 48 * rtmrsCount 76 | bytes report_data = 12; // should be 64 bytes 77 | } 78 | 79 | message Ecdsa256BitQuoteV4AuthData { 80 | // The ECDSA 256-bit signature. 81 | bytes signature = 1; // should be 64 bytes 82 | 83 | // The ECDSA 256-bit public key of the attestation key. 84 | bytes ecdsa_attestation_key = 2; // should be 64 bytes 85 | 86 | // The certification data. 87 | CertificationData certification_data = 3; 88 | } 89 | 90 | message CertificationData { 91 | // Supported values: 92 | // - 1 (PCK identifier: PPID in plain text, CPUSVN and PCESVN) 93 | // - 2 (PCK identifier: PPID encrypted using RSA-2048-OAEP, CPUSVN and PCESVN) 94 | // - 3 (PCK identifier: PPID encrypted using RSA-3072-OAEP, CPUSVN and PCESVN) 95 | // - 4 (PCK Leaf Certificate in plain text, currently not supported) 96 | // - 5 (Concatenated PCK Cert Chain) 97 | // - 6 (QE Report Certification Data) 98 | // - 7 (PLATFORM_MANIFEST, currently not supported) 99 | uint32 certificate_data_type = 1; 100 | 101 | // Size of Certification Data field 102 | uint32 size = 2; 103 | 104 | // Certification Data Type: 105 | // - 1: Byte array that contains concatenation of PPID, CPUSVN,PCESVN (LE), 106 | // PCEID (LE). 107 | // - 2: Byte array that contains concatenation of PPID encrypted using 108 | // RSA-2048-OAEP, CPUSVN, PCESVN (LE), PCEID (LE). 109 | // - 3: Byte array that contains concatenation of PPID encrypted using 110 | // RSA-3072-OAEP, CPUSVN, PCESVN (LE), PCEID (LE). 111 | // - 4: PCK Leaf Certificate 112 | // - 5: Concatenated PCK Cert Chain (PEM formatted). PCK LeafCert|| 113 | // Intermediate CA Cert|| Root CA Cert 114 | //- 6: QE Report Certification Data 115 | //- 7: PLATFORM_MANIFEST (currently not supported) 116 | 117 | QEReportCertificationData qe_report_certification_data = 3; 118 | } 119 | 120 | message QEReportCertificationData { 121 | EnclaveReport qe_report = 1; 122 | bytes qe_report_signature = 2; // should be 64 bytes 123 | QeAuthData qe_auth_data = 3; 124 | PCKCertificateChainData pck_certificate_chain_data = 4; 125 | } 126 | message PCKCertificateChainData { 127 | // Supported values: 128 | // - 1 (PCK identifier: PPID in plain text, CPUSVN and PCESVN) 129 | // - 2 (PCK identifier: PPID encrypted using RSA-2048-OAEP, CPUSVN and PCESVN) 130 | // - 3 (PCK identifier: PPID encrypted using RSA-3072-OAEP, CPUSVN and PCESVN) 131 | // - 4 (PCK Leaf Certificate in plain text, currently not supported) 132 | // - 5 (Concatenated PCK Cert Chain) 133 | // - 6 (QE Report Certification Data) 134 | // - 7 (PLATFORM_MANIFEST, currently not supported) 135 | uint32 certificate_data_type = 1; 136 | 137 | // Size of Certification Data field 138 | uint32 size = 2; 139 | bytes pck_cert_chain = 3; 140 | } 141 | 142 | message QeAuthData { 143 | // The parsed data size. 144 | uint32 parsed_data_size = 1; // should be 2 bytes 145 | // The data. 146 | bytes data = 2; 147 | } 148 | 149 | message EnclaveReport { 150 | bytes cpu_svn = 1; // should be 16 bytes 151 | uint32 misc_select = 2; // should be 4 bytes 152 | bytes reserved1 = 3; // should be 28 bytes 153 | bytes attributes = 4; // should be 16 bytes 154 | bytes mr_enclave = 5; // should be 32 bytes 155 | bytes reserved2 = 6; // should be 32 bytes 156 | bytes mr_signer = 7; // should be 32 bytes 157 | bytes reserved3 = 8; // should be 96 bytes 158 | uint32 isv_prod_id = 9; // should be 2 bytes 159 | uint32 isv_svn = 10; // should be 2 bytes 160 | bytes reserved4 = 11; // should be 60 bytes 161 | bytes report_data = 12; // should be 64 bytes 162 | } 163 | -------------------------------------------------------------------------------- /client/linuxabi/linux_abi.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Package linuxabi describes the ABI required for the TDX ioctl commands 16 | package linuxabi 17 | 18 | import ( 19 | "fmt" 20 | "reflect" 21 | "unsafe" 22 | ) 23 | 24 | const ( 25 | iocNrbits = 8 26 | iocTypebits = 8 27 | iocSizebits = 14 28 | iocDirbits = 2 29 | iocNrshift = 0 30 | iocTypeshift = (iocNrshift + iocNrbits) 31 | iocSizeshift = (iocTypeshift + iocTypebits) 32 | iocDirshift = (iocSizeshift + iocSizebits) 33 | iocWrite = 1 34 | iocRead = 2 35 | // Linux /dev/tdx-guest ioctl interface 36 | iocTypeTdxGuestReq = 'T' 37 | iocTdxWithoutNrWithoutSize = ((iocWrite | iocRead) << iocDirshift) | 38 | (iocTypeTdxGuestReq << iocTypeshift) 39 | // IocTdxGetReport is the ioctl command for getting an attestation report. 40 | IocTdxGetReport = iocTdxWithoutNrWithoutSize | (unsafe.Sizeof(TdxReportReq{}) << iocSizeshift) | (0x1 << iocNrshift) 41 | // IocTdxGetQuote is the ioctl command for getting an attestation quote. 42 | IocTdxGetQuote = iocTdxWithoutNrWithoutSize | (unsafe.Sizeof(TdxQuoteReqABI{}) << iocSizeshift) | (0x2 << iocNrshift) 43 | // TdReportDataSize is a constant for TDX ReportData size 44 | TdReportDataSize = 64 45 | // TdReportSize is a constant for TDX Report size 46 | TdReportSize = 1024 47 | // HeaderSize is the size of header to serialized quote request 48 | HeaderSize = 4 49 | // ReqBufSize is a constant for serialized Tdx quote response 50 | ReqBufSize = 4 * 4 * 1024 51 | // TdxUUIDSize is a constant for intel TDQE ID 52 | TdxUUIDSize = 16 53 | // GetQuoteReq is a constant for report request 54 | GetQuoteReq = 0 55 | // GetQuoteResp is a constant for report response 56 | GetQuoteResp = 1 57 | ) 58 | 59 | // EsResult is the status code type for Linux's GHCB communication results. 60 | type EsResult int 61 | 62 | // constant for TD quote status code. 63 | const ( 64 | GetQuoteSuccess = 0 65 | GetQuoteInFlight = 0xffffffffffffffff 66 | GetQuoteError = 0x8000000000000000 67 | GetQuoteServiceUnavailable = 0x8000000000000001 68 | ) 69 | const ( 70 | // TdxAttestSuccess denotes success 71 | TdxAttestSuccess = iota 72 | // TdxAttestErrorBusy returns when device driver is busy 73 | TdxAttestErrorBusy = 0x0009 74 | // TdxAttestErrorQuoteFailure denotes failure to get the TD Quote 75 | TdxAttestErrorQuoteFailure = 0x0008 76 | // TdxAttestErrorNotSupported denotes request feature is not supported 77 | TdxAttestErrorNotSupported = 0x0007 78 | // TdxAttestErrorUnexpected denotes Unexpected error 79 | TdxAttestErrorUnexpected = 0x0001 80 | ) 81 | 82 | // TdxReportReq is Linux's tdx-guest ABI for TDX Report. The 83 | // types here enhance runtime safety when using Ioctl as an interface. 84 | type TdxReportReq struct { 85 | /* Report data of 64 bytes */ 86 | ReportData [TdReportDataSize]byte 87 | /* Actual TD Report Data */ 88 | TdReport [TdReportSize]byte 89 | } 90 | 91 | // MsgHeader is used to add header field to serialized request and response message. 92 | type MsgHeader struct { 93 | MajorVersion uint16 94 | MinorVersion uint16 95 | MsgType uint32 96 | Size uint32 // size of the whole message, include this header, in byte 97 | ErrorCode uint32 // used in response only 98 | } 99 | 100 | // SerializedGetQuoteReq is used to serialized the request message to get quote. 101 | type SerializedGetQuoteReq struct { 102 | Header MsgHeader // header.type = GET_QUOTE_REQ 103 | ReportSize uint32 // cannot be 0 104 | IDListSize uint32 // length of id_list, in byte, can be 0 105 | ReportIDList [TdReportSize]uint8 // report followed by id list - [TODO revisit if attestation key ID is included] 106 | } 107 | 108 | // TdxQuoteHdr is Linux's tdx-guest ABI for quote header 109 | type TdxQuoteHdr struct { 110 | /* Quote version, filled by TD */ 111 | Version uint64 112 | /* Status code of Quote request, filled by VMM */ 113 | Status uint64 114 | /* Length of TDREPORT, filled by TD */ 115 | InLen uint32 116 | /* Length of Quote, filled by VMM */ 117 | OutLen uint32 118 | /* Actual Quote data or TDREPORT on input */ 119 | Data [ReqBufSize]byte 120 | } 121 | 122 | // ABI returns the object itself. 123 | func (r *TdxQuoteHdr) ABI() BinaryConversion { return r } 124 | 125 | // Pointer returns a pointer to the object itself. 126 | func (r *TdxQuoteHdr) Pointer() unsafe.Pointer { return unsafe.Pointer(r) } 127 | 128 | // Finish is a no-op. 129 | func (r *TdxQuoteHdr) Finish(BinaryConvertible) error { 130 | return nil 131 | } 132 | 133 | // TdxQuoteReqABI is Linux's tdx-guest ABI for quote response 134 | type TdxQuoteReqABI struct { 135 | Buffer unsafe.Pointer 136 | Length uint64 137 | } 138 | 139 | // TdxQuoteReq is Linux's tdx-guest ABI for TDX Report. The 140 | // types here enhance runtime safety when using Ioctl as an interface. 141 | type TdxQuoteReq struct { 142 | Buffer BinaryConvertible 143 | Length uint64 144 | } 145 | 146 | // ABI returns the object itself. 147 | func (r *TdxQuoteReq) ABI() BinaryConversion { 148 | return &TdxQuoteReqABI{ 149 | Buffer: unsafe.Pointer(r.Buffer.ABI().Pointer()), 150 | Length: r.Length, 151 | } 152 | } 153 | 154 | // Pointer returns a pointer to the object itself. 155 | func (r *TdxQuoteReqABI) Pointer() unsafe.Pointer { return unsafe.Pointer(r) } 156 | 157 | // Finish is a no-op. 158 | func (r *TdxQuoteReqABI) Finish(b BinaryConvertible) error { 159 | _, ok := b.(*TdxQuoteReq) 160 | if !ok { 161 | return fmt.Errorf("Finish argument is %v. Expects a *TdxReportReq", reflect.TypeOf(b)) 162 | } 163 | return nil 164 | } 165 | 166 | // BinaryConversion is an interface that abstracts a "stand-in" object that passes through an ABI 167 | // boundary and can finalize changes to the original object. 168 | type BinaryConversion interface { 169 | Pointer() unsafe.Pointer 170 | Finish(BinaryConvertible) error 171 | } 172 | 173 | // BinaryConvertible is an interface for an object that can produce a partner BinaryConversion 174 | // object to allow its representation to pass the ABI boundary. 175 | type BinaryConvertible interface { 176 | ABI() BinaryConversion 177 | } 178 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TDX Guest 2 | 3 | 4 | This project offers libraries for a simple wrapper around quote providing tools 5 | such as the `go-configfs-tsm` library, or the `/dev/tdx_guest` device in Linux, 6 | as well as a library for attestation verification of fundamental components of 7 | an attestation quote. 8 | 9 | 10 | This project is split into two complementary roles. The first role is producing 11 | an attestation quote, and the second is checking an attestation quote. The 12 | `client` library produces quote, the `verify` library verifies quote's 13 | signatures and certificates. 14 | 15 | 16 | ## `client` 17 | 18 | 19 | This library should be used within the confidential workload to collect an 20 | attestation quote along with requisite certificates. 21 | 22 | 23 | Your main interactions with it will be to first get the quote provider, or 24 | open the device, then get an attestation quote with your provided 64 bytes of 25 | user data (typically a nonce), and then close the device. For convenience, the 26 | attestation with its associated certificates can be collected in a 27 | wire-transmittable protocol buffer format. 28 | 29 | 30 | ### `func GetQuoteProvider() (*LinuxConfigFsQuoteProvider, error)` 31 | 32 | 33 | This function creates an instance of a quote provider which uses the go-configfs-tsm 34 | library to fetch attestation quotes via ConfigFS. 35 | 36 | 37 | ### `func OpenDevice() (*LinuxDevice, error)` 38 | 39 | 40 | This function creates a file descriptor to the `/dev/tdx_guest` device and 41 | returns an object that has methods encapsulating commands to the device. When 42 | done, remember to `Close()` the device. 43 | Note:- The Device interface is deprecated, and use of quote provider interface 44 | is recommended for fetching attestation quote. 45 | 46 | 47 | ### `func GetQuote(quoteProvider any, reportData [64]byte) (any, error)` 48 | 49 | 50 | This function takes an object implementing either the `QuoteProvider` interface 51 | (e.g. `LinuxConfigFsQuoteProvider`), or the `Device` interface (e.g., a `LinuxDevice`) 52 | along with report data which typically consists of a nonce value. 53 | It returns the protocol buffer representation of the attestation quote. 54 | 55 | 56 | You can use `GetRawQuote` to get the TDX Quote in byte array format. 57 | 58 | 59 | ### `func (d Device) Close() error` 60 | 61 | 62 | Closes the device. 63 | 64 | ## `verify` 65 | 66 | This library will check the signature, certificate chain and basic 67 | well-formedness properties of an attestation quote. The requirements for quote 68 | well-formedness come from the [Intel TDX specification](https://cdrdv2.intel.com/v1/dl/getContent/733568), 69 | and the requirements for certificate well-formedness come from the 70 | [Intel PCK Certificate specification](https://api.trustedservices.intel.com/documents/Intel_SGX_PCK_Certificate_CRL_Spec-1.5.pdf). 71 | 72 | The presence of the PCK Certificate Chain within the input attestation quote is 73 | expected. 74 | 75 | ### `func TdxQuote(quote *pb.QuoteV4, options *Options) error` 76 | 77 | This function verifies that the attestation has a valid signature and 78 | certificate chain. It provides an optional verification against the collateral 79 | obtained from the Intel PCS API and also offers an optional check against 80 | the certificate revocation list (CRL). By default, the option to verify against 81 | collaterals and the certificate revocation list(CRL) is disabled. The 82 | verification using collaterals is based on [Intel PCS API specification](https://api.portal.trustedservices.intel.com/provisioning-certification) 83 | documentation. 84 | 85 | Example expected invocation: 86 | 87 | ``` 88 | verify.TdxQuote(myAttestation, verify.Options()) 89 | ``` 90 | 91 | #### `Options` type 92 | 93 | This type contains five fields: 94 | 95 | * `GetCollateral bool`: if true, then `TdxQuote` will download the collateral 96 | from Intel PCS API service and check against collateral obtained. 97 | Must be `true` if `CheckRevocations` is true. 98 | * `CheckRevocations bool`: if true, then `TdxQuote` will download the 99 | certificate revocation list (CRL) from Intel PCS API service and check for 100 | revocations. 101 | * `Getter HTTPSGetter`: if `nil`, uses `DefaultHTTPSGetter()`. 102 | The `HTTPSGetter` interface consists of a single method `Get(url string) 103 | (map[string][]string, []byte, error)` that should return the headers and body 104 | of the HTTPS response. 105 | * `Now time.Time`: if `nil`, uses `time.Now()`. It is the time at which to verify 106 | the validity of certificates and collaterals. 107 | * `TrustedRoots *x509.CertPool`: if `nil`, uses the library's embedded 108 | certificate. 109 | Certificate chain verification is performed using trusted roots. 110 | 111 | ## `rtmr` 112 | 113 | This library should be used within the confidential workload to perform a hash 114 | extend operation into the TDX RTMR register. 115 | 116 | ### `func ExtendEventLog(rtmrIndex int, hashAlgo crypto.Hash, eventLog []byte) error` 117 | 118 | This function calculates the hash digest of a provided event log and extends it 119 | in the corresponding RTMR register. However, it's important to note that TDX 120 | currently only supports the SHA-384 hash algorithm. If `hashAlgo` is not SHA-384, 121 | the function will return an error. 122 | 123 | ### `func ExtendDigest(rtmrIndex int, digest []byte) error` 124 | 125 | This function extends a SHA-384 digest into the corresponding RTMR register. 126 | If the length of the given digest does not match that of a SHA-384 digest, 127 | the function will return an error. 128 | 129 | ## License 130 | 131 | 132 | go-tdx-guest is released under the Apache 2.0 license. 133 | 134 | 135 | ``` 136 | Copyright 2023 Google LLC 137 | 138 | 139 | Licensed under the Apache License, Version 2.0 (the "License"); 140 | you may not use this file except in compliance with the License. 141 | You may obtain a copy of the License at 142 | 143 | 144 | http://www.apache.org/licenses/LICENSE-2.0 145 | 146 | 147 | Unless required by applicable law or agreed to in writing, software 148 | distributed under the License is distributed on an "AS IS" BASIS, 149 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 150 | See the License for the specific language governing permissions and 151 | limitations under the License. 152 | ``` 153 | 154 | 155 | ## Links 156 | 157 | 158 | * [Intel TDX specification](https://cdrdv2.intel.com/v1/dl/getContent/733568) 159 | * [Intel PCK Certificate specification](https://api.trustedservices.intel.com/documents/Intel_SGX_PCK_Certificate_CRL_Spec-1.5.pdf) 160 | * [Intel PCS API specification](https://api.portal.trustedservices.intel.com/provisioning-certification) 161 | 162 | 163 | ## Disclaimers 164 | 165 | 166 | This is not an officially supported Google product. -------------------------------------------------------------------------------- /tools/extend/extend.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Package main implements a CLI tool for extending measurements into RTMR registers. 16 | package main 17 | 18 | import ( 19 | "bytes" 20 | "crypto" 21 | "crypto/rand" 22 | _ "crypto/sha512" // To get SHA384 recognized by crypto.Hash. 23 | "encoding/hex" 24 | "flag" 25 | "fmt" 26 | "io" 27 | "os" 28 | 29 | "github.com/google/go-tdx-guest/rtmr" 30 | "github.com/google/logger" 31 | ) 32 | 33 | const ( 34 | // Exit code 1 - tool usage error. 35 | exitTool = 1 36 | ) 37 | 38 | var ( 39 | infile = flag.String("in", "-", "Path to the input event log. Stdin is \"-\".") 40 | quiet = flag.Bool("quiet", false, "If true, writes nothing the stdout or stderr. Success is exit code 0, failure exit code 1.") 41 | verbosity = flag.Int("verbosity", 0, "The output verbosity. Higher number means more verbose output.") 42 | index = flag.Int("rtmr", 2, "The rtmr index. Must be 2 or 3. Defaults to 2.") 43 | // Use the dev-mode-verify flag for developer-specific testing of rtmr sysfs interface. 44 | devModeVerify = flag.Bool("dev-mode-verify", false, "Enable developer mode to run the RTMR verification test suite. Intended for debugging purposes only.") 45 | ) 46 | 47 | func dieWith(err error, exitCode int) { 48 | if !*quiet { 49 | fmt.Fprintf(os.Stderr, "FATAL: %v\n", err) 50 | } 51 | os.Exit(exitCode) 52 | } 53 | 54 | func die(err error) { 55 | dieWith(err, exitTool) 56 | } 57 | 58 | func readEventLog() ([]byte, error) { 59 | var in io.Reader 60 | if *infile == "-" { 61 | in = os.Stdin 62 | } else { 63 | f, err := os.Open(*infile) 64 | if err != nil { 65 | return nil, fmt.Errorf("could not open input file %q: %v", *infile, err) 66 | } 67 | defer f.Close() 68 | in = f 69 | } 70 | contents, err := io.ReadAll(in) 71 | if err != nil { 72 | return nil, fmt.Errorf("could not read %q: %v", *infile, err) 73 | } 74 | return contents, nil 75 | } 76 | 77 | // runRtmrExtensionTestVerification performs multiple rounds of extending an rtmr and verifies the result. 78 | // It reads the initial state, performs a hardware extension via the sysfs interface, and compares the new 79 | // hardware state against a software-simulated extension to ensure they match. 80 | func runRtmrExtensionTestVerification() { 81 | // Define test cases for each RTMR and its corresponding hash algorithm. 82 | testCases := []struct { 83 | name string 84 | rtmrIndex int 85 | hashAlgo crypto.Hash 86 | }{ 87 | {"RTMR0_SHA384", 0, crypto.SHA384}, 88 | {"RTMR1_SHA384", 1, crypto.SHA384}, 89 | {"RTMR2_SHA384", 2, crypto.SHA384}, 90 | {"RTMR3_SHA384", 3, crypto.SHA384}, 91 | } 92 | 93 | overallResult := true // Track the overall success of all verification tests. 94 | 95 | fmt.Println("Starting RTMR extension verification...") 96 | 97 | for _, tc := range testCases { 98 | fmt.Printf("--- Running verification for %s ---\n", tc.name) 99 | filePath := fmt.Sprintf("/sys/class/misc/tdx_guest/measurements/rtmr%d:sha384", tc.rtmrIndex) 100 | digestSize := tc.hashAlgo.Size() 101 | 102 | // Check if the sysfs file for the rtmr exists before proceeding. 103 | if _, err := os.Stat(filePath); os.IsNotExist(err) { 104 | fmt.Printf("Skipping test: RTMR sysfs file not found at %s\n\n", filePath) 105 | continue 106 | } 107 | 108 | casePassed := true 109 | // Run 10 rounds of extend-and-verify for each rtmr to ensure robustness. 110 | for i := 0; i < 10; i++ { 111 | // 1. Read the initial RTMR value from the sysfs file. 112 | initialData, err := os.ReadFile(filePath) 113 | if err != nil { 114 | fmt.Printf("Round %d: failed to read original digest from %s: %v\n", i+1, filePath, err) 115 | os.Exit(1) 116 | } 117 | 118 | // 2. Validate the size of the initial data. For a SHA384-based rtmr, 119 | // the existing value must be a valid SHA384 digest. 120 | if len(initialData) != crypto.SHA384.Size() { 121 | fmt.Printf("Round %d: initial data from %s has incorrect size: got %d bytes, want %d bytes\n", 122 | i+1, filePath, len(initialData), crypto.SHA384.Size()) 123 | os.Exit(1) 124 | } 125 | 126 | // 3. Start software calculation of the expected measurement. 127 | hasher := tc.hashAlgo.New() 128 | hasher.Write(initialData) 129 | 130 | // 4. Generate a random event digest to extend into the rtmr. 131 | eventDigest := make([]byte, digestSize) 132 | if _, err := rand.Read(eventDigest); err != nil { 133 | fmt.Printf("Round %d: error generating random data: %v\n", i+1, err) 134 | os.Exit(1) 135 | } 136 | 137 | hasher.Write(eventDigest) 138 | wantDigest := hasher.Sum(nil) 139 | 140 | // 5. Perform the hardware extension by writing the event digest to the sysfs node. 141 | if err := rtmr.ExtendDigestSysfs(tc.rtmrIndex, eventDigest); err != nil { 142 | fmt.Printf("Round %d: error extending RTMR via sysfs: %v\n", i+1, err) 143 | os.Exit(1) 144 | } 145 | 146 | // 6. Read the new rtmr value back from the hardware. 147 | gotDigest, err := os.ReadFile(filePath) 148 | if err != nil { 149 | fmt.Printf("Round %d: failed to read back from %s for verification: %v\n", i+1, filePath, err) 150 | os.Exit(1) 151 | } 152 | 153 | // 7. Compare the software-calculated value with the actual hardware value. 154 | if !bytes.Equal(wantDigest, gotDigest) { 155 | fmt.Printf("Round %d: VERIFICATION FAILED:\n Expected: %s\n Got: %s\n", 156 | i+1, hex.EncodeToString(wantDigest), hex.EncodeToString(gotDigest)) 157 | overallResult = false 158 | casePassed = false 159 | } else { 160 | fmt.Printf("Round %d: Verification PASSED\n", i+1) 161 | } 162 | } 163 | if casePassed { 164 | fmt.Printf("--- All rounds for %s PASSED ---\n\n", tc.name) 165 | } else { 166 | fmt.Printf("--- Verification for %s FAILED ---\n\n", tc.name) 167 | } 168 | } 169 | 170 | if overallResult { 171 | fmt.Println("=====================================") 172 | fmt.Println("✅ All verification test cases passed.") 173 | fmt.Println("=====================================") 174 | os.Exit(0) 175 | } 176 | 177 | fmt.Println("=====================================") 178 | fmt.Println("❌ One or more verification tests FAILED.") 179 | fmt.Println("=====================================") 180 | os.Exit(1) 181 | } 182 | 183 | func main() { 184 | logger.Init("", false, false, os.Stdout) 185 | flag.Parse() 186 | logger.SetLevel(logger.Level(*verbosity)) 187 | 188 | if *devModeVerify { 189 | runRtmrExtensionTestVerification() 190 | return 191 | } 192 | 193 | eventLog, err := readEventLog() 194 | if err != nil { 195 | die(err) 196 | } 197 | err = rtmr.ExtendEventLog(*index, crypto.SHA384, eventLog) 198 | if err != nil { 199 | die(err) 200 | } 201 | if !*quiet { 202 | logger.V(1).Infof("Extended measurement into rtmr %d successfully", *index) 203 | } 204 | } 205 | -------------------------------------------------------------------------------- /abi/abi_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package abi 16 | 17 | import ( 18 | "bytes" 19 | "testing" 20 | 21 | pb "github.com/google/go-tdx-guest/proto/tdx" 22 | test "github.com/google/go-tdx-guest/testing/testdata" 23 | ) 24 | 25 | func TestQuoteToProto(t *testing.T) { 26 | expectedError := "unable to determine quote format since bytes length is less than 2 bytes" 27 | var emptyRawQuote []uint8 28 | _, err := QuoteToProto(emptyRawQuote) 29 | if err == nil || err.Error() != expectedError { 30 | t.Errorf("error found: %v, want error: %s", err, expectedError) 31 | } 32 | 33 | _, err = QuoteToProto(test.RawQuote) 34 | if err != nil { 35 | t.Fatal(err) 36 | } 37 | } 38 | 39 | func TestQuoteToAbiBytes(t *testing.T) { 40 | quote, err := QuoteToProto(test.RawQuote) 41 | if err != nil { 42 | t.Fatal(err) 43 | } 44 | rawQuote, err := QuoteToAbiBytes(quote) 45 | if err != nil { 46 | t.Fatal(err) 47 | } 48 | if !bytes.Equal(test.RawQuote, rawQuote) { 49 | t.Errorf("raw quote bytes got %v. Expected %v", rawQuote, test.RawQuote) 50 | } 51 | } 52 | 53 | func TestNilToAbiBytesConversions(t *testing.T) { 54 | 55 | if _, err := QuoteToAbiBytes(nil); err != ErrQuoteNil { 56 | t.Error(err) 57 | } 58 | if _, err := signedDataToAbiBytes(nil); err != ErrQuoteV4AuthDataNil { 59 | t.Error(err) 60 | } 61 | if _, err := certificationDataToAbiBytes(nil); err != ErrCertificationDataNil { 62 | t.Error(err) 63 | } 64 | if _, err := qeReportCertificationDataToAbiBytes(nil); err != ErrQeReportCertificationDataNil { 65 | t.Error(err) 66 | } 67 | if _, err := qeAuthDataToAbiBytes(nil); err != ErrQeAuthDataNil { 68 | t.Error(err) 69 | } 70 | if _, err := pckCertificateChainToAbiBytes(nil); err != ErrPckCertChainNil { 71 | t.Error(err) 72 | } 73 | if _, err := TdQuoteBodyToAbiBytes(nil); err != ErrTDQuoteBodyNil { 74 | t.Error(err) 75 | } 76 | if _, err := HeaderToAbiBytes(nil); err != ErrHeaderNil { 77 | t.Error(err) 78 | } 79 | if _, err := EnclaveReportToAbiBytes(nil); err != ErrQeReportNil { 80 | t.Error(err) 81 | } 82 | } 83 | 84 | func TestInvalidConversionsToAbiBytes(t *testing.T) { 85 | expectedErrors := []string{ 86 | "QuoteV4 invalid: QuoteV4 Header error: header is nil", 87 | "QuoteV4 AuthData invalid: signature size is 0 bytes. Expected 64 bytes", 88 | "certification data invalid: certification data type invalid, got 0, expected 6", 89 | "certification data invalid: certification data type invalid, got 7, expected 6", 90 | "certification data invalid: QE Report certification data error: QE Report certification data is nil", 91 | "QE Report certification data invalid: QE Report error: QE Report is nil", 92 | "QE AuthData invalid: parsed data size is 0 bytes. Expected 1 bytes", 93 | "PCK certificate chain data invalid: PCK certificate chain data type invalid, got 0, expected 5", 94 | "PCK certificate chain data invalid: PCK certificate chain data type invalid, got 7, expected 5", 95 | "PCK certificate chain data invalid: PCK certificate chain size is 0. Expected size 2", 96 | "TD quote body invalid: teeTcbSvn size is 0 bytes. Expected 16 bytes", 97 | "header invalid: version 0 not supported", 98 | "header invalid: version 1 not supported", 99 | "header invalid: attestation key type not supported", 100 | "header invalid: TEE type is not TDX", 101 | "QE Report invalid: cpuSvn size is 0 bytes. Expected 16 bytes", 102 | } 103 | if _, err := QuoteToAbiBytes(&pb.QuoteV4{}); err == nil || err.Error() != expectedErrors[0] { 104 | t.Errorf("error found: %v, want error: %s", err, expectedErrors[0]) 105 | } 106 | if _, err := signedDataToAbiBytes(&pb.Ecdsa256BitQuoteV4AuthData{}); err == nil || err.Error() != expectedErrors[1] { 107 | t.Errorf("error found: %v, want error: %s", err, expectedErrors[1]) 108 | } 109 | if _, err := certificationDataToAbiBytes(&pb.CertificationData{}); err == nil || err.Error() != expectedErrors[2] { 110 | t.Errorf("error found: %v, want error: %s", err, expectedErrors[2]) 111 | } 112 | if _, err := certificationDataToAbiBytes(&pb.CertificationData{CertificateDataType: 7}); err == nil || err.Error() != expectedErrors[3] { 113 | t.Errorf("error found: %v, want error: %s", err, expectedErrors[3]) 114 | } 115 | if _, err := certificationDataToAbiBytes(&pb.CertificationData{CertificateDataType: qeReportCertificationDataType, Size: 2}); err == nil || err.Error() != expectedErrors[4] { 116 | t.Errorf("error found: %v, want error: %s", err, expectedErrors[4]) 117 | } 118 | if _, err := qeReportCertificationDataToAbiBytes(&pb.QEReportCertificationData{}); err == nil || err.Error() != expectedErrors[5] { 119 | t.Errorf("error found: %v, want error: %s", err, expectedErrors[5]) 120 | } 121 | if _, err := qeAuthDataToAbiBytes(&pb.QeAuthData{ParsedDataSize: 1}); err == nil || err.Error() != expectedErrors[6] { 122 | 123 | t.Errorf("error found: %v, want error: %s", err, expectedErrors[6]) 124 | } 125 | if _, err := pckCertificateChainToAbiBytes(&pb.PCKCertificateChainData{}); err == nil || err.Error() != expectedErrors[7] { 126 | t.Errorf("error found: %v, want error: %s", err, expectedErrors[7]) 127 | } 128 | if _, err := pckCertificateChainToAbiBytes(&pb.PCKCertificateChainData{CertificateDataType: 7}); err == nil || err.Error() != expectedErrors[8] { 129 | t.Errorf("error found: %v, want error: %s", err, expectedErrors[8]) 130 | } 131 | if _, err := pckCertificateChainToAbiBytes(&pb.PCKCertificateChainData{CertificateDataType: pckReportCertificationDataType, Size: 2}); err == nil || err.Error() != expectedErrors[9] { 132 | t.Errorf("error found: %v, want error: %s", err, expectedErrors[9]) 133 | } 134 | if _, err := TdQuoteBodyToAbiBytes(&pb.TDQuoteBody{}); err == nil || err.Error() != expectedErrors[10] { 135 | t.Errorf("error found: %v, want error: %s", err, expectedErrors[10]) 136 | } 137 | if _, err := HeaderToAbiBytes(&pb.Header{}); err == nil || err.Error() != expectedErrors[11] { 138 | t.Errorf("error found: %v, want error: %s", err, expectedErrors[11]) 139 | } 140 | if _, err := HeaderToAbiBytes(&pb.Header{Version: 1}); err == nil || err.Error() != expectedErrors[12] { 141 | t.Errorf("error found: %v, want error: %s", err, expectedErrors[12]) 142 | } 143 | if _, err := HeaderToAbiBytes(&pb.Header{Version: QuoteVersion, AttestationKeyType: 1}); err == nil || err.Error() != expectedErrors[13] { 144 | t.Errorf("error found: %v, want error: %s", err, expectedErrors[13]) 145 | } 146 | if _, err := HeaderToAbiBytes(&pb.Header{Version: QuoteVersion, AttestationKeyType: AttestationKeyType, TeeType: 0x01}); err == nil || err.Error() != expectedErrors[14] { 147 | t.Errorf("error found: %v, want error: %s", err, expectedErrors[14]) 148 | } 149 | if _, err := EnclaveReportToAbiBytes(&pb.EnclaveReport{}); err == nil || err.Error() != expectedErrors[15] { 150 | t.Errorf("error found: %v, want error: %s", err, expectedErrors[15]) 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /testing/test_cases.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Package testing defines the mock tdx-guest device 16 | package testing 17 | 18 | import ( 19 | labi "github.com/google/go-tdx-guest/client/linuxabi" 20 | "github.com/google/go-tdx-guest/pcs" 21 | "github.com/google/go-tdx-guest/testing/testdata" 22 | ) 23 | 24 | var pckCrlIssuerChain = []string{ 25 | "-----BEGIN%20CERTIFICATE-----%0AMIICljCCAj2gAwIBAgIVAJVvXc29G%2BHpQEnJ1PQzzgFXC95UMAoGCCqGSM49BAMC%0AMGgxGjAYBgNVBAMMEUludGVsIFNHWCBSb290IENBMRowGAYDVQQKDBFJbnRlbCBD%0Ab3Jwb3JhdGlvbjEUMBIGA1UEBwwLU2FudGEgQ2xhcmExCzAJBgNVBAgMAkNBMQsw%0ACQYDVQQGEwJVUzAeFw0xODA1MjExMDUwMTBaFw0zMzA1MjExMDUwMTBaMHAxIjAg%0ABgNVBAMMGUludGVsIFNHWCBQQ0sgUGxhdGZvcm0gQ0ExGjAYBgNVBAoMEUludGVs%0AIENvcnBvcmF0aW9uMRQwEgYDVQQHDAtTYW50YSBDbGFyYTELMAkGA1UECAwCQ0Ex%0ACzAJBgNVBAYTAlVTMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAENSB%2F7t21lXSO%0A2Cuzpxw74eJB72EyDGgW5rXCtx2tVTLq6hKk6z%2BUiRZCnqR7psOvgqFeSxlmTlJl%0AeTmi2WYz3qOBuzCBuDAfBgNVHSMEGDAWgBQiZQzWWp00ifODtJVSv1AbOScGrDBS%0ABgNVHR8ESzBJMEegRaBDhkFodHRwczovL2NlcnRpZmljYXRlcy50cnVzdGVkc2Vy%0AdmljZXMuaW50ZWwuY29tL0ludGVsU0dYUm9vdENBLmRlcjAdBgNVHQ4EFgQUlW9d%0Azb0b4elAScnU9DPOAVcL3lQwDgYDVR0PAQH%2FBAQDAgEGMBIGA1UdEwEB%2FwQIMAYB%0AAf8CAQAwCgYIKoZIzj0EAwIDRwAwRAIgXsVki0w%2Bi6VYGW3UF%2F22uaXe0YJDj1Ue%0AnA%2BTjD1ai5cCICYb1SAmD5xkfTVpvo4UoyiSYxrDWLmUR4CI9NKyfPN%2B%0A-----END%20CERTIFICATE-----%0A-----BEGIN%20CERTIFICATE-----%0AMIICjzCCAjSgAwIBAgIUImUM1lqdNInzg7SVUr9QGzknBqwwCgYIKoZIzj0EAwIw%0AaDEaMBgGA1UEAwwRSW50ZWwgU0dYIFJvb3QgQ0ExGjAYBgNVBAoMEUludGVsIENv%0AcnBvcmF0aW9uMRQwEgYDVQQHDAtTYW50YSBDbGFyYTELMAkGA1UECAwCQ0ExCzAJ%0ABgNVBAYTAlVTMB4XDTE4MDUyMTEwNDUxMFoXDTQ5MTIzMTIzNTk1OVowaDEaMBgG%0AA1UEAwwRSW50ZWwgU0dYIFJvb3QgQ0ExGjAYBgNVBAoMEUludGVsIENvcnBvcmF0%0AaW9uMRQwEgYDVQQHDAtTYW50YSBDbGFyYTELMAkGA1UECAwCQ0ExCzAJBgNVBAYT%0AAlVTMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEC6nEwMDIYZOj%2FiPWsCzaEKi7%0A1OiOSLRFhWGjbnBVJfVnkY4u3IjkDYYL0MxO4mqsyYjlBalTVYxFP2sJBK5zlKOB%0AuzCBuDAfBgNVHSMEGDAWgBQiZQzWWp00ifODtJVSv1AbOScGrDBSBgNVHR8ESzBJ%0AMEegRaBDhkFodHRwczovL2NlcnRpZmljYXRlcy50cnVzdGVkc2VydmljZXMuaW50%0AZWwuY29tL0ludGVsU0dYUm9vdENBLmRlcjAdBgNVHQ4EFgQUImUM1lqdNInzg7SV%0AUr9QGzknBqwwDgYDVR0PAQH%2FBAQDAgEGMBIGA1UdEwEB%2FwQIMAYBAf8CAQEwCgYI%0AKoZIzj0EAwIDSQAwRgIhAOW%2F5QkR%2BS9CiSDcNoowLuPRLsWGf%2FYi7GSX94BgwTwg%0AAiEA4J0lrHoMs%2BXo5o%2FsX6O9QWxHRAvZUGOdRQ7cvqRXaqI%3D%0A-----END%20CERTIFICATE-----%0A"} 26 | 27 | var tcbInfoIssuerChain = []string{ 28 | "-----BEGIN%20CERTIFICATE-----%0AMIICizCCAjKgAwIBAgIUfjiC1ftVKUpASY5FhAPpFJG99FUwCgYIKoZIzj0EAwIw%0AaDEaMBgGA1UEAwwRSW50ZWwgU0dYIFJvb3QgQ0ExGjAYBgNVBAoMEUludGVsIENv%0AcnBvcmF0aW9uMRQwEgYDVQQHDAtTYW50YSBDbGFyYTELMAkGA1UECAwCQ0ExCzAJ%0ABgNVBAYTAlVTMB4XDTE4MDUyMTEwNTAxMFoXDTI1MDUyMTEwNTAxMFowbDEeMBwG%0AA1UEAwwVSW50ZWwgU0dYIFRDQiBTaWduaW5nMRowGAYDVQQKDBFJbnRlbCBDb3Jw%0Ab3JhdGlvbjEUMBIGA1UEBwwLU2FudGEgQ2xhcmExCzAJBgNVBAgMAkNBMQswCQYD%0AVQQGEwJVUzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABENFG8xzydWRfK92bmGv%0AP%2BmAh91PEyV7Jh6FGJd5ndE9aBH7R3E4A7ubrlh%2FzN3C4xvpoouGlirMba%2BW2lju%0AypajgbUwgbIwHwYDVR0jBBgwFoAUImUM1lqdNInzg7SVUr9QGzknBqwwUgYDVR0f%0ABEswSTBHoEWgQ4ZBaHR0cHM6Ly9jZXJ0aWZpY2F0ZXMudHJ1c3RlZHNlcnZpY2Vz%0ALmludGVsLmNvbS9JbnRlbFNHWFJvb3RDQS5kZXIwHQYDVR0OBBYEFH44gtX7VSlK%0AQEmORYQD6RSRvfRVMA4GA1UdDwEB%2FwQEAwIGwDAMBgNVHRMBAf8EAjAAMAoGCCqG%0ASM49BAMCA0cAMEQCIB9C8wOAN%2FImxDtGACV246KcqjagZOR0kyctyBrsGGJVAiAj%0AftbrNGsGU8YH211dRiYNoPPu19Zp%2Fze8JmhujB0oBw%3D%3D%0A-----END%20CERTIFICATE-----%0A-----BEGIN%20CERTIFICATE-----%0AMIICjzCCAjSgAwIBAgIUImUM1lqdNInzg7SVUr9QGzknBqwwCgYIKoZIzj0EAwIw%0AaDEaMBgGA1UEAwwRSW50ZWwgU0dYIFJvb3QgQ0ExGjAYBgNVBAoMEUludGVsIENv%0AcnBvcmF0aW9uMRQwEgYDVQQHDAtTYW50YSBDbGFyYTELMAkGA1UECAwCQ0ExCzAJ%0ABgNVBAYTAlVTMB4XDTE4MDUyMTEwNDUxMFoXDTQ5MTIzMTIzNTk1OVowaDEaMBgG%0AA1UEAwwRSW50ZWwgU0dYIFJvb3QgQ0ExGjAYBgNVBAoMEUludGVsIENvcnBvcmF0%0AaW9uMRQwEgYDVQQHDAtTYW50YSBDbGFyYTELMAkGA1UECAwCQ0ExCzAJBgNVBAYT%0AAlVTMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEC6nEwMDIYZOj%2FiPWsCzaEKi7%0A1OiOSLRFhWGjbnBVJfVnkY4u3IjkDYYL0MxO4mqsyYjlBalTVYxFP2sJBK5zlKOB%0AuzCBuDAfBgNVHSMEGDAWgBQiZQzWWp00ifODtJVSv1AbOScGrDBSBgNVHR8ESzBJ%0AMEegRaBDhkFodHRwczovL2NlcnRpZmljYXRlcy50cnVzdGVkc2VydmljZXMuaW50%0AZWwuY29tL0ludGVsU0dYUm9vdENBLmRlcjAdBgNVHQ4EFgQUImUM1lqdNInzg7SV%0AUr9QGzknBqwwDgYDVR0PAQH%2FBAQDAgEGMBIGA1UdEwEB%2FwQIMAYBAf8CAQEwCgYI%0AKoZIzj0EAwIDSQAwRgIhAOW%2F5QkR%2BS9CiSDcNoowLuPRLsWGf%2FYi7GSX94BgwTwg%0AAiEA4J0lrHoMs%2BXo5o%2FsX6O9QWxHRAvZUGOdRQ7cvqRXaqI%3D%0A-----END%20CERTIFICATE-----%0A", 29 | } 30 | 31 | var qeIdentityIssuerChain = []string{ 32 | "-----BEGIN%20CERTIFICATE-----%0AMIICizCCAjKgAwIBAgIUfjiC1ftVKUpASY5FhAPpFJG99FUwCgYIKoZIzj0EAwIw%0AaDEaMBgGA1UEAwwRSW50ZWwgU0dYIFJvb3QgQ0ExGjAYBgNVBAoMEUludGVsIENv%0AcnBvcmF0aW9uMRQwEgYDVQQHDAtTYW50YSBDbGFyYTELMAkGA1UECAwCQ0ExCzAJ%0ABgNVBAYTAlVTMB4XDTE4MDUyMTEwNTAxMFoXDTI1MDUyMTEwNTAxMFowbDEeMBwG%0AA1UEAwwVSW50ZWwgU0dYIFRDQiBTaWduaW5nMRowGAYDVQQKDBFJbnRlbCBDb3Jw%0Ab3JhdGlvbjEUMBIGA1UEBwwLU2FudGEgQ2xhcmExCzAJBgNVBAgMAkNBMQswCQYD%0AVQQGEwJVUzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABENFG8xzydWRfK92bmGv%0AP%2BmAh91PEyV7Jh6FGJd5ndE9aBH7R3E4A7ubrlh%2FzN3C4xvpoouGlirMba%2BW2lju%0AypajgbUwgbIwHwYDVR0jBBgwFoAUImUM1lqdNInzg7SVUr9QGzknBqwwUgYDVR0f%0ABEswSTBHoEWgQ4ZBaHR0cHM6Ly9jZXJ0aWZpY2F0ZXMudHJ1c3RlZHNlcnZpY2Vz%0ALmludGVsLmNvbS9JbnRlbFNHWFJvb3RDQS5kZXIwHQYDVR0OBBYEFH44gtX7VSlK%0AQEmORYQD6RSRvfRVMA4GA1UdDwEB%2FwQEAwIGwDAMBgNVHRMBAf8EAjAAMAoGCCqG%0ASM49BAMCA0cAMEQCIB9C8wOAN%2FImxDtGACV246KcqjagZOR0kyctyBrsGGJVAiAj%0AftbrNGsGU8YH211dRiYNoPPu19Zp%2Fze8JmhujB0oBw%3D%3D%0A-----END%20CERTIFICATE-----%0A-----BEGIN%20CERTIFICATE-----%0AMIICjzCCAjSgAwIBAgIUImUM1lqdNInzg7SVUr9QGzknBqwwCgYIKoZIzj0EAwIw%0AaDEaMBgGA1UEAwwRSW50ZWwgU0dYIFJvb3QgQ0ExGjAYBgNVBAoMEUludGVsIENv%0AcnBvcmF0aW9uMRQwEgYDVQQHDAtTYW50YSBDbGFyYTELMAkGA1UECAwCQ0ExCzAJ%0ABgNVBAYTAlVTMB4XDTE4MDUyMTEwNDUxMFoXDTQ5MTIzMTIzNTk1OVowaDEaMBgG%0AA1UEAwwRSW50ZWwgU0dYIFJvb3QgQ0ExGjAYBgNVBAoMEUludGVsIENvcnBvcmF0%0AaW9uMRQwEgYDVQQHDAtTYW50YSBDbGFyYTELMAkGA1UECAwCQ0ExCzAJBgNVBAYT%0AAlVTMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEC6nEwMDIYZOj%2FiPWsCzaEKi7%0A1OiOSLRFhWGjbnBVJfVnkY4u3IjkDYYL0MxO4mqsyYjlBalTVYxFP2sJBK5zlKOB%0AuzCBuDAfBgNVHSMEGDAWgBQiZQzWWp00ifODtJVSv1AbOScGrDBSBgNVHR8ESzBJ%0AMEegRaBDhkFodHRwczovL2NlcnRpZmljYXRlcy50cnVzdGVkc2VydmljZXMuaW50%0AZWwuY29tL0ludGVsU0dYUm9vdENBLmRlcjAdBgNVHQ4EFgQUImUM1lqdNInzg7SV%0AUr9QGzknBqwwDgYDVR0PAQH%2FBAQDAgEGMBIGA1UdEwEB%2FwQIMAYBAf8CAQEwCgYI%0AKoZIzj0EAwIDSQAwRgIhAOW%2F5QkR%2BS9CiSDcNoowLuPRLsWGf%2FYi7GSX94BgwTwg%0AAiEA4J0lrHoMs%2BXo5o%2FsX6O9QWxHRAvZUGOdRQ7cvqRXaqI%3D%0A-----END%20CERTIFICATE-----%0A", 33 | } 34 | 35 | // PckCrlHeader is the response header for pck crl 36 | var PckCrlHeader = map[string][]string{ 37 | pcs.SgxPckCrlIssuerChainPhrase: pckCrlIssuerChain, 38 | } 39 | 40 | // TcbInfoHeader is the response header for pck crl 41 | var TcbInfoHeader = map[string][]string{ 42 | pcs.TcbInfoIssuerChainPhrase: tcbInfoIssuerChain, 43 | } 44 | 45 | // QeIdentityHeader is the response header for pck crl 46 | var QeIdentityHeader = map[string][]string{ 47 | pcs.SgxQeIdentityIssuerChainPhrase: qeIdentityIssuerChain, 48 | } 49 | 50 | // TestGetter is a local getter tied to the included sample quote 51 | var TestGetter = &Getter{ 52 | Responses: map[string]HTTPResponse{ 53 | "https://api.trustedservices.intel.com/tdx/certification/v4/qe/identity": { 54 | Header: QeIdentityHeader, 55 | Body: testdata.QeIdentityBody, 56 | }, 57 | "https://api.trustedservices.intel.com/tdx/certification/v4/tcb?fmspc=50806f000000": { 58 | Header: TcbInfoHeader, 59 | Body: testdata.TcbInfoBody, 60 | }, 61 | "https://api.trustedservices.intel.com/sgx/certification/v4/pckcrl?ca=platform&encoding=der": { 62 | Header: PckCrlHeader, 63 | Body: testdata.PckCrlBody, 64 | }, 65 | "https://certificates.trustedservices.intel.com/IntelSGXRootCA.der": { 66 | Header: nil, 67 | Body: testdata.RootCrlBody, 68 | }, 69 | }, 70 | } 71 | 72 | // reportdata defines a ReportData example that is all zeros except the last byte is 1. 73 | var reportdata = [64]byte{ 74 | 0, 0, 0, 0, 0, 0, 0, 0, 75 | 0, 0, 0, 0, 0, 0, 0, 0, 76 | 0, 0, 0, 0, 0, 0, 0, 0, 77 | 0, 0, 0, 0, 0, 0, 0, 0, 78 | 0, 0, 0, 0, 0, 0, 0, 0, 79 | 0, 0, 0, 0, 0, 0, 0, 0, 80 | 0, 0, 0, 0, 0, 0, 0, 0, 81 | 0, 0, 0, 0, 0, 0, 0, 1} 82 | 83 | // TestCase represents a get_quote input/output test case. 84 | type TestCase struct { 85 | Name string 86 | Input [labi.TdReportDataSize]uint8 87 | Report [labi.TdReportSize]uint8 88 | Quote []uint8 89 | EsResult labi.EsResult 90 | WantErr string 91 | } 92 | 93 | // TestCases returns common test cases for get_report. 94 | func TestCases() []TestCase { 95 | var report [1024]byte 96 | copy(report[:], testdata.RawReport) 97 | return []TestCase{ 98 | { 99 | Name: "zeros", 100 | Input: reportdata, 101 | Report: report, 102 | Quote: testdata.RawQuote, 103 | }, 104 | } 105 | } 106 | 107 | // TcQuoteProvider returns a mock quote provider populated from test cases inputs and expected outputs. 108 | func TcQuoteProvider(tcs []TestCase) (*TdxQuoteProvider, error) { 109 | rawQuoteResponses := map[[labi.TdReportDataSize]byte][]uint8{} 110 | for _, tc := range tcs { 111 | rawQuoteResponses[tc.Input] = tc.Quote 112 | } 113 | return &TdxQuoteProvider{ 114 | isSupported: true, 115 | rawQuoteResponse: rawQuoteResponses, 116 | }, nil 117 | } 118 | 119 | // TcDevice returns a mock device populated from test cases inputs and expected outputs. 120 | func TcDevice(tcs []TestCase) (*Device, error) { 121 | reportResponses := map[[labi.TdReportDataSize]byte]any{} 122 | quoteResponses := map[[labi.TdReportSize]byte]any{} 123 | for _, tc := range tcs { 124 | reportResponses[tc.Input] = &GetReportResponse{ 125 | Resp: labi.TdxReportReq{ 126 | TdReport: tc.Report, 127 | }, 128 | EsResult: tc.EsResult, 129 | } 130 | var idQuote [labi.ReqBufSize]byte 131 | copy(idQuote[:], tc.Quote) 132 | quoteResponses[tc.Report] = &GetQuoteResponse{ 133 | Resp: labi.TdxQuoteHdr{ 134 | Status: labi.GetQuoteSuccess, 135 | OutLen: uint32(len(tc.Quote)), 136 | Data: idQuote, 137 | }, 138 | EsResult: tc.EsResult, 139 | } 140 | } 141 | return &Device{ 142 | reportResponse: reportResponses, 143 | quoteResponse: quoteResponses, 144 | }, nil 145 | } 146 | -------------------------------------------------------------------------------- /validate/validate_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package validate 16 | 17 | import ( 18 | "strings" 19 | "testing" 20 | 21 | pb "github.com/google/go-tdx-guest/proto/tdx" 22 | vr "github.com/google/go-tdx-guest/verify" 23 | 24 | "github.com/google/go-tdx-guest/abi" 25 | "github.com/google/go-tdx-guest/testing/testdata" 26 | ) 27 | 28 | func convert(a []byte, x byte) []byte { 29 | for i := range a { 30 | a[i] = x 31 | } 32 | return a 33 | } 34 | 35 | func TestTdxQuote(t *testing.T) { 36 | if err := TdxQuote(nil, nil); err != vr.ErrOptionsNil { 37 | t.Error(err) 38 | } 39 | qeSvn := uint16(0) 40 | pceSvn := uint16(0) 41 | qeVendorID := []byte{0x93, 0x9a, 0x72, 0x33, 0xf7, 0x9c, 0x4c, 0xa9, 0x94, 0xa, 0xd, 0xb3, 0x95, 0x7f, 0x6, 0x7} 42 | teeTcbSvn := []byte{0x3, 0x0, 0x4, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0} 43 | mrSeam := []byte{0x2f, 0xd2, 0x79, 0xc1, 0x61, 0x64, 0xa9, 0x3d, 0xd5, 0xbf, 0x37, 0x3d, 0x83, 0x43, 0x28, 0xd4, 44 | 0x60, 0x8, 0xc2, 0xb6, 0x93, 0xaf, 0x9e, 0xbb, 0x86, 0x5b, 0x8, 0xb2, 0xce, 0xd3, 0x20, 0xc9, 45 | 0xa8, 0x9b, 0x48, 0x69, 0xa9, 0xfa, 0xb6, 0xf, 0xbe, 0x9d, 0xc, 0x5a, 0x53, 0x63, 0xc6, 0x56} 46 | tdAttributes := []byte{0x0, 0x0, 0x0, 0x40, 0x0, 0x0, 0x0, 0x0} 47 | xfam := []byte{0xe7, 0x1a, 0x6, 0x0, 0x0, 0x0, 0x0, 0x0} 48 | mrTd := []byte{0x63, 0x63, 0xb8, 0x4, 0x36, 0x68, 0xa3, 0xad, 0x95, 0x32, 0x78, 0xe1, 0x3, 0x89, 0x57, 0x4d, 49 | 0x32, 0x6c, 0x67, 0x49, 0xfb, 0x78, 0xaa, 0x81, 0xe, 0xcd, 0x93, 0x36, 0x92, 0x3d, 0xb8, 0x6f, 50 | 0x22, 0xfc, 0x0, 0xb8, 0xdc, 0xd4, 0x4, 0xbc, 0x10, 0xd5, 0xe1, 0x19, 0xd7, 0x21, 0x5c, 0xbb} 51 | mrConfigID := []byte{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 52 | 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 53 | 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0} 54 | mrOwner := []byte{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 55 | 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 56 | 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0} 57 | mrOwnerConfig := []byte{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 58 | 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 59 | 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0} 60 | rtmr0 := []byte{0x29, 0x27, 0xda, 0x70, 0x46, 0x1c, 0xd6, 0x32, 0x66, 0xf4, 0x32, 0x30, 0xcc, 0x18, 0x49, 0xc0, 61 | 0x3e, 0xf2, 0x5e, 0xbe, 0x49, 0x0, 0x62, 0xa8, 0x1, 0xd8, 0xfc, 0xc8, 0xa, 0xf4, 0x29, 0x76, 62 | 0x82, 0x3a, 0xdf, 0x8, 0xf8, 0x33, 0xc1, 0xe5, 0xb, 0x51, 0x77, 0x9c, 0x65, 0x93, 0xf3, 0x2a} 63 | rtmr1 := []byte{0x2c, 0x70, 0xb, 0x8b, 0xa9, 0xb8, 0x57, 0x83, 0xf8, 0xbe, 0x9f, 0xb9, 0x44, 0x36, 0x47, 0xbd, 64 | 0xc0, 0xbb, 0x3c, 0x50, 0x74, 0x7f, 0x6, 0x29, 0x7c, 0xc6, 0x53, 0x8c, 0x25, 0xa5, 0xf5, 0x89, 65 | 0xc4, 0xb5, 0x6d, 0x3, 0x5c, 0x59, 0x10, 0x7c, 0x6b, 0xc5, 0x80, 0xd, 0xb2, 0xca, 0xcb, 0x61} 66 | rtmr2 := []byte{0x86, 0x52, 0xf0, 0xca, 0xab, 0xa7, 0xe2, 0x15, 0xea, 0x44, 0x2d, 0xc3, 0x6a, 0x44, 0x99, 0xd8, 67 | 0xfe, 0xc3, 0x36, 0x2f, 0x3a, 0xb, 0x2c, 0xa1, 0x51, 0xcb, 0xe4, 0xb3, 0xe6, 0x46, 0x6f, 0xe5, 68 | 0x9c, 0x73, 0x68, 0xb3, 0xc2, 0x28, 0x7f, 0xc7, 0xc3, 0xbf, 0x5c, 0x92, 0x4e, 0xb4, 0x42, 0x4e} 69 | rtmr3 := []byte{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 70 | 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 71 | 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0} 72 | reportData := []byte{0x6c, 0x62, 0xde, 0xc1, 0xb8, 0x19, 0x17, 0x49, 0xa3, 0x1d, 0xab, 0x49, 0xb, 0xe5, 0x32, 0xa3, 73 | 0x59, 0x44, 0xde, 0xa4, 0x7c, 0xae, 0xf1, 0xf9, 0x80, 0x86, 0x39, 0x93, 0xd9, 0x89, 0x95, 0x45, 74 | 0xeb, 0x74, 0x6, 0xa3, 0x8d, 0x1e, 0xed, 0x31, 0x3b, 0x98, 0x7a, 0x46, 0x7d, 0xac, 0xea, 0xd6, 75 | 0xf0, 0xc8, 0x7a, 0x6d, 0x76, 0x6c, 0x66, 0xf6, 0xf2, 0x9f, 0x8a, 0xcb, 0x28, 0x1f, 0x11, 0x13} 76 | 77 | mknonce := func(front []byte) []byte { 78 | result := make([]byte, 64) 79 | copy(result[:], front) 80 | return result 81 | } 82 | 83 | make2darray := func(size int) [][]byte { 84 | a := make([][]byte, 4) 85 | for i := range a { 86 | a[i] = make([]byte, size) 87 | } 88 | return a 89 | } 90 | 91 | nonce12345 := mknonce([]byte{1, 2, 3, 4, 5}) 92 | 93 | quoteFn := func(nonce []byte) any { 94 | quote, err := abi.QuoteToProto(testdata.RawQuote) 95 | if err != nil { 96 | t.Fatal(err) 97 | } 98 | data := make([]byte, abi.ReportDataSize) 99 | copy(data, nonce[:]) 100 | switch q := quote.(type) { 101 | case *pb.QuoteV4: 102 | q.TdQuoteBody.ReportData = data 103 | default: 104 | t.Fatal("unsupported quote type") 105 | } 106 | return quote 107 | } 108 | quoteSample := quoteFn(reportData) 109 | quote12345 := quoteFn(nonce12345) 110 | 111 | quote12345WithDebug := quoteFn(nonce12345) 112 | quote12345WithDebug.(*pb.QuoteV4).TdQuoteBody.TdAttributes[0] |= 1 113 | 114 | type testCase struct { 115 | name string 116 | quote any 117 | opts *Options 118 | wantErr string 119 | } 120 | tests := []testCase{ 121 | { 122 | name: "deep check", 123 | quote: quoteSample, 124 | opts: &Options{ 125 | HeaderOptions: HeaderOptions{ 126 | MinimumQeSvn: qeSvn, 127 | MinimumPceSvn: pceSvn, 128 | QeVendorID: qeVendorID, 129 | }, 130 | TdQuoteBodyOptions: TdQuoteBodyOptions{ 131 | MinimumTeeTcbSvn: teeTcbSvn, 132 | MrSeam: mrSeam, 133 | TdAttributes: tdAttributes, 134 | Xfam: xfam, 135 | MrTd: mrTd, 136 | MrConfigID: mrConfigID, 137 | MrOwner: mrOwner, 138 | MrOwnerConfig: mrOwnerConfig, 139 | Rtmrs: [][]byte{rtmr0, rtmr1, rtmr2, rtmr3}, 140 | ReportData: reportData, 141 | EnableTdDebugCheck: true, 142 | }, 143 | }, 144 | }, 145 | { 146 | name: "min QE security-version check", 147 | quote: quote12345, 148 | opts: &Options{ 149 | HeaderOptions: HeaderOptions{ 150 | MinimumQeSvn: uint16(2), 151 | }, 152 | }, 153 | wantErr: "QE security-version number 0 is less than the required minimum 2", 154 | }, 155 | { 156 | name: "min Pce security-version check", 157 | quote: quote12345, 158 | opts: &Options{ 159 | HeaderOptions: HeaderOptions{ 160 | MinimumPceSvn: uint16(2), 161 | }, 162 | }, 163 | wantErr: "PCE security-version number 0 is less than the required minimum 2", 164 | }, 165 | { 166 | name: "min TEE TCB security-version check", 167 | quote: quote12345, 168 | opts: &Options{ 169 | TdQuoteBodyOptions: TdQuoteBodyOptions{ 170 | MinimumTeeTcbSvn: []byte{0x4, 0x0, 0x4, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}, 171 | }, 172 | }, 173 | wantErr: "TEE TCB security-version number [3 0 4 0 0 0 0 0 0 0 0 0 0 0 0 0] is less than the required minimum [4 0 4 0 0 0 0 0 0 0 0 0 0 0 0 0]", 174 | }, 175 | { 176 | name: "Test incorrect MR_SEAM", 177 | quote: quote12345, 178 | opts: &Options{ 179 | TdQuoteBodyOptions: TdQuoteBodyOptions{ 180 | MrSeam: make([]byte, abi.MrSeamSize), 181 | }, 182 | }, 183 | wantErr: "quote field MR_SEAM", 184 | }, 185 | { 186 | name: "Test incorrect TD_ATTRIBUTES", 187 | quote: quote12345, 188 | opts: &Options{ 189 | TdQuoteBodyOptions: TdQuoteBodyOptions{ 190 | TdAttributes: make([]byte, abi.TdAttributesSize), 191 | }, 192 | }, 193 | wantErr: "quote field TD_ATTRIBUTES", 194 | }, 195 | { 196 | name: "Test incorrect XFAM", 197 | quote: quote12345, 198 | opts: &Options{ 199 | TdQuoteBodyOptions: TdQuoteBodyOptions{ 200 | Xfam: make([]byte, abi.XfamSize), 201 | }, 202 | }, 203 | wantErr: "quote field XFAM", 204 | }, 205 | { 206 | name: "Test incorrect MR_TD", 207 | quote: quote12345, 208 | opts: &Options{ 209 | TdQuoteBodyOptions: TdQuoteBodyOptions{ 210 | MrTd: make([]byte, abi.MrTdSize), 211 | }, 212 | }, 213 | wantErr: "quote field MR_TD", 214 | }, 215 | { 216 | name: "Test incorrect MR_TD of all incorrect", 217 | quote: quote12345, 218 | opts: &Options{ 219 | TdQuoteBodyOptions: TdQuoteBodyOptions{ 220 | AnyMrTd: [][]byte{make([]byte, abi.MrTdSize), make([]byte, abi.MrTdSize)}, 221 | }, 222 | }, 223 | wantErr: "no value in AnyMrTd matched", 224 | }, 225 | { 226 | name: "Test correct MR_TD of many incorrect", 227 | quote: quote12345, 228 | opts: &Options{ 229 | TdQuoteBodyOptions: TdQuoteBodyOptions{ 230 | AnyMrTd: [][]byte{make([]byte, abi.MrTdSize), make([]byte, abi.MrTdSize), mrTd}, 231 | }, 232 | }, 233 | }, 234 | { 235 | name: "Test incorrect MR_CONFIG_ID", 236 | quote: quote12345, 237 | opts: &Options{ 238 | TdQuoteBodyOptions: TdQuoteBodyOptions{ 239 | MrConfigID: convert(make([]byte, abi.MrConfigIDSize), 1), 240 | }, 241 | }, 242 | wantErr: "quote field MR_CONFIG_ID", 243 | }, 244 | { 245 | name: "Test incorrect MR_OWNER", 246 | quote: quote12345, 247 | opts: &Options{ 248 | TdQuoteBodyOptions: TdQuoteBodyOptions{ 249 | MrOwner: convert(make([]byte, abi.MrOwnerSize), 1), 250 | }, 251 | }, 252 | wantErr: "quote field MR_OWNER", 253 | }, 254 | { 255 | name: "Test incorrect MR_OWNER_CONFIG", 256 | quote: quote12345, 257 | opts: &Options{ 258 | TdQuoteBodyOptions: TdQuoteBodyOptions{ 259 | MrOwnerConfig: convert(make([]byte, abi.MrOwnerConfigSize), 1), 260 | }, 261 | }, 262 | wantErr: "quote field MR_OWNER_CONFIG", 263 | }, 264 | {name: "Test incorrect RTMRS", 265 | quote: quote12345, 266 | opts: &Options{TdQuoteBodyOptions: TdQuoteBodyOptions{ 267 | Rtmrs: make2darray(abi.RtmrSize), 268 | }, 269 | }, 270 | wantErr: "quote field RTMR", 271 | }, 272 | { 273 | name: "Test incorrect REPORT_DATA", 274 | quote: quote12345, 275 | opts: &Options{ 276 | TdQuoteBodyOptions: TdQuoteBodyOptions{ 277 | ReportData: make([]byte, abi.ReportDataSize), 278 | }, 279 | }, 280 | wantErr: "quote field REPORT_DATA", 281 | }, 282 | { 283 | name: "Test incorrect QE_VENDOR_ID", 284 | quote: quote12345, 285 | opts: &Options{ 286 | HeaderOptions: HeaderOptions{QeVendorID: make([]byte, abi.QeVendorIDSize)}, 287 | }, 288 | wantErr: "quote field QE_VENDOR_ID", 289 | }, 290 | { 291 | name: "Test disallowed TD_ATTRIBUTES DEBUG bit set", 292 | quote: quote12345WithDebug, 293 | opts: &Options{ 294 | TdQuoteBodyOptions: TdQuoteBodyOptions{EnableTdDebugCheck: true}, 295 | }, 296 | wantErr: "TD_ATTRIBUTES DEBUG bit is set, but debug is not allowed", 297 | }, 298 | } 299 | 300 | for _, tc := range tests { 301 | if err := TdxQuote(tc.quote, tc.opts); (err == nil && tc.wantErr != "") || 302 | (err != nil && (tc.wantErr == "" || !strings.Contains(err.Error(), tc.wantErr))) { 303 | t.Errorf("%s: TdxQuote() errored unexpectedly. Got '%v', want '%s'", tc.name, err, tc.wantErr) 304 | } 305 | } 306 | } 307 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /tools/check/check_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package main 16 | 17 | import ( 18 | "encoding/hex" 19 | "fmt" 20 | "os" 21 | "os/exec" 22 | "strconv" 23 | "testing" 24 | 25 | ccpb "github.com/google/go-tdx-guest/proto/checkconfig" 26 | "github.com/google/logger" 27 | "google.golang.org/protobuf/proto" 28 | "google.golang.org/protobuf/reflect/protoreflect" 29 | ) 30 | 31 | // Returns true if the test should be skipped for the protobuf case since the field 32 | // can't be set to the expected value. 33 | type setterFn func(p *ccpb.Policy, value string, t *testing.T) bool 34 | 35 | // Represents a test case that will set a flag or config field to a good or bad value. 36 | // We use this data to check that 37 | // 38 | // - flags alone lead to expected check success or failure, 39 | // - a config alone leads to expected check success or failure, 40 | // - a config set to a bad value and a flag set to a good value leads 41 | // to an expected override and success. 42 | // - a config set to a good value and a flag set to a bad value leads 43 | // to an expected override and failure. 44 | type testCase struct { 45 | flag string 46 | good string 47 | bad []string 48 | setter setterFn 49 | } 50 | 51 | var check string 52 | 53 | func TestMain(m *testing.M) { 54 | if output, err := exec.Command("go", "build", "-buildvcs=false", ".").CombinedOutput(); err != nil { 55 | die(fmt.Errorf("could not build check tool: %v, %s", err, output)) 56 | } 57 | check = "./check" 58 | 59 | logger.Init("CheckTestLog", false, false, os.Stderr) 60 | 61 | code := m.Run() 62 | // Cleanup `check` binary after all tests are done. 63 | os.Remove("check") 64 | os.Exit(code) 65 | } 66 | 67 | func withBaseArgs(config string, args ...string) []string { 68 | base := []string{ 69 | "-in", "../../testing/testdata/tdx_prod_quote_SPR_E4.dat", 70 | "-test_local_getter", 71 | } 72 | 73 | if config != "" { 74 | base = append(base, fmt.Sprintf("-config=%s", config)) 75 | } 76 | result := make([]string, len(args)+len(base)) 77 | copy(result, base) 78 | copy(result[len(base):], args) 79 | return result 80 | } 81 | 82 | func setField(p *ccpb.Policy, policy string, name string, value any) { 83 | if policy == "header_policy" { 84 | s := p.HeaderPolicy 85 | r := s.ProtoReflect() 86 | ty := r.Descriptor() 87 | r.Set(ty.Fields().ByName(protoreflect.Name(name)), protoreflect.ValueOf(value)) 88 | } else if policy == "td_quote_body_policy" { 89 | s := p.TdQuoteBodyPolicy 90 | r := s.ProtoReflect() 91 | ty := r.Descriptor() 92 | r.Set(ty.Fields().ByName(protoreflect.Name(name)), protoreflect.ValueOf(value)) 93 | } 94 | } 95 | 96 | func bytesSetter(name string, policy string) setterFn { 97 | return func(p *ccpb.Policy, value string, _ *testing.T) bool { 98 | v, err := hex.DecodeString(value) 99 | if err != nil { 100 | return true 101 | } 102 | setField(p, policy, name, v) 103 | return false 104 | } 105 | } 106 | 107 | func uint32setter(name string, policy string) setterFn { 108 | return func(p *ccpb.Policy, value string, _ *testing.T) bool { 109 | u, err := strconv.ParseUint(value, 10, 32) 110 | if err != nil { 111 | return true 112 | } 113 | setField(p, policy, name, uint32(u)) 114 | return false 115 | } 116 | } 117 | 118 | func testCases() []testCase { 119 | return []testCase{ 120 | { 121 | flag: "minimum_qe_svn", 122 | good: "0", 123 | bad: []string{ 124 | "1", 125 | "21", 126 | "666666", 127 | }, 128 | setter: uint32setter("minimum_qe_svn", "header_policy"), 129 | }, 130 | { 131 | flag: "minimum_pce_svn", 132 | good: "0", 133 | bad: []string{ 134 | "121", // right size 135 | "666666", // wrong size 136 | }, 137 | setter: uint32setter("minimum_pce_svn", "header_policy"), 138 | }, 139 | { 140 | flag: "qe_vendor_id", 141 | good: "939a7233f79c4ca9940a0db3957f0607", 142 | bad: []string{"00000000000000000000000000000001"}, 143 | setter: bytesSetter("qe_vendor_id", "header_policy"), 144 | }, 145 | { 146 | flag: "minimum_tee_tcb_svn", 147 | good: "03000400000000000000000000000000", 148 | bad: []string{"03000400000000000000000000000011"}, 149 | setter: bytesSetter("minimum_tee_tcb_svn", "td_quote_body_policy"), 150 | }, 151 | { 152 | flag: "mr_seam", 153 | good: "2fd279c16164a93dd5bf373d834328d46008c2b693af9ebb865b08b2ced320c9a89b4869a9fab60fbe9d0c5a5363c656", 154 | bad: []string{ 155 | "2fd279c16164a93dd5bf373d834328d46008c2b693af9ebb865b08b2ced320c9a89b4869a9fab60fbe9d0c5a5363c326"}, 156 | setter: bytesSetter("mr_seam", "td_quote_body_policy"), 157 | }, 158 | { 159 | flag: "td_attributes", 160 | good: "0000004000000000", 161 | bad: []string{"0000004000000011"}, 162 | setter: bytesSetter("td_attributes", "td_quote_body_policy"), 163 | }, 164 | { 165 | flag: "xfam", 166 | good: "e71a060000000000", 167 | bad: []string{}, 168 | setter: bytesSetter("xfam", "td_quote_body_policy"), 169 | }, 170 | { 171 | flag: "mr_td", 172 | good: "6363b8043668a3ad953278e10389574d326c6749fb78aa810ecd9336923db86f22fc00b8dcd404bc10d5e119d7215cbb", 173 | bad: []string{ 174 | "6363b8043668a3ad953278e10389574d326c6749fb78aa810ecd9336923db86f22fc00b8dcd404bc10d5e119d1115cbb", 175 | "6363b8000000a3ad953278e10389574d326c6749fb78aa810ecd9336923db86f22fc00b8dcd404bc10d5e119d7215cbb", 176 | }, 177 | setter: bytesSetter("mr_td", "td_quote_body_policy"), 178 | }, 179 | { 180 | flag: "mr_config_id", 181 | good: "000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", 182 | bad: []string{"000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000011"}, 183 | setter: bytesSetter("mr_config_id", "td_quote_body_policy"), 184 | }, 185 | { 186 | flag: "mr_owner", 187 | good: "000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", 188 | bad: []string{"000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000022"}, 189 | setter: bytesSetter("mr_owner", "td_quote_body_policy"), 190 | }, 191 | { 192 | flag: "mr_owner_config", 193 | good: "000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", 194 | bad: []string{"0", "not a hex"}, 195 | setter: bytesSetter("mr_owner_config", "td_quote_body_policy"), 196 | }, 197 | { 198 | flag: "report_data", 199 | good: "6c62dec1b8191749a31dab490be532a35944dea47caef1f980863993d9899545eb7406a38d1eed313b987a467dacead6f0c87a6d766c66f6f29f8acb281f1113", 200 | bad: []string{"6c62dec1b8191749a31dab490be532a35944dea47caef1f980863993d9899545eb7406a38d1eed313b987a467dacead6f0c87a6d766c66f6f29f8acb281f2213"}, 201 | setter: bytesSetter("report_data", "td_quote_body_policy"), 202 | }, 203 | } 204 | } 205 | 206 | // Writes contents to a file that the runner gets a path to and can use, then deletes the file. 207 | func withTempFile(contents []byte, t *testing.T, runner func(path string)) { 208 | file, err := os.CreateTemp(".", "temp") 209 | if err != nil { 210 | t.Fatal(err) 211 | } 212 | defer os.Remove(file.Name()) 213 | 214 | n, err := file.Write(contents) 215 | if err != nil { 216 | t.Fatal(err) 217 | } 218 | if n != len(contents) { 219 | t.Fatalf("incomplete write to %q. Wrote %d, want %d", file.Name(), n, len(contents)) 220 | } 221 | runner(file.Name()) 222 | } 223 | 224 | func withTestConfig(p *ccpb.Policy, t *testing.T, runner func(path string)) { 225 | config := &ccpb.Config{Policy: p} 226 | 227 | out, err := proto.Marshal(config) 228 | if err != nil { 229 | t.Fatal(err) 230 | } 231 | withTempFile(out, t, runner) 232 | } 233 | 234 | func TestCheckGoodFlags(t *testing.T) { 235 | for _, tc := range testCases() { 236 | // Singular good flag 237 | t.Run(tc.flag, func(t *testing.T) { 238 | cmd := exec.Command(check, withBaseArgs("", fmt.Sprintf("-%s=%s", tc.flag, tc.good))...) 239 | if output, err := cmd.CombinedOutput(); err != nil { 240 | t.Errorf("%s failed unexpectedly: %v (%s)", cmd, err, output) 241 | } 242 | }) 243 | } 244 | } 245 | 246 | func TestCheckBadFlags(t *testing.T) { 247 | for _, tc := range testCases() { 248 | // Singular bad flags 249 | for i, bad := range tc.bad { 250 | t.Run(fmt.Sprintf("%s[%d]", tc.flag, i+1), func(t *testing.T) { 251 | cmd := exec.Command(check, withBaseArgs("", fmt.Sprintf("-%s=%s", tc.flag, bad))...) 252 | if output, err := cmd.CombinedOutput(); err == nil { 253 | t.Errorf("%s succeeded unexpectedly: %s", cmd, output) 254 | } 255 | }) 256 | } 257 | } 258 | } 259 | 260 | func TestRtmrs(t *testing.T) { 261 | validRtmrs := []string{ 262 | "2927da70461cd63266f43230cc1849c03ef25ebe490062a801d8fcc80af42976823adf08f833c1e50b51779c6593f32a,2c700b8ba9b85783f8be9fb9443647bdc0bb3c50747f06297cc6538c25a5f589c4b56d035c59107c6bc5800db2cacb61,8652f0caaba7e215ea442dc36a4499d8fec3362f3a0b2ca151cbe4b3e6466fe59c7368b3c2287fc7c3bf5c924eb4424e,000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", 263 | "2927da70461cd63266f43230cc1849c03ef25ebe490062a801d8fcc80af42976823adf08f833c1e50b51779c6593f32a,2c700b8ba9b85783f8be9fb9443647bdc0bb3c50747f06297cc6538c25a5f589c4b56d035c59107c6bc5800db2cacb61,,", 264 | ",,,000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", 265 | } 266 | invalidRtmrs := []string{ 267 | ",", "0,0,0,0", "0,0,", 268 | } 269 | for _, tc := range validRtmrs { 270 | t.Run("valid_rtmrs", func(t *testing.T) { 271 | cmd := exec.Command(check, withBaseArgs("", fmt.Sprintf("-%s=%s", "rtmrs", tc))...) 272 | if output, err := cmd.CombinedOutput(); err != nil { 273 | t.Errorf("%s failed unexpectedly: %v (%s)", cmd, err, output) 274 | } 275 | }) 276 | } 277 | 278 | for _, tc := range invalidRtmrs { 279 | t.Run("invalid_rtmrs", func(t *testing.T) { 280 | cmd := exec.Command(check, withBaseArgs("", fmt.Sprintf("-%s=%s", "rtmrs", tc))...) 281 | if output, err := cmd.CombinedOutput(); err == nil { 282 | t.Errorf("%s succeeded unexpectedly: %s", cmd, output) 283 | } 284 | }) 285 | } 286 | } 287 | 288 | func TestCheckGoodFields(t *testing.T) { 289 | for _, tc := range testCases() { 290 | t.Run(tc.flag, func(t *testing.T) { 291 | p := &ccpb.Policy{HeaderPolicy: &ccpb.HeaderPolicy{}, TdQuoteBodyPolicy: &ccpb.TDQuoteBodyPolicy{}} 292 | if tc.setter(p, tc.good, t) { 293 | t.Fatal("unexpected parse failure") 294 | } 295 | withTestConfig(p, t, func(path string) { 296 | cmd := exec.Command(check, withBaseArgs(path)...) 297 | if output, err := cmd.CombinedOutput(); err != nil { 298 | t.Errorf("%s (%v) failed unexpectedly: %v, %s", cmd, p, err, output) 299 | } 300 | }) 301 | }) 302 | } 303 | } 304 | 305 | func TestCheckBadFields(t *testing.T) { 306 | for _, tc := range testCases() { 307 | for i, bad := range tc.bad { 308 | t.Run(fmt.Sprintf("%s_bad[%d]", tc.flag, i+1), func(t *testing.T) { 309 | p := &ccpb.Policy{HeaderPolicy: &ccpb.HeaderPolicy{}, TdQuoteBodyPolicy: &ccpb.TDQuoteBodyPolicy{}} 310 | if tc.setter(p, bad, t) { 311 | return 312 | } 313 | withTestConfig(p, t, func(path string) { 314 | cmd := exec.Command(check, withBaseArgs(path)...) 315 | if output, err := cmd.CombinedOutput(); err == nil { 316 | t.Errorf("%s (%v) succeeded unexpectedly: %s", cmd, p, output) 317 | } 318 | }) 319 | }) 320 | } 321 | } 322 | } 323 | 324 | func TestCheckGoodFlagOverridesBadField(t *testing.T) { 325 | for _, tc := range testCases() { 326 | for i, bad := range tc.bad { 327 | t.Run(fmt.Sprintf("%s_bad[%d]", tc.flag, i+1), func(t *testing.T) { 328 | p := &ccpb.Policy{HeaderPolicy: &ccpb.HeaderPolicy{}, TdQuoteBodyPolicy: &ccpb.TDQuoteBodyPolicy{}} 329 | if tc.setter(p, bad, t) { 330 | return 331 | } 332 | withTestConfig(p, t, func(path string) { 333 | cmd := exec.Command(check, withBaseArgs(path, fmt.Sprintf("-%s=%s", tc.flag, tc.good))...) 334 | if output, err := cmd.CombinedOutput(); err != nil { 335 | t.Errorf("%s (%v) failed unexpectedly: %v, %s", cmd, p, err, output) 336 | } 337 | }) 338 | }) 339 | } 340 | } 341 | } 342 | 343 | func TestCheckBadFlagOverridesGoodField(t *testing.T) { 344 | for _, tc := range testCases() { 345 | for i, bad := range tc.bad { 346 | t.Run(fmt.Sprintf("%s_bad[%d]", tc.flag, i+1), func(t *testing.T) { 347 | p := &ccpb.Policy{HeaderPolicy: &ccpb.HeaderPolicy{}, TdQuoteBodyPolicy: &ccpb.TDQuoteBodyPolicy{}} 348 | if tc.setter(p, tc.good, t) { 349 | t.Fatal("unexpected parse failure") 350 | } 351 | withTestConfig(p, t, func(path string) { 352 | cmd := exec.Command(check, withBaseArgs(path, fmt.Sprintf("-%s=%s", tc.flag, bad))...) 353 | if output, err := cmd.CombinedOutput(); err == nil { 354 | t.Errorf("%s (%v) succeeded unexpectedly: %s", cmd, p, output) 355 | } 356 | }) 357 | }) 358 | } 359 | } 360 | } 361 | 362 | func TestNetworkFlags(t *testing.T) { 363 | // check_crl = true should fail if get_collateral = false 364 | cmd := exec.Command(check, withBaseArgs("", fmt.Sprintf("-%s=%s", "check_crl", "true"))...) 365 | if output, err := cmd.CombinedOutput(); err == nil { 366 | t.Errorf("%s check_crl flag succeeded unexpectedly: %v, %s", cmd, err, output) 367 | } 368 | } 369 | 370 | func TestCaBundles(t *testing.T) { 371 | correctPath := []string{"../../verify/trusted_root.pem", 372 | "../../verify/trusted_root.pem,../../verify/trusted_root.pem"} 373 | fakePath := []string{"fake_path"} 374 | 375 | for _, path := range correctPath { 376 | cmd := exec.Command(check, withBaseArgs("", fmt.Sprintf("-%s=%s", "trusted_roots", path))...) 377 | if output, err := cmd.CombinedOutput(); err != nil { 378 | t.Errorf("%s correct path for trusted roots failed unexpectedly: %v, %s", cmd, err, output) 379 | } 380 | } 381 | for _, path := range fakePath { 382 | cmd := exec.Command(check, withBaseArgs("", fmt.Sprintf("-%s=%s", "trusted_roots", path))...) 383 | if output, err := cmd.CombinedOutput(); err == nil { 384 | t.Errorf("%s fake path for trusted roots succeeded unexpectedly: %s", cmd, output) 385 | } 386 | } 387 | } 388 | -------------------------------------------------------------------------------- /validate/validate.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Package validate provides the library functions to validate a TDX quote 16 | package validate 17 | 18 | import ( 19 | "bytes" 20 | "encoding/binary" 21 | "encoding/hex" 22 | "fmt" 23 | 24 | "github.com/google/go-tdx-guest/abi" 25 | ccpb "github.com/google/go-tdx-guest/proto/checkconfig" 26 | pb "github.com/google/go-tdx-guest/proto/tdx" 27 | vr "github.com/google/go-tdx-guest/verify" 28 | "github.com/google/logger" 29 | "go.uber.org/multierr" 30 | ) 31 | 32 | const ( 33 | // If bit X is 1 in xfamFixed1, it must be 1 in any xfam. 34 | xfamFixed1 = 0x00000003 35 | // If bit X is 0 in xfamFixed0, it must be 0 in any xfam. 36 | xfamFixed0 = 0x0006DBE7 37 | // If bit X is 1 in tdAttributesFixed1, it must be 1 in any tdAttributes. 38 | tdAttributesFixed1 = 0x0 39 | tdxAttributesSeptVeDisSupport = 1 << 28 40 | tdxAttributesPksSupport = 1 << 30 41 | tdxAttributesPerfmonSupport = 1 << 63 42 | // Supported ATTRIBUTES bits depend on the supported features - bits 0 (DEBUG), 30 (PKS), 63 (PERFMON) 43 | // and 28 (SEPT VE DISABLE) 44 | // If bit X is 0 in tdAttributesFixed0, it must be 0 in any tdAttributes. 45 | tdAttributesFixed0 = 0x1 | tdxAttributesSeptVeDisSupport | tdxAttributesPksSupport | tdxAttributesPerfmonSupport 46 | tdAttributesDebugBit = 0x1 47 | rtmrsCount = 4 48 | ) 49 | 50 | // Options represents validation options for a TDX attestation Quote. 51 | type Options struct { 52 | HeaderOptions HeaderOptions 53 | TdQuoteBodyOptions TdQuoteBodyOptions 54 | } 55 | 56 | // HeaderOptions represents validation options for a TDX attestation Quote Header. 57 | type HeaderOptions struct { 58 | // MinimumQeSvn is the minimum QE security version number. Not checked if nil. 59 | MinimumQeSvn uint16 60 | // MinimumPceSvn is the minimum PCE security version number. Not checked if nil. 61 | MinimumPceSvn uint16 62 | // QeVendorID is the expected QE_VENDOR_ID field. Must be nil or 16 bytes long. Not checked if nil. 63 | QeVendorID []byte 64 | } 65 | 66 | // TdQuoteBodyOptions represents validation options for a TDX attestation Quote's TD Quote body. 67 | type TdQuoteBodyOptions struct { 68 | // MinimumTeeTcbSvn is the component-wise minimum TEE_TCB security version number. Must be nil or 16 bytes long. Not checked if nil. 69 | MinimumTeeTcbSvn []byte 70 | // MrSeam is the expected MR_SEAM field. Must be nil or 48 bytes long. Not checked if nil. 71 | MrSeam []byte 72 | // TdAttributes is the expected TD_ATTRIBUTES field. Must be nil or 8 bytes long. Not checked if nil. 73 | TdAttributes []byte 74 | // Xfam is the expected XFAM field. Must be nil or 8 bytes long. Not checked if nil. 75 | Xfam []byte 76 | // MrTd is the expected MR_TD field. Must be nil or 48 bytes long. Not checked if nil. 77 | MrTd []byte 78 | // MrConfigID is the expected MR_CONFIG_ID field. Must be nil or 48 bytes long. Not checked if nil. 79 | MrConfigID []byte 80 | // MrOwner is the expected MR_OWNER field. Must be nil or 48 bytes long. Not checked if nil. 81 | MrOwner []byte 82 | // MrOwnerConfig is the expected MR_OWNER_CONFIG field. Must be nil or 48 bytes long. Not checked if nil. 83 | MrOwnerConfig []byte 84 | // Rtmrs is the expected RTMRS field. Must be nil or 48 * rtmrsCount. Not checked if nil. 85 | Rtmrs [][]byte 86 | // ReportData is the expected REPORT_DATA field. Must be nil or 64 bytes long. Not checked if nil. 87 | ReportData []byte 88 | // MrTd is any permitted MR_TD field. Must be nil or each entry 48 bytes long. Not checked if nil. 89 | AnyMrTd [][]byte 90 | // EnableTdDebugCheck determines whether the DEBUG bit (bit 0) in TD_ATTRIBUTES is checked. 91 | // If true, the DEBUG bit must be 0. If false, the DEBUG bit is not checked. 92 | EnableTdDebugCheck bool 93 | } 94 | 95 | func lengthCheck(name string, length int, value []byte) error { 96 | if value != nil && len(value) != length { 97 | return fmt.Errorf("option %q length is %d. Want %d", name, len(value), length) 98 | } 99 | return nil 100 | } 101 | 102 | func lengthCheckMany(name string, constraint func(int) error, length int, value [][]byte) error { 103 | if len(value) == 0 { 104 | return nil 105 | } 106 | if constraint != nil { 107 | if err := constraint(len(value)); err != nil { 108 | return err 109 | } 110 | } 111 | 112 | for i := range value { 113 | if len(value[i]) != 0 && len(value[i]) != length { 114 | return fmt.Errorf("option %q[%d] length is %d. Want %d", name, i, len(value), length) 115 | } 116 | } 117 | return nil 118 | } 119 | 120 | func checkOptionsLengths(opts *Options) error { 121 | eqConstraint := func(size int) error { 122 | if size != rtmrsCount { 123 | return fmt.Errorf("option 'rtmrs' size is %d. Want %d", size, rtmrsCount) 124 | } 125 | return nil 126 | } 127 | return multierr.Combine( 128 | lengthCheck("mr_seam", abi.MrSeamSize, opts.TdQuoteBodyOptions.MrSeam), 129 | lengthCheck("td_attributes", abi.TdAttributesSize, opts.TdQuoteBodyOptions.TdAttributes), 130 | lengthCheck("xfam", abi.XfamSize, opts.TdQuoteBodyOptions.Xfam), 131 | lengthCheck("mr_td", abi.MrTdSize, opts.TdQuoteBodyOptions.MrTd), 132 | lengthCheck("mr_config_id", abi.MrConfigIDSize, opts.TdQuoteBodyOptions.MrConfigID), 133 | lengthCheck("mr_owner", abi.MrOwnerSize, opts.TdQuoteBodyOptions.MrOwner), 134 | lengthCheck("mr_owner_config", abi.MrOwnerConfigSize, opts.TdQuoteBodyOptions.MrOwnerConfig), 135 | lengthCheck("report_data", abi.ReportDataSize, opts.TdQuoteBodyOptions.ReportData), 136 | lengthCheck("qe_vendor_id", abi.QeVendorIDSize, opts.HeaderOptions.QeVendorID), 137 | lengthCheckMany("rtmrs", eqConstraint, abi.RtmrSize, opts.TdQuoteBodyOptions.Rtmrs), 138 | lengthCheckMany("any_mr_td", nil, abi.MrTdSize, opts.TdQuoteBodyOptions.AnyMrTd), 139 | ) 140 | } 141 | 142 | // PolicyToOptions returns an Options object that is represented by a Policy message. 143 | func PolicyToOptions(policy *ccpb.Policy) (*Options, error) { 144 | if policy.GetHeaderPolicy().GetMinimumQeSvn() > 65535 { 145 | return nil, fmt.Errorf("minimum_qe_svn is %d. Expect 0-65535", policy.GetHeaderPolicy().GetMinimumQeSvn()) 146 | } 147 | if policy.GetHeaderPolicy().GetMinimumPceSvn() > 65535 { 148 | return nil, fmt.Errorf("minimum_pce_svn is %d. Expect 0-65535", policy.GetHeaderPolicy().GetMinimumPceSvn()) 149 | } 150 | opts := &Options{ 151 | HeaderOptions: HeaderOptions{ 152 | MinimumQeSvn: uint16(policy.GetHeaderPolicy().GetMinimumQeSvn()), 153 | MinimumPceSvn: uint16(policy.GetHeaderPolicy().GetMinimumPceSvn()), 154 | QeVendorID: policy.GetHeaderPolicy().GetQeVendorId(), 155 | }, 156 | TdQuoteBodyOptions: TdQuoteBodyOptions{ 157 | MinimumTeeTcbSvn: policy.GetTdQuoteBodyPolicy().GetMinimumTeeTcbSvn(), 158 | MrSeam: policy.GetTdQuoteBodyPolicy().GetMrSeam(), 159 | TdAttributes: policy.GetTdQuoteBodyPolicy().GetTdAttributes(), 160 | Xfam: policy.GetTdQuoteBodyPolicy().GetXfam(), 161 | MrTd: policy.GetTdQuoteBodyPolicy().GetMrTd(), 162 | MrConfigID: policy.GetTdQuoteBodyPolicy().GetMrConfigId(), 163 | MrOwner: policy.GetTdQuoteBodyPolicy().GetMrOwner(), 164 | MrOwnerConfig: policy.GetTdQuoteBodyPolicy().GetMrOwnerConfig(), 165 | Rtmrs: policy.GetTdQuoteBodyPolicy().GetRtmrs(), 166 | ReportData: policy.GetTdQuoteBodyPolicy().GetReportData(), 167 | AnyMrTd: policy.GetTdQuoteBodyPolicy().GetAnyMrTd(), 168 | EnableTdDebugCheck: policy.GetTdQuoteBodyPolicy().GetEnableTdDebugCheck(), 169 | }, 170 | } 171 | 172 | if err := checkOptionsLengths(opts); err != nil { 173 | return nil, err 174 | } 175 | return opts, nil 176 | } 177 | 178 | func byteCheckRtmr(size int, given, required [][]byte) error { 179 | if len(required) == 0 { 180 | logger.V(1).Info("Skipping validation check for RTMRs field: input provided is nil") 181 | return nil 182 | } 183 | if len(required) != rtmrsCount { 184 | return fmt.Errorf("RTMR field size(%d) is not equal to expected size(4)", len(required)) 185 | } 186 | for i, bs := range required { 187 | if err := byteCheck("Rtmrs", fmt.Sprintf("RTMR[%d]", i+1), size, given[i], bs); err != nil { 188 | return err 189 | } 190 | } 191 | return nil 192 | } 193 | 194 | func byteCheckAny(size int, given []byte, allowed [][]byte) error { 195 | if len(allowed) == 0 { 196 | logger.V(1).Info("Skipping validation check for MRTD field: input provided is nil") 197 | return nil 198 | } 199 | for i, bs := range allowed { 200 | if err := byteCheck("MrTd", fmt.Sprintf("AnyMrTd[%d]", i), size, given, bs); err == nil { 201 | return nil 202 | } 203 | } 204 | return fmt.Errorf("no value in AnyMrTd matched %s", hex.EncodeToString(given)) 205 | } 206 | 207 | func byteCheck(option, field string, size int, given, required []byte) error { 208 | if len(required) == 0 { 209 | logger.V(1).Infof("Skipping validation check for %s field: input provided is nil", field) 210 | return nil 211 | } 212 | if len(required) != size { 213 | return fmt.Errorf("option %v must be nil or %d bytes", option, size) 214 | } 215 | 216 | logger.V(2).Infof("Quote field %s value is %s, and expected value is %s", field, hex.EncodeToString(given), hex.EncodeToString(required)) 217 | if !bytes.Equal(required, given) { 218 | return fmt.Errorf("quote field %s is %s. Expect %s", 219 | field, hex.EncodeToString(given), hex.EncodeToString(required)) 220 | } 221 | 222 | logger.V(1).Infof("Successfully validated %s field", field) 223 | return nil 224 | } 225 | 226 | func exactByteMatch(quote *pb.QuoteV4, opts *Options) error { 227 | givenRtmr := quote.GetTdQuoteBody().GetRtmrs() 228 | return multierr.Combine( 229 | byteCheck("MrSeam", "MR_SEAM", abi.MrSeamSize, quote.GetTdQuoteBody().GetMrSeam(), opts.TdQuoteBodyOptions.MrSeam), 230 | byteCheck("TdAttributes", "TD_ATTRIBUTES", abi.TdAttributesSize, quote.GetTdQuoteBody().GetTdAttributes(), opts.TdQuoteBodyOptions.TdAttributes), 231 | byteCheck("Xfam", "XFAM", abi.XfamSize, quote.GetTdQuoteBody().GetXfam(), opts.TdQuoteBodyOptions.Xfam), 232 | byteCheck("MrTd", "MR_TD", abi.MrTdSize, quote.GetTdQuoteBody().GetMrTd(), opts.TdQuoteBodyOptions.MrTd), 233 | byteCheck("MrConfigID", "MR_CONFIG_ID", abi.MrConfigIDSize, quote.GetTdQuoteBody().GetMrConfigId(), opts.TdQuoteBodyOptions.MrConfigID), 234 | byteCheck("MrOwner", "MR_OWNER", abi.MrOwnerSize, quote.GetTdQuoteBody().GetMrOwner(), opts.TdQuoteBodyOptions.MrOwner), 235 | byteCheck("MrOwnerConfig", "MR_OWNER_CONFIG", abi.MrOwnerConfigSize, quote.GetTdQuoteBody().GetMrOwnerConfig(), opts.TdQuoteBodyOptions.MrOwnerConfig), 236 | byteCheckRtmr(abi.RtmrSize, givenRtmr, opts.TdQuoteBodyOptions.Rtmrs), 237 | byteCheckAny(abi.MrTdSize, quote.GetTdQuoteBody().GetMrTd(), opts.TdQuoteBodyOptions.AnyMrTd), 238 | byteCheck("ReportData", "REPORT_DATA", abi.ReportDataSize, quote.GetTdQuoteBody().GetReportData(), opts.TdQuoteBodyOptions.ReportData), 239 | byteCheck("QeVendorID", "QE_VENDOR_ID", abi.QeVendorIDSize, quote.GetHeader().GetQeVendorId(), opts.HeaderOptions.QeVendorID), 240 | ) 241 | } 242 | 243 | func isSvnHigherOrEqual(quoteSvn []byte, optionSvn []byte) bool { 244 | if optionSvn == nil { 245 | return true 246 | } 247 | for i := range quoteSvn { 248 | if quoteSvn[i] < optionSvn[i] { 249 | return false 250 | } 251 | } 252 | return true 253 | } 254 | 255 | func minVersionCheck(quote *pb.QuoteV4, opts *Options) error { 256 | logger.V(1).Info("Setting the minimum_qe_svn parameter value to ", opts.HeaderOptions.MinimumQeSvn) 257 | logger.V(1).Info("Setting the minimum_pce_svn parameter value to ", opts.HeaderOptions.MinimumPceSvn) 258 | logger.V(1).Info("Setting the minimum_tee_tcb_svn parameter value to ", opts.TdQuoteBodyOptions.MinimumTeeTcbSvn) 259 | 260 | logger.V(2).Infof("TEE TCB security-version number is %v, and minimum_tee_tcb_svn value is %v", quote.GetTdQuoteBody().GetTeeTcbSvn(), opts.TdQuoteBodyOptions.MinimumTeeTcbSvn) 261 | if !isSvnHigherOrEqual(quote.GetTdQuoteBody().GetTeeTcbSvn(), opts.TdQuoteBodyOptions.MinimumTeeTcbSvn) { 262 | return fmt.Errorf("TEE TCB security-version number %d is less than the required minimum %d", 263 | quote.GetTdQuoteBody().GetTeeTcbSvn(), opts.TdQuoteBodyOptions.MinimumTeeTcbSvn) 264 | } 265 | 266 | logger.V(1).Info("Successfully validated TEE TCB security-version number") 267 | qeSvn := binary.LittleEndian.Uint16(quote.GetHeader().GetQeSvn()) 268 | pceSvn := binary.LittleEndian.Uint16(quote.GetHeader().GetPceSvn()) 269 | logger.V(2).Infof("QE security-version number is %d, and minimum_qe_svn value is %d", qeSvn, opts.HeaderOptions.MinimumQeSvn) 270 | if qeSvn < opts.HeaderOptions.MinimumQeSvn { 271 | return fmt.Errorf("QE security-version number %d is less than the required minimum %d", 272 | qeSvn, opts.HeaderOptions.MinimumQeSvn) 273 | } 274 | logger.V(1).Info("Successfully validated QE security-version number") 275 | 276 | logger.V(2).Infof("PCE security-version number is %d, and minimum_pce_svn value is %d", pceSvn, opts.HeaderOptions.MinimumPceSvn) 277 | if pceSvn < opts.HeaderOptions.MinimumPceSvn { 278 | return fmt.Errorf("PCE security-version number %d is less than the required minimum %d", 279 | pceSvn, opts.HeaderOptions.MinimumPceSvn) 280 | } 281 | 282 | logger.V(1).Info("Successfully validated PCE security-version number") 283 | return nil 284 | } 285 | 286 | func validateXfam(value []byte, fixed1, fixed0 uint64) error { 287 | if len(value) == 0 { 288 | return nil 289 | } 290 | if len(value) != abi.XfamSize { 291 | return fmt.Errorf("xfam size is invalid") 292 | } 293 | xfam := binary.LittleEndian.Uint64(value) 294 | logger.V(2).Infof("XFAM value is %v, XFAMFixed0 value is %v and XFAMFixed1 value is %v", xfam, fixed0, fixed1) 295 | if xfam&fixed1 != fixed1 { 296 | return fmt.Errorf("unauthorized xfam 0x%x as xfamFixed1 0x%x bits are unset", xfam, fixed1) 297 | } 298 | if xfam&(^fixed0) != 0 { 299 | return fmt.Errorf("unauthorized xfam 0x%x as xfamFixed0 0x%x bits are set", xfam, fixed0) 300 | } 301 | logger.V(1).Info("Successfully validated XFAM field") 302 | return nil 303 | } 304 | 305 | func validateTdAttributes(value []byte, fixed1, fixed0 uint64, enableTdDebugCheck bool) error { 306 | if len(value) == 0 { 307 | return nil 308 | } 309 | if len(value) != abi.TdAttributesSize { 310 | return fmt.Errorf("tdAttributes size is invalid") 311 | } 312 | tdAttributes := binary.LittleEndian.Uint64(value) 313 | 314 | if enableTdDebugCheck { 315 | if (tdAttributes & tdAttributesDebugBit) != 0 { 316 | return fmt.Errorf("TD_ATTRIBUTES DEBUG bit is set, but debug is not allowed") 317 | } 318 | } 319 | 320 | logger.V(2).Infof("TdAttributes value is %v, TdAttributesFixed0 value is %v and TdAttributesFixed1 value is %v", tdAttributes, fixed0, fixed1) 321 | if tdAttributes&fixed1 != fixed1 { 322 | return fmt.Errorf("unauthorized tdAttributes 0x%x as tdAttributesFixed1 0x%x bits are unset", tdAttributes, fixed1) 323 | } 324 | if tdAttributes&(^fixed0) != 0 { 325 | return fmt.Errorf("unauthorized tdAttributes 0x%x as tdAttributesFixed0 0x%x bits are set", tdAttributes, fixed0) 326 | } 327 | logger.V(1).Info("Successfully validated TdAttributes field") 328 | return nil 329 | } 330 | 331 | // TdxQuote validates fields of the protobuf representation of an attestation Quote 332 | // against expectations depending on supported quote formats - QuoteV4. 333 | // Does not check the attestation certificates or signature. 334 | func TdxQuote(quote any, options *Options) error { 335 | if options == nil { 336 | return vr.ErrOptionsNil 337 | } 338 | switch q := quote.(type) { 339 | case *pb.QuoteV4: 340 | return tdxQuoteV4(q, options) 341 | default: 342 | return fmt.Errorf("unsupported quote type: %T", quote) 343 | } 344 | } 345 | 346 | // tdxQuoteV4 validates QuoteV4 fields of the protobuf representation of an attestation Quote 347 | // against expectations. Does not check the attestation certificates or signature. 348 | func tdxQuoteV4(quote *pb.QuoteV4, options *Options) error { 349 | if err := abi.CheckQuoteV4(quote); err != nil { 350 | return fmt.Errorf("QuoteV4 invalid: %v", err) 351 | } 352 | logger.V(1).Info("Validating the TDX Quote using input parameters") 353 | return multierr.Combine( 354 | exactByteMatch(quote, options), 355 | minVersionCheck(quote, options), 356 | validateXfam(quote.GetTdQuoteBody().GetXfam(), xfamFixed1, xfamFixed0), 357 | validateTdAttributes(quote.GetTdQuoteBody().GetTdAttributes(), tdAttributesFixed1, tdAttributesFixed0, options.TdQuoteBodyOptions.EnableTdDebugCheck), 358 | ) 359 | } 360 | 361 | // RawTdxQuote checks the raw bytes representation of an attestation quote. 362 | func RawTdxQuote(raw []byte, options *Options) error { 363 | quote, err := abi.QuoteToProto(raw) 364 | if err != nil { 365 | return fmt.Errorf("could not convert raw bytes to QuoteV4: %v", err) 366 | } 367 | return TdxQuote(quote, options) 368 | } 369 | -------------------------------------------------------------------------------- /tools/check/check.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Package main implements a CLI tool for checking Intel TDX quotes. 16 | package main 17 | 18 | import ( 19 | "encoding/hex" 20 | "errors" 21 | "flag" 22 | "fmt" 23 | "io" 24 | "os" 25 | "strconv" 26 | "strings" 27 | "time" 28 | 29 | "github.com/google/go-eventlog/ccel" 30 | "github.com/google/go-eventlog/extract" 31 | "github.com/google/go-sev-guest/tools/lib/cmdline" 32 | "github.com/google/go-tdx-guest/abi" 33 | ccpb "github.com/google/go-tdx-guest/proto/checkconfig" 34 | pb "github.com/google/go-tdx-guest/proto/tdx" 35 | "github.com/google/go-tdx-guest/rtmr" 36 | testcases "github.com/google/go-tdx-guest/testing" 37 | "github.com/google/go-tdx-guest/validate" 38 | "github.com/google/go-tdx-guest/verify" 39 | "github.com/google/go-tdx-guest/verify/trust" 40 | "github.com/google/logger" 41 | "go.uber.org/multierr" 42 | "google.golang.org/protobuf/encoding/prototext" 43 | "google.golang.org/protobuf/proto" 44 | ) 45 | 46 | const ( 47 | defaultCheckCrl = false 48 | defaultGetCollateral = false 49 | defaultTimeout = 2 * time.Minute 50 | defaultMaxRetryDelay = 30 * time.Second 51 | defaultMinQeSvn = 0 52 | defaultMinPceSvn = 0 53 | 54 | // Exit code 1 - tool usage error. 55 | exitTool = 1 56 | // Exit code 2 - quote verification error. 57 | exitVerify = 2 58 | // Exit code 3 - network error while downloading collateral. 59 | exitNetwork = 3 60 | // Exit code 4 - the quote did not validate according to policy. 61 | exitPolicy = 4 62 | // Exit code 5 - CCEL event log parsing error. 63 | exitParse = 5 64 | ccelEventLogFile = "/sys/firmware/acpi/tables/data/CCEL" 65 | acpiTableFile = "/sys/firmware/acpi/tables/CCEL" 66 | parseCcelOutputFile = "result.textproto" 67 | ) 68 | 69 | var ( 70 | infile = flag.String("in", "-", "Path to the TDX quote to check. Stdin is \"-\".") 71 | inform = flag.String("inform", "bin", "The input format for the TDX quote. One of \"bin\", \"proto\", \"textproto\".") 72 | 73 | configProto = flag.String("config", "", 74 | ("A path to a serialized check.Config protobuf. Any individual field flags will" + 75 | " overwrite the message's associated field. By default, the file will be unmarshalled as binary," + 76 | " but if it ends in .textproto, it will be unmarshalled as prototext instead.")) 77 | quiet = flag.Bool("quiet", false, "If true, writes nothing the stdout or stderr. Success is exit code 0, failure exit code 1.") 78 | parseCCEL = flag.Bool("parse_ccel", false, "If true, parses the CCEL, and replays the events against the RTMR values from the verified TD quote. This will dump the result into a \"result.textproto\" file under the current directory.") 79 | verbosity = flag.Int("verbosity", 0, "The output verbosity. Higher number means more verbose output") 80 | 81 | qevendoridS = flag.String("qe_vendor_id", "", "The expected QE_VENDOR_ID field as a hex string. Must encode 16 bytes. Unchecked if unset.") 82 | qevendorid = cmdline.Bytes("-qe_vendor_id", abi.QeVendorIDSize, qevendoridS) 83 | mrseamS = flag.String("mr_seam", "", "The expected MR_SEAM field as a hex string. Must encode 48 bytes. Unchecked if unset.") 84 | mrseam = cmdline.Bytes("-mr_seam", abi.MrSeamSize, mrseamS) 85 | tdattributesS = flag.String("td_attributes", "", "The expected TD_ATTRIBUTES field as a hex string. Must encode 8 bytes. Unchecked if unset.") 86 | tdattributes = cmdline.Bytes("-td_attributes", abi.TdAttributesSize, tdattributesS) 87 | xfamS = flag.String("xfam", "", "The expected XFAM field as a hex string. Must encode 8 bytes. Unchecked if unset.") 88 | xfam = cmdline.Bytes("-xfam", abi.XfamSize, xfamS) 89 | mrtdS = flag.String("mr_td", "", "The expected MR_TD field as a hex string. Must encode 48 bytes. Unchecked if unset.") 90 | mrtd = cmdline.Bytes("-mr_td", abi.MrTdSize, mrtdS) 91 | mrconfigidS = flag.String("mr_config_id", "", "The expected MR_CONFIG_ID field as a hex string. Must encode 48 bytes. Unchecked if unset.") 92 | mrconfigid = cmdline.Bytes("-mr_config_id", abi.MrConfigIDSize, mrconfigidS) 93 | mrownerS = flag.String("mr_owner", "", "The expected MR_OWNER field as a hex string. Must encode 48 bytes. Unchecked if unset.") 94 | mrowner = cmdline.Bytes("-mr_owner", abi.MrOwnerSize, mrownerS) 95 | mrownerconfigS = flag.String("mr_owner_config", "", "The expected MR_OWNER_CONFIG field as a hex string. Must encode 48 bytes. Unchecked if unset.") 96 | mrownerconfig = cmdline.Bytes("-mr_owner_config", abi.MrOwnerConfigSize, mrownerconfigS) 97 | reportdataS = flag.String("report_data", "", "The expected REPORT_DATA field as a hex string. Must encode 64 bytes. Unchecked if unset.") 98 | reportdata = cmdline.Bytes("-report_data", abi.ReportDataSize, reportdataS) 99 | minteetcbsvnS = flag.String("minimum_tee_tcb_svn", "", "The minimum acceptable value for TEE_TCB_SVN field as a hex string. Must encode 16 bytes. Unchecked if unset.") 100 | minteetcbsvn = cmdline.Bytes("-minimum_tee_tcb_svn", abi.TeeTcbSvnSize, minteetcbsvnS) 101 | 102 | rtmrs = flag.String("rtmrs", "", 103 | "Comma-separated hex strings representing expected values of RTMRS field. Expected 4 strings, either empty or each must encode 48 bytes. Unchecked if unset") 104 | 105 | cabundles = flag.String("trusted_roots", "", 106 | "Comma-separated paths to CA bundles for the Intel TDX. Must be in PEM format, Root CA certificate. If unset, uses embedded root certificate.") 107 | // Optional Uint16. We don't want 0 to override the policy message, so instead of parsing 108 | // as Uint16 up front, we keep the flag a string and parse later if given. 109 | minqesvn = flag.String("minimum_qe_svn", "", "The minimum acceptable value for QE_SVN field.") 110 | minpcesvn = flag.String("minimum_pce_svn", "", "The minimum acceptable value for PCE_SVN field.") 111 | 112 | // Optional Bool 113 | checkcrl = flag.String("check_crl", "", "Download and check the CRL for revoked certificates. -get_collateral must be true.") 114 | getcollateral = flag.String("get_collateral", "", "If true, then permitted to download necessary collaterals for additional checks.") 115 | timeout = flag.Duration("timeout", defaultTimeout, "Duration to continue to retry failed HTTP requests.") 116 | maxRetryDelay = flag.Duration("max_retry_delay", defaultMaxRetryDelay, "Maximum Duration to wait between HTTP request retries.") 117 | testLocalGetter = flag.Bool("test_local_getter", false, "Use this flag only to test this CLI tool when network access is not available") 118 | 119 | // Assign the values of the flags to the corresponding proto fields 120 | config = &ccpb.Config{ 121 | RootOfTrust: &ccpb.RootOfTrust{}, 122 | Policy: &ccpb.Policy{HeaderPolicy: &ccpb.HeaderPolicy{}, TdQuoteBodyPolicy: &ccpb.TDQuoteBodyPolicy{}}, 123 | } 124 | ) 125 | 126 | func parseQuoteBytes(b []byte) (any, error) { 127 | quote, err := abi.QuoteToProto(b) 128 | if err != nil { 129 | return nil, fmt.Errorf("could not parse the TDX Quote from %q: %v", *infile, err) 130 | } 131 | 132 | return quote, nil 133 | } 134 | 135 | func parseQuote(b []byte) (any, error) { 136 | switch *inform { 137 | case "bin": 138 | return parseQuoteBytes(b) 139 | case "proto": 140 | result := &pb.QuoteV4{} 141 | if err := proto.Unmarshal(b, result); err != nil { 142 | return nil, fmt.Errorf("could not parse %q as proto: %v", *infile, err) 143 | } 144 | return result, nil 145 | case "textproto": 146 | result := &pb.QuoteV4{} 147 | if err := prototext.Unmarshal(b, result); err != nil { 148 | return nil, fmt.Errorf("could not parse %q as textproto: %v", *infile, err) 149 | } 150 | return result, nil 151 | default: 152 | return nil, fmt.Errorf("unknown value -inform=%s", *inform) 153 | } 154 | } 155 | 156 | func parseUint(p string, bits int) (uint64, error) { 157 | base := 10 158 | prepped := p 159 | if strings.HasPrefix(strings.ToLower(p), "0x") { 160 | base = 16 161 | prepped = prepped[2:] 162 | } else if strings.HasPrefix(strings.ToLower(p), "0o") { 163 | base = 8 164 | prepped = prepped[2:] 165 | } else if strings.HasPrefix(strings.ToLower(p), "0b") { 166 | base = 2 167 | prepped = prepped[2:] 168 | } 169 | info64, err := strconv.ParseUint(prepped, base, bits) 170 | if err != nil { 171 | return 0, fmt.Errorf("%q must be empty or a %d-bit number: %v", p, bits, err) 172 | } 173 | return info64, nil 174 | } 175 | 176 | func readQuote() (any, error) { 177 | var in io.Reader 178 | var f *os.File 179 | if *infile == "-" { 180 | in = os.Stdin 181 | } else { 182 | file, err := os.Open(*infile) 183 | if err != nil { 184 | return nil, fmt.Errorf("could not open %q: %v", *infile, err) 185 | } 186 | f = file 187 | in = file 188 | } 189 | defer func() { 190 | if f != nil { 191 | f.Close() 192 | } 193 | }() 194 | 195 | contents, err := io.ReadAll(in) 196 | if err != nil { 197 | return nil, fmt.Errorf("could not read %q: %v", *infile, err) 198 | } 199 | return parseQuote(contents) 200 | } 201 | 202 | func dieWith(err error, exitCode int) { 203 | if !*quiet { 204 | fmt.Fprintf(os.Stderr, "FATAL: %v\n", err) 205 | } 206 | os.Exit(exitCode) 207 | } 208 | 209 | func die(err error) { 210 | dieWith(err, exitTool) 211 | } 212 | 213 | func parseConfig(path string) error { 214 | if path == "" { 215 | return nil 216 | } 217 | 218 | contents, err := os.ReadFile(path) 219 | if err != nil { 220 | return fmt.Errorf("could not read %q: %v", path, err) 221 | } 222 | if strings.HasSuffix(path, ".textproto") { 223 | err = prototext.Unmarshal(contents, config) 224 | } else { 225 | err = proto.Unmarshal(contents, config) 226 | } 227 | if err != nil { 228 | return fmt.Errorf("could not deserialize %q: %v", path, err) 229 | } 230 | // Populate fields that should not be nil 231 | if config.RootOfTrust == nil { 232 | config.RootOfTrust = &ccpb.RootOfTrust{} 233 | } 234 | if config.Policy == nil { 235 | config.Policy = &ccpb.Policy{HeaderPolicy: &ccpb.HeaderPolicy{}, TdQuoteBodyPolicy: &ccpb.TDQuoteBodyPolicy{}} 236 | } 237 | return nil 238 | } 239 | 240 | func parsePaths(s string) ([]string, error) { 241 | if s == "" { 242 | return nil, nil 243 | } 244 | paths := strings.Split(s, ",") 245 | var result []string 246 | for _, path := range paths { 247 | p := strings.TrimSpace(path) 248 | stat, err := os.Stat(p) 249 | if err != nil { 250 | return nil, fmt.Errorf("path error for %q: %v", p, err) 251 | } 252 | if stat.IsDir() { 253 | return nil, fmt.Errorf("path is not a file: %q", p) 254 | } 255 | result = append(result, p) 256 | } 257 | return result, nil 258 | } 259 | 260 | func parseRtmrs(s string) ([][]byte, error) { 261 | if s == "" { 262 | return nil, nil 263 | } 264 | hexstrings := strings.Split(s, ",") 265 | var result [][]byte 266 | for _, hexstring := range hexstrings { 267 | h, err := hex.DecodeString(strings.TrimSpace(hexstring)) 268 | if err != nil { 269 | return nil, fmt.Errorf("could not parse RTMRS value as hex-encoded string: %q", hexstring) 270 | } 271 | result = append(result, h) 272 | } 273 | return result, nil 274 | } 275 | 276 | func setBool(value *bool, name, flag string, defaultValue bool) error { 277 | if flag == "" { 278 | if !configProtoPresent() { 279 | *value = defaultValue 280 | } 281 | } else if flag == "true" { 282 | *value = true 283 | } else if flag == "false" { 284 | *value = false 285 | } else { 286 | return fmt.Errorf("flag -%s=%s invalid. Must be one of unset, \"true\", or \"false\"", 287 | name, flag) 288 | } 289 | return nil 290 | } 291 | 292 | func setUint(value *uint64, bits int, name, flag string, defaultValue uint64) error { 293 | if flag == "" { 294 | if !configProtoPresent() { 295 | *value = defaultValue 296 | } 297 | } else { 298 | u, err := parseUint(flag, bits) 299 | if err != nil { 300 | return fmt.Errorf("invalid -%s=%s: %v", name, flag, err) 301 | } 302 | *value = u 303 | } 304 | return nil 305 | } 306 | 307 | func setUint32(value *uint32, name, flag string, defaultValue uint64) error { 308 | v := uint64(*value) 309 | if err := setUint(&v, 32, name, flag, defaultValue); err != nil { 310 | return err 311 | } 312 | *value = uint32(v) 313 | return nil 314 | } 315 | 316 | func configProtoPresent() bool { 317 | return *configProto != "" 318 | } 319 | 320 | func populateRootOfTrust() error { 321 | rot := config.RootOfTrust 322 | 323 | if err := setBool(&rot.CheckCrl, "check_crl", *checkcrl, defaultCheckCrl); err != nil { 324 | return err 325 | } 326 | if err := setBool(&rot.GetCollateral, "get_collateral", *getcollateral, defaultGetCollateral); err != nil { 327 | return err 328 | } 329 | paths, err := parsePaths(*cabundles) 330 | if err != nil { 331 | return err 332 | } 333 | if len(paths) > 0 { 334 | rot.CabundlePaths = paths 335 | } 336 | return nil 337 | } 338 | 339 | // Populate fields of the config proto from flags if they override. 340 | func populateConfig() error { 341 | policy := config.Policy 342 | 343 | setNonNil := func(dest *[]byte, value []byte) { 344 | if value != nil { 345 | *dest = value 346 | } 347 | } 348 | setRtmrs := func(dest *[][]byte, flag string) error { 349 | if flag != "" { 350 | val, err := parseRtmrs(flag) 351 | if err != nil { 352 | return err 353 | } 354 | *dest = val 355 | } 356 | return nil 357 | } 358 | 359 | setNonNil(&policy.HeaderPolicy.QeVendorId, *qevendorid) 360 | setNonNil(&policy.TdQuoteBodyPolicy.MinimumTeeTcbSvn, *minteetcbsvn) 361 | setNonNil(&policy.TdQuoteBodyPolicy.MrSeam, *mrseam) 362 | setNonNil(&policy.TdQuoteBodyPolicy.TdAttributes, *tdattributes) 363 | setNonNil(&policy.TdQuoteBodyPolicy.Xfam, *xfam) 364 | setNonNil(&policy.TdQuoteBodyPolicy.MrTd, *mrtd) 365 | setNonNil(&policy.TdQuoteBodyPolicy.MrConfigId, *mrconfigid) 366 | setNonNil(&policy.TdQuoteBodyPolicy.MrOwner, *mrowner) 367 | setNonNil(&policy.TdQuoteBodyPolicy.MrOwnerConfig, *mrownerconfig) 368 | setNonNil(&policy.TdQuoteBodyPolicy.ReportData, *reportdata) 369 | 370 | return multierr.Combine( 371 | setUint32(&policy.HeaderPolicy.MinimumQeSvn, "minimum_qe_svn", *minqesvn, defaultMinQeSvn), 372 | setUint32(&policy.HeaderPolicy.MinimumPceSvn, "minimum_pce_svn", *minpcesvn, defaultMinPceSvn), 373 | setRtmrs(&policy.TdQuoteBodyPolicy.Rtmrs, *rtmrs), 374 | ) 375 | } 376 | 377 | func parseCcelAndWriteOutput(quote any) error { 378 | rtmrBank, err := rtmr.GetRtmrsFromTdQuote(quote) 379 | if err != nil { 380 | return fmt.Errorf("failed to extract RTMR banks from TD quote: %w", err) 381 | } 382 | ccelData, err := os.ReadFile(ccelEventLogFile) 383 | if err != nil { 384 | return fmt.Errorf("failed to read CCEL data file: %w", err) 385 | } 386 | ccelTable, err := os.ReadFile(acpiTableFile) 387 | if err != nil { 388 | return fmt.Errorf("failed to read CCEL ACPI table file: %w", err) 389 | } 390 | fls, err := ccel.ReplayAndExtract(ccelTable, ccelData, *rtmrBank, extract.Opts{Loader: extract.GRUB}) 391 | if err != nil { 392 | return fmt.Errorf("failed to extract CCEL event log info: %w", err) 393 | } 394 | mo := prototext.MarshalOptions{ 395 | Multiline: true, 396 | Indent: " ", 397 | EmitASCII: true, 398 | } 399 | bytes, err := mo.Marshal(fls) 400 | if err != nil { 401 | return fmt.Errorf("failed to marshal FirmwareLogState to bytes: %w", err) 402 | } 403 | if err := os.WriteFile(parseCcelOutputFile, bytes, 0644); err != nil { 404 | return fmt.Errorf("failed to write output file: %w", err) 405 | } 406 | return nil 407 | } 408 | 409 | func main() { 410 | logger.Init("", false, false, os.Stdout) 411 | flag.Parse() 412 | cmdline.Parse("auto") 413 | logger.SetLevel(logger.Level(*verbosity)) 414 | 415 | logger.V(1).Info("Parsing input parameters") 416 | if err := parseConfig(*configProto); err != nil { 417 | die(err) 418 | } 419 | if err := multierr.Combine(populateRootOfTrust(), 420 | populateConfig()); err != nil { 421 | die(err) 422 | } 423 | if config.RootOfTrust.CheckCrl && !config.RootOfTrust.GetCollateral { 424 | die(errors.New("cannot specify both -check_crl=true and -get_collateral=false")) 425 | } 426 | 427 | quote, err := readQuote() 428 | if err != nil { 429 | die(err) 430 | } 431 | logger.V(1).Info("TDX Quote parsed successfully") 432 | 433 | sopts, err := verify.RootOfTrustToOptions(config.RootOfTrust) 434 | if err != nil { 435 | die(err) 436 | } 437 | logger.V(1).Info("Input parameters parsed successfully") 438 | 439 | var getter trust.HTTPSGetter 440 | getter = &trust.SimpleHTTPSGetter{} 441 | if *testLocalGetter { 442 | getter = testcases.TestGetter 443 | } 444 | sopts.Getter = &trust.RetryHTTPSGetter{ 445 | Timeout: *timeout, 446 | MaxRetryDelay: *maxRetryDelay, 447 | Getter: getter, 448 | } 449 | 450 | logger.V(1).Info("Verifying the TDX Quote from input") 451 | if err := verify.TdxQuote(quote, sopts); err != nil { 452 | // Make the exit code more helpful when there are network errors 453 | // that affected the result. 454 | exitCode := exitVerify 455 | clarify := func(err error) bool { 456 | if err == nil { 457 | return false 458 | } 459 | var crlNetworkErr *verify.CRLUnavailableErr 460 | var collateralNetworkErr *trust.AttestationRecreationErr 461 | if errors.As(err, &crlNetworkErr) || errors.As(err, &collateralNetworkErr) { 462 | exitCode = exitNetwork 463 | return true 464 | } 465 | 466 | return false 467 | } 468 | if !clarify(err) { 469 | clarify(errors.Unwrap(err)) 470 | } 471 | dieWith(fmt.Errorf("could not verify the TDX Quote: %v", err), exitCode) 472 | } 473 | logger.Info("TDX Quote verified successfully") 474 | 475 | opts, err := validate.PolicyToOptions(config.Policy) 476 | if err != nil { 477 | die(err) 478 | } 479 | if err := validate.TdxQuote(quote, opts); err != nil { 480 | dieWith(fmt.Errorf("error validating the TDX Quote: %v", err), exitPolicy) 481 | } 482 | if *parseCCEL { 483 | if err := parseCcelAndWriteOutput(quote); err != nil { 484 | dieWith(fmt.Errorf("error parsing CCEL event log: %v", err), exitParse) 485 | } 486 | } 487 | logger.V(1).Info("TDX Quote validated successfully") 488 | } 489 | --------------------------------------------------------------------------------