Skip to content

Commit 41ef4d1

Browse files
committed
simple chain validation
1 parent 02b96e2 commit 41ef4d1

7 files changed

Lines changed: 247 additions & 127 deletions

File tree

README.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,19 @@ func main() {
6262
log.Fatalf("attestation: attested bundle differs from the expected one")
6363
}
6464

65+
// Verify validity instant
66+
instant := time.Now()
67+
if !(instant.After(res.LeafCert.NotBefore) && instant.Before(res.LeafCert.NotAfter)) {
68+
log.Fatalf("attestation: not valid at expected time")
69+
}
70+
71+
// (optional) verify the key id of the signer
72+
expectedKeyID := []byte("myexpectedkeyid")
73+
if !bytes.Equal(expectedKeyID, res.KeyID) {
74+
log.Fatalf("attestation: unexpected signer id ")
75+
}
76+
77+
6578
fmt.Printf("Attestation successful. Sign count: %d\n", res.AuthenticatorData.SignCount)
6679
}
6780
```

appattest/appattest_impl.go

Lines changed: 19 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@ package appattest
22

33
import (
44
"crypto/x509"
5-
"time"
5+
"encoding/pem"
6+
"fmt"
67

78
"github.com/pkg/errors"
89
"github.com/splitsecure/go-app-attest/authenticatordata"
@@ -28,13 +29,11 @@ type Attestor interface {
2829
}
2930

3031
type AttestorImpl struct {
31-
aaroots *x509.CertPool
32-
nowfn func() time.Time
32+
aaroots []*x509.Certificate
3333
}
3434

3535
type optionsState struct {
36-
aaroots *x509.CertPool
37-
nowfn func() time.Time
36+
aaroots []*x509.Certificate
3837
}
3938

4039
type option struct {
@@ -47,17 +46,10 @@ func newoption(fn func(*optionsState)) option {
4746
}
4847
}
4948

50-
// WithAppAttestRoots lets the user provide its own authoritative certs pool
51-
func WithAppAttestRoots(pool *x509.CertPool) option {
49+
// WithAppAttestRoots lets the user provide its own authoritative certificates
50+
func WithAppAttestRoots(certs []*x509.Certificate) option {
5251
return newoption(func(s *optionsState) {
53-
s.aaroots = pool
54-
})
55-
}
56-
57-
// WithNowFn lets the user provide its own time.Now function
58-
func WithNowFn(now func() time.Time) option {
59-
return newoption(func(os *optionsState) {
60-
os.nowfn = now
52+
s.aaroots = certs
6153
})
6254
}
6355

@@ -73,41 +65,37 @@ func New(
7365
option.apply(&optionsState)
7466
}
7567

76-
// determine pool
68+
// determine root certificates
7769
if optionsState.aaroots == nil {
7870
// use the certificate provided by the library
79-
att.aaroots = x509.NewCertPool()
80-
if !att.aaroots.AppendCertsFromPEM([]byte(appattestRootCAPEM)) {
81-
return nil, errors.New("loading library provided app attest ca")
71+
block, _ := pem.Decode([]byte(appattestRootCAPEM))
72+
if block == nil {
73+
return nil, errors.New("failed to parse app attest root CA PEM")
74+
}
75+
cert, err := x509.ParseCertificate(block.Bytes)
76+
if err != nil {
77+
return nil, fmt.Errorf("parsing app attest root CA: %w", err)
8278
}
79+
att.aaroots = []*x509.Certificate{cert}
8380
} else {
84-
// use the user provided pool
81+
// use the user provided certificates
8582
att.aaroots = optionsState.aaroots
8683
}
8784

88-
// determine timefn
89-
if optionsState.nowfn == nil {
90-
att.nowfn = time.Now
91-
} else {
92-
att.nowfn = optionsState.nowfn
93-
}
94-
9585
return att, nil
9686
}
9787

9888
type VerifyAttestationInput struct {
9989
ServerChallenge []byte
10090
AttestationCBOR []byte
101-
KeyIdentifier []byte
10291

10392
OutAuthenticatorData *authenticatordata.T
10493
}
10594

10695
func (at *AttestorImpl) VerifyAttestation(in *VerifyAttestationInput) (VerifyAttestationOutput, error) {
107-
subtleIn := VerifyAttestationInputStateless{
96+
subtleIn := VerifyAttestationInputPure{
10897
AttestationInput: in,
109-
Time: at.nowfn(),
11098
AARoots: at.aaroots,
11199
}
112-
return VerifyAttestationStateless(&subtleIn)
100+
return VerifyAttestationPure(&subtleIn)
113101
}

appattest/appattest_impl_test.go

Lines changed: 12 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -13,21 +13,11 @@ import (
1313
)
1414

1515
func TestAppAttest(t *testing.T) {
16-
nowFn := func() time.Time {
17-
t, err := time.Parse(time.DateOnly, "2024-04-18")
18-
if err != nil {
19-
panic(err)
20-
}
21-
return t
22-
}
23-
2416
// create an attestor
2517
bundleDigest, err := base64.StdEncoding.AppendDecode(nil, []byte("FVhAM8lQuf6dUUziohGjJtcaprEBSrTG+i+9qdmqGKY="))
2618
require.NoError(t, err)
2719

28-
attestor, err := appattest.New(
29-
appattest.WithNowFn(nowFn),
30-
)
20+
attestor, err := appattest.New()
3121
require.NoError(t, err)
3222

3323
// deserialize the sample case
@@ -41,32 +31,26 @@ func TestAppAttest(t *testing.T) {
4131
req := appattest.VerifyAttestationInput{
4232
ServerChallenge: []byte("test_server_challenge"),
4333
AttestationCBOR: attestationCBOR,
44-
KeyIdentifier: keyIdentifier,
4534
}
4635

4736
res, err := attestor.VerifyAttestation(&req)
4837
require.NoError(t, err)
4938
assert.Equal(t, uint32(0), res.AuthenticatorData.SignCount)
5039
require.Equal(t, bundleDigest, res.BundleDigest)
5140
require.Equal(t, appattest.AAGUIDProd, res.EnvironmentGUID)
41+
42+
validInstant := time.Date(2024, 4, 18, 0, 0, 0, 0, time.UTC)
43+
assert.True(t, validInstant.Before(res.LeafCert.NotAfter))
44+
assert.True(t, validInstant.After(res.LeafCert.NotBefore))
45+
assert.Equal(t, keyIdentifier, res.KeyID)
5246
}
5347

5448
func TestAppAttestDev(t *testing.T) {
55-
nowFn := func() time.Time {
56-
t, err := time.Parse(time.DateOnly, "2024-09-05")
57-
if err != nil {
58-
panic(err)
59-
}
60-
return t
61-
}
62-
6349
// pre-hash has the following shape: ABC6DEF.com.example.fooapp
6450
bundleDigest, err := base64.StdEncoding.AppendDecode(nil, []byte("FcoOH+2hZbXEsTrH0Orwx24jatXg6mk7q+38tfqkUbg="))
6551
require.NoError(t, err)
6652

67-
attestor, err := appattest.New(
68-
appattest.WithNowFn(nowFn),
69-
)
53+
attestor, err := appattest.New()
7054
require.NoError(t, err)
7155

7256
// deserialize the sample case
@@ -81,14 +65,17 @@ func TestAppAttestDev(t *testing.T) {
8165
req := appattest.VerifyAttestationInput{
8266
ServerChallenge: chalSum[:],
8367
AttestationCBOR: attestationCBOR,
84-
KeyIdentifier: keyIdentifier,
8568
}
8669

8770
res, err := attestor.VerifyAttestation(&req)
8871
require.NoError(t, err)
8972
assert.Equal(t, uint32(0), res.AuthenticatorData.SignCount)
9073
assert.Equal(t, bundleDigest, res.BundleDigest)
9174
assert.Equal(t, appattest.AAGUIDDev, res.EnvironmentGUID)
75+
validInstant := time.Date(2025, 4, 5, 0, 0, 0, 0, time.UTC)
76+
assert.True(t, validInstant.Before(res.LeafCert.NotAfter))
77+
assert.True(t, validInstant.After(res.LeafCert.NotBefore))
78+
assert.Equal(t, keyIdentifier, res.KeyID)
9279
}
9380

9481
func FuzzAttestationData(f *testing.F) {
@@ -104,28 +91,15 @@ func FuzzAttestationData(f *testing.F) {
10491
f.Add(tc) // Use f.Add to provide a seed corpus
10592
}
10693

107-
// prepare an attestor
108-
nowFn := func() time.Time {
109-
t, err := time.Parse(time.DateOnly, "2024-09-05")
110-
if err != nil {
111-
panic(err)
112-
}
113-
return t
114-
}
115-
116-
attestor, err := appattest.New(
117-
appattest.WithNowFn(nowFn),
118-
)
94+
attestor, err := appattest.New()
11995
require.NoError(f, err)
12096

12197
chalSum := sha256.Sum256([]byte("server_challenge"))
122-
keyIdentifier, err := base64.StdEncoding.AppendDecode(nil, []byte("B3hMn1CG/4of7s+TUkKLrS/6FAxsGft2N6PJhrzXI4E="))
12398
require.NoError(f, err)
12499

125100
out := authenticatordata.T{}
126101
req := appattest.VerifyAttestationInput{
127102
ServerChallenge: chalSum[:],
128-
KeyIdentifier: keyIdentifier,
129103
OutAuthenticatorData: &out,
130104
}
131105

appattest/verify_attestation.go

Lines changed: 36 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@ import (
77
"crypto/x509"
88
"encoding/asn1"
99
"encoding/hex"
10+
"encoding/pem"
1011
"fmt"
12+
"os"
1113
"reflect"
1214
"slices"
1315
"time"
@@ -21,10 +23,18 @@ const (
2123
Format = "apple-appattest"
2224
)
2325

24-
type VerifyAttestationInputStateless struct {
26+
var (
27+
NonceOID = asn1.ObjectIdentifier{1, 2, 840, 113635, 100, 8, 2}
28+
AAGUIDProd = Environment("appattest\x00\x00\x00\x00\x00\x00\x00")
29+
AAGUIDDev = Environment("appattestdevelop")
30+
)
31+
32+
type Environment = []byte
33+
34+
type VerifyAttestationInputPure struct {
2535
AttestationInput *VerifyAttestationInput
2636
Time time.Time
27-
AARoots *x509.CertPool
37+
AARoots []*x509.Certificate
2838
}
2939

3040
type VerifyAttestationOutput struct {
@@ -33,15 +43,16 @@ type VerifyAttestationOutput struct {
3343

3444
EnvironmentGUID Environment
3545
BundleDigest []byte
46+
KeyID []byte
3647
}
3748

3849
// AttestedPubkey returns the key from the leaf certificate
3950
func (o *VerifyAttestationOutput) AttestedPubkey() *ecdsa.PublicKey {
4051
return o.LeafCert.PublicKey.(*ecdsa.PublicKey)
4152
}
4253

43-
// VerifyAttestationStateless performs attestation without the guardrails provided by AppAttestImpl.
44-
func VerifyAttestationStateless(in *VerifyAttestationInputStateless) (VerifyAttestationOutput, error) {
54+
// VerifyAttestationPure performs attestation without the guardrails provided by AppAttestImpl.
55+
func VerifyAttestationPure(in *VerifyAttestationInputPure) (VerifyAttestationOutput, error) {
4556
// unmarshal the attestation object
4657
attestObj := AttestationObject{}
4758
err := cbor.Unmarshal(in.AttestationInput.AttestationCBOR, &attestObj)
@@ -54,24 +65,30 @@ func VerifyAttestationStateless(in *VerifyAttestationInputStateless) (VerifyAtte
5465
return VerifyAttestationOutput{}, fmt.Errorf("attestation object format mismatch: expected '%s', got '%s'", Format, attestObj.Format)
5566
}
5667

57-
// create a new cert verifier using the intermediates provided in the attestation object
58-
verifyOpts := x509.VerifyOptions{}
59-
if err := populateVerifyOpts(&verifyOpts, &attestObj, in.AARoots); err != nil {
60-
return VerifyAttestationOutput{}, fmt.Errorf("populating verify opts: %w", err)
68+
// Parse certificate chain from bytes to certificates
69+
chain := make([]*x509.Certificate, len(attestObj.AttestationStatement.X509CertChain))
70+
for i, certBytes := range attestObj.AttestationStatement.X509CertChain {
71+
cert, err := x509.ParseCertificate(certBytes)
72+
if err != nil {
73+
return VerifyAttestationOutput{}, fmt.Errorf("parsing certificate at index %d: %w", i, err)
74+
}
75+
chain[i] = cert
6176
}
62-
verifyOpts.CurrentTime = in.Time
6377

64-
// parse the leaf certificate
65-
leafCert, err := x509.ParseCertificate(attestObj.AttestationStatement.X509CertChain[0])
66-
if err != nil {
67-
return VerifyAttestationOutput{}, fmt.Errorf("parsing leaf certificate: %w", err)
78+
if err := VerifyChain(chain, in.AARoots); err != nil {
79+
return VerifyAttestationOutput{}, fmt.Errorf("verifying certificate chain: %w", err)
6880
}
6981

70-
// verify the leaf certificate
71-
_, err = leafCert.Verify(verifyOpts)
72-
if err != nil {
73-
return VerifyAttestationOutput{}, fmt.Errorf("verifying leaf certificate: %w", err)
82+
// get the leaf certificate (first in chain)
83+
leafCert := chain[0]
84+
pblock := pem.Block{
85+
Type: "CERTIFICATE",
86+
Bytes: leafCert.Raw,
7487
}
88+
if err := pem.Encode(os.Stdout, &pblock); err != nil {
89+
panic(err)
90+
}
91+
fmt.Println()
7592

7693
// > 2. Create clientDataHash as the SHA256 hash of the one-time challenge your server sends
7794
// > to your app before performing the attestation,
@@ -102,11 +119,6 @@ func VerifyAttestationStateless(in *VerifyAttestationInputStateless) (VerifyAtte
102119

103120
computedPubkeyHash := ComputeKeyHash(certPubKey)
104121

105-
// assert that the public key of the leaf certificate matches the key handle returned by the app
106-
if !bytes.Equal(in.AttestationInput.KeyIdentifier, computedPubkeyHash[:]) {
107-
return VerifyAttestationOutput{}, fmt.Errorf("key identifier did not match public key of leaf certificate: %s != %s", hex.EncodeToString(computedPubkeyHash[:]), hex.EncodeToString(in.AttestationInput.KeyIdentifier))
108-
}
109-
110122
authenticatorData := in.AttestationInput.OutAuthenticatorData
111123
if authenticatorData == nil {
112124
authenticatorData = &authenticatordata.T{}
@@ -117,7 +129,7 @@ func VerifyAttestationStateless(in *VerifyAttestationInputStateless) (VerifyAtte
117129
}
118130

119131
// > 9. Verify that the authenticator data’s credentialId field is the same as the key identifier.
120-
if !bytes.Equal(in.AttestationInput.KeyIdentifier, authenticatorData.AttestedCredentialData.CredentialID) {
132+
if !bytes.Equal(computedPubkeyHash[:], authenticatorData.AttestedCredentialData.CredentialID) {
121133
return VerifyAttestationOutput{}, fmt.Errorf("key identifier did not match attested credential id of authenticator data")
122134
}
123135

@@ -127,29 +139,10 @@ func VerifyAttestationStateless(in *VerifyAttestationInputStateless) (VerifyAtte
127139

128140
EnvironmentGUID: authenticatorData.AttestedCredentialData.AAGUID,
129141
BundleDigest: authenticatorData.RelayingPartyHash,
142+
KeyID: computedPubkeyHash[:],
130143
}, nil
131144
}
132145

133-
func populateVerifyOpts(dst *x509.VerifyOptions, attObj *AttestationObject, aaroots *x509.CertPool) (err error) {
134-
if len(attObj.AttestationStatement.X509CertChain) < 1 {
135-
return errors.New("expected at least one certificate in x509 cert chain")
136-
}
137-
138-
// set the intermediates
139-
dst.Intermediates = x509.NewCertPool()
140-
// skip the first element, it's the leaf certificate
141-
for _, inter := range attObj.AttestationStatement.X509CertChain[1:] {
142-
cert, err := x509.ParseCertificate(inter)
143-
if err != nil {
144-
return errors.Wrap(err, "parsing intermediate")
145-
}
146-
dst.Intermediates.AddCert(cert)
147-
dst.Roots = aaroots
148-
}
149-
150-
return nil
151-
}
152-
153146
func extractNonceFromCert(c *x509.Certificate) ([]byte, error) {
154147
var oidValue []byte
155148
for _, ext := range c.Extensions {
@@ -216,11 +209,3 @@ func ComputeNonce(authData, clientDataHash []byte) (res [sha256.Size]byte, err e
216209
func ComputeKeyHash(key *ecdsa.PublicKey) [sha256.Size]byte {
217210
return sha256.Sum256(ellipticPointToX962Uncompressed(key))
218211
}
219-
220-
type Environment = []byte
221-
222-
var (
223-
NonceOID = asn1.ObjectIdentifier{1, 2, 840, 113635, 100, 8, 2}
224-
AAGUIDProd = Environment("appattest\x00\x00\x00\x00\x00\x00\x00")
225-
AAGUIDDev = Environment("appattestdevelop")
226-
)

0 commit comments

Comments
 (0)