diff --git a/cmd/kube-etcd-signer-server/serve.go b/cmd/kube-etcd-signer-server/serve.go index f69b9f7..1785c7b 100644 --- a/cmd/kube-etcd-signer-server/serve.go +++ b/cmd/kube-etcd-signer-server/serve.go @@ -21,11 +21,15 @@ var ( serveOpts struct { caCrtFile string caKeyFile string + mCACrtFile string + mCAKeyFile string + mCASigner bool sCrtFile string sKeyFile string addr string peerCertDur string serverCertDur string + metricCertDur string csrDir string } ) @@ -36,7 +40,10 @@ func init() { serveCmd.PersistentFlags().StringVar(&serveOpts.caKeyFile, "cakey", "", "CA private key file for signer") serveCmd.PersistentFlags().StringVar(&serveOpts.sCrtFile, "servcrt", "", "Server certificate file for signer") serveCmd.PersistentFlags().StringVar(&serveOpts.sKeyFile, "servkey", "", "Server private key file for signer") + serveCmd.PersistentFlags().StringVar(&serveOpts.mCACrtFile, "metric-cacrt", "", "CA certificate file for metrics signer") + serveCmd.PersistentFlags().StringVar(&serveOpts.mCAKeyFile, "metric-cakey", "", "CA private key file for metrics signer") serveCmd.PersistentFlags().StringVar(&serveOpts.addr, "address", "0.0.0.0:6443", "Address on which the signer listens for requests") + serveCmd.PersistentFlags().StringVar(&serveOpts.metricCertDur, "metriccertdur", "8760h", "Certificate duration for etcd metrics certs (defaults to 365 days)") serveCmd.PersistentFlags().StringVar(&serveOpts.peerCertDur, "peercertdur", "8760h", "Certificate duration for etcd peer certs (defaults to 365 days)") serveCmd.PersistentFlags().StringVar(&serveOpts.serverCertDur, "servercertdur", "8760h", "Certificate duration for etcd server certs (defaults to 365 days)") serveCmd.PersistentFlags().StringVar(&serveOpts.csrDir, "csrdir", "", "Directory location where signer will save CSRs.") @@ -44,9 +51,17 @@ func init() { // validateServeOpts validates the user flag values given to the signer server func validateServeOpts(cmd *cobra.Command, args []string) error { - if serveOpts.caCrtFile == "" || serveOpts.caKeyFile == "" { - return errors.New("both --cacrt and --cakey are required flags") + caPair := 0 + if serveOpts.caCrtFile != "" && serveOpts.caKeyFile != "" { + caPair++ } + if serveOpts.mCACrtFile != "" && serveOpts.mCAKeyFile != "" { + caPair++ + } + if caPair == 0 { + return errors.New("no signer CA flags passed one cert/key pair is required") + } + if serveOpts.sCrtFile == "" || serveOpts.sKeyFile == "" { return errors.New("both --servcrt and --servkey are required flags") } @@ -67,13 +82,24 @@ func runCmdServe(cmd *cobra.Command, args []string) error { if err != nil { return fmt.Errorf("error parsing duration for etcd server cert: %v", err) } + mCertDur, err := time.ParseDuration(serveOpts.metricCertDur) + if err != nil { + return fmt.Errorf("error parsing duration for etcd metric cert: %v", err) + } + + ca := signer.SignerCAFiles{ + CACert: serveOpts.caCrtFile, + CAKey: serveOpts.caKeyFile, + MetricCACert: serveOpts.mCACrtFile, + MetricCAKey: serveOpts.mCAKeyFile, + } c := signer.Config{ - CACertFile: serveOpts.caCrtFile, - CAKeyFile: serveOpts.caKeyFile, + SignerCAFiles: ca, ServerCertFile: serveOpts.sCrtFile, ServerKeyFile: serveOpts.sKeyFile, ListenAddress: serveOpts.addr, + EtcdMetricCertDuration: mCertDur, EtcdPeerCertDuration: pCertDur, EtcdServerCertDuration: sCertDur, CSRDir: serveOpts.csrDir, diff --git a/pkg/certsigner/signer.go b/pkg/certsigner/signer.go index ec26764..ab45e22 100644 --- a/pkg/certsigner/signer.go +++ b/pkg/certsigner/signer.go @@ -30,6 +30,7 @@ import ( const ( etcdPeer = "EtcdPeer" etcdServer = "EtcdServer" + etcdMetric = "EtcdMetric" ) var ( @@ -39,6 +40,8 @@ var ( ErrInvalidOrg = errors.New("invalid organization") // ErrInvalidCN defines a global error for invalid subject common name ErrInvalidCN = errors.New("invalid subject Common Name") + // ErrProfileSupport defines a global error for a profile which was not backed by a CA signer cert.. + ErrProfileSupport = errors.New("csr profile is not currently supported") ) // CertServer is the object that handles the HTTP requests and responses. @@ -52,6 +55,10 @@ type CertServer struct { csrDir string // signer is the object that handles the approval of the CSRs signer *CertSigner + // policy + policy *config.Signing + // caFiles + caFiles *SignerCAFiles } // CertSigner signs a certiifcate using a `cfssl` Signer. @@ -72,16 +79,16 @@ type CertSigner struct { // Config holds the configuration values required to start a new signer type Config struct { - // CACertFile is the file location of the Certificate Authority certificate - CACertFile string - // CAKeyFile is the file location of the Certificate Authority private key - CAKeyFile string + // SignerCAFiles + SignerCAFiles // ServerCertFile is the file location of the server certificate ServerCertFile string // ServerKeyFile is the file location of the server private key ServerKeyFile string // ListenAddress is the address at which the server listens for requests ListenAddress string + // EtcdMetricCertDuration + EtcdMetricCertDuration time.Duration // EtcdPeerCertDuration is the cert duration for the `EtcdPeer` profile EtcdPeerCertDuration time.Duration // EtcdServerCertDuration is the cert duration for the `EtcdServer` profile @@ -90,6 +97,28 @@ type Config struct { CSRDir string } +// SignerCAFiles holds the file paths to the signer CA assets +type SignerCAFiles struct { + // CACert is the file location of the Certificate Authority certificate + CACert string + // CAKey is the file location of the Certificate Authority private key + CAKey string + // MetricCACert is the file location of the metrics Certificate Authority certificate + MetricCACert string + // MetricCAKey is the file location of the metrics Certificate Authority private key + MetricCAKey string +} + +// SignerCA stores the PEM encoded cert and key blocks. +type SignerCA struct { + // caCert is the x509 PEM encoded certificate of the CA used for the + // cfssl signer + caCert *x509.Certificate + // caCert is the x509 PEM encoded private key of the CA used for the + // cfssl signer + caKey crypto.Signer +} + // loggingHandler is the HTTP handler that logs information about requests received by the server type loggingHandler struct { h http.Handler @@ -103,16 +132,14 @@ func (l *loggingHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { // NewServer returns a CertServer object that has a CertSigner object // as a part of it func NewServer(c Config) (*CertServer, error) { - signer, err := NewSigner(c) - if err != nil { - return nil, fmt.Errorf("error setting up a signer: %v", err) - } - + policy := signerPolicy(c) mux := mux.NewRouter() server := &CertServer{ mux: mux, csrDir: c.CSRDir, - signer: signer, + policy: &policy, + + caFiles: &c.SignerCAFiles, } mux.HandleFunc("/apis/certificates.k8s.io/v1beta1/certificatesigningrequests", server.HandlePostCSR).Methods("POST") @@ -125,29 +152,58 @@ func (s *CertServer) ServeHTTP(w http.ResponseWriter, r *http.Request) { s.mux.ServeHTTP(w, r) } -// NewSigner returns a CertSigner object after filling in its attibutes -// from the `Config` provided. -func NewSigner(c Config) (*CertSigner, error) { - ca, err := ioutil.ReadFile(c.CACertFile) +// newSignerCA returns a SignerCA object of PEM encoded CA cert and keys based on the profile passed. +func newSignerCA(sc *SignerCAFiles, csr *capi.CertificateSigningRequest) (*SignerCA, error) { + var caCert, caKey string + + profile, err := getProfile(csr) if err != nil { - return nil, fmt.Errorf("error reading CA cert file %q: %v", c.CACertFile, err) + return nil, err } - cakey, err := ioutil.ReadFile(c.CAKeyFile) - if err != nil { - return nil, fmt.Errorf("error reading CA key file %q: %v", c.CAKeyFile, err) + switch profile { + case "EtcdMetric": + if sc.MetricCAKey != "" && sc.MetricCACert != "" { + caCert = sc.MetricCACert + caKey = sc.MetricCAKey + break + } + return nil, ErrProfileSupport + case "EtcdServer", "EtcdPeer": + if sc.CAKey != "" && sc.CACert != "" { + caCert = sc.CACert + caKey = sc.CAKey + break + } + return nil, ErrProfileSupport + default: + return nil, ErrInvalidOrg } + ca, err := ioutil.ReadFile(caCert) + if err != nil { + return nil, fmt.Errorf("error reading CA cert file %q: %v", caCert, err) + } + cakey, err := ioutil.ReadFile(caKey) + if err != nil { + return nil, fmt.Errorf("error reading CA key file %q: %v", caKey, err) + } parsedCA, err := helpers.ParseCertificatePEM(ca) if err != nil { - return nil, fmt.Errorf("error parsing CA cert file %q: %v", c.CACertFile, err) + return nil, fmt.Errorf("error parsing CA cert file %q: %v", caCert, err) } - privateKey, err := helpers.ParsePrivateKeyPEM(cakey) if err != nil { return nil, fmt.Errorf("Malformed private key %v", err) } - // policy is the signature configuration policy for the signer. + return &SignerCA{ + caCert: parsedCA, + caKey: privateKey, + }, nil +} + +// signerPolicy +func signerPolicy(c Config) config.Signing { policy := config.Signing{ Profiles: map[string]*config.SigningProfile{ etcdPeer: &config.SigningProfile{ @@ -169,6 +225,16 @@ func NewSigner(c Config) (*CertSigner, error) { Expiry: c.EtcdServerCertDuration, ExpiryString: c.EtcdServerCertDuration.String(), }, + etcdMetric: &config.SigningProfile{ + Usage: []string{ + string(capi.UsageKeyEncipherment), + string(capi.UsageDigitalSignature), + string(capi.UsageClientAuth), + string(capi.UsageServerAuth), + }, + Expiry: c.EtcdMetricCertDuration, + ExpiryString: c.EtcdMetricCertDuration.String(), + }, }, Default: &config.SigningProfile{ Usage: []string{ @@ -180,14 +246,20 @@ func NewSigner(c Config) (*CertSigner, error) { }, } - cfs, err := local.NewSigner(privateKey, parsedCA, signer.DefaultSigAlgo(privateKey), &policy) + return policy +} + +// NewSigner returns a CertSigner object after filling in its attibutes +// from the `Config` provided. +func NewSigner(s *SignerCA, policy *config.Signing) (*CertSigner, error) { + cfs, err := local.NewSigner(s.caKey, s.caCert, signer.DefaultSigAlgo(s.caKey), policy) if err != nil { return nil, fmt.Errorf("error setting up local cfssl signer: %v", err) } return &CertSigner{ - caCert: parsedCA, - caKey: privateKey, + caCert: s.caCert, + caKey: s.caKey, cfsslSigner: cfs, }, nil } @@ -198,18 +270,9 @@ func NewSigner(c Config) (*CertSigner, error) { // // Note: A signed certificate is issued only for etcd profiles. func (s *CertSigner) Sign(csr *capi.CertificateSigningRequest) (*capi.CertificateSigningRequest, error) { - x509CSR, err := csrutil.ParseCSR(csr) - if err != nil { - return nil, fmt.Errorf("error parsing CSR, %v", err) - } - - if err := x509CSR.CheckSignature(); err != nil { - return nil, fmt.Errorf("error validating signature of CSR: %v", err) - } - // the following step ensures that the signer server only signs CSRs from etcd nodes // that have a specific profile. All other requests are denied immediately. - profile, err := getProfile(x509CSR) + profile, err := getProfile(csr) if err != nil { csr.Status.Conditions = []capi.CertificateSigningRequestCondition{ capi.CertificateSigningRequestCondition{ @@ -245,18 +308,36 @@ func (s *CertSigner) Sign(csr *capi.CertificateSigningRequest) (*capi.Certificat // getProfile returns the profile corresponding to the CSR Subject. For now only // `etcd-peers` and `etcd-servers` are considered valid profiles. -func getProfile(csr *x509.CertificateRequest) (string, error) { - if csr.Subject.Organization != nil && len(csr.Subject.Organization) == 1 && csr.Subject.Organization[0] == "system:etcd-peers" { - if !strings.HasPrefix(csr.Subject.CommonName, "system:etcd-peer:") { - return "", ErrInvalidCN - } - return etcdPeer, nil +func getProfile(csr *capi.CertificateSigningRequest) (string, error) { + x509CSR, err := csrutil.ParseCSR(csr) + if err != nil { + return "", fmt.Errorf("error parsing CSR, %v", err) + } + if err := x509CSR.CheckSignature(); err != nil { + return "", fmt.Errorf("error validating signature of CSR: %v", err) } - if csr.Subject.Organization != nil && len(csr.Subject.Organization) == 1 && csr.Subject.Organization[0] == "system:etcd-servers" { - if !strings.HasPrefix(csr.Subject.CommonName, "system:etcd-server:") { - return "", ErrInvalidCN + if x509CSR.Subject.Organization == nil || len(x509CSR.Subject.Organization) == 0 { + return "", ErrInvalidOrg + } + + org := x509CSR.Subject.Organization[0] + cn := fmt.Sprintf(org[:len(org)-1]+"%s", ":") + switch org { + case "system:etcd-peers": + if strings.HasPrefix(x509CSR.Subject.CommonName, cn) { + return etcdPeer, nil + } + break + case "system:etcd-servers": + if strings.HasPrefix(x509CSR.Subject.CommonName, cn) { + return etcdServer, nil + } + break + case "system:etcd-metrics": + if strings.HasPrefix(x509CSR.Subject.CommonName, cn) { + return etcdMetric, nil } - return etcdServer, nil + break } return "", ErrInvalidOrg } @@ -287,7 +368,21 @@ func (s *CertServer) HandlePostCSR(w http.ResponseWriter, r *http.Request) { return } - signedCSR, err := s.signer.Sign(csr) + signerCA, err := newSignerCA(s.caFiles, csr) + if err != nil { + glog.Errorf("Error signing CSR provided in request from agent: %v", err) + http.Error(w, "Error signing csr", http.StatusBadRequest) + return + } + + signer, err := NewSigner(signerCA, s.policy) + if err != nil { + glog.Errorf("Error signing CSR provided in request from agent: %v", err) + http.Error(w, "Error signing csr", http.StatusBadRequest) + return + } + + signedCSR, err := signer.Sign(csr) if err != nil { glog.Errorf("Error signing CSR provided in request from agent: %v", err) http.Error(w, "Error signing csr", http.StatusBadRequest) diff --git a/pkg/certsigner/signer_test.go b/pkg/certsigner/signer_test.go index c80109f..42dea93 100644 --- a/pkg/certsigner/signer_test.go +++ b/pkg/certsigner/signer_test.go @@ -1,17 +1,25 @@ package certsigner import ( + "crypto/x509" + "encoding/pem" + "fmt" "io/ioutil" "os" + "reflect" "testing" + "time" capi "k8s.io/api/certificates/v1beta1" ) var ( - caCrtFile = "ca.crt" - caKeyFile = "ca.key" - csrBytes = []byte(`-----BEGIN CERTIFICATE REQUEST----- + caCrtFile = "ca.crt" + caKeyFile = "ca.key" + caMetricsCrtFile = "metric-ca.crt" + caMetricsKeyFile = "metric-ca.key" + + csrBytes = []byte(`-----BEGIN CERTIFICATE REQUEST----- MIICojCCAYoCAQAwXTELMAkGA1UEBhMCWFgxFTATBgNVBAcMDERlZmF1bHQgQ2l0 eTEaMBgGA1UECgwRc3lzdGVtOmV0Y2QtcGVlcnMxGzAZBgNVBAMMEnN5c3RlbTpl dGNkLXBlZXI6MTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANALpBiW @@ -27,6 +35,34 @@ MPVwDE1P3IxkmghoVtUJ9hM0AwRXaGD8pHWXq3JiryxoIGXCz2p0oEGczKmkxBki X6IGYVFVglScoQS4xQGiyhxzZhgAjKFsRaAWjcpU6LkSpF9org3KpZtcKeV/ZwZT 5KuRy6rsTWvlX/8onttDqtsipBkyVKlBsrsnfO3A0XwhEt79h9fnxMK94K0quTVA jvINeymP +-----END CERTIFICATE REQUEST-----`) + + csrMetricsBytes = []byte(`-----BEGIN CERTIFICATE REQUEST----- +MIIEpjCCAo4CAQAwYTELMAkGA1UEBhMCWFgxFTATBgNVBAcMDERlZmF1bHQgQ2l0 +eTEcMBoGA1UECgwTc3lzdGVtOmV0Y2QtbWV0cmljczEdMBsGA1UEAwwUc3lzdGVt +OmV0Y2QtbWV0cmljOjEwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDJ +kSE7th+Tu26k1me4guVF96CD7xA1xs9ujR7+/KMGKPYq+oPFZKOMX5xJziY4uF29 +Nhp3VTTKVgU8QOT6Y0yaG4yjp6BPCSXY0/HEVPe2Z87xdPDGU2vrB5oeHVFRXDZ1 +vDpODQ0M+F97aLqpQKXtthBjGaEmJH/DB4BqSGOH+5fDTggCWa0NfiscLNX99Tkf +jb1+fC/7bDpz3i/0op34pvS+hv1U9vFLlZpz7imka42lh6xutnOvUItTsngzZ2c+ +54oUmb1Vyji1NG7rGZWsHtjPYXgvHq85hQwqlbVFYt169pfcDQGYwy+NSh847AN8 +ORa2OQt4V9B8ZTTDdn4CQ93nTMTwKKf2dwcTxj84ABl4eYFWPIw6xOsPEOCtgNsw +2GpD5csh7hdsjI0WOw81fUfA9xbtc7SvP5vinCzIt2RE3sFuKXQPmlVLrtG5nQQg +aH/HXFXxYKrJhFTiDyGay1pat1UhDbmio8CCcSgSgb8O/M9ZSVVh0fEHOr2e38vo +K1p0VgHZ3ITL4/a/3Ex70sA/PWzYvrGb/FOhP1CRLx0CstQjVgp4dQqPlZJWy3qy +iyZgR5xCgx9nElRKh2ntJmyv9msMmYQUgNmt2uPHoOAaYAj49lz5VtttwXv23YwX +20ODXe3d6pwDeCGbBj2Atkp2Jvc5z4aG/ZqyeQUVjwIDAQABoAAwDQYJKoZIhvcN +AQELBQADggIBAAlddAVwfZeZqJQwd/0WsLq0mH3TcevKkRAy/mUP/Jna+ug8I+xw +DjWjKXv0EyXif3IFP7mzrmaW1ksqcOwoUBGMMc17kcXVRP9VGWM0YmXYU2TrjIkg +YQTVdQXP5+W7VwHL6m/rdsOt/zy1OFVAn88f5hrNEnkmv7MwwK+BgWMdRT3lTWbi +HQF86vjW/0joR+L0vGBJxlGJma+c6wxxsPPi0eRDJuThV9Yzr0VhXLiLlimm1e2N +dT9JL2n/RAtb6LbJfqqFWvpr64ZnPz/s+bhMnd+Ufk5cVMqov4jvZijZQlhqR8R/ +qSaopgZfFdQuL2uZLTHrDNTZfLIXiu7qIyRvlpCPJgyJ29LVBzxPurg1euhln/pr +G94cfrZe/dMEQvEQrSSyUcxOXzeYm3+uT+xIp7yK5z5V8w7qmR+PUqRIKOZ6TBSU +YxCa7IieBKN82wm9X2FulifD1LVUvHaztGnhipV2UmLo0Wrwo78jHJz0nw0bYEMd +Ba+1yisQjcSz21Zsl7lbm2MKfrR8cx5AoP2Xn75jGe9L+n6YEx2MEHZp7/s7X1hf +Ax2vGHbg+P6Yof8alxqe2AHzAtlbUSmwXsf8efs/3Uey07x55fC7O42hj5VNLbfV +qqOj7/EZleCtQplqyOwF8Mt6h4LZBE4lgB27HNtX5VAcZsUnVTtJrO0e -----END CERTIFICATE REQUEST-----`) caCrtBytes = []byte(`-----BEGIN CERTIFICATE----- @@ -58,6 +94,38 @@ E/Mb0p/otozmx7Y7QIkXSh//H/x8Bi/vLRtNQmbgCYu6iNNOKRTiVtaYRcy7vj4Z iE6rkr//NhxuZeaBDItIRC4uRcSF8noeFkGuQGb22vf8HnwDKnNF9Ty6Zg8CfRVv Rt0zd4OjeRzVNivCQ3ilpj5uv2vob9+9svKVatdFYst93eaBvWGd1hbsev7T/3t3 bA== +-----END CERTIFICATE-----`) + + caMetricsCrtBytes = []byte(`-----BEGIN CERTIFICATE----- +MIIFXTCCA0WgAwIBAgIURQbfHSBCKeD2UL952fkS5lgYJowwDQYJKoZIhvcNAQEL +BQAwPjELMAkGA1UEBhMCWFgxFTATBgNVBAcMDERlZmF1bHQgQ2l0eTEYMBYGA1UE +CgwPZmFrZS1tZXRyaWNzLWNhMB4XDTE5MDMyMDExNTMyM1oXDTE5MDQxOTExNTMy +M1owPjELMAkGA1UEBhMCWFgxFTATBgNVBAcMDERlZmF1bHQgQ2l0eTEYMBYGA1UE +CgwPZmFrZS1tZXRyaWNzLWNhMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKC +AgEAyZEhO7Yfk7tupNZnuILlRfegg+8QNcbPbo0e/vyjBij2KvqDxWSjjF+cSc4m +OLhdvTYad1U0ylYFPEDk+mNMmhuMo6egTwkl2NPxxFT3tmfO8XTwxlNr6weaHh1R +UVw2dbw6Tg0NDPhfe2i6qUCl7bYQYxmhJiR/wweAakhjh/uXw04IAlmtDX4rHCzV +/fU5H429fnwv+2w6c94v9KKd+Kb0vob9VPbxS5Wac+4ppGuNpYesbrZzr1CLU7J4 +M2dnPueKFJm9Vco4tTRu6xmVrB7Yz2F4Lx6vOYUMKpW1RWLdevaX3A0BmMMvjUof +OOwDfDkWtjkLeFfQfGU0w3Z+AkPd50zE8Cin9ncHE8Y/OAAZeHmBVjyMOsTrDxDg +rYDbMNhqQ+XLIe4XbIyNFjsPNX1HwPcW7XO0rz+b4pwsyLdkRN7Bbil0D5pVS67R +uZ0EIGh/x1xV8WCqyYRU4g8hmstaWrdVIQ25oqPAgnEoEoG/DvzPWUlVYdHxBzq9 +nt/L6CtadFYB2dyEy+P2v9xMe9LAPz1s2L6xm/xToT9QkS8dArLUI1YKeHUKj5WS +Vst6sosmYEecQoMfZxJUSodp7SZsr/ZrDJmEFIDZrdrjx6DgGmAI+PZc+VbbbcF7 +9t2MF9tDg13t3eqcA3ghmwY9gLZKdib3Oc+Ghv2asnkFFY8CAwEAAaNTMFEwHQYD +VR0OBBYEFBB/Exg1rCBHZOZJK24YrRKi7ArnMB8GA1UdIwQYMBaAFBB/Exg1rCBH +ZOZJK24YrRKi7ArnMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggIB +AJqp6IpzHjuM5Pnfv3L9HFbVYp/q4XrvUKw9vUXD4mADL8XQVYCOLi9Zixfz1Ke9 +Gmk7sUZXlcAAQH80DecqB71XT0tuGobSrUOVVVM0qZsTt8pedDfQTwHqz+wl8MNE +Pd/Rte4VgwHQjaTYX6CnmqmnTvZVHwj5myi00xpU3sFxHk1brUEnEe7nyuR4dJ8k +DR/qtB9alR/mJwse0bWQI4lo9QakEoA5PLFSvUnXxNqOiQR3tRAxist6Td6g1Upe +/se6yv+1IRAuZXoSJpwmoRnxSLVZtD6EGLZK35hRjfvZx+G2422rst56thzVC4VS +yFrH5lgT66cyn3rw8cgqwufb5pspB4Qsc05Y0Ky1PQBst2rvhWiJaMJNma2+R/3/ +EMyCIbsp2niXwPLkqSOCKhpbOBlPYlfvEY2PivI9CF2FnFv7vyhqk6n7L79puKJk +/1VriB6uUVG8HdAzEazXULYSOPsgZ7kwr+8hI6jkrMPsisjIPMbdvD/pkyEh9iu7 +4lfBjgE382fdY2ShbHVdjq6dbPmH3Ds7/RaQQP8esZs29ll9Y7irr63RZkY3OiR0 +/aYkCM2BLtayR9wuunBEFGPwAZLIQKHk5MJSLsix9l0uV+N897agsKQ87Luxn1vw +Rd1ImiuaAD0Dz9rSJQyZrzFGvZRCqTUC9c5Vp1/X8djW -----END CERTIFICATE-----`) caKeyBytes = []byte(`-----BEGIN RSA PRIVATE KEY----- @@ -110,12 +178,66 @@ Aef45gm4fR4GRShOxIKg2QG5M8d5EBWqY0tb2e9/G0fG8y77T8UjjgGMwdpKVwzI 0A5XeOdE3Y0hq7k55zf6j1UjrJ4eDstSzW0UhwOci4/Z7nPQfx0u/zW8miLkUwIw KA95ukt2rsMZ5ay6gC/lb2TYCvEyD9GXCpIW2OiC/KCW2MXltNWKGqE6ASRBWsdF vCuqdwASd/1MJWpe/v+PPcIJzSLRfDcPkIWfOJKBaeIagnSHKqGECIs+Jv8F +-----END RSA PRIVATE KEY-----`) + + caMetricsKeyBytes = []byte(`-----BEGIN RSA PRIVATE KEY----- +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 -----END RSA PRIVATE KEY-----`) ) func loadAllCrts(t *testing.T) { loadCrt(caKeyBytes, caKeyFile, t) loadCrt(caCrtBytes, caCrtFile, t) + loadCrt(caMetricsKeyBytes, caMetricsKeyFile, t) + loadCrt(caMetricsCrtBytes, caMetricsCrtFile, t) } func loadCrt(data []byte, filename string, t *testing.T) { @@ -124,24 +246,155 @@ func loadCrt(data []byte, filename string, t *testing.T) { } } +func TestNewSignerCA(t *testing.T) { + type TestData struct { + SignerCAFiles + csrFileBytes []byte + want string + } + + for _, test := range []TestData{ + { + SignerCAFiles{caCrtFile, caKeyFile, "", ""}, + csrBytes, + "ok", + }, + { + SignerCAFiles{caCrtFile, caKeyFile, "", ""}, + csrMetricsBytes, + "error", + }, + { + SignerCAFiles{caCrtFile, caKeyFile, caMetricsCrtFile, caMetricsKeyFile}, + csrMetricsBytes, + "ok", + }, + { + SignerCAFiles{caCrtFile, caKeyFile, caMetricsCrtFile, caMetricsKeyFile}, + csrBytes, + "ok", + }, + { + SignerCAFiles{"", "", caMetricsCrtFile, caMetricsKeyFile}, + csrBytes, + "error", + }, + { + SignerCAFiles{"", "", caMetricsCrtFile, caMetricsKeyFile}, + csrMetricsBytes, + "ok", + }, + } { + + loadAllCrts(t) + caFiles := test.SignerCAFiles + csr := createCSR(test.csrFileBytes) + profile, _ := getProfile(csr) + _, err := newSignerCA(&caFiles, csr) + got := gotError(err) + if test.want != got { + t.Errorf("NewSignerCA profile %s want (%s) got (%s) error: %v", profile, test.want, got, err) + } + if err := cleanUp(caFiles); err != nil { + t.Fatalf("NewSignerCA error deleting files %v", err) + } + } +} + func TestSign(t *testing.T) { - loadAllCrts(t) - config := Config{ - CACertFile: caCrtFile, - CAKeyFile: caKeyFile, - ListenAddress: "0.0.0.0:6443", - EtcdPeerCertDuration: 100, - EtcdServerCertDuration: 100, + type TestData struct { + SignerCAFiles + csrFileBytes []byte + caCrt []byte + dnsName string + want string } - s, err := NewSigner(config) - if err != nil { - t.Errorf("error setting up signer:%v", err) + for _, test := range []TestData{ + { + SignerCAFiles{caCrtFile, caKeyFile, "", ""}, + csrBytes, + caCrtBytes, + "system:etcd-peer:1", + "ok", + }, + { + SignerCAFiles{caCrtFile, caKeyFile, caMetricsCrtFile, caMetricsKeyFile}, + csrMetricsBytes, + caMetricsCrtBytes, + "system:etcd-metric:1", + "ok", + }, + { + SignerCAFiles{caCrtFile, caKeyFile, caMetricsCrtFile, caMetricsKeyFile}, + csrMetricsBytes, + caMetricsCrtBytes, + "google.com", + "error", + }, + } { + + loadAllCrts(t) + + caFiles := test.SignerCAFiles + config := Config{ + SignerCAFiles: caFiles, + ListenAddress: "0.0.0.0:6443", + EtcdMetricCertDuration: 1 * time.Hour, + EtcdPeerCertDuration: 1 * time.Hour, + EtcdServerCertDuration: 1 * time.Hour, + } + + csr := createCSR(test.csrFileBytes) + policy := signerPolicy(config) + signerCA, err := newSignerCA(&caFiles, csr) + if err != nil { + t.Errorf("error setting up signerCA:%v", err) + } + s, err := NewSigner(signerCA, &policy) + if err != nil { + t.Errorf("error setting up signer:%v", err) + } + + signedCSR, errMsg := s.Sign(csr) + if errMsg != nil { + t.Errorf("error signing csr: %v", errMsg) + } + if len(signedCSR.Status.Certificate) == 0 { + t.Errorf("csr not signed: %v", err) + } + + roots := x509.NewCertPool() + ok := roots.AppendCertsFromPEM(test.caCrt) + if !ok { + t.Errorf("failed to parse root certificate") + } + block, _ := pem.Decode([]byte(signedCSR.Status.Certificate)) + if block == nil { + t.Errorf("failed to parse certificate PEM") + } + + opts := x509.VerifyOptions{ + DNSName: test.dnsName, + Roots: roots, + } + cert, _ := x509.ParseCertificate(block.Bytes) + _, verr := cert.Verify(opts) + got := gotError(verr) + if got != test.want { + t.Errorf("TestSign want %s got %s err %v", test.want, got, verr) + } + // cleanup + if err := cleanUp(caFiles); err != nil { + t.Fatalf("error deleting files %v", err) + } } +} - csr := &capi.CertificateSigningRequest{ +func createCSR(csr []byte) *capi.CertificateSigningRequest { + return &capi.CertificateSigningRequest{ Spec: capi.CertificateSigningRequestSpec{ - Request: csrBytes, + Request: csr, Usages: []capi.KeyUsage{ capi.UsageSigning, capi.UsageKeyEncipherment, @@ -150,22 +403,29 @@ func TestSign(t *testing.T) { }, }, } +} - signedCSR, errMsg := s.Sign(csr) - if errMsg != nil { - t.Errorf("error signing csr: %v", errMsg) - } - - if len(signedCSR.Status.Certificate) == 0 { - t.Errorf("csr not signed: %v", err) - - } - - if err := os.Remove(caCrtFile); err != nil { - t.Errorf("error deleting file %s: %v", caCrtFile, err) +func cleanUp(files SignerCAFiles) error { + f := reflect.ValueOf(files) + for i := 0; i < f.NumField(); i++ { + file := fmt.Sprintf("%s", f.Field(i).Interface()) + if file == "" { + continue + } + if err := os.Remove(file); err != nil { + return err + } } + return nil +} - if err := os.Remove(caKeyFile); err != nil { - t.Errorf("error deleting file %s: %v", caKeyFile, err) +func gotError(err error) string { + switch t := err.(type) { + case nil: + return "ok" + case error: + return "error" + default: + return fmt.Sprintf("invalid type: %v", t) } }