COCOS-560 - EAT (#561)

* feat: Implement EAT (Evidence Attestation Token) generation and verification for attestation responses, replacing raw quotes with EAT tokens in the attestation service and protobuf.

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

* style: standardize comment formatting and fix a debug log format specifier.

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

* fix pkg test

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

* feat: Introduce named constants for OEM IDs and use them in attestation claim extraction.

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

* feat: Implement and test minimum length validation for EAT nonce in `NewEATClaims`.

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

* feat: Add EATClaims.Sanitize method and integrate it into the validator to enforce claim dependencies.

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

* feat: Add Signature field to SNPExtensions and TDXExtensions for enhanced claim validation

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

* feat: Update dependencies and improve code structure in attestation package

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

* feat: Introduce comprehensive test suites for EAT, ATLS, TDX, Azure SNP, and vTPM attestation, and improve EAT decoder robustness.

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

* feat: Add encryption and admin keys, an encrypted algorithm file, and update go.mod to use go-jose/v4.

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

* feat: add new encryption and KBS admin keys while improving TDX attestation test error handling.

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

* feat: Add new KBS admin and encryption keys, an encrypted linear regression algorithm, and refactor TDX test error message checks.

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

* feat: Implement Azure SNP attestation policy, update certificate verification, and add key management.

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

* refactor: replace hardcoded string literals with variables in Azure SNP attestation tests.

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

* feat: Refactor TDX EAT claims to use individual RTMR fields with `tdx_` prefixes and add an `IntUse` field.

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

---------

Signed-off-by: Sammy Oina <sammyoina@gmail.com>
Signed-off-by: SammyOina <sammyoina@gmail.com>
This commit is contained in:
Sammy Kerata Oina
2026-02-11 18:16:35 +03:00
committed by GitHub
parent a3265bc346
commit de50b6d2d4
40 changed files with 2655 additions and 109 deletions
+13
View File
@@ -20,6 +20,7 @@ import (
"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/quoteprovider"
"github.com/ultravioletrs/cocos/pkg/attestation/vtpm"
"google.golang.org/protobuf/proto"
@@ -154,6 +155,18 @@ func (a verifier) VerifyAttestation(report []byte, teeNonce []byte, vTpmNonce []
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)
}
// Verify the embedded binary report
return v.VerifyAttestation(claims.RawReport, teeNonce, vTpmNonce)
}
func (a verifier) JSONToPolicy(path string) error {
return vtpm.ReadPolicy(path, a.Policy)
}
+102
View File
@@ -0,0 +1,102 @@
// Copyright (c) Ultraviolet
// SPDX-License-Identifier: Apache-2.0
package azure
import (
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"crypto/x509/pkix"
"encoding/json"
"math/big"
"net/http"
"net/http/httptest"
"testing"
"time"
jose "github.com/go-jose/go-jose/v4"
"github.com/golang-jwt/jwt/v5"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestGenerateAttestationPolicy_Success(t *testing.T) {
key, err := rsa.GenerateKey(rand.Reader, 2048)
require.NoError(t, err)
template := x509.Certificate{
SerialNumber: big.NewInt(1),
Subject: pkix.Name{CommonName: "test"},
NotBefore: time.Now(),
NotAfter: time.Now().Add(time.Hour),
KeyUsage: x509.KeyUsageDigitalSignature,
}
certDER, err := x509.CreateCertificate(rand.Reader, &template, &template, &key.PublicKey, key)
require.NoError(t, err)
cert, err := x509.ParseCertificate(certDER)
require.NoError(t, err)
jwk := jose.JSONWebKey{
Key: &key.PublicKey,
KeyID: testKID,
Algorithm: "RS256",
Use: "sig",
Certificates: []*x509.Certificate{cert},
}
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
jwks := jose.JSONWebKeySet{
Keys: []jose.JSONWebKey{jwk},
}
_ = json.NewEncoder(w).Encode(jwks)
}))
defer server.Close()
originalMaaURL := MaaURL
MaaURL = server.URL
defer func() { MaaURL = originalMaaURL }()
token := createTestToken(t, key, server.URL)
policy, err := GenerateAttestationPolicy(token, "Milan", 0)
require.NoError(t, err)
require.NotNil(t, policy)
assert.Equal(t, "SEV_PRODUCT_MILAN", policy.Config.Policy.Product.Name.String())
}
func createTestToken(t *testing.T, key *rsa.PrivateKey, jku string) string {
claims := jwt.MapClaims{
"iss": "https://test-issuer.com",
"aud": "test-audience",
"exp": time.Now().Add(time.Hour).Unix(),
"iat": time.Now().Unix(),
"x-ms-isolation-tee": map[string]any{
"x-ms-sevsnpvm-familyId": "0102030405060708090a0b0c0d0e0f10",
"x-ms-sevsnpvm-imageId": "0102030405060708090a0b0c0d0e0f10",
"x-ms-sevsnpvm-launchmeasurement": "0102030405060708090a0b0c0d0e0f100102030405060708090a0b0c0d0e0f100102030405060708090a0b0c0d0e0f10",
"x-ms-sevsnpvm-bootloader-svn": float64(1),
"x-ms-sevsnpvm-tee-svn": float64(2),
"x-ms-sevsnpvm-snpfw-svn": float64(3),
"x-ms-sevsnpvm-microcode-svn": float64(4),
"x-ms-sevsnpvm-guestsvn": float64(5),
"x-ms-sevsnpvm-idkeydigest": "0102030405060708090a0b0c0d0e0f100102030405060708090a0b0c0d0e0f100102030405060708090a0b0c0d0e0f10",
"x-ms-sevsnpvm-reportid": "0102030405060708090a0b0c0d0e0f100102030405060708090a0b0c0d0e0f10",
},
}
token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims)
token.Header["jku"] = jku
token.Header["kid"] = testKID
signedToken, err := token.SignedString(key)
require.NoError(t, err)
return signedToken
}
func TestGenerateAttestationPolicy_InvalidToken(t *testing.T) {
// Test with invalid token string
_, err := GenerateAttestationPolicy("invalid-token", "Milan", 0)
assert.Error(t, err)
assert.Contains(t, err.Error(), "failed to validate token")
}
+262
View File
@@ -0,0 +1,262 @@
// Copyright (c) Ultraviolet
// SPDX-License-Identifier: Apache-2.0
package azure
import (
"bytes"
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"crypto/x509/pkix"
"encoding/json"
"math/big"
"net/http"
"net/http/httptest"
"testing"
"time"
jose "github.com/go-jose/go-jose/v4"
"github.com/golang-jwt/jwt/v5"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestGenerateAttestationPolicy(t *testing.T) {
privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
require.NoError(t, err)
template := &x509.Certificate{
SerialNumber: big.NewInt(1),
Subject: pkix.Name{
Organization: []string{"Test Org"},
},
NotBefore: time.Now().Add(-1 * time.Hour),
NotAfter: time.Now().Add(1 * time.Hour),
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
BasicConstraintsValid: true,
}
certDER, err := x509.CreateCertificate(rand.Reader, template, template, &privateKey.PublicKey, privateKey)
require.NoError(t, err)
cert, err := x509.ParseCertificate(certDER)
require.NoError(t, err)
tests := []struct {
name string
token string
product string
policy uint64
setupServer func(t *testing.T, key *rsa.PrivateKey, cert *x509.Certificate) *httptest.Server
wantErr bool
errorMessage string
setupTokenJKU bool
}{
{
name: "valid token and claims",
product: "Milan-B0",
policy: 0,
setupServer: func(t *testing.T, key *rsa.PrivateKey, cert *x509.Certificate) *httptest.Server {
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case openIDConfigPath:
config := map[string]any{
"jwks_uri": "http://" + r.Host + certsPath,
}
w.Header().Set("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(config); err != nil {
t.Errorf("failed to encode config: %v", err)
}
case certsPath:
jwks := generateJWKS(&key.PublicKey, cert)
w.Header().Set("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(jwks); err != nil {
t.Errorf("failed to encode jwks: %v", err)
}
default:
w.WriteHeader(http.StatusNotFound)
}
}))
},
setupTokenJKU: true,
wantErr: false,
},
{
name: "invalid token format",
token: "invalid-token",
product: "Milan-B0",
policy: 0,
setupServer: nil,
wantErr: true,
errorMessage: "failed to parse token",
setupTokenJKU: false,
},
{
name: "missing familyId",
product: "Milan-B0",
policy: 0,
setupServer: func(t *testing.T, key *rsa.PrivateKey, cert *x509.Certificate) *httptest.Server {
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case openIDConfigPath:
config := map[string]any{
"jwks_uri": "http://" + r.Host + certsPath,
}
w.Header().Set("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(config); err != nil {
t.Errorf("failed to encode config: %v", err)
}
case certsPath:
jwks := generateJWKS(&key.PublicKey, cert)
w.Header().Set("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(jwks); err != nil {
t.Errorf("failed to encode jwks: %v", err)
}
}
}))
},
setupTokenJKU: true,
wantErr: true,
errorMessage: "failed to get familyId from claims",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var tokenString string
var server *httptest.Server
if tt.setupServer != nil {
server = tt.setupServer(t, privateKey, cert)
defer server.Close()
originalURL := MaaURL
MaaURL = "" // Clear it so it uses JKU
defer func() { MaaURL = originalURL }()
}
if tt.token != "" {
tokenString = tt.token
} else {
// Generate token
claims := createValidClaims()
if tt.name == "missing familyId" {
if tee, ok := claims["x-ms-isolation-tee"].(map[string]any); ok {
delete(tee, "x-ms-sevsnpvm-familyId")
}
}
jku := ""
if tt.setupTokenJKU && server != nil {
jku = server.URL
}
var err error
tokenString, err = signToken(claims, privateKey, jku)
require.NoError(t, err)
}
config, err := GenerateAttestationPolicy(tokenString, tt.product, tt.policy)
if tt.wantErr {
assert.Error(t, err)
if tt.errorMessage != "" {
assert.Contains(t, err.Error(), tt.errorMessage)
}
assert.Nil(t, config)
} else {
assert.NoError(t, err)
assert.NotNil(t, config)
}
})
}
}
func TestVerifier_VerifyEAT(t *testing.T) {
tests := []struct {
name string
eatToken []byte
teeNonce []byte
vTpmNonce []byte
setupToken func() ([]byte, error)
wantErr bool
errorMessage string
}{
{
name: "invalid cbor",
eatToken: []byte("invalid-cbor"),
teeNonce: testNonce,
vTpmNonce: testNonce,
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
v := NewVerifier(&bytes.Buffer{})
token := tt.eatToken
if tt.setupToken != nil {
var err error
token, err = tt.setupToken()
require.NoError(t, err)
}
err := v.VerifyEAT(token, tt.teeNonce, tt.vTpmNonce)
if tt.wantErr {
assert.Error(t, err)
if tt.errorMessage != "" {
}
} else {
assert.NoError(t, err)
}
})
}
}
// Helper functions
func createValidClaims() jwt.MapClaims {
return jwt.MapClaims{
"iss": "https://test-issuer.com",
"aud": "test-audience",
"exp": time.Now().Add(time.Hour).Unix(),
"iat": time.Now().Unix(),
"nbf": time.Now().Add(-1 * time.Hour).Unix(),
"x-ms-isolation-tee": map[string]any{
"x-ms-sevsnpvm-familyId": "1234567890abcdef",
"x-ms-sevsnpvm-imageId": "fedcba0987654321",
"x-ms-sevsnpvm-launchmeasurement": "abcdef1234567890",
"x-ms-sevsnpvm-bootloader-svn": float64(1),
"x-ms-sevsnpvm-tee-svn": float64(2),
"x-ms-sevsnpvm-snpfw-svn": float64(3),
"x-ms-sevsnpvm-microcode-svn": float64(4),
"x-ms-sevsnpvm-guestsvn": float64(5),
"x-ms-sevsnpvm-idkeydigest": "1234567890abcdef",
"x-ms-sevsnpvm-reportid": "fedcba0987654321",
},
}
}
func signToken(claims jwt.MapClaims, key *rsa.PrivateKey, jku string) (string, error) {
token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims)
token.Header["kid"] = testKID
if jku != "" {
token.Header["jku"] = jku
}
return token.SignedString(key)
}
func generateJWKS(pubKey *rsa.PublicKey, cert *x509.Certificate) *jose.JSONWebKeySet {
key := jose.JSONWebKey{
Key: pubKey,
KeyID: testKID,
Algorithm: "RS256",
Use: "sig",
Certificates: []*x509.Certificate{cert},
}
return &jose.JSONWebKeySet{
Keys: []jose.JSONWebKey{key},
}
}
+9 -6
View File
@@ -22,8 +22,11 @@ import (
)
var (
testNonce = []byte("test-nonce-12345678901234567890123456789012")
testReport = []byte("test-report-data")
testNonce = []byte("test-nonce-12345678901234567890123456789012")
testReport = []byte("test-report-data")
testKID = "test-kid"
openIDConfigPath = "/.well-known/openid_configuration"
certsPath = "/certs"
)
func TestNewProvider(t *testing.T) {
@@ -459,19 +462,19 @@ func TestIntegration_FullAttestationFlow(t *testing.T) {
if err := json.NewEncoder(w).Encode(response); err != nil {
t.Fatalf("Failed to encode response: %v", err)
}
case "/.well-known/openid_configuration":
case openIDConfigPath:
config := map[string]any{
"jwks_uri": "maaServer.URL" + "/certs",
"jwks_uri": "maaServer.URL" + certsPath,
}
w.Header().Set("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(config); err != nil {
t.Fatalf("Failed to encode OpenID configuration: %v", err)
}
case "/certs":
case certsPath:
jwks := map[string]any{
"keys": []map[string]any{
{
"kid": "test-kid",
"kid": testKID,
"kty": "RSA",
"use": "sig",
"n": "test-n-value",