diff --git a/.gitignore b/.gitignore index 336d34b..2675c61 100644 --- a/.gitignore +++ b/.gitignore @@ -6,4 +6,5 @@ certstrap # Nice to have in .gitignore .idea/ # .DS_Store file sometimes generated by Mac computers -.DS_Store \ No newline at end of file +.DS_Store +*.swp diff --git a/README.md b/README.md index 339038f..4b27be7 100644 --- a/README.md +++ b/README.md @@ -81,11 +81,21 @@ Created out/Alice.crt from out/Alice.csr signed by out/CertAuth.key ``` #### PKCS Format: -If you'd like to convert your certificate and key to PKCS12 format, simply run: + +If you'd like to convert your certificate and key to PKCS12 format, + +``` +$ ./certstrap export-pfx Alice --chain "CertAuth" +Created out/Alice.pfx from out/Alice.crt and out/Alice.key with chain [out/CertAuth.crt] +``` +`Alice.key` and `Alice.crt` make up the leaf private key and certificate pair of your choosing (generated by a `sign` command), +with `CertAuth.crt` being the certificate authority certificate that was used to sign it. The output PKCS12 file is `Alice.pfx` + +or simply run openssl: + ``` -$ openssl pkcs12 -export -out outputCert.p12 -inkey inputKey.key -in inputCert.crt -certfile CA.crt +$ openssl pkcs12 -export -out out/Alice.pfx -inkey out/Alice.key -in out/Alice.crt -certfile out/CertAuth.crt ``` -`inputKey.key` and `inputCert.crt` make up the leaf private key and certificate pair of your choosing (generated by a `sign` command), with `CA.crt` being the certificate authority certificate that was used to sign it. The output PKCS12 file is `outputCert.p12` ### Key Algorithms: Certstrap supports curves P-224, P-256, P-384, P-521, and Ed25519. Curve names can be specified by name as part of the `init` and `request_cert` commands: diff --git a/certstrap.go b/certstrap.go index a8666af..eeeefcb 100644 --- a/certstrap.go +++ b/certstrap.go @@ -47,6 +47,7 @@ func main() { cmd.NewCertRequestCommand(), cmd.NewSignCommand(), cmd.NewRevokeCommand(), + cmd.NewExportPfxCommand(), } app.Before = func(c *cli.Context) error { return cmd.InitDepot(c.String("depot-path")) diff --git a/cmd/export_pfx.go b/cmd/export_pfx.go new file mode 100644 index 0000000..cb35b3e --- /dev/null +++ b/cmd/export_pfx.go @@ -0,0 +1,114 @@ +/*- + * Copyright 2015 Square Inc. + * Copyright 2014 CoreOS + * + * 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 cmd + +import ( + "fmt" + "os" + "strings" + + "github.com/square/certstrap/depot" + "github.com/square/certstrap/pkix" + "github.com/urfave/cli" +) + +// ExportPfxCommand sets up a "export-pfx" command to export certificate chain to personal information exchange format +func NewExportPfxCommand() cli.Command { + return cli.Command{ + Name: "export-pfx", + Usage: "Export certificate chain to personal information exchange format", + Description: "Export certificate chain for host to personal information exchange format, including root certificate and key.", + Flags: []cli.Flag{ + cli.StringFlag{Name: "passphrase", Value: "", Usage: "Passphrase to de- and encrypt private-key PEM block"}, + cli.StringFlag{Name: "chain", Value: "", Usage: "Names of chained parent certificates; comma delimited"}, + cli.BoolFlag{Name: "stdout", Usage: "Print signing request to stdout in addition to saving file"}, + }, + Action: exportPfxAction, + } +} + +func exportPfxAction(c *cli.Context) { + var err error + + if len(c.Args()) != 1 { + fmt.Fprintln(os.Stderr, "One name must be provided.") + os.Exit(1) + } + + formattedName := strings.Replace(c.Args()[0], " ", "_", -1) + + if depot.CheckPersonalInformationExchange(d, formattedName) { + fmt.Fprintln(os.Stderr, "PFX has existed!") + os.Exit(1) + } + + passphrase := []byte{} + + key, err := depot.GetPrivateKey(d, formattedName) + if err != nil { + passphrase, err = getPassPhrase(c, "Certificate key") + if err != nil { + fmt.Fprintln(os.Stderr, "Get certificate key error:", err) + os.Exit(1) + } + key, err = depot.GetEncryptedPrivateKey(d, formattedName, passphrase) + if err != nil { + fmt.Fprintln(os.Stderr, "Get certificate key error:", err) + os.Exit(1) + } + } + + cert, err := depot.GetCertificate(d, formattedName) + if err != nil { + fmt.Fprintln(os.Stderr, "Get certificate error:", err) + os.Exit(1) + } + + caCrts := []*pkix.Certificate{} + var formattedCANames []string = []string{} + if c.IsSet("chain") { + var formattedCAName string + chain := strings.Split(c.String("chain"), ",") + for i := range chain { + formattedCAName = strings.Replace(chain[i], " ", "_", -1) + ca, err := depot.GetCertificate(d, formattedCAName) + if err != nil { + fmt.Fprintln(os.Stderr, "Get chained certificate error:", err) + os.Exit(1) + } + caCrts = append(caCrts, ca) + formattedCANames = append(formattedCANames, fmt.Sprintf("%s/%s.crt", depotDir, formattedCAName)) + } + } + + pfxBytes, err := depot.PutPersonalInformationExchange(d, formattedName, cert, key, caCrts, passphrase) + if err != nil { + fmt.Fprintln(os.Stderr, "Export certificate chain to personal information exchange format failed:", err) + os.Exit(1) + } else { + fmt.Printf("Created %s/%s.pfx from %s/%s.crt and %s/%s.key", depotDir, formattedName, depotDir, formattedName, depotDir, formattedName) + if len(formattedCANames) > 0 { + fmt.Printf(" with chain %s", formattedCANames) + } + fmt.Printf("\n") + } + + if c.Bool("stdout") { + fmt.Printf(string(pfxBytes[:])) + } +} diff --git a/depot/pkix.go b/depot/pkix.go index 8a90eb8..dc70aea 100644 --- a/depot/pkix.go +++ b/depot/pkix.go @@ -18,9 +18,12 @@ package depot import ( + "crypto/rand" + "crypto/x509" "strings" "github.com/square/certstrap/pkix" + "software.sslmate.com/src/go-pkcs12" ) const ( @@ -28,6 +31,7 @@ const ( csrSuffix = ".csr" privKeySuffix = ".key" crlSuffix = ".crl" + pfxSuffix = ".pfx" ) // CrtTag returns a tag corresponding to a certificate @@ -50,6 +54,11 @@ func CrlTag(prefix string) *Tag { return &Tag{prefix + crlSuffix, LeafPerm} } +// PfxTag returns a tag corresponding to a personal information exchange +func PfxTag(prefix string) *Tag { + return &Tag{prefix + pfxSuffix, LeafPerm} +} + // GetNameFromCrtTag returns the host name from a certificate file tag func GetNameFromCrtTag(tag *Tag) string { return getName(tag, crtSuffix) @@ -112,6 +121,11 @@ func CheckCertificateSigningRequest(d Depot, name string) bool { return d.Check(CsrTag(name)) } +// CheckPersonalInformationExchange checks the depot for existence of a pfx file for a given name +func CheckPersonalInformationExchange(d Depot, name string) bool { + return d.Check(PfxTag(name)) +} + // GetCertificateSigningRequest retrieves a certificate signing request file for a given host name from the depot func GetCertificateSigningRequest(d Depot, name string) (crt *pkix.CertificateSigningRequest, err error) { b, err := d.Get(CsrTag(name)) @@ -176,7 +190,28 @@ func PutCertificateRevocationList(d Depot, name string, crl *pkix.CertificateRev return d.Put(CrlTag(name), b) } -//GetCertificateRevocationList gets a CRL file for a given name and ca in the depot. +// PutPersonalInformationExchange creates a Personal Information Exchange certificate file for a given name in the depot +func PutPersonalInformationExchange(d Depot, name string, crt *pkix.Certificate, key *pkix.Key, caCrts []*pkix.Certificate, passphrase []byte) ([]byte, error) { + c, err := crt.GetRawCertificate() + if err != nil { + return nil, err + } + chain := []*x509.Certificate{} + for i := range caCrts { + cc, err := caCrts[i].GetRawCertificate() + if err != nil { + return nil, err + } + chain = append(chain, cc) + } + b, err := pkcs12.Encode(rand.Reader, key.Private, c, chain, string(passphrase)) + if err != nil { + return nil, err + } + return b, d.Put(PfxTag(name), b) +} + +// GetCertificateRevocationList gets a CRL file for a given name and ca in the depot. func GetCertificateRevocationList(d Depot, name string) (*pkix.CertificateRevocationList, error) { b, err := d.Get(CrlTag(name)) if err != nil { diff --git a/go.mod b/go.mod index 704432e..c039ddf 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,7 @@ require ( github.com/howeyc/gopass v0.0.0-20170109162249-bf9dde6d0d2c github.com/urfave/cli v1.22.13 go.step.sm/crypto v0.25.1 + software.sslmate.com/src/go-pkcs12 v0.2.1 ) require ( @@ -13,7 +14,7 @@ require ( github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect - golang.org/x/crypto v0.6.0 // indirect - golang.org/x/sys v0.5.0 // indirect - golang.org/x/term v0.5.0 // indirect + golang.org/x/crypto v0.11.0 // indirect + golang.org/x/sys v0.10.0 // indirect + golang.org/x/term v0.10.0 // indirect ) diff --git a/go.sum b/go.sum index 2f183e3..342a377 100644 --- a/go.sum +++ b/go.sum @@ -26,14 +26,16 @@ github.com/urfave/cli v1.22.13 h1:wsLILXG8qCJNse/qAgLNf23737Cx05GflHg/PJGe1Ok= github.com/urfave/cli v1.22.13/go.mod h1:VufqObjsMTF2BBwKawpx9R8eAneNEWhoO0yx8Vd+FkE= go.step.sm/crypto v0.25.1 h1:e08ioZBiZoHrWG0tJOUDPwqoF3PTRiFebINDEw3yPpo= go.step.sm/crypto v0.25.1/go.mod h1:4pUEuZ+4OAf2f70RgW5oRv/rJudibcAAWQg5prC3DT8= -golang.org/x/crypto v0.6.0 h1:qfktjS5LUO+fFKeJXZ+ikTRijMmljikvG68fpMMruSc= -golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= -golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU= -golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/term v0.5.0 h1:n2a8QNdAb0sZNpU9R1ALUXBbY+w51fCQDN+7EdxNBsY= -golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/crypto v0.11.0 h1:6Ewdq3tDic1mg5xRO4milcWCfMVQhI4NkqWWvqejpuA= +golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio= +golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA= +golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.10.0 h1:3R7pNqamzBraeqj/Tj8qt1aQ2HpmlC+Cx/qL/7hn4/c= +golang.org/x/term v0.10.0/go.mod h1:lpqdcUyK/oCiQxvxVrppt5ggO2KCZ5QblwqPnfZ6d5o= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +software.sslmate.com/src/go-pkcs12 v0.2.1 h1:tbT1jjaeFOF230tzOIRJ6U5S1jNqpsSyNjzDd58H3J8= +software.sslmate.com/src/go-pkcs12 v0.2.1/go.mod h1:Qiz0EyvDRJjjxGyUQa2cCNZn/wMyzrRJ/qcDXOQazLI=