Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions config/configuration.go
Original file line number Diff line number Diff line change
Expand Up @@ -843,6 +843,27 @@ const (
// - Y
// - N
SocketUseSSL string = "SocketUseSSL"

// SocketAlwaysPresentClientCertificate if set to Y, an initiator always presents its configured
// client certificate to the server, bypassing crypto/tls's RFC 8446 filtering against the
// certificate_authorities list advertised in the server's CertificateRequest.
//
// Some counterparties (e.g. stunnel servers pinning exact leaf certificates) advertise a list
// that does not contain the client certificate's issuer; Go would then silently send no
// certificate, failing the handshake, while OpenSSL-based clients send the configured
// certificate regardless. Setting this to Y matches the OpenSSL behavior.
//
// Only meaningful when SocketPrivateKeyFile/SocketCertificateFile (or the Bytes variants)
// are supplied.
//
// Required: No
//
// Default: N
//
// Valid Values:
// - Y
// - N
SocketAlwaysPresentClientCertificate string = "SocketAlwaysPresentClientCertificate"
)

const (
Expand Down
20 changes: 20 additions & 0 deletions tls.go
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,26 @@ func loadTLSConfig(settings *SessionSettings) (*tls.Config, error) {
tlsConfig.Certificates[0] = certificate
}

if len(tlsConfig.Certificates) > 0 && settings.HasSetting(config.SocketAlwaysPresentClientCertificate) {
alwaysPresent, err := settings.BoolSetting(config.SocketAlwaysPresentClientCertificate)
if err != nil {
return nil, err
}

if alwaysPresent {
// As an initiator, always present the configured client certificate,
// bypassing the RFC 8446 acceptable-CAs filtering. Some counterparties
// (e.g. stunnel servers pinning exact leaf certificates) advertise a
// certificate_authorities list that does not contain the client cert's
// issuer; crypto/tls would then silently send no certificate at all,
// while OpenSSL-based clients send the configured cert regardless.
clientCert := &tlsConfig.Certificates[0]
tlsConfig.GetClientCertificate = func(*tls.CertificateRequestInfo) (*tls.Certificate, error) {
return clientCert, nil
}
}
}

if !allowSkipClientCerts {
tlsConfig.ClientAuth = tls.RequireAndVerifyClientCert
}
Expand Down
50 changes: 50 additions & 0 deletions tls_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -331,3 +331,53 @@ func (s *TLSTestSuite) TestServerNameWithCertsFromBytes() {
s.NotNil(tlsConfig)
s.Equal("DummyServerNameWithCerts", tlsConfig.ServerName)
}

func (s *TLSTestSuite) TestAlwaysPresentClientCertificate() {
s.settings.GlobalSettings().Set(config.SocketPrivateKeyFile, s.PrivateKeyFile)
s.settings.GlobalSettings().Set(config.SocketCertificateFile, s.CertificateFile)
s.settings.GlobalSettings().Set(config.SocketAlwaysPresentClientCertificate, "Y")

tlsConfig, err := loadTLSConfig(s.settings.GlobalSettings())
s.Nil(err)
s.NotNil(tlsConfig)
s.Require().NotNil(tlsConfig.GetClientCertificate)

// The configured certificate is returned even when the server advertises an
// acceptable-CAs list that does not include the certificate's issuer.
cert, err := tlsConfig.GetClientCertificate(&tls.CertificateRequestInfo{
AcceptableCAs: [][]byte{[]byte("unrelated-ca-name")},
})
s.Nil(err)
s.Require().NotNil(cert)
s.Equal(tlsConfig.Certificates[0].Certificate, cert.Certificate)
}

func (s *TLSTestSuite) TestAlwaysPresentClientCertificateDefaultOff() {
s.settings.GlobalSettings().Set(config.SocketPrivateKeyFile, s.PrivateKeyFile)
s.settings.GlobalSettings().Set(config.SocketCertificateFile, s.CertificateFile)

tlsConfig, err := loadTLSConfig(s.settings.GlobalSettings())
s.Nil(err)
s.NotNil(tlsConfig)
s.Nil(tlsConfig.GetClientCertificate)

s.SetupTest()
s.settings.GlobalSettings().Set(config.SocketPrivateKeyFile, s.PrivateKeyFile)
s.settings.GlobalSettings().Set(config.SocketCertificateFile, s.CertificateFile)
s.settings.GlobalSettings().Set(config.SocketAlwaysPresentClientCertificate, "N")

tlsConfig, err = loadTLSConfig(s.settings.GlobalSettings())
s.Nil(err)
s.NotNil(tlsConfig)
s.Nil(tlsConfig.GetClientCertificate)
}

func (s *TLSTestSuite) TestAlwaysPresentClientCertificateWithoutCerts() {
s.settings.GlobalSettings().Set(config.SocketUseSSL, "Y")
s.settings.GlobalSettings().Set(config.SocketAlwaysPresentClientCertificate, "Y")

tlsConfig, err := loadTLSConfig(s.settings.GlobalSettings())
s.Nil(err)
s.NotNil(tlsConfig)
s.Nil(tlsConfig.GetClientCertificate)
}
Loading