mirror of
https://github.com/ultravioletrs/cocos.git
synced 2026-08-07 07:14:50 +00:00
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:
committed by
GitHub
parent
a3265bc346
commit
de50b6d2d4
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
@@ -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},
|
||||
}
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user