forked from TrueCloudLab/lego
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{
|
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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -55,6 +55,7 @@ type ObtainRequest struct {
|
||||||
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...)
|
||||||
|
|
||||||
|
for i, link := range links {
|
||||||
|
cert, issuer, err := c.core.Certificates.Get(link, bundle)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
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)
|
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.IssuerCertificate = issuer
|
||||||
certRes.Certificate = cert
|
certRes.Certificate = cert
|
||||||
certRes.CertURL = order.Certificate
|
certRes.CertURL = link
|
||||||
certRes.CertStableURL = order.Certificate
|
certRes.CertStableURL = link
|
||||||
|
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
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
|
||||||
|
|
|
@ -76,6 +76,82 @@ rzFL1KZfz+HZdnFwFW2T2gVW8L3ii1l9AJDuKzlvjUH3p6bgihVq02sjT8mx+GM2
|
||||||
-----END CERTIFICATE-----
|
-----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) {
|
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{
|
||||||
|
Order: acme.Order{
|
||||||
Status: acme.StatusValid,
|
Status: acme.StatusValid,
|
||||||
Certificate: apiURL + "/certificate",
|
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{
|
||||||
|
Order: acme.Order{
|
||||||
Status: acme.StatusValid,
|
Status: acme.StatusValid,
|
||||||
Certificate: apiURL + "/certificate",
|
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{
|
||||||
|
Order: acme.Order{
|
||||||
Status: acme.StatusValid,
|
Status: acme.StatusValid,
|
||||||
Certificate: apiURL + "/certificate",
|
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()
|
||||||
|
|
|
@ -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.",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -127,6 +131,7 @@ func renewForDomains(ctx *cli.Context, client *lego.Client, certsStorage *Certif
|
||||||
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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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.",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -162,6 +166,7 @@ func obtainCertificate(ctx *cli.Context, client *lego.Client) (*certificate.Reso
|
||||||
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"))
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
Loading…
Reference in a new issue