Skip to content

Commit b8d1fee

Browse files
authored
Merge pull request #65 from Klarrio/fix-certificate-validation
Fix certificate validation
2 parents 0db5165 + 3c385ba commit b8d1fee

File tree

4 files changed

+262
-20
lines changed

4 files changed

+262
-20
lines changed

README.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,14 @@ See:
141141
--tls-insecure-skip-verify It controls whether a client verifies the server's certificate chain and host name
142142
--tls-same-client-cert-enable Use only when mutual TLS is enabled on proxy and broker. It controls whether a proxy validates if proxy client certificate exactly matches brokers client cert (tls-client-cert-file)
143143
144+
--proxy-listener-tls-client-cert-validate-subject bool Whether to validate client certificate subject (default false)
145+
--proxy-listener-tls-required-client-subject-common-name string Required client certificate subject common name
146+
--proxy-listener-tls-required-client-subject-country stringArray Required client certificate subject country
147+
--proxy-listener-tls-required-client-subject-province stringArray Required client certificate subject province
148+
--proxy-listener-tls-required-client-subject-locality stringArray Required client certificate subject locality
149+
--proxy-listener-tls-required-client-subject-organization stringArray Required client certificate subject organization
150+
--proxy-listener-tls-required-client-subject-organizational-unit stringArray Required client certificate subject organizational unit
151+
144152
### Usage example
145153
146154
kafka-proxy server --bootstrap-server-mapping "192.168.99.100:32400,0.0.0.0:32399"
@@ -312,6 +320,29 @@ Connect through test HTTP Proxy server using CONNECT method
312320
--forward-proxy http://my-proxy-user:my-proxy-password@localhost:3128
313321
```
314322
323+
### Validating client certificate DN
324+
325+
Sometimes it might be necessary to not only validate that the client certificate is valid but also that the client certificate DN is issued for a concrete use case. This can be achieved using the following set of arguments:
326+
327+
```
328+
--proxy-listener-tls-client-cert-validate-subject bool Whether to validate client certificate subject (default false)
329+
--proxy-listener-tls-required-client-subject-common-name string Required client certificate subject common name
330+
--proxy-listener-tls-required-client-subject-country stringArray Required client certificate subject country
331+
--proxy-listener-tls-required-client-subject-province stringArray Required client certificate subject province
332+
--proxy-listener-tls-required-client-subject-locality stringArray Required client certificate subject locality
333+
--proxy-listener-tls-required-client-subject-organization stringArray Required client certificate subject organization
334+
--proxy-listener-tls-required-client-subject-organizational-unit stringArray Required client certificate subject organizational unit
335+
```
336+
337+
By setting `--proxy-listener-tls-client-cert-validate-subject true`, Kafka Proxy will inspect client certificate DN fields for the expected values set with the `--proxy-listener-tls-required-client-*` arguments. The matches are always exact and used together, fo all non empty values. For example, to allow a valid certificate for `country=DE` and `organization=grepplabs`, configure Kafka Proxy in the following way:
338+
339+
```
340+
kafka-proxy server \
341+
--proxy-listener-tls-client-cert-validate-subject true \
342+
--proxy-listener-tls-required-client-subject-country DE \
343+
--proxy-listener-tls-required-client-subject-organization grepplabs
344+
```
345+
315346
### Kubernetes sidecar container example
316347
317348
```yaml

cmd/kafka-proxy/server.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ func initFlags() {
108108
Server.Flags().StringSliceVar(&c.Proxy.TLS.ListenerCurvePreferences, "proxy-listener-curve-preferences", []string{}, "List of curve preferences")
109109

110110
Server.Flags().BoolVar(&c.Proxy.TLS.ClientCert.ValidateSubject, "proxy-listener-tls-client-cert-validate-subject", false, "Whether to validate client certificate subject")
111-
Server.Flags().StringVar(&c.Proxy.TLS.ClientCert.Subject.CommonName, "proxy-listener-tls-client-cert-subject-common-name", "", "Required client certificate subject common name")
111+
Server.Flags().StringVar(&c.Proxy.TLS.ClientCert.Subject.CommonName, "proxy-listener-tls-required-client-subject-common-name", "", "Required client certificate subject common name")
112112
Server.Flags().StringSliceVar(&c.Proxy.TLS.ClientCert.Subject.Country, "proxy-listener-tls-required-client-subject-country", []string{}, "Required client certificate subject country")
113113
Server.Flags().StringSliceVar(&c.Proxy.TLS.ClientCert.Subject.Province, "proxy-listener-tls-required-client-subject-province", []string{}, "Required client certificate subject province")
114114
Server.Flags().StringSliceVar(&c.Proxy.TLS.ClientCert.Subject.Locality, "proxy-listener-tls-required-client-subject-locality", []string{}, "Required client certificate subject locality")

proxy/tls.go

Lines changed: 139 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"fmt"
88
"io/ioutil"
99
"net"
10+
"sort"
1011
"strings"
1112
"time"
1213

@@ -15,6 +16,22 @@ import (
1516
"github.com/pkg/errors"
1617
)
1718

19+
type clientCertSubjectField string
20+
21+
const (
22+
clientCertSubjectCommonName = "CN"
23+
clientCertSubjectCountry = "C"
24+
clientCertSubjectProvince = "S"
25+
clientCertSubjectLocality = "L"
26+
clientCertSubjectOrganization = "O"
27+
clientCertSubjectOrganizationalUnit = "OU"
28+
)
29+
30+
type clientCertExpectedData struct {
31+
fields map[clientCertSubjectField]string
32+
parts []string
33+
}
34+
1835
var (
1936
defaultCurvePreferences = []tls.CurveID{
2037
tls.CurveP256,
@@ -122,35 +139,139 @@ func newTLSListenerConfig(conf *config.Config) (*tls.Config, error) {
122139
cfg.ClientAuth = tls.RequireAndVerifyClientCert
123140
}
124141

125-
cfg.VerifyPeerCertificate = func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error {
142+
cfg.VerifyPeerCertificate = tlsClientCertVerificationFunc(conf)
143+
144+
return cfg, nil
145+
}
146+
147+
func tlsClientCertVerificationFunc(conf *config.Config) func([][]byte, [][]*x509.Certificate) error {
148+
expectedData := getClientCertExpectedData(conf)
149+
return func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error {
126150
if conf.Proxy.TLS.ClientCert.ValidateSubject {
127-
expected := fmt.Sprintf("s:/CN=%s/C=%v/S=%v/L=%v/O=%v/OU=%v",
128-
conf.Proxy.TLS.ClientCert.Subject.CommonName,
129-
conf.Proxy.TLS.ClientCert.Subject.Country,
130-
conf.Proxy.TLS.ClientCert.Subject.Province,
131-
conf.Proxy.TLS.ClientCert.Subject.Locality,
132-
conf.Proxy.TLS.ClientCert.Subject.Organization,
133-
conf.Proxy.TLS.ClientCert.Subject.OrganizationalUnit)
151+
152+
if len(expectedData.fields) == 0 {
153+
return nil // nothing to validate
154+
}
155+
134156
for _, chain := range verifiedChains {
135157
for _, cert := range chain {
136-
current := fmt.Sprintf("s:/CN=%s/C=%v/S=%v/L=%v/O=%v/OU=%v",
137-
cert.Subject.CommonName,
138-
cert.Subject.Country,
139-
cert.Subject.Province,
140-
cert.Subject.Locality,
141-
cert.Subject.Organization,
142-
cert.Subject.OrganizationalUnit)
143-
if current == expected {
158+
159+
certificateAcceptable := true
160+
161+
for k, v := range expectedData.fields {
162+
switch k {
163+
case clientCertSubjectCommonName:
164+
if v != cert.Subject.CommonName {
165+
certificateAcceptable = false
166+
break
167+
}
168+
case clientCertSubjectCountry:
169+
currentValues := cert.Subject.Country
170+
sort.Strings(currentValues)
171+
if fmt.Sprintf("%v", currentValues) != v {
172+
certificateAcceptable = false
173+
break
174+
}
175+
case clientCertSubjectProvince:
176+
currentValues := cert.Subject.Province
177+
sort.Strings(currentValues)
178+
if fmt.Sprintf("%v", currentValues) != v {
179+
certificateAcceptable = false
180+
break
181+
}
182+
case clientCertSubjectLocality:
183+
currentValues := cert.Subject.Locality
184+
sort.Strings(currentValues)
185+
if fmt.Sprintf("%v", currentValues) != v {
186+
certificateAcceptable = false
187+
break
188+
}
189+
case clientCertSubjectOrganization:
190+
currentValues := cert.Subject.Organization
191+
sort.Strings(currentValues)
192+
if fmt.Sprintf("%v", currentValues) != v {
193+
certificateAcceptable = false
194+
break
195+
}
196+
case clientCertSubjectOrganizationalUnit:
197+
currentValues := cert.Subject.OrganizationalUnit
198+
sort.Strings(currentValues)
199+
if fmt.Sprintf("%v", currentValues) != v {
200+
certificateAcceptable = false
201+
break
202+
}
203+
}
204+
}
205+
206+
if certificateAcceptable {
144207
return nil
145208
}
209+
146210
}
147211
}
148-
return fmt.Errorf("tls: no client certificate presented required subject '%s'", expected)
212+
213+
return fmt.Errorf("tls: no client certificate presented required subject '%s'", strings.Join(expectedData.parts, "/"))
214+
149215
}
150216
return nil
151217
}
218+
}
219+
220+
func getClientCertExpectedData(conf *config.Config) *clientCertExpectedData {
221+
222+
expectedFields := map[clientCertSubjectField]string{}
223+
expectedParts := []string{"s:"} // these are calculated here because the order is relevant to us
224+
values := []string{}
225+
226+
if conf.Proxy.TLS.ClientCert.Subject.CommonName != "" {
227+
expectedFields[clientCertSubjectCommonName] = conf.Proxy.TLS.ClientCert.Subject.CommonName
228+
expectedParts = append(expectedParts, fmt.Sprintf("%s=%s", clientCertSubjectCommonName, expectedFields[clientCertSubjectCommonName]))
229+
}
230+
values = removeEmptyStrings(conf.Proxy.TLS.ClientCert.Subject.Country)
231+
if len(values) > 0 {
232+
sort.Strings(values)
233+
expectedFields[clientCertSubjectCountry] = fmt.Sprintf("%v", values)
234+
expectedParts = append(expectedParts, fmt.Sprintf("%s=%s", clientCertSubjectCountry, expectedFields[clientCertSubjectCountry]))
235+
}
236+
values = removeEmptyStrings(conf.Proxy.TLS.ClientCert.Subject.Province)
237+
if len(values) > 0 {
238+
sort.Strings(values)
239+
expectedFields[clientCertSubjectProvince] = fmt.Sprintf("%v", values)
240+
expectedParts = append(expectedParts, fmt.Sprintf("%s=%s", clientCertSubjectProvince, expectedFields[clientCertSubjectProvince]))
241+
}
242+
values = removeEmptyStrings(conf.Proxy.TLS.ClientCert.Subject.Locality)
243+
if len(values) > 0 {
244+
sort.Strings(values)
245+
expectedFields[clientCertSubjectLocality] = fmt.Sprintf("%v", values)
246+
expectedParts = append(expectedParts, fmt.Sprintf("%s=%s", clientCertSubjectLocality, expectedFields[clientCertSubjectLocality]))
247+
}
248+
values = removeEmptyStrings(conf.Proxy.TLS.ClientCert.Subject.Organization)
249+
if len(values) > 0 {
250+
sort.Strings(values)
251+
expectedFields[clientCertSubjectOrganization] = fmt.Sprintf("%v", values)
252+
expectedParts = append(expectedParts, fmt.Sprintf("%s=%s", clientCertSubjectOrganization, expectedFields[clientCertSubjectOrganization]))
253+
}
254+
values = removeEmptyStrings(conf.Proxy.TLS.ClientCert.Subject.OrganizationalUnit)
255+
if len(values) > 0 {
256+
sort.Strings(values)
257+
expectedFields[clientCertSubjectOrganizationalUnit] = fmt.Sprintf("%v", values)
258+
expectedParts = append(expectedParts, fmt.Sprintf("%s=%s", clientCertSubjectOrganizationalUnit, expectedFields[clientCertSubjectOrganizationalUnit]))
259+
}
260+
return &clientCertExpectedData{
261+
parts: expectedParts,
262+
fields: expectedFields,
263+
}
264+
}
152265

153-
return cfg, nil
266+
func removeEmptyStrings(input []string) []string {
267+
output := []string{}
268+
for _, value := range input {
269+
if value == "" {
270+
continue
271+
}
272+
output = append(output, value)
273+
}
274+
return output
154275
}
155276

156277
func getCipherSuites(enabledCipherSuites []string) ([]uint16, error) {

proxy/tls_test.go

Lines changed: 91 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ func TestValidEnabledClientCertSubjectValidate(t *testing.T) {
5858
testSubject := pkix.Name{
5959
CommonName: "integration-test",
6060
Country: []string{"DE"},
61+
Province: []string{"NRW"},
6162
Locality: []string{"test-file"},
6263
Organization: []string{"integration-test"},
6364
OrganizationalUnit: []string{"invalid-OrganizationalUnit"},
@@ -68,6 +69,7 @@ func TestValidEnabledClientCertSubjectValidate(t *testing.T) {
6869
c.Proxy.TLS.ClientCert.ValidateSubject = true
6970
c.Proxy.TLS.ClientCert.Subject.CommonName = testSubject.CommonName
7071
c.Proxy.TLS.ClientCert.Subject.Country = testSubject.Country
72+
c.Proxy.TLS.ClientCert.Subject.Province = testSubject.Province
7173
c.Proxy.TLS.ClientCert.Subject.Locality = testSubject.Locality
7274
c.Proxy.TLS.ClientCert.Subject.Organization = testSubject.Organization
7375
c.Proxy.TLS.ClientCert.Subject.OrganizationalUnit = testSubject.OrganizationalUnit
@@ -89,6 +91,7 @@ func TestInvalidEnabledClientCertSubjectValidate(t *testing.T) {
8991
testSubject := pkix.Name{
9092
CommonName: "integration-test",
9193
Country: []string{"DE"},
94+
Province: []string{"NRW"},
9295
Locality: []string{"test-file"},
9396
Organization: []string{"integration-test"},
9497
OrganizationalUnit: []string{"invalid-OrganizationalUnit"},
@@ -99,6 +102,7 @@ func TestInvalidEnabledClientCertSubjectValidate(t *testing.T) {
99102
c.Proxy.TLS.ClientCert.ValidateSubject = true
100103
c.Proxy.TLS.ClientCert.Subject.CommonName = testSubject.CommonName
101104
c.Proxy.TLS.ClientCert.Subject.Country = testSubject.Country
105+
c.Proxy.TLS.ClientCert.Subject.Province = testSubject.Province
102106
c.Proxy.TLS.ClientCert.Subject.Locality = testSubject.Locality
103107
c.Proxy.TLS.ClientCert.Subject.Organization = testSubject.Organization
104108
c.Proxy.TLS.ClientCert.Subject.OrganizationalUnit = []string{"expected-OrganizationalUnit"}
@@ -113,7 +117,93 @@ func TestInvalidEnabledClientCertSubjectValidate(t *testing.T) {
113117
_, _, _, err := makeTLSPipe(c, nil)
114118

115119
a.NotNil(err)
116-
a.Contains(err.Error(), "tls: no client certificate presented required subject 's:/CN=integration-test/C=[DE]/S=[]/L=[test-file]/O=[integration-test]/OU=[expected-OrganizationalUnit]'")
120+
a.Contains(err.Error(), "tls: no client certificate presented required subject 's:/CN=integration-test/C=[DE]/S=[NRW]/L=[test-file]/O=[integration-test]/OU=[expected-OrganizationalUnit]'")
121+
}
122+
123+
func TestValidEnabledClientCertSubjectMayContainNotRequiredValues(t *testing.T) {
124+
a := assert.New(t)
125+
testSubject := pkix.Name{
126+
CommonName: "integration-test",
127+
Country: []string{"DE"},
128+
Province: []string{"NRW"},
129+
Locality: []string{"locality-not-validated"},
130+
Organization: []string{"integration-test"},
131+
OrganizationalUnit: []string{"invalid-OrganizationalUnit"},
132+
}
133+
bundle := NewCertsBundleWithSubject(testSubject)
134+
defer bundle.Close()
135+
c := new(config.Config)
136+
c.Proxy.TLS.ClientCert.ValidateSubject = true
137+
c.Proxy.TLS.ClientCert.Subject.CommonName = testSubject.CommonName
138+
c.Proxy.TLS.ClientCert.Subject.Country = testSubject.Country
139+
c.Proxy.TLS.ClientCert.Subject.Province = testSubject.Province
140+
c.Proxy.TLS.ClientCert.Subject.Organization = testSubject.Organization
141+
c.Proxy.TLS.ClientCert.Subject.OrganizationalUnit = testSubject.OrganizationalUnit
142+
c.Proxy.TLS.ListenerCertFile = bundle.ServerCert.Name()
143+
c.Proxy.TLS.ListenerKeyFile = bundle.ServerKey.Name()
144+
c.Proxy.TLS.CAChainCertFile = bundle.CACert.Name()
145+
146+
c.Kafka.TLS.CAChainCertFile = bundle.CACert.Name()
147+
c.Kafka.TLS.ClientCertFile = bundle.ClientCert.Name()
148+
c.Kafka.TLS.ClientKeyFile = bundle.ClientKey.Name()
149+
150+
_, _, _, err := makeTLSPipe(c, nil)
151+
152+
a.Nil(err)
153+
}
154+
155+
func TestValidEnabledClientCertSubjectMayContainValuesInDifferentOrder(t *testing.T) {
156+
a := assert.New(t)
157+
testSubject := pkix.Name{
158+
CommonName: "integration-test",
159+
Country: []string{"DE", "PL"},
160+
Province: []string{"NRW"},
161+
Organization: []string{"integration-test"},
162+
OrganizationalUnit: []string{"invalid-OrganizationalUnit"},
163+
}
164+
bundle := NewCertsBundleWithSubject(testSubject)
165+
defer bundle.Close()
166+
c := new(config.Config)
167+
c.Proxy.TLS.ClientCert.ValidateSubject = true
168+
c.Proxy.TLS.ClientCert.Subject.Country = []string{"PL", "DE"}
169+
c.Proxy.TLS.ListenerCertFile = bundle.ServerCert.Name()
170+
c.Proxy.TLS.ListenerKeyFile = bundle.ServerKey.Name()
171+
c.Proxy.TLS.CAChainCertFile = bundle.CACert.Name()
172+
173+
c.Kafka.TLS.CAChainCertFile = bundle.CACert.Name()
174+
c.Kafka.TLS.ClientCertFile = bundle.ClientCert.Name()
175+
c.Kafka.TLS.ClientKeyFile = bundle.ClientKey.Name()
176+
177+
_, _, _, err := makeTLSPipe(c, nil)
178+
179+
a.Nil(err)
180+
}
181+
182+
func TestValidEnabledClientCertSubjectEemptyValuesAreIgnored(t *testing.T) {
183+
a := assert.New(t)
184+
testSubject := pkix.Name{
185+
CommonName: "integration-test",
186+
Country: []string{"DE", "PL"},
187+
Province: []string{"NRW"},
188+
Organization: []string{"integration-test"},
189+
OrganizationalUnit: []string{"invalid-OrganizationalUnit"},
190+
}
191+
bundle := NewCertsBundleWithSubject(testSubject)
192+
defer bundle.Close()
193+
c := new(config.Config)
194+
c.Proxy.TLS.ClientCert.ValidateSubject = true
195+
c.Proxy.TLS.ClientCert.Subject.Country = []string{"PL", "", "DE"}
196+
c.Proxy.TLS.ListenerCertFile = bundle.ServerCert.Name()
197+
c.Proxy.TLS.ListenerKeyFile = bundle.ServerKey.Name()
198+
c.Proxy.TLS.CAChainCertFile = bundle.CACert.Name()
199+
200+
c.Kafka.TLS.CAChainCertFile = bundle.CACert.Name()
201+
c.Kafka.TLS.ClientCertFile = bundle.ClientCert.Name()
202+
c.Kafka.TLS.ClientKeyFile = bundle.ClientKey.Name()
203+
204+
_, _, _, err := makeTLSPipe(c, nil)
205+
206+
a.Nil(err)
117207
}
118208

119209
func TestTLSUnknownAuthorityNoCAChainCert(t *testing.T) {

0 commit comments

Comments
 (0)