forked from TrueCloudLab/lego
feat: replace v1 by v2.
This commit is contained in:
parent
2cdb0e9e8a
commit
76cb7789ce
42 changed files with 559 additions and 3848 deletions
|
@ -7,7 +7,7 @@ import (
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
|
|
||||||
"github.com/xenolf/lego/acmev2"
|
"github.com/xenolf/lego/acme"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Account represents a users local saved credentials
|
// Account represents a users local saved credentials
|
||||||
|
|
|
@ -7,9 +7,6 @@ const (
|
||||||
// HTTP01 is the "http-01" ACME challenge https://github.com/ietf-wg-acme/acme/blob/master/draft-ietf-acme-acme.md#http
|
// HTTP01 is the "http-01" ACME challenge https://github.com/ietf-wg-acme/acme/blob/master/draft-ietf-acme-acme.md#http
|
||||||
// Note: HTTP01ChallengePath returns the URL path to fulfill this challenge
|
// Note: HTTP01ChallengePath returns the URL path to fulfill this challenge
|
||||||
HTTP01 = Challenge("http-01")
|
HTTP01 = Challenge("http-01")
|
||||||
// TLSSNI01 is the "tls-sni-01" ACME challenge https://github.com/ietf-wg-acme/acme/blob/master/draft-ietf-acme-acme.md#tls-with-server-name-indication-tls-sni
|
|
||||||
// Note: TLSSNI01ChallengeCert returns a certificate to fulfill this challenge
|
|
||||||
TLSSNI01 = Challenge("tls-sni-01")
|
|
||||||
// DNS01 is the "dns-01" ACME challenge https://github.com/ietf-wg-acme/acme/blob/master/draft-ietf-acme-acme.md#dns
|
// DNS01 is the "dns-01" ACME challenge https://github.com/ietf-wg-acme/acme/blob/master/draft-ietf-acme-acme.md#dns
|
||||||
// Note: DNS01Record returns a DNS record which will fulfill this challenge
|
// Note: DNS01Record returns a DNS record which will fulfill this challenge
|
||||||
DNS01 = Challenge("dns-01")
|
DNS01 = Challenge("dns-01")
|
||||||
|
|
615
acme/client.go
615
acme/client.go
|
@ -5,13 +5,11 @@ import (
|
||||||
"crypto"
|
"crypto"
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"log"
|
"log"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
|
||||||
"regexp"
|
"regexp"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
@ -82,27 +80,26 @@ func NewClient(caDirURL string, user User, keyType KeyType) (*Client, error) {
|
||||||
return nil, fmt.Errorf("get directory at '%s': %v", caDirURL, err)
|
return nil, fmt.Errorf("get directory at '%s': %v", caDirURL, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if dir.NewRegURL == "" {
|
if dir.NewAccountURL == "" {
|
||||||
return nil, errors.New("directory missing new registration URL")
|
return nil, errors.New("directory missing new registration URL")
|
||||||
}
|
}
|
||||||
if dir.NewAuthzURL == "" {
|
if dir.NewOrderURL == "" {
|
||||||
return nil, errors.New("directory missing new authz URL")
|
return nil, errors.New("directory missing new order URL")
|
||||||
}
|
}
|
||||||
if dir.NewCertURL == "" {
|
/*if dir.RevokeCertURL == "" {
|
||||||
return nil, errors.New("directory missing new certificate URL")
|
|
||||||
}
|
|
||||||
if dir.RevokeCertURL == "" {
|
|
||||||
return nil, errors.New("directory missing revoke certificate URL")
|
return nil, errors.New("directory missing revoke certificate URL")
|
||||||
}
|
}*/
|
||||||
|
|
||||||
jws := &jws{privKey: privKey, directoryURL: caDirURL}
|
jws := &jws{privKey: privKey, getNonceURL: dir.NewNonceURL}
|
||||||
|
if reg := user.GetRegistration(); reg != nil {
|
||||||
|
jws.kid = reg.URI
|
||||||
|
}
|
||||||
|
|
||||||
// REVIEW: best possibility?
|
// REVIEW: best possibility?
|
||||||
// Add all available solvers with the right index as per ACME
|
// Add all available solvers with the right index as per ACME
|
||||||
// spec to this map. Otherwise they won`t be found.
|
// spec to this map. Otherwise they won`t be found.
|
||||||
solvers := make(map[Challenge]solver)
|
solvers := make(map[Challenge]solver)
|
||||||
solvers[HTTP01] = &httpChallenge{jws: jws, validate: validate, provider: &HTTPProviderServer{}}
|
solvers[HTTP01] = &httpChallenge{jws: jws, validate: validate, provider: &HTTPProviderServer{}}
|
||||||
solvers[TLSSNI01] = &tlsSNIChallenge{jws: jws, validate: validate, provider: &TLSProviderServer{}}
|
|
||||||
|
|
||||||
return &Client{directory: dir, user: user, jws: jws, keyType: keyType, solvers: solvers}, nil
|
return &Client{directory: dir, user: user, jws: jws, keyType: keyType, solvers: solvers}, nil
|
||||||
}
|
}
|
||||||
|
@ -112,8 +109,6 @@ func (c *Client) SetChallengeProvider(challenge Challenge, p ChallengeProvider)
|
||||||
switch challenge {
|
switch challenge {
|
||||||
case HTTP01:
|
case HTTP01:
|
||||||
c.solvers[challenge] = &httpChallenge{jws: c.jws, validate: validate, provider: p}
|
c.solvers[challenge] = &httpChallenge{jws: c.jws, validate: validate, provider: p}
|
||||||
case TLSSNI01:
|
|
||||||
c.solvers[challenge] = &tlsSNIChallenge{jws: c.jws, validate: validate, provider: p}
|
|
||||||
case DNS01:
|
case DNS01:
|
||||||
c.solvers[challenge] = &dnsChallenge{jws: c.jws, validate: validate, provider: p}
|
c.solvers[challenge] = &dnsChallenge{jws: c.jws, validate: validate, provider: p}
|
||||||
default:
|
default:
|
||||||
|
@ -141,24 +136,6 @@ func (c *Client) SetHTTPAddress(iface string) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetTLSAddress specifies a custom interface:port to be used for TLS based challenges.
|
|
||||||
// If this option is not used, the default port 443 and all interfaces will be used.
|
|
||||||
// To only specify a port and no interface use the ":port" notation.
|
|
||||||
//
|
|
||||||
// NOTE: This REPLACES any custom TLS-SNI provider previously set by calling
|
|
||||||
// c.SetChallengeProvider with the default TLS-SNI challenge provider.
|
|
||||||
func (c *Client) SetTLSAddress(iface string) error {
|
|
||||||
host, port, err := net.SplitHostPort(iface)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if chlng, ok := c.solvers[TLSSNI01]; ok {
|
|
||||||
chlng.(*tlsSNIChallenge).provider = NewTLSProviderServer(host, port)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ExcludeChallenges explicitly removes challenges from the pool for solving.
|
// ExcludeChallenges explicitly removes challenges from the pool for solving.
|
||||||
func (c *Client) ExcludeChallenges(challenges []Challenge) {
|
func (c *Client) ExcludeChallenges(challenges []Challenge) {
|
||||||
// Loop through all challenges and delete the requested one if found.
|
// Loop through all challenges and delete the requested one if found.
|
||||||
|
@ -167,61 +144,124 @@ func (c *Client) ExcludeChallenges(challenges []Challenge) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetToSURL returns the current ToS URL from the Directory
|
||||||
|
func (c *Client) GetToSURL() string {
|
||||||
|
return c.directory.Meta.TermsOfService
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetExternalAccountRequired returns the External Account Binding requirement of the Directory
|
||||||
|
func (c *Client) GetExternalAccountRequired() bool {
|
||||||
|
return c.directory.Meta.ExternalAccountRequired
|
||||||
|
}
|
||||||
|
|
||||||
// Register the current account to the ACME server.
|
// Register the current account to the ACME server.
|
||||||
func (c *Client) Register() (*RegistrationResource, error) {
|
func (c *Client) Register(tosAgreed bool) (*RegistrationResource, error) {
|
||||||
if c == nil || c.user == nil {
|
if c == nil || c.user == nil {
|
||||||
return nil, errors.New("acme: cannot register a nil client or user")
|
return nil, errors.New("acme: cannot register a nil client or user")
|
||||||
}
|
}
|
||||||
logf("[INFO] acme: Registering account for %s", c.user.GetEmail())
|
logf("[INFO] acme: Registering account for %s", c.user.GetEmail())
|
||||||
|
|
||||||
regMsg := registrationMessage{
|
accMsg := accountMessage{}
|
||||||
Resource: "new-reg",
|
|
||||||
}
|
|
||||||
if c.user.GetEmail() != "" {
|
if c.user.GetEmail() != "" {
|
||||||
regMsg.Contact = []string{"mailto:" + c.user.GetEmail()}
|
accMsg.Contact = []string{"mailto:" + c.user.GetEmail()}
|
||||||
} else {
|
} else {
|
||||||
regMsg.Contact = []string{}
|
accMsg.Contact = []string{}
|
||||||
}
|
}
|
||||||
|
accMsg.TermsOfServiceAgreed = tosAgreed
|
||||||
|
|
||||||
var serverReg Registration
|
var serverReg accountMessage
|
||||||
var regURI string
|
hdr, err := postJSON(c.jws, c.directory.NewAccountURL, accMsg, &serverReg)
|
||||||
hdr, err := postJSON(c.jws, c.directory.NewRegURL, regMsg, &serverReg)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
remoteErr, ok := err.(RemoteError)
|
remoteErr, ok := err.(RemoteError)
|
||||||
if ok && remoteErr.StatusCode == 409 {
|
if ok && remoteErr.StatusCode == 409 {
|
||||||
regURI = hdr.Get("Location")
|
|
||||||
regMsg = registrationMessage{
|
|
||||||
Resource: "reg",
|
|
||||||
}
|
|
||||||
if hdr, err = postJSON(c.jws, regURI, regMsg, &serverReg); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
reg := &RegistrationResource{Body: serverReg}
|
reg := &RegistrationResource{
|
||||||
|
URI: hdr.Get("Location"),
|
||||||
links := parseLinks(hdr["Link"])
|
Body: serverReg,
|
||||||
|
|
||||||
if regURI == "" {
|
|
||||||
regURI = hdr.Get("Location")
|
|
||||||
}
|
|
||||||
reg.URI = regURI
|
|
||||||
if links["terms-of-service"] != "" {
|
|
||||||
reg.TosURL = links["terms-of-service"]
|
|
||||||
}
|
|
||||||
|
|
||||||
if links["next"] != "" {
|
|
||||||
reg.NewAuthzURL = links["next"]
|
|
||||||
} else {
|
|
||||||
return nil, errors.New("acme: The server did not return 'next' link to proceed")
|
|
||||||
}
|
}
|
||||||
|
c.jws.kid = reg.URI
|
||||||
|
|
||||||
return reg, nil
|
return reg, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Register the current account to the ACME server.
|
||||||
|
func (c *Client) RegisterWithExternalAccountBinding(tosAgreed bool, kid string, hmacEncoded string) (*RegistrationResource, error) {
|
||||||
|
if c == nil || c.user == nil {
|
||||||
|
return nil, errors.New("acme: cannot register a nil client or user")
|
||||||
|
}
|
||||||
|
logf("[INFO] acme: Registering account (EAB) for %s", c.user.GetEmail())
|
||||||
|
|
||||||
|
accMsg := accountMessage{}
|
||||||
|
if c.user.GetEmail() != "" {
|
||||||
|
accMsg.Contact = []string{"mailto:" + c.user.GetEmail()}
|
||||||
|
} else {
|
||||||
|
accMsg.Contact = []string{}
|
||||||
|
}
|
||||||
|
accMsg.TermsOfServiceAgreed = tosAgreed
|
||||||
|
|
||||||
|
hmac, err := base64.RawURLEncoding.DecodeString(hmacEncoded)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("acme: could not decode hmac key: %s", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
eabJWS, err := c.jws.signEABContent(c.directory.NewAccountURL, kid, hmac)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("acme: error signing eab content: %s", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
eabPayload := eabJWS.FullSerialize()
|
||||||
|
|
||||||
|
accMsg.ExternalAccountBinding = []byte(eabPayload)
|
||||||
|
|
||||||
|
var serverReg accountMessage
|
||||||
|
hdr, err := postJSON(c.jws, c.directory.NewAccountURL, accMsg, &serverReg)
|
||||||
|
if err != nil {
|
||||||
|
remoteErr, ok := err.(RemoteError)
|
||||||
|
if ok && remoteErr.StatusCode == 409 {
|
||||||
|
} else {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
reg := &RegistrationResource{
|
||||||
|
URI: hdr.Get("Location"),
|
||||||
|
Body: serverReg,
|
||||||
|
}
|
||||||
|
c.jws.kid = reg.URI
|
||||||
|
|
||||||
|
return reg, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResolveAccountByKey will attempt to look up an account using the given account key
|
||||||
|
// and return its registration resource.
|
||||||
|
func (c *Client) ResolveAccountByKey() (*RegistrationResource, error) {
|
||||||
|
logf("[INFO] acme: Trying to resolve account by key")
|
||||||
|
|
||||||
|
acc := accountMessage{OnlyReturnExisting: true}
|
||||||
|
hdr, err := postJSON(c.jws, c.directory.NewAccountURL, acc, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
accountLink := hdr.Get("Location")
|
||||||
|
if accountLink == "" {
|
||||||
|
return nil, errors.New("Server did not return the account link")
|
||||||
|
}
|
||||||
|
|
||||||
|
var retAccount accountMessage
|
||||||
|
c.jws.kid = accountLink
|
||||||
|
hdr, err = postJSON(c.jws, accountLink, accountMessage{}, &retAccount)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &RegistrationResource{URI: accountLink, Body: retAccount}, nil
|
||||||
|
}
|
||||||
|
|
||||||
// DeleteRegistration deletes the client's user registration from the ACME
|
// DeleteRegistration deletes the client's user registration from the ACME
|
||||||
// server.
|
// server.
|
||||||
func (c *Client) DeleteRegistration() error {
|
func (c *Client) DeleteRegistration() error {
|
||||||
|
@ -230,12 +270,11 @@ func (c *Client) DeleteRegistration() error {
|
||||||
}
|
}
|
||||||
logf("[INFO] acme: Deleting account for %s", c.user.GetEmail())
|
logf("[INFO] acme: Deleting account for %s", c.user.GetEmail())
|
||||||
|
|
||||||
regMsg := registrationMessage{
|
accMsg := accountMessage{
|
||||||
Resource: "reg",
|
Status: "deactivated",
|
||||||
Delete: true,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err := postJSON(c.jws, c.user.GetRegistration().URI, regMsg, nil)
|
_, err := postJSON(c.jws, c.user.GetRegistration().URI, accMsg, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -255,46 +294,23 @@ func (c *Client) QueryRegistration() (*RegistrationResource, error) {
|
||||||
// Log the URL here instead of the email as the email may not be set
|
// Log the URL here instead of the email as the email may not be set
|
||||||
logf("[INFO] acme: Querying account for %s", c.user.GetRegistration().URI)
|
logf("[INFO] acme: Querying account for %s", c.user.GetRegistration().URI)
|
||||||
|
|
||||||
regMsg := registrationMessage{
|
accMsg := accountMessage{}
|
||||||
Resource: "reg",
|
|
||||||
}
|
|
||||||
|
|
||||||
var serverReg Registration
|
var serverReg accountMessage
|
||||||
hdr, err := postJSON(c.jws, c.user.GetRegistration().URI, regMsg, &serverReg)
|
_, err := postJSON(c.jws, c.user.GetRegistration().URI, accMsg, &serverReg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
reg := &RegistrationResource{Body: serverReg}
|
reg := &RegistrationResource{Body: serverReg}
|
||||||
|
|
||||||
links := parseLinks(hdr["Link"])
|
|
||||||
// Location: header is not returned so this needs to be populated off of
|
// Location: header is not returned so this needs to be populated off of
|
||||||
// existing URI
|
// existing URI
|
||||||
reg.URI = c.user.GetRegistration().URI
|
reg.URI = c.user.GetRegistration().URI
|
||||||
if links["terms-of-service"] != "" {
|
|
||||||
reg.TosURL = links["terms-of-service"]
|
|
||||||
}
|
|
||||||
|
|
||||||
if links["next"] != "" {
|
|
||||||
reg.NewAuthzURL = links["next"]
|
|
||||||
} else {
|
|
||||||
return nil, errors.New("acme: No new-authz link in response to registration query")
|
|
||||||
}
|
|
||||||
|
|
||||||
return reg, nil
|
return reg, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// AgreeToTOS updates the Client registration and sends the agreement to
|
|
||||||
// the server.
|
|
||||||
func (c *Client) AgreeToTOS() error {
|
|
||||||
reg := c.user.GetRegistration()
|
|
||||||
|
|
||||||
reg.Body.Agreement = c.user.GetRegistration().TosURL
|
|
||||||
reg.Body.Resource = "reg"
|
|
||||||
_, err := postJSON(c.jws, c.user.GetRegistration().URI, c.user.GetRegistration().Body, nil)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// ObtainCertificateForCSR tries to obtain a certificate matching the CSR passed into it.
|
// ObtainCertificateForCSR tries to obtain a certificate matching the CSR passed into it.
|
||||||
// The domains are inferred from the CommonName and SubjectAltNames, if any. The private key
|
// The domains are inferred from the CommonName and SubjectAltNames, if any. The private key
|
||||||
// for this CSR is not required.
|
// for this CSR is not required.
|
||||||
|
@ -302,7 +318,7 @@ func (c *Client) AgreeToTOS() error {
|
||||||
// your issued certificate as a bundle.
|
// your issued certificate as a bundle.
|
||||||
// This function will never return a partial certificate. If one domain in the list fails,
|
// This function will never return a partial certificate. If one domain in the list fails,
|
||||||
// the whole certificate will fail.
|
// the whole certificate will fail.
|
||||||
func (c *Client) ObtainCertificateForCSR(csr x509.CertificateRequest, bundle bool) (CertificateResource, map[string]error) {
|
func (c *Client) ObtainCertificateForCSR(csr x509.CertificateRequest, bundle bool) (*CertificateResource, error) {
|
||||||
// figure out what domains it concerns
|
// figure out what domains it concerns
|
||||||
// start with the common name
|
// start with the common name
|
||||||
domains := []string{csr.Subject.CommonName}
|
domains := []string{csr.Subject.CommonName}
|
||||||
|
@ -327,35 +343,44 @@ DNSNames:
|
||||||
logf("[INFO][%s] acme: Obtaining SAN certificate given a CSR", strings.Join(domains, ", "))
|
logf("[INFO][%s] acme: Obtaining SAN certificate given a CSR", strings.Join(domains, ", "))
|
||||||
}
|
}
|
||||||
|
|
||||||
challenges, failures := c.getChallenges(domains)
|
order, err := c.createOrderForIdentifiers(domains)
|
||||||
// If any challenge fails - return. Do not generate partial SAN certificates.
|
if err != nil {
|
||||||
if len(failures) > 0 {
|
return nil, err
|
||||||
for _, auth := range challenges {
|
}
|
||||||
|
authz, err := c.getAuthzForOrder(order)
|
||||||
|
if err != nil {
|
||||||
|
// If any challenge fails, return. Do not generate partial SAN certificates.
|
||||||
|
/*for _, auth := range authz {
|
||||||
c.disableAuthz(auth)
|
c.disableAuthz(auth)
|
||||||
}
|
}*/
|
||||||
|
return nil, err
|
||||||
return CertificateResource{}, failures
|
|
||||||
}
|
}
|
||||||
|
|
||||||
errs := c.solveChallenges(challenges)
|
err = c.solveChallengeForAuthz(authz)
|
||||||
// If any challenge fails - return. Do not generate partial SAN certificates.
|
if err != nil {
|
||||||
if len(errs) > 0 {
|
// If any challenge fails, return. Do not generate partial SAN certificates.
|
||||||
return CertificateResource{}, errs
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
logf("[INFO][%s] acme: Validations succeeded; requesting certificates", strings.Join(domains, ", "))
|
logf("[INFO][%s] acme: Validations succeeded; requesting certificates", strings.Join(domains, ", "))
|
||||||
|
|
||||||
cert, err := c.requestCertificateForCsr(challenges, bundle, csr.Raw, nil)
|
failures := make(ObtainError)
|
||||||
|
cert, err := c.requestCertificateForCsr(order, bundle, csr.Raw, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
for _, chln := range challenges {
|
for _, chln := range authz {
|
||||||
failures[chln.Domain] = err
|
failures[chln.Identifier.Value] = err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add the CSR to the certificate so that it can be used for renewals.
|
// Add the CSR to the certificate so that it can be used for renewals.
|
||||||
cert.CSR = pemEncode(&csr)
|
cert.CSR = pemEncode(&csr)
|
||||||
|
|
||||||
return cert, failures
|
// do not return an empty failures map, because
|
||||||
|
// it would still be a non-nil error value
|
||||||
|
if len(failures) > 0 {
|
||||||
|
return cert, failures
|
||||||
|
}
|
||||||
|
return cert, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ObtainCertificate tries to obtain a single certificate using all domains passed into it.
|
// ObtainCertificate tries to obtain a single certificate using all domains passed into it.
|
||||||
|
@ -367,39 +392,52 @@ DNSNames:
|
||||||
// your issued certificate as a bundle.
|
// your issued certificate as a bundle.
|
||||||
// This function will never return a partial certificate. If one domain in the list fails,
|
// This function will never return a partial certificate. If one domain in the list fails,
|
||||||
// the whole certificate will fail.
|
// the whole certificate will fail.
|
||||||
func (c *Client) ObtainCertificate(domains []string, bundle bool, privKey crypto.PrivateKey, mustStaple bool) (CertificateResource, map[string]error) {
|
func (c *Client) ObtainCertificate(domains []string, bundle bool, privKey crypto.PrivateKey, mustStaple bool) (*CertificateResource, error) {
|
||||||
|
if len(domains) == 0 {
|
||||||
|
return nil, errors.New("No domains to obtain a certificate for")
|
||||||
|
}
|
||||||
|
|
||||||
if bundle {
|
if bundle {
|
||||||
logf("[INFO][%s] acme: Obtaining bundled SAN certificate", strings.Join(domains, ", "))
|
logf("[INFO][%s] acme: Obtaining bundled SAN certificate", strings.Join(domains, ", "))
|
||||||
} else {
|
} else {
|
||||||
logf("[INFO][%s] acme: Obtaining SAN certificate", strings.Join(domains, ", "))
|
logf("[INFO][%s] acme: Obtaining SAN certificate", strings.Join(domains, ", "))
|
||||||
}
|
}
|
||||||
|
|
||||||
challenges, failures := c.getChallenges(domains)
|
order, err := c.createOrderForIdentifiers(domains)
|
||||||
// If any challenge fails - return. Do not generate partial SAN certificates.
|
if err != nil {
|
||||||
if len(failures) > 0 {
|
return nil, err
|
||||||
for _, auth := range challenges {
|
}
|
||||||
|
authz, err := c.getAuthzForOrder(order)
|
||||||
|
if err != nil {
|
||||||
|
// If any challenge fails, return. Do not generate partial SAN certificates.
|
||||||
|
/*for _, auth := range authz {
|
||||||
c.disableAuthz(auth)
|
c.disableAuthz(auth)
|
||||||
}
|
}*/
|
||||||
|
return nil, err
|
||||||
return CertificateResource{}, failures
|
|
||||||
}
|
}
|
||||||
|
|
||||||
errs := c.solveChallenges(challenges)
|
err = c.solveChallengeForAuthz(authz)
|
||||||
// If any challenge fails - return. Do not generate partial SAN certificates.
|
if err != nil {
|
||||||
if len(errs) > 0 {
|
// If any challenge fails, return. Do not generate partial SAN certificates.
|
||||||
return CertificateResource{}, errs
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
logf("[INFO][%s] acme: Validations succeeded; requesting certificates", strings.Join(domains, ", "))
|
logf("[INFO][%s] acme: Validations succeeded; requesting certificates", strings.Join(domains, ", "))
|
||||||
|
|
||||||
cert, err := c.requestCertificate(challenges, bundle, privKey, mustStaple)
|
failures := make(ObtainError)
|
||||||
|
cert, err := c.requestCertificateForOrder(order, bundle, privKey, mustStaple)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
for _, chln := range challenges {
|
for _, auth := range authz {
|
||||||
failures[chln.Domain] = err
|
failures[auth.Identifier.Value] = err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return cert, failures
|
// do not return an empty failures map, because
|
||||||
|
// it would still be a non-nil error value
|
||||||
|
if len(failures) > 0 {
|
||||||
|
return cert, failures
|
||||||
|
}
|
||||||
|
return cert, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// RevokeCertificate takes a PEM encoded certificate or bundle and tries to revoke it at the CA.
|
// RevokeCertificate takes a PEM encoded certificate or bundle and tries to revoke it at the CA.
|
||||||
|
@ -416,7 +454,7 @@ func (c *Client) RevokeCertificate(certificate []byte) error {
|
||||||
|
|
||||||
encodedCert := base64.URLEncoding.EncodeToString(x509Cert.Raw)
|
encodedCert := base64.URLEncoding.EncodeToString(x509Cert.Raw)
|
||||||
|
|
||||||
_, err = postJSON(c.jws, c.directory.RevokeCertURL, revokeCertMessage{Resource: "revoke-cert", Certificate: encodedCert}, nil)
|
_, err = postJSON(c.jws, c.directory.RevokeCertURL, revokeCertMessage{Certificate: encodedCert}, nil)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -428,17 +466,17 @@ func (c *Client) RevokeCertificate(certificate []byte) error {
|
||||||
// If bundle is true, the []byte contains both the issuer certificate and
|
// If bundle is true, the []byte contains both the issuer certificate and
|
||||||
// your issued certificate as a bundle.
|
// your issued certificate as a bundle.
|
||||||
// For private key reuse the PrivateKey property of the passed in CertificateResource should be non-nil.
|
// For private key reuse the PrivateKey property of the passed in CertificateResource should be non-nil.
|
||||||
func (c *Client) RenewCertificate(cert CertificateResource, bundle, mustStaple bool) (CertificateResource, error) {
|
func (c *Client) RenewCertificate(cert CertificateResource, bundle, mustStaple bool) (*CertificateResource, error) {
|
||||||
// Input certificate is PEM encoded. Decode it here as we may need the decoded
|
// 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.
|
// cert later on in the renewal process. The input may be a bundle or a single certificate.
|
||||||
certificates, err := parsePEMBundle(cert.Certificate)
|
certificates, err := parsePEMBundle(cert.Certificate)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return CertificateResource{}, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
x509Cert := certificates[0]
|
x509Cert := certificates[0]
|
||||||
if x509Cert.IsCA {
|
if x509Cert.IsCA {
|
||||||
return CertificateResource{}, fmt.Errorf("[%s] Certificate bundle starts with a CA certificate", cert.Domain)
|
return nil, fmt.Errorf("[%s] Certificate bundle starts with a CA certificate", cert.Domain)
|
||||||
}
|
}
|
||||||
|
|
||||||
// This is just meant to be informal for the user.
|
// This is just meant to be informal for the user.
|
||||||
|
@ -451,22 +489,21 @@ func (c *Client) RenewCertificate(cert CertificateResource, bundle, mustStaple b
|
||||||
if len(cert.CSR) > 0 {
|
if len(cert.CSR) > 0 {
|
||||||
csr, err := pemDecodeTox509CSR(cert.CSR)
|
csr, err := pemDecodeTox509CSR(cert.CSR)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return CertificateResource{}, err
|
return nil, err
|
||||||
}
|
}
|
||||||
newCert, failures := c.ObtainCertificateForCSR(*csr, bundle)
|
newCert, failures := c.ObtainCertificateForCSR(*csr, bundle)
|
||||||
return newCert, failures[cert.Domain]
|
return newCert, failures
|
||||||
}
|
}
|
||||||
|
|
||||||
var privKey crypto.PrivateKey
|
var privKey crypto.PrivateKey
|
||||||
if cert.PrivateKey != nil {
|
if cert.PrivateKey != nil {
|
||||||
privKey, err = parsePEMPrivateKey(cert.PrivateKey)
|
privKey, err = parsePEMPrivateKey(cert.PrivateKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return CertificateResource{}, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var domains []string
|
var domains []string
|
||||||
var failures map[string]error
|
|
||||||
// check for SAN certificate
|
// check for SAN certificate
|
||||||
if len(x509Cert.DNSNames) > 1 {
|
if len(x509Cert.DNSNames) > 1 {
|
||||||
domains = append(domains, x509Cert.Subject.CommonName)
|
domains = append(domains, x509Cert.Subject.CommonName)
|
||||||
|
@ -480,233 +517,242 @@ func (c *Client) RenewCertificate(cert CertificateResource, bundle, mustStaple b
|
||||||
domains = append(domains, x509Cert.Subject.CommonName)
|
domains = append(domains, x509Cert.Subject.CommonName)
|
||||||
}
|
}
|
||||||
|
|
||||||
newCert, failures := c.ObtainCertificate(domains, bundle, privKey, mustStaple)
|
newCert, err := c.ObtainCertificate(domains, bundle, privKey, mustStaple)
|
||||||
return newCert, failures[cert.Domain]
|
return newCert, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) createOrderForIdentifiers(domains []string) (orderResource, error) {
|
||||||
|
|
||||||
|
var identifiers []identifier
|
||||||
|
for _, domain := range domains {
|
||||||
|
identifiers = append(identifiers, identifier{Type: "dns", Value: domain})
|
||||||
|
}
|
||||||
|
|
||||||
|
order := orderMessage{
|
||||||
|
Identifiers: identifiers,
|
||||||
|
}
|
||||||
|
|
||||||
|
var response orderMessage
|
||||||
|
hdr, err := postJSON(c.jws, c.directory.NewOrderURL, order, &response)
|
||||||
|
if err != nil {
|
||||||
|
return orderResource{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
orderRes := orderResource{
|
||||||
|
URL: hdr.Get("Location"),
|
||||||
|
Domains: domains,
|
||||||
|
orderMessage: response,
|
||||||
|
}
|
||||||
|
return orderRes, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Looks through the challenge combinations to find a solvable match.
|
// Looks through the challenge combinations to find a solvable match.
|
||||||
// Then solves the challenges in series and returns.
|
// Then solves the challenges in series and returns.
|
||||||
func (c *Client) solveChallenges(challenges []authorizationResource) map[string]error {
|
func (c *Client) solveChallengeForAuthz(authorizations []authorization) error {
|
||||||
|
failures := make(ObtainError)
|
||||||
|
|
||||||
// loop through the resources, basically through the domains.
|
// loop through the resources, basically through the domains.
|
||||||
failures := make(map[string]error)
|
for _, authz := range authorizations {
|
||||||
for _, authz := range challenges {
|
if authz.Status == "valid" {
|
||||||
if authz.Body.Status == "valid" {
|
|
||||||
// Boulder might recycle recent validated authz (see issue #267)
|
// Boulder might recycle recent validated authz (see issue #267)
|
||||||
logf("[INFO][%s] acme: Authorization already valid; skipping challenge", authz.Domain)
|
logf("[INFO][%s] acme: Authorization already valid; skipping challenge", authz.Identifier.Value)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// no solvers - no solving
|
// no solvers - no solving
|
||||||
if solvers := c.chooseSolvers(authz.Body, authz.Domain); solvers != nil {
|
if i, solver := c.chooseSolver(authz, authz.Identifier.Value); solver != nil {
|
||||||
for i, solver := range solvers {
|
err := solver.Solve(authz.Challenges[i], authz.Identifier.Value)
|
||||||
// TODO: do not immediately fail if one domain fails to validate.
|
if err != nil {
|
||||||
err := solver.Solve(authz.Body.Challenges[i], authz.Domain)
|
//c.disableAuthz(authz.Identifier)
|
||||||
if err != nil {
|
failures[authz.Identifier.Value] = err
|
||||||
c.disableAuthz(authz)
|
|
||||||
failures[authz.Domain] = err
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
c.disableAuthz(authz)
|
//c.disableAuthz(authz)
|
||||||
failures[authz.Domain] = fmt.Errorf("[%s] acme: Could not determine solvers", authz.Domain)
|
failures[authz.Identifier.Value] = fmt.Errorf("[%s] acme: Could not determine solvers", authz.Identifier.Value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return failures
|
// be careful not to return an empty failures map, for
|
||||||
}
|
// even an empty ObtainError is a non-nil error value
|
||||||
|
if len(failures) > 0 {
|
||||||
// Checks all combinations from the server and returns an array of
|
return failures
|
||||||
// solvers which should get executed in series.
|
|
||||||
func (c *Client) chooseSolvers(auth authorization, domain string) map[int]solver {
|
|
||||||
for _, combination := range auth.Combinations {
|
|
||||||
solvers := make(map[int]solver)
|
|
||||||
for _, idx := range combination {
|
|
||||||
if solver, ok := c.solvers[auth.Challenges[idx].Type]; ok {
|
|
||||||
solvers[idx] = solver
|
|
||||||
} else {
|
|
||||||
logf("[INFO][%s] acme: Could not find solver for: %s", domain, auth.Challenges[idx].Type)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we can solve the whole combination, return the solvers
|
|
||||||
if len(solvers) == len(combination) {
|
|
||||||
return solvers
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Checks all challenges from the server in order and returns the first matching solver.
|
||||||
|
func (c *Client) chooseSolver(auth authorization, domain string) (int, solver) {
|
||||||
|
for i, challenge := range auth.Challenges {
|
||||||
|
if solver, ok := c.solvers[Challenge(challenge.Type)]; ok {
|
||||||
|
return i, solver
|
||||||
|
}
|
||||||
|
logf("[INFO][%s] acme: Could not find solver for: %s", domain, challenge.Type)
|
||||||
|
}
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
// Get the challenges needed to proof our identifier to the ACME server.
|
// Get the challenges needed to proof our identifier to the ACME server.
|
||||||
func (c *Client) getChallenges(domains []string) ([]authorizationResource, map[string]error) {
|
func (c *Client) getAuthzForOrder(order orderResource) ([]authorization, error) {
|
||||||
resc, errc := make(chan authorizationResource), make(chan domainError)
|
resc, errc := make(chan authorization), make(chan domainError)
|
||||||
|
|
||||||
delay := time.Second / overallRequestLimit
|
delay := time.Second / overallRequestLimit
|
||||||
|
|
||||||
for _, domain := range domains {
|
for _, authzURL := range order.Authorizations {
|
||||||
time.Sleep(delay)
|
time.Sleep(delay)
|
||||||
|
|
||||||
go func(domain string) {
|
go func(authzURL string) {
|
||||||
authMsg := authorization{Resource: "new-authz", Identifier: identifier{Type: "dns", Value: domain}}
|
|
||||||
var authz authorization
|
var authz authorization
|
||||||
hdr, err := postJSON(c.jws, c.user.GetRegistration().NewAuthzURL, authMsg, &authz)
|
_, err := getJSON(authzURL, &authz)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errc <- domainError{Domain: domain, Error: err}
|
errc <- domainError{Domain: authz.Identifier.Value, Error: err}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
links := parseLinks(hdr["Link"])
|
resc <- authz
|
||||||
if links["next"] == "" {
|
}(authzURL)
|
||||||
logf("[ERROR][%s] acme: Server did not provide next link to proceed", domain)
|
|
||||||
errc <- domainError{Domain: domain, Error: errors.New("Server did not provide next link to proceed")}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
resc <- authorizationResource{Body: authz, NewCertURL: links["next"], AuthURL: hdr.Get("Location"), Domain: domain}
|
|
||||||
}(domain)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
responses := make(map[string]authorizationResource)
|
var responses []authorization
|
||||||
failures := make(map[string]error)
|
failures := make(ObtainError)
|
||||||
for i := 0; i < len(domains); i++ {
|
for i := 0; i < len(order.Authorizations); i++ {
|
||||||
select {
|
select {
|
||||||
case res := <-resc:
|
case res := <-resc:
|
||||||
responses[res.Domain] = res
|
responses = append(responses, res)
|
||||||
case err := <-errc:
|
case err := <-errc:
|
||||||
failures[err.Domain] = err.Error
|
failures[err.Domain] = err.Error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
challenges := make([]authorizationResource, 0, len(responses))
|
logAuthz(order)
|
||||||
for _, domain := range domains {
|
|
||||||
if challenge, ok := responses[domain]; ok {
|
|
||||||
challenges = append(challenges, challenge)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
logAuthz(challenges)
|
|
||||||
|
|
||||||
close(resc)
|
close(resc)
|
||||||
close(errc)
|
close(errc)
|
||||||
|
|
||||||
return challenges, failures
|
// be careful to not return an empty failures map;
|
||||||
|
// even if empty, they become non-nil error values
|
||||||
|
if len(failures) > 0 {
|
||||||
|
return responses, failures
|
||||||
|
}
|
||||||
|
return responses, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func logAuthz(authz []authorizationResource) {
|
func logAuthz(order orderResource) {
|
||||||
for _, auth := range authz {
|
for i, auth := range order.Authorizations {
|
||||||
logf("[INFO][%s] AuthURL: %s", auth.Domain, auth.AuthURL)
|
logf("[INFO][%s] AuthURL: %s", order.Identifiers[i].Value, auth)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// cleanAuthz loops through the passed in slice and disables any auths which are not "valid"
|
// cleanAuthz loops through the passed in slice and disables any auths which are not "valid"
|
||||||
func (c *Client) disableAuthz(auth authorizationResource) error {
|
func (c *Client) disableAuthz(authURL string) error {
|
||||||
var disabledAuth authorization
|
var disabledAuth authorization
|
||||||
_, err := postJSON(c.jws, auth.AuthURL, deactivateAuthMessage{Resource: "authz", Status: "deactivated"}, &disabledAuth)
|
_, err := postJSON(c.jws, authURL, deactivateAuthMessage{Status: "deactivated"}, &disabledAuth)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) requestCertificate(authz []authorizationResource, bundle bool, privKey crypto.PrivateKey, mustStaple bool) (CertificateResource, error) {
|
func (c *Client) requestCertificateForOrder(order orderResource, bundle bool, privKey crypto.PrivateKey, mustStaple bool) (*CertificateResource, error) {
|
||||||
if len(authz) == 0 {
|
|
||||||
return CertificateResource{}, errors.New("Passed no authorizations to requestCertificate!")
|
|
||||||
}
|
|
||||||
|
|
||||||
var err error
|
var err error
|
||||||
if privKey == nil {
|
if privKey == nil {
|
||||||
privKey, err = generatePrivateKey(c.keyType)
|
privKey, err = generatePrivateKey(c.keyType)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return CertificateResource{}, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// determine certificate name(s) based on the authorization resources
|
// determine certificate name(s) based on the authorization resources
|
||||||
commonName := authz[0]
|
commonName := order.Domains[0]
|
||||||
var san []string
|
var san []string
|
||||||
for _, auth := range authz[1:] {
|
for _, auth := range order.Identifiers {
|
||||||
san = append(san, auth.Domain)
|
san = append(san, auth.Value)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: should the CSR be customizable?
|
// TODO: should the CSR be customizable?
|
||||||
csr, err := generateCsr(privKey, commonName.Domain, san, mustStaple)
|
csr, err := generateCsr(privKey, commonName, san, mustStaple)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return CertificateResource{}, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return c.requestCertificateForCsr(authz, bundle, csr, pemEncode(privKey))
|
return c.requestCertificateForCsr(order, bundle, csr, pemEncode(privKey))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) requestCertificateForCsr(authz []authorizationResource, bundle bool, csr []byte, privateKeyPem []byte) (CertificateResource, error) {
|
func (c *Client) requestCertificateForCsr(order orderResource, bundle bool, csr []byte, privateKeyPem []byte) (*CertificateResource, error) {
|
||||||
commonName := authz[0]
|
commonName := order.Domains[0]
|
||||||
|
|
||||||
var authURLs []string
|
csrString := base64.RawURLEncoding.EncodeToString(csr)
|
||||||
for _, auth := range authz[1:] {
|
var retOrder orderMessage
|
||||||
authURLs = append(authURLs, auth.AuthURL)
|
_, error := postJSON(c.jws, order.Finalize, csrMessage{Csr: csrString}, &retOrder)
|
||||||
|
if error != nil {
|
||||||
|
return nil, error
|
||||||
}
|
}
|
||||||
|
|
||||||
csrString := base64.URLEncoding.EncodeToString(csr)
|
if retOrder.Status == "invalid" {
|
||||||
jsonBytes, err := json.Marshal(csrMessage{Resource: "new-cert", Csr: csrString, Authorizations: authURLs})
|
return nil, error
|
||||||
if err != nil {
|
|
||||||
return CertificateResource{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, err := c.jws.post(commonName.NewCertURL, jsonBytes)
|
|
||||||
if err != nil {
|
|
||||||
return CertificateResource{}, err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
certRes := CertificateResource{
|
certRes := CertificateResource{
|
||||||
Domain: commonName.Domain,
|
Domain: commonName,
|
||||||
CertURL: resp.Header.Get("Location"),
|
CertURL: retOrder.Certificate,
|
||||||
PrivateKey: privateKeyPem,
|
PrivateKey: privateKeyPem,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if retOrder.Status == "valid" {
|
||||||
|
// if the certificate is available right away, short cut!
|
||||||
|
ok, err := c.checkCertResponse(retOrder, &certRes, bundle)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if ok {
|
||||||
|
return &certRes, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
maxChecks := 1000
|
maxChecks := 1000
|
||||||
for i := 0; i < maxChecks; i++ {
|
for i := 0; i < maxChecks; i++ {
|
||||||
done, err := c.checkCertResponse(resp, &certRes, bundle)
|
_, err := getJSON(order.URL, &retOrder)
|
||||||
resp.Body.Close()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return CertificateResource{}, err
|
return nil, err
|
||||||
|
}
|
||||||
|
done, err := c.checkCertResponse(retOrder, &certRes, bundle)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
}
|
}
|
||||||
if done {
|
if done {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
if i == maxChecks-1 {
|
if i == maxChecks-1 {
|
||||||
return CertificateResource{}, fmt.Errorf("polled for certificate %d times; giving up", i)
|
return nil, fmt.Errorf("polled for certificate %d times; giving up", i)
|
||||||
}
|
|
||||||
resp, err = httpGet(certRes.CertURL)
|
|
||||||
if err != nil {
|
|
||||||
return CertificateResource{}, err
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return certRes, nil
|
return &certRes, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// checkCertResponse checks resp to see if a certificate is contained in the
|
// checkCertResponse checks to see if the certificate is ready and a link is contained in the
|
||||||
// response, and if so, loads it into certRes and returns true. If the cert
|
// response. if so, loads it into certRes and returns true. If the cert
|
||||||
// is not yet ready, it returns false. This function honors the waiting period
|
// is not yet ready, it returns false. The certRes input
|
||||||
// required by the Retry-After header of the response, if specified. This
|
|
||||||
// function may read from resp.Body but does NOT close it. The certRes input
|
|
||||||
// should already have the Domain (common name) field populated. If bundle is
|
// should already have the Domain (common name) field populated. If bundle is
|
||||||
// true, the certificate will be bundled with the issuer's cert.
|
// true, the certificate will be bundled with the issuer's cert.
|
||||||
func (c *Client) checkCertResponse(resp *http.Response, certRes *CertificateResource, bundle bool) (bool, error) {
|
func (c *Client) checkCertResponse(order orderMessage, certRes *CertificateResource, bundle bool) (bool, error) {
|
||||||
switch resp.StatusCode {
|
|
||||||
case 201, 202:
|
switch order.Status {
|
||||||
|
case "valid":
|
||||||
|
resp, err := httpGet(order.Certificate)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
cert, err := ioutil.ReadAll(limitReader(resp.Body, maxBodySize))
|
cert, err := ioutil.ReadAll(limitReader(resp.Body, maxBodySize))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// The server returns a body with a length of zero if the
|
// The issuer certificate link is always supplied via an "up" link
|
||||||
// certificate was not ready at the time this request completed.
|
// in the response headers of a new certificate.
|
||||||
// Otherwise the body is the certificate.
|
links := parseLinks(resp.Header["Link"])
|
||||||
if len(cert) > 0 {
|
if link, ok := links["up"]; ok {
|
||||||
certRes.CertStableURL = resp.Header.Get("Content-Location")
|
issuerCert, err := c.getIssuerCertificate(link)
|
||||||
certRes.AccountRef = c.user.GetRegistration().URI
|
|
||||||
|
|
||||||
issuedCert := pemEncode(derCertificateBytes(cert))
|
|
||||||
|
|
||||||
// The issuer certificate link is always supplied via an "up" link
|
|
||||||
// in the response headers of a new certificate.
|
|
||||||
links := parseLinks(resp.Header["Link"])
|
|
||||||
issuerCert, err := c.getIssuerCertificate(links["up"])
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// If we fail to acquire the issuer cert, return the issued certificate - do not fail.
|
// If we fail to acquire the issuer cert, return the issued certificate - do not fail.
|
||||||
logf("[WARNING][%s] acme: Could not bundle issuer certificate: %v", certRes.Domain, err)
|
logf("[WARNING][%s] acme: Could not bundle issuer certificate: %v", certRes.Domain, err)
|
||||||
|
@ -716,31 +762,26 @@ func (c *Client) checkCertResponse(resp *http.Response, certRes *CertificateReso
|
||||||
// If bundle is true, we want to return a certificate bundle.
|
// If bundle is true, we want to return a certificate bundle.
|
||||||
// To do this, we append the issuer cert to the issued cert.
|
// To do this, we append the issuer cert to the issued cert.
|
||||||
if bundle {
|
if bundle {
|
||||||
issuedCert = append(issuedCert, issuerCert...)
|
cert = append(cert, issuerCert...)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
certRes.IssuerCertificate = issuerCert
|
||||||
}
|
}
|
||||||
|
|
||||||
certRes.Certificate = issuedCert
|
|
||||||
certRes.IssuerCertificate = issuerCert
|
|
||||||
logf("[INFO][%s] Server responded with a certificate.", certRes.Domain)
|
|
||||||
return true, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// The certificate was granted but is not yet issued.
|
certRes.Certificate = cert
|
||||||
// Check retry-after and loop.
|
certRes.CertURL = order.Certificate
|
||||||
ra := resp.Header.Get("Retry-After")
|
certRes.CertStableURL = order.Certificate
|
||||||
retryAfter, err := strconv.Atoi(ra)
|
logf("[INFO][%s] Server responded with a certificate.", certRes.Domain)
|
||||||
if err != nil {
|
return true, nil
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
|
|
||||||
logf("[INFO][%s] acme: Server responded with status 202; retrying after %ds", certRes.Domain, retryAfter)
|
|
||||||
time.Sleep(time.Duration(retryAfter) * time.Second)
|
|
||||||
|
|
||||||
|
case "processing":
|
||||||
return false, nil
|
return false, nil
|
||||||
default:
|
case "invalid":
|
||||||
return false, handleHTTPError(resp)
|
return false, errors.New("Order has invalid state: invalid")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// getIssuerCertificate requests the issuer certificate
|
// getIssuerCertificate requests the issuer certificate
|
||||||
|
@ -786,10 +827,10 @@ func parseLinks(links []string) map[string]string {
|
||||||
|
|
||||||
// validate makes the ACME server start validating a
|
// validate makes the ACME server start validating a
|
||||||
// challenge response, only returning once it is done.
|
// challenge response, only returning once it is done.
|
||||||
func validate(j *jws, domain, uri string, chlng challenge) error {
|
func validate(j *jws, domain, uri string, c challenge) error {
|
||||||
var challengeResponse challenge
|
var chlng challenge
|
||||||
|
|
||||||
hdr, err := postJSON(j, uri, chlng, &challengeResponse)
|
hdr, err := postJSON(j, uri, c, &chlng)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -797,27 +838,27 @@ func validate(j *jws, domain, uri string, chlng challenge) error {
|
||||||
// After the path is sent, the ACME server will access our server.
|
// After the path is sent, the ACME server will access our server.
|
||||||
// Repeatedly check the server for an updated status on our request.
|
// Repeatedly check the server for an updated status on our request.
|
||||||
for {
|
for {
|
||||||
switch challengeResponse.Status {
|
switch chlng.Status {
|
||||||
case "valid":
|
case "valid":
|
||||||
logf("[INFO][%s] The server validated our request", domain)
|
logf("[INFO][%s] The server validated our request", domain)
|
||||||
return nil
|
return nil
|
||||||
case "pending":
|
case "pending":
|
||||||
break
|
break
|
||||||
case "invalid":
|
case "invalid":
|
||||||
return handleChallengeError(challengeResponse)
|
return handleChallengeError(chlng)
|
||||||
default:
|
default:
|
||||||
return errors.New("The server returned an unexpected state.")
|
return errors.New("The server returned an unexpected state")
|
||||||
}
|
}
|
||||||
|
|
||||||
ra, err := strconv.Atoi(hdr.Get("Retry-After"))
|
ra, err := strconv.Atoi(hdr.Get("Retry-After"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// The ACME server MUST return a Retry-After.
|
// The ACME server MUST return a Retry-After.
|
||||||
// If it doesn't, we'll just poll hard.
|
// If it doesn't, we'll just poll hard.
|
||||||
ra = 1
|
ra = 5
|
||||||
}
|
}
|
||||||
time.Sleep(time.Duration(ra) * time.Second)
|
time.Sleep(time.Duration(ra) * time.Second)
|
||||||
|
|
||||||
hdr, err = getJSON(uri, &challengeResponse)
|
hdr, err = getJSON(uri, &chlng)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,7 +27,13 @@ func TestNewClient(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
data, _ := json.Marshal(directory{NewAuthzURL: "http://test", NewCertURL: "http://test", NewRegURL: "http://test", RevokeCertURL: "http://test"})
|
data, _ := json.Marshal(directory{
|
||||||
|
NewNonceURL: "http://test",
|
||||||
|
NewAccountURL: "http://test",
|
||||||
|
NewOrderURL: "http://test",
|
||||||
|
RevokeCertURL: "http://test",
|
||||||
|
KeyChangeURL: "http://test",
|
||||||
|
})
|
||||||
w.Write(data)
|
w.Write(data)
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
@ -47,7 +53,7 @@ func TestNewClient(t *testing.T) {
|
||||||
t.Errorf("Expected keyType to be %s but was %s", keyType, client.keyType)
|
t.Errorf("Expected keyType to be %s but was %s", keyType, client.keyType)
|
||||||
}
|
}
|
||||||
|
|
||||||
if expected, actual := 2, len(client.solvers); actual != expected {
|
if expected, actual := 1, len(client.solvers); actual != expected {
|
||||||
t.Fatalf("Expected %d solver(s), got %d", expected, actual)
|
t.Fatalf("Expected %d solver(s), got %d", expected, actual)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -65,7 +71,13 @@ func TestClientOptPort(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
data, _ := json.Marshal(directory{NewAuthzURL: "http://test", NewCertURL: "http://test", NewRegURL: "http://test", RevokeCertURL: "http://test"})
|
data, _ := json.Marshal(directory{
|
||||||
|
NewNonceURL: "http://test",
|
||||||
|
NewAccountURL: "http://test",
|
||||||
|
NewOrderURL: "http://test",
|
||||||
|
RevokeCertURL: "http://test",
|
||||||
|
KeyChangeURL: "http://test",
|
||||||
|
})
|
||||||
w.Write(data)
|
w.Write(data)
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
@ -76,7 +88,6 @@ func TestClientOptPort(t *testing.T) {
|
||||||
t.Fatalf("Could not create client: %v", err)
|
t.Fatalf("Could not create client: %v", err)
|
||||||
}
|
}
|
||||||
client.SetHTTPAddress(net.JoinHostPort(optHost, optPort))
|
client.SetHTTPAddress(net.JoinHostPort(optHost, optPort))
|
||||||
client.SetTLSAddress(net.JoinHostPort(optHost, optPort))
|
|
||||||
|
|
||||||
httpSolver, ok := client.solvers[HTTP01].(*httpChallenge)
|
httpSolver, ok := client.solvers[HTTP01].(*httpChallenge)
|
||||||
if !ok {
|
if !ok {
|
||||||
|
@ -92,7 +103,7 @@ func TestClientOptPort(t *testing.T) {
|
||||||
t.Errorf("Expected http-01 to have iface %s but was %s", optHost, got)
|
t.Errorf("Expected http-01 to have iface %s but was %s", optHost, got)
|
||||||
}
|
}
|
||||||
|
|
||||||
httpsSolver, ok := client.solvers[TLSSNI01].(*tlsSNIChallenge)
|
/* httpsSolver, ok := client.solvers[TLSSNI01].(*tlsSNIChallenge)
|
||||||
if !ok {
|
if !ok {
|
||||||
t.Fatal("Expected tls-sni-01 solver to be httpChallenge type")
|
t.Fatal("Expected tls-sni-01 solver to be httpChallenge type")
|
||||||
}
|
}
|
||||||
|
@ -104,19 +115,15 @@ func TestClientOptPort(t *testing.T) {
|
||||||
}
|
}
|
||||||
if got := httpsSolver.provider.(*TLSProviderServer).iface; got != optHost {
|
if got := httpsSolver.provider.(*TLSProviderServer).iface; got != optHost {
|
||||||
t.Errorf("Expected tls-sni-01 to have port %s but was %s", optHost, got)
|
t.Errorf("Expected tls-sni-01 to have port %s but was %s", optHost, got)
|
||||||
}
|
} */
|
||||||
|
|
||||||
// test setting different host
|
// test setting different host
|
||||||
optHost = "127.0.0.1"
|
optHost = "127.0.0.1"
|
||||||
client.SetHTTPAddress(net.JoinHostPort(optHost, optPort))
|
client.SetHTTPAddress(net.JoinHostPort(optHost, optPort))
|
||||||
client.SetTLSAddress(net.JoinHostPort(optHost, optPort))
|
|
||||||
|
|
||||||
if got := httpSolver.provider.(*HTTPProviderServer).iface; got != optHost {
|
if got := httpSolver.provider.(*HTTPProviderServer).iface; got != optHost {
|
||||||
t.Errorf("Expected http-01 to have iface %s but was %s", optHost, got)
|
t.Errorf("Expected http-01 to have iface %s but was %s", optHost, got)
|
||||||
}
|
}
|
||||||
if got := httpsSolver.provider.(*TLSProviderServer).port; got != optPort {
|
|
||||||
t.Errorf("Expected tls-sni-01 to have port %s but was %s", optPort, got)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestNotHoldingLockWhileMakingHTTPRequests(t *testing.T) {
|
func TestNotHoldingLockWhileMakingHTTPRequests(t *testing.T) {
|
||||||
|
@ -124,12 +131,12 @@ func TestNotHoldingLockWhileMakingHTTPRequests(t *testing.T) {
|
||||||
time.Sleep(250 * time.Millisecond)
|
time.Sleep(250 * time.Millisecond)
|
||||||
w.Header().Add("Replay-Nonce", "12345")
|
w.Header().Add("Replay-Nonce", "12345")
|
||||||
w.Header().Add("Retry-After", "0")
|
w.Header().Add("Retry-After", "0")
|
||||||
writeJSONResponse(w, &challenge{Type: "http-01", Status: "Valid", URI: "http://example.com/", Token: "token"})
|
writeJSONResponse(w, &challenge{Type: "http-01", Status: "Valid", URL: "http://example.com/", Token: "token"})
|
||||||
}))
|
}))
|
||||||
defer ts.Close()
|
defer ts.Close()
|
||||||
|
|
||||||
privKey, _ := rsa.GenerateKey(rand.Reader, 512)
|
privKey, _ := rsa.GenerateKey(rand.Reader, 512)
|
||||||
j := &jws{privKey: privKey, directoryURL: ts.URL}
|
j := &jws{privKey: privKey, getNonceURL: ts.URL}
|
||||||
ch := make(chan bool)
|
ch := make(chan bool)
|
||||||
resultCh := make(chan bool)
|
resultCh := make(chan bool)
|
||||||
go func() {
|
go func() {
|
||||||
|
@ -163,12 +170,12 @@ func TestValidate(t *testing.T) {
|
||||||
case "POST":
|
case "POST":
|
||||||
st := statuses[0]
|
st := statuses[0]
|
||||||
statuses = statuses[1:]
|
statuses = statuses[1:]
|
||||||
writeJSONResponse(w, &challenge{Type: "http-01", Status: st, URI: "http://example.com/", Token: "token"})
|
writeJSONResponse(w, &challenge{Type: "http-01", Status: st, URL: "http://example.com/", Token: "token"})
|
||||||
|
|
||||||
case "GET":
|
case "GET":
|
||||||
st := statuses[0]
|
st := statuses[0]
|
||||||
statuses = statuses[1:]
|
statuses = statuses[1:]
|
||||||
writeJSONResponse(w, &challenge{Type: "http-01", Status: st, URI: "http://example.com/", Token: "token"})
|
writeJSONResponse(w, &challenge{Type: "http-01", Status: st, URL: "http://example.com/", Token: "token"})
|
||||||
|
|
||||||
default:
|
default:
|
||||||
http.Error(w, r.Method, http.StatusMethodNotAllowed)
|
http.Error(w, r.Method, http.StatusMethodNotAllowed)
|
||||||
|
@ -177,7 +184,7 @@ func TestValidate(t *testing.T) {
|
||||||
defer ts.Close()
|
defer ts.Close()
|
||||||
|
|
||||||
privKey, _ := rsa.GenerateKey(rand.Reader, 512)
|
privKey, _ := rsa.GenerateKey(rand.Reader, 512)
|
||||||
j := &jws{privKey: privKey, directoryURL: ts.URL}
|
j := &jws{privKey: privKey, getNonceURL: ts.URL}
|
||||||
|
|
||||||
tsts := []struct {
|
tsts := []struct {
|
||||||
name string
|
name string
|
||||||
|
@ -186,10 +193,10 @@ func TestValidate(t *testing.T) {
|
||||||
}{
|
}{
|
||||||
{"POST-unexpected", []string{"weird"}, "unexpected"},
|
{"POST-unexpected", []string{"weird"}, "unexpected"},
|
||||||
{"POST-valid", []string{"valid"}, ""},
|
{"POST-valid", []string{"valid"}, ""},
|
||||||
{"POST-invalid", []string{"invalid"}, "Error Detail"},
|
{"POST-invalid", []string{"invalid"}, "Error"},
|
||||||
{"GET-unexpected", []string{"pending", "weird"}, "unexpected"},
|
{"GET-unexpected", []string{"pending", "weird"}, "unexpected"},
|
||||||
{"GET-valid", []string{"pending", "valid"}, ""},
|
{"GET-valid", []string{"pending", "valid"}, ""},
|
||||||
{"GET-invalid", []string{"pending", "invalid"}, "Error Detail"},
|
{"GET-invalid", []string{"pending", "invalid"}, "Error"},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tst := range tsts {
|
for _, tst := range tsts {
|
||||||
|
@ -209,9 +216,15 @@ func TestGetChallenges(t *testing.T) {
|
||||||
case "GET", "HEAD":
|
case "GET", "HEAD":
|
||||||
w.Header().Add("Replay-Nonce", "12345")
|
w.Header().Add("Replay-Nonce", "12345")
|
||||||
w.Header().Add("Retry-After", "0")
|
w.Header().Add("Retry-After", "0")
|
||||||
writeJSONResponse(w, directory{NewAuthzURL: ts.URL, NewCertURL: ts.URL, NewRegURL: ts.URL, RevokeCertURL: ts.URL})
|
writeJSONResponse(w, directory{
|
||||||
|
NewNonceURL: ts.URL,
|
||||||
|
NewAccountURL: ts.URL,
|
||||||
|
NewOrderURL: ts.URL,
|
||||||
|
RevokeCertURL: ts.URL,
|
||||||
|
KeyChangeURL: ts.URL,
|
||||||
|
})
|
||||||
case "POST":
|
case "POST":
|
||||||
writeJSONResponse(w, authorization{})
|
writeJSONResponse(w, orderMessage{})
|
||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
defer ts.Close()
|
defer ts.Close()
|
||||||
|
@ -224,7 +237,7 @@ func TestGetChallenges(t *testing.T) {
|
||||||
}
|
}
|
||||||
user := mockUser{
|
user := mockUser{
|
||||||
email: "test@test.com",
|
email: "test@test.com",
|
||||||
regres: &RegistrationResource{NewAuthzURL: ts.URL},
|
regres: &RegistrationResource{URI: ts.URL},
|
||||||
privatekey: key,
|
privatekey: key,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -233,12 +246,60 @@ func TestGetChallenges(t *testing.T) {
|
||||||
t.Fatalf("Could not create client: %v", err)
|
t.Fatalf("Could not create client: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
_, failures := client.getChallenges([]string{"example.com"})
|
_, err = client.createOrderForIdentifiers([]string{"example.com"})
|
||||||
if failures["example.com"] == nil {
|
if err != nil {
|
||||||
t.Fatal("Expecting \"Server did not provide next link to proceed\" error, got nil")
|
t.Fatal("Expecting \"Server did not provide next link to proceed\" error, got nil")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestResolveAccountByKey(t *testing.T) {
|
||||||
|
keyBits := 512
|
||||||
|
keyType := RSA2048
|
||||||
|
key, err := rsa.GenerateKey(rand.Reader, keyBits)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("Could not generate test key:", err)
|
||||||
|
}
|
||||||
|
user := mockUser{
|
||||||
|
email: "test@test.com",
|
||||||
|
regres: new(RegistrationResource),
|
||||||
|
privatekey: key,
|
||||||
|
}
|
||||||
|
|
||||||
|
var ts *httptest.Server
|
||||||
|
ts = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
switch r.RequestURI {
|
||||||
|
case "/directory":
|
||||||
|
writeJSONResponse(w, directory{
|
||||||
|
NewNonceURL: ts.URL + "/nonce",
|
||||||
|
NewAccountURL: ts.URL + "/account",
|
||||||
|
NewOrderURL: ts.URL + "/newOrder",
|
||||||
|
RevokeCertURL: ts.URL + "/revokeCert",
|
||||||
|
KeyChangeURL: ts.URL + "/keyChange",
|
||||||
|
})
|
||||||
|
case "/nonce":
|
||||||
|
w.Header().Add("Replay-Nonce", "12345")
|
||||||
|
w.Header().Add("Retry-After", "0")
|
||||||
|
case "/account":
|
||||||
|
w.Header().Set("Location", ts.URL+"/account_recovery")
|
||||||
|
case "/account_recovery":
|
||||||
|
writeJSONResponse(w, accountMessage{
|
||||||
|
Status: "valid",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
|
client, err := NewClient(ts.URL+"/directory", user, keyType)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Could not create client: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if res, err := client.ResolveAccountByKey(); err != nil {
|
||||||
|
t.Fatalf("Unexpected error resolving account by key: %v", err)
|
||||||
|
} else if res.Body.Status != "valid" {
|
||||||
|
t.Errorf("Unexpected account status: %v", res.Body.Status)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// writeJSONResponse marshals the body as JSON and writes it to the response.
|
// writeJSONResponse marshals the body as JSON and writes it to the response.
|
||||||
func writeJSONResponse(w http.ResponseWriter, body interface{}) {
|
func writeJSONResponse(w http.ResponseWriter, body interface{}) {
|
||||||
bs, err := json.Marshal(body)
|
bs, err := json.Marshal(body)
|
||||||
|
|
|
@ -18,10 +18,10 @@ import (
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"math/big"
|
"math/big"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"golang.org/x/crypto/ocsp"
|
"golang.org/x/crypto/ocsp"
|
||||||
|
jose "gopkg.in/square/go-jose.v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
// KeyType represents the key algo as well as the key size or curve to use.
|
// KeyType represents the key algo as well as the key size or curve to use.
|
||||||
|
@ -135,9 +135,9 @@ func getKeyAuthorization(token string, key interface{}) (string, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate the Key Authorization for the challenge
|
// Generate the Key Authorization for the challenge
|
||||||
jwk := keyAsJWK(publicKey)
|
jwk := &jose.JSONWebKey{Key: publicKey}
|
||||||
if jwk == nil {
|
if jwk == nil {
|
||||||
return "", errors.New("Could not generate JWK from key.")
|
return "", errors.New("Could not generate JWK from key")
|
||||||
}
|
}
|
||||||
thumbBytes, err := jwk.Thumbprint(crypto.SHA256)
|
thumbBytes, err := jwk.Thumbprint(crypto.SHA256)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -145,11 +145,7 @@ func getKeyAuthorization(token string, key interface{}) (string, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// unpad the base64URL
|
// unpad the base64URL
|
||||||
keyThumb := base64.URLEncoding.EncodeToString(thumbBytes)
|
keyThumb := base64.RawURLEncoding.EncodeToString(thumbBytes)
|
||||||
index := strings.Index(keyThumb, "=")
|
|
||||||
if index != -1 {
|
|
||||||
keyThumb = keyThumb[:index]
|
|
||||||
}
|
|
||||||
|
|
||||||
return token + "." + keyThumb, nil
|
return token + "." + keyThumb, nil
|
||||||
}
|
}
|
||||||
|
@ -176,7 +172,7 @@ func parsePEMBundle(bundle []byte) ([]*x509.Certificate, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(certificates) == 0 {
|
if len(certificates) == 0 {
|
||||||
return nil, errors.New("No certificates were found while parsing the bundle.")
|
return nil, errors.New("No certificates were found while parsing the bundle")
|
||||||
}
|
}
|
||||||
|
|
||||||
return certificates, nil
|
return certificates, nil
|
||||||
|
|
|
@ -29,6 +29,7 @@ var defaultNameservers = []string{
|
||||||
"google-public-dns-b.google.com:53",
|
"google-public-dns-b.google.com:53",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RecursiveNameservers are used to pre-check DNS propagations
|
||||||
var RecursiveNameservers = getNameservers(defaultResolvConf, defaultNameservers)
|
var RecursiveNameservers = getNameservers(defaultResolvConf, defaultNameservers)
|
||||||
|
|
||||||
// DNSTimeout is used to override the default DNS timeout of 10 seconds.
|
// DNSTimeout is used to override the default DNS timeout of 10 seconds.
|
||||||
|
@ -57,8 +58,7 @@ func getNameservers(path string, defaults []string) []string {
|
||||||
func DNS01Record(domain, keyAuth string) (fqdn string, value string, ttl int) {
|
func DNS01Record(domain, keyAuth string) (fqdn string, value string, ttl int) {
|
||||||
keyAuthShaBytes := sha256.Sum256([]byte(keyAuth))
|
keyAuthShaBytes := sha256.Sum256([]byte(keyAuth))
|
||||||
// base64URL encoding without padding
|
// base64URL encoding without padding
|
||||||
keyAuthSha := base64.URLEncoding.EncodeToString(keyAuthShaBytes[:sha256.Size])
|
value = base64.RawURLEncoding.EncodeToString(keyAuthShaBytes[:sha256.Size])
|
||||||
value = strings.TrimRight(keyAuthSha, "=")
|
|
||||||
ttl = 120
|
ttl = 120
|
||||||
fqdn = fmt.Sprintf("_acme-challenge.%s.", domain)
|
fqdn = fmt.Sprintf("_acme-challenge.%s.", domain)
|
||||||
return
|
return
|
||||||
|
@ -114,7 +114,7 @@ func (s *dnsChallenge) Solve(chlng challenge, domain string) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return s.validate(s.jws, domain, chlng.URI, challenge{Resource: "challenge", Type: chlng.Type, Token: chlng.Token, KeyAuthorization: keyAuth})
|
return s.validate(s.jws, domain, chlng.URL, challenge{Type: chlng.Type, Token: chlng.Token, KeyAuthorization: keyAuth})
|
||||||
}
|
}
|
||||||
|
|
||||||
// checkDNSPropagation checks if the expected TXT record has been propagated to all authoritative nameservers.
|
// checkDNSPropagation checks if the expected TXT record has been propagated to all authoritative nameservers.
|
||||||
|
|
|
@ -100,9 +100,9 @@ func TestDNSValidServerResponse(t *testing.T) {
|
||||||
}))
|
}))
|
||||||
|
|
||||||
manualProvider, _ := NewDNSProviderManual()
|
manualProvider, _ := NewDNSProviderManual()
|
||||||
jws := &jws{privKey: privKey, directoryURL: ts.URL}
|
jws := &jws{privKey: privKey, getNonceURL: ts.URL}
|
||||||
solver := &dnsChallenge{jws: jws, validate: validate, provider: manualProvider}
|
solver := &dnsChallenge{jws: jws, validate: validate, provider: manualProvider}
|
||||||
clientChallenge := challenge{Type: "dns01", Status: "pending", URI: ts.URL, Token: "http8"}
|
clientChallenge := challenge{Type: "dns01", Status: "pending", URL: ts.URL, Token: "http8"}
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
time.Sleep(time.Second * 2)
|
time.Sleep(time.Second * 2)
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package acme
|
package acme
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
|
@ -9,8 +10,8 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
tosAgreementError = "Must agree to subscriber agreement before any further actions"
|
tosAgreementError = "Terms of service have changed"
|
||||||
invalidNonceError = "JWS has invalid anti-replay nonce"
|
invalidNonceError = "urn:ietf:params:acme:error:badNonce"
|
||||||
)
|
)
|
||||||
|
|
||||||
// RemoteError is the base type for all errors specific to the ACME protocol.
|
// RemoteError is the base type for all errors specific to the ACME protocol.
|
||||||
|
@ -42,27 +43,23 @@ type domainError struct {
|
||||||
Error error
|
Error error
|
||||||
}
|
}
|
||||||
|
|
||||||
type challengeError struct {
|
// ObtainError is returned when there are specific errors available
|
||||||
RemoteError
|
// per domain. For example in ObtainCertificate
|
||||||
records []validationRecord
|
type ObtainError map[string]error
|
||||||
}
|
|
||||||
|
|
||||||
func (c challengeError) Error() string {
|
func (e ObtainError) Error() string {
|
||||||
|
buffer := bytes.NewBufferString("acme: Error -> One or more domains had a problem:\n")
|
||||||
var errStr string
|
for dom, err := range e {
|
||||||
for _, validation := range c.records {
|
buffer.WriteString(fmt.Sprintf("[%s] %s\n", dom, err))
|
||||||
errStr = errStr + fmt.Sprintf("\tValidation for %s:%s\n\tResolved to:\n\t\t%s\n\tUsed: %s\n\n",
|
|
||||||
validation.Hostname, validation.Port, strings.Join(validation.ResolvedAddresses, "\n\t\t"), validation.UsedAddress)
|
|
||||||
}
|
}
|
||||||
|
return buffer.String()
|
||||||
return fmt.Sprintf("%s\nError Detail:\n%s", c.RemoteError.Error(), errStr)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleHTTPError(resp *http.Response) error {
|
func handleHTTPError(resp *http.Response) error {
|
||||||
var errorDetail RemoteError
|
var errorDetail RemoteError
|
||||||
|
|
||||||
contentType := resp.Header.Get("Content-Type")
|
contentType := resp.Header.Get("Content-Type")
|
||||||
if contentType == "application/json" || contentType == "application/problem+json" {
|
if contentType == "application/json" || strings.HasPrefix(contentType, "application/problem+json") {
|
||||||
err := json.NewDecoder(resp.Body).Decode(&errorDetail)
|
err := json.NewDecoder(resp.Body).Decode(&errorDetail)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -82,7 +79,7 @@ func handleHTTPError(resp *http.Response) error {
|
||||||
return TOSError{errorDetail}
|
return TOSError{errorDetail}
|
||||||
}
|
}
|
||||||
|
|
||||||
if errorDetail.StatusCode == http.StatusBadRequest && strings.HasPrefix(errorDetail.Detail, invalidNonceError) {
|
if errorDetail.StatusCode == http.StatusBadRequest && errorDetail.Type == invalidNonceError {
|
||||||
return NonceError{errorDetail}
|
return NonceError{errorDetail}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -90,5 +87,5 @@ func handleHTTPError(resp *http.Response) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleChallengeError(chlng challenge) error {
|
func handleChallengeError(chlng challenge) error {
|
||||||
return challengeError{chlng.Error, chlng.ValidationRecords}
|
return chlng.Error
|
||||||
}
|
}
|
||||||
|
|
|
@ -102,7 +102,7 @@ func getJSON(uri string, respBody interface{}) (http.Header, error) {
|
||||||
func postJSON(j *jws, uri string, reqBody, respBody interface{}) (http.Header, error) {
|
func postJSON(j *jws, uri string, reqBody, respBody interface{}) (http.Header, error) {
|
||||||
jsonBytes, err := json.Marshal(reqBody)
|
jsonBytes, err := json.Marshal(reqBody)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.New("Failed to marshal network message...")
|
return nil, errors.New("Failed to marshal network message")
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, err := j.post(uri, jsonBytes)
|
resp, err := j.post(uri, jsonBytes)
|
||||||
|
|
|
@ -37,5 +37,5 @@ func (s *httpChallenge) Solve(chlng challenge, domain string) error {
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
return s.validate(s.jws, domain, chlng.URI, challenge{Resource: "challenge", Type: chlng.Type, Token: chlng.Token, KeyAuthorization: keyAuth})
|
return s.validate(s.jws, domain, chlng.URL, challenge{Type: chlng.Type, Token: chlng.Token, KeyAuthorization: keyAuth})
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,7 +11,7 @@ import (
|
||||||
func TestHTTPChallenge(t *testing.T) {
|
func TestHTTPChallenge(t *testing.T) {
|
||||||
privKey, _ := rsa.GenerateKey(rand.Reader, 512)
|
privKey, _ := rsa.GenerateKey(rand.Reader, 512)
|
||||||
j := &jws{privKey: privKey}
|
j := &jws{privKey: privKey}
|
||||||
clientChallenge := challenge{Type: HTTP01, Token: "http1"}
|
clientChallenge := challenge{Type: string(HTTP01), Token: "http1"}
|
||||||
mockValidate := func(_ *jws, _, _ string, chlng challenge) error {
|
mockValidate := func(_ *jws, _, _ string, chlng challenge) error {
|
||||||
uri := "http://localhost:23457/.well-known/acme-challenge/" + chlng.Token
|
uri := "http://localhost:23457/.well-known/acme-challenge/" + chlng.Token
|
||||||
resp, err := httpGet(uri)
|
resp, err := httpGet(uri)
|
||||||
|
@ -46,7 +46,7 @@ func TestHTTPChallenge(t *testing.T) {
|
||||||
func TestHTTPChallengeInvalidPort(t *testing.T) {
|
func TestHTTPChallengeInvalidPort(t *testing.T) {
|
||||||
privKey, _ := rsa.GenerateKey(rand.Reader, 128)
|
privKey, _ := rsa.GenerateKey(rand.Reader, 128)
|
||||||
j := &jws{privKey: privKey}
|
j := &jws{privKey: privKey}
|
||||||
clientChallenge := challenge{Type: HTTP01, Token: "http2"}
|
clientChallenge := challenge{Type: string(HTTP01), Token: "http2"}
|
||||||
solver := &httpChallenge{jws: j, validate: stubValidate, provider: &HTTPProviderServer{port: "123456"}}
|
solver := &httpChallenge{jws: j, validate: stubValidate, provider: &HTTPProviderServer{port: "123456"}}
|
||||||
|
|
||||||
if err := solver.Solve(clientChallenge, "localhost:123456"); err == nil {
|
if err := solver.Solve(clientChallenge, "localhost:123456"); err == nil {
|
||||||
|
|
82
acme/jws.go
82
acme/jws.go
|
@ -10,37 +10,27 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"gopkg.in/square/go-jose.v1"
|
"gopkg.in/square/go-jose.v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
type jws struct {
|
type jws struct {
|
||||||
directoryURL string
|
getNonceURL string
|
||||||
privKey crypto.PrivateKey
|
privKey crypto.PrivateKey
|
||||||
nonces nonceManager
|
kid string
|
||||||
}
|
nonces nonceManager
|
||||||
|
|
||||||
func keyAsJWK(key interface{}) *jose.JsonWebKey {
|
|
||||||
switch k := key.(type) {
|
|
||||||
case *ecdsa.PublicKey:
|
|
||||||
return &jose.JsonWebKey{Key: k, Algorithm: "EC"}
|
|
||||||
case *rsa.PublicKey:
|
|
||||||
return &jose.JsonWebKey{Key: k, Algorithm: "RSA"}
|
|
||||||
|
|
||||||
default:
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Posts a JWS signed message to the specified URL.
|
// Posts a JWS signed message to the specified URL.
|
||||||
// It does NOT close the response body, so the caller must
|
// It does NOT close the response body, so the caller must
|
||||||
// do that if no error was returned.
|
// do that if no error was returned.
|
||||||
func (j *jws) post(url string, content []byte) (*http.Response, error) {
|
func (j *jws) post(url string, content []byte) (*http.Response, error) {
|
||||||
signedContent, err := j.signContent(content)
|
signedContent, err := j.signContent(url, content)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("Failed to sign content -> %s", err.Error())
|
return nil, fmt.Errorf("Failed to sign content -> %s", err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, err := httpPost(url, "application/jose+json", bytes.NewBuffer([]byte(signedContent.FullSerialize())))
|
data := bytes.NewBuffer([]byte(signedContent.FullSerialize()))
|
||||||
|
resp, err := httpPost(url, "application/jose+json", data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("Failed to HTTP POST to %s -> %s", url, err.Error())
|
return nil, fmt.Errorf("Failed to HTTP POST to %s -> %s", url, err.Error())
|
||||||
}
|
}
|
||||||
|
@ -53,7 +43,7 @@ func (j *jws) post(url string, content []byte) (*http.Response, error) {
|
||||||
return resp, nil
|
return resp, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (j *jws) signContent(content []byte) (*jose.JsonWebSignature, error) {
|
func (j *jws) signContent(url string, content []byte) (*jose.JSONWebSignature, error) {
|
||||||
|
|
||||||
var alg jose.SignatureAlgorithm
|
var alg jose.SignatureAlgorithm
|
||||||
switch k := j.privKey.(type) {
|
switch k := j.privKey.(type) {
|
||||||
|
@ -67,11 +57,28 @@ func (j *jws) signContent(content []byte) (*jose.JsonWebSignature, error) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
signer, err := jose.NewSigner(alg, j.privKey)
|
jsonKey := jose.JSONWebKey{
|
||||||
|
Key: j.privKey,
|
||||||
|
KeyID: j.kid,
|
||||||
|
}
|
||||||
|
|
||||||
|
signKey := jose.SigningKey{
|
||||||
|
Algorithm: alg,
|
||||||
|
Key: jsonKey,
|
||||||
|
}
|
||||||
|
options := jose.SignerOptions{
|
||||||
|
NonceSource: j,
|
||||||
|
ExtraHeaders: make(map[jose.HeaderKey]interface{}),
|
||||||
|
}
|
||||||
|
options.ExtraHeaders["url"] = url
|
||||||
|
if j.kid == "" {
|
||||||
|
options.EmbedJWK = true
|
||||||
|
}
|
||||||
|
|
||||||
|
signer, err := jose.NewSigner(signKey, &options)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("Failed to create jose signer -> %s", err.Error())
|
return nil, fmt.Errorf("Failed to create jose signer -> %s", err.Error())
|
||||||
}
|
}
|
||||||
signer.SetNonceSource(j)
|
|
||||||
|
|
||||||
signed, err := signer.Sign(content)
|
signed, err := signer.Sign(content)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -80,12 +87,41 @@ func (j *jws) signContent(content []byte) (*jose.JsonWebSignature, error) {
|
||||||
return signed, nil
|
return signed, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (j *jws) signEABContent(url, kid string, hmac []byte) (*jose.JSONWebSignature, error) {
|
||||||
|
jwk := jose.JSONWebKey{Key: j.privKey}
|
||||||
|
jwkJSON, err := jwk.Public().MarshalJSON()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("acme: error encoding eab jwk key: %s", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
signer, err := jose.NewSigner(
|
||||||
|
jose.SigningKey{Algorithm: jose.HS256, Key: hmac},
|
||||||
|
&jose.SignerOptions{
|
||||||
|
EmbedJWK: false,
|
||||||
|
ExtraHeaders: map[jose.HeaderKey]interface{}{
|
||||||
|
"kid": kid,
|
||||||
|
"url": url,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("Failed to create External Account Binding jose signer -> %s", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
signed, err := signer.Sign(jwkJSON)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("Failed to External Account Binding sign content -> %s", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
return signed, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (j *jws) Nonce() (string, error) {
|
func (j *jws) Nonce() (string, error) {
|
||||||
if nonce, ok := j.nonces.Pop(); ok {
|
if nonce, ok := j.nonces.Pop(); ok {
|
||||||
return nonce, nil
|
return nonce, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return getNonce(j.directoryURL)
|
return getNonce(j.getNonceURL)
|
||||||
}
|
}
|
||||||
|
|
||||||
type nonceManager struct {
|
type nonceManager struct {
|
||||||
|
@ -124,7 +160,7 @@ func getNonce(url string) (string, error) {
|
||||||
func getNonceFromResponse(resp *http.Response) (string, error) {
|
func getNonceFromResponse(resp *http.Response) (string, error) {
|
||||||
nonce := resp.Header.Get("Replay-Nonce")
|
nonce := resp.Header.Get("Replay-Nonce")
|
||||||
if nonce == "" {
|
if nonce == "" {
|
||||||
return "", fmt.Errorf("Server did not respond with a proper nonce header.")
|
return "", fmt.Errorf("Server did not respond with a proper nonce header")
|
||||||
}
|
}
|
||||||
|
|
||||||
return nonce, nil
|
return nonce, nil
|
||||||
|
|
123
acme/messages.go
123
acme/messages.go
|
@ -1,59 +1,62 @@
|
||||||
package acme
|
package acme
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"gopkg.in/square/go-jose.v1"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type directory struct {
|
|
||||||
NewAuthzURL string `json:"new-authz"`
|
|
||||||
NewCertURL string `json:"new-cert"`
|
|
||||||
NewRegURL string `json:"new-reg"`
|
|
||||||
RevokeCertURL string `json:"revoke-cert"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type registrationMessage struct {
|
|
||||||
Resource string `json:"resource"`
|
|
||||||
Contact []string `json:"contact"`
|
|
||||||
Delete bool `json:"delete,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Registration is returned by the ACME server after the registration
|
|
||||||
// The client implementation should save this registration somewhere.
|
|
||||||
type Registration struct {
|
|
||||||
Resource string `json:"resource,omitempty"`
|
|
||||||
ID int `json:"id"`
|
|
||||||
Key jose.JsonWebKey `json:"key"`
|
|
||||||
Contact []string `json:"contact"`
|
|
||||||
Agreement string `json:"agreement,omitempty"`
|
|
||||||
Authorizations string `json:"authorizations,omitempty"`
|
|
||||||
Certificates string `json:"certificates,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// RegistrationResource represents all important informations about a registration
|
// RegistrationResource represents all important informations about a registration
|
||||||
// of which the client needs to keep track itself.
|
// of which the client needs to keep track itself.
|
||||||
type RegistrationResource struct {
|
type RegistrationResource struct {
|
||||||
Body Registration `json:"body,omitempty"`
|
Body accountMessage `json:"body,omitempty"`
|
||||||
URI string `json:"uri,omitempty"`
|
URI string `json:"uri,omitempty"`
|
||||||
NewAuthzURL string `json:"new_authzr_uri,omitempty"`
|
|
||||||
TosURL string `json:"terms_of_service,omitempty"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type authorizationResource struct {
|
type directory struct {
|
||||||
Body authorization
|
NewNonceURL string `json:"newNonce"`
|
||||||
Domain string
|
NewAccountURL string `json:"newAccount"`
|
||||||
NewCertURL string
|
NewOrderURL string `json:"newOrder"`
|
||||||
AuthURL string
|
RevokeCertURL string `json:"revokeCert"`
|
||||||
|
KeyChangeURL string `json:"keyChange"`
|
||||||
|
Meta struct {
|
||||||
|
TermsOfService string `json:"termsOfService"`
|
||||||
|
Website string `json:"website"`
|
||||||
|
CaaIdentities []string `json:"caaIdentities"`
|
||||||
|
ExternalAccountRequired bool `json:"externalAccountRequired"`
|
||||||
|
} `json:"meta"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type accountMessage struct {
|
||||||
|
Status string `json:"status,omitempty"`
|
||||||
|
Contact []string `json:"contact,omitempty"`
|
||||||
|
TermsOfServiceAgreed bool `json:"termsOfServiceAgreed,omitempty"`
|
||||||
|
Orders string `json:"orders,omitempty"`
|
||||||
|
OnlyReturnExisting bool `json:"onlyReturnExisting,omitempty"`
|
||||||
|
ExternalAccountBinding json.RawMessage `json:"externalAccountBinding,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type orderResource struct {
|
||||||
|
URL string `json:"url,omitempty"`
|
||||||
|
Domains []string `json:"domains,omitempty"`
|
||||||
|
orderMessage `json:"body,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type orderMessage struct {
|
||||||
|
Status string `json:"status,omitempty"`
|
||||||
|
Expires string `json:"expires,omitempty"`
|
||||||
|
Identifiers []identifier `json:"identifiers"`
|
||||||
|
NotBefore string `json:"notBefore,omitempty"`
|
||||||
|
NotAfter string `json:"notAfter,omitempty"`
|
||||||
|
Authorizations []string `json:"authorizations,omitempty"`
|
||||||
|
Finalize string `json:"finalize,omitempty"`
|
||||||
|
Certificate string `json:"certificate,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type authorization struct {
|
type authorization struct {
|
||||||
Resource string `json:"resource,omitempty"`
|
Status string `json:"status"`
|
||||||
Identifier identifier `json:"identifier"`
|
Expires time.Time `json:"expires"`
|
||||||
Status string `json:"status,omitempty"`
|
Identifier identifier `json:"identifier"`
|
||||||
Expires time.Time `json:"expires,omitempty"`
|
Challenges []challenge `json:"challenges"`
|
||||||
Challenges []challenge `json:"challenges,omitempty"`
|
|
||||||
Combinations [][]int `json:"combinations,omitempty"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type identifier struct {
|
type identifier struct {
|
||||||
|
@ -61,41 +64,29 @@ type identifier struct {
|
||||||
Value string `json:"value"`
|
Value string `json:"value"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type validationRecord struct {
|
|
||||||
URI string `json:"url,omitempty"`
|
|
||||||
Hostname string `json:"hostname,omitempty"`
|
|
||||||
Port string `json:"port,omitempty"`
|
|
||||||
ResolvedAddresses []string `json:"addressesResolved,omitempty"`
|
|
||||||
UsedAddress string `json:"addressUsed,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type challenge struct {
|
type challenge struct {
|
||||||
Resource string `json:"resource,omitempty"`
|
URL string `json:"url"`
|
||||||
Type Challenge `json:"type,omitempty"`
|
Type string `json:"type"`
|
||||||
Status string `json:"status,omitempty"`
|
Status string `json:"status"`
|
||||||
URI string `json:"uri,omitempty"`
|
Token string `json:"token"`
|
||||||
Token string `json:"token,omitempty"`
|
Validated time.Time `json:"validated"`
|
||||||
KeyAuthorization string `json:"keyAuthorization,omitempty"`
|
KeyAuthorization string `json:"keyAuthorization"`
|
||||||
TLS bool `json:"tls,omitempty"`
|
Error RemoteError `json:"error"`
|
||||||
Iterations int `json:"n,omitempty"`
|
|
||||||
Error RemoteError `json:"error,omitempty"`
|
|
||||||
ValidationRecords []validationRecord `json:"validationRecord,omitempty"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type csrMessage struct {
|
type csrMessage struct {
|
||||||
Resource string `json:"resource,omitempty"`
|
Csr string `json:"csr"`
|
||||||
Csr string `json:"csr"`
|
}
|
||||||
Authorizations []string `json:"authorizations"`
|
|
||||||
|
type emptyObjectMessage struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
type revokeCertMessage struct {
|
type revokeCertMessage struct {
|
||||||
Resource string `json:"resource"`
|
|
||||||
Certificate string `json:"certificate"`
|
Certificate string `json:"certificate"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type deactivateAuthMessage struct {
|
type deactivateAuthMessage struct {
|
||||||
Resource string `json:"resource,omitempty"`
|
Status string `jsom:"status"`
|
||||||
Status string `jsom:"status"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// CertificateResource represents a CA issued certificate.
|
// CertificateResource represents a CA issued certificate.
|
||||||
|
|
|
@ -1,67 +0,0 @@
|
||||||
package acme
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/rsa"
|
|
||||||
"crypto/sha256"
|
|
||||||
"crypto/tls"
|
|
||||||
"encoding/hex"
|
|
||||||
"fmt"
|
|
||||||
"log"
|
|
||||||
)
|
|
||||||
|
|
||||||
type tlsSNIChallenge struct {
|
|
||||||
jws *jws
|
|
||||||
validate validateFunc
|
|
||||||
provider ChallengeProvider
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *tlsSNIChallenge) Solve(chlng challenge, domain string) error {
|
|
||||||
// FIXME: https://github.com/ietf-wg-acme/acme/pull/22
|
|
||||||
// Currently we implement this challenge to track boulder, not the current spec!
|
|
||||||
|
|
||||||
logf("[INFO][%s] acme: Trying to solve TLS-SNI-01", domain)
|
|
||||||
|
|
||||||
// Generate the Key Authorization for the challenge
|
|
||||||
keyAuth, err := getKeyAuthorization(chlng.Token, t.jws.privKey)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
err = t.provider.Present(domain, chlng.Token, keyAuth)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("[%s] error presenting token: %v", domain, err)
|
|
||||||
}
|
|
||||||
defer func() {
|
|
||||||
err := t.provider.CleanUp(domain, chlng.Token, keyAuth)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("[%s] error cleaning up: %v", domain, err)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
return t.validate(t.jws, domain, chlng.URI, challenge{Resource: "challenge", Type: chlng.Type, Token: chlng.Token, KeyAuthorization: keyAuth})
|
|
||||||
}
|
|
||||||
|
|
||||||
// TLSSNI01ChallengeCert returns a certificate and target domain for the `tls-sni-01` challenge
|
|
||||||
func TLSSNI01ChallengeCert(keyAuth string) (tls.Certificate, string, error) {
|
|
||||||
// generate a new RSA key for the certificates
|
|
||||||
tempPrivKey, err := generatePrivateKey(RSA2048)
|
|
||||||
if err != nil {
|
|
||||||
return tls.Certificate{}, "", err
|
|
||||||
}
|
|
||||||
rsaPrivKey := tempPrivKey.(*rsa.PrivateKey)
|
|
||||||
rsaPrivPEM := pemEncode(rsaPrivKey)
|
|
||||||
|
|
||||||
zBytes := sha256.Sum256([]byte(keyAuth))
|
|
||||||
z := hex.EncodeToString(zBytes[:sha256.Size])
|
|
||||||
domain := fmt.Sprintf("%s.%s.acme.invalid", z[:32], z[32:])
|
|
||||||
tempCertPEM, err := generatePemCert(rsaPrivKey, domain)
|
|
||||||
if err != nil {
|
|
||||||
return tls.Certificate{}, "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
certificate, err := tls.X509KeyPair(tempCertPEM, rsaPrivPEM)
|
|
||||||
if err != nil {
|
|
||||||
return tls.Certificate{}, "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
return certificate, domain, nil
|
|
||||||
}
|
|
|
@ -1,62 +0,0 @@
|
||||||
package acme
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/tls"
|
|
||||||
"fmt"
|
|
||||||
"net"
|
|
||||||
"net/http"
|
|
||||||
)
|
|
||||||
|
|
||||||
// TLSProviderServer implements ChallengeProvider for `TLS-SNI-01` challenge
|
|
||||||
// It may be instantiated without using the NewTLSProviderServer function if
|
|
||||||
// you want only to use the default values.
|
|
||||||
type TLSProviderServer struct {
|
|
||||||
iface string
|
|
||||||
port string
|
|
||||||
done chan bool
|
|
||||||
listener net.Listener
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewTLSProviderServer creates a new TLSProviderServer on the selected interface and port.
|
|
||||||
// Setting iface and / or port to an empty string will make the server fall back to
|
|
||||||
// the "any" interface and port 443 respectively.
|
|
||||||
func NewTLSProviderServer(iface, port string) *TLSProviderServer {
|
|
||||||
return &TLSProviderServer{iface: iface, port: port}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Present makes the keyAuth available as a cert
|
|
||||||
func (s *TLSProviderServer) Present(domain, token, keyAuth string) error {
|
|
||||||
if s.port == "" {
|
|
||||||
s.port = "443"
|
|
||||||
}
|
|
||||||
|
|
||||||
cert, _, err := TLSSNI01ChallengeCert(keyAuth)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
tlsConf := new(tls.Config)
|
|
||||||
tlsConf.Certificates = []tls.Certificate{cert}
|
|
||||||
|
|
||||||
s.listener, err = tls.Listen("tcp", net.JoinHostPort(s.iface, s.port), tlsConf)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("Could not start HTTPS server for challenge -> %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
s.done = make(chan bool)
|
|
||||||
go func() {
|
|
||||||
http.Serve(s.listener, nil)
|
|
||||||
s.done <- true
|
|
||||||
}()
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// CleanUp closes the HTTP server.
|
|
||||||
func (s *TLSProviderServer) CleanUp(domain, token, keyAuth string) error {
|
|
||||||
if s.listener == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
s.listener.Close()
|
|
||||||
<-s.done
|
|
||||||
return nil
|
|
||||||
}
|
|
|
@ -1,65 +0,0 @@
|
||||||
package acme
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/rand"
|
|
||||||
"crypto/rsa"
|
|
||||||
"crypto/sha256"
|
|
||||||
"crypto/tls"
|
|
||||||
"encoding/hex"
|
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestTLSSNIChallenge(t *testing.T) {
|
|
||||||
privKey, _ := rsa.GenerateKey(rand.Reader, 512)
|
|
||||||
j := &jws{privKey: privKey}
|
|
||||||
clientChallenge := challenge{Type: TLSSNI01, Token: "tlssni1"}
|
|
||||||
mockValidate := func(_ *jws, _, _ string, chlng challenge) error {
|
|
||||||
conn, err := tls.Dial("tcp", "localhost:23457", &tls.Config{
|
|
||||||
InsecureSkipVerify: true,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("Expected to connect to challenge server without an error. %s", err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
// Expect the server to only return one certificate
|
|
||||||
connState := conn.ConnectionState()
|
|
||||||
if count := len(connState.PeerCertificates); count != 1 {
|
|
||||||
t.Errorf("Expected the challenge server to return exactly one certificate but got %d", count)
|
|
||||||
}
|
|
||||||
|
|
||||||
remoteCert := connState.PeerCertificates[0]
|
|
||||||
if count := len(remoteCert.DNSNames); count != 1 {
|
|
||||||
t.Errorf("Expected the challenge certificate to have exactly one DNSNames entry but had %d", count)
|
|
||||||
}
|
|
||||||
|
|
||||||
zBytes := sha256.Sum256([]byte(chlng.KeyAuthorization))
|
|
||||||
z := hex.EncodeToString(zBytes[:sha256.Size])
|
|
||||||
domain := fmt.Sprintf("%s.%s.acme.invalid", z[:32], z[32:])
|
|
||||||
|
|
||||||
if remoteCert.DNSNames[0] != domain {
|
|
||||||
t.Errorf("Expected the challenge certificate DNSName to match %s but was %s", domain, remoteCert.DNSNames[0])
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
solver := &tlsSNIChallenge{jws: j, validate: mockValidate, provider: &TLSProviderServer{port: "23457"}}
|
|
||||||
|
|
||||||
if err := solver.Solve(clientChallenge, "localhost:23457"); err != nil {
|
|
||||||
t.Errorf("Solve error: got %v, want nil", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestTLSSNIChallengeInvalidPort(t *testing.T) {
|
|
||||||
privKey, _ := rsa.GenerateKey(rand.Reader, 128)
|
|
||||||
j := &jws{privKey: privKey}
|
|
||||||
clientChallenge := challenge{Type: TLSSNI01, Token: "tlssni2"}
|
|
||||||
solver := &tlsSNIChallenge{jws: j, validate: stubValidate, provider: &TLSProviderServer{port: "123456"}}
|
|
||||||
|
|
||||||
if err := solver.Solve(clientChallenge, "localhost:123456"); err == nil {
|
|
||||||
t.Errorf("Solve error: got %v, want error", err)
|
|
||||||
} else if want, want18 := "invalid port 123456", "123456: invalid port"; !strings.HasSuffix(err.Error(), want) && !strings.HasSuffix(err.Error(), want18) {
|
|
||||||
t.Errorf("Solve error: got %q, want suffix %q", err.Error(), want)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,13 +0,0 @@
|
||||||
package acme
|
|
||||||
|
|
||||||
// Challenge is a string that identifies a particular type and version of ACME challenge.
|
|
||||||
type Challenge string
|
|
||||||
|
|
||||||
const (
|
|
||||||
// HTTP01 is the "http-01" ACME challenge https://github.com/ietf-wg-acme/acme/blob/master/draft-ietf-acme-acme.md#http
|
|
||||||
// Note: HTTP01ChallengePath returns the URL path to fulfill this challenge
|
|
||||||
HTTP01 = Challenge("http-01")
|
|
||||||
// DNS01 is the "dns-01" ACME challenge https://github.com/ietf-wg-acme/acme/blob/master/draft-ietf-acme-acme.md#dns
|
|
||||||
// Note: DNS01Record returns a DNS record which will fulfill this challenge
|
|
||||||
DNS01 = Challenge("dns-01")
|
|
||||||
)
|
|
867
acmev2/client.go
867
acmev2/client.go
|
@ -1,867 +0,0 @@
|
||||||
// Package acme implements the ACME protocol for Let's Encrypt and other conforming providers.
|
|
||||||
package acme
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto"
|
|
||||||
"crypto/x509"
|
|
||||||
"encoding/base64"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"io/ioutil"
|
|
||||||
"log"
|
|
||||||
"net"
|
|
||||||
"regexp"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
// Logger is an optional custom logger.
|
|
||||||
Logger *log.Logger
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
// maxBodySize is the maximum size of body that we will read.
|
|
||||||
maxBodySize = 1024 * 1024
|
|
||||||
|
|
||||||
// overallRequestLimit is the overall number of request per second limited on the
|
|
||||||
// “new-reg”, “new-authz” and “new-cert” endpoints. From the documentation the
|
|
||||||
// limitation is 20 requests per second, but using 20 as value doesn't work but 18 do
|
|
||||||
overallRequestLimit = 18
|
|
||||||
)
|
|
||||||
|
|
||||||
// logf writes a log entry. It uses Logger if not
|
|
||||||
// nil, otherwise it uses the default log.Logger.
|
|
||||||
func logf(format string, args ...interface{}) {
|
|
||||||
if Logger != nil {
|
|
||||||
Logger.Printf(format, args...)
|
|
||||||
} else {
|
|
||||||
log.Printf(format, args...)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// User interface is to be implemented by users of this library.
|
|
||||||
// It is used by the client type to get user specific information.
|
|
||||||
type User interface {
|
|
||||||
GetEmail() string
|
|
||||||
GetRegistration() *RegistrationResource
|
|
||||||
GetPrivateKey() crypto.PrivateKey
|
|
||||||
}
|
|
||||||
|
|
||||||
// Interface for all challenge solvers to implement.
|
|
||||||
type solver interface {
|
|
||||||
Solve(challenge challenge, domain string) error
|
|
||||||
}
|
|
||||||
|
|
||||||
type validateFunc func(j *jws, domain, uri string, chlng challenge) error
|
|
||||||
|
|
||||||
// Client is the user-friendy way to ACME
|
|
||||||
type Client struct {
|
|
||||||
directory directory
|
|
||||||
user User
|
|
||||||
jws *jws
|
|
||||||
keyType KeyType
|
|
||||||
solvers map[Challenge]solver
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewClient creates a new ACME client on behalf of the user. The client will depend on
|
|
||||||
// the ACME directory located at caDirURL for the rest of its actions. A private
|
|
||||||
// key of type keyType (see KeyType contants) will be generated when requesting a new
|
|
||||||
// certificate if one isn't provided.
|
|
||||||
func NewClient(caDirURL string, user User, keyType KeyType) (*Client, error) {
|
|
||||||
privKey := user.GetPrivateKey()
|
|
||||||
if privKey == nil {
|
|
||||||
return nil, errors.New("private key was nil")
|
|
||||||
}
|
|
||||||
|
|
||||||
var dir directory
|
|
||||||
if _, err := getJSON(caDirURL, &dir); err != nil {
|
|
||||||
return nil, fmt.Errorf("get directory at '%s': %v", caDirURL, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if dir.NewAccountURL == "" {
|
|
||||||
return nil, errors.New("directory missing new registration URL")
|
|
||||||
}
|
|
||||||
if dir.NewOrderURL == "" {
|
|
||||||
return nil, errors.New("directory missing new order URL")
|
|
||||||
}
|
|
||||||
/*if dir.RevokeCertURL == "" {
|
|
||||||
return nil, errors.New("directory missing revoke certificate URL")
|
|
||||||
}*/
|
|
||||||
|
|
||||||
jws := &jws{privKey: privKey, getNonceURL: dir.NewNonceURL}
|
|
||||||
if reg := user.GetRegistration(); reg != nil {
|
|
||||||
jws.kid = reg.URI
|
|
||||||
}
|
|
||||||
|
|
||||||
// REVIEW: best possibility?
|
|
||||||
// Add all available solvers with the right index as per ACME
|
|
||||||
// spec to this map. Otherwise they won`t be found.
|
|
||||||
solvers := make(map[Challenge]solver)
|
|
||||||
solvers[HTTP01] = &httpChallenge{jws: jws, validate: validate, provider: &HTTPProviderServer{}}
|
|
||||||
|
|
||||||
return &Client{directory: dir, user: user, jws: jws, keyType: keyType, solvers: solvers}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetChallengeProvider specifies a custom provider p that can solve the given challenge type.
|
|
||||||
func (c *Client) SetChallengeProvider(challenge Challenge, p ChallengeProvider) error {
|
|
||||||
switch challenge {
|
|
||||||
case HTTP01:
|
|
||||||
c.solvers[challenge] = &httpChallenge{jws: c.jws, validate: validate, provider: p}
|
|
||||||
case DNS01:
|
|
||||||
c.solvers[challenge] = &dnsChallenge{jws: c.jws, validate: validate, provider: p}
|
|
||||||
default:
|
|
||||||
return fmt.Errorf("Unknown challenge %v", challenge)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetHTTPAddress specifies a custom interface:port to be used for HTTP based challenges.
|
|
||||||
// If this option is not used, the default port 80 and all interfaces will be used.
|
|
||||||
// To only specify a port and no interface use the ":port" notation.
|
|
||||||
//
|
|
||||||
// NOTE: This REPLACES any custom HTTP provider previously set by calling
|
|
||||||
// c.SetChallengeProvider with the default HTTP challenge provider.
|
|
||||||
func (c *Client) SetHTTPAddress(iface string) error {
|
|
||||||
host, port, err := net.SplitHostPort(iface)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if chlng, ok := c.solvers[HTTP01]; ok {
|
|
||||||
chlng.(*httpChallenge).provider = NewHTTPProviderServer(host, port)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ExcludeChallenges explicitly removes challenges from the pool for solving.
|
|
||||||
func (c *Client) ExcludeChallenges(challenges []Challenge) {
|
|
||||||
// Loop through all challenges and delete the requested one if found.
|
|
||||||
for _, challenge := range challenges {
|
|
||||||
delete(c.solvers, challenge)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetToSURL returns the current ToS URL from the Directory
|
|
||||||
func (c *Client) GetToSURL() string {
|
|
||||||
return c.directory.Meta.TermsOfService
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetExternalAccountRequired returns the External Account Binding requirement of the Directory
|
|
||||||
func (c *Client) GetExternalAccountRequired() bool {
|
|
||||||
return c.directory.Meta.ExternalAccountRequired
|
|
||||||
}
|
|
||||||
|
|
||||||
// Register the current account to the ACME server.
|
|
||||||
func (c *Client) Register(tosAgreed bool) (*RegistrationResource, error) {
|
|
||||||
if c == nil || c.user == nil {
|
|
||||||
return nil, errors.New("acme: cannot register a nil client or user")
|
|
||||||
}
|
|
||||||
logf("[INFO] acme: Registering account for %s", c.user.GetEmail())
|
|
||||||
|
|
||||||
accMsg := accountMessage{}
|
|
||||||
if c.user.GetEmail() != "" {
|
|
||||||
accMsg.Contact = []string{"mailto:" + c.user.GetEmail()}
|
|
||||||
} else {
|
|
||||||
accMsg.Contact = []string{}
|
|
||||||
}
|
|
||||||
accMsg.TermsOfServiceAgreed = tosAgreed
|
|
||||||
|
|
||||||
var serverReg accountMessage
|
|
||||||
hdr, err := postJSON(c.jws, c.directory.NewAccountURL, accMsg, &serverReg)
|
|
||||||
if err != nil {
|
|
||||||
remoteErr, ok := err.(RemoteError)
|
|
||||||
if ok && remoteErr.StatusCode == 409 {
|
|
||||||
} else {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
reg := &RegistrationResource{
|
|
||||||
URI: hdr.Get("Location"),
|
|
||||||
Body: serverReg,
|
|
||||||
}
|
|
||||||
c.jws.kid = reg.URI
|
|
||||||
|
|
||||||
return reg, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Register the current account to the ACME server.
|
|
||||||
func (c *Client) RegisterWithExternalAccountBinding(tosAgreed bool, kid string, hmacEncoded string) (*RegistrationResource, error) {
|
|
||||||
if c == nil || c.user == nil {
|
|
||||||
return nil, errors.New("acme: cannot register a nil client or user")
|
|
||||||
}
|
|
||||||
logf("[INFO] acme: Registering account (EAB) for %s", c.user.GetEmail())
|
|
||||||
|
|
||||||
accMsg := accountMessage{}
|
|
||||||
if c.user.GetEmail() != "" {
|
|
||||||
accMsg.Contact = []string{"mailto:" + c.user.GetEmail()}
|
|
||||||
} else {
|
|
||||||
accMsg.Contact = []string{}
|
|
||||||
}
|
|
||||||
accMsg.TermsOfServiceAgreed = tosAgreed
|
|
||||||
|
|
||||||
hmac, err := base64.RawURLEncoding.DecodeString(hmacEncoded)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("acme: could not decode hmac key: %s", err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
eabJWS, err := c.jws.signEABContent(c.directory.NewAccountURL, kid, hmac)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("acme: error signing eab content: %s", err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
eabPayload := eabJWS.FullSerialize()
|
|
||||||
|
|
||||||
accMsg.ExternalAccountBinding = []byte(eabPayload)
|
|
||||||
|
|
||||||
var serverReg accountMessage
|
|
||||||
hdr, err := postJSON(c.jws, c.directory.NewAccountURL, accMsg, &serverReg)
|
|
||||||
if err != nil {
|
|
||||||
remoteErr, ok := err.(RemoteError)
|
|
||||||
if ok && remoteErr.StatusCode == 409 {
|
|
||||||
} else {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
reg := &RegistrationResource{
|
|
||||||
URI: hdr.Get("Location"),
|
|
||||||
Body: serverReg,
|
|
||||||
}
|
|
||||||
c.jws.kid = reg.URI
|
|
||||||
|
|
||||||
return reg, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// ResolveAccountByKey will attempt to look up an account using the given account key
|
|
||||||
// and return its registration resource.
|
|
||||||
func (c *Client) ResolveAccountByKey() (*RegistrationResource, error) {
|
|
||||||
logf("[INFO] acme: Trying to resolve account by key")
|
|
||||||
|
|
||||||
acc := accountMessage{OnlyReturnExisting: true}
|
|
||||||
hdr, err := postJSON(c.jws, c.directory.NewAccountURL, acc, nil)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
accountLink := hdr.Get("Location")
|
|
||||||
if accountLink == "" {
|
|
||||||
return nil, errors.New("Server did not return the account link")
|
|
||||||
}
|
|
||||||
|
|
||||||
var retAccount accountMessage
|
|
||||||
c.jws.kid = accountLink
|
|
||||||
hdr, err = postJSON(c.jws, accountLink, accountMessage{}, &retAccount)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return &RegistrationResource{URI: accountLink, Body: retAccount}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeleteRegistration deletes the client's user registration from the ACME
|
|
||||||
// server.
|
|
||||||
func (c *Client) DeleteRegistration() error {
|
|
||||||
if c == nil || c.user == nil {
|
|
||||||
return errors.New("acme: cannot unregister a nil client or user")
|
|
||||||
}
|
|
||||||
logf("[INFO] acme: Deleting account for %s", c.user.GetEmail())
|
|
||||||
|
|
||||||
accMsg := accountMessage{
|
|
||||||
Status: "deactivated",
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err := postJSON(c.jws, c.user.GetRegistration().URI, accMsg, nil)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// QueryRegistration runs a POST request on the client's registration and
|
|
||||||
// returns the result.
|
|
||||||
//
|
|
||||||
// This is similar to the Register function, but acting on an existing
|
|
||||||
// registration link and resource.
|
|
||||||
func (c *Client) QueryRegistration() (*RegistrationResource, error) {
|
|
||||||
if c == nil || c.user == nil {
|
|
||||||
return nil, errors.New("acme: cannot query the registration of a nil client or user")
|
|
||||||
}
|
|
||||||
// Log the URL here instead of the email as the email may not be set
|
|
||||||
logf("[INFO] acme: Querying account for %s", c.user.GetRegistration().URI)
|
|
||||||
|
|
||||||
accMsg := accountMessage{}
|
|
||||||
|
|
||||||
var serverReg accountMessage
|
|
||||||
_, err := postJSON(c.jws, c.user.GetRegistration().URI, accMsg, &serverReg)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
reg := &RegistrationResource{Body: serverReg}
|
|
||||||
|
|
||||||
// Location: header is not returned so this needs to be populated off of
|
|
||||||
// existing URI
|
|
||||||
reg.URI = c.user.GetRegistration().URI
|
|
||||||
|
|
||||||
return reg, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ObtainCertificateForCSR tries to obtain a certificate matching the CSR passed into it.
|
|
||||||
// The domains are inferred from the CommonName and SubjectAltNames, if any. The private key
|
|
||||||
// for this CSR is not required.
|
|
||||||
// If bundle is true, the []byte contains both the issuer certificate and
|
|
||||||
// your issued certificate as a bundle.
|
|
||||||
// This function will never return a partial certificate. If one domain in the list fails,
|
|
||||||
// the whole certificate will fail.
|
|
||||||
func (c *Client) ObtainCertificateForCSR(csr x509.CertificateRequest, bundle bool) (*CertificateResource, error) {
|
|
||||||
// figure out what domains it concerns
|
|
||||||
// start with the common name
|
|
||||||
domains := []string{csr.Subject.CommonName}
|
|
||||||
|
|
||||||
// loop over the SubjectAltName DNS names
|
|
||||||
DNSNames:
|
|
||||||
for _, sanName := range csr.DNSNames {
|
|
||||||
for _, existingName := range domains {
|
|
||||||
if existingName == sanName {
|
|
||||||
// duplicate; skip this name
|
|
||||||
continue DNSNames
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// name is unique
|
|
||||||
domains = append(domains, sanName)
|
|
||||||
}
|
|
||||||
|
|
||||||
if bundle {
|
|
||||||
logf("[INFO][%s] acme: Obtaining bundled SAN certificate given a CSR", strings.Join(domains, ", "))
|
|
||||||
} else {
|
|
||||||
logf("[INFO][%s] acme: Obtaining SAN certificate given a CSR", strings.Join(domains, ", "))
|
|
||||||
}
|
|
||||||
|
|
||||||
order, err := c.createOrderForIdentifiers(domains)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
authz, err := c.getAuthzForOrder(order)
|
|
||||||
if err != nil {
|
|
||||||
// If any challenge fails, return. Do not generate partial SAN certificates.
|
|
||||||
/*for _, auth := range authz {
|
|
||||||
c.disableAuthz(auth)
|
|
||||||
}*/
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
err = c.solveChallengeForAuthz(authz)
|
|
||||||
if err != nil {
|
|
||||||
// If any challenge fails, return. Do not generate partial SAN certificates.
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
logf("[INFO][%s] acme: Validations succeeded; requesting certificates", strings.Join(domains, ", "))
|
|
||||||
|
|
||||||
failures := make(ObtainError)
|
|
||||||
cert, err := c.requestCertificateForCsr(order, bundle, csr.Raw, nil)
|
|
||||||
if err != nil {
|
|
||||||
for _, chln := range authz {
|
|
||||||
failures[chln.Identifier.Value] = err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add the CSR to the certificate so that it can be used for renewals.
|
|
||||||
cert.CSR = pemEncode(&csr)
|
|
||||||
|
|
||||||
// do not return an empty failures map, because
|
|
||||||
// it would still be a non-nil error value
|
|
||||||
if len(failures) > 0 {
|
|
||||||
return cert, failures
|
|
||||||
}
|
|
||||||
return cert, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ObtainCertificate tries to obtain a single certificate using all domains passed into it.
|
|
||||||
// The first domain in domains is used for the CommonName field of the certificate, all other
|
|
||||||
// domains are added using the Subject Alternate Names extension. A new private key is generated
|
|
||||||
// for every invocation of this function. If you do not want that you can supply your own private key
|
|
||||||
// in the privKey parameter. If this parameter is non-nil it will be used instead of generating a new one.
|
|
||||||
// If bundle is true, the []byte contains both the issuer certificate and
|
|
||||||
// your issued certificate as a bundle.
|
|
||||||
// This function will never return a partial certificate. If one domain in the list fails,
|
|
||||||
// the whole certificate will fail.
|
|
||||||
func (c *Client) ObtainCertificate(domains []string, bundle bool, privKey crypto.PrivateKey, mustStaple bool) (*CertificateResource, error) {
|
|
||||||
if len(domains) == 0 {
|
|
||||||
return nil, errors.New("No domains to obtain a certificate for")
|
|
||||||
}
|
|
||||||
|
|
||||||
if bundle {
|
|
||||||
logf("[INFO][%s] acme: Obtaining bundled SAN certificate", strings.Join(domains, ", "))
|
|
||||||
} else {
|
|
||||||
logf("[INFO][%s] acme: Obtaining SAN certificate", strings.Join(domains, ", "))
|
|
||||||
}
|
|
||||||
|
|
||||||
order, err := c.createOrderForIdentifiers(domains)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
authz, err := c.getAuthzForOrder(order)
|
|
||||||
if err != nil {
|
|
||||||
// If any challenge fails, return. Do not generate partial SAN certificates.
|
|
||||||
/*for _, auth := range authz {
|
|
||||||
c.disableAuthz(auth)
|
|
||||||
}*/
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
err = c.solveChallengeForAuthz(authz)
|
|
||||||
if err != nil {
|
|
||||||
// If any challenge fails, return. Do not generate partial SAN certificates.
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
logf("[INFO][%s] acme: Validations succeeded; requesting certificates", strings.Join(domains, ", "))
|
|
||||||
|
|
||||||
failures := make(ObtainError)
|
|
||||||
cert, err := c.requestCertificateForOrder(order, bundle, privKey, mustStaple)
|
|
||||||
if err != nil {
|
|
||||||
for _, auth := range authz {
|
|
||||||
failures[auth.Identifier.Value] = err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// do not return an empty failures map, because
|
|
||||||
// it would still be a non-nil error value
|
|
||||||
if len(failures) > 0 {
|
|
||||||
return cert, failures
|
|
||||||
}
|
|
||||||
return cert, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// RevokeCertificate takes a PEM encoded certificate or bundle and tries to revoke it at the CA.
|
|
||||||
func (c *Client) RevokeCertificate(certificate []byte) error {
|
|
||||||
certificates, err := parsePEMBundle(certificate)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
x509Cert := certificates[0]
|
|
||||||
if x509Cert.IsCA {
|
|
||||||
return fmt.Errorf("Certificate bundle starts with a CA certificate")
|
|
||||||
}
|
|
||||||
|
|
||||||
encodedCert := base64.URLEncoding.EncodeToString(x509Cert.Raw)
|
|
||||||
|
|
||||||
_, err = postJSON(c.jws, c.directory.RevokeCertURL, revokeCertMessage{Certificate: encodedCert}, nil)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// RenewCertificate takes a CertificateResource and tries to renew the certificate.
|
|
||||||
// If the renewal process succeeds, the new certificate will ge returned in a new CertResource.
|
|
||||||
// Please be aware that this function will return a new certificate in ANY case that is not an error.
|
|
||||||
// If the server does not provide us with a new cert on a GET request to the CertURL
|
|
||||||
// this function will start a new-cert flow where a new certificate gets generated.
|
|
||||||
// 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 CertificateResource should be non-nil.
|
|
||||||
func (c *Client) RenewCertificate(cert CertificateResource, bundle, mustStaple bool) (*CertificateResource, 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.
|
|
||||||
certificates, err := parsePEMBundle(cert.Certificate)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
x509Cert := certificates[0]
|
|
||||||
if x509Cert.IsCA {
|
|
||||||
return nil, fmt.Errorf("[%s] Certificate bundle starts with a CA certificate", cert.Domain)
|
|
||||||
}
|
|
||||||
|
|
||||||
// This is just meant to be informal for the user.
|
|
||||||
timeLeft := x509Cert.NotAfter.Sub(time.Now().UTC())
|
|
||||||
logf("[INFO][%s] acme: Trying renewal with %d hours remaining", cert.Domain, int(timeLeft.Hours()))
|
|
||||||
|
|
||||||
// We always need to request a new certificate to renew.
|
|
||||||
// Start by checking to see if the certificate was based off a CSR, and
|
|
||||||
// use that if it's defined.
|
|
||||||
if len(cert.CSR) > 0 {
|
|
||||||
csr, err := pemDecodeTox509CSR(cert.CSR)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
newCert, failures := c.ObtainCertificateForCSR(*csr, bundle)
|
|
||||||
return newCert, failures
|
|
||||||
}
|
|
||||||
|
|
||||||
var privKey crypto.PrivateKey
|
|
||||||
if cert.PrivateKey != nil {
|
|
||||||
privKey, err = parsePEMPrivateKey(cert.PrivateKey)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var domains []string
|
|
||||||
// check for SAN certificate
|
|
||||||
if len(x509Cert.DNSNames) > 1 {
|
|
||||||
domains = append(domains, x509Cert.Subject.CommonName)
|
|
||||||
for _, sanDomain := range x509Cert.DNSNames {
|
|
||||||
if sanDomain == x509Cert.Subject.CommonName {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
domains = append(domains, sanDomain)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
domains = append(domains, x509Cert.Subject.CommonName)
|
|
||||||
}
|
|
||||||
|
|
||||||
newCert, err := c.ObtainCertificate(domains, bundle, privKey, mustStaple)
|
|
||||||
return newCert, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) createOrderForIdentifiers(domains []string) (orderResource, error) {
|
|
||||||
|
|
||||||
var identifiers []identifier
|
|
||||||
for _, domain := range domains {
|
|
||||||
identifiers = append(identifiers, identifier{Type: "dns", Value: domain})
|
|
||||||
}
|
|
||||||
|
|
||||||
order := orderMessage{
|
|
||||||
Identifiers: identifiers,
|
|
||||||
}
|
|
||||||
|
|
||||||
var response orderMessage
|
|
||||||
hdr, err := postJSON(c.jws, c.directory.NewOrderURL, order, &response)
|
|
||||||
if err != nil {
|
|
||||||
return orderResource{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
orderRes := orderResource{
|
|
||||||
URL: hdr.Get("Location"),
|
|
||||||
Domains: domains,
|
|
||||||
orderMessage: response,
|
|
||||||
}
|
|
||||||
return orderRes, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Looks through the challenge combinations to find a solvable match.
|
|
||||||
// Then solves the challenges in series and returns.
|
|
||||||
func (c *Client) solveChallengeForAuthz(authorizations []authorization) error {
|
|
||||||
failures := make(ObtainError)
|
|
||||||
|
|
||||||
// loop through the resources, basically through the domains.
|
|
||||||
for _, authz := range authorizations {
|
|
||||||
if authz.Status == "valid" {
|
|
||||||
// Boulder might recycle recent validated authz (see issue #267)
|
|
||||||
logf("[INFO][%s] acme: Authorization already valid; skipping challenge", authz.Identifier.Value)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// no solvers - no solving
|
|
||||||
if i, solver := c.chooseSolver(authz, authz.Identifier.Value); solver != nil {
|
|
||||||
err := solver.Solve(authz.Challenges[i], authz.Identifier.Value)
|
|
||||||
if err != nil {
|
|
||||||
//c.disableAuthz(authz.Identifier)
|
|
||||||
failures[authz.Identifier.Value] = err
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
//c.disableAuthz(authz)
|
|
||||||
failures[authz.Identifier.Value] = fmt.Errorf("[%s] acme: Could not determine solvers", authz.Identifier.Value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// be careful not to return an empty failures map, for
|
|
||||||
// even an empty ObtainError is a non-nil error value
|
|
||||||
if len(failures) > 0 {
|
|
||||||
return failures
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Checks all challenges from the server in order and returns the first matching solver.
|
|
||||||
func (c *Client) chooseSolver(auth authorization, domain string) (int, solver) {
|
|
||||||
for i, challenge := range auth.Challenges {
|
|
||||||
if solver, ok := c.solvers[Challenge(challenge.Type)]; ok {
|
|
||||||
return i, solver
|
|
||||||
}
|
|
||||||
logf("[INFO][%s] acme: Could not find solver for: %s", domain, challenge.Type)
|
|
||||||
}
|
|
||||||
return 0, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the challenges needed to proof our identifier to the ACME server.
|
|
||||||
func (c *Client) getAuthzForOrder(order orderResource) ([]authorization, error) {
|
|
||||||
resc, errc := make(chan authorization), make(chan domainError)
|
|
||||||
|
|
||||||
delay := time.Second / overallRequestLimit
|
|
||||||
|
|
||||||
for _, authzURL := range order.Authorizations {
|
|
||||||
time.Sleep(delay)
|
|
||||||
|
|
||||||
go func(authzURL string) {
|
|
||||||
var authz authorization
|
|
||||||
_, err := getJSON(authzURL, &authz)
|
|
||||||
if err != nil {
|
|
||||||
errc <- domainError{Domain: authz.Identifier.Value, Error: err}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
resc <- authz
|
|
||||||
}(authzURL)
|
|
||||||
}
|
|
||||||
|
|
||||||
var responses []authorization
|
|
||||||
failures := make(ObtainError)
|
|
||||||
for i := 0; i < len(order.Authorizations); i++ {
|
|
||||||
select {
|
|
||||||
case res := <-resc:
|
|
||||||
responses = append(responses, res)
|
|
||||||
case err := <-errc:
|
|
||||||
failures[err.Domain] = err.Error
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
logAuthz(order)
|
|
||||||
|
|
||||||
close(resc)
|
|
||||||
close(errc)
|
|
||||||
|
|
||||||
// be careful to not return an empty failures map;
|
|
||||||
// even if empty, they become non-nil error values
|
|
||||||
if len(failures) > 0 {
|
|
||||||
return responses, failures
|
|
||||||
}
|
|
||||||
return responses, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func logAuthz(order orderResource) {
|
|
||||||
for i, auth := range order.Authorizations {
|
|
||||||
logf("[INFO][%s] AuthURL: %s", order.Identifiers[i].Value, auth)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// cleanAuthz loops through the passed in slice and disables any auths which are not "valid"
|
|
||||||
func (c *Client) disableAuthz(authURL string) error {
|
|
||||||
var disabledAuth authorization
|
|
||||||
_, err := postJSON(c.jws, authURL, deactivateAuthMessage{Status: "deactivated"}, &disabledAuth)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) requestCertificateForOrder(order orderResource, bundle bool, privKey crypto.PrivateKey, mustStaple bool) (*CertificateResource, error) {
|
|
||||||
|
|
||||||
var err error
|
|
||||||
if privKey == nil {
|
|
||||||
privKey, err = generatePrivateKey(c.keyType)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// determine certificate name(s) based on the authorization resources
|
|
||||||
commonName := order.Domains[0]
|
|
||||||
var san []string
|
|
||||||
for _, auth := range order.Identifiers {
|
|
||||||
san = append(san, auth.Value)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: should the CSR be customizable?
|
|
||||||
csr, err := generateCsr(privKey, commonName, san, mustStaple)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.requestCertificateForCsr(order, bundle, csr, pemEncode(privKey))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) requestCertificateForCsr(order orderResource, bundle bool, csr []byte, privateKeyPem []byte) (*CertificateResource, error) {
|
|
||||||
commonName := order.Domains[0]
|
|
||||||
|
|
||||||
csrString := base64.RawURLEncoding.EncodeToString(csr)
|
|
||||||
var retOrder orderMessage
|
|
||||||
_, error := postJSON(c.jws, order.Finalize, csrMessage{Csr: csrString}, &retOrder)
|
|
||||||
if error != nil {
|
|
||||||
return nil, error
|
|
||||||
}
|
|
||||||
|
|
||||||
if retOrder.Status == "invalid" {
|
|
||||||
return nil, error
|
|
||||||
}
|
|
||||||
|
|
||||||
certRes := CertificateResource{
|
|
||||||
Domain: commonName,
|
|
||||||
CertURL: retOrder.Certificate,
|
|
||||||
PrivateKey: privateKeyPem,
|
|
||||||
}
|
|
||||||
|
|
||||||
if retOrder.Status == "valid" {
|
|
||||||
// if the certificate is available right away, short cut!
|
|
||||||
ok, err := c.checkCertResponse(retOrder, &certRes, bundle)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if ok {
|
|
||||||
return &certRes, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
maxChecks := 1000
|
|
||||||
for i := 0; i < maxChecks; i++ {
|
|
||||||
_, err := getJSON(order.URL, &retOrder)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
done, err := c.checkCertResponse(retOrder, &certRes, bundle)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if done {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
if i == maxChecks-1 {
|
|
||||||
return nil, fmt.Errorf("polled for certificate %d times; giving up", i)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return &certRes, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// checkCertResponse checks to see if the certificate is ready and a link is contained in the
|
|
||||||
// response. if so, loads it into certRes and returns true. If the cert
|
|
||||||
// is not yet ready, it returns false. 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 *Client) checkCertResponse(order orderMessage, certRes *CertificateResource, bundle bool) (bool, error) {
|
|
||||||
|
|
||||||
switch order.Status {
|
|
||||||
case "valid":
|
|
||||||
resp, err := httpGet(order.Certificate)
|
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
|
|
||||||
cert, err := ioutil.ReadAll(limitReader(resp.Body, maxBodySize))
|
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// The issuer certificate link is always supplied via an "up" link
|
|
||||||
// in the response headers of a new certificate.
|
|
||||||
links := parseLinks(resp.Header["Link"])
|
|
||||||
if link, ok := links["up"]; ok {
|
|
||||||
issuerCert, err := c.getIssuerCertificate(link)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
// If we fail to acquire the issuer cert, return the issued certificate - do not fail.
|
|
||||||
logf("[WARNING][%s] acme: Could not bundle issuer certificate: %v", certRes.Domain, err)
|
|
||||||
} else {
|
|
||||||
issuerCert = pemEncode(derCertificateBytes(issuerCert))
|
|
||||||
|
|
||||||
// If bundle is true, we want to return a certificate bundle.
|
|
||||||
// To do this, we append the issuer cert to the issued cert.
|
|
||||||
if bundle {
|
|
||||||
cert = append(cert, issuerCert...)
|
|
||||||
}
|
|
||||||
|
|
||||||
certRes.IssuerCertificate = issuerCert
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
certRes.Certificate = cert
|
|
||||||
certRes.CertURL = order.Certificate
|
|
||||||
certRes.CertStableURL = order.Certificate
|
|
||||||
logf("[INFO][%s] Server responded with a certificate.", certRes.Domain)
|
|
||||||
return true, nil
|
|
||||||
|
|
||||||
case "processing":
|
|
||||||
return false, nil
|
|
||||||
case "invalid":
|
|
||||||
return false, errors.New("Order has invalid state: invalid")
|
|
||||||
}
|
|
||||||
|
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// getIssuerCertificate requests the issuer certificate
|
|
||||||
func (c *Client) getIssuerCertificate(url string) ([]byte, error) {
|
|
||||||
logf("[INFO] acme: Requesting issuer cert from %s", url)
|
|
||||||
resp, err := httpGet(url)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
issuerBytes, err := ioutil.ReadAll(limitReader(resp.Body, maxBodySize))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = x509.ParseCertificate(issuerBytes)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return issuerBytes, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseLinks(links []string) map[string]string {
|
|
||||||
aBrkt := regexp.MustCompile("[<>]")
|
|
||||||
slver := regexp.MustCompile("(.+) *= *\"(.+)\"")
|
|
||||||
linkMap := make(map[string]string)
|
|
||||||
|
|
||||||
for _, link := range links {
|
|
||||||
|
|
||||||
link = aBrkt.ReplaceAllString(link, "")
|
|
||||||
parts := strings.Split(link, ";")
|
|
||||||
|
|
||||||
matches := slver.FindStringSubmatch(parts[1])
|
|
||||||
if len(matches) > 0 {
|
|
||||||
linkMap[matches[2]] = parts[0]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return linkMap
|
|
||||||
}
|
|
||||||
|
|
||||||
// validate makes the ACME server start validating a
|
|
||||||
// challenge response, only returning once it is done.
|
|
||||||
func validate(j *jws, domain, uri string, c challenge) error {
|
|
||||||
var chlng challenge
|
|
||||||
|
|
||||||
hdr, err := postJSON(j, uri, c, &chlng)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// After the path is sent, the ACME server will access our server.
|
|
||||||
// Repeatedly check the server for an updated status on our request.
|
|
||||||
for {
|
|
||||||
switch chlng.Status {
|
|
||||||
case "valid":
|
|
||||||
logf("[INFO][%s] The server validated our request", domain)
|
|
||||||
return nil
|
|
||||||
case "pending":
|
|
||||||
break
|
|
||||||
case "invalid":
|
|
||||||
return handleChallengeError(chlng)
|
|
||||||
default:
|
|
||||||
return errors.New("The server returned an unexpected state")
|
|
||||||
}
|
|
||||||
|
|
||||||
ra, err := strconv.Atoi(hdr.Get("Retry-After"))
|
|
||||||
if err != nil {
|
|
||||||
// The ACME server MUST return a Retry-After.
|
|
||||||
// If it doesn't, we'll just poll hard.
|
|
||||||
ra = 5
|
|
||||||
}
|
|
||||||
time.Sleep(time.Duration(ra) * time.Second)
|
|
||||||
|
|
||||||
hdr, err = getJSON(uri, &chlng)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,330 +0,0 @@
|
||||||
package acme
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto"
|
|
||||||
"crypto/rand"
|
|
||||||
"crypto/rsa"
|
|
||||||
"encoding/json"
|
|
||||||
"net"
|
|
||||||
"net/http"
|
|
||||||
"net/http/httptest"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestNewClient(t *testing.T) {
|
|
||||||
keyBits := 32 // small value keeps test fast
|
|
||||||
keyType := RSA2048
|
|
||||||
key, err := rsa.GenerateKey(rand.Reader, keyBits)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal("Could not generate test key:", err)
|
|
||||||
}
|
|
||||||
user := mockUser{
|
|
||||||
email: "test@test.com",
|
|
||||||
regres: new(RegistrationResource),
|
|
||||||
privatekey: key,
|
|
||||||
}
|
|
||||||
|
|
||||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
data, _ := json.Marshal(directory{
|
|
||||||
NewNonceURL: "http://test",
|
|
||||||
NewAccountURL: "http://test",
|
|
||||||
NewOrderURL: "http://test",
|
|
||||||
RevokeCertURL: "http://test",
|
|
||||||
KeyChangeURL: "http://test",
|
|
||||||
})
|
|
||||||
w.Write(data)
|
|
||||||
}))
|
|
||||||
|
|
||||||
client, err := NewClient(ts.URL, user, keyType)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Could not create client: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if client.jws == nil {
|
|
||||||
t.Fatalf("Expected client.jws to not be nil")
|
|
||||||
}
|
|
||||||
if expected, actual := key, client.jws.privKey; actual != expected {
|
|
||||||
t.Errorf("Expected jws.privKey to be %p but was %p", expected, actual)
|
|
||||||
}
|
|
||||||
|
|
||||||
if client.keyType != keyType {
|
|
||||||
t.Errorf("Expected keyType to be %s but was %s", keyType, client.keyType)
|
|
||||||
}
|
|
||||||
|
|
||||||
if expected, actual := 1, len(client.solvers); actual != expected {
|
|
||||||
t.Fatalf("Expected %d solver(s), got %d", expected, actual)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestClientOptPort(t *testing.T) {
|
|
||||||
keyBits := 32 // small value keeps test fast
|
|
||||||
key, err := rsa.GenerateKey(rand.Reader, keyBits)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal("Could not generate test key:", err)
|
|
||||||
}
|
|
||||||
user := mockUser{
|
|
||||||
email: "test@test.com",
|
|
||||||
regres: new(RegistrationResource),
|
|
||||||
privatekey: key,
|
|
||||||
}
|
|
||||||
|
|
||||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
data, _ := json.Marshal(directory{
|
|
||||||
NewNonceURL: "http://test",
|
|
||||||
NewAccountURL: "http://test",
|
|
||||||
NewOrderURL: "http://test",
|
|
||||||
RevokeCertURL: "http://test",
|
|
||||||
KeyChangeURL: "http://test",
|
|
||||||
})
|
|
||||||
w.Write(data)
|
|
||||||
}))
|
|
||||||
|
|
||||||
optPort := "1234"
|
|
||||||
optHost := ""
|
|
||||||
client, err := NewClient(ts.URL, user, RSA2048)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Could not create client: %v", err)
|
|
||||||
}
|
|
||||||
client.SetHTTPAddress(net.JoinHostPort(optHost, optPort))
|
|
||||||
|
|
||||||
httpSolver, ok := client.solvers[HTTP01].(*httpChallenge)
|
|
||||||
if !ok {
|
|
||||||
t.Fatal("Expected http-01 solver to be httpChallenge type")
|
|
||||||
}
|
|
||||||
if httpSolver.jws != client.jws {
|
|
||||||
t.Error("Expected http-01 to have same jws as client")
|
|
||||||
}
|
|
||||||
if got := httpSolver.provider.(*HTTPProviderServer).port; got != optPort {
|
|
||||||
t.Errorf("Expected http-01 to have port %s but was %s", optPort, got)
|
|
||||||
}
|
|
||||||
if got := httpSolver.provider.(*HTTPProviderServer).iface; got != optHost {
|
|
||||||
t.Errorf("Expected http-01 to have iface %s but was %s", optHost, got)
|
|
||||||
}
|
|
||||||
|
|
||||||
/* httpsSolver, ok := client.solvers[TLSSNI01].(*tlsSNIChallenge)
|
|
||||||
if !ok {
|
|
||||||
t.Fatal("Expected tls-sni-01 solver to be httpChallenge type")
|
|
||||||
}
|
|
||||||
if httpsSolver.jws != client.jws {
|
|
||||||
t.Error("Expected tls-sni-01 to have same jws as client")
|
|
||||||
}
|
|
||||||
if got := httpsSolver.provider.(*TLSProviderServer).port; got != optPort {
|
|
||||||
t.Errorf("Expected tls-sni-01 to have port %s but was %s", optPort, got)
|
|
||||||
}
|
|
||||||
if got := httpsSolver.provider.(*TLSProviderServer).iface; got != optHost {
|
|
||||||
t.Errorf("Expected tls-sni-01 to have port %s but was %s", optHost, got)
|
|
||||||
} */
|
|
||||||
|
|
||||||
// test setting different host
|
|
||||||
optHost = "127.0.0.1"
|
|
||||||
client.SetHTTPAddress(net.JoinHostPort(optHost, optPort))
|
|
||||||
|
|
||||||
if got := httpSolver.provider.(*HTTPProviderServer).iface; got != optHost {
|
|
||||||
t.Errorf("Expected http-01 to have iface %s but was %s", optHost, got)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestNotHoldingLockWhileMakingHTTPRequests(t *testing.T) {
|
|
||||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
time.Sleep(250 * time.Millisecond)
|
|
||||||
w.Header().Add("Replay-Nonce", "12345")
|
|
||||||
w.Header().Add("Retry-After", "0")
|
|
||||||
writeJSONResponse(w, &challenge{Type: "http-01", Status: "Valid", URL: "http://example.com/", Token: "token"})
|
|
||||||
}))
|
|
||||||
defer ts.Close()
|
|
||||||
|
|
||||||
privKey, _ := rsa.GenerateKey(rand.Reader, 512)
|
|
||||||
j := &jws{privKey: privKey, getNonceURL: ts.URL}
|
|
||||||
ch := make(chan bool)
|
|
||||||
resultCh := make(chan bool)
|
|
||||||
go func() {
|
|
||||||
j.Nonce()
|
|
||||||
ch <- true
|
|
||||||
}()
|
|
||||||
go func() {
|
|
||||||
j.Nonce()
|
|
||||||
ch <- true
|
|
||||||
}()
|
|
||||||
go func() {
|
|
||||||
<-ch
|
|
||||||
<-ch
|
|
||||||
resultCh <- true
|
|
||||||
}()
|
|
||||||
select {
|
|
||||||
case <-resultCh:
|
|
||||||
case <-time.After(400 * time.Millisecond):
|
|
||||||
t.Fatal("JWS is probably holding a lock while making HTTP request")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestValidate(t *testing.T) {
|
|
||||||
var statuses []string
|
|
||||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
// Minimal stub ACME server for validation.
|
|
||||||
w.Header().Add("Replay-Nonce", "12345")
|
|
||||||
w.Header().Add("Retry-After", "0")
|
|
||||||
switch r.Method {
|
|
||||||
case "HEAD":
|
|
||||||
case "POST":
|
|
||||||
st := statuses[0]
|
|
||||||
statuses = statuses[1:]
|
|
||||||
writeJSONResponse(w, &challenge{Type: "http-01", Status: st, URL: "http://example.com/", Token: "token"})
|
|
||||||
|
|
||||||
case "GET":
|
|
||||||
st := statuses[0]
|
|
||||||
statuses = statuses[1:]
|
|
||||||
writeJSONResponse(w, &challenge{Type: "http-01", Status: st, URL: "http://example.com/", Token: "token"})
|
|
||||||
|
|
||||||
default:
|
|
||||||
http.Error(w, r.Method, http.StatusMethodNotAllowed)
|
|
||||||
}
|
|
||||||
}))
|
|
||||||
defer ts.Close()
|
|
||||||
|
|
||||||
privKey, _ := rsa.GenerateKey(rand.Reader, 512)
|
|
||||||
j := &jws{privKey: privKey, getNonceURL: ts.URL}
|
|
||||||
|
|
||||||
tsts := []struct {
|
|
||||||
name string
|
|
||||||
statuses []string
|
|
||||||
want string
|
|
||||||
}{
|
|
||||||
{"POST-unexpected", []string{"weird"}, "unexpected"},
|
|
||||||
{"POST-valid", []string{"valid"}, ""},
|
|
||||||
{"POST-invalid", []string{"invalid"}, "Error"},
|
|
||||||
{"GET-unexpected", []string{"pending", "weird"}, "unexpected"},
|
|
||||||
{"GET-valid", []string{"pending", "valid"}, ""},
|
|
||||||
{"GET-invalid", []string{"pending", "invalid"}, "Error"},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tst := range tsts {
|
|
||||||
statuses = tst.statuses
|
|
||||||
if err := validate(j, "example.com", ts.URL, challenge{Type: "http-01", Token: "token"}); err == nil && tst.want != "" {
|
|
||||||
t.Errorf("[%s] validate: got error %v, want something with %q", tst.name, err, tst.want)
|
|
||||||
} else if err != nil && !strings.Contains(err.Error(), tst.want) {
|
|
||||||
t.Errorf("[%s] validate: got error %v, want something with %q", tst.name, err, tst.want)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestGetChallenges(t *testing.T) {
|
|
||||||
var ts *httptest.Server
|
|
||||||
ts = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
switch r.Method {
|
|
||||||
case "GET", "HEAD":
|
|
||||||
w.Header().Add("Replay-Nonce", "12345")
|
|
||||||
w.Header().Add("Retry-After", "0")
|
|
||||||
writeJSONResponse(w, directory{
|
|
||||||
NewNonceURL: ts.URL,
|
|
||||||
NewAccountURL: ts.URL,
|
|
||||||
NewOrderURL: ts.URL,
|
|
||||||
RevokeCertURL: ts.URL,
|
|
||||||
KeyChangeURL: ts.URL,
|
|
||||||
})
|
|
||||||
case "POST":
|
|
||||||
writeJSONResponse(w, orderMessage{})
|
|
||||||
}
|
|
||||||
}))
|
|
||||||
defer ts.Close()
|
|
||||||
|
|
||||||
keyBits := 512 // small value keeps test fast
|
|
||||||
keyType := RSA2048
|
|
||||||
key, err := rsa.GenerateKey(rand.Reader, keyBits)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal("Could not generate test key:", err)
|
|
||||||
}
|
|
||||||
user := mockUser{
|
|
||||||
email: "test@test.com",
|
|
||||||
regres: &RegistrationResource{URI: ts.URL},
|
|
||||||
privatekey: key,
|
|
||||||
}
|
|
||||||
|
|
||||||
client, err := NewClient(ts.URL, user, keyType)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Could not create client: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = client.createOrderForIdentifiers([]string{"example.com"})
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal("Expecting \"Server did not provide next link to proceed\" error, got nil")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestResolveAccountByKey(t *testing.T) {
|
|
||||||
keyBits := 512
|
|
||||||
keyType := RSA2048
|
|
||||||
key, err := rsa.GenerateKey(rand.Reader, keyBits)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal("Could not generate test key:", err)
|
|
||||||
}
|
|
||||||
user := mockUser{
|
|
||||||
email: "test@test.com",
|
|
||||||
regres: new(RegistrationResource),
|
|
||||||
privatekey: key,
|
|
||||||
}
|
|
||||||
|
|
||||||
var ts *httptest.Server
|
|
||||||
ts = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
switch r.RequestURI {
|
|
||||||
case "/directory":
|
|
||||||
writeJSONResponse(w, directory{
|
|
||||||
NewNonceURL: ts.URL + "/nonce",
|
|
||||||
NewAccountURL: ts.URL + "/account",
|
|
||||||
NewOrderURL: ts.URL + "/newOrder",
|
|
||||||
RevokeCertURL: ts.URL + "/revokeCert",
|
|
||||||
KeyChangeURL: ts.URL + "/keyChange",
|
|
||||||
})
|
|
||||||
case "/nonce":
|
|
||||||
w.Header().Add("Replay-Nonce", "12345")
|
|
||||||
w.Header().Add("Retry-After", "0")
|
|
||||||
case "/account":
|
|
||||||
w.Header().Set("Location", ts.URL+"/account_recovery")
|
|
||||||
case "/account_recovery":
|
|
||||||
writeJSONResponse(w, accountMessage{
|
|
||||||
Status: "valid",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}))
|
|
||||||
|
|
||||||
client, err := NewClient(ts.URL+"/directory", user, keyType)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Could not create client: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if res, err := client.ResolveAccountByKey(); err != nil {
|
|
||||||
t.Fatalf("Unexpected error resolving account by key: %v", err)
|
|
||||||
} else if res.Body.Status != "valid" {
|
|
||||||
t.Errorf("Unexpected account status: %v", res.Body.Status)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// writeJSONResponse marshals the body as JSON and writes it to the response.
|
|
||||||
func writeJSONResponse(w http.ResponseWriter, body interface{}) {
|
|
||||||
bs, err := json.Marshal(body)
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
|
||||||
if _, err := w.Write(bs); err != nil {
|
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// stubValidate is like validate, except it does nothing.
|
|
||||||
func stubValidate(j *jws, domain, uri string, chlng challenge) error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type mockUser struct {
|
|
||||||
email string
|
|
||||||
regres *RegistrationResource
|
|
||||||
privatekey *rsa.PrivateKey
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u mockUser) GetEmail() string { return u.email }
|
|
||||||
func (u mockUser) GetRegistration() *RegistrationResource { return u.regres }
|
|
||||||
func (u mockUser) GetPrivateKey() crypto.PrivateKey { return u.privatekey }
|
|
342
acmev2/crypto.go
342
acmev2/crypto.go
|
@ -1,342 +0,0 @@
|
||||||
package acme
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"crypto"
|
|
||||||
"crypto/ecdsa"
|
|
||||||
"crypto/elliptic"
|
|
||||||
"crypto/rand"
|
|
||||||
"crypto/rsa"
|
|
||||||
"crypto/x509"
|
|
||||||
"crypto/x509/pkix"
|
|
||||||
"encoding/asn1"
|
|
||||||
"encoding/base64"
|
|
||||||
"encoding/pem"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"io/ioutil"
|
|
||||||
"math/big"
|
|
||||||
"net/http"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"golang.org/x/crypto/ocsp"
|
|
||||||
jose "gopkg.in/square/go-jose.v2"
|
|
||||||
)
|
|
||||||
|
|
||||||
// KeyType represents the key algo as well as the key size or curve to use.
|
|
||||||
type KeyType string
|
|
||||||
type derCertificateBytes []byte
|
|
||||||
|
|
||||||
// Constants for all key types we support.
|
|
||||||
const (
|
|
||||||
EC256 = KeyType("P256")
|
|
||||||
EC384 = KeyType("P384")
|
|
||||||
RSA2048 = KeyType("2048")
|
|
||||||
RSA4096 = KeyType("4096")
|
|
||||||
RSA8192 = KeyType("8192")
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
// OCSPGood means that the certificate is valid.
|
|
||||||
OCSPGood = ocsp.Good
|
|
||||||
// OCSPRevoked means that the certificate has been deliberately revoked.
|
|
||||||
OCSPRevoked = ocsp.Revoked
|
|
||||||
// OCSPUnknown means that the OCSP responder doesn't know about the certificate.
|
|
||||||
OCSPUnknown = ocsp.Unknown
|
|
||||||
// OCSPServerFailed means that the OCSP responder failed to process the request.
|
|
||||||
OCSPServerFailed = ocsp.ServerFailed
|
|
||||||
)
|
|
||||||
|
|
||||||
// Constants for OCSP must staple
|
|
||||||
var (
|
|
||||||
tlsFeatureExtensionOID = asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 1, 24}
|
|
||||||
ocspMustStapleFeature = []byte{0x30, 0x03, 0x02, 0x01, 0x05}
|
|
||||||
)
|
|
||||||
|
|
||||||
// GetOCSPForCert takes a PEM encoded cert or cert bundle returning the raw OCSP response,
|
|
||||||
// the parsed response, and an error, if any. The returned []byte can be passed directly
|
|
||||||
// into the OCSPStaple property of a tls.Certificate. If the bundle only contains the
|
|
||||||
// issued certificate, this function will try to get the issuer certificate from the
|
|
||||||
// IssuingCertificateURL in the certificate. If the []byte and/or ocsp.Response return
|
|
||||||
// values are nil, the OCSP status may be assumed OCSPUnknown.
|
|
||||||
func GetOCSPForCert(bundle []byte) ([]byte, *ocsp.Response, error) {
|
|
||||||
certificates, err := parsePEMBundle(bundle)
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// We expect the certificate slice to be ordered downwards the chain.
|
|
||||||
// SRV CRT -> CA. We need to pull the leaf and issuer certs out of it,
|
|
||||||
// which should always be the first two certificates. If there's no
|
|
||||||
// OCSP server listed in the leaf cert, there's nothing to do. And if
|
|
||||||
// we have only one certificate so far, we need to get the issuer cert.
|
|
||||||
issuedCert := certificates[0]
|
|
||||||
if len(issuedCert.OCSPServer) == 0 {
|
|
||||||
return nil, nil, errors.New("no OCSP server specified in cert")
|
|
||||||
}
|
|
||||||
if len(certificates) == 1 {
|
|
||||||
// TODO: build fallback. If this fails, check the remaining array entries.
|
|
||||||
if len(issuedCert.IssuingCertificateURL) == 0 {
|
|
||||||
return nil, nil, errors.New("no issuing certificate URL")
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, err := httpGet(issuedCert.IssuingCertificateURL[0])
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
issuerBytes, err := ioutil.ReadAll(limitReader(resp.Body, 1024*1024))
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
issuerCert, err := x509.ParseCertificate(issuerBytes)
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Insert it into the slice on position 0
|
|
||||||
// We want it ordered right SRV CRT -> CA
|
|
||||||
certificates = append(certificates, issuerCert)
|
|
||||||
}
|
|
||||||
issuerCert := certificates[1]
|
|
||||||
|
|
||||||
// Finally kick off the OCSP request.
|
|
||||||
ocspReq, err := ocsp.CreateRequest(issuedCert, issuerCert, nil)
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
reader := bytes.NewReader(ocspReq)
|
|
||||||
req, err := httpPost(issuedCert.OCSPServer[0], "application/ocsp-request", reader)
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
defer req.Body.Close()
|
|
||||||
|
|
||||||
ocspResBytes, err := ioutil.ReadAll(limitReader(req.Body, 1024*1024))
|
|
||||||
ocspRes, err := ocsp.ParseResponse(ocspResBytes, issuerCert)
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return ocspResBytes, ocspRes, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func getKeyAuthorization(token string, key interface{}) (string, error) {
|
|
||||||
var publicKey crypto.PublicKey
|
|
||||||
switch k := key.(type) {
|
|
||||||
case *ecdsa.PrivateKey:
|
|
||||||
publicKey = k.Public()
|
|
||||||
case *rsa.PrivateKey:
|
|
||||||
publicKey = k.Public()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate the Key Authorization for the challenge
|
|
||||||
jwk := &jose.JSONWebKey{Key: publicKey}
|
|
||||||
if jwk == nil {
|
|
||||||
return "", errors.New("Could not generate JWK from key")
|
|
||||||
}
|
|
||||||
thumbBytes, err := jwk.Thumbprint(crypto.SHA256)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
// unpad the base64URL
|
|
||||||
keyThumb := base64.RawURLEncoding.EncodeToString(thumbBytes)
|
|
||||||
|
|
||||||
return token + "." + keyThumb, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// parsePEMBundle parses a certificate bundle from top to bottom and returns
|
|
||||||
// a slice of x509 certificates. This function will error if no certificates are found.
|
|
||||||
func parsePEMBundle(bundle []byte) ([]*x509.Certificate, error) {
|
|
||||||
var certificates []*x509.Certificate
|
|
||||||
var certDERBlock *pem.Block
|
|
||||||
|
|
||||||
for {
|
|
||||||
certDERBlock, bundle = pem.Decode(bundle)
|
|
||||||
if certDERBlock == nil {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
if certDERBlock.Type == "CERTIFICATE" {
|
|
||||||
cert, err := x509.ParseCertificate(certDERBlock.Bytes)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
certificates = append(certificates, cert)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(certificates) == 0 {
|
|
||||||
return nil, errors.New("No certificates were found while parsing the bundle")
|
|
||||||
}
|
|
||||||
|
|
||||||
return certificates, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func parsePEMPrivateKey(key []byte) (crypto.PrivateKey, error) {
|
|
||||||
keyBlock, _ := pem.Decode(key)
|
|
||||||
|
|
||||||
switch keyBlock.Type {
|
|
||||||
case "RSA PRIVATE KEY":
|
|
||||||
return x509.ParsePKCS1PrivateKey(keyBlock.Bytes)
|
|
||||||
case "EC PRIVATE KEY":
|
|
||||||
return x509.ParseECPrivateKey(keyBlock.Bytes)
|
|
||||||
default:
|
|
||||||
return nil, errors.New("Unknown PEM header value")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func generatePrivateKey(keyType KeyType) (crypto.PrivateKey, error) {
|
|
||||||
|
|
||||||
switch keyType {
|
|
||||||
case EC256:
|
|
||||||
return ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
|
||||||
case EC384:
|
|
||||||
return ecdsa.GenerateKey(elliptic.P384(), rand.Reader)
|
|
||||||
case RSA2048:
|
|
||||||
return rsa.GenerateKey(rand.Reader, 2048)
|
|
||||||
case RSA4096:
|
|
||||||
return rsa.GenerateKey(rand.Reader, 4096)
|
|
||||||
case RSA8192:
|
|
||||||
return rsa.GenerateKey(rand.Reader, 8192)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil, fmt.Errorf("Invalid KeyType: %s", keyType)
|
|
||||||
}
|
|
||||||
|
|
||||||
func generateCsr(privateKey crypto.PrivateKey, domain string, san []string, mustStaple bool) ([]byte, error) {
|
|
||||||
template := x509.CertificateRequest{
|
|
||||||
Subject: pkix.Name{
|
|
||||||
CommonName: domain,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(san) > 0 {
|
|
||||||
template.DNSNames = san
|
|
||||||
}
|
|
||||||
|
|
||||||
if mustStaple {
|
|
||||||
template.ExtraExtensions = append(template.ExtraExtensions, pkix.Extension{
|
|
||||||
Id: tlsFeatureExtensionOID,
|
|
||||||
Value: ocspMustStapleFeature,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return x509.CreateCertificateRequest(rand.Reader, &template, privateKey)
|
|
||||||
}
|
|
||||||
|
|
||||||
func pemEncode(data interface{}) []byte {
|
|
||||||
var pemBlock *pem.Block
|
|
||||||
switch key := data.(type) {
|
|
||||||
case *ecdsa.PrivateKey:
|
|
||||||
keyBytes, _ := x509.MarshalECPrivateKey(key)
|
|
||||||
pemBlock = &pem.Block{Type: "EC PRIVATE KEY", Bytes: keyBytes}
|
|
||||||
case *rsa.PrivateKey:
|
|
||||||
pemBlock = &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(key)}
|
|
||||||
break
|
|
||||||
case *x509.CertificateRequest:
|
|
||||||
pemBlock = &pem.Block{Type: "CERTIFICATE REQUEST", Bytes: key.Raw}
|
|
||||||
break
|
|
||||||
case derCertificateBytes:
|
|
||||||
pemBlock = &pem.Block{Type: "CERTIFICATE", Bytes: []byte(data.(derCertificateBytes))}
|
|
||||||
}
|
|
||||||
|
|
||||||
return pem.EncodeToMemory(pemBlock)
|
|
||||||
}
|
|
||||||
|
|
||||||
func pemDecode(data []byte) (*pem.Block, error) {
|
|
||||||
pemBlock, _ := pem.Decode(data)
|
|
||||||
if pemBlock == nil {
|
|
||||||
return nil, fmt.Errorf("Pem decode did not yield a valid block. Is the certificate in the right format?")
|
|
||||||
}
|
|
||||||
|
|
||||||
return pemBlock, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func pemDecodeTox509(pem []byte) (*x509.Certificate, error) {
|
|
||||||
pemBlock, err := pemDecode(pem)
|
|
||||||
if pemBlock == nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return x509.ParseCertificate(pemBlock.Bytes)
|
|
||||||
}
|
|
||||||
|
|
||||||
func pemDecodeTox509CSR(pem []byte) (*x509.CertificateRequest, error) {
|
|
||||||
pemBlock, err := pemDecode(pem)
|
|
||||||
if pemBlock == nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if pemBlock.Type != "CERTIFICATE REQUEST" {
|
|
||||||
return nil, fmt.Errorf("PEM block is not a certificate request")
|
|
||||||
}
|
|
||||||
|
|
||||||
return x509.ParseCertificateRequest(pemBlock.Bytes)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetPEMCertExpiration returns the "NotAfter" date of a PEM encoded certificate.
|
|
||||||
// The certificate has to be PEM encoded. Any other encodings like DER will fail.
|
|
||||||
func GetPEMCertExpiration(cert []byte) (time.Time, error) {
|
|
||||||
pemBlock, err := pemDecode(cert)
|
|
||||||
if pemBlock == nil {
|
|
||||||
return time.Time{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return getCertExpiration(pemBlock.Bytes)
|
|
||||||
}
|
|
||||||
|
|
||||||
// getCertExpiration returns the "NotAfter" date of a DER encoded certificate.
|
|
||||||
func getCertExpiration(cert []byte) (time.Time, error) {
|
|
||||||
pCert, err := x509.ParseCertificate(cert)
|
|
||||||
if err != nil {
|
|
||||||
return time.Time{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return pCert.NotAfter, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func generatePemCert(privKey *rsa.PrivateKey, domain string) ([]byte, error) {
|
|
||||||
derBytes, err := generateDerCert(privKey, time.Time{}, domain)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: derBytes}), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func generateDerCert(privKey *rsa.PrivateKey, expiration time.Time, domain string) ([]byte, error) {
|
|
||||||
serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
|
|
||||||
serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if expiration.IsZero() {
|
|
||||||
expiration = time.Now().Add(365)
|
|
||||||
}
|
|
||||||
|
|
||||||
template := x509.Certificate{
|
|
||||||
SerialNumber: serialNumber,
|
|
||||||
Subject: pkix.Name{
|
|
||||||
CommonName: "ACME Challenge TEMP",
|
|
||||||
},
|
|
||||||
NotBefore: time.Now(),
|
|
||||||
NotAfter: expiration,
|
|
||||||
|
|
||||||
KeyUsage: x509.KeyUsageKeyEncipherment,
|
|
||||||
BasicConstraintsValid: true,
|
|
||||||
DNSNames: []string{domain},
|
|
||||||
}
|
|
||||||
|
|
||||||
return x509.CreateCertificate(rand.Reader, &template, &template, &privKey.PublicKey, privKey)
|
|
||||||
}
|
|
||||||
|
|
||||||
func limitReader(rd io.ReadCloser, numBytes int64) io.ReadCloser {
|
|
||||||
return http.MaxBytesReader(nil, rd, numBytes)
|
|
||||||
}
|
|
|
@ -1,93 +0,0 @@
|
||||||
package acme
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"crypto/rand"
|
|
||||||
"crypto/rsa"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestGeneratePrivateKey(t *testing.T) {
|
|
||||||
key, err := generatePrivateKey(RSA2048)
|
|
||||||
if err != nil {
|
|
||||||
t.Error("Error generating private key:", err)
|
|
||||||
}
|
|
||||||
if key == nil {
|
|
||||||
t.Error("Expected key to not be nil, but it was")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestGenerateCSR(t *testing.T) {
|
|
||||||
key, err := rsa.GenerateKey(rand.Reader, 512)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal("Error generating private key:", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
csr, err := generateCsr(key, "fizz.buzz", nil, true)
|
|
||||||
if err != nil {
|
|
||||||
t.Error("Error generating CSR:", err)
|
|
||||||
}
|
|
||||||
if csr == nil || len(csr) == 0 {
|
|
||||||
t.Error("Expected CSR with data, but it was nil or length 0")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestPEMEncode(t *testing.T) {
|
|
||||||
buf := bytes.NewBufferString("TestingRSAIsSoMuchFun")
|
|
||||||
|
|
||||||
reader := MockRandReader{b: buf}
|
|
||||||
key, err := rsa.GenerateKey(reader, 32)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal("Error generating private key:", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
data := pemEncode(key)
|
|
||||||
|
|
||||||
if data == nil {
|
|
||||||
t.Fatal("Expected result to not be nil, but it was")
|
|
||||||
}
|
|
||||||
if len(data) != 127 {
|
|
||||||
t.Errorf("Expected PEM encoding to be length 127, but it was %d", len(data))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestPEMCertExpiration(t *testing.T) {
|
|
||||||
privKey, err := generatePrivateKey(RSA2048)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal("Error generating private key:", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
expiration := time.Now().Add(365)
|
|
||||||
expiration = expiration.Round(time.Second)
|
|
||||||
certBytes, err := generateDerCert(privKey.(*rsa.PrivateKey), expiration, "test.com")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal("Error generating cert:", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
buf := bytes.NewBufferString("TestingRSAIsSoMuchFun")
|
|
||||||
|
|
||||||
// Some random string should return an error.
|
|
||||||
if ctime, err := GetPEMCertExpiration(buf.Bytes()); err == nil {
|
|
||||||
t.Errorf("Expected getCertExpiration to return an error for garbage string but returned %v", ctime)
|
|
||||||
}
|
|
||||||
|
|
||||||
// A DER encoded certificate should return an error.
|
|
||||||
if _, err := GetPEMCertExpiration(certBytes); err == nil {
|
|
||||||
t.Errorf("Expected getCertExpiration to return an error for DER certificates but returned none.")
|
|
||||||
}
|
|
||||||
|
|
||||||
// A PEM encoded certificate should work ok.
|
|
||||||
pemCert := pemEncode(derCertificateBytes(certBytes))
|
|
||||||
if ctime, err := GetPEMCertExpiration(pemCert); err != nil || !ctime.Equal(expiration.UTC()) {
|
|
||||||
t.Errorf("Expected getCertExpiration to return %v but returned %v. Error: %v", expiration, ctime, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type MockRandReader struct {
|
|
||||||
b *bytes.Buffer
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r MockRandReader) Read(p []byte) (int, error) {
|
|
||||||
return r.b.Read(p)
|
|
||||||
}
|
|
|
@ -1,309 +0,0 @@
|
||||||
package acme
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/sha256"
|
|
||||||
"encoding/base64"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"log"
|
|
||||||
"net"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/miekg/dns"
|
|
||||||
)
|
|
||||||
|
|
||||||
type preCheckDNSFunc func(fqdn, value string) (bool, error)
|
|
||||||
|
|
||||||
var (
|
|
||||||
// PreCheckDNS checks DNS propagation before notifying ACME that
|
|
||||||
// the DNS challenge is ready.
|
|
||||||
PreCheckDNS preCheckDNSFunc = checkDNSPropagation
|
|
||||||
fqdnToZone = map[string]string{}
|
|
||||||
)
|
|
||||||
|
|
||||||
const defaultResolvConf = "/etc/resolv.conf"
|
|
||||||
|
|
||||||
var defaultNameservers = []string{
|
|
||||||
"google-public-dns-a.google.com:53",
|
|
||||||
"google-public-dns-b.google.com:53",
|
|
||||||
}
|
|
||||||
|
|
||||||
// RecursiveNameservers are used to pre-check DNS propagations
|
|
||||||
var RecursiveNameservers = getNameservers(defaultResolvConf, defaultNameservers)
|
|
||||||
|
|
||||||
// DNSTimeout is used to override the default DNS timeout of 10 seconds.
|
|
||||||
var DNSTimeout = 10 * time.Second
|
|
||||||
|
|
||||||
// getNameservers attempts to get systems nameservers before falling back to the defaults
|
|
||||||
func getNameservers(path string, defaults []string) []string {
|
|
||||||
config, err := dns.ClientConfigFromFile(path)
|
|
||||||
if err != nil || len(config.Servers) == 0 {
|
|
||||||
return defaults
|
|
||||||
}
|
|
||||||
|
|
||||||
systemNameservers := []string{}
|
|
||||||
for _, server := range config.Servers {
|
|
||||||
// ensure all servers have a port number
|
|
||||||
if _, _, err := net.SplitHostPort(server); err != nil {
|
|
||||||
systemNameservers = append(systemNameservers, net.JoinHostPort(server, "53"))
|
|
||||||
} else {
|
|
||||||
systemNameservers = append(systemNameservers, server)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return systemNameservers
|
|
||||||
}
|
|
||||||
|
|
||||||
// DNS01Record returns a DNS record which will fulfill the `dns-01` challenge
|
|
||||||
func DNS01Record(domain, keyAuth string) (fqdn string, value string, ttl int) {
|
|
||||||
keyAuthShaBytes := sha256.Sum256([]byte(keyAuth))
|
|
||||||
// base64URL encoding without padding
|
|
||||||
value = base64.RawURLEncoding.EncodeToString(keyAuthShaBytes[:sha256.Size])
|
|
||||||
ttl = 120
|
|
||||||
fqdn = fmt.Sprintf("_acme-challenge.%s.", domain)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// dnsChallenge implements the dns-01 challenge according to ACME 7.5
|
|
||||||
type dnsChallenge struct {
|
|
||||||
jws *jws
|
|
||||||
validate validateFunc
|
|
||||||
provider ChallengeProvider
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *dnsChallenge) Solve(chlng challenge, domain string) error {
|
|
||||||
logf("[INFO][%s] acme: Trying to solve DNS-01", domain)
|
|
||||||
|
|
||||||
if s.provider == nil {
|
|
||||||
return errors.New("No DNS Provider configured")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate the Key Authorization for the challenge
|
|
||||||
keyAuth, err := getKeyAuthorization(chlng.Token, s.jws.privKey)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
err = s.provider.Present(domain, chlng.Token, keyAuth)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("Error presenting token: %s", err)
|
|
||||||
}
|
|
||||||
defer func() {
|
|
||||||
err := s.provider.CleanUp(domain, chlng.Token, keyAuth)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("Error cleaning up %s: %v ", domain, err)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
fqdn, value, _ := DNS01Record(domain, keyAuth)
|
|
||||||
|
|
||||||
logf("[INFO][%s] Checking DNS record propagation using %+v", domain, RecursiveNameservers)
|
|
||||||
|
|
||||||
var timeout, interval time.Duration
|
|
||||||
switch provider := s.provider.(type) {
|
|
||||||
case ChallengeProviderTimeout:
|
|
||||||
timeout, interval = provider.Timeout()
|
|
||||||
default:
|
|
||||||
timeout, interval = 60*time.Second, 2*time.Second
|
|
||||||
}
|
|
||||||
|
|
||||||
err = WaitFor(timeout, interval, func() (bool, error) {
|
|
||||||
return PreCheckDNS(fqdn, value)
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return s.validate(s.jws, domain, chlng.URL, challenge{Type: chlng.Type, Token: chlng.Token, KeyAuthorization: keyAuth})
|
|
||||||
}
|
|
||||||
|
|
||||||
// checkDNSPropagation checks if the expected TXT record has been propagated to all authoritative nameservers.
|
|
||||||
func checkDNSPropagation(fqdn, value string) (bool, error) {
|
|
||||||
// Initial attempt to resolve at the recursive NS
|
|
||||||
r, err := dnsQuery(fqdn, dns.TypeTXT, RecursiveNameservers, true)
|
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
if r.Rcode == dns.RcodeSuccess {
|
|
||||||
// If we see a CNAME here then use the alias
|
|
||||||
for _, rr := range r.Answer {
|
|
||||||
if cn, ok := rr.(*dns.CNAME); ok {
|
|
||||||
if cn.Hdr.Name == fqdn {
|
|
||||||
fqdn = cn.Target
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
authoritativeNss, err := lookupNameservers(fqdn)
|
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return checkAuthoritativeNss(fqdn, value, authoritativeNss)
|
|
||||||
}
|
|
||||||
|
|
||||||
// checkAuthoritativeNss queries each of the given nameservers for the expected TXT record.
|
|
||||||
func checkAuthoritativeNss(fqdn, value string, nameservers []string) (bool, error) {
|
|
||||||
for _, ns := range nameservers {
|
|
||||||
r, err := dnsQuery(fqdn, dns.TypeTXT, []string{net.JoinHostPort(ns, "53")}, false)
|
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if r.Rcode != dns.RcodeSuccess {
|
|
||||||
return false, fmt.Errorf("NS %s returned %s for %s", ns, dns.RcodeToString[r.Rcode], fqdn)
|
|
||||||
}
|
|
||||||
|
|
||||||
var found bool
|
|
||||||
for _, rr := range r.Answer {
|
|
||||||
if txt, ok := rr.(*dns.TXT); ok {
|
|
||||||
if strings.Join(txt.Txt, "") == value {
|
|
||||||
found = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !found {
|
|
||||||
return false, fmt.Errorf("NS %s did not return the expected TXT record", ns)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// dnsQuery will query a nameserver, iterating through the supplied servers as it retries
|
|
||||||
// The nameserver should include a port, to facilitate testing where we talk to a mock dns server.
|
|
||||||
func dnsQuery(fqdn string, rtype uint16, nameservers []string, recursive bool) (in *dns.Msg, err error) {
|
|
||||||
m := new(dns.Msg)
|
|
||||||
m.SetQuestion(fqdn, rtype)
|
|
||||||
m.SetEdns0(4096, false)
|
|
||||||
|
|
||||||
if !recursive {
|
|
||||||
m.RecursionDesired = false
|
|
||||||
}
|
|
||||||
|
|
||||||
// Will retry the request based on the number of servers (n+1)
|
|
||||||
for i := 1; i <= len(nameservers)+1; i++ {
|
|
||||||
ns := nameservers[i%len(nameservers)]
|
|
||||||
udp := &dns.Client{Net: "udp", Timeout: DNSTimeout}
|
|
||||||
in, _, err = udp.Exchange(m, ns)
|
|
||||||
|
|
||||||
if err == dns.ErrTruncated {
|
|
||||||
tcp := &dns.Client{Net: "tcp", Timeout: DNSTimeout}
|
|
||||||
// If the TCP request succeeds, the err will reset to nil
|
|
||||||
in, _, err = tcp.Exchange(m, ns)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err == nil {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// lookupNameservers returns the authoritative nameservers for the given fqdn.
|
|
||||||
func lookupNameservers(fqdn string) ([]string, error) {
|
|
||||||
var authoritativeNss []string
|
|
||||||
|
|
||||||
zone, err := FindZoneByFqdn(fqdn, RecursiveNameservers)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("Could not determine the zone: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
r, err := dnsQuery(zone, dns.TypeNS, RecursiveNameservers, true)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, rr := range r.Answer {
|
|
||||||
if ns, ok := rr.(*dns.NS); ok {
|
|
||||||
authoritativeNss = append(authoritativeNss, strings.ToLower(ns.Ns))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(authoritativeNss) > 0 {
|
|
||||||
return authoritativeNss, nil
|
|
||||||
}
|
|
||||||
return nil, fmt.Errorf("Could not determine authoritative nameservers")
|
|
||||||
}
|
|
||||||
|
|
||||||
// FindZoneByFqdn determines the zone apex for the given fqdn by recursing up the
|
|
||||||
// domain labels until the nameserver returns a SOA record in the answer section.
|
|
||||||
func FindZoneByFqdn(fqdn string, nameservers []string) (string, error) {
|
|
||||||
// Do we have it cached?
|
|
||||||
if zone, ok := fqdnToZone[fqdn]; ok {
|
|
||||||
return zone, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
labelIndexes := dns.Split(fqdn)
|
|
||||||
for _, index := range labelIndexes {
|
|
||||||
domain := fqdn[index:]
|
|
||||||
|
|
||||||
in, err := dnsQuery(domain, dns.TypeSOA, nameservers, true)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Any response code other than NOERROR and NXDOMAIN is treated as error
|
|
||||||
if in.Rcode != dns.RcodeNameError && in.Rcode != dns.RcodeSuccess {
|
|
||||||
return "", fmt.Errorf("Unexpected response code '%s' for %s",
|
|
||||||
dns.RcodeToString[in.Rcode], domain)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if we got a SOA RR in the answer section
|
|
||||||
if in.Rcode == dns.RcodeSuccess {
|
|
||||||
|
|
||||||
// CNAME records cannot/should not exist at the root of a zone.
|
|
||||||
// So we skip a domain when a CNAME is found.
|
|
||||||
if dnsMsgContainsCNAME(in) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, ans := range in.Answer {
|
|
||||||
if soa, ok := ans.(*dns.SOA); ok {
|
|
||||||
zone := soa.Hdr.Name
|
|
||||||
fqdnToZone[fqdn] = zone
|
|
||||||
return zone, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return "", fmt.Errorf("Could not find the start of authority")
|
|
||||||
}
|
|
||||||
|
|
||||||
// dnsMsgContainsCNAME checks for a CNAME answer in msg
|
|
||||||
func dnsMsgContainsCNAME(msg *dns.Msg) bool {
|
|
||||||
for _, ans := range msg.Answer {
|
|
||||||
if _, ok := ans.(*dns.CNAME); ok {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// ClearFqdnCache clears the cache of fqdn to zone mappings. Primarily used in testing.
|
|
||||||
func ClearFqdnCache() {
|
|
||||||
fqdnToZone = map[string]string{}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ToFqdn converts the name into a fqdn appending a trailing dot.
|
|
||||||
func ToFqdn(name string) string {
|
|
||||||
n := len(name)
|
|
||||||
if n == 0 || name[n-1] == '.' {
|
|
||||||
return name
|
|
||||||
}
|
|
||||||
return name + "."
|
|
||||||
}
|
|
||||||
|
|
||||||
// UnFqdn converts the fqdn into a name removing the trailing dot.
|
|
||||||
func UnFqdn(name string) string {
|
|
||||||
n := len(name)
|
|
||||||
if n != 0 && name[n-1] == '.' {
|
|
||||||
return name[:n-1]
|
|
||||||
}
|
|
||||||
return name
|
|
||||||
}
|
|
|
@ -1,53 +0,0 @@
|
||||||
package acme
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bufio"
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
dnsTemplate = "%s %d IN TXT \"%s\""
|
|
||||||
)
|
|
||||||
|
|
||||||
// DNSProviderManual is an implementation of the ChallengeProvider interface
|
|
||||||
type DNSProviderManual struct{}
|
|
||||||
|
|
||||||
// NewDNSProviderManual returns a DNSProviderManual instance.
|
|
||||||
func NewDNSProviderManual() (*DNSProviderManual, error) {
|
|
||||||
return &DNSProviderManual{}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Present prints instructions for manually creating the TXT record
|
|
||||||
func (*DNSProviderManual) Present(domain, token, keyAuth string) error {
|
|
||||||
fqdn, value, ttl := DNS01Record(domain, keyAuth)
|
|
||||||
dnsRecord := fmt.Sprintf(dnsTemplate, fqdn, ttl, value)
|
|
||||||
|
|
||||||
authZone, err := FindZoneByFqdn(fqdn, RecursiveNameservers)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
logf("[INFO] acme: Please create the following TXT record in your %s zone:", authZone)
|
|
||||||
logf("[INFO] acme: %s", dnsRecord)
|
|
||||||
logf("[INFO] acme: Press 'Enter' when you are done")
|
|
||||||
|
|
||||||
reader := bufio.NewReader(os.Stdin)
|
|
||||||
_, _ = reader.ReadString('\n')
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// CleanUp prints instructions for manually removing the TXT record
|
|
||||||
func (*DNSProviderManual) CleanUp(domain, token, keyAuth string) error {
|
|
||||||
fqdn, _, ttl := DNS01Record(domain, keyAuth)
|
|
||||||
dnsRecord := fmt.Sprintf(dnsTemplate, fqdn, ttl, "...")
|
|
||||||
|
|
||||||
authZone, err := FindZoneByFqdn(fqdn, RecursiveNameservers)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
logf("[INFO] acme: You can now remove this TXT record from your %s zone:", authZone)
|
|
||||||
logf("[INFO] acme: %s", dnsRecord)
|
|
||||||
return nil
|
|
||||||
}
|
|
|
@ -1,200 +0,0 @@
|
||||||
package acme
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bufio"
|
|
||||||
"crypto/rand"
|
|
||||||
"crypto/rsa"
|
|
||||||
"net/http"
|
|
||||||
"net/http/httptest"
|
|
||||||
"os"
|
|
||||||
"reflect"
|
|
||||||
"sort"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
var lookupNameserversTestsOK = []struct {
|
|
||||||
fqdn string
|
|
||||||
nss []string
|
|
||||||
}{
|
|
||||||
{"books.google.com.ng.",
|
|
||||||
[]string{"ns1.google.com.", "ns2.google.com.", "ns3.google.com.", "ns4.google.com."},
|
|
||||||
},
|
|
||||||
{"www.google.com.",
|
|
||||||
[]string{"ns1.google.com.", "ns2.google.com.", "ns3.google.com.", "ns4.google.com."},
|
|
||||||
},
|
|
||||||
{"physics.georgetown.edu.",
|
|
||||||
[]string{"ns1.georgetown.edu.", "ns2.georgetown.edu.", "ns3.georgetown.edu."},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
var lookupNameserversTestsErr = []struct {
|
|
||||||
fqdn string
|
|
||||||
error string
|
|
||||||
}{
|
|
||||||
// invalid tld
|
|
||||||
{"_null.n0n0.",
|
|
||||||
"Could not determine the zone",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
var findZoneByFqdnTests = []struct {
|
|
||||||
fqdn string
|
|
||||||
zone string
|
|
||||||
}{
|
|
||||||
{"mail.google.com.", "google.com."}, // domain is a CNAME
|
|
||||||
{"foo.google.com.", "google.com."}, // domain is a non-existent subdomain
|
|
||||||
{"example.com.ac.", "ac."}, // domain is a eTLD
|
|
||||||
{"cross-zone-example.assets.sh.", "assets.sh."}, // domain is a cross-zone CNAME
|
|
||||||
}
|
|
||||||
|
|
||||||
var checkAuthoritativeNssTests = []struct {
|
|
||||||
fqdn, value string
|
|
||||||
ns []string
|
|
||||||
ok bool
|
|
||||||
}{
|
|
||||||
// TXT RR w/ expected value
|
|
||||||
{"8.8.8.8.asn.routeviews.org.", "151698.8.8.024", []string{"asnums.routeviews.org."},
|
|
||||||
true,
|
|
||||||
},
|
|
||||||
// No TXT RR
|
|
||||||
{"ns1.google.com.", "", []string{"ns2.google.com."},
|
|
||||||
false,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
var checkAuthoritativeNssTestsErr = []struct {
|
|
||||||
fqdn, value string
|
|
||||||
ns []string
|
|
||||||
error string
|
|
||||||
}{
|
|
||||||
// TXT RR /w unexpected value
|
|
||||||
{"8.8.8.8.asn.routeviews.org.", "fe01=", []string{"asnums.routeviews.org."},
|
|
||||||
"did not return the expected TXT record",
|
|
||||||
},
|
|
||||||
// No TXT RR
|
|
||||||
{"ns1.google.com.", "fe01=", []string{"ns2.google.com."},
|
|
||||||
"did not return the expected TXT record",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
var checkResolvConfServersTests = []struct {
|
|
||||||
fixture string
|
|
||||||
expected []string
|
|
||||||
defaults []string
|
|
||||||
}{
|
|
||||||
{"testdata/resolv.conf.1", []string{"10.200.3.249:53", "10.200.3.250:5353", "[2001:4860:4860::8844]:53", "[10.0.0.1]:5353"}, []string{"127.0.0.1:53"}},
|
|
||||||
{"testdata/resolv.conf.nonexistant", []string{"127.0.0.1:53"}, []string{"127.0.0.1:53"}},
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestDNSValidServerResponse(t *testing.T) {
|
|
||||||
PreCheckDNS = func(fqdn, value string) (bool, error) {
|
|
||||||
return true, nil
|
|
||||||
}
|
|
||||||
privKey, _ := rsa.GenerateKey(rand.Reader, 512)
|
|
||||||
|
|
||||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
w.Header().Add("Replay-Nonce", "12345")
|
|
||||||
w.Write([]byte("{\"type\":\"dns01\",\"status\":\"valid\",\"uri\":\"http://some.url\",\"token\":\"http8\"}"))
|
|
||||||
}))
|
|
||||||
|
|
||||||
manualProvider, _ := NewDNSProviderManual()
|
|
||||||
jws := &jws{privKey: privKey, getNonceURL: ts.URL}
|
|
||||||
solver := &dnsChallenge{jws: jws, validate: validate, provider: manualProvider}
|
|
||||||
clientChallenge := challenge{Type: "dns01", Status: "pending", URL: ts.URL, Token: "http8"}
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
time.Sleep(time.Second * 2)
|
|
||||||
f := bufio.NewWriter(os.Stdout)
|
|
||||||
defer f.Flush()
|
|
||||||
f.WriteString("\n")
|
|
||||||
}()
|
|
||||||
|
|
||||||
if err := solver.Solve(clientChallenge, "example.com"); err != nil {
|
|
||||||
t.Errorf("VALID: Expected Solve to return no error but the error was -> %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestPreCheckDNS(t *testing.T) {
|
|
||||||
ok, err := PreCheckDNS("acme-staging.api.letsencrypt.org", "fe01=")
|
|
||||||
if err != nil || !ok {
|
|
||||||
t.Errorf("preCheckDNS failed for acme-staging.api.letsencrypt.org")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestLookupNameserversOK(t *testing.T) {
|
|
||||||
for _, tt := range lookupNameserversTestsOK {
|
|
||||||
nss, err := lookupNameservers(tt.fqdn)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("#%s: got %q; want nil", tt.fqdn, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
sort.Strings(nss)
|
|
||||||
sort.Strings(tt.nss)
|
|
||||||
|
|
||||||
if !reflect.DeepEqual(nss, tt.nss) {
|
|
||||||
t.Errorf("#%s: got %v; want %v", tt.fqdn, nss, tt.nss)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestLookupNameserversErr(t *testing.T) {
|
|
||||||
for _, tt := range lookupNameserversTestsErr {
|
|
||||||
_, err := lookupNameservers(tt.fqdn)
|
|
||||||
if err == nil {
|
|
||||||
t.Fatalf("#%s: expected %q (error); got <nil>", tt.fqdn, tt.error)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !strings.Contains(err.Error(), tt.error) {
|
|
||||||
t.Errorf("#%s: expected %q (error); got %q", tt.fqdn, tt.error, err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestFindZoneByFqdn(t *testing.T) {
|
|
||||||
for _, tt := range findZoneByFqdnTests {
|
|
||||||
res, err := FindZoneByFqdn(tt.fqdn, RecursiveNameservers)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("FindZoneByFqdn failed for %s: %v", tt.fqdn, err)
|
|
||||||
}
|
|
||||||
if res != tt.zone {
|
|
||||||
t.Errorf("%s: got %s; want %s", tt.fqdn, res, tt.zone)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCheckAuthoritativeNss(t *testing.T) {
|
|
||||||
for _, tt := range checkAuthoritativeNssTests {
|
|
||||||
ok, _ := checkAuthoritativeNss(tt.fqdn, tt.value, tt.ns)
|
|
||||||
if ok != tt.ok {
|
|
||||||
t.Errorf("%s: got %t; want %t", tt.fqdn, ok, tt.ok)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCheckAuthoritativeNssErr(t *testing.T) {
|
|
||||||
for _, tt := range checkAuthoritativeNssTestsErr {
|
|
||||||
_, err := checkAuthoritativeNss(tt.fqdn, tt.value, tt.ns)
|
|
||||||
if err == nil {
|
|
||||||
t.Fatalf("#%s: expected %q (error); got <nil>", tt.fqdn, tt.error)
|
|
||||||
}
|
|
||||||
if !strings.Contains(err.Error(), tt.error) {
|
|
||||||
t.Errorf("#%s: expected %q (error); got %q", tt.fqdn, tt.error, err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestResolveConfServers(t *testing.T) {
|
|
||||||
for _, tt := range checkResolvConfServersTests {
|
|
||||||
result := getNameservers(tt.fixture, tt.defaults)
|
|
||||||
|
|
||||||
sort.Strings(result)
|
|
||||||
sort.Strings(tt.expected)
|
|
||||||
if !reflect.DeepEqual(result, tt.expected) {
|
|
||||||
t.Errorf("#%s: expected %q; got %q", tt.fixture, tt.expected, result)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,91 +0,0 @@
|
||||||
package acme
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"io/ioutil"
|
|
||||||
"net/http"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
tosAgreementError = "Terms of service have changed"
|
|
||||||
invalidNonceError = "urn:ietf:params:acme:error:badNonce"
|
|
||||||
)
|
|
||||||
|
|
||||||
// RemoteError is the base type for all errors specific to the ACME protocol.
|
|
||||||
type RemoteError struct {
|
|
||||||
StatusCode int `json:"status,omitempty"`
|
|
||||||
Type string `json:"type"`
|
|
||||||
Detail string `json:"detail"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e RemoteError) Error() string {
|
|
||||||
return fmt.Sprintf("acme: Error %d - %s - %s", e.StatusCode, e.Type, e.Detail)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TOSError represents the error which is returned if the user needs to
|
|
||||||
// accept the TOS.
|
|
||||||
// TODO: include the new TOS url if we can somehow obtain it.
|
|
||||||
type TOSError struct {
|
|
||||||
RemoteError
|
|
||||||
}
|
|
||||||
|
|
||||||
// NonceError represents the error which is returned if the
|
|
||||||
// nonce sent by the client was not accepted by the server.
|
|
||||||
type NonceError struct {
|
|
||||||
RemoteError
|
|
||||||
}
|
|
||||||
|
|
||||||
type domainError struct {
|
|
||||||
Domain string
|
|
||||||
Error error
|
|
||||||
}
|
|
||||||
|
|
||||||
// ObtainError is returned when there are specific errors available
|
|
||||||
// per domain. For example in ObtainCertificate
|
|
||||||
type ObtainError map[string]error
|
|
||||||
|
|
||||||
func (e ObtainError) Error() string {
|
|
||||||
buffer := bytes.NewBufferString("acme: Error -> One or more domains had a problem:\n")
|
|
||||||
for dom, err := range e {
|
|
||||||
buffer.WriteString(fmt.Sprintf("[%s] %s\n", dom, err))
|
|
||||||
}
|
|
||||||
return buffer.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
func handleHTTPError(resp *http.Response) error {
|
|
||||||
var errorDetail RemoteError
|
|
||||||
|
|
||||||
contentType := resp.Header.Get("Content-Type")
|
|
||||||
if contentType == "application/json" || strings.HasPrefix(contentType, "application/problem+json") {
|
|
||||||
err := json.NewDecoder(resp.Body).Decode(&errorDetail)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
detailBytes, err := ioutil.ReadAll(limitReader(resp.Body, maxBodySize))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
errorDetail.Detail = string(detailBytes)
|
|
||||||
}
|
|
||||||
|
|
||||||
errorDetail.StatusCode = resp.StatusCode
|
|
||||||
|
|
||||||
// Check for errors we handle specifically
|
|
||||||
if errorDetail.StatusCode == http.StatusForbidden && errorDetail.Detail == tosAgreementError {
|
|
||||||
return TOSError{errorDetail}
|
|
||||||
}
|
|
||||||
|
|
||||||
if errorDetail.StatusCode == http.StatusBadRequest && errorDetail.Type == invalidNonceError {
|
|
||||||
return NonceError{errorDetail}
|
|
||||||
}
|
|
||||||
|
|
||||||
return errorDetail
|
|
||||||
}
|
|
||||||
|
|
||||||
func handleChallengeError(chlng challenge) error {
|
|
||||||
return chlng.Error
|
|
||||||
}
|
|
160
acmev2/http.go
160
acmev2/http.go
|
@ -1,160 +0,0 @@
|
||||||
package acme
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"net"
|
|
||||||
"net/http"
|
|
||||||
"runtime"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
// UserAgent (if non-empty) will be tacked onto the User-Agent string in requests.
|
|
||||||
var UserAgent string
|
|
||||||
|
|
||||||
// HTTPClient is an HTTP client with a reasonable timeout value.
|
|
||||||
var HTTPClient = http.Client{
|
|
||||||
Transport: &http.Transport{
|
|
||||||
Proxy: http.ProxyFromEnvironment,
|
|
||||||
Dial: (&net.Dialer{
|
|
||||||
Timeout: 30 * time.Second,
|
|
||||||
KeepAlive: 30 * time.Second,
|
|
||||||
}).Dial,
|
|
||||||
TLSHandshakeTimeout: 15 * time.Second,
|
|
||||||
ResponseHeaderTimeout: 15 * time.Second,
|
|
||||||
ExpectContinueTimeout: 1 * time.Second,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
const (
|
|
||||||
// defaultGoUserAgent is the Go HTTP package user agent string. Too
|
|
||||||
// bad it isn't exported. If it changes, we should update it here, too.
|
|
||||||
defaultGoUserAgent = "Go-http-client/1.1"
|
|
||||||
|
|
||||||
// ourUserAgent is the User-Agent of this underlying library package.
|
|
||||||
ourUserAgent = "xenolf-acme"
|
|
||||||
)
|
|
||||||
|
|
||||||
// httpHead performs a HEAD request with a proper User-Agent string.
|
|
||||||
// The response body (resp.Body) is already closed when this function returns.
|
|
||||||
func httpHead(url string) (resp *http.Response, err error) {
|
|
||||||
req, err := http.NewRequest("HEAD", url, nil)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to head %q: %v", url, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
req.Header.Set("User-Agent", userAgent())
|
|
||||||
|
|
||||||
resp, err = HTTPClient.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
return resp, fmt.Errorf("failed to do head %q: %v", url, err)
|
|
||||||
}
|
|
||||||
resp.Body.Close()
|
|
||||||
return resp, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// httpPost performs a POST request with a proper User-Agent string.
|
|
||||||
// Callers should close resp.Body when done reading from it.
|
|
||||||
func httpPost(url string, bodyType string, body io.Reader) (resp *http.Response, err error) {
|
|
||||||
req, err := http.NewRequest("POST", url, body)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to post %q: %v", url, err)
|
|
||||||
}
|
|
||||||
req.Header.Set("Content-Type", bodyType)
|
|
||||||
req.Header.Set("User-Agent", userAgent())
|
|
||||||
|
|
||||||
return HTTPClient.Do(req)
|
|
||||||
}
|
|
||||||
|
|
||||||
// httpGet performs a GET request with a proper User-Agent string.
|
|
||||||
// Callers should close resp.Body when done reading from it.
|
|
||||||
func httpGet(url string) (resp *http.Response, err error) {
|
|
||||||
req, err := http.NewRequest("GET", url, nil)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to get %q: %v", url, err)
|
|
||||||
}
|
|
||||||
req.Header.Set("User-Agent", userAgent())
|
|
||||||
|
|
||||||
return HTTPClient.Do(req)
|
|
||||||
}
|
|
||||||
|
|
||||||
// getJSON performs an HTTP GET request and parses the response body
|
|
||||||
// as JSON, into the provided respBody object.
|
|
||||||
func getJSON(uri string, respBody interface{}) (http.Header, error) {
|
|
||||||
resp, err := httpGet(uri)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to get json %q: %v", uri, err)
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
if resp.StatusCode >= http.StatusBadRequest {
|
|
||||||
return resp.Header, handleHTTPError(resp)
|
|
||||||
}
|
|
||||||
|
|
||||||
return resp.Header, json.NewDecoder(resp.Body).Decode(respBody)
|
|
||||||
}
|
|
||||||
|
|
||||||
// postJSON performs an HTTP POST request and parses the response body
|
|
||||||
// as JSON, into the provided respBody object.
|
|
||||||
func postJSON(j *jws, uri string, reqBody, respBody interface{}) (http.Header, error) {
|
|
||||||
jsonBytes, err := json.Marshal(reqBody)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.New("Failed to marshal network message")
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, err := j.post(uri, jsonBytes)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("Failed to post JWS message. -> %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
if resp.StatusCode >= http.StatusBadRequest {
|
|
||||||
|
|
||||||
err := handleHTTPError(resp)
|
|
||||||
|
|
||||||
switch err.(type) {
|
|
||||||
|
|
||||||
case NonceError:
|
|
||||||
|
|
||||||
// Retry once if the nonce was invalidated
|
|
||||||
|
|
||||||
retryResp, err := j.post(uri, jsonBytes)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("Failed to post JWS message. -> %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
defer retryResp.Body.Close()
|
|
||||||
|
|
||||||
if retryResp.StatusCode >= http.StatusBadRequest {
|
|
||||||
return retryResp.Header, handleHTTPError(retryResp)
|
|
||||||
}
|
|
||||||
|
|
||||||
if respBody == nil {
|
|
||||||
return retryResp.Header, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return retryResp.Header, json.NewDecoder(retryResp.Body).Decode(respBody)
|
|
||||||
|
|
||||||
default:
|
|
||||||
return resp.Header, err
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
if respBody == nil {
|
|
||||||
return resp.Header, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return resp.Header, json.NewDecoder(resp.Body).Decode(respBody)
|
|
||||||
}
|
|
||||||
|
|
||||||
// userAgent builds and returns the User-Agent string to use in requests.
|
|
||||||
func userAgent() string {
|
|
||||||
ua := fmt.Sprintf("%s (%s; %s) %s %s", defaultGoUserAgent, runtime.GOOS, runtime.GOARCH, ourUserAgent, UserAgent)
|
|
||||||
return strings.TrimSpace(ua)
|
|
||||||
}
|
|
|
@ -1,41 +0,0 @@
|
||||||
package acme
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"log"
|
|
||||||
)
|
|
||||||
|
|
||||||
type httpChallenge struct {
|
|
||||||
jws *jws
|
|
||||||
validate validateFunc
|
|
||||||
provider ChallengeProvider
|
|
||||||
}
|
|
||||||
|
|
||||||
// HTTP01ChallengePath returns the URL path for the `http-01` challenge
|
|
||||||
func HTTP01ChallengePath(token string) string {
|
|
||||||
return "/.well-known/acme-challenge/" + token
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *httpChallenge) Solve(chlng challenge, domain string) error {
|
|
||||||
|
|
||||||
logf("[INFO][%s] acme: Trying to solve HTTP-01", domain)
|
|
||||||
|
|
||||||
// Generate the Key Authorization for the challenge
|
|
||||||
keyAuth, err := getKeyAuthorization(chlng.Token, s.jws.privKey)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
err = s.provider.Present(domain, chlng.Token, keyAuth)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("[%s] error presenting token: %v", domain, err)
|
|
||||||
}
|
|
||||||
defer func() {
|
|
||||||
err := s.provider.CleanUp(domain, chlng.Token, keyAuth)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("[%s] error cleaning up: %v", domain, err)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
return s.validate(s.jws, domain, chlng.URL, challenge{Type: chlng.Type, Token: chlng.Token, KeyAuthorization: keyAuth})
|
|
||||||
}
|
|
|
@ -1,79 +0,0 @@
|
||||||
package acme
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"net"
|
|
||||||
"net/http"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
// HTTPProviderServer implements ChallengeProvider for `http-01` challenge
|
|
||||||
// It may be instantiated without using the NewHTTPProviderServer function if
|
|
||||||
// you want only to use the default values.
|
|
||||||
type HTTPProviderServer struct {
|
|
||||||
iface string
|
|
||||||
port string
|
|
||||||
done chan bool
|
|
||||||
listener net.Listener
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewHTTPProviderServer creates a new HTTPProviderServer on the selected interface and port.
|
|
||||||
// Setting iface and / or port to an empty string will make the server fall back to
|
|
||||||
// the "any" interface and port 80 respectively.
|
|
||||||
func NewHTTPProviderServer(iface, port string) *HTTPProviderServer {
|
|
||||||
return &HTTPProviderServer{iface: iface, port: port}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Present starts a web server and makes the token available at `HTTP01ChallengePath(token)` for web requests.
|
|
||||||
func (s *HTTPProviderServer) Present(domain, token, keyAuth string) error {
|
|
||||||
if s.port == "" {
|
|
||||||
s.port = "80"
|
|
||||||
}
|
|
||||||
|
|
||||||
var err error
|
|
||||||
s.listener, err = net.Listen("tcp", net.JoinHostPort(s.iface, s.port))
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("Could not start HTTP server for challenge -> %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
s.done = make(chan bool)
|
|
||||||
go s.serve(domain, token, keyAuth)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// CleanUp closes the HTTP server and removes the token from `HTTP01ChallengePath(token)`
|
|
||||||
func (s *HTTPProviderServer) CleanUp(domain, token, keyAuth string) error {
|
|
||||||
if s.listener == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
s.listener.Close()
|
|
||||||
<-s.done
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *HTTPProviderServer) serve(domain, token, keyAuth string) {
|
|
||||||
path := HTTP01ChallengePath(token)
|
|
||||||
|
|
||||||
// The handler validates the HOST header and request type.
|
|
||||||
// For validation it then writes the token the server returned with the challenge
|
|
||||||
mux := http.NewServeMux()
|
|
||||||
mux.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
if strings.HasPrefix(r.Host, domain) && r.Method == "GET" {
|
|
||||||
w.Header().Add("Content-Type", "text/plain")
|
|
||||||
w.Write([]byte(keyAuth))
|
|
||||||
logf("[INFO][%s] Served key authentication", domain)
|
|
||||||
} else {
|
|
||||||
logf("[WARN] Received request for domain %s with method %s but the domain did not match any challenge. Please ensure your are passing the HOST header properly.", r.Host, r.Method)
|
|
||||||
w.Write([]byte("TEST"))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
httpServer := &http.Server{
|
|
||||||
Handler: mux,
|
|
||||||
}
|
|
||||||
// Once httpServer is shut down we don't want any lingering
|
|
||||||
// connections, so disable KeepAlives.
|
|
||||||
httpServer.SetKeepAlivesEnabled(false)
|
|
||||||
httpServer.Serve(s.listener)
|
|
||||||
s.done <- true
|
|
||||||
}
|
|
|
@ -1,57 +0,0 @@
|
||||||
package acme
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/rand"
|
|
||||||
"crypto/rsa"
|
|
||||||
"io/ioutil"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestHTTPChallenge(t *testing.T) {
|
|
||||||
privKey, _ := rsa.GenerateKey(rand.Reader, 512)
|
|
||||||
j := &jws{privKey: privKey}
|
|
||||||
clientChallenge := challenge{Type: string(HTTP01), Token: "http1"}
|
|
||||||
mockValidate := func(_ *jws, _, _ string, chlng challenge) error {
|
|
||||||
uri := "http://localhost:23457/.well-known/acme-challenge/" + chlng.Token
|
|
||||||
resp, err := httpGet(uri)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
if want := "text/plain"; resp.Header.Get("Content-Type") != want {
|
|
||||||
t.Errorf("Get(%q) Content-Type: got %q, want %q", uri, resp.Header.Get("Content-Type"), want)
|
|
||||||
}
|
|
||||||
|
|
||||||
body, err := ioutil.ReadAll(resp.Body)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
bodyStr := string(body)
|
|
||||||
|
|
||||||
if bodyStr != chlng.KeyAuthorization {
|
|
||||||
t.Errorf("Get(%q) Body: got %q, want %q", uri, bodyStr, chlng.KeyAuthorization)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
solver := &httpChallenge{jws: j, validate: mockValidate, provider: &HTTPProviderServer{port: "23457"}}
|
|
||||||
|
|
||||||
if err := solver.Solve(clientChallenge, "localhost:23457"); err != nil {
|
|
||||||
t.Errorf("Solve error: got %v, want nil", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestHTTPChallengeInvalidPort(t *testing.T) {
|
|
||||||
privKey, _ := rsa.GenerateKey(rand.Reader, 128)
|
|
||||||
j := &jws{privKey: privKey}
|
|
||||||
clientChallenge := challenge{Type: string(HTTP01), Token: "http2"}
|
|
||||||
solver := &httpChallenge{jws: j, validate: stubValidate, provider: &HTTPProviderServer{port: "123456"}}
|
|
||||||
|
|
||||||
if err := solver.Solve(clientChallenge, "localhost:123456"); err == nil {
|
|
||||||
t.Errorf("Solve error: got %v, want error", err)
|
|
||||||
} else if want, want18 := "invalid port 123456", "123456: invalid port"; !strings.HasSuffix(err.Error(), want) && !strings.HasSuffix(err.Error(), want18) {
|
|
||||||
t.Errorf("Solve error: got %q, want suffix %q", err.Error(), want)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,100 +0,0 @@
|
||||||
package acme
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
"net/http/httptest"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestHTTPHeadUserAgent(t *testing.T) {
|
|
||||||
var ua, method string
|
|
||||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
ua = r.Header.Get("User-Agent")
|
|
||||||
method = r.Method
|
|
||||||
}))
|
|
||||||
defer ts.Close()
|
|
||||||
|
|
||||||
_, err := httpHead(ts.URL)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if method != "HEAD" {
|
|
||||||
t.Errorf("Expected method to be HEAD, got %s", method)
|
|
||||||
}
|
|
||||||
if !strings.Contains(ua, ourUserAgent) {
|
|
||||||
t.Errorf("Expected User-Agent to contain '%s', got: '%s'", ourUserAgent, ua)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestHTTPGetUserAgent(t *testing.T) {
|
|
||||||
var ua, method string
|
|
||||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
ua = r.Header.Get("User-Agent")
|
|
||||||
method = r.Method
|
|
||||||
}))
|
|
||||||
defer ts.Close()
|
|
||||||
|
|
||||||
res, err := httpGet(ts.URL)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
res.Body.Close()
|
|
||||||
|
|
||||||
if method != "GET" {
|
|
||||||
t.Errorf("Expected method to be GET, got %s", method)
|
|
||||||
}
|
|
||||||
if !strings.Contains(ua, ourUserAgent) {
|
|
||||||
t.Errorf("Expected User-Agent to contain '%s', got: '%s'", ourUserAgent, ua)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestHTTPPostUserAgent(t *testing.T) {
|
|
||||||
var ua, method string
|
|
||||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
ua = r.Header.Get("User-Agent")
|
|
||||||
method = r.Method
|
|
||||||
}))
|
|
||||||
defer ts.Close()
|
|
||||||
|
|
||||||
res, err := httpPost(ts.URL, "text/plain", strings.NewReader("falalalala"))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
res.Body.Close()
|
|
||||||
|
|
||||||
if method != "POST" {
|
|
||||||
t.Errorf("Expected method to be POST, got %s", method)
|
|
||||||
}
|
|
||||||
if !strings.Contains(ua, ourUserAgent) {
|
|
||||||
t.Errorf("Expected User-Agent to contain '%s', got: '%s'", ourUserAgent, ua)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestUserAgent(t *testing.T) {
|
|
||||||
ua := userAgent()
|
|
||||||
|
|
||||||
if !strings.Contains(ua, defaultGoUserAgent) {
|
|
||||||
t.Errorf("Expected UA to contain %s, got '%s'", defaultGoUserAgent, ua)
|
|
||||||
}
|
|
||||||
if !strings.Contains(ua, ourUserAgent) {
|
|
||||||
t.Errorf("Expected UA to contain %s, got '%s'", ourUserAgent, ua)
|
|
||||||
}
|
|
||||||
if strings.HasSuffix(ua, " ") {
|
|
||||||
t.Errorf("UA should not have trailing spaces; got '%s'", ua)
|
|
||||||
}
|
|
||||||
|
|
||||||
// customize the UA by appending a value
|
|
||||||
UserAgent = "MyApp/1.2.3"
|
|
||||||
ua = userAgent()
|
|
||||||
if !strings.Contains(ua, defaultGoUserAgent) {
|
|
||||||
t.Errorf("Expected UA to contain %s, got '%s'", defaultGoUserAgent, ua)
|
|
||||||
}
|
|
||||||
if !strings.Contains(ua, ourUserAgent) {
|
|
||||||
t.Errorf("Expected UA to contain %s, got '%s'", ourUserAgent, ua)
|
|
||||||
}
|
|
||||||
if !strings.Contains(ua, UserAgent) {
|
|
||||||
t.Errorf("Expected custom UA to contain %s, got '%s'", UserAgent, ua)
|
|
||||||
}
|
|
||||||
}
|
|
167
acmev2/jws.go
167
acmev2/jws.go
|
@ -1,167 +0,0 @@
|
||||||
package acme
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"crypto"
|
|
||||||
"crypto/ecdsa"
|
|
||||||
"crypto/elliptic"
|
|
||||||
"crypto/rsa"
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
"sync"
|
|
||||||
|
|
||||||
"gopkg.in/square/go-jose.v2"
|
|
||||||
)
|
|
||||||
|
|
||||||
type jws struct {
|
|
||||||
getNonceURL string
|
|
||||||
privKey crypto.PrivateKey
|
|
||||||
kid string
|
|
||||||
nonces nonceManager
|
|
||||||
}
|
|
||||||
|
|
||||||
// Posts a JWS signed message to the specified URL.
|
|
||||||
// It does NOT close the response body, so the caller must
|
|
||||||
// do that if no error was returned.
|
|
||||||
func (j *jws) post(url string, content []byte) (*http.Response, error) {
|
|
||||||
signedContent, err := j.signContent(url, content)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("Failed to sign content -> %s", err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
data := bytes.NewBuffer([]byte(signedContent.FullSerialize()))
|
|
||||||
resp, err := httpPost(url, "application/jose+json", data)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("Failed to HTTP POST to %s -> %s", url, err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
nonce, nonceErr := getNonceFromResponse(resp)
|
|
||||||
if nonceErr == nil {
|
|
||||||
j.nonces.Push(nonce)
|
|
||||||
}
|
|
||||||
|
|
||||||
return resp, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (j *jws) signContent(url string, content []byte) (*jose.JSONWebSignature, error) {
|
|
||||||
|
|
||||||
var alg jose.SignatureAlgorithm
|
|
||||||
switch k := j.privKey.(type) {
|
|
||||||
case *rsa.PrivateKey:
|
|
||||||
alg = jose.RS256
|
|
||||||
case *ecdsa.PrivateKey:
|
|
||||||
if k.Curve == elliptic.P256() {
|
|
||||||
alg = jose.ES256
|
|
||||||
} else if k.Curve == elliptic.P384() {
|
|
||||||
alg = jose.ES384
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
jsonKey := jose.JSONWebKey{
|
|
||||||
Key: j.privKey,
|
|
||||||
KeyID: j.kid,
|
|
||||||
}
|
|
||||||
|
|
||||||
signKey := jose.SigningKey{
|
|
||||||
Algorithm: alg,
|
|
||||||
Key: jsonKey,
|
|
||||||
}
|
|
||||||
options := jose.SignerOptions{
|
|
||||||
NonceSource: j,
|
|
||||||
ExtraHeaders: make(map[jose.HeaderKey]interface{}),
|
|
||||||
}
|
|
||||||
options.ExtraHeaders["url"] = url
|
|
||||||
if j.kid == "" {
|
|
||||||
options.EmbedJWK = true
|
|
||||||
}
|
|
||||||
|
|
||||||
signer, err := jose.NewSigner(signKey, &options)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("Failed to create jose signer -> %s", err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
signed, err := signer.Sign(content)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("Failed to sign content -> %s", err.Error())
|
|
||||||
}
|
|
||||||
return signed, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (j *jws) signEABContent(url, kid string, hmac []byte) (*jose.JSONWebSignature, error) {
|
|
||||||
jwk := jose.JSONWebKey{Key: j.privKey}
|
|
||||||
jwkJSON, err := jwk.Public().MarshalJSON()
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("acme: error encoding eab jwk key: %s", err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
signer, err := jose.NewSigner(
|
|
||||||
jose.SigningKey{Algorithm: jose.HS256, Key: hmac},
|
|
||||||
&jose.SignerOptions{
|
|
||||||
EmbedJWK: false,
|
|
||||||
ExtraHeaders: map[jose.HeaderKey]interface{}{
|
|
||||||
"kid": kid,
|
|
||||||
"url": url,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("Failed to create External Account Binding jose signer -> %s", err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
signed, err := signer.Sign(jwkJSON)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("Failed to External Account Binding sign content -> %s", err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
return signed, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (j *jws) Nonce() (string, error) {
|
|
||||||
if nonce, ok := j.nonces.Pop(); ok {
|
|
||||||
return nonce, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return getNonce(j.getNonceURL)
|
|
||||||
}
|
|
||||||
|
|
||||||
type nonceManager struct {
|
|
||||||
nonces []string
|
|
||||||
sync.Mutex
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n *nonceManager) Pop() (string, bool) {
|
|
||||||
n.Lock()
|
|
||||||
defer n.Unlock()
|
|
||||||
|
|
||||||
if len(n.nonces) == 0 {
|
|
||||||
return "", false
|
|
||||||
}
|
|
||||||
|
|
||||||
nonce := n.nonces[len(n.nonces)-1]
|
|
||||||
n.nonces = n.nonces[:len(n.nonces)-1]
|
|
||||||
return nonce, true
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n *nonceManager) Push(nonce string) {
|
|
||||||
n.Lock()
|
|
||||||
defer n.Unlock()
|
|
||||||
n.nonces = append(n.nonces, nonce)
|
|
||||||
}
|
|
||||||
|
|
||||||
func getNonce(url string) (string, error) {
|
|
||||||
resp, err := httpHead(url)
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("Failed to get nonce from HTTP HEAD -> %s", err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
return getNonceFromResponse(resp)
|
|
||||||
}
|
|
||||||
|
|
||||||
func getNonceFromResponse(resp *http.Response) (string, error) {
|
|
||||||
nonce := resp.Header.Get("Replay-Nonce")
|
|
||||||
if nonce == "" {
|
|
||||||
return "", fmt.Errorf("Server did not respond with a proper nonce header")
|
|
||||||
}
|
|
||||||
|
|
||||||
return nonce, nil
|
|
||||||
}
|
|
|
@ -1,106 +0,0 @@
|
||||||
package acme
|
|
||||||
|
|
||||||
import (
|
|
||||||
"time"
|
|
||||||
"encoding/json"
|
|
||||||
)
|
|
||||||
|
|
||||||
// RegistrationResource represents all important informations about a registration
|
|
||||||
// of which the client needs to keep track itself.
|
|
||||||
type RegistrationResource struct {
|
|
||||||
Body accountMessage `json:"body,omitempty"`
|
|
||||||
URI string `json:"uri,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type directory struct {
|
|
||||||
NewNonceURL string `json:"newNonce"`
|
|
||||||
NewAccountURL string `json:"newAccount"`
|
|
||||||
NewOrderURL string `json:"newOrder"`
|
|
||||||
RevokeCertURL string `json:"revokeCert"`
|
|
||||||
KeyChangeURL string `json:"keyChange"`
|
|
||||||
Meta struct {
|
|
||||||
TermsOfService string `json:"termsOfService"`
|
|
||||||
Website string `json:"website"`
|
|
||||||
CaaIdentities []string `json:"caaIdentities"`
|
|
||||||
ExternalAccountRequired bool `json:"externalAccountRequired"`
|
|
||||||
} `json:"meta"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type accountMessage struct {
|
|
||||||
Status string `json:"status,omitempty"`
|
|
||||||
Contact []string `json:"contact,omitempty"`
|
|
||||||
TermsOfServiceAgreed bool `json:"termsOfServiceAgreed,omitempty"`
|
|
||||||
Orders string `json:"orders,omitempty"`
|
|
||||||
OnlyReturnExisting bool `json:"onlyReturnExisting,omitempty"`
|
|
||||||
ExternalAccountBinding json.RawMessage `json:"externalAccountBinding,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type orderResource struct {
|
|
||||||
URL string `json:"url,omitempty"`
|
|
||||||
Domains []string `json:"domains,omitempty"`
|
|
||||||
orderMessage `json:"body,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type orderMessage struct {
|
|
||||||
Status string `json:"status,omitempty"`
|
|
||||||
Expires string `json:"expires,omitempty"`
|
|
||||||
Identifiers []identifier `json:"identifiers"`
|
|
||||||
NotBefore string `json:"notBefore,omitempty"`
|
|
||||||
NotAfter string `json:"notAfter,omitempty"`
|
|
||||||
Authorizations []string `json:"authorizations,omitempty"`
|
|
||||||
Finalize string `json:"finalize,omitempty"`
|
|
||||||
Certificate string `json:"certificate,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type authorization struct {
|
|
||||||
Status string `json:"status"`
|
|
||||||
Expires time.Time `json:"expires"`
|
|
||||||
Identifier identifier `json:"identifier"`
|
|
||||||
Challenges []challenge `json:"challenges"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type identifier struct {
|
|
||||||
Type string `json:"type"`
|
|
||||||
Value string `json:"value"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type challenge struct {
|
|
||||||
URL string `json:"url"`
|
|
||||||
Type string `json:"type"`
|
|
||||||
Status string `json:"status"`
|
|
||||||
Token string `json:"token"`
|
|
||||||
Validated time.Time `json:"validated"`
|
|
||||||
KeyAuthorization string `json:"keyAuthorization"`
|
|
||||||
Error RemoteError `json:"error"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type csrMessage struct {
|
|
||||||
Csr string `json:"csr"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type emptyObjectMessage struct {
|
|
||||||
}
|
|
||||||
|
|
||||||
type revokeCertMessage struct {
|
|
||||||
Certificate string `json:"certificate"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type deactivateAuthMessage struct {
|
|
||||||
Status string `jsom:"status"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// CertificateResource represents a CA issued certificate.
|
|
||||||
// PrivateKey, Certificate and IssuerCertificate are all
|
|
||||||
// already PEM encoded and can be directly written to disk.
|
|
||||||
// Certificate may be a certificate bundle, depending on the
|
|
||||||
// options supplied to create it.
|
|
||||||
type CertificateResource struct {
|
|
||||||
Domain string `json:"domain"`
|
|
||||||
CertURL string `json:"certUrl"`
|
|
||||||
CertStableURL string `json:"certStableUrl"`
|
|
||||||
AccountRef string `json:"accountRef,omitempty"`
|
|
||||||
PrivateKey []byte `json:"-"`
|
|
||||||
Certificate []byte `json:"-"`
|
|
||||||
IssuerCertificate []byte `json:"-"`
|
|
||||||
CSR []byte `json:"-"`
|
|
||||||
}
|
|
|
@ -1 +0,0 @@
|
||||||
package acme
|
|
|
@ -1,28 +0,0 @@
|
||||||
package acme
|
|
||||||
|
|
||||||
import "time"
|
|
||||||
|
|
||||||
// ChallengeProvider enables implementing a custom challenge
|
|
||||||
// provider. Present presents the solution to a challenge available to
|
|
||||||
// be solved. CleanUp will be called by the challenge if Present ends
|
|
||||||
// in a non-error state.
|
|
||||||
type ChallengeProvider interface {
|
|
||||||
Present(domain, token, keyAuth string) error
|
|
||||||
CleanUp(domain, token, keyAuth string) error
|
|
||||||
}
|
|
||||||
|
|
||||||
// ChallengeProviderTimeout allows for implementing a
|
|
||||||
// ChallengeProvider where an unusually long timeout is required when
|
|
||||||
// waiting for an ACME challenge to be satisfied, such as when
|
|
||||||
// checking for DNS record progagation. If an implementor of a
|
|
||||||
// ChallengeProvider provides a Timeout method, then the return values
|
|
||||||
// of the Timeout method will be used when appropriate by the acme
|
|
||||||
// package. The interval value is the time between checks.
|
|
||||||
//
|
|
||||||
// The default values used for timeout and interval are 60 seconds and
|
|
||||||
// 2 seconds respectively. These are used when no Timeout method is
|
|
||||||
// defined for the ChallengeProvider.
|
|
||||||
type ChallengeProviderTimeout interface {
|
|
||||||
ChallengeProvider
|
|
||||||
Timeout() (timeout, interval time.Duration)
|
|
||||||
}
|
|
5
acmev2/testdata/resolv.conf.1
vendored
5
acmev2/testdata/resolv.conf.1
vendored
|
@ -1,5 +0,0 @@
|
||||||
domain company.com
|
|
||||||
nameserver 10.200.3.249
|
|
||||||
nameserver 10.200.3.250:5353
|
|
||||||
nameserver 2001:4860:4860::8844
|
|
||||||
nameserver [10.0.0.1]:5353
|
|
|
@ -1,29 +0,0 @@
|
||||||
package acme
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
// WaitFor polls the given function 'f', once every 'interval', up to 'timeout'.
|
|
||||||
func WaitFor(timeout, interval time.Duration, f func() (bool, error)) error {
|
|
||||||
var lastErr string
|
|
||||||
timeup := time.After(timeout)
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-timeup:
|
|
||||||
return fmt.Errorf("Time limit exceeded. Last error: %s", lastErr)
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
|
|
||||||
stop, err := f()
|
|
||||||
if stop {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
lastErr = err.Error()
|
|
||||||
}
|
|
||||||
|
|
||||||
time.Sleep(interval)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,26 +0,0 @@
|
||||||
package acme
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestWaitForTimeout(t *testing.T) {
|
|
||||||
c := make(chan error)
|
|
||||||
go func() {
|
|
||||||
err := WaitFor(3*time.Second, 1*time.Second, func() (bool, error) {
|
|
||||||
return false, nil
|
|
||||||
})
|
|
||||||
c <- err
|
|
||||||
}()
|
|
||||||
|
|
||||||
timeout := time.After(4 * time.Second)
|
|
||||||
select {
|
|
||||||
case <-timeout:
|
|
||||||
t.Fatal("timeout exceeded")
|
|
||||||
case err := <-c:
|
|
||||||
if err == nil {
|
|
||||||
t.Errorf("expected timeout error; got %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
2
cli.go
2
cli.go
|
@ -11,7 +11,7 @@ import (
|
||||||
"text/tabwriter"
|
"text/tabwriter"
|
||||||
|
|
||||||
"github.com/urfave/cli"
|
"github.com/urfave/cli"
|
||||||
"github.com/xenolf/lego/acmev2"
|
"github.com/xenolf/lego/acme"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Logger is used to log errors; if nil, the default log.Logger is used.
|
// Logger is used to log errors; if nil, the default log.Logger is used.
|
||||||
|
|
|
@ -15,7 +15,7 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/urfave/cli"
|
"github.com/urfave/cli"
|
||||||
"github.com/xenolf/lego/acmev2"
|
"github.com/xenolf/lego/acme"
|
||||||
"github.com/xenolf/lego/providers/dns"
|
"github.com/xenolf/lego/providers/dns"
|
||||||
"github.com/xenolf/lego/providers/http/memcached"
|
"github.com/xenolf/lego/providers/http/memcached"
|
||||||
"github.com/xenolf/lego/providers/http/webroot"
|
"github.com/xenolf/lego/providers/http/webroot"
|
||||||
|
@ -302,7 +302,6 @@ func run(c *cli.Context) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
var cert *acme.CertificateResource
|
var cert *acme.CertificateResource
|
||||||
var err error
|
|
||||||
|
|
||||||
if hasDomains {
|
if hasDomains {
|
||||||
// obtain a certificate, generating a new private key
|
// obtain a certificate, generating a new private key
|
||||||
|
|
|
@ -8,7 +8,7 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/urfave/cli"
|
"github.com/urfave/cli"
|
||||||
"github.com/xenolf/lego/acmev2"
|
"github.com/xenolf/lego/acme"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Configuration type from CLI and config files.
|
// Configuration type from CLI and config files.
|
||||||
|
|
|
@ -2,13 +2,10 @@ package gandi
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto"
|
"crypto"
|
||||||
"crypto/rand"
|
|
||||||
"crypto/rsa"
|
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"os"
|
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
@ -92,61 +89,6 @@ func TestDNSProvider(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestDNSProviderLive performs a live test to obtain a certificate
|
|
||||||
// using the Let's Encrypt staging server. It runs provided that both
|
|
||||||
// the environment variables GANDI_API_KEY and GANDI_TEST_DOMAIN are
|
|
||||||
// set. Otherwise the test is skipped.
|
|
||||||
//
|
|
||||||
// To complete this test, go test must be run with the -timeout=40m
|
|
||||||
// flag, since the default timeout of 10m is insufficient.
|
|
||||||
func TestDNSProviderLive(t *testing.T) {
|
|
||||||
apiKey := os.Getenv("GANDI_API_KEY")
|
|
||||||
domain := os.Getenv("GANDI_TEST_DOMAIN")
|
|
||||||
if apiKey == "" || domain == "" {
|
|
||||||
t.Skip("skipping live test")
|
|
||||||
}
|
|
||||||
// create a user.
|
|
||||||
const rsaKeySize = 2048
|
|
||||||
privateKey, err := rsa.GenerateKey(rand.Reader, rsaKeySize)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
myUser := user{
|
|
||||||
Email: "test@example.com",
|
|
||||||
key: privateKey,
|
|
||||||
}
|
|
||||||
// create a client using staging server
|
|
||||||
client, err := acme.NewClient(stagingServer, &myUser, acme.RSA2048)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
provider, err := NewDNSProviderCredentials(apiKey)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
err = client.SetChallengeProvider(acme.DNS01, provider)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
client.ExcludeChallenges([]acme.Challenge{acme.HTTP01, acme.TLSSNI01})
|
|
||||||
// register and agree tos
|
|
||||||
reg, err := client.Register()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
myUser.Registration = reg
|
|
||||||
err = client.AgreeToTOS()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
// complete the challenge
|
|
||||||
bundle := false
|
|
||||||
_, failures := client.ObtainCertificate([]string{domain}, bundle, nil, false)
|
|
||||||
if len(failures) > 0 {
|
|
||||||
t.Fatal(failures)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// serverResponses is the XML-RPC Request->Response map used by the
|
// serverResponses is the XML-RPC Request->Response map used by the
|
||||||
// fake RPC server. It was generated by recording a real RPC session
|
// fake RPC server. It was generated by recording a real RPC session
|
||||||
// which resulted in the successful issue of a cert, and then
|
// which resulted in the successful issue of a cert, and then
|
||||||
|
|
|
@ -2,13 +2,10 @@ package gandiv5
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto"
|
"crypto"
|
||||||
"crypto/rand"
|
|
||||||
"crypto/rsa"
|
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"os"
|
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
@ -92,61 +89,6 @@ func TestDNSProvider(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestDNSProviderLive performs a live test to obtain a certificate
|
|
||||||
// using the Let's Encrypt staging server. It runs provided that both
|
|
||||||
// the environment variables GANDIV5_API_KEY and GANDI_TEST_DOMAIN are
|
|
||||||
// set. Otherwise the test is skipped.
|
|
||||||
//
|
|
||||||
// To complete this test, go test must be run with the -timeout=40m
|
|
||||||
// flag, since the default timeout of 10m is insufficient.
|
|
||||||
func TestDNSProviderLive(t *testing.T) {
|
|
||||||
apiKey := os.Getenv("GANDIV5_API_KEY")
|
|
||||||
domain := os.Getenv("GANDI_TEST_DOMAIN")
|
|
||||||
if apiKey == "" || domain == "" {
|
|
||||||
t.Skip("skipping live test")
|
|
||||||
}
|
|
||||||
// create a user.
|
|
||||||
const rsaKeySize = 2048
|
|
||||||
privateKey, err := rsa.GenerateKey(rand.Reader, rsaKeySize)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
myUser := user{
|
|
||||||
Email: "test@example.com",
|
|
||||||
key: privateKey,
|
|
||||||
}
|
|
||||||
// create a client using staging server
|
|
||||||
client, err := acme.NewClient(stagingServer, &myUser, acme.RSA2048)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
provider, err := NewDNSProviderCredentials(apiKey)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
err = client.SetChallengeProvider(acme.DNS01, provider)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
client.ExcludeChallenges([]acme.Challenge{acme.HTTP01, acme.TLSSNI01})
|
|
||||||
// register and agree tos
|
|
||||||
reg, err := client.Register()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
myUser.Registration = reg
|
|
||||||
err = client.AgreeToTOS()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
// complete the challenge
|
|
||||||
bundle := false
|
|
||||||
_, failures := client.ObtainCertificate([]string{domain}, bundle, nil, false)
|
|
||||||
if len(failures) > 0 {
|
|
||||||
t.Fatal(failures)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// serverResponses is the JSON Request->Response map used by the
|
// serverResponses is the JSON Request->Response map used by the
|
||||||
// fake JSON server.
|
// fake JSON server.
|
||||||
var serverResponses = map[string]string{
|
var serverResponses = map[string]string{
|
||||||
|
|
Loading…
Reference in a new issue