diff --git a/pkg/clientaccess/token.go b/pkg/clientaccess/token.go index cfad23b740..1dce962e86 100644 --- a/pkg/clientaccess/token.go +++ b/pkg/clientaccess/token.go @@ -15,6 +15,7 @@ import ( "time" "github.com/pkg/errors" + certutil "github.com/rancher/dynamiclistener/cert" "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 { CACerts []byte `json:"cacerts,omitempty"` BaseURL string `json:"baseurl,omitempty"` @@ -52,7 +52,8 @@ type Info struct { // String returns the token data, templated according to the token format 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, @@ -70,7 +71,7 @@ func ParseAndValidateToken(server string, token string) (*Info, error) { 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. func ParseAndValidateTokenForUser(server, token, username string) (*Info, error) { info, err := parseToken(token) @@ -95,17 +96,49 @@ func (i *Info) setAndValidateServer(server string) error { return i.validateCAHash() } -// validateCACerts returns a boolean indicating whether or not a CA bundle matches the provided hash, -// and a string containing the hash of the CA bundle. +// validateCACerts returns a boolean indicating whether or not a CA bundle matches the +// provided hash, and a string containing the hash of the CA bundle. func validateCACerts(cacerts []byte, hash string) (bool, string) { - newHash := hashCA(cacerts) + newHash, _ := hashCA(cacerts) return hash == newHash, newHash } -// hashCA returns the hex-encoded SHA256 digest of a byte array. -func hashCA(cacerts []byte) string { - digest := sha256.Sum256(cacerts) - return hex.EncodeToString(digest[:]) +// hashCA returns the hex-encoded SHA256 digest of a CA bundle. +// If the certificate bundle contains only a single certificate, a legacy hash is generated from +// the literal bytes of the file; usually a PEM-encoded self-signed cluster CA certificate. +// 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, @@ -326,19 +359,24 @@ func put(u string, body []byte, client *http.Client, username, password string) return nil } -func FormatToken(token, certFile string) (string, error) { - if len(token) == 0 { - return token, nil +// FormatToken takes a username:password string, and a path to a certificate bundle, and +// returns a string containing the full K10 format token string. If the credentials are +// 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 := "" - if len(certFile) > 0 { - b, err := os.ReadFile(certFile) - if err != nil { - return "", nil - } - digest := sha256.Sum256(b) - certHash = tokenPrefix + hex.EncodeToString(digest[:]) + "::" + b, err := os.ReadFile(certFile) + if err != nil { + return "", err } - return certHash + token, nil + + digest, err := hashCA(b) + if err != nil { + return "", err + } + + return tokenPrefix + digest + "::" + creds, nil } diff --git a/pkg/clientaccess/token_test.go b/pkg/clientaccess/token_test.go index b16e4f1ff9..76c458797e 100644 --- a/pkg/clientaccess/token_test.go +++ b/pkg/clientaccess/token_test.go @@ -29,13 +29,14 @@ func Test_UnitTrustedCA(t *testing.T) { assert := assert.New(t) server := newTLSServer(t, defaultUsername, defaultPassword, false) defer server.Close() + digest, _ := hashCA(getServerCA(server)) testInfo := &Info{ CACerts: getServerCA(server), BaseURL: server.URL, Username: defaultUsername, Password: defaultPassword, - caHash: hashCA(getServerCA(server)), + caHash: digest, } testCases := []struct { @@ -82,13 +83,14 @@ func Test_UnitUntrustedCA(t *testing.T) { assert := assert.New(t) server := newTLSServer(t, defaultUsername, defaultPassword, false) defer server.Close() + digest, _ := hashCA(getServerCA(server)) testInfo := &Info{ CACerts: getServerCA(server), BaseURL: server.URL, Username: defaultUsername, Password: defaultPassword, - caHash: hashCA(getServerCA(server)), + caHash: digest, } testCases := []struct { @@ -140,6 +142,7 @@ func Test_UnitInvalidTokens(t *testing.T) { assert := assert.New(t) server := newTLSServer(t, defaultUsername, defaultPassword, false) defer server.Close() + digest, _ := hashCA(getServerCA(server)) testCases := []struct { server string @@ -153,7 +156,7 @@ func Test_UnitInvalidTokens(t *testing.T) { {server.URL, "K10XX::x:y", "invalid token CA hash length"}, {server.URL, "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 { @@ -172,13 +175,14 @@ func Test_UnitInvalidCredentials(t *testing.T) { assert := assert.New(t) server := newTLSServer(t, defaultUsername, defaultPassword, false) defer server.Close() + digest, _ := hashCA(getServerCA(server)) testInfo := &Info{ CACerts: getServerCA(server), BaseURL: server.URL, Username: "nobody", Password: "invalid", - caHash: hashCA(getServerCA(server)), + caHash: digest, } 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 func getServerCA(server *httptest.Server) []byte { - certLen := len(server.TLS.Certificates) - return pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: server.TLS.Certificates[certLen-1].Certificate[0]}) + bytes := []byte{} + 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 diff --git a/pkg/daemons/control/deps/deps.go b/pkg/daemons/control/deps/deps.go index e439b8c7c7..be10d9ebca 100644 --- a/pkg/daemons/control/deps/deps.go +++ b/pkg/daemons/control/deps/deps.go @@ -1,6 +1,7 @@ package deps import ( + "bytes" "crypto" cryptorand "crypto/rand" "crypto/sha256" @@ -573,22 +574,7 @@ func fieldsChanged(certFile string, commonName string, organization []string, sa return false } - verifyOpts := x509.VerifyOptions{ - 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 + return !bytes.Equal(certificates[0].AuthorityKeyId, caCertificates[0].SubjectKeyId) } func createClientCertKey(regen bool, commonName string, organization []string, altNames *certutil.AltNames, extKeyUsage []x509.ExtKeyUsage, caCertFile, caKeyFile, certFile, keyFile string) (bool, error) {