From 5031b0a96cf223d7242fd971c6de644c84dd3c20 Mon Sep 17 00:00:00 2001 From: Gregory Haskins Date: Fri, 28 Apr 2017 14:50:48 -0400 Subject: [PATCH] [FAB-3456] cryptogen: Add support for x509 SANs The "What" ================= This patch adds support for defining x509 "Subject Alternative Names" (SAN) (https://en.wikipedia.org/wiki/Subject_Alternative_Name). This feature allows an x509 to present multiple valid identities. For example, multiple DNS names representing one key-pair/cert. By default, all x509s generated are populated with two default SAN entries: CommonName and Hostname. Users may extend this with additional definitions via the template engine. See "cryptogen showtemplate" for details. The "Why" ================== Peers deployed in certain contexts such as container orchastration platforms may find certain DNS relationships that can be complex. For instance, two containers "foo" and "bar" might have FDQNs "foo.baz.cluster.local" and "bar.baz.cluster.local" within Kubernetes, just "foo" or "bar" from within the "baz.cluster.local" domain, or a completely different DNS name if the services are mapped outside of the Kubernetes platform. Different schemes may sometimes be easy to use in one context, and difficult to use in another. SAN extentions to x509 means that we don't have to choose. We can simply annotate the x509 for all the valid scenarios while still offering full security. Fixes FAB-3456 Change-Id: Ie6a3864c5675f51097e0b4348bf05ba8c4ef3870 Signed-off-by: Greg Haskins --- common/tools/cryptogen/ca/ca_test.go | 6 +- common/tools/cryptogen/ca/generator.go | 3 +- common/tools/cryptogen/main.go | 171 +++++++++++++++--------- common/tools/cryptogen/msp/generator.go | 4 +- common/tools/cryptogen/msp/msp_test.go | 6 +- 5 files changed, 115 insertions(+), 75 deletions(-) diff --git a/common/tools/cryptogen/ca/ca_test.go b/common/tools/cryptogen/ca/ca_test.go index 8d2b84f8578..9e098abce33 100644 --- a/common/tools/cryptogen/ca/ca_test.go +++ b/common/tools/cryptogen/ca/ca_test.go @@ -71,7 +71,7 @@ func TestGenerateSignCertificate(t *testing.T) { rootCA, err := ca.NewCA(caDir, testCA2Name, testCA2Name) assert.NoError(t, err, "Error generating CA") - _, err = rootCA.SignCertificate(certDir, testName, ecPubKey) + _, err = rootCA.SignCertificate(certDir, testName, nil, ecPubKey) assert.NoError(t, err, "Failed to generate signed certificate") // check to make sure the signed public key was stored @@ -79,7 +79,7 @@ func TestGenerateSignCertificate(t *testing.T) { assert.Equal(t, true, checkForFile(pemFile), "Expected to find file "+pemFile) - _, err = rootCA.SignCertificate(certDir, "empty/CA", ecPubKey) + _, err = rootCA.SignCertificate(certDir, "empty/CA", nil, ecPubKey) assert.Error(t, err, "Bad name should fail") // use an empty CA to test error path @@ -87,7 +87,7 @@ func TestGenerateSignCertificate(t *testing.T) { Name: "badCA", SignCert: &x509.Certificate{}, } - _, err = badCA.SignCertificate(certDir, testName, &ecdsa.PublicKey{}) + _, err = badCA.SignCertificate(certDir, testName, nil, &ecdsa.PublicKey{}) assert.Error(t, err, "Empty CA should not be able to sign") cleanup(testDir) diff --git a/common/tools/cryptogen/ca/generator.go b/common/tools/cryptogen/ca/generator.go index 3d991d627b0..f49e8f8aec3 100644 --- a/common/tools/cryptogen/ca/generator.go +++ b/common/tools/cryptogen/ca/generator.go @@ -86,7 +86,7 @@ func NewCA(baseDir, org, name string) (*CA, error) { // SignCertificate creates a signed certificate based on a built-in template // and saves it in baseDir/name -func (ca *CA) SignCertificate(baseDir, name string, pub *ecdsa.PublicKey) (*x509.Certificate, error) { +func (ca *CA) SignCertificate(baseDir, name string, sans []string, pub *ecdsa.PublicKey) (*x509.Certificate, error) { template := x509Template() template.ExtKeyUsage = []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth} @@ -96,6 +96,7 @@ func (ca *CA) SignCertificate(baseDir, name string, pub *ecdsa.PublicKey) (*x509 subject.CommonName = name template.Subject = subject + template.DNSNames = sans cert, err := genCertificateECDSA(baseDir, name, &template, ca.SignCert, pub, ca.Signer) diff --git a/common/tools/cryptogen/main.go b/common/tools/cryptogen/main.go index 75d1308f345..89a8b8d4d37 100644 --- a/common/tools/cryptogen/main.go +++ b/common/tools/cryptogen/main.go @@ -46,21 +46,23 @@ type HostnameData struct { Domain string } -type CommonNameData struct { - Hostname string - Domain string +type SpecData struct { + Hostname string + Domain string + CommonName string } type NodeTemplate struct { - Count int `yaml:"Count"` - Start int `yaml:"Start"` - Hostname string `yaml:"Hostname"` + Count int `yaml:"Count"` + Start int `yaml:"Start"` + Hostname string `yaml:"Hostname"` + SANS []string `yaml:"SANS"` } type NodeSpec struct { - Hostname string `yaml:"Hostname"` - AltHostnames []string `yaml:"AltHostnames"` - CommonName string `yaml:"CommonName"` + Hostname string `yaml:"Hostname"` + CommonName string `yaml:"CommonName"` + SANS []string `yaml:"SANS"` } type UsersSpec struct { @@ -132,10 +134,20 @@ PeerOrgs: # # which obtains its values from the Spec.Hostname and # Org.Domain, respectively. + # - SANS: (Optional) Specifies one or more Subject Alternative Names + # the be set in the resulting x509. Accepts template + # variables {{.Hostname}}, {{.Domain}}, {{.CommonName}} + # NOTE: Two implicit entries are created for you: + # - {{ .CommonName }} + # - {{ .Hostname }} # --------------------------------------------------------------------------- # Specs: # - Hostname: foo # implicitly "foo.org1.example.com" # CommonName: foo27.org5.example.com # overrides Hostname-based FQDN set above + # SANS: + # - "bar.{{.Domain}}" + # - "altfoo.{{.Domain}}" + # - "{{.Hostname}}.org6.net" # - Hostname: bar # - Hostname: baz @@ -155,6 +167,8 @@ PeerOrgs: Count: 1 # Start: 5 # Hostname: {{.Prefix}}{{.Index}} # default + # SANS: + # - "{{.Hostname}}.alt.{{.Domain}}" # --------------------------------------------------------------------------- # "Users" @@ -234,7 +248,7 @@ func generate() { } for _, orgSpec := range config.PeerOrgs { - err = generateNodeSpec(&orgSpec, "peer") + err = renderOrgSpec(&orgSpec, "peer") if err != nil { fmt.Printf("Error processing peer configuration: %s", err) os.Exit(-1) @@ -243,7 +257,7 @@ func generate() { } for _, orgSpec := range config.OrdererOrgs { - generateNodeSpec(&orgSpec, "orderer") + renderOrgSpec(&orgSpec, "orderer") if err != nil { fmt.Printf("Error processing orderer configuration: %s", err) os.Exit(-1) @@ -252,12 +266,7 @@ func generate() { } } -func parseTemplate(input, defaultInput string, data interface{}) (string, error) { - - // Use the default if the input is an empty string - if len(input) == 0 { - input = defaultInput - } +func parseTemplate(input string, data interface{}) (string, error) { t, err := template.New("parse").Parse(input) if err != nil { @@ -273,16 +282,51 @@ func parseTemplate(input, defaultInput string, data interface{}) (string, error) return output.String(), nil } -func renderCN(domain string, spec NodeSpec) (string, error) { - data := CommonNameData{ +func parseTemplateWithDefault(input, defaultInput string, data interface{}) (string, error) { + + // Use the default if the input is an empty string + if len(input) == 0 { + input = defaultInput + } + + return parseTemplate(input, data) +} + +func renderNodeSpec(domain string, spec *NodeSpec) error { + data := SpecData{ Hostname: spec.Hostname, Domain: domain, } - return parseTemplate(spec.CommonName, defaultCNTemplate, data) + // Process our CommonName + cn, err := parseTemplateWithDefault(spec.CommonName, defaultCNTemplate, data) + if err != nil { + return err + } + + spec.CommonName = cn + data.CommonName = cn + + // Save off our original, unprocessed SANS entries + origSANS := spec.SANS + + // Set our implicit SANS entries for CN/Hostname + spec.SANS = []string{cn, spec.Hostname} + + // Finally, process any remaining SANS entries + for _, _san := range origSANS { + san, err := parseTemplate(_san, data) + if err != nil { + return err + } + + spec.SANS = append(spec.SANS, san) + } + + return nil } -func generateNodeSpec(orgSpec *OrgSpec, prefix string) error { +func renderOrgSpec(orgSpec *OrgSpec, prefix string) error { // First process all of our templated nodes for i := 0; i < orgSpec.Template.Count; i++ { data := HostnameData{ @@ -291,34 +335,36 @@ func generateNodeSpec(orgSpec *OrgSpec, prefix string) error { Domain: orgSpec.Domain, } - hostname, err := parseTemplate(orgSpec.Template.Hostname, defaultHostnameTemplate, data) + hostname, err := parseTemplateWithDefault(orgSpec.Template.Hostname, defaultHostnameTemplate, data) if err != nil { return err } - spec := NodeSpec{Hostname: hostname} + spec := NodeSpec{ + Hostname: hostname, + SANS: orgSpec.Template.SANS, + } orgSpec.Specs = append(orgSpec.Specs, spec) } // Touch up all general node-specs to add the domain for idx, spec := range orgSpec.Specs { - finalCN, err := renderCN(orgSpec.Domain, spec) + err := renderNodeSpec(orgSpec.Domain, &spec) if err != nil { return err } - orgSpec.Specs[idx].CommonName = finalCN + orgSpec.Specs[idx] = spec } // Process the CA node-spec in the same manner if len(orgSpec.CA.Hostname) == 0 { orgSpec.CA.Hostname = "ca" } - finalCN, err := renderCN(orgSpec.Domain, orgSpec.CA) + err := renderNodeSpec(orgSpec.Domain, &orgSpec.CA) if err != nil { return err } - orgSpec.CA.CommonName = finalCN return nil } @@ -346,27 +392,27 @@ func generatePeerOrg(baseDir string, orgSpec OrgSpec) { os.Exit(1) } - peerNames := []string{} - for _, spec := range orgSpec.Specs { - peerNames = append(peerNames, spec.CommonName) - } - generateNodes(peersDir, peerNames, rootCA) + generateNodes(peersDir, orgSpec.Specs, rootCA) // TODO: add ability to specify usernames - usernames := []string{} + users := []NodeSpec{} for j := 1; j <= orgSpec.Users.Count; j++ { - usernames = append(usernames, fmt.Sprintf("%s%d@%s", - userBaseName, j, orgName)) + user := NodeSpec{ + CommonName: fmt.Sprintf("%s%d@%s", userBaseName, j, orgName), + } + + users = append(users, user) } // add an admin user - adminUserName := fmt.Sprintf("%s@%s", - adminBaseName, orgName) + adminUser := NodeSpec{ + CommonName: fmt.Sprintf("%s@%s", adminBaseName, orgName), + } - usernames = append(usernames, adminUserName) - generateNodes(usersDir, usernames, rootCA) + users = append(users, adminUser) + generateNodes(usersDir, users, rootCA) // copy the admin cert to the org's MSP admincerts - err = copyAdminCert(usersDir, adminCertsDir, adminUserName) + err = copyAdminCert(usersDir, adminCertsDir, adminUser.CommonName) if err != nil { fmt.Printf("Error copying admin cert for org %s:\n%v\n", orgName, err) @@ -374,13 +420,12 @@ func generatePeerOrg(baseDir string, orgSpec OrgSpec) { } // copy the admin cert to each of the org's peer's MSP admincerts - for _, peerName := range peerNames { + for _, spec := range orgSpec.Specs { err = copyAdminCert(usersDir, - filepath.Join(peersDir, peerName, "msp", "admincerts"), - adminUserName) + filepath.Join(peersDir, spec.CommonName, "msp", "admincerts"), adminUser.CommonName) if err != nil { fmt.Printf("Error copying admin cert for org %s peer %s:\n%v\n", - orgName, peerName, err) + orgName, spec.CommonName, err) os.Exit(1) } } @@ -407,17 +452,16 @@ func copyAdminCert(usersDir, adminCertsDir, adminUserName string) error { } -func generateNodes(baseDir string, nodeNames []string, rootCA *ca.CA) { +func generateNodes(baseDir string, nodes []NodeSpec, rootCA *ca.CA) { - for _, nodeName := range nodeNames { - nodeDir := filepath.Join(baseDir, nodeName) - err := msp.GenerateLocalMSP(nodeDir, nodeName, rootCA) + for _, node := range nodes { + nodeDir := filepath.Join(baseDir, node.CommonName) + err := msp.GenerateLocalMSP(nodeDir, node.CommonName, node.SANS, rootCA) if err != nil { - fmt.Printf("Error generating local MSP for %s:\n%v\n", nodeName, err) + fmt.Printf("Error generating local MSP for %s:\n%v\n", node, err) os.Exit(1) } } - } func generateOrdererOrg(baseDir string, orgSpec OrgSpec) { @@ -442,25 +486,20 @@ func generateOrdererOrg(baseDir string, orgSpec OrgSpec) { os.Exit(1) } - // TODO: add ability to specify orderer names - // for name just use default base name - ordererNames := []string{} - for _, spec := range orgSpec.Specs { - ordererNames = append(ordererNames, spec.CommonName) - } - generateNodes(orderersDir, ordererNames, rootCA) + generateNodes(orderersDir, orgSpec.Specs, rootCA) - adminUserName := fmt.Sprintf("%s@%s", - adminBaseName, orgName) + adminUser := NodeSpec{ + CommonName: fmt.Sprintf("%s@%s", adminBaseName, orgName), + } // generate an admin for the orderer org - usernames := []string{} + users := []NodeSpec{} // add an admin user - usernames = append(usernames, adminUserName) - generateNodes(usersDir, usernames, rootCA) + users = append(users, adminUser) + generateNodes(usersDir, users, rootCA) // copy the admin cert to the org's MSP admincerts - err = copyAdminCert(usersDir, adminCertsDir, adminUserName) + err = copyAdminCert(usersDir, adminCertsDir, adminUser.CommonName) if err != nil { fmt.Printf("Error copying admin cert for org %s:\n%v\n", orgName, err) @@ -468,12 +507,12 @@ func generateOrdererOrg(baseDir string, orgSpec OrgSpec) { } // copy the admin cert to each of the org's orderers's MSP admincerts - for _, ordererName := range ordererNames { + for _, spec := range orgSpec.Specs { err = copyAdminCert(usersDir, - filepath.Join(orderersDir, ordererName, "msp", "admincerts"), adminUserName) + filepath.Join(orderersDir, spec.CommonName, "msp", "admincerts"), adminUser.CommonName) if err != nil { fmt.Printf("Error copying admin cert for org %s orderer %s:\n%v\n", - orgName, ordererName, err) + orgName, spec.CommonName, err) os.Exit(1) } } diff --git a/common/tools/cryptogen/msp/generator.go b/common/tools/cryptogen/msp/generator.go index d956bbe138f..33c77b2228a 100644 --- a/common/tools/cryptogen/msp/generator.go +++ b/common/tools/cryptogen/msp/generator.go @@ -29,7 +29,7 @@ import ( "github.com/hyperledger/fabric/common/tools/cryptogen/csp" ) -func GenerateLocalMSP(baseDir, name string, rootCA *ca.CA) error { +func GenerateLocalMSP(baseDir, name string, sans []string, rootCA *ca.CA) error { // create folder structure mspDir := filepath.Join(baseDir, "msp") @@ -60,7 +60,7 @@ func GenerateLocalMSP(baseDir, name string, rootCA *ca.CA) error { return err } - cert, err := rootCA.SignCertificate(filepath.Join(mspDir, "signcerts"), name, ecPubKey) + cert, err := rootCA.SignCertificate(filepath.Join(mspDir, "signcerts"), name, sans, ecPubKey) if err != nil { return err } diff --git a/common/tools/cryptogen/msp/msp_test.go b/common/tools/cryptogen/msp/msp_test.go index fb6d1d59b7e..d5dab4053b1 100644 --- a/common/tools/cryptogen/msp/msp_test.go +++ b/common/tools/cryptogen/msp/msp_test.go @@ -38,14 +38,14 @@ func TestGenerateLocalMSP(t *testing.T) { cleanup(testDir) - err := msp.GenerateLocalMSP(testDir, testName, &ca.CA{}) + err := msp.GenerateLocalMSP(testDir, testName, nil, &ca.CA{}) assert.Error(t, err, "Empty CA should have failed") caDir := filepath.Join(testDir, "ca") mspDir := filepath.Join(testDir, "msp") rootCA, err := ca.NewCA(caDir, testCAOrg, testCAName) assert.NoError(t, err, "Error generating CA") - err = msp.GenerateLocalMSP(testDir, testName, rootCA) + err = msp.GenerateLocalMSP(testDir, testName, nil, rootCA) assert.NoError(t, err, "Failed to generate local MSP") // check to see that the right files were generated/saved @@ -70,7 +70,7 @@ func TestGenerateLocalMSP(t *testing.T) { assert.NoError(t, err, "Error setting up local MSP") rootCA.Name = "test/fail" - err = msp.GenerateLocalMSP(testDir, testName, rootCA) + err = msp.GenerateLocalMSP(testDir, testName, nil, rootCA) assert.Error(t, err, "Should have failed with CA name 'test/fail'") t.Log(err) cleanup(testDir)