mirror of
https://github.com/jhillyerd/inbucket.git
synced 2026-10-29 05:32:31 +00:00
feat: reload TLS certificates when their files change (#625)
This commit is contained in:
committed by
GitHub
parent
9876bb2bd9
commit
8075e8d2a3
@@ -9,6 +9,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/inbucket/inbucket/v3/pkg/config"
|
||||
"github.com/inbucket/inbucket/v3/pkg/server/tlscert"
|
||||
"github.com/inbucket/inbucket/v3/pkg/storage"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
@@ -28,14 +29,13 @@ func NewServer(pop3Config config.POP3, store storage.Store) (*Server, error) {
|
||||
slog := log.With().Str("module", "pop3").Str("phase", "tls").Logger()
|
||||
tlsConfig := &tls.Config{}
|
||||
if pop3Config.TLSEnabled {
|
||||
var err error
|
||||
tlsConfig.Certificates = make([]tls.Certificate, 1)
|
||||
tlsConfig.Certificates[0], err = tls.LoadX509KeyPair(pop3Config.TLSCert, pop3Config.TLSPrivKey)
|
||||
reloader, err := tlscert.NewReloader(pop3Config.TLSCert, pop3Config.TLSPrivKey, slog)
|
||||
if err != nil {
|
||||
slog.Error().Msgf("Failed loading X509 KeyPair: %v", err)
|
||||
return nil, fmt.Errorf("failed to configure TLS; %v", err)
|
||||
// Do not silently turn off Security.
|
||||
}
|
||||
tlsConfig.GetCertificate = reloader.GetCertificate
|
||||
slog.Debug().Msg("TLS config available")
|
||||
} else {
|
||||
tlsConfig = nil
|
||||
|
||||
@@ -3,12 +3,15 @@ package pop3
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/mail"
|
||||
"net/textproto"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strings"
|
||||
"testing"
|
||||
@@ -765,6 +768,66 @@ func TestNewServerCertificateError(t *testing.T) {
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
// TestForceTLSServesRotatedCertificate verifies that replacing the certificate files on disk
|
||||
// changes the certificate presented by later full handshakes, without restarting the server,
|
||||
// while a client resuming an earlier TLS session keeps that session's certificate.
|
||||
func TestForceTLSServesRotatedCertificate(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
certPath, keyPath := filepath.Join(dir, "cert.pem"), filepath.Join(dir, "key.pem")
|
||||
writeKeyPair := func(version int) []byte {
|
||||
cert, privKey, err := generateCertificate(t)
|
||||
require.NoError(t, err)
|
||||
mtime := time.Date(2026, 1, 1, 0, 0, version, 0, time.UTC)
|
||||
require.NoError(t, os.WriteFile(certPath, certToPem(cert), 0o600))
|
||||
require.NoError(t, os.WriteFile(keyPath, privKeyToPem(privKey), 0o600))
|
||||
require.NoError(t, os.Chtimes(certPath, mtime, mtime))
|
||||
require.NoError(t, os.Chtimes(keyPath, mtime, mtime))
|
||||
return cert
|
||||
}
|
||||
first := writeKeyPair(1)
|
||||
|
||||
server, err := NewServer(config.POP3{
|
||||
Addr: "127.0.0.1:0",
|
||||
Domain: "inbucket.local",
|
||||
Timeout: 5 * time.Second,
|
||||
ForceTLS: true,
|
||||
TLSEnabled: true,
|
||||
TLSCert: certPath,
|
||||
TLSPrivKey: keyPath,
|
||||
}, test.NewStore())
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(server.Drain)
|
||||
|
||||
// peerCertificate starts a session and returns the certificate the server presents, and
|
||||
// whether the TLS session was resumed.
|
||||
peerCertificate := func(tlsConfig *tls.Config) ([]byte, bool) {
|
||||
tlsConn := tls.Client(setupPOPSession(t, server), tlsConfig)
|
||||
defer func() { _ = tlsConn.Close() }()
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
require.NoError(t, tlsConn.HandshakeContext(ctx))
|
||||
// Read the greeting so the server is waiting on input when the client closes. Reading
|
||||
// also processes the session ticket the server sends after the handshake.
|
||||
readPOP3Greeting(t, textproto.NewConn(tlsConn))
|
||||
state := tlsConn.ConnectionState()
|
||||
return state.PeerCertificates[0].Raw, state.DidResume
|
||||
}
|
||||
fresh := &tls.Config{InsecureSkipVerify: true}
|
||||
caching := &tls.Config{InsecureSkipVerify: true, ClientSessionCache: tls.NewLRUClientSessionCache(1)}
|
||||
|
||||
cert, _ := peerCertificate(fresh)
|
||||
assert.Equal(t, first, cert)
|
||||
cert, _ = peerCertificate(caching)
|
||||
assert.Equal(t, first, cert)
|
||||
|
||||
second := writeKeyPair(2)
|
||||
cert, _ = peerCertificate(fresh)
|
||||
assert.Equal(t, second, cert, "full handshake after rotation")
|
||||
cert, resumed := peerCertificate(caching)
|
||||
assert.True(t, resumed, "caching client should resume its session")
|
||||
assert.Equal(t, first, cert, "resumed session after rotation")
|
||||
}
|
||||
|
||||
// TestValidateMsgNum exercises the shared message-number validation helper
|
||||
// directly.
|
||||
func TestValidateMsgNum(t *testing.T) {
|
||||
|
||||
Reference in New Issue
Block a user