Skip to content

Commit 0f3b462

Browse files
nadahallimchain0
andauthored
Add teeattestation package for TEE attestation validation (#1899)
* Add teeattestation package for TEE attestation validation * Remove caRootsPEM param from ValidateAttestation ValidateAttestation now always uses the hardcoded AWS Nitro root cert. ValidateAttestationWithRoots is available for testing with fake enclaves that use self-signed CA roots. * Fix lint: errors.New, rename FakeAttestor, nolint for CBOR tags * Fix goimports alignment in CBOR struct tags * Check PCR count before accessing indices * Rename package fake to nitrofake * Merge teeattestation into root module * Use fmt.Sprintf for fixed-format PCR JSON in nitrofake * add relay metadata fields for remote capability execution (#1948) * Remove nitrite from Nitro attestation validation * Run gomodtidy after cbor upgrade --------- Co-authored-by: mchain0 <maciej.wisniewski@smartcontract.com>
1 parent 2dd27f1 commit 0f3b462

10 files changed

Lines changed: 742 additions & 3 deletions

File tree

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ require (
1010
github.com/bytecodealliance/wasmtime-go/v28 v28.0.0
1111
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc
1212
github.com/dominikbraun/graph v0.23.0
13-
github.com/fxamacker/cbor/v2 v2.7.0
13+
github.com/fxamacker/cbor/v2 v2.9.0
1414
github.com/gagliardetto/utilz v0.1.3
1515
github.com/go-json-experiment/json v0.0.0-20250223041408-d3c622f1b874
1616
github.com/go-playground/validator/v10 v10.26.0

go.sum

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pkg/capabilities/v2/actions/confidentialrelay/types.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,9 @@ type SecretsResponseResult struct {
4141
// CapabilityRequestParams is the JSON-RPC params for "confidential.capability.execute".
4242
type CapabilityRequestParams struct {
4343
WorkflowID string `json:"workflow_id"`
44+
Owner string `json:"owner,omitempty"`
45+
ExecutionID string `json:"execution_id,omitempty"`
46+
ReferenceID string `json:"reference_id,omitempty"`
4447
CapabilityID string `json:"capability_id"`
4548
Payload string `json:"payload"`
4649
Attestation string `json:"attestation,omitempty"`

pkg/teeattestation/hash.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
// Package teeattestation provides platform-agnostic primitives for TEE
2+
// attestation validation. Platform-specific validators (e.g. AWS Nitro)
3+
// live in subpackages.
4+
package teeattestation
5+
6+
import "crypto/sha256"
7+
8+
// DomainSeparator is prepended to attestation payloads before hashing.
9+
const DomainSeparator = "CONFIDENTIAL_COMPUTE_PAYLOAD"
10+
11+
// DomainHash computes SHA-256 over DomainSeparator + "\n" + tag + "\n" + data.
12+
// This is the standard domain-separated hash used for attestation UserData
13+
// throughout the system.
14+
func DomainHash(tag string, data []byte) []byte {
15+
h := sha256.New()
16+
h.Write([]byte(DomainSeparator))
17+
h.Write([]byte("\n" + tag + "\n"))
18+
h.Write(data)
19+
return h.Sum(nil)
20+
}

pkg/teeattestation/hash_test.go

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
package teeattestation
2+
3+
import (
4+
"crypto/sha256"
5+
"testing"
6+
)
7+
8+
func TestDomainHash(t *testing.T) {
9+
tag := "TestTag"
10+
data := []byte(`{"key":"value"}`)
11+
12+
got := DomainHash(tag, data)
13+
14+
h := sha256.New()
15+
h.Write([]byte(DomainSeparator))
16+
h.Write([]byte("\n" + tag + "\n"))
17+
h.Write(data)
18+
want := h.Sum(nil)
19+
20+
if len(got) != sha256.Size {
21+
t.Fatalf("expected %d bytes, got %d", sha256.Size, len(got))
22+
}
23+
for i := range want {
24+
if got[i] != want[i] {
25+
t.Fatalf("hash mismatch at byte %d: want %x, got %x", i, want, got)
26+
}
27+
}
28+
}
29+
30+
func TestDomainHash_DifferentTags(t *testing.T) {
31+
data := []byte("same-data")
32+
h1 := DomainHash("Tag1", data)
33+
h2 := DomainHash("Tag2", data)
34+
35+
for i := range h1 {
36+
if h1[i] != h2[i] {
37+
return
38+
}
39+
}
40+
t.Fatal("different tags should produce different hashes")
41+
}
42+
43+
func TestDomainHash_DifferentData(t *testing.T) {
44+
tag := "SameTag"
45+
h1 := DomainHash(tag, []byte("data-a"))
46+
h2 := DomainHash(tag, []byte("data-b"))
47+
48+
for i := range h1 {
49+
if h1[i] != h2[i] {
50+
return
51+
}
52+
}
53+
t.Fatal("different data should produce different hashes")
54+
}
Lines changed: 218 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,218 @@
1+
// Package nitrofake provides an Attestor that produces structurally valid
2+
// COSE Sign1 attestation documents. These documents pass the local Nitro
3+
// attestation validator's full validation chain (CBOR parsing, cert chain,
4+
// ECDSA signature, UserData, PCRs) without requiring real Nitro hardware.
5+
package nitrofake
6+
7+
import (
8+
"crypto/ecdsa"
9+
"crypto/elliptic"
10+
"crypto/rand"
11+
"crypto/sha512"
12+
"crypto/x509"
13+
"crypto/x509/pkix"
14+
"encoding/hex"
15+
"encoding/pem"
16+
"fmt"
17+
"math/big"
18+
"time"
19+
20+
"github.com/fxamacker/cbor/v2"
21+
)
22+
23+
// Attestor produces structurally valid COSE Sign1 attestation documents
24+
// that pass the local Nitro validator with a custom CA root.
25+
type Attestor struct {
26+
rootKey *ecdsa.PrivateKey
27+
rootCert *x509.Certificate
28+
rootCertDER []byte
29+
leafKey *ecdsa.PrivateKey
30+
leafCert *x509.Certificate
31+
leafCertDER []byte
32+
pcrs map[uint][]byte
33+
}
34+
35+
// NewAttestor generates a self-signed P-384 root CA, a leaf cert signed
36+
// by that root, and deterministic 48-byte fake PCR values.
37+
func NewAttestor() (*Attestor, error) {
38+
rootKey, err := ecdsa.GenerateKey(elliptic.P384(), rand.Reader)
39+
if err != nil {
40+
return nil, fmt.Errorf("generate root key: %w", err)
41+
}
42+
rootTemplate := &x509.Certificate{
43+
SerialNumber: big.NewInt(1),
44+
Subject: pkix.Name{CommonName: "Fake Nitro Root CA"},
45+
NotBefore: time.Now().Add(-1 * time.Hour),
46+
NotAfter: time.Now().Add(10 * 365 * 24 * time.Hour),
47+
KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign,
48+
IsCA: true,
49+
BasicConstraintsValid: true,
50+
}
51+
rootCertDER, err := x509.CreateCertificate(rand.Reader, rootTemplate, rootTemplate, &rootKey.PublicKey, rootKey)
52+
if err != nil {
53+
return nil, fmt.Errorf("create root cert: %w", err)
54+
}
55+
rootCert, err := x509.ParseCertificate(rootCertDER)
56+
if err != nil {
57+
return nil, fmt.Errorf("parse root cert: %w", err)
58+
}
59+
60+
leafKey, err := ecdsa.GenerateKey(elliptic.P384(), rand.Reader)
61+
if err != nil {
62+
return nil, fmt.Errorf("generate leaf key: %w", err)
63+
}
64+
leafTemplate := &x509.Certificate{
65+
SerialNumber: big.NewInt(2),
66+
Subject: pkix.Name{CommonName: "Fake Nitro Enclave"},
67+
NotBefore: time.Now().Add(-1 * time.Hour),
68+
NotAfter: time.Now().Add(24 * time.Hour),
69+
KeyUsage: x509.KeyUsageDigitalSignature,
70+
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageAny},
71+
SignatureAlgorithm: x509.ECDSAWithSHA384,
72+
}
73+
leafCertDER, err := x509.CreateCertificate(rand.Reader, leafTemplate, rootCert, &leafKey.PublicKey, rootKey)
74+
if err != nil {
75+
return nil, fmt.Errorf("create leaf cert: %w", err)
76+
}
77+
leafCert, err := x509.ParseCertificate(leafCertDER)
78+
if err != nil {
79+
return nil, fmt.Errorf("parse leaf cert: %w", err)
80+
}
81+
82+
pcrs := map[uint][]byte{
83+
0: sha384Sum([]byte("fake-pcr-0")),
84+
1: sha384Sum([]byte("fake-pcr-1")),
85+
2: sha384Sum([]byte("fake-pcr-2")),
86+
}
87+
88+
return &Attestor{
89+
rootKey: rootKey,
90+
rootCert: rootCert,
91+
rootCertDER: rootCertDER,
92+
leafKey: leafKey,
93+
leafCert: leafCert,
94+
leafCertDER: leafCertDER,
95+
pcrs: pcrs,
96+
}, nil
97+
}
98+
99+
// CreateAttestation builds a COSE Sign1 document encoding a Nitro-like
100+
// attestation with the given userData.
101+
func (f *Attestor) CreateAttestation(userData []byte) ([]byte, error) {
102+
doc := attestationDocument{
103+
ModuleID: "fake-enclave-module",
104+
Timestamp: uint64(time.Now().UnixMilli()), //nolint:gosec // timestamp is always positive
105+
Digest: "SHA384",
106+
PCRs: f.pcrs,
107+
Certificate: f.leafCertDER,
108+
CABundle: [][]byte{f.rootCertDER},
109+
UserData: userData,
110+
}
111+
112+
payloadBytes, err := cbor.Marshal(doc)
113+
if err != nil {
114+
return nil, fmt.Errorf("cbor encode document: %w", err)
115+
}
116+
117+
header := coseHeader{Alg: int64(-35)}
118+
protectedBytes, err := cbor.Marshal(header)
119+
if err != nil {
120+
return nil, fmt.Errorf("cbor encode protected header: %w", err)
121+
}
122+
123+
sigStruct := coseSignature{
124+
Context: "Signature1",
125+
Protected: protectedBytes,
126+
ExternalAAD: []byte{},
127+
Payload: payloadBytes,
128+
}
129+
sigStructBytes, err := cbor.Marshal(sigStruct)
130+
if err != nil {
131+
return nil, fmt.Errorf("cbor encode sig structure: %w", err)
132+
}
133+
134+
hash := sha512.Sum384(sigStructBytes)
135+
r, s, err := ecdsa.Sign(rand.Reader, f.leafKey, hash[:])
136+
if err != nil {
137+
return nil, fmt.Errorf("ecdsa sign: %w", err)
138+
}
139+
140+
signature := make([]byte, 96)
141+
rBytes := r.Bytes()
142+
sBytes := s.Bytes()
143+
copy(signature[48-len(rBytes):48], rBytes)
144+
copy(signature[96-len(sBytes):96], sBytes)
145+
146+
outer := cosePayload{
147+
Protected: protectedBytes,
148+
Payload: payloadBytes,
149+
Signature: signature,
150+
}
151+
result, err := cbor.Marshal(outer)
152+
if err != nil {
153+
return nil, fmt.Errorf("cbor encode cose sign1: %w", err)
154+
}
155+
return result, nil
156+
}
157+
158+
// CARoots returns an x509.CertPool containing the fake root CA certificate.
159+
func (f *Attestor) CARoots() *x509.CertPool {
160+
pool := x509.NewCertPool()
161+
pool.AddCert(f.rootCert)
162+
return pool
163+
}
164+
165+
// CARootsPEM returns the root CA certificate in PEM format.
166+
func (f *Attestor) CARootsPEM() string {
167+
return string(pem.EncodeToMemory(&pem.Block{
168+
Type: "CERTIFICATE",
169+
Bytes: f.rootCertDER,
170+
}))
171+
}
172+
173+
// TrustedPCRsJSON returns the PCR values as a JSON object matching the
174+
// format expected by the attestation validator.
175+
func (f *Attestor) TrustedPCRsJSON() []byte {
176+
return []byte(fmt.Sprintf(`{"pcr0":"%s","pcr1":"%s","pcr2":"%s"}`,
177+
hex.EncodeToString(f.pcrs[0]),
178+
hex.EncodeToString(f.pcrs[1]),
179+
hex.EncodeToString(f.pcrs[2]),
180+
))
181+
}
182+
183+
func sha384Sum(data []byte) []byte {
184+
h := sha512.Sum384(data)
185+
return h[:]
186+
}
187+
188+
type attestationDocument struct {
189+
ModuleID string `cbor:"module_id"`
190+
Timestamp uint64 `cbor:"timestamp"`
191+
Digest string `cbor:"digest"`
192+
PCRs map[uint][]byte `cbor:"pcrs"`
193+
Certificate []byte `cbor:"certificate"`
194+
CABundle [][]byte `cbor:"cabundle"`
195+
PublicKey []byte `cbor:"public_key,omitempty"`
196+
UserData []byte `cbor:"user_data,omitempty"`
197+
Nonce []byte `cbor:"nonce,omitempty"`
198+
}
199+
200+
type coseHeader struct {
201+
Alg int64 `cbor:"1,keyasint"`
202+
}
203+
204+
type cosePayload struct {
205+
_ struct{} `cbor:",toarray"` //nolint:revive // idiomatic CBOR array encoding
206+
Protected []byte
207+
Unprotected cbor.RawMessage
208+
Payload []byte
209+
Signature []byte
210+
}
211+
212+
type coseSignature struct {
213+
_ struct{} `cbor:",toarray"` //nolint:revive // idiomatic CBOR array encoding
214+
Context string
215+
Protected []byte
216+
ExternalAAD []byte
217+
Payload []byte
218+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
package nitrofake
2+
3+
import (
4+
"testing"
5+
6+
"github.com/stretchr/testify/require"
7+
8+
"github.com/smartcontractkit/chainlink-common/pkg/teeattestation/nitro"
9+
)
10+
11+
func TestAttestor_RoundTrip(t *testing.T) {
12+
fa, err := NewAttestor()
13+
require.NoError(t, err)
14+
15+
userData := []byte("test-user-data-12345")
16+
attestation, err := fa.CreateAttestation(userData)
17+
require.NoError(t, err)
18+
require.NotEmpty(t, attestation)
19+
20+
err = nitro.ValidateAttestationWithRoots(attestation, userData, fa.TrustedPCRsJSON(), fa.CARootsPEM())
21+
require.NoError(t, err)
22+
}
23+
24+
func TestAttestor_TrustedPCRsJSON(t *testing.T) {
25+
fa, err := NewAttestor()
26+
require.NoError(t, err)
27+
28+
pcrsJSON := fa.TrustedPCRsJSON()
29+
require.NotEmpty(t, pcrsJSON)
30+
require.Contains(t, string(pcrsJSON), `"pcr0"`)
31+
require.Contains(t, string(pcrsJSON), `"pcr1"`)
32+
require.Contains(t, string(pcrsJSON), `"pcr2"`)
33+
}
34+
35+
func TestAttestor_CARootsPEM(t *testing.T) {
36+
fa, err := NewAttestor()
37+
require.NoError(t, err)
38+
39+
pemStr := fa.CARootsPEM()
40+
require.Contains(t, pemStr, "BEGIN CERTIFICATE")
41+
require.Contains(t, pemStr, "END CERTIFICATE")
42+
}

0 commit comments

Comments
 (0)