Skip to content
Draft
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ For details about compatibility between different releases, see the **Commitment

- Add tracing for LBS LNS and TTIGW protocol handlers.
- TTGC LBS Root CUPS claiming support.
- Validate that root CA certificates are actually CA certificates on startup of The Things Stack, rejecting leaf certificates configured as root CAs.

### Changed

Expand Down
36 changes: 36 additions & 0 deletions config/messages.json
Original file line number Diff line number Diff line change
Expand Up @@ -3671,6 +3671,33 @@
"file": "config.go"
}
},
"error:pkg/config/tlsconfig:not_ca_certificate": {
"translations": {
"en": "certificate with subject `{subject}` is not a CA certificate"
},
"description": {
"package": "pkg/config/tlsconfig",
"file": "config.go"
}
},
"error:pkg/config/tlsconfig:parse_certificate": {
"translations": {
"en": "parse certificate"
},
"description": {
"package": "pkg/config/tlsconfig",
"file": "config.go"
}
},
"error:pkg/config/tlsconfig:parse_pem": {
"translations": {
"en": "parse PEM"
},
"description": {
"package": "pkg/config/tlsconfig",
"file": "config.go"
}
},
"error:pkg/config/tlsconfig:tls_cipher_suite_invalid": {
"translations": {
"en": "invalid TLS cipher suite {cipher}"
Expand Down Expand Up @@ -3707,6 +3734,15 @@
"file": "config.go"
}
},
"error:pkg/config/tlsconfig:unexpected_pem_type": {
"translations": {
"en": "unexpected PEM block of type `{pem_type}`, expected CERTIFICATE"
},
"description": {
"package": "pkg/config/tlsconfig",
"file": "config.go"
}
},
"error:pkg/config:format": {
"translations": {
"en": "invalid format `{input}`"
Expand Down
48 changes: 47 additions & 1 deletion pkg/config/tlsconfig/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,11 @@
package tlsconfig

import (
"bytes"
"context"
"crypto/tls"
"crypto/x509"
"encoding/pem"
"os"
"strings"
"sync/atomic"
Expand Down Expand Up @@ -46,6 +48,14 @@ var (
errMissingACMEDir = errors.Define("missing_acme_dir", "missing ACME storage directory")
errMissingACMEEndpoint = errors.Define("missing_acme_endpoint", "missing ACME endpoint")
errMissingACMEDefaultHost = errors.Define("missing_acme_default_host", "missing ACME default host")
errParsePEM = errors.DefineInvalidArgument("parse_pem", "parse PEM")
errUnexpectedPEMType = errors.DefineInvalidArgument(
"unexpected_pem_type", "unexpected PEM block of type `{pem_type}`, expected CERTIFICATE",
)
errParseCertificate = errors.DefineInvalidArgument("parse_certificate", "parse certificate")
errNotCACertificate = errors.DefineInvalidArgument(
"not_ca_certificate", "certificate with subject `{subject}` is not a CA certificate",
)
)

// Initialize initializes the autocert manager for the ACME configuration.
Expand Down Expand Up @@ -190,17 +200,53 @@ func (c Client) ApplyTo(tlsConfig *tls.Config) error {
}

if len(rootCABytes) > 0 {
certs, err := parseAndValidateCACerts(rootCABytes)
if err != nil {
return err
}

if tlsConfig.RootCAs == nil {
if tlsConfig.RootCAs, err = x509.SystemCertPool(); err != nil {
tlsConfig.RootCAs = x509.NewCertPool()
}
}
tlsConfig.RootCAs.AppendCertsFromPEM(rootCABytes)

for _, cert := range certs {
tlsConfig.RootCAs.AddCert(cert)
}
}
tlsConfig.InsecureSkipVerify = c.InsecureSkipVerify
return nil
}

func parseAndValidateCACerts(pemBytes []byte) ([]*x509.Certificate, error) {
var certs []*x509.Certificate
for {
pemBytes = bytes.TrimSpace(pemBytes)
if len(pemBytes) == 0 {
break
}
block, rest := pem.Decode(pemBytes)
if block == nil {
return nil, errParsePEM.New()
}
if block.Type != "CERTIFICATE" {
return nil, errUnexpectedPEMType.WithAttributes("pem_type", block.Type)
}
pemBytes = rest

cert, err := x509.ParseCertificate(block.Bytes)
if err != nil {
return nil, errParseCertificate.WithCause(err)
}
if !cert.IsCA {
return nil, errNotCACertificate.WithAttributes("subject", cert.Subject.String())
}
certs = append(certs, cert)
}
return certs, nil
}

func readCert(fileReader FileReader, certFile, keyFile string) (*tls.Certificate, error) {
readFile := os.ReadFile
if fileReader != nil {
Expand Down
122 changes: 119 additions & 3 deletions pkg/config/tlsconfig/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ func (m mockFileReader) ReadFile(name string) ([]byte, error) {
return nil, fmt.Errorf("not found")
}

func genCert() (certPEM []byte, keyPEM []byte) {
func genCertWithOptions(isCA bool) (certPEM []byte, keyPEM []byte) {
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
panic(err)
Expand All @@ -60,10 +60,13 @@ func genCert() (certPEM []byte, keyPEM []byte) {
},
NotBefore: now,
NotAfter: now.Add(time.Hour),
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign,
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
BasicConstraintsValid: true,
IsCA: true,
IsCA: isCA,
}
if isCA {
cert.KeyUsage |= x509.KeyUsageCertSign
}
certBytes, err := x509.CreateCertificate(rand.Reader, cert, cert, key.Public(), key)
if err != nil {
Expand All @@ -80,6 +83,10 @@ func genCert() (certPEM []byte, keyPEM []byte) {
})
}

func genCert() (certPEM []byte, keyPEM []byte) {
return genCertWithOptions(true)
}

func TestApplyTLSClientConfig(t *testing.T) {
t.Parallel()
a := assertions.New(t)
Expand Down Expand Up @@ -107,6 +114,115 @@ func TestApplyTLSClientConfig(t *testing.T) {
})
}

func TestApplyTLSClientConfigRejectsLeafCert(t *testing.T) {
t.Parallel()
leafCert, _ := genCertWithOptions(false)
caCert, _ := genCertWithOptions(true)

t.Run("LeafCertificate", func(t *testing.T) {
t.Parallel()
a := assertions.New(t)
tlsConfig := &tls.Config{} //nolint:gosec
err := (&tlsconfig.Client{
FileReader: mockFileReader{
"leaf.pem": leafCert,
},
RootCA: "leaf.pem",
}).ApplyTo(tlsConfig)
a.So(err, should.NotBeNil)
a.So(err.Error(), should.ContainSubstring, "not a CA certificate")
})

t.Run("CACertificate", func(t *testing.T) {
t.Parallel()
a := assertions.New(t)
tlsConfig := &tls.Config{} //nolint:gosec
err := (&tlsconfig.Client{
FileReader: mockFileReader{
"ca.pem": caCert,
},
RootCA: "ca.pem",
}).ApplyTo(tlsConfig)
a.So(err, should.BeNil)
a.So(tlsConfig.RootCAs, should.NotBeNil)
})

t.Run("MultipleCertsOneLeaf", func(t *testing.T) {
t.Parallel()
a := assertions.New(t)
combined := append(caCert, leafCert...)
tlsConfig := &tls.Config{} //nolint:gosec
err := (&tlsconfig.Client{
FileReader: mockFileReader{
"mixed.pem": combined,
},
RootCA: "mixed.pem",
}).ApplyTo(tlsConfig)
a.So(err, should.NotBeNil)
a.So(err.Error(), should.ContainSubstring, "not a CA certificate")
})

t.Run("MultipleCACertificates", func(t *testing.T) {
t.Parallel()
a := assertions.New(t)
caCert2, _ := genCertWithOptions(true)
combined := append(caCert, caCert2...)
tlsConfig := &tls.Config{} //nolint:gosec
err := (&tlsconfig.Client{
FileReader: mockFileReader{
"cas.pem": combined,
},
RootCA: "cas.pem",
}).ApplyTo(tlsConfig)
a.So(err, should.BeNil)
a.So(tlsConfig.RootCAs, should.NotBeNil)
})

t.Run("InvalidPEM", func(t *testing.T) {
t.Parallel()
a := assertions.New(t)
tlsConfig := &tls.Config{} //nolint:gosec
err := (&tlsconfig.Client{
FileReader: mockFileReader{
"bad.pem": []byte("not a pem"),
},
RootCA: "bad.pem",
}).ApplyTo(tlsConfig)
a.So(err, should.NotBeNil)
a.So(err.Error(), should.ContainSubstring, "parse PEM")
})

t.Run("NonCertificatePEMBlock", func(t *testing.T) {
t.Parallel()
a := assertions.New(t)
_, keyPEM := genCertWithOptions(true)
tlsConfig := &tls.Config{} //nolint:gosec
err := (&tlsconfig.Client{
FileReader: mockFileReader{
"key.pem": keyPEM,
},
RootCA: "key.pem",
}).ApplyTo(tlsConfig)
a.So(err, should.NotBeNil)
a.So(err.Error(), should.ContainSubstring, "unexpected PEM block")
})

t.Run("TrailingNewlines", func(t *testing.T) {
t.Parallel()
a := assertions.New(t)
withNewlines := append(caCert, '\n', '\n', '\n')
tlsConfig := &tls.Config{} //nolint:gosec
err := (&tlsconfig.Client{
FileReader: mockFileReader{
"ca.pem": withNewlines,
},
RootCA: "ca.pem",
}).ApplyTo(tlsConfig)
a.So(err, should.BeNil)
a.So(tlsConfig.RootCAs, should.NotBeNil)
})
}

func TestApplyTLSServerAuth(t *testing.T) {
t.Parallel()
a := assertions.New(t)
Expand Down
4 changes: 4 additions & 0 deletions pkg/webui/locales/ja.json
Original file line number Diff line number Diff line change
Expand Up @@ -2296,10 +2296,14 @@
"error:pkg/config/tlsconfig:missing_acme_default_host": "",
"error:pkg/config/tlsconfig:missing_acme_dir": "ACEMの保存先が見つかりません",
"error:pkg/config/tlsconfig:missing_acme_endpoint": "ACMEエンドポイントが見つかりません",
"error:pkg/config/tlsconfig:not_ca_certificate": "",
"error:pkg/config/tlsconfig:parse_certificate": "",
"error:pkg/config/tlsconfig:parse_pem": "",
"error:pkg/config/tlsconfig:tls_cipher_suite_invalid": "無効なTLS暗号スイート{cipher}",
"error:pkg/config/tlsconfig:tls_config_source_invalid": "無効なTLS設定ソース `{source}`",
"error:pkg/config/tlsconfig:tls_key_vault_id": "無効なTLS鍵のID",
"error:pkg/config/tlsconfig:tls_source_empty": "TLSソースが空",
"error:pkg/config/tlsconfig:unexpected_pem_type": "",
"error:pkg/config:format": "無効なフォーマット `{input}`",
"error:pkg/config:missing_blob_config": "Blobストア設定が見つかりません",
"error:pkg/config:unknown_blob_provider": "無効なBlobストアプロバイダ `{provider}`",
Expand Down
Loading