diff --git a/config/configuration.go b/config/configuration.go index 1425cde7b..0e220a085 100644 --- a/config/configuration.go +++ b/config/configuration.go @@ -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 ( diff --git a/tls.go b/tls.go index 335d8e5d7..23ff804b2 100644 --- a/tls.go +++ b/tls.go @@ -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 } diff --git a/tls_test.go b/tls_test.go index ea9ea93a6..86cb09a7c 100644 --- a/tls_test.go +++ b/tls_test.go @@ -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) +}