Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
# Changelog

## [Unreleased]

### Changed

- Minimum Go version is now go1.25.8, following our support policy.
- SPIFFE TLS config hooks now use `VerifyConnection` instead of `VerifyPeerCertificate`, so authorization also runs on resumed TLS sessions. Callers that previously extended a returned config by wrapping `VerifyPeerCertificate` must now extend `VerifyConnection` using manual chaining.


## [2.6.0] - 2025-08-21

### Changed
Expand Down
8 changes: 4 additions & 4 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ protoc_gen_go_grpc_base_dir := $(build_dir)/protoc-gen-go-grpc
protoc_gen_go_grpc_dir := $(protoc_gen_go_grpc_base_dir)/$(protoc_gen_go_grpc_version)-go$(go_version)
protoc_gen_go_grpc_bin := $(protoc_gen_go_grpc_dir)/protoc-gen-go-grpc

golangci_lint_version = v2.0.2
golangci_lint_version = v2.11.4
golangci_lint_dir = $(build_dir)/golangci_lint/$(golangci_lint_version)
golangci_lint_bin = $(golangci_lint_dir)/golangci-lint

Expand All @@ -85,17 +85,17 @@ apiprotos := \
# Toolchain
#############################################################################

go_version_full := 1.24.6
go_version_full := 1.25.8
go_version := $(go_version_full:.0=)
go_dir := $(build_dir)/go/$(go_version)

ifeq ($(os1),windows)
go_bin_dir = $(go_dir)/go/bin
go_url = https://storage.googleapis.com/golang/go$(go_version).$(os1)-$(arch2).zip
go_url = https://dl.google.com/go/go$(go_version_full).$(os1)-$(arch2).zip
exe=".exe"
else
go_bin_dir = $(go_dir)/bin
go_url = https://storage.googleapis.com/golang/go$(go_version).$(os1)-$(arch2).tar.gz
go_url = https://dl.google.com/go/go$(go_version_full).$(os1)-$(arch2).tar.gz
exe=
endif

Expand Down
2 changes: 1 addition & 1 deletion examples/spiffe-jwt-using-proxy/proxy/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ func run(ctx context.Context) error {

func handler(p *httputil.ReverseProxy) func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
log.Printf("%s %s", r.Method, r.URL)
log.Printf("%q %q", r.Method, r.URL.String()) //nolint:gosec // intentional request logging; values are quoted to avoid log forging
p.ServeHTTP(w, r)
}
}
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
module github.com/spiffe/go-spiffe/v2

go 1.24.0
go 1.25.8

require (
github.com/Microsoft/go-winio v0.6.2
Expand Down
5 changes: 5 additions & 0 deletions spiffetls/spiffetls_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -496,6 +496,11 @@ func setupTestEnv(t *testing.T) (*testEnv, func()) {

// Create custom workload API sources for the server
wlCtx, wlCancel := context.WithTimeout(context.Background(), time.Second*5)
defer func() {
if testEnv.err != nil {
wlCancel()
}
}()
testEnv.wlCancel = wlCancel
testEnv.wlAPIClientA, testEnv.err = workloadapi.New(wlCtx, workloadapi.WithAddr(testEnv.wlAPIServerA.Addr()))
if testEnv.err != nil {
Expand Down
141 changes: 123 additions & 18 deletions spiffetls/tlsconfig/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,40 @@ func TLSClientConfig(bundle x509bundle.Source, authorizer Authorizer, opts ...Op
}

// HookTLSClientConfig sets up the TLS configuration to verify and authorize
// the server X509-SVID. If there is an existing callback set for
// VerifyPeerCertificate it will be wrapped by this package and invoked
// after SPIFFE authentication has completed.
// the server X509-SVID.
//
// SPIFFE authentication uses VerifyConnection, so it runs on every handshake,
// including resumed sessions. VerifyPeerCertificate is not used for SPIFFE
// authentication.
//
// If config.VerifyPeerCertificate or config.VerifyConnection is already set
// when this function is called, those callbacks are preserved and invoked
// after SPIFFE authentication succeeds, first VerifyPeerCertificate, then
// VerifyConnection.
//
// Callers that previously extended the returned config by wrapping
// VerifyPeerCertificate must now extend VerifyConnection instead. Manual
// chaining is recommended:
//
// prev := config.VerifyConnection
// config.VerifyConnection = func(cs tls.ConnectionState) error {
// if prev != nil {
// if err := prev(cs); err != nil {
// return err
// }
// }
// // additional post-SPIFFE checks
// return nil
// }
//
// Note: opts is accepted for API consistency with related Hook functions but
// has no effect in this function.
func HookTLSClientConfig(config *tls.Config, bundle x509bundle.Source, authorizer Authorizer, opts ...Option) {
prevVerifyPeerCertificate := config.VerifyPeerCertificate
prevVerifyConnection := config.VerifyConnection
resetAuthFields(config)
config.InsecureSkipVerify = true
config.VerifyPeerCertificate = WrapVerifyPeerCertificate(config.VerifyPeerCertificate, bundle, authorizer, opts...)
config.VerifyConnection = wrapVerifyConnection(prevVerifyPeerCertificate, prevVerifyConnection, bundle, authorizer)
}

// A Option changes the defaults used to by mTLS ClientConfig functions.
Expand Down Expand Up @@ -64,14 +91,27 @@ func MTLSClientConfig(svid x509svid.Source, bundle x509bundle.Source, authorizer
}

// HookMTLSClientConfig sets up the TLS configuration to present an X509-SVID
// to the server and verify and authorize the server X509-SVID. If there is an
// existing callback set for VerifyPeerCertificate it will be wrapped by
// this package and invoked after SPIFFE authentication has completed.
// to the server and verify and authorize the server X509-SVID.
//
// SPIFFE authentication uses VerifyConnection, so it runs on every handshake,
// including resumed sessions. VerifyPeerCertificate is not used for SPIFFE
// authentication.
//
// If config.VerifyPeerCertificate or config.VerifyConnection is already set
// when this function is called, those callbacks are preserved and invoked
// after SPIFFE authentication succeeds, first VerifyPeerCertificate, then
// VerifyConnection.
//
// Callers that previously extended the returned config by wrapping
// VerifyPeerCertificate must now extend VerifyConnection instead. See
// HookTLSClientConfig for an example.
func HookMTLSClientConfig(config *tls.Config, svid x509svid.Source, bundle x509bundle.Source, authorizer Authorizer, opts ...Option) {
prevVerifyPeerCertificate := config.VerifyPeerCertificate
prevVerifyConnection := config.VerifyConnection
resetAuthFields(config)
config.GetClientCertificate = GetClientCertificate(svid, opts...)
config.InsecureSkipVerify = true
config.VerifyPeerCertificate = WrapVerifyPeerCertificate(config.VerifyPeerCertificate, bundle, authorizer, opts...)
config.VerifyConnection = wrapVerifyConnection(prevVerifyPeerCertificate, prevVerifyConnection, bundle, authorizer)
}

// MTLSWebClientConfig returns a TLS configuration which presents an X509-SVID
Expand Down Expand Up @@ -116,15 +156,27 @@ func MTLSServerConfig(svid x509svid.Source, bundle x509bundle.Source, authorizer
}

// HookMTLSServerConfig sets up the TLS configuration to present an X509-SVID
// to the client and require, verify, and authorize the client X509-SVID. If
// there is an existing callback set for VerifyPeerCertificate it will be
// wrapped by this package and invoked after SPIFFE authentication has
// completed.
// to the client and require, verify, and authorize the client X509-SVID.
//
// SPIFFE authentication uses VerifyConnection, so it runs on every handshake,
// including resumed sessions. VerifyPeerCertificate is not used for SPIFFE
// authentication.
//
// If config.VerifyPeerCertificate or config.VerifyConnection is already set
// when this function is called, those callbacks are preserved and invoked
// after SPIFFE authentication succeeds, first VerifyPeerCertificate, then
// VerifyConnection.
//
// Callers that previously extended the returned config by wrapping
// VerifyPeerCertificate must now extend VerifyConnection instead. See
// HookTLSClientConfig for an example.
func HookMTLSServerConfig(config *tls.Config, svid x509svid.Source, bundle x509bundle.Source, authorizer Authorizer, opts ...Option) {
prevVerifyPeerCertificate := config.VerifyPeerCertificate
prevVerifyConnection := config.VerifyConnection
resetAuthFields(config)
config.ClientAuth = tls.RequireAnyClientCert
config.GetCertificate = GetCertificate(svid, opts...)
config.VerifyPeerCertificate = WrapVerifyPeerCertificate(config.VerifyPeerCertificate, bundle, authorizer, opts...)
config.VerifyConnection = wrapVerifyConnection(prevVerifyPeerCertificate, prevVerifyConnection, bundle, authorizer)
}

// MTLSWebServerConfig returns a TLS configuration which presents a web
Expand All @@ -136,16 +188,29 @@ func MTLSWebServerConfig(cert *tls.Certificate, bundle x509bundle.Source, author
return config
}

// HookMTLSWebServerConfig sets up the TLS configuration to presents a web
// HookMTLSWebServerConfig sets up the TLS configuration to present a web
// server certificate to the client and require, verify, and authorize client
// X509-SVIDs. If there is an existing callback set for VerifyPeerCertificate
// it will be wrapped by this package and invoked after SPIFFE
// authentication has completed.
// X509-SVIDs.
//
// SPIFFE authentication uses VerifyConnection, so it runs on every handshake,
// including resumed sessions. VerifyPeerCertificate is not used for SPIFFE
// authentication.
//
// If config.VerifyPeerCertificate or config.VerifyConnection is already set
// when this function is called, those callbacks are preserved and invoked
// after SPIFFE authentication succeeds, first VerifyPeerCertificate, then
// VerifyConnection.
//
// Callers that previously extended the returned config by wrapping
// VerifyPeerCertificate must now extend VerifyConnection instead. See
// HookTLSClientConfig for an example.
func HookMTLSWebServerConfig(config *tls.Config, cert *tls.Certificate, bundle x509bundle.Source, authorizer Authorizer, opts ...Option) {
prevVerifyPeerCertificate := config.VerifyPeerCertificate
prevVerifyConnection := config.VerifyConnection
resetAuthFields(config)
config.ClientAuth = tls.RequireAnyClientCert
config.Certificates = []tls.Certificate{*cert}
config.VerifyPeerCertificate = WrapVerifyPeerCertificate(config.VerifyPeerCertificate, bundle, authorizer, opts...)
config.VerifyConnection = wrapVerifyConnection(prevVerifyPeerCertificate, prevVerifyConnection, bundle, authorizer)
}

// GetCertificate returns a GetCertificate callback for tls.Config. It uses the
Expand Down Expand Up @@ -204,6 +269,44 @@ func WrapVerifyPeerCertificate(wrapped func([][]byte, [][]*x509.Certificate) err
}
}

func wrapVerifyConnection(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm worried about backwards compatibility. There are two situations:

  1. somebody passes a tls.Config where either VeriyPeerCertificate/VerifyConnection is set. This code handles that fine.
  2. somebody takes the returned tls.Config and wraps VerifyPeerCertificate. We'd break the latter here, because we no longer set VerifyPeerCertificate.

I need to think about how we handle this. Open to suggestions. It may be the case that we have to accept a small breaking change. In any case, whatever we pick, the nuances should be documented on the functions.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point. The implementation already preserves the input-side case by capturing any existing config.VerifyPeerCertificate / config.VerifyConnection callbacks and invoking them after SPIFFE auth succeeds. The remaining change is the narrow returned-config case: callers that previously wrapped VerifyPeerCertificate on the returned config now need to extend VerifyConnection instead, since SPIFFE auth has to live on VerifyConnection to also run on resumed sessions. I updated the function docs to call out that nuance explicitly, and I added focused tests for the preserved input-callback behavior and chaining/error propagation. So this is a narrow breaking change, but I think it is the right tradeoff here.

@strideynet strideynet Apr 17, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just to throw my 2c into the mix:

About a month or two back, I had some interesting conversations with folks on the topic of TLS resumption. My original stance had been that it made sense to re-verify certificates upon resumption of a connection but I did hear a strong counter argument that has made me somewhat reconsider.

The argument goes that if you are not continually monitoring open TLS connections and dropping them if they are no longer "valid" (i.e CA has rotated, or cert has expired), then what makes a resumption event particularly remarkable that you need to re-perform this verification?

Curious on others thoughts on this since I haven't quite made my mind up either way.

Another element came to mind for me as well - which perhaps @maxlambrecht may be better suited to answer in regards to: In Go, the peer certificate is stored as part of the session state/ticket and allows us to re-verify upon resumption. Is this the case for Rust/other languages as well?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For Rust, I think this is more about the TLS library than the language itself. In rustls, peer_certificates() is available for both full and resumed handshakes, so the peer certificate chain is still available after resumption. But at least on the TLS 1.3 client resumption path, rustls does not re-run the full certificate verification. It treats resumption as a continuation of the previous session and restores the cached server certificate chain into the connection state.

So for rustls, my understanding is: yes, peer cert material is available on resume, but no, that does not mean the chain is verified again from scratch on that path.

For Java and Python, I think the general idea is similar, but it may depend on the TLS implementation. I have not verified the exact behavior there in enough detail to say more confidently.

prevVerifyPeerCertificate func([][]byte, [][]*x509.Certificate) error,
prevVerifyConnection func(tls.ConnectionState) error,
bundle x509bundle.Source,
authorizer Authorizer,
) func(tls.ConnectionState) error {
return func(cs tls.ConnectionState) error {
rawCerts := rawPeerCertificates(cs.PeerCertificates)
id, certs, err := x509svid.ParseAndVerify(rawCerts, bundle)
if err != nil {
return err
}

if err := authorizer(id, certs); err != nil {
return err
}

if prevVerifyPeerCertificate != nil {
if err := prevVerifyPeerCertificate(rawCerts, certs); err != nil {
return err
}
}

if prevVerifyConnection != nil {
return prevVerifyConnection(cs)
}
return nil
}
}

func rawPeerCertificates(certs []*x509.Certificate) [][]byte {
raw := make([][]byte, 0, len(certs))
for _, cert := range certs {
raw = append(raw, cert.Raw)
}
return raw
}

func getTLSCertificate(svid x509svid.Source, trace Trace) (*tls.Certificate, error) {
var traceVal interface{}
if trace.GetCertificate != nil {
Expand Down Expand Up @@ -252,4 +355,6 @@ func resetAuthFields(config *tls.Config) {
config.InsecureSkipVerify = false
config.NameToCertificate = nil //nolint:staticcheck // setting to nil is OK
config.RootCAs = nil
config.VerifyPeerCertificate = nil
config.VerifyConnection = nil
}
Loading
Loading