diff --git a/cmd/spire-agent/cli/run/run.go b/cmd/spire-agent/cli/run/run.go index 7c66301f65..1eca0c662f 100644 --- a/cmd/spire-agent/cli/run/run.go +++ b/cmd/spire-agent/cli/run/run.go @@ -38,6 +38,7 @@ import ( "github.com/spiffe/spire/pkg/common/log" "github.com/spiffe/spire/pkg/common/pemutil" "github.com/spiffe/spire/pkg/common/telemetry" + "github.com/spiffe/spire/pkg/common/tlspolicy" ) const ( @@ -118,6 +119,7 @@ type experimentalConfig struct { NamedPipeName string `hcl:"named_pipe_name"` AdminNamedPipeName string `hcl:"admin_named_pipe_name"` UseSyncAuthorizedEntries bool `hcl:"use_sync_authorized_entries"` + PQKEMMode string `hcl:"pq_kem_mode"` Flags fflag.RawConfig `hcl:"feature_flags"` @@ -595,6 +597,11 @@ func NewAgentConfig(c *Config, logOptions []log.Option, allowUnknownConfig bool) ac.AvailabilityTarget = t } + ac.TLSPolicy.PQKEMMode, err = tlspolicy.ParsePQKEMMode(log.NewHCLogAdapter(logger, "tlspolicy"), c.Agent.Experimental.PQKEMMode) + if err != nil { + return nil, fmt.Errorf("pq_kem_mode config option %q is invalid: %w", c.Agent.Experimental.PQKEMMode, err) + } + if cmp.Diff(experimentalConfig{}, c.Agent.Experimental) != "" { logger.Warn("Experimental features have been enabled. Please see doc/upgrading.md for upgrade and compatibility considerations for experimental features.") } diff --git a/cmd/spire-server/cli/run/run.go b/cmd/spire-server/cli/run/run.go index 6734190c59..45abce8de7 100644 --- a/cmd/spire-server/cli/run/run.go +++ b/cmd/spire-server/cli/run/run.go @@ -35,6 +35,7 @@ import ( "github.com/spiffe/spire/pkg/common/health" "github.com/spiffe/spire/pkg/common/log" "github.com/spiffe/spire/pkg/common/telemetry" + "github.com/spiffe/spire/pkg/common/tlspolicy" "github.com/spiffe/spire/pkg/server" "github.com/spiffe/spire/pkg/server/authpolicy" bundleClient "github.com/spiffe/spire/pkg/server/bundle/client" @@ -108,6 +109,7 @@ type experimentalConfig struct { EventsBasedCache bool `hcl:"events_based_cache"` PruneEventsOlderThan string `hcl:"prune_events_older_than"` SQLTransactionTimeout string `hcl:"sql_transaction_timeout"` + PQKEMMode string `hcl:"pq_kem_mode"` Flags fflag.RawConfig `hcl:"feature_flags"` @@ -508,6 +510,11 @@ func NewServerConfig(c *Config, logOptions []log.Option, allowUnknownConfig bool sc.ProfilingFreq = c.Server.ProfilingFreq sc.ProfilingNames = c.Server.ProfilingNames + sc.TLSPolicy.PQKEMMode, err = tlspolicy.ParsePQKEMMode(log.NewHCLogAdapter(logger, "tlspolicy"), c.Server.Experimental.PQKEMMode) + if err != nil { + return nil, fmt.Errorf("invalid pq_kem_mode: %q: %w", c.Server.Experimental.PQKEMMode, err) + } + for _, adminID := range c.Server.AdminIDs { id, err := spiffeid.FromString(adminID) if err != nil { diff --git a/cmd/spire-server/cli/run/run_test.go b/cmd/spire-server/cli/run/run_test.go index 754a43fdc9..e54280c283 100644 --- a/cmd/spire-server/cli/run/run_test.go +++ b/cmd/spire-server/cli/run/run_test.go @@ -16,6 +16,7 @@ import ( "github.com/spiffe/go-spiffe/v2/spiffeid" "github.com/spiffe/spire/pkg/common/catalog" "github.com/spiffe/spire/pkg/common/log" + "github.com/spiffe/spire/pkg/common/tlspolicy" "github.com/spiffe/spire/pkg/server" bundleClient "github.com/spiffe/spire/pkg/server/bundle/client" "github.com/spiffe/spire/pkg/server/credtemplate" @@ -64,6 +65,7 @@ func TestParseConfigGood(t *testing.T) { _, ok := trustDomainConfig.EndpointProfile.(bundleClient.HTTPSWebProfile) assert.True(t, ok) assert.True(t, c.Server.AuditLogEnabled) + assert.Equal(t, c.Server.Experimental.PQKEMMode, "require") testParseConfigGoodOS(t, c) // Parse/reprint cycle trims outer whitespace @@ -455,6 +457,16 @@ func TestMergeInput(t *testing.T) { require.True(t, c.Server.AuditLogEnabled) }, }, + { + msg: "pq_kem_mode should be configurable by file", + fileInput: func(c *Config) { + c.Server.Experimental.PQKEMMode = "attempt" + }, + cliFlags: []string{}, + test: func(t *testing.T, c *Config) { + require.Equal(t, c.Server.Experimental.PQKEMMode, "attempt") + }, + }, } cases = append(cases, mergeInputCasesOS(t)...) @@ -1160,6 +1172,49 @@ func TestNewServerConfig(t *testing.T) { }, c.AdminIDs) }, }, + { + msg: "post-quantum KEM mode is set (default)", + input: func(c *Config) {}, + test: func(t *testing.T, c *server.Config) { + require.Equal(t, tlspolicy.PQKEMModeDefault, c.TLSPolicy.PQKEMMode) + }, + }, + { + msg: "post-quantum KEM mode is set (explicit default)", + input: func(c *Config) { + c.Server.Experimental.PQKEMMode = "default" + }, + test: func(t *testing.T, c *server.Config) { + require.Equal(t, tlspolicy.PQKEMModeDefault, c.TLSPolicy.PQKEMMode) + }, + }, + { + msg: "post-quantum KEM mode is set (attempt)", + input: func(c *Config) { + c.Server.Experimental.PQKEMMode = "attempt" + }, + test: func(t *testing.T, c *server.Config) { + if tlspolicy.SupportsPQKEM { + require.Equal(t, tlspolicy.PQKEMModeAttempt, c.TLSPolicy.PQKEMMode) + } else { + require.Equal(t, tlspolicy.PQKEMModeDefault, c.TLSPolicy.PQKEMMode) + } + }, + }, + { + msg: "post-quantum KEM mode is set (require)", + input: func(c *Config) { + c.Server.Experimental.PQKEMMode = "require" + }, + expectError: !tlspolicy.SupportsPQKEM, + test: func(t *testing.T, c *server.Config) { + if tlspolicy.SupportsPQKEM { + require.Equal(t, tlspolicy.PQKEMModeRequire, c.TLSPolicy.PQKEMMode) + } else { + require.Nil(t, c) + } + }, + }, } cases = append(cases, newServerConfigCasesOS(t)...) diff --git a/doc/plugin_server_upstreamauthority_spire.md b/doc/plugin_server_upstreamauthority_spire.md index 83bdb6140a..f1a25955ec 100644 --- a/doc/plugin_server_upstreamauthority_spire.md +++ b/doc/plugin_server_upstreamauthority_spire.md @@ -17,9 +17,20 @@ The plugin accepts the following configuration options: These are the current experimental configurations: -| experimental | Description | Default | -|------------------------------|-----------------------------------------------------------------------------------------------------------|---------| -| workload_api_named_pipe_name | Pipe name of the Workload API named pipe (Windows only; e.g. pipe name of the SPIRE Agent API named pipe) | +| experimental | Description | Default | +|------------------------------|----------------------------------------------------------------------------------------------------------------|---------| +| workload_api_named_pipe_name | Pipe name of the Workload API named pipe (Windows only; e.g. pipe name of the SPIRE Agent API named pipe) | | +| pq_kem_mode | Whether to use a post-quantum key exchange method for TLS handshake. Set to "default", "attempt" or "require". | default | + +The `pq_kem_mode` option supports the following options: + +| `pq_kem_mode` Value | Description | +|:--------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| default | Inherit system default key exchange configuration. Whether a post-quantum-safe key exchange method is available may depend on environmental configuration (e.g. GODEBUG). | +| attempt | Opportunistically attempt to negotiate a post-quantum-safe key exchange method. | +| require | Require negotiation of a post-quantum-safe key exchange method. | + +The `pq_kem_mode` option is currently experimental and may be changed or removed in a future release. Currently, use of this option requires Go 1.23 or later, as this is the first Go release supporting at least one post-quantum-safe key exchange method. Sample configuration (Unix): diff --git a/doc/spire_agent.md b/doc/spire_agent.md index de5054d607..1bee5c6903 100644 --- a/doc/spire_agent.md +++ b/doc/spire_agent.md @@ -71,12 +71,23 @@ This may be useful for templating configuration files, for example across differ | `workload_x509_svid_key_type` | The workload X509 SVID key type <rsa-2048|ec-p256> | ec-p256 | | `availability_target` | The minimum amount of time desired to gracefully handle SPIRE Server or Agent downtime. This configurable influences how aggressively X509 SVIDs should be rotated. If set, must be at least 24h. See [Availability Target](#availability-target) | | -| experimental | Description | Default | -|:---------------------------|------------------------------------------------------------------------------------|-------------------------| -| `named_pipe_name` | Pipe name to bind the SPIRE Agent API named pipe (Windows only) | \spire-agent\public\api | -| `sync_interval` | Sync interval with SPIRE server with exponential backoff | 5 sec | -| `x509_svid_cache_max_size` | Soft limit of max number of SVIDs that would be stored in LRU cache (deprecated) | 1000 | -| `disable_lru_cache` | Reverts back to use the SPIRE Agent non-LRU cache for storing SVIDs (deprecated) | false | +| experimental | Description | Default | +|:---------------------------|------------------------------------------------------------------------------------------------------------------|-------------------------| +| `named_pipe_name` | Pipe name to bind the SPIRE Agent API named pipe (Windows only) | \spire-agent\public\api | +| `sync_interval` | Sync interval with SPIRE server with exponential backoff | 5 sec | +| `x509_svid_cache_max_size` | Soft limit of max number of SVIDs that would be stored in LRU cache (deprecated) | 1000 | +| `disable_lru_cache` | Reverts back to use the SPIRE Agent non-LRU cache for storing SVIDs (deprecated) | false | +| `pq_kem_mode` | Whether to use a post-quantum key exchange method for TLS handshake. Set to "default", "attempt" or "require". | default | + +The `pq_kem_mode` option supports the following options: + +| `pq_kem_mode` Value | Description | +|:--------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| default | Inherit system default key exchange configuration. Whether a post-quantum-safe key exchange method is available may depend on environmental configuration (e.g. GODEBUG). | +| attempt | Opportunistically attempt to negotiate a post-quantum-safe key exchange method. | +| require | Require negotiation of a post-quantum-safe key exchange method. | + +The `pq_kem_mode` option is currently experimental and may be changed or removed in a future release. Currently, use of this option requires Go 1.23 or later, as this is the first Go release supporting at least one post-quantum-safe key exchange method. ### Initial trust bundle configuration diff --git a/doc/spire_server.md b/doc/spire_server.md index 1304225cc7..2d1c64226e 100644 --- a/doc/spire_server.md +++ b/doc/spire_server.md @@ -96,6 +96,7 @@ This may be useful for templating configuration files, for example across differ | `prune_events_older_than`| How old an event can be before being deleted. Used with events based cache. Decreasing this will keep the events table smaller, but will increase risk of missing an event if connection to the database is down. | 12h | | `auth_opa_policy_engine` | The [auth opa_policy engine](/doc/authorization_policy_engine.md) used for authorization decisions | default SPIRE authorization policy | | `named_pipe_name` | Pipe name of the SPIRE Server API named pipe (Windows only) | \spire-server\private\api | +| `pq_kem_mode` | Whether to use a post-quantum key exchange method for TLS handshake. Set to "default", "attempt" or "require". | default | | ratelimit | Description | Default | |:--------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------|---------| @@ -111,6 +112,16 @@ This may be useful for templating configuration files, for example across differ | `rego_path` | File to retrieve OPA rego policy for authorization. | | | `policy_data_path` | File to retrieve databindings for policy evaluation. | | +The `pq_kem_mode` option supports the following options: + +| `pq_kem_mode` Value | Description | +|:--------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| default | Inherit system default key exchange configuration. Whether a post-quantum-safe key exchange method is available may depend on environmental configuration (e.g. GODEBUG). | +| attempt | Opportunistically attempt to negotiate a post-quantum-safe key exchange method. | +| require | Require negotiation of a post-quantum-safe key exchange method. | + +The `pq_kem_mode` option is currently experimental and may be changed or removed in a future release. Currently, use of this option requires Go 1.23 or later, as this is the first Go release supporting at least one post-quantum-safe key exchange method. + ### Profiling Names These are the available profiles that can be set in the `profiling_names` configuration value: diff --git a/pkg/agent/agent.go b/pkg/agent/agent.go index 433bad4417..615fa63008 100644 --- a/pkg/agent/agent.go +++ b/pkg/agent/agent.go @@ -258,6 +258,7 @@ func (a *Agent) attest(ctx context.Context, sto storage.Storage, cat catalog.Cat Log: a.c.Log.WithField(telemetry.SubsystemName, telemetry.Attestor), ServerAddress: a.c.ServerAddress, NodeAttestor: na, + TLSPolicy: a.c.TLSPolicy, } return node_attestor.New(&config).Attest(ctx) } @@ -282,6 +283,7 @@ func (a *Agent) newManager(ctx context.Context, sto storage.Storage, cat catalog SVIDStoreCache: cache, NodeAttestor: na, RotationStrategy: rotationutil.NewRotationStrategy(a.c.AvailabilityTarget), + TLSPolicy: a.c.TLSPolicy, } mgr := manager.New(config) diff --git a/pkg/agent/attestor/node/node.go b/pkg/agent/attestor/node/node.go index aa0cf63355..a6129231b6 100644 --- a/pkg/agent/attestor/node/node.go +++ b/pkg/agent/attestor/node/node.go @@ -25,6 +25,7 @@ import ( "github.com/spiffe/spire/pkg/common/telemetry" telemetry_agent "github.com/spiffe/spire/pkg/common/telemetry/agent" telemetry_common "github.com/spiffe/spire/pkg/common/telemetry/common" + "github.com/spiffe/spire/pkg/common/tlspolicy" "github.com/spiffe/spire/pkg/common/util" "github.com/spiffe/spire/pkg/common/x509util" "github.com/zeebo/errs" @@ -58,6 +59,7 @@ type Config struct { Log logrus.FieldLogger ServerAddress string NodeAttestor nodeattestor.NodeAttestor + TLSPolicy tlspolicy.Policy } type attestor struct { @@ -256,6 +258,7 @@ func (a *attestor) serverConn(ctx context.Context, bundle *spiffebundle.Bundle) Address: a.c.ServerAddress, TrustDomain: a.c.TrustDomain, GetBundle: bundle.X509Authorities, + TLSPolicy: a.c.TLSPolicy, }) } diff --git a/pkg/agent/client/client.go b/pkg/agent/client/client.go index 1ec3522f58..e4699b0663 100644 --- a/pkg/agent/client/client.go +++ b/pkg/agent/client/client.go @@ -20,6 +20,7 @@ import ( "github.com/spiffe/spire-api-sdk/proto/spire/api/types" "github.com/spiffe/spire/pkg/common/bundleutil" "github.com/spiffe/spire/pkg/common/telemetry" + "github.com/spiffe/spire/pkg/common/tlspolicy" "github.com/spiffe/spire/proto/spire/common" "google.golang.org/grpc" "google.golang.org/grpc/codes" @@ -92,6 +93,9 @@ type Config struct { // RotMtx is used to prevent the creation of new connections during SVID rotations RotMtx *sync.RWMutex + + // TLSPolicy determines the post-quantum-safe policy to apply to all TLS connections. + TLSPolicy tlspolicy.Policy } type client struct { @@ -371,6 +375,7 @@ func (c *client) dial(ctx context.Context) (*grpc.ClientConn, error) { } return agentCert }, + TLSPolicy: c.c.TLSPolicy, dialContext: c.dialContext, }) } diff --git a/pkg/agent/client/dial.go b/pkg/agent/client/dial.go index 031572b833..2b6689af28 100644 --- a/pkg/agent/client/dial.go +++ b/pkg/agent/client/dial.go @@ -14,6 +14,7 @@ import ( "github.com/spiffe/go-spiffe/v2/spiffetls/tlsconfig" "github.com/spiffe/go-spiffe/v2/svid/x509svid" "github.com/spiffe/spire/pkg/common/idutil" + "github.com/spiffe/spire/pkg/common/tlspolicy" "github.com/spiffe/spire/pkg/common/x509util" "google.golang.org/grpc" "google.golang.org/grpc/credentials" @@ -38,6 +39,9 @@ type DialServerConfig struct { // certificate to present to the server during the TLS handshake. GetAgentCertificate func() *tls.Certificate + // TLSPolicy determines the post-quantum-safe policy to apply to all TLS connections. + TLSPolicy tlspolicy.Policy + // dialContext is an optional constructor for the grpc client connection. dialContext func(ctx context.Context, target string, opts ...grpc.DialOption) (*grpc.ClientConn, error) } @@ -57,6 +61,11 @@ func DialServer(ctx context.Context, config DialServerConfig) (*grpc.ClientConn, tlsConfig = tlsconfig.MTLSClientConfig(newX509SVIDSource(config.GetAgentCertificate), bundleSource, authorizer) } + err = tlspolicy.ApplyPolicy(tlsConfig, config.TLSPolicy) + if err != nil { + return nil, err + } + ctx, cancel := context.WithTimeout(ctx, defaultDialTimeout) defer cancel() diff --git a/pkg/agent/config.go b/pkg/agent/config.go index 1d964be1d9..0766c3598c 100644 --- a/pkg/agent/config.go +++ b/pkg/agent/config.go @@ -12,6 +12,7 @@ import ( "github.com/spiffe/spire/pkg/common/catalog" "github.com/spiffe/spire/pkg/common/health" "github.com/spiffe/spire/pkg/common/telemetry" + "github.com/spiffe/spire/pkg/common/tlspolicy" ) type Config struct { @@ -103,6 +104,9 @@ type Config struct { // AvailabilityTarget controls how frequently rotate SVIDs AvailabilityTarget time.Duration + + // TLSPolicy determines the post-quantum-safe TLS policy to apply to all TLS connections. + TLSPolicy tlspolicy.Policy } func New(c *Config) *Agent { diff --git a/pkg/agent/manager/config.go b/pkg/agent/manager/config.go index f5d71bbe12..a0c77aa18f 100644 --- a/pkg/agent/manager/config.go +++ b/pkg/agent/manager/config.go @@ -18,6 +18,7 @@ import ( "github.com/spiffe/spire/pkg/agent/workloadkey" "github.com/spiffe/spire/pkg/common/rotationutil" "github.com/spiffe/spire/pkg/common/telemetry" + "github.com/spiffe/spire/pkg/common/tlspolicy" ) // Config holds a cache manager configuration @@ -42,6 +43,7 @@ type Config struct { DisableLRUCache bool NodeAttestor nodeattestor.NodeAttestor RotationStrategy *rotationutil.RotationStrategy + TLSPolicy tlspolicy.Policy // Clk is the clock the manager will use to get time Clk clock.Clock @@ -89,6 +91,7 @@ func newManager(c *Config) *manager { NodeAttestor: c.NodeAttestor, Reattestable: c.Reattestable, RotationStrategy: c.RotationStrategy, + TLSPolicy: c.TLSPolicy, } svidRotator, client := svid.NewRotator(rotCfg) diff --git a/pkg/agent/svid/rotator.go b/pkg/agent/svid/rotator.go index fd532c44c8..9218c7edf9 100644 --- a/pkg/agent/svid/rotator.go +++ b/pkg/agent/svid/rotator.go @@ -313,6 +313,7 @@ func (r *rotator) serverConn(ctx context.Context, bundle *spiffebundle.Bundle) ( Address: r.c.ServerAddr, TrustDomain: r.c.TrustDomain, GetBundle: bundle.X509Authorities, + TLSPolicy: r.c.TLSPolicy, }) } diff --git a/pkg/agent/svid/rotator_config.go b/pkg/agent/svid/rotator_config.go index 6eb4b0538d..3dce096bda 100644 --- a/pkg/agent/svid/rotator_config.go +++ b/pkg/agent/svid/rotator_config.go @@ -17,6 +17,7 @@ import ( "github.com/spiffe/spire/pkg/agent/plugin/nodeattestor" "github.com/spiffe/spire/pkg/common/rotationutil" "github.com/spiffe/spire/pkg/common/telemetry" + "github.com/spiffe/spire/pkg/common/tlspolicy" ) const DefaultRotatorInterval = 5 * time.Second @@ -43,6 +44,9 @@ type RotatorConfig struct { Clk clock.Clock RotationStrategy *rotationutil.RotationStrategy + + // TLSPolicy determines the post-quantum-safe policy for TLS connections. + TLSPolicy tlspolicy.Policy } func NewRotator(c *RotatorConfig) (Rotator, client.Client) { @@ -85,6 +89,7 @@ func newRotator(c *RotatorConfig) (*rotator, client.Client) { } return s.SVID, s.Key, rootCAs }, + TLSPolicy: c.TLSPolicy, } client := client.New(cfg) diff --git a/pkg/common/tlspolicy/parse.go b/pkg/common/tlspolicy/parse.go new file mode 100644 index 0000000000..5a22f2c39f --- /dev/null +++ b/pkg/common/tlspolicy/parse.go @@ -0,0 +1,141 @@ +// Package tlspolicy provides for configuration and enforcement of policies +// relating to TLS. +package tlspolicy + +import ( + "crypto/tls" + "errors" + "fmt" + + "github.com/hashicorp/go-hclog" +) + +// SupportsPQKEM is a constant indicating whether the version of Go used to +// build, and the build configuration, supports a post-quantum safe TLS key +// exchange method. +const SupportsPQKEM = supportsPQKEM + +// Post-quantum TLS KEM mode. Determines whether a post-quantum safe KEM should +// be used when establishing a TLS connection. +type PQKEMMode int + +const ( + // Do not require use of a post-quantum KEM when establishing a TLS + // connection. Whether a post-quantum KEM is attempted depends on + // environmental configuration (e.g. GODEBUG setting tlskyber) and the target + // Go version at build time. + PQKEMModeDefault PQKEMMode = iota + + // Attempt use of a post-quantum KEM as the most preferred key exchange + // method when establishing a TLS connection. + // Support for this requires Go 1.23 or later. + // Configuring this will cause connections to fail if support is not available. + PQKEMModeAttempt + + // Require use of a post-quantum KEM when establishing a TLS connection. + // Attempts to initiate a connection with a key exchange method which is not + // post-quantum safe will fail. Support for this requires Go 1.23 or later. + // Configuring this will cause connections to fail if support is not available. + PQKEMModeRequire +) + +// ParsePQKEMMode parses a string into a PQKEMMode value or returns +// an error. +func ParsePQKEMMode(logger hclog.Logger, value string) (mode PQKEMMode, err error) { + if value != "" { + logger.Warn("pq_kem_mode is experimental and may be changed or removed in a future release") + } + + switch value { + case "": + if SupportsPQKEM { + logger.Debug("pq_kem_mode supported in this build; post-quantum-safe TLS key exchange may or may not be used depending on system configuration") + } else { + logger.Debug("pq_kem_mode not supported in this build") + } + return PQKEMModeDefault, nil + + case "default": + if SupportsPQKEM { + logger.Debug("pq_kem_mode supported and explicitly set to 'default'; post-quantum-safe TLS key exchange may or may not be used depending on system configuration") + } else { + logger.Debug("pq_kem_mode explicitly set to 'default'; post-quantum-safe TLS key exchange not supported in this build") + } + return PQKEMModeDefault, nil + + case "attempt": + if !SupportsPQKEM { + logger.Warn("pq_kem_mode set to 'attempt' but no post-quantum-safe key exchange methods are supported in this build (requires Go 1.23); ignoring") + return PQKEMModeDefault, nil + } + + logger.Debug("pq_kem_mode supported and configured in 'attempt' mode") + return PQKEMModeAttempt, nil + + case "require": + if !SupportsPQKEM { + err = errors.New("pq_kem_mode set to 'require' but not supported in this build; requires Go 1.23") + logger.Error(err.Error()) + return PQKEMModeDefault, err + } + + logger.Debug("pq_kem_mode supported and configured in 'require' mode - will require post-quantum security for all TLS connections") + return PQKEMModeRequire, nil + + default: + return PQKEMModeDefault, fmt.Errorf("pq_kem_mode of %q is invalid; must be one of ['', 'default', 'attempt', 'require']", value) + } +} + +// Policy describes policy options to be applied to a TLS configuration. +// +// A zero-initialised Policy provides reasonable defaults. +type Policy struct { + // PQKEMMode specifies the post-quantum KEM policy to use. + PQKEMMode PQKEMMode +} + +// Not exported by crypto/tls, so we define it here from the I-D. +const x25519Kyber768Draft00 tls.CurveID = 0x6399 + +// ApplyPolicy applies the policy options in policy to a given tls.Config, +// which is assumed to have already been obtained from the go-spiffe +// tlsconfig package. +func ApplyPolicy(config *tls.Config, policy Policy) error { + // Apply post-quantum KEM mode option. + switch policy.PQKEMMode { + case PQKEMModeDefault: + // Nothing to do - allow default curve preferences. + + case PQKEMModeAttempt: + if len(config.CurvePreferences) == 0 { + // This is copied from the crypto/tls default curve list. + config.CurvePreferences = []tls.CurveID{ + x25519Kyber768Draft00, + tls.X25519, + tls.CurveP256, + tls.CurveP384, + tls.CurveP521, + } + } else if config.CurvePreferences[0] != x25519Kyber768Draft00 { + // Prepend X25519Kyber768Draft00 to the list, making it most preferred. + curves := make([]tls.CurveID, 0, len(config.CurvePreferences)+1) + curves = append(curves, x25519Kyber768Draft00) + curves = append(curves, config.CurvePreferences...) + config.CurvePreferences = curves + } + + case PQKEMModeRequire: + // List only known PQ-safe KEMs as valid curves. + config.CurvePreferences = []tls.CurveID{ + x25519Kyber768Draft00, + } + + // Require TLS 1.3, as all PQ-safe KEMs require it anyway. + if config.MinVersion < tls.VersionTLS13 { + config.MinVersion = tls.VersionTLS13 + } + } + + return nil +} diff --git a/pkg/common/tlspolicy/parse_test.go b/pkg/common/tlspolicy/parse_test.go new file mode 100644 index 0000000000..16b348f036 --- /dev/null +++ b/pkg/common/tlspolicy/parse_test.go @@ -0,0 +1,38 @@ +package tlspolicy + +import ( + "testing" + + "github.com/spiffe/spire/pkg/common/log" + "github.com/stretchr/testify/require" +) + +func TestParsePQKEMMode(t *testing.T) { + require := require.New(t) + logger, err := log.NewLogger(log.WithLevel("ERROR")) + require.NoError(err) + + for _, s := range []struct { + Name string + Value PQKEMMode + ExpectError bool + }{ + {"", PQKEMModeDefault, false}, + {"default", PQKEMModeDefault, false}, + {"attempt", PQKEMModeAttempt, false}, + {"require", PQKEMModeRequire, !SupportsPQKEM}, + {"foo", PQKEMModeDefault, true}, + } { + r, err := ParsePQKEMMode(log.NewHCLogAdapter(logger, "tlspolicy"), s.Name) + if s.ExpectError { + require.Error(err) + } else { + require.NoError(err) + if SupportsPQKEM { + require.Equal(r, s.Value) + } else { + require.Equal(r, PQKEMModeDefault) + } + } + } +} diff --git a/pkg/common/tlspolicy/pqkem_no.go b/pkg/common/tlspolicy/pqkem_no.go new file mode 100644 index 0000000000..6fb827dd34 --- /dev/null +++ b/pkg/common/tlspolicy/pqkem_no.go @@ -0,0 +1,5 @@ +//go:build !go1.23 + +package tlspolicy + +const supportsPQKEM = false diff --git a/pkg/common/tlspolicy/pqkem_yes.go b/pkg/common/tlspolicy/pqkem_yes.go new file mode 100644 index 0000000000..ae0845771e --- /dev/null +++ b/pkg/common/tlspolicy/pqkem_yes.go @@ -0,0 +1,5 @@ +//go:build go1.23 + +package tlspolicy + +const supportsPQKEM = true diff --git a/pkg/server/bundle/client/client.go b/pkg/server/bundle/client/client.go index 8b1adbd35f..424c4c5864 100644 --- a/pkg/server/bundle/client/client.go +++ b/pkg/server/bundle/client/client.go @@ -13,6 +13,7 @@ import ( "github.com/spiffe/go-spiffe/v2/spiffeid" "github.com/spiffe/go-spiffe/v2/spiffetls/tlsconfig" "github.com/spiffe/spire/pkg/common/bundleutil" + "github.com/spiffe/spire/pkg/common/tlspolicy" "github.com/zeebo/errs" ) @@ -41,6 +42,10 @@ type ClientConfig struct { //revive:disable-line:exported name stutter is intent // mutateTransportHook is a hook to influence the transport used during // tests. mutateTransportHook func(*http.Transport) + + // TLSPolicy specifies the post-quantum-security policy used for TLS + // connections. + TLSPolicy tlspolicy.Policy } // Client is used to fetch a bundle and metadata from a bundle endpoint @@ -66,6 +71,11 @@ func NewClient(config ClientConfig) (Client, error) { authorizer := tlsconfig.AuthorizeID(endpointID) transport.TLSClientConfig = tlsconfig.TLSClientConfig(bundle, authorizer) + + err := tlspolicy.ApplyPolicy(transport.TLSClientConfig, config.TLSPolicy) + if err != nil { + return nil, err + } } if config.mutateTransportHook != nil { config.mutateTransportHook(transport) diff --git a/pkg/server/config.go b/pkg/server/config.go index fdbef83671..c25a76208b 100644 --- a/pkg/server/config.go +++ b/pkg/server/config.go @@ -10,6 +10,7 @@ import ( common "github.com/spiffe/spire/pkg/common/catalog" "github.com/spiffe/spire/pkg/common/health" "github.com/spiffe/spire/pkg/common/telemetry" + "github.com/spiffe/spire/pkg/common/tlspolicy" loggerv1 "github.com/spiffe/spire/pkg/server/api/logger/v1" "github.com/spiffe/spire/pkg/server/authpolicy" bundle_client "github.com/spiffe/spire/pkg/server/bundle/client" @@ -120,6 +121,9 @@ type Config struct { // calculation (prefer the TTL passed by the downstream caller, then fall // back to the default X509 CA TTL). UseLegacyDownstreamX509CATTL bool + + // TLSPolicy determines the policy settings to apply to all TLS connections. + TLSPolicy tlspolicy.Policy } type ExperimentalConfig struct { diff --git a/pkg/server/endpoints/config.go b/pkg/server/endpoints/config.go index 33b4d747eb..c3ce2cdb55 100644 --- a/pkg/server/endpoints/config.go +++ b/pkg/server/endpoints/config.go @@ -14,6 +14,7 @@ import ( "github.com/spiffe/go-spiffe/v2/spiffeid" "github.com/spiffe/spire/pkg/common/bundleutil" "github.com/spiffe/spire/pkg/common/telemetry" + "github.com/spiffe/spire/pkg/common/tlspolicy" "github.com/spiffe/spire/pkg/server/api" agentv1 "github.com/spiffe/spire/pkg/server/api/agent/v1" bundlev1 "github.com/spiffe/spire/pkg/server/api/bundle/v1" @@ -106,6 +107,10 @@ type Config struct { // calculation (prefer the TTL passed by the downstream caller, then fall // back to the default X509 CA TTL). UseLegacyDownstreamX509CATTL bool + + // TLSPolicy determines the post-quantum-safe policy used for all TLS + // connections. + TLSPolicy tlspolicy.Policy } func (c *Config) maybeMakeBundleEndpointServer() (Server, func(context.Context) error) { diff --git a/pkg/server/endpoints/endpoints.go b/pkg/server/endpoints/endpoints.go index caf6f017f1..6897f3e520 100644 --- a/pkg/server/endpoints/endpoints.go +++ b/pkg/server/endpoints/endpoints.go @@ -31,6 +31,7 @@ import ( "github.com/spiffe/spire/pkg/common/auth" "github.com/spiffe/spire/pkg/common/peertracker" "github.com/spiffe/spire/pkg/common/telemetry" + "github.com/spiffe/spire/pkg/common/tlspolicy" "github.com/spiffe/spire/pkg/common/util" "github.com/spiffe/spire/pkg/server/api" "github.com/spiffe/spire/pkg/server/api/middleware" @@ -83,6 +84,7 @@ type Endpoints struct { AuditLogEnabled bool AuthPolicyEngine *authpolicy.Engine AdminIDs []spiffeid.ID + TLSPolicy tlspolicy.Policy } type APIServers struct { @@ -174,6 +176,7 @@ func New(ctx context.Context, c Config) (*Endpoints, error) { AuditLogEnabled: c.AuditLogEnabled, AuthPolicyEngine: c.AuthPolicyEngine, AdminIDs: c.AdminIDs, + TLSPolicy: c.TLSPolicy, }, nil } @@ -349,6 +352,11 @@ func (e *Endpoints) getTLSConfig(ctx context.Context) func(*tls.ClientHelloInfo) }) spiffeTLSConfig := tlsconfig.MTLSServerConfig(svidSrc, bundleSrc, nil) + err := tlspolicy.ApplyPolicy(spiffeTLSConfig, e.TLSPolicy) + if err != nil { + return nil, err + } + // provided client certificates will be validated using the custom VerifyPeerCertificate hook spiffeTLSConfig.ClientAuth = tls.RequestClientCert spiffeTLSConfig.MinVersion = tls.VersionTLS12 diff --git a/pkg/server/endpoints/endpoints_test.go b/pkg/server/endpoints/endpoints_test.go index 883e89b33b..e42e6268be 100644 --- a/pkg/server/endpoints/endpoints_test.go +++ b/pkg/server/endpoints/endpoints_test.go @@ -23,6 +23,7 @@ import ( svidv1 "github.com/spiffe/spire-api-sdk/proto/spire/api/server/svid/v1" trustdomainv1 "github.com/spiffe/spire-api-sdk/proto/spire/api/server/trustdomain/v1" "github.com/spiffe/spire-api-sdk/proto/spire/api/types" + "github.com/spiffe/spire/pkg/common/tlspolicy" "github.com/spiffe/spire/pkg/common/util" "github.com/spiffe/spire/pkg/server/authpolicy" "github.com/spiffe/spire/pkg/server/ca/manager" @@ -101,6 +102,9 @@ func TestNew(t *testing.T) { RateLimit: rateLimit, Clock: clk, AuthPolicyEngine: pe, + TLSPolicy: tlspolicy.Policy{ + PQKEMMode: tlspolicy.PQKEMModeRequire, + }, }) require.NoError(t, err) assert.Equal(t, tcpAddr, endpoints.TCPAddr) @@ -116,6 +120,7 @@ func TestNew(t *testing.T) { assert.NotNil(t, endpoints.APIServers.SVIDServer) assert.NotNil(t, endpoints.BundleEndpointServer) assert.NotNil(t, endpoints.EntryFetcherPruneEventsTask) + assert.Equal(t, endpoints.TLSPolicy.PQKEMMode, tlspolicy.PQKEMModeRequire) assert.Equal(t, cat.GetDataStore(), endpoints.DataStore) assert.Equal(t, log, endpoints.Log) assert.Equal(t, metrics, endpoints.Metrics) @@ -225,6 +230,10 @@ func TestListenAndServe(t *testing.T) { AdminIDs: []spiffeid.ID{foreignAdminSVID.ID}, } + if tlspolicy.SupportsPQKEM { + endpoints.TLSPolicy.PQKEMMode = tlspolicy.PQKEMModeRequire + } + // Prime the datastore with the: // - bundle used to verify client certificates. // - agent attested node information @@ -257,19 +266,29 @@ func TestListenAndServe(t *testing.T) { require.NoError(t, err) defer localConn.Close() - noauthConn := dialTCP(tlsconfig.TLSClientConfig(ca.X509Bundle(), tlsconfig.AuthorizeID(serverID))) + noauthConfig := tlsconfig.TLSClientConfig(ca.X509Bundle(), tlsconfig.AuthorizeID(serverID)) + require.NoError(t, tlspolicy.ApplyPolicy(noauthConfig, endpoints.TLSPolicy)) + noauthConn := dialTCP(noauthConfig) defer noauthConn.Close() - agentConn := dialTCP(tlsconfig.MTLSClientConfig(agentSVID, ca.X509Bundle(), tlsconfig.AuthorizeID(serverID))) + agentConfig := tlsconfig.MTLSClientConfig(agentSVID, ca.X509Bundle(), tlsconfig.AuthorizeID(serverID)) + require.NoError(t, tlspolicy.ApplyPolicy(agentConfig, endpoints.TLSPolicy)) + agentConn := dialTCP(agentConfig) defer agentConn.Close() - adminConn := dialTCP(tlsconfig.MTLSClientConfig(adminSVID, ca.X509Bundle(), tlsconfig.AuthorizeID(serverID))) + adminConfig := tlsconfig.MTLSClientConfig(adminSVID, ca.X509Bundle(), tlsconfig.AuthorizeID(serverID)) + require.NoError(t, tlspolicy.ApplyPolicy(adminConfig, endpoints.TLSPolicy)) + adminConn := dialTCP(adminConfig) defer adminConn.Close() - downstreamConn := dialTCP(tlsconfig.MTLSClientConfig(downstreamSVID, ca.X509Bundle(), tlsconfig.AuthorizeID(serverID))) + downstreamConfig := tlsconfig.MTLSClientConfig(downstreamSVID, ca.X509Bundle(), tlsconfig.AuthorizeID(serverID)) + require.NoError(t, tlspolicy.ApplyPolicy(downstreamConfig, endpoints.TLSPolicy)) + downstreamConn := dialTCP(downstreamConfig) defer downstreamConn.Close() - federatedAdminConn := dialTCP(tlsconfig.MTLSClientConfig(foreignAdminSVID, ca.X509Bundle(), tlsconfig.AuthorizeID(serverID))) + federatedAdminConfig := tlsconfig.MTLSClientConfig(foreignAdminSVID, ca.X509Bundle(), tlsconfig.AuthorizeID(serverID)) + require.NoError(t, tlspolicy.ApplyPolicy(federatedAdminConfig, endpoints.TLSPolicy)) + federatedAdminConn := dialTCP(federatedAdminConfig) defer federatedAdminConn.Close() t.Run("Bad Client SVID", func(t *testing.T) { @@ -278,8 +297,12 @@ func TestListenAndServe(t *testing.T) { badSVID := testca.New(t, testTD).CreateX509SVID(agentID) ctx, cancel := context.WithTimeout(ctx, time.Second) defer cancel() + + tlsConfig := tlsconfig.MTLSClientConfig(badSVID, ca.X509Bundle(), tlsconfig.AuthorizeID(serverID)) + require.NoError(t, tlspolicy.ApplyPolicy(tlsConfig, endpoints.TLSPolicy)) + badConn, err := grpc.DialContext(ctx, endpoints.TCPAddr.String(), grpc.WithBlock(), grpc.FailOnNonTempDialError(true), //nolint: staticcheck // It is going to be resolved on #5152 - grpc.WithTransportCredentials(credentials.NewTLS(tlsconfig.MTLSClientConfig(badSVID, ca.X509Bundle(), tlsconfig.AuthorizeID(serverID)))), + grpc.WithTransportCredentials(credentials.NewTLS(tlsConfig)), ) if !assert.Error(t, err, "dialing should have failed") { // close the conn if the dialing unexpectedly succeeded @@ -331,6 +354,8 @@ func TestListenAndServe(t *testing.T) { unfederatedConfig := tlsconfig.MTLSClientConfig(unfederatedForeignAdminSVID, ca.X509Bundle(), tlsconfig.AuthorizeID(serverID)) for _, config := range []*tls.Config{unauthenticatedConfig, unauthorizedConfig, unfederatedConfig} { + require.NoError(t, tlspolicy.ApplyPolicy(config, endpoints.TLSPolicy)) + conn, err := grpc.NewClient(endpoints.TCPAddr.String(), grpc.WithTransportCredentials(credentials.NewTLS(config)), ) diff --git a/pkg/server/plugin/upstreamauthority/spire/spire.go b/pkg/server/plugin/upstreamauthority/spire/spire.go index 1df3d7513a..0e42fa4baf 100644 --- a/pkg/server/plugin/upstreamauthority/spire/spire.go +++ b/pkg/server/plugin/upstreamauthority/spire/spire.go @@ -19,6 +19,7 @@ import ( "github.com/spiffe/spire/pkg/common/coretypes/jwtkey" "github.com/spiffe/spire/pkg/common/coretypes/x509certificate" "github.com/spiffe/spire/pkg/common/idutil" + "github.com/spiffe/spire/pkg/common/tlspolicy" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" "google.golang.org/protobuf/proto" @@ -41,6 +42,7 @@ type Configuration struct { type experimentalConfig struct { WorkloadAPINamedPipeName string `hcl:"workload_api_named_pipe_name" json:"workload_api_named_pipe_name"` + PQKEMMode string `hcl:"pq_kem_mode" json:"pq_kem_mode"` } func BuiltIn() catalog.BuiltIn { @@ -123,7 +125,13 @@ func (p *Plugin) Configure(_ context.Context, req *configv1.ConfigureRequest) (* return nil, status.Errorf(codes.Internal, "unable to build server ID: %v", err) } - p.serverClient = newServerClient(serverID, serverAddr, workloadAPIAddr, p.log) + var tlsPolicy tlspolicy.Policy + tlsPolicy.PQKEMMode, err = tlspolicy.ParsePQKEMMode(p.log, p.config.Experimental.PQKEMMode) + if err != nil { + return nil, status.Errorf(codes.InvalidArgument, "invalid pq_kem_mode value: %v", err) + } + + p.serverClient = newServerClient(serverID, serverAddr, workloadAPIAddr, p.log, tlsPolicy) return &configv1.ConfigureResponse{}, nil } diff --git a/pkg/server/plugin/upstreamauthority/spire/spire_server_client.go b/pkg/server/plugin/upstreamauthority/spire/spire_server_client.go index 0ef93c43a9..e827ef9504 100644 --- a/pkg/server/plugin/upstreamauthority/spire/spire_server_client.go +++ b/pkg/server/plugin/upstreamauthority/spire/spire_server_client.go @@ -15,6 +15,7 @@ import ( bundlev1 "github.com/spiffe/spire-api-sdk/proto/spire/api/server/bundle/v1" svidv1 "github.com/spiffe/spire-api-sdk/proto/spire/api/server/svid/v1" "github.com/spiffe/spire-api-sdk/proto/spire/api/types" + "github.com/spiffe/spire/pkg/common/tlspolicy" "github.com/spiffe/spire/pkg/common/util" "github.com/spiffe/spire/pkg/common/x509util" "google.golang.org/grpc" @@ -24,12 +25,13 @@ import ( ) // newServerClient creates a new spire-server client -func newServerClient(serverID spiffeid.ID, serverAddr string, workloadAPIAddr net.Addr, log hclog.Logger) *serverClient { +func newServerClient(serverID spiffeid.ID, serverAddr string, workloadAPIAddr net.Addr, log hclog.Logger, tlsPolicy tlspolicy.Policy) *serverClient { return &serverClient{ serverID: serverID, serverAddr: serverAddr, workloadAPIAddr: workloadAPIAddr, log: &logAdapter{log: log}, + tlsPolicy: tlsPolicy, } } @@ -39,6 +41,7 @@ type serverClient struct { serverAddr string workloadAPIAddr net.Addr log logger.Logger + tlsPolicy tlspolicy.Policy mtx sync.RWMutex source *workloadapi.X509Source @@ -60,6 +63,12 @@ func (c *serverClient) start(ctx context.Context) error { } tlsConfig := tlsconfig.MTLSClientConfig(source, source, tlsconfig.AuthorizeID(c.serverID)) + err = tlspolicy.ApplyPolicy(tlsConfig, c.tlsPolicy) + if err != nil { + source.Close() + return status.Errorf(codes.Internal, "error applying TLS policy: %v", err) + } + conn, err := grpc.NewClient(c.serverAddr, grpc.WithTransportCredentials(credentials.NewTLS(tlsConfig))) if err != nil { diff --git a/pkg/server/server.go b/pkg/server/server.go index b24c86e754..c512239ac7 100644 --- a/pkg/server/server.go +++ b/pkg/server/server.go @@ -403,6 +403,7 @@ func (s *Server) newEndpointsServer(ctx context.Context, catalog catalog.Catalog BundleManager: bundleManager, AdminIDs: s.config.AdminIDs, UseLegacyDownstreamX509CATTL: s.config.UseLegacyDownstreamX509CATTL, + TLSPolicy: s.config.TLSPolicy, } if s.config.Federation.BundleEndpoint != nil { config.BundleEndpoint.Address = s.config.Federation.BundleEndpoint.Address diff --git a/test/fixture/config/server_good_posix.conf b/test/fixture/config/server_good_posix.conf index ae273f4c95..8da542c6b7 100644 --- a/test/fixture/config/server_good_posix.conf +++ b/test/fixture/config/server_good_posix.conf @@ -5,6 +5,9 @@ server { trust_domain = "example.org" log_level = "INFO" audit_log_enabled = true + experimental { + pq_kem_mode = "require" + } federation { bundle_endpoint { address = "0.0.0.0" diff --git a/test/fixture/config/server_good_windows.conf b/test/fixture/config/server_good_windows.conf index 3accbbc1ef..c46990cbd4 100644 --- a/test/fixture/config/server_good_windows.conf +++ b/test/fixture/config/server_good_windows.conf @@ -4,6 +4,9 @@ server { trust_domain = "example.org" log_level = "INFO" audit_log_enabled = true + experimental { + pq_kem_mode = "require" + } federation { bundle_endpoint { address = "0.0.0.0"