Skip to content

Commit e3b0586

Browse files
authored
feat: add client certificate support to TLSCertLoader (#27090)
Add support for client certificates to tlsconfig.TLSCertLoader. Also add TLSCertLoader.SetupTLSConfig to simplify using a TLSCertLoader with a tls.Config object. Clean cherry-pick from master-1.x. (cherry picked from commit 7050b3d) Closes: #27089
1 parent 56a3f97 commit e3b0586

File tree

5 files changed

+197
-8
lines changed

5 files changed

+197
-8
lines changed

pkg/testing/selfsigned/selfsigned.go

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,12 @@ type CertOptions struct {
4242

4343
// CombinedFile indicates if the certificate and key should be combined into a single file
4444
CombinedFile bool
45+
46+
// CAOrganization sets the CA certificate's Subject.Organization field
47+
CAOrganization string
48+
49+
// CACommonName sets the CA certificate's Subject.CommonName field
50+
CACommonName string
4551
}
4652

4753
type CertOpt func(*CertOptions)
@@ -82,6 +88,13 @@ func WithCombinedFile() CertOpt {
8288
}
8389
}
8490

91+
func WithCASubject(organization, commonName string) CertOpt {
92+
return func(o *CertOptions) {
93+
o.CAOrganization = organization
94+
o.CACommonName = commonName
95+
}
96+
}
97+
8598
func NewSelfSignedCert(t *testing.T, opts ...CertOpt) *Cert {
8699
t.Helper()
87100
tmpdir := t.TempDir()
@@ -108,6 +121,14 @@ func NewSelfSignedCert(t *testing.T, opts ...CertOpt) *Cert {
108121
options.NotAfter = time.Now().Add(7 * 24 * time.Hour)
109122
}
110123

124+
if options.CAOrganization == "" {
125+
options.CAOrganization = "my_test_ca"
126+
}
127+
128+
if options.CACommonName == "" {
129+
options.CACommonName = "My Test CA"
130+
}
131+
111132
// Sanity check options.
112133
require.NotEmpty(t, options.DNSNames)
113134

@@ -129,8 +150,8 @@ func NewSelfSignedCert(t *testing.T, opts ...CertOpt) *Cert {
129150
BasicConstraintsValid: true,
130151

131152
Subject: pkix.Name{
132-
Organization: []string{"my_test_ca"},
133-
CommonName: "My Test CA",
153+
Organization: []string{options.CAOrganization},
154+
CommonName: options.CACommonName,
134155
},
135156

136157
IsCA: true,

pkg/tlsconfig/certconfig.go

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,11 @@ const (
3030
)
3131

3232
var (
33-
ErrCertificateNil = errors.New("TLS certificate is nil")
34-
ErrCertificateEmpty = errors.New("TLS certificate is empty")
35-
ErrLoadedCertificateInvalid = errors.New("LoadedCertificate is invalid")
36-
ErrPathEmpty = errors.New("empty path")
33+
ErrCertificateNil = errors.New("TLS certificate is nil")
34+
ErrCertificateEmpty = errors.New("TLS certificate is empty")
35+
ErrCertificateRequestInfoNil = errors.New("CertificateRequestInfo is nil")
36+
ErrLoadedCertificateInvalid = errors.New("LoadedCertificate is invalid")
37+
ErrPathEmpty = errors.New("empty path")
3738
)
3839

3940
// LoadedCertificate encapsulates information about a loaded certificate.
@@ -275,6 +276,18 @@ func (cl *TLSCertLoader) Certificate() *tls.Certificate {
275276
return cl.cert
276277
}
277278

279+
// SetupTLSConfig modifies tlsConfig to use cl for server and client certificates.
280+
// tlsConfig may be nil. If other fields like tlsConfig.Certificates or
281+
// tlsConfig.NameToCertificate have been set, then cl's certificate may not be used
282+
// as expected.
283+
func (cl *TLSCertLoader) SetupTLSConfig(tlsConfig *tls.Config) {
284+
if tlsConfig == nil {
285+
return
286+
}
287+
tlsConfig.GetCertificate = cl.GetCertificate
288+
tlsConfig.GetClientCertificate = cl.GetClientCertificate
289+
}
290+
278291
// GetCertificate is for use with a tls.Config's GetCertificate member. This allows a
279292
// tls.Config to dynamically update its certificate when Load changes the active
280293
// certificate.
@@ -290,6 +303,23 @@ func (cl *TLSCertLoader) GetCertificate(*tls.ClientHelloInfo) (*tls.Certificate,
290303
}
291304
}
292305

306+
// GetClientCertificate is for use with a tls.Config's GetClientCertificate member. This allows a
307+
// tls.Config to dynamically update its client certificates when Load changes the active
308+
// certificate.
309+
func (cl *TLSCertLoader) GetClientCertificate(cri *tls.CertificateRequestInfo) (*tls.Certificate, error) {
310+
if cri == nil {
311+
return new(tls.Certificate), ErrCertificateRequestInfoNil
312+
}
313+
cert := cl.Certificate()
314+
if cert == nil {
315+
return new(tls.Certificate), ErrCertificateNil
316+
}
317+
if err := cri.SupportsCertificate(cert); err != nil {
318+
return new(tls.Certificate), err
319+
}
320+
return cert, nil
321+
}
322+
293323
// Leaf returns the parsed x509 certificate of the currently loaded certificate.
294324
// If no certificate is loaded then nil is returned.
295325
func (cl *TLSCertLoader) Leaf() *x509.Certificate {

pkg/tlsconfig/certconfig_test.go

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
package tlsconfig
22

33
import (
4+
"crypto/tls"
45
"crypto/x509"
6+
"encoding/pem"
57
"fmt"
68
"math/big"
79
"os"
@@ -488,3 +490,139 @@ func TestTLSCertLoader_VerifyLoad(t *testing.T) {
488490
require.Equal(t, sn2, cl.Leaf().SerialNumber.String())
489491
}
490492
}
493+
494+
func TestTLSCertLoader_GetClientCertificate(t *testing.T) {
495+
ss := selfsigned.NewSelfSignedCert(t, selfsigned.WithDNSName("client.influxdata.edge"))
496+
497+
cl, err := NewTLSCertLoader(ss.CertPath, ss.KeyPath)
498+
require.NoError(t, err)
499+
require.NotNil(t, cl)
500+
defer func() {
501+
require.NoError(t, cl.Close())
502+
}()
503+
504+
// Test happy path: certificate supports the request.
505+
// The selfsigned package creates RSA certificates, so we use RSA signature schemes.
506+
t.Run("supported certificate", func(t *testing.T) {
507+
cri := &tls.CertificateRequestInfo{
508+
SignatureSchemes: []tls.SignatureScheme{
509+
tls.PKCS1WithSHA256,
510+
tls.PKCS1WithSHA384,
511+
tls.PKCS1WithSHA512,
512+
},
513+
}
514+
515+
cert, err := cl.GetClientCertificate(cri)
516+
require.NoError(t, err)
517+
require.NotNil(t, cert)
518+
require.Equal(t, cl.Certificate(), cert)
519+
})
520+
521+
t.Run("nil CertificateRequestInfo", func(t *testing.T) {
522+
cert, err := cl.GetClientCertificate(nil)
523+
require.ErrorIs(t, err, ErrCertificateRequestInfoNil)
524+
require.NotNil(t, cert)
525+
require.Empty(t, cert.Certificate)
526+
})
527+
528+
// Test unsupported certificate: CertificateRequestInfo only accepts Ed25519,
529+
// but our certificate uses RSA.
530+
t.Run("unsupported certificate", func(t *testing.T) {
531+
cri := &tls.CertificateRequestInfo{
532+
SignatureSchemes: []tls.SignatureScheme{
533+
tls.Ed25519, // Our RSA cert doesn't support this
534+
},
535+
}
536+
537+
cert, err := cl.GetClientCertificate(cri)
538+
require.ErrorContains(t, err, "doesn't support any of the certificate's signature algorithms")
539+
// GetClientCertificate must return a non-nil certificate even on error
540+
// (per the tls.Config.GetClientCertificate contract).
541+
require.NotNil(t, cert)
542+
// The returned certificate should be an empty certificate, not the loaded one.
543+
require.NotEqual(t, cl.Certificate(), cert)
544+
require.Empty(t, cert.Certificate)
545+
})
546+
547+
// Test with AcceptableCAs that include our CA.
548+
t.Run("acceptable CA", func(t *testing.T) {
549+
// Verify that if we change cri to ss's CA subject then we do get cert.
550+
caCert, err := os.ReadFile(ss.CACertPath)
551+
require.NoError(t, err)
552+
553+
// Parse the CA cert to get its RawSubject for AcceptableCAs.
554+
block, _ := pem.Decode(caCert)
555+
require.NotNil(t, block)
556+
parsedCA, err := x509.ParseCertificate(block.Bytes)
557+
require.NoError(t, err)
558+
559+
cri := &tls.CertificateRequestInfo{
560+
SignatureSchemes: []tls.SignatureScheme{
561+
tls.PKCS1WithSHA256,
562+
},
563+
AcceptableCAs: [][]byte{parsedCA.RawSubject},
564+
}
565+
566+
cert, err := cl.GetClientCertificate(cri)
567+
require.NoError(t, err)
568+
require.NotNil(t, cert)
569+
require.Equal(t, cl.Certificate(), cert)
570+
})
571+
572+
// Test with AcceptableCAs that don't include our CA.
573+
t.Run("unacceptable CA", func(t *testing.T) {
574+
// Create a certificate with a different CA subject.
575+
ss2 := selfsigned.NewSelfSignedCert(t,
576+
selfsigned.WithCASubject("different_org", "Different CA"),
577+
)
578+
caCert2, err := os.ReadFile(ss2.CACertPath)
579+
require.NoError(t, err)
580+
581+
// Parse the CA cert to get its RawSubject for AcceptableCAs.
582+
block2, _ := pem.Decode(caCert2)
583+
require.NotNil(t, block2)
584+
parsedCA2, err := x509.ParseCertificate(block2.Bytes)
585+
require.NoError(t, err)
586+
587+
cri := &tls.CertificateRequestInfo{
588+
SignatureSchemes: []tls.SignatureScheme{
589+
tls.PKCS1WithSHA256,
590+
},
591+
AcceptableCAs: [][]byte{parsedCA2.RawSubject},
592+
}
593+
594+
cert, err := cl.GetClientCertificate(cri)
595+
require.ErrorContains(t, err, "not signed by an acceptable CA")
596+
require.NotNil(t, cert)
597+
require.Empty(t, cert.Certificate)
598+
})
599+
}
600+
601+
func TestTLSCertLoader_SetupTLSConfig(t *testing.T) {
602+
ss := selfsigned.NewSelfSignedCert(t)
603+
604+
cl, err := NewTLSCertLoader(ss.CertPath, ss.KeyPath)
605+
require.NoError(t, err)
606+
require.NotNil(t, cl)
607+
defer func() {
608+
require.NoError(t, cl.Close())
609+
}()
610+
611+
t.Run("nil config", func(t *testing.T) {
612+
require.NotPanics(t, func() {
613+
cl.SetupTLSConfig(nil)
614+
})
615+
})
616+
617+
t.Run("sets callbacks", func(t *testing.T) {
618+
tlsConfig := &tls.Config{}
619+
620+
require.Nil(t, tlsConfig.GetCertificate)
621+
require.Nil(t, tlsConfig.GetClientCertificate)
622+
623+
cl.SetupTLSConfig(tlsConfig)
624+
625+
require.NotNil(t, tlsConfig.GetCertificate)
626+
require.NotNil(t, tlsConfig.GetClientCertificate)
627+
})
628+
}

services/httpd/service.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,7 @@ func (s *Service) Open() error {
158158
}
159159

160160
tlsConfig := s.tlsConfig.Clone()
161-
tlsConfig.GetCertificate = s.certLoader.GetCertificate
161+
s.certLoader.SetupTLSConfig(tlsConfig)
162162

163163
listener, err := tls.Listen("tcp", s.addr, tlsConfig)
164164
if err != nil {

services/opentsdb/service.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,7 @@ func (s *Service) Open() error {
141141
s.certLoader = certLoader
142142

143143
tlsConfig := s.tlsConfig.Clone()
144-
tlsConfig.GetCertificate = s.certLoader.GetCertificate
144+
s.certLoader.SetupTLSConfig(tlsConfig)
145145

146146
listener, err := tls.Listen("tcp", s.BindAddress, tlsConfig)
147147
if err != nil {

0 commit comments

Comments
 (0)