diff --git a/cmd/fabric-ca-client/command.go b/cmd/fabric-ca-client/command.go index 1a24d5dfd..130b255fc 100644 --- a/cmd/fabric-ca-client/command.go +++ b/cmd/fabric-ca-client/command.go @@ -27,6 +27,7 @@ const ( register = "register" revoke = "revoke" getcacert = "getcacert" + gencsr = "gencsr" ) // Command is the object for fabric-ca-client commands @@ -44,10 +45,14 @@ func NewCommand(commandName string) *Command { // Certain client commands can only be executed if enrollment credentials // are present func (cmd *Command) requiresEnrollment() bool { - return cmd.name != enroll && cmd.name != getcacert + return cmd.name != enroll && cmd.name != getcacert && cmd.name != gencsr } // Create default client configuration file only during an enroll command func (cmd *Command) shouldCreateDefaultConfig() bool { - return cmd.name == enroll + return cmd.name == enroll || cmd.name == gencsr +} + +func (cmd *Command) requiresUser() bool { + return cmd.name != gencsr } diff --git a/cmd/fabric-ca-client/config.go b/cmd/fabric-ca-client/config.go index afba3b1b0..d19573de6 100644 --- a/cmd/fabric-ca-client/config.go +++ b/cmd/fabric-ca-client/config.go @@ -24,6 +24,9 @@ import ( "path/filepath" "strings" + "reflect" + + "github.com/cloudflare/cfssl/csr" "github.com/cloudflare/cfssl/log" "github.com/hyperledger/fabric-ca/api" "github.com/hyperledger/fabric-ca/lib" @@ -204,6 +207,10 @@ var ( // and translated to Attributes field in registration cfgAttrs []string + // cfgCsrNames are the certificate signing request names specified via flags + // or env variables + cfgCsrNames []string + // clientCfg is the client's config clientCfg *lib.ClientConfig ) @@ -241,7 +248,7 @@ func configInit(command string) error { // command requires if cmd.shouldCreateDefaultConfig() { if !util.FileExists(cfgFileName) { - err = createDefaultConfigFile() + err = createDefaultConfigFile(cmd) if err != nil { return fmt.Errorf("Failed to create default configuration file: %s", err) } @@ -293,13 +300,18 @@ func configInit(command string) error { return err } + err = processCsrNames() + if err != nil { + return err + } + // Check for separaters and insert values back into slice normalizeStringSlices() return nil } -func createDefaultConfigFile() error { +func createDefaultConfigFile(cmd *Command) error { // Create a default config, if URL provided via CLI or envar update config files var cfg string fabricCAServerURL := viper.GetString("url") @@ -318,9 +330,17 @@ func createDefaultConfigFile() error { // Do string subtitution to get the default config cfg = strings.Replace(defaultCfgTemplate, "<<>>", fabricCAServerURL, 1) cfg = strings.Replace(cfg, "<<>>", myhost, 1) - user, _, err := util.GetUser() - if err != nil { - return err + + var user string + var err error + + if cmd.requiresUser() { + user, _, err = util.GetUser() + if err != nil { + return err + } + } else { + user = "" } cfg = strings.Replace(cfg, "<<>>", user, 1) @@ -350,6 +370,26 @@ func processAttributes() error { return nil } +// processAttributes parses attributes from command line or env variable +func processCsrNames() error { + if cfgCsrNames != nil { + clientCfg.CSR.Names = make([]csr.Name, len(cfgCsrNames)) + for idx, name := range cfgCsrNames { + sname := strings.SplitN(name, "=", 2) + if len(sname) != 2 { + return fmt.Errorf("CSR name/value '%s' is missing '=' ; it must be of the form =", name) + } + v := reflect.ValueOf(&clientCfg.CSR.Names[idx]).Elem().FieldByName(sname[0]) + if v.IsValid() { + v.SetString(sname[1]) + } else { + return fmt.Errorf("Invalid CSR name: '%s'", sname[0]) + } + } + } + return nil +} + func checkForEnrollment() error { log.Debug("Checking for enrollment") client := lib.Client{ diff --git a/cmd/fabric-ca-client/gencsr.go b/cmd/fabric-ca-client/gencsr.go new file mode 100644 index 000000000..8782ed137 --- /dev/null +++ b/cmd/fabric-ca-client/gencsr.go @@ -0,0 +1,73 @@ +/* +Copyright IBM Corp. 2017 All Rights Reserved. + +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 main + +import ( + "fmt" + "path/filepath" + + "github.com/cloudflare/cfssl/log" + "github.com/spf13/cobra" +) + +// initCmd represents the init command +var gencsrCmd = &cobra.Command{ + Use: "gencsr", + Short: "Generate a CSR", + Long: "Generate a Certificate Signing Request for an identity", + RunE: func(cmd *cobra.Command, args []string) error { + if len(args) > 0 { + return fmt.Errorf(extraArgsError, args, cmd.UsageString()) + } + + err := runGenCSR(cmd) + if err != nil { + return err + } + + return nil + }, +} + +// csrCommonName is the certificate signing request common name specified via the flag +var csrCommonName string + +func init() { + gencsrCmd.Flags().StringVar(&csrCommonName, "csr.cn", "", "The common name for the certificate signing request") + rootCmd.AddCommand(gencsrCmd) +} + +// The client enroll main logic +func runGenCSR(cmd *cobra.Command) error { + log.Debug("Entered runGenCSR") + + err := configInit(cmd.Name()) + if err != nil { + return err + } + + if csrCommonName != "" { + clientCfg.CSR.CN = csrCommonName + } + + err = clientCfg.GenCSR(filepath.Dir(cfgFileName)) + if err != nil { + return err + } + + return nil +} diff --git a/cmd/fabric-ca-client/main.go b/cmd/fabric-ca-client/main.go index 93c223f1a..780d88ad4 100644 --- a/cmd/fabric-ca-client/main.go +++ b/cmd/fabric-ca-client/main.go @@ -85,7 +85,8 @@ func init() { &cfgAttrs, "id.attrs", "", nil, "A list of comma-separated attributes of the form = (e.g. foo=foo1,bar=bar1)") util.FlagString(pflags, "myhost", "m", host, "Hostname to include in the certificate signing request during enrollment") - + pflags.StringSliceVarP( + &cfgCsrNames, "csr.names", "", nil, "A list of comma-separated CSR names of the form = (e.g. C=CA,O=Org1)") clientCfg = &lib.ClientConfig{} tags := map[string]string{ "skip.csr.cn": "true", // Skip CN on client side as enrollment ID is used as CN diff --git a/cmd/fabric-ca-client/main_test.go b/cmd/fabric-ca-client/main_test.go index 84eccc5f9..343fcd19f 100644 --- a/cmd/fabric-ca-client/main_test.go +++ b/cmd/fabric-ca-client/main_test.go @@ -309,6 +309,58 @@ func testEnroll(t *testing.T) { os.Remove(defYaml) } +// TestGencsr tests fabric-ca-client gencsr +func TestGencsr(t *testing.T) { + t.Log("Testing gencsr CMD") + defYaml = util.GetDefaultConfigFile("fabric-ca-client") + + os.Remove(defYaml) // Clean up any left over config file + + mspDir := filepath.Join(filepath.Dir(defYaml), "msp") + + os.RemoveAll(mspDir) + + defer os.Remove(defYaml) + + err := RunMain([]string{cmdName, "gencsr", "--csr.cn", "identity", "--csr.names", "C=CA,O=Org1,OU=OU1", "-M", mspDir}) + if err != nil { + t.Errorf("client gencsr failed: %s", err) + } + + signcerts := path.Join(mspDir, "signcerts") + assertOneFileInDir(signcerts, t) + + files, err := ioutil.ReadDir(signcerts) + if err != nil { + t.Fatalf("Failed to get number of files in directory '%s': %s", signcerts, err) + } + + if files[0].Name() != "identity.csr" { + t.Fatalf("Failed to find identity.csr in '%s': %s", signcerts, err) + } + + cfgCsrNames = []string{} + err = RunMain([]string{cmdName, "gencsr", "--csr.cn", "identity", "--csr.names", "C=CA,O=Org1,FOO=BAR", "-M", mspDir}) + if err == nil { + t.Error("Should have failed: Invalid CSR name") + } + + cfgCsrNames = []string{} + err = RunMain([]string{cmdName, "gencsr", "--csr.cn", "identity", "--csr.names", "C:CA,O=Org1,OU=OU2", "-M", mspDir}) + if err == nil { + t.Error("Should have failed: No '=' for name/value pair") + } + + cfgCsrNames = []string{} + csrCommonName = "" + clientCfg.CSR.CN = "" + + err = RunMain([]string{cmdName, "gencsr", "-c", defYaml, "--csr.names", "C=CA,O=Org1,OU=OU1", "-M", mspDir}) + if err == nil { + t.Error("Should have failed: CSR CN not specified.") + } +} + // TestMOption tests to make sure that the key is stored in the correct // directory when the "-M" option is used. // This also ensures the intermediatecerts directory structure is populated diff --git a/lib/client_test.go b/lib/client_test.go index ac32b3fbd..5f455357b 100644 --- a/lib/client_test.go +++ b/lib/client_test.go @@ -677,6 +677,67 @@ func TestCLISendBadPost(t *testing.T) { } } +// Test to make sure that CSR is generated by GenCSR function +func TestCLIGenCSR(t *testing.T) { + config := new(ClientConfig) + + homeDir := filepath.Join(testdataDir, "identity") + + os.RemoveAll(homeDir) + defer os.RemoveAll(homeDir) + + config.CSR.CN = "identity" + err := config.GenCSR(homeDir) + if err != nil { + t.Fatalf("Failed to generate CSR: %s", err) + } + csrFile := filepath.Join(homeDir, "msp", "signcerts", "identity.csr") + _, err = os.Stat(csrFile) + if os.IsNotExist(err) { + t.Fatalf("CSR file does not exist at %s", csrFile) + } + os.RemoveAll(homeDir) + + // Error cases + //CN is missing + config.CSR.CN = "" + err = config.GenCSR(homeDir) + if err == nil { + t.Fatalf("GenCSR should fail as CN is missing: %s", err) + } + + // Fail to write file + config.CSR.CN = strings.Repeat("a", 260) + err = config.GenCSR(homeDir) + t.Logf("ClientConfig.GenCSR error %v", err) + if err == nil { + t.Error("ClientConfig.GenCSR should have failed due to invalid filename") + } + + // Fail to gen key + config.CSR = api.CSRInfo{ + CN: "TestGenCSR", + KeyRequest: &csr.BasicKeyRequest{ + A: "dsa", + S: 256, + }, + } + err = config.GenCSR(homeDir) + t.Logf("ClientConfig.GenCSR error %v", err) + if err == nil { + t.Error("ClientConfig.GenCSR should have failed due to unsupported algorithm") + } + + // Fail to init client + config.MSPDir = string(make([]byte, 1)) + err = config.GenCSR(homeDir) + t.Logf("ClientConfig.GenCSR error %v", err) + if err == nil { + t.Error("ClientConfig.GenCSR should have failed to init client") + } + +} + // Test to make sure that once an identity is revoked, all subsequent commands // invoked by revoked user should be rejected by server for all its issued certificates func TestRevokedIdentity(t *testing.T) { diff --git a/lib/clientconfig.go b/lib/clientconfig.go index 6cc25fa39..551d1c757 100644 --- a/lib/clientconfig.go +++ b/lib/clientconfig.go @@ -19,9 +19,12 @@ package lib import ( "fmt" "net/url" + "path" + "github.com/cloudflare/cfssl/log" "github.com/hyperledger/fabric-ca/api" "github.com/hyperledger/fabric-ca/lib/tls" + "github.com/hyperledger/fabric-ca/util" "github.com/hyperledger/fabric/bccsp/factory" ) @@ -70,3 +73,32 @@ func (c *ClientConfig) Enroll(rawurl, home string) (*EnrollmentResponse, error) client := &Client{HomeDir: home, Config: c} return client.Enroll(&c.Enrollment) } + +// GenCSR generates a certificate signing request and writes the CSR to a file. +func (c *ClientConfig) GenCSR(home string) error { + + client := &Client{HomeDir: home, Config: c} + // Generate the CSR + + err := client.Init() + if err != nil { + return err + } + + if c.CSR.CN == "" { + return fmt.Errorf("CSR common name not specified; use '--csr.cn' flag") + } + + csrPEM, _, err := client.GenCSR(&c.CSR, c.CSR.CN) + if err != nil { + return err + } + + csrFile := path.Join(client.Config.MSPDir, "signcerts", fmt.Sprintf("%s.csr", c.CSR.CN)) + err = util.WriteFile(csrFile, csrPEM, 0644) + if err != nil { + return fmt.Errorf("Failed to store the csr: %s", err) + } + log.Infof("Stored CSR at %s", csrFile) + return nil +}