Files
cocos/pkg/attestation/azure/snp.go
T
Sammy Kerata Oina 6169766666
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
NOISSUE - Fix agent startup issues (#605)
* Update attestationFromCert function to include ccPlatform parameter for enhanced attestation processing

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

* chore: migrate dependencies from supermq to magistrala and update build configurations

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

* chore: update project dependencies, repository source, and support TDX QuoteV5 attestation

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

---------

Signed-off-by: Sammy Oina <sammyoina@gmail.com>
2026-06-11 17:08:24 +02:00

316 lines
9.1 KiB
Go

// Copyright (c) Ultraviolet
// SPDX-License-Identifier: Apache-2.0
package azure
import (
"bytes"
"context"
"fmt"
"io"
"net/http"
"github.com/absmach/magistrala/pkg/errors"
"github.com/edgelesssys/go-azguestattestation/maa"
"github.com/golang-jwt/jwt/v5"
"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/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)
)
type provider struct{}
func NewProvider() attestation.Provider {
return provider{}
}
func (a provider) Attestation(teeNonce []byte, vTpmNonce []byte) ([]byte, error) {
if isAzureTDX() {
return a.TeeAttestation(teeNonce)
}
var tokenNonce [vtpm.Nonce]byte
copy(tokenNonce[:], teeNonce)
params, err := maa.NewParameters(context.Background(), tokenNonce[:], http.DefaultClient, nil)
if err != nil {
return nil, fmt.Errorf("failed to get report: %w", err)
}
snpReport, err := report.ParseAttestation(params.SNPReport, "bin")
if err != nil {
return nil, fmt.Errorf("failed to parse SNP report: %w", err)
}
quote, err := vtpm.FetchQuote(vTpmNonce)
if err != nil {
return nil, fmt.Errorf("failed to fetch quote: %w", err)
}
quote.TeeAttestation = &attest.Attestation_SevSnpAttestation{
SevSnpAttestation: snpReport,
}
return proto.Marshal(quote)
}
func (a provider) TeeAttestation(teeNonce []byte) ([]byte, error) {
if isAzureTDX() {
return fetchAzureTDXQuote(teeNonce)
}
var tokenNonce [vtpm.Nonce]byte
copy(tokenNonce[:], teeNonce)
params, err := maa.NewParameters(context.Background(), tokenNonce[:], http.DefaultClient, nil)
if err != nil {
return nil, fmt.Errorf("failed to get report: %w", err)
}
return params.SNPReport, nil
}
func (a provider) VTpmAttestation(vTpmNonce []byte) ([]byte, error) {
quote, err := vtpm.FetchQuote(vTpmNonce)
if err != nil {
return []byte{}, errors.Wrap(vtpm.ErrFetchQuote, err)
}
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) {
if isAzureTDX() {
token, err := FetchAzureTDXAttestationToken(tokenNonce, MaaURL)
if err != nil {
return nil, errors.Wrap(ErrFetchAzureToken, err)
}
return token, nil
}
token, err := DefaultMaaClient.Attest(context.Background(), tokenNonce, MaaURL, http.DefaultClient)
if err != nil {
return nil, errors.Wrap(ErrFetchAzureToken, err)
}
return []byte(token), nil
}
type verifier struct {
writer io.Writer
}
func NewVerifier(writer io.Writer) attestation.Verifier {
return verifier{
writer: writer,
}
}
// VerifyEAT verifies an EAT token and extracts the binary report for verification.
func (v verifier) VerifyEAT(eatToken []byte, teeNonce []byte, vTpmNonce []byte) error {
// 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 {
tdxErr := verifyTDXQuoteWithCoRIM(report, manifest)
if tdxErr == nil {
return nil
}
return fmt.Errorf("failed to unmarshal attestation report: %w; Azure TDX verification failed: %v", err, tdxErr)
}
// Extract measurement from SEV-SNP report if present
snpRep := attestation.GetSevSnpAttestation()
if snpRep == nil {
if tdxErr := verifyTDXQuoteWithCoRIM(report, manifest); tdxErr == nil {
return 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 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
}
// 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)
}
tee, ok := claims["x-ms-isolation-tee"].(map[string]any)
if !ok {
return nil, fmt.Errorf("failed to get tee from claims")
}
measurementString, ok := tee["x-ms-sevsnpvm-launchmeasurement"].(string)
if !ok {
return nil, fmt.Errorf("failed to get measurement from claims")
}
hostDataString, ok := tee["x-ms-sevsnpvm-hostdata"].(string)
if !ok {
// Host data is optional
hostDataString = ""
}
guestSVNFloat, ok := tee["x-ms-sevsnpvm-guestsvn"].(float64)
if !ok {
return nil, fmt.Errorf("failed to get guest SVN from claims")
}
// 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.
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 validateToken(token string) (map[string]any, error) {
unverifiedToken, _, err := new(jwt.Parser).ParseUnverified(token, jwt.MapClaims{})
if err != nil {
return nil, fmt.Errorf("failed to parse token: %w", err)
}
jku, jkuOk := unverifiedToken.Header["jku"].(string)
if !jkuOk {
return nil, fmt.Errorf("token is missing jku or kid in header")
}
MaaUrlCerts := MaaURL
if MaaURL == "" {
MaaUrlCerts = jku
}
keySet, err := maa.GetKeySet(context.Background(), MaaUrlCerts, http.DefaultClient)
if err != nil {
return nil, fmt.Errorf("failed to get key set: %w", err)
}
claims, err := maa.ValidateToken(token, keySet)
if err != nil {
return nil, fmt.Errorf("failed to validate token: %w", err)
}
return claims, nil
}