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/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) 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 60a62dd2..143be1d3 100644 --- a/acme/challenge.go +++ b/acme/challenge.go @@ -354,7 +354,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) { @@ -382,7 +382,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) { @@ -593,13 +593,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 { @@ -690,13 +694,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/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") }, }, diff --git a/acme/errors.go b/acme/errors.go index 95c006b7..34421500 100644 --- a/acme/errors.go +++ b/acme/errors.go @@ -335,9 +335,12 @@ 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 { - return e.Detail + if e.Err == nil { + return e.Detail + } + return e.Err.Error() } // Cause returns the internal error and implements the Causer interface. 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/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 diff --git a/authority/provisioner/acme.go b/authority/provisioner/acme.go index cdbfeece..ba5fa779 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. @@ -192,6 +192,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 } @@ -291,12 +314,6 @@ func (p *ACME) IsChallengeEnabled(ctx context.Context, challenge ACMEChallenge) 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 -} - // IsAttestationFormatEnabled checks if the given attestation format is enabled. // By default apple, step and tpm are enabled, to disable any of them the // AttestationFormat provisioner property should have at least one element. @@ -314,3 +331,9 @@ 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. +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/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/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 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...)...) } 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", diff --git a/go.mod b/go.mod index fb265baf..cb27d4ae 100644 --- a/go.mod +++ b/go.mod @@ -18,6 +18,7 @@ require ( github.com/go-piv/piv-go v1.10.0 // indirect github.com/golang/mock v1.6.0 github.com/google/go-cmp v0.5.8 + github.com/google/go-tpm v0.3.3 github.com/google/uuid v1.3.0 github.com/googleapis/gax-go/v2 v2.4.0 github.com/hashicorp/vault/api v1.3.1 @@ -31,6 +32,7 @@ require ( github.com/newrelic/go-agent/v3 v3.18.0 github.com/pkg/errors v0.9.1 github.com/rs/xid v1.2.1 + github.com/ryboe/q v1.0.17 github.com/sirupsen/logrus v1.8.1 github.com/slackhq/nebula v1.5.2 github.com/smallstep/assert v0.0.0-20200723003110-82e2b9b3b262 @@ -40,7 +42,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 @@ -51,11 +53,6 @@ require ( gopkg.in/square/go-jose.v2 v2.6.0 ) -require ( - github.com/google/go-tpm v0.3.3 - github.com/ryboe/q v1.0.17 -) - require ( cloud.google.com/go/compute v1.6.1 // indirect cloud.google.com/go/iam v0.1.0 // indirect diff --git a/go.sum b/go.sum index e37f8fb9..0e4aaee6 100644 --- a/go.sum +++ b/go.sum @@ -675,8 +675,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=