COCOS-439 - TDX Attestation support (#459)
CI / ci (push) Has been cancelled

* inital tdx attestation support

* fix fetching and verification errors

* fix tests

* fix lint problems

* fix reading attestation policy

* add tdx package

* remove Makefile configuration for staticly built binaries
This commit is contained in:
Danko Miladinovic
2025-07-01 00:44:29 +02:00
committed by GitHub
parent 79c66a89c3
commit 9c8ddfd2b1
26 changed files with 1658 additions and 954 deletions
+29 -5
View File
@@ -24,6 +24,7 @@ import (
"github.com/ultravioletrs/cocos/pkg/attestation"
"github.com/ultravioletrs/cocos/pkg/attestation/azure"
"github.com/ultravioletrs/cocos/pkg/attestation/quoteprovider"
"github.com/ultravioletrs/cocos/pkg/attestation/tdx"
"github.com/ultravioletrs/cocos/pkg/attestation/vtpm"
)
@@ -59,14 +60,37 @@ func formTeeData(pubKey []byte, teeNonce []byte) []byte {
func getPlatformProvider(platformType attestation.PlatformType, pubKey []byte) (attestation.Provider, error) {
switch platformType {
case attestation.SNPvTPM:
return vtpm.New(pubKey, true, vmpl2, nil), nil
return vtpm.NewProvider(pubKey, true, vmpl2), nil
case attestation.Azure:
return azure.New(nil), nil
return azure.NewProvider(), nil
case attestation.TDX:
return tdx.NewProvider(), nil
default:
return nil, fmt.Errorf("unsupported platform type: %d", platformType)
}
}
func getPlatformVerifier(platformType attestation.PlatformType, pubKey []byte) (attestation.Verifier, error) {
var verifier attestation.Verifier
switch platformType {
case attestation.SNPvTPM:
verifier = vtpm.NewVerifier(pubKey, nil)
case attestation.Azure:
verifier = azure.NewVerifier(nil)
case attestation.TDX:
verifier = tdx.NewVerifier()
default:
return nil, fmt.Errorf("unsupported platform type: %d", platformType)
}
err := verifier.JSONToPolicy(attestation.AttestationPolicyPath)
if err != nil {
return nil, err
}
return verifier, nil
}
//export callVerificationValidationCallback
func callVerificationValidationCallback(platformType C.int, pubKey *C.uchar, pubKeyLen C.int, attestReport *C.uchar, attestReportSize C.int, teeNonceByte *C.uchar, vTPMNonceByte *C.uchar) C.int {
pubKeyCert := C.GoBytes(unsafe.Pointer(pubKey), pubKeyLen)
@@ -74,16 +98,16 @@ func callVerificationValidationCallback(platformType C.int, pubKey *C.uchar, pub
vTPMNonce := C.GoBytes(unsafe.Pointer(vTPMNonceByte), vtpm.Nonce)
pType := attestation.PlatformType(int(platformType))
attestationReport := C.GoBytes(unsafe.Pointer(attestReport), attestReportSize)
teeData := formTeeData(pubKeyCert, teeNonceData)
provider, err := getPlatformProvider(pType, pubKeyCert)
verifier, err := getPlatformVerifier(pType, pubKeyCert)
if err != nil {
fmt.Fprintf(os.Stderr, "no attestation provider found for platform type %s", err.Error())
return C.int(-1)
}
err = provider.VerifyAttestation(attestationReport, teeData, vTPMNonce)
err = verifier.VerifyAttestation(attestationReport, teeData, vTPMNonce)
if err != nil {
fmt.Fprintf(os.Stderr, "verification callback failed %s", err.Error())
return C.int(-1)
+19 -70
View File
@@ -4,17 +4,14 @@
package attestation
import (
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"github.com/absmach/magistrala/pkg/errors"
"github.com/google/go-sev-guest/client"
"github.com/google/go-sev-guest/proto/check"
tdxcliet "github.com/google/go-tdx-guest/client"
"github.com/google/go-tpm/legacy/tpm2"
"google.golang.org/protobuf/encoding/protojson"
)
type PlatformType int
@@ -25,6 +22,7 @@ const (
SNPvTPM
AzureToken
Azure
TDX
NoCC
)
@@ -33,16 +31,7 @@ const (
azureApiVersion = "2021-02-01"
)
var (
AttestationPolicy = Config{Config: &check.Config{Policy: &check.Policy{}, RootOfTrust: &check.RootOfTrust{}}, PcrConfig: &PcrConfig{}}
ErrAttestationPolicyOpen = errors.New("failed to open Attestation Policy file")
ErrAttestationPolicyDecode = errors.New("failed to decode Attestation Policy file")
ErrAttestationPolicyMissing = errors.New("failed due to missing Attestation Policy file")
ErrAttestationPolicyEncode = errors.New("failed to encode the Attestation Policy")
ErrProtoMarshalFailed = errors.New("failed to marshal protojson")
ErrJsonMarshalFailed = errors.New("failed to marshal json")
ErrJsonUnarshalFailed = errors.New("failed to unmarshal json")
)
var AttestationPolicyPath string
type PcrValues struct {
Sha256 map[string]string `json:"sha256"`
@@ -68,65 +57,14 @@ type Provider interface {
Attestation(teeNonce []byte, vTpmNonce []byte) ([]byte, error)
TeeAttestation(teeNonce []byte) ([]byte, error)
VTpmAttestation(vTpmNonce []byte) ([]byte, error)
VerifyAttestation(report []byte, teeNonce []byte, vTpmNonce []byte) error
VerifTeeAttestation(report []byte, teeNonce []byte) error
VerifVTpmAttestation(report []byte, vTpmNonce []byte) error
AzureAttestationToken(tokenNonce []byte) ([]byte, error)
}
func ReadAttestationPolicy(policyPath string, attestationConfiguration *Config) error {
if policyPath != "" {
policyData, err := os.ReadFile(policyPath)
if err != nil {
return errors.Wrap(ErrAttestationPolicyOpen, err)
}
return ReadAttestationPolicyFromByte(policyData, attestationConfiguration)
}
return ErrAttestationPolicyMissing
}
func ReadAttestationPolicyFromByte(policyData []byte, attestationConfiguration *Config) error {
unmarshalOptions := protojson.UnmarshalOptions{AllowPartial: true, DiscardUnknown: true}
if err := unmarshalOptions.Unmarshal(policyData, attestationConfiguration.Config); err != nil {
return errors.Wrap(ErrAttestationPolicyDecode, err)
}
if err := json.Unmarshal(policyData, attestationConfiguration.PcrConfig); err != nil {
return errors.Wrap(ErrAttestationPolicyDecode, err)
}
return nil
}
func ConvertAttestationPolicyToJSON(attestationConfiguration *Config) ([]byte, error) {
pbJson, err := protojson.Marshal(attestationConfiguration.Config)
if err != nil {
return nil, errors.Wrap(ErrProtoMarshalFailed, err)
}
var pbMap map[string]interface{}
if err := json.Unmarshal(pbJson, &pbMap); err != nil {
return nil, errors.Wrap(ErrJsonUnarshalFailed, err)
}
pcrJson, err := json.Marshal(attestationConfiguration.PcrConfig)
if err != nil {
return nil, errors.Wrap(ErrJsonMarshalFailed, err)
}
var pcrMap map[string]interface{}
if err := json.Unmarshal(pcrJson, &pcrMap); err != nil {
return nil, errors.Wrap(ErrJsonUnarshalFailed, err)
}
for k, v := range pcrMap {
pbMap[k] = v
}
return json.MarshalIndent(pbMap, "", " ")
type Verifier interface {
VerifyAttestation(report []byte, teeNonce []byte, vTpmNonce []byte) error
VerifTeeAttestation(report []byte, teeNonce []byte) error
VerifVTpmAttestation(report []byte, vTpmNonce []byte) error
JSONToPolicy(path string) error
}
// CCPlatform returns the type of the confidential computing platform.
@@ -135,6 +73,7 @@ func CCPlatform() PlatformType {
{SevGuestvTPMExists, SNPvTPM},
{SevGuesDeviceExists, SNP},
{isAzureVM, Azure},
{TDXGuestDeviceExists, TDX},
}
for _, c := range checks {
@@ -197,3 +136,13 @@ func isAzureVM() bool {
return false
}
func TDXGuestDeviceExists() bool {
d, err := tdxcliet.OpenDevice()
if err != nil {
return false
}
d.Close()
return true
}
+55 -20
View File
@@ -25,16 +25,20 @@ import (
"google.golang.org/protobuf/proto"
)
var MaaURL = "https://sharedeus2.eus2.attest.azure.net"
var (
MaaURL = "https://sharedeus2.eus2.attest.azure.net"
ErrFetchAzureToken = errors.New("failed to fetch Azure token")
)
var _ attestation.Provider = (*provider)(nil)
var (
_ attestation.Provider = (*provider)(nil)
_ attestation.Verifier = (*verifier)(nil)
)
type provider struct {
writer io.Writer
}
type provider struct{}
func New(writer io.Writer) attestation.Provider {
return provider{writer: writer}
func NewProvider() attestation.Provider {
return provider{}
}
func (a provider) Attestation(teeNonce []byte, vTpmNonce []byte) ([]byte, error) {
@@ -84,20 +88,56 @@ func (a provider) VTpmAttestation(vTpmNonce []byte) ([]byte, error) {
return proto.Marshal(quote)
}
func (a provider) VerifTeeAttestation(report []byte, teeNonce []byte) error {
func (a provider) AzureAttestationToken(tokenNonce []byte) ([]byte, error) {
quote, err := FetchAzureAttestationToken(tokenNonce, MaaURL)
if err != nil {
return nil, errors.Wrap(ErrFetchAzureToken, err)
}
return quote, nil
}
type verifier struct {
writer io.Writer
Policy *attestation.Config
}
func NewVerifier(writer io.Writer) attestation.Verifier {
policy := &attestation.Config{
Config: &check.Config{Policy: &check.Policy{}, RootOfTrust: &check.RootOfTrust{}},
PcrConfig: &attestation.PcrConfig{},
}
return verifier{
writer: writer,
Policy: policy,
}
}
func NewVerifierWithPolicy(writer io.Writer, policy *attestation.Config) attestation.Verifier {
if policy == nil {
return NewVerifier(writer)
}
return verifier{
writer: writer,
Policy: policy,
}
}
func (a verifier) VerifTeeAttestation(report []byte, teeNonce []byte) error {
attestationReport, err := abi.ReportCertsToProto(report)
if err != nil {
return errors.Wrap(fmt.Errorf("failed to convert TEE report to proto"), err)
}
return quoteprovider.VerifyAttestationReportTLS(attestationReport, teeNonce)
return quoteprovider.VerifyAttestationReportTLS(attestationReport, teeNonce, a.Policy)
}
func (a provider) VerifVTpmAttestation(report []byte, vTpmNonce []byte) error {
return vtpm.VerifyQuote(report, nil, vTpmNonce, a.writer)
func (a verifier) VerifVTpmAttestation(report []byte, vTpmNonce []byte) error {
return vtpm.VerifyQuote(report, nil, vTpmNonce, a.writer, a.Policy)
}
func (a provider) VerifyAttestation(report []byte, teeNonce []byte, vTpmNonce []byte) error {
func (a verifier) VerifyAttestation(report []byte, teeNonce []byte, vTpmNonce []byte) error {
var tokenNonce [vtpm.Nonce]byte
copy(tokenNonce[:], teeNonce)
@@ -108,20 +148,15 @@ func (a provider) VerifyAttestation(report []byte, teeNonce []byte, vTpmNonce []
}
snpReport := quote.GetSevSnpAttestation()
if err = quoteprovider.VerifyAttestationReportTLS(snpReport, nil); err != nil {
if err = quoteprovider.VerifyAttestationReportTLS(snpReport, nil, a.Policy); err != nil {
return fmt.Errorf("failed to verify vTPM attestation report: %w", err)
}
return nil
}
func (a provider) AzureAttestationToken(tokenNonce []byte) ([]byte, error) {
quote, err := FetchAzureAttestationToken(tokenNonce, MaaURL)
if err != nil {
return nil, errors.Wrap(vtpm.ErrFetchAzureToken, err)
}
return quote, nil
func (a verifier) JSONToPolicy(path string) error {
return vtpm.ReadPolicy(path, a.Policy)
}
func GenerateAttestationPolicy(token, product string, policy uint64) (*attestation.Config, error) {
-12
View File
@@ -24,15 +24,3 @@ func (e *EmptyProvider) VTpmAttestation(vTpmNonce []byte) ([]byte, error) {
func (e *EmptyProvider) AzureAttestationToken(nonce []byte) ([]byte, error) {
return nil, nil
}
func (e *EmptyProvider) VerifTeeAttestation(report []byte, teeNonce []byte) error {
return nil
}
func (e *EmptyProvider) VerifVTpmAttestation(report []byte, vTpmNonce []byte) error {
return nil
}
func (e *EmptyProvider) VerifyAttestation(report []byte, teeNonce []byte, vTpmNonce []byte) error {
return nil
}
+8 -17
View File
@@ -22,7 +22,6 @@ import (
"github.com/google/go-sev-guest/verify/trust"
"github.com/google/logger"
"github.com/ultravioletrs/cocos/pkg/attestation"
"google.golang.org/protobuf/proto"
)
const (
@@ -39,8 +38,8 @@ var (
)
var (
errProductLine = errors.New(fmt.Sprintf("product name must be %s or %s", sevProductNameMilan, sevProductNameGenoa))
errAttVerification = errors.New("attestation verification failed")
ErrProductLine = errors.New(fmt.Sprintf("product name must be %s or %s", sevProductNameMilan, sevProductNameGenoa))
ErrAttVerification = errors.New("attestation verification failed")
errAttValidation = errors.New("attestation validation failed")
)
@@ -73,21 +72,16 @@ func fillInAttestationLocal(attestation *sevsnp.Attestation, cfg *check.Config)
return nil
}
func copyConfig(attConf *check.Config) (*check.Config, error) {
copy := proto.Clone(attConf).(*check.Config)
return copy, nil
}
func verifyReport(attestationPB *sevsnp.Attestation, cfg *check.Config) error {
sopts, err := verify.RootOfTrustToOptions(cfg.RootOfTrust)
if err != nil {
return fmt.Errorf("failed to get root of trust options: %v", errors.Wrap(errAttVerification, err))
return fmt.Errorf("failed to get root of trust options: %v", errors.Wrap(ErrAttVerification, err))
}
if cfg.Policy.Product == nil {
productName := GetProductName(cfg.RootOfTrust.ProductLine)
if productName == sevsnp.SevProduct_SEV_PRODUCT_UNKNOWN {
return errProductLine
return ErrProductLine
}
sopts.Product = &sevsnp.SevProduct{
@@ -108,7 +102,7 @@ func verifyReport(attestationPB *sevsnp.Attestation, cfg *check.Config) error {
}
if err := verify.SnpAttestation(attestationPB, sopts); err != nil {
return errors.Wrap(errAttVerification, err)
return errors.Wrap(ErrAttVerification, err)
}
return nil
@@ -117,7 +111,7 @@ func verifyReport(attestationPB *sevsnp.Attestation, cfg *check.Config) error {
func validateReport(attestationPB *sevsnp.Attestation, cfg *check.Config) error {
opts, err := validate.PolicyToOptions(cfg.Policy)
if err != nil {
return fmt.Errorf("failed to get policy for validation: %v", errors.Wrap(errAttVerification, err))
return fmt.Errorf("failed to get policy for validation: %v", errors.Wrap(ErrAttVerification, err))
}
if err = validate.SnpAttestation(attestationPB, opts); err != nil {
@@ -131,11 +125,8 @@ func GetLeveledQuoteProvider() (client.LeveledQuoteProvider, error) {
return client.GetLeveledQuoteProvider()
}
func VerifyAttestationReportTLS(attestationPB *sevsnp.Attestation, reportData []byte) error {
config, err := copyConfig(attestation.AttestationPolicy.Config)
if err != nil {
return errors.Wrap(fmt.Errorf("failed to create a copy of attestation policy"), err)
}
func VerifyAttestationReportTLS(attestationPB *sevsnp.Attestation, reportData []byte, policy *attestation.Config) error {
config := policy.Config
// Certificate chain is populated based on the extra data that is appended to the SEV-SNP attestation report.
// This data is not part of the attestation report and it will be ignored.
-145
View File
@@ -12,13 +12,10 @@ import (
"testing"
"github.com/absmach/magistrala/pkg/errors"
"github.com/google/go-sev-guest/abi"
"github.com/google/go-sev-guest/proto/check"
"github.com/google/go-sev-guest/proto/sevsnp"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
config "github.com/ultravioletrs/cocos/pkg/attestation"
"google.golang.org/protobuf/encoding/protojson"
)
func TestFillInAttestationLocal(t *testing.T) {
@@ -70,145 +67,3 @@ func TestFillInAttestationLocal(t *testing.T) {
})
}
}
func TestVerifyAttestationReportSuccess(t *testing.T) {
attestationPB, reportData := prepVerifyAttReport(t)
tests := []struct {
name string
attestationReport *sevsnp.Attestation
reportData []byte
goodProduct int
err error
}{
{
name: "Valid attestation, validation and verification is performed succsessfully",
attestationReport: attestationPB,
reportData: reportData,
goodProduct: 1,
err: nil,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := VerifyAttestationReportTLS(tt.attestationReport, tt.reportData)
assert.True(t, errors.Contains(err, tt.err), fmt.Sprintf("expected error %v, got %v", tt.err, err))
})
}
}
func TestVerifyAttestationReportMalformedSignature(t *testing.T) {
attestationPB, reportData := prepVerifyAttReport(t)
// Change random data so in the signature so the signature failes
attestationPB.Report.Signature[0] = attestationPB.Report.Signature[0] ^ 0x01
tests := []struct {
name string
attestationReport *sevsnp.Attestation
reportData []byte
err error
}{
{
name: "Valid attestation, distorted signature",
attestationReport: attestationPB,
reportData: reportData,
err: errAttVerification,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := VerifyAttestationReportTLS(tt.attestationReport, tt.reportData)
assert.True(t, errors.Contains(err, tt.err), fmt.Sprintf("expected error %v, got %v", tt.err, err))
})
}
}
func TestVerifyAttestationReportUnknownProduct(t *testing.T) {
attestationPB, reportData := prepVerifyAttReport(t)
tests := []struct {
name string
attestationReport *sevsnp.Attestation
reportData []byte
err error
}{
{
name: "Valid attestation, unknown product",
attestationReport: attestationPB,
reportData: reportData,
err: errProductLine,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
config.AttestationPolicy.Config.RootOfTrust.ProductLine = ""
config.AttestationPolicy.Config.Policy.Product = nil
err := VerifyAttestationReportTLS(tt.attestationReport, tt.reportData)
assert.True(t, errors.Contains(err, tt.err), fmt.Sprintf("expected error %v, got %v", tt.err, err))
})
}
}
func TestVerifyAttestationReportMalformedPolicy(t *testing.T) {
attestationPB, reportData := prepVerifyAttReport(t)
// Change random data in the measurement so the measurement does not match
attestationPB.Report.Measurement[0] = attestationPB.Report.Measurement[0] ^ 0x01
tests := []struct {
name string
attestationReport *sevsnp.Attestation
reportData []byte
err error
}{
{
name: "Valid attestation, malformed policy (measurement)",
attestationReport: attestationPB,
reportData: reportData,
err: errAttVerification,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := VerifyAttestationReportTLS(tt.attestationReport, tt.reportData)
assert.True(t, errors.Contains(err, tt.err), fmt.Sprintf("expected error %v, got %v", tt.err, err))
})
}
}
func prepVerifyAttReport(t *testing.T) (*sevsnp.Attestation, []byte) {
file, err := os.ReadFile("../../../attestation.bin")
require.NoError(t, err)
if len(file) < abi.ReportSize {
file = append(file, make([]byte, abi.ReportSize-len(file))...)
}
rr, err := abi.ReportCertsToProto(file)
require.NoError(t, err)
config.AttestationPolicy = config.Config{Config: &check.Config{Policy: &check.Policy{}, RootOfTrust: &check.RootOfTrust{}}, PcrConfig: &config.PcrConfig{}}
attestationPolicyFile, err := os.ReadFile("../../../scripts/attestation_policy/attestation_policy.json")
require.NoError(t, err)
unmarshalOptions := protojson.UnmarshalOptions{DiscardUnknown: true}
err = unmarshalOptions.Unmarshal(attestationPolicyFile, config.AttestationPolicy.Config)
require.NoError(t, err)
config.AttestationPolicy.Config.Policy.Product = &sevsnp.SevProduct{Name: sevsnp.SevProduct_SEV_PRODUCT_MILAN}
config.AttestationPolicy.Config.Policy.FamilyId = rr.Report.FamilyId
config.AttestationPolicy.Config.Policy.ImageId = rr.Report.ImageId
config.AttestationPolicy.Config.Policy.Measurement = rr.Report.Measurement
config.AttestationPolicy.Config.Policy.HostData = rr.Report.HostData
config.AttestationPolicy.Config.Policy.ReportIdMa = rr.Report.ReportIdMa
config.AttestationPolicy.Config.RootOfTrust.ProductLine = sevProductNameMilan
return rr, rr.Report.ReportData
}
+148
View File
@@ -0,0 +1,148 @@
// Copyright (c) Ultraviolet
// SPDX-License-Identifier: Apache-2.0
//go:build !embed
// +build !embed
package tdx
import (
"fmt"
"os"
"time"
"github.com/absmach/magistrala/pkg/errors"
"github.com/google/go-tdx-guest/abi"
"github.com/google/go-tdx-guest/client"
"github.com/google/go-tdx-guest/proto/checkconfig"
valdatetdx "github.com/google/go-tdx-guest/validate"
verifytdx "github.com/google/go-tdx-guest/verify"
trusttdx "github.com/google/go-tdx-guest/verify/trust"
"github.com/ultravioletrs/cocos/pkg/attestation"
"google.golang.org/protobuf/encoding/protojson"
)
var errOpenTDXDevice = errors.New("failed to open TDX device")
var (
_ attestation.Provider = (*provider)(nil)
_ attestation.Verifier = (*verifier)(nil)
)
var (
timeout = time.Minute * 2
maxTryDelay = time.Second * 30
)
type provider struct{}
func NewProvider() attestation.Provider {
return provider{}
}
func (v provider) Attestation(teeNonce []byte, vTpmNonce []byte) ([]byte, error) {
return v.TeeAttestation(teeNonce)
}
func (v provider) TeeAttestation(teeNonce []byte) ([]byte, error) {
quoteprovider, err := client.GetQuoteProvider()
if err != nil {
return nil, errors.Wrap(err, errOpenTDXDevice)
}
return quoteprovider.GetRawQuote([64]byte(teeNonce))
}
func (v provider) VTpmAttestation(vTpmNonce []byte) ([]byte, error) {
return nil, errors.New("vTPM attestation fetch is not supported")
}
func (v provider) AzureAttestationToken(tokenNonce []byte) ([]byte, error) {
return nil, errors.New("Azure attestation token is not supported")
}
type verifier struct {
Policy *checkconfig.Config
}
func NewVerifier() attestation.Verifier {
Policy := &checkconfig.Config{
RootOfTrust: &checkconfig.RootOfTrust{},
Policy: &checkconfig.Policy{HeaderPolicy: &checkconfig.HeaderPolicy{}, TdQuoteBodyPolicy: &checkconfig.TDQuoteBodyPolicy{}},
}
return verifier{
Policy: Policy,
}
}
func NewVerifierWithPolicy(policy *checkconfig.Config) attestation.Verifier {
if policy == nil {
return NewVerifier()
}
return verifier{
Policy: policy,
}
}
func (v verifier) VerifTeeAttestation(report []byte, teeNonce []byte) error {
if v.Policy == nil {
return fmt.Errorf("tdx policy is not provided")
}
quote, err := abi.QuoteToProto(report)
if err != nil {
return err
}
sopts, err := verifytdx.RootOfTrustToOptions(v.Policy.RootOfTrust)
if err != nil {
return err
}
sopts.Getter = &trusttdx.RetryHTTPSGetter{
Timeout: timeout,
MaxRetryDelay: maxTryDelay,
Getter: &trusttdx.SimpleHTTPSGetter{},
}
if err := verifytdx.TdxQuote(quote, sopts); err != nil {
return err
}
opts, err := valdatetdx.PolicyToOptions(v.Policy.Policy)
if err != nil {
return err
}
if err := valdatetdx.TdxQuote(quote, opts); err != nil {
return err
}
return nil
}
func (v verifier) VerifVTpmAttestation(report []byte, vTpmNonce []byte) error {
return errors.New("VTPM attestation verification is not supported")
}
func (v verifier) VerifyAttestation(report []byte, teeNonce []byte, vTpmNonce []byte) error {
return v.VerifTeeAttestation(report, teeNonce)
}
func (v verifier) JSONToPolicy(path string) error {
return ReadTDXAttestationPolicy(path, v.Policy)
}
func ReadTDXAttestationPolicy(policyPath string, policy *checkconfig.Config) error {
policyByte, err := os.ReadFile(policyPath)
if err != nil {
return err
}
if err := protojson.Unmarshal(policyByte, policy); err != nil {
return err
}
return nil
}
+124 -25
View File
@@ -9,6 +9,7 @@ import (
"crypto/sha256"
"crypto/sha512"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"os"
@@ -16,6 +17,7 @@ import (
"github.com/absmach/magistrala/pkg/errors"
"github.com/google/go-sev-guest/abi"
"github.com/google/go-sev-guest/proto/check"
"github.com/google/go-sev-guest/proto/sevsnp"
"github.com/google/go-tpm-tools/client"
"github.com/google/go-tpm-tools/proto/attest"
@@ -26,11 +28,15 @@ import (
"github.com/ultravioletrs/cocos/pkg/attestation"
"github.com/ultravioletrs/cocos/pkg/attestation/quoteprovider"
"golang.org/x/crypto/sha3"
"google.golang.org/protobuf/encoding/protojson"
"google.golang.org/protobuf/encoding/prototext"
"google.golang.org/protobuf/proto"
)
var _ attestation.Provider = (*provider)(nil)
var (
_ attestation.Provider = (*provider)(nil)
_ attestation.Verifier = (*verifier)(nil)
)
const (
eventLog = "/sys/kernel/security/tpm0/binary_bios_measurements"
@@ -43,10 +49,15 @@ const (
)
var (
ExternalTPM io.ReadWriteCloser
ErrNoHashAlgo = errors.New("hash algo is not supported")
ErrFetchQuote = errors.New("failed to fetch vTPM quote")
ErrFetchAzureToken = errors.New("failed to fetch Azure token")
ExternalTPM io.ReadWriteCloser
ErrNoHashAlgo = errors.New("hash algo is not supported")
ErrFetchQuote = errors.New("failed to fetch vTPM quote")
ErrAttestationPolicyOpen = errors.New("failed to open Attestation Policy file")
ErrAttestationPolicyDecode = errors.New("failed to decode Attestation Policy file")
ErrAttestationPolicyMissing = errors.New("failed due to missing Attestation Policy file")
ErrProtoMarshalFailed = errors.New("failed to marshal protojson")
ErrJsonMarshalFailed = errors.New("failed to marshal json")
ErrJsonUnarshalFailed = errors.New("failed to unmarshal json")
)
type tpm struct {
@@ -97,15 +108,13 @@ type provider struct {
pubKey []byte
teeAttestaion bool
vmpl uint
writer io.Writer
}
func New(pubKey []byte, teeAttestation bool, vmpl uint, writer io.Writer) attestation.Provider {
func NewProvider(pubKey []byte, teeAttestation bool, vmpl uint) attestation.Provider {
return &provider{
pubKey: pubKey,
teeAttestaion: teeAttestation,
vmpl: vmpl,
writer: writer,
}
}
@@ -126,26 +135,61 @@ func (v provider) VTpmAttestation(vTpmNonce []byte) ([]byte, error) {
return proto.Marshal(quote)
}
func (v provider) VerifTeeAttestation(report []byte, teeNonce []byte) error {
func (v provider) AzureAttestationToken(tokenNonce []byte) ([]byte, error) {
return nil, errors.New("Azure attestation token is not supported")
}
type verifier struct {
pubKey []byte
writer io.Writer
Policy *attestation.Config
}
func NewVerifier(pubKey []byte, writer io.Writer) attestation.Verifier {
policy := &attestation.Config{
Config: &check.Config{Policy: &check.Policy{}, RootOfTrust: &check.RootOfTrust{}},
PcrConfig: &attestation.PcrConfig{},
}
return &verifier{
pubKey: pubKey,
writer: writer,
Policy: policy,
}
}
func NewVerifierWithPolicy(pubKey []byte, writer io.Writer, policy *attestation.Config) attestation.Verifier {
if policy == nil {
return NewVerifier(pubKey, writer)
}
return &verifier{
pubKey: pubKey,
writer: writer,
Policy: policy,
}
}
func (v verifier) VerifTeeAttestation(report []byte, teeNonce []byte) error {
attestReport, err := abi.ReportToProto(report)
if err != nil {
return errors.Wrap(fmt.Errorf("failed to convert TEE report to proto"), err)
}
attestationReport := sevsnp.Attestation{Report: attestReport, CertificateChain: nil}
return quoteprovider.VerifyAttestationReportTLS(&attestationReport, teeNonce)
return quoteprovider.VerifyAttestationReportTLS(&attestationReport, teeNonce, v.Policy)
}
func (v provider) VerifVTpmAttestation(report []byte, vTpmNonce []byte) error {
return VerifyQuote(report, v.pubKey, vTpmNonce, v.writer)
func (v verifier) VerifVTpmAttestation(report []byte, vTpmNonce []byte) error {
return VerifyQuote(report, v.pubKey, vTpmNonce, v.writer, v.Policy)
}
func (v provider) VerifyAttestation(report []byte, teeNonce []byte, vTpmNonce []byte) error {
return VTPMVerify(report, v.pubKey, teeNonce, vTpmNonce, v.writer)
func (v verifier) VerifyAttestation(report []byte, teeNonce []byte, vTpmNonce []byte) error {
return VTPMVerify(report, v.pubKey, teeNonce, vTpmNonce, v.writer, v.Policy)
}
func (v provider) AzureAttestationToken(tokenNonce []byte) ([]byte, error) {
return nil, errors.New("Azure attestation token is not supported")
func (v verifier) JSONToPolicy(path string) error {
return ReadPolicy(path, v.Policy)
}
func Attest(teeNonce []byte, vTPMNonce []byte, teeAttestaion bool, vmpl uint) ([]byte, error) {
@@ -164,8 +208,8 @@ func Attest(teeNonce []byte, vTPMNonce []byte, teeAttestaion bool, vmpl uint) ([
return marshalQuote(attestation)
}
func VTPMVerify(quote []byte, pubKeyTLS []byte, teeNonce []byte, vtpmNonce []byte, writer io.Writer) error {
if err := VerifyQuote(quote, pubKeyTLS, vtpmNonce, writer); err != nil {
func VTPMVerify(quote []byte, pubKeyTLS []byte, teeNonce []byte, vtpmNonce []byte, writer io.Writer, policy *attestation.Config) error {
if err := VerifyQuote(quote, pubKeyTLS, vtpmNonce, writer, policy); err != nil {
return fmt.Errorf("failed to verify vTPM quote: %v", err)
}
@@ -176,14 +220,14 @@ func VTPMVerify(quote []byte, pubKeyTLS []byte, teeNonce []byte, vtpmNonce []byt
return errors.Wrap(fmt.Errorf("failed to unmarshal quote"), err)
}
if err := quoteprovider.VerifyAttestationReportTLS(attestation.GetSevSnpAttestation(), teeNonce); err != nil {
if err := quoteprovider.VerifyAttestationReportTLS(attestation.GetSevSnpAttestation(), teeNonce, policy); err != nil {
return fmt.Errorf("failed to verify TEE attestation report: %v", err)
}
return nil
}
func VerifyQuote(quote []byte, pubKeyTLS []byte, vtpmNonce []byte, writer io.Writer) error {
func VerifyQuote(quote []byte, pubKeyTLS []byte, vtpmNonce []byte, writer io.Writer, policy *attestation.Config) error {
attestation := &attest.Attestation{}
err := proto.Unmarshal(quote, attestation)
@@ -209,7 +253,7 @@ func VerifyQuote(quote []byte, pubKeyTLS []byte, vtpmNonce []byte, writer io.Wri
s256, s384 := calculatePCRTLSKey(pubKeyTLS)
if err := checkExpectedPCRValues(attestation, s256, s384); err != nil {
if err := checkExpectedPCRValues(attestation, s256, s384, policy); err != nil {
return fmt.Errorf("PCR values do not match expected PCR values: %w", err)
}
@@ -286,7 +330,7 @@ func addTEEAttestation(attestation *attest.Attestation, nonce []byte, vmpl uint)
return nil
}
func checkExpectedPCRValues(attQuote *attest.Attestation, ePcr256, ePcr384 []byte) error {
func checkExpectedPCRValues(attQuote *attest.Attestation, ePcr256, ePcr384 []byte, policy *attestation.Config) error {
quotes := attQuote.GetQuotes()
for i := range quotes {
quote := quotes[i]
@@ -294,21 +338,21 @@ func checkExpectedPCRValues(attQuote *attest.Attestation, ePcr256, ePcr384 []byt
var pcr15 []byte
switch quote.Pcrs.Hash {
case ptpm.HashAlgo_SHA256:
pcrMap = attestation.AttestationPolicy.PcrConfig.PCRValues.Sha256
pcrMap = policy.PcrConfig.PCRValues.Sha256
if ePcr256 == nil {
pcr15 = make([]byte, 32)
} else {
pcr15 = ePcr256
}
case ptpm.HashAlgo_SHA384:
pcrMap = attestation.AttestationPolicy.PcrConfig.PCRValues.Sha384
pcrMap = policy.PcrConfig.PCRValues.Sha384
if ePcr384 == nil {
pcr15 = make([]byte, 48)
} else {
pcr15 = ePcr384
}
case ptpm.HashAlgo_SHA1:
pcrMap = attestation.AttestationPolicy.PcrConfig.PCRValues.Sha1
pcrMap = policy.PcrConfig.PCRValues.Sha1
pcr15 = []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}
default:
return errors.Wrap(ErrNoHashAlgo, fmt.Errorf("algo: %s", ptpm.HashAlgo_name[int32(quote.Pcrs.Hash)]))
@@ -383,3 +427,58 @@ func GetPCRSHA256Value(index int) ([]byte, error) {
func GetPCRSHA384Value(index int) ([]byte, error) {
return getPCRValue(index, tpm2.AlgSHA384)
}
func ReadPolicy(policyPath string, attestationConfiguration *attestation.Config) error {
if policyPath != "" {
policyData, err := os.ReadFile(policyPath)
if err != nil {
return errors.Wrap(ErrAttestationPolicyOpen, err)
}
return ReadPolicyFromByte(policyData, attestationConfiguration)
}
return ErrAttestationPolicyMissing
}
func ReadPolicyFromByte(policyData []byte, attestationConfiguration *attestation.Config) error {
unmarshalOptions := protojson.UnmarshalOptions{AllowPartial: true, DiscardUnknown: true}
if err := unmarshalOptions.Unmarshal(policyData, attestationConfiguration.Config); err != nil {
return errors.Wrap(ErrAttestationPolicyDecode, err)
}
if err := json.Unmarshal(policyData, attestationConfiguration.PcrConfig); err != nil {
return errors.Wrap(ErrAttestationPolicyDecode, err)
}
return nil
}
func ConvertPolicyToJSON(attestationConfiguration *attestation.Config) ([]byte, error) {
pbJson, err := protojson.Marshal(attestationConfiguration.Config)
if err != nil {
return nil, errors.Wrap(ErrProtoMarshalFailed, err)
}
var pbMap map[string]interface{}
if err := json.Unmarshal(pbJson, &pbMap); err != nil {
return nil, errors.Wrap(ErrJsonUnarshalFailed, err)
}
pcrJson, err := json.Marshal(attestationConfiguration.PcrConfig)
if err != nil {
return nil, errors.Wrap(ErrJsonMarshalFailed, err)
}
var pcrMap map[string]interface{}
if err := json.Unmarshal(pcrJson, &pcrMap); err != nil {
return nil, errors.Wrap(ErrJsonUnarshalFailed, err)
}
for k, v := range pcrMap {
pbMap[k] = v
}
return json.MarshalIndent(pbMap, "", " ")
}
+233
View File
@@ -0,0 +1,233 @@
// Copyright (c) Ultraviolet
// SPDX-License-Identifier: Apache-2.0
package vtpm
import (
"fmt"
"os"
"path/filepath"
"testing"
"github.com/absmach/magistrala/pkg/errors"
"github.com/google/go-sev-guest/abi"
"github.com/google/go-sev-guest/proto/check"
"github.com/google/go-sev-guest/proto/sevsnp"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/ultravioletrs/cocos/pkg/attestation"
"github.com/ultravioletrs/cocos/pkg/attestation/quoteprovider"
"google.golang.org/protobuf/encoding/protojson"
)
const sevProductNameMilan = "Milan"
var policy = attestation.Config{Config: &check.Config{Policy: &check.Policy{}, RootOfTrust: &check.RootOfTrust{}}, PcrConfig: &attestation.PcrConfig{}}
func TestVerifyAttestationReportMalformedSignature(t *testing.T) {
tempDir, err := os.MkdirTemp("", "policy")
require.NoError(t, err)
defer os.RemoveAll(tempDir)
attestationPB, reportData := prepVerifyAttReport(t)
err = setAttestationPolicy(attestationPB, tempDir)
require.NoError(t, err)
// Change random data so in the signature so the signature failes
attestationPB.Report.Signature[0] = attestationPB.Report.Signature[0] ^ 0x01
tests := []struct {
name string
attestationReport *sevsnp.Attestation
reportData []byte
err error
}{
{
name: "Valid attestation, distorted signature",
attestationReport: attestationPB,
reportData: reportData,
err: quoteprovider.ErrAttVerification,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := quoteprovider.VerifyAttestationReportTLS(tt.attestationReport, tt.reportData, &policy)
assert.True(t, errors.Contains(err, tt.err), fmt.Sprintf("expected error %v, got %v", tt.err, err))
})
}
}
func TestVerifyAttestationReportUnknownProduct(t *testing.T) {
tempDir, err := os.MkdirTemp("", "policy")
require.NoError(t, err)
defer os.RemoveAll(tempDir)
attestationPB, reportData := prepVerifyAttReport(t)
err = setAttestationPolicy(attestationPB, tempDir)
require.NoError(t, err)
err = changeProductAttestationPolicy()
require.NoError(t, err)
tests := []struct {
name string
attestationReport *sevsnp.Attestation
reportData []byte
err error
}{
{
name: "Valid attestation, unknown product",
attestationReport: attestationPB,
reportData: reportData,
err: quoteprovider.ErrProductLine,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := quoteprovider.VerifyAttestationReportTLS(tt.attestationReport, tt.reportData, &policy)
assert.True(t, errors.Contains(err, tt.err), fmt.Sprintf("expected error %v, got %v", tt.err, err))
})
}
}
func TestVerifyAttestationReportSuccess(t *testing.T) {
tempDir, err := os.MkdirTemp("", "policy")
require.NoError(t, err)
defer os.RemoveAll(tempDir)
attestationPB, reportData := prepVerifyAttReport(t)
err = setAttestationPolicy(attestationPB, tempDir)
require.NoError(t, err)
tests := []struct {
name string
attestationReport *sevsnp.Attestation
reportData []byte
goodProduct int
err error
}{
{
name: "Valid attestation, validation and verification is performed succsessfully",
attestationReport: attestationPB,
reportData: reportData,
goodProduct: 1,
err: nil,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := quoteprovider.VerifyAttestationReportTLS(tt.attestationReport, tt.reportData, &policy)
assert.True(t, errors.Contains(err, tt.err), fmt.Sprintf("expected error %v, got %v", tt.err, err))
})
}
}
func TestVerifyAttestationReportMalformedPolicy(t *testing.T) {
tempDir, err := os.MkdirTemp("", "policy")
require.NoError(t, err)
defer os.RemoveAll(tempDir)
attestationPB, reportData := prepVerifyAttReport(t)
err = setAttestationPolicy(attestationPB, tempDir)
require.NoError(t, err)
// Change random data in the measurement so the measurement does not match
attestationPB.Report.Measurement[0] = attestationPB.Report.Measurement[0] ^ 0x01
tests := []struct {
name string
attestationReport *sevsnp.Attestation
reportData []byte
err error
}{
{
name: "Valid attestation, malformed policy (measurement)",
attestationReport: attestationPB,
reportData: reportData,
err: quoteprovider.ErrAttVerification,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := quoteprovider.VerifyAttestationReportTLS(tt.attestationReport, tt.reportData, &policy)
assert.True(t, errors.Contains(err, tt.err), fmt.Sprintf("expected error %v, got %v", tt.err, err))
})
}
}
func prepVerifyAttReport(t *testing.T) (*sevsnp.Attestation, []byte) {
file, err := os.ReadFile("../../../attestation.bin")
require.NoError(t, err)
if len(file) < abi.ReportSize {
file = append(file, make([]byte, abi.ReportSize-len(file))...)
}
rr, err := abi.ReportCertsToProto(file)
require.NoError(t, err)
return rr, rr.Report.ReportData
}
func setAttestationPolicy(rr *sevsnp.Attestation, policyDirectory string) error {
attestationPolicyFile, err := os.ReadFile("../../../scripts/attestation_policy/attestation_policy.json")
if err != nil {
return err
}
unmarshalOptions := protojson.UnmarshalOptions{DiscardUnknown: true}
err = unmarshalOptions.Unmarshal(attestationPolicyFile, policy)
if err != nil {
return err
}
policy.Config.Policy.Product = &sevsnp.SevProduct{Name: sevsnp.SevProduct_SEV_PRODUCT_MILAN}
policy.Config.Policy.FamilyId = rr.Report.FamilyId
policy.Config.Policy.ImageId = rr.Report.ImageId
policy.Config.Policy.Measurement = rr.Report.Measurement
policy.Config.Policy.HostData = rr.Report.HostData
policy.Config.Policy.ReportIdMa = rr.Report.ReportIdMa
policy.Config.RootOfTrust.ProductLine = sevProductNameMilan
policyByte, err := ConvertPolicyToJSON(&policy)
if err != nil {
return err
}
policyPath := filepath.Join(policyDirectory, "attestation_policy.json")
err = os.WriteFile(policyPath, policyByte, 0o644)
if err != nil {
return nil
}
attestation.AttestationPolicyPath = policyPath
return nil
}
func changeProductAttestationPolicy() error {
err := ReadPolicy(attestation.AttestationPolicyPath, &policy)
if err != nil {
return err
}
policy.Config.RootOfTrust.ProductLine = ""
policy.Config.Policy.Product = nil
policyByte, err := ConvertPolicyToJSON(&policy)
if err != nil {
return err
}
if err := os.WriteFile(attestation.AttestationPolicyPath, policyByte, 0o644); err != nil {
return nil
}
return nil
}
+1 -2
View File
@@ -15,7 +15,6 @@ import (
"github.com/ultravioletrs/cocos/agent"
agentgrpc "github.com/ultravioletrs/cocos/agent/api/grpc"
"github.com/ultravioletrs/cocos/agent/mocks"
"github.com/ultravioletrs/cocos/pkg/attestation"
pkggrpc "github.com/ultravioletrs/cocos/pkg/clients/grpc"
"google.golang.org/grpc"
"google.golang.org/grpc/health"
@@ -113,7 +112,7 @@ func TestAgentClientIntegration(t *testing.T) {
},
AttestedTLS: true,
},
err: attestation.ErrAttestationPolicyMissing,
err: errors.New("failed to stat attestation policy file"),
},
}
+9 -2
View File
@@ -24,11 +24,18 @@ import (
func setupATLS(cfg AgentClientConfig) (credentials.TransportCredentials, security, error) {
security := withaTLS
err := attestation.ReadAttestationPolicy(cfg.AttestationPolicy, &attestation.AttestationPolicy)
info, err := os.Stat(cfg.AttestationPolicy)
if err != nil {
return nil, withoutTLS, errors.Wrap(fmt.Errorf("failed to read Attestation Policy"), err)
return nil, withoutTLS, errors.Wrap(fmt.Errorf("failed to stat attestation policy file"), err)
}
if !info.Mode().IsRegular() {
return nil, withoutTLS, fmt.Errorf("attestation policy file is not a regular file: %s", cfg.AttestationPolicy)
}
attestation.AttestationPolicyPath = cfg.AttestationPolicy
var insecureSkipVerify bool = true
var rootCAs *x509.CertPool = nil
+7 -6
View File
@@ -20,6 +20,7 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/ultravioletrs/cocos/pkg/attestation"
"github.com/ultravioletrs/cocos/pkg/attestation/vtpm"
)
func TestNewClient(t *testing.T) {
@@ -108,7 +109,7 @@ func TestNewClient(t *testing.T) {
AttestationPolicy: "no such file",
},
wantErr: true,
err: fmt.Errorf("failed to read Attestation Policy"),
err: fmt.Errorf("failed to stat attestation policy file"),
},
{
name: "Fail with invalid ServerCAFile",
@@ -226,25 +227,25 @@ func TestReadAttestationPolicy(t *testing.T) {
name: "Invalid JSON",
manifestPath: "invalid_manifest.json",
fileContent: invalidJSON,
err: attestation.ErrAttestationPolicyDecode,
err: vtpm.ErrAttestationPolicyDecode,
},
{
name: "Non-existent file",
manifestPath: "nonexistent.json",
fileContent: "",
err: attestation.ErrAttestationPolicyOpen,
err: vtpm.ErrAttestationPolicyOpen,
},
{
name: "Empty manifest path",
manifestPath: "",
fileContent: "",
err: attestation.ErrAttestationPolicyMissing,
err: vtpm.ErrAttestationPolicyMissing,
},
{
name: "Invalid JSON PCR",
manifestPath: "invalid_manifest.json",
fileContent: invalidJSONPCR,
err: attestation.ErrAttestationPolicyDecode,
err: vtpm.ErrAttestationPolicyDecode,
},
}
@@ -257,7 +258,7 @@ func TestReadAttestationPolicy(t *testing.T) {
}
config := attestation.Config{Config: &check.Config{}, PcrConfig: &attestation.PcrConfig{}}
err := attestation.ReadAttestationPolicy(tt.manifestPath, &config)
err := vtpm.ReadPolicy(tt.manifestPath, &config)
assert.True(t, errors.Contains(err, tt.err), fmt.Sprintf("expected error %v, got %v", tt.err, err))
if tt.err == nil {