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:
parent
dd38dce6aa
commit
30e4987f99
9 changed files with 281 additions and 62 deletions
|
@ -25,41 +25,48 @@ func (o *OrderService) New(domains []string) (acme.ExtendedOrder, error) {
|
|||
}
|
||||
|
||||
return acme.ExtendedOrder{
|
||||
Location: resp.Header.Get("Location"),
|
||||
Order: order,
|
||||
Order: order,
|
||||
Location: resp.Header.Get("Location"),
|
||||
AlternateChainLinks: getLinks(resp.Header, "alternate"),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// 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 {
|
||||
return acme.Order{}, errors.New("order[get]: empty URL")
|
||||
return acme.ExtendedOrder{}, errors.New("order[get]: empty URL")
|
||||
}
|
||||
|
||||
var order acme.Order
|
||||
_, err := o.core.postAsGet(orderURL, &order)
|
||||
resp, err := o.core.postAsGet(orderURL, &order)
|
||||
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.
|
||||
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{
|
||||
Csr: base64.RawURLEncoding.EncodeToString(csr),
|
||||
}
|
||||
|
||||
var order acme.Order
|
||||
_, err := o.core.post(orderURL, csrMsg, &order)
|
||||
resp, err := o.core.post(orderURL, csrMsg, &order)
|
||||
if err != nil {
|
||||
return acme.Order{}, err
|
||||
return acme.ExtendedOrder{}, err
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
|
|
@ -41,6 +41,10 @@ func TestOrderService_New(t *testing.T) {
|
|||
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{
|
||||
Status: acme.StatusValid,
|
||||
Identifiers: order.Identifiers,
|
||||
|
@ -62,6 +66,12 @@ func TestOrderService_New(t *testing.T) {
|
|||
Status: "valid",
|
||||
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)
|
||||
}
|
||||
|
|
|
@ -11,19 +11,30 @@ type service struct {
|
|||
|
||||
// getLink get a rel into the Link header.
|
||||
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 _, m := range linkExpr.FindAllStringSubmatch(link, -1) {
|
||||
if len(m) != 3 {
|
||||
continue
|
||||
}
|
||||
if m[2] == rel {
|
||||
return m[1]
|
||||
links = append(links, m[1])
|
||||
}
|
||||
}
|
||||
}
|
||||
return ""
|
||||
|
||||
return links
|
||||
}
|
||||
|
||||
// getLocation get the value of the header Location.
|
||||
|
|
|
@ -108,6 +108,11 @@ type ExtendedOrder struct {
|
|||
Order
|
||||
// The order URL, contains the value of the response header `Location`
|
||||
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.
|
||||
|
|
|
@ -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.
|
||||
type ObtainRequest struct {
|
||||
Domains []string
|
||||
Bundle bool
|
||||
PrivateKey crypto.PrivateKey
|
||||
MustStaple bool
|
||||
Domains []string
|
||||
Bundle bool
|
||||
PrivateKey crypto.PrivateKey
|
||||
MustStaple bool
|
||||
PreferredChain string
|
||||
}
|
||||
|
||||
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, ", "))
|
||||
|
||||
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 {
|
||||
for _, auth := range authz {
|
||||
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.
|
||||
// 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
|
||||
// start with the common name
|
||||
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, ", "))
|
||||
|
||||
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 {
|
||||
for _, auth := range authz {
|
||||
failures[challenge.GetTargetedDomain(auth)] = err
|
||||
|
@ -198,7 +199,7 @@ func (c *Certifier) ObtainForCSR(csr x509.CertificateRequest, bundle bool) (*Res
|
|||
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 {
|
||||
var err error
|
||||
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 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)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -247,7 +248,7 @@ func (c *Certifier) getForCSR(domains []string, order acme.ExtendedOrder, bundle
|
|||
|
||||
if respOrder.Status == acme.StatusValid {
|
||||
// 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 {
|
||||
return nil, errR
|
||||
}
|
||||
|
@ -268,7 +269,7 @@ func (c *Certifier) getForCSR(domains []string, order acme.ExtendedOrder, bundle
|
|||
return false, errW
|
||||
}
|
||||
|
||||
done, errW := c.checkResponse(ord, certRes, bundle)
|
||||
done, errW := c.checkResponse(ord, certRes, bundle, preferredChain)
|
||||
if errW != nil {
|
||||
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.
|
||||
//
|
||||
// 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)
|
||||
if err != nil || !valid {
|
||||
return valid, err
|
||||
}
|
||||
|
||||
cert, issuer, err := c.core.Certificates.Get(order.Certificate, bundle)
|
||||
if err != nil {
|
||||
return false, err
|
||||
links := append([]string{order.Certificate}, order.AlternateChainLinks...)
|
||||
|
||||
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)
|
||||
|
||||
certRes.IssuerCertificate = issuer
|
||||
certRes.Certificate = cert
|
||||
certRes.CertURL = order.Certificate
|
||||
certRes.CertStableURL = order.Certificate
|
||||
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)
|
||||
|
||||
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.
|
||||
//
|
||||
// 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.
|
||||
// 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.
|
||||
|
@ -364,7 +394,7 @@ func (c *Certifier) Renew(certRes Resource, bundle, mustStaple bool) (*Resource,
|
|||
return nil, errP
|
||||
}
|
||||
|
||||
return c.ObtainForCSR(*csr, bundle)
|
||||
return c.ObtainForCSR(*csr, bundle, preferredChain)
|
||||
}
|
||||
|
||||
var privateKey crypto.PrivateKey
|
||||
|
@ -491,7 +521,22 @@ func (c *Certifier) Get(url string, bundle bool) (*Resource, error) {
|
|||
}, 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 {
|
||||
case acme.StatusValid:
|
||||
return true, nil
|
||||
|
|
|
@ -76,6 +76,82 @@ rzFL1KZfz+HZdnFwFW2T2gVW8L3ii1l9AJDuKzlvjUH3p6bgihVq02sjT8mx+GM2
|
|||
-----END CERTIFICATE-----
|
||||
`
|
||||
|
||||
const certResponseMock2 = `
|
||||
-----BEGIN CERTIFICATE-----
|
||||
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
|
||||
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) {
|
||||
mux, apiURL, tearDown := tester.SetupFakeAPI()
|
||||
defer tearDown()
|
||||
|
@ -95,14 +171,16 @@ func Test_checkResponse(t *testing.T) {
|
|||
|
||||
certifier := NewCertifier(core, &resolverMock{}, CertifierOptions{KeyType: certcrypto.RSA2048})
|
||||
|
||||
order := acme.Order{
|
||||
Status: acme.StatusValid,
|
||||
Certificate: apiURL + "/certificate",
|
||||
order := acme.ExtendedOrder{
|
||||
Order: acme.Order{
|
||||
Status: acme.StatusValid,
|
||||
Certificate: apiURL + "/certificate",
|
||||
},
|
||||
}
|
||||
certRes := &Resource{}
|
||||
bundle := false
|
||||
|
||||
valid, err := certifier.checkResponse(order, certRes, bundle)
|
||||
valid, err := certifier.checkResponse(order, certRes, bundle, "")
|
||||
require.NoError(t, err)
|
||||
assert.True(t, valid)
|
||||
assert.NotNil(t, certRes)
|
||||
|
@ -143,14 +221,16 @@ func Test_checkResponse_issuerRelUp(t *testing.T) {
|
|||
|
||||
certifier := NewCertifier(core, &resolverMock{}, CertifierOptions{KeyType: certcrypto.RSA2048})
|
||||
|
||||
order := acme.Order{
|
||||
Status: acme.StatusValid,
|
||||
Certificate: apiURL + "/certificate",
|
||||
order := acme.ExtendedOrder{
|
||||
Order: acme.Order{
|
||||
Status: acme.StatusValid,
|
||||
Certificate: apiURL + "/certificate",
|
||||
},
|
||||
}
|
||||
certRes := &Resource{}
|
||||
bundle := false
|
||||
|
||||
valid, err := certifier.checkResponse(order, certRes, bundle)
|
||||
valid, err := certifier.checkResponse(order, certRes, bundle, "")
|
||||
require.NoError(t, err)
|
||||
assert.True(t, valid)
|
||||
assert.NotNil(t, certRes)
|
||||
|
@ -182,14 +262,16 @@ func Test_checkResponse_embeddedIssuer(t *testing.T) {
|
|||
|
||||
certifier := NewCertifier(core, &resolverMock{}, CertifierOptions{KeyType: certcrypto.RSA2048})
|
||||
|
||||
order := acme.Order{
|
||||
Status: acme.StatusValid,
|
||||
Certificate: apiURL + "/certificate",
|
||||
order := acme.ExtendedOrder{
|
||||
Order: acme.Order{
|
||||
Status: acme.StatusValid,
|
||||
Certificate: apiURL + "/certificate",
|
||||
},
|
||||
}
|
||||
certRes := &Resource{}
|
||||
bundle := false
|
||||
|
||||
valid, err := certifier.checkResponse(order, certRes, bundle)
|
||||
valid, err := certifier.checkResponse(order, certRes, bundle, "")
|
||||
require.NoError(t, err)
|
||||
assert.True(t, valid)
|
||||
assert.NotNil(t, certRes)
|
||||
|
@ -202,6 +284,55 @@ func Test_checkResponse_embeddedIssuer(t *testing.T) {
|
|||
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) {
|
||||
mux, apiURL, tearDown := tester.SetupFakeAPI()
|
||||
defer tearDown()
|
||||
|
|
|
@ -58,6 +58,10 @@ func createRenew() cli.Command {
|
|||
Name: "renew-hook",
|
||||
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{
|
||||
Domains: merge(certDomains, domains),
|
||||
Bundle: bundle,
|
||||
PrivateKey: privateKey,
|
||||
MustStaple: ctx.Bool("must-staple"),
|
||||
Domains: merge(certDomains, domains),
|
||||
Bundle: bundle,
|
||||
PrivateKey: privateKey,
|
||||
MustStaple: ctx.Bool("must-staple"),
|
||||
PreferredChain: ctx.String("preferred-chain"),
|
||||
}
|
||||
certRes, err := client.Certificate.Obtain(request)
|
||||
if err != nil {
|
||||
|
@ -168,7 +173,7 @@ 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(*csr, bundle)
|
||||
certRes, err := client.Certificate.ObtainForCSR(*csr, bundle, ctx.String("preferred-chain"))
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
|
|
@ -43,6 +43,10 @@ func createRun() cli.Command {
|
|||
Name: "run-hook",
|
||||
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 {
|
||||
// obtain a certificate, generating a new private key
|
||||
request := certificate.ObtainRequest{
|
||||
Domains: domains,
|
||||
Bundle: bundle,
|
||||
MustStaple: ctx.Bool("must-staple"),
|
||||
Domains: domains,
|
||||
Bundle: bundle,
|
||||
MustStaple: ctx.Bool("must-staple"),
|
||||
PreferredChain: ctx.String("preferred-chain"),
|
||||
}
|
||||
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
|
||||
return client.Certificate.ObtainForCSR(*csr, bundle)
|
||||
return client.Certificate.ObtainForCSR(*csr, bundle, ctx.String("preferred-chain"))
|
||||
}
|
||||
|
|
|
@ -307,7 +307,7 @@ func TestChallengeTLS_Client_ObtainForCSR(t *testing.T) {
|
|||
csr, err := x509.ParseCertificateRequest(csrRaw)
|
||||
require.NoError(t, err)
|
||||
|
||||
resource, err := client.Certificate.ObtainForCSR(*csr, true)
|
||||
resource, err := client.Certificate.ObtainForCSR(*csr, true, "")
|
||||
require.NoError(t, err)
|
||||
|
||||
require.NotNil(t, resource)
|
||||
|
|
Loading…
Reference in a new issue