Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion coordinator/internal/stateguard/credentials.go
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ func (c *Credentials) ServerHandshake(rawConn net.Conn) (net.Conn, credentials.A
}
for i, opt := range tdxOpts {
name := fmt.Sprintf("tdx-%d", i)
validators = append(validators, tdx.NewValidatorWithReportSetter(opt.VerifyOpts, &tdx.StaticValidateOptsGenerator{Opts: opt.ValidateOpts},
validators = append(validators, tdx.NewValidatorWithReportSetter(opt.VerifyOpts, &tdx.StaticValidateOptsGenerator{Opts: opt.ValidateOpts}, opt.AllowedPIIDs,
logger.NewWithAttrs(logger.NewNamed(c.logger, "validator"), map[string]string{"reference-values": name}), &authInfo, name))
}

Expand Down
6 changes: 5 additions & 1 deletion dev-docs/e2e/tcb-specs.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,11 @@
],
"tdx": [
{
"MrSeam": "7bf063280e94fb051f5dd7b1fc59ce9aac42bb961df8d44b709c9b0ff87a7b4df648657ba6d1189589feab1d5a3c9a9d"
"MrSeam": "7bf063280e94fb051f5dd7b1fc59ce9aac42bb961df8d44b709c9b0ff87a7b4df648657ba6d1189589feab1d5a3c9a9d",
"AllowedPIIDs": [
"59028de46725be119ee89a10002b191c",
"e90210702a2cc5ad9764f29ddc8fde8c"
]
}
]
}
55 changes: 55 additions & 0 deletions docs/docs/architecture/components/manifest.md
Original file line number Diff line number Diff line change
Expand Up @@ -305,6 +305,59 @@ Attributes specifies various attested properties of the TDX VM and is documented

The extended features available mask (`XFAM`) determines the set of extended features available for use by the guest and is documented in Section 3.4.2 (`XFAM`) in the [TDX ABI Spec].

### `ReferenceValues.tdx.AllowedPIIDs`

These are matched against the `PIID` field from the PCK certificate, as documented in section 1.3.5 of the [SGX PCK Spec].
If the list is empty or null, all PIIDs are accepted.

In case hardware is operated by you instead of a third party, or you are able to gain physical access to the hardware to audit it,
you can obtain the PIID with the following steps:

1. Install and run Intel's [`PCKIDRetrievalTool`].
This should place a CSV file in your working directory.
2. Retrieve the following fields from the CSV file:
- `EncryptedPPID`
- `PCE_ID`
- `CPUSVN`
- `PCE ISVSVN`
3. Use these values to [request a PCK certificate from Intel PCS](https://api.portal.trustedservices.intel.com/content/documentation.html#pcs-certificate-v4).
Note that the response contains intermediate certificates in the `SGX-PCK-Certificate-Issuer-Chain` header that are required to verify the PCK certificate's signature.
4. Verify that the PCK certificate chains back to Intel's root, for example with `openssl verify`.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the attestation flow usually relying on general PKI? Otherwise this might weaken our security regarding MITM, assuming we are only using a specific pinned certificate elsewhere to verify quotes/certs.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The library uses the TrustedRoots option to verify the PCK certificate (verifyPCKCertificationChain).

What I'm trying to say here is that you get a PCK cert with curl and you have a trusted root cert, but in order to verify the chain you'll need to use the intermediate from the response header. Should I make that more clear?

5. Parse the PCK certificate to find the SGX extension address:

```sh
openssl asn1parse -in pck.pem
```

Example output, showing the extension address `624` right after its ASN.1 OID:

```txt
613:d=5 hl=2 l= 9 prim: OBJECT :1.2.840.113741.1.13.1
624:d=5 hl=4 l= 554 prim: OCTET STRING [HEX DUMP]: [...]
```

6. Parse the SGX extension to find the PIID:

```sh
openssl asn1parse -in pck.pem --strparse $ADDRESS
```

Example output with `ADDRESS=624`, showing the PIID right after its ASN.1 OID:

```txt
454:d=2 hl=2 l= 10 prim: OBJECT :1.2.840.113741.1.13.1.6
466:d=2 hl=2 l= 16 prim: OCTET STRING [HEX DUMP]:E90210702A2CC5AD9764F29DDC8FDE8C
```

Copy the value shown after `[HEX DUMP]` into the `AllowedPIIDs` field.

:::warning

The EncryptedPPID must be retrieved from a machine by physically accessing it.
If you retrieve this value via a remote channel, your traffic could already be redirected to a hostile environment that allows an attacker physical access.

:::

## `WorkloadOwnerKeyDigests` {#workload-owner-key-digests}

A list of workload owner public key digests.
Expand All @@ -330,3 +383,5 @@ Doing the same for the `SeedshareOwnerKeys` field makes Coordinator recovery and
[`snphost`]: https://github.com/virtee/snphost
[SEV ABI Spec]: https://www.amd.com/content/dam/amd/en/documents/developer/56860.pdf
[TDX ABI Spec]: https://cdrdv2.intel.com/v1/dl/getContent/733579
[SGX PCK Spec]: https://api.trustedservices.intel.com/documents/Intel_SGX_PCK_Certificate_CRL_Spec-1.5.pdf
[`PCKIDRetrievalTool`]: https://github.com/intel/confidential-computing.tee.dcap/blob/717f2a91ca732c3309b0c59d21757463133eb440/tools/PCKRetrievalTool/README.txt
35 changes: 35 additions & 0 deletions e2e/attestation/attestation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,41 @@ func TestAttestation(t *testing.T) {
}), "contrast set should fail due to non-allowed chip ID")
})

// Same as above, but for TDX PIIDs.
t.Run("allowed-piids", func(t *testing.T) {
platform, err := platforms.FromString(contrasttest.Flags.PlatformStr)
require.NoError(t, err)
if !platforms.IsTDX(platform) {
t.Skip()
}

require := require.New(t)
ct := contrasttest.New(t)

runtimeHandler, err := manifest.RuntimeHandler(platform)
require.NoError(err)
resources := kuberesource.CoordinatorBundle()
resources = kuberesource.PatchRuntimeHandlers(resources, runtimeHandler)
resources = kuberesource.AddPortForwarders(resources)
ct.Init(t, resources)

require.True(t.Run("generate", ct.Generate), "contrast generate needs to succeed for subsequent tests")
require.True(t.Run("apply", ct.Apply), "Kubernetes resources need to be applied for subsequent tests")

ct.PatchManifest(t, func(m manifest.Manifest) manifest.Manifest {
for i := range m.ReferenceValues.TDX {
m.ReferenceValues.TDX[i].AllowedPIIDs = []manifest.HexString{
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
}
}
return m
})
require.True(t.Run("set", func(t *testing.T) {
err := ct.RunSet(t.Context())
require.ErrorContains(err, "not in allowed PIIDs")
}), "contrast set should fail due to non-allowed PIID")
})

// Test that it is okay to have failing validators as long as one validator passes.
t.Run("non-matching-validators", func(t *testing.T) {
platform, err := platforms.FromString(contrasttest.Flags.PlatformStr)
Expand Down
1 change: 1 addition & 0 deletions e2e/internal/contrasttest/contrasttest.go
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,7 @@ func PatchReferenceValues(ctx context.Context, k *kubeclient.Kubeclient, platfor
for _, manifestTDX := range m.ReferenceValues.TDX {
for _, overwriteTDX := range baremetalRefVal.TDX {
manifestTDX.MrSeam = overwriteTDX.MrSeam
manifestTDX.AllowedPIIDs = overwriteTDX.AllowedPIIDs
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same logic needs to be added in the justfile

// Filter to only use the reference values of specified baremetal SNP runners
tdxReferenceValues = append(tdxReferenceValues, manifestTDX)
}
Expand Down
107 changes: 104 additions & 3 deletions internal/attestation/tdx/validator.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,24 @@
package tdx

import (
"bytes"
"context"
"crypto/x509"
"crypto/x509/pkix"
"encoding/asn1"
"encoding/hex"
"encoding/pem"
"fmt"
"log/slog"
"slices"

"github.com/edgelesssys/contrast/internal/attestation"
"github.com/edgelesssys/contrast/internal/oid"
"github.com/google/go-tdx-guest/proto/tdx"
"github.com/google/go-tdx-guest/validate"
"github.com/google/go-tdx-guest/verify"
"golang.org/x/crypto/cryptobyte"
cryptobyte_asn1 "golang.org/x/crypto/cryptobyte/asn1"
"google.golang.org/protobuf/encoding/protojson"
"google.golang.org/protobuf/proto"
)
Expand All @@ -24,6 +30,7 @@ import (
type Validator struct {
verifyOpts *verify.Options
validateOptsGen validateOptsGenerator
allowedPIIDs [][]byte
reportSetter attestation.ReportSetter
logger *slog.Logger
name string
Expand All @@ -45,18 +52,19 @@ func (v *StaticValidateOptsGenerator) TDXValidateOpts(_ *tdx.QuoteV4) (*validate
}

// NewValidator returns a new Validator.
func NewValidator(VerifyOpts *verify.Options, optsGen validateOptsGenerator, log *slog.Logger, name string) *Validator {
func NewValidator(VerifyOpts *verify.Options, optsGen validateOptsGenerator, allowedPIIDs [][]byte, log *slog.Logger, name string) *Validator {
return &Validator{
verifyOpts: VerifyOpts,
validateOptsGen: optsGen,
allowedPIIDs: allowedPIIDs,
logger: log,
name: name,
}
}

// NewValidatorWithReportSetter returns a new Validator with a report setter.
func NewValidatorWithReportSetter(verifyOpts *verify.Options, optsGen validateOptsGenerator, log *slog.Logger, reportSetter attestation.ReportSetter, name string) *Validator {
v := NewValidator(verifyOpts, optsGen, log, name)
func NewValidatorWithReportSetter(verifyOpts *verify.Options, optsGen validateOptsGenerator, allowedPIIDs [][]byte, log *slog.Logger, reportSetter attestation.ReportSetter, name string) *Validator {
v := NewValidator(verifyOpts, optsGen, allowedPIIDs, log, name)
v.reportSetter = reportSetter
return v
}
Expand Down Expand Up @@ -109,6 +117,23 @@ func (v *Validator) Validate(ctx context.Context, attDocRaw []byte, reportData [
return fmt.Errorf("validating report data: %w", err)
}

//
// Additional checks.
//

// Check for allowed PIIDs.
if len(v.allowedPIIDs) != 0 {
piid, err := getPIID(quote)
if err != nil {
return fmt.Errorf("reading PIID from quote: %w", err)
}
if !slices.ContainsFunc(v.allowedPIIDs, func(id []byte) bool {
return bytes.Equal(id, piid)
}) {
return fmt.Errorf("PIID %x not in allowed PIIDs", piid)
}
}

if v.reportSetter != nil {
report := tdxReport{quote: quote}
v.reportSetter.SetReport(report)
Expand All @@ -132,3 +157,79 @@ func (t tdxReport) HostData() []byte {
func (t tdxReport) ClaimsToCertExtension() ([]pkix.Extension, error) {
return claimsToCertExtension(t.quote)
}

// getPIID extracts the PIID from the PCK certificate inside a TDX quote.
func getPIID(quote *tdx.QuoteV4) ([]byte, error) {
pckCertChain := quote.GetSignedData().GetCertificationData().GetQeReportCertificationData().GetPckCertificateChainData().PckCertChain

// The certChain input is a concatenated list of PEM-encoded X.509 certificates.
// https://download.01.org/intel-sgx/latest/dcap-latest/linux/docs/Intel_TDX_DCAP_Quoting_Library_API.pdf, A.3.9

var pckBlock *pem.Block
var pck *x509.Certificate
for len(pckCertChain) > 0 {
pckBlock, pckCertChain = pem.Decode(pckCertChain)
if pckBlock == nil {
break
}
candidate, err := x509.ParseCertificate(pckBlock.Bytes)
if err != nil {
return nil, fmt.Errorf("parsing PCK certificate: %w", err)
}

// PCK certificates have specified static names.
// https://api.trustedservices.intel.com/documents/Intel_SGX_PCK_Certificate_CRL_Spec-1.5.pdf, 1.3.5
if candidate.Subject.CommonName == "Intel SGX PCK Certificate" {
pck = candidate
break
}
}
if pck == nil {
return nil, fmt.Errorf("no PCK certificate found in TDX quote")
}

// The PCK certificate contains an SGX Extension, which is a nested list of ASN.1 objects.
// https://api.trustedservices.intel.com/documents/Intel_SGX_PCK_Certificate_CRL_Spec-1.5.pdf, 1.3.5

// Ideally, we would just be using
// https://pkg.go.dev/github.com/google/go-tdx-guest/pcs#PckCertificateExtensions to access
// these extensions, but it currently lacks the PIID field.
// TODO(burgerdev): implement upstream

var sgxExtensions cryptobyte.String
for _, ext := range pck.Extensions {
if !ext.Id.Equal(oid.SGXExtensionsOID) {
continue
}
extValue := cryptobyte.String(ext.Value)
if !extValue.ReadASN1(&sgxExtensions, cryptobyte_asn1.SEQUENCE) {
return nil, fmt.Errorf("could not read SGX extensions from PCK cert")
}
}
if sgxExtensions == nil {
return nil, fmt.Errorf("no SGX extensions found on PCK certificate")
}
for !sgxExtensions.Empty() {
var extension cryptobyte.String
if !sgxExtensions.ReadASN1(&extension, cryptobyte_asn1.SEQUENCE) {
return nil, fmt.Errorf("could not parse SGX extension")
}
var id asn1.ObjectIdentifier
if !extension.ReadASN1ObjectIdentifier(&id) {
return nil, fmt.Errorf("could not parse SGX extension OID")
}
if !id.Equal(oid.PlatformInstanceIDOID) {
continue
}
var piid cryptobyte.String
if !extension.ReadASN1(&piid, cryptobyte_asn1.OCTET_STRING) {
return nil, fmt.Errorf("could not parse SGX extension value")
}

if len(piid) != 16 {
return nil, fmt.Errorf("expected PIID of size 16, got %d", len(piid))
}
return piid, nil
}
return nil, fmt.Errorf("no PIID extension found in PCK SGX extensions")
}
Loading