COCOS-391- GCP Attestation policy (#405)

* Add AgentGrpcHost configuration to agent server

Signed-off-by: Sammy Oina <sammyoina@gmail.com>

* Add SHA1 support to PcrValues and implement GCP attestation functions

Signed-off-by: Sammy Oina <sammyoina@gmail.com>

* Add GCP attestation policy and OVMF download commands

Signed-off-by: Sammy Oina <sammyoina@gmail.com>

* Add vTPM attestation support and update protobuf versions

Signed-off-by: Sammy Oina <sammyoina@gmail.com>

* Remove Host field from AgentConfig and update related references

Signed-off-by: Sammy Oina <sammyoina@gmail.com>

* Update GCP attestation policy to accept vCPU count as an argument

Signed-off-by: Sammy Oina <sammyoina@gmail.com>

* Add SHA512 digest verification for OVMF file in GCP download command

Signed-off-by: Sammy Oina <sammyoina@gmail.com>

* Update OVMF object name format in GCP attestation package

Signed-off-by: Sammy Oina <sammyoina@gmail.com>

* Refactor attestation policy structure to use nested Config field

Signed-off-by: Sammy Oina <sammyoina@gmail.com>

---------

Signed-off-by: Sammy Oina <sammyoina@gmail.com>
This commit is contained in:
Sammy Kerata Oina
2025-03-19 11:39:46 +03:00
committed by GitHub
parent ebc8f1bba4
commit c14f1d7b6c
20 changed files with 630 additions and 168 deletions
+1
View File
@@ -31,6 +31,7 @@ var (
type PcrValues struct {
Sha256 map[string]string `json:"sha256"`
Sha384 map[string]string `json:"sha384"`
Sha1 map[string]string `json:"sha1"`
}
type PcrConfig struct {
+111
View File
@@ -0,0 +1,111 @@
// Copyright (c) Ultraviolet
// SPDX-License-Identifier: Apache-2.0
package gcp
import (
"context"
"fmt"
"io"
"cloud.google.com/go/storage"
"github.com/golang/protobuf/proto"
"github.com/google/gce-tcb-verifier/proto/endorsement"
"github.com/google/go-sev-guest/proto/check"
"github.com/google/go-sev-guest/proto/sevsnp"
"github.com/google/go-sev-guest/tools/lib/report"
config "github.com/ultravioletrs/cocos/pkg/attestation"
)
const (
// Offset of the 384-bit measurement in the report.
// The measurement is 48 bytes long and starts at offset 0x90.
// https://www.amd.com/content/dam/amd/en/documents/epyc-technical-docs/specifications/56860.pdf page 56
measurementOffset = 0x90
measurementSize = 48
bucketName = "gce_tcb_integrity"
objectName = "ovmf_x64_csm/sevsnp/%s.binarypb"
ovmfObjectName = "ovmf_x64_csm/%s.fd"
)
func Extract384BitMeasurement(attestation *sevsnp.Attestation) (string, error) {
if attestation == nil {
return "", fmt.Errorf("report is nil")
}
reportBin, err := report.Transform(attestation, "bin")
if err != nil {
return "", fmt.Errorf("failed to transform report to binary: %v", err)
}
if len(reportBin) < measurementOffset+measurementSize {
return "", fmt.Errorf("report is too short to contain the 384-bit measurement")
}
measurement := reportBin[measurementOffset : measurementOffset+measurementSize]
return fmt.Sprintf("%x", measurement), nil
}
func GetLaunchEndorsement(ctx context.Context, measurement384 string) (*endorsement.VMGoldenMeasurement, error) {
client, err := storage.NewClient(ctx)
if err != nil {
return &endorsement.VMGoldenMeasurement{}, fmt.Errorf("failed to create storage client: %v", err)
}
reader, err := client.Bucket(bucketName).Object(fmt.Sprintf(objectName, measurement384)).NewReader(ctx)
if err != nil {
return &endorsement.VMGoldenMeasurement{}, fmt.Errorf("failed to create reader: %v", err)
}
defer reader.Close()
launchEndorsements, err := io.ReadAll(reader)
if err != nil {
return &endorsement.VMGoldenMeasurement{}, fmt.Errorf("failed to read object: %v", err)
}
var endorsementPB endorsement.VMLaunchEndorsement
if err := proto.Unmarshal(launchEndorsements, &endorsementPB); err != nil {
return &endorsement.VMGoldenMeasurement{}, fmt.Errorf("failed to unmarshal launch endorsement: %v", err)
}
var goldenUEFI endorsement.VMGoldenMeasurement
if err := proto.Unmarshal(endorsementPB.SerializedUefiGolden, &goldenUEFI); err != nil {
return &endorsement.VMGoldenMeasurement{}, fmt.Errorf("failed to unmarshal golden UEFI: %v", err)
}
return &goldenUEFI, nil
}
func GenerateAttestationPolicy(endorsement *endorsement.VMGoldenMeasurement, vcpuNum uint32) (*config.Config, error) {
attestationPolicy := config.Config{PcrConfig: &config.PcrConfig{}, Config: &check.Config{RootOfTrust: &check.RootOfTrust{}, Policy: &check.Policy{}}}
attestationPolicy.Config.Policy.Policy = endorsement.SevSnp.Policy
attestationPolicy.Config.Policy.Measurement = endorsement.SevSnp.Measurements[vcpuNum]
attestationPolicy.Config.RootOfTrust.DisallowNetwork = false
attestationPolicy.Config.RootOfTrust.CheckCrl = true
attestationPolicy.Config.RootOfTrust.Product = "Milan"
attestationPolicy.Config.RootOfTrust.ProductLine = "Milan"
return &attestationPolicy, nil
}
func DownloadOvmfFile(ctx context.Context, digest string) ([]byte, error) {
client, err := storage.NewClient(ctx)
if err != nil {
return []byte{}, fmt.Errorf("failed to create storage client: %v", err)
}
reader, err := client.Bucket(bucketName).Object(fmt.Sprintf(ovmfObjectName, digest)).NewReader(ctx)
if err != nil {
return []byte{}, fmt.Errorf("failed to create reader: %v", err)
}
defer reader.Close()
ovmf, err := io.ReadAll(reader)
if err != nil {
return []byte{}, fmt.Errorf("failed to read object: %v", err)
}
return ovmf, nil
}
+14 -11
View File
@@ -126,7 +126,7 @@ func VTPMVerify(quote []byte, pubKeyTLS []byte, teeNonce []byte, vtpmNonce []byt
err := proto.Unmarshal(quote, attestation)
if err != nil {
return fmt.Errorf("fail to unmarshal quote: %v", err)
return errors.Wrap(fmt.Errorf("failed to unmarshal quote"), err)
}
ak := attestation.GetAkPub()
@@ -142,7 +142,7 @@ func VTPMVerify(quote []byte, pubKeyTLS []byte, teeNonce []byte, vtpmNonce []byt
reportData, err := createTEEAttestationReportNonce(pubKeyTLS, ak, teeNonce)
if err != nil {
return fmt.Errorf("fail to calculate report data: %v", err)
return errors.Wrap(fmt.Errorf("failed to create TEE attestation report nonce"), err)
}
if err := quoteprovider.VerifyAttestationReportTLS(attestation.GetSevSnpAttestation(), reportData); err != nil {
@@ -151,7 +151,7 @@ func VTPMVerify(quote []byte, pubKeyTLS []byte, teeNonce []byte, vtpmNonce []byt
_, err = server.VerifyAttestation(attestation, server.VerifyOpts{Nonce: vtpmNonce, TrustedAKs: []crypto.PublicKey{cryptoPub}})
if err != nil {
return fmt.Errorf("verifying attestation: %w", err)
return errors.Wrap(fmt.Errorf("failed to verify attestation"), err)
}
s256, s384 := calculatePCRTLSKey(pubKeyTLS)
@@ -196,7 +196,7 @@ func createTEEAttestationReportNonce(pubKeyTLS []byte, ak []byte, nonce []byte)
func marshalQuote(attestation *attest.Attestation) ([]byte, error) {
out, err := proto.Marshal(attestation)
if err != nil {
return []byte{}, fmt.Errorf("failed to marshal vTPM attestation report: %v", err)
return []byte{}, errors.Wrap(fmt.Errorf("failed to marshal vTPM attestation report"), err)
}
return out, nil
@@ -211,7 +211,7 @@ func fetchVTPMQuote(nonce []byte) (*attest.Attestation, error) {
attestationKey, err := client.AttestationKeyRSA(rwc)
if err != nil {
return nil, fmt.Errorf("failed to create attestation key: %v", err)
return nil, errors.Wrap(fmt.Errorf("failed to create attestation key: %v", err), err)
}
defer attestationKey.Close()
@@ -222,12 +222,12 @@ func fetchVTPMQuote(nonce []byte) (*attest.Attestation, error) {
attestOpts.TCGEventLog, err = client.GetEventLog(rwc)
if err != nil {
return nil, fmt.Errorf("failed to retrieve TCG Event Log: %w", err)
return nil, errors.Wrap(fmt.Errorf("failed to retrieve TCG Event Log: %v", err), err)
}
attestation, err := attestationKey.Attest(attestOpts)
if err != nil {
return nil, fmt.Errorf("failed to collect attestation report: %v", err)
return nil, errors.Wrap(fmt.Errorf("failed to attest: %v", err), err)
}
return attestation, nil
@@ -241,7 +241,7 @@ func addTEEAttestation(attestation *attest.Attestation, nonce []byte) (*attest.A
extReport, err := abi.ReportCertsToProto(rawTeeAttestation)
if err != nil {
return attestation, fmt.Errorf("failed to export the TEE report: %v", err)
return attestation, errors.Wrap(fmt.Errorf("failed to convert TEE report to proto"), err)
}
attestation.TeeAttestation = &attest.Attestation_SevSnpAttestation{
SevSnpAttestation: extReport,
@@ -250,7 +250,7 @@ func addTEEAttestation(attestation *attest.Attestation, nonce []byte) (*attest.A
return attestation, nil
}
func checkExpectedPCRValues(attestation *attest.Attestation, ePcr256 []byte, ePcr384 []byte) error {
func checkExpectedPCRValues(attestation *attest.Attestation, ePcr256, ePcr384 []byte) error {
quotes := attestation.GetQuotes()
for i := range quotes {
quote := quotes[i]
@@ -263,6 +263,9 @@ func checkExpectedPCRValues(attestation *attest.Attestation, ePcr256 []byte, ePc
case tpm.HashAlgo_SHA384:
pcrMap = config.AttestationPolicy.PcrConfig.PCRValues.Sha384
pcr15 = ePcr384
case tpm.HashAlgo_SHA1:
pcrMap = config.AttestationPolicy.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", tpm.HashAlgo_name[int32(quote.Pcrs.Hash)]))
}
@@ -275,11 +278,11 @@ func checkExpectedPCRValues(attestation *attest.Attestation, ePcr256 []byte, ePc
for i, v := range pcrMap {
index, err := strconv.ParseInt(i, 10, 32)
if err != nil {
return fmt.Errorf("error converting PCR index to int32: %v\n", err)
return errors.Wrap(fmt.Errorf("error converting PCR index to int32"), err)
}
value, err := hex.DecodeString(v)
if err != nil {
return fmt.Errorf("error converting PCR value to byte: %v\n", err)
return errors.Wrap(fmt.Errorf("error converting PCR value to byte"), err)
}
if !bytes.Equal(quote.Pcrs.Pcrs[uint32(index)], value) {
return fmt.Errorf("for algo %s PCR[%d] expected %s but found %s", tpm.HashAlgo_name[int32(quote.Pcrs.Hash)], index, hex.EncodeToString(value), hex.EncodeToString(quote.Pcrs.Pcrs[uint32(index)]))