Skip to content

Commit e87d885

Browse files
authored
feat: tls (#330)
2 parents 934bb68 + 4571362 commit e87d885

File tree

11 files changed

+846
-117
lines changed

11 files changed

+846
-117
lines changed

config.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ type Config struct {
9090
DisplayMaxBrightness int `json:"display_max_brightness"`
9191
DisplayDimAfterSec int `json:"display_dim_after_sec"`
9292
DisplayOffAfterSec int `json:"display_off_after_sec"`
93-
TLSMode string `json:"tls_mode"`
93+
TLSMode string `json:"tls_mode"` // options: "self-signed", "user-defined", ""
9494
UsbConfig *usbgadget.Config `json:"usb_config"`
9595
UsbDevices *usbgadget.Devices `json:"usb_devices"`
9696
}
@@ -169,6 +169,8 @@ func SaveConfig() error {
169169
configLock.Lock()
170170
defer configLock.Unlock()
171171

172+
logger.Trace().Str("path", configPath).Msg("Saving config")
173+
172174
file, err := os.Create(configPath)
173175
if err != nil {
174176
return fmt.Errorf("failed to create config file: %w", err)

internal/websecure/log.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package websecure
2+
3+
import (
4+
"os"
5+
6+
"github.com/rs/zerolog"
7+
)
8+
9+
var defaultLogger = zerolog.New(os.Stdout).With().Str("component", "websecure").Logger()

internal/websecure/selfsign.go

Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
package websecure
2+
3+
import (
4+
"crypto/ecdsa"
5+
"crypto/elliptic"
6+
"crypto/rand"
7+
"crypto/tls"
8+
"crypto/x509"
9+
"crypto/x509/pkix"
10+
"net"
11+
"strings"
12+
"time"
13+
14+
"github.com/rs/zerolog"
15+
"golang.org/x/net/idna"
16+
)
17+
18+
const selfSignerCAMagicName = "__ca__"
19+
20+
type SelfSigner struct {
21+
store *CertStore
22+
log *zerolog.Logger
23+
24+
caInfo pkix.Name
25+
26+
DefaultDomain string
27+
DefaultOrg string
28+
DefaultOU string
29+
}
30+
31+
func NewSelfSigner(
32+
store *CertStore,
33+
log *zerolog.Logger,
34+
defaultDomain,
35+
defaultOrg,
36+
defaultOU,
37+
caName string,
38+
) *SelfSigner {
39+
return &SelfSigner{
40+
store: store,
41+
log: log,
42+
DefaultDomain: defaultDomain,
43+
DefaultOrg: defaultOrg,
44+
DefaultOU: defaultOU,
45+
caInfo: pkix.Name{
46+
CommonName: caName,
47+
Organization: []string{defaultOrg},
48+
OrganizationalUnit: []string{defaultOU},
49+
},
50+
}
51+
}
52+
53+
func (s *SelfSigner) getCA() *tls.Certificate {
54+
return s.createSelfSignedCert(selfSignerCAMagicName)
55+
}
56+
57+
func (s *SelfSigner) createSelfSignedCert(hostname string) *tls.Certificate {
58+
if tlsCert := s.store.certificates[hostname]; tlsCert != nil {
59+
return tlsCert
60+
}
61+
62+
// check if hostname is the CA magic name
63+
var ca *tls.Certificate
64+
if hostname != selfSignerCAMagicName {
65+
ca = s.getCA()
66+
if ca == nil {
67+
s.log.Error().Msg("Failed to get CA certificate")
68+
return nil
69+
}
70+
}
71+
72+
s.log.Info().Str("hostname", hostname).Msg("Creating self-signed certificate")
73+
74+
// lock the store while creating the certificate (do not move upwards)
75+
s.store.certLock.Lock()
76+
defer s.store.certLock.Unlock()
77+
78+
priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
79+
if err != nil {
80+
s.log.Error().Err(err).Msg("Failed to generate private key")
81+
return nil
82+
}
83+
84+
notBefore := time.Now()
85+
notAfter := notBefore.AddDate(1, 0, 0)
86+
87+
serialNumber, err := generateSerialNumber()
88+
if err != nil {
89+
s.log.Error().Err(err).Msg("Failed to generate serial number")
90+
return nil
91+
}
92+
93+
dnsName := hostname
94+
ip := net.ParseIP(hostname)
95+
if ip != nil {
96+
dnsName = s.DefaultDomain
97+
}
98+
99+
// set up CSR
100+
isCA := hostname == selfSignerCAMagicName
101+
subject := pkix.Name{
102+
CommonName: hostname,
103+
Organization: []string{s.DefaultOrg},
104+
OrganizationalUnit: []string{s.DefaultOU},
105+
}
106+
keyUsage := x509.KeyUsageDigitalSignature
107+
extKeyUsage := []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}
108+
109+
// check if hostname is the CA magic name, and if so, set the subject to the CA info
110+
if isCA {
111+
subject = s.caInfo
112+
keyUsage |= x509.KeyUsageCertSign
113+
extKeyUsage = append(extKeyUsage, x509.ExtKeyUsageClientAuth)
114+
notAfter = notBefore.AddDate(10, 0, 0)
115+
}
116+
117+
cert := x509.Certificate{
118+
SerialNumber: serialNumber,
119+
Subject: subject,
120+
NotBefore: notBefore,
121+
NotAfter: notAfter,
122+
IsCA: isCA,
123+
KeyUsage: keyUsage,
124+
ExtKeyUsage: extKeyUsage,
125+
BasicConstraintsValid: true,
126+
}
127+
128+
// set up DNS names and IP addresses
129+
if !isCA {
130+
cert.DNSNames = []string{dnsName}
131+
if ip != nil {
132+
cert.IPAddresses = []net.IP{ip}
133+
}
134+
}
135+
136+
// set up parent certificate
137+
parent := &cert
138+
parentPriv := priv
139+
if ca != nil {
140+
parent, err = x509.ParseCertificate(ca.Certificate[0])
141+
if err != nil {
142+
s.log.Error().Err(err).Msg("Failed to parse parent certificate")
143+
return nil
144+
}
145+
parentPriv = ca.PrivateKey.(*ecdsa.PrivateKey)
146+
}
147+
148+
certBytes, err := x509.CreateCertificate(rand.Reader, &cert, parent, &priv.PublicKey, parentPriv)
149+
if err != nil {
150+
s.log.Error().Err(err).Msg("Failed to create certificate")
151+
return nil
152+
}
153+
154+
tlsCert := &tls.Certificate{
155+
Certificate: [][]byte{certBytes},
156+
PrivateKey: priv,
157+
}
158+
if ca != nil {
159+
tlsCert.Certificate = append(tlsCert.Certificate, ca.Certificate...)
160+
}
161+
162+
s.store.certificates[hostname] = tlsCert
163+
s.store.saveCertificate(hostname)
164+
165+
return tlsCert
166+
}
167+
168+
// GetCertificate returns the certificate for the given hostname
169+
// returns nil if the certificate is not found
170+
func (s *SelfSigner) GetCertificate(info *tls.ClientHelloInfo) (*tls.Certificate, error) {
171+
var hostname string
172+
if info.ServerName != "" && info.ServerName != selfSignerCAMagicName {
173+
hostname = info.ServerName
174+
} else {
175+
hostname = strings.Split(info.Conn.LocalAddr().String(), ":")[0]
176+
}
177+
178+
s.log.Info().Str("hostname", hostname).Strs("supported_protos", info.SupportedProtos).Msg("TLS handshake")
179+
180+
// convert hostname to punycode
181+
h, err := idna.Lookup.ToASCII(hostname)
182+
if err != nil {
183+
s.log.Warn().Str("hostname", hostname).Err(err).Str("remote_addr", info.Conn.RemoteAddr().String()).Msg("Hostname is not valid")
184+
hostname = s.DefaultDomain
185+
} else {
186+
hostname = h
187+
}
188+
189+
cert := s.createSelfSignedCert(hostname)
190+
return cert, nil
191+
}

internal/websecure/store.go

Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
package websecure
2+
3+
import (
4+
"crypto/tls"
5+
"fmt"
6+
"os"
7+
"path"
8+
"strings"
9+
"sync"
10+
11+
"github.com/rs/zerolog"
12+
)
13+
14+
type CertStore struct {
15+
certificates map[string]*tls.Certificate
16+
certLock *sync.Mutex
17+
18+
storePath string
19+
20+
log *zerolog.Logger
21+
}
22+
23+
func NewCertStore(storePath string, log *zerolog.Logger) *CertStore {
24+
if log == nil {
25+
log = &defaultLogger
26+
}
27+
28+
return &CertStore{
29+
certificates: make(map[string]*tls.Certificate),
30+
certLock: &sync.Mutex{},
31+
32+
storePath: storePath,
33+
log: log,
34+
}
35+
}
36+
37+
func (s *CertStore) ensureStorePath() error {
38+
// check if directory exists
39+
stat, err := os.Stat(s.storePath)
40+
if err == nil {
41+
if stat.IsDir() {
42+
return nil
43+
}
44+
45+
return fmt.Errorf("TLS store path exists but is not a directory: %s", s.storePath)
46+
}
47+
48+
if os.IsNotExist(err) {
49+
s.log.Trace().Str("path", s.storePath).Msg("TLS store directory does not exist, creating directory")
50+
err = os.MkdirAll(s.storePath, 0755)
51+
if err != nil {
52+
return fmt.Errorf("Failed to create TLS store path: %w", err)
53+
}
54+
return nil
55+
}
56+
57+
return fmt.Errorf("Failed to check TLS store path: %w", err)
58+
}
59+
60+
func (s *CertStore) LoadCertificates() {
61+
err := s.ensureStorePath()
62+
if err != nil {
63+
s.log.Error().Err(err).Msg("Failed to ensure store path")
64+
return
65+
}
66+
67+
files, err := os.ReadDir(s.storePath)
68+
if err != nil {
69+
s.log.Error().Err(err).Msg("Failed to read TLS directory")
70+
return
71+
}
72+
73+
for _, file := range files {
74+
if file.IsDir() {
75+
continue
76+
}
77+
78+
if strings.HasSuffix(file.Name(), ".crt") {
79+
s.loadCertificate(strings.TrimSuffix(file.Name(), ".crt"))
80+
}
81+
}
82+
}
83+
84+
func (s *CertStore) loadCertificate(hostname string) {
85+
s.certLock.Lock()
86+
defer s.certLock.Unlock()
87+
88+
keyFile := path.Join(s.storePath, hostname+".key")
89+
crtFile := path.Join(s.storePath, hostname+".crt")
90+
91+
cert, err := tls.LoadX509KeyPair(crtFile, keyFile)
92+
if err != nil {
93+
s.log.Error().Err(err).Str("hostname", hostname).Msg("Failed to load certificate")
94+
return
95+
}
96+
97+
s.certificates[hostname] = &cert
98+
99+
s.log.Info().Str("hostname", hostname).Msg("Loaded certificate")
100+
}
101+
102+
// GetCertificate returns the certificate for the given hostname
103+
// returns nil if the certificate is not found
104+
func (s *CertStore) GetCertificate(hostname string) *tls.Certificate {
105+
s.certLock.Lock()
106+
defer s.certLock.Unlock()
107+
108+
return s.certificates[hostname]
109+
}
110+
111+
// ValidateAndSaveCertificate validates the certificate and saves it to the store
112+
// returns are:
113+
// - error: if the certificate is invalid or if there's any error during saving the certificate
114+
// - error: if there's any warning or error during saving the certificate
115+
func (s *CertStore) ValidateAndSaveCertificate(hostname string, cert string, key string, ignoreWarning bool) (error, error) {
116+
tlsCert, err := tls.X509KeyPair([]byte(cert), []byte(key))
117+
if err != nil {
118+
return fmt.Errorf("Failed to parse certificate: %w", err), nil
119+
}
120+
121+
// this can be skipped as current implementation supports one custom certificate only
122+
if tlsCert.Leaf != nil {
123+
// add recover to avoid panic
124+
defer func() {
125+
if r := recover(); r != nil {
126+
s.log.Error().Interface("recovered", r).Msg("Failed to verify hostname")
127+
}
128+
}()
129+
130+
if err = tlsCert.Leaf.VerifyHostname(hostname); err != nil {
131+
if !ignoreWarning {
132+
return nil, fmt.Errorf("Certificate does not match hostname: %w", err)
133+
}
134+
s.log.Warn().Err(err).Msg("Certificate does not match hostname")
135+
}
136+
}
137+
138+
s.certLock.Lock()
139+
s.certificates[hostname] = &tlsCert
140+
s.certLock.Unlock()
141+
142+
s.saveCertificate(hostname)
143+
144+
return nil, nil
145+
}
146+
147+
func (s *CertStore) saveCertificate(hostname string) {
148+
// check if certificate already exists
149+
tlsCert := s.certificates[hostname]
150+
if tlsCert == nil {
151+
s.log.Error().Str("hostname", hostname).Msg("Certificate for hostname does not exist, skipping saving certificate")
152+
return
153+
}
154+
155+
err := s.ensureStorePath()
156+
if err != nil {
157+
s.log.Error().Err(err).Msg("Failed to ensure store path")
158+
return
159+
}
160+
161+
keyFile := path.Join(s.storePath, hostname+".key")
162+
crtFile := path.Join(s.storePath, hostname+".crt")
163+
164+
if err := keyToFile(tlsCert, keyFile); err != nil {
165+
s.log.Error().Err(err).Msg("Failed to save key file")
166+
return
167+
}
168+
169+
if err := certToFile(tlsCert, crtFile); err != nil {
170+
s.log.Error().Err(err).Msg("Failed to save certificate")
171+
return
172+
}
173+
174+
s.log.Info().Str("hostname", hostname).Msg("Saved certificate")
175+
}

0 commit comments

Comments
 (0)