import (
"container/list"
"crypto"
+ "crypto/ecdsa"
+ "crypto/ed25519"
+ "crypto/elliptic"
"crypto/rand"
+ "crypto/rsa"
"crypto/sha512"
"crypto/x509"
"errors"
// from, or write to, this connection; that will cause the TLS
// connection to fail.
Conn net.Conn
+
+ // config is embedded by the GetCertificate or GetConfigForClient caller,
+ // for use with SupportsCertificate.
+ config *Config
}
// CertificateRequestInfo contains information from a server's
return &c.Certificates[0], nil
}
+// SupportsCertificate returns nil if the provided certificate is supported by
+// the client that sent the ClientHello. Otherwise, it returns an error
+// describing the reason for the incompatibility.
+//
+// If this ClientHelloInfo was passed to a GetConfigForClient or GetCertificate
+// callback, this method will take into account the associated Config. Note that
+// if GetConfigForClient returns a different Config, the change can't be
+// accounted for by this method.
+//
+// This function will call x509.ParseCertificate unless c.Leaf is set, which can
+// incur a significant performance cost.
+func (chi *ClientHelloInfo) SupportsCertificate(c *Certificate) error {
+ // Note we don't currently support certificate_authorities nor
+ // signature_algorithms_cert, and don't check the algorithms of the
+ // signatures on the chain (which anyway are a SHOULD, see RFC 8446,
+ // Section 4.4.2.2).
+
+ config := chi.config
+ if config == nil {
+ config = &Config{}
+ }
+ vers, ok := config.mutualVersion(chi.SupportedVersions)
+ if !ok {
+ return errors.New("no mutually supported protocol versions")
+ }
+
+ // If the client specified the name they are trying to connect to, the
+ // certificate needs to be valid for it.
+ if chi.ServerName != "" {
+ x509Cert, err := c.leaf()
+ if err != nil {
+ return fmt.Errorf("failed to parse certificate: %w", err)
+ }
+ if err := x509Cert.VerifyHostname(chi.ServerName); err != nil {
+ return fmt.Errorf("certificate is not valid for requested server name: %w", err)
+ }
+ }
+
+ // supportsRSAFallback returns nil if the certificate and connection support
+ // the static RSA key exchange, and unsupported otherwise. The logic for
+ // supporting static RSA is completely disjoint from the logic for
+ // supporting signed key exchanges, so we just check it as a fallback.
+ supportsRSAFallback := func(unsupported error) error {
+ // TLS 1.3 dropped support for the static RSA key exchange.
+ if vers == VersionTLS13 {
+ return unsupported
+ }
+ // The static RSA key exchange works by decrypting a challenge with the
+ // RSA private key, not by signing, so check the PrivateKey implements
+ // crypto.Decrypter, like *rsa.PrivateKey does.
+ if priv, ok := c.PrivateKey.(crypto.Decrypter); ok {
+ if _, ok := priv.Public().(*rsa.PublicKey); !ok {
+ return unsupported
+ }
+ } else {
+ return unsupported
+ }
+ // Finally, there needs to be a mutual cipher suite that uses the static
+ // RSA key exchange instead of ECDHE.
+ rsaCipherSuite := selectCipherSuite(chi.CipherSuites, config.cipherSuites(), func(c *cipherSuite) bool {
+ if c.flags&suiteECDHE != 0 {
+ return false
+ }
+ if vers < VersionTLS12 && c.flags&suiteTLS12 != 0 {
+ return false
+ }
+ return true
+ })
+ if rsaCipherSuite == nil {
+ return unsupported
+ }
+ return nil
+ }
+
+ // If the client sent the signature_algorithms extension, ensure it supports
+ // schemes we can use with this certificate and TLS version.
+ if len(chi.SignatureSchemes) > 0 {
+ if _, err := selectSignatureScheme(vers, c, chi.SignatureSchemes); err != nil {
+ return supportsRSAFallback(err)
+ }
+ }
+
+ // In TLS 1.3 we are done because supported_groups is only relevant to the
+ // ECDHE computation, point format negotiation is removed, cipher suites are
+ // only relevant to the AEAD choice, and static RSA does not exist.
+ if vers == VersionTLS13 {
+ return nil
+ }
+
+ // The only signed key exchange we support is ECDHE.
+ if !supportsECDHE(config, chi.SupportedCurves, chi.SupportedPoints) {
+ return supportsRSAFallback(errors.New("client doesn't support ECDHE, can only use legacy RSA key exchange"))
+ }
+
+ var ecdsaCipherSuite bool
+ if priv, ok := c.PrivateKey.(crypto.Signer); ok {
+ switch pub := priv.Public().(type) {
+ case *ecdsa.PublicKey:
+ var curve CurveID
+ switch pub.Curve {
+ case elliptic.P256():
+ curve = CurveP256
+ case elliptic.P384():
+ curve = CurveP384
+ case elliptic.P521():
+ curve = CurveP521
+ default:
+ return supportsRSAFallback(unsupportedCertificateError(c))
+ }
+ var curveOk bool
+ for _, c := range chi.SupportedCurves {
+ if c == curve && config.supportsCurve(c) {
+ curveOk = true
+ break
+ }
+ }
+ if !curveOk {
+ return errors.New("client doesn't support certificate curve")
+ }
+ ecdsaCipherSuite = true
+ case ed25519.PublicKey:
+ if vers < VersionTLS12 || len(chi.SignatureSchemes) == 0 {
+ return errors.New("connection doesn't support Ed25519")
+ }
+ ecdsaCipherSuite = true
+ case *rsa.PublicKey:
+ default:
+ return supportsRSAFallback(unsupportedCertificateError(c))
+ }
+ } else {
+ return supportsRSAFallback(unsupportedCertificateError(c))
+ }
+
+ // Make sure that there is a mutually supported cipher suite that works with
+ // this certificate. Cipher suite selection will then apply the logic in
+ // reverse to pick it. See also serverHandshakeState.cipherSuiteOk.
+ cipherSuite := selectCipherSuite(chi.CipherSuites, config.cipherSuites(), func(c *cipherSuite) bool {
+ if c.flags&suiteECDHE == 0 {
+ return false
+ }
+ if c.flags&suiteECSign != 0 {
+ if !ecdsaCipherSuite {
+ return false
+ }
+ } else {
+ if ecdsaCipherSuite {
+ return false
+ }
+ }
+ if vers < VersionTLS12 && c.flags&suiteTLS12 != 0 {
+ return false
+ }
+ return true
+ })
+ if cipherSuite == nil {
+ return supportsRSAFallback(errors.New("client doesn't support any cipher suites compatible with the certificate"))
+ }
+
+ return nil
+}
+
// BuildNameToCertificate parses c.Certificates and builds c.NameToCertificate
// from the CommonName and SubjectAlternateName fields of each of the leaf
// certificates.
}
func testingKey(s string) string { return strings.ReplaceAll(s, "TESTING KEY", "PRIVATE KEY") }
+
+func TestClientHelloInfo_SupportsCertificate(t *testing.T) {
+ rsaCert := &Certificate{
+ Certificate: [][]byte{testRSACertificate},
+ PrivateKey: testRSAPrivateKey,
+ }
+ ecdsaCert := &Certificate{
+ // ECDSA P-256 certificate
+ Certificate: [][]byte{testP256Certificate},
+ PrivateKey: testP256PrivateKey,
+ }
+ ed25519Cert := &Certificate{
+ Certificate: [][]byte{testEd25519Certificate},
+ PrivateKey: testEd25519PrivateKey,
+ }
+
+ tests := []struct {
+ c *Certificate
+ chi *ClientHelloInfo
+ wantErr string
+ }{
+ {rsaCert, &ClientHelloInfo{
+ ServerName: "example.golang",
+ SignatureSchemes: []SignatureScheme{PSSWithSHA256},
+ SupportedVersions: []uint16{VersionTLS13},
+ }, ""},
+ {ecdsaCert, &ClientHelloInfo{
+ SignatureSchemes: []SignatureScheme{PSSWithSHA256, ECDSAWithP256AndSHA256},
+ SupportedVersions: []uint16{VersionTLS13, VersionTLS12},
+ }, ""},
+ {rsaCert, &ClientHelloInfo{
+ ServerName: "example.com",
+ SignatureSchemes: []SignatureScheme{PSSWithSHA256},
+ SupportedVersions: []uint16{VersionTLS13},
+ }, "not valid for requested server name"},
+ {ecdsaCert, &ClientHelloInfo{
+ SignatureSchemes: []SignatureScheme{ECDSAWithP384AndSHA384},
+ SupportedVersions: []uint16{VersionTLS13},
+ }, "signature algorithms"},
+
+ {rsaCert, &ClientHelloInfo{
+ CipherSuites: []uint16{TLS_RSA_WITH_AES_128_GCM_SHA256},
+ SignatureSchemes: []SignatureScheme{PKCS1WithSHA1},
+ SupportedVersions: []uint16{VersionTLS13, VersionTLS12},
+ }, "signature algorithms"},
+ {rsaCert, &ClientHelloInfo{
+ CipherSuites: []uint16{TLS_RSA_WITH_AES_128_GCM_SHA256},
+ SignatureSchemes: []SignatureScheme{PKCS1WithSHA1},
+ SupportedVersions: []uint16{VersionTLS13, VersionTLS12},
+ config: &Config{
+ MaxVersion: VersionTLS12,
+ },
+ }, ""}, // Check that mutual version selection works.
+
+ {ecdsaCert, &ClientHelloInfo{
+ CipherSuites: []uint16{TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256},
+ SupportedCurves: []CurveID{CurveP256},
+ SupportedPoints: []uint8{pointFormatUncompressed},
+ SignatureSchemes: []SignatureScheme{ECDSAWithP256AndSHA256},
+ SupportedVersions: []uint16{VersionTLS12},
+ }, ""},
+ {ecdsaCert, &ClientHelloInfo{
+ CipherSuites: []uint16{TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256},
+ SupportedCurves: []CurveID{CurveP256},
+ SupportedPoints: []uint8{pointFormatUncompressed},
+ SignatureSchemes: []SignatureScheme{ECDSAWithP384AndSHA384},
+ SupportedVersions: []uint16{VersionTLS12},
+ }, ""}, // TLS 1.2 does not restrict curves based on the SignatureScheme.
+ {ecdsaCert, &ClientHelloInfo{
+ CipherSuites: []uint16{TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256},
+ SupportedCurves: []CurveID{CurveP256},
+ SupportedPoints: []uint8{pointFormatUncompressed},
+ SignatureSchemes: nil,
+ SupportedVersions: []uint16{VersionTLS12},
+ }, ""}, // TLS 1.2 comes with default signature schemes.
+ {ecdsaCert, &ClientHelloInfo{
+ CipherSuites: []uint16{TLS_RSA_WITH_AES_128_GCM_SHA256},
+ SupportedCurves: []CurveID{CurveP256},
+ SupportedPoints: []uint8{pointFormatUncompressed},
+ SignatureSchemes: []SignatureScheme{ECDSAWithP256AndSHA256},
+ SupportedVersions: []uint16{VersionTLS12},
+ }, "cipher suite"},
+ {ecdsaCert, &ClientHelloInfo{
+ CipherSuites: []uint16{TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256},
+ SupportedCurves: []CurveID{CurveP256},
+ SupportedPoints: []uint8{pointFormatUncompressed},
+ SignatureSchemes: []SignatureScheme{ECDSAWithP256AndSHA256},
+ SupportedVersions: []uint16{VersionTLS12},
+ config: &Config{
+ CipherSuites: []uint16{TLS_RSA_WITH_AES_128_GCM_SHA256},
+ },
+ }, "cipher suite"},
+ {ecdsaCert, &ClientHelloInfo{
+ CipherSuites: []uint16{TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256},
+ SupportedCurves: []CurveID{CurveP384},
+ SupportedPoints: []uint8{pointFormatUncompressed},
+ SignatureSchemes: []SignatureScheme{ECDSAWithP256AndSHA256},
+ SupportedVersions: []uint16{VersionTLS12},
+ }, "certificate curve"},
+ {ecdsaCert, &ClientHelloInfo{
+ CipherSuites: []uint16{TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256},
+ SupportedCurves: []CurveID{CurveP256},
+ SupportedPoints: []uint8{1},
+ SignatureSchemes: []SignatureScheme{ECDSAWithP256AndSHA256},
+ SupportedVersions: []uint16{VersionTLS12},
+ }, "doesn't support ECDHE"},
+ {ecdsaCert, &ClientHelloInfo{
+ CipherSuites: []uint16{TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256},
+ SupportedCurves: []CurveID{CurveP256},
+ SupportedPoints: []uint8{pointFormatUncompressed},
+ SignatureSchemes: []SignatureScheme{PSSWithSHA256},
+ SupportedVersions: []uint16{VersionTLS12},
+ }, "signature algorithms"},
+
+ {ed25519Cert, &ClientHelloInfo{
+ CipherSuites: []uint16{TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256},
+ SupportedCurves: []CurveID{CurveP256}, // only relevant for ECDHE support
+ SupportedPoints: []uint8{pointFormatUncompressed},
+ SignatureSchemes: []SignatureScheme{Ed25519},
+ SupportedVersions: []uint16{VersionTLS12},
+ }, ""},
+ {ed25519Cert, &ClientHelloInfo{
+ CipherSuites: []uint16{TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256},
+ SupportedCurves: []CurveID{CurveP256}, // only relevant for ECDHE support
+ SupportedPoints: []uint8{pointFormatUncompressed},
+ SignatureSchemes: []SignatureScheme{Ed25519},
+ SupportedVersions: []uint16{VersionTLS10},
+ }, "doesn't support Ed25519"},
+ {ed25519Cert, &ClientHelloInfo{
+ CipherSuites: []uint16{TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256},
+ SupportedCurves: []CurveID{},
+ SupportedPoints: []uint8{pointFormatUncompressed},
+ SignatureSchemes: []SignatureScheme{Ed25519},
+ SupportedVersions: []uint16{VersionTLS12},
+ }, "doesn't support ECDHE"},
+
+ {rsaCert, &ClientHelloInfo{
+ CipherSuites: []uint16{TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA},
+ SupportedCurves: []CurveID{CurveP256}, // only relevant for ECDHE support
+ SupportedPoints: []uint8{pointFormatUncompressed},
+ SupportedVersions: []uint16{VersionTLS10},
+ }, ""},
+ {rsaCert, &ClientHelloInfo{
+ CipherSuites: []uint16{TLS_RSA_WITH_AES_128_GCM_SHA256},
+ SupportedVersions: []uint16{VersionTLS12},
+ }, ""}, // static RSA fallback
+ }
+ for i, tt := range tests {
+ err := tt.chi.SupportsCertificate(tt.c)
+ switch {
+ case tt.wantErr == "" && err != nil:
+ t.Errorf("%d: unexpected error: %v", i, err)
+ case tt.wantErr != "" && err == nil:
+ t.Errorf("%d: unexpected success", i)
+ case tt.wantErr != "" && !strings.Contains(err.Error(), tt.wantErr):
+ t.Errorf("%d: got error %q, expected %q", i, err, tt.wantErr)
+ }
+ }
+}