2016-10-24 05:22:00 +00:00
package tlsutil
import (
"crypto/tls"
"crypto/x509"
2018-05-25 18:47:19 +00:00
"encoding/pem"
2016-10-24 05:22:00 +00:00
"fmt"
"io/ioutil"
"net"
2018-05-08 20:32:07 +00:00
"strings"
2016-10-24 05:22:00 +00:00
"time"
2017-11-15 01:53:23 +00:00
"github.com/hashicorp/nomad/nomad/structs/config"
2016-10-24 05:22:00 +00:00
)
2018-05-09 20:30:02 +00:00
// supportedTLSVersions are the current TLS versions that Nomad supports
var supportedTLSVersions = map [ string ] uint16 {
"tls10" : tls . VersionTLS10 ,
"tls11" : tls . VersionTLS11 ,
"tls12" : tls . VersionTLS12 ,
}
// supportedTLSCiphers are the complete list of TLS ciphers supported by Nomad
var supportedTLSCiphers = map [ string ] uint16 {
"TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305" : tls . TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305 ,
"TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305" : tls . TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305 ,
"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256" : tls . TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 ,
"TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256" : tls . TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 ,
"TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384" : tls . TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 ,
"TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384" : tls . TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384 ,
"TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256" : tls . TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256 ,
"TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA" : tls . TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA ,
"TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256" : tls . TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256 ,
"TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA" : tls . TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA ,
"TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA" : tls . TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA ,
"TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA" : tls . TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA ,
"TLS_RSA_WITH_AES_128_GCM_SHA256" : tls . TLS_RSA_WITH_AES_128_GCM_SHA256 ,
"TLS_RSA_WITH_AES_256_GCM_SHA384" : tls . TLS_RSA_WITH_AES_256_GCM_SHA384 ,
"TLS_RSA_WITH_AES_128_CBC_SHA256" : tls . TLS_RSA_WITH_AES_128_CBC_SHA256 ,
"TLS_RSA_WITH_AES_128_CBC_SHA" : tls . TLS_RSA_WITH_AES_128_CBC_SHA ,
"TLS_RSA_WITH_AES_256_CBC_SHA" : tls . TLS_RSA_WITH_AES_256_CBC_SHA ,
}
2018-08-07 18:14:40 +00:00
var rsa string = "RSA"
var ecdsa string = "ECDSA"
var supportedCipherSignatures = map [ string ] string {
"TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305" : rsa ,
"TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305" : ecdsa ,
"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256" : rsa ,
"TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256" : ecdsa ,
"TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384" : rsa ,
"TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384" : ecdsa ,
"TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256" : rsa ,
"TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA" : rsa ,
"TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256" : ecdsa ,
"TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA" : ecdsa ,
"TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA" : rsa ,
"TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA" : ecdsa ,
"TLS_RSA_WITH_AES_128_GCM_SHA256" : rsa ,
"TLS_RSA_WITH_AES_256_GCM_SHA384" : rsa ,
"TLS_RSA_WITH_AES_128_CBC_SHA256" : rsa ,
"TLS_RSA_WITH_AES_128_CBC_SHA" : rsa ,
"TLS_RSA_WITH_AES_256_CBC_SHA" : rsa ,
}
var signatureAlgorithmMapping = map [ x509 . SignatureAlgorithm ] string {
x509 . MD2WithRSA : rsa ,
x509 . MD5WithRSA : rsa ,
x509 . SHA1WithRSA : rsa ,
x509 . SHA256WithRSA : rsa ,
x509 . SHA384WithRSA : rsa ,
x509 . SHA512WithRSA : rsa ,
x509 . ECDSAWithSHA1 : ecdsa ,
x509 . ECDSAWithSHA256 : ecdsa ,
x509 . ECDSAWithSHA384 : ecdsa ,
x509 . ECDSAWithSHA512 : ecdsa ,
x509 . SHA256WithRSAPSS : rsa ,
x509 . SHA384WithRSAPSS : rsa ,
x509 . SHA512WithRSAPSS : rsa ,
}
func cipherSuitesSupportSignatureAlgorithm ( supportedSignature x509 . SignatureAlgorithm , supportedCipherSuites [ ] string ) ( bool , error ) {
supportedSignatureAlgorithm , ok := signatureAlgorithmMapping [ supportedSignature ]
if ! ok {
return false , fmt . Errorf ( "Unsupported signature scheme: %s" , supportedSignature . String ( ) )
}
for _ , cipher := range supportedCipherSuites {
cipherSupportedAlg := supportedCipherSignatures [ cipher ]
if supportedSignatureAlgorithm == cipherSupportedAlg {
return true , nil
}
}
return false , fmt . Errorf ( "Specified cipher suites don't support %s, consider adding more cipher suites with this signature algorithm." , supportedSignatureAlgorithm )
}
2018-05-09 20:30:02 +00:00
// defaultTLSCiphers are the TLS Ciphers that are supported by default
2018-06-07 21:20:42 +00:00
var defaultTLSCiphers = [ ] string {
"TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384" ,
"TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384" ,
"TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305" ,
"TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305" ,
2018-05-09 20:30:02 +00:00
"TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256" ,
2018-06-07 21:20:42 +00:00
"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256" ,
2018-05-09 20:30:02 +00:00
"TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384" ,
2018-06-07 21:20:42 +00:00
"TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384" ,
"TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256" ,
"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256" ,
2018-05-09 20:30:02 +00:00
}
2016-11-01 18:55:29 +00:00
// RegionSpecificWrapper is used to invoke a static Region and turns a
// RegionWrapper into a Wrapper type.
func RegionSpecificWrapper ( region string , tlsWrap RegionWrapper ) Wrapper {
if tlsWrap == nil {
return nil
}
return func ( conn net . Conn ) ( net . Conn , error ) {
return tlsWrap ( region , conn )
}
}
// RegionWrapper is a function that is used to wrap a non-TLS connection and
// returns an appropriate TLS connection or error. This takes a Region as an
// argument.
type RegionWrapper func ( region string , conn net . Conn ) ( net . Conn , error )
2016-10-24 20:46:22 +00:00
// Wrapper wraps a connection and enables TLS on it.
2016-10-24 05:22:00 +00:00
type Wrapper func ( conn net . Conn ) ( net . Conn , error )
// Config used to create tls.Config
type Config struct {
// VerifyIncoming is used to verify the authenticity of incoming connections.
// This means that TCP requests are forbidden, only allowing for TLS. TLS connections
// must match a provided certificate authority. This can be used to force client auth.
VerifyIncoming bool
// VerifyOutgoing is used to verify the authenticity of outgoing connections.
// This means that TLS requests are used, and TCP requests are not made. TLS connections
// must match a provided certificate authority. This is used to verify authenticity of
// server nodes.
VerifyOutgoing bool
// VerifyServerHostname is used to enable hostname verification of servers. This
// ensures that the certificate presented is valid for server.<datacenter>.<domain>.
// This prevents a compromised client from being restarted as a server, and then
// intercepting request traffic as well as being added as a raft peer. This should be
// enabled by default with VerifyOutgoing, but for legacy reasons we cannot break
// existing clients.
VerifyServerHostname bool
// CAFile is a path to a certificate authority file. This is used with VerifyIncoming
// or VerifyOutgoing to verify the TLS connection.
CAFile string
// CertFile is used to provide a TLS certificate that is used for serving TLS connections.
// Must be provided to serve TLS connections.
CertFile string
// KeyFile is used to provide a TLS key that is used for serving TLS connections.
// Must be provided to serve TLS connections.
KeyFile string
2017-11-15 01:53:23 +00:00
// KeyLoader dynamically reloads TLS configuration.
KeyLoader * config . KeyLoader
2018-05-08 20:32:07 +00:00
// CipherSuites have a default safe configuration, or operators can override
// these values for acceptable safe alternatives.
CipherSuites [ ] uint16
2018-05-09 20:30:02 +00:00
2018-05-23 17:34:53 +00:00
// PreferServerCipherSuites controls whether the server selects the
// client's most preferred ciphersuite, or the server's most preferred
// ciphersuite. If true then the server's preference, as expressed in
// the order of elements in CipherSuites, is used.
PreferServerCipherSuites bool
2018-05-30 01:30:49 +00:00
2018-05-09 20:30:02 +00:00
// MinVersion contains the minimum SSL/TLS version that is accepted.
MinVersion uint16
2016-10-24 05:22:00 +00:00
}
2018-05-23 21:25:30 +00:00
func NewTLSConfiguration ( newConf * config . TLSConfig , verifyIncoming , verifyOutgoing bool ) ( * Config , error ) {
2018-08-06 17:54:57 +00:00
ciphers , err := ParseCiphers ( newConf )
2018-05-08 20:32:07 +00:00
if err != nil {
return nil , err
}
2018-05-09 20:30:02 +00:00
minVersion , err := ParseMinVersion ( newConf . TLSMinVersion )
if err != nil {
return nil , err
}
2018-01-16 13:02:39 +00:00
return & Config {
2018-05-23 17:34:53 +00:00
VerifyIncoming : verifyIncoming ,
VerifyOutgoing : verifyOutgoing ,
VerifyServerHostname : newConf . VerifyServerHostname ,
CAFile : newConf . CAFile ,
CertFile : newConf . CertFile ,
KeyFile : newConf . KeyFile ,
KeyLoader : newConf . GetKeyLoader ( ) ,
CipherSuites : ciphers ,
MinVersion : minVersion ,
PreferServerCipherSuites : newConf . TLSPreferServerCipherSuites ,
2018-05-08 20:32:07 +00:00
} , nil
2018-01-16 13:02:39 +00:00
}
2016-10-24 05:22:00 +00:00
// AppendCA opens and parses the CA file and adds the certificates to
// the provided CertPool.
func ( c * Config ) AppendCA ( pool * x509 . CertPool ) error {
if c . CAFile == "" {
return nil
}
// Read the file
data , err := ioutil . ReadFile ( c . CAFile )
if err != nil {
return fmt . Errorf ( "Failed to read CA file: %v" , err )
}
2018-05-29 22:25:43 +00:00
block , rest := pem . Decode ( data )
2018-05-29 22:44:30 +00:00
if err := validateCertificate ( block ) ; err != nil {
return err
2018-05-25 18:47:19 +00:00
}
2018-05-29 22:44:30 +00:00
for len ( rest ) > 0 {
block , rest = pem . Decode ( rest )
if err := validateCertificate ( block ) ; err != nil {
return err
}
2018-05-25 18:47:19 +00:00
}
2016-10-24 05:22:00 +00:00
if ! pool . AppendCertsFromPEM ( data ) {
2018-05-25 18:47:19 +00:00
return fmt . Errorf ( "Failed to add any CA certificates" )
2016-10-24 05:22:00 +00:00
}
2018-05-29 22:44:30 +00:00
return nil
}
2018-05-29 22:25:43 +00:00
2018-05-29 22:44:30 +00:00
// validateCertificate checks to ensure a certificate is valid. If it is not,
// return a descriptive error of why the certificate is invalid.
func validateCertificate ( block * pem . Block ) error {
if block == nil {
return fmt . Errorf ( "Failed to decode CA file from pem format" )
}
2018-05-29 22:25:43 +00:00
2018-05-29 22:44:30 +00:00
// Parse the certificate to ensure that it is properly formatted
if _ , err := x509 . ParseCertificates ( block . Bytes ) ; err != nil {
return fmt . Errorf ( "Failed to parse CA file: %v" , err )
2018-05-29 22:25:43 +00:00
}
2016-10-24 05:22:00 +00:00
return nil
}
2017-11-15 01:53:23 +00:00
// LoadKeyPair is used to open and parse a certificate and key file
func ( c * Config ) LoadKeyPair ( ) ( * tls . Certificate , error ) {
2016-10-24 05:22:00 +00:00
if c . CertFile == "" || c . KeyFile == "" {
return nil , nil
}
2017-11-15 01:53:23 +00:00
if c . KeyLoader == nil {
return nil , fmt . Errorf ( "No Keyloader object to perform LoadKeyPair" )
}
cert , err := c . KeyLoader . LoadKeyPair ( c . CertFile , c . KeyFile )
2016-10-24 05:22:00 +00:00
if err != nil {
return nil , fmt . Errorf ( "Failed to load cert/key pair: %v" , err )
}
2017-11-15 01:53:23 +00:00
return cert , err
2016-10-24 05:22:00 +00:00
}
// OutgoingTLSConfig generates a TLS configuration for outgoing
// requests. It will return a nil config if this configuration should
2017-11-15 01:53:23 +00:00
// not use TLS for outgoing connections. Provides a callback to
// fetch certificates, allowing for reloading on the fly.
2016-10-24 05:22:00 +00:00
func ( c * Config ) OutgoingTLSConfig ( ) ( * tls . Config , error ) {
// If VerifyServerHostname is true, that implies VerifyOutgoing
if c . VerifyServerHostname {
c . VerifyOutgoing = true
}
if ! c . VerifyOutgoing {
return nil , nil
}
// Create the tlsConfig
tlsConfig := & tls . Config {
2018-05-23 17:34:53 +00:00
RootCAs : x509 . NewCertPool ( ) ,
InsecureSkipVerify : true ,
CipherSuites : c . CipherSuites ,
MinVersion : c . MinVersion ,
PreferServerCipherSuites : c . PreferServerCipherSuites ,
2016-10-24 05:22:00 +00:00
}
2016-11-01 18:55:29 +00:00
if c . VerifyServerHostname {
tlsConfig . InsecureSkipVerify = false
}
2016-10-24 05:22:00 +00:00
// Ensure we have a CA if VerifyOutgoing is set
if c . VerifyOutgoing && c . CAFile == "" {
return nil , fmt . Errorf ( "VerifyOutgoing set, and no CA certificate provided!" )
}
// Parse the CA cert if any
err := c . AppendCA ( tlsConfig . RootCAs )
if err != nil {
return nil , err
}
2017-11-15 01:53:23 +00:00
cert , err := c . LoadKeyPair ( )
2016-10-24 05:22:00 +00:00
if err != nil {
return nil , err
} else if cert != nil {
2017-11-15 01:53:23 +00:00
tlsConfig . GetCertificate = c . KeyLoader . GetOutgoingCertificate
tlsConfig . GetClientCertificate = c . KeyLoader . GetClientCertificate
2016-10-24 05:22:00 +00:00
}
return tlsConfig , nil
}
2016-10-24 20:46:22 +00:00
// OutgoingTLSWrapper returns a a Wrapper based on the OutgoingTLS
2016-10-24 05:22:00 +00:00
// configuration. If hostname verification is on, the wrapper
// will properly generate the dynamic server name for verification.
2016-11-01 18:55:29 +00:00
func ( c * Config ) OutgoingTLSWrapper ( ) ( RegionWrapper , error ) {
2016-10-24 05:22:00 +00:00
// Get the TLS config
tlsConfig , err := c . OutgoingTLSConfig ( )
if err != nil {
return nil , err
}
// Check if TLS is not enabled
if tlsConfig == nil {
return nil , nil
}
2016-11-01 18:55:29 +00:00
// Generate the wrapper based on hostname verification
if c . VerifyServerHostname {
wrapper := func ( region string , conn net . Conn ) ( net . Conn , error ) {
2017-02-28 00:00:19 +00:00
conf := tlsConfig . Clone ( )
2016-11-01 18:55:29 +00:00
conf . ServerName = "server." + region + ".nomad"
2017-02-28 00:00:19 +00:00
return WrapTLSClient ( conn , conf )
2016-11-01 18:55:29 +00:00
}
return wrapper , nil
} else {
wrapper := func ( dc string , c net . Conn ) ( net . Conn , error ) {
return WrapTLSClient ( c , tlsConfig )
}
return wrapper , nil
2016-10-24 05:22:00 +00:00
}
2016-11-01 18:55:29 +00:00
2016-10-24 05:22:00 +00:00
}
// Wrap a net.Conn into a client tls connection, performing any
// additional verification as needed.
//
// As of go 1.3, crypto/tls only supports either doing no certificate
// verification, or doing full verification including of the peer's
// DNS name. For consul, we want to validate that the certificate is
// signed by a known CA, but because consul doesn't use DNS names for
// node names, we don't verify the certificate DNS names. Since go 1.3
// no longer supports this mode of operation, we have to do it
// manually.
func WrapTLSClient ( conn net . Conn , tlsConfig * tls . Config ) ( net . Conn , error ) {
var err error
var tlsConn * tls . Conn
tlsConn = tls . Client ( conn , tlsConfig )
// If crypto/tls is doing verification, there's no need to do
// our own.
if tlsConfig . InsecureSkipVerify == false {
return tlsConn , nil
}
if err = tlsConn . Handshake ( ) ; err != nil {
tlsConn . Close ( )
return nil , err
}
// The following is lightly-modified from the doFullHandshake
// method in crypto/tls's handshake_client.go.
opts := x509 . VerifyOptions {
Roots : tlsConfig . RootCAs ,
CurrentTime : time . Now ( ) ,
DNSName : "" ,
Intermediates : x509 . NewCertPool ( ) ,
}
certs := tlsConn . ConnectionState ( ) . PeerCertificates
for i , cert := range certs {
if i == 0 {
continue
}
opts . Intermediates . AddCert ( cert )
}
_ , err = certs [ 0 ] . Verify ( opts )
if err != nil {
tlsConn . Close ( )
return nil , err
}
return tlsConn , err
}
// IncomingTLSConfig generates a TLS configuration for incoming requests
func ( c * Config ) IncomingTLSConfig ( ) ( * tls . Config , error ) {
// Create the tlsConfig
tlsConfig := & tls . Config {
2018-05-23 17:34:53 +00:00
ClientCAs : x509 . NewCertPool ( ) ,
ClientAuth : tls . NoClientCert ,
CipherSuites : c . CipherSuites ,
MinVersion : c . MinVersion ,
PreferServerCipherSuites : c . PreferServerCipherSuites ,
2016-10-24 05:22:00 +00:00
}
// Parse the CA cert if any
err := c . AppendCA ( tlsConfig . ClientCAs )
if err != nil {
return nil , err
}
// Add cert/key
2017-11-15 01:53:23 +00:00
cert , err := c . LoadKeyPair ( )
2016-10-24 05:22:00 +00:00
if err != nil {
return nil , err
} else if cert != nil {
2017-11-15 01:53:23 +00:00
tlsConfig . GetCertificate = c . KeyLoader . GetOutgoingCertificate
2016-10-24 05:22:00 +00:00
}
// Check if we require verification
if c . VerifyIncoming {
tlsConfig . ClientAuth = tls . RequireAndVerifyClientCert
if c . CAFile == "" {
return nil , fmt . Errorf ( "VerifyIncoming set, and no CA certificate provided!" )
}
if cert == nil {
return nil , fmt . Errorf ( "VerifyIncoming set, and no Cert/Key pair provided!" )
}
}
return tlsConfig , nil
}
2018-05-08 20:32:07 +00:00
// ParseCiphers parses ciphersuites from the comma-separated string into
// recognized slice
2018-08-06 17:54:57 +00:00
func ParseCiphers ( tlsConfig * config . TLSConfig ) ( [ ] uint16 , error ) {
2018-05-08 20:32:07 +00:00
suites := [ ] uint16 { }
2018-08-06 17:54:57 +00:00
cipherStr := strings . TrimSpace ( tlsConfig . TLSCipherSuites )
2018-05-08 20:32:07 +00:00
var ciphers [ ] string
if cipherStr == "" {
2018-05-09 20:30:02 +00:00
ciphers = defaultTLSCiphers
2018-05-08 20:32:07 +00:00
} else {
2018-08-06 17:54:57 +00:00
ciphers = strings . Split ( tlsConfig . TLSCipherSuites , "," )
2018-05-08 20:32:07 +00:00
}
for _ , cipher := range ciphers {
2018-05-09 20:30:02 +00:00
c , ok := supportedTLSCiphers [ cipher ]
if ! ok {
return suites , fmt . Errorf ( "unsupported TLS cipher %q" , cipher )
2018-05-08 20:32:07 +00:00
}
2018-05-09 20:30:02 +00:00
suites = append ( suites , c )
2018-05-08 20:32:07 +00:00
}
return suites , nil
}
2018-05-09 20:30:02 +00:00
// ParseMinVersion parses the specified minimum TLS version for the Nomad agent
func ParseMinVersion ( version string ) ( uint16 , error ) {
if version == "" {
return supportedTLSVersions [ "tls12" ] , nil
}
vers , ok := supportedTLSVersions [ version ]
if ! ok {
return 0 , fmt . Errorf ( "unsupported TLS version %q" , version )
}
return vers , nil
}
2018-06-08 18:33:58 +00:00
2018-06-08 19:10:10 +00:00
// ShouldReloadRPCConnections compares two TLS Configurations and determines
// whether they differ such that RPC connections should be reloaded
2018-06-08 18:33:58 +00:00
func ShouldReloadRPCConnections ( old , new * config . TLSConfig ) ( bool , error ) {
2018-06-08 19:10:10 +00:00
var certificateInfoEqual bool
var rpcInfoEqual bool
2018-06-08 18:33:58 +00:00
// If already configured with TLS, compare with the new TLS configuration
if new != nil {
var err error
2018-06-08 19:10:10 +00:00
certificateInfoEqual , err = new . CertificateInfoIsEqual ( old )
2018-06-08 18:33:58 +00:00
if err != nil {
return false , err
}
2018-06-08 19:10:10 +00:00
} else if new == nil && old == nil {
certificateInfoEqual = true
2018-06-08 18:33:58 +00:00
}
2018-06-08 19:10:10 +00:00
if new != nil && old != nil && new . EnableRPC == old . EnableRPC {
rpcInfoEqual = true
2018-06-08 18:33:58 +00:00
}
2018-06-08 19:10:10 +00:00
return ( ! rpcInfoEqual || ! certificateInfoEqual ) , nil
2018-06-08 18:33:58 +00:00
}