diff --git a/crypto/pem/pem.go b/crypto/pem/pem.go new file mode 100644 index 0000000..8d84246 --- /dev/null +++ b/crypto/pem/pem.go @@ -0,0 +1,189 @@ +/* +Copyright 2023 The Dapr Authors +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package pem + +import ( + "bytes" + "crypto" + "crypto/ecdsa" + "crypto/ed25519" + "crypto/rsa" + "crypto/x509" + "encoding/pem" + "errors" + "fmt" +) + +// DecodePEMCertificatesChain takes a PEM-encoded x509 certificates byte array +// and returns all certificates in a slice of x509.Certificate objects. +// Expects certificates to be a chain with leaf certificate to be first in the +// byte array. +func DecodePEMCertificatesChain(crtb []byte) ([]*x509.Certificate, error) { + certs, err := DecodePEMCertificates(crtb) + if err != nil { + return nil, err + } + + for i := 0; i < len(certs)-1; i++ { + if certs[i].CheckSignatureFrom(certs[i+1]) != nil { + return nil, errors.New("certificate chain is not valid") + } + } + + return certs, nil +} + +// DecodePEMCertificatesChain takes a PEM-encoded x509 certificates byte array +// and returns all certificates in a slice of x509.Certificate objects. +func DecodePEMCertificates(crtb []byte) ([]*x509.Certificate, error) { + certs := []*x509.Certificate{} + for len(crtb) > 0 { + var err error + var cert *x509.Certificate + + cert, crtb, err = decodeCertificatePEM(crtb) + if err != nil { + return nil, err + } + if cert != nil { + // it's a cert, add to pool + certs = append(certs, cert) + } + } + + if len(certs) == 0 { + return nil, errors.New("no certificates found") + } + + return certs, nil +} + +func decodeCertificatePEM(crtb []byte) (*x509.Certificate, []byte, error) { + block, crtb := pem.Decode(crtb) + if block == nil { + return nil, nil, nil + } + if block.Type != "CERTIFICATE" { + return nil, nil, nil + } + c, err := x509.ParseCertificate(block.Bytes) + return c, crtb, err +} + +// DecodePEMPrivateKey takes a key PEM byte array and returns an object that +// represents either an RSA or EC private key. +func DecodePEMPrivateKey(key []byte) (crypto.Signer, error) { + block, _ := pem.Decode(key) + if block == nil { + return nil, errors.New("key is not PEM encoded") + } + + switch block.Type { + case "EC PRIVATE KEY": + return x509.ParseECPrivateKey(block.Bytes) + case "RSA PRIVATE KEY": + return x509.ParsePKCS1PrivateKey(block.Bytes) + case "PRIVATE KEY": + key, err := x509.ParsePKCS8PrivateKey(block.Bytes) + if err != nil { + return nil, err + } + return key.(crypto.Signer), nil + default: + return nil, fmt.Errorf("unsupported block type %s", block.Type) + } +} + +// EncodePrivateKey will encode a private key into PEM format. +func EncodePrivateKey(key any) ([]byte, error) { + var ( + keyBytes []byte + err error + blockType string + ) + + switch key := key.(type) { + case *ecdsa.PrivateKey, *ed25519.PrivateKey: + keyBytes, err = x509.MarshalPKCS8PrivateKey(key) + if err != nil { + return nil, err + } + blockType = "PRIVATE KEY" + default: + return nil, fmt.Errorf("unsupported key type %T", key) + } + + return pem.EncodeToMemory(&pem.Block{ + Type: blockType, Bytes: keyBytes, + }), nil +} + +// EncodeX509 will encode a single *x509.Certificate into PEM format. +func EncodeX509(cert *x509.Certificate) ([]byte, error) { + caPem := bytes.NewBuffer([]byte{}) + err := pem.Encode(caPem, &pem.Block{Type: "CERTIFICATE", Bytes: cert.Raw}) + if err != nil { + return nil, err + } + + return caPem.Bytes(), nil +} + +// EncodeX509Chain will encode a list of *x509.Certificates into a PEM format chain. +// Self-signed certificates are not included as per +// https://datatracker.ietf.org/doc/html/rfc5246#section-7.4.2 +// Certificates are output in the order they're given; if the input is not ordered +// as specified in RFC5246 section 7.4.2, the resulting chain might not be valid +// for use in TLS. +func EncodeX509Chain(certs []*x509.Certificate) ([]byte, error) { + if len(certs) == 0 { + return nil, errors.New("no certificates in chain") + } + + certPEM := bytes.NewBuffer([]byte{}) + for _, cert := range certs { + if cert == nil { + continue + } + + if cert.CheckSignatureFrom(cert) == nil { + // Don't include self-signed certificate + continue + } + + err := pem.Encode(certPEM, &pem.Block{Type: "CERTIFICATE", Bytes: cert.Raw}) + if err != nil { + return nil, err + } + } + + return certPEM.Bytes(), nil +} + +// PublicKeysEqual compares two given public keys for equality. +// The definition of "equality" depends on the type of the public keys. +// Returns true if the keys are the same, false if they differ or an error if +// the key type of `a` cannot be determined. +func PublicKeysEqual(a, b crypto.PublicKey) (bool, error) { + switch pub := a.(type) { + case *rsa.PublicKey: + return pub.Equal(b), nil + case *ecdsa.PublicKey: + return pub.Equal(b), nil + case ed25519.PublicKey: + return pub.Equal(b), nil + default: + return false, fmt.Errorf("unrecognised public key type: %T", a) + } +} diff --git a/crypto/spiffe/context/context.go b/crypto/spiffe/context/context.go new file mode 100644 index 0000000..4f03890 --- /dev/null +++ b/crypto/spiffe/context/context.go @@ -0,0 +1,35 @@ +/* +Copyright 2024 The Dapr Authors +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package context + +import ( + "context" + + "github.com/spiffe/go-spiffe/v2/svid/x509svid" + + "github.com/dapr/kit/crypto/spiffe" +) + +type ctxkey int + +const svidKey ctxkey = iota + +func With(ctx context.Context, spiffe *spiffe.SPIFFE) context.Context { + return context.WithValue(ctx, svidKey, spiffe.SVIDSource()) +} + +func From(ctx context.Context) (x509svid.Source, bool) { + svid, ok := ctx.Value(svidKey).(x509svid.Source) + return svid, ok +} diff --git a/crypto/spiffe/spiffe.go b/crypto/spiffe/spiffe.go new file mode 100644 index 0000000..4904272 --- /dev/null +++ b/crypto/spiffe/spiffe.go @@ -0,0 +1,182 @@ +/* +Copyright 2024 The Dapr Authors +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package spiffe + +import ( + "context" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/x509" + "errors" + "fmt" + "sync" + "sync/atomic" + "time" + + "github.com/spiffe/go-spiffe/v2/svid/x509svid" + "k8s.io/utils/clock" + + "github.com/dapr/kit/logger" +) + +type ( + RequestSVIDFn func(context.Context, []byte) ([]*x509.Certificate, error) +) + +type Options struct { + Log logger.Logger + RequestSVIDFn RequestSVIDFn +} + +// SPIFFE is a readable/writeable store of a SPIFFE X.509 SVID. +// Used to manage a workload SVID, and share read-only interfaces to consumers. +type SPIFFE struct { + currentSVID *x509svid.SVID + requestSVIDFn RequestSVIDFn + + log logger.Logger + lock sync.RWMutex + clock clock.Clock + running atomic.Bool + readyCh chan struct{} +} + +func New(opts Options) *SPIFFE { + return &SPIFFE{ + requestSVIDFn: opts.RequestSVIDFn, + log: opts.Log, + clock: clock.RealClock{}, + readyCh: make(chan struct{}), + } +} + +func (s *SPIFFE) Run(ctx context.Context) error { + if !s.running.CompareAndSwap(false, true) { + return errors.New("already running") + } + + s.lock.Lock() + s.log.Info("Fetching initial identity certificate") + initialCert, err := s.fetchIdentityCertificate(ctx) + if err != nil { + close(s.readyCh) + s.lock.Unlock() + return fmt.Errorf("failed to retrieve the initial identity certificate: %w", err) + } + + s.currentSVID = initialCert + close(s.readyCh) + s.lock.Unlock() + + s.log.Infof("Security is initialized successfully") + s.runRotation(ctx) + + return nil +} + +// Ready blocks until SPIFFE is ready or the context is done which will return +// the context error. +func (s *SPIFFE) Ready(ctx context.Context) error { + select { + case <-ctx.Done(): + return ctx.Err() + case <-s.readyCh: + return nil + } +} + +// runRotation starts up the manager responsible for renewing the workload +// certificate. Receives the initial certificate to calculate the next rotation +// time. +func (s *SPIFFE) runRotation(ctx context.Context) { + defer s.log.Debug("stopping workload cert expiry watcher") + s.lock.RLock() + cert := s.currentSVID.Certificates[0] + s.lock.RUnlock() + renewTime := renewalTime(cert.NotBefore, cert.NotAfter) + s.log.Infof("Starting workload cert expiry watcher; current cert expires on: %s, renewing at %s", + cert.NotAfter.String(), renewTime.String()) + + for { + select { + case <-s.clock.After(min(time.Minute, renewTime.Sub(s.clock.Now()))): + if s.clock.Now().Before(renewTime) { + continue + } + s.log.Infof("Renewing workload cert; current cert expires on: %s", cert.NotAfter.String()) + svid, err := s.fetchIdentityCertificate(ctx) + if err != nil { + s.log.Errorf("Error renewing identity certificate, trying again in 10 seconds: %s", err) + select { + case <-s.clock.After(10 * time.Second): + continue + case <-ctx.Done(): + return + } + } + s.lock.Lock() + s.currentSVID = svid + cert = svid.Certificates[0] + s.lock.Unlock() + renewTime = renewalTime(cert.NotBefore, cert.NotAfter) + s.log.Infof("Successfully renewed workload cert; new cert expires on: %s", cert.NotAfter.String()) + + case <-ctx.Done(): + return + } + } +} + +// fetchIdentityCertificate fetches a new SVID using the configured requester. +func (s *SPIFFE) fetchIdentityCertificate(ctx context.Context) (*x509svid.SVID, error) { + key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + return nil, fmt.Errorf("failed to generate private key: %w", err) + } + + csrDER, err := x509.CreateCertificateRequest(rand.Reader, new(x509.CertificateRequest), key) + if err != nil { + return nil, fmt.Errorf("failed to create sidecar csr: %w", err) + } + + workloadcert, err := s.requestSVIDFn(ctx, csrDER) + if err != nil { + return nil, err + } + + if len(workloadcert) == 0 { + return nil, errors.New("no certificates received from sentry") + } + + spiffeID, err := x509svid.IDFromCert(workloadcert[0]) + if err != nil { + return nil, fmt.Errorf("error parsing spiffe id from newly signed certificate: %w", err) + } + + return &x509svid.SVID{ + ID: spiffeID, + Certificates: workloadcert, + PrivateKey: key, + }, nil +} + +func (s *SPIFFE) SVIDSource() x509svid.Source { + return &svidSource{spiffe: s} +} + +// renewalTime is 50% through the certificate validity period. +func renewalTime(notBefore, notAfter time.Time) time.Time { + return notBefore.Add(notAfter.Sub(notBefore) / 2) +} diff --git a/crypto/spiffe/spiffe_test.go b/crypto/spiffe/spiffe_test.go new file mode 100644 index 0000000..038b73b --- /dev/null +++ b/crypto/spiffe/spiffe_test.go @@ -0,0 +1,198 @@ +/* +Copyright 2024 The Dapr Authors +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package spiffe + +import ( + "context" + "crypto/x509" + "errors" + "sync/atomic" + "testing" + "time" + + "github.com/spiffe/go-spiffe/v2/spiffeid" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + clocktesting "k8s.io/utils/clock/testing" + + "github.com/dapr/kit/crypto/test" + "github.com/dapr/kit/logger" +) + +func Test_renewalTime(t *testing.T) { + now := time.Now() + assert.Equal(t, now, renewalTime(now, now)) + + in1Min := now.Add(time.Minute) + in30 := now.Add(time.Second * 30) + assert.Equal(t, in30, renewalTime(now, in1Min)) +} + +func Test_Run(t *testing.T) { + t.Run("should return error multiple Runs are called", func(t *testing.T) { + pki := test.GenPKI(t, test.PKIOptions{ + LeafID: spiffeid.RequireFromString("spiffe://example.com/foo/bar"), + }) + ctx, cancel := context.WithCancel(context.Background()) + s := New(Options{ + Log: logger.NewLogger("test"), + RequestSVIDFn: func(context.Context, []byte) ([]*x509.Certificate, error) { + return []*x509.Certificate{pki.LeafCert}, nil + }, + }) + + errCh := make(chan error) + go func() { + errCh <- s.Run(ctx) + }() + go func() { + errCh <- s.Run(ctx) + }() + + select { + case err := <-errCh: + require.Error(t, err) + case <-time.After(time.Second): + assert.Fail(t, "Expected error") + } + + cancel() + select { + case err := <-errCh: + require.NoError(t, err) + case <-time.After(time.Second): + assert.Fail(t, "First Run should have returned and returned no error ") + } + }) + + t.Run("should return error if initial fetch errors", func(t *testing.T) { + s := New(Options{ + Log: logger.NewLogger("test"), + RequestSVIDFn: func(context.Context, []byte) ([]*x509.Certificate, error) { + return nil, errors.New("this is an error") + }, + }) + + require.Error(t, s.Run(context.Background())) + }) + + t.Run("should renew certificate when it has expired", func(t *testing.T) { + pki := test.GenPKI(t, test.PKIOptions{ + LeafID: spiffeid.RequireFromString("spiffe://example.com/foo/bar"), + }) + + var fetches atomic.Int32 + s := New(Options{ + Log: logger.NewLogger("test"), + RequestSVIDFn: func(context.Context, []byte) ([]*x509.Certificate, error) { + fetches.Add(1) + return []*x509.Certificate{pki.LeafCert}, nil + }, + }) + now := time.Now() + clock := clocktesting.NewFakeClock(now) + s.clock = clock + + ctx, cancel := context.WithCancel(context.Background()) + errCh := make(chan error) + go func() { + errCh <- s.Run(ctx) + }() + + select { + case <-s.readyCh: + assert.Fail(t, "readyCh should not be closed") + default: + } + + assert.Eventually(t, clock.HasWaiters, time.Second, time.Millisecond) + assert.Equal(t, int32(1), fetches.Load()) + + clock.Step(pki.LeafCert.NotAfter.Sub(now) / 2) + assert.EventuallyWithT(t, func(c *assert.CollectT) { + assert.Equal(c, int32(2), fetches.Load()) + }, time.Second, time.Millisecond) + + cancel() + select { + case err := <-errCh: + require.NoError(t, err) + case <-time.After(time.Second): + assert.Fail(t, "First Run should have returned and returned no error ") + } + }) + + t.Run("if renewal failed, should try again in 10 seconds", func(t *testing.T) { + pki := test.GenPKI(t, test.PKIOptions{ + LeafID: spiffeid.RequireFromString("spiffe://example.com/foo/bar"), + }) + + respCert := []*x509.Certificate{pki.LeafCert} + var respErr error + + var fetches atomic.Int32 + s := New(Options{ + Log: logger.NewLogger("test"), + RequestSVIDFn: func(context.Context, []byte) ([]*x509.Certificate, error) { + fetches.Add(1) + return respCert, respErr + }, + }) + now := time.Now() + clock := clocktesting.NewFakeClock(now) + s.clock = clock + + ctx, cancel := context.WithCancel(context.Background()) + errCh := make(chan error) + go func() { + errCh <- s.Run(ctx) + }() + + select { + case <-s.readyCh: + assert.Fail(t, "readyCh should not be closed") + default: + } + + assert.Eventually(t, clock.HasWaiters, time.Second, time.Millisecond) + assert.Equal(t, int32(1), fetches.Load()) + + respCert = nil + respErr = errors.New("this is an error") + clock.Step(pki.LeafCert.NotAfter.Sub(now) / 2) + assert.EventuallyWithT(t, func(c *assert.CollectT) { + assert.Equal(c, int32(2), fetches.Load()) + }, time.Second, time.Millisecond) + + assert.Eventually(t, clock.HasWaiters, time.Second, time.Millisecond) + clock.Step(time.Second * 5) + assert.Eventually(t, clock.HasWaiters, time.Second, time.Millisecond) + assert.Equal(t, int32(2), fetches.Load()) + + clock.Step(time.Second * 5) + assert.Eventually(t, clock.HasWaiters, time.Second, time.Millisecond) + clock.Step(1) + assert.EventuallyWithT(t, func(c *assert.CollectT) { + assert.Equal(c, int32(3), fetches.Load()) + }, time.Second, time.Millisecond) + + cancel() + select { + case err := <-errCh: + require.NoError(t, err) + case <-time.After(time.Second): + assert.Fail(t, "First Run should have returned and returned no error ") + } + }) +} diff --git a/crypto/spiffe/svidsource.go b/crypto/spiffe/svidsource.go new file mode 100644 index 0000000..4260452 --- /dev/null +++ b/crypto/spiffe/svidsource.go @@ -0,0 +1,41 @@ +/* +Copyright 2024 The Dapr Authors +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package spiffe + +import ( + "errors" + + "github.com/spiffe/go-spiffe/v2/svid/x509svid" +) + +// svidSource is an implementation of the Go spiffe x509svid Source interface. +type svidSource struct { + spiffe *SPIFFE +} + +// GetX509SVID returns the current X.509 certificate identity as a SPIFFE SVID. +// Implements the go-spiffe x509 source interface. +func (s *svidSource) GetX509SVID() (*x509svid.SVID, error) { + s.spiffe.lock.RLock() + defer s.spiffe.lock.RUnlock() + + <-s.spiffe.readyCh + + svid := s.spiffe.currentSVID + if svid == nil { + return nil, errors.New("no SVID available") + } + + return svid, nil +} diff --git a/crypto/spiffe/svidsource_test.go b/crypto/spiffe/svidsource_test.go new file mode 100644 index 0000000..3df5860 --- /dev/null +++ b/crypto/spiffe/svidsource_test.go @@ -0,0 +1,24 @@ +/* +Copyright 2024 The Dapr Authors +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package spiffe + +import ( + "testing" + + "github.com/spiffe/go-spiffe/v2/svid/x509svid" +) + +func Test_svidSource(*testing.T) { + var _ x509svid.Source = new(svidSource) +} diff --git a/crypto/spiffe/trustanchors/file.go b/crypto/spiffe/trustanchors/file.go new file mode 100644 index 0000000..604925e --- /dev/null +++ b/crypto/spiffe/trustanchors/file.go @@ -0,0 +1,233 @@ +/* +Copyright 2024 The Dapr Authors +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package trustanchors + +import ( + "context" + "errors" + "fmt" + "os" + "path/filepath" + "sync" + "sync/atomic" + "time" + + "github.com/spiffe/go-spiffe/v2/bundle/x509bundle" + "github.com/spiffe/go-spiffe/v2/spiffeid" + "k8s.io/utils/clock" + + "github.com/dapr/kit/concurrency" + "github.com/dapr/kit/crypto/pem" + "github.com/dapr/kit/fswatcher" + "github.com/dapr/kit/logger" +) + +type OptionsFile struct { + Log logger.Logger + Path string +} + +// file is a TrustAnchors implementation that uses a file as the source of trust +// anchors. The trust anchors will be updated when the file changes. +type file struct { + log logger.Logger + path string + bundle *x509bundle.Bundle + rootPEM []byte + + // fswatcherInterval is the interval at which the trust anchors file changes + // are batched. Used for testing only, and 500ms otherwise. + fsWatcherInterval time.Duration + + // initFileWatchInterval is the interval at which the trust anchors file is + // checked for the first time. Used for testing only, and 1 second otherwise. + initFileWatchInterval time.Duration + + // subs is a list of channels to notify when the trust anchors are updated. + subs []chan<- struct{} + + lock sync.RWMutex + clock clock.Clock + running atomic.Bool + readyCh chan struct{} + closeCh chan struct{} + caEvent chan struct{} +} + +func FromFile(opts OptionsFile) Interface { + return &file{ + fsWatcherInterval: time.Millisecond * 500, + initFileWatchInterval: time.Second, + + log: opts.Log, + path: opts.Path, + clock: clock.RealClock{}, + readyCh: make(chan struct{}), + closeCh: make(chan struct{}), + caEvent: make(chan struct{}), + } +} + +func (f *file) Run(ctx context.Context) error { + if !f.running.CompareAndSwap(false, true) { + return errors.New("trust anchors is already running") + } + + defer close(f.closeCh) + + for { + _, err := os.Stat(f.path) + if err == nil { + break + } + if !errors.Is(err, os.ErrNotExist) { + return err + } + + // Trust anchors file not be provided yet, wait. + select { + case <-ctx.Done(): + return fmt.Errorf("failed to find trust anchors file '%s': %w", f.path, ctx.Err()) + case <-f.clock.After(f.initFileWatchInterval): + f.log.Warnf("Trust anchors file '%s' not found, waiting...", f.path) + } + } + + f.log.Infof("Trust anchors file '%s' found", f.path) + + if err := f.updateAnchors(ctx); err != nil { + return err + } + + fs, err := fswatcher.New(fswatcher.Options{ + Targets: []string{filepath.Dir(f.path)}, + Interval: &f.fsWatcherInterval, + }) + if err != nil { + return fmt.Errorf("failed to create file watcher: %w", err) + } + + close(f.readyCh) + + f.log.Infof("Watching trust anchors file '%s' for changes", f.path) + return concurrency.NewRunnerManager( + func(ctx context.Context) error { + return fs.Run(ctx, f.caEvent) + }, + func(ctx context.Context) error { + for { + select { + case <-ctx.Done(): + return nil + case <-f.caEvent: + f.log.Info("Trust anchors file changed, reloading trust anchors") + + if err = f.updateAnchors(ctx); err != nil { + return fmt.Errorf("failed to read trust anchors file '%s': %v", f.path, err) + } + } + } + }, + ).Run(ctx) +} + +func (f *file) CurrentTrustAnchors(ctx context.Context) ([]byte, error) { + select { + case <-ctx.Done(): + return nil, ctx.Err() + case <-f.closeCh: + return nil, errors.New("trust anchors is closed") + case <-f.readyCh: + } + + f.lock.RLock() + defer f.lock.RUnlock() + rootPEM := make([]byte, len(f.rootPEM)) + copy(rootPEM, f.rootPEM) + return rootPEM, nil +} + +func (f *file) updateAnchors(ctx context.Context) error { + f.lock.Lock() + defer f.lock.Unlock() + + rootPEMs, err := os.ReadFile(f.path) + if err != nil { + return fmt.Errorf("failed to read trust anchors file '%s': %w", f.path, err) + } + + trustAnchorCerts, err := pem.DecodePEMCertificates(rootPEMs) + if err != nil { + return fmt.Errorf("failed to decode trust anchors: %w", err) + } + + f.rootPEM = rootPEMs + f.bundle = x509bundle.FromX509Authorities(spiffeid.TrustDomain{}, trustAnchorCerts) + + var wg sync.WaitGroup + defer wg.Wait() + + wg.Add(len(f.subs)) + for _, ch := range f.subs { + go func(chi chan<- struct{}) { + defer wg.Done() + select { + case chi <- struct{}{}: + case <-ctx.Done(): + } + }(ch) + } + + return nil +} + +func (f *file) GetX509BundleForTrustDomain(_ spiffeid.TrustDomain) (*x509bundle.Bundle, error) { + select { + case <-f.closeCh: + return nil, errors.New("trust anchors is closed") + case <-f.readyCh: + } + + f.lock.RLock() + defer f.lock.RUnlock() + bundle := f.bundle + return bundle, nil +} + +func (f *file) Watch(ctx context.Context, ch chan<- []byte) { + f.lock.Lock() + sub := make(chan struct{}, 5) + f.subs = append(f.subs, sub) + f.lock.Unlock() + + for { + select { + case <-ctx.Done(): + return + case <-f.closeCh: + return + case <-sub: + f.lock.RLock() + rootPEM := make([]byte, len(f.rootPEM)) + copy(rootPEM, f.rootPEM) + f.lock.RUnlock() + + select { + case ch <- rootPEM: + case <-ctx.Done(): + case <-f.closeCh: + } + } + } +} diff --git a/crypto/spiffe/trustanchors/file_test.go b/crypto/spiffe/trustanchors/file_test.go new file mode 100644 index 0000000..6650e34 --- /dev/null +++ b/crypto/spiffe/trustanchors/file_test.go @@ -0,0 +1,574 @@ +/* +Copyright 2024 The Dapr Authors +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package trustanchors + +import ( + "context" + "os" + "path/filepath" + "testing" + "time" + + "github.com/spiffe/go-spiffe/v2/spiffeid" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/dapr/kit/crypto/test" + "github.com/dapr/kit/logger" +) + +func TestFile_Run(t *testing.T) { + t.Run("if Run multiple times, expect error", func(t *testing.T) { + tmp := filepath.Join(t.TempDir(), "ca.crt") + ta := FromFile(OptionsFile{ + Log: logger.NewLogger("test"), + Path: tmp, + }) + f, ok := ta.(*file) + require.True(t, ok) + f.initFileWatchInterval = time.Millisecond + + ctx, cancel := context.WithCancel(context.Background()) + errCh := make(chan error) + go func() { + errCh <- f.Run(ctx) + }() + go func() { + errCh <- f.Run(ctx) + }() + + select { + case err := <-errCh: + require.Error(t, err) + case <-time.After(time.Second): + assert.Fail(t, "Expected error") + } + + select { + case <-f.closeCh: + assert.Fail(t, "closeCh should not be closed") + default: + } + + cancel() + + select { + case err := <-errCh: + require.ErrorIs(t, err, context.Canceled) + case <-time.After(time.Second): + assert.Fail(t, "First Run should have returned and returned no error ") + } + }) + + t.Run("if file is not found and context cancelled, should return ctx.Err", func(t *testing.T) { + tmp := filepath.Join(t.TempDir(), "ca.crt") + + ta := FromFile(OptionsFile{ + Log: logger.NewLogger("test"), + Path: tmp, + }) + f, ok := ta.(*file) + require.True(t, ok) + f.initFileWatchInterval = time.Millisecond + + ctx, cancel := context.WithCancel(context.Background()) + errCh := make(chan error) + go func() { + errCh <- f.Run(ctx) + }() + + cancel() + + select { + case err := <-errCh: + require.ErrorIs(t, err, context.Canceled) + case <-time.After(time.Second): + assert.Fail(t, "First Run should have returned and returned no error ") + } + }) + + t.Run("if file found but is empty, should return error", func(t *testing.T) { + tmp := filepath.Join(t.TempDir(), "ca.crt") + require.NoError(t, os.WriteFile(tmp, nil, 0o600)) + + ta := FromFile(OptionsFile{ + Log: logger.NewLogger("test"), + Path: tmp, + }) + f, ok := ta.(*file) + require.True(t, ok) + f.initFileWatchInterval = time.Millisecond + + errCh := make(chan error) + go func() { + errCh <- f.Run(context.Background()) + }() + + select { + case err := <-errCh: + require.Error(t, err) + case <-time.After(time.Second): + assert.Fail(t, "expected error") + } + }) + + t.Run("if file found but is only garbage data, expect error", func(t *testing.T) { + tmp := filepath.Join(t.TempDir(), "ca.crt") + require.NoError(t, os.WriteFile(tmp, []byte("garbage data"), 0o600)) + + ta := FromFile(OptionsFile{ + Log: logger.NewLogger("test"), + Path: tmp, + }) + f, ok := ta.(*file) + require.True(t, ok) + f.initFileWatchInterval = time.Millisecond + + errCh := make(chan error) + go func() { + errCh <- f.Run(context.Background()) + }() + + select { + case err := <-errCh: + require.Error(t, err) + case <-time.After(time.Second): + assert.Fail(t, "expected error") + } + }) + + t.Run("if file found but is only garbage data in root, expect error", func(t *testing.T) { + pki := test.GenPKI(t, test.PKIOptions{}) + root := pki.RootCertPEM[10:] + tmp := filepath.Join(t.TempDir(), "ca.crt") + require.NoError(t, os.WriteFile(tmp, root, 0o600)) + + ta := FromFile(OptionsFile{ + Log: logger.NewLogger("test"), + Path: tmp, + }) + f, ok := ta.(*file) + require.True(t, ok) + f.initFileWatchInterval = time.Millisecond + + errCh := make(chan error) + go func() { + errCh <- f.Run(context.Background()) + }() + + select { + case err := <-errCh: + require.Error(t, err) + case <-time.After(time.Second): + assert.Fail(t, "expected error") + } + }) + + t.Run("single root should be correctly parsed from file", func(t *testing.T) { + pki := test.GenPKI(t, test.PKIOptions{}) + tmp := filepath.Join(t.TempDir(), "ca.crt") + require.NoError(t, os.WriteFile(tmp, pki.RootCertPEM, 0o600)) + + ta := FromFile(OptionsFile{ + Log: logger.NewLogger("test"), + Path: tmp, + }) + f, ok := ta.(*file) + require.True(t, ok) + f.initFileWatchInterval = time.Millisecond + + errCh := make(chan error) + go func() { + errCh <- f.Run(context.Background()) + }() + + select { + case <-f.readyCh: + case <-time.After(time.Second): + assert.Fail(t, "expected to be ready in time") + } + + b, err := f.CurrentTrustAnchors(context.Background()) + require.NoError(t, err) + assert.Equal(t, pki.RootCertPEM, b) + }) + + t.Run("garbage data outside of root should be ignored", func(t *testing.T) { + pki := test.GenPKI(t, test.PKIOptions{}) + //nolint:gocritic + root := append(pki.RootCertPEM, []byte("garbage data")...) + tmp := filepath.Join(t.TempDir(), "ca.crt") + require.NoError(t, os.WriteFile(tmp, root, 0o600)) + + ta := FromFile(OptionsFile{ + Log: logger.NewLogger("test"), + Path: tmp, + }) + f, ok := ta.(*file) + require.True(t, ok) + f.initFileWatchInterval = time.Millisecond + + errCh := make(chan error) + go func() { + errCh <- f.Run(context.Background()) + }() + + select { + case <-f.readyCh: + case <-time.After(time.Second): + assert.Fail(t, "expected to be ready in time") + } + + b, err := f.CurrentTrustAnchors(context.Background()) + require.NoError(t, err) + assert.Equal(t, root, b) + }) + + t.Run("multiple roots should be parsed", func(t *testing.T) { + pki1, pki2 := test.GenPKI(t, test.PKIOptions{}), test.GenPKI(t, test.PKIOptions{}) + //nolint:gocritic + roots := append(pki1.RootCertPEM, pki2.RootCertPEM...) + tmp := filepath.Join(t.TempDir(), "ca.crt") + require.NoError(t, os.WriteFile(tmp, roots, 0o600)) + + ta := FromFile(OptionsFile{ + Log: logger.NewLogger("test"), + Path: tmp, + }) + f, ok := ta.(*file) + require.True(t, ok) + f.initFileWatchInterval = time.Millisecond + + errCh := make(chan error) + go func() { + errCh <- f.Run(context.Background()) + }() + + select { + case <-f.readyCh: + case <-time.After(time.Second): + assert.Fail(t, "expected to be ready in time") + } + + b, err := f.CurrentTrustAnchors(context.Background()) + require.NoError(t, err) + assert.Equal(t, roots, b) + }) + + t.Run("writing a bad root PEM file should make Run return error", func(t *testing.T) { + pki1, pki2 := test.GenPKI(t, test.PKIOptions{}), test.GenPKI(t, test.PKIOptions{}) + //nolint:gocritic + roots := append(pki1.RootCertPEM, pki2.RootCertPEM...) + tmp := filepath.Join(t.TempDir(), "ca.crt") + require.NoError(t, os.WriteFile(tmp, roots, 0o600)) + + ta := FromFile(OptionsFile{ + Log: logger.NewLogger("test"), + Path: tmp, + }) + f, ok := ta.(*file) + require.True(t, ok) + f.initFileWatchInterval = time.Millisecond + f.fsWatcherInterval = time.Millisecond + + errCh := make(chan error) + go func() { + errCh <- f.Run(context.Background()) + }() + + select { + case <-f.readyCh: + case <-time.After(time.Second): + assert.Fail(t, "expected to be ready in time") + } + + require.NoError(t, os.WriteFile(tmp, []byte("garbage data"), 0o600)) + + select { + case err := <-errCh: + require.Error(t, err) + case <-time.After(time.Second): + assert.Fail(t, "expected error to be returned from Run") + } + }) +} + +func TestFile_GetX509BundleForTrustDomain(t *testing.T) { + t.Run("Should return full PEM regardless given trust domain", func(t *testing.T) { + pki := test.GenPKI(t, test.PKIOptions{}) + //nolint:gocritic + root := append(pki.RootCertPEM, []byte("garbage data")...) + tmp := filepath.Join(t.TempDir(), "ca.crt") + require.NoError(t, os.WriteFile(tmp, root, 0o600)) + ta := FromFile(OptionsFile{ + Log: logger.NewLogger("test"), + Path: tmp, + }) + f, ok := ta.(*file) + require.True(t, ok) + + errCh := make(chan error) + ctx, cancel := context.WithCancel(context.Background()) + go func() { + errCh <- ta.Run(ctx) + }() + t.Cleanup(func() { + cancel() + select { + case err := <-errCh: + require.NoError(t, err) + case <-time.After(time.Second): + assert.Fail(t, "expected Run to return") + } + }) + + trustDomain1, err := spiffeid.TrustDomainFromString("example.com") + require.NoError(t, err) + bundle, err := f.GetX509BundleForTrustDomain(trustDomain1) + require.NoError(t, err) + assert.Equal(t, f.bundle, bundle) + b1, err := bundle.Marshal() + require.NoError(t, err) + assert.Equal(t, pki.RootCertPEM, b1) + + trustDomain2, err := spiffeid.TrustDomainFromString("another-example.org") + require.NoError(t, err) + bundle, err = f.GetX509BundleForTrustDomain(trustDomain2) + require.NoError(t, err) + assert.Equal(t, f.bundle, bundle) + b2, err := bundle.Marshal() + require.NoError(t, err) + assert.Equal(t, pki.RootCertPEM, b2) + }) +} + +func TestFile_Watch(t *testing.T) { + t.Run("should return when Run context has been cancelled", func(t *testing.T) { + pki := test.GenPKI(t, test.PKIOptions{}) + tmp := filepath.Join(t.TempDir(), "ca.crt") + require.NoError(t, os.WriteFile(tmp, pki.RootCertPEM, 0o600)) + + ta := FromFile(OptionsFile{ + Log: logger.NewLogger("test"), + Path: tmp, + }) + f, ok := ta.(*file) + require.True(t, ok) + f.initFileWatchInterval = time.Millisecond + + errCh := make(chan error) + ctx, cancel := context.WithCancel(context.Background()) + go func() { + errCh <- f.Run(ctx) + }() + + watchDone := make(chan struct{}) + go func() { + ta.Watch(context.Background(), make(chan []byte)) + close(watchDone) + }() + + cancel() + + select { + case err := <-errCh: + require.NoError(t, err) + case <-time.After(time.Second): + assert.Fail(t, "expected error to be returned from Run") + } + + select { + case <-watchDone: + case <-time.After(time.Second): + assert.Fail(t, "expected Watch to have returned") + } + }) + + t.Run("should return when given context has been cancelled", func(t *testing.T) { + pki := test.GenPKI(t, test.PKIOptions{}) + tmp := filepath.Join(t.TempDir(), "ca.crt") + require.NoError(t, os.WriteFile(tmp, pki.RootCertPEM, 0o600)) + + ta := FromFile(OptionsFile{ + Log: logger.NewLogger("test"), + Path: tmp, + }) + f, ok := ta.(*file) + require.True(t, ok) + f.initFileWatchInterval = time.Millisecond + + errCh := make(chan error) + ctx1, cancel1 := context.WithCancel(context.Background()) + go func() { + errCh <- f.Run(ctx1) + }() + + watchDone := make(chan struct{}) + ctx2, cancel2 := context.WithCancel(context.Background()) + go func() { + ta.Watch(ctx2, make(chan []byte)) + close(watchDone) + }() + + cancel2() + + select { + case <-watchDone: + case <-time.After(time.Second): + assert.Fail(t, "expected Watch to have returned") + } + + cancel1() + + select { + case err := <-errCh: + require.NoError(t, err) + case <-time.After(time.Second): + assert.Fail(t, "expected error to be returned from Run") + } + }) + + t.Run("should update Watch subscribers when root PEM has been changed", func(t *testing.T) { + pki1 := test.GenPKI(t, test.PKIOptions{}) + pki2 := test.GenPKI(t, test.PKIOptions{}) + pki3 := test.GenPKI(t, test.PKIOptions{}) + tmp := filepath.Join(t.TempDir(), "ca.crt") + require.NoError(t, os.WriteFile(tmp, pki1.RootCertPEM, 0o600)) + + ta := FromFile(OptionsFile{ + Log: logger.NewLogger("test"), + Path: tmp, + }) + f, ok := ta.(*file) + require.True(t, ok) + f.initFileWatchInterval = time.Millisecond + f.fsWatcherInterval = time.Millisecond + + errCh := make(chan error) + ctx, cancel := context.WithCancel(context.Background()) + go func() { + errCh <- f.Run(ctx) + }() + + select { + case <-f.readyCh: + case <-time.After(time.Second): + assert.Fail(t, "expected to be ready in time") + } + + watchDone1, watchDone2 := make(chan struct{}), make(chan struct{}) + tCh1, tCh2 := make(chan []byte), make(chan []byte) + go func() { + ta.Watch(context.Background(), tCh1) + close(watchDone1) + }() + go func() { + ta.Watch(context.Background(), tCh2) + close(watchDone2) + }() + + //nolint:gocritic + roots := append(pki1.RootCertPEM, pki2.RootCertPEM...) + require.NoError(t, os.WriteFile(tmp, roots, 0o600)) + + for _, ch := range []chan []byte{tCh1, tCh2} { + select { + case b := <-ch: + assert.Equal(t, string(roots), string(b)) + case <-time.After(time.Second): + assert.Fail(t, "failed to get subscribed file watch in time") + } + } + + //nolint:gocritic + roots = append(pki1.RootCertPEM, append(pki2.RootCertPEM, pki3.RootCertPEM...)...) + require.NoError(t, os.WriteFile(tmp, roots, 0o600)) + + for _, ch := range []chan []byte{tCh1, tCh2} { + select { + case b := <-ch: + assert.Equal(t, string(roots), string(b)) + case <-time.After(time.Second): + assert.Fail(t, "failed to get subscribed file watch in time") + } + } + + cancel() + + for _, ch := range []chan struct{}{watchDone1, watchDone2} { + select { + case <-ch: + case <-time.After(time.Second): + assert.Fail(t, "expected Watch to have returned") + } + } + + select { + case err := <-errCh: + require.NoError(t, err) + case <-time.After(time.Second): + assert.Fail(t, "expected error to be returned from Run") + } + }) +} + +func TestFile_CurrentTrustAnchors(t *testing.T) { + t.Run("returns trust anchors as they change", func(t *testing.T) { + pki1, pki2, pki3 := test.GenPKI(t, test.PKIOptions{}), test.GenPKI(t, test.PKIOptions{}), test.GenPKI(t, test.PKIOptions{}) + tmp := filepath.Join(t.TempDir(), "ca.crt") + require.NoError(t, os.WriteFile(tmp, pki1.RootCertPEM, 0o600)) + + ta := FromFile(OptionsFile{ + Log: logger.NewLogger("test"), + Path: tmp, + }) + f, ok := ta.(*file) + require.True(t, ok) + f.initFileWatchInterval = time.Millisecond + f.fsWatcherInterval = time.Millisecond + + ctx, cancel := context.WithCancel(context.Background()) + errCh := make(chan error) + go func() { + errCh <- f.Run(ctx) + }() + + //nolint:gocritic + roots := append(pki1.RootCertPEM, pki2.RootCertPEM...) + require.NoError(t, os.WriteFile(tmp, roots, 0o600)) + assert.EventuallyWithT(t, func(c *assert.CollectT) { + pem, err := ta.CurrentTrustAnchors(context.Background()) + require.NoError(t, err) + assert.Equal(c, roots, pem) + }, time.Second, time.Millisecond) + + //nolint:gocritic + roots = append(pki1.RootCertPEM, append(pki2.RootCertPEM, pki3.RootCertPEM...)...) + require.NoError(t, os.WriteFile(tmp, roots, 0o600)) + assert.EventuallyWithT(t, func(c *assert.CollectT) { + pem, err := ta.CurrentTrustAnchors(context.Background()) + require.NoError(t, err) + assert.Equal(c, roots, pem) + }, time.Second, time.Millisecond) + + cancel() + + select { + case err := <-errCh: + require.NoError(t, err) + case <-time.After(time.Second): + assert.Fail(t, "expected error to be returned from Run") + } + }) +} diff --git a/crypto/spiffe/trustanchors/static.go b/crypto/spiffe/trustanchors/static.go new file mode 100644 index 0000000..ced48f1 --- /dev/null +++ b/crypto/spiffe/trustanchors/static.go @@ -0,0 +1,74 @@ +/* +Copyright 2024 The Dapr Authors +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package trustanchors + +import ( + "context" + "errors" + "fmt" + "sync/atomic" + + "github.com/spiffe/go-spiffe/v2/bundle/x509bundle" + "github.com/spiffe/go-spiffe/v2/spiffeid" + + "github.com/dapr/kit/crypto/pem" +) + +// static is a TrustAcnhors implementation that uses a static list of trust +// anchors. +type static struct { + bundle *x509bundle.Bundle + anchors []byte + running atomic.Bool + closeCh chan struct{} +} + +func FromStatic(anchors []byte) (Interface, error) { + trustAnchorCerts, err := pem.DecodePEMCertificates(anchors) + if err != nil { + return nil, fmt.Errorf("failed to decode trust anchors: %w", err) + } + + return &static{ + anchors: anchors, + bundle: x509bundle.FromX509Authorities(spiffeid.TrustDomain{}, trustAnchorCerts), + closeCh: make(chan struct{}), + }, nil +} + +func (s *static) CurrentTrustAnchors(context.Context) ([]byte, error) { + bundle := make([]byte, len(s.anchors)) + copy(bundle, s.anchors) + return bundle, nil +} + +func (s *static) Run(ctx context.Context) error { + if !s.running.CompareAndSwap(false, true) { + return errors.New("trust anchors source is already running") + } + <-ctx.Done() + close(s.closeCh) + return nil +} + +func (s *static) GetX509BundleForTrustDomain(spiffeid.TrustDomain) (*x509bundle.Bundle, error) { + return s.bundle, nil +} + +func (s *static) Watch(ctx context.Context, _ chan<- []byte) { + select { + case <-ctx.Done(): + case <-s.closeCh: + } +} diff --git a/crypto/spiffe/trustanchors/static_test.go b/crypto/spiffe/trustanchors/static_test.go new file mode 100644 index 0000000..004e195 --- /dev/null +++ b/crypto/spiffe/trustanchors/static_test.go @@ -0,0 +1,210 @@ +/* +Copyright 2024 The Dapr Authors +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package trustanchors + +import ( + "context" + "testing" + "time" + + "github.com/spiffe/go-spiffe/v2/spiffeid" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/dapr/kit/crypto/test" +) + +func TestFromStatic(t *testing.T) { + t.Run("empty root should return error", func(t *testing.T) { + _, err := FromStatic(nil) + require.Error(t, err) + }) + + t.Run("garbage data should return error", func(t *testing.T) { + _, err := FromStatic([]byte("garbage data")) + require.Error(t, err) + }) + + t.Run("just garbage data should return error", func(t *testing.T) { + _, err := FromStatic([]byte("garbage data")) + require.Error(t, err) + }) + + t.Run("garbage data in root should return error", func(t *testing.T) { + pki := test.GenPKI(t, test.PKIOptions{}) + root := pki.RootCertPEM[10:] + _, err := FromStatic(root) + require.Error(t, err) + }) + + t.Run("single root should be correctly parsed", func(t *testing.T) { + pki := test.GenPKI(t, test.PKIOptions{}) + ta, err := FromStatic(pki.RootCertPEM) + require.NoError(t, err) + taPEM, err := ta.CurrentTrustAnchors(context.Background()) + require.NoError(t, err) + assert.Equal(t, pki.RootCertPEM, taPEM) + }) + + t.Run("garbage data outside of root should be ignored", func(t *testing.T) { + pki := test.GenPKI(t, test.PKIOptions{}) + //nolint:gocritic + root := append(pki.RootCertPEM, []byte("garbage data")...) + ta, err := FromStatic(root) + require.NoError(t, err) + taPEM, err := ta.CurrentTrustAnchors(context.Background()) + require.NoError(t, err) + assert.Equal(t, root, taPEM) + }) + + t.Run("multiple roots should be correctly parsed", func(t *testing.T) { + pki1, pki2 := test.GenPKI(t, test.PKIOptions{}), test.GenPKI(t, test.PKIOptions{}) + //nolint:gocritic + roots := append(pki1.RootCertPEM, pki2.RootCertPEM...) + ta, err := FromStatic(roots) + require.NoError(t, err) + taPEM, err := ta.CurrentTrustAnchors(context.Background()) + require.NoError(t, err) + assert.Equal(t, roots, taPEM) + }) +} + +func TestStatic_GetX509BundleForTrustDomain(t *testing.T) { + t.Run("Should return full PEM regardless given trust domain", func(t *testing.T) { + pki := test.GenPKI(t, test.PKIOptions{}) + //nolint:gocritic + root := append(pki.RootCertPEM, []byte("garbage data")...) + ta, err := FromStatic(root) + require.NoError(t, err) + s, ok := ta.(*static) + require.True(t, ok) + + trustDomain1, err := spiffeid.TrustDomainFromString("example.com") + require.NoError(t, err) + bundle, err := s.GetX509BundleForTrustDomain(trustDomain1) + require.NoError(t, err) + assert.Equal(t, s.bundle, bundle) + b1, err := bundle.Marshal() + require.NoError(t, err) + assert.Equal(t, pki.RootCertPEM, b1) + + trustDomain2, err := spiffeid.TrustDomainFromString("another-example.org") + require.NoError(t, err) + bundle, err = s.GetX509BundleForTrustDomain(trustDomain2) + require.NoError(t, err) + assert.Equal(t, s.bundle, bundle) + b2, err := bundle.Marshal() + require.NoError(t, err) + assert.Equal(t, pki.RootCertPEM, b2) + }) +} + +func TestStatic_Run(t *testing.T) { + t.Run("Run multiple times should return error", func(t *testing.T) { + pki := test.GenPKI(t, test.PKIOptions{}) + ta, err := FromStatic(pki.RootCertPEM) + require.NoError(t, err) + s, ok := ta.(*static) + require.True(t, ok) + + ctx, cancel := context.WithCancel(context.Background()) + errCh := make(chan error) + go func() { + errCh <- s.Run(ctx) + }() + go func() { + errCh <- s.Run(ctx) + }() + + select { + case err := <-errCh: + require.Error(t, err) + case <-time.After(time.Second): + assert.Fail(t, "Expected error") + } + + select { + case <-s.closeCh: + assert.Fail(t, "closeCh should not be closed") + default: + } + + cancel() + + select { + case err := <-errCh: + require.NoError(t, err) + case <-time.After(time.Second): + assert.Fail(t, "First Run should have returned and returned no error ") + } + }) +} + +func TestStatic_Watch(t *testing.T) { + t.Run("should return when context is cancelled", func(t *testing.T) { + pki := test.GenPKI(t, test.PKIOptions{}) + ta, err := FromStatic(pki.RootCertPEM) + require.NoError(t, err) + + ctx, cancel := context.WithCancel(context.Background()) + doneCh := make(chan struct{}) + + go func() { + ta.Watch(ctx, nil) + close(doneCh) + }() + + cancel() + + select { + case <-doneCh: + case <-time.After(time.Second): + assert.Fail(t, "Expected doneCh to be closed") + } + }) + + t.Run("should return when cancel is closed via closed Run", func(t *testing.T) { + pki := test.GenPKI(t, test.PKIOptions{}) + ta, err := FromStatic(pki.RootCertPEM) + require.NoError(t, err) + + ctx, cancel := context.WithCancel(context.Background()) + doneCh := make(chan struct{}) + errCh := make(chan error) + + go func() { + errCh <- ta.Run(ctx) + }() + + go func() { + ta.Watch(context.Background(), nil) + close(doneCh) + }() + + cancel() + + select { + case <-doneCh: + case <-time.After(time.Second): + assert.Fail(t, "Expected doneCh to be closed") + } + + select { + case err := <-errCh: + require.NoError(t, err) + case <-time.After(time.Second): + assert.Fail(t, "Expected Run to return no error") + } + }) +} diff --git a/crypto/spiffe/trustanchors/trustanchors.go b/crypto/spiffe/trustanchors/trustanchors.go new file mode 100644 index 0000000..a75fe83 --- /dev/null +++ b/crypto/spiffe/trustanchors/trustanchors.go @@ -0,0 +1,39 @@ +/* +Copyright 2024 The Dapr Authors +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package trustanchors + +import ( + "context" + + "github.com/spiffe/go-spiffe/v2/bundle/x509bundle" +) + +// Interface exposes a SPIFFE trust anchor from a source. +// Allows consumers to get the current trust anchor bundle, and subscribe to +// bundle updates. +type Interface interface { + // Source implements the SPIFFE trust anchor bundle source. + x509bundle.Source + + // CurrentTrustAnchors returns the current trust anchor PEM bundle. + CurrentTrustAnchors(ctx context.Context) ([]byte, error) + + // Watch watches for changes to the trust domains and returns the PEM encoded + // trust domain roots. + // Returns when the given context is canceled. + Watch(ctx context.Context, ch chan<- []byte) + + // Run starts the trust anchor source. + Run(ctx context.Context) error +} diff --git a/crypto/test/pki.go b/crypto/test/pki.go new file mode 100644 index 0000000..1b2e52c --- /dev/null +++ b/crypto/test/pki.go @@ -0,0 +1,239 @@ +/* +Copyright 2024 The Dapr Authors +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implieh. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package test + +import ( + "context" + "crypto" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/x509" + "crypto/x509/pkix" + "encoding/pem" + "math/big" + "net" + "net/url" + "testing" + "time" + + "github.com/spiffe/go-spiffe/v2/bundle/x509bundle" + "github.com/spiffe/go-spiffe/v2/spiffegrpc/grpccredentials" + "github.com/spiffe/go-spiffe/v2/spiffeid" + "github.com/spiffe/go-spiffe/v2/spiffetls/tlsconfig" + "github.com/spiffe/go-spiffe/v2/svid/x509svid" + "github.com/stretchr/testify/require" + "google.golang.org/grpc" + "google.golang.org/grpc/examples/helloworld/helloworld" + "google.golang.org/grpc/peer" +) + +type PKIOptions struct { + LeafDNS string + LeafID spiffeid.ID + ClientDNS string + ClientID spiffeid.ID +} + +type PKI struct { + RootCertPEM []byte + RootCert *x509.Certificate + LeafCert *x509.Certificate + LeafCertPEM []byte + LeafPKPEM []byte + LeafPK crypto.Signer + ClientCertPEM []byte + ClientCert *x509.Certificate + ClientPKPEM []byte + ClientPK crypto.Signer + + leafID spiffeid.ID + clientID spiffeid.ID +} + +func GenPKI(t *testing.T, opts PKIOptions) PKI { + t.Helper() + pki, err := GenPKIError(opts) + require.NoError(t, err) + return pki +} + +func GenPKIError(opts PKIOptions) (PKI, error) { + rootPK, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + return PKI{}, err + } + + rootCert := &x509.Certificate{ + SerialNumber: big.NewInt(1), + Subject: pkix.Name{CommonName: "Dapr Test Root CA"}, + NotBefore: time.Now(), + NotAfter: time.Now().Add(time.Hour), + IsCA: true, + KeyUsage: x509.KeyUsageCertSign, + BasicConstraintsValid: true, + } + rootCertBytes, err := x509.CreateCertificate(rand.Reader, rootCert, rootCert, &rootPK.PublicKey, rootPK) + if err != nil { + return PKI{}, err + } + + rootCert, err = x509.ParseCertificate(rootCertBytes) + if err != nil { + return PKI{}, err + } + + rootCertPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: rootCertBytes}) + + leafCertPEM, leafPKPEM, leafCert, leafPK, err := genLeafCert(rootPK, rootCert, opts.LeafID, opts.LeafDNS) + if err != nil { + return PKI{}, err + } + clientCertPEM, clientPKPEM, clientCert, clientPK, err := genLeafCert(rootPK, rootCert, opts.ClientID, opts.ClientDNS) + if err != nil { + return PKI{}, err + } + + return PKI{ + RootCert: rootCert, + RootCertPEM: rootCertPEM, + LeafCertPEM: leafCertPEM, + LeafPKPEM: leafPKPEM, + LeafCert: leafCert, + LeafPK: leafPK, + ClientCertPEM: clientCertPEM, + ClientPKPEM: clientPKPEM, + ClientCert: clientCert, + ClientPK: clientPK, + leafID: opts.LeafID, + clientID: opts.ClientID, + }, nil +} + +func (p PKI) ClientGRPCCtx(t *testing.T) context.Context { + t.Helper() + + bundle := x509bundle.New(spiffeid.RequireTrustDomainFromString("example.org")) + bundle.AddX509Authority(p.RootCert) + serverSVID := &mockSVID{ + bundle: bundle, + svid: &x509svid.SVID{ + ID: p.leafID, + Certificates: []*x509.Certificate{p.LeafCert}, + PrivateKey: p.LeafPK, + }, + } + + clientSVID := &mockSVID{ + bundle: bundle, + svid: &x509svid.SVID{ + ID: p.clientID, + Certificates: []*x509.Certificate{p.ClientCert}, + PrivateKey: p.ClientPK, + }, + } + + server := grpc.NewServer(grpc.Creds(grpccredentials.MTLSServerCredentials(serverSVID, serverSVID, tlsconfig.AuthorizeAny()))) + gs := new(greeterServer) + helloworld.RegisterGreeterServer(server, gs) + + lis, err := net.Listen("tcp", "localhost:0") + require.NoError(t, err) + + go func() { + server.Serve(lis) + }() + conn, err := grpc.DialContext(context.Background(), lis.Addr().String(), + grpc.WithTransportCredentials(grpccredentials.MTLSClientCredentials(clientSVID, clientSVID, tlsconfig.AuthorizeAny())), + ) + require.NoError(t, err) + + _, err = helloworld.NewGreeterClient(conn).SayHello(context.Background(), new(helloworld.HelloRequest)) + require.NoError(t, err) + + lis.Close() + server.Stop() + + return gs.ctx +} + +func genLeafCert(rootPK *ecdsa.PrivateKey, rootCert *x509.Certificate, id spiffeid.ID, dns string) ([]byte, []byte, *x509.Certificate, crypto.Signer, error) { + pk, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + return nil, nil, nil, nil, err + } + + pkBytes, err := x509.MarshalPKCS8PrivateKey(pk) + if err != nil { + return nil, nil, nil, nil, err + } + + cert := &x509.Certificate{ + SerialNumber: big.NewInt(1), + NotBefore: time.Now(), + NotAfter: time.Now().Add(time.Hour), + KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment, + ExtKeyUsage: []x509.ExtKeyUsage{ + x509.ExtKeyUsageServerAuth, + x509.ExtKeyUsageClientAuth, + }, + } + + if len(dns) > 0 { + cert.DNSNames = []string{dns} + } + + if !id.IsZero() { + cert.URIs = []*url.URL{id.URL()} + } + + certBytes, err := x509.CreateCertificate(rand.Reader, cert, rootCert, &pk.PublicKey, rootPK) + if err != nil { + return nil, nil, nil, nil, err + } + + cert, err = x509.ParseCertificate(certBytes) + if err != nil { + return nil, nil, nil, nil, err + } + + pkPEM := pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: pkBytes}) + certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certBytes}) + + return certPEM, pkPEM, cert, pk, nil +} + +type mockSVID struct { + svid *x509svid.SVID + bundle *x509bundle.Bundle +} + +func (m *mockSVID) GetX509BundleForTrustDomain(_ spiffeid.TrustDomain) (*x509bundle.Bundle, error) { + return m.bundle, nil +} + +func (m *mockSVID) GetX509SVID() (*x509svid.SVID, error) { + return m.svid, nil +} + +type greeterServer struct { + helloworld.UnimplementedGreeterServer + ctx context.Context +} + +func (s *greeterServer) SayHello(ctx context.Context, _ *helloworld.HelloRequest) (*helloworld.HelloReply, error) { + p, _ := peer.FromContext(ctx) + s.ctx = peer.NewContext(context.Background(), p) + return new(helloworld.HelloReply), nil +} diff --git a/go.mod b/go.mod index 2a7cbe7..4f87f71 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/dapr/kit -go 1.20 +go 1.21 require ( github.com/alphadose/haxmap v1.3.1 @@ -11,14 +11,16 @@ require ( github.com/mitchellh/mapstructure v1.5.1-0.20220423185008-bf980b35cac4 github.com/sirupsen/logrus v1.9.3 github.com/spf13/cast v1.5.1 + github.com/spiffe/go-spiffe/v2 v2.1.7 github.com/stretchr/testify v1.8.4 github.com/tidwall/transform v0.0.0-20201103190739-32f242e2dbde golang.org/x/crypto v0.19.0 golang.org/x/exp v0.0.0-20231006140011-7918f672742d golang.org/x/tools v0.14.0 - google.golang.org/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d - google.golang.org/grpc v1.57.0 - google.golang.org/protobuf v1.31.0 + google.golang.org/genproto/googleapis/rpc v0.0.0-20231016165738-49dd2c1f3d0b + google.golang.org/grpc v1.60.1 + google.golang.org/grpc/examples v0.0.0-20230224211313-3775f633ce20 + google.golang.org/protobuf v1.32.0 k8s.io/apimachinery v0.26.9 k8s.io/utils v0.0.0-20230726121419-3b25d923346b ) @@ -35,8 +37,11 @@ require ( github.com/lestrrat-go/option v1.0.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/segmentio/asm v1.2.0 // indirect + github.com/zeebo/errs v1.3.0 // indirect golang.org/x/mod v0.13.0 // indirect + golang.org/x/net v0.17.0 // indirect golang.org/x/sys v0.17.0 // indirect + golang.org/x/text v0.14.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index ead8f3d..032c15a 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow= +github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= github.com/alphadose/haxmap v1.3.1 h1:KmZh75duO1tC8pt3LmUwoTYiZ9sh4K52FX8p7/yrlqU= github.com/alphadose/haxmap v1.3.1/go.mod h1:rjHw1IAqbxm0S3U5tD16GoKsiAd8FWx5BJ2IYqXwgmM= github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM= @@ -5,12 +7,14 @@ github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyY github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/decred/dcrd/crypto/blake256 v1.0.1/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 h1:8UrgZ3GkP4i/CLijOJx79Yu+etlyjdBU4sfcs2WYQMs= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0= github.com/frankban/quicktest v1.14.4 h1:g2rn0vABPOOXmZUj+vbmUp0lPoXEMuhTpIluN0XL9UY= +github.com/frankban/quicktest v1.14.4/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= +github.com/go-jose/go-jose/v3 v3.0.1 h1:pWmKFVtt+Jl0vBZTIpz/eAKwsm6LkIxDVVbFHKkchhA= +github.com/go-jose/go-jose/v3 v3.0.1/go.mod h1:RNkWWRld676jZEYoV3+XK8L2ZnNSvIsxFMht0mSX+u8= github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= @@ -20,11 +24,15 @@ github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/gofuzz v1.1.0 h1:Hsa8mG0dQ46ij8Sl2AYJDUv1oA9/d6Vk+3LG99Oe02g= +github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/lestrrat-go/blackmagic v1.0.2 h1:Cg2gVSc9h7sz9NOByczrbUvLopQmXrfFx//N+AkAr5k= github.com/lestrrat-go/blackmagic v1.0.2/go.mod h1:UrEqBzIR2U6CnzVyUtfM6oZNMt/7O7Vohk2J0OGSAtU= github.com/lestrrat-go/httpcc v1.0.1 h1:ydWCStUeJLkpYyjLDHihupbn2tYmZ7m22BGkcvZZrIE= @@ -33,19 +41,18 @@ github.com/lestrrat-go/httprc v1.0.4 h1:bAZymwoZQb+Oq8MEbyipag7iSq6YIga8Wj6GOiJG github.com/lestrrat-go/httprc v1.0.4/go.mod h1:mwwz3JMTPBjHUkkDv/IGJ39aALInZLrhBp0X7KGUZlo= github.com/lestrrat-go/iter v1.0.2 h1:gMXo1q4c2pHmC3dn8LzRhJfP1ceCbgSiT9lUydIzltI= github.com/lestrrat-go/iter v1.0.2/go.mod h1:Momfcq3AnRlRjI5b5O8/G5/BvpzrhoFTZcn06fEOPt4= -github.com/lestrrat-go/jwx/v2 v2.0.15 h1:XvR2lQdX+mZechmqWxqQb2foU3hgAn5+Rj0ICa0I6sU= -github.com/lestrrat-go/jwx/v2 v2.0.15/go.mod h1:jBHyESp4e7QxfERM0UKkQ80/94paqNIEcdEfiUYz5zE= github.com/lestrrat-go/jwx/v2 v2.0.20 h1:sAgXuWS/t8ykxS9Bi2Qtn5Qhpakw1wrcjxChudjolCc= github.com/lestrrat-go/jwx/v2 v2.0.20/go.mod h1:UlCSmKqw+agm5BsOBfEAbTvKsEApaGNqHAEUTv5PJC4= -github.com/lestrrat-go/option v1.0.0/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= github.com/lestrrat-go/option v1.0.1 h1:oAzP2fvZGQKWkvHa1/SAcFolBEca1oN+mQ7eooNBEYU= github.com/lestrrat-go/option v1.0.1/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= github.com/mitchellh/mapstructure v1.5.1-0.20220423185008-bf980b35cac4 h1:BpfhmLKZf+SjVanKKhCgf3bg+511DmU9eDQTen7LLbY= github.com/mitchellh/mapstructure v1.5.1-0.20220423185008-bf980b35cac4/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys= github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= @@ -53,100 +60,76 @@ github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVs github.com/spf13/cast v1.5.1 h1:R+kOtfhWQE6TVQzY+4D7wJLBgkdVasCEFxSUBYBYIlA= github.com/spf13/cast v1.5.1/go.mod h1:b9PdjNptOpzXr7Rq1q9gJML/2cdGQAo69NKzQ10KN48= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spiffe/go-spiffe/v2 v2.1.7 h1:VUkM1yIyg/x8X7u1uXqSRVRCdMdfRIEdFBzpqoeASGk= +github.com/spiffe/go-spiffe/v2 v2.1.7/go.mod h1:QJDGdhXllxjxvd5B+2XnhhXB/+rC8gr+lNrtOryiWeE= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/tidwall/transform v0.0.0-20201103190739-32f242e2dbde h1:AMNpJRc7P+GTwVbl8DkK2I9I8BBUzNiHuH/tlxrpan0= github.com/tidwall/transform v0.0.0-20201103190739-32f242e2dbde/go.mod h1:MvrEmduDUz4ST5pGZ7CABCnOU5f3ZiOAZzT6b1A6nX8= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +github.com/zeebo/errs v1.3.0 h1:hmiaKqgYZzcVgRL1Vkc1Mn2914BbzB0IBxs+ebeutGs= +github.com/zeebo/errs v1.3.0/go.mod h1:sgbWHsvVuTPHcqJJGQ1WhI5KbWlHYz+2+2C/LSEtCw4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc= -golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo= golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI= golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.13.0 h1:I/DsJXRlw/8l/0c24sM9yb0T4z9liZTduXvdAWYiysY= golang.org/x/mod v0.13.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= -golang.org/x/net v0.16.0 h1:7eBu7KsSvFDtSXUIDbh3aqlK4DPsZ1rByC8PFfBThos= +golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= +golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.4.0 h1:zxkM55ReGkDlKSM+Fu41A+zmbZuaPVbGMzvvdUPznYQ= +golang.org/x/sync v0.4.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= -golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= -golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= -golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= -golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.14.0 h1:jvNa2pY0M4r62jkRQ6RwEZZyPcymeL9XZMLBbV7U2nc= golang.org/x/tools v0.14.0/go.mod h1:uYBEerGOWcJyEORxN+Ek8+TT266gXkNlHdJBwexUsBg= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d h1:uvYuEyMHKNt+lT4K3bN6fGswmK8qSvcreM3BwjDh+y4= -google.golang.org/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d/go.mod h1:+Bk1OCOj40wS2hwAMA+aCW9ypzm63QTBBHp6lQ3p+9M= -google.golang.org/grpc v1.57.0 h1:kfzNeI/klCGD2YPMUlaGNT3pxvYfga7smW3Vth8Zsiw= -google.golang.org/grpc v1.57.0/go.mod h1:Sd+9RMTACXwmub0zcNY2c4arhtrbBYD1AUHI/dt16Mo= +google.golang.org/genproto/googleapis/rpc v0.0.0-20231016165738-49dd2c1f3d0b h1:ZlWIi1wSK56/8hn4QcBp/j9M7Gt3U/3hZw3mC7vDICo= +google.golang.org/genproto/googleapis/rpc v0.0.0-20231016165738-49dd2c1f3d0b/go.mod h1:swOH3j0KzcDDgGUWr+SNpyTen5YrXjS3eyPzFYKc6lc= +google.golang.org/grpc v1.60.1 h1:26+wFr+cNqSGFcOXcabYC0lUVJVRa2Sb2ortSK7VrEU= +google.golang.org/grpc v1.60.1/go.mod h1:OlCHIeLYqSSsLi6i49B5QGdzaMZK9+M7LXN2FKz4eGM= +google.golang.org/grpc/examples v0.0.0-20230224211313-3775f633ce20 h1:MLBCGN1O7GzIx+cBiwfYPwtmZ41U3Mn/cotLJciaArI= +google.golang.org/grpc/examples v0.0.0-20230224211313-3775f633ce20/go.mod h1:Nr5H8+MlGWr5+xX/STzdoEqJrO+YteqFbMyCsrb6mH0= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= -google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I= +google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=