From de4187b696724dd0d55dca8e3a02b5f79ded349a Mon Sep 17 00:00:00 2001 From: Saad Karim Date: Thu, 11 May 2017 16:53:22 -0400 Subject: [PATCH] [FAB-3845] Configuration of intermediate CA via CLI Certain configurations can be set for an intermediate CA. These configurations were missing command line flags, preventing users from enrolling intermediate CAs from the command line. Added these missing flags to the fabric-ca server to fix configuration of an intermediate CA using command line flags. The following flags are now available: --intermediate.enrollment.hosts --intermediate.enrollment.label --intermediate.enrollment.profile --intermediate.parentserver.caname -u, --intermediate.parentserver.url --intermediate.tls.certfiles --intermediate.tls.client.certfile --intermediate.tls.client.keyfile Change-Id: I2526abc47d3ff054523b0fd23f7f75e6e9a71848 Signed-off-by: Saad Karim --- cmd/fabric-ca-client/config.go | 31 ++++++ cmd/fabric-ca-client/main_test.go | 4 +- cmd/fabric-ca-server/config.go | 37 +++++++ lib/ca.go | 15 ++- lib/caconfig.go | 16 ++- lib/client.go | 8 +- lib/client_whitebox_test.go | 7 +- lib/identity.go | 8 +- lib/server.go | 9 +- lib/server_test.go | 57 +++++++++- lib/tls/tls.go | 29 ++++- lib/tls/tls_test.go | 101 ++++++++++++++++-- lib/util.go | 3 +- .../ca1/fabric-ca-server-config.yaml | 7 +- 14 files changed, 287 insertions(+), 45 deletions(-) diff --git a/cmd/fabric-ca-client/config.go b/cmd/fabric-ca-client/config.go index bdf546a3e..85a4b2c0c 100644 --- a/cmd/fabric-ca-client/config.go +++ b/cmd/fabric-ca-client/config.go @@ -91,6 +91,13 @@ mspdir: ############################################################################# # TLS section for secure socket connection +# +# certfiles - PEM-encoded list of trusted root certificate files +# client: +# certfile - PEM-encoded certificate file for when client authentication +# is enabled on server +# keyfile - PEM-encoded key file for when client authentication +# is enabled on server ############################################################################# tls: # TLS section for secure socket connection @@ -103,6 +110,17 @@ tls: # Certificate Signing Request section for generating the CSR for # an enrollment certificate (ECert) # +# cn - Used by CAs to determine which domain the certificate is to be generated for +# names - A list of name objects. Each name object should contain at least one +# "C", "L", "O", "OU", or "ST" value (or any combination of these). These values are: +# "C": country +# "L": locality or municipality (such as city or town name) +# "O": organisation +# "OU": organisational unit, such as the department responsible for owning the key; +# it can also be used for a "Doing Business As" (DBS) name +# "ST": the state or province +# hosts - A list of space-separated host names which the certificate should be valid for +# # NOTE: The serialnumber field below, if specified, becomes part of the issued # certificate's DN (Distinquished Name). For example, one use case for this is # a company with its own CA (Certificate Authority) which issues certificates @@ -131,10 +149,17 @@ csr: ############################################################################# # Registration section used to register a new identity with fabric-ca server +# +# name - Unique name of the identity +# type - Type of identity being registered (e.g. 'peer, app, user') +# maxenrollments - The maximum number of times the secret can be reused to enroll +# affiliation - The identity's affiliation +# attributes - List of name/value pairs of attribute for identity ############################################################################# id: name: type: + maxenrollments: affiliation: attributes: - name: @@ -142,13 +167,19 @@ id: ############################################################################# # Enrollment section used to enroll an identity with fabric-ca server +# +# hosts - A comma-separated list of host names which the certificate should be valid for +# profile - Name of the signing profile to use in issuing the certificate +# label - Label to use in HSM operations ############################################################################# enrollment: hosts: profile: label: +############################################################################# # Name of the CA to connect to within the fabric-ca server +############################################################################# caname: ############################################################################# diff --git a/cmd/fabric-ca-client/main_test.go b/cmd/fabric-ca-client/main_test.go index 23365de00..a6c7a068d 100644 --- a/cmd/fabric-ca-client/main_test.go +++ b/cmd/fabric-ca-client/main_test.go @@ -30,7 +30,7 @@ import ( "strings" "testing" - "github.com/cloudflare/cfssl/csr" + "github.com/hyperledger/fabric-ca/api" "github.com/hyperledger/fabric-ca/lib" "github.com/hyperledger/fabric-ca/lib/dbutil" "github.com/hyperledger/fabric-ca/util" @@ -751,7 +751,7 @@ func getCAConfig() *lib.CAConfig { Certfile: certfile, }, Affiliations: affiliations, - CSR: csr.CertificateRequest{ + CSR: api.CSRInfo{ CN: "TestCN", }, } diff --git a/cmd/fabric-ca-server/config.go b/cmd/fabric-ca-server/config.go index 247ab2015..73dbdedb5 100644 --- a/cmd/fabric-ca-server/config.go +++ b/cmd/fabric-ca-server/config.go @@ -283,6 +283,43 @@ bccsp: cacount: cafiles: + +############################################################################# +# Intermediate CA which acts as a client of the root (or parent) CA. +# +# Parentserver section +# url - The URL of the parent server +# caname - Name of the CA to enroll with on the server +# +# Enrollment section used to enroll an identity with fabric-ca server +# hosts - A comma-separated list of host names which the certificate should +# be valid for +# profile - Name of the signing profile to use in issuing the certificate +# label - Label to use in HSM operations +# +# TLS section for secure socket connection +# certfiles - PEM-encoded list of trusted root certificate files +# client: +# certfile - PEM-encoded certificate file for when client authentication +# is enabled on server +# keyfile - PEM-encoded key file for when client authentication +# is enabled on server +############################################################################# +intermediate: + parentserver: + url: + caname: + + enrollment: + hosts: + profile: + label: + + tls: + certfiles: + client: + certfile: + keyfile: ` ) diff --git a/lib/ca.go b/lib/ca.go index f6b01f212..899dd193c 100644 --- a/lib/ca.go +++ b/lib/ca.go @@ -217,14 +217,19 @@ func (ca *CA) initKeyMaterial(renew bool) error { // Get the CA certificate for this CA func (ca *CA) getCACert() (cert []byte, err error) { - log.Debugf("Getting CA cert; parent server URL is '%s'", ca.Config.ParentServer.URL) - if ca.Config.ParentServer.URL != "" { + log.Debugf("Getting CA cert; parent server URL is '%s'", ca.Config.Intermediate.ParentServer.URL) + if ca.Config.Intermediate.ParentServer.URL != "" { // This is an intermediate CA, so call the parent fabric-ca-server // to get the cert clientCfg := ca.Config.Client if clientCfg == nil { clientCfg = &ClientConfig{} } + // Copy over the intermediate configuration into client configuration + clientCfg.TLS = ca.Config.Intermediate.TLS + clientCfg.Enrollment = ca.Config.Intermediate.Enrollment + clientCfg.CAName = ca.Config.Intermediate.ParentServer.CAName + clientCfg.CSR = ca.Config.CSR if clientCfg.Enrollment.Profile == "" { clientCfg.Enrollment.Profile = "ca" } @@ -236,7 +241,7 @@ func (ca *CA) getCACert() (cert []byte, err error) { } log.Debugf("Intermediate enrollment request: %v", clientCfg.Enrollment) var resp *EnrollmentResponse - resp, err = clientCfg.Enroll(ca.Config.ParentServer.URL, ca.HomeDir) + resp, err = clientCfg.Enroll(ca.Config.Intermediate.ParentServer.URL, ca.HomeDir) if err != nil { return nil, err } @@ -308,7 +313,7 @@ func (ca *CA) getCAChain() (chain []byte, err error) { return util.ReadFile(certAuth.Chainfile) } // Otherwise, if this is a root CA, we always return the contents of the CACertfile - if ca.Config.ParentServer.URL == "" { + if ca.Config.Intermediate.ParentServer.URL == "" { return util.ReadFile(certAuth.Certfile) } // If this is an intermediate CA but the ca.Chainfile doesn't exist, @@ -457,7 +462,7 @@ func (ca *CA) initEnrollmentSigner() (err error) { } // Make sure the policy reflects the new remote - parentServerURL := ca.Config.ParentServer.URL + parentServerURL := ca.Config.Intermediate.ParentServer.URL if parentServerURL != "" { err = policy.OverrideRemotes(parentServerURL) if err != nil { diff --git a/lib/caconfig.go b/lib/caconfig.go index cfe5bb1bb..4559f0e5b 100644 --- a/lib/caconfig.go +++ b/lib/caconfig.go @@ -18,7 +18,7 @@ package lib import ( "github.com/cloudflare/cfssl/config" - "github.com/cloudflare/cfssl/csr" + "github.com/hyperledger/fabric-ca/api" "github.com/hyperledger/fabric-ca/lib/ldap" "github.com/hyperledger/fabric-ca/lib/tls" "github.com/hyperledger/fabric-ca/util" @@ -69,15 +69,17 @@ csr: // "skip" - to skip the field. type CAConfig struct { CA CAInfo - ParentServer ParentServer Signing *config.Signing - CSR csr.CertificateRequest + CSR api.CSRInfo Registry CAConfigRegistry Affiliations map[string]interface{} LDAP ldap.Config DB CAConfigDB CSP *factory.FactoryOpts `mapstructure:"bccsp"` + // Optional client config for an intermediate server which acts as a client + // of the root (or parent) server Client *ClientConfig + Intermediate IntermediateCA } // CAInfo is the CA information on a fabric-ca-server @@ -118,6 +120,14 @@ type ParentServer struct { CAName string `help:"Name of the CA to connect to on fabric-ca-serve"` } +// IntermediateCA contains parent server information, TLS configuration, and +// enrollment request for an intermetiate CA +type IntermediateCA struct { + ParentServer ParentServer + TLS tls.ClientTLSConfig + Enrollment api.EnrollmentRequest +} + func (cc *CAConfigIdentity) String() string { return util.StructToString(cc) } diff --git a/lib/client.go b/lib/client.go index 5b192497c..0e4519313 100644 --- a/lib/client.go +++ b/lib/client.go @@ -172,10 +172,10 @@ func (c *Client) Enroll(req *api.EnrollmentRequest) (*EnrollmentResponse, error) CAName: req.CAName, } - reqNet.Hosts = signer.SplitHosts(req.Hosts) - reqNet.Request = string(csrPEM) - reqNet.Profile = req.Profile - reqNet.Label = req.Label + reqNet.SignRequest.Hosts = signer.SplitHosts(req.Hosts) + reqNet.SignRequest.Request = string(csrPEM) + reqNet.SignRequest.Profile = req.Profile + reqNet.SignRequest.Label = req.Label body, err := util.Marshal(reqNet, "SignRequest") if err != nil { diff --git a/lib/client_whitebox_test.go b/lib/client_whitebox_test.go index 334a12a9d..f589323f2 100644 --- a/lib/client_whitebox_test.go +++ b/lib/client_whitebox_test.go @@ -113,7 +113,6 @@ func getEnrollmentPayload(t *testing.T, c *Client) ([]byte, error) { // Get the body of the request sreq := signer.SignRequest{ - Hosts: signer.SplitHosts(req.Hosts), Request: string(csrPEM), Profile: req.Profile, Label: req.Label, @@ -141,8 +140,10 @@ func getServer(port int, home, parentURL string, maxEnroll int, t *testing.T) *S }, CA: CA{ Config: &CAConfig{ - ParentServer: ParentServer{ - URL: parentURL, + Intermediate: IntermediateCA{ + ParentServer: ParentServer{ + URL: parentURL, + }, }, Affiliations: affiliations, Registry: CAConfigRegistry{ diff --git a/lib/identity.go b/lib/identity.go index 86c738196..2405c5a70 100644 --- a/lib/identity.go +++ b/lib/identity.go @@ -117,10 +117,10 @@ func (i *Identity) Reenroll(req *api.ReenrollmentRequest) (*EnrollmentResponse, } // Get the body of the request - reqNet.Hosts = signer.SplitHosts(req.Hosts) - reqNet.Request = string(csrPEM) - reqNet.Profile = req.Profile - reqNet.Label = req.Label + reqNet.SignRequest.Hosts = signer.SplitHosts(req.Hosts) + reqNet.SignRequest.Request = string(csrPEM) + reqNet.SignRequest.Profile = req.Profile + reqNet.SignRequest.Label = req.Label body, err := util.Marshal(reqNet, "SignRequest") if err != nil { diff --git a/lib/server.go b/lib/server.go index 9d69d0131..a2ea31cd6 100644 --- a/lib/server.go +++ b/lib/server.go @@ -35,6 +35,7 @@ import ( "strings" "github.com/cloudflare/cfssl/log" + stls "github.com/hyperledger/fabric-ca/lib/tls" "github.com/hyperledger/fabric-ca/util" "github.com/spf13/viper" @@ -509,13 +510,9 @@ func (s *Server) checkAndEnableProfiling() error { // Make all file names in the config absolute func (s *Server) makeFileNamesAbsolute() error { log.Debug("Making server filenames absolute") - err := s.CA.makeFileNamesAbsolute() + err := stls.AbsTLSServer(&s.Config.TLS, s.HomeDir) if err != nil { return err } - fields := []*string{ - &s.Config.TLS.CertFile, - &s.Config.TLS.KeyFile, - } - return util.MakeFileNamesAbsolute(fields, s.HomeDir) + return nil } diff --git a/lib/server_test.go b/lib/server_test.go index 4714f80cf..85287f90b 100644 --- a/lib/server_test.go +++ b/lib/server_test.go @@ -262,6 +262,50 @@ func TestIntermediateServer(t *testing.T) { } } +func TestIntermediateServerWithTLS(t *testing.T) { + var err error + + rootServer := getRootServer(t) + if rootServer == nil { + return + } + rootServer.Config.TLS.Enabled = true + rootServer.Config.TLS.CertFile = "../../testdata/tls_server-cert.pem" + rootServer.Config.TLS.KeyFile = "../../testdata/tls_server-key.pem" + rootServer.Config.TLS.ClientAuth.Type = "RequireAndVerifyClientCert" + rootServer.Config.TLS.ClientAuth.CertFiles = []string{"../../testdata/root.pem"} + err = rootServer.Start() + if err != nil { + t.Fatalf("Root server start failed: %s", err) + } + + parentURL := fmt.Sprintf("https://admin:adminpw@localhost:%d", rootPort) + intermediateServer := getServer(intermediatePort, intermediateDir, parentURL, 0, t) + if intermediateServer == nil { + return + } + intermediateServer.CA.Config.Intermediate.TLS.CertFiles = []string{"../../testdata/root.pem"} + intermediateServer.CA.Config.Intermediate.TLS.Client.CertFile = "../../testdata/tls_client-cert.pem" + intermediateServer.CA.Config.Intermediate.TLS.Client.KeyFile = "../../testdata/tls_client-key.pem" + + err = intermediateServer.Start() + if err != nil { + t.Errorf("Intermediate server start failed: %s", err) + } + + time.Sleep(time.Second) + + err = intermediateServer.Stop() + if err != nil { + t.Errorf("Intermediate server stop failed: %s", err) + } + + err = rootServer.Stop() + if err != nil { + t.Errorf("Root server stop failed: %s", err) + } +} + func TestRunningTLSServer(t *testing.T) { srv := getServer(rootPort, testdataDir, "", 0, t) @@ -535,11 +579,14 @@ func TestMultiCAWithIntermediate(t *testing.T) { // Start it err = intermediatesrv.Start() if err != nil { - t.Errorf("Intermediate server start failed: %s", err) + t.Errorf("Failed to start intermediate server: %s", err) } time.Sleep(time.Second) // Stop it - intermediatesrv.Stop() + err = intermediatesrv.Stop() + if err != nil { + t.Error("Failed to stop intermediate server: ", err) + } if !util.FileExists("../testdata/ca/intermediateca/ca1/ca-chain.pem") { t.Error("Failed to enroll intermediate ca") @@ -839,8 +886,10 @@ func getServer(port int, home, parentURL string, maxEnroll int, t *testing.T) *S }, CA: CA{ Config: &CAConfig{ - ParentServer: ParentServer{ - URL: parentURL, + Intermediate: IntermediateCA{ + ParentServer: ParentServer{ + URL: parentURL, + }, }, Affiliations: affiliations, Registry: CAConfigRegistry{ diff --git a/lib/tls/tls.go b/lib/tls/tls.go index c0dc55bc7..b18b2b897 100644 --- a/lib/tls/tls.go +++ b/lib/tls/tls.go @@ -82,13 +82,13 @@ func GetClientTLSConfig(cfg *ClientTLSConfig) (*tls.Config, error) { rootCAPool := x509.NewCertPool() if len(cfg.CertFiles) == 0 { - return nil, errors.New("No CA certificate files provided") + return nil, errors.New("No root CA TLS certificate files provided") } for _, cacert := range cfg.CertFiles { caCert, err := ioutil.ReadFile(cacert) if err != nil { - return nil, err + return nil, fmt.Errorf("Failed to read '%s': %s", cacert, err) } ok := rootCAPool.AppendCertsFromPEM(caCert) if !ok { @@ -129,6 +129,31 @@ func AbsTLSClient(cfg *ClientTLSConfig, configDir string) error { return nil } +// AbsTLSServer makes TLS client files absolute +func AbsTLSServer(cfg *ServerTLSConfig, configDir string) error { + var err error + + for i := 0; i < len(cfg.ClientAuth.CertFiles); i++ { + cfg.ClientAuth.CertFiles[i], err = util.MakeFileAbs(cfg.ClientAuth.CertFiles[i], configDir) + if err != nil { + return err + } + + } + + cfg.CertFile, err = util.MakeFileAbs(cfg.CertFile, configDir) + if err != nil { + return err + } + + cfg.KeyFile, err = util.MakeFileAbs(cfg.KeyFile, configDir) + if err != nil { + return err + } + + return nil +} + func checkCertDates(certFile string) error { log.Debug("Check client TLS certificate for valid dates") certPEM, err := ioutil.ReadFile(certFile) diff --git a/lib/tls/tls_test.go b/lib/tls/tls_test.go index 6a1e22e6e..0d231bb5a 100644 --- a/lib/tls/tls_test.go +++ b/lib/tls/tls_test.go @@ -17,16 +17,26 @@ limitations under the License. package tls import ( + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "encoding/pem" + "errors" + "fmt" + "math/big" + "os" "testing" + "time" "github.com/stretchr/testify/assert" ) const ( - configDir = "../../testdata" - caCert = "root.pem" - certFile = "tls_client-cert.pem" - keyFile = "tls_client-key.pem" + configDir = "../../testdata" + caCert = "root.pem" + certFile = "tls_client-cert.pem" + keyFile = "tls_client-key.pem" + expiredCert = "../../testdata/expiredcert.pem" ) type testTLSConfig struct { @@ -43,9 +53,12 @@ func TestGetClientTLSConfig(t *testing.T) { }, } - AbsTLSClient(cfg, configDir) + err := AbsTLSClient(cfg, configDir) + if err != nil { + t.Errorf("Failed to get absolute path for client TLS config: %s", err) + } - _, err := GetClientTLSConfig(cfg) + _, err = GetClientTLSConfig(cfg) if err != nil { t.Errorf("Failed to get TLS Config: %s", err) } @@ -76,7 +89,7 @@ func TestGetClientTLSConfigInvalidArgs(t *testing.T) { AbsTLSClient(cfg, configDir) _, err = GetClientTLSConfig(cfg) assert.Error(t, err) - assert.Contains(t, err.Error(), "No CA certificate files provided") + assert.Contains(t, err.Error(), "No root CA TLS certificate files provided") // 3. cfg = &ClientTLSConfig{ @@ -101,7 +114,7 @@ func TestGetClientTLSConfigInvalidArgs(t *testing.T) { } _, err = GetClientTLSConfig(cfg) assert.Error(t, err) - assert.Contains(t, err.Error(), "No CA certificate files provided") + assert.Contains(t, err.Error(), "No root CA TLS certificate files provided") // 5. cfg = &ClientTLSConfig{ @@ -115,5 +128,77 @@ func TestGetClientTLSConfigInvalidArgs(t *testing.T) { _, err = GetClientTLSConfig(cfg) assert.Error(t, err) assert.Contains(t, err.Error(), "no-root.pem: no such file or directory") +} + +func TestAbsServerTLSConfig(t *testing.T) { + cfg := &ServerTLSConfig{ + KeyFile: "tls_client-key.pem", + CertFile: "tls_client-cert.pem", + ClientAuth: ClientAuth{ + CertFiles: []string{"root.pem"}, + }, + } + + err := AbsTLSServer(cfg, configDir) + if err != nil { + t.Errorf("Failed to get absolute path for server TLS config: %s", err) + } +} + +func TestCheckCertDates(t *testing.T) { + err := checkCertDates(expiredCert) + if err == nil { + assert.Error(t, errors.New("Expired certificate should have resulted in an error")) + } + + err = createTestCertificate() + if err != nil { + assert.Error(t, err) + } + + err = checkCertDates("notbefore.pem") + if err == nil { + assert.Error(t, errors.New("Future valid certificate should have resulted in an error")) + } + if err != nil { + assert.Contains(t, err.Error(), "Certificate provided not valid until later date") + } + + os.Remove("notbefore.pem") +} + +func createTestCertificate() error { + // Dynamically create a certificate with future valid date for testing purposes + certTemplate := &x509.Certificate{ + IsCA: true, + BasicConstraintsValid: true, + SubjectKeyId: []byte{1, 2, 3}, + SerialNumber: big.NewInt(1234), + NotBefore: time.Now().Add(time.Hour * 24), + NotAfter: time.Now().Add(time.Hour * 48), + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth}, + KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign, + } + // generate private key + privatekey, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + return fmt.Errorf("Error occurred during key generation: %s", err) + } + publickey := &privatekey.PublicKey + // create a self-signed certificate. template = parent + var parent = certTemplate + cert, err := x509.CreateCertificate(rand.Reader, certTemplate, parent, publickey, privatekey) + if err != nil { + return fmt.Errorf("Error occurred during certificate creation: %s", err) + } + + pemfile, _ := os.Create("notbefore.pem") + var pemkey = &pem.Block{ + Type: "CERTIFICATE", + Bytes: cert, + } + pem.Encode(pemfile, pemkey) + pemfile.Close() + return nil } diff --git a/lib/util.go b/lib/util.go index 64adfdf1d..160b06c11 100644 --- a/lib/util.go +++ b/lib/util.go @@ -102,6 +102,7 @@ func UnmarshalConfig(config interface{}, vp *viper.Viper, caFile string, server, "ldap.tls.certfiles", "db.tls.certfiles", "cafiles", + "intermediate.tls.certfiles", } err = util.ViperUnmarshal(config, sliceFields, vp) if err != nil { @@ -109,7 +110,7 @@ func UnmarshalConfig(config interface{}, vp *viper.Viper, caFile string, server, } if server { serverCfg := config.(*ServerConfig) - err = vp.Unmarshal(&serverCfg.CAcfg) + err = util.ViperUnmarshal(&serverCfg.CAcfg, sliceFields, vp) if err != nil { return fmt.Errorf("Incorrect format in file '%s': %s", caFile, err) } diff --git a/testdata/ca/intermediateca/ca1/fabric-ca-server-config.yaml b/testdata/ca/intermediateca/ca1/fabric-ca-server-config.yaml index 359363f49..bdef253a9 100644 --- a/testdata/ca/intermediateca/ca1/fabric-ca-server-config.yaml +++ b/testdata/ca/intermediateca/ca1/fabric-ca-server-config.yaml @@ -20,6 +20,7 @@ ca: csr: cn: fabric-ca-server-ca1 -parentserver: - url: http://admin:adminpw@localhost:7055 - caname: rootca2 +intermediate: + parentserver: + url: http://admin:adminpw@localhost:7055 + caname: rootca2