Skip to content

Commit 7747687

Browse files
committed
Add support for local TLS.
1 parent ba05db9 commit 7747687

File tree

3 files changed

+177
-2
lines changed

3 files changed

+177
-2
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ environment variables that you can set.
7575
| Variable Name | Description | Default Value |
7676
|-----------------------------|---------------------------------------------------------|---------------|
7777
| `TLS_DOMAIN` | Comma-separated list of domain names to use for TLS provisioning. If not set, TLS will be disabled. | None |
78+
| `TLS_LOCAL` | Whether to use a self-signed certificate authority for TLS certificate provisioning. | Disabled |
7879
| `TARGET_PORT` | The port that your Puma server should run on. Thruster will set `PORT` to this value when starting your server. | 3000 |
7980
| `CACHE_SIZE` | The size of the HTTP cache in bytes. | 64MB |
8081
| `MAX_CACHE_ITEM_SIZE` | The maximum size of a single item in the HTTP cache in bytes. | 1MB |

internal/config.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ type Config struct {
4848
MaxRequestBody int
4949

5050
TLSDomains []string
51+
TLSLocal bool
5152
ACMEDirectoryURL string
5253
EAB_KID string
5354
EAB_HMACKey string
@@ -87,6 +88,7 @@ func NewConfig() (*Config, error) {
8788
MaxRequestBody: getEnvInt("MAX_REQUEST_BODY", defaultMaxRequestBody),
8889

8990
TLSDomains: getEnvStrings("TLS_DOMAIN", []string{}),
91+
TLSLocal: getEnvBool("TLS_LOCAL", false),
9092
ACMEDirectoryURL: getEnvString("ACME_DIRECTORY", defaultACMEDirectoryURL),
9193
EAB_KID: getEnvString("EAB_KID", ""),
9294
EAB_HMACKey: getEnvString("EAB_HMAC_KEY", ""),
@@ -108,7 +110,7 @@ func NewConfig() (*Config, error) {
108110
}
109111

110112
func (c *Config) HasTLS() bool {
111-
return len(c.TLSDomains) > 0
113+
return len(c.TLSDomains) > 0 || c.TLSLocal
112114
}
113115

114116
func findEnv(key string) (string, bool) {

internal/server.go

Lines changed: 173 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,21 @@ import (
77
"log/slog"
88
"net"
99
"net/http"
10+
"os"
1011
"time"
1112

13+
"crypto/rand"
14+
"crypto/rsa"
15+
"crypto/tls"
16+
"crypto/x509"
17+
"crypto/x509/pkix"
18+
"encoding/pem"
19+
"errors"
20+
"math/big"
21+
1222
"golang.org/x/crypto/acme"
1323
"golang.org/x/crypto/acme/autocert"
24+
"golang.org/x/net/idna"
1425
)
1526

1627
type Server struct {
@@ -38,7 +49,13 @@ func (s *Server) Start() {
3849
s.httpServer.Handler = manager.HTTPHandler(http.HandlerFunc(httpRedirectHandler))
3950

4051
s.httpsServer = s.defaultHttpServer(httpsAddress)
41-
s.httpsServer.TLSConfig = manager.TLSConfig()
52+
53+
if s.config.TLSLocal {
54+
s.httpsServer.TLSConfig = s.localTLSConfig()
55+
} else {
56+
s.httpsServer.TLSConfig = manager.TLSConfig()
57+
}
58+
4259
s.httpsServer.Handler = s.handler
4360

4461
go s.httpServer.ListenAndServe()
@@ -84,6 +101,161 @@ func (s *Server) certManager() *autocert.Manager {
84101
}
85102
}
86103

104+
func (s *Server) localTLSConfig() *tls.Config {
105+
return &tls.Config{
106+
GetCertificate: s.getLocalCertificate,
107+
NextProtos: []string{
108+
"h2", "http/1.1", // enable HTTP/2
109+
},
110+
}
111+
}
112+
113+
func (s *Server) getLocalCertificate(hello *tls.ClientHelloInfo) (*tls.Certificate, error) {
114+
name := hello.ServerName
115+
if name == "" {
116+
return nil, errors.New("thruster_local_tls: missing server name")
117+
}
118+
119+
name, err := idna.Lookup.ToASCII(name)
120+
if err != nil {
121+
return nil, errors.New("thruster/local_tls: server name contains invalid character")
122+
}
123+
124+
keyUsage := x509.KeyUsageDigitalSignature
125+
keyUsage |= x509.KeyUsageKeyEncipherment
126+
127+
serialNumber, err := rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), 128))
128+
if err != nil {
129+
return nil, err
130+
}
131+
132+
template := x509.Certificate{
133+
SerialNumber: serialNumber,
134+
Subject: pkix.Name{
135+
Organization: []string{"Thruster Local"},
136+
},
137+
NotBefore: time.Now(),
138+
NotAfter: time.Now().Add(365 * 10 * 24 * time.Hour),
139+
KeyUsage: keyUsage,
140+
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
141+
BasicConstraintsValid: true,
142+
}
143+
144+
if ip := net.ParseIP(name); ip != nil {
145+
template.IPAddresses = append(template.IPAddresses, ip)
146+
} else {
147+
template.DNSNames = append(template.DNSNames, name)
148+
}
149+
150+
authority, err := s.getLocalAuthority()
151+
if err != nil {
152+
return nil, err
153+
}
154+
155+
priv, err := rsa.GenerateKey(rand.Reader, 2048)
156+
if err != nil {
157+
return nil, err
158+
}
159+
160+
authcert, err := x509.ParseCertificate(authority.Certificate[0])
161+
if err != nil {
162+
return nil, err
163+
}
164+
165+
derBytes, err := x509.CreateCertificate(rand.Reader, &template, authcert, &priv.PublicKey, authority.PrivateKey)
166+
if err != nil {
167+
return nil, err
168+
}
169+
170+
cert := &tls.Certificate{
171+
Certificate: [][]byte{authority.Certificate[0], derBytes},
172+
PrivateKey: authority.PrivateKey,
173+
}
174+
175+
return cert, nil
176+
}
177+
178+
func (s *Server) getLocalAuthority() (*tls.Certificate, error) {
179+
180+
cert, err := tls.LoadX509KeyPair(fmt.Sprintf("%s/authority.crt", s.config.StoragePath), fmt.Sprintf("%s/authority.pem", s.config.StoragePath))
181+
if err == nil {
182+
return &cert, nil
183+
}
184+
185+
err = os.Mkdir(s.config.StoragePath, 0750)
186+
187+
keyUsage := x509.KeyUsageDigitalSignature
188+
keyUsage |= x509.KeyUsageKeyEncipherment
189+
keyUsage |= x509.KeyUsageCertSign
190+
191+
serialNumber, err := rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), 128))
192+
if err != nil {
193+
return nil, err
194+
}
195+
196+
template := x509.Certificate{
197+
SerialNumber: serialNumber,
198+
Subject: pkix.Name{
199+
Organization: []string{"Thruster Local CA"},
200+
},
201+
NotBefore: time.Now(),
202+
NotAfter: time.Now().Add(365 * 10 * 24 * time.Hour),
203+
KeyUsage: keyUsage,
204+
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
205+
BasicConstraintsValid: true,
206+
IsCA: true,
207+
}
208+
209+
priv, err := rsa.GenerateKey(rand.Reader, 2048)
210+
if err != nil {
211+
return nil, err
212+
}
213+
214+
derBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, &priv.PublicKey, priv)
215+
if err != nil {
216+
return nil, err
217+
}
218+
219+
certOut, err := os.Create(fmt.Sprintf("%s/authority.crt", s.config.StoragePath))
220+
if err != nil {
221+
return nil, err
222+
}
223+
224+
if err := pem.Encode(certOut, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes}); err != nil {
225+
return nil, err
226+
}
227+
228+
if err := certOut.Close(); err != nil {
229+
return nil, err
230+
}
231+
232+
keyOut, err := os.Create(fmt.Sprintf("%s/authority.pem", s.config.StoragePath))
233+
if err != nil {
234+
return nil, err
235+
}
236+
237+
privBytes, err := x509.MarshalPKCS8PrivateKey(priv)
238+
239+
if err != nil {
240+
return nil, err
241+
}
242+
243+
if err := pem.Encode(keyOut, &pem.Block{Type: "PRIVATE KEY", Bytes: privBytes}); err != nil {
244+
return nil, err
245+
}
246+
247+
if err := keyOut.Close(); err != nil {
248+
return nil, err
249+
}
250+
251+
cer := tls.Certificate{
252+
Certificate: [][]byte{derBytes},
253+
PrivateKey: priv,
254+
}
255+
256+
return &cer, nil
257+
}
258+
87259
func (s *Server) externalAccountBinding() *acme.ExternalAccountBinding {
88260
if s.config.EAB_KID == "" || s.config.EAB_HMACKey == "" {
89261
return nil

0 commit comments

Comments
 (0)