Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add export-pfx command #215

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@ certstrap
# Nice to have in .gitignore
.idea/
# .DS_Store file sometimes generated by Mac computers
.DS_Store
.DS_Store
*.swp
16 changes: 13 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
1 change: 1 addition & 0 deletions certstrap.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"))
Expand Down
114 changes: 114 additions & 0 deletions cmd/export_pfx.go
Original file line number Diff line number Diff line change
@@ -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[:]))
}
}
37 changes: 36 additions & 1 deletion depot/pkix.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,16 +18,20 @@
package depot

import (
"crypto/rand"
"crypto/x509"
"strings"

"github.com/square/certstrap/pkix"
"software.sslmate.com/src/go-pkcs12"
)

const (
crtSuffix = ".crt"
csrSuffix = ".csr"
privKeySuffix = ".key"
crlSuffix = ".crl"
pfxSuffix = ".pfx"
)

// CrtTag returns a tag corresponding to a certificate
Expand All @@ -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)
Expand Down Expand Up @@ -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))
Expand Down Expand Up @@ -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 {
Expand Down
7 changes: 4 additions & 3 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,15 @@ 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 (
filippo.io/edwards25519 v1.0.0 // indirect
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
)
14 changes: 8 additions & 6 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=