mirror of
https://github.com/edgelesssys/constellation.git
synced 2024-12-12 01:14:28 -05:00
daaa7755a7
* Remove formatter factory * Enable `constellation verify` with JSON output for Azure TDX --------- Signed-off-by: Daniel Weiße <dw@edgeless.systems>
180 lines
5.4 KiB
Go
180 lines
5.4 KiB
Go
/*
|
|
Copyright (c) Edgeless Systems GmbH
|
|
|
|
SPDX-License-Identifier: AGPL-3.0-only
|
|
*/
|
|
|
|
package tdx
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/base64"
|
|
"encoding/binary"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
|
|
"github.com/edgelesssys/constellation/v2/internal/attestation"
|
|
"github.com/edgelesssys/constellation/v2/internal/attestation/azure"
|
|
"github.com/edgelesssys/constellation/v2/internal/attestation/variant"
|
|
"github.com/edgelesssys/constellation/v2/internal/attestation/vtpm"
|
|
"github.com/google/go-tpm/legacy/tpm2"
|
|
)
|
|
|
|
const (
|
|
imdsURL = "http://169.254.169.254/acc/tdquote"
|
|
indexHCLReport = 0x1400001
|
|
hclDataOffset = 1216
|
|
hclReportTypeOffset = 8
|
|
hclReportTypeOffsetStart = hclDataOffset + hclReportTypeOffset
|
|
hclRequestDataSizeOffset = 16
|
|
runtimeDataSizeOffset = hclDataOffset + hclRequestDataSizeOffset
|
|
hclRequestDataOffset = 20
|
|
runtimeDataOffset = hclDataOffset + hclRequestDataOffset
|
|
tdReportSize = 1024
|
|
hwReportStart = 32
|
|
hwReportEnd = 1216
|
|
)
|
|
|
|
const (
|
|
hclReportTypeInvalid uint32 = iota
|
|
hclReportTypeReserved
|
|
hclReportTypeSNP
|
|
hclReportTypeTVM
|
|
hclReportTypeTDX
|
|
)
|
|
|
|
// Issuer for Azure confidential VM attestation using TDX.
|
|
type Issuer struct {
|
|
variant.AzureTDX
|
|
*vtpm.Issuer
|
|
|
|
quoteGetter quoteGetter
|
|
}
|
|
|
|
// NewIssuer initializes a new Azure Issuer.
|
|
func NewIssuer(log attestation.Logger) *Issuer {
|
|
i := &Issuer{
|
|
quoteGetter: imdsQuoteGetter{
|
|
client: &http.Client{Transport: &http.Transport{Proxy: nil}},
|
|
},
|
|
}
|
|
|
|
i.Issuer = vtpm.NewIssuer(
|
|
vtpm.OpenVTPM,
|
|
azure.GetAttestationKey,
|
|
i.getInstanceInfo,
|
|
log,
|
|
)
|
|
return i
|
|
}
|
|
|
|
func (i *Issuer) getInstanceInfo(ctx context.Context, tpm io.ReadWriteCloser, _ []byte) ([]byte, error) {
|
|
// Read HCL report from TPM
|
|
report, err := tpm2.NVReadEx(tpm, indexHCLReport, tpm2.HandleOwner, "", 0)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Parse the report from the TPM
|
|
hwReport, runtimeData, err := parseHCLReport(report)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("getting HCL report: %w", err)
|
|
}
|
|
|
|
// Get quote from IMDS API
|
|
quote, err := i.quoteGetter.getQuote(ctx, hwReport)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("getting quote: %w", err)
|
|
}
|
|
|
|
instanceInfo := InstanceInfo{
|
|
AttestationReport: quote,
|
|
RuntimeData: runtimeData,
|
|
}
|
|
instanceInfoJSON, err := json.Marshal(instanceInfo)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("marshalling instance info: %w", err)
|
|
}
|
|
return instanceInfoJSON, nil
|
|
}
|
|
|
|
func parseHCLReport(report []byte) (hwReport, runtimeData []byte, err error) {
|
|
// First, ensure the extracted report is actually for TDX
|
|
if len(report) < hclReportTypeOffsetStart+4 {
|
|
return nil, nil, fmt.Errorf("invalid HCL report: expected at least %d bytes to read HCL report type, got %d", hclReportTypeOffsetStart+4, len(report))
|
|
}
|
|
reportType := binary.LittleEndian.Uint32(report[hclReportTypeOffsetStart : hclReportTypeOffsetStart+4])
|
|
if reportType != hclReportTypeTDX {
|
|
return nil, nil, fmt.Errorf("invalid HCL report type: expected TDX (%d), got %d", hclReportTypeTDX, reportType)
|
|
}
|
|
|
|
// We need the td report (generally called HW report in Azure's samples) from the HCL report to send to the IMDS API
|
|
if len(report) < hwReportStart+tdReportSize {
|
|
return nil, nil, fmt.Errorf("invalid HCL report: expected at least %d bytes to read td report, got %d", hwReportStart+tdReportSize, len(report))
|
|
}
|
|
hwReport = report[hwReportStart : hwReportStart+tdReportSize]
|
|
|
|
// We also need the runtime data to verify the attestation key later on the validator side
|
|
if len(report) < runtimeDataSizeOffset+4 {
|
|
return nil, nil, fmt.Errorf("invalid HCL report: expected at least %d bytes to read runtime data size, got %d", runtimeDataSizeOffset+4, len(report))
|
|
}
|
|
runtimeDataSize := int(binary.LittleEndian.Uint32(report[runtimeDataSizeOffset : runtimeDataSizeOffset+4]))
|
|
if len(report) < runtimeDataOffset+runtimeDataSize {
|
|
return nil, nil, fmt.Errorf("invalid HCL report: expected at least %d bytes to read runtime data, got %d", runtimeDataOffset+runtimeDataSize, len(report))
|
|
}
|
|
runtimeData = report[runtimeDataOffset : runtimeDataOffset+runtimeDataSize]
|
|
|
|
return hwReport, runtimeData, nil
|
|
}
|
|
|
|
// imdsQuoteGetter issues TDX quotes using Azure's IMDS API.
|
|
type imdsQuoteGetter struct {
|
|
client *http.Client
|
|
}
|
|
|
|
func (i imdsQuoteGetter) getQuote(ctx context.Context, hwReport []byte) ([]byte, error) {
|
|
encodedReportJSON, err := json.Marshal(quoteRequest{
|
|
Report: base64.RawURLEncoding.EncodeToString(hwReport),
|
|
})
|
|
if err != nil {
|
|
return nil, fmt.Errorf("marshalling encoded report: %w", err)
|
|
}
|
|
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodPost, imdsURL, bytes.NewReader(encodedReportJSON))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("creating request: %w", err)
|
|
}
|
|
req.Header.Add("Content-Type", "application/json")
|
|
|
|
res, err := i.client.Do(req)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("sending request: %w", err)
|
|
}
|
|
defer res.Body.Close()
|
|
|
|
if res.StatusCode != http.StatusOK {
|
|
return nil, fmt.Errorf("unexpected status code: %d", res.StatusCode)
|
|
}
|
|
var quoteRes quoteResponse
|
|
if err := json.NewDecoder(res.Body).Decode("eRes); err != nil {
|
|
return nil, fmt.Errorf("decoding response: %w", err)
|
|
}
|
|
|
|
return base64.RawURLEncoding.DecodeString(quoteRes.Quote)
|
|
}
|
|
|
|
type quoteRequest struct {
|
|
Report string `json:"report"`
|
|
}
|
|
|
|
type quoteResponse struct {
|
|
Quote string `json:"quote"`
|
|
}
|
|
|
|
type quoteGetter interface {
|
|
getQuote(ctx context.Context, encodedHWReport []byte) ([]byte, error)
|
|
}
|