Skip to content

Commit

Permalink
feat: option to set CSR emails (#2423)
Browse files Browse the repository at this point in the history
  • Loading branch information
ldez authored Feb 6, 2025
1 parent e644196 commit 4349dfc
Show file tree
Hide file tree
Showing 4 changed files with 131 additions and 42 deletions.
27 changes: 22 additions & 5 deletions certcrypto/crypto.go
Original file line number Diff line number Diff line change
Expand Up @@ -135,10 +135,26 @@ func GeneratePrivateKey(keyType KeyType) (crypto.PrivateKey, error) {
return nil, fmt.Errorf("invalid KeyType: %s", keyType)
}

// Deprecated: uses [CreateCSR] instead.
func GenerateCSR(privateKey crypto.PrivateKey, domain string, san []string, mustStaple bool) ([]byte, error) {
return CreateCSR(privateKey, CSROptions{
Domain: domain,
SAN: san,
MustStaple: mustStaple,
})
}

type CSROptions struct {
Domain string
SAN []string
MustStaple bool
EmailAddresses []string
}

func CreateCSR(privateKey crypto.PrivateKey, opts CSROptions) ([]byte, error) {
var dnsNames []string
var ipAddresses []net.IP
for _, altname := range san {
for _, altname := range opts.SAN {
if ip := net.ParseIP(altname); ip != nil {
ipAddresses = append(ipAddresses, ip)
} else {
Expand All @@ -147,12 +163,13 @@ func GenerateCSR(privateKey crypto.PrivateKey, domain string, san []string, must
}

template := x509.CertificateRequest{
Subject: pkix.Name{CommonName: domain},
DNSNames: dnsNames,
IPAddresses: ipAddresses,
Subject: pkix.Name{CommonName: opts.Domain},
DNSNames: dnsNames,
EmailAddresses: opts.EmailAddresses,
IPAddresses: ipAddresses,
}

if mustStaple {
if opts.MustStaple {
template.ExtraExtensions = append(template.ExtraExtensions, pkix.Extension{
Id: tlsFeatureExtensionOID,
Value: ocspMustStapleFeature,
Expand Down
70 changes: 45 additions & 25 deletions certcrypto/crypto_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,63 +33,83 @@ func TestGenerateCSR(t *testing.T) {
testCases := []struct {
desc string
privateKey crypto.PrivateKey
domain string
san []string
mustStaple bool
opts CSROptions
expected expected
}{
{
desc: "without SAN (nil)",
privateKey: privateKey,
domain: "lego.acme",
mustStaple: true,
expected: expected{len: 245},
opts: CSROptions{
Domain: "lego.acme",
MustStaple: true,
},
expected: expected{len: 245},
},
{
desc: "without SAN (empty)",
privateKey: privateKey,
domain: "lego.acme",
san: []string{},
mustStaple: true,
expected: expected{len: 245},
opts: CSROptions{
Domain: "lego.acme",
SAN: []string{},
MustStaple: true,
},
expected: expected{len: 245},
},
{
desc: "with SAN",
privateKey: privateKey,
domain: "lego.acme",
san: []string{"a.lego.acme", "b.lego.acme", "c.lego.acme"},
mustStaple: true,
expected: expected{len: 296},
opts: CSROptions{
Domain: "lego.acme",
SAN: []string{"a.lego.acme", "b.lego.acme", "c.lego.acme"},
MustStaple: true,
},
expected: expected{len: 296},
},
{
desc: "no domain",
privateKey: privateKey,
domain: "",
mustStaple: true,
expected: expected{len: 225},
opts: CSROptions{
Domain: "",
MustStaple: true,
},
expected: expected{len: 225},
},
{
desc: "no domain with SAN",
privateKey: privateKey,
domain: "",
san: []string{"a.lego.acme", "b.lego.acme", "c.lego.acme"},
mustStaple: true,
expected: expected{len: 276},
opts: CSROptions{
Domain: "",
SAN: []string{"a.lego.acme", "b.lego.acme", "c.lego.acme"},
MustStaple: true,
},
expected: expected{len: 276},
},
{
desc: "private key nil",
privateKey: nil,
domain: "fizz.buzz",
mustStaple: true,
expected: expected{error: true},
opts: CSROptions{
Domain: "fizz.buzz",
MustStaple: true,
},
expected: expected{error: true},
},
{
desc: "with email addresses",
privateKey: privateKey,
opts: CSROptions{
Domain: "example.com",
SAN: []string{"example.org"},
EmailAddresses: []string{"[email protected]", "[email protected]"},
},
expected: expected{len: 287},
},
}

for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
t.Parallel()

csr, err := GenerateCSR(test.privateKey, test.domain, test.san, test.mustStaple)
csr, err := CreateCSR(test.privateKey, test.opts)

if test.expected.error {
require.Error(t, err)
Expand Down
37 changes: 25 additions & 12 deletions certificate/certificates.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,9 +65,10 @@ type Resource struct {
// If `AlwaysDeactivateAuthorizations` is true, the authorizations are also relinquished if the obtain request was successful.
// See https://datatracker.ietf.org/doc/html/rfc8555#section-7.5.2.
type ObtainRequest struct {
Domains []string
PrivateKey crypto.PrivateKey
MustStaple bool
Domains []string
PrivateKey crypto.PrivateKey
MustStaple bool
EmailAddresses []string

NotBefore time.Time
NotAfter time.Time
Expand Down Expand Up @@ -194,7 +195,7 @@ func (c *Certifier) Obtain(request ObtainRequest) (*Resource, error) {
log.Infof("[%s] acme: Validations succeeded; requesting certificates", strings.Join(domains, ", "))

failures := newObtainError()
cert, err := c.getForOrder(domains, order, request.Bundle, request.PrivateKey, request.MustStaple, request.PreferredChain)
cert, err := c.getForOrder(domains, order, request)
if err != nil {
for _, auth := range authz {
failures.Add(challenge.GetTargetedDomain(auth), err)
Expand Down Expand Up @@ -280,7 +281,9 @@ func (c *Certifier) ObtainForCSR(request ObtainForCSRRequest) (*Resource, error)
return cert, failures.Join()
}

func (c *Certifier) getForOrder(domains []string, order acme.ExtendedOrder, bundle bool, privateKey crypto.PrivateKey, mustStaple bool, preferredChain string) (*Resource, error) {
func (c *Certifier) getForOrder(domains []string, order acme.ExtendedOrder, request ObtainRequest) (*Resource, error) {
privateKey := request.PrivateKey

if privateKey == nil {
var err error
privateKey, err = certcrypto.GeneratePrivateKey(c.options.KeyType)
Expand Down Expand Up @@ -312,13 +315,19 @@ func (c *Certifier) getForOrder(domains []string, order acme.ExtendedOrder, bund
}
}

// TODO: should the CSR be customizable?
csr, err := certcrypto.GenerateCSR(privateKey, commonName, san, mustStaple)
csrOptions := certcrypto.CSROptions{
Domain: commonName,
SAN: san,
MustStaple: request.MustStaple,
EmailAddresses: request.EmailAddresses,
}

csr, err := certcrypto.CreateCSR(privateKey, csrOptions)
if err != nil {
return nil, err
}

return c.getForCSR(domains, order, bundle, csr, certcrypto.PEMEncode(privateKey), preferredChain)
return c.getForCSR(domains, order, request.Bundle, csr, certcrypto.PEMEncode(privateKey), request.PreferredChain)
}

func (c *Certifier) getForCSR(domains []string, order acme.ExtendedOrder, bundle bool, csr, privateKeyPem []byte, preferredChain string) (*Resource, error) {
Expand Down Expand Up @@ -451,12 +460,15 @@ type RenewOptions struct {
NotBefore time.Time
NotAfter time.Time
// If true, the []byte contains both the issuer certificate and your issued certificate as a bundle.
Bundle bool
PreferredChain string
Profile string
Bundle bool
PreferredChain string

Profile string

AlwaysDeactivateAuthorizations bool
// Not supported for CSR request.
MustStaple bool
MustStaple bool
EmailAddresses []string
}

// Renew takes a Resource and tries to renew the certificate.
Expand Down Expand Up @@ -548,6 +560,7 @@ func (c *Certifier) RenewWithOptions(certRes Resource, options *RenewOptions) (*
request.NotAfter = options.NotAfter
request.Bundle = options.Bundle
request.PreferredChain = options.PreferredChain
request.EmailAddresses = options.EmailAddresses
request.Profile = options.Profile
request.AlwaysDeactivateAuthorizations = options.AlwaysDeactivateAuthorizations
}
Expand Down
39 changes: 39 additions & 0 deletions e2e/challenges_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,45 @@ func TestChallengeHTTP_Client_Obtain_profile(t *testing.T) {
assert.Empty(t, resource.CSR)
}

func TestChallengeHTTP_Client_Obtain_emails_csr(t *testing.T) {
err := os.Setenv("LEGO_CA_CERTIFICATES", "./fixtures/certs/pebble.minica.pem")
require.NoError(t, err)
defer func() { _ = os.Unsetenv("LEGO_CA_CERTIFICATES") }()

privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
require.NoError(t, err, "Could not generate test key")

user := &fakeUser{privateKey: privateKey}
config := lego.NewConfig(user)
config.CADirURL = load.PebbleOptions.HealthCheckURL

client, err := lego.NewClient(config)
require.NoError(t, err)

err = client.Challenge.SetHTTP01Provider(http01.NewProviderServer("", "5002"))
require.NoError(t, err)

reg, err := client.Registration.Register(registration.RegisterOptions{TermsOfServiceAgreed: true})
require.NoError(t, err)
user.registration = reg

request := certificate.ObtainRequest{
Domains: []string{"acme.wtf"},
Bundle: true,
EmailAddresses: []string{"[email protected]"},
}
resource, err := client.Certificate.Obtain(request)
require.NoError(t, err)

require.NotNil(t, resource)
assert.Equal(t, "acme.wtf", resource.Domain)
assert.Regexp(t, `https://localhost:14000/certZ/[\w\d]{14,}`, resource.CertURL)
assert.Regexp(t, `https://localhost:14000/certZ/[\w\d]{14,}`, resource.CertStableURL)
assert.NotEmpty(t, resource.Certificate)
assert.NotEmpty(t, resource.IssuerCertificate)
assert.Empty(t, resource.CSR)
}

func TestChallengeHTTP_Client_Obtain_notBefore_notAfter(t *testing.T) {
err := os.Setenv("LEGO_CA_CERTIFICATES", "./fixtures/certs/pebble.minica.pem")
require.NoError(t, err)
Expand Down

0 comments on commit 4349dfc

Please sign in to comment.