feat: update CertID format as per draft-ietf-acme-ari-02 (#2066)
This commit is contained in:
parent
bb830677d1
commit
3c73f624ba
7 changed files with 33 additions and 182 deletions
|
@ -236,4 +236,4 @@ 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 16 of func `renewForDomains` is high'
|
text: 'cyclomatic complexity 15 of func `renewForDomains` 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-01](https://datatracker.ietf.org/doc/draft-ietf-acme-ari/): Renewal Information (ARI) Extension
|
- Support [draft-ietf-acme-ari-02](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
|
||||||
|
|
|
@ -329,9 +329,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. Updating Renewal Information) https://datatracker.ietf.org/doc/draft-ietf-acme-ari/
|
// - (4.2. RenewalInfo Objects) https://datatracker.ietf.org/doc/html/draft-ietf-acme-ari-02#section-4.2
|
||||||
type RenewalInfoUpdateRequest struct {
|
type RenewalInfoUpdateRequest struct {
|
||||||
// CertID is the base64url-encoded [RFC4648] bytes of a DER-encoded CertID ASN.1 sequence [RFC6960] with any trailing '=' characters stripped.
|
// 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:
|
||||||
|
// https://datatracker.ietf.org/doc/html/draft-ietf-acme-ari-02#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,
|
||||||
|
|
|
@ -1,14 +1,11 @@
|
||||||
package certificate
|
package certificate
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto"
|
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
"crypto/x509/pkix"
|
|
||||||
"encoding/asn1"
|
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"math/big"
|
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
@ -19,10 +16,6 @@ import (
|
||||||
// RenewalInfoRequest contains the necessary renewal information.
|
// RenewalInfoRequest contains the necessary renewal information.
|
||||||
type RenewalInfoRequest struct {
|
type RenewalInfoRequest struct {
|
||||||
Cert *x509.Certificate
|
Cert *x509.Certificate
|
||||||
Issuer *x509.Certificate
|
|
||||||
// HashName must be the string representation of a crypto.Hash constant in the golang.org/x/crypto package (e.g. "SHA-256").
|
|
||||||
// The correct value depends on the algorithm expected by the ACME server's ARI implementation.
|
|
||||||
HashName string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// RenewalInfoResponse is a wrapper around acme.RenewalInfoResponse that provides a method for determining when to renew a certificate.
|
// RenewalInfoResponse is a wrapper around acme.RenewalInfoResponse that provides a method for determining when to renew a certificate.
|
||||||
|
@ -72,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 := makeCertID(req.Cert, req.Issuer, req.HashName)
|
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)
|
||||||
}
|
}
|
||||||
|
@ -100,7 +93,7 @@ func (c *Certifier) GetRenewalInfo(req RenewalInfoRequest) (*RenewalInfoResponse
|
||||||
//
|
//
|
||||||
// https://datatracker.ietf.org/doc/draft-ietf-acme-ari
|
// https://datatracker.ietf.org/doc/draft-ietf-acme-ari
|
||||||
func (c *Certifier) UpdateRenewalInfo(req RenewalInfoRequest) error {
|
func (c *Certifier) UpdateRenewalInfo(req RenewalInfoRequest) error {
|
||||||
certID, err := makeCertID(req.Cert, req.Issuer, req.HashName)
|
certID, err := makeARICertID(req.Cert)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("error making certID: %w", err)
|
return fmt.Errorf("error making certID: %w", err)
|
||||||
}
|
}
|
||||||
|
@ -116,89 +109,14 @@ func (c *Certifier) UpdateRenewalInfo(req RenewalInfoRequest) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// makeCertID returns a base64url-encoded string that uniquely identifies a certificate to endpoints
|
// makeARICertID constructs a certificate identifier as described in draft-ietf-acme-ari-02, section 4.1.
|
||||||
// that implement the draft-ietf-acme-ari specification: https://datatracker.ietf.org/doc/draft-ietf-acme-ari.
|
func makeARICertID(leaf *x509.Certificate) (string, error) {
|
||||||
// hashName must be the string representation of a crypto.Hash constant in the golang.org/x/crypto package.
|
|
||||||
// Supported hash functions are SHA-1, SHA-256, SHA-384, and SHA-512.
|
|
||||||
func makeCertID(leaf, issuer *x509.Certificate, hashName string) (string, error) {
|
|
||||||
if leaf == nil {
|
if leaf == nil {
|
||||||
return "", fmt.Errorf("leaf certificate is nil")
|
return "", errors.New("leaf certificate is nil")
|
||||||
}
|
|
||||||
if issuer == nil {
|
|
||||||
return "", fmt.Errorf("issuer certificate is nil")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var hashFunc crypto.Hash
|
return fmt.Sprintf("%s.%s",
|
||||||
var oid asn1.ObjectIdentifier
|
strings.TrimRight(base64.URLEncoding.EncodeToString(leaf.AuthorityKeyId), "="),
|
||||||
|
strings.TrimRight(base64.URLEncoding.EncodeToString(leaf.SerialNumber.Bytes()), "="),
|
||||||
switch hashName {
|
), nil
|
||||||
// The following correlation of hashFunc to OID is copied from a private mapping in golang.org/x/crypto/ocsp:
|
|
||||||
// https://cs.opensource.google/go/x/crypto/+/refs/tags/v0.8.0:ocsp/ocsp.go;l=156
|
|
||||||
case crypto.SHA1.String():
|
|
||||||
hashFunc = crypto.SHA1
|
|
||||||
oid = asn1.ObjectIdentifier([]int{1, 3, 14, 3, 2, 26})
|
|
||||||
|
|
||||||
case crypto.SHA256.String():
|
|
||||||
hashFunc = crypto.SHA256
|
|
||||||
oid = asn1.ObjectIdentifier([]int{2, 16, 840, 1, 101, 3, 4, 2, 1})
|
|
||||||
|
|
||||||
case crypto.SHA384.String():
|
|
||||||
hashFunc = crypto.SHA384
|
|
||||||
oid = asn1.ObjectIdentifier([]int{2, 16, 840, 1, 101, 3, 4, 2, 2})
|
|
||||||
|
|
||||||
case crypto.SHA512.String():
|
|
||||||
hashFunc = crypto.SHA512
|
|
||||||
oid = asn1.ObjectIdentifier([]int{2, 16, 840, 1, 101, 3, 4, 2, 3})
|
|
||||||
|
|
||||||
default:
|
|
||||||
return "", fmt.Errorf("hashName %q is not supported by this package", hashName)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !hashFunc.Available() {
|
|
||||||
// This should never happen.
|
|
||||||
return "", fmt.Errorf("hash function %q is not available on your platform", hashFunc)
|
|
||||||
}
|
|
||||||
|
|
||||||
var spki struct {
|
|
||||||
Algorithm pkix.AlgorithmIdentifier
|
|
||||||
PublicKey asn1.BitString
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err := asn1.Unmarshal(issuer.RawSubjectPublicKeyInfo, &spki)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
h := hashFunc.New()
|
|
||||||
h.Write(spki.PublicKey.RightAlign())
|
|
||||||
issuerKeyHash := h.Sum(nil)
|
|
||||||
|
|
||||||
h.Reset()
|
|
||||||
h.Write(issuer.RawSubject)
|
|
||||||
issuerNameHash := h.Sum(nil)
|
|
||||||
|
|
||||||
type certID struct {
|
|
||||||
HashAlgorithm pkix.AlgorithmIdentifier
|
|
||||||
IssuerNameHash []byte
|
|
||||||
IssuerKeyHash []byte
|
|
||||||
SerialNumber *big.Int
|
|
||||||
}
|
|
||||||
|
|
||||||
// DER-encode the CertID ASN.1 sequence [RFC6960].
|
|
||||||
certIDBytes, err := asn1.Marshal(certID{
|
|
||||||
HashAlgorithm: pkix.AlgorithmIdentifier{
|
|
||||||
Algorithm: oid,
|
|
||||||
},
|
|
||||||
IssuerNameHash: issuerNameHash,
|
|
||||||
IssuerKeyHash: issuerKeyHash,
|
|
||||||
SerialNumber: leaf.SerialNumber,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
// base64url-encode [RFC4648] the bytes of the DER-encoded CertID ASN.1 sequence [RFC6960].
|
|
||||||
encodedBytes := base64.URLEncoding.EncodeToString(certIDBytes)
|
|
||||||
|
|
||||||
// Any trailing '=' characters MUST be stripped.
|
|
||||||
return strings.TrimRight(encodedBytes, "="), nil
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
package certificate
|
package certificate
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto"
|
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"crypto/rsa"
|
"crypto/rsa"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
@ -40,36 +39,14 @@ TXysJCeyiGnR+KOOjOOQ9ZlO5JUK3OE4hagPLfaIpDDy6RXQt3ss0iNLuB1+IOtp
|
||||||
HX2RteNJx7YYNeX3Uf960mgo5an6vE8QNAsIoNHYrGyEmXDhTRe9mCHyiW2S7fZq
|
HX2RteNJx7YYNeX3Uf960mgo5an6vE8QNAsIoNHYrGyEmXDhTRe9mCHyiW2S7fZq
|
||||||
o9q12g==
|
o9q12g==
|
||||||
-----END CERTIFICATE-----`
|
-----END CERTIFICATE-----`
|
||||||
ariIssuerPEM = `-----BEGIN CERTIFICATE-----
|
ariLeafCertID = "OM8w0VGlx1SqpUk1pFCxlOMxmaU.PqNFaGVEHxw"
|
||||||
MIIDSzCCAjOgAwIBAgIIOhNWtJ7Igr0wDQYJKoZIhvcNAQELBQAwIDEeMBwGA1UE
|
|
||||||
AxMVbWluaWNhIHJvb3QgY2EgM2ExMzU2MCAXDTIyMDMxNzE3NTEwOVoYDzIxMjIw
|
|
||||||
MzE3MTc1MTA5WjAgMR4wHAYDVQQDExVtaW5pY2Egcm9vdCBjYSAzYTEzNTYwggEi
|
|
||||||
MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDc3P6cxcCZ7FQOQrYuigReSa8T
|
|
||||||
IOPNKmlmX9OrTkPwjThiMNEETYKO1ea99yXPK36LUHC6OLmZ9jVQW2Ny1qwQCOy6
|
|
||||||
TrquhnwKgtkBMDAZBLySSEXYdKL3r0jA4sflW130/OLwhstU/yv0J8+pj7eSVOR3
|
|
||||||
zJBnYd1AqnXHRSwQm299KXgqema7uwsa8cgjrXsBzAhrwrvYlVhpWFSv3lQRDFQg
|
|
||||||
c5Z/ZDV9i26qiaJsCCmdisJZWN7N2luUgxdRqzZ4Cr2Xoilg3T+hkb2y/d6ttsPA
|
|
||||||
kaSA+pq3q6Qa7/qfGdT5WuUkcHpvKNRWqnwT9rCYlmG00r3hGgc42D/z1VvfAgMB
|
|
||||||
AAGjgYYwgYMwDgYDVR0PAQH/BAQDAgKEMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggr
|
|
||||||
BgEFBQcDAjASBgNVHRMBAf8ECDAGAQH/AgEAMB0GA1UdDgQWBBQ4zzDRUaXHVKql
|
|
||||||
STWkULGU4zGZpTAfBgNVHSMEGDAWgBQ4zzDRUaXHVKqlSTWkULGU4zGZpTANBgkq
|
|
||||||
hkiG9w0BAQsFAAOCAQEArbDHhEjGedjb/YjU80aFTPWOMRjgyfQaPPgyxwX6Dsid
|
|
||||||
1i2H1x4ud4ntz3sTZZxdQIrOqtlIWTWVCjpStwGxaC+38SdreiTTwy/nikXGa/6W
|
|
||||||
ZyQRppR3agh/pl5LHVO6GsJz3YHa7wQhEhj3xsRwa9VrRXgHbLGbPOFVRTHPjaPg
|
|
||||||
Gtsv2PN3f67DsPHF47ASqyOIRpLZPQmZIw6D3isJwfl+8CzvlB1veO0Q3uh08IJc
|
|
||||||
fspYQXvFBzYa64uKxNAJMi4Pby8cf4r36Wnb7cL4ho3fOHgAltxdW8jgibRzqZpQ
|
|
||||||
QKyxn2jX7kxeUDt0hFDJE8lOrhP73m66eBNzxe//FQ==
|
|
||||||
-----END CERTIFICATE-----`
|
|
||||||
ariLeafCertID = "MFswCwYJYIZIAWUDBAIBBCCeWLRusNLb--vmWOkxm34qDjTMWkc3utIhOMoMwKDqbgQg2iiKWySZrD-6c88HMZ6vhIHZPamChLlzGHeZ7pTS8jYCCD6jRWhlRB8c"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
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)
|
||||||
issuer, err := certcrypto.ParsePEMCertificate([]byte(ariIssuerPEM))
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
actual, err := makeCertID(leaf, issuer, crypto.SHA256.String())
|
actual, err := makeARICertID(leaf)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Equal(t, ariLeafCertID, actual)
|
assert.Equal(t, ariLeafCertID, actual)
|
||||||
}
|
}
|
||||||
|
@ -77,8 +54,6 @@ func Test_makeCertID(t *testing.T) {
|
||||||
func TestCertifier_GetRenewalInfo(t *testing.T) {
|
func TestCertifier_GetRenewalInfo(t *testing.T) {
|
||||||
leaf, err := certcrypto.ParsePEMCertificate([]byte(ariLeafPEM))
|
leaf, err := certcrypto.ParsePEMCertificate([]byte(ariLeafPEM))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
issuer, err := certcrypto.ParsePEMCertificate([]byte(ariIssuerPEM))
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
// Test with a fake API.
|
// Test with a fake API.
|
||||||
mux, apiURL := tester.SetupFakeAPI(t)
|
mux, apiURL := tester.SetupFakeAPI(t)
|
||||||
|
@ -109,7 +84,7 @@ func TestCertifier_GetRenewalInfo(t *testing.T) {
|
||||||
|
|
||||||
certifier := NewCertifier(core, &resolverMock{}, CertifierOptions{KeyType: certcrypto.RSA2048})
|
certifier := NewCertifier(core, &resolverMock{}, CertifierOptions{KeyType: certcrypto.RSA2048})
|
||||||
|
|
||||||
ri, err := certifier.GetRenewalInfo(RenewalInfoRequest{leaf, issuer, crypto.SHA256.String()})
|
ri, err := certifier.GetRenewalInfo(RenewalInfoRequest{leaf})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.NotNil(t, ri)
|
require.NotNil(t, ri)
|
||||||
assert.Equal(t, "2020-03-17T17:51:09Z", ri.SuggestedWindow.Start.Format(time.RFC3339))
|
assert.Equal(t, "2020-03-17T17:51:09Z", ri.SuggestedWindow.Start.Format(time.RFC3339))
|
||||||
|
@ -120,8 +95,6 @@ func TestCertifier_GetRenewalInfo(t *testing.T) {
|
||||||
func TestCertifier_GetRenewalInfo_errors(t *testing.T) {
|
func TestCertifier_GetRenewalInfo_errors(t *testing.T) {
|
||||||
leaf, err := certcrypto.ParsePEMCertificate([]byte(ariLeafPEM))
|
leaf, err := certcrypto.ParsePEMCertificate([]byte(ariLeafPEM))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
issuer, err := certcrypto.ParsePEMCertificate([]byte(ariIssuerPEM))
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
key, err := rsa.GenerateKey(rand.Reader, 2048)
|
key, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||||
require.NoError(t, err, "Could not generate test key")
|
require.NoError(t, err, "Could not generate test key")
|
||||||
|
@ -135,7 +108,7 @@ func TestCertifier_GetRenewalInfo_errors(t *testing.T) {
|
||||||
{
|
{
|
||||||
desc: "API timeout",
|
desc: "API timeout",
|
||||||
httpClient: &http.Client{Timeout: 500 * time.Millisecond}, // HTTP client that times out after 500ms.
|
httpClient: &http.Client{Timeout: 500 * time.Millisecond}, // HTTP client that times out after 500ms.
|
||||||
request: RenewalInfoRequest{leaf, issuer, crypto.SHA256.String()},
|
request: RenewalInfoRequest{leaf},
|
||||||
handler: func(w http.ResponseWriter, r *http.Request) {
|
handler: func(w http.ResponseWriter, r *http.Request) {
|
||||||
// API that takes 2ms to respond.
|
// API that takes 2ms to respond.
|
||||||
time.Sleep(2 * time.Millisecond)
|
time.Sleep(2 * time.Millisecond)
|
||||||
|
@ -144,20 +117,12 @@ func TestCertifier_GetRenewalInfo_errors(t *testing.T) {
|
||||||
{
|
{
|
||||||
desc: "API error",
|
desc: "API error",
|
||||||
httpClient: http.DefaultClient,
|
httpClient: http.DefaultClient,
|
||||||
request: RenewalInfoRequest{leaf, issuer, crypto.SHA256.String()},
|
request: RenewalInfoRequest{leaf},
|
||||||
handler: func(w http.ResponseWriter, r *http.Request) {
|
handler: func(w http.ResponseWriter, r *http.Request) {
|
||||||
// API that responds with error instead of renewal info.
|
// API that responds with error instead of renewal info.
|
||||||
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
|
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
|
||||||
desc: "Issuer certificate is nil",
|
|
||||||
httpClient: http.DefaultClient,
|
|
||||||
request: RenewalInfoRequest{leaf, nil, crypto.SHA256.String()},
|
|
||||||
handler: func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, test := range testCases {
|
for _, test := range testCases {
|
||||||
|
@ -183,8 +148,6 @@ func TestCertifier_GetRenewalInfo_errors(t *testing.T) {
|
||||||
func TestCertifier_UpdateRenewalInfo(t *testing.T) {
|
func TestCertifier_UpdateRenewalInfo(t *testing.T) {
|
||||||
leaf, err := certcrypto.ParsePEMCertificate([]byte(ariLeafPEM))
|
leaf, err := certcrypto.ParsePEMCertificate([]byte(ariLeafPEM))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
issuer, err := certcrypto.ParsePEMCertificate([]byte(ariIssuerPEM))
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
key, err := rsa.GenerateKey(rand.Reader, 2048)
|
key, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||||
require.NoError(t, err, "Could not generate test key")
|
require.NoError(t, err, "Could not generate test key")
|
||||||
|
@ -217,15 +180,13 @@ func TestCertifier_UpdateRenewalInfo(t *testing.T) {
|
||||||
|
|
||||||
certifier := NewCertifier(core, &resolverMock{}, CertifierOptions{KeyType: certcrypto.RSA2048})
|
certifier := NewCertifier(core, &resolverMock{}, CertifierOptions{KeyType: certcrypto.RSA2048})
|
||||||
|
|
||||||
err = certifier.UpdateRenewalInfo(RenewalInfoRequest{leaf, issuer, crypto.SHA256.String()})
|
err = certifier.UpdateRenewalInfo(RenewalInfoRequest{leaf})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCertifier_UpdateRenewalInfo_errors(t *testing.T) {
|
func TestCertifier_UpdateRenewalInfo_errors(t *testing.T) {
|
||||||
leaf, err := certcrypto.ParsePEMCertificate([]byte(ariLeafPEM))
|
leaf, err := certcrypto.ParsePEMCertificate([]byte(ariLeafPEM))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
issuer, err := certcrypto.ParsePEMCertificate([]byte(ariIssuerPEM))
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
key, err := rsa.GenerateKey(rand.Reader, 2048)
|
key, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||||
require.NoError(t, err, "Could not generate test key")
|
require.NoError(t, err, "Could not generate test key")
|
||||||
|
@ -236,11 +197,7 @@ func TestCertifier_UpdateRenewalInfo_errors(t *testing.T) {
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
desc: "API error",
|
desc: "API error",
|
||||||
request: RenewalInfoRequest{leaf, issuer, crypto.SHA256.String()},
|
request: RenewalInfoRequest{leaf},
|
||||||
},
|
|
||||||
{
|
|
||||||
desc: "Certificate is nil",
|
|
||||||
request: RenewalInfoRequest{nil, issuer, crypto.SHA256.String()},
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -53,11 +53,6 @@ func createRenew() *cli.Command {
|
||||||
Name: "ari-enable",
|
Name: "ari-enable",
|
||||||
Usage: "Use the renewalInfo endpoint (draft-ietf-acme-ari) to check if a certificate should be renewed.",
|
Usage: "Use the renewalInfo endpoint (draft-ietf-acme-ari) to check if a certificate should be renewed.",
|
||||||
},
|
},
|
||||||
&cli.StringFlag{
|
|
||||||
Name: "ari-hash-name",
|
|
||||||
Value: crypto.SHA256.String(),
|
|
||||||
Usage: "The string representation of the hash expected by the renewalInfo endpoint (e.g. \"SHA-256\").",
|
|
||||||
},
|
|
||||||
&cli.DurationFlag{
|
&cli.DurationFlag{
|
||||||
Name: "ari-wait-to-renew-duration",
|
Name: "ari-wait-to-renew-duration",
|
||||||
Usage: "The maximum duration you're willing to sleep for a renewal time returned by the renewalInfo endpoint.",
|
Usage: "The maximum duration you're willing to sleep for a renewal time returned by the renewalInfo endpoint.",
|
||||||
|
@ -146,11 +141,7 @@ func renewForDomains(ctx *cli.Context, client *lego.Client, certsStorage *Certif
|
||||||
|
|
||||||
var ariRenewalTime *time.Time
|
var ariRenewalTime *time.Time
|
||||||
if ctx.Bool("ari-enable") {
|
if ctx.Bool("ari-enable") {
|
||||||
if len(certificates) < 2 {
|
ariRenewalTime = getARIRenewalTime(ctx, cert, domain, client)
|
||||||
log.Warnf("[%s] Certificate bundle does not contain issuer, cannot use the renewalInfo endpoint", domain)
|
|
||||||
} else {
|
|
||||||
ariRenewalTime = getARIRenewalTime(ctx, certificates[0], certificates[1], domain, client)
|
|
||||||
}
|
|
||||||
if ariRenewalTime != nil {
|
if ariRenewalTime != nil {
|
||||||
now := time.Now().UTC()
|
now := time.Now().UTC()
|
||||||
// Figure out if we need to sleep before renewing.
|
// Figure out if we need to sleep before renewing.
|
||||||
|
@ -216,11 +207,7 @@ func renewForDomains(ctx *cli.Context, client *lego.Client, certsStorage *Certif
|
||||||
|
|
||||||
if ariRenewalTime != nil {
|
if ariRenewalTime != nil {
|
||||||
// Post to the renewalInfo endpoint to indicate that we have renewed and replaced the certificate.
|
// Post to the renewalInfo endpoint to indicate that we have renewed and replaced the certificate.
|
||||||
err := client.Certificate.UpdateRenewalInfo(certificate.RenewalInfoRequest{
|
err := client.Certificate.UpdateRenewalInfo(certificate.RenewalInfoRequest{Cert: certificates[0]})
|
||||||
Cert: certificates[0],
|
|
||||||
Issuer: certificates[1],
|
|
||||||
HashName: ctx.String("ari-hash-name"),
|
|
||||||
})
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Warnf("[%s] Failed to update renewal info: %v", domain, err)
|
log.Warnf("[%s] Failed to update renewal info: %v", domain, err)
|
||||||
}
|
}
|
||||||
|
@ -255,11 +242,7 @@ func renewForCSR(ctx *cli.Context, client *lego.Client, certsStorage *Certificat
|
||||||
|
|
||||||
var ariRenewalTime *time.Time
|
var ariRenewalTime *time.Time
|
||||||
if ctx.Bool("ari-enable") {
|
if ctx.Bool("ari-enable") {
|
||||||
if len(certificates) < 2 {
|
ariRenewalTime = getARIRenewalTime(ctx, cert, domain, client)
|
||||||
log.Warnf("[%s] Certificate bundle does not contain issuer, cannot use the renewalInfo endpoint", domain)
|
|
||||||
} else {
|
|
||||||
ariRenewalTime = getARIRenewalTime(ctx, certificates[0], certificates[1], domain, client)
|
|
||||||
}
|
|
||||||
if ariRenewalTime != nil {
|
if ariRenewalTime != nil {
|
||||||
now := time.Now().UTC()
|
now := time.Now().UTC()
|
||||||
// Figure out if we need to sleep before renewing.
|
// Figure out if we need to sleep before renewing.
|
||||||
|
@ -296,11 +279,7 @@ func renewForCSR(ctx *cli.Context, client *lego.Client, certsStorage *Certificat
|
||||||
|
|
||||||
if ariRenewalTime != nil {
|
if ariRenewalTime != nil {
|
||||||
// Post to the renewalInfo endpoint to indicate that we have renewed and replaced the certificate.
|
// Post to the renewalInfo endpoint to indicate that we have renewed and replaced the certificate.
|
||||||
err := client.Certificate.UpdateRenewalInfo(certificate.RenewalInfoRequest{
|
err := client.Certificate.UpdateRenewalInfo(certificate.RenewalInfoRequest{Cert: certificates[0]})
|
||||||
Cert: certificates[0],
|
|
||||||
Issuer: certificates[1],
|
|
||||||
HashName: ctx.String("ari-hash-name"),
|
|
||||||
})
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Warnf("[%s] Failed to update renewal info: %v", domain, err)
|
log.Warnf("[%s] Failed to update renewal info: %v", domain, err)
|
||||||
}
|
}
|
||||||
|
@ -331,23 +310,19 @@ func needRenewal(x509Cert *x509.Certificate, domain string, days int) bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
// getARIRenewalTime checks if the certificate needs to be renewed using the renewalInfo endpoint.
|
// getARIRenewalTime checks if the certificate needs to be renewed using the renewalInfo endpoint.
|
||||||
func getARIRenewalTime(ctx *cli.Context, cert, issuer *x509.Certificate, domain string, client *lego.Client) *time.Time {
|
func getARIRenewalTime(ctx *cli.Context, cert *x509.Certificate, domain string, client *lego.Client) *time.Time {
|
||||||
if cert.IsCA {
|
if cert.IsCA {
|
||||||
log.Fatalf("[%s] Certificate bundle starts with a CA certificate", domain)
|
log.Fatalf("[%s] Certificate bundle starts with a CA certificate", domain)
|
||||||
}
|
}
|
||||||
|
|
||||||
renewalInfo, err := client.Certificate.GetRenewalInfo(certificate.RenewalInfoRequest{
|
renewalInfo, err := client.Certificate.GetRenewalInfo(certificate.RenewalInfoRequest{Cert: cert})
|
||||||
Cert: cert,
|
|
||||||
Issuer: issuer,
|
|
||||||
HashName: ctx.String("ari-hash-name"),
|
|
||||||
})
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, api.ErrNoARI) {
|
if errors.Is(err, api.ErrNoARI) {
|
||||||
// The server does not advertise a renewal info endpoint.
|
// The server does not advertise a renewal info endpoint.
|
||||||
log.Warnf("[%s] acme: %w", domain, err)
|
log.Warnf("[%s] acme: %v", domain, err)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
log.Warnf("[%s] acme: calling renewal info endpoint: %w", domain, err)
|
log.Warnf("[%s] acme: calling renewal info endpoint: %v", domain, err)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -82,8 +82,7 @@ USAGE:
|
||||||
|
|
||||||
OPTIONS:
|
OPTIONS:
|
||||||
--days value The number of days left on a certificate to renew it. (default: 0)
|
--days value The number of days left on a certificate to renew it. (default: 0)
|
||||||
--ari-enable Use the renewalInfo endpoint (draft-ietf-acme-ari) to check if a certificate should be renewed. (default: false)
|
--ari-enable Use the renewalInfo endpoint (draft-ietf-acme-ari-02) to check if a certificate should be renewed. (default: false)
|
||||||
--ari-hash-name value The string representation of the hash expected by the renewalInfo endpoint (e.g. "SHA-256").
|
|
||||||
--ari-wait-to-renew-duration value The maximum duration you're willing to sleep for a renewal time returned by the renewalInfo endpoint. (default: 0s)
|
--ari-wait-to-renew-duration value The maximum duration you're willing to sleep for a renewal time returned by the renewalInfo endpoint. (default: 0s)
|
||||||
--reuse-key Used to indicate you want to reuse your current private key for the new certificate. (default: false)
|
--reuse-key Used to indicate you want to reuse your current private key for the new certificate. (default: false)
|
||||||
--no-bundle Do not create a certificate bundle by adding the issuers certificate to the new certificate. (default: false)
|
--no-bundle Do not create a certificate bundle by adding the issuers certificate to the new certificate. (default: false)
|
||||||
|
|
Loading…
Reference in a new issue