Added a --pfx, and --pfx.pass option to generate a PKCS#12 (.pfx) file. (#1387)

This commit is contained in:
Carlos Alberto Costa Beppler 2021-12-25 15:36:01 -03:00 committed by GitHub
parent f4b153f26f
commit a6855cb69f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 107 additions and 15 deletions

View file

@ -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 {

View file

@ -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.",

View file

@ -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

3
go.mod
View file

@ -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
)

6
go.sum
View file

@ -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=