Skip to content

Commit 76bf69e

Browse files
committed
Remove nitrite from Nitro attestation validation
1 parent 767c996 commit 76bf69e

6 files changed

Lines changed: 267 additions & 35 deletions

File tree

go.mod

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@ require (
2121
github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.0.1
2222
github.com/hashicorp/go-hclog v1.6.3
2323
github.com/hashicorp/go-plugin v1.7.0
24-
github.com/hf/nitrite v0.0.0-20241225144000-c2d5d3c4f303
2524
github.com/iancoleman/strcase v0.3.0
2625
github.com/invopop/jsonschema v0.13.0
2726
github.com/jackc/pgx/v4 v4.18.3

go.sum

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

pkg/teeattestation/nitro/fake/fake.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// Package nitrofake provides an Attestor 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.
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.
55
package nitrofake
66

77
import (
@@ -21,7 +21,7 @@ import (
2121
)
2222

2323
// Attestor produces structurally valid COSE Sign1 attestation documents
24-
// that pass nitrite.Verify with a custom CA root.
24+
// that pass the local Nitro validator with a custom CA root.
2525
type Attestor struct {
2626
rootKey *ecdsa.PrivateKey
2727
rootCert *x509.Certificate

pkg/teeattestation/nitro/fake/fake_test.go

Lines changed: 3 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,10 @@ package nitrofake
22

33
import (
44
"testing"
5-
"time"
65

7-
"github.com/hf/nitrite"
86
"github.com/stretchr/testify/require"
7+
8+
"github.com/smartcontractkit/chainlink-common/pkg/teeattestation/nitro"
99
)
1010

1111
func TestAttestor_RoundTrip(t *testing.T) {
@@ -17,19 +17,8 @@ func TestAttestor_RoundTrip(t *testing.T) {
1717
require.NoError(t, err)
1818
require.NotEmpty(t, attestation)
1919

20-
result, err := nitrite.Verify(attestation, nitrite.VerifyOptions{
21-
CurrentTime: time.Now(),
22-
Roots: fa.CARoots(),
23-
})
20+
err = nitro.ValidateAttestationWithRoots(attestation, userData, fa.TrustedPCRsJSON(), fa.CARootsPEM())
2421
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)
3322
}
3423

3524
func TestAttestor_TrustedPCRsJSON(t *testing.T) {

pkg/teeattestation/nitro/validate.go

Lines changed: 9 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,6 @@ import (
99
"errors"
1010
"fmt"
1111
"time"
12-
13-
"github.com/hf/nitrite"
1412
)
1513

1614
// HexBytes is a custom type that unmarshals hex strings into a byte slice
@@ -71,35 +69,32 @@ func ValidateAttestationWithRoots(attestation, expectedUserData, trustedMeasurem
7169
if !ok {
7270
return errors.New("failed to parse CA roots")
7371
}
74-
result, err := nitrite.Verify(attestation, nitrite.VerifyOptions{
75-
CurrentTime: time.Now(),
76-
Roots: pool,
77-
})
72+
result, err := verifyAttestationDocument(attestation, pool, time.Now())
7873
if err != nil {
7974
return fmt.Errorf("failed to verify nitro attestation: %w", err)
8075
}
81-
if !result.SignatureOK {
76+
if !result.signatureOK {
8277
return errors.New("signature verification failed")
8378
}
8479

85-
if !bytes.Equal(expectedUserData, result.Document.UserData) {
86-
return fmt.Errorf("expected user data %x, got %x", expectedUserData, result.Document.UserData)
80+
if !bytes.Equal(expectedUserData, result.document.UserData) {
81+
return fmt.Errorf("expected user data %x, got %x", expectedUserData, result.document.UserData)
8782
}
8883

8984
var trustedPCRs PCRs
9085
if err := json.Unmarshal(trustedMeasurements, &trustedPCRs); err != nil {
9186
return fmt.Errorf("failed to unmarshal trusted PCRs: %w", err)
9287
}
93-
if len(result.Document.PCRs) < 3 {
94-
return fmt.Errorf("attestation document has %d PCRs, need at least 3", len(result.Document.PCRs))
88+
if len(result.document.PCRs) < 3 {
89+
return fmt.Errorf("attestation document has %d PCRs, need at least 3", len(result.document.PCRs))
9590
}
96-
if !bytes.Equal(result.Document.PCRs[0], trustedPCRs.PCR0) {
91+
if !bytes.Equal(result.document.PCRs[0], trustedPCRs.PCR0) {
9792
return fmt.Errorf("PCR0 mismatch: expected %x", trustedPCRs.PCR0)
9893
}
99-
if !bytes.Equal(result.Document.PCRs[1], trustedPCRs.PCR1) {
94+
if !bytes.Equal(result.document.PCRs[1], trustedPCRs.PCR1) {
10095
return fmt.Errorf("PCR1 mismatch: expected %x", trustedPCRs.PCR1)
10196
}
102-
if !bytes.Equal(result.Document.PCRs[2], trustedPCRs.PCR2) {
97+
if !bytes.Equal(result.document.PCRs[2], trustedPCRs.PCR2) {
10398
return fmt.Errorf("PCR2 mismatch: expected %x", trustedPCRs.PCR2)
10499
}
105100
return nil

pkg/teeattestation/nitro/verify.go

Lines changed: 251 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,251 @@
1+
package nitro
2+
3+
import (
4+
"crypto/ecdsa"
5+
"crypto/sha256"
6+
"crypto/sha512"
7+
"crypto/x509"
8+
"errors"
9+
"fmt"
10+
"math/big"
11+
"time"
12+
13+
"github.com/fxamacker/cbor/v2"
14+
)
15+
16+
var (
17+
errBadCOSESign1Structure = errors.New("data is not a COSE Sign1 array")
18+
errEmptyProtectedSection = errors.New("COSE Sign1 protected section is nil or empty")
19+
errEmptyPayloadSection = errors.New("COSE Sign1 payload section is nil or empty")
20+
errEmptySignatureSection = errors.New("COSE Sign1 signature section is nil or empty")
21+
errUnsupportedSignatureAlgorithm = errors.New("COSE Sign1 algorithm is not ECDSA P-384")
22+
errBadAttestationDocument = errors.New("bad attestation document")
23+
errMandatoryFieldsMissing = errors.New("attestation document is missing mandatory fields")
24+
errBadDigest = errors.New("attestation digest is not SHA384")
25+
errBadTimestamp = errors.New("attestation timestamp is 0")
26+
errBadPCRs = errors.New("attestation pcrs is less than 1 or more than 32")
27+
errBadPCRIndex = errors.New("attestation pcr index is not in [0, 32)")
28+
errBadPCRValue = errors.New("attestation pcr value length is invalid")
29+
errBadCABundle = errors.New("attestation cabundle is empty")
30+
errBadCABundleItem = errors.New("attestation cabundle item is empty or too large")
31+
errBadPublicKey = errors.New("attestation public_key length is invalid")
32+
errBadUserData = errors.New("attestation user_data length is invalid")
33+
errBadNonce = errors.New("attestation nonce length is invalid")
34+
errBadCertificatePublicKeyAlgo = errors.New("attestation certificate public key algorithm is not ECDSA")
35+
errBadCertificateSigningAlgo = errors.New("attestation certificate signature algorithm is not ECDSAWithSHA384")
36+
errBadSignature = errors.New("attestation signature does not match certificate")
37+
)
38+
39+
type verifyResult struct {
40+
document *attestationDocument
41+
signatureOK bool
42+
}
43+
44+
type attestationDocument struct {
45+
ModuleID string `cbor:"module_id"`
46+
Timestamp uint64 `cbor:"timestamp"`
47+
Digest string `cbor:"digest"`
48+
PCRs map[uint][]byte `cbor:"pcrs"`
49+
Certificate []byte `cbor:"certificate"`
50+
CABundle [][]byte `cbor:"cabundle"`
51+
PublicKey []byte `cbor:"public_key,omitempty"`
52+
UserData []byte `cbor:"user_data,omitempty"`
53+
Nonce []byte `cbor:"nonce,omitempty"`
54+
}
55+
56+
type coseProtectedHeader struct {
57+
Alg any `cbor:"1,keyasint,omitempty"`
58+
}
59+
60+
type coseSign1 struct {
61+
_ struct{} `cbor:",toarray"` //nolint:revive // idiomatic CBOR array encoding
62+
Protected []byte
63+
Unprotected cbor.RawMessage
64+
Payload []byte
65+
Signature []byte
66+
}
67+
68+
type coseSignatureInput struct {
69+
_ struct{} `cbor:",toarray"` //nolint:revive // idiomatic CBOR array encoding
70+
Context string
71+
Protected []byte
72+
ExternalAAD []byte
73+
Payload []byte
74+
}
75+
76+
func verifyAttestationDocument(data []byte, roots *x509.CertPool, currentTime time.Time) (*verifyResult, error) {
77+
var sign1 coseSign1
78+
if err := cbor.Unmarshal(data, &sign1); err != nil {
79+
return nil, errBadCOSESign1Structure
80+
}
81+
if len(sign1.Protected) == 0 {
82+
return nil, errEmptyProtectedSection
83+
}
84+
if len(sign1.Payload) == 0 {
85+
return nil, errEmptyPayloadSection
86+
}
87+
if len(sign1.Signature) == 0 {
88+
return nil, errEmptySignatureSection
89+
}
90+
91+
var protected coseProtectedHeader
92+
if err := cbor.Unmarshal(sign1.Protected, &protected); err != nil {
93+
return nil, errBadCOSESign1Structure
94+
}
95+
if err := validateProtectedAlgorithm(protected.Alg); err != nil {
96+
return nil, err
97+
}
98+
99+
var doc attestationDocument
100+
if err := cbor.Unmarshal(sign1.Payload, &doc); err != nil {
101+
return nil, errBadAttestationDocument
102+
}
103+
if err := validateAttestationPayload(&doc); err != nil {
104+
return nil, err
105+
}
106+
107+
leafCert, intermediates, err := parseCertificateChain(&doc)
108+
if err != nil {
109+
return nil, err
110+
}
111+
if currentTime.IsZero() {
112+
currentTime = time.Now()
113+
}
114+
if _, err := leafCert.Verify(x509.VerifyOptions{
115+
Intermediates: intermediates,
116+
Roots: roots,
117+
CurrentTime: currentTime,
118+
KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageAny},
119+
}); err != nil {
120+
return nil, err
121+
}
122+
123+
sigStructure, err := cbor.Marshal(&coseSignatureInput{
124+
Context: "Signature1",
125+
Protected: sign1.Protected,
126+
ExternalAAD: []byte{},
127+
Payload: sign1.Payload,
128+
})
129+
if err != nil {
130+
return nil, fmt.Errorf("build signature structure: %w", err)
131+
}
132+
133+
pubKey, ok := leafCert.PublicKey.(*ecdsa.PublicKey)
134+
if !ok {
135+
return nil, errBadCertificatePublicKeyAlgo
136+
}
137+
signatureOK := verifyECDSASignature(pubKey, sigStructure, sign1.Signature)
138+
if !signatureOK {
139+
return &verifyResult{document: &doc, signatureOK: false}, errBadSignature
140+
}
141+
142+
return &verifyResult{document: &doc, signatureOK: true}, nil
143+
}
144+
145+
func validateProtectedAlgorithm(alg any) error {
146+
switch v := alg.(type) {
147+
case int64:
148+
if v == -35 {
149+
return nil
150+
}
151+
case string:
152+
if v == "ES384" {
153+
return nil
154+
}
155+
}
156+
return errUnsupportedSignatureAlgorithm
157+
}
158+
159+
func validateAttestationPayload(doc *attestationDocument) error {
160+
if doc.ModuleID == "" || doc.Digest == "" || doc.Timestamp == 0 || doc.PCRs == nil || doc.Certificate == nil || doc.CABundle == nil {
161+
return errMandatoryFieldsMissing
162+
}
163+
if doc.Digest != "SHA384" {
164+
return errBadDigest
165+
}
166+
if doc.Timestamp < 1 {
167+
return errBadTimestamp
168+
}
169+
if len(doc.PCRs) < 1 || len(doc.PCRs) > 32 {
170+
return errBadPCRs
171+
}
172+
for idx, value := range doc.PCRs {
173+
if idx > 31 {
174+
return errBadPCRIndex
175+
}
176+
if value == nil || (len(value) != 32 && len(value) != 48 && len(value) != 64) {
177+
return errBadPCRValue
178+
}
179+
}
180+
if len(doc.CABundle) < 1 {
181+
return errBadCABundle
182+
}
183+
for _, item := range doc.CABundle {
184+
if item == nil || len(item) < 1 || len(item) > 1024 {
185+
return errBadCABundleItem
186+
}
187+
}
188+
if doc.PublicKey != nil && len(doc.PublicKey) > 1024 {
189+
return errBadPublicKey
190+
}
191+
if doc.UserData != nil && len(doc.UserData) > 1024 {
192+
return errBadUserData
193+
}
194+
if doc.Nonce != nil && len(doc.Nonce) > 1024 {
195+
return errBadNonce
196+
}
197+
return nil
198+
}
199+
200+
func parseCertificateChain(doc *attestationDocument) (*x509.Certificate, *x509.CertPool, error) {
201+
leafCert, err := x509.ParseCertificate(doc.Certificate)
202+
if err != nil {
203+
return nil, nil, err
204+
}
205+
if leafCert.PublicKeyAlgorithm != x509.ECDSA {
206+
return nil, nil, errBadCertificatePublicKeyAlgo
207+
}
208+
if leafCert.SignatureAlgorithm != x509.ECDSAWithSHA384 {
209+
return nil, nil, errBadCertificateSigningAlgo
210+
}
211+
212+
intermediates := x509.NewCertPool()
213+
for _, der := range doc.CABundle {
214+
cert, err := x509.ParseCertificate(der)
215+
if err != nil {
216+
return nil, nil, err
217+
}
218+
intermediates.AddCert(cert)
219+
}
220+
return leafCert, intermediates, nil
221+
}
222+
223+
func verifyECDSASignature(publicKey *ecdsa.PublicKey, sigStructure, signature []byte) bool {
224+
hash, ok := hashForCurve(publicKey, sigStructure)
225+
if !ok || len(signature) != 2*len(hash) {
226+
return false
227+
}
228+
229+
r := new(big.Int).SetBytes(signature[:len(hash)])
230+
s := new(big.Int).SetBytes(signature[len(hash):])
231+
return ecdsa.Verify(publicKey, hash, r, s)
232+
}
233+
234+
func hashForCurve(publicKey *ecdsa.PublicKey, sigStructure []byte) ([]byte, bool) {
235+
switch publicKey.Curve.Params().Name {
236+
case "P-224":
237+
sum := sha256.Sum224(sigStructure)
238+
return sum[:], true
239+
case "P-256":
240+
sum := sha256.Sum256(sigStructure)
241+
return sum[:], true
242+
case "P-384":
243+
sum := sha512.Sum384(sigStructure)
244+
return sum[:], true
245+
case "P-512":
246+
sum := sha512.Sum512(sigStructure)
247+
return sum[:], true
248+
default:
249+
return nil, false
250+
}
251+
}

0 commit comments

Comments
 (0)