mirror of https://github.com/k3s-io/k3s
Fix CA cert hash for root certs
Signed-off-by: Brad Davidson <brad.davidson@rancher.com>pull/6911/head
parent
0919ec6755
commit
58d40327b4
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
Loading…
Reference in New Issue