feat: implement 'replaces' field in newOrder and draft-ietf-acme-ari-03 CertID changes (#2114)
This commit is contained in:
parent
adea063bb1
commit
6dd8d035d1
8 changed files with 79 additions and 182 deletions
|
@ -226,6 +226,6 @@ issues:
|
||||||
- path: providers/dns/hosttech/internal/client_test.go
|
- path: providers/dns/hosttech/internal/client_test.go
|
||||||
text: 'Duplicate words \(0\) found'
|
text: 'Duplicate words \(0\) found'
|
||||||
- path: cmd/cmd_renew.go
|
- path: cmd/cmd_renew.go
|
||||||
text: 'cyclomatic complexity 15 of func `renewForDomains` is high'
|
text: 'cyclomatic complexity \d+ of func `renewForDomains` is high'
|
||||||
- path: providers/dns/cpanel/cpanel.go
|
- path: providers/dns/cpanel/cpanel.go
|
||||||
text: 'cyclomatic complexity 13 of func `\(\*DNSProvider\)\.CleanUp` is high'
|
text: 'cyclomatic complexity 13 of func `\(\*DNSProvider\)\.CleanUp` is high'
|
||||||
|
|
|
@ -16,7 +16,7 @@ Let's Encrypt client and ACME library written in Go.
|
||||||
- ACME v2 [RFC 8555](https://www.rfc-editor.org/rfc/rfc8555.html)
|
- ACME v2 [RFC 8555](https://www.rfc-editor.org/rfc/rfc8555.html)
|
||||||
- Support [RFC 8737](https://www.rfc-editor.org/rfc/rfc8737.html): TLS Application‑Layer Protocol Negotiation (ALPN) Challenge Extension
|
- Support [RFC 8737](https://www.rfc-editor.org/rfc/rfc8737.html): TLS Application‑Layer Protocol Negotiation (ALPN) Challenge Extension
|
||||||
- Support [RFC 8738](https://www.rfc-editor.org/rfc/rfc8738.html): certificates for IP addresses
|
- Support [RFC 8738](https://www.rfc-editor.org/rfc/rfc8738.html): certificates for IP addresses
|
||||||
- Support [draft-ietf-acme-ari-02](https://datatracker.ietf.org/doc/draft-ietf-acme-ari/): Renewal Information (ARI) Extension
|
- Support [draft-ietf-acme-ari-03](https://datatracker.ietf.org/doc/draft-ietf-acme-ari/): Renewal Information (ARI) Extension
|
||||||
- Register with CA
|
- Register with CA
|
||||||
- Obtain certificates, both from scratch or with an existing CSR
|
- Obtain certificates, both from scratch or with an existing CSR
|
||||||
- Renew certificates
|
- Renew certificates
|
||||||
|
|
|
@ -13,6 +13,10 @@ import (
|
||||||
type OrderOptions struct {
|
type OrderOptions struct {
|
||||||
NotBefore time.Time
|
NotBefore time.Time
|
||||||
NotAfter time.Time
|
NotAfter time.Time
|
||||||
|
// A string uniquely identifying a previously-issued certificate which this
|
||||||
|
// order is intended to replace.
|
||||||
|
// - https://datatracker.ietf.org/doc/html/draft-ietf-acme-ari-03#section-5
|
||||||
|
ReplacesCertID string
|
||||||
}
|
}
|
||||||
|
|
||||||
type OrderService service
|
type OrderService service
|
||||||
|
@ -45,6 +49,10 @@ func (o *OrderService) NewWithOptions(domains []string, opts *OrderOptions) (acm
|
||||||
if !opts.NotBefore.IsZero() {
|
if !opts.NotBefore.IsZero() {
|
||||||
orderReq.NotBefore = opts.NotBefore.Format(time.RFC3339)
|
orderReq.NotBefore = opts.NotBefore.Format(time.RFC3339)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if o.core.GetDirectory().RenewalInfo != "" {
|
||||||
|
orderReq.Replaces = opts.ReplacesCertID
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var order acme.Order
|
var order acme.Order
|
||||||
|
|
|
@ -181,6 +181,12 @@ type Order struct {
|
||||||
// certificate (optional, string):
|
// certificate (optional, string):
|
||||||
// A URL for the certificate that has been issued in response to this order
|
// A URL for the certificate that has been issued in response to this order
|
||||||
Certificate string `json:"certificate,omitempty"`
|
Certificate string `json:"certificate,omitempty"`
|
||||||
|
|
||||||
|
// replaces (optional, string):
|
||||||
|
// replaces (string, optional): A string uniquely identifying a
|
||||||
|
// previously-issued certificate which this order is intended to replace.
|
||||||
|
// - https://datatracker.ietf.org/doc/html/draft-ietf-acme-ari-03#section-5
|
||||||
|
Replaces string `json:"replaces,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Authorization the ACME authorization object.
|
// Authorization the ACME authorization object.
|
||||||
|
@ -329,11 +335,11 @@ type RenewalInfoResponse struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
// RenewalInfoUpdateRequest is the JWS payload for POST requests made to the renewalInfo endpoint.
|
// RenewalInfoUpdateRequest is the JWS payload for POST requests made to the renewalInfo endpoint.
|
||||||
// - (4.2. RenewalInfo Objects) https://datatracker.ietf.org/doc/html/draft-ietf-acme-ari-02#section-4.2
|
// - (4.2. RenewalInfo Objects) https://datatracker.ietf.org/doc/html/draft-ietf-acme-ari-03#section-4.2
|
||||||
type RenewalInfoUpdateRequest struct {
|
type RenewalInfoUpdateRequest struct {
|
||||||
// CertID is a composite string in the format: base64url(AKI) || '.' || base64url(Serial), where AKI is the
|
// CertID is a composite string in the format: base64url(AKI) || '.' || base64url(Serial), where AKI is the
|
||||||
// certificate's authority key identifier and Serial is the certificate's serial number. For details, see:
|
// certificate's authority key identifier and Serial is the certificate's serial number. For details, see:
|
||||||
// https://datatracker.ietf.org/doc/html/draft-ietf-acme-ari-02#section-4.1
|
// https://datatracker.ietf.org/doc/html/draft-ietf-acme-ari-03#section-4.1
|
||||||
CertID string `json:"certID"`
|
CertID string `json:"certID"`
|
||||||
// Replaced is required and indicates whether or not the client considers the certificate to have been replaced.
|
// Replaced is required and indicates whether or not the client considers the certificate to have been replaced.
|
||||||
// A certificate is considered replaced when its revocation would not disrupt any ongoing services,
|
// A certificate is considered replaced when its revocation would not disrupt any ongoing services,
|
||||||
|
|
|
@ -63,6 +63,10 @@ type ObtainRequest struct {
|
||||||
Bundle bool
|
Bundle bool
|
||||||
PreferredChain string
|
PreferredChain string
|
||||||
AlwaysDeactivateAuthorizations bool
|
AlwaysDeactivateAuthorizations bool
|
||||||
|
// A string uniquely identifying a previously-issued certificate which this
|
||||||
|
// order is intended to replace.
|
||||||
|
// - https://datatracker.ietf.org/doc/html/draft-ietf-acme-ari-03#section-5
|
||||||
|
ReplacesCertID string
|
||||||
}
|
}
|
||||||
|
|
||||||
// ObtainForCSRRequest The request to obtain a certificate matching the CSR passed into it.
|
// ObtainForCSRRequest The request to obtain a certificate matching the CSR passed into it.
|
||||||
|
@ -79,6 +83,10 @@ type ObtainForCSRRequest struct {
|
||||||
Bundle bool
|
Bundle bool
|
||||||
PreferredChain string
|
PreferredChain string
|
||||||
AlwaysDeactivateAuthorizations bool
|
AlwaysDeactivateAuthorizations bool
|
||||||
|
// A string uniquely identifying a previously-issued certificate which this
|
||||||
|
// order is intended to replace.
|
||||||
|
// - https://datatracker.ietf.org/doc/html/draft-ietf-acme-ari-03#section-5
|
||||||
|
ReplacesCertID string
|
||||||
}
|
}
|
||||||
|
|
||||||
type resolver interface {
|
type resolver interface {
|
||||||
|
@ -124,8 +132,9 @@ func (c *Certifier) Obtain(request ObtainRequest) (*Resource, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
orderOpts := &api.OrderOptions{
|
orderOpts := &api.OrderOptions{
|
||||||
NotBefore: request.NotBefore,
|
NotBefore: request.NotBefore,
|
||||||
NotAfter: request.NotAfter,
|
NotAfter: request.NotAfter,
|
||||||
|
ReplacesCertID: request.ReplacesCertID,
|
||||||
}
|
}
|
||||||
|
|
||||||
order, err := c.core.Orders.NewWithOptions(domains, orderOpts)
|
order, err := c.core.Orders.NewWithOptions(domains, orderOpts)
|
||||||
|
@ -189,8 +198,9 @@ func (c *Certifier) ObtainForCSR(request ObtainForCSRRequest) (*Resource, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
orderOpts := &api.OrderOptions{
|
orderOpts := &api.OrderOptions{
|
||||||
NotBefore: request.NotBefore,
|
NotBefore: request.NotBefore,
|
||||||
NotAfter: request.NotAfter,
|
NotAfter: request.NotAfter,
|
||||||
|
ReplacesCertID: request.ReplacesCertID,
|
||||||
}
|
}
|
||||||
|
|
||||||
order, err := c.core.Orders.NewWithOptions(domains, orderOpts)
|
order, err := c.core.Orders.NewWithOptions(domains, orderOpts)
|
||||||
|
|
|
@ -2,12 +2,12 @@ package certificate
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
|
"encoding/asn1"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"strings"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/go-acme/lego/v4/acme"
|
"github.com/go-acme/lego/v4/acme"
|
||||||
|
@ -65,7 +65,7 @@ func (r *RenewalInfoResponse) ShouldRenewAt(now time.Time, willingToSleep time.D
|
||||||
//
|
//
|
||||||
// https://datatracker.ietf.org/doc/draft-ietf-acme-ari
|
// https://datatracker.ietf.org/doc/draft-ietf-acme-ari
|
||||||
func (c *Certifier) GetRenewalInfo(req RenewalInfoRequest) (*RenewalInfoResponse, error) {
|
func (c *Certifier) GetRenewalInfo(req RenewalInfoRequest) (*RenewalInfoResponse, error) {
|
||||||
certID, err := makeARICertID(req.Cert)
|
certID, err := MakeARICertID(req.Cert)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("error making certID: %w", err)
|
return nil, fmt.Errorf("error making certID: %w", err)
|
||||||
}
|
}
|
||||||
|
@ -84,39 +84,32 @@ func (c *Certifier) GetRenewalInfo(req RenewalInfoRequest) (*RenewalInfoResponse
|
||||||
return &info, nil
|
return &info, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateRenewalInfo sends an update to the ACME server's renewal info endpoint to indicate that the client has successfully replaced a certificate.
|
// MakeARICertID constructs a certificate identifier as described in draft-ietf-acme-ari-03, section 4.1.
|
||||||
// A certificate is considered replaced when its revocation would not disrupt any ongoing services,
|
func MakeARICertID(leaf *x509.Certificate) (string, error) {
|
||||||
// for instance because it has been renewed and the new certificate is in use, or because it is no longer in use.
|
|
||||||
//
|
|
||||||
// Note: this endpoint is part of a draft specification, not all ACME servers will implement it.
|
|
||||||
// This method will return api.ErrNoARI if the server does not advertise a renewal info endpoint.
|
|
||||||
//
|
|
||||||
// https://datatracker.ietf.org/doc/draft-ietf-acme-ari
|
|
||||||
func (c *Certifier) UpdateRenewalInfo(req RenewalInfoRequest) error {
|
|
||||||
certID, err := makeARICertID(req.Cert)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("error making certID: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = c.core.Certificates.UpdateRenewalInfo(acme.RenewalInfoUpdateRequest{
|
|
||||||
CertID: certID,
|
|
||||||
Replaced: true,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// makeARICertID constructs a certificate identifier as described in draft-ietf-acme-ari-02, section 4.1.
|
|
||||||
func makeARICertID(leaf *x509.Certificate) (string, error) {
|
|
||||||
if leaf == nil {
|
if leaf == nil {
|
||||||
return "", errors.New("leaf certificate is nil")
|
return "", errors.New("leaf certificate is nil")
|
||||||
}
|
}
|
||||||
|
|
||||||
return fmt.Sprintf("%s.%s",
|
// Marshal the Serial Number into DER.
|
||||||
strings.TrimRight(base64.URLEncoding.EncodeToString(leaf.AuthorityKeyId), "="),
|
der, err := asn1.Marshal(leaf.SerialNumber)
|
||||||
strings.TrimRight(base64.URLEncoding.EncodeToString(leaf.SerialNumber.Bytes()), "="),
|
if err != nil {
|
||||||
), nil
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the DER encoded bytes are sufficient (at least 3 bytes: tag,
|
||||||
|
// length, and value).
|
||||||
|
if len(der) < 3 {
|
||||||
|
return "", errors.New("invalid DER encoding of serial number")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract only the integer bytes from the DER encoded Serial Number
|
||||||
|
// Skipping the first 2 bytes (tag and length).
|
||||||
|
serial := base64.RawURLEncoding.EncodeToString(der[2:])
|
||||||
|
|
||||||
|
// Convert the Authority Key Identifier to base64url encoding without
|
||||||
|
// padding.
|
||||||
|
aki := base64.RawURLEncoding.EncodeToString(leaf.AuthorityKeyId)
|
||||||
|
|
||||||
|
// Construct the final identifier by concatenating AKI and Serial Number.
|
||||||
|
return fmt.Sprintf("%s.%s", aki, serial), nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,8 +3,6 @@ package certificate
|
||||||
import (
|
import (
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"crypto/rsa"
|
"crypto/rsa"
|
||||||
"encoding/json"
|
|
||||||
"io"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
@ -13,40 +11,28 @@ import (
|
||||||
"github.com/go-acme/lego/v4/acme/api"
|
"github.com/go-acme/lego/v4/acme/api"
|
||||||
"github.com/go-acme/lego/v4/certcrypto"
|
"github.com/go-acme/lego/v4/certcrypto"
|
||||||
"github.com/go-acme/lego/v4/platform/tester"
|
"github.com/go-acme/lego/v4/platform/tester"
|
||||||
"github.com/go-jose/go-jose/v4"
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
ariLeafPEM = `-----BEGIN CERTIFICATE-----
|
ariLeafPEM = `-----BEGIN CERTIFICATE-----
|
||||||
MIIDMDCCAhigAwIBAgIIPqNFaGVEHxwwDQYJKoZIhvcNAQELBQAwIDEeMBwGA1UE
|
MIIBQzCB66ADAgECAgUAh2VDITAKBggqhkjOPQQDAjAVMRMwEQYDVQQDEwpFeGFt
|
||||||
AxMVbWluaWNhIHJvb3QgY2EgM2ExMzU2MB4XDTIyMDMxNzE3NTEwOVoXDTI0MDQx
|
cGxlIENBMCIYDzAwMDEwMTAxMDAwMDAwWhgPMDAwMTAxMDEwMDAwMDBaMBYxFDAS
|
||||||
NjE3NTEwOVowFjEUMBIGA1UEAxMLZXhhbXBsZS5jb20wggEiMA0GCSqGSIb3DQEB
|
BgNVBAMTC2V4YW1wbGUuY29tMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEeBZu
|
||||||
AQUAA4IBDwAwggEKAoIBAQCgm9K/c+il2Pf0f8qhgxn9SKqXq88cOm9ov9AVRbPA
|
7cbpAYNXZLbbh8rNIzuOoqOOtmxA1v7cRm//AwyMwWxyHz4zfwmBhcSrf47NUAFf
|
||||||
OWAAewqX2yUAwI4LZBGEgzGzTATkiXfoJ3cN3k39cH6tBbb3iSPuEn7OZpIk9D+e
|
qzLQ2PPQxdTXREYEnKMjMCEwHwYDVR0jBBgwFoAUaYhba4dGQEHhs3uEe6CuLN4B
|
||||||
3Q9/hX+N/jlWkaTB/FNA+7aE5IVWhmdczYilXa10V9r+RcvACJt0gsipBZVJ4jfJ
|
yNQwCgYIKoZIzj0EAwIDRwAwRAIge09+S5TZAlw5tgtiVvuERV6cT4mfutXIlwTb
|
||||||
HnWJJGRZzzxqG/xkQmpXxZO7nOPFc8SxYKWdfcgp+rjR2ogYhSz7BfKoVakGPbpX
|
+FYN/8oCIClDsqBklhB9KAelFiYt9+6FDj3z4KGVelYM5MdsO3pK
|
||||||
vZOuT9z4kkHra/WjwlkQhtHoTXdAxH3qC2UjMzO57Tx+otj0CxAv9O7CTJXISywB
|
|
||||||
vEVcmTSZkHS3eZtvvIwPx7I30ITRkYk/tLl1MbyB3SiZAgMBAAGjeDB2MA4GA1Ud
|
|
||||||
DwEB/wQEAwIFoDAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwDAYDVR0T
|
|
||||||
AQH/BAIwADAfBgNVHSMEGDAWgBQ4zzDRUaXHVKqlSTWkULGU4zGZpTAWBgNVHREE
|
|
||||||
DzANggtleGFtcGxlLmNvbTANBgkqhkiG9w0BAQsFAAOCAQEAx0aYvmCk7JYGNEXe
|
|
||||||
+hrOfKawkHYzWvA92cI/Oi6h+oSdHZ2UKzwFNf37cVKZ37FCrrv5pFP/xhhHvrNV
|
|
||||||
EnOx4IaF7OrnaTu5miZiUWuvRQP7ZGmGNFYbLTEF6/dj+WqyYdVaWzxRqHFu1ptC
|
|
||||||
TXysJCeyiGnR+KOOjOOQ9ZlO5JUK3OE4hagPLfaIpDDy6RXQt3ss0iNLuB1+IOtp
|
|
||||||
1URpvffLZQ8xPsEgOZyPWOcabTwJrtqBwily+lwPFn2mChUx846LwQfxtsXU/lJg
|
|
||||||
HX2RteNJx7YYNeX3Uf960mgo5an6vE8QNAsIoNHYrGyEmXDhTRe9mCHyiW2S7fZq
|
|
||||||
o9q12g==
|
|
||||||
-----END CERTIFICATE-----`
|
-----END CERTIFICATE-----`
|
||||||
ariLeafCertID = "OM8w0VGlx1SqpUk1pFCxlOMxmaU.PqNFaGVEHxw"
|
ariLeafCertID = "aYhba4dGQEHhs3uEe6CuLN4ByNQ.AIdlQyE"
|
||||||
)
|
)
|
||||||
|
|
||||||
func Test_makeCertID(t *testing.T) {
|
func Test_makeCertID(t *testing.T) {
|
||||||
leaf, err := certcrypto.ParsePEMCertificate([]byte(ariLeafPEM))
|
leaf, err := certcrypto.ParsePEMCertificate([]byte(ariLeafPEM))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
actual, err := makeARICertID(leaf)
|
actual, err := MakeARICertID(leaf)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Equal(t, ariLeafCertID, actual)
|
assert.Equal(t, ariLeafCertID, actual)
|
||||||
}
|
}
|
||||||
|
@ -145,85 +131,6 @@ func TestCertifier_GetRenewalInfo_errors(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCertifier_UpdateRenewalInfo(t *testing.T) {
|
|
||||||
leaf, err := certcrypto.ParsePEMCertificate([]byte(ariLeafPEM))
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
key, err := rsa.GenerateKey(rand.Reader, 2048)
|
|
||||||
require.NoError(t, err, "Could not generate test key")
|
|
||||||
|
|
||||||
// Test with a fake API.
|
|
||||||
mux, apiURL := tester.SetupFakeAPI(t)
|
|
||||||
mux.HandleFunc("/renewalInfo", func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
if r.Method != http.MethodPost {
|
|
||||||
http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
body, rsbErr := readSignedBody(r, key)
|
|
||||||
if rsbErr != nil {
|
|
||||||
http.Error(w, rsbErr.Error(), http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var req acme.RenewalInfoUpdateRequest
|
|
||||||
err = json.Unmarshal(body, &req)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
assert.True(t, req.Replaced)
|
|
||||||
assert.Equal(t, ariLeafCertID, req.CertID)
|
|
||||||
|
|
||||||
w.WriteHeader(http.StatusOK)
|
|
||||||
})
|
|
||||||
|
|
||||||
core, err := api.New(http.DefaultClient, "lego-test", apiURL+"/dir", "", key)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
certifier := NewCertifier(core, &resolverMock{}, CertifierOptions{KeyType: certcrypto.RSA2048})
|
|
||||||
|
|
||||||
err = certifier.UpdateRenewalInfo(RenewalInfoRequest{leaf})
|
|
||||||
require.NoError(t, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCertifier_UpdateRenewalInfo_errors(t *testing.T) {
|
|
||||||
leaf, err := certcrypto.ParsePEMCertificate([]byte(ariLeafPEM))
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
key, err := rsa.GenerateKey(rand.Reader, 2048)
|
|
||||||
require.NoError(t, err, "Could not generate test key")
|
|
||||||
|
|
||||||
testCases := []struct {
|
|
||||||
desc string
|
|
||||||
request RenewalInfoRequest
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
desc: "API error",
|
|
||||||
request: RenewalInfoRequest{leaf},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, test := range testCases {
|
|
||||||
test := test
|
|
||||||
t.Run(test.desc, func(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
mux, apiURL := tester.SetupFakeAPI(t)
|
|
||||||
|
|
||||||
// Always returns an error.
|
|
||||||
mux.HandleFunc("/renewalInfo", func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
|
|
||||||
})
|
|
||||||
|
|
||||||
core, err := api.New(http.DefaultClient, "lego-test", apiURL+"/dir", "", key)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
certifier := NewCertifier(core, &resolverMock{}, CertifierOptions{KeyType: certcrypto.RSA2048})
|
|
||||||
|
|
||||||
err = certifier.UpdateRenewalInfo(test.request)
|
|
||||||
require.Error(t, err)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRenewalInfoResponse_ShouldRenew(t *testing.T) {
|
func TestRenewalInfoResponse_ShouldRenew(t *testing.T) {
|
||||||
now := time.Now().UTC()
|
now := time.Now().UTC()
|
||||||
|
|
||||||
|
@ -289,26 +196,3 @@ func TestRenewalInfoResponse_ShouldRenew(t *testing.T) {
|
||||||
assert.Nil(t, rt)
|
assert.Nil(t, rt)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func readSignedBody(r *http.Request, privateKey *rsa.PrivateKey) ([]byte, error) {
|
|
||||||
reqBody, err := io.ReadAll(r.Body)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
sigAlgs := []jose.SignatureAlgorithm{jose.RS256}
|
|
||||||
jws, err := jose.ParseSigned(string(reqBody), sigAlgs)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
body, err := jws.Verify(&jose.JSONWebKey{
|
|
||||||
Key: privateKey.Public(),
|
|
||||||
Algorithm: "RSA",
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return body, nil
|
|
||||||
}
|
|
||||||
|
|
|
@ -187,6 +187,11 @@ func renewForDomains(ctx *cli.Context, client *lego.Client, certsStorage *Certif
|
||||||
time.Sleep(sleepTime)
|
time.Sleep(sleepTime)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
replacesCertID, err := certificate.MakeARICertID(cert)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Error while construction the ARI CertID for domain %s\n\t%v", domain, err)
|
||||||
|
}
|
||||||
|
|
||||||
request := certificate.ObtainRequest{
|
request := certificate.ObtainRequest{
|
||||||
Domains: merge(certDomains, domains),
|
Domains: merge(certDomains, domains),
|
||||||
PrivateKey: privateKey,
|
PrivateKey: privateKey,
|
||||||
|
@ -196,6 +201,7 @@ func renewForDomains(ctx *cli.Context, client *lego.Client, certsStorage *Certif
|
||||||
Bundle: bundle,
|
Bundle: bundle,
|
||||||
PreferredChain: ctx.String("preferred-chain"),
|
PreferredChain: ctx.String("preferred-chain"),
|
||||||
AlwaysDeactivateAuthorizations: ctx.Bool("always-deactivate-authorizations"),
|
AlwaysDeactivateAuthorizations: ctx.Bool("always-deactivate-authorizations"),
|
||||||
|
ReplacesCertID: replacesCertID,
|
||||||
}
|
}
|
||||||
|
|
||||||
certRes, err := client.Certificate.Obtain(request)
|
certRes, err := client.Certificate.Obtain(request)
|
||||||
|
@ -205,14 +211,6 @@ func renewForDomains(ctx *cli.Context, client *lego.Client, certsStorage *Certif
|
||||||
|
|
||||||
certsStorage.SaveResource(certRes)
|
certsStorage.SaveResource(certRes)
|
||||||
|
|
||||||
if ariRenewalTime != nil {
|
|
||||||
// Post to the renewalInfo endpoint to indicate that we have renewed and replaced the certificate.
|
|
||||||
err := client.Certificate.UpdateRenewalInfo(certificate.RenewalInfoRequest{Cert: certificates[0]})
|
|
||||||
if err != nil {
|
|
||||||
log.Warnf("[%s] Failed to update renewal info: %v", domain, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
meta[renewEnvCertDomain] = domain
|
meta[renewEnvCertDomain] = domain
|
||||||
meta[renewEnvCertPath] = certsStorage.GetFileName(domain, ".crt")
|
meta[renewEnvCertPath] = certsStorage.GetFileName(domain, ".crt")
|
||||||
meta[renewEnvCertKeyPath] = certsStorage.GetFileName(domain, ".key")
|
meta[renewEnvCertKeyPath] = certsStorage.GetFileName(domain, ".key")
|
||||||
|
@ -264,6 +262,11 @@ func renewForCSR(ctx *cli.Context, client *lego.Client, certsStorage *Certificat
|
||||||
timeLeft := cert.NotAfter.Sub(time.Now().UTC())
|
timeLeft := cert.NotAfter.Sub(time.Now().UTC())
|
||||||
log.Infof("[%s] acme: Trying renewal with %d hours remaining", domain, int(timeLeft.Hours()))
|
log.Infof("[%s] acme: Trying renewal with %d hours remaining", domain, int(timeLeft.Hours()))
|
||||||
|
|
||||||
|
replacesCertID, err := certificate.MakeARICertID(cert)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Error while construction the ARI CertID for domain %s\n\t%v", domain, err)
|
||||||
|
}
|
||||||
|
|
||||||
request := certificate.ObtainForCSRRequest{
|
request := certificate.ObtainForCSRRequest{
|
||||||
CSR: csr,
|
CSR: csr,
|
||||||
NotBefore: getTime(ctx, "not-before"),
|
NotBefore: getTime(ctx, "not-before"),
|
||||||
|
@ -271,6 +274,7 @@ func renewForCSR(ctx *cli.Context, client *lego.Client, certsStorage *Certificat
|
||||||
Bundle: bundle,
|
Bundle: bundle,
|
||||||
PreferredChain: ctx.String("preferred-chain"),
|
PreferredChain: ctx.String("preferred-chain"),
|
||||||
AlwaysDeactivateAuthorizations: ctx.Bool("always-deactivate-authorizations"),
|
AlwaysDeactivateAuthorizations: ctx.Bool("always-deactivate-authorizations"),
|
||||||
|
ReplacesCertID: replacesCertID,
|
||||||
}
|
}
|
||||||
|
|
||||||
certRes, err := client.Certificate.ObtainForCSR(request)
|
certRes, err := client.Certificate.ObtainForCSR(request)
|
||||||
|
@ -280,14 +284,6 @@ func renewForCSR(ctx *cli.Context, client *lego.Client, certsStorage *Certificat
|
||||||
|
|
||||||
certsStorage.SaveResource(certRes)
|
certsStorage.SaveResource(certRes)
|
||||||
|
|
||||||
if ariRenewalTime != nil {
|
|
||||||
// Post to the renewalInfo endpoint to indicate that we have renewed and replaced the certificate.
|
|
||||||
err := client.Certificate.UpdateRenewalInfo(certificate.RenewalInfoRequest{Cert: certificates[0]})
|
|
||||||
if err != nil {
|
|
||||||
log.Warnf("[%s] Failed to update renewal info: %v", domain, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
meta[renewEnvCertDomain] = domain
|
meta[renewEnvCertDomain] = domain
|
||||||
meta[renewEnvCertPath] = certsStorage.GetFileName(domain, ".crt")
|
meta[renewEnvCertPath] = certsStorage.GetFileName(domain, ".crt")
|
||||||
meta[renewEnvCertKeyPath] = certsStorage.GetFileName(domain, ".key")
|
meta[renewEnvCertKeyPath] = certsStorage.GetFileName(domain, ".key")
|
||||||
|
|
Loading…
Reference in a new issue