Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,19 @@ func main() {
log.Fatalf("attestation: attested bundle differs from the expected one")
}

// Verify validity instant
instant := time.Now()
if !(instant.After(res.LeafCert.NotBefore) && instant.Before(res.LeafCert.NotAfter)) {
log.Fatalf("attestation: not valid at expected time")
}

// (optional) verify the key id of the signer
expectedKeyID := []byte("myexpectedkeyid")
if !bytes.Equal(expectedKeyID, res.KeyID) {
log.Fatalf("attestation: unexpected signer id ")
}


fmt.Printf("Attestation successful. Sign count: %d\n", res.AuthenticatorData.SignCount)
}
```
Expand Down
50 changes: 19 additions & 31 deletions appattest/appattest_impl.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ package appattest

import (
"crypto/x509"
"time"
"encoding/pem"
"fmt"

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

type AttestorImpl struct {
aaroots *x509.CertPool
nowfn func() time.Time
aaroots []*x509.Certificate
}

type optionsState struct {
aaroots *x509.CertPool
nowfn func() time.Time
aaroots []*x509.Certificate
}

type option struct {
Expand All @@ -47,17 +46,10 @@ func newoption(fn func(*optionsState)) option {
}
}

// WithAppAttestRoots lets the user provide its own authoritative certs pool
func WithAppAttestRoots(pool *x509.CertPool) option {
// WithAppAttestRoots lets the user provide its own authoritative certificates
func WithAppAttestRoots(certs []*x509.Certificate) option {
return newoption(func(s *optionsState) {
s.aaroots = pool
})
}

// WithNowFn lets the user provide its own time.Now function
func WithNowFn(now func() time.Time) option {
return newoption(func(os *optionsState) {
os.nowfn = now
s.aaroots = certs
})
}

Expand All @@ -73,41 +65,37 @@ func New(
option.apply(&optionsState)
}

// determine pool
// determine root certificates
if optionsState.aaroots == nil {
// use the certificate provided by the library
att.aaroots = x509.NewCertPool()
if !att.aaroots.AppendCertsFromPEM([]byte(appattestRootCAPEM)) {
return nil, errors.New("loading library provided app attest ca")
block, _ := pem.Decode([]byte(appattestRootCAPEM))
if block == nil {
return nil, errors.New("failed to parse app attest root CA PEM")
}
cert, err := x509.ParseCertificate(block.Bytes)
if err != nil {
return nil, fmt.Errorf("parsing app attest root CA: %w", err)
}
att.aaroots = []*x509.Certificate{cert}
} else {
// use the user provided pool
// use the user provided certificates
att.aaroots = optionsState.aaroots
}

// determine timefn
if optionsState.nowfn == nil {
att.nowfn = time.Now
} else {
att.nowfn = optionsState.nowfn
}

return att, nil
}

type VerifyAttestationInput struct {
ServerChallenge []byte
AttestationCBOR []byte
KeyIdentifier []byte

OutAuthenticatorData *authenticatordata.T
}

func (at *AttestorImpl) VerifyAttestation(in *VerifyAttestationInput) (VerifyAttestationOutput, error) {
subtleIn := VerifyAttestationInputStateless{
subtleIn := VerifyAttestationInputPure{
AttestationInput: in,
Time: at.nowfn(),
AARoots: at.aaroots,
}
return VerifyAttestationStateless(&subtleIn)
return VerifyAttestationPure(&subtleIn)
}
50 changes: 12 additions & 38 deletions appattest/appattest_impl_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,21 +13,11 @@ import (
)

func TestAppAttest(t *testing.T) {
nowFn := func() time.Time {
t, err := time.Parse(time.DateOnly, "2024-04-18")
if err != nil {
panic(err)
}
return t
}

// create an attestor
bundleDigest, err := base64.StdEncoding.AppendDecode(nil, []byte("FVhAM8lQuf6dUUziohGjJtcaprEBSrTG+i+9qdmqGKY="))
require.NoError(t, err)

attestor, err := appattest.New(
appattest.WithNowFn(nowFn),
)
attestor, err := appattest.New()
require.NoError(t, err)

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

res, err := attestor.VerifyAttestation(&req)
require.NoError(t, err)
assert.Equal(t, uint32(0), res.AuthenticatorData.SignCount)
require.Equal(t, bundleDigest, res.BundleDigest)
require.Equal(t, appattest.AAGUIDProd, res.EnvironmentGUID)

validInstant := time.Date(2024, 4, 18, 0, 0, 0, 0, time.UTC)
assert.True(t, validInstant.Before(res.LeafCert.NotAfter))
assert.True(t, validInstant.After(res.LeafCert.NotBefore))
assert.Equal(t, keyIdentifier, res.KeyID)
}

func TestAppAttestDev(t *testing.T) {
nowFn := func() time.Time {
t, err := time.Parse(time.DateOnly, "2024-09-05")
if err != nil {
panic(err)
}
return t
}

// pre-hash has the following shape: ABC6DEF.com.example.fooapp
bundleDigest, err := base64.StdEncoding.AppendDecode(nil, []byte("FcoOH+2hZbXEsTrH0Orwx24jatXg6mk7q+38tfqkUbg="))
require.NoError(t, err)

attestor, err := appattest.New(
appattest.WithNowFn(nowFn),
)
attestor, err := appattest.New()
require.NoError(t, err)

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

res, err := attestor.VerifyAttestation(&req)
require.NoError(t, err)
assert.Equal(t, uint32(0), res.AuthenticatorData.SignCount)
assert.Equal(t, bundleDigest, res.BundleDigest)
assert.Equal(t, appattest.AAGUIDDev, res.EnvironmentGUID)
validInstant := time.Date(2025, 4, 5, 0, 0, 0, 0, time.UTC)
assert.True(t, validInstant.Before(res.LeafCert.NotAfter))
assert.True(t, validInstant.After(res.LeafCert.NotBefore))
assert.Equal(t, keyIdentifier, res.KeyID)
}

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

// prepare an attestor
nowFn := func() time.Time {
t, err := time.Parse(time.DateOnly, "2024-09-05")
if err != nil {
panic(err)
}
return t
}

attestor, err := appattest.New(
appattest.WithNowFn(nowFn),
)
attestor, err := appattest.New()
require.NoError(f, err)

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

out := authenticatordata.T{}
req := appattest.VerifyAttestationInput{
ServerChallenge: chalSum[:],
KeyIdentifier: keyIdentifier,
OutAuthenticatorData: &out,
}

Expand Down
87 changes: 36 additions & 51 deletions appattest/verify_attestation.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ import (
"crypto/x509"
"encoding/asn1"
"encoding/hex"
"encoding/pem"
"fmt"
"os"
"reflect"
"slices"
"time"
Expand All @@ -21,10 +23,18 @@ const (
Format = "apple-appattest"
)

type VerifyAttestationInputStateless struct {
var (
NonceOID = asn1.ObjectIdentifier{1, 2, 840, 113635, 100, 8, 2}
AAGUIDProd = Environment("appattest\x00\x00\x00\x00\x00\x00\x00")
AAGUIDDev = Environment("appattestdevelop")
)

type Environment = []byte

type VerifyAttestationInputPure struct {
AttestationInput *VerifyAttestationInput
Time time.Time
AARoots *x509.CertPool
AARoots []*x509.Certificate
}

type VerifyAttestationOutput struct {
Expand All @@ -33,15 +43,16 @@ type VerifyAttestationOutput struct {

EnvironmentGUID Environment
BundleDigest []byte
KeyID []byte
}

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

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

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

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

// verify the leaf certificate
_, err = leafCert.Verify(verifyOpts)
if err != nil {
return VerifyAttestationOutput{}, fmt.Errorf("verifying leaf certificate: %w", err)
// get the leaf certificate (first in chain)
leafCert := chain[0]
pblock := pem.Block{
Type: "CERTIFICATE",
Bytes: leafCert.Raw,
}
if err := pem.Encode(os.Stdout, &pblock); err != nil {
panic(err)
}
fmt.Println()

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

computedPubkeyHash := ComputeKeyHash(certPubKey)

// assert that the public key of the leaf certificate matches the key handle returned by the app
if !bytes.Equal(in.AttestationInput.KeyIdentifier, computedPubkeyHash[:]) {
return VerifyAttestationOutput{}, fmt.Errorf("key identifier did not match public key of leaf certificate: %s != %s", hex.EncodeToString(computedPubkeyHash[:]), hex.EncodeToString(in.AttestationInput.KeyIdentifier))
}

authenticatorData := in.AttestationInput.OutAuthenticatorData
if authenticatorData == nil {
authenticatorData = &authenticatordata.T{}
Expand All @@ -117,7 +129,7 @@ func VerifyAttestationStateless(in *VerifyAttestationInputStateless) (VerifyAtte
}

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

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

EnvironmentGUID: authenticatorData.AttestedCredentialData.AAGUID,
BundleDigest: authenticatorData.RelayingPartyHash,
KeyID: computedPubkeyHash[:],
}, nil
}

func populateVerifyOpts(dst *x509.VerifyOptions, attObj *AttestationObject, aaroots *x509.CertPool) (err error) {
if len(attObj.AttestationStatement.X509CertChain) < 1 {
return errors.New("expected at least one certificate in x509 cert chain")
}

// set the intermediates
dst.Intermediates = x509.NewCertPool()
// skip the first element, it's the leaf certificate
for _, inter := range attObj.AttestationStatement.X509CertChain[1:] {
cert, err := x509.ParseCertificate(inter)
if err != nil {
return errors.Wrap(err, "parsing intermediate")
}
dst.Intermediates.AddCert(cert)
dst.Roots = aaroots
}

return nil
}

func extractNonceFromCert(c *x509.Certificate) ([]byte, error) {
var oidValue []byte
for _, ext := range c.Extensions {
Expand Down Expand Up @@ -216,11 +209,3 @@ func ComputeNonce(authData, clientDataHash []byte) (res [sha256.Size]byte, err e
func ComputeKeyHash(key *ecdsa.PublicKey) [sha256.Size]byte {
return sha256.Sum256(ellipticPointToX962Uncompressed(key))
}

type Environment = []byte

var (
NonceOID = asn1.ObjectIdentifier{1, 2, 840, 113635, 100, 8, 2}
AAGUIDProd = Environment("appattest\x00\x00\x00\x00\x00\x00\x00")
AAGUIDDev = Environment("appattestdevelop")
)
Loading