From 4de327dc8886bba17f8207499b0727d3a475dac2 Mon Sep 17 00:00:00 2001 From: joshvanl Date: Fri, 20 Mar 2026 17:01:22 +0000 Subject: [PATCH 1/4] Add SPIFFE-based signer Introduce crypto/spiffe/signer, which provides cryptographic signing and verification using the workload's X.509 SVID identity and trust bundles. Supports Ed25519, ECDSA, and RSA key types. Signed-off-by: joshvanl --- crypto/spiffe/signer/signer.go | 206 ++++++++++ crypto/spiffe/signer/signer_test.go | 452 ++++++++++++++++++++++ crypto/spiffe/trustanchors/fake/fake.go | 51 +++ crypto/spiffe/trustanchors/multi/multi.go | 3 +- 4 files changed, 710 insertions(+), 2 deletions(-) create mode 100644 crypto/spiffe/signer/signer.go create mode 100644 crypto/spiffe/signer/signer_test.go create mode 100644 crypto/spiffe/trustanchors/fake/fake.go diff --git a/crypto/spiffe/signer/signer.go b/crypto/spiffe/signer/signer.go new file mode 100644 index 0000000..c44439f --- /dev/null +++ b/crypto/spiffe/signer/signer.go @@ -0,0 +1,206 @@ +/* +Copyright 2026 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 signer + +import ( + "crypto" + "crypto/ecdsa" + "crypto/ed25519" + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "errors" + "fmt" + "math/big" + "time" + + "github.com/spiffe/go-spiffe/v2/svid/x509svid" + + "github.com/dapr/kit/crypto/spiffe/trustanchors" +) + +// Signer provides cryptographic signing and verification using the workload's +// X.509 identity and trust bundles. Callers use it for raw digest signing and +// certificate chain verification without needing direct access to SVID sources +// or trust anchors. +type Signer struct { + svidSource x509svid.Source + trustAnchors trustanchors.Interface +} + +// New creates a Signer from an SVID source and trust anchors. +// The svidSource may be nil for verify-only usage (Sign will return an error). +func New(svidSource x509svid.Source, trustAnchors trustanchors.Interface) *Signer { + return &Signer{ + svidSource: svidSource, + trustAnchors: trustAnchors, + } +} + +// Sign signs the given digest using the current SVID's private key. +// Returns the signature bytes and the DER-encoded certificate chain (leaf + +// intermediates concatenated). +func (s *Signer) Sign(digest []byte) ([]byte, []byte, error) { + if s.svidSource == nil { + return nil, nil, errors.New("signing not available: no SVID source configured") + } + svid, err := s.svidSource.GetX509SVID() + if err != nil { + return nil, nil, fmt.Errorf("failed to get X.509 SVID: %w", err) + } + + if len(svid.Certificates) == 0 { + return nil, nil, errors.New("SVID has no certificates") + } + + var certChainDER []byte + for _, cert := range svid.Certificates { + certChainDER = append(certChainDER, cert.Raw...) + } + + sig, err := signWithKey(svid.PrivateKey, digest) + if err != nil { + return nil, nil, fmt.Errorf("failed to sign: %w", err) + } + + return sig, certChainDER, nil +} + +// Verify verifies a cryptographic signature against the given digest using the +// public key from the provided DER-encoded certificate chain. +func (s *Signer) Verify(digest, sig, certChainDER []byte) error { + leaf, err := parseLeafCert(certChainDER) + if err != nil { + return err + } + return verifyWithKey(leaf.PublicKey, digest, sig) +} + +// VerifyCertChainOfTrust verifies that the given DER-encoded certificate chain +// is trusted by the current trust anchors. The trust domain is extracted from +// the leaf certificate's SPIFFE ID (URI SAN). +func (s *Signer) VerifyCertChainOfTrust(certChainDER []byte) error { + certs, err := x509.ParseCertificates(certChainDER) + if err != nil { + return fmt.Errorf("failed to parse certificate chain: %w", err) + } + if len(certs) == 0 { + return errors.New("certificate chain is empty") + } + + leaf := certs[0] + + spiffeID, err := x509svid.IDFromCert(leaf) + if err != nil { + return fmt.Errorf("failed to extract SPIFFE ID from certificate: %w", err) + } + + bundle, err := s.trustAnchors.GetX509BundleForTrustDomain(spiffeID.TrustDomain()) + if err != nil { + return fmt.Errorf("failed to get trust bundle for trust domain %q: %w", spiffeID.TrustDomain(), err) + } + + authorities := bundle.X509Authorities() + if len(authorities) == 0 { + return fmt.Errorf("trust bundle for trust domain %q has no X.509 authorities", spiffeID.TrustDomain()) + } + + roots := x509.NewCertPool() + for _, anchor := range authorities { + roots.AddCert(anchor) + } + + intermediates := x509.NewCertPool() + for _, c := range certs[1:] { + intermediates.AddCert(c) + } + + _, err = leaf.Verify(x509.VerifyOptions{ + Roots: roots, + Intermediates: intermediates, + KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageAny}, + // Use the leaf's NotAfter minus one minute as the verification time. + // This avoids failures from expired short-lived SVIDs and from + // backdated NotBefore (sentry backdates SVIDs for clock-skew + // tolerance, which can place NotBefore before the CA's NotBefore). + CurrentTime: leaf.NotAfter.Add(-time.Minute), + }) + if err != nil { + return fmt.Errorf("certificate chain-of-trust verification failed: %w", err) + } + + return nil +} + +// signWithKey signs the given digest with the private key. +func signWithKey(key crypto.Signer, digest []byte) ([]byte, error) { + switch k := key.(type) { + case ed25519.PrivateKey: + return ed25519.Sign(k, digest), nil + case *ecdsa.PrivateKey: + r, s, err := ecdsa.Sign(rand.Reader, k, digest) + if err != nil { + return nil, err + } + byteLen := (k.Curve.Params().BitSize + 7) / 8 + sig := make([]byte, 2*byteLen) + rBytes := r.Bytes() + sBytes := s.Bytes() + copy(sig[byteLen-len(rBytes):byteLen], rBytes) + copy(sig[2*byteLen-len(sBytes):], sBytes) + return sig, nil + case *rsa.PrivateKey: + return rsa.SignPKCS1v15(rand.Reader, k, crypto.SHA256, digest) + default: + return nil, fmt.Errorf("unsupported key type: %T", key) + } +} + +// verifyWithKey verifies a signature against the given digest and public key. +func verifyWithKey(pubKey crypto.PublicKey, digest, sig []byte) error { + switch k := pubKey.(type) { + case ed25519.PublicKey: + if !ed25519.Verify(k, digest, sig) { + return errors.New("ed25519 signature verification failed") + } + return nil + case *ecdsa.PublicKey: + byteLen := (k.Curve.Params().BitSize + 7) / 8 + if len(sig) != 2*byteLen { + return fmt.Errorf("invalid ECDSA signature length: got %d, want %d", len(sig), 2*byteLen) + } + r := new(big.Int).SetBytes(sig[:byteLen]) + s := new(big.Int).SetBytes(sig[byteLen:]) + if !ecdsa.Verify(k, digest, r, s) { + return errors.New("ecdsa signature verification failed") + } + return nil + case *rsa.PublicKey: + return rsa.VerifyPKCS1v15(k, crypto.SHA256, digest, sig) + default: + return fmt.Errorf("unsupported public key type: %T", pubKey) + } +} + +// parseLeafCert parses a DER-encoded certificate chain and returns the leaf. +func parseLeafCert(chainDER []byte) (*x509.Certificate, error) { + certs, err := x509.ParseCertificates(chainDER) + if err != nil { + return nil, fmt.Errorf("failed to parse certificate chain: %w", err) + } + if len(certs) == 0 { + return nil, errors.New("certificate chain is empty") + } + return certs[0], nil +} diff --git a/crypto/spiffe/signer/signer_test.go b/crypto/spiffe/signer/signer_test.go new file mode 100644 index 0000000..1d5b926 --- /dev/null +++ b/crypto/spiffe/signer/signer_test.go @@ -0,0 +1,452 @@ +/* +Copyright 2026 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 signer + +import ( + "crypto" + "crypto/ecdsa" + "crypto/ed25519" + "crypto/elliptic" + "crypto/rand" + "crypto/rsa" + "crypto/sha256" + "crypto/x509" + "crypto/x509/pkix" + "errors" + "math/big" + "net/url" + "testing" + "time" + + "github.com/spiffe/go-spiffe/v2/svid/x509svid" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/dapr/kit/crypto/spiffe/trustanchors/fake" +) + +type staticSVIDSource struct { + svid *x509svid.SVID + err error +} + +func (s *staticSVIDSource) GetX509SVID() (*x509svid.SVID, error) { + return s.svid, s.err +} + +func generateEd25519Cert(t *testing.T) ([]byte, *x509.Certificate, ed25519.PrivateKey) { + t.Helper() + pub, priv, err := ed25519.GenerateKey(rand.Reader) + require.NoError(t, err) + + template := &x509.Certificate{ + SerialNumber: big.NewInt(1), + Subject: pkix.Name{CommonName: "test-ed25519"}, + NotBefore: time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC), + NotAfter: time.Date(2027, 1, 1, 0, 0, 0, 0, time.UTC), + URIs: []*url.URL{{Scheme: "spiffe", Host: "example.org", Path: "/ns/default/app-a"}}, + } + + certDER, err := x509.CreateCertificate(rand.Reader, template, template, pub, priv) + require.NoError(t, err) + cert, err := x509.ParseCertificate(certDER) + require.NoError(t, err) + return certDER, cert, priv +} + +func generateECDSACert(t *testing.T) ([]byte, *x509.Certificate, *ecdsa.PrivateKey) { + t.Helper() + priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + require.NoError(t, err) + + template := &x509.Certificate{ + SerialNumber: big.NewInt(1), + Subject: pkix.Name{CommonName: "test-ecdsa"}, + NotBefore: time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC), + NotAfter: time.Date(2027, 1, 1, 0, 0, 0, 0, time.UTC), + URIs: []*url.URL{{Scheme: "spiffe", Host: "example.org", Path: "/ns/default/app-b"}}, + } + + certDER, err := x509.CreateCertificate(rand.Reader, template, template, &priv.PublicKey, priv) + require.NoError(t, err) + cert, err := x509.ParseCertificate(certDER) + require.NoError(t, err) + return certDER, cert, priv +} + +func generateRSACert(t *testing.T) ([]byte, *x509.Certificate, *rsa.PrivateKey) { + t.Helper() + priv, err := rsa.GenerateKey(rand.Reader, 2048) + require.NoError(t, err) + + template := &x509.Certificate{ + SerialNumber: big.NewInt(1), + Subject: pkix.Name{CommonName: "test-rsa"}, + NotBefore: time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC), + NotAfter: time.Date(2027, 1, 1, 0, 0, 0, 0, time.UTC), + URIs: []*url.URL{{Scheme: "spiffe", Host: "example.org", Path: "/ns/default/app-c"}}, + } + + certDER, err := x509.CreateCertificate(rand.Reader, template, template, &priv.PublicKey, priv) + require.NoError(t, err) + cert, err := x509.ParseCertificate(certDER) + require.NoError(t, err) + return certDER, cert, priv +} + +func generateCA(t *testing.T) ([]byte, *x509.Certificate, ed25519.PrivateKey) { + t.Helper() + pub, priv, err := ed25519.GenerateKey(rand.Reader) + require.NoError(t, err) + + template := &x509.Certificate{ + SerialNumber: big.NewInt(1), + Subject: pkix.Name{CommonName: "Test CA"}, + NotBefore: time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC), + NotAfter: time.Date(2027, 1, 1, 0, 0, 0, 0, time.UTC), + IsCA: true, + BasicConstraintsValid: true, + KeyUsage: x509.KeyUsageCertSign, + } + + caDER, err := x509.CreateCertificate(rand.Reader, template, template, pub, priv) + require.NoError(t, err) + ca, err := x509.ParseCertificate(caDER) + require.NoError(t, err) + return caDER, ca, priv +} + +func generateLeafSignedByCA(t *testing.T, ca *x509.Certificate, caKey ed25519.PrivateKey) ([]byte, *x509.Certificate, ed25519.PrivateKey) { + t.Helper() + pub, priv, err := ed25519.GenerateKey(rand.Reader) + require.NoError(t, err) + + template := &x509.Certificate{ + SerialNumber: big.NewInt(2), + Subject: pkix.Name{CommonName: "test leaf"}, + NotBefore: time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC), + NotAfter: time.Date(2027, 1, 1, 0, 0, 0, 0, time.UTC), + URIs: []*url.URL{{Scheme: "spiffe", Host: "example.org", Path: "/ns/default/app-a"}}, + } + + leafDER, err := x509.CreateCertificate(rand.Reader, template, ca, pub, caKey) + require.NoError(t, err) + leaf, err := x509.ParseCertificate(leafDER) + require.NoError(t, err) + return leafDER, leaf, priv +} + +// testDigest returns a SHA256 digest of the input, which is the format used +// by the production SignatureInput function and required by RSA signing. +func testDigest(input string) []byte { + h := sha256.Sum256([]byte(input)) + return h[:] +} + +func newSVIDSource(cert *x509.Certificate, key crypto.Signer) *staticSVIDSource { + id, _ := x509svid.IDFromCert(cert) + return &staticSVIDSource{svid: &x509svid.SVID{ + ID: id, + Certificates: []*x509.Certificate{cert}, + PrivateKey: key, + }} +} + +func TestNew(t *testing.T) { + t.Parallel() + + t.Run("nil svidSource for verify-only", func(t *testing.T) { + t.Parallel() + s := New(nil, fake.New()) + require.NotNil(t, s) + }) + + t.Run("nil trustAnchors for sign-only", func(t *testing.T) { + t.Parallel() + certDER, cert, priv := generateEd25519Cert(t) + _ = certDER + s := New(newSVIDSource(cert, priv), nil) + require.NotNil(t, s) + }) + + t.Run("both present", func(t *testing.T) { + t.Parallel() + _, cert, priv := generateEd25519Cert(t) + s := New(newSVIDSource(cert, priv), fake.New(cert)) + require.NotNil(t, s) + }) +} + +func TestSign_NilSVIDSource(t *testing.T) { + t.Parallel() + s := New(nil, fake.New()) + _, _, err := s.Sign(testDigest("hello")) + require.Error(t, err) + assert.Contains(t, err.Error(), "no SVID source configured") +} + +func TestSign_SVIDSourceError(t *testing.T) { + t.Parallel() + source := &staticSVIDSource{err: errors.New("svid unavailable")} + s := New(source, nil) + _, _, err := s.Sign(testDigest("hello")) + require.Error(t, err) + assert.Contains(t, err.Error(), "svid unavailable") +} + +func TestSign_NoCertificates(t *testing.T) { + t.Parallel() + source := &staticSVIDSource{svid: &x509svid.SVID{ + Certificates: nil, + PrivateKey: ed25519.NewKeyFromSeed(make([]byte, 32)), + }} + s := New(source, nil) + _, _, err := s.Sign(testDigest("hello")) + require.Error(t, err) + assert.Contains(t, err.Error(), "no certificates") +} + +func TestSignAndVerify_Ed25519(t *testing.T) { + t.Parallel() + _, cert, priv := generateEd25519Cert(t) + s := New(newSVIDSource(cert, priv), nil) + + digest := testDigest("test digest") + sig, certChain, err := s.Sign(digest) + require.NoError(t, err) + require.NotEmpty(t, sig) + require.NotEmpty(t, certChain) + + err = s.Verify(digest, sig, certChain) + require.NoError(t, err) +} + +func TestSignAndVerify_ECDSA(t *testing.T) { + t.Parallel() + _, cert, priv := generateECDSACert(t) + s := New(newSVIDSource(cert, priv), nil) + + digest := testDigest("test digest") + sig, certChain, err := s.Sign(digest) + require.NoError(t, err) + + err = s.Verify(digest, sig, certChain) + require.NoError(t, err) +} + +func TestSignAndVerify_RSA(t *testing.T) { + t.Parallel() + _, cert, priv := generateRSACert(t) + s := New(newSVIDSource(cert, priv), nil) + + digest := testDigest("test digest") + sig, certChain, err := s.Sign(digest) + require.NoError(t, err) + + err = s.Verify(digest, sig, certChain) + require.NoError(t, err) +} + +func TestVerify_TamperedDigest(t *testing.T) { + t.Parallel() + _, cert, priv := generateEd25519Cert(t) + s := New(newSVIDSource(cert, priv), nil) + + sig, certChain, err := s.Sign(testDigest("original")) + require.NoError(t, err) + + err = s.Verify(testDigest("tampered"), sig, certChain) + require.Error(t, err) +} + +func TestVerify_TamperedSignature(t *testing.T) { + t.Parallel() + _, cert, priv := generateEd25519Cert(t) + s := New(newSVIDSource(cert, priv), nil) + + digest := testDigest("test") + sig, certChain, err := s.Sign(digest) + require.NoError(t, err) + + // Flip a byte in the signature. + sig[0] ^= 0xff + + err = s.Verify(digest, sig, certChain) + require.Error(t, err) +} + +func TestVerify_InvalidCertChain(t *testing.T) { + t.Parallel() + s := New(nil, fake.New()) + err := s.Verify(testDigest("digest"), []byte("sig"), []byte("not-a-cert")) + require.Error(t, err) + assert.Contains(t, err.Error(), "failed to parse") +} + +func TestVerify_EmptyCertChain(t *testing.T) { + t.Parallel() + s := New(nil, fake.New()) + // Valid DER but empty is not possible; just use nil. + err := s.Verify(testDigest("digest"), []byte("sig"), nil) + require.Error(t, err) +} + +func TestSign_ReturnsCertChainDER(t *testing.T) { + t.Parallel() + certDER, cert, priv := generateEd25519Cert(t) + s := New(newSVIDSource(cert, priv), nil) + + _, certChain, err := s.Sign(testDigest("digest")) + require.NoError(t, err) + assert.Equal(t, certDER, certChain) +} + +func TestVerifyCertChainOfTrust_SelfSigned(t *testing.T) { + t.Parallel() + certDER, cert, _ := generateEd25519Cert(t) + ta := fake.New(cert) + s := New(nil, ta) + + err := s.VerifyCertChainOfTrust(certDER) + require.NoError(t, err) +} + +func TestVerifyCertChainOfTrust_CAChain(t *testing.T) { + t.Parallel() + caDER, ca, caKey := generateCA(t) + leafDER, _, _ := generateLeafSignedByCA(t, ca, caKey) + + chainDER := append(leafDER, caDER...) + ta := fake.New(ca) + s := New(nil, ta) + + err := s.VerifyCertChainOfTrust(chainDER) + require.NoError(t, err) +} + +func TestVerifyCertChainOfTrust_IntermediateChain(t *testing.T) { + t.Parallel() + _, rootCA, rootKey := generateCA(t) + + // Create intermediate CA signed by root. + intermPub, intermPriv, err := ed25519.GenerateKey(rand.Reader) + require.NoError(t, err) + intermTemplate := &x509.Certificate{ + SerialNumber: big.NewInt(2), + Subject: pkix.Name{CommonName: "Intermediate CA"}, + NotBefore: time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC), + NotAfter: time.Date(2027, 1, 1, 0, 0, 0, 0, time.UTC), + IsCA: true, + BasicConstraintsValid: true, + KeyUsage: x509.KeyUsageCertSign, + } + intermDER, err := x509.CreateCertificate(rand.Reader, intermTemplate, rootCA, intermPub, rootKey) + require.NoError(t, err) + intermCA, err := x509.ParseCertificate(intermDER) + require.NoError(t, err) + + // Create leaf signed by intermediate. + leafDER, _, _ := generateLeafSignedByCA(t, intermCA, intermPriv) + + chainDER := append(leafDER, intermDER...) + ta := fake.New(rootCA) + s := New(nil, ta) + + err = s.VerifyCertChainOfTrust(chainDER) + require.NoError(t, err) +} + +func TestVerifyCertChainOfTrust_WrongTrustAnchor(t *testing.T) { + t.Parallel() + caDER, ca, caKey := generateCA(t) + leafDER, _, _ := generateLeafSignedByCA(t, ca, caKey) + chainDER := append(leafDER, caDER...) + + // Different CA as trust anchor. + _, wrongCA, _ := generateCA(t) + ta := fake.New(wrongCA) + s := New(nil, ta) + + err := s.VerifyCertChainOfTrust(chainDER) + require.Error(t, err) + assert.Contains(t, err.Error(), "chain-of-trust verification failed") +} + +func TestVerifyCertChainOfTrust_EmptyChain(t *testing.T) { + t.Parallel() + ta := fake.New() + s := New(nil, ta) + + err := s.VerifyCertChainOfTrust(nil) + require.Error(t, err) +} + +func TestVerifyCertChainOfTrust_InvalidDER(t *testing.T) { + t.Parallel() + ta := fake.New() + s := New(nil, ta) + + err := s.VerifyCertChainOfTrust([]byte("not-a-cert")) + require.Error(t, err) + assert.Contains(t, err.Error(), "failed to parse") +} + +func TestSignAndVerify_RoundTrip_AllKeyTypes(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + certGen func(t *testing.T) (*x509.Certificate, crypto.Signer) + }{ + { + name: "ed25519", + certGen: func(t *testing.T) (*x509.Certificate, crypto.Signer) { + _, cert, priv := generateEd25519Cert(t) + return cert, priv + }, + }, + { + name: "ecdsa", + certGen: func(t *testing.T) (*x509.Certificate, crypto.Signer) { + _, cert, priv := generateECDSACert(t) + return cert, priv + }, + }, + { + name: "rsa", + certGen: func(t *testing.T) (*x509.Certificate, crypto.Signer) { + _, cert, priv := generateRSACert(t) + return cert, priv + }, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + cert, priv := tc.certGen(t) + s := New(newSVIDSource(cert, priv), fake.New(cert)) + + digest := testDigest("round trip test for " + tc.name) + sig, certChain, err := s.Sign(digest) + require.NoError(t, err) + + err = s.Verify(digest, sig, certChain) + require.NoError(t, err) + + err = s.VerifyCertChainOfTrust(certChain) + require.NoError(t, err) + }) + } +} diff --git a/crypto/spiffe/trustanchors/fake/fake.go b/crypto/spiffe/trustanchors/fake/fake.go new file mode 100644 index 0000000..234a145 --- /dev/null +++ b/crypto/spiffe/trustanchors/fake/fake.go @@ -0,0 +1,51 @@ +/* +Copyright 2026 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 fake + +import ( + "context" + "crypto/x509" + + "github.com/spiffe/go-spiffe/v2/bundle/jwtbundle" + "github.com/spiffe/go-spiffe/v2/bundle/x509bundle" + "github.com/spiffe/go-spiffe/v2/spiffeid" + + "github.com/dapr/kit/crypto/spiffe/trustanchors" +) + +type Fake struct { + trustanchors.Interface + bundle *x509bundle.Bundle +} + +func New(authorities ...*x509.Certificate) *Fake { + td := spiffeid.TrustDomain{} + bundle := x509bundle.New(td) + for _, a := range authorities { + bundle.AddX509Authority(a) + } + return &Fake{bundle: bundle} +} + +func (f *Fake) GetX509BundleForTrustDomain(spiffeid.TrustDomain) (*x509bundle.Bundle, error) { + return f.bundle, nil +} + +func (f *Fake) GetJWTBundleForTrustDomain(spiffeid.TrustDomain) (*jwtbundle.Bundle, error) { + return nil, nil +} + +func (f *Fake) CurrentTrustAnchors(context.Context) ([]byte, error) { return nil, nil } +func (f *Fake) Watch(context.Context, chan<- []byte) {} +func (f *Fake) Run(context.Context) error { return nil } diff --git a/crypto/spiffe/trustanchors/multi/multi.go b/crypto/spiffe/trustanchors/multi/multi.go index 2eda697..e78b62b 100644 --- a/crypto/spiffe/trustanchors/multi/multi.go +++ b/crypto/spiffe/trustanchors/multi/multi.go @@ -82,5 +82,4 @@ func (m *multi) GetJWTBundleForTrustDomain(td spiffeid.TrustDomain) (*jwtbundle. return nil, ErrTrustDomainNotFound } -func (m *multi) Watch(context.Context, chan<- []byte) { -} +func (m *multi) Watch(context.Context, chan<- []byte) {} From 34ca2d21c6a546a51a74e485f6f0e407e8d78d8f Mon Sep 17 00:00:00 2001 From: joshvanl Date: Fri, 20 Mar 2026 17:26:01 +0000 Subject: [PATCH 2/4] Review comments Signed-off-by: joshvanl --- crypto/spiffe/signer/signer.go | 4 ++++ crypto/spiffe/signer/signer_test.go | 12 ++++++++---- crypto/spiffe/trustanchors/fake/fake.go | 3 --- 3 files changed, 12 insertions(+), 7 deletions(-) diff --git a/crypto/spiffe/signer/signer.go b/crypto/spiffe/signer/signer.go index c44439f..7727594 100644 --- a/crypto/spiffe/signer/signer.go +++ b/crypto/spiffe/signer/signer.go @@ -91,6 +91,10 @@ func (s *Signer) Verify(digest, sig, certChainDER []byte) error { // is trusted by the current trust anchors. The trust domain is extracted from // the leaf certificate's SPIFFE ID (URI SAN). func (s *Signer) VerifyCertChainOfTrust(certChainDER []byte) error { + if s.trustAnchors == nil { + return errors.New("chain-of-trust verification not available: no trust anchors configured") + } + certs, err := x509.ParseCertificates(certChainDER) if err != nil { return fmt.Errorf("failed to parse certificate chain: %w", err) diff --git a/crypto/spiffe/signer/signer_test.go b/crypto/spiffe/signer/signer_test.go index 1d5b926..7fa493e 100644 --- a/crypto/spiffe/signer/signer_test.go +++ b/crypto/spiffe/signer/signer_test.go @@ -26,6 +26,7 @@ import ( "errors" "math/big" "net/url" + "slices" "testing" "time" @@ -175,9 +176,12 @@ func TestNew(t *testing.T) { t.Run("nil trustAnchors for sign-only", func(t *testing.T) { t.Parallel() certDER, cert, priv := generateEd25519Cert(t) - _ = certDER s := New(newSVIDSource(cert, priv), nil) require.NotNil(t, s) + + err := s.VerifyCertChainOfTrust(certDER) + require.Error(t, err) + assert.Contains(t, err.Error(), "no trust anchors configured") }) t.Run("both present", func(t *testing.T) { @@ -327,7 +331,7 @@ func TestVerifyCertChainOfTrust_CAChain(t *testing.T) { caDER, ca, caKey := generateCA(t) leafDER, _, _ := generateLeafSignedByCA(t, ca, caKey) - chainDER := append(leafDER, caDER...) + chainDER := slices.Concat(leafDER, caDER) ta := fake.New(ca) s := New(nil, ta) @@ -359,7 +363,7 @@ func TestVerifyCertChainOfTrust_IntermediateChain(t *testing.T) { // Create leaf signed by intermediate. leafDER, _, _ := generateLeafSignedByCA(t, intermCA, intermPriv) - chainDER := append(leafDER, intermDER...) + chainDER := slices.Concat(leafDER, intermDER) ta := fake.New(rootCA) s := New(nil, ta) @@ -371,7 +375,7 @@ func TestVerifyCertChainOfTrust_WrongTrustAnchor(t *testing.T) { t.Parallel() caDER, ca, caKey := generateCA(t) leafDER, _, _ := generateLeafSignedByCA(t, ca, caKey) - chainDER := append(leafDER, caDER...) + chainDER := slices.Concat(leafDER, caDER) // Different CA as trust anchor. _, wrongCA, _ := generateCA(t) diff --git a/crypto/spiffe/trustanchors/fake/fake.go b/crypto/spiffe/trustanchors/fake/fake.go index 234a145..2302573 100644 --- a/crypto/spiffe/trustanchors/fake/fake.go +++ b/crypto/spiffe/trustanchors/fake/fake.go @@ -20,12 +20,9 @@ import ( "github.com/spiffe/go-spiffe/v2/bundle/jwtbundle" "github.com/spiffe/go-spiffe/v2/bundle/x509bundle" "github.com/spiffe/go-spiffe/v2/spiffeid" - - "github.com/dapr/kit/crypto/spiffe/trustanchors" ) type Fake struct { - trustanchors.Interface bundle *x509bundle.Bundle } From a9b313365c65f7aad9588481853071c82e63406b Mon Sep 17 00:00:00 2001 From: joshvanl Date: Mon, 23 Mar 2026 12:17:19 +0000 Subject: [PATCH 3/4] Rename Verify to VerifySignature to clarify it only checks the cryptographic signature and not the certificate chain of trust. VerifyCertChainOfTrust now takes a signingTime parameter instead of using leaf.NotAfter as the verification time. This allows callers to verify historical signatures using the timestamp of the event, correctly accepting certificates that were valid at signing time while rejecting expired certificates used for new signatures. Switch ECDSA signing from R|S (IEEE P1363) to ASN.1 DER encoding using SignASN1/VerifyASN1, which is the standard Go/X.509 format. Signed-off-by: joshvanl --- crypto/spiffe/signer/signer.go | 41 +++++++++-------------------- crypto/spiffe/signer/signer_test.go | 32 +++++++++++----------- 2 files changed, 28 insertions(+), 45 deletions(-) diff --git a/crypto/spiffe/signer/signer.go b/crypto/spiffe/signer/signer.go index 7727594..550dc1e 100644 --- a/crypto/spiffe/signer/signer.go +++ b/crypto/spiffe/signer/signer.go @@ -22,7 +22,6 @@ import ( "crypto/x509" "errors" "fmt" - "math/big" "time" "github.com/spiffe/go-spiffe/v2/svid/x509svid" @@ -77,9 +76,11 @@ func (s *Signer) Sign(digest []byte) ([]byte, []byte, error) { return sig, certChainDER, nil } -// Verify verifies a cryptographic signature against the given digest using the -// public key from the provided DER-encoded certificate chain. -func (s *Signer) Verify(digest, sig, certChainDER []byte) error { +// VerifySignature verifies a cryptographic signature against the given digest +// using the public key from the provided DER-encoded certificate chain. This +// only checks the cryptographic signature; use VerifyCertChainOfTrust to +// validate the certificate chain against trust anchors. +func (s *Signer) VerifySignature(digest, sig, certChainDER []byte) error { leaf, err := parseLeafCert(certChainDER) if err != nil { return err @@ -89,8 +90,10 @@ func (s *Signer) Verify(digest, sig, certChainDER []byte) error { // VerifyCertChainOfTrust verifies that the given DER-encoded certificate chain // is trusted by the current trust anchors. The trust domain is extracted from -// the leaf certificate's SPIFFE ID (URI SAN). -func (s *Signer) VerifyCertChainOfTrust(certChainDER []byte) error { +// the leaf certificate's SPIFFE ID (URI SAN). The signingTime is used as the +// verification time, allowing validation of certificates that were valid at the +// time of signing even if they have since expired. +func (s *Signer) VerifyCertChainOfTrust(certChainDER []byte, signingTime time.Time) error { if s.trustAnchors == nil { return errors.New("chain-of-trust verification not available: no trust anchors configured") } @@ -134,11 +137,7 @@ func (s *Signer) VerifyCertChainOfTrust(certChainDER []byte) error { Roots: roots, Intermediates: intermediates, KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageAny}, - // Use the leaf's NotAfter minus one minute as the verification time. - // This avoids failures from expired short-lived SVIDs and from - // backdated NotBefore (sentry backdates SVIDs for clock-skew - // tolerance, which can place NotBefore before the CA's NotBefore). - CurrentTime: leaf.NotAfter.Add(-time.Minute), + CurrentTime: signingTime, }) if err != nil { return fmt.Errorf("certificate chain-of-trust verification failed: %w", err) @@ -153,17 +152,7 @@ func signWithKey(key crypto.Signer, digest []byte) ([]byte, error) { case ed25519.PrivateKey: return ed25519.Sign(k, digest), nil case *ecdsa.PrivateKey: - r, s, err := ecdsa.Sign(rand.Reader, k, digest) - if err != nil { - return nil, err - } - byteLen := (k.Curve.Params().BitSize + 7) / 8 - sig := make([]byte, 2*byteLen) - rBytes := r.Bytes() - sBytes := s.Bytes() - copy(sig[byteLen-len(rBytes):byteLen], rBytes) - copy(sig[2*byteLen-len(sBytes):], sBytes) - return sig, nil + return ecdsa.SignASN1(rand.Reader, k, digest) case *rsa.PrivateKey: return rsa.SignPKCS1v15(rand.Reader, k, crypto.SHA256, digest) default: @@ -180,13 +169,7 @@ func verifyWithKey(pubKey crypto.PublicKey, digest, sig []byte) error { } return nil case *ecdsa.PublicKey: - byteLen := (k.Curve.Params().BitSize + 7) / 8 - if len(sig) != 2*byteLen { - return fmt.Errorf("invalid ECDSA signature length: got %d, want %d", len(sig), 2*byteLen) - } - r := new(big.Int).SetBytes(sig[:byteLen]) - s := new(big.Int).SetBytes(sig[byteLen:]) - if !ecdsa.Verify(k, digest, r, s) { + if !ecdsa.VerifyASN1(k, digest, sig) { return errors.New("ecdsa signature verification failed") } return nil diff --git a/crypto/spiffe/signer/signer_test.go b/crypto/spiffe/signer/signer_test.go index 7fa493e..b1ce7d7 100644 --- a/crypto/spiffe/signer/signer_test.go +++ b/crypto/spiffe/signer/signer_test.go @@ -179,7 +179,7 @@ func TestNew(t *testing.T) { s := New(newSVIDSource(cert, priv), nil) require.NotNil(t, s) - err := s.VerifyCertChainOfTrust(certDER) + err := s.VerifyCertChainOfTrust(certDER, time.Now()) require.Error(t, err) assert.Contains(t, err.Error(), "no trust anchors configured") }) @@ -232,7 +232,7 @@ func TestSignAndVerify_Ed25519(t *testing.T) { require.NotEmpty(t, sig) require.NotEmpty(t, certChain) - err = s.Verify(digest, sig, certChain) + err = s.VerifySignature(digest, sig, certChain) require.NoError(t, err) } @@ -245,7 +245,7 @@ func TestSignAndVerify_ECDSA(t *testing.T) { sig, certChain, err := s.Sign(digest) require.NoError(t, err) - err = s.Verify(digest, sig, certChain) + err = s.VerifySignature(digest, sig, certChain) require.NoError(t, err) } @@ -258,7 +258,7 @@ func TestSignAndVerify_RSA(t *testing.T) { sig, certChain, err := s.Sign(digest) require.NoError(t, err) - err = s.Verify(digest, sig, certChain) + err = s.VerifySignature(digest, sig, certChain) require.NoError(t, err) } @@ -270,7 +270,7 @@ func TestVerify_TamperedDigest(t *testing.T) { sig, certChain, err := s.Sign(testDigest("original")) require.NoError(t, err) - err = s.Verify(testDigest("tampered"), sig, certChain) + err = s.VerifySignature(testDigest("tampered"), sig, certChain) require.Error(t, err) } @@ -286,14 +286,14 @@ func TestVerify_TamperedSignature(t *testing.T) { // Flip a byte in the signature. sig[0] ^= 0xff - err = s.Verify(digest, sig, certChain) + err = s.VerifySignature(digest, sig, certChain) require.Error(t, err) } func TestVerify_InvalidCertChain(t *testing.T) { t.Parallel() s := New(nil, fake.New()) - err := s.Verify(testDigest("digest"), []byte("sig"), []byte("not-a-cert")) + err := s.VerifySignature(testDigest("digest"), []byte("sig"), []byte("not-a-cert")) require.Error(t, err) assert.Contains(t, err.Error(), "failed to parse") } @@ -302,7 +302,7 @@ func TestVerify_EmptyCertChain(t *testing.T) { t.Parallel() s := New(nil, fake.New()) // Valid DER but empty is not possible; just use nil. - err := s.Verify(testDigest("digest"), []byte("sig"), nil) + err := s.VerifySignature(testDigest("digest"), []byte("sig"), nil) require.Error(t, err) } @@ -322,7 +322,7 @@ func TestVerifyCertChainOfTrust_SelfSigned(t *testing.T) { ta := fake.New(cert) s := New(nil, ta) - err := s.VerifyCertChainOfTrust(certDER) + err := s.VerifyCertChainOfTrust(certDER, time.Now()) require.NoError(t, err) } @@ -335,7 +335,7 @@ func TestVerifyCertChainOfTrust_CAChain(t *testing.T) { ta := fake.New(ca) s := New(nil, ta) - err := s.VerifyCertChainOfTrust(chainDER) + err := s.VerifyCertChainOfTrust(chainDER, time.Now()) require.NoError(t, err) } @@ -367,7 +367,7 @@ func TestVerifyCertChainOfTrust_IntermediateChain(t *testing.T) { ta := fake.New(rootCA) s := New(nil, ta) - err = s.VerifyCertChainOfTrust(chainDER) + err = s.VerifyCertChainOfTrust(chainDER, time.Now()) require.NoError(t, err) } @@ -382,7 +382,7 @@ func TestVerifyCertChainOfTrust_WrongTrustAnchor(t *testing.T) { ta := fake.New(wrongCA) s := New(nil, ta) - err := s.VerifyCertChainOfTrust(chainDER) + err := s.VerifyCertChainOfTrust(chainDER, time.Now()) require.Error(t, err) assert.Contains(t, err.Error(), "chain-of-trust verification failed") } @@ -392,7 +392,7 @@ func TestVerifyCertChainOfTrust_EmptyChain(t *testing.T) { ta := fake.New() s := New(nil, ta) - err := s.VerifyCertChainOfTrust(nil) + err := s.VerifyCertChainOfTrust(nil, time.Now()) require.Error(t, err) } @@ -401,7 +401,7 @@ func TestVerifyCertChainOfTrust_InvalidDER(t *testing.T) { ta := fake.New() s := New(nil, ta) - err := s.VerifyCertChainOfTrust([]byte("not-a-cert")) + err := s.VerifyCertChainOfTrust([]byte("not-a-cert"), time.Now()) require.Error(t, err) assert.Contains(t, err.Error(), "failed to parse") } @@ -446,10 +446,10 @@ func TestSignAndVerify_RoundTrip_AllKeyTypes(t *testing.T) { sig, certChain, err := s.Sign(digest) require.NoError(t, err) - err = s.Verify(digest, sig, certChain) + err = s.VerifySignature(digest, sig, certChain) require.NoError(t, err) - err = s.VerifyCertChainOfTrust(certChain) + err = s.VerifyCertChainOfTrust(certChain, time.Now()) require.NoError(t, err) }) } From 4beecdb2b21be7ff3b7106f6a691b34991c8ca73 Mon Sep 17 00:00:00 2001 From: joshvanl Date: Mon, 23 Mar 2026 12:58:35 +0000 Subject: [PATCH 4/4] lint Signed-off-by: joshvanl --- crypto/spiffe/signer/signer.go | 7 +++++++ crypto/spiffe/signer/signer_test.go | 20 ++++++++++++++++++++ crypto/spiffe/trustanchors/fake/fake.go | 2 ++ 3 files changed, 29 insertions(+) diff --git a/crypto/spiffe/signer/signer.go b/crypto/spiffe/signer/signer.go index 550dc1e..03e88f3 100644 --- a/crypto/spiffe/signer/signer.go +++ b/crypto/spiffe/signer/signer.go @@ -54,6 +54,7 @@ func (s *Signer) Sign(digest []byte) ([]byte, []byte, error) { if s.svidSource == nil { return nil, nil, errors.New("signing not available: no SVID source configured") } + svid, err := s.svidSource.GetX509SVID() if err != nil { return nil, nil, fmt.Errorf("failed to get X.509 SVID: %w", err) @@ -85,6 +86,7 @@ func (s *Signer) VerifySignature(digest, sig, certChainDER []byte) error { if err != nil { return err } + return verifyWithKey(leaf.PublicKey, digest, sig) } @@ -102,6 +104,7 @@ func (s *Signer) VerifyCertChainOfTrust(certChainDER []byte, signingTime time.Ti if err != nil { return fmt.Errorf("failed to parse certificate chain: %w", err) } + if len(certs) == 0 { return errors.New("certificate chain is empty") } @@ -167,11 +170,13 @@ func verifyWithKey(pubKey crypto.PublicKey, digest, sig []byte) error { if !ed25519.Verify(k, digest, sig) { return errors.New("ed25519 signature verification failed") } + return nil case *ecdsa.PublicKey: if !ecdsa.VerifyASN1(k, digest, sig) { return errors.New("ecdsa signature verification failed") } + return nil case *rsa.PublicKey: return rsa.VerifyPKCS1v15(k, crypto.SHA256, digest, sig) @@ -186,8 +191,10 @@ func parseLeafCert(chainDER []byte) (*x509.Certificate, error) { if err != nil { return nil, fmt.Errorf("failed to parse certificate chain: %w", err) } + if len(certs) == 0 { return nil, errors.New("certificate chain is empty") } + return certs[0], nil } diff --git a/crypto/spiffe/signer/signer_test.go b/crypto/spiffe/signer/signer_test.go index b1ce7d7..c8bab35 100644 --- a/crypto/spiffe/signer/signer_test.go +++ b/crypto/spiffe/signer/signer_test.go @@ -48,6 +48,7 @@ func (s *staticSVIDSource) GetX509SVID() (*x509svid.SVID, error) { func generateEd25519Cert(t *testing.T) ([]byte, *x509.Certificate, ed25519.PrivateKey) { t.Helper() + pub, priv, err := ed25519.GenerateKey(rand.Reader) require.NoError(t, err) @@ -63,11 +64,13 @@ func generateEd25519Cert(t *testing.T) ([]byte, *x509.Certificate, ed25519.Priva require.NoError(t, err) cert, err := x509.ParseCertificate(certDER) require.NoError(t, err) + return certDER, cert, priv } func generateECDSACert(t *testing.T) ([]byte, *x509.Certificate, *ecdsa.PrivateKey) { t.Helper() + priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) require.NoError(t, err) @@ -83,11 +86,13 @@ func generateECDSACert(t *testing.T) ([]byte, *x509.Certificate, *ecdsa.PrivateK require.NoError(t, err) cert, err := x509.ParseCertificate(certDER) require.NoError(t, err) + return certDER, cert, priv } func generateRSACert(t *testing.T) ([]byte, *x509.Certificate, *rsa.PrivateKey) { t.Helper() + priv, err := rsa.GenerateKey(rand.Reader, 2048) require.NoError(t, err) @@ -103,11 +108,13 @@ func generateRSACert(t *testing.T) ([]byte, *x509.Certificate, *rsa.PrivateKey) require.NoError(t, err) cert, err := x509.ParseCertificate(certDER) require.NoError(t, err) + return certDER, cert, priv } func generateCA(t *testing.T) ([]byte, *x509.Certificate, ed25519.PrivateKey) { t.Helper() + pub, priv, err := ed25519.GenerateKey(rand.Reader) require.NoError(t, err) @@ -125,11 +132,13 @@ func generateCA(t *testing.T) ([]byte, *x509.Certificate, ed25519.PrivateKey) { require.NoError(t, err) ca, err := x509.ParseCertificate(caDER) require.NoError(t, err) + return caDER, ca, priv } func generateLeafSignedByCA(t *testing.T, ca *x509.Certificate, caKey ed25519.PrivateKey) ([]byte, *x509.Certificate, ed25519.PrivateKey) { t.Helper() + pub, priv, err := ed25519.GenerateKey(rand.Reader) require.NoError(t, err) @@ -145,6 +154,7 @@ func generateLeafSignedByCA(t *testing.T, ca *x509.Certificate, caKey ed25519.Pr require.NoError(t, err) leaf, err := x509.ParseCertificate(leafDER) require.NoError(t, err) + return leafDER, leaf, priv } @@ -157,6 +167,7 @@ func testDigest(input string) []byte { func newSVIDSource(cert *x509.Certificate, key crypto.Signer) *staticSVIDSource { id, _ := x509svid.IDFromCert(cert) + return &staticSVIDSource{svid: &x509svid.SVID{ ID: id, Certificates: []*x509.Certificate{cert}, @@ -169,6 +180,7 @@ func TestNew(t *testing.T) { t.Run("nil svidSource for verify-only", func(t *testing.T) { t.Parallel() + s := New(nil, fake.New()) require.NotNil(t, s) }) @@ -194,6 +206,7 @@ func TestNew(t *testing.T) { func TestSign_NilSVIDSource(t *testing.T) { t.Parallel() + s := New(nil, fake.New()) _, _, err := s.Sign(testDigest("hello")) require.Error(t, err) @@ -202,6 +215,7 @@ func TestSign_NilSVIDSource(t *testing.T) { func TestSign_SVIDSourceError(t *testing.T) { t.Parallel() + source := &staticSVIDSource{err: errors.New("svid unavailable")} s := New(source, nil) _, _, err := s.Sign(testDigest("hello")) @@ -211,6 +225,7 @@ func TestSign_SVIDSourceError(t *testing.T) { func TestSign_NoCertificates(t *testing.T) { t.Parallel() + source := &staticSVIDSource{svid: &x509svid.SVID{ Certificates: nil, PrivateKey: ed25519.NewKeyFromSeed(make([]byte, 32)), @@ -292,6 +307,7 @@ func TestVerify_TamperedSignature(t *testing.T) { func TestVerify_InvalidCertChain(t *testing.T) { t.Parallel() + s := New(nil, fake.New()) err := s.VerifySignature(testDigest("digest"), []byte("sig"), []byte("not-a-cert")) require.Error(t, err) @@ -300,6 +316,7 @@ func TestVerify_InvalidCertChain(t *testing.T) { func TestVerify_EmptyCertChain(t *testing.T) { t.Parallel() + s := New(nil, fake.New()) // Valid DER but empty is not possible; just use nil. err := s.VerifySignature(testDigest("digest"), []byte("sig"), nil) @@ -346,6 +363,7 @@ func TestVerifyCertChainOfTrust_IntermediateChain(t *testing.T) { // Create intermediate CA signed by root. intermPub, intermPriv, err := ed25519.GenerateKey(rand.Reader) require.NoError(t, err) + intermTemplate := &x509.Certificate{ SerialNumber: big.NewInt(2), Subject: pkix.Name{CommonName: "Intermediate CA"}, @@ -389,6 +407,7 @@ func TestVerifyCertChainOfTrust_WrongTrustAnchor(t *testing.T) { func TestVerifyCertChainOfTrust_EmptyChain(t *testing.T) { t.Parallel() + ta := fake.New() s := New(nil, ta) @@ -398,6 +417,7 @@ func TestVerifyCertChainOfTrust_EmptyChain(t *testing.T) { func TestVerifyCertChainOfTrust_InvalidDER(t *testing.T) { t.Parallel() + ta := fake.New() s := New(nil, ta) diff --git a/crypto/spiffe/trustanchors/fake/fake.go b/crypto/spiffe/trustanchors/fake/fake.go index 2302573..64b954d 100644 --- a/crypto/spiffe/trustanchors/fake/fake.go +++ b/crypto/spiffe/trustanchors/fake/fake.go @@ -28,10 +28,12 @@ type Fake struct { func New(authorities ...*x509.Certificate) *Fake { td := spiffeid.TrustDomain{} + bundle := x509bundle.New(td) for _, a := range authorities { bundle.AddX509Authority(a) } + return &Fake{bundle: bundle} }