Add preferred-chain option to support "alternate" certificate links (#1227)

Co-authored-by: Ludovic Fernandez <ldez@users.noreply.github.com>
This commit is contained in:
Masayuki Matsuki 2020-09-02 09:22:53 +09:00 committed by GitHub
parent dd38dce6aa
commit 30e4987f99
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 281 additions and 62 deletions

View file

@ -25,41 +25,48 @@ func (o *OrderService) New(domains []string) (acme.ExtendedOrder, error) {
} }
return acme.ExtendedOrder{ return acme.ExtendedOrder{
Location: resp.Header.Get("Location"), Order: order,
Order: order, Location: resp.Header.Get("Location"),
AlternateChainLinks: getLinks(resp.Header, "alternate"),
}, nil }, nil
} }
// Get Gets an order. // Get Gets an order.
func (o *OrderService) Get(orderURL string) (acme.Order, error) { func (o *OrderService) Get(orderURL string) (acme.ExtendedOrder, error) {
if len(orderURL) == 0 { if len(orderURL) == 0 {
return acme.Order{}, errors.New("order[get]: empty URL") return acme.ExtendedOrder{}, errors.New("order[get]: empty URL")
} }
var order acme.Order var order acme.Order
_, err := o.core.postAsGet(orderURL, &order) resp, err := o.core.postAsGet(orderURL, &order)
if err != nil { if err != nil {
return acme.Order{}, err return acme.ExtendedOrder{}, err
} }
return order, nil return acme.ExtendedOrder{
Order: order,
AlternateChainLinks: getLinks(resp.Header, "alternate"),
}, nil
} }
// UpdateForCSR Updates an order for a CSR. // UpdateForCSR Updates an order for a CSR.
func (o *OrderService) UpdateForCSR(orderURL string, csr []byte) (acme.Order, error) { func (o *OrderService) UpdateForCSR(orderURL string, csr []byte) (acme.ExtendedOrder, error) {
csrMsg := acme.CSRMessage{ csrMsg := acme.CSRMessage{
Csr: base64.RawURLEncoding.EncodeToString(csr), Csr: base64.RawURLEncoding.EncodeToString(csr),
} }
var order acme.Order var order acme.Order
_, err := o.core.post(orderURL, csrMsg, &order) resp, err := o.core.post(orderURL, csrMsg, &order)
if err != nil { if err != nil {
return acme.Order{}, err return acme.ExtendedOrder{}, err
} }
if order.Status == acme.StatusInvalid { if order.Status == acme.StatusInvalid {
return acme.Order{}, order.Error return acme.ExtendedOrder{}, order.Error
} }
return order, nil return acme.ExtendedOrder{
Order: order,
AlternateChainLinks: getLinks(resp.Header, "alternate"),
}, nil
} }

View file

@ -41,6 +41,10 @@ func TestOrderService_New(t *testing.T) {
return return
} }
w.Header().Add("Link", `<https://example.com/acme/cert/1>;rel="alternate"`)
w.Header().Add("Link", `<https://example.com/acme/cert/2>;title="foo";rel="alternate"`)
w.Header().Add("Link", `<https://example.com/acme/cert/3>;title="foo";rel="alternate", <https://example.com/acme/cert/4>;rel="alternate"`)
err = tester.WriteJSONResponse(w, acme.Order{ err = tester.WriteJSONResponse(w, acme.Order{
Status: acme.StatusValid, Status: acme.StatusValid,
Identifiers: order.Identifiers, Identifiers: order.Identifiers,
@ -62,6 +66,12 @@ func TestOrderService_New(t *testing.T) {
Status: "valid", Status: "valid",
Identifiers: []acme.Identifier{{Type: "dns", Value: "example.com"}}, Identifiers: []acme.Identifier{{Type: "dns", Value: "example.com"}},
}, },
AlternateChainLinks: []string{
"https://example.com/acme/cert/1",
"https://example.com/acme/cert/2",
"https://example.com/acme/cert/3",
"https://example.com/acme/cert/4",
},
} }
assert.Equal(t, expected, order) assert.Equal(t, expected, order)
} }

View file

@ -11,19 +11,30 @@ type service struct {
// getLink get a rel into the Link header. // getLink get a rel into the Link header.
func getLink(header http.Header, rel string) string { func getLink(header http.Header, rel string) string {
linkExpr := regexp.MustCompile(`<(.+?)>;\s*rel="(.+?)"`) links := getLinks(header, rel)
if len(links) < 1 {
return ""
}
return links[0]
}
func getLinks(header http.Header, rel string) []string {
linkExpr := regexp.MustCompile(`<(.+?)>(?:;[^;]+)*?;\s*rel="(.+?)"`)
var links []string
for _, link := range header["Link"] { for _, link := range header["Link"] {
for _, m := range linkExpr.FindAllStringSubmatch(link, -1) { for _, m := range linkExpr.FindAllStringSubmatch(link, -1) {
if len(m) != 3 { if len(m) != 3 {
continue continue
} }
if m[2] == rel { if m[2] == rel {
return m[1] links = append(links, m[1])
} }
} }
} }
return ""
return links
} }
// getLocation get the value of the header Location. // getLocation get the value of the header Location.

View file

@ -108,6 +108,11 @@ type ExtendedOrder struct {
Order Order
// The order URL, contains the value of the response header `Location` // The order URL, contains the value of the response header `Location`
Location string `json:"-"` Location string `json:"-"`
// AlternateChainLinks (optional, array of string):
// URLs of "alternate" link relation
// - https://tools.ietf.org/html/rfc8555#section-7.4.2
AlternateChainLinks []string `json:"-"`
} }
// Order the ACME order Object. // Order the ACME order Object.

View file

@ -51,10 +51,11 @@ type Resource struct {
// //
// If bundle is true, the []byte contains both the issuer certificate and your issued certificate as a bundle. // If bundle is true, the []byte contains both the issuer certificate and your issued certificate as a bundle.
type ObtainRequest struct { type ObtainRequest struct {
Domains []string Domains []string
Bundle bool Bundle bool
PrivateKey crypto.PrivateKey PrivateKey crypto.PrivateKey
MustStaple bool MustStaple bool
PreferredChain string
} }
type resolver interface { type resolver interface {
@ -121,7 +122,7 @@ func (c *Certifier) Obtain(request ObtainRequest) (*Resource, error) {
log.Infof("[%s] acme: Validations succeeded; requesting certificates", strings.Join(domains, ", ")) log.Infof("[%s] acme: Validations succeeded; requesting certificates", strings.Join(domains, ", "))
failures := make(obtainError) failures := make(obtainError)
cert, err := c.getForOrder(domains, order, request.Bundle, request.PrivateKey, request.MustStaple) cert, err := c.getForOrder(domains, order, request.Bundle, request.PrivateKey, request.MustStaple, request.PreferredChain)
if err != nil { if err != nil {
for _, auth := range authz { for _, auth := range authz {
failures[challenge.GetTargetedDomain(auth)] = err failures[challenge.GetTargetedDomain(auth)] = err
@ -145,7 +146,7 @@ func (c *Certifier) Obtain(request ObtainRequest) (*Resource, error) {
// //
// This function will never return a partial certificate. // This function will never return a partial certificate.
// If one domain in the list fails, the whole certificate will fail. // If one domain in the list fails, the whole certificate will fail.
func (c *Certifier) ObtainForCSR(csr x509.CertificateRequest, bundle bool) (*Resource, error) { func (c *Certifier) ObtainForCSR(csr x509.CertificateRequest, bundle bool, preferredChain string) (*Resource, error) {
// figure out what domains it concerns // figure out what domains it concerns
// start with the common name // start with the common name
domains := certcrypto.ExtractDomainsCSR(&csr) domains := certcrypto.ExtractDomainsCSR(&csr)
@ -178,7 +179,7 @@ func (c *Certifier) ObtainForCSR(csr x509.CertificateRequest, bundle bool) (*Res
log.Infof("[%s] acme: Validations succeeded; requesting certificates", strings.Join(domains, ", ")) log.Infof("[%s] acme: Validations succeeded; requesting certificates", strings.Join(domains, ", "))
failures := make(obtainError) failures := make(obtainError)
cert, err := c.getForCSR(domains, order, bundle, csr.Raw, nil) cert, err := c.getForCSR(domains, order, bundle, csr.Raw, nil, preferredChain)
if err != nil { if err != nil {
for _, auth := range authz { for _, auth := range authz {
failures[challenge.GetTargetedDomain(auth)] = err failures[challenge.GetTargetedDomain(auth)] = err
@ -198,7 +199,7 @@ func (c *Certifier) ObtainForCSR(csr x509.CertificateRequest, bundle bool) (*Res
return cert, nil return cert, nil
} }
func (c *Certifier) getForOrder(domains []string, order acme.ExtendedOrder, bundle bool, privateKey crypto.PrivateKey, mustStaple bool) (*Resource, error) { func (c *Certifier) getForOrder(domains []string, order acme.ExtendedOrder, bundle bool, privateKey crypto.PrivateKey, mustStaple bool, preferredChain string) (*Resource, error) {
if privateKey == nil { if privateKey == nil {
var err error var err error
privateKey, err = certcrypto.GeneratePrivateKey(c.options.KeyType) privateKey, err = certcrypto.GeneratePrivateKey(c.options.KeyType)
@ -229,10 +230,10 @@ func (c *Certifier) getForOrder(domains []string, order acme.ExtendedOrder, bund
return nil, err return nil, err
} }
return c.getForCSR(domains, order, bundle, csr, certcrypto.PEMEncode(privateKey)) return c.getForCSR(domains, order, bundle, csr, certcrypto.PEMEncode(privateKey), preferredChain)
} }
func (c *Certifier) getForCSR(domains []string, order acme.ExtendedOrder, bundle bool, csr, privateKeyPem []byte) (*Resource, error) { func (c *Certifier) getForCSR(domains []string, order acme.ExtendedOrder, bundle bool, csr, privateKeyPem []byte, preferredChain string) (*Resource, error) {
respOrder, err := c.core.Orders.UpdateForCSR(order.Finalize, csr) respOrder, err := c.core.Orders.UpdateForCSR(order.Finalize, csr)
if err != nil { if err != nil {
return nil, err return nil, err
@ -247,7 +248,7 @@ func (c *Certifier) getForCSR(domains []string, order acme.ExtendedOrder, bundle
if respOrder.Status == acme.StatusValid { if respOrder.Status == acme.StatusValid {
// if the certificate is available right away, short cut! // if the certificate is available right away, short cut!
ok, errR := c.checkResponse(respOrder, certRes, bundle) ok, errR := c.checkResponse(respOrder, certRes, bundle, preferredChain)
if errR != nil { if errR != nil {
return nil, errR return nil, errR
} }
@ -268,7 +269,7 @@ func (c *Certifier) getForCSR(domains []string, order acme.ExtendedOrder, bundle
return false, errW return false, errW
} }
done, errW := c.checkResponse(ord, certRes, bundle) done, errW := c.checkResponse(ord, certRes, bundle, preferredChain)
if errW != nil { if errW != nil {
return false, errW return false, errW
} }
@ -287,23 +288,52 @@ func (c *Certifier) getForCSR(domains []string, order acme.ExtendedOrder, bundle
// The certRes input should already have the Domain (common name) field populated. // The certRes input should already have the Domain (common name) field populated.
// //
// If bundle is true, the certificate will be bundled with the issuer's cert. // If bundle is true, the certificate will be bundled with the issuer's cert.
func (c *Certifier) checkResponse(order acme.Order, certRes *Resource, bundle bool) (bool, error) { func (c *Certifier) checkResponse(order acme.ExtendedOrder, certRes *Resource, bundle bool, preferredChain string) (bool, error) {
valid, err := checkOrderStatus(order) valid, err := checkOrderStatus(order)
if err != nil || !valid { if err != nil || !valid {
return valid, err return valid, err
} }
cert, issuer, err := c.core.Certificates.Get(order.Certificate, bundle) links := append([]string{order.Certificate}, order.AlternateChainLinks...)
if err != nil {
return false, err for i, link := range links {
cert, issuer, err := c.core.Certificates.Get(link, bundle)
if err != nil {
return false, err
}
// Set the default certificate
if i == 0 {
certRes.IssuerCertificate = issuer
certRes.Certificate = cert
certRes.CertURL = link
certRes.CertStableURL = link
}
if preferredChain == "" {
log.Infof("[%s] Server responded with a certificate.", certRes.Domain)
return true, nil
}
ok, err := hasPreferredChain(issuer, preferredChain)
if err != nil {
return false, err
}
if ok {
log.Infof("[%s] Server responded with a certificate for the preferred certificate chains %q.", certRes.Domain, preferredChain)
certRes.IssuerCertificate = issuer
certRes.Certificate = cert
certRes.CertURL = link
certRes.CertStableURL = link
return true, nil
}
} }
log.Infof("[%s] Server responded with a certificate.", certRes.Domain) log.Infof("lego has been configured to prefer certificate chains with issuer %q, but no chain from the CA matched this issuer. Using the default certificate chain instead.", preferredChain)
certRes.IssuerCertificate = issuer
certRes.Certificate = cert
certRes.CertURL = order.Certificate
certRes.CertStableURL = order.Certificate
return true, nil return true, nil
} }
@ -337,7 +367,7 @@ func (c *Certifier) Revoke(cert []byte) error {
// If bundle is true, the []byte contains both the issuer certificate and your issued certificate as a bundle. // 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. // For private key reuse the PrivateKey property of the passed in Resource should be non-nil.
func (c *Certifier) Renew(certRes Resource, bundle, mustStaple bool) (*Resource, error) { func (c *Certifier) Renew(certRes Resource, bundle, mustStaple bool, preferredChain string) (*Resource, error) {
// Input certificate is PEM encoded. // Input certificate is PEM encoded.
// Decode it here as we may need the decoded cert later on in the renewal process. // 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. // The input may be a bundle or a single certificate.
@ -364,7 +394,7 @@ func (c *Certifier) Renew(certRes Resource, bundle, mustStaple bool) (*Resource,
return nil, errP return nil, errP
} }
return c.ObtainForCSR(*csr, bundle) return c.ObtainForCSR(*csr, bundle, preferredChain)
} }
var privateKey crypto.PrivateKey var privateKey crypto.PrivateKey
@ -491,7 +521,22 @@ func (c *Certifier) Get(url string, bundle bool) (*Resource, error) {
}, nil }, nil
} }
func checkOrderStatus(order acme.Order) (bool, error) { func hasPreferredChain(issuer []byte, preferredChain string) (bool, error) {
certs, err := certcrypto.ParsePEMBundle(issuer)
if err != nil {
return false, err
}
for _, cert := range certs {
if cert.Issuer.CommonName == preferredChain {
return true, nil
}
}
return false, nil
}
func checkOrderStatus(order acme.ExtendedOrder) (bool, error) {
switch order.Status { switch order.Status {
case acme.StatusValid: case acme.StatusValid:
return true, nil return true, nil

View file

@ -76,6 +76,82 @@ rzFL1KZfz+HZdnFwFW2T2gVW8L3ii1l9AJDuKzlvjUH3p6bgihVq02sjT8mx+GM2
-----END CERTIFICATE----- -----END CERTIFICATE-----
` `
const certResponseMock2 = `
-----BEGIN CERTIFICATE-----
MIIFUzCCBDugAwIBAgISA/z9btaZCSo/qlVwmJrHpoyPMA0GCSqGSIb3DQEBCwUA
MEoxCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1MZXQncyBFbmNyeXB0MSMwIQYDVQQD
ExpMZXQncyBFbmNyeXB0IEF1dGhvcml0eSBYMzAeFw0yMDA3MjUwNjUxNDRaFw0y
MDEwMjMwNjUxNDRaMBgxFjAUBgNVBAMTDW5hdHVyZS5nbG9iYWwwggEiMA0GCSqG
SIb3DQEBAQUAA4IBDwAwggEKAoIBAQDN/PF8lWub3i+lO3CLl/HJAM86pQH9hWej
Whci1PPNzKyEByJq2psNLCO1W1mXK3ClWSyifptCf7+AAFAOoBojPMwjaKMziw1M
BxAQiX8MzZLv4Hr4Uk08cQX31QHiEpOv4pMHqB0UpodTYY10dZnDdyJHaGKzxfJh
nQPYIVto+UegcVu9iZIDow7ugoT2Gh8nB8jOAc4wtBgmylgeAFmYR6QZ4PYSYFh0
DLZGGB1WuU/4YC5OciwTDv5EiqP3KM3NdkmGhPY0A3jcTrjN+HhcE4pYBtG1wHi8
PEuqqKyCLa3AjHq4WrZyCCkCMXPbIDS1Qt7botDmUZr/26xJZnl5AgMBAAGjggJj
MIICXzAOBgNVHQ8BAf8EBAMCBaAwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUF
BwMCMAwGA1UdEwEB/wQCMAAwHQYDVR0OBBYEFFm72Cv7LnjVhcLqUujrykUr70lF
MB8GA1UdIwQYMBaAFKhKamMEfd265tE5t6ZFZe/zqOyhMG8GCCsGAQUFBwEBBGMw
YTAuBggrBgEFBQcwAYYiaHR0cDovL29jc3AuaW50LXgzLmxldHNlbmNyeXB0Lm9y
ZzAvBggrBgEFBQcwAoYjaHR0cDovL2NlcnQuaW50LXgzLmxldHNlbmNyeXB0Lm9y
Zy8wGAYDVR0RBBEwD4INbmF0dXJlLmdsb2JhbDBMBgNVHSAERTBDMAgGBmeBDAEC
ATA3BgsrBgEEAYLfEwEBATAoMCYGCCsGAQUFBwIBFhpodHRwOi8vY3BzLmxldHNl
bmNyeXB0Lm9yZzCCAQUGCisGAQQB1nkCBAIEgfYEgfMA8QB3ALIeBcyLos2KIE6H
ZvkruYolIGdr2vpw57JJUy3vi5BeAAABc4T006IAAAQDAEgwRgIhAPEEvCEMkekD
8XLDaxHPnJ85UZL72JqGgNK+7I/NdFNuAiEA5D78b4V1YsD8wvWz/sk6Ks8VgjED
eKGl/TyXwKEpzEIAdgBvU3asMfAxGdiZAKRRFf93FRwR2QLBACkGjbIImjfZEwAA
AXOE9NPrAAAEAwBHMEUCIAu4YFfGZIN/P+0eRG0krSddHKCSf6rqr6aVqUWkJY3F
AiEAz0HkTe0alED1gW9nEAJ1qqK1MLMjRM8SsUv9Is86+CwwDQYJKoZIhvcNAQEL
BQADggEBAGriSVi9YuBnm50w84gjlinmeGdvxgugblIoEqKoXd3d5/zx0DvW9Tm6
YGfXsvAJUSCag7dZ/s/PEu23jKNdFoaBmDaUHHKnUwbWWF7/ptYZ+YuDVGOJo8PL
CULNfUMon20rPU9smzW4BFDBZ6KmX/r4Q8cQ7FLOqKdcng0yMcqIfq4cBxEvd0uQ
pHR3AwCjAIGpV6Q9WHHiHx+SEd/Xc18Z5pXa9m3Rz4i6Mfv+AYLtnsZDxcH81cVM
7rYp80vhXM9tFd4wyrqLuaVZgYD1ylxTYpTI7sijIq4Sl984f3IPA/olN+zK6E8d
EbiufIcKeju/aSellDzzBabEo80YT4o=
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIIDSjCCAjKgAwIBAgIQRK+wgNajJ7qJMDmGLvhAazANBgkqhkiG9w0BAQUFADA/
MSQwIgYDVQQKExtEaWdpdGFsIFNpZ25hdHVyZSBUcnVzdCBDby4xFzAVBgNVBAMT
DkRTVCBSb290IENBIFgzMB4XDTAwMDkzMDIxMTIxOVoXDTIxMDkzMDE0MDExNVow
PzEkMCIGA1UEChMbRGlnaXRhbCBTaWduYXR1cmUgVHJ1c3QgQ28uMRcwFQYDVQQD
Ew5EU1QgUm9vdCBDQSBYMzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB
AN+v6ZdQCINXtMxiZfaQguzH0yxrMMpb7NnDfcdAwRgUi+DoM3ZJKuM/IUmTrE4O
rz5Iy2Xu/NMhD2XSKtkyj4zl93ewEnu1lcCJo6m67XMuegwGMoOifooUMM0RoOEq
OLl5CjH9UL2AZd+3UWODyOKIYepLYYHsUmu5ouJLGiifSKOeDNoJjj4XLh7dIN9b
xiqKqy69cK3FCxolkHRyxXtqqzTWMIn/5WgTe1QLyNau7Fqckh49ZLOMxt+/yUFw
7BZy1SbsOFU5Q9D8/RhcQPGX69Wam40dutolucbY38EVAjqr2m7xPi71XAicPNaD
aeQQmxkqtilX4+U9m5/wAl0CAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNV
HQ8BAf8EBAMCAQYwHQYDVR0OBBYEFMSnsaR7LHH62+FLkHX/xBVghYkQMA0GCSqG
SIb3DQEBBQUAA4IBAQCjGiybFwBcqR7uKGY3Or+Dxz9LwwmglSBd49lZRNI+DT69
ikugdB/OEIKcdBodfpga3csTS7MgROSR6cz8faXbauX+5v3gTt23ADq1cEmv8uXr
AvHRAosZy5Q6XkjEGB5YGV8eAlrwDPGxrancWYaLbumR9YbK+rlmM6pZW87ipxZz
R8srzJmwN0jP41ZL9c8PDHIyh8bwRLtTcm1D9SZImlJnt1ir/md2cXjbDaJWFBM5
JDGFoqgCWjBH4d1QB7wCCZAA62RjYJsWvIjJEubSfZGL+T0yjWW06XyxV3bqxbYo
Ob8VZRzI9neWagqNdwvYkQsEjgfbKbYK7p2CNTUQ
-----END CERTIFICATE-----
`
const issuerMock2 = `-----BEGIN CERTIFICATE-----
MIIDSjCCAjKgAwIBAgIQRK+wgNajJ7qJMDmGLvhAazANBgkqhkiG9w0BAQUFADA/
MSQwIgYDVQQKExtEaWdpdGFsIFNpZ25hdHVyZSBUcnVzdCBDby4xFzAVBgNVBAMT
DkRTVCBSb290IENBIFgzMB4XDTAwMDkzMDIxMTIxOVoXDTIxMDkzMDE0MDExNVow
PzEkMCIGA1UEChMbRGlnaXRhbCBTaWduYXR1cmUgVHJ1c3QgQ28uMRcwFQYDVQQD
Ew5EU1QgUm9vdCBDQSBYMzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB
AN+v6ZdQCINXtMxiZfaQguzH0yxrMMpb7NnDfcdAwRgUi+DoM3ZJKuM/IUmTrE4O
rz5Iy2Xu/NMhD2XSKtkyj4zl93ewEnu1lcCJo6m67XMuegwGMoOifooUMM0RoOEq
OLl5CjH9UL2AZd+3UWODyOKIYepLYYHsUmu5ouJLGiifSKOeDNoJjj4XLh7dIN9b
xiqKqy69cK3FCxolkHRyxXtqqzTWMIn/5WgTe1QLyNau7Fqckh49ZLOMxt+/yUFw
7BZy1SbsOFU5Q9D8/RhcQPGX69Wam40dutolucbY38EVAjqr2m7xPi71XAicPNaD
aeQQmxkqtilX4+U9m5/wAl0CAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNV
HQ8BAf8EBAMCAQYwHQYDVR0OBBYEFMSnsaR7LHH62+FLkHX/xBVghYkQMA0GCSqG
SIb3DQEBBQUAA4IBAQCjGiybFwBcqR7uKGY3Or+Dxz9LwwmglSBd49lZRNI+DT69
ikugdB/OEIKcdBodfpga3csTS7MgROSR6cz8faXbauX+5v3gTt23ADq1cEmv8uXr
AvHRAosZy5Q6XkjEGB5YGV8eAlrwDPGxrancWYaLbumR9YbK+rlmM6pZW87ipxZz
R8srzJmwN0jP41ZL9c8PDHIyh8bwRLtTcm1D9SZImlJnt1ir/md2cXjbDaJWFBM5
JDGFoqgCWjBH4d1QB7wCCZAA62RjYJsWvIjJEubSfZGL+T0yjWW06XyxV3bqxbYo
Ob8VZRzI9neWagqNdwvYkQsEjgfbKbYK7p2CNTUQ
-----END CERTIFICATE-----
`
func Test_checkResponse(t *testing.T) { func Test_checkResponse(t *testing.T) {
mux, apiURL, tearDown := tester.SetupFakeAPI() mux, apiURL, tearDown := tester.SetupFakeAPI()
defer tearDown() defer tearDown()
@ -95,14 +171,16 @@ func Test_checkResponse(t *testing.T) {
certifier := NewCertifier(core, &resolverMock{}, CertifierOptions{KeyType: certcrypto.RSA2048}) certifier := NewCertifier(core, &resolverMock{}, CertifierOptions{KeyType: certcrypto.RSA2048})
order := acme.Order{ order := acme.ExtendedOrder{
Status: acme.StatusValid, Order: acme.Order{
Certificate: apiURL + "/certificate", Status: acme.StatusValid,
Certificate: apiURL + "/certificate",
},
} }
certRes := &Resource{} certRes := &Resource{}
bundle := false bundle := false
valid, err := certifier.checkResponse(order, certRes, bundle) valid, err := certifier.checkResponse(order, certRes, bundle, "")
require.NoError(t, err) require.NoError(t, err)
assert.True(t, valid) assert.True(t, valid)
assert.NotNil(t, certRes) assert.NotNil(t, certRes)
@ -143,14 +221,16 @@ func Test_checkResponse_issuerRelUp(t *testing.T) {
certifier := NewCertifier(core, &resolverMock{}, CertifierOptions{KeyType: certcrypto.RSA2048}) certifier := NewCertifier(core, &resolverMock{}, CertifierOptions{KeyType: certcrypto.RSA2048})
order := acme.Order{ order := acme.ExtendedOrder{
Status: acme.StatusValid, Order: acme.Order{
Certificate: apiURL + "/certificate", Status: acme.StatusValid,
Certificate: apiURL + "/certificate",
},
} }
certRes := &Resource{} certRes := &Resource{}
bundle := false bundle := false
valid, err := certifier.checkResponse(order, certRes, bundle) valid, err := certifier.checkResponse(order, certRes, bundle, "")
require.NoError(t, err) require.NoError(t, err)
assert.True(t, valid) assert.True(t, valid)
assert.NotNil(t, certRes) assert.NotNil(t, certRes)
@ -182,14 +262,16 @@ func Test_checkResponse_embeddedIssuer(t *testing.T) {
certifier := NewCertifier(core, &resolverMock{}, CertifierOptions{KeyType: certcrypto.RSA2048}) certifier := NewCertifier(core, &resolverMock{}, CertifierOptions{KeyType: certcrypto.RSA2048})
order := acme.Order{ order := acme.ExtendedOrder{
Status: acme.StatusValid, Order: acme.Order{
Certificate: apiURL + "/certificate", Status: acme.StatusValid,
Certificate: apiURL + "/certificate",
},
} }
certRes := &Resource{} certRes := &Resource{}
bundle := false bundle := false
valid, err := certifier.checkResponse(order, certRes, bundle) valid, err := certifier.checkResponse(order, certRes, bundle, "")
require.NoError(t, err) require.NoError(t, err)
assert.True(t, valid) assert.True(t, valid)
assert.NotNil(t, certRes) assert.NotNil(t, certRes)
@ -202,6 +284,55 @@ func Test_checkResponse_embeddedIssuer(t *testing.T) {
assert.Equal(t, issuerMock, string(certRes.IssuerCertificate), "IssuerCertificate") assert.Equal(t, issuerMock, string(certRes.IssuerCertificate), "IssuerCertificate")
} }
func Test_checkResponse_alternate(t *testing.T) {
mux, apiURL, tearDown := tester.SetupFakeAPI()
defer tearDown()
mux.HandleFunc("/certificate", func(w http.ResponseWriter, _ *http.Request) {
_, err := w.Write([]byte(certResponseMock))
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
})
mux.HandleFunc("/certificate2", func(w http.ResponseWriter, _ *http.Request) {
_, err := w.Write([]byte(certResponseMock2))
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
})
key, err := rsa.GenerateKey(rand.Reader, 2048)
require.NoError(t, err, "Could not generate test key")
core, err := api.New(http.DefaultClient, "lego-test", apiURL+"/dir", "", key)
require.NoError(t, err)
certifier := NewCertifier(core, &resolverMock{}, CertifierOptions{KeyType: certcrypto.RSA2048})
order := acme.ExtendedOrder{
Order: acme.Order{
Status: acme.StatusValid,
Certificate: apiURL + "/certificate",
},
AlternateChainLinks: []string{apiURL + "/certificate2"},
}
certRes := &Resource{}
bundle := false
valid, err := certifier.checkResponse(order, certRes, bundle, "DST Root CA X3")
require.NoError(t, err)
assert.True(t, valid)
assert.NotNil(t, certRes)
assert.Equal(t, "", certRes.Domain)
assert.Contains(t, certRes.CertStableURL, "/certificate2")
assert.Contains(t, certRes.CertURL, "/certificate2")
assert.Nil(t, certRes.CSR)
assert.Nil(t, certRes.PrivateKey)
assert.Equal(t, certResponseMock2, string(certRes.Certificate), "Certificate")
assert.Equal(t, issuerMock2, string(certRes.IssuerCertificate), "IssuerCertificate")
}
func Test_Get(t *testing.T) { func Test_Get(t *testing.T) {
mux, apiURL, tearDown := tester.SetupFakeAPI() mux, apiURL, tearDown := tester.SetupFakeAPI()
defer tearDown() defer tearDown()

View file

@ -58,6 +58,10 @@ func createRenew() cli.Command {
Name: "renew-hook", Name: "renew-hook",
Usage: "Define a hook. The hook is executed only when the certificates are effectively renewed.", Usage: "Define a hook. The hook is executed only when the certificates are effectively renewed.",
}, },
cli.StringFlag{
Name: "preferred-chain",
Usage: "If the CA offers multiple certificate chains, prefer the chain with an issuer matching this Subject Common Name. If no match, the default offered chain will be used.",
},
}, },
} }
} }
@ -123,10 +127,11 @@ func renewForDomains(ctx *cli.Context, client *lego.Client, certsStorage *Certif
} }
request := certificate.ObtainRequest{ request := certificate.ObtainRequest{
Domains: merge(certDomains, domains), Domains: merge(certDomains, domains),
Bundle: bundle, Bundle: bundle,
PrivateKey: privateKey, PrivateKey: privateKey,
MustStaple: ctx.Bool("must-staple"), MustStaple: ctx.Bool("must-staple"),
PreferredChain: ctx.String("preferred-chain"),
} }
certRes, err := client.Certificate.Obtain(request) certRes, err := client.Certificate.Obtain(request)
if err != nil { if err != nil {
@ -168,7 +173,7 @@ 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()))
certRes, err := client.Certificate.ObtainForCSR(*csr, bundle) certRes, err := client.Certificate.ObtainForCSR(*csr, bundle, ctx.String("preferred-chain"))
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }

View file

@ -43,6 +43,10 @@ func createRun() cli.Command {
Name: "run-hook", Name: "run-hook",
Usage: "Define a hook. The hook is executed when the certificates are effectively created.", Usage: "Define a hook. The hook is executed when the certificates are effectively created.",
}, },
cli.StringFlag{
Name: "preferred-chain",
Usage: "If the CA offers multiple certificate chains, prefer the chain with an issuer matching this Subject Common Name. If no match, the default offered chain will be used.",
},
}, },
} }
} }
@ -159,9 +163,10 @@ func obtainCertificate(ctx *cli.Context, client *lego.Client) (*certificate.Reso
if len(domains) > 0 { if len(domains) > 0 {
// obtain a certificate, generating a new private key // obtain a certificate, generating a new private key
request := certificate.ObtainRequest{ request := certificate.ObtainRequest{
Domains: domains, Domains: domains,
Bundle: bundle, Bundle: bundle,
MustStaple: ctx.Bool("must-staple"), MustStaple: ctx.Bool("must-staple"),
PreferredChain: ctx.String("preferred-chain"),
} }
return client.Certificate.Obtain(request) return client.Certificate.Obtain(request)
} }
@ -173,5 +178,5 @@ func obtainCertificate(ctx *cli.Context, client *lego.Client) (*certificate.Reso
} }
// obtain a certificate for this CSR // obtain a certificate for this CSR
return client.Certificate.ObtainForCSR(*csr, bundle) return client.Certificate.ObtainForCSR(*csr, bundle, ctx.String("preferred-chain"))
} }

View file

@ -307,7 +307,7 @@ func TestChallengeTLS_Client_ObtainForCSR(t *testing.T) {
csr, err := x509.ParseCertificateRequest(csrRaw) csr, err := x509.ParseCertificateRequest(csrRaw)
require.NoError(t, err) require.NoError(t, err)
resource, err := client.Certificate.ObtainForCSR(*csr, true) resource, err := client.Certificate.ObtainForCSR(*csr, true, "")
require.NoError(t, err) require.NoError(t, err)
require.NotNil(t, resource) require.NotNil(t, resource)