Add unit tests for apple and step attestations

This commit is contained in:
Mariano Cano 2022-09-15 18:19:52 -07:00
parent 42102d88d5
commit 6b73a020e3
3 changed files with 419 additions and 16 deletions

View file

@ -350,7 +350,7 @@ func deviceAttest01Validate(ctx context.Context, ch *Challenge, db DB, jwk *jose
switch att.Format { switch att.Format {
case "apple": case "apple":
data, err := doAppleAttestationFormat(ctx, ch, db, &att) data, err := doAppleAttestationFormat(ctx, prov, ch, &att)
if err != nil { if err != nil {
var acmeError *Error var acmeError *Error
if errors.As(err, &acmeError) { if errors.As(err, &acmeError) {
@ -378,7 +378,7 @@ func deviceAttest01Validate(ctx context.Context, ch *Challenge, db DB, jwk *jose
return storeError(ctx, db, ch, true, NewError(ErrorBadAttestationStatementType, "permanent identifier does not match")) return storeError(ctx, db, ch, true, NewError(ErrorBadAttestationStatementType, "permanent identifier does not match"))
} }
case "step": case "step":
data, err := doStepAttestationFormat(ctx, ch, jwk, &att) data, err := doStepAttestationFormat(ctx, prov, ch, jwk, &att)
if err != nil { if err != nil {
var acmeError *Error var acmeError *Error
if errors.As(err, &acmeError) { if errors.As(err, &acmeError) {
@ -444,13 +444,17 @@ type appleAttestationData struct {
Certificate *x509.Certificate Certificate *x509.Certificate
} }
func doAppleAttestationFormat(ctx context.Context, ch *Challenge, db DB, att *AttestationObject) (*appleAttestationData, error) { func doAppleAttestationFormat(ctx context.Context, prov Provisioner, ch *Challenge, att *AttestationObject) (*appleAttestationData, error) {
root, err := pemutil.ParseCertificate([]byte(appleEnterpriseAttestationRootCA)) // Use configured or default attestation roots if none is configured.
if err != nil { roots, ok := prov.GetAttestationRoots()
return nil, WrapErrorISE(err, "error parsing apple enterprise ca") if !ok {
root, err := pemutil.ParseCertificate([]byte(appleEnterpriseAttestationRootCA))
if err != nil {
return nil, WrapErrorISE(err, "error parsing apple enterprise ca")
}
roots = x509.NewCertPool()
roots.AddCert(root)
} }
roots := x509.NewCertPool()
roots.AddCert(root)
x5c, ok := att.AttStatement["x5c"].([]interface{}) x5c, ok := att.AttStatement["x5c"].([]interface{})
if !ok { if !ok {
@ -541,13 +545,17 @@ type stepAttestationData struct {
SerialNumber string SerialNumber string
} }
func doStepAttestationFormat(ctx context.Context, ch *Challenge, jwk *jose.JSONWebKey, att *AttestationObject) (*stepAttestationData, error) { func doStepAttestationFormat(ctx context.Context, prov Provisioner, ch *Challenge, jwk *jose.JSONWebKey, att *AttestationObject) (*stepAttestationData, error) {
root, err := pemutil.ParseCertificate([]byte(yubicoPIVRootCA)) // Use configured or default attestation roots if none is configured.
if err != nil { roots, ok := prov.GetAttestationRoots()
return nil, WrapErrorISE(err, "error parsing root ca") if !ok {
root, err := pemutil.ParseCertificate([]byte(yubicoPIVRootCA))
if err != nil {
return nil, WrapErrorISE(err, "error parsing root ca")
}
roots = x509.NewCertPool()
roots.AddCert(root)
} }
roots := x509.NewCertPool()
roots.AddCert(root)
// Extract x5c and verify certificate // Extract x5c and verify certificate
x5c, ok := att.AttStatement["x5c"].([]interface{}) x5c, ok := att.AttStatement["x5c"].([]interface{})

View file

@ -4,6 +4,8 @@ import (
"bytes" "bytes"
"context" "context"
"crypto" "crypto"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand" "crypto/rand"
"crypto/rsa" "crypto/rsa"
"crypto/sha256" "crypto/sha256"
@ -13,6 +15,7 @@ import (
"encoding/asn1" "encoding/asn1"
"encoding/base64" "encoding/base64"
"encoding/hex" "encoding/hex"
"encoding/pem"
"errors" "errors"
"fmt" "fmt"
"io" "io"
@ -20,13 +23,18 @@ import (
"net" "net"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"reflect"
"strings" "strings"
"testing" "testing"
"time" "time"
"go.step.sm/crypto/jose" "github.com/fxamacker/cbor/v2"
"github.com/smallstep/assert" "github.com/smallstep/assert"
"github.com/smallstep/certificates/authority/config"
"github.com/smallstep/certificates/authority/provisioner"
"go.step.sm/crypto/jose"
"go.step.sm/crypto/keyutil"
"go.step.sm/crypto/minica"
) )
type mockClient struct { type mockClient struct {
@ -2400,3 +2408,381 @@ func Test_http01ChallengeHost(t *testing.T) {
}) })
} }
} }
func Test_doAppleAttestationFormat(t *testing.T) {
makeProvisioner := func(roots []byte) Provisioner {
prov := &provisioner.ACME{
Type: "ACME",
Name: "acme",
Challenges: []provisioner.ACMEChallenge{provisioner.DEVICE_ATTEST_01},
AttestationRoots: roots,
}
if err := prov.Init(provisioner.Config{
Claims: config.GlobalProvisionerClaims,
}); err != nil {
t.Fatal(err)
}
return prov
}
ctx := context.Background()
ca, err := minica.New()
if err != nil {
t.Fatal(err)
}
caRoot := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: ca.Root.Raw})
signer, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
t.Fatal(err)
}
leaf, err := ca.Sign(&x509.Certificate{
Subject: pkix.Name{CommonName: "attestation cert"},
PublicKey: signer.Public(),
ExtraExtensions: []pkix.Extension{
{Id: oidAppleSerialNumber, Value: []byte("serial-number")},
{Id: oidAppleUniqueDeviceIdentifier, Value: []byte("udid")},
{Id: oidAppleSecureEnclaveProcessorOSVersion, Value: []byte("16.0")},
{Id: oidAppleNonce, Value: []byte("nonce")},
},
})
if err != nil {
t.Fatal(err)
}
type args struct {
ctx context.Context
prov Provisioner
ch *Challenge
att *AttestationObject
}
tests := []struct {
name string
args args
want *appleAttestationData
wantErr bool
}{
{"ok", args{ctx, makeProvisioner(caRoot), &Challenge{}, &AttestationObject{
Format: "apple",
AttStatement: map[string]interface{}{
"x5c": []interface{}{leaf.Raw, ca.Intermediate.Raw},
},
}}, &appleAttestationData{
Nonce: []byte("nonce"),
SerialNumber: "serial-number",
UDID: "udid",
SEPVersion: "16.0",
Certificate: leaf,
}, false},
{"fail apple issuer", args{ctx, makeProvisioner(nil), &Challenge{}, &AttestationObject{
Format: "apple",
AttStatement: map[string]interface{}{
"x5c": []interface{}{leaf.Raw, ca.Intermediate.Raw},
},
}}, nil, true},
{"fail missing x5c", args{ctx, makeProvisioner(caRoot), &Challenge{}, &AttestationObject{
Format: "apple",
AttStatement: map[string]interface{}{
"foo": "bar",
},
}}, nil, true},
{"fail empty issuer", args{ctx, makeProvisioner(caRoot), &Challenge{}, &AttestationObject{
Format: "apple",
AttStatement: map[string]interface{}{
"x5c": []interface{}{},
},
}}, nil, true},
{"fail leaf type", args{ctx, makeProvisioner(caRoot), &Challenge{}, &AttestationObject{
Format: "apple",
AttStatement: map[string]interface{}{
"x5c": []interface{}{"leaf", ca.Intermediate.Raw},
},
}}, nil, true},
{"fail leaf parse", args{ctx, makeProvisioner(caRoot), &Challenge{}, &AttestationObject{
Format: "apple",
AttStatement: map[string]interface{}{
"x5c": []interface{}{leaf.Raw[:100], ca.Intermediate.Raw},
},
}}, nil, true},
{"fail intermediate type", args{ctx, makeProvisioner(caRoot), &Challenge{}, &AttestationObject{
Format: "apple",
AttStatement: map[string]interface{}{
"x5c": []interface{}{leaf.Raw, "intermediate"},
},
}}, nil, true},
{"fail intermediate parse", args{ctx, makeProvisioner(caRoot), &Challenge{}, &AttestationObject{
Format: "apple",
AttStatement: map[string]interface{}{
"x5c": []interface{}{leaf.Raw, ca.Intermediate.Raw[:100]},
},
}}, nil, true},
{"fail verify", args{ctx, makeProvisioner(caRoot), &Challenge{}, &AttestationObject{
Format: "apple",
AttStatement: map[string]interface{}{
"x5c": []interface{}{leaf.Raw},
},
}}, nil, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := doAppleAttestationFormat(tt.args.ctx, tt.args.prov, tt.args.ch, tt.args.att)
if (err != nil) != tt.wantErr {
t.Errorf("doAppleAttestationFormat() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("doAppleAttestationFormat() = %v, want %v", got, tt.want)
}
})
}
}
func Test_doStepAttestationFormat(t *testing.T) {
ctx := context.Background()
ca, err := minica.New()
if err != nil {
t.Fatal(err)
}
caRoot := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: ca.Root.Raw})
makeProvisioner := func(roots []byte) Provisioner {
prov := &provisioner.ACME{
Type: "ACME",
Name: "acme",
Challenges: []provisioner.ACMEChallenge{provisioner.DEVICE_ATTEST_01},
AttestationRoots: roots,
}
if err := prov.Init(provisioner.Config{
Claims: config.GlobalProvisionerClaims,
}); err != nil {
t.Fatal(err)
}
return prov
}
makeLeaf := func(signer crypto.Signer, serialNumber []byte) *x509.Certificate {
leaf, err := ca.Sign(&x509.Certificate{
Subject: pkix.Name{CommonName: "attestation cert"},
PublicKey: signer.Public(),
ExtraExtensions: []pkix.Extension{
{Id: oidYubicoSerialNumber, Value: serialNumber},
},
})
if err != nil {
t.Fatal(err)
}
return leaf
}
mustSigner := func(kty, crv string, size int) crypto.Signer {
s, err := keyutil.GenerateSigner(kty, crv, size)
if err != nil {
t.Fatal(err)
}
return s
}
signer, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
t.Fatal(err)
}
serialNumber, err := asn1.Marshal(1234)
if err != nil {
t.Fatal(err)
}
leaf := makeLeaf(signer, serialNumber)
jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0)
if err != nil {
t.Fatal(err)
}
keyAuth, err := KeyAuthorization("token", jwk)
if err != nil {
t.Fatal(err)
}
keyAuthSum := sha256.Sum256([]byte(keyAuth))
sig, err := signer.Sign(rand.Reader, keyAuthSum[:], crypto.SHA256)
if err != nil {
t.Fatal(err)
}
cborSig, err := cbor.Marshal(sig)
if err != nil {
t.Fatal(err)
}
otherSigner, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
t.Fatal(err)
}
otherSig, err := otherSigner.Sign(rand.Reader, keyAuthSum[:], crypto.SHA256)
if err != nil {
t.Fatal(err)
}
otherCBORSig, err := cbor.Marshal(otherSig)
if err != nil {
t.Fatal(err)
}
type args struct {
ctx context.Context
prov Provisioner
ch *Challenge
jwk *jose.JSONWebKey
att *AttestationObject
}
tests := []struct {
name string
args args
want *stepAttestationData
wantErr bool
}{
{"ok", args{ctx, makeProvisioner(caRoot), &Challenge{Token: "token"}, jwk, &AttestationObject{
Format: "step",
AttStatement: map[string]interface{}{
"x5c": []interface{}{leaf.Raw, ca.Intermediate.Raw},
"alg": -7,
"sig": cborSig,
},
}}, &stepAttestationData{
SerialNumber: "1234",
Certificate: leaf,
}, false},
{"fail yubico issuer", args{ctx, makeProvisioner(nil), &Challenge{Token: "token"}, jwk, &AttestationObject{
Format: "step",
AttStatement: map[string]interface{}{
"x5c": []interface{}{leaf.Raw, ca.Intermediate.Raw},
"alg": -7,
"sig": cborSig,
},
}}, nil, true},
{"fail x5c type", args{ctx, makeProvisioner(caRoot), &Challenge{Token: "token"}, jwk, &AttestationObject{
Format: "step",
AttStatement: map[string]interface{}{
"x5c": [][]byte{leaf.Raw, ca.Intermediate.Raw},
"alg": -7,
"sig": cborSig,
},
}}, nil, true},
{"fail x5c empty", args{ctx, makeProvisioner(caRoot), &Challenge{Token: "token"}, jwk, &AttestationObject{
Format: "step",
AttStatement: map[string]interface{}{
"x5c": []interface{}{},
"alg": -7,
"sig": cborSig,
},
}}, nil, true},
{"fail leaf type", args{ctx, makeProvisioner(caRoot), &Challenge{Token: "token"}, jwk, &AttestationObject{
Format: "step",
AttStatement: map[string]interface{}{
"x5c": []interface{}{"leaf", ca.Intermediate.Raw},
"alg": -7,
"sig": cborSig,
},
}}, nil, true},
{"fail leaf parse", args{ctx, makeProvisioner(caRoot), &Challenge{Token: "token"}, jwk, &AttestationObject{
Format: "step",
AttStatement: map[string]interface{}{
"x5c": []interface{}{leaf.Raw[:100], ca.Intermediate.Raw},
"alg": -7,
"sig": cborSig,
},
}}, nil, true},
{"fail intermediate type", args{ctx, makeProvisioner(caRoot), &Challenge{Token: "token"}, jwk, &AttestationObject{
Format: "step",
AttStatement: map[string]interface{}{
"x5c": []interface{}{leaf.Raw, "intermediate"},
"alg": -7,
"sig": cborSig,
},
}}, nil, true},
{"fail intermediate parse", args{ctx, makeProvisioner(caRoot), &Challenge{Token: "token"}, jwk, &AttestationObject{
Format: "step",
AttStatement: map[string]interface{}{
"x5c": []interface{}{leaf.Raw, ca.Intermediate.Raw[:100]},
"alg": -7,
"sig": cborSig,
},
}}, nil, true},
{"fail verify", args{ctx, makeProvisioner(caRoot), &Challenge{Token: "token"}, jwk, &AttestationObject{
Format: "step",
AttStatement: map[string]interface{}{
"x5c": []interface{}{leaf.Raw},
"alg": -7,
"sig": cborSig,
},
}}, nil, true},
{"fail sig type", args{ctx, makeProvisioner(caRoot), &Challenge{Token: "token"}, jwk, &AttestationObject{
Format: "step",
AttStatement: map[string]interface{}{
"x5c": []interface{}{leaf.Raw, ca.Intermediate.Raw},
"alg": -7,
"sig": string(cborSig),
},
}}, nil, true},
{"fail sig unmarshal", args{ctx, makeProvisioner(caRoot), &Challenge{Token: "token"}, jwk, &AttestationObject{
Format: "step",
AttStatement: map[string]interface{}{
"x5c": []interface{}{leaf.Raw, ca.Intermediate.Raw},
"alg": -7,
"sig": []byte("bad-sig"),
},
}}, nil, true},
{"fail keyAuthorization", args{ctx, makeProvisioner(caRoot), &Challenge{Token: "token"}, &jose.JSONWebKey{Key: []byte("not an asymmetric key")}, &AttestationObject{
Format: "step",
AttStatement: map[string]interface{}{
"x5c": []interface{}{leaf.Raw, ca.Intermediate.Raw},
"alg": -7,
"sig": cborSig,
},
}}, nil, true},
{"fail sig verify P-256", args{ctx, makeProvisioner(caRoot), &Challenge{Token: "token"}, jwk, &AttestationObject{
Format: "step",
AttStatement: map[string]interface{}{
"x5c": []interface{}{leaf.Raw, ca.Intermediate.Raw},
"alg": -7,
"sig": otherCBORSig,
},
}}, nil, true},
{"fail sig verify P-384", args{ctx, makeProvisioner(caRoot), &Challenge{Token: "token"}, jwk, &AttestationObject{
Format: "step",
AttStatement: map[string]interface{}{
"x5c": []interface{}{makeLeaf(mustSigner("EC", "P-384", 0), serialNumber).Raw, ca.Intermediate.Raw},
"alg": -7,
"sig": cborSig,
},
}}, nil, true},
{"fail sig verify RSA", args{ctx, makeProvisioner(caRoot), &Challenge{Token: "token"}, jwk, &AttestationObject{
Format: "step",
AttStatement: map[string]interface{}{
"x5c": []interface{}{makeLeaf(mustSigner("RSA", "", 2048), serialNumber).Raw, ca.Intermediate.Raw},
"alg": -7,
"sig": cborSig,
},
}}, nil, true},
{"fail sig verify Ed25519", args{ctx, makeProvisioner(caRoot), &Challenge{Token: "token"}, jwk, &AttestationObject{
Format: "step",
AttStatement: map[string]interface{}{
"x5c": []interface{}{makeLeaf(mustSigner("OKP", "Ed25519", 0), serialNumber).Raw, ca.Intermediate.Raw},
"alg": -7,
"sig": cborSig,
},
}}, nil, true},
{"fail unmarshal serial number", args{ctx, makeProvisioner(caRoot), &Challenge{Token: "token"}, jwk, &AttestationObject{
Format: "step",
AttStatement: map[string]interface{}{
"x5c": []interface{}{makeLeaf(signer, []byte("bad-serial")).Raw, ca.Intermediate.Raw},
"alg": -7,
"sig": cborSig,
},
}}, nil, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := doStepAttestationFormat(tt.args.ctx, tt.args.prov, tt.args.ch, tt.args.jwk, tt.args.att)
if (err != nil) != tt.wantErr {
t.Errorf("doStepAttestationFormat() error = %#v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("doStepAttestationFormat() = %v, want %v", got, tt.want)
}
})
}
}

View file

@ -73,6 +73,7 @@ type Provisioner interface {
AuthorizeRevoke(ctx context.Context, token string) error AuthorizeRevoke(ctx context.Context, token string) error
IsChallengeEnabled(ctx context.Context, challenge provisioner.ACMEChallenge) bool IsChallengeEnabled(ctx context.Context, challenge provisioner.ACMEChallenge) bool
IsAttestationFormatEnabled(ctx context.Context, format provisioner.ACMEAttestationFormat) bool IsAttestationFormatEnabled(ctx context.Context, format provisioner.ACMEAttestationFormat) bool
GetAttestationRoots() (*x509.CertPool, bool)
GetID() string GetID() string
GetName() string GetName() string
DefaultTLSCertDuration() time.Duration DefaultTLSCertDuration() time.Duration
@ -113,6 +114,7 @@ type MockProvisioner struct {
MauthorizeRevoke func(ctx context.Context, token string) error MauthorizeRevoke func(ctx context.Context, token string) error
MisChallengeEnabled func(ctx context.Context, challenge provisioner.ACMEChallenge) bool MisChallengeEnabled func(ctx context.Context, challenge provisioner.ACMEChallenge) bool
MisAttFormatEnabled func(ctx context.Context, format provisioner.ACMEAttestationFormat) bool MisAttFormatEnabled func(ctx context.Context, format provisioner.ACMEAttestationFormat) bool
MgetAttestationRoots func() (*x509.CertPool, bool)
MdefaultTLSCertDuration func() time.Duration MdefaultTLSCertDuration func() time.Duration
MgetOptions func() *provisioner.Options MgetOptions func() *provisioner.Options
} }
@ -165,6 +167,13 @@ func (m *MockProvisioner) IsAttestationFormatEnabled(ctx context.Context, format
return m.Merr == nil return m.Merr == nil
} }
func (m *MockProvisioner) GetAttestationRoots() (*x509.CertPool, bool) {
if m.MgetAttestationRoots != nil {
return m.MgetAttestationRoots()
}
return m.Mret1.(*x509.CertPool), m.Mret1 != nil
}
// DefaultTLSCertDuration mock // DefaultTLSCertDuration mock
func (m *MockProvisioner) DefaultTLSCertDuration() time.Duration { func (m *MockProvisioner) DefaultTLSCertDuration() time.Duration {
if m.MdefaultTLSCertDuration != nil { if m.MdefaultTLSCertDuration != nil {