COCOS-577 - Introduce Go-based CoRIM generation and deprecate Rust attestation policy scripts. (#578)
CI / lint (push) Has been cancelled
CI / test (agent) (push) Has been cancelled
CI / test (cli) (push) Has been cancelled
CI / test (cmd) (push) Has been cancelled
CI / test (internal) (push) Has been cancelled
CI / test (manager, true) (push) Has been cancelled
CI / test (pkg) (push) Has been cancelled
CI / upload-coverage (push) Has been cancelled

* feat: Introduce Go-based CoRIM generation and deprecate Rust attestation policy scripts.

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

* feat: Update dependencies and refactor attestation policy handling

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

* refactor: Migrate attestation verification to use CoRIM and remove deprecated policy handling and EAT verification tests.

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

* Removed the `tdx` and `sev-snp` attestation policy scripts and their build configurations, along with related build and installation steps from the main Makefile.

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

* chore: Remove Rust CI workflow and Cargo Dependabot configuration, and enhance Go test setup for attestation policy paths.

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

* refactor: Use WriteString instead of Write([]byte) for writing policy file content in test.

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

* feat: Refactor `ca-bundle` command to fetch bundles by product string using a configurable HTTP getter with improved error handling, and simplify `attestation_policy` command usage.

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

* fix: ignore return value of cmd.Help()

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

* feat: Implement CoRIM generation for Azure and GCP attestation policies and add a CLI command to download and verify GCP OVMF files.

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

* feat: Upgrade Python virtual environment setup to include setuptools and wheel, append computation ID to Docker container names, and improve test robustness with error assertions and conditional skips for runtime tests.

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

* test: Enhance attestation verification tests, including CoRIM integration and specific platform types like Azure SNP, vTPM, TDX, and IGVM.

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

* feat: Add comprehensive test cases for `VerifyWithCoRIM` including success and measurement mismatch, and refine reference value validation.

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

* feat: Add Azure and TDX attestation verification tests and abstract external service dependencies for improved testability.

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

* feat: Add new test cases for Azure measurement extraction, EAT platform types, IGVM measurement stopping, vTPM CoRIM verification, and GCP OVMF download CLI.

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

* test: enhance CLI CoRIM generation and ATLS certificate verification tests, and refactor the Azure MAA client to use an interface.

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

---------

Signed-off-by: Sammy Oina <sammyoina@gmail.com>
This commit is contained in:
Sammy Kerata Oina
2026-03-19 19:01:24 +03:00
committed by GitHub
parent da31d76c94
commit c1cbcec851
74 changed files with 3662 additions and 8288 deletions
+132 -164
View File
@@ -4,8 +4,8 @@
package azure
import (
"bytes"
"context"
"encoding/hex"
"fmt"
"io"
"net/http"
@@ -13,23 +13,33 @@ import (
"github.com/absmach/supermq/pkg/errors"
"github.com/edgelesssys/go-azguestattestation/maa"
"github.com/golang-jwt/jwt/v5"
"github.com/google/go-sev-guest/abi"
"github.com/google/go-sev-guest/kds"
"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"
"github.com/google/go-tpm-tools/proto/attest"
"github.com/ultravioletrs/cocos/pkg/attestation"
"github.com/ultravioletrs/cocos/pkg/attestation/eat"
"github.com/ultravioletrs/cocos/pkg/attestation/vtpm"
"github.com/veraison/corim/comid"
"github.com/veraison/corim/corim"
"google.golang.org/protobuf/proto"
)
// TokenValidator defines the interface for Azure token validation.
type TokenValidator interface {
Validate(token string) (map[string]any, error)
}
type azureTokenValidator struct{}
func (v *azureTokenValidator) Validate(token string) (map[string]any, error) {
return validateToken(token)
}
var (
MaaURL = "https://sharedeus2.eus2.attest.azure.net"
ErrFetchAzureToken = errors.New("failed to fetch Azure token")
)
var DefaultValidator TokenValidator = &azureTokenValidator{}
var (
_ attestation.Provider = (*provider)(nil)
_ attestation.Verifier = (*verifier)(nil)
@@ -79,6 +89,7 @@ func (a provider) TeeAttestation(teeNonce []byte) ([]byte, error) {
}
func (a provider) VTpmAttestation(vTpmNonce []byte) ([]byte, error) {
fmt.Printf("DEBUG: VTpmAttestation: vtpm.ExternalTPM is %T at %p\n", vtpm.ExternalTPM, &vtpm.ExternalTPM)
quote, err := vtpm.FetchQuote(vTpmNonce)
if err != nil {
return []byte{}, errors.Wrap(vtpm.ErrFetchQuote, err)
@@ -87,91 +98,134 @@ func (a provider) VTpmAttestation(vTpmNonce []byte) ([]byte, error) {
return proto.Marshal(quote)
}
type MaaClient interface {
Attest(ctx context.Context, nonce []byte, maaURL string, client *http.Client) (string, error)
}
type defaultMaaClient struct{}
func (c *defaultMaaClient) Attest(ctx context.Context, nonce []byte, maaURL string, client *http.Client) (string, error) {
return maa.Attest(ctx, nonce, maaURL, client)
}
var DefaultMaaClient MaaClient = &defaultMaaClient{}
func (a provider) AzureAttestationToken(tokenNonce []byte) ([]byte, error) {
quote, err := FetchAzureAttestationToken(tokenNonce, MaaURL)
token, err := DefaultMaaClient.Attest(context.Background(), tokenNonce, MaaURL, http.DefaultClient)
if err != nil {
return nil, errors.Wrap(ErrFetchAzureToken, err)
}
return quote, nil
return []byte(token), 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 vtpm.VerifySEVAttestationReportTLS(attestationReport, teeNonce, a.Policy)
}
func (a verifier) VerifVTpmAttestation(report []byte, vTpmNonce []byte) error {
return vtpm.VerifyQuote(report, vTpmNonce, a.writer, a.Policy)
}
func (a verifier) VerifyAttestation(report []byte, teeNonce []byte, vTpmNonce []byte) error {
var tokenNonce [vtpm.Nonce]byte
copy(tokenNonce[:], teeNonce)
quote := &attest.Attestation{}
err := proto.Unmarshal(report, quote)
if err != nil {
return fmt.Errorf("failed to unmarshal vTPM quote: %w", err)
}
snpReport := quote.GetSevSnpAttestation()
if err = vtpm.VerifySEVAttestationReportTLS(snpReport, nil, a.Policy); err != nil {
return fmt.Errorf("failed to verify vTPM attestation report: %w", err)
}
return nil
}
// VerifyEAT verifies an EAT token and extracts the binary report for verification.
func (v verifier) VerifyEAT(eatToken []byte, teeNonce []byte, vTpmNonce []byte) error {
// Decode EAT token
claims, err := eat.Decode(eatToken, nil)
if err != nil {
return fmt.Errorf("failed to decode EAT token: %w", err)
// EAT verification logic is handled by certificate_verifier calling VerifyWithCoRIM
// But legacy interface might require VerifyEAT.
// In certificate_verifier.go, platformVerifier returns attestation.Verifier.
// certificate_verifier calls v.VerifyWithCoRIM directly (type assertion?).
// No, attestation.Verifier interface must have VerifyWithCoRIM.
// I previously updated Verifier interface to have VerifyWithCoRIM and VerifyEAT.
// But VerifyEAT implementation here calls VerifyAttestation which calls legacy.
// I should probably remove VerifyEAT from here if interface doesn't REQUIRE it or if I can stub it.
// But certificate_verifier calls v.VerifyWithCoRIM.
// Does it call VerifyEAT?
// certificate_verifier call: `func (v *certificateVerifier) verifyCertificateExtension` calls `eat.DecodeCBOR` then `verifier.VerifyWithCoRIM`.
// So VerifyEAT is NOT called by certificate_verifier.
// Is VerifyEAT in interface?
// If yes, I must keep it or stub it.
// I'll stub it to return error "not implemented used VerifyWithCoRIM".
return fmt.Errorf("VerifyEAT is deprecated, use VerifyWithCoRIM")
}
func (v verifier) VerifyWithCoRIM(report []byte, manifest *corim.UnsignedCorim) error {
attestation := &attest.Attestation{}
if err := proto.Unmarshal(report, attestation); err != nil {
return fmt.Errorf("failed to unmarshal attestation report: %w", err)
}
// Verify the embedded binary report
return v.VerifyAttestation(claims.RawReport, teeNonce, vTpmNonce)
// Extract measurement from SEV-SNP report if present
snpRep := attestation.GetSevSnpAttestation()
if snpRep == nil {
return fmt.Errorf("no SEV-SNP attestation found in report")
}
measurement := snpRep.GetReport().GetMeasurement()
if len(measurement) == 0 {
return fmt.Errorf("no measurement in SEV-SNP report")
}
// Parse CoMID from CoRIM
if len(manifest.Tags) == 0 {
return fmt.Errorf("no tags in CoRIM")
}
for _, tag := range manifest.Tags {
if !bytes.HasPrefix(tag, corim.ComidTag) {
continue
}
tagValue := tag[len(corim.ComidTag):]
var c comid.Comid
if err := c.FromCBOR(tagValue); err != nil {
return fmt.Errorf("failed to parse CoMID: %w", err)
}
// Match measurements
if c.Triples.ReferenceValues != nil {
for _, rv := range *c.Triples.ReferenceValues {
if err := rv.Valid(); err != nil {
continue
}
for _, m := range rv.Measurements {
if m.Val.Digests == nil {
continue
}
for _, digest := range *m.Val.Digests {
if string(digest.HashValue) == string(measurement) {
return nil // Match found
}
}
}
}
}
}
return fmt.Errorf("no matching reference value found in CoRIM for Azure SEV-SNP")
}
func (a verifier) JSONToPolicy(path string) error {
return vtpm.ReadPolicy(path, a.Policy)
func FetchAzureAttestationToken(tokenNonce []byte, maaURL string) ([]byte, error) {
token, err := DefaultMaaClient.Attest(context.Background(), tokenNonce, maaURL, http.DefaultClient)
if err != nil {
return nil, fmt.Errorf("error fetching azure token: %w", err)
}
return []byte(token), nil
}
func GenerateAttestationPolicy(token, product string, policy uint64) (*attestation.Config, error) {
claims, err := validateToken(token)
// AzureMeasurementData contains the exact fields extracted from an Azure attestation token
// needed to construct a CoRIM policy for the SNP platform.
type AzureMeasurementData struct {
Measurement string
HostData string
Policy uint64
SVN uint64
}
// ExtractAzureMeasurement extracts the core SNP measurements from an Azure Attestation Token.
func ExtractAzureMeasurement(token string) (*AzureMeasurementData, error) {
claims, err := DefaultValidator.Validate(token)
if err != nil {
return nil, fmt.Errorf("failed to validate token: %w", err)
}
@@ -181,120 +235,34 @@ func GenerateAttestationPolicy(token, product string, policy uint64) (*attestati
return nil, fmt.Errorf("failed to get tee from claims")
}
familyIdString, ok := tee["x-ms-sevsnpvm-familyId"].(string)
if !ok {
return nil, fmt.Errorf("failed to get familyId from claims")
}
familyId, err := hex.DecodeString(familyIdString)
if err != nil {
return nil, fmt.Errorf("failed to decode familyId: %w", err)
}
imageIdString, ok := tee["x-ms-sevsnpvm-imageId"].(string)
if !ok {
return nil, fmt.Errorf("failed to get imageId from claims")
}
imageId, err := hex.DecodeString(imageIdString)
if err != nil {
return nil, fmt.Errorf("failed to decode imageId: %w", err)
}
measurementString, ok := tee["x-ms-sevsnpvm-launchmeasurement"].(string)
if !ok {
return nil, fmt.Errorf("failed to get measurement from claims")
}
measurement, err := hex.DecodeString(measurementString)
if err != nil {
return nil, fmt.Errorf("failed to decode measurement: %w", err)
}
bootloaderVersion, ok := tee["x-ms-sevsnpvm-bootloader-svn"].(float64)
hostDataString, ok := tee["x-ms-sevsnpvm-hostdata"].(string)
if !ok {
return nil, fmt.Errorf("failed to get bootloader version from claims")
// Host data is optional
hostDataString = ""
}
teeVersion, ok := tee["x-ms-sevsnpvm-tee-svn"].(float64)
if !ok {
return nil, fmt.Errorf("failed to get tee version from claims")
}
snpVersion, ok := tee["x-ms-sevsnpvm-snpfw-svn"].(float64)
if !ok {
return nil, fmt.Errorf("failed to get snp version from claims")
}
microcodeVersion, ok := tee["x-ms-sevsnpvm-microcode-svn"].(float64)
if !ok {
return nil, fmt.Errorf("failed to get microcode version from claims")
}
minimalTCBParts := kds.TCBParts{
BlSpl: uint8(bootloaderVersion),
TeeSpl: uint8(teeVersion),
SnpSpl: uint8(snpVersion),
UcodeSpl: uint8(microcodeVersion),
}
// Minimum TCB at the moment is not valid and will be fixed in the future.
_, err = kds.ComposeTCBParts(minimalTCBParts)
if err != nil {
return nil, fmt.Errorf("failed to compose TCB parts: %w", err)
}
guestSVN, ok := tee["x-ms-sevsnpvm-guestsvn"].(float64)
guestSVNFloat, ok := tee["x-ms-sevsnpvm-guestsvn"].(float64)
if !ok {
return nil, fmt.Errorf("failed to get guest SVN from claims")
}
idKeyDigestString, ok := tee["x-ms-sevsnpvm-idkeydigest"].(string)
if !ok {
return nil, fmt.Errorf("failed to get idKeyDigest from claims")
}
idKeyDigest, err := hex.DecodeString(idKeyDigestString)
if err != nil {
return nil, fmt.Errorf("failed to decode idKeyDigest: %w", err)
}
// We default the SNP policy to 0 if not provided, though typically Azure sets this
// in x-ms-sevsnpvm-policy based on the guest. For now, we will return 0 and rely on
// callers to provide the policy if they want to override.
reportIDString, ok := tee["x-ms-sevsnpvm-reportid"].(string)
if !ok {
return nil, fmt.Errorf("failed to get reportID from claims")
}
reportID, err := hex.DecodeString(reportIDString)
if err != nil {
return nil, fmt.Errorf("failed to decode reportID: %w", err)
}
sevSnpProduct := vtpm.GetSEVProductName(product)
return &attestation.Config{
Config: &check.Config{
RootOfTrust: &check.RootOfTrust{
CheckCrl: true,
},
Policy: &check.Policy{
ImageId: imageId,
FamilyId: familyId,
Measurement: measurement,
MinimumGuestSvn: uint32(guestSVN),
TrustedIdKeyHashes: [][]byte{idKeyDigest},
ReportId: reportID,
Product: &sevsnp.SevProduct{Name: sevSnpProduct},
Policy: policy,
},
},
PcrConfig: &attestation.PcrConfig{},
return &AzureMeasurementData{
Measurement: measurementString,
HostData: hostDataString,
SVN: uint64(guestSVNFloat),
Policy: 0, // The policy is usually passed externally in Azure's case, or decoded separately
}, nil
}
func FetchAzureAttestationToken(tokenNonce []byte, maaURL string) ([]byte, error) {
token, err := maa.Attest(context.Background(), tokenNonce, maaURL, http.DefaultClient)
if err != nil {
return nil, fmt.Errorf("error fetching azure token: %w", err)
}
return []byte(token), nil
}
func validateToken(token string) (map[string]any, error) {
unverifiedToken, _, err := new(jwt.Parser).ParseUnverified(token, jwt.MapClaims{})
if err != nil {