Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: option to set CSR emails #2423

Merged
merged 1 commit into from
Feb 6, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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