Skip to content

Commit

Permalink
tls: add 'certs' option to TLS block for multi-certs support
Browse files Browse the repository at this point in the history
```
tls {
  certs = [
    {
      cert_file: "./configs/certs/srva-cert.pem"
      key_file:  "./configs/certs/srva-key.pem"
    },
    {
      cert_file: "./configs/certs/srvb-cert.pem"
      key_file:  "./configs/certs/srvb-key.pem"
    }
  ]
  ca_file: "./configs/certs/ca.pem"
  verify:  true
  timeout: 2
}
```

Signed-off-by: Waldemar Quevedo <wally@nats.io>
  • Loading branch information
wallyqs committed Dec 15, 2023
1 parent 1958e35 commit a3f98bc
Show file tree
Hide file tree
Showing 6 changed files with 299 additions and 26 deletions.
95 changes: 91 additions & 4 deletions server/config_check_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -298,7 +298,7 @@ func TestConfigCheck(t *testing.T) {
hello = "world"
}
`,
err: errors.New(`error parsing tls config, unknown field ["hello"]`),
err: errors.New(`error parsing tls config, unknown field "hello"`),
errorLine: 3,
errorPos: 5,
},
Expand All @@ -311,7 +311,7 @@ func TestConfigCheck(t *testing.T) {
}
}
`,
err: errors.New(`error parsing tls config, unknown field ["foo"]`),
err: errors.New(`error parsing tls config, unknown field "foo"`),
errorLine: 4,
errorPos: 7,
},
Expand All @@ -326,7 +326,7 @@ func TestConfigCheck(t *testing.T) {
preferences = []
}
`,
err: errors.New(`error parsing tls config, unknown field ["preferences"]`),
err: errors.New(`error parsing tls config, unknown field "preferences"`),
errorLine: 7,
errorPos: 7,
},
Expand All @@ -342,7 +342,7 @@ func TestConfigCheck(t *testing.T) {
suites = []
}
`,
err: errors.New(`error parsing tls config, unknown field ["suites"]`),
err: errors.New(`error parsing tls config, unknown field "suites"`),
errorLine: 8,
errorPos: 7,
},
Expand Down Expand Up @@ -1832,6 +1832,93 @@ func TestConfigCheck(t *testing.T) {
errorLine: 4,
errorPos: 6,
},
{
name: "TLS multiple certs",
config: `
port: -1
tls {
certs: [
{ cert_file: "configs/certs/server.pem", key_file: "configs/certs/key.pem"},
{ cert_file: "configs/certs/cert.new.pem", key_file: "configs/certs/key.new.pem"},
]
}
`,
err: nil,
},
{
name: "TLS multiple certs, bad type",
config: `
port: -1
tls {
certs: [
{ cert_file: "configs/certs/server.pem", key_file: 123 },
{ cert_file: "configs/certs/cert.new.pem", key_file: "configs/certs/key.new.pem"},
]
}
`,
err: fmt.Errorf("error parsing certificates config: unsupported type int64"),
errorLine: 5,
errorPos: 49,
},
{
name: "TLS multiple certs, missing key_file",
config: `
port: -1
tls {
certs: [
{ cert_file: "configs/certs/server.pem" }
{ cert_file: "configs/certs/cert.new.pem", key_file: "configs/certs/key.new.pem"}
]
}
`,
err: fmt.Errorf("error parsing certificates config: both 'cert_file' and 'cert_key' options are required"),
errorLine: 5,
errorPos: 10,
},
{
name: "TLS multiple certs and single cert options at the same time",
config: `
port: -1
tls {
cert_file: "configs/certs/server.pem"
key_file: "configs/certs/key.pem"
certs: [
{ cert_file: "configs/certs/server.pem", key_file: "configs/certs/key.pem"},
{ cert_file: "configs/certs/cert.new.pem", key_file: "configs/certs/key.new.pem"},
]
}
`,
err: fmt.Errorf("error parsing tls config, cannot combine 'cert_file' option with 'certs' option"),
errorLine: 3,
errorPos: 5,
},
{
name: "TLS multiple certs used but not configured, but cert_file configured",
config: `
port: -1
tls {
cert_file: "configs/certs/server.pem"
key_file: "configs/certs/key.pem"
certs: []
}
`,
err: nil,
},
{
name: "TLS multiple certs, missing bad path",
config: `
port: -1
tls {
certs: [
{ cert_file: "configs/certs/cert.new.pem", key_file: "configs/certs/key.new.pem"}
{ cert_file: "configs/certs/server.pem", key_file: "configs/certs/key.new.pom" }
]
}
`,
err: fmt.Errorf("error parsing X509 certificate/key pair 2/2: open configs/certs/key.new.pom: no such file or directory"),
errorLine: 3,
errorPos: 5,
},
}

checkConfig := func(config string) error {
Expand Down
13 changes: 13 additions & 0 deletions server/configs/reload/tls_multi_cert_1.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Simple TLS config file

listen: 127.0.0.1:-1

tls {
certs = [
{
cert_file: "../test/configs/certs/srva-cert.pem"
key_file: "../test/configs/certs/srva-key.pem"
}
]
timeout: 2
}
19 changes: 19 additions & 0 deletions server/configs/reload/tls_multi_cert_2.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Simple TLS config file

listen: 127.0.0.1:-1

tls {
certs = [
{
cert_file: "../test/configs/certs/srva-cert.pem"
key_file: "../test/configs/certs/srva-key.pem"
},
{
cert_file: "../test/configs/certs/srvb-cert.pem"
key_file: "../test/configs/certs/srvb-key.pem"
}
]
ca_file: "../test/configs/certs/ca.pem"
verify: true
timeout: 2
}
13 changes: 13 additions & 0 deletions server/configs/reload/tls_multi_cert_3.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Simple TLS config file

listen: 127.0.0.1:-1

tls {
certs = [
{
cert_file: "../test/configs/certs/srvb-cert.pem"
key_file: "../test/configs/certs/srvb-key.pem"
}
]
timeout: 2
}
89 changes: 67 additions & 22 deletions server/opts.go
Original file line number Diff line number Diff line change
Expand Up @@ -669,6 +669,13 @@ type TLSConfigOpts struct {
CertMatchBy certstore.MatchByType
CertMatch string
OCSPPeerConfig *certidp.OCSPPeerConfig
Certificates []*TLSCertPairOpt
}

// TLSCertPairOpt are the paths to a certificate and private key.
type TLSCertPairOpt struct {
CertFile string
KeyFile string
}

// OCSPConfig represents the options of OCSP stapling options.
Expand Down Expand Up @@ -4180,7 +4187,7 @@ func parseTLS(v interface{}, isClientCtx bool) (t *TLSConfigOpts, retErr error)
)
defer convertPanicToError(&lt, &retErr)

_, v = unwrapValue(v, &lt)
tk, v := unwrapValue(v, &lt)
tlsm = v.(map[string]interface{})
for mk, mv := range tlsm {
tk, mv := unwrapValue(mv, &lt)
Expand Down Expand Up @@ -4381,10 +4388,46 @@ func parseTLS(v interface{}, isClientCtx bool) (t *TLSConfigOpts, retErr error)
default:
return nil, &configErr{tk, fmt.Sprintf("error parsing ocsp peer config: unsupported type %T", v)}
}
case "certs", "certificates":
certs, ok := mv.([]interface{})
if !ok {
return nil, &configErr{tk, fmt.Sprintf("error parsing certificates config: unsupported type %T", v)}
}
tc.Certificates = make([]*TLSCertPairOpt, len(certs))
for i, v := range certs {
tk, vv := unwrapValue(v, &lt)
pair, ok := vv.(map[string]interface{})
if !ok {
return nil, &configErr{tk, fmt.Sprintf("error parsing certificates config: unsupported type %T", vv)}
}
certPair := &TLSCertPairOpt{}
for k, v := range pair {
tk, vv = unwrapValue(v, &lt)
file, ok := vv.(string)
if !ok {
return nil, &configErr{tk, fmt.Sprintf("error parsing certificates config: unsupported type %T", vv)}
}
switch k {
case "cert_file":
certPair.CertFile = file
case "key_file":
certPair.KeyFile = file
default:
return nil, &configErr{tk, fmt.Sprintf("error parsing tls certs config, unknown field %q", k)}
}
}
if certPair.CertFile == _EMPTY_ || certPair.KeyFile == _EMPTY_ {
return nil, &configErr{tk, "error parsing certificates config: both 'cert_file' and 'cert_key' options are required"}
}
tc.Certificates[i] = certPair
}
default:
return nil, &configErr{tk, fmt.Sprintf("error parsing tls config, unknown field [%q]", mk)}
return nil, &configErr{tk, fmt.Sprintf("error parsing tls config, unknown field %q", mk)}
}
}
if len(tc.Certificates) > 0 && tc.CertFile != _EMPTY_ {
return nil, &configErr{tk, "error parsing tls config, cannot combine 'cert_file' option with 'certs' option"}
}

// If cipher suites were not specified then use the defaults
if tc.Ciphers == nil {
Expand Down Expand Up @@ -4681,33 +4724,35 @@ func GenTLSConfig(tc *TLSConfigOpts) (*tls.Config, error) {
case tc.CertFile == _EMPTY_ && tc.KeyFile != _EMPTY_:
return nil, fmt.Errorf("missing 'cert_file' in TLS configuration")
case tc.CertFile != _EMPTY_ && tc.KeyFile != _EMPTY_:
// To support multiple certs (for SANs) without rearchitecting our
// configuration, we accept a list, separated by the platform list
// separator; both specs must contain the same count of entries.
// (So, colon-separated on Unix, semi-colon on Windows).
certFiles := strings.Split(tc.CertFile, string(os.PathListSeparator))
keyFiles := strings.Split(tc.KeyFile, string(os.PathListSeparator))
if len(certFiles) != len(keyFiles) {
return nil, fmt.Errorf("TLS configuration has %d entries in cert_file but %d in key_file", len(certFiles), len(keyFiles))
}
config.Certificates = make([]tls.Certificate, len(certFiles))
// Now load in cert and private keys
for i := range certFiles {
cert, err := tls.LoadX509KeyPair(certFiles[i], keyFiles[i])
// Now load in cert and private key
cert, err := tls.LoadX509KeyPair(tc.CertFile, tc.KeyFile)
if err != nil {
return nil, fmt.Errorf("error parsing X509 certificate/key pair: %v", err)
}
cert.Leaf, err = x509.ParseCertificate(cert.Certificate[0])
if err != nil {
return nil, fmt.Errorf("error parsing certificate: %v", err)
}
config.Certificates = []tls.Certificate{cert}
case tc.CertStore != certstore.STOREEMPTY:
err := certstore.TLSConfig(tc.CertStore, tc.CertMatchBy, tc.CertMatch, &config)
if err != nil {
return nil, err
}
case tc.Certificates != nil:
// Multiple certificate support.
config.Certificates = make([]tls.Certificate, len(tc.Certificates))
for i, certPair := range tc.Certificates {
cert, err := tls.LoadX509KeyPair(certPair.CertFile, certPair.KeyFile)
if err != nil {
return nil, fmt.Errorf("error parsing X509 certificate/key pair: %v", err)
return nil, fmt.Errorf("error parsing X509 certificate/key pair %d/%d: %v", i+1, len(tc.Certificates), err)
}
cert.Leaf, err = x509.ParseCertificate(cert.Certificate[0])
if err != nil {
return nil, fmt.Errorf("error parsing certificate: %v", err)
return nil, fmt.Errorf("error parsing certificate %d/%d: %v", i+1, len(tc.Certificates), err)
}
config.Certificates[i] = cert
}
case tc.CertStore != certstore.STOREEMPTY:
err := certstore.TLSConfig(tc.CertStore, tc.CertMatchBy, tc.CertMatch, &config)
if err != nil {
return nil, err
}
}

// Require client certificates as needed
Expand Down

0 comments on commit a3f98bc

Please sign in to comment.