Skip to content

Commit

Permalink
crypto/tls: implement (*ClientHelloInfo).SupportsCertificate
Browse files Browse the repository at this point in the history
We'll also use this function for a better selection logic from
Config.Certificates in a later CL.

Updates #32426

Change-Id: Ie239574d02eb7fd2cf025ec36721c8c7e082d0bc
Reviewed-on: https://go-review.googlesource.com/c/go/+/205057
Reviewed-by: Katie Hockman <katie@golang.org>
  • Loading branch information
FiloSottile committed Nov 12, 2019
1 parent ec73263 commit dd01738
Show file tree
Hide file tree
Showing 3 changed files with 329 additions and 0 deletions.
169 changes: 169 additions & 0 deletions src/crypto/tls/common.go
Expand Up @@ -7,7 +7,11 @@ package tls
import (
"container/list"
"crypto"
"crypto/ecdsa"
"crypto/ed25519"
"crypto/elliptic"
"crypto/rand"
"crypto/rsa"
"crypto/sha512"
"crypto/x509"
"errors"
Expand Down Expand Up @@ -384,6 +388,10 @@ type ClientHelloInfo struct {
// 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
Expand Down Expand Up @@ -901,6 +909,167 @@ func (c *Config) getCertificate(clientHello *ClientHelloInfo) (*Certificate, err
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.
Expand Down
1 change: 1 addition & 0 deletions src/crypto/tls/handshake_server.go
Expand Up @@ -801,5 +801,6 @@ func clientHelloInfo(c *Conn, clientHello *clientHelloMsg) *ClientHelloInfo {
SupportedProtos: clientHello.alpnProtocols,
SupportedVersions: supportedVersions,
Conn: c.conn,
config: c.config,
}
}
159 changes: 159 additions & 0 deletions src/crypto/tls/tls_test.go
Expand Up @@ -1045,3 +1045,162 @@ func TestBuildNameToCertificate_doesntModifyCertificates(t *testing.T) {
}

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)
}
}
}

0 comments on commit dd01738

Please sign in to comment.