diff --git a/acme/api/account_test.go b/acme/api/account_test.go index a67e1a62..d74b5433 100644 --- a/acme/api/account_test.go +++ b/acme/api/account_test.go @@ -3,6 +3,7 @@ package api import ( "bytes" "context" + "crypto/x509" "encoding/json" "fmt" "io" @@ -49,6 +50,10 @@ func (*fakeProvisioner) IsAttestationFormatEnabled(ctx context.Context, format p return true } +func (*fakeProvisioner) GetAttestationRoots() (*x509.CertPool, bool) { + return nil, false +} + func (*fakeProvisioner) AuthorizeRevoke(ctx context.Context, token string) error { return nil } func (*fakeProvisioner) GetID() string { return "" } func (*fakeProvisioner) GetName() string { return "" } diff --git a/acme/challenge.go b/acme/challenge.go index cc860f95..47c46490 100644 --- a/acme/challenge.go +++ b/acme/challenge.go @@ -350,7 +350,7 @@ func deviceAttest01Validate(ctx context.Context, ch *Challenge, db DB, jwk *jose switch att.Format { case "apple": - data, err := doAppleAttestationFormat(ctx, ch, db, &att) + data, err := doAppleAttestationFormat(ctx, prov, ch, &att) if err != nil { var acmeError *Error 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")) } case "step": - data, err := doStepAttestationFormat(ctx, ch, jwk, &att) + data, err := doStepAttestationFormat(ctx, prov, ch, jwk, &att) if err != nil { var acmeError *Error if errors.As(err, &acmeError) { @@ -444,13 +444,17 @@ type appleAttestationData struct { Certificate *x509.Certificate } -func doAppleAttestationFormat(ctx context.Context, ch *Challenge, db DB, att *AttestationObject) (*appleAttestationData, error) { - root, err := pemutil.ParseCertificate([]byte(appleEnterpriseAttestationRootCA)) - if err != nil { - return nil, WrapErrorISE(err, "error parsing apple enterprise ca") +func doAppleAttestationFormat(ctx context.Context, prov Provisioner, ch *Challenge, att *AttestationObject) (*appleAttestationData, error) { + // Use configured or default attestation roots if none is configured. + roots, ok := prov.GetAttestationRoots() + 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{}) if !ok { @@ -541,13 +545,17 @@ type stepAttestationData struct { SerialNumber string } -func doStepAttestationFormat(ctx context.Context, ch *Challenge, jwk *jose.JSONWebKey, att *AttestationObject) (*stepAttestationData, error) { - root, err := pemutil.ParseCertificate([]byte(yubicoPIVRootCA)) - if err != nil { - return nil, WrapErrorISE(err, "error parsing root ca") +func doStepAttestationFormat(ctx context.Context, prov Provisioner, ch *Challenge, jwk *jose.JSONWebKey, att *AttestationObject) (*stepAttestationData, error) { + // Use configured or default attestation roots if none is configured. + roots, ok := prov.GetAttestationRoots() + 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 x5c, ok := att.AttStatement["x5c"].([]interface{}) diff --git a/acme/challenge_test.go b/acme/challenge_test.go index 3f7e214d..90aafa97 100644 --- a/acme/challenge_test.go +++ b/acme/challenge_test.go @@ -4,6 +4,8 @@ import ( "bytes" "context" "crypto" + "crypto/ecdsa" + "crypto/elliptic" "crypto/rand" "crypto/rsa" "crypto/sha256" @@ -13,6 +15,7 @@ import ( "encoding/asn1" "encoding/base64" "encoding/hex" + "encoding/pem" "errors" "fmt" "io" @@ -20,13 +23,18 @@ import ( "net" "net/http" "net/http/httptest" + "reflect" "strings" "testing" "time" - "go.step.sm/crypto/jose" - + "github.com/fxamacker/cbor/v2" "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 { @@ -37,8 +45,25 @@ type mockClient struct { func (m *mockClient) Get(url string) (*http.Response, error) { return m.get(url) } func (m *mockClient) LookupTxt(name string) ([]string, error) { return m.lookupTxt(name) } -func (m *mockClient) TLSDial(network, addr string, config *tls.Config) (*tls.Conn, error) { - return m.tlsDial(network, addr, config) +func (m *mockClient) TLSDial(network, addr string, tlsConfig *tls.Config) (*tls.Conn, error) { + return m.tlsDial(network, addr, tlsConfig) +} + +func mustAttestationProvisioner(t *testing.T, roots []byte) Provisioner { + t.Helper() + + 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 } func Test_storeError(t *testing.T) { @@ -2400,3 +2425,352 @@ func Test_http01ChallengeHost(t *testing.T) { }) } } + +func Test_doAppleAttestationFormat(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}) + 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, mustAttestationProvisioner(t, 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, mustAttestationProvisioner(t, nil), &Challenge{}, &AttestationObject{ + Format: "apple", + AttStatement: map[string]interface{}{ + "x5c": []interface{}{leaf.Raw, ca.Intermediate.Raw}, + }, + }}, nil, true}, + {"fail missing x5c", args{ctx, mustAttestationProvisioner(t, caRoot), &Challenge{}, &AttestationObject{ + Format: "apple", + AttStatement: map[string]interface{}{ + "foo": "bar", + }, + }}, nil, true}, + {"fail empty issuer", args{ctx, mustAttestationProvisioner(t, caRoot), &Challenge{}, &AttestationObject{ + Format: "apple", + AttStatement: map[string]interface{}{ + "x5c": []interface{}{}, + }, + }}, nil, true}, + {"fail leaf type", args{ctx, mustAttestationProvisioner(t, caRoot), &Challenge{}, &AttestationObject{ + Format: "apple", + AttStatement: map[string]interface{}{ + "x5c": []interface{}{"leaf", ca.Intermediate.Raw}, + }, + }}, nil, true}, + {"fail leaf parse", args{ctx, mustAttestationProvisioner(t, caRoot), &Challenge{}, &AttestationObject{ + Format: "apple", + AttStatement: map[string]interface{}{ + "x5c": []interface{}{leaf.Raw[:100], ca.Intermediate.Raw}, + }, + }}, nil, true}, + {"fail intermediate type", args{ctx, mustAttestationProvisioner(t, caRoot), &Challenge{}, &AttestationObject{ + Format: "apple", + AttStatement: map[string]interface{}{ + "x5c": []interface{}{leaf.Raw, "intermediate"}, + }, + }}, nil, true}, + {"fail intermediate parse", args{ctx, mustAttestationProvisioner(t, caRoot), &Challenge{}, &AttestationObject{ + Format: "apple", + AttStatement: map[string]interface{}{ + "x5c": []interface{}{leaf.Raw, ca.Intermediate.Raw[:100]}, + }, + }}, nil, true}, + {"fail verify", args{ctx, mustAttestationProvisioner(t, 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}) + + 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, mustAttestationProvisioner(t, 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, mustAttestationProvisioner(t, 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, mustAttestationProvisioner(t, 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, mustAttestationProvisioner(t, 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, mustAttestationProvisioner(t, 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, mustAttestationProvisioner(t, 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, mustAttestationProvisioner(t, 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, mustAttestationProvisioner(t, 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, mustAttestationProvisioner(t, 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, mustAttestationProvisioner(t, 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, mustAttestationProvisioner(t, 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, mustAttestationProvisioner(t, 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, mustAttestationProvisioner(t, 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, mustAttestationProvisioner(t, 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, mustAttestationProvisioner(t, 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, mustAttestationProvisioner(t, 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, mustAttestationProvisioner(t, 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) + } + }) + } +} diff --git a/acme/common.go b/acme/common.go index b7260386..91cf772b 100644 --- a/acme/common.go +++ b/acme/common.go @@ -73,6 +73,7 @@ type Provisioner interface { AuthorizeRevoke(ctx context.Context, token string) error IsChallengeEnabled(ctx context.Context, challenge provisioner.ACMEChallenge) bool IsAttestationFormatEnabled(ctx context.Context, format provisioner.ACMEAttestationFormat) bool + GetAttestationRoots() (*x509.CertPool, bool) GetID() string GetName() string DefaultTLSCertDuration() time.Duration @@ -113,6 +114,7 @@ type MockProvisioner struct { MauthorizeRevoke func(ctx context.Context, token string) error MisChallengeEnabled func(ctx context.Context, challenge provisioner.ACMEChallenge) bool MisAttFormatEnabled func(ctx context.Context, format provisioner.ACMEAttestationFormat) bool + MgetAttestationRoots func() (*x509.CertPool, bool) MdefaultTLSCertDuration func() time.Duration MgetOptions func() *provisioner.Options } @@ -165,6 +167,13 @@ func (m *MockProvisioner) IsAttestationFormatEnabled(ctx context.Context, format 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 func (m *MockProvisioner) DefaultTLSCertDuration() time.Duration { if m.MdefaultTLSCertDuration != nil { diff --git a/authority/provisioner/acme.go b/authority/provisioner/acme.go index 33fa351c..5955ac6a 100644 --- a/authority/provisioner/acme.go +++ b/authority/provisioner/acme.go @@ -3,6 +3,7 @@ package provisioner import ( "context" "crypto/x509" + "encoding/pem" "fmt" "net" "strings" @@ -95,10 +96,14 @@ type ACME struct { // provisioner. If this value is not set the default apple, step and tpm // will be used. AttestationFormats []ACMEAttestationFormat `json:"attestationFormats,omitempty"` - Claims *Claims `json:"claims,omitempty"` - Options *Options `json:"options,omitempty"` - - ctl *Controller + // AttestationRoots contains a bundle of root certificates in PEM format + // that will be used to verify the attestation certificates. If provided, + // this bundle will be used even for well-known CAs like Apple and Yubico. + AttestationRoots []byte `json:"attestationRoots,omitempty"` + Claims *Claims `json:"claims,omitempty"` + Options *Options `json:"options,omitempty"` + attestationRootPool *x509.CertPool + ctl *Controller } // GetID returns the provisioner unique identifier. @@ -166,6 +171,29 @@ func (p *ACME) Init(config Config) (err error) { } } + // Parse attestation roots. + // The pool will be nil if the there are not roots. + if rest := p.AttestationRoots; len(rest) > 0 { + var block *pem.Block + var hasCert bool + p.attestationRootPool = x509.NewCertPool() + for rest != nil { + block, rest = pem.Decode(rest) + if block == nil { + break + } + cert, err := x509.ParseCertificate(block.Bytes) + if err != nil { + return errors.New("error parsing attestationRoots: malformed certificate") + } + p.attestationRootPool.AddCert(cert) + hasCert = true + } + if !hasCert { + return errors.New("error parsing attestationRoots: no certificates found") + } + } + p.ctl, err = NewController(p, p.Claims, config, p.Options) return } @@ -282,3 +310,12 @@ func (p *ACME) IsAttestationFormatEnabled(ctx context.Context, format ACMEAttest } return false } + +// GetAttestationRoots returns certificate pool with the configured attestation +// roots and reports if the pool contains at least one certificate. +// +// TODO(hs): we may not want to expose the root pool like this; call into an +// interface function instead to authorize? +func (p *ACME) GetAttestationRoots() (*x509.CertPool, bool) { + return p.attestationRootPool, p.attestationRootPool != nil +} diff --git a/authority/provisioner/acme_test.go b/authority/provisioner/acme_test.go index 6152a8c9..bfd85303 100644 --- a/authority/provisioner/acme_test.go +++ b/authority/provisioner/acme_test.go @@ -1,11 +1,13 @@ package provisioner import ( + "bytes" "context" "crypto/x509" "errors" "fmt" "net/http" + "os" "testing" "time" @@ -77,6 +79,15 @@ func TestACME_Getters(t *testing.T) { } func TestACME_Init(t *testing.T) { + appleCA, err := os.ReadFile("testdata/certs/apple-att-ca.crt") + if err != nil { + t.Fatal(err) + } + yubicoCA, err := os.ReadFile("testdata/certs/yubico-piv-ca.crt") + if err != nil { + t.Fatal(err) + } + type ProvisionerValidateTest struct { p *ACME err error @@ -120,6 +131,18 @@ func TestACME_Init(t *testing.T) { err: errors.New("acme attestation format \"zar\" is not supported"), } }, + "fail-parse-attestation-roots": func(t *testing.T) ProvisionerValidateTest { + return ProvisionerValidateTest{ + p: &ACME{Name: "foo", Type: "bar", AttestationRoots: []byte("-----BEGIN CERTIFICATE-----\nZm9v\n-----END CERTIFICATE-----")}, + err: errors.New("error parsing attestationRoots: malformed certificate"), + } + }, + "fail-empty-attestation-roots": func(t *testing.T) ProvisionerValidateTest { + return ProvisionerValidateTest{ + p: &ACME{Name: "foo", Type: "bar", AttestationRoots: []byte("\n")}, + err: errors.New("error parsing attestationRoots: no certificates found"), + } + }, "ok": func(t *testing.T) ProvisionerValidateTest { return ProvisionerValidateTest{ p: &ACME{Name: "foo", Type: "bar"}, @@ -132,6 +155,7 @@ func TestACME_Init(t *testing.T) { Type: "bar", Challenges: []ACMEChallenge{DNS_01, DEVICE_ATTEST_01}, AttestationFormats: []ACMEAttestationFormat{APPLE, STEP}, + AttestationRoots: bytes.Join([][]byte{appleCA, yubicoCA}, []byte("\n")), }, } }, @@ -144,6 +168,7 @@ func TestACME_Init(t *testing.T) { for name, get := range tests { t.Run(name, func(t *testing.T) { tc := get(t) + t.Log(string(tc.p.AttestationRoots)) err := tc.p.Init(config) if err != nil { if assert.NotNil(t, tc.err) { @@ -346,3 +371,58 @@ func TestACME_IsAttestationFormatEnabled(t *testing.T) { }) } } + +func TestACME_GetAttestationRoots(t *testing.T) { + appleCA, err := os.ReadFile("testdata/certs/apple-att-ca.crt") + if err != nil { + t.Fatal(err) + } + yubicoCA, err := os.ReadFile("testdata/certs/yubico-piv-ca.crt") + if err != nil { + t.Fatal(err) + } + + pool := x509.NewCertPool() + pool.AppendCertsFromPEM(appleCA) + pool.AppendCertsFromPEM(yubicoCA) + + type fields struct { + Type string + Name string + AttestationRoots []byte + } + tests := []struct { + name string + fields fields + want *x509.CertPool + want1 bool + }{ + {"ok", fields{"ACME", "acme", bytes.Join([][]byte{appleCA, yubicoCA}, []byte("\n"))}, pool, true}, + {"nil", fields{"ACME", "acme", nil}, nil, false}, + {"empty", fields{"ACME", "acme", []byte{}}, nil, false}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + p := &ACME{ + Type: tt.fields.Type, + Name: tt.fields.Name, + AttestationRoots: tt.fields.AttestationRoots, + } + if err := p.Init(Config{ + Claims: globalProvisionerClaims, + Audiences: testAudiences, + }); err != nil { + t.Fatal(err) + } + got, got1 := p.GetAttestationRoots() + if tt.want == nil && got != nil { + t.Errorf("ACME.GetAttestationRoots() got = %v, want %v", got, tt.want) + } else if !tt.want.Equal(got) { + t.Errorf("ACME.GetAttestationRoots() got = %v, want %v", got, tt.want) + } + if got1 != tt.want1 { + t.Errorf("ACME.GetAttestationRoots() got1 = %v, want %v", got1, tt.want1) + } + }) + } +} diff --git a/authority/provisioner/testdata/certs/apple-att-ca.crt b/authority/provisioner/testdata/certs/apple-att-ca.crt new file mode 100644 index 00000000..2e5e3b3b --- /dev/null +++ b/authority/provisioner/testdata/certs/apple-att-ca.crt @@ -0,0 +1,14 @@ +-----BEGIN CERTIFICATE----- +MIICJDCCAamgAwIBAgIUQsDCuyxyfFxeq/bxpm8frF15hzcwCgYIKoZIzj0EAwMw +UTEtMCsGA1UEAwwkQXBwbGUgRW50ZXJwcmlzZSBBdHRlc3RhdGlvbiBSb290IENB +MRMwEQYDVQQKDApBcHBsZSBJbmMuMQswCQYDVQQGEwJVUzAeFw0yMjAyMTYxOTAx +MjRaFw00NzAyMjAwMDAwMDBaMFExLTArBgNVBAMMJEFwcGxlIEVudGVycHJpc2Ug +QXR0ZXN0YXRpb24gUm9vdCBDQTETMBEGA1UECgwKQXBwbGUgSW5jLjELMAkGA1UE +BhMCVVMwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAT6Jigq+Ps9Q4CoT8t8q+UnOe2p +oT9nRaUfGhBTbgvqSGXPjVkbYlIWYO+1zPk2Sz9hQ5ozzmLrPmTBgEWRcHjA2/y7 +7GEicps9wn2tj+G89l3INNDKETdxSPPIZpPj8VmjQjBAMA8GA1UdEwEB/wQFMAMB +Af8wHQYDVR0OBBYEFPNqTQGd8muBpV5du+UIbVbi+d66MA4GA1UdDwEB/wQEAwIB +BjAKBggqhkjOPQQDAwNpADBmAjEA1xpWmTLSpr1VH4f8Ypk8f3jMUKYz4QPG8mL5 +8m9sX/b2+eXpTv2pH4RZgJjucnbcAjEA4ZSB6S45FlPuS/u4pTnzoz632rA+xW/T +ZwFEh9bhKjJ+5VQ9/Do1os0u3LEkgN/r +-----END CERTIFICATE----- \ No newline at end of file diff --git a/authority/provisioner/testdata/certs/yubico-piv-ca.crt b/authority/provisioner/testdata/certs/yubico-piv-ca.crt new file mode 100644 index 00000000..b0a92199 --- /dev/null +++ b/authority/provisioner/testdata/certs/yubico-piv-ca.crt @@ -0,0 +1,19 @@ +-----BEGIN CERTIFICATE----- +MIIDFzCCAf+gAwIBAgIDBAZHMA0GCSqGSIb3DQEBCwUAMCsxKTAnBgNVBAMMIFl1 +YmljbyBQSVYgUm9vdCBDQSBTZXJpYWwgMjYzNzUxMCAXDTE2MDMxNDAwMDAwMFoY +DzIwNTIwNDE3MDAwMDAwWjArMSkwJwYDVQQDDCBZdWJpY28gUElWIFJvb3QgQ0Eg +U2VyaWFsIDI2Mzc1MTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMN2 +cMTNR6YCdcTFRxuPy31PabRn5m6pJ+nSE0HRWpoaM8fc8wHC+Tmb98jmNvhWNE2E +ilU85uYKfEFP9d6Q2GmytqBnxZsAa3KqZiCCx2LwQ4iYEOb1llgotVr/whEpdVOq +joU0P5e1j1y7OfwOvky/+AXIN/9Xp0VFlYRk2tQ9GcdYKDmqU+db9iKwpAzid4oH +BVLIhmD3pvkWaRA2H3DA9t7H/HNq5v3OiO1jyLZeKqZoMbPObrxqDg+9fOdShzgf +wCqgT3XVmTeiwvBSTctyi9mHQfYd2DwkaqxRnLbNVyK9zl+DzjSGp9IhVPiVtGet +X02dxhQnGS7K6BO0Qe8CAwEAAaNCMEAwHQYDVR0OBBYEFMpfyvLEojGc6SJf8ez0 +1d8Cv4O/MA8GA1UdEwQIMAYBAf8CAQEwDgYDVR0PAQH/BAQDAgEGMA0GCSqGSIb3 +DQEBCwUAA4IBAQBc7Ih8Bc1fkC+FyN1fhjWioBCMr3vjneh7MLbA6kSoyWF70N3s +XhbXvT4eRh0hvxqvMZNjPU/VlRn6gLVtoEikDLrYFXN6Hh6Wmyy1GTnspnOvMvz2 +lLKuym9KYdYLDgnj3BeAvzIhVzzYSeU77/Cupofj093OuAswW0jYvXsGTyix6B3d +bW5yWvyS9zNXaqGaUmP3U9/b6DlHdDogMLu3VLpBB9bm5bjaKWWJYgWltCVgUbFq +Fqyi4+JE014cSgR57Jcu3dZiehB6UtAPgad9L5cNvua/IWRmm+ANy3O2LH++Pyl8 +SREzU8onbBsjMg9QDiSf5oJLKvd/Ren+zGY7 +-----END CERTIFICATE----- \ No newline at end of file