v2.31.0-dev21 b59533b

public_tls_wiring_test.go RawHistoryFolder

1package app
2
3import (
4	"context"
5	"crypto/rand"
6	"crypto/rsa"
7	"crypto/tls"
8	"crypto/x509"
9	"math/big"
10	"net"
11	"testing"
12	"time"
13
14	"github.com/stretchr/testify/assert"
15	"github.com/stretchr/testify/mock"
16	"github.com/stretchr/testify/require"
17
18	pkiadapter "github.com/bnema/gordon/internal/adapters/out/pki"
19	inmocks "github.com/bnema/gordon/internal/boundaries/in/mocks"
20	outmocks "github.com/bnema/gordon/internal/boundaries/out/mocks"
21	"github.com/bnema/gordon/internal/domain"
22	pkiusecase "github.com/bnema/gordon/internal/usecase/pki"
23	"github.com/bnema/zerowrap"
24)
25
26// generateTestCert creates a self-signed certificate for the given hostnames.
27func generateTestCert(t *testing.T, hosts ...string) tls.Certificate {
28	t.Helper()
29
30	privKey, err := rsa.GenerateKey(rand.Reader, 2048)
31	require.NoError(t, err)
32
33	template := &x509.Certificate{
34		SerialNumber: big.NewInt(1),
35		NotBefore:    time.Now().Add(-1 * time.Hour),
36		NotAfter:     time.Now().Add(24 * time.Hour),
37		IPAddresses:  []net.IP{net.ParseIP("127.0.0.1")},
38	}
39	for _, host := range hosts {
40		if ip := net.ParseIP(host); ip != nil {
41			template.IPAddresses = append(template.IPAddresses, ip)
42		} else {
43			template.DNSNames = append(template.DNSNames, host)
44		}
45	}
46
47	certDER, err := x509.CreateCertificate(rand.Reader, template, template, &privKey.PublicKey, privKey)
48	require.NoError(t, err)
49
50	return tls.Certificate{
51		Certificate: [][]byte{certDER},
52		PrivateKey:  privKey,
53	}
54}
55
56// TestCertificateSelector_StaticCertWins tests that a static certificate matching
57// the server name is returned before public TLS.
58func TestCertificateSelector_StaticCertWins(t *testing.T) {
59	staticCert := generateTestCert(t, "static.example.com")
60	selector := &certificateSelector{
61		staticCerts: prepareStaticTLSCertificates([]tls.Certificate{staticCert}),
62		publicTLS:   inmocks.NewMockPublicTLSService(t),
63	}
64
65	got, err := selector.GetCertificate(&tls.ClientHelloInfo{ServerName: "static.example.com"})
66	require.NoError(t, err)
67	require.NotNil(t, got)
68	assert.Equal(t, staticCert.Certificate[0], got.Certificate[0],
69		"static cert should be returned, not public TLS")
70}
71
72func TestCertificateSelector_PublicTLSErrorFallsThroughWithoutLocalPKI(t *testing.T) {
73	publicTLS := inmocks.NewMockPublicTLSService(t)
74	publicTLS.EXPECT().GetCertificateForHost("acme-required.example.com").Return(nil, domain.ErrTLSRouteNotCovered)
75	selector := &certificateSelector{publicTLS: publicTLS}
76
77	got, err := selector.GetCertificate(&tls.ClientHelloInfo{ServerName: "acme-required.example.com"})
78	require.NoError(t, err)
79	assert.Nil(t, got, "public TLS error with no local PKI returns nil,nil")
80}
81
82// TestCertificateSelector_PublicTLSNilNil tests that when public TLS returns
83// nil,nil and no local PKI, the selector returns nil,nil.
84func TestCertificateSelector_PublicTLSErrorFallsThroughToLocalPKI(t *testing.T) {
85	publicTLS := inmocks.NewMockPublicTLSService(t)
86	publicTLS.EXPECT().GetCertificateForHost("acme-required.example.com").Return(nil, domain.ErrTLSRouteNotCovered)
87
88	routes := outmocks.NewMockRouteChecker(t)
89	routes.EXPECT().GetRoutes(mock.Anything).Return([]domain.Route{{Domain: "acme-required.example.com"}}).Maybe()
90	routes.EXPECT().GetExternalRoutes().Return(map[string]string{}).Maybe()
91	ca, err := pkiadapter.NewCA(t.TempDir(), zerowrap.Default())
92	require.NoError(t, err)
93	pkiSvc := pkiusecase.NewService(context.Background(), ca, routes, zerowrap.Default())
94	defer pkiSvc.Stop()
95
96	selector := &certificateSelector{publicTLS: publicTLS, localPKI: pkiSvc}
97	got, err := selector.GetCertificate(&tls.ClientHelloInfo{ServerName: "acme-required.example.com"})
98	require.NoError(t, err)
99	require.NotNil(t, got, "public TLS error should fall through to local PKI")
100}
101
102func TestCertificateSelector_PublicTLSNilNil(t *testing.T) {
103	publicTLS := inmocks.NewMockPublicTLSService(t)
104	publicTLS.EXPECT().GetCertificateForHost("non-acme.example.com").Return(nil, nil)
105	selector := &certificateSelector{publicTLS: publicTLS}
106
107	got, err := selector.GetCertificate(&tls.ClientHelloInfo{ServerName: "non-acme.example.com"})
108	require.NoError(t, err)
109	assert.Nil(t, got, "nil,nil from public TLS with no local PKI returns nil,nil")
110}
111
112// TestCertificateSelector_NoSource tests that when no source has a matching cert,
113// nil, nil is returned.
114func TestCertificateSelector_NoSource(t *testing.T) {
115	selector := &certificateSelector{}
116
117	got, err := selector.GetCertificate(&tls.ClientHelloInfo{ServerName: "unknown.example.com"})
118	assert.NoError(t, err)
119	assert.Nil(t, got)
120}
121
122// TestMatchingStaticCert tests the matchingStaticCert helper.
123func TestMatchingStaticCert(t *testing.T) {
124	certA := generateTestCert(t, "app.example.com")
125	certB := generateTestCert(t, "api.example.com")
126
127	certs := []tls.Certificate{certA, certB}
128
129	// Exact match
130	got := matchingStaticCert(certs, "app.example.com")
131	require.NotNil(t, got)
132	assert.Equal(t, certA.Certificate[0], got.Certificate[0])
133
134	// Second cert match
135	got = matchingStaticCert(certs, "api.example.com")
136	require.NotNil(t, got)
137	assert.Equal(t, certB.Certificate[0], got.Certificate[0])
138
139	// No match
140	got = matchingStaticCert(certs, "other.example.com")
141	assert.Nil(t, got)
142
143	// Empty server name returns the first configured static certificate.
144	got = matchingStaticCert(certs, "")
145	require.NotNil(t, got)
146	assert.Equal(t, certA.Certificate[0], got.Certificate[0])
147
148	// Empty cert list
149	got = matchingStaticCert(nil, "app.example.com")
150	assert.Nil(t, got)
151}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 7

Tags 106

↑↓ moveenter openesc close