From df975122a0da0de28ca0ef8501f099ca66de79c2 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Mon, 12 Sep 2022 16:30:41 -0700 Subject: [PATCH 01/13] Upgrade linkedca and add entry to changelog --- CHANGELOG.md | 2 ++ go.mod | 2 +- go.sum | 4 ++-- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 73d06543..c0071b52 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. --- ## [Unreleased] +### Added +- Added support for ACME device-attest-01 challenge. ## [0.22.1] - 2022-08-31 ### Fixed diff --git a/go.mod b/go.mod index 159c5b6b..29263a53 100644 --- a/go.mod +++ b/go.mod @@ -40,7 +40,7 @@ require ( go.mozilla.org/pkcs7 v0.0.0-20210826202110-33d05740a352 go.step.sm/cli-utils v0.7.4 go.step.sm/crypto v0.19.0 - go.step.sm/linkedca v0.18.1-0.20220909212924-c69cf68797cb + go.step.sm/linkedca v0.19.0-rc.1 golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3 golang.org/x/net v0.0.0-20220607020251-c690dde0001d golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba // indirect diff --git a/go.sum b/go.sum index eabffc39..e3937bd9 100644 --- a/go.sum +++ b/go.sum @@ -641,8 +641,8 @@ go.step.sm/cli-utils v0.7.4/go.mod h1:taSsY8haLmXoXM3ZkywIyRmVij/4Aj0fQbNTlJvv71 go.step.sm/crypto v0.9.0/go.mod h1:+CYG05Mek1YDqi5WK0ERc6cOpKly2i/a5aZmU1sfGj0= go.step.sm/crypto v0.19.0 h1:WxjUDeTDpuPZ1IR3v6c4jc6WdlQlS5IYYQBhfnG5uW0= go.step.sm/crypto v0.19.0/go.mod h1:qZ+pNU1nV+THwP7TPTNCRMRr9xrRURhETTAK7U5psfw= -go.step.sm/linkedca v0.18.1-0.20220909212924-c69cf68797cb h1:qCG7i7PAZcTDLqyFmOzBBl5tfyHI033U5jONS9DuN+8= -go.step.sm/linkedca v0.18.1-0.20220909212924-c69cf68797cb/go.mod h1:qSuYlIIhvPmA2+DSSS03E2IXhbXWTLW61Xh9zDQJ3VM= +go.step.sm/linkedca v0.19.0-rc.1 h1:8XcQvanelK1g0ijl5/itmmAIsqD2QSMHGqcWzJwwJCU= +go.step.sm/linkedca v0.19.0-rc.1/go.mod h1:G35baT7Qnh6VsRCjzSfi5xsYw0ERrU+I1aIuZswMBeA= go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= From 25f0bf31f4e1413109bbf3cdaa519378cd9f9133 Mon Sep 17 00:00:00 2001 From: max furman Date: Wed, 14 Sep 2022 13:53:30 -0400 Subject: [PATCH 02/13] Update build status svg and link to github actions --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 1efeb4a9..9544e7cd 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,7 @@ To get up and running quickly, or as an alternative to running your own `step-ca [![GitHub release](https://img.shields.io/github/release/smallstep/certificates.svg)](https://github.com/smallstep/certificates/releases/latest) [![Go Report Card](https://goreportcard.com/badge/github.com/smallstep/certificates)](https://goreportcard.com/report/github.com/smallstep/certificates) -[![Build Status](https://travis-ci.com/smallstep/certificates.svg?branch=master)](https://travis-ci.com/smallstep/certificates) +[![Build Status](https://github.com/smallstep/certificates/actions/workflows/test.yml/badge.svg)](https://github.com/smallstep/certificates) [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) [![CLA assistant](https://cla-assistant.io/readme/badge/smallstep/certificates)](https://cla-assistant.io/smallstep/certificates) From 8fc4a58242a9d6c4ade78b022d2dcf470b6d43b6 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Thu, 15 Sep 2022 13:05:39 -0700 Subject: [PATCH 03/13] Fix nil pointer exception, missing error --- authority/authorize.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/authority/authorize.go b/authority/authorize.go index 91f1b3cb..8f916e1d 100644 --- a/authority/authorize.go +++ b/authority/authorize.go @@ -434,7 +434,7 @@ func (a *Authority) AuthorizeRenewToken(ctx context.Context, ott string) (*x509. audiences := a.config.GetAudiences().Renew if !matchesAudience(claims.Audience, audiences) { - return nil, errs.InternalServerErr(err, errs.WithMessage("error validating renew token: invalid audience claim (aud)")) + return nil, errs.InternalServerErr(jose.ErrInvalidAudience, errs.WithMessage("error validating renew token: invalid audience claim (aud)")) } // validate issuer: old versions used the provisioner name, new version uses From ee7307bd41292117e6832d02882fdab473e94d16 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Thu, 15 Sep 2022 14:45:14 -0700 Subject: [PATCH 04/13] Cherry-pick acme.go from acdfdf3 --- authority/provisioner/acme.go | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/authority/provisioner/acme.go b/authority/provisioner/acme.go index 33fa351c..afcaf08a 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" @@ -98,6 +99,10 @@ type ACME struct { Claims *Claims `json:"claims,omitempty"` Options *Options `json:"options,omitempty"` + // TODO(hs): WIP configuration for ACME Device Attestation + AttestationRoots []byte `json:"attestationRoots"` + attestationRootPool *x509.CertPool + ctl *Controller } @@ -155,6 +160,7 @@ func (p *ACME) Init(config Config) (err error) { return errors.New("provisioner name cannot be empty") } +<<<<<<< HEAD for _, c := range p.Challenges { if err := c.Validate(); err != nil { return err @@ -166,6 +172,29 @@ func (p *ACME) Init(config Config) (err error) { } } +======= + // TODO(hs): WIP configuration for ACME Device Attestation + p.attestationRootPool = x509.NewCertPool() + + var ( + block *pem.Block + rest = p.AttestationRoots + ) + for rest != nil { + block, rest = pem.Decode(rest) + if block == nil { + break + } + cert, err := x509.ParseCertificate(block.Bytes) + if err != nil { + return errors.Wrap(err, "error parsing x509 certificate from PEM block") + } + p.attestationRootPool.AddCert(cert) + } + + // TODO(hs): need validation for number of certs? The current ones are only for the `tpm` type; not for Apple or Yubico. + +>>>>>>> acdfdf34 (Add `tpm` attestation with configurable roots) p.ctl, err = NewController(p, p.Claims, config, p.Options) return } @@ -282,3 +311,9 @@ func (p *ACME) IsAttestationFormatEnabled(ctx context.Context, format ACMEAttest } return false } + +// 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, error) { + return p.attestationRootPool, nil +} From 42102d88d5c12ee9ab50b10b2a3b7d5654660c21 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Thu, 15 Sep 2022 15:50:04 -0700 Subject: [PATCH 05/13] Fix merge and add unit tests --- authority/provisioner/acme.go | 64 ++++++++------- authority/provisioner/acme_test.go | 80 +++++++++++++++++++ .../testdata/certs/apple-att-ca.crt | 14 ++++ .../testdata/certs/yubico-piv-ca.crt | 19 +++++ 4 files changed, 146 insertions(+), 31 deletions(-) create mode 100644 authority/provisioner/testdata/certs/apple-att-ca.crt create mode 100644 authority/provisioner/testdata/certs/yubico-piv-ca.crt diff --git a/authority/provisioner/acme.go b/authority/provisioner/acme.go index afcaf08a..5955ac6a 100644 --- a/authority/provisioner/acme.go +++ b/authority/provisioner/acme.go @@ -96,14 +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"` - - // TODO(hs): WIP configuration for ACME Device Attestation - AttestationRoots []byte `json:"attestationRoots"` + // 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 + ctl *Controller } // GetID returns the provisioner unique identifier. @@ -160,7 +160,6 @@ func (p *ACME) Init(config Config) (err error) { return errors.New("provisioner name cannot be empty") } -<<<<<<< HEAD for _, c := range p.Challenges { if err := c.Validate(); err != nil { return err @@ -172,29 +171,29 @@ func (p *ACME) Init(config Config) (err error) { } } -======= - // TODO(hs): WIP configuration for ACME Device Attestation - p.attestationRootPool = x509.NewCertPool() - - var ( - block *pem.Block - rest = p.AttestationRoots - ) - for rest != nil { - block, rest = pem.Decode(rest) - if block == nil { - break + // 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 } - cert, err := x509.ParseCertificate(block.Bytes) - if err != nil { - return errors.Wrap(err, "error parsing x509 certificate from PEM block") + if !hasCert { + return errors.New("error parsing attestationRoots: no certificates found") } - p.attestationRootPool.AddCert(cert) } - // TODO(hs): need validation for number of certs? The current ones are only for the `tpm` type; not for Apple or Yubico. - ->>>>>>> acdfdf34 (Add `tpm` attestation with configurable roots) p.ctl, err = NewController(p, p.Claims, config, p.Options) return } @@ -312,8 +311,11 @@ func (p *ACME) IsAttestationFormatEnabled(ctx context.Context, format ACMEAttest return false } -// 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, error) { - return p.attestationRootPool, nil +// 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 From 6b73a020e3688f688974ece1d9aab025ef29e090 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Thu, 15 Sep 2022 18:19:52 -0700 Subject: [PATCH 06/13] Add unit tests for apple and step attestations --- acme/challenge.go | 36 ++-- acme/challenge_test.go | 390 ++++++++++++++++++++++++++++++++++++++++- acme/common.go | 9 + 3 files changed, 419 insertions(+), 16 deletions(-) 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..32d78166 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 { @@ -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) + } + }) + } +} 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 { From 829530ae90e5bbcbfd61b118d5dca89e271f637d Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Thu, 15 Sep 2022 18:24:43 -0700 Subject: [PATCH 07/13] Fix linter errors --- acme/api/account_test.go | 5 +++++ acme/challenge_test.go | 4 ++-- 2 files changed, 7 insertions(+), 2 deletions(-) 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_test.go b/acme/challenge_test.go index 32d78166..856c04e3 100644 --- a/acme/challenge_test.go +++ b/acme/challenge_test.go @@ -45,8 +45,8 @@ 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 Test_storeError(t *testing.T) { From 498549c95c90ccfaf950825be68f6cd4cd1a0dec Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Fri, 16 Sep 2022 10:02:10 -0700 Subject: [PATCH 08/13] Extract common function used in tests --- acme/challenge_test.go | 98 ++++++++++++++++++------------------------ 1 file changed, 43 insertions(+), 55 deletions(-) diff --git a/acme/challenge_test.go b/acme/challenge_test.go index 856c04e3..90aafa97 100644 --- a/acme/challenge_test.go +++ b/acme/challenge_test.go @@ -49,6 +49,23 @@ func (m *mockClient) TLSDial(network, addr string, tlsConfig *tls.Config) (*tls. 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) { type test struct { ch *Challenge @@ -2410,21 +2427,6 @@ 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 { @@ -2461,7 +2463,7 @@ func Test_doAppleAttestationFormat(t *testing.T) { want *appleAttestationData wantErr bool }{ - {"ok", args{ctx, makeProvisioner(caRoot), &Challenge{}, &AttestationObject{ + {"ok", args{ctx, mustAttestationProvisioner(t, caRoot), &Challenge{}, &AttestationObject{ Format: "apple", AttStatement: map[string]interface{}{ "x5c": []interface{}{leaf.Raw, ca.Intermediate.Raw}, @@ -2473,49 +2475,49 @@ func Test_doAppleAttestationFormat(t *testing.T) { SEPVersion: "16.0", Certificate: leaf, }, false}, - {"fail apple issuer", args{ctx, makeProvisioner(nil), &Challenge{}, &AttestationObject{ + {"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, makeProvisioner(caRoot), &Challenge{}, &AttestationObject{ + {"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, makeProvisioner(caRoot), &Challenge{}, &AttestationObject{ + {"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, makeProvisioner(caRoot), &Challenge{}, &AttestationObject{ + {"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, makeProvisioner(caRoot), &Challenge{}, &AttestationObject{ + {"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, makeProvisioner(caRoot), &Challenge{}, &AttestationObject{ + {"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, makeProvisioner(caRoot), &Challenge{}, &AttestationObject{ + {"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, makeProvisioner(caRoot), &Challenge{}, &AttestationObject{ + {"fail verify", args{ctx, mustAttestationProvisioner(t, caRoot), &Challenge{}, &AttestationObject{ Format: "apple", AttStatement: map[string]interface{}{ "x5c": []interface{}{leaf.Raw}, @@ -2544,20 +2546,6 @@ func Test_doStepAttestationFormat(t *testing.T) { } 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"}, @@ -2633,7 +2621,7 @@ func Test_doStepAttestationFormat(t *testing.T) { want *stepAttestationData wantErr bool }{ - {"ok", args{ctx, makeProvisioner(caRoot), &Challenge{Token: "token"}, jwk, &AttestationObject{ + {"ok", args{ctx, mustAttestationProvisioner(t, caRoot), &Challenge{Token: "token"}, jwk, &AttestationObject{ Format: "step", AttStatement: map[string]interface{}{ "x5c": []interface{}{leaf.Raw, ca.Intermediate.Raw}, @@ -2644,7 +2632,7 @@ func Test_doStepAttestationFormat(t *testing.T) { SerialNumber: "1234", Certificate: leaf, }, false}, - {"fail yubico issuer", args{ctx, makeProvisioner(nil), &Challenge{Token: "token"}, jwk, &AttestationObject{ + {"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}, @@ -2652,7 +2640,7 @@ func Test_doStepAttestationFormat(t *testing.T) { "sig": cborSig, }, }}, nil, true}, - {"fail x5c type", args{ctx, makeProvisioner(caRoot), &Challenge{Token: "token"}, jwk, &AttestationObject{ + {"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}, @@ -2660,7 +2648,7 @@ func Test_doStepAttestationFormat(t *testing.T) { "sig": cborSig, }, }}, nil, true}, - {"fail x5c empty", args{ctx, makeProvisioner(caRoot), &Challenge{Token: "token"}, jwk, &AttestationObject{ + {"fail x5c empty", args{ctx, mustAttestationProvisioner(t, caRoot), &Challenge{Token: "token"}, jwk, &AttestationObject{ Format: "step", AttStatement: map[string]interface{}{ "x5c": []interface{}{}, @@ -2668,7 +2656,7 @@ func Test_doStepAttestationFormat(t *testing.T) { "sig": cborSig, }, }}, nil, true}, - {"fail leaf type", args{ctx, makeProvisioner(caRoot), &Challenge{Token: "token"}, jwk, &AttestationObject{ + {"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}, @@ -2676,7 +2664,7 @@ func Test_doStepAttestationFormat(t *testing.T) { "sig": cborSig, }, }}, nil, true}, - {"fail leaf parse", args{ctx, makeProvisioner(caRoot), &Challenge{Token: "token"}, jwk, &AttestationObject{ + {"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}, @@ -2684,7 +2672,7 @@ func Test_doStepAttestationFormat(t *testing.T) { "sig": cborSig, }, }}, nil, true}, - {"fail intermediate type", args{ctx, makeProvisioner(caRoot), &Challenge{Token: "token"}, jwk, &AttestationObject{ + {"fail intermediate type", args{ctx, mustAttestationProvisioner(t, caRoot), &Challenge{Token: "token"}, jwk, &AttestationObject{ Format: "step", AttStatement: map[string]interface{}{ "x5c": []interface{}{leaf.Raw, "intermediate"}, @@ -2692,7 +2680,7 @@ func Test_doStepAttestationFormat(t *testing.T) { "sig": cborSig, }, }}, nil, true}, - {"fail intermediate parse", args{ctx, makeProvisioner(caRoot), &Challenge{Token: "token"}, jwk, &AttestationObject{ + {"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]}, @@ -2700,7 +2688,7 @@ func Test_doStepAttestationFormat(t *testing.T) { "sig": cborSig, }, }}, nil, true}, - {"fail verify", args{ctx, makeProvisioner(caRoot), &Challenge{Token: "token"}, jwk, &AttestationObject{ + {"fail verify", args{ctx, mustAttestationProvisioner(t, caRoot), &Challenge{Token: "token"}, jwk, &AttestationObject{ Format: "step", AttStatement: map[string]interface{}{ "x5c": []interface{}{leaf.Raw}, @@ -2708,7 +2696,7 @@ func Test_doStepAttestationFormat(t *testing.T) { "sig": cborSig, }, }}, nil, true}, - {"fail sig type", args{ctx, makeProvisioner(caRoot), &Challenge{Token: "token"}, jwk, &AttestationObject{ + {"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}, @@ -2716,7 +2704,7 @@ func Test_doStepAttestationFormat(t *testing.T) { "sig": string(cborSig), }, }}, nil, true}, - {"fail sig unmarshal", args{ctx, makeProvisioner(caRoot), &Challenge{Token: "token"}, jwk, &AttestationObject{ + {"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}, @@ -2724,7 +2712,7 @@ func Test_doStepAttestationFormat(t *testing.T) { "sig": []byte("bad-sig"), }, }}, nil, true}, - {"fail keyAuthorization", args{ctx, makeProvisioner(caRoot), &Challenge{Token: "token"}, &jose.JSONWebKey{Key: []byte("not an asymmetric key")}, &AttestationObject{ + {"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}, @@ -2732,7 +2720,7 @@ func Test_doStepAttestationFormat(t *testing.T) { "sig": cborSig, }, }}, nil, true}, - {"fail sig verify P-256", args{ctx, makeProvisioner(caRoot), &Challenge{Token: "token"}, jwk, &AttestationObject{ + {"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}, @@ -2740,7 +2728,7 @@ func Test_doStepAttestationFormat(t *testing.T) { "sig": otherCBORSig, }, }}, nil, true}, - {"fail sig verify P-384", args{ctx, makeProvisioner(caRoot), &Challenge{Token: "token"}, jwk, &AttestationObject{ + {"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}, @@ -2748,7 +2736,7 @@ func Test_doStepAttestationFormat(t *testing.T) { "sig": cborSig, }, }}, nil, true}, - {"fail sig verify RSA", args{ctx, makeProvisioner(caRoot), &Challenge{Token: "token"}, jwk, &AttestationObject{ + {"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}, @@ -2756,7 +2744,7 @@ func Test_doStepAttestationFormat(t *testing.T) { "sig": cborSig, }, }}, nil, true}, - {"fail sig verify Ed25519", args{ctx, makeProvisioner(caRoot), &Challenge{Token: "token"}, jwk, &AttestationObject{ + {"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}, @@ -2764,7 +2752,7 @@ func Test_doStepAttestationFormat(t *testing.T) { "sig": cborSig, }, }}, nil, true}, - {"fail unmarshal serial number", args{ctx, makeProvisioner(caRoot), &Challenge{Token: "token"}, jwk, &AttestationObject{ + {"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}, From b0d24fb801dab2be71b3375829cfb6f6c3d7d072 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Fri, 16 Sep 2022 12:35:09 -0700 Subject: [PATCH 09/13] Fix test with gcloud local credentialss --- cas/cloudcas/cloudcas_test.go | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/cas/cloudcas/cloudcas_test.go b/cas/cloudcas/cloudcas_test.go index eee25956..e5fbf58e 100644 --- a/cas/cloudcas/cloudcas_test.go +++ b/cas/cloudcas/cloudcas_test.go @@ -14,6 +14,7 @@ import ( "io" "net" "os" + "path/filepath" "reflect" "testing" "time" @@ -402,6 +403,14 @@ func TestNew_real(t *testing.T) { }) } + failDefaultCredentials := true + if home, err := os.UserHomeDir(); err == nil { + file := filepath.Join(home, ".config", "gcloud", "application_default_credentials.json") + if _, err := os.Stat(file); err == nil { + failDefaultCredentials = false + } + } + type args struct { ctx context.Context opts apiv1.Options @@ -412,7 +421,7 @@ func TestNew_real(t *testing.T) { args args wantErr bool }{ - {"fail default credentials", true, args{context.Background(), apiv1.Options{CertificateAuthority: testAuthorityName}}, true}, + {"fail default credentials", true, args{context.Background(), apiv1.Options{CertificateAuthority: testAuthorityName}}, failDefaultCredentials}, {"fail certificate authority", false, args{context.Background(), apiv1.Options{}}, true}, {"fail with credentials", false, args{context.Background(), apiv1.Options{ CertificateAuthority: testAuthorityName, CredentialsFile: "testdata/missing.json", From 34c6c65671ec59fc63a045bd128d3a35a6f4fb7b Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Fri, 16 Sep 2022 12:37:41 -0700 Subject: [PATCH 10/13] Pass attestation information to the Sign method Attestation information might be useful in authorizing webhooks --- acme/order.go | 10 ++++++++++ authority/provisioner/sign_options.go | 6 ++++++ authority/tls.go | 6 ++++++ 3 files changed, 22 insertions(+) diff --git a/acme/order.go b/acme/order.go index ee76a364..2eddad53 100644 --- a/acme/order.go +++ b/acme/order.go @@ -157,6 +157,9 @@ func (o *Order) Finalize(ctx context.Context, db DB, csr *x509.CertificateReques data := x509util.NewTemplateData() data.SetCommonName(csr.Subject.CommonName) + // Custom sign options passed to authority.Sign + var extraOptions []provisioner.SignOption + // TODO: support for multiple identifiers? var permanentIdentifier string for i := range o.Identifiers { @@ -173,6 +176,9 @@ func (o *Order) Finalize(ctx context.Context, db DB, csr *x509.CertificateReques Type: x509util.PermanentIdentifierType, Value: permanentIdentifier, }) + extraOptions = append(extraOptions, provisioner.AttestationData{ + PermanentIdentifier: permanentIdentifier, + }) } else { defaultTemplate = x509util.DefaultLeafTemplate sans, err := o.sans(csr) @@ -193,7 +199,11 @@ func (o *Order) Finalize(ctx context.Context, db DB, csr *x509.CertificateReques if err != nil { return WrapErrorISE(err, "error creating template options from ACME provisioner") } + + // Build extra signing options. signOps = append(signOps, templateOptions) + signOps = append(signOps, extraOptions...) + // Sign a new certificate. certChain, err := auth.Sign(csr, provisioner.SignOptions{ NotBefore: provisioner.NewTimeDuration(o.NotBefore), diff --git a/authority/provisioner/sign_options.go b/authority/provisioner/sign_options.go index c3868e5f..8a0363a6 100644 --- a/authority/provisioner/sign_options.go +++ b/authority/provisioner/sign_options.go @@ -77,6 +77,12 @@ func (fn CertificateEnforcerFunc) Enforce(cert *x509.Certificate) error { return fn(cert) } +// AttestationData is a SignOption used to pass attestation information to the +// sign methods. +type AttestationData struct { + PermanentIdentifier string +} + // emailOnlyIdentity is a CertificateRequestValidator that checks that the only // SAN provided is the given email address. type emailOnlyIdentity string diff --git a/authority/tls.go b/authority/tls.go index c7e2dd09..632ac238 100644 --- a/authority/tls.go +++ b/authority/tls.go @@ -94,6 +94,7 @@ func (a *Authority) Sign(csr *x509.CertificateRequest, signOpts provisioner.Sign var prov provisioner.Interface var pInfo *casapi.ProvisionerInfo + var attData provisioner.AttestationData for _, op := range extraOpts { switch k := op.(type) { // Capture current provisioner @@ -129,6 +130,11 @@ func (a *Authority) Sign(csr *x509.CertificateRequest, signOpts provisioner.Sign case provisioner.CertificateEnforcer: certEnforcers = append(certEnforcers, k) + // Extra information from ACME attestations. + case provisioner.AttestationData: + attData = k + // TODO(mariano,areed): remove me once attData is used. + _ = attData default: return nil, errs.InternalServer("authority.Sign; invalid extra option type %T", append([]interface{}{k}, opts...)...) } From 8cf6675ce459432c4713e41cea951d87b3627ee8 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Mon, 19 Sep 2022 12:48:35 -0700 Subject: [PATCH 11/13] Return the internal error instead of the ACME error For ACME errors, return the internal error string instead of the ACME one on the "Error() string" function. This way the logs will have more information about the cause of an error. Fixes #1057 --- acme/errors.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/acme/errors.go b/acme/errors.go index 95c006b7..3158b19b 100644 --- a/acme/errors.go +++ b/acme/errors.go @@ -337,7 +337,10 @@ func (e *Error) StatusCode() int { // Error allows AError to implement the error interface. func (e *Error) Error() string { - return e.Detail + if e.Err == nil { + return e.Detail + } + return e.Err.Error() } // Cause returns the internal error and implements the Causer interface. From 226d36f66ff8ec8fa81008637cc108316b710fb8 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Mon, 19 Sep 2022 14:17:30 -0700 Subject: [PATCH 12/13] Fix unit tests --- acme/db/nosql/authz_test.go | 8 ++++---- acme/db/nosql/challenge_test.go | 8 ++++---- acme/db/nosql/order_test.go | 12 ++++++------ 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/acme/db/nosql/authz_test.go b/acme/db/nosql/authz_test.go index 2e5dd3ea..c41fabb5 100644 --- a/acme/db/nosql/authz_test.go +++ b/acme/db/nosql/authz_test.go @@ -77,7 +77,7 @@ func TestDB_getDBAuthz(t *testing.T) { Token: "token", CreatedAt: now, ExpiresAt: now.Add(5 * time.Minute), - Error: acme.NewErrorISE("force"), + Error: acme.NewErrorISE("The server experienced an internal error"), ChallengeIDs: []string{"foo", "bar"}, Wildcard: true, } @@ -254,7 +254,7 @@ func TestDB_GetAuthorization(t *testing.T) { Token: "token", CreatedAt: now, ExpiresAt: now.Add(5 * time.Minute), - Error: acme.NewErrorISE("force"), + Error: acme.NewErrorISE("The server experienced an internal error"), ChallengeIDs: []string{"foo", "bar"}, Wildcard: true, } @@ -532,7 +532,7 @@ func TestDB_UpdateAuthorization(t *testing.T) { assert.Equals(t, dbNew.Wildcard, dbaz.Wildcard) assert.Equals(t, dbNew.CreatedAt, dbaz.CreatedAt) assert.Equals(t, dbNew.ExpiresAt, dbaz.ExpiresAt) - assert.Equals(t, dbNew.Error.Error(), acme.NewError(acme.ErrorMalformedType, "malformed").Error()) + assert.Equals(t, dbNew.Error.Error(), acme.NewError(acme.ErrorMalformedType, "The request message was malformed").Error()) return nil, false, errors.New("force") }, }, @@ -582,7 +582,7 @@ func TestDB_UpdateAuthorization(t *testing.T) { assert.Equals(t, dbNew.Wildcard, dbaz.Wildcard) assert.Equals(t, dbNew.CreatedAt, dbaz.CreatedAt) assert.Equals(t, dbNew.ExpiresAt, dbaz.ExpiresAt) - assert.Equals(t, dbNew.Error.Error(), acme.NewError(acme.ErrorMalformedType, "malformed").Error()) + assert.Equals(t, dbNew.Error.Error(), acme.NewError(acme.ErrorMalformedType, "The request message was malformed").Error()) return nu, true, nil }, }, diff --git a/acme/db/nosql/challenge_test.go b/acme/db/nosql/challenge_test.go index 4da5679b..08c5a608 100644 --- a/acme/db/nosql/challenge_test.go +++ b/acme/db/nosql/challenge_test.go @@ -72,7 +72,7 @@ func TestDB_getDBChallenge(t *testing.T) { Value: "test.ca.smallstep.com", CreatedAt: clock.Now(), ValidatedAt: "foobar", - Error: acme.NewErrorISE("force"), + Error: acme.NewErrorISE("The server experienced an internal error"), } b, err := json.Marshal(dbc) assert.FatalError(t, err) @@ -264,7 +264,7 @@ func TestDB_GetChallenge(t *testing.T) { Value: "test.ca.smallstep.com", CreatedAt: clock.Now(), ValidatedAt: "foobar", - Error: acme.NewErrorISE("force"), + Error: acme.NewErrorISE("The server experienced an internal error"), } b, err := json.Marshal(dbc) assert.FatalError(t, err) @@ -354,7 +354,7 @@ func TestDB_UpdateChallenge(t *testing.T) { ID: chID, Status: acme.StatusValid, ValidatedAt: "foobar", - Error: acme.NewError(acme.ErrorMalformedType, "malformed"), + Error: acme.NewError(acme.ErrorMalformedType, "The request message was malformed"), } return test{ ch: updCh, @@ -428,7 +428,7 @@ func TestDB_UpdateChallenge(t *testing.T) { assert.Equals(t, dbNew.CreatedAt, dbc.CreatedAt) assert.Equals(t, dbNew.Status, acme.StatusValid) assert.Equals(t, dbNew.ValidatedAt, "foobar") - assert.Equals(t, dbNew.Error.Error(), acme.NewError(acme.ErrorMalformedType, "malformed").Error()) + assert.Equals(t, dbNew.Error.Error(), acme.NewError(acme.ErrorMalformedType, "The request message was malformed").Error()) return nu, true, nil }, }, diff --git a/acme/db/nosql/order_test.go b/acme/db/nosql/order_test.go index e92eb684..ff9396bd 100644 --- a/acme/db/nosql/order_test.go +++ b/acme/db/nosql/order_test.go @@ -80,7 +80,7 @@ func TestDB_getDBOrder(t *testing.T) { {Type: "dns", Value: "example.foo.com"}, }, AuthorizationIDs: []string{"foo", "bar"}, - Error: acme.NewError(acme.ErrorMalformedType, "force"), + Error: acme.NewError(acme.ErrorMalformedType, "The request message was malformed"), } b, err := json.Marshal(dbo) assert.FatalError(t, err) @@ -185,7 +185,7 @@ func TestDB_GetOrder(t *testing.T) { {Type: "dns", Value: "example.foo.com"}, }, AuthorizationIDs: []string{"foo", "bar"}, - Error: acme.NewError(acme.ErrorMalformedType, "force"), + Error: acme.NewError(acme.ErrorMalformedType, "The request message was malformed"), } b, err := json.Marshal(dbo) assert.FatalError(t, err) @@ -284,7 +284,7 @@ func TestDB_UpdateOrder(t *testing.T) { ID: orderID, Status: acme.StatusValid, CertificateID: "certID", - Error: acme.NewError(acme.ErrorMalformedType, "force"), + Error: acme.NewError(acme.ErrorMalformedType, "The request message was malformed"), } return test{ o: o, @@ -324,7 +324,7 @@ func TestDB_UpdateOrder(t *testing.T) { ID: orderID, Status: acme.StatusValid, CertificateID: "certID", - Error: acme.NewError(acme.ErrorMalformedType, "force"), + Error: acme.NewError(acme.ErrorMalformedType, "The request message was malformed"), } return test{ o: o, @@ -372,7 +372,7 @@ func TestDB_UpdateOrder(t *testing.T) { assert.Equals(t, tc.o.ID, dbo.ID) assert.Equals(t, tc.o.CertificateID, "certID") assert.Equals(t, tc.o.Status, acme.StatusValid) - assert.Equals(t, tc.o.Error.Error(), acme.NewError(acme.ErrorMalformedType, "force").Error()) + assert.Equals(t, tc.o.Error.Error(), acme.NewError(acme.ErrorMalformedType, "The request message was malformed").Error()) } } }) @@ -659,7 +659,7 @@ func TestDB_updateAddOrderIDs(t *testing.T) { assert.Equals(t, newdbo.ID, "foo") assert.Equals(t, newdbo.Status, acme.StatusInvalid) assert.Equals(t, newdbo.ExpiresAt, expiry) - assert.Equals(t, newdbo.Error.Error(), acme.NewError(acme.ErrorMalformedType, "order has expired").Error()) + assert.Equals(t, newdbo.Error.Error(), acme.NewError(acme.ErrorMalformedType, "The request message was malformed").Error()) return nil, false, errors.New("force") }, }, From 7dc2067cb272c9bae0de213a3870bc795fee7ab3 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Mon, 19 Sep 2022 14:24:39 -0700 Subject: [PATCH 13/13] Update acme/errors.go Co-authored-by: Max --- acme/errors.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/acme/errors.go b/acme/errors.go index 3158b19b..34421500 100644 --- a/acme/errors.go +++ b/acme/errors.go @@ -335,7 +335,7 @@ func (e *Error) StatusCode() int { return e.Status } -// Error allows AError to implement the error interface. +// Error implements the error interface. func (e *Error) Error() string { if e.Err == nil { return e.Detail