From 4eab81a9ebebe25d5b2041d9b2bce5a07fc45812 Mon Sep 17 00:00:00 2001 From: pschou Date: Sat, 27 Jan 2024 17:44:14 -0500 Subject: [PATCH] feat(cli): add format option for PFX encoding (#2063) Co-authored-by: Fernandez Ludovic --- cmd/certs_storage.go | 64 ++++++++++++++++++++++++++++++++------ cmd/flags.go | 18 ++++++++--- docs/data/zz_cli_help.toml | 5 +-- go.mod | 8 ++--- go.sum | 19 ++++++----- 5 files changed, 84 insertions(+), 30 deletions(-) diff --git a/cmd/certs_storage.go b/cmd/certs_storage.go index 3ddf0b1063..46f04bfe8b 100644 --- a/cmd/certs_storage.go +++ b/cmd/certs_storage.go @@ -3,10 +3,10 @@ package cmd import ( "bytes" "crypto" - "crypto/rand" "crypto/x509" "encoding/json" "encoding/pem" + "errors" "fmt" "os" "path/filepath" @@ -55,17 +55,27 @@ type CertificatesStorage struct { pem bool pfx bool pfxPassword string + pfxFormat string filename string // Deprecated } // NewCertificatesStorage create a new certificates storage. func NewCertificatesStorage(ctx *cli.Context) *CertificatesStorage { + pfxFormat := ctx.String("pfx.format") + + switch pfxFormat { + case "DES", "RC2", "SHA256": + default: + log.Fatalf("Invalid PFX format: %s", pfxFormat) + } + return &CertificatesStorage{ rootPath: filepath.Join(ctx.String("path"), baseCertificatesFolderName), archivePath: filepath.Join(ctx.String("path"), baseArchivesFolderName), pem: ctx.Bool("pem"), pfx: ctx.Bool("pfx"), pfxPassword: ctx.String("pfx.pass"), + pfxFormat: pfxFormat, filename: ctx.String("filename"), } } @@ -218,14 +228,9 @@ func (s *CertificatesStorage) WritePFXFile(domain string, certRes *certificate.R 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) + certChain, err := getCertificateChain(certRes) if err != nil { - return fmt.Errorf("unable to load Issuer Certificate for domain %s: %w", domain, err) + return fmt.Errorf("unable to get certificate chain for domain %s: %w", domain, err) } keyPemBlock, _ := pem.Decode(certRes.PrivateKey) @@ -251,7 +256,12 @@ func (s *CertificatesStorage) WritePFXFile(domain string, certRes *certificate.R 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) + encoder, err := getPFXEncoder(s.pfxFormat) + if err != nil { + return fmt.Errorf("PFX encoder: %w", err) + } + + pfxBytes, err := encoder.Encode(privateKey, cert, certChain, s.pfxPassword) if err != nil { return fmt.Errorf("unable to encode PFX data for domain %s: %w", domain, err) } @@ -285,6 +295,42 @@ func (s *CertificatesStorage) MoveToArchive(domain string) error { return nil } +func getCertificateChain(certRes *certificate.Resource) ([]*x509.Certificate, error) { + chainCertPemBlock, rest := pem.Decode(certRes.IssuerCertificate) + if chainCertPemBlock == nil { + return nil, errors.New("unable to parse Issuer Certificate") + } + + var certChain []*x509.Certificate + for chainCertPemBlock != nil { + chainCert, err := x509.ParseCertificate(chainCertPemBlock.Bytes) + if err != nil { + return nil, fmt.Errorf("unable to parse Chain Certificate: %w", err) + } + + certChain = append(certChain, chainCert) + chainCertPemBlock, rest = pem.Decode(rest) // Try decoding the next pem block + } + + return certChain, nil +} + +func getPFXEncoder(pfxFormat string) (*pkcs12.Encoder, error) { + var encoder *pkcs12.Encoder + switch pfxFormat { + case "SHA256": + encoder = pkcs12.Modern2023 + case "DES": + encoder = pkcs12.LegacyDES + case "RC2": + encoder = pkcs12.LegacyRC2 + default: + return nil, fmt.Errorf("invalid PFX format: %s", pfxFormat) + } + + return encoder, nil +} + // sanitizedDomain Make sure no funny chars are in the cert names (like wildcards ;)). func sanitizedDomain(domain string) string { safe, err := idna.ToASCII(strings.NewReplacer(":", "-", "*", "_").Replace(domain)) diff --git a/cmd/flags.go b/cmd/flags.go index 5d08fa20f6..1d8ca58e06 100644 --- a/cmd/flags.go +++ b/cmd/flags.go @@ -133,13 +133,21 @@ func CreateFlags(defaultPath string) []cli.Flag { Usage: "Generate an additional .pem (base64) file by concatenating the .key and .crt files together.", }, &cli.BoolFlag{ - Name: "pfx", - Usage: "Generate an additional .pfx (PKCS#12) file by concatenating the .key and .crt and issuer .crt files together.", + Name: "pfx", + Usage: "Generate an additional .pfx (PKCS#12) file by concatenating the .key and .crt and issuer .crt files together.", + EnvVars: []string{"LEGO_PFX"}, }, &cli.StringFlag{ - Name: "pfx.pass", - Usage: "The password used to encrypt the .pfx (PCKS#12) file.", - Value: pkcs12.DefaultPassword, + Name: "pfx.pass", + Usage: "The password used to encrypt the .pfx (PCKS#12) file.", + Value: pkcs12.DefaultPassword, + EnvVars: []string{"LEGO_PFX_PASSWORD"}, + }, + &cli.StringFlag{ + Name: "pfx.format", + Usage: "The encoding format to use when encrypting the .pfx (PCKS#12) file. Supported: RC2, DES, SHA256.", + Value: "RC2", + EnvVars: []string{"LEGO_PFX_FORMAT"}, }, &cli.IntFlag{ Name: "cert.timeout", diff --git a/docs/data/zz_cli_help.toml b/docs/data/zz_cli_help.toml index a877121b75..07515b7be7 100644 --- a/docs/data/zz_cli_help.toml +++ b/docs/data/zz_cli_help.toml @@ -44,8 +44,9 @@ 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 server queries. (default: 10) --pem Generate an additional .pem (base64) file by concatenating the .key and .crt files together. (default: false) - --pfx Generate an additional .pfx (PKCS#12) file by concatenating the .key and .crt and issuer .crt files together. (default: false) - --pfx.pass value The password used to encrypt the .pfx (PCKS#12) file. (default: "changeit") + --pfx Generate an additional .pfx (PKCS#12) file by concatenating the .key and .crt and issuer .crt files together. (default: false) [$LEGO_PFX] + --pfx.pass value The password used to encrypt the .pfx (PCKS#12) file. (default: "changeit") [$LEGO_PFX_PASSWORD] + --pfx.format value The encoding format to use when encrypting the .pfx (PCKS#12) file. Supported: RC2, DES, SHA256. (default: "RC2") [$LEGO_PFX_FORMAT] --cert.timeout value Set the certificate timeout value to a specific value in seconds. Only used when obtaining certificates. (default: 30) --user-agent value Add to the user-agent sent to the CA to identify an application embedding lego-cli --help, -h show help diff --git a/go.mod b/go.mod index efe3576bdc..2d991cd590 100644 --- a/go.mod +++ b/go.mod @@ -74,14 +74,14 @@ require ( github.com/vultr/govultr/v2 v2.17.2 github.com/yandex-cloud/go-genproto v0.0.0-20220805142335-27b56ddae16f github.com/yandex-cloud/go-sdk v0.0.0-20220805164847-cf028e604997 - golang.org/x/crypto v0.10.0 + golang.org/x/crypto v0.11.0 golang.org/x/net v0.11.0 golang.org/x/oauth2 v0.9.0 golang.org/x/time v0.3.0 google.golang.org/api v0.111.0 gopkg.in/ns1/ns1-go.v2 v2.7.6 gopkg.in/yaml.v2 v2.4.0 - software.sslmate.com/src/go-pkcs12 v0.2.0 + software.sslmate.com/src/go-pkcs12 v0.4.0 ) require ( @@ -155,8 +155,8 @@ require ( go.opencensus.io v0.24.0 // indirect go.uber.org/ratelimit v0.2.0 // indirect golang.org/x/mod v0.11.0 // indirect - golang.org/x/sys v0.9.0 // indirect - golang.org/x/text v0.10.0 // indirect + golang.org/x/sys v0.10.0 // indirect + golang.org/x/text v0.11.0 // indirect golang.org/x/tools v0.10.0 // indirect google.golang.org/appengine v1.6.7 // indirect google.golang.org/genproto v0.0.0-20230223222841-637eb2293923 // indirect diff --git a/go.sum b/go.sum index 487e6bd83e..532a2fc2b0 100644 --- a/go.sum +++ b/go.sum @@ -652,9 +652,8 @@ golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5y golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20211202192323-5770296d904e/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.10.0 h1:LKqV2xt9+kDzSTfOhx4FrkEBcMrAgHSYgzywV9zcGmM= -golang.org/x/crypto v0.10.0/go.mod h1:o4eNf7Ede1fv+hwOwZsTHl9EsPFO6q6ZvYR8vYfY45I= +golang.org/x/crypto v0.11.0 h1:6Ewdq3tDic1mg5xRO4milcWCfMVQhI4NkqWWvqejpuA= +golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -769,12 +768,12 @@ golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.9.0 h1:KS/R3tvhPqvJvwcKfnBHJwwthS11LRhmM5D59eEXa0s= -golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +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.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.9.0 h1:GRRCnKYhdQrD8kfRAdQ6Zcw1P0OcELxGLKJvtjVMZ28= +golang.org/x/term v0.10.0 h1:3R7pNqamzBraeqj/Tj8qt1aQ2HpmlC+Cx/qL/7hn4/c= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= @@ -783,8 +782,8 @@ golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.10.0 h1:UpjohKhiEgNc0CSauXmwYftY1+LlaC75SJwh0SgCX58= -golang.org/x/text v0.10.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.11.0 h1:LAntKIrcmeSKERyiOh0XMV39LXS8IE9UL2yP7+f5ij4= +golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20201208040808-7e3f01d25324/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -915,5 +914,5 @@ honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWh honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= -software.sslmate.com/src/go-pkcs12 v0.2.0 h1:nlFkj7bTysH6VkC4fGphtjXRbezREPgrHuJG20hBGPE= -software.sslmate.com/src/go-pkcs12 v0.2.0/go.mod h1:23rNcYsMabIc1otwLpTkCCPwUq6kQsTyowttG/as0kQ= +software.sslmate.com/src/go-pkcs12 v0.4.0 h1:H2g08FrTvSFKUj+D309j1DPfk5APnIdAQAB8aEykJ5k= +software.sslmate.com/src/go-pkcs12 v0.4.0/go.mod h1:Qiz0EyvDRJjjxGyUQa2cCNZn/wMyzrRJ/qcDXOQazLI=