From a6855cb69f4c30b424b33281302ca706976fa13a Mon Sep 17 00:00:00 2001 From: Carlos Alberto Costa Beppler <beppler@gmail.com> Date: Sat, 25 Dec 2021 15:36:01 -0300 Subject: [PATCH] Added a --pfx, and --pfx.pass option to generate a PKCS#12 (.pfx) file. (#1387) --- cmd/certs_storage.go | 101 +++++++++++++++++++++++++++---- cmd/flags.go | 10 +++ docs/content/usage/cli/_index.md | 2 + go.mod | 3 +- go.sum | 6 +- 5 files changed, 107 insertions(+), 15 deletions(-) diff --git a/cmd/certs_storage.go b/cmd/certs_storage.go index d61f265d..bb62cb46 100644 --- a/cmd/certs_storage.go +++ b/cmd/certs_storage.go @@ -2,8 +2,12 @@ package cmd import ( "bytes" + "crypto" + "crypto/rand" "crypto/x509" "encoding/json" + "encoding/pem" + "fmt" "os" "path/filepath" "strconv" @@ -15,6 +19,7 @@ import ( "github.com/go-acme/lego/v4/log" "github.com/urfave/cli" "golang.org/x/net/idna" + "software.sslmate.com/src/go-pkcs12" ) const ( @@ -40,6 +45,8 @@ type CertificatesStorage struct { rootPath string archivePath string pem bool + pfx bool + pfxPassword string filename string // Deprecated } @@ -49,6 +56,8 @@ func NewCertificatesStorage(ctx *cli.Context) *CertificatesStorage { rootPath: filepath.Join(ctx.GlobalString("path"), baseCertificatesFolderName), archivePath: filepath.Join(ctx.GlobalString("path"), baseArchivesFolderName), pem: ctx.GlobalBool("pem"), + pfx: ctx.GlobalBool("pfx"), + pfxPassword: ctx.GlobalString("pfx.pass"), filename: ctx.GlobalString("filename"), } } @@ -88,22 +97,15 @@ func (s *CertificatesStorage) SaveResource(certRes *certificate.Resource) { } } + // if we were given a CSR, we don't know the private key if certRes.PrivateKey != nil { - // if we were given a CSR, we don't know the private key - err = s.WriteFile(domain, ".key", certRes.PrivateKey) + err = s.WriteCertificateFiles(domain, certRes) if err != nil { log.Fatalf("Unable to save PrivateKey for domain %s\n\t%v", domain, err) } - - if s.pem { - err = s.WriteFile(domain, ".pem", bytes.Join([][]byte{certRes.Certificate, certRes.PrivateKey}, nil)) - if err != nil { - log.Fatalf("Unable to save Certificate and PrivateKey in .pem for domain %s\n\t%v", domain, err) - } - } - } else if s.pem { - // we don't have the private key; can't write the .pem file - log.Fatalf("Unable to save pem without private key for domain %s\n\t%v; are you using a CSR?", domain, err) + } else if s.pem || s.pfx { + // we don't have the private key; can't write the .pem or .pfx file + log.Fatalf("Unable to save PEM or PFX without private key for domain %s. Are you using a CSR?", domain) } jsonBytes, err := json.MarshalIndent(certRes, "", "\t") @@ -174,6 +176,81 @@ func (s *CertificatesStorage) WriteFile(domain, extension string, data []byte) e return os.WriteFile(filePath, data, filePerm) } +func (s *CertificatesStorage) WriteCertificateFiles(domain string, certRes *certificate.Resource) error { + err := s.WriteFile(domain, ".key", certRes.PrivateKey) + if err != nil { + return fmt.Errorf("unable to save key file: %w", err) + } + + if s.pem { + err = s.WriteFile(domain, ".pem", bytes.Join([][]byte{certRes.Certificate, certRes.PrivateKey}, nil)) + if err != nil { + return fmt.Errorf("unable to save PEM file: %w", err) + } + } + + if s.pfx { + err = s.WritePFXFile(domain, certRes) + if err != nil { + return fmt.Errorf("unable to save PFX file: %w", err) + } + } + + return nil +} + +func (s *CertificatesStorage) WritePFXFile(domain string, certRes *certificate.Resource) error { + certPemBlock, _ := pem.Decode(certRes.Certificate) + if certPemBlock == nil { + return fmt.Errorf("unable to parse Certificate for domain %s", domain) + } + + cert, err := x509.ParseCertificate(certPemBlock.Bytes) + if err != nil { + return fmt.Errorf("unable to load Certificate for domain %s: %w", domain, err) + } + + issuerCertPemBlock, _ := pem.Decode(certRes.IssuerCertificate) + if issuerCertPemBlock == nil { + return fmt.Errorf("unable to parse Issuer Certificate for domain %s", domain) + } + + issuerCert, err := x509.ParseCertificate(issuerCertPemBlock.Bytes) + if err != nil { + return fmt.Errorf("unable to load Issuer Certificate for domain %s: %w", domain, err) + } + + keyPemBlock, _ := pem.Decode(certRes.PrivateKey) + if keyPemBlock == nil { + return fmt.Errorf("unable to parse PrivateKey for domain %s", domain) + } + + var privateKey crypto.Signer + var keyErr error + + switch keyPemBlock.Type { + case "RSA PRIVATE KEY": + privateKey, keyErr = x509.ParsePKCS1PrivateKey(keyPemBlock.Bytes) + if keyErr != nil { + return fmt.Errorf("unable to load RSA PrivateKey for domain %s: %w", domain, keyErr) + } + case "EC PRIVATE KEY": + privateKey, keyErr = x509.ParseECPrivateKey(keyPemBlock.Bytes) + if keyErr != nil { + return fmt.Errorf("unable to load EC PrivateKey for domain %s: %w", domain, keyErr) + } + default: + return fmt.Errorf("unsupported PrivateKey type '%s' for domain %s", keyPemBlock.Type, domain) + } + + pfxBytes, err := pkcs12.Encode(rand.Reader, privateKey, cert, []*x509.Certificate{issuerCert}, s.pfxPassword) + if err != nil { + return fmt.Errorf("unable to encode PFX data for domain %s: %w", domain, err) + } + + return s.WriteFile(domain, ".pfx", pfxBytes) +} + func (s *CertificatesStorage) MoveToArchive(domain string) error { matches, err := filepath.Glob(filepath.Join(s.rootPath, sanitizedDomain(domain)+".*")) if err != nil { diff --git a/cmd/flags.go b/cmd/flags.go index 8307b2c9..dcc90018 100644 --- a/cmd/flags.go +++ b/cmd/flags.go @@ -3,6 +3,7 @@ package cmd import ( "github.com/go-acme/lego/v4/lego" "github.com/urfave/cli" + pkcs12 "software.sslmate.com/src/go-pkcs12" ) func CreateFlags(defaultPath string) []cli.Flag { @@ -111,6 +112,15 @@ func CreateFlags(defaultPath string) []cli.Flag { Name: "pem", Usage: "Generate a .pem file by concatenating the .key and .crt files together.", }, + cli.BoolFlag{ + Name: "pfx", + Usage: "Generate a .pfx (PKCS#12) file by with the .key and .crt and issuer .crt files together.", + }, + cli.StringFlag{ + Name: "pfx.pass", + Usage: "The password used to encrypt the .pfx (PCKS#12) file.", + Value: pkcs12.DefaultPassword, + }, cli.IntFlag{ Name: "cert.timeout", Usage: "Set the certificate timeout value to a specific value in seconds. Only used when obtaining certificates.", diff --git a/docs/content/usage/cli/_index.md b/docs/content/usage/cli/_index.md index 14f20859..c32d0755 100644 --- a/docs/content/usage/cli/_index.md +++ b/docs/content/usage/cli/_index.md @@ -50,6 +50,8 @@ GLOBAL OPTIONS: --http-timeout value Set the HTTP timeout value to a specific value in seconds. (default: 0) --dns-timeout value Set the DNS timeout value to a specific value in seconds. Used only when performing authoritative name servers queries. (default: 10) --pem Generate a .pem file by concatenating the .key and .crt files together. + --pfx Generate a .pfx (PKCS#12) file by with the .key and .crt and issuer .crt files together. + --pfx.pass The password used to encrypt the .pfx (PCKS#12) file (default: changeit). --cert.timeout value Set the certificate timeout value to a specific value in seconds. Only used when obtaining certificates. (default: 30) --help, -h show help --version, -v print the version diff --git a/go.mod b/go.mod index 651b01b5..334ac1bd 100644 --- a/go.mod +++ b/go.mod @@ -56,11 +56,12 @@ require ( github.com/vinyldns/go-vinyldns v0.9.16 github.com/vultr/govultr/v2 v2.7.1 golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e - golang.org/x/net v0.0.0-20210614182718-04defd469f4e + golang.org/x/net v0.0.0-20210510120150-4163338589ed golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d golang.org/x/time v0.0.0-20210611083556-38a9dc6acbc6 google.golang.org/api v0.20.0 gopkg.in/ns1/ns1-go.v2 v2.6.2 gopkg.in/square/go-jose.v2 v2.6.0 gopkg.in/yaml.v2 v2.4.0 + software.sslmate.com/src/go-pkcs12 v0.0.0-20210415151418-c5206de65a78 ) diff --git a/go.sum b/go.sum index 2e2d569f..3d15eb93 100644 --- a/go.sum +++ b/go.sum @@ -515,6 +515,7 @@ golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPh golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= +golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8= golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e h1:gsTQYXdTw2Gq7RBsWvlQ91b+aEQ6bXFUngBGuR8sPpI= golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= @@ -579,9 +580,8 @@ golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwY golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= +golang.org/x/net v0.0.0-20210510120150-4163338589ed h1:p9UgmWI9wKpfYmgaV/IZKGdXc5qEK45tDwwwDyjS26I= golang.org/x/net v0.0.0-20210510120150-4163338589ed/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20210614182718-04defd469f4e h1:XpT3nA5TvE525Ne3hInMh6+GETgn27Zfm9dxsThnX2Q= -golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -799,3 +799,5 @@ honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9 rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= +software.sslmate.com/src/go-pkcs12 v0.0.0-20210415151418-c5206de65a78 h1:SqYE5+A2qvRhErbsXFfUEUmpWEKxxRSMgGLkvRAFOV4= +software.sslmate.com/src/go-pkcs12 v0.0.0-20210415151418-c5206de65a78/go.mod h1:B7Wf0Ya4DHF9Yw+qfZuJijQYkWicqDa+79Ytmmq3Kjg=