Fix CA cert hash for root certs

Signed-off-by: Brad Davidson <brad.davidson@rancher.com>
This commit is contained in:
Brad Davidson 2022-12-08 04:05:01 +00:00 committed by Brad Davidson
parent 0919ec6755
commit 58d40327b4
3 changed files with 79 additions and 45 deletions

View File

@ -15,6 +15,7 @@ import (
"time" "time"
"github.com/pkg/errors" "github.com/pkg/errors"
certutil "github.com/rancher/dynamiclistener/cert"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
) )
@ -40,8 +41,7 @@ var (
} }
) )
type OverrideURLCallback func(config []byte) (*url.URL, error) // Info contains fields that track parsed parts of a cluster join token
type Info struct { type Info struct {
CACerts []byte `json:"cacerts,omitempty"` CACerts []byte `json:"cacerts,omitempty"`
BaseURL string `json:"baseurl,omitempty"` BaseURL string `json:"baseurl,omitempty"`
@ -52,7 +52,8 @@ type Info struct {
// String returns the token data, templated according to the token format // String returns the token data, templated according to the token format
func (i *Info) String() string { func (i *Info) String() string {
return fmt.Sprintf(tokenFormat, tokenPrefix, hashCA(i.CACerts), i.Username, i.Password) digest, _ := hashCA(i.CACerts)
return fmt.Sprintf(tokenFormat, tokenPrefix, digest, i.Username, i.Password)
} }
// ParseAndValidateToken parses a token, downloads and validates the server's CA bundle, // ParseAndValidateToken parses a token, downloads and validates the server's CA bundle,
@ -70,7 +71,7 @@ func ParseAndValidateToken(server string, token string) (*Info, error) {
return info, nil return info, nil
} }
// ParseAndValidateToken parses a token with user override, downloads and // ParseAndValidateTokenForUser parses a token with user override, downloads and
// validates the server's CA bundle, and validates it according to the caHash from the token if set. // validates the server's CA bundle, and validates it according to the caHash from the token if set.
func ParseAndValidateTokenForUser(server, token, username string) (*Info, error) { func ParseAndValidateTokenForUser(server, token, username string) (*Info, error) {
info, err := parseToken(token) info, err := parseToken(token)
@ -95,17 +96,49 @@ func (i *Info) setAndValidateServer(server string) error {
return i.validateCAHash() return i.validateCAHash()
} }
// validateCACerts returns a boolean indicating whether or not a CA bundle matches the provided hash, // validateCACerts returns a boolean indicating whether or not a CA bundle matches the
// and a string containing the hash of the CA bundle. // provided hash, and a string containing the hash of the CA bundle.
func validateCACerts(cacerts []byte, hash string) (bool, string) { func validateCACerts(cacerts []byte, hash string) (bool, string) {
newHash := hashCA(cacerts) newHash, _ := hashCA(cacerts)
return hash == newHash, newHash return hash == newHash, newHash
} }
// hashCA returns the hex-encoded SHA256 digest of a byte array. // hashCA returns the hex-encoded SHA256 digest of a CA bundle.
func hashCA(cacerts []byte) string { // If the certificate bundle contains only a single certificate, a legacy hash is generated from
digest := sha256.Sum256(cacerts) // the literal bytes of the file; usually a PEM-encoded self-signed cluster CA certificate.
return hex.EncodeToString(digest[:]) // If the certificate bundle contains more than one certificate, the hash is instead generated
// from the DER-encoded root certificate in the bundle. This allows for rotating or renewing the
// cluster CA, as long as the root CA remains the same.
func hashCA(b []byte) (string, error) {
certs, err := certutil.ParseCertsPEM(b)
if err != nil {
return "", err
}
if len(certs) > 1 {
// Bundle contains more than one cert; find the root for the first cert in the bundle and
// hash the DER of this, instead of just hashing the raw bytes of the whole file.
roots := x509.NewCertPool()
intermediates := x509.NewCertPool()
for i, cert := range certs {
if i > 0 {
if len(cert.AuthorityKeyId) == 0 || bytes.Equal(cert.AuthorityKeyId, cert.SubjectKeyId) {
roots.AddCert(cert)
} else {
intermediates.AddCert(cert)
}
}
}
if chains, err := certs[0].Verify(x509.VerifyOptions{Roots: roots, Intermediates: intermediates}); err == nil {
// It's possible but unlikely that there could be multiple valid chains back to a root
// certificate. Just use the first.
chain := chains[0]
b = chain[len(chain)-1].Raw
}
}
digest := sha256.Sum256(b)
return hex.EncodeToString(digest[:]), nil
} }
// ParseUsernamePassword returns the username and password portion of a token string, // ParseUsernamePassword returns the username and password portion of a token string,
@ -326,19 +359,24 @@ func put(u string, body []byte, client *http.Client, username, password string)
return nil return nil
} }
func FormatToken(token, certFile string) (string, error) { // FormatToken takes a username:password string, and a path to a certificate bundle, and
if len(token) == 0 { // returns a string containing the full K10 format token string. If the credentials are
return token, nil // empty, an empty token is returned. If the certificate bundle does not exist or does not
// contain a valid bundle, an error is returned.
func FormatToken(creds, certFile string) (string, error) {
if len(creds) == 0 {
return "", nil
} }
certHash := "" b, err := os.ReadFile(certFile)
if len(certFile) > 0 { if err != nil {
b, err := os.ReadFile(certFile) return "", err
if err != nil {
return "", nil
}
digest := sha256.Sum256(b)
certHash = tokenPrefix + hex.EncodeToString(digest[:]) + "::"
} }
return certHash + token, nil
digest, err := hashCA(b)
if err != nil {
return "", err
}
return tokenPrefix + digest + "::" + creds, nil
} }

View File

@ -29,13 +29,14 @@ func Test_UnitTrustedCA(t *testing.T) {
assert := assert.New(t) assert := assert.New(t)
server := newTLSServer(t, defaultUsername, defaultPassword, false) server := newTLSServer(t, defaultUsername, defaultPassword, false)
defer server.Close() defer server.Close()
digest, _ := hashCA(getServerCA(server))
testInfo := &Info{ testInfo := &Info{
CACerts: getServerCA(server), CACerts: getServerCA(server),
BaseURL: server.URL, BaseURL: server.URL,
Username: defaultUsername, Username: defaultUsername,
Password: defaultPassword, Password: defaultPassword,
caHash: hashCA(getServerCA(server)), caHash: digest,
} }
testCases := []struct { testCases := []struct {
@ -82,13 +83,14 @@ func Test_UnitUntrustedCA(t *testing.T) {
assert := assert.New(t) assert := assert.New(t)
server := newTLSServer(t, defaultUsername, defaultPassword, false) server := newTLSServer(t, defaultUsername, defaultPassword, false)
defer server.Close() defer server.Close()
digest, _ := hashCA(getServerCA(server))
testInfo := &Info{ testInfo := &Info{
CACerts: getServerCA(server), CACerts: getServerCA(server),
BaseURL: server.URL, BaseURL: server.URL,
Username: defaultUsername, Username: defaultUsername,
Password: defaultPassword, Password: defaultPassword,
caHash: hashCA(getServerCA(server)), caHash: digest,
} }
testCases := []struct { testCases := []struct {
@ -140,6 +142,7 @@ func Test_UnitInvalidTokens(t *testing.T) {
assert := assert.New(t) assert := assert.New(t)
server := newTLSServer(t, defaultUsername, defaultPassword, false) server := newTLSServer(t, defaultUsername, defaultPassword, false)
defer server.Close() defer server.Close()
digest, _ := hashCA(getServerCA(server))
testCases := []struct { testCases := []struct {
server string server string
@ -153,7 +156,7 @@ func Test_UnitInvalidTokens(t *testing.T) {
{server.URL, "K10XX::x:y", "invalid token CA hash length"}, {server.URL, "K10XX::x:y", "invalid token CA hash length"},
{server.URL, {server.URL,
"K10XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX::x:y", "K10XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX::x:y",
"token CA hash does not match the Cluster CA certificate hash: XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX != " + hashCA(getServerCA(server))}, "token CA hash does not match the Cluster CA certificate hash: XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX != " + digest},
} }
for _, testCase := range testCases { for _, testCase := range testCases {
@ -172,13 +175,14 @@ func Test_UnitInvalidCredentials(t *testing.T) {
assert := assert.New(t) assert := assert.New(t)
server := newTLSServer(t, defaultUsername, defaultPassword, false) server := newTLSServer(t, defaultUsername, defaultPassword, false)
defer server.Close() defer server.Close()
digest, _ := hashCA(getServerCA(server))
testInfo := &Info{ testInfo := &Info{
CACerts: getServerCA(server), CACerts: getServerCA(server),
BaseURL: server.URL, BaseURL: server.URL,
Username: "nobody", Username: "nobody",
Password: "invalid", Password: "invalid",
caHash: hashCA(getServerCA(server)), caHash: digest,
} }
testCases := []string{ testCases := []string{
@ -381,8 +385,14 @@ func newTLSServer(t *testing.T, username, password string, sendWrongCA bool) *ht
// getServerCA returns a byte slice containing the PEM encoding of the server's CA certificate // getServerCA returns a byte slice containing the PEM encoding of the server's CA certificate
func getServerCA(server *httptest.Server) []byte { func getServerCA(server *httptest.Server) []byte {
certLen := len(server.TLS.Certificates) bytes := []byte{}
return pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: server.TLS.Certificates[certLen-1].Certificate[0]}) for i, cert := range server.TLS.Certificates {
if i == 0 {
continue // Just return the chain, not the leaf server cert
}
bytes = append(bytes, pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: cert.Certificate[0]})...)
}
return bytes
} }
// writeServerCA writes the PEM-encoded server certificate to a given path // writeServerCA writes the PEM-encoded server certificate to a given path

View File

@ -1,6 +1,7 @@
package deps package deps
import ( import (
"bytes"
"crypto" "crypto"
cryptorand "crypto/rand" cryptorand "crypto/rand"
"crypto/sha256" "crypto/sha256"
@ -573,22 +574,7 @@ func fieldsChanged(certFile string, commonName string, organization []string, sa
return false return false
} }
verifyOpts := x509.VerifyOptions{ return !bytes.Equal(certificates[0].AuthorityKeyId, caCertificates[0].SubjectKeyId)
Roots: x509.NewCertPool(),
KeyUsages: []x509.ExtKeyUsage{
x509.ExtKeyUsageAny,
},
}
for _, cert := range caCertificates {
verifyOpts.Roots.AddCert(cert)
}
if _, err := certificates[0].Verify(verifyOpts); err != nil {
return true
}
return false
} }
func createClientCertKey(regen bool, commonName string, organization []string, altNames *certutil.AltNames, extKeyUsage []x509.ExtKeyUsage, caCertFile, caKeyFile, certFile, keyFile string) (bool, error) { func createClientCertKey(regen bool, commonName string, organization []string, altNames *certutil.AltNames, extKeyUsage []x509.ExtKeyUsage, caCertFile, caKeyFile, certFile, keyFile string) (bool, error) {