Skip to content

Commit 4eab81a

Browse files
pschouldez
andauthored
feat(cli): add format option for PFX encoding (#2063)
Co-authored-by: Fernandez Ludovic <[email protected]>
1 parent 9c1a856 commit 4eab81a

File tree

5 files changed

+84
-30
lines changed

5 files changed

+84
-30
lines changed

cmd/certs_storage.go

Lines changed: 55 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,10 @@ package cmd
33
import (
44
"bytes"
55
"crypto"
6-
"crypto/rand"
76
"crypto/x509"
87
"encoding/json"
98
"encoding/pem"
9+
"errors"
1010
"fmt"
1111
"os"
1212
"path/filepath"
@@ -55,17 +55,27 @@ type CertificatesStorage struct {
5555
pem bool
5656
pfx bool
5757
pfxPassword string
58+
pfxFormat string
5859
filename string // Deprecated
5960
}
6061

6162
// NewCertificatesStorage create a new certificates storage.
6263
func NewCertificatesStorage(ctx *cli.Context) *CertificatesStorage {
64+
pfxFormat := ctx.String("pfx.format")
65+
66+
switch pfxFormat {
67+
case "DES", "RC2", "SHA256":
68+
default:
69+
log.Fatalf("Invalid PFX format: %s", pfxFormat)
70+
}
71+
6372
return &CertificatesStorage{
6473
rootPath: filepath.Join(ctx.String("path"), baseCertificatesFolderName),
6574
archivePath: filepath.Join(ctx.String("path"), baseArchivesFolderName),
6675
pem: ctx.Bool("pem"),
6776
pfx: ctx.Bool("pfx"),
6877
pfxPassword: ctx.String("pfx.pass"),
78+
pfxFormat: pfxFormat,
6979
filename: ctx.String("filename"),
7080
}
7181
}
@@ -218,14 +228,9 @@ func (s *CertificatesStorage) WritePFXFile(domain string, certRes *certificate.R
218228
return fmt.Errorf("unable to load Certificate for domain %s: %w", domain, err)
219229
}
220230

221-
issuerCertPemBlock, _ := pem.Decode(certRes.IssuerCertificate)
222-
if issuerCertPemBlock == nil {
223-
return fmt.Errorf("unable to parse Issuer Certificate for domain %s", domain)
224-
}
225-
226-
issuerCert, err := x509.ParseCertificate(issuerCertPemBlock.Bytes)
231+
certChain, err := getCertificateChain(certRes)
227232
if err != nil {
228-
return fmt.Errorf("unable to load Issuer Certificate for domain %s: %w", domain, err)
233+
return fmt.Errorf("unable to get certificate chain for domain %s: %w", domain, err)
229234
}
230235

231236
keyPemBlock, _ := pem.Decode(certRes.PrivateKey)
@@ -251,7 +256,12 @@ func (s *CertificatesStorage) WritePFXFile(domain string, certRes *certificate.R
251256
return fmt.Errorf("unsupported PrivateKey type '%s' for domain %s", keyPemBlock.Type, domain)
252257
}
253258

254-
pfxBytes, err := pkcs12.Encode(rand.Reader, privateKey, cert, []*x509.Certificate{issuerCert}, s.pfxPassword)
259+
encoder, err := getPFXEncoder(s.pfxFormat)
260+
if err != nil {
261+
return fmt.Errorf("PFX encoder: %w", err)
262+
}
263+
264+
pfxBytes, err := encoder.Encode(privateKey, cert, certChain, s.pfxPassword)
255265
if err != nil {
256266
return fmt.Errorf("unable to encode PFX data for domain %s: %w", domain, err)
257267
}
@@ -285,6 +295,42 @@ func (s *CertificatesStorage) MoveToArchive(domain string) error {
285295
return nil
286296
}
287297

298+
func getCertificateChain(certRes *certificate.Resource) ([]*x509.Certificate, error) {
299+
chainCertPemBlock, rest := pem.Decode(certRes.IssuerCertificate)
300+
if chainCertPemBlock == nil {
301+
return nil, errors.New("unable to parse Issuer Certificate")
302+
}
303+
304+
var certChain []*x509.Certificate
305+
for chainCertPemBlock != nil {
306+
chainCert, err := x509.ParseCertificate(chainCertPemBlock.Bytes)
307+
if err != nil {
308+
return nil, fmt.Errorf("unable to parse Chain Certificate: %w", err)
309+
}
310+
311+
certChain = append(certChain, chainCert)
312+
chainCertPemBlock, rest = pem.Decode(rest) // Try decoding the next pem block
313+
}
314+
315+
return certChain, nil
316+
}
317+
318+
func getPFXEncoder(pfxFormat string) (*pkcs12.Encoder, error) {
319+
var encoder *pkcs12.Encoder
320+
switch pfxFormat {
321+
case "SHA256":
322+
encoder = pkcs12.Modern2023
323+
case "DES":
324+
encoder = pkcs12.LegacyDES
325+
case "RC2":
326+
encoder = pkcs12.LegacyRC2
327+
default:
328+
return nil, fmt.Errorf("invalid PFX format: %s", pfxFormat)
329+
}
330+
331+
return encoder, nil
332+
}
333+
288334
// sanitizedDomain Make sure no funny chars are in the cert names (like wildcards ;)).
289335
func sanitizedDomain(domain string) string {
290336
safe, err := idna.ToASCII(strings.NewReplacer(":", "-", "*", "_").Replace(domain))

cmd/flags.go

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -133,13 +133,21 @@ func CreateFlags(defaultPath string) []cli.Flag {
133133
Usage: "Generate an additional .pem (base64) file by concatenating the .key and .crt files together.",
134134
},
135135
&cli.BoolFlag{
136-
Name: "pfx",
137-
Usage: "Generate an additional .pfx (PKCS#12) file by concatenating the .key and .crt and issuer .crt files together.",
136+
Name: "pfx",
137+
Usage: "Generate an additional .pfx (PKCS#12) file by concatenating the .key and .crt and issuer .crt files together.",
138+
EnvVars: []string{"LEGO_PFX"},
138139
},
139140
&cli.StringFlag{
140-
Name: "pfx.pass",
141-
Usage: "The password used to encrypt the .pfx (PCKS#12) file.",
142-
Value: pkcs12.DefaultPassword,
141+
Name: "pfx.pass",
142+
Usage: "The password used to encrypt the .pfx (PCKS#12) file.",
143+
Value: pkcs12.DefaultPassword,
144+
EnvVars: []string{"LEGO_PFX_PASSWORD"},
145+
},
146+
&cli.StringFlag{
147+
Name: "pfx.format",
148+
Usage: "The encoding format to use when encrypting the .pfx (PCKS#12) file. Supported: RC2, DES, SHA256.",
149+
Value: "RC2",
150+
EnvVars: []string{"LEGO_PFX_FORMAT"},
143151
},
144152
&cli.IntFlag{
145153
Name: "cert.timeout",

docs/data/zz_cli_help.toml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,9 @@ GLOBAL OPTIONS:
4444
--http-timeout value Set the HTTP timeout value to a specific value in seconds. (default: 0)
4545
--dns-timeout value Set the DNS timeout value to a specific value in seconds. Used only when performing authoritative name server queries. (default: 10)
4646
--pem Generate an additional .pem (base64) file by concatenating the .key and .crt files together. (default: false)
47-
--pfx Generate an additional .pfx (PKCS#12) file by concatenating the .key and .crt and issuer .crt files together. (default: false)
48-
--pfx.pass value The password used to encrypt the .pfx (PCKS#12) file. (default: "changeit")
47+
--pfx Generate an additional .pfx (PKCS#12) file by concatenating the .key and .crt and issuer .crt files together. (default: false) [$LEGO_PFX]
48+
--pfx.pass value The password used to encrypt the .pfx (PCKS#12) file. (default: "changeit") [$LEGO_PFX_PASSWORD]
49+
--pfx.format value The encoding format to use when encrypting the .pfx (PCKS#12) file. Supported: RC2, DES, SHA256. (default: "RC2") [$LEGO_PFX_FORMAT]
4950
--cert.timeout value Set the certificate timeout value to a specific value in seconds. Only used when obtaining certificates. (default: 30)
5051
--user-agent value Add to the user-agent sent to the CA to identify an application embedding lego-cli
5152
--help, -h show help

go.mod

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -74,14 +74,14 @@ require (
7474
github.com/vultr/govultr/v2 v2.17.2
7575
github.com/yandex-cloud/go-genproto v0.0.0-20220805142335-27b56ddae16f
7676
github.com/yandex-cloud/go-sdk v0.0.0-20220805164847-cf028e604997
77-
golang.org/x/crypto v0.10.0
77+
golang.org/x/crypto v0.11.0
7878
golang.org/x/net v0.11.0
7979
golang.org/x/oauth2 v0.9.0
8080
golang.org/x/time v0.3.0
8181
google.golang.org/api v0.111.0
8282
gopkg.in/ns1/ns1-go.v2 v2.7.6
8383
gopkg.in/yaml.v2 v2.4.0
84-
software.sslmate.com/src/go-pkcs12 v0.2.0
84+
software.sslmate.com/src/go-pkcs12 v0.4.0
8585
)
8686

8787
require (
@@ -155,8 +155,8 @@ require (
155155
go.opencensus.io v0.24.0 // indirect
156156
go.uber.org/ratelimit v0.2.0 // indirect
157157
golang.org/x/mod v0.11.0 // indirect
158-
golang.org/x/sys v0.9.0 // indirect
159-
golang.org/x/text v0.10.0 // indirect
158+
golang.org/x/sys v0.10.0 // indirect
159+
golang.org/x/text v0.11.0 // indirect
160160
golang.org/x/tools v0.10.0 // indirect
161161
google.golang.org/appengine v1.6.7 // indirect
162162
google.golang.org/genproto v0.0.0-20230223222841-637eb2293923 // indirect

go.sum

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -652,9 +652,8 @@ golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5y
652652
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
653653
golang.org/x/crypto v0.0.0-20211202192323-5770296d904e/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
654654
golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
655-
golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
656-
golang.org/x/crypto v0.10.0 h1:LKqV2xt9+kDzSTfOhx4FrkEBcMrAgHSYgzywV9zcGmM=
657-
golang.org/x/crypto v0.10.0/go.mod h1:o4eNf7Ede1fv+hwOwZsTHl9EsPFO6q6ZvYR8vYfY45I=
655+
golang.org/x/crypto v0.11.0 h1:6Ewdq3tDic1mg5xRO4milcWCfMVQhI4NkqWWvqejpuA=
656+
golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio=
658657
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
659658
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
660659
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
769768
golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
770769
golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
771770
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
772-
golang.org/x/sys v0.9.0 h1:KS/R3tvhPqvJvwcKfnBHJwwthS11LRhmM5D59eEXa0s=
773-
golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
771+
golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA=
772+
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
774773
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
775774
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
776775
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
777-
golang.org/x/term v0.9.0 h1:GRRCnKYhdQrD8kfRAdQ6Zcw1P0OcELxGLKJvtjVMZ28=
776+
golang.org/x/term v0.10.0 h1:3R7pNqamzBraeqj/Tj8qt1aQ2HpmlC+Cx/qL/7hn4/c=
778777
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
779778
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
780779
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=
783782
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
784783
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
785784
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
786-
golang.org/x/text v0.10.0 h1:UpjohKhiEgNc0CSauXmwYftY1+LlaC75SJwh0SgCX58=
787-
golang.org/x/text v0.10.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
785+
golang.org/x/text v0.11.0 h1:LAntKIrcmeSKERyiOh0XMV39LXS8IE9UL2yP7+f5ij4=
786+
golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
788787
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
789788
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
790789
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
915914
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
916915
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
917916
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
918-
software.sslmate.com/src/go-pkcs12 v0.2.0 h1:nlFkj7bTysH6VkC4fGphtjXRbezREPgrHuJG20hBGPE=
919-
software.sslmate.com/src/go-pkcs12 v0.2.0/go.mod h1:23rNcYsMabIc1otwLpTkCCPwUq6kQsTyowttG/as0kQ=
917+
software.sslmate.com/src/go-pkcs12 v0.4.0 h1:H2g08FrTvSFKUj+D309j1DPfk5APnIdAQAB8aEykJ5k=
918+
software.sslmate.com/src/go-pkcs12 v0.4.0/go.mod h1:Qiz0EyvDRJjjxGyUQa2cCNZn/wMyzrRJ/qcDXOQazLI=

0 commit comments

Comments
 (0)