Skip to content

Commit

Permalink
[FAB-6321] Store CA certs in child-first order
Browse files Browse the repository at this point in the history
Currently, getcacerts and enrollment requests return
CA chain in parent-first order (root CA cert at the top, followed
by intermediate CA cert). TLS spec expects the certs to be
in reverse order. This change fixes this problem and provides
for CA_CHAIN_PARENT_FIRST environment variable that can be set
to get the old behavior.

Change-Id: I3883cfb8954b8dcef88804ba923d19ba9a3c587e
Signed-off-by: Anil Ambati <[email protected]>
  • Loading branch information
Anil Ambati committed Nov 15, 2017
1 parent 797d745 commit b3c00ea
Show file tree
Hide file tree
Showing 6 changed files with 317 additions and 73 deletions.
103 changes: 60 additions & 43 deletions cmd/fabric-ca-client/getcacert.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ limitations under the License.
package main

import (
"bytes"
"crypto/x509"
"encoding/pem"
"fmt"
"net/url"
Expand Down Expand Up @@ -87,8 +89,8 @@ func (c *ClientCmd) runGetCACert() error {
}

// Store the CAChain in the CACerts folder of MSP (Membership Service Provider)
// The 1st cert in the chain goes into MSP 'cacerts' directory.
// The others (if any) go into the MSP 'intermediates' directory.
// The root cert in the chain goes into MSP 'cacerts' directory.
// The others (if any) go into the MSP 'intermediatecerts' directory.
func storeCAChain(config *lib.ClientConfig, si *lib.GetServerInfoResponse) error {
mspDir := config.MSPDir
// Get a unique name to use for filenames
Expand All @@ -102,46 +104,72 @@ func storeCAChain(config *lib.ClientConfig, si *lib.GetServerInfoResponse) error
}
fname = strings.Replace(fname, ":", "-", -1)
fname = strings.Replace(fname, ".", "-", -1) + ".pem"
// Split the root and intermediate certs
block, intermediateCerts := pem.Decode(si.CAChain)
if block == nil {
return errors.New("No root certificate was found")
}
rootCert := pem.EncodeToMemory(block)
dirPrefix := dirPrefixByProfile(config.Enrollment.Profile)
// Store the root certificate in "cacerts"
certsDir := fmt.Sprintf("%scacerts", dirPrefix)
err = storeCerts("root certificate", mspDir, certsDir, fname, rootCert)
if err != nil {
return err
tlsfname := fmt.Sprintf("tls-%s", fname)

rootCACertsDir := path.Join(mspDir, "cacerts")
intCACertsDir := path.Join(mspDir, "intermediatecerts")
tlsRootCACertsDir := path.Join(mspDir, "tlscacerts")
tlsIntCACertsDir := path.Join(mspDir, "tlsintermediatecerts")

var rootBlks [][]byte
var intBlks [][]byte
chain := si.CAChain
for len(chain) > 0 {
var block *pem.Block
block, chain = pem.Decode(chain)
if block == nil {
break
}

cert, err := x509.ParseCertificate(block.Bytes)
if err != nil {
return errors.Wrap(err, "Failed to parse certificate in the CA chain")
}

if !cert.IsCA {
return errors.New("A certificate in the CA chain is not a CA certificate")
}

// If authority key id is not present or if it is present and equal to subject key id,
// then it is a root certificate
if len(cert.AuthorityKeyId) == 0 || bytes.Equal(cert.AuthorityKeyId, cert.SubjectKeyId) {
rootBlks = append(rootBlks, pem.EncodeToMemory(block))
} else {
intBlks = append(intBlks, pem.EncodeToMemory(block))
}
}
// Store the intermediate certs if there are any
if len(intermediateCerts) > 0 {
certsDir = fmt.Sprintf("%sintermediatecerts", dirPrefix)
err = storeCerts("intermediate certificates", mspDir, certsDir, fname, intermediateCerts)

// Store the root certificates in the "cacerts" msp folder
certBytes := bytes.Join(rootBlks, []byte(""))
if config.Enrollment.Profile == "tls" {
err := storeCert("TLS root CA certificate", tlsRootCACertsDir, tlsfname, certBytes)
if err != nil {
return err
}
} else {
err = storeCert("root CA certificate", rootCACertsDir, fname, certBytes)
if err != nil {
return err
}
}
return nil
}

func storeCerts(what, mspDir, subDir, fname string, cert []byte) error {
err := storeFile(fmt.Sprintf("CA %s", what), mspDir, subDir, fname, cert)
if err != nil {
return err
}
tlsfname := fmt.Sprintf("tls-%s", fname)
tlsCertsDir := fmt.Sprintf("tls%s", subDir)
err = storeFile(fmt.Sprintf("TLS %s", what), mspDir, tlsCertsDir, tlsfname, cert)
if err != nil {
return err
// Store the intermediate certificates in the "intermediatecerts" msp folder
certBytes = bytes.Join(intBlks, []byte(""))
if config.Enrollment.Profile == "tls" {
err = storeCert("TLS intermediate certificates", tlsIntCACertsDir, tlsfname, certBytes)
if err != nil {
return err
}
} else {
err = storeCert("intermediate CA certificates", intCACertsDir, fname, certBytes)
if err != nil {
return err
}
}
return nil
}

func storeFile(what, mspDir, subDir, fname string, contents []byte) error {
dir := path.Join(mspDir, subDir)
func storeCert(what, dir, fname string, contents []byte) error {
err := os.MkdirAll(dir, 0755)
if err != nil {
return errors.Wrapf(err, "Failed to create directory for %s at '%s'", what, dir)
Expand All @@ -154,14 +182,3 @@ func storeFile(what, mspDir, subDir, fname string, contents []byte) error {
log.Infof("Stored %s at %s", what, fpath)
return nil
}

// Return the prefix to add to the "cacerts" and "intermediatecerts" directories
// based on the target profile. If the profile is "tls", these directories become
// "tlscacerts" and "tlsintermediatecerts", respectively. There is no prefix for
// any other profile.
func dirPrefixByProfile(profile string) string {
if profile == "tls" {
return "tls"
}
return ""
}
158 changes: 147 additions & 11 deletions cmd/fabric-ca-client/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ package main

import (
"bufio"
"bytes"
"crypto/x509"
"encoding/hex"
"encoding/pem"
Expand Down Expand Up @@ -625,7 +626,7 @@ func TestGencsr(t *testing.T) {
}

signcerts := path.Join(mspDir, "signcerts")
assertOneFileInDir(signcerts, t)
assertFilesInDir(signcerts, 1, t)

files, err := ioutil.ReadDir(signcerts)
if err != nil {
Expand Down Expand Up @@ -682,14 +683,32 @@ func TestMOption(t *testing.T) {
if err != nil {
t.Fatalf("client enroll -u failed: %s", err)
}
assertOneFileInDir(path.Join(homedir, mspdir, "keystore"), t)
assertOneFileInDir(path.Join(homedir, mspdir, "cacerts"), t)
assertOneFileInDir(path.Join(homedir, mspdir, "intermediatecerts"), t)
assertOneFileInDir(path.Join(homedir, mspdir, "tlscacerts"), t)
assertOneFileInDir(path.Join(homedir, mspdir, "tlsintermediatecerts"), t)

assertFilesInDir(path.Join(homedir, mspdir, "keystore"), 1, t)
assertFilesInDir(path.Join(homedir, mspdir, "cacerts"), 1, t)
assertFilesInDir(path.Join(homedir, mspdir, "intermediatecerts"), 1, t)
validCertsInDir(path.Join(homedir, mspdir, "cacerts"), path.Join(homedir, mspdir, "intermediatecerts"), t)
_, err = ioutil.ReadDir(path.Join(homedir, mspdir, "tlscacerts"))
assert.Error(t, err, "The MSP folder 'tlscacerts' should not exist")
_, err = ioutil.ReadDir(path.Join(homedir, mspdir, "tlsintermediatecerts"))
assert.Error(t, err, "The MSP folder 'tlsintermediatecerts' should not exist")

homedir = path.Join(moptionDir, "client")
mspdir = "msp3" // relative to homedir
err = RunMain([]string{
cmdName, "enroll",
"-u", fmt.Sprintf("http://admin:adminpw@localhost:%d", intCAPort),
"-c", path.Join(homedir, "config.yaml"),
"-M", mspdir, "--enrollment.profile", "tls", "-d"})
if err != nil {
t.Fatalf("client enroll -u failed: %s", err)
}
assertFilesInDir(path.Join(homedir, mspdir, "keystore"), 1, t)
assertFilesInDir(path.Join(homedir, mspdir, "tlscacerts"), 1, t)
assertFilesInDir(path.Join(homedir, mspdir, "tlsintermediatecerts"), 1, t)
validCertsInDir(path.Join(homedir, mspdir, "tlscacerts"), path.Join(homedir, mspdir, "tlsintermediatecerts"), t)
assertFilesInDir(path.Join(homedir, mspdir, "cacerts"), 0, t)
_, err = ioutil.ReadDir(path.Join(homedir, mspdir, "intermediatecerts"))
assert.Error(t, err, "The MSP folder 'intermediatecerts' should not exist")

// Test case: msp and home are in different paths
// Enroll the bootstrap user and then register another user. Since msp
Expand All @@ -712,7 +731,7 @@ func TestMOption(t *testing.T) {
assert.NoError(t, err, "Register command should not fail even though -M option is not specified")
}

// Checks to see if root and intermediate certificate are correctly getting stored in their respective directories
// Checks to see if root and intermediate certificates are correctly getting stored in their respective directories
func validCertsInDir(rootCertDir, interCertsDir string, t *testing.T) {
files, err := ioutil.ReadDir(rootCertDir)
file := files[0].Name()
Expand All @@ -733,6 +752,123 @@ func validCertsInDir(rootCertDir, interCertsDir string, t *testing.T) {
}
}

// TestThreeCAHierarchy runs testThreeCAHierarchy test with and without
// setting the environment variable CA_CHAIN_PARENT_FIRST
func TestThreeCAHierarchy(t *testing.T) {
parentFirstEnvVal := os.Getenv(lib.CAChainParentFirstEnvVar)
os.Unsetenv(lib.CAChainParentFirstEnvVar)
defer os.Setenv(lib.CAChainParentFirstEnvVar, parentFirstEnvVal)
testThreeCAHierarchy(t)

os.Setenv(lib.CAChainParentFirstEnvVar, "true")
testThreeCAHierarchy(t)
}

// testThreeCAHierarchy tests three CA hierarchy (root CA -- intermediate CA -- Issuing CA)
// The client enrolls a user with the Issuing CA and checks if the there is one root CA cert
// in the 'cacerts' folder of client msp and two intermediate CA certs in the pem file in
// the 'intermediatecerts' folder.
func testThreeCAHierarchy(t *testing.T) {
validateCACerts := func(rootCertDir, interCertsDir string) {
files, err := ioutil.ReadDir(rootCertDir)
file := files[0].Name()
rootCertPath := filepath.Join(rootCertDir, file)
rootcaCertBytes, err := util.ReadFile(rootCertPath)
assert.NoError(t, err, "Failed to read root CA certificate file %s", rootCertPath)
rootcerts, err := util.GetX509CertificatesFromPEM(rootcaCertBytes)
assert.NoError(t, err, "Failed to retrieve root certificate from root CA certificate file")
assert.Equal(t, 1, len(rootcerts), "There should be only one root CA certificate")
assert.True(t, reflect.DeepEqual(rootcerts[0].Subject, rootcerts[0].Issuer),
"Not a valid root certificate '%s' stored in the '%s' directory",
rootCertPath, filepath.Base(rootCertDir))

interCertPath := filepath.Join(interCertsDir, file)
intcaCertBytes, err := util.ReadFile(interCertPath)
assert.NoError(t, err, "Failed to read intermediate CA certificates file %s", interCertPath)
intcerts, err := util.GetX509CertificatesFromPEM(intcaCertBytes)
assert.NoError(t, err, "Failed to retrieve certs from intermediate CA certificates file")
assert.Equal(t, 2, len(intcerts), "There should be 2 intermediate CA certificates")
if os.Getenv(lib.CAChainParentFirstEnvVar) != "" {
// Assert that first int CA cert's issuer must be root CA's subject
assert.True(t, bytes.Equal(intcerts[0].RawIssuer, rootcerts[0].RawSubject), "Intermediate CA's issuer should be root CA's subject")
// Assert that second int CA cert's issuer must be second int CA's subject
assert.True(t, bytes.Equal(intcerts[1].RawIssuer, intcerts[0].RawSubject), "Issuing CA's issuer should be intermediate CA's subject")
} else {
// Assert that first int CA cert's issuer must be second int CA's subject
assert.True(t, bytes.Equal(intcerts[0].RawIssuer, intcerts[1].RawSubject), "Issuing CA's issuer should be intermediate CA's subject")
// Assert that second int CA cert's issuer must be root CA's subject
assert.True(t, bytes.Equal(intcerts[1].RawIssuer, rootcerts[0].RawSubject), "Intermediate CA's issuer should be root CA's subject")
}
}

multiIntCATestDir := "multi-intca-test"
os.RemoveAll(multiIntCATestDir)
defer os.RemoveAll(multiIntCATestDir)

// Create and start the Root CA server
rootCAPort := 7173
rootServer := startServer(path.Join(multiIntCATestDir, "rootServer"), rootCAPort, "", t)
if rootServer == nil {
return
}
defer rootServer.Stop()

// Create and start the Intermediate CA server
rootCAURL := fmt.Sprintf("http://admin:adminpw@localhost:%d", rootCAPort)
intCAPort := 7174
intServer := startServer(path.Join(multiIntCATestDir, "intServer"), intCAPort, rootCAURL, t)
if intServer == nil {
return
}
defer intServer.Stop()

// Stop the Intermediate CA server to register identity of the Issuing CA
err := intServer.Stop()
if err != nil {
t.Fatal("Failed to stop intermediate CA server after registering identity for the Issuing CA server")
}

// Register an identity for Issuing CA with the Intermediate CA, this identity will be used by the Issuing
// CA to get it's CA certificate
intCA1Admin := "int-ca1-admin"
err = intServer.RegisterBootstrapUser(intCA1Admin, "adminpw", "")
if err != nil {
t.Fatal("Failed to register identity for the Issuing CA server")
}

// Restart the Intermediate CA server
err = intServer.Start()
if err != nil {
t.Fatal("Failed to start intermediate CA server after registering identity for the Issuing CA server")
}

// Create and start the Issuing CA server
intCAURL := fmt.Sprintf("http://%s:adminpw@localhost:%d", intCA1Admin, intCAPort)
intCA1Port := 7175
intServer1 := startServer(path.Join(multiIntCATestDir, "intServer1"), intCA1Port, intCAURL, t)
if intServer1 == nil {
return
}
defer intServer1.Stop()

// Enroll bootstrap admin of the Issuing CA
homedir := path.Join(multiIntCATestDir, "client")
mspdir := "msp" // relative to homedir
err = RunMain([]string{
cmdName, "enroll",
"-u", fmt.Sprintf("http://admin:adminpw@localhost:%d", intCA1Port),
"-c", path.Join(homedir, "config.yaml"),
"-M", mspdir, "-d"})
if err != nil {
t.Fatalf("Client enroll -u failed: %s", err)
}

assertFilesInDir(path.Join(homedir, mspdir, "keystore"), 1, t)
assertFilesInDir(path.Join(homedir, mspdir, "cacerts"), 1, t)
assertFilesInDir(path.Join(homedir, mspdir, "intermediatecerts"), 1, t)
validateCACerts(path.Join(homedir, mspdir, "cacerts"), path.Join(homedir, mspdir, "intermediatecerts"))
}

// TestReenroll tests fabric-ca-client reenroll
func testReenroll(t *testing.T) {
t.Log("Testing Reenroll command")
Expand Down Expand Up @@ -1653,14 +1789,14 @@ func extraArgErrorTest(in *TestData, t *testing.T) {
}

// Make sure there is exactly one file in a directory
func assertOneFileInDir(dir string, t *testing.T) {
func assertFilesInDir(dir string, numFiles int, t *testing.T) {
files, err := ioutil.ReadDir(dir)
if err != nil {
t.Fatalf("Failed to get number of files in directory '%s': %s", dir, err)
}
count := len(files)
if count != 1 {
t.Fatalf("expecting 1 file in %s but found %d", dir, count)
if count != numFiles {
t.Fatalf("Expecting %d file in %s but found %d", numFiles, dir, count)
}
}

Expand Down
5 changes: 5 additions & 0 deletions docs/source/users-guide.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1167,6 +1167,11 @@ The following command will install CA2's certificate chain into peer1's MSP dire
export FABRIC_CA_CLIENT_HOME=$HOME/fabric-ca/clients/peer1
fabric-ca-client getcacert -u http://localhost:7055 -M $FABRIC_CA_CLIENT_HOME/msp
By default, the Fabric CA server returns the CA chain in child-first order. This means that each CA
certificate in the chain is followed by its issuer's CA certificate. If you need the Fabric CA server
to return the CA chain in the opposite order, then set the environment variable ``CA_CHAIN_PARENT_FIRST``
to ``true`` and restart the Fabric CA server. The Fabric CA client will handle either order appropriately.

Reenrolling an Identity
~~~~~~~~~~~~~~~~~~~~~~~

Expand Down
Loading

0 comments on commit b3c00ea

Please sign in to comment.