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}