Skip to content

Commit

Permalink
feat: support custom duration for certificate (#1925)
Browse files Browse the repository at this point in the history
  • Loading branch information
ldez committed May 28, 2023
1 parent 8bf0cee commit c341e6a
Show file tree
Hide file tree
Showing 8 changed files with 268 additions and 40 deletions.
22 changes: 22 additions & 0 deletions acme/api/order.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,26 @@ import (
"encoding/base64"
"errors"
"net"
"time"

"github.com/go-acme/lego/v4/acme"
)

// OrderOptions used to create an order (optional).
type OrderOptions struct {
NotBefore time.Time
NotAfter time.Time
}

type OrderService service

// New Creates a new order.
func (o *OrderService) New(domains []string) (acme.ExtendedOrder, error) {
return o.NewWithOptions(domains, nil)
}

// NewWithOptions Creates a new order.
func (o *OrderService) NewWithOptions(domains []string, opts *OrderOptions) (acme.ExtendedOrder, error) {
var identifiers []acme.Identifier
for _, domain := range domains {
ident := acme.Identifier{Value: domain, Type: "dns"}
Expand All @@ -25,6 +37,16 @@ func (o *OrderService) New(domains []string) (acme.ExtendedOrder, error) {

orderReq := acme.Order{Identifiers: identifiers}

if opts != nil {
if !opts.NotAfter.IsZero() {
orderReq.NotAfter = opts.NotAfter.Format(time.RFC3339)
}

if !opts.NotBefore.IsZero() {
orderReq.NotBefore = opts.NotBefore.Format(time.RFC3339)
}
}

var order acme.Order
resp, err := o.core.post(o.core.GetDirectory().NewOrderURL, orderReq, &order)
if err != nil {
Expand Down
62 changes: 51 additions & 11 deletions acme/api/order_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"io"
"net/http"
"testing"
"time"

"github.com/go-acme/lego/v4/acme"
"github.com/go-acme/lego/v4/platform/tester"
Expand All @@ -15,7 +16,7 @@ import (
"github.com/stretchr/testify/require"
)

func TestOrderService_New(t *testing.T) {
func TestOrderService_NewWithOptions(t *testing.T) {
mux, apiURL := tester.SetupFakeAPI(t)

// small value keeps test fast
Expand All @@ -42,8 +43,15 @@ func TestOrderService_New(t *testing.T) {
}

err = tester.WriteJSONResponse(w, acme.Order{
Status: acme.StatusValid,
Identifiers: order.Identifiers,
Status: acme.StatusValid,
Expires: order.Expires,
Identifiers: order.Identifiers,
NotBefore: order.NotBefore,
NotAfter: order.NotAfter,
Error: order.Error,
Authorizations: order.Authorizations,
Finalize: order.Finalize,
Certificate: order.Certificate,
})
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
Expand All @@ -54,16 +62,48 @@ func TestOrderService_New(t *testing.T) {
core, err := New(http.DefaultClient, "lego-test", apiURL+"/dir", "", privateKey)
require.NoError(t, err)

order, err := core.Orders.New([]string{"example.com"})
require.NoError(t, err)

expected := acme.ExtendedOrder{
Order: acme.Order{
Status: "valid",
Identifiers: []acme.Identifier{{Type: "dns", Value: "example.com"}},
testCases := []struct {
desc string
opts *OrderOptions
expected acme.ExtendedOrder
}{
{
desc: "simple",
expected: acme.ExtendedOrder{
Order: acme.Order{
Status: "valid",
Identifiers: []acme.Identifier{{Type: "dns", Value: "example.com"}},
},
},
},
{
desc: "with options",
opts: &OrderOptions{
NotBefore: time.Date(2023, 1, 1, 1, 0, 0, 0, time.UTC),
NotAfter: time.Date(2023, 1, 2, 1, 0, 0, 0, time.UTC),
},
expected: acme.ExtendedOrder{
Order: acme.Order{
Status: "valid",
Identifiers: []acme.Identifier{{Type: "dns", Value: "example.com"}},
NotBefore: "2023-01-01T01:00:00Z",
NotAfter: "2023-01-02T01:00:00Z",
},
},
},
}

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

order, err := core.Orders.NewWithOptions([]string{"example.com"}, test.opts)
require.NoError(t, err)

assert.Equal(t, test.expected, order)
})
}
assert.Equal(t, expected, order)
}

func readSignedBody(r *http.Request, privateKey *rsa.PrivateKey) ([]byte, error) {
Expand Down
96 changes: 78 additions & 18 deletions certificate/certificates.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,10 +54,13 @@ 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
Domains []string
PrivateKey crypto.PrivateKey
MustStaple bool

NotBefore time.Time
NotAfter time.Time
Bundle bool
PrivateKey crypto.PrivateKey
MustStaple bool
PreferredChain string
AlwaysDeactivateAuthorizations bool
}
Expand All @@ -69,7 +72,10 @@ type ObtainRequest 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 ObtainForCSRRequest struct {
CSR *x509.CertificateRequest
CSR *x509.CertificateRequest

NotBefore time.Time
NotAfter time.Time
Bundle bool
PreferredChain string
AlwaysDeactivateAuthorizations bool
Expand Down Expand Up @@ -117,7 +123,12 @@ func (c *Certifier) Obtain(request ObtainRequest) (*Resource, error) {
log.Infof("[%s] acme: Obtaining SAN certificate", strings.Join(domains, ", "))
}

order, err := c.core.Orders.New(domains)
orderOpts := &api.OrderOptions{
NotBefore: request.NotBefore,
NotAfter: request.NotAfter,
}

order, err := c.core.Orders.NewWithOptions(domains, orderOpts)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -182,7 +193,12 @@ func (c *Certifier) ObtainForCSR(request ObtainForCSRRequest) (*Resource, error)
log.Infof("[%s] acme: Obtaining SAN certificate given a CSR", strings.Join(domains, ", "))
}

order, err := c.core.Orders.New(domains)
orderOpts := &api.OrderOptions{
NotBefore: request.NotBefore,
NotAfter: request.NotAfter,
}

order, err := c.core.Orders.NewWithOptions(domains, orderOpts)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -388,6 +404,18 @@ func (c *Certifier) RevokeWithReason(cert []byte, reason *uint) error {
return c.core.Certificates.Revoke(revokeMsg)
}

// RenewOptions options used by Certifier.RenewWithOptions.
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
AlwaysDeactivateAuthorizations bool
// Not supported for CSR request.
MustStaple bool
}

// Renew takes a Resource and tries to renew the certificate.
//
// If the renewal process succeeds, the new certificate will be returned in a new CertResource.
Expand All @@ -398,7 +426,26 @@ func (c *Certifier) RevokeWithReason(cert []byte, reason *uint) error {
// If bundle is true, the []byte contains both the issuer certificate and your issued certificate as a bundle.
//
// For private key reuse the PrivateKey property of the passed in Resource should be non-nil.
// Deprecated: use RenewWithOptions instead.
func (c *Certifier) Renew(certRes Resource, bundle, mustStaple bool, preferredChain string) (*Resource, error) {
return c.RenewWithOptions(certRes, &RenewOptions{
Bundle: bundle,
PreferredChain: preferredChain,
MustStaple: mustStaple,
})
}

// RenewWithOptions takes a Resource and tries to renew the certificate.
//
// If the renewal process succeeds, the new certificate will be returned in a new CertResource.
// Please be aware that this function will return a new certificate in ANY case that is not an error.
// If the server does not provide us with a new cert on a GET request to the CertURL
// this function will start a new-cert flow where a new certificate gets generated.
//
// If bundle is true, the []byte contains both the issuer certificate and your issued certificate as a bundle.
//
// For private key reuse the PrivateKey property of the passed in Resource should be non-nil.
func (c *Certifier) RenewWithOptions(certRes Resource, options *RenewOptions) (*Resource, error) {
// Input certificate is PEM encoded.
// Decode it here as we may need the decoded cert later on in the renewal process.
// The input may be a bundle or a single certificate.
Expand All @@ -425,11 +472,17 @@ func (c *Certifier) Renew(certRes Resource, bundle, mustStaple bool, preferredCh
return nil, errP
}

return c.ObtainForCSR(ObtainForCSRRequest{
CSR: csr,
Bundle: bundle,
PreferredChain: preferredChain,
})
request := ObtainForCSRRequest{CSR: csr}

if options != nil {
request.NotBefore = options.NotBefore
request.NotAfter = options.NotAfter
request.Bundle = options.Bundle
request.PreferredChain = options.PreferredChain
request.AlwaysDeactivateAuthorizations = options.AlwaysDeactivateAuthorizations
}

return c.ObtainForCSR(request)
}

var privateKey crypto.PrivateKey
Expand All @@ -440,14 +493,21 @@ func (c *Certifier) Renew(certRes Resource, bundle, mustStaple bool, preferredCh
}
}

query := ObtainRequest{
Domains: certcrypto.ExtractDomains(x509Cert),
Bundle: bundle,
PrivateKey: privateKey,
MustStaple: mustStaple,
PreferredChain: preferredChain,
request := ObtainRequest{
Domains: certcrypto.ExtractDomains(x509Cert),
PrivateKey: privateKey,
}

if options != nil {
request.MustStaple = options.MustStaple
request.NotBefore = options.NotBefore
request.NotAfter = options.NotAfter
request.Bundle = options.Bundle
request.PreferredChain = options.PreferredChain
request.AlwaysDeactivateAuthorizations = options.AlwaysDeactivateAuthorizations
}
return c.Obtain(query)

return c.Obtain(request)
}

// GetOCSP takes a PEM encoded cert or cert bundle returning the raw OCSP response,
Expand Down
29 changes: 23 additions & 6 deletions cmd/cmd_renew.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,9 +75,15 @@ func createRenew() *cli.Command {
Usage: "Include the OCSP must staple TLS extension in the CSR and generated certificate." +
" Only works if the CSR is generated by lego.",
},
&cli.StringFlag{
Name: "renew-hook",
Usage: "Define a hook. The hook is executed only when the certificates are effectively renewed.",
&cli.TimestampFlag{
Name: "not-before",
Usage: "Set the notBefore field in the certificate (RFC3339 format)",
Layout: time.RFC3339,
},
&cli.TimestampFlag{
Name: "not-after",
Usage: "Set the notAfter field in the certificate (RFC3339 format)",
Layout: time.RFC3339,
},
&cli.StringFlag{
Name: "preferred-chain",
Expand All @@ -88,6 +94,10 @@ func createRenew() *cli.Command {
Name: "always-deactivate-authorizations",
Usage: "Force the authorizations to be relinquished even if the certificate request was successful.",
},
&cli.StringFlag{
Name: "renew-hook",
Usage: "Define a hook. The hook is executed only when the certificates are effectively renewed.",
},
&cli.BoolFlag{
Name: "no-random-sleep",
Usage: "Do not add a random sleep before the renewal." +
Expand Down Expand Up @@ -188,12 +198,15 @@ func renewForDomains(ctx *cli.Context, client *lego.Client, certsStorage *Certif

request := certificate.ObtainRequest{
Domains: merge(certDomains, domains),
Bundle: bundle,
PrivateKey: privateKey,
MustStaple: ctx.Bool("must-staple"),
NotBefore: getTime(ctx, "not-before"),
NotAfter: getTime(ctx, "not-after"),
Bundle: bundle,
PreferredChain: ctx.String("preferred-chain"),
AlwaysDeactivateAuthorizations: ctx.Bool("always-deactivate-authorizations"),
}

certRes, err := client.Certificate.Obtain(request)
if err != nil {
log.Fatal(err)
Expand Down Expand Up @@ -265,12 +278,16 @@ func renewForCSR(ctx *cli.Context, client *lego.Client, certsStorage *Certificat
timeLeft := cert.NotAfter.Sub(time.Now().UTC())
log.Infof("[%s] acme: Trying renewal with %d hours remaining", domain, int(timeLeft.Hours()))

certRes, err := client.Certificate.ObtainForCSR(certificate.ObtainForCSRRequest{
request := certificate.ObtainForCSRRequest{
CSR: csr,
NotBefore: getTime(ctx, "not-before"),
NotAfter: getTime(ctx, "not-after"),
Bundle: bundle,
PreferredChain: ctx.String("preferred-chain"),
AlwaysDeactivateAuthorizations: ctx.Bool("always-deactivate-authorizations"),
})
}

certRes, err := client.Certificate.ObtainForCSR(request)
if err != nil {
log.Fatal(err)
}
Expand Down
Loading

0 comments on commit c341e6a

Please sign in to comment.