Skip to content

Commit f9dede0

Browse files
committed
Add teeattestation package for TEE attestation validation
1 parent e8eaeb7 commit f9dede0

7 files changed

Lines changed: 476 additions & 0 deletions

File tree

pkg/teeattestation/go.mod

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
module github.com/smartcontractkit/chainlink-common/pkg/teeattestation
2+
3+
go 1.25.3
4+
5+
require (
6+
github.com/fxamacker/cbor/v2 v2.9.0
7+
github.com/hf/nitrite v0.0.0-20241225144000-c2d5d3c4f303
8+
github.com/stretchr/testify v1.11.1
9+
)
10+
11+
require (
12+
github.com/davecgh/go-spew v1.1.1 // indirect
13+
github.com/pmezard/go-difflib v1.0.0 // indirect
14+
github.com/x448/float16 v0.8.4 // indirect
15+
gopkg.in/yaml.v3 v3.0.1 // indirect
16+
)

pkg/teeattestation/go.sum

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

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

0 commit comments

Comments
 (0)