diff --git a/server/opts.go b/server/opts.go index 0d1848b3a6..6bf2cc8ef3 100644 --- a/server/opts.go +++ b/server/opts.go @@ -729,6 +729,7 @@ type TLSConfigOpts struct { CaCertsMatch []string OCSPPeerConfig *certidp.OCSPPeerConfig Certificates []*TLSCertPairOpt + MinVersion uint16 } // TLSCertPairOpt are the paths to a certificate and private key. @@ -4560,6 +4561,24 @@ func parseCurvePreferences(curveName string) (tls.CurveID, error) { return curve, nil } +func parseTLSVersion(v any) (uint16, error) { + var tlsVersionNumber uint16 + switch v := v.(type) { + case string: + n, err := tlsVersionFromString(v) + if err != nil { + return 0, err + } + tlsVersionNumber = n + default: + return 0, fmt.Errorf("'min_version' wrong type: %v", v) + } + if tlsVersionNumber < tls.VersionTLS12 { + return 0, fmt.Errorf("unsupported TLS version: %s", tls.VersionName(tlsVersionNumber)) + } + return tlsVersionNumber, nil +} + // Helper function to parse TLS configs. func parseTLS(v any, isClientCtx bool) (t *TLSConfigOpts, retErr error) { var ( @@ -4825,6 +4844,12 @@ func parseTLS(v any, isClientCtx bool) (t *TLSConfigOpts, retErr error) { } tc.Certificates[i] = certPair } + case "min_version": + minVersion, err := parseTLSVersion(mv) + if err != nil { + return nil, &configErr{tk, fmt.Sprintf("error parsing tls config: %v", err)} + } + tc.MinVersion = minVersion default: return nil, &configErr{tk, fmt.Sprintf("error parsing tls config, unknown field %q", mk)} } @@ -5199,6 +5224,13 @@ func GenTLSConfig(tc *TLSConfigOpts) (*tls.Config, error) { } config.ClientCAs = pool } + // Allow setting TLS minimum version. + if tc.MinVersion > 0 { + if tc.MinVersion < tls.VersionTLS12 { + return nil, fmt.Errorf("unsupported minimum TLS version: %s", tls.VersionName(tc.MinVersion)) + } + config.MinVersion = tc.MinVersion + } return &config, nil } diff --git a/server/reload.go b/server/reload.go index 80efcbb4c5..6d9af46278 100644 --- a/server/reload.go +++ b/server/reload.go @@ -1151,7 +1151,7 @@ func imposeOrder(value any) error { slices.SortFunc(value.Gateways, func(i, j *RemoteGatewayOpts) int { return cmp.Compare(i.Name, j.Name) }) case WebsocketOpts: slices.Sort(value.AllowedOrigins) - case string, bool, uint8, int, int32, int64, time.Duration, float64, nil, LeafNodeOpts, ClusterOpts, *tls.Config, PinnedCertSet, + case string, bool, uint8, uint16, int, int32, int64, time.Duration, float64, nil, LeafNodeOpts, ClusterOpts, *tls.Config, PinnedCertSet, *URLAccResolver, *MemAccResolver, *DirAccResolver, *CacheDirAccResolver, Authentication, MQTTOpts, jwt.TagList, *OCSPConfig, map[string]string, JSLimitOpts, StoreCipher, *OCSPResponseCacheConfig: // explicitly skipped types diff --git a/server/server.go b/server/server.go index 120f70db6b..b198e24891 100644 --- a/server/server.go +++ b/server/server.go @@ -3506,6 +3506,20 @@ func tlsVersion(ver uint16) string { return fmt.Sprintf("Unknown [0x%x]", ver) } +func tlsVersionFromString(ver string) (uint16, error) { + switch ver { + case "1.0": + return tls.VersionTLS10, nil + case "1.1": + return tls.VersionTLS11, nil + case "1.2": + return tls.VersionTLS12, nil + case "1.3": + return tls.VersionTLS13, nil + } + return 0, fmt.Errorf("Unknown version: %v", ver) +} + // We use hex here so we don't need multiple versions func tlsCipher(cs uint16) string { name, present := cipherMapByID[cs] diff --git a/server/server_test.go b/server/server_test.go index e0b6d6e77d..bb300af4d9 100644 --- a/server/server_test.go +++ b/server/server_test.go @@ -217,7 +217,102 @@ func TestTLSVersions(t *testing.T) { } } -func TestTlsCipher(t *testing.T) { +func TestTLSMinVersionConfig(t *testing.T) { + tmpl := ` + listen: "127.0.0.1:-1" + tls { + cert_file: "../test/configs/certs/server-cert.pem" + key_file: "../test/configs/certs/server-key.pem" + timeout: 1 + min_version: %s + } + ` + conf := createConfFile(t, []byte(fmt.Sprintf(tmpl, `"1.3"`))) + s, o := RunServerWithConfig(conf) + defer s.Shutdown() + + connect := func(t *testing.T, tlsConf *tls.Config, expectedErr error) { + t.Helper() + opts := []nats.Option{} + if tlsConf != nil { + opts = append(opts, nats.Secure(tlsConf)) + } + opts = append(opts, nats.RootCAs("../test/configs/certs/ca.pem")) + nc, err := nats.Connect(fmt.Sprintf("tls://localhost:%d", o.Port), opts...) + if expectedErr == nil { + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + } else if err == nil || err.Error() != expectedErr.Error() { + nc.Close() + t.Fatalf("Expected error %v, got: %v", expectedErr, err) + } + } + + // Cannot connect with client requiring a lower minimum TLS Version. + connect(t, &tls.Config{ + MaxVersion: tls.VersionTLS12, + }, errors.New(`remote error: tls: protocol version not supported`)) + + // Should connect since matching minimum TLS version. + connect(t, &tls.Config{ + MinVersion: tls.VersionTLS13, + }, nil) + + // Reloading with invalid values should fail. + if err := os.WriteFile(conf, []byte(fmt.Sprintf(tmpl, `"1.0"`)), 0666); err != nil { + t.Fatalf("Error creating config file: %v", err) + } + if err := s.Reload(); err == nil { + t.Fatalf("Expected reload to fail: %v", err) + } + + // Reloading with original values and no changes should be ok. + if err := os.WriteFile(conf, []byte(fmt.Sprintf(tmpl, `"1.3"`)), 0666); err != nil { + t.Fatalf("Error creating config file: %v", err) + } + if err := s.Reload(); err != nil { + t.Fatalf("Unexpected error reloading TLS version: %v", err) + } + + // Reloading with a new minimum lower version. + if err := os.WriteFile(conf, []byte(fmt.Sprintf(tmpl, `"1.2"`)), 0666); err != nil { + t.Fatalf("Error creating config file: %v", err) + } + if err := s.Reload(); err != nil { + t.Fatalf("Unexpected error reloading: %v", err) + } + + // Should connect since now matching minimum TLS version. + connect(t, &tls.Config{ + MaxVersion: tls.VersionTLS12, + }, nil) + connect(t, &tls.Config{ + MinVersion: tls.VersionTLS13, + }, nil) + + // Setting unsupported TLS versions + if err := os.WriteFile(conf, []byte(fmt.Sprintf(tmpl, `"1.4"`)), 0666); err != nil { + t.Fatalf("Error creating config file: %v", err) + } + if err := s.Reload(); err == nil || !strings.Contains(err.Error(), `Unknown version: 1.4`) { + t.Fatalf("Unexpected error reloading: %v", err) + } + + tc := &TLSConfigOpts{ + CertFile: "../test/configs/certs/server-cert.pem", + KeyFile: "../test/configs/certs/server-key.pem", + CaFile: "../test/configs/certs/ca.pem", + Timeout: 4.0, + MinVersion: tls.VersionTLS11, + } + _, err := GenTLSConfig(tc) + if err == nil || err.Error() != `unsupported minimum TLS version: TLS 1.1` { + t.Fatalf("Expected error generating TLS config: %v", err) + } +} + +func TestTLSCipher(t *testing.T) { if strings.Compare(tlsCipher(0x0005), "TLS_RSA_WITH_RC4_128_SHA") != 0 { t.Fatalf("Invalid tls cipher") }