From 5fc1a9a87ca18d4181a0cf2fa207c9a8369d2756 Mon Sep 17 00:00:00 2001 From: liz Date: Mon, 5 Nov 2018 14:29:38 -0500 Subject: [PATCH] Option to generate CSRs instead of issued certificates --- cmd/kubeadm/app/cmd/alpha/BUILD | 1 + cmd/kubeadm/app/cmd/alpha/certs.go | 15 ++ cmd/kubeadm/app/cmd/alpha/certs_test.go | 14 ++ cmd/kubeadm/app/cmd/options/certs.go | 10 ++ cmd/kubeadm/app/cmd/options/constant.go | 6 + cmd/kubeadm/app/cmd/phases/BUILD | 11 +- cmd/kubeadm/app/cmd/phases/certs.go | 29 +++- cmd/kubeadm/app/cmd/phases/certs_test.go | 77 ++++++++++ cmd/kubeadm/app/phases/certs/BUILD | 1 + cmd/kubeadm/app/phases/certs/certs.go | 46 ++++++ cmd/kubeadm/app/phases/certs/certs_test.go | 116 +++++++++++++++ cmd/kubeadm/app/util/pkiutil/pki_helpers.go | 137 +++++++++++++++++- .../app/util/pkiutil/pki_helpers_test.go | 27 +++- cmd/kubeadm/test/cmd/BUILD | 3 + cmd/kubeadm/test/cmd/init_test.go | 30 ++++ cmd/kubeadm/test/cmd/util.go | 12 +- 16 files changed, 526 insertions(+), 9 deletions(-) create mode 100644 cmd/kubeadm/app/cmd/phases/certs_test.go diff --git a/cmd/kubeadm/app/cmd/alpha/BUILD b/cmd/kubeadm/app/cmd/alpha/BUILD index 48301d27a3..1e2b35048c 100644 --- a/cmd/kubeadm/app/cmd/alpha/BUILD +++ b/cmd/kubeadm/app/cmd/alpha/BUILD @@ -62,6 +62,7 @@ go_test( embed = [":go_default_library"], deps = [ "//cmd/kubeadm/app/constants:go_default_library", + "//cmd/kubeadm/app/phases/certs:go_default_library", "//cmd/kubeadm/app/util/pkiutil:go_default_library", "//cmd/kubeadm/test:go_default_library", "//cmd/kubeadm/test/certs:go_default_library", diff --git a/cmd/kubeadm/app/cmd/alpha/certs.go b/cmd/kubeadm/app/cmd/alpha/certs.go index 320a21e993..08ee5294d2 100644 --- a/cmd/kubeadm/app/cmd/alpha/certs.go +++ b/cmd/kubeadm/app/cmd/alpha/certs.go @@ -76,6 +76,8 @@ type renewConfig struct { kubeconfigPath string cfg kubeadmapiv1beta1.InitConfiguration useAPI bool + useCSR bool + csrPath string } func getRenewSubCommands() []*cobra.Command { @@ -126,6 +128,8 @@ func addFlags(cmd *cobra.Command, cfg *renewConfig) { options.AddConfigFlag(cmd.Flags(), &cfg.cfgPath) options.AddCertificateDirFlag(cmd.Flags(), &cfg.cfg.CertificatesDir) options.AddKubeConfigFlag(cmd.Flags(), &cfg.kubeconfigPath) + options.AddCSRFlag(cmd.Flags(), &cfg.useCSR) + options.AddCSRDirFlag(cmd.Flags(), &cfg.csrPath) cmd.Flags().BoolVar(&cfg.useAPI, "use-api", cfg.useAPI, "Use the Kubernetes certificate API to renew certificates") } @@ -133,6 +137,17 @@ func generateRenewalFunction(cert *certsphase.KubeadmCert, caCert *certsphase.Ku return func() { internalcfg, err := configutil.ConfigFileAndDefaultsToInternalConfig(cfg.cfgPath, &cfg.cfg) kubeadmutil.CheckErr(err) + + if cfg.useCSR { + path := cfg.csrPath + if path == "" { + path = cfg.cfg.CertificatesDir + } + err := certsphase.CreateCSR(cert, internalcfg, path) + kubeadmutil.CheckErr(err) + return + } + renewer, err := getRenewer(cfg, caCert.BaseName) kubeadmutil.CheckErr(err) diff --git a/cmd/kubeadm/app/cmd/alpha/certs_test.go b/cmd/kubeadm/app/cmd/alpha/certs_test.go index be73d693de..47798fd76c 100644 --- a/cmd/kubeadm/app/cmd/alpha/certs_test.go +++ b/cmd/kubeadm/app/cmd/alpha/certs_test.go @@ -30,6 +30,7 @@ import ( "github.com/spf13/cobra" kubeadmconstants "k8s.io/kubernetes/cmd/kubeadm/app/constants" + "k8s.io/kubernetes/cmd/kubeadm/app/phases/certs" "k8s.io/kubernetes/cmd/kubeadm/app/util/pkiutil" testutil "k8s.io/kubernetes/cmd/kubeadm/test" certstestutil "k8s.io/kubernetes/cmd/kubeadm/test/certs" @@ -219,3 +220,16 @@ func TestRunRenewCommands(t *testing.T) { }) } } + +func TestRenewUsingCSR(t *testing.T) { + tmpDir := testutil.SetupTempDir(t) + defer os.RemoveAll(tmpDir) + cert := &certs.KubeadmCertEtcdServer + + renewCmds := getRenewSubCommands() + cmdtestutil.RunSubCommand(t, renewCmds, cert.Name, "--csr-only", "--csr-dir="+tmpDir) + + if _, _, err := pkiutil.TryLoadCSRAndKeyFromDisk(tmpDir, cert.BaseName); err != nil { + t.Fatalf("couldn't load certificate %q: %v", cert.BaseName, err) + } +} diff --git a/cmd/kubeadm/app/cmd/options/certs.go b/cmd/kubeadm/app/cmd/options/certs.go index b1dddc5db9..9d826dc844 100644 --- a/cmd/kubeadm/app/cmd/options/certs.go +++ b/cmd/kubeadm/app/cmd/options/certs.go @@ -22,3 +22,13 @@ import "github.com/spf13/pflag" func AddCertificateDirFlag(fs *pflag.FlagSet, certsDir *string) { fs.StringVar(certsDir, CertificatesDir, *certsDir, "The path where to save the certificates") } + +// AddCSRFlag adds the --csr-only flag to the given flagset +func AddCSRFlag(fs *pflag.FlagSet, csr *bool) { + fs.BoolVar(csr, CSROnly, *csr, "Create CSRs instead of generating certificates") +} + +// AddCSRDirFlag adds the --csr-dir flag to the given flagset +func AddCSRDirFlag(fs *pflag.FlagSet, csrDir *string) { + fs.StringVar(csrDir, CSRDir, *csrDir, "The path to output the CSRs and private keys to") +} diff --git a/cmd/kubeadm/app/cmd/options/constant.go b/cmd/kubeadm/app/cmd/options/constant.go index 575016f1c6..acb51ecd55 100644 --- a/cmd/kubeadm/app/cmd/options/constant.go +++ b/cmd/kubeadm/app/cmd/options/constant.go @@ -78,3 +78,9 @@ const SchedulerExtraArgs = "scheduler-extra-args" // SkipTokenPrint flag instruct kubeadm to skip printing of the default bootstrap token generated by 'kubeadm init'. const SkipTokenPrint = "skip-token-print" + +// CSROnly flag instructs kubeadm to create CSRs instead of automatically creating or renewing certs +const CSROnly = "csr-only" + +// CSRDir flag sets the location for CSRs and flags to be output +const CSRDir = "csr-dir" diff --git a/cmd/kubeadm/app/cmd/phases/BUILD b/cmd/kubeadm/app/cmd/phases/BUILD index 63bddeb5d9..932f6b732f 100644 --- a/cmd/kubeadm/app/cmd/phases/BUILD +++ b/cmd/kubeadm/app/cmd/phases/BUILD @@ -52,6 +52,7 @@ go_library( "//vendor/github.com/pkg/errors:go_default_library", "//vendor/github.com/renstrom/dedent:go_default_library", "//vendor/github.com/spf13/cobra:go_default_library", + "//vendor/github.com/spf13/pflag:go_default_library", "//vendor/k8s.io/klog:go_default_library", "//vendor/k8s.io/utils/exec:go_default_library", ], @@ -59,10 +60,18 @@ go_library( go_test( name = "go_default_test", - srcs = ["util_test.go"], + srcs = [ + "certs_test.go", + "util_test.go", + ], embed = [":go_default_library"], deps = [ + "//cmd/kubeadm/app/apis/kubeadm:go_default_library", "//cmd/kubeadm/app/apis/kubeadm/v1beta1:go_default_library", + "//cmd/kubeadm/app/cmd/phases/workflow:go_default_library", + "//cmd/kubeadm/app/phases/certs:go_default_library", + "//cmd/kubeadm/app/util/pkiutil:go_default_library", + "//cmd/kubeadm/test:go_default_library", "//pkg/version:go_default_library", ], ) diff --git a/cmd/kubeadm/app/cmd/phases/certs.go b/cmd/kubeadm/app/cmd/phases/certs.go index d33bc68829..e75929e13c 100644 --- a/cmd/kubeadm/app/cmd/phases/certs.go +++ b/cmd/kubeadm/app/cmd/phases/certs.go @@ -21,6 +21,7 @@ import ( "strings" "github.com/pkg/errors" + "github.com/spf13/pflag" kubeadmapi "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm" kubeadmscheme "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm/scheme" kubeadmapiv1beta1 "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm/v1beta1" @@ -47,6 +48,11 @@ var ( ` + cmdutil.AlphaDisclaimer) ) +var ( + csrOnly bool + csrDir string +) + // certsData defines the behavior that a runtime data struct passed to the certs phase should // have. Please note that we are using an interface in order to make this phase reusable in different workflows // (and thus with different runtime data struct, all of them requested to be compliant to this interface) @@ -65,9 +71,17 @@ func NewCertsPhase() workflow.Phase { Phases: newCertSubPhases(), Run: runCerts, InheritFlags: getCertPhaseFlags("all"), + LocalFlags: localFlags(), } } +func localFlags() *pflag.FlagSet { + set := pflag.NewFlagSet("csr", pflag.ExitOnError) + options.AddCSRFlag(set, &csrOnly) + options.AddCSRDirFlag(set, &csrDir) + return set +} + // newCertSubPhases returns sub phases for certs phase func newCertSubPhases() []workflow.Phase { subPhases := []workflow.Phase{} @@ -109,6 +123,7 @@ func newCertSubPhase(certSpec *certsphase.KubeadmCert, run func(c workflow.RunDa ), Run: run, InheritFlags: getCertPhaseFlags(certSpec.Name), + LocalFlags: localFlags(), } return phase } @@ -117,6 +132,8 @@ func getCertPhaseFlags(name string) []string { flags := []string{ options.CertificatesDir, options.CfgPath, + options.CSROnly, + options.CSRDir, } if name == "all" || name == "apiserver" { flags = append(flags, @@ -140,7 +157,8 @@ func getSANDescription(certSpec *certsphase.KubeadmCert) string { defaultInternalConfig := &kubeadmapi.InitConfiguration{} kubeadmscheme.Scheme.Default(defaultConfig) - kubeadmscheme.Scheme.Convert(defaultConfig, defaultInternalConfig, nil) + err := kubeadmscheme.Scheme.Convert(defaultConfig, defaultInternalConfig, nil) + kubeadmutil.CheckErr(err) certConfig, err := certSpec.GetConfig(defaultInternalConfig) kubeadmutil.CheckErr(err) @@ -236,6 +254,15 @@ func runCertPhase(cert *certsphase.KubeadmCert, caCert *certsphase.KubeadmCert) return nil } + if csrOnly { + fmt.Printf("[certs] Generating CSR for %s instead of certificate\n", cert.BaseName) + if csrDir == "" { + csrDir = data.CertificateWriteDir() + } + + return certsphase.CreateCSR(cert, data.Cfg(), csrDir) + } + // if using external etcd, skips etcd certificates generation if data.Cfg().Etcd.External != nil && cert.CAName == "etcd-ca" { fmt.Printf("[certs] External etcd mode: Skipping %s certificate authority generation\n", cert.BaseName) diff --git a/cmd/kubeadm/app/cmd/phases/certs_test.go b/cmd/kubeadm/app/cmd/phases/certs_test.go new file mode 100644 index 0000000000..06fc61ce63 --- /dev/null +++ b/cmd/kubeadm/app/cmd/phases/certs_test.go @@ -0,0 +1,77 @@ +/* +Copyright 2018 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package phases + +import ( + "os" + "testing" + + kubeadmapi "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm" + "k8s.io/kubernetes/cmd/kubeadm/app/cmd/phases/workflow" + "k8s.io/kubernetes/cmd/kubeadm/app/phases/certs" + "k8s.io/kubernetes/cmd/kubeadm/app/util/pkiutil" + testutil "k8s.io/kubernetes/cmd/kubeadm/test" +) + +type testCertsData struct { + cfg *kubeadmapi.InitConfiguration +} + +func (t *testCertsData) Cfg() *kubeadmapi.InitConfiguration { return t.cfg } +func (t *testCertsData) ExternalCA() bool { return false } +func (t *testCertsData) CertificateDir() string { return t.cfg.CertificatesDir } +func (t *testCertsData) CertificateWriteDir() string { return t.cfg.CertificatesDir } + +func TestCertsWithCSRs(t *testing.T) { + csrDir := testutil.SetupTempDir(t) + defer os.RemoveAll(csrDir) + certDir := testutil.SetupTempDir(t) + defer os.RemoveAll(certDir) + cert := &certs.KubeadmCertAPIServer + + certsData := &testCertsData{ + cfg: testutil.GetDefaultInternalConfig(t), + } + certsData.cfg.CertificatesDir = certDir + + // global vars + csrOnly = true + csrDir = certDir + + phase := NewCertsPhase() + // find the api cert phase + var apiServerPhase *workflow.Phase + for _, phase := range phase.Phases { + if phase.Name == cert.Name { + apiServerPhase = &phase + break + } + } + + if apiServerPhase == nil { + t.Fatalf("couldn't find apiserver phase") + } + + err := apiServerPhase.Run(certsData) + if err != nil { + t.Fatalf("couldn't run API server phase: %v", err) + } + + if _, _, err := pkiutil.TryLoadCSRAndKeyFromDisk(csrDir, cert.BaseName); err != nil { + t.Fatalf("couldn't load certificate %q: %v", cert.BaseName, err) + } +} diff --git a/cmd/kubeadm/app/phases/certs/BUILD b/cmd/kubeadm/app/phases/certs/BUILD index b6947cdc12..a138ced2e8 100644 --- a/cmd/kubeadm/app/phases/certs/BUILD +++ b/cmd/kubeadm/app/phases/certs/BUILD @@ -21,6 +21,7 @@ go_test( "//cmd/kubeadm/test/certs:go_default_library", "//staging/src/k8s.io/client-go/util/cert:go_default_library", "//vendor/github.com/pkg/errors:go_default_library", + "//vendor/github.com/stretchr/testify/assert:go_default_library", ], ) diff --git a/cmd/kubeadm/app/phases/certs/certs.go b/cmd/kubeadm/app/phases/certs/certs.go index 8872ac75df..e89176686e 100644 --- a/cmd/kubeadm/app/phases/certs/certs.go +++ b/cmd/kubeadm/app/phases/certs/certs.go @@ -130,6 +130,25 @@ func CreateCACertAndKeyFiles(certSpec *KubeadmCert, cfg *kubeadmapi.InitConfigur ) } +// NewCSR will generate a new CSR and accompanying key +func NewCSR(certSpec *KubeadmCert, cfg *kubeadmapi.InitConfiguration) (*x509.CertificateRequest, *rsa.PrivateKey, error) { + certConfig, err := certSpec.GetConfig(cfg) + if err != nil { + return nil, nil, fmt.Errorf("failed to retrieve cert configuration: %v", err) + } + + return pkiutil.NewCSRAndKey(certConfig) +} + +// CreateCSR creates a certificate signing request +func CreateCSR(certSpec *KubeadmCert, cfg *kubeadmapi.InitConfiguration, path string) error { + csr, key, err := NewCSR(certSpec, cfg) + if err != nil { + return err + } + return writeCSRFilesIfNotExist(path, certSpec.BaseName, csr, key) +} + // CreateCertAndKeyFilesWithCA loads the given certificate authority from disk, then generates and writes out the given certificate and key. // The certSpec and caCertSpec should both be one of the variables from this package. func CreateCertAndKeyFilesWithCA(certSpec *KubeadmCert, caCertSpec *KubeadmCert, cfg *kubeadmapi.InitConfiguration) error { @@ -276,6 +295,33 @@ func writeKeyFilesIfNotExist(pkiDir string, baseName string, key *rsa.PrivateKey return nil } +// writeCertificateAuthorithyFilesIfNotExist write a new CSR to the given path. +// If there already is a CSR file at the given path; kubeadm tries to load it and check if it's a valid certificate. +// otherwise this function returns an error. +func writeCSRFilesIfNotExist(csrDir string, baseName string, csr *x509.CertificateRequest, key *rsa.PrivateKey) error { + if pkiutil.CSROrKeyExist(csrDir, baseName) { + _, _, err := pkiutil.TryLoadCSRAndKeyFromDisk(csrDir, baseName) + if err != nil { + return errors.Wrapf(err, "%s CSR existed but it could not be loaded properly", baseName) + } + + fmt.Printf("[certs] Using the existing %q CSR\n", baseName) + } else { + // Write .key and .csr files to disk + fmt.Printf("[certs] Generating %q key and CSR\n", baseName) + + if err := pkiutil.WriteKey(csrDir, baseName, key); err != nil { + return errors.Wrapf(err, "failure while saving %s key", baseName) + } + + if err := pkiutil.WriteCSR(csrDir, baseName, csr); err != nil { + return errors.Wrapf(err, "failure while saving %s CSR", baseName) + } + } + + return nil +} + type certKeyLocation struct { pkiDir string caBaseName string diff --git a/cmd/kubeadm/app/phases/certs/certs_test.go b/cmd/kubeadm/app/phases/certs/certs_test.go index 3e32c03710..cd5561b0ef 100644 --- a/cmd/kubeadm/app/phases/certs/certs_test.go +++ b/cmd/kubeadm/app/phases/certs/certs_test.go @@ -18,13 +18,16 @@ package certs import ( "crypto/rsa" + "crypto/sha256" "crypto/x509" + "io/ioutil" "os" "path" "path/filepath" "testing" "github.com/pkg/errors" + "github.com/stretchr/testify/assert" certutil "k8s.io/client-go/util/cert" kubeadmapi "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm" @@ -55,6 +58,18 @@ func createTestCert(t *testing.T, caCert *x509.Certificate, caKey *rsa.PrivateKe return cert, key } +func createTestCSR(t *testing.T) (*x509.CertificateRequest, *rsa.PrivateKey) { + csr, key, err := pkiutil.NewCSRAndKey( + &certutil.Config{ + CommonName: "testCert", + }) + if err != nil { + t.Fatalf("couldn't create test cert: %v", err) + } + + return csr, key +} + func TestWriteCertificateAuthorithyFilesIfNotExist(t *testing.T) { setupCert, setupKey := createCACert(t) caCert, caKey := createCACert(t) @@ -209,6 +224,75 @@ func TestWriteCertificateFilesIfNotExist(t *testing.T) { } } +func TestWriteCSRFilesIfNotExist(t *testing.T) { + csr, key := createTestCSR(t) + csr2, key2 := createTestCSR(t) + + var tests = []struct { + name string + setupFunc func(csrPath string) error + expectedError bool + expectedCSR *x509.CertificateRequest + }{ + { + name: "no files exist", + expectedCSR: csr, + }, + { + name: "other key exists", + setupFunc: func(csrPath string) error { + if err := pkiutil.WriteCSR(csrPath, "dummy", csr2); err != nil { + return err + } + return pkiutil.WriteKey(csrPath, "dummy", key2) + }, + expectedCSR: csr2, + }, + { + name: "existing CSR is garbage", + setupFunc: func(csrPath string) error { + return ioutil.WriteFile(path.Join(csrPath, "dummy.csr"), []byte("a--bunch--of-garbage"), os.ModePerm) + }, + expectedError: true, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + tmpdir := testutil.SetupTempDir(t) + defer os.RemoveAll(tmpdir) + + if test.setupFunc != nil { + if err := test.setupFunc(tmpdir); err != nil { + t.Fatalf("couldn't set up test: %v", err) + } + } + + if err := writeCSRFilesIfNotExist(tmpdir, "dummy", csr, key); err != nil { + if test.expectedError { + return + } + t.Fatalf("unexpected error %v: ", err) + } + + if test.expectedError { + t.Fatal("Expected error, but got none") + } + + parsedCSR, _, err := pkiutil.TryLoadCSRAndKeyFromDisk(tmpdir, "dummy") + if err != nil { + t.Fatalf("couldn't load csr and key: %v", err) + } + + if sha256.Sum256(test.expectedCSR.Raw) != sha256.Sum256(parsedCSR.Raw) { + t.Error("expected csr's fingerprint does not match ") + } + + }) + } + +} + func TestWriteKeyFilesIfNotExist(t *testing.T) { setupKey, _ := NewServiceAccountSigningKey() @@ -606,6 +690,38 @@ func TestValidateMethods(t *testing.T) { } } +func TestNewCSR(t *testing.T) { + kubeadmCert := KubeadmCertAPIServer + cfg := testutil.GetDefaultInternalConfig(t) + + certConfig, err := kubeadmCert.GetConfig(cfg) + if err != nil { + t.Fatalf("couldn't get cert config: %v", err) + } + + csr, _, err := NewCSR(&kubeadmCert, cfg) + + if err != nil { + t.Errorf("invalid signature on CSR: %v", err) + } + + assert.ElementsMatch(t, certConfig.Organization, csr.Subject.Organization, "organizations not equal") + + if csr.Subject.CommonName != certConfig.CommonName { + t.Errorf("expected common name %q, got %q", certConfig.CommonName, csr.Subject.CommonName) + } + + assert.ElementsMatch(t, certConfig.AltNames.DNSNames, csr.DNSNames, "dns names not equal") + + assert.Len(t, csr.IPAddresses, len(certConfig.AltNames.IPs)) + + for i, ip := range csr.IPAddresses { + if !ip.Equal(certConfig.AltNames.IPs[i]) { + t.Errorf("[%d]: %v != %v", i, ip, certConfig.AltNames.IPs[i]) + } + } +} + type pkiFiles map[string]interface{} func writePKIFiles(t *testing.T, dir string, files pkiFiles) { diff --git a/cmd/kubeadm/app/util/pkiutil/pki_helpers.go b/cmd/kubeadm/app/util/pkiutil/pki_helpers.go index 6d95b24906..20cc1ce309 100644 --- a/cmd/kubeadm/app/util/pkiutil/pki_helpers.go +++ b/cmd/kubeadm/app/util/pkiutil/pki_helpers.go @@ -17,9 +17,14 @@ limitations under the License. package pkiutil import ( + "crypto" + cryptorand "crypto/rand" "crypto/rsa" "crypto/x509" + "crypto/x509/pkix" + "encoding/pem" "fmt" + "io/ioutil" "net" "os" "path/filepath" @@ -65,6 +70,21 @@ func NewCertAndKey(caCert *x509.Certificate, caKey *rsa.PrivateKey, config *cert return cert, key, nil } +// NewCSRAndKey generates a new key and CSR and that could be signed to create the given certificate +func NewCSRAndKey(config *certutil.Config) (*x509.CertificateRequest, *rsa.PrivateKey, error) { + key, err := certutil.NewPrivateKey() + if err != nil { + return nil, nil, errors.Wrap(err, "unable to create private key") + } + + csr, err := NewCSR(*config, key) + if err != nil { + return nil, nil, errors.Wrap(err, "unable to generate CSR") + } + + return csr, key, nil +} + // HasServerAuth returns true if the given certificate is a ServerAuth func HasServerAuth(cert *x509.Certificate) bool { for i := range cert.ExtKeyUsage { @@ -78,7 +98,7 @@ func HasServerAuth(cert *x509.Certificate) bool { // WriteCertAndKey stores certificate and key at the specified location func WriteCertAndKey(pkiPath string, name string, cert *x509.Certificate, key *rsa.PrivateKey) error { if err := WriteKey(pkiPath, name, key); err != nil { - return err + return errors.Wrap(err, "couldn't write key") } return WriteCert(pkiPath, name, cert) @@ -112,6 +132,27 @@ func WriteKey(pkiPath, name string, key *rsa.PrivateKey) error { return nil } +// WriteCSR writes the pem-encoded CSR data to csrPath. +// The CSR file will be created with file mode 0644. +// If the CSR file already exists, it will be overwritten. +// The parent directory of the csrPath will be created as needed with file mode 0755. +func WriteCSR(csrDir, name string, csr *x509.CertificateRequest) error { + if csr == nil { + return errors.New("certificate request cannot be nil when writing to file") + } + + csrPath := pathForCSR(csrDir, name) + if err := os.MkdirAll(filepath.Dir(csrPath), os.FileMode(0755)); err != nil { + return errors.Wrapf(err, "failed to make directory %s", filepath.Dir(csrPath)) + } + + if err := ioutil.WriteFile(csrPath, EncodeCSRPEM(csr), os.FileMode(0644)); err != nil { + return errors.Wrapf(err, "unable to write CSR to file %s", csrPath) + } + + return nil +} + // WritePublicKey stores the given public key at the given location func WritePublicKey(pkiPath, name string, key *rsa.PublicKey) error { if key == nil { @@ -145,16 +186,27 @@ func CertOrKeyExist(pkiPath, name string) bool { return true } +// CSROrKeyExist returns true if one of the CSR or key exists +func CSROrKeyExist(csrDir, name string) bool { + csrPath := pathForCSR(csrDir, name) + keyPath := pathForKey(csrDir, name) + + _, csrErr := os.Stat(csrPath) + _, keyErr := os.Stat(keyPath) + + return !(os.IsNotExist(csrErr) && os.IsNotExist(keyErr)) +} + // TryLoadCertAndKeyFromDisk tries to load a cert and a key from the disk and validates that they are valid func TryLoadCertAndKeyFromDisk(pkiPath, name string) (*x509.Certificate, *rsa.PrivateKey, error) { cert, err := TryLoadCertFromDisk(pkiPath, name) if err != nil { - return nil, nil, err + return nil, nil, errors.Wrap(err, "failed to load certificate") } key, err := TryLoadKeyFromDisk(pkiPath, name) if err != nil { - return nil, nil, err + return nil, nil, errors.Wrap(err, "failed to load key") } return cert, key, nil @@ -207,6 +259,23 @@ func TryLoadKeyFromDisk(pkiPath, name string) (*rsa.PrivateKey, error) { return key, nil } +// TryLoadCSRAndKeyFromDisk tries to load the CSR and key from the disk +func TryLoadCSRAndKeyFromDisk(pkiPath, name string) (*x509.CertificateRequest, *rsa.PrivateKey, error) { + csrPath := pathForCSR(pkiPath, name) + + csr, err := CertificateRequestFromFile(csrPath) + if err != nil { + return nil, nil, errors.Wrapf(err, "couldn't load the certificate request %s", csrPath) + } + + key, err := TryLoadKeyFromDisk(pkiPath, name) + if err != nil { + return nil, nil, errors.Wrap(err, "couldn't load key file") + } + + return csr, key, nil +} + // TryLoadPrivatePublicKeyFromDisk tries to load the key from the disk and validates that it is valid func TryLoadPrivatePublicKeyFromDisk(pkiPath, name string) (*rsa.PrivateKey, *rsa.PublicKey, error) { privateKeyPath := pathForKey(pkiPath, name) @@ -253,6 +322,10 @@ func pathForPublicKey(pkiPath, name string) string { return filepath.Join(pkiPath, fmt.Sprintf("%s.pub", name)) } +func pathForCSR(pkiPath, name string) string { + return filepath.Join(pkiPath, fmt.Sprintf("%s.csr", name)) +} + // GetAPIServerAltNames builds an AltNames object for to be used when generating apiserver certificate func GetAPIServerAltNames(cfg *kubeadmapi.InitConfiguration) (*certutil.AltNames, error) { // advertise address @@ -373,3 +446,61 @@ func appendSANsToAltNames(altNames *certutil.AltNames, SANs []string, certName s } } } + +// EncodeCSRPEM returns PEM-encoded CSR data +func EncodeCSRPEM(csr *x509.CertificateRequest) []byte { + block := pem.Block{ + Type: certutil.CertificateRequestBlockType, + Bytes: csr.Raw, + } + return pem.EncodeToMemory(&block) +} + +func parseCSRPEM(pemCSR []byte) (*x509.CertificateRequest, error) { + block, _ := pem.Decode(pemCSR) + if block == nil { + return nil, fmt.Errorf("data doesn't contain a valid certificate request") + } + + if block.Type != certutil.CertificateRequestBlockType { + var block *pem.Block + return nil, fmt.Errorf("expected block type %q, but PEM had type %v", certutil.CertificateRequestBlockType, block.Type) + } + + return x509.ParseCertificateRequest(block.Bytes) +} + +// CertificateRequestFromFile returns the CertificateRequest from a given PEM-encoded file. +// Returns an error if the file could not be read or if the CSR could not be parsed. +func CertificateRequestFromFile(file string) (*x509.CertificateRequest, error) { + pemBlock, err := ioutil.ReadFile(file) + if err != nil { + return nil, errors.Wrap(err, "failed to read file") + } + + csr, err := parseCSRPEM(pemBlock) + if err != nil { + return nil, fmt.Errorf("error reading certificate request file %s: %v", file, err) + } + return csr, nil +} + +// NewCSR creates a new CSR +func NewCSR(cfg certutil.Config, key crypto.Signer) (*x509.CertificateRequest, error) { + template := &x509.CertificateRequest{ + Subject: pkix.Name{ + CommonName: cfg.CommonName, + Organization: cfg.Organization, + }, + DNSNames: cfg.AltNames.DNSNames, + IPAddresses: cfg.AltNames.IPs, + } + + csrBytes, err := x509.CreateCertificateRequest(cryptorand.Reader, template, key) + + if err != nil { + return nil, errors.Wrap(err, "failed to create a CSR") + } + + return x509.ParseCertificateRequest(csrBytes) +} diff --git a/cmd/kubeadm/app/util/pkiutil/pki_helpers_test.go b/cmd/kubeadm/app/util/pkiutil/pki_helpers_test.go index 41eba0c291..ae8f224bf9 100644 --- a/cmd/kubeadm/app/util/pkiutil/pki_helpers_test.go +++ b/cmd/kubeadm/app/util/pkiutil/pki_helpers_test.go @@ -29,6 +29,24 @@ import ( kubeadmapi "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm" ) +// certificateRequest is an x509 certificate request in PEM encoded format +// openssl req -new -key rsa2048.pem -sha256 -nodes -out x509certrequest.pem -subj "/C=US/CN=not-valid" +const certificateRequest = `-----BEGIN CERTIFICATE REQUEST----- +MIICZjCCAU4CAQAwITELMAkGA1UEBhMCVVMxEjAQBgNVBAMMCW5vdC12YWxpZDCC +ASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMdoBxV0SbSS+7XrgVDF/P4x +tqyun+DLxeRF5265ZOFRJDXCJgYH7wKlxlkEaHZQhnNmnqFiy96MHSKaiQmlkEm4 +EhlqTf38yEWx+t98A0CDbHsIPZ0/+MPCjb2kf+OfBXJJl908io0grs02jxN9lceL +RFrKT6vaB+6i7LxbPQcOmjF7OUqWS6S2qSpShw2GY+mJz4HM7OFb9RcN4izh+GF6 +7hajYgt7pAFyWF1ua/H98Ysn4FVgIYk30rHCNBkQpJnna7EyGYuj08VuFa088W9g +c/DCpL+VgBDwTel9tfeMxRAoLIPF9iJ8Ftr7dsRZ/Y/SnxfUJo2ed8y7dgIiLuEC +AwEAAaAAMA0GCSqGSIb3DQEBCwUAA4IBAQCOjPB/4LKa2G7LarMMLAeNqvWF9SIG +y2VGQoTn9D5blXMvnfzWSYgU6nBzf/E/32q26OwiCriuOPXfxM/cxEMOJ62u7b50 +OR52JFvQdONsCZaLgylGWppl0YeqylbTosHjsWJNlp+zjXcQHjCQ9OoLgfmrwYyD +2MsYJR4p7JZ2ZN8FF1hgMUrDzypZ0NSBKAiQMU9TFhxgyk75RNDtmX+2K35zqLyr +0otimyYwPCGPD2GHwNfvu1oP0A+/cT+rCPz6AlXhWEbz2JkLo6/muRfRl0QSRgHE +Q3+eWlA1YdqEBwvp3NEQI9BtMnzxJVWA5dvYluMNllsV/q8s2IEEAFG9 +-----END CERTIFICATE REQUEST-----` + func TestNewCertificateAuthority(t *testing.T) { cert, key, err := NewCertificateAuthority(&certutil.Config{CommonName: "kubernetes"}) @@ -435,6 +453,13 @@ func TestPathForPublicKey(t *testing.T) { } } +func TestPathForCSR(t *testing.T) { + csrPath := pathForCSR("/foo", "bar") + if csrPath != "/foo/bar.csr" { + t.Errorf("unexpected certificate path: %s", csrPath) + } +} + func TestGetAPIServerAltNames(t *testing.T) { var tests = []struct { @@ -444,7 +469,7 @@ func TestGetAPIServerAltNames(t *testing.T) { expectedIPAddresses []string }{ { - name: "ControlPlaneEndpoint DNS", + name: "", cfg: &kubeadmapi.InitConfiguration{ LocalAPIEndpoint: kubeadmapi.APIEndpoint{AdvertiseAddress: "1.2.3.4"}, ClusterConfiguration: kubeadmapi.ClusterConfiguration{ diff --git a/cmd/kubeadm/test/cmd/BUILD b/cmd/kubeadm/test/cmd/BUILD index 1e2a09a8c9..d7abaae45b 100644 --- a/cmd/kubeadm/test/cmd/BUILD +++ b/cmd/kubeadm/test/cmd/BUILD @@ -33,6 +33,9 @@ go_test( "skip", ], deps = [ + "//cmd/kubeadm/app/phases/certs:go_default_library", + "//cmd/kubeadm/app/util/pkiutil:go_default_library", + "//cmd/kubeadm/test:go_default_library", "//vendor/github.com/renstrom/dedent:go_default_library", "//vendor/sigs.k8s.io/yaml:go_default_library", ], diff --git a/cmd/kubeadm/test/cmd/init_test.go b/cmd/kubeadm/test/cmd/init_test.go index a4dfe55c43..f005be75d2 100644 --- a/cmd/kubeadm/test/cmd/init_test.go +++ b/cmd/kubeadm/test/cmd/init_test.go @@ -20,6 +20,9 @@ import ( "testing" "github.com/renstrom/dedent" + "k8s.io/kubernetes/cmd/kubeadm/app/phases/certs" + "k8s.io/kubernetes/cmd/kubeadm/app/util/pkiutil" + testutil "k8s.io/kubernetes/cmd/kubeadm/test" ) func runKubeadmInit(args ...string) (string, string, error) { @@ -191,6 +194,33 @@ func TestCmdInitConfig(t *testing.T) { } } +func TestCmdInitCertPhaseCSR(t *testing.T) { + if *kubeadmCmdSkip { + t.Log("kubeadm cmd tests being skipped") + t.Skip() + } + + csrDir := testutil.SetupTempDir(t) + + cert := &certs.KubeadmCertKubeletClient + kubeadmPath := getKubeadmPath() + _, _, err := RunCmd(kubeadmPath, + "init", + "phase", + "certs", + cert.BaseName, + "--csr-only", + "--csr-dir="+csrDir, + ) + if err != nil { + t.Fatalf("couldn't run kubeadm: %v", err) + } + + if _, _, err := pkiutil.TryLoadCSRAndKeyFromDisk(csrDir, cert.BaseName); err != nil { + t.Fatalf("couldn't load certificate %q: %v", cert.BaseName, err) + } +} + func TestCmdInitAPIPort(t *testing.T) { if *kubeadmCmdSkip { t.Log("kubeadm cmd tests being skipped") diff --git a/cmd/kubeadm/test/cmd/util.go b/cmd/kubeadm/test/cmd/util.go index 5bed6d3578..0e9656c44d 100644 --- a/cmd/kubeadm/test/cmd/util.go +++ b/cmd/kubeadm/test/cmd/util.go @@ -18,24 +18,30 @@ package kubeadm import ( "bytes" - "github.com/pkg/errors" "os/exec" "testing" + "github.com/pkg/errors" + "github.com/spf13/cobra" ) // Forked from test/e2e/framework because the e2e framework is quite bloated // for our purposes here, and modified to remove undesired logging. -// RunCmd is a utility function for kubeadm testing that executes a specified command -func RunCmd(command string, args ...string) (string, string, error) { +func runCmdNoWrap(command string, args ...string) (string, string, error) { var bout, berr bytes.Buffer cmd := exec.Command(command, args...) cmd.Stdout = &bout cmd.Stderr = &berr err := cmd.Run() stdout, stderr := bout.String(), berr.String() + return stdout, stderr, err +} + +// RunCmd is a utility function for kubeadm testing that executes a specified command +func RunCmd(command string, args ...string) (string, string, error) { + stdout, stderr, err := runCmdNoWrap(command, args...) if err != nil { return "", "", errors.Wrapf(err, "error running %s %v; \nstdout %q, \nstderr %q, \ngot error", command, args, stdout, stderr)