Skip to content

Commit

Permalink
[FAB-5389] gencsr command for fabric-ca-client
Browse files Browse the repository at this point in the history
Adding a gencsr command to fabric-ca-client for [FAB-5389]
to generate certificate signing requests and to write them
to a file without any interaction with the fabric-ca-server.

This allows the user to send ceritificate signing requests to
an external CA for private keys generated by the BCCSP for
the msp directory.

Change-Id: Id17b56c122d82875a3cfb0d2332e8cacd26b0eea
Signed-off-by: Jonathan Patchell <Jonathan.Patchell@gemalto.com>
  • Loading branch information
Jonathan Patchell authored and Jonathan Patchell committed Aug 15, 2017
1 parent 8335b0c commit 77f76df
Show file tree
Hide file tree
Showing 7 changed files with 272 additions and 8 deletions.
9 changes: 7 additions & 2 deletions cmd/fabric-ca-client/command.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ const (
register = "register"
revoke = "revoke"
getcacert = "getcacert"
gencsr = "gencsr"
)

// Command is the object for fabric-ca-client commands
Expand All @@ -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
}
50 changes: 45 additions & 5 deletions cmd/fabric-ca-client/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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
)
Expand Down Expand Up @@ -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)
}
Expand Down Expand Up @@ -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")
Expand All @@ -318,9 +330,17 @@ func createDefaultConfigFile() error {
// Do string subtitution to get the default config
cfg = strings.Replace(defaultCfgTemplate, "<<<URL>>>", fabricCAServerURL, 1)
cfg = strings.Replace(cfg, "<<<MYHOST>>>", 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, "<<<ENROLLMENT_ID>>>", user, 1)

Expand Down Expand Up @@ -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>=<value>", 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{
Expand Down
73 changes: 73 additions & 0 deletions cmd/fabric-ca-client/gencsr.go
Original file line number Diff line number Diff line change
@@ -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
}
3 changes: 2 additions & 1 deletion cmd/fabric-ca-client/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,8 @@ func init() {
&cfgAttrs, "id.attrs", "", nil, "A list of comma-separated attributes of the form <name>=<value> (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 <name>=<value> (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
Expand Down
52 changes: 52 additions & 0 deletions cmd/fabric-ca-client/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
61 changes: 61 additions & 0 deletions lib/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
32 changes: 32 additions & 0 deletions lib/clientconfig.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)

Expand Down Expand Up @@ -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
}

0 comments on commit 77f76df

Please sign in to comment.