2015-06-08 00:36:07 +00:00
|
|
|
package acme
|
|
|
|
|
|
|
|
import (
|
|
|
|
"crypto/rsa"
|
2015-10-18 22:42:04 +00:00
|
|
|
"crypto/x509"
|
2015-06-13 01:55:53 +00:00
|
|
|
"encoding/base64"
|
2015-06-08 00:36:07 +00:00
|
|
|
"encoding/json"
|
|
|
|
"errors"
|
2015-06-08 21:54:15 +00:00
|
|
|
"fmt"
|
2015-06-08 00:36:07 +00:00
|
|
|
"io/ioutil"
|
|
|
|
"log"
|
|
|
|
"net/http"
|
|
|
|
"regexp"
|
2015-10-18 00:16:15 +00:00
|
|
|
"strconv"
|
2015-06-08 00:36:07 +00:00
|
|
|
"strings"
|
2015-10-18 00:16:15 +00:00
|
|
|
"time"
|
2015-06-08 00:36:07 +00:00
|
|
|
)
|
|
|
|
|
2015-12-05 21:01:08 +00:00
|
|
|
var (
|
|
|
|
// Logger is an optional custom logger.
|
|
|
|
Logger *log.Logger
|
|
|
|
)
|
2015-06-08 00:36:07 +00:00
|
|
|
|
2015-11-06 06:43:42 +00:00
|
|
|
// 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...)
|
2015-06-08 00:36:07 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// 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() *rsa.PrivateKey
|
|
|
|
}
|
|
|
|
|
2015-06-13 02:50:36 +00:00
|
|
|
// Interface for all challenge solvers to implement.
|
2015-06-11 13:31:09 +00:00
|
|
|
type solver interface {
|
2015-06-13 01:55:53 +00:00
|
|
|
Solve(challenge challenge, domain string) error
|
2015-06-10 13:11:01 +00:00
|
|
|
}
|
|
|
|
|
2015-12-27 18:08:17 +00:00
|
|
|
type validateFunc func(j *jws, domain, uri string, chlng challenge) error
|
|
|
|
|
2015-06-08 00:36:07 +00:00
|
|
|
// Client is the user-friendy way to ACME
|
|
|
|
type Client struct {
|
2015-10-24 01:55:18 +00:00
|
|
|
directory directory
|
|
|
|
user User
|
|
|
|
jws *jws
|
|
|
|
keyBits int
|
|
|
|
issuerCert []byte
|
|
|
|
solvers map[string]solver
|
2015-06-08 00:36:07 +00:00
|
|
|
}
|
|
|
|
|
2015-11-20 19:01:06 +00:00
|
|
|
// NewClient creates a new ACME client on behalf of user. The client will depend on
|
|
|
|
// the ACME directory located at caDirURL for the rest of its actions. It will
|
|
|
|
// generate private keys for certificates of size keyBits. And, if the challenge
|
|
|
|
// type requires it, the client will open a port at optPort to solve the challenge.
|
2015-12-05 21:01:08 +00:00
|
|
|
//
|
|
|
|
// If optSolvers is nil, the value of DefaultSolvers is used. If given explicitly,
|
|
|
|
// it is a set of solver names to enable. The "http-01" and "tls-sni-01" solvers
|
|
|
|
// take an optional TCP port to listen on after a colon, e.g. "http-01:80". If
|
|
|
|
// the port is not specified, the port required by the spec will be used.
|
2015-12-27 17:28:20 +00:00
|
|
|
func NewClient(caDirURL string, user User, keyBits int) (*Client, error) {
|
2015-11-20 19:01:06 +00:00
|
|
|
privKey := user.GetPrivateKey()
|
2015-10-27 23:00:42 +00:00
|
|
|
if privKey == nil {
|
|
|
|
return nil, errors.New("private key was nil")
|
2015-06-08 00:36:07 +00:00
|
|
|
}
|
2015-10-27 23:00:42 +00:00
|
|
|
|
|
|
|
if err := privKey.Validate(); err != nil {
|
|
|
|
return nil, fmt.Errorf("invalid private key: %v", err)
|
|
|
|
}
|
|
|
|
|
2015-09-26 20:59:16 +00:00
|
|
|
var dir directory
|
2015-12-05 21:32:53 +00:00
|
|
|
if _, err := getJSON(caDirURL, &dir); err != nil {
|
2015-12-05 15:59:15 +00:00
|
|
|
return nil, fmt.Errorf("get directory at '%s': %v", caDirURL, err)
|
2015-10-27 23:00:42 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if dir.NewRegURL == "" {
|
|
|
|
return nil, errors.New("directory missing new registration URL")
|
|
|
|
}
|
|
|
|
if dir.NewAuthzURL == "" {
|
|
|
|
return nil, errors.New("directory missing new authz URL")
|
2015-09-26 20:59:16 +00:00
|
|
|
}
|
2015-10-27 23:00:42 +00:00
|
|
|
if dir.NewCertURL == "" {
|
|
|
|
return nil, errors.New("directory missing new certificate URL")
|
|
|
|
}
|
|
|
|
if dir.RevokeCertURL == "" {
|
|
|
|
return nil, errors.New("directory missing revoke certificate URL")
|
2015-09-26 20:59:16 +00:00
|
|
|
}
|
|
|
|
|
2015-11-20 19:01:06 +00:00
|
|
|
jws := &jws{privKey: privKey, directoryURL: caDirURL}
|
2015-11-12 01:55:28 +00:00
|
|
|
|
|
|
|
// 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[string]solver)
|
2015-12-27 18:08:17 +00:00
|
|
|
solvers["http-01"] = &httpChallenge{jws: jws, validate: validate}
|
|
|
|
solvers["tls-sni-01"] = &tlsSNIChallenge{jws: jws, validate: validate}
|
2015-11-12 01:55:28 +00:00
|
|
|
|
2015-11-20 19:01:06 +00:00
|
|
|
return &Client{directory: dir, user: user, jws: jws, keyBits: keyBits, solvers: solvers}, nil
|
2015-06-08 00:36:07 +00:00
|
|
|
}
|
|
|
|
|
2015-12-27 17:28:20 +00:00
|
|
|
// SetHTTPPort specifies a custom port to be used for HTTP based challenges.
|
2015-12-27 17:30:04 +00:00
|
|
|
func (c *Client) SetHTTPPort(port string) {
|
2015-12-27 17:56:36 +00:00
|
|
|
if chlng, ok := c.solvers["http-01"]; ok {
|
|
|
|
chlng.(*httpChallenge).optPort = port
|
|
|
|
}
|
2015-12-27 17:28:20 +00:00
|
|
|
}
|
|
|
|
|
2015-12-27 17:56:36 +00:00
|
|
|
// SetTLSPort specifies a custom port to be used for TLS based challenges.
|
|
|
|
func (c *Client) SetTLSPort(port string) {
|
|
|
|
if chlng, ok := c.solvers["tls-sni-01"]; ok {
|
|
|
|
chlng.(*tlsSNIChallenge).optPort = port
|
|
|
|
}
|
2015-12-27 17:28:20 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// ExcludeChallenges explicitly removes challenges from the pool for solving.
|
|
|
|
func (c *Client) ExcludeChallenges(challenges []string) {
|
|
|
|
// Loop through all challenges and delete the requested one if found.
|
|
|
|
for _, challenge := range challenges {
|
|
|
|
if _, ok := c.solvers[challenge]; ok {
|
|
|
|
delete(c.solvers, challenge)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2015-06-08 00:36:07 +00:00
|
|
|
// Register the current account to the ACME server.
|
|
|
|
func (c *Client) Register() (*RegistrationResource, error) {
|
2015-11-07 06:22:32 +00:00
|
|
|
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())
|
2015-10-23 14:24:02 +00:00
|
|
|
|
2015-10-30 23:11:33 +00:00
|
|
|
regMsg := registrationMessage{
|
2015-10-23 14:24:02 +00:00
|
|
|
Resource: "new-reg",
|
2015-10-30 23:11:33 +00:00
|
|
|
}
|
|
|
|
if c.user.GetEmail() != "" {
|
|
|
|
regMsg.Contact = []string{"mailto:" + c.user.GetEmail()}
|
|
|
|
} else {
|
|
|
|
regMsg.Contact = []string{}
|
|
|
|
}
|
|
|
|
|
2015-06-08 00:36:07 +00:00
|
|
|
var serverReg Registration
|
2015-12-05 15:59:15 +00:00
|
|
|
hdr, err := postJSON(c.jws, c.directory.NewRegURL, regMsg, &serverReg)
|
2015-06-08 00:36:07 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
reg := &RegistrationResource{Body: serverReg}
|
|
|
|
|
2015-12-05 15:59:15 +00:00
|
|
|
links := parseLinks(hdr["Link"])
|
|
|
|
reg.URI = hdr.Get("Location")
|
2015-06-08 00:36:07 +00:00
|
|
|
if links["terms-of-service"] != "" {
|
|
|
|
reg.TosURL = links["terms-of-service"]
|
|
|
|
}
|
|
|
|
|
|
|
|
if links["next"] != "" {
|
|
|
|
reg.NewAuthzURL = links["next"]
|
|
|
|
} else {
|
2015-11-07 06:22:32 +00:00
|
|
|
return nil, errors.New("acme: The server did not return 'next' link to proceed")
|
2015-06-08 00:36:07 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return reg, nil
|
|
|
|
}
|
|
|
|
|
2015-10-23 08:15:57 +00:00
|
|
|
// AgreeToTOS updates the Client registration and sends the agreement to
|
2015-06-08 21:54:15 +00:00
|
|
|
// the server.
|
2015-10-23 08:15:57 +00:00
|
|
|
func (c *Client) AgreeToTOS() error {
|
2015-06-08 21:54:15 +00:00
|
|
|
c.user.GetRegistration().Body.Agreement = c.user.GetRegistration().TosURL
|
2015-09-26 17:45:52 +00:00
|
|
|
c.user.GetRegistration().Body.Resource = "reg"
|
2015-12-05 15:59:15 +00:00
|
|
|
_, err := postJSON(c.jws, c.user.GetRegistration().URI, c.user.GetRegistration().Body, nil)
|
|
|
|
return err
|
2015-06-08 21:54:15 +00:00
|
|
|
}
|
|
|
|
|
2015-12-05 22:07:12 +00:00
|
|
|
// ObtainCertificate tries to obtain a single certificate using all domains passed into it.
|
2015-11-18 18:53:42 +00:00
|
|
|
// 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.
|
|
|
|
// If bundle is true, the []byte contains both the issuer certificate and
|
|
|
|
// your issued certificate as a bundle.
|
2015-12-05 22:07:12 +00:00
|
|
|
func (c *Client) ObtainCertificate(domains []string, bundle bool) (CertificateResource, map[string]error) {
|
2015-11-11 00:01:15 +00:00
|
|
|
if bundle {
|
2015-12-15 20:13:40 +00:00
|
|
|
logf("[INFO][%s] acme: Obtaining bundled SAN certificate", strings.Join(domains, ", "))
|
2015-11-11 00:01:15 +00:00
|
|
|
} else {
|
2015-12-15 20:13:40 +00:00
|
|
|
logf("[INFO][%s] acme: Obtaining SAN certificate", strings.Join(domains, ", "))
|
2015-11-11 00:01:15 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
challenges, failures := c.getChallenges(domains)
|
2015-11-17 21:22:25 +00:00
|
|
|
// If any challenge fails - return. Do not generate partial SAN certificates.
|
|
|
|
if len(failures) > 0 {
|
2015-11-11 00:01:15 +00:00
|
|
|
return CertificateResource{}, failures
|
|
|
|
}
|
|
|
|
|
|
|
|
errs := c.solveChallenges(challenges)
|
2015-11-17 18:45:15 +00:00
|
|
|
// If any challenge fails - return. Do not generate partial SAN certificates.
|
|
|
|
if len(errs) > 0 {
|
2015-11-17 22:07:13 +00:00
|
|
|
return CertificateResource{}, errs
|
2015-11-11 00:01:15 +00:00
|
|
|
}
|
|
|
|
|
2015-12-15 20:13:40 +00:00
|
|
|
logf("[INFO][%s] acme: Validations succeeded; requesting certificates", strings.Join(domains, ", "))
|
2015-11-11 00:01:15 +00:00
|
|
|
|
2015-11-17 21:22:25 +00:00
|
|
|
cert, err := c.requestCertificate(challenges, bundle)
|
2015-11-11 00:01:15 +00:00
|
|
|
if err != nil {
|
2015-11-17 21:22:25 +00:00
|
|
|
for _, chln := range challenges {
|
2015-11-11 00:01:15 +00:00
|
|
|
failures[chln.Domain] = err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return cert, failures
|
|
|
|
}
|
|
|
|
|
2015-10-24 01:55:18 +00:00
|
|
|
// RevokeCertificate takes a PEM encoded certificate or bundle and tries to revoke it at the CA.
|
2015-09-27 12:51:44 +00:00
|
|
|
func (c *Client) RevokeCertificate(certificate []byte) error {
|
2015-10-24 01:55:18 +00:00
|
|
|
certificates, err := parsePEMBundle(certificate)
|
2015-10-19 01:18:06 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2015-10-24 02:31:12 +00:00
|
|
|
x509Cert := certificates[0]
|
2015-10-24 01:55:18 +00:00
|
|
|
if x509Cert.IsCA {
|
2015-10-24 02:31:12 +00:00
|
|
|
return fmt.Errorf("Certificate bundle starts with a CA certificate")
|
2015-10-24 01:55:18 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
encodedCert := base64.URLEncoding.EncodeToString(x509Cert.Raw)
|
2015-09-27 12:51:44 +00:00
|
|
|
|
2015-12-05 15:59:15 +00:00
|
|
|
_, err = postJSON(c.jws, c.directory.RevokeCertURL, revokeCertMessage{Resource: "revoke-cert", Certificate: encodedCert}, nil)
|
|
|
|
return err
|
2015-09-27 12:51:44 +00:00
|
|
|
}
|
|
|
|
|
2015-10-18 22:42:04 +00:00
|
|
|
// RenewCertificate takes a CertificateResource and tries to renew the certificate.
|
2015-10-25 22:37:26 +00:00
|
|
|
// If the renewal process succeeds, the new certificate will ge returned in a new CertResource.
|
2015-10-18 22:42:04 +00:00
|
|
|
// 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
|
2015-10-19 01:18:06 +00:00
|
|
|
// this function will start a new-cert flow where a new certificate gets generated.
|
2015-10-24 01:55:18 +00:00
|
|
|
// If bundle is true, the []byte contains both the issuer certificate and
|
|
|
|
// your issued certificate as a bundle.
|
2015-12-05 22:07:12 +00:00
|
|
|
func (c *Client) RenewCertificate(cert CertificateResource, bundle bool) (CertificateResource, error) {
|
2015-10-18 22:42:04 +00:00
|
|
|
// Input certificate is PEM encoded. Decode it here as we may need the decoded
|
2015-10-24 01:55:18 +00:00
|
|
|
// cert later on in the renewal process. The input may be a bundle or a single certificate.
|
|
|
|
certificates, err := parsePEMBundle(cert.Certificate)
|
2015-10-18 22:42:04 +00:00
|
|
|
if err != nil {
|
2015-10-19 01:18:06 +00:00
|
|
|
return CertificateResource{}, err
|
2015-10-18 22:42:04 +00:00
|
|
|
}
|
|
|
|
|
2015-10-24 02:31:12 +00:00
|
|
|
x509Cert := certificates[0]
|
2015-10-24 01:55:18 +00:00
|
|
|
if x509Cert.IsCA {
|
2015-10-24 02:31:12 +00:00
|
|
|
return CertificateResource{}, fmt.Errorf("[%s] Certificate bundle starts with a CA certificate", cert.Domain)
|
2015-10-24 01:55:18 +00:00
|
|
|
}
|
|
|
|
|
2015-10-18 22:42:04 +00:00
|
|
|
// This is just meant to be informal for the user.
|
|
|
|
timeLeft := x509Cert.NotAfter.Sub(time.Now().UTC())
|
2015-12-15 20:13:40 +00:00
|
|
|
logf("[INFO][%s] acme: Trying renewal with %d hours remaining", cert.Domain, int(timeLeft.Hours()))
|
2015-10-18 22:42:04 +00:00
|
|
|
|
|
|
|
// The first step of renewal is to check if we get a renewed cert
|
|
|
|
// directly from the cert URL.
|
|
|
|
resp, err := http.Get(cert.CertURL)
|
2015-10-22 04:16:29 +00:00
|
|
|
if err != nil {
|
|
|
|
return CertificateResource{}, err
|
|
|
|
}
|
|
|
|
defer resp.Body.Close()
|
2015-10-18 22:42:04 +00:00
|
|
|
serverCertBytes, err := ioutil.ReadAll(resp.Body)
|
|
|
|
if err != nil {
|
2015-10-19 01:18:06 +00:00
|
|
|
return CertificateResource{}, err
|
2015-10-18 22:42:04 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
serverCert, err := x509.ParseCertificate(serverCertBytes)
|
|
|
|
if err != nil {
|
2015-10-19 01:18:06 +00:00
|
|
|
return CertificateResource{}, err
|
2015-10-18 22:42:04 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// If the server responds with a different certificate we are effectively renewed.
|
|
|
|
// TODO: Further test if we can actually use the new certificate (Our private key works)
|
|
|
|
if !x509Cert.Equal(serverCert) {
|
2015-12-15 20:13:40 +00:00
|
|
|
logf("[INFO][%s] acme: Server responded with renewed certificate", cert.Domain)
|
2015-10-24 01:55:18 +00:00
|
|
|
issuedCert := pemEncode(derCertificateBytes(serverCertBytes))
|
|
|
|
// If bundle is true, we want to return a certificate bundle.
|
|
|
|
// To do this, we need the issuer certificate.
|
|
|
|
if bundle {
|
|
|
|
// 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 we fail to aquire the issuer cert, return the issued certificate - do not fail.
|
2015-12-15 20:13:40 +00:00
|
|
|
logf("[ERROR][%s] acme: Could not bundle issuer certificate: %v", cert.Domain, err)
|
2015-10-24 01:55:18 +00:00
|
|
|
} else {
|
2015-10-24 02:31:12 +00:00
|
|
|
// Success - append the issuer cert to the issued cert.
|
2015-10-24 01:55:18 +00:00
|
|
|
issuerCert = pemEncode(derCertificateBytes(issuerCert))
|
2015-10-24 02:31:12 +00:00
|
|
|
issuedCert = append(issuedCert, issuerCert...)
|
|
|
|
cert.Certificate = issuedCert
|
2015-10-24 01:55:18 +00:00
|
|
|
}
|
|
|
|
}
|
2015-10-24 02:31:12 +00:00
|
|
|
|
|
|
|
cert.Certificate = issuedCert
|
2015-10-18 22:42:04 +00:00
|
|
|
return cert, nil
|
|
|
|
}
|
|
|
|
|
2015-12-05 22:07:12 +00:00
|
|
|
newCert, failures := c.ObtainCertificate([]string{cert.Domain}, bundle)
|
|
|
|
return newCert, failures[cert.Domain]
|
2015-10-18 22:42:04 +00:00
|
|
|
}
|
|
|
|
|
2015-06-10 23:11:14 +00:00
|
|
|
// Looks through the challenge combinations to find a solvable match.
|
|
|
|
// Then solves the challenges in series and returns.
|
2015-11-11 00:01:15 +00:00
|
|
|
func (c *Client) solveChallenges(challenges []authorizationResource) map[string]error {
|
2015-06-10 23:11:14 +00:00
|
|
|
// loop through the resources, basically through the domains.
|
2015-11-02 00:01:00 +00:00
|
|
|
failures := make(map[string]error)
|
2015-06-11 22:15:13 +00:00
|
|
|
for _, authz := range challenges {
|
|
|
|
// no solvers - no solving
|
2015-06-13 16:37:30 +00:00
|
|
|
if solvers := c.chooseSolvers(authz.Body, authz.Domain); solvers != nil {
|
2015-06-11 22:15:13 +00:00
|
|
|
for i, solver := range solvers {
|
2015-06-13 01:55:53 +00:00
|
|
|
// TODO: do not immediately fail if one domain fails to validate.
|
|
|
|
err := solver.Solve(authz.Body.Challenges[i], authz.Domain)
|
|
|
|
if err != nil {
|
2015-11-02 00:01:00 +00:00
|
|
|
failures[authz.Domain] = err
|
2015-06-13 01:55:53 +00:00
|
|
|
}
|
2015-06-11 22:15:13 +00:00
|
|
|
}
|
|
|
|
} else {
|
2015-12-15 20:13:40 +00:00
|
|
|
failures[authz.Domain] = fmt.Errorf("[%s] acme: Could not determine solvers", authz.Domain)
|
2015-06-11 22:15:13 +00:00
|
|
|
}
|
2015-06-10 13:11:01 +00:00
|
|
|
}
|
2015-06-11 22:15:13 +00:00
|
|
|
|
2015-11-02 00:01:00 +00:00
|
|
|
return failures
|
2015-06-11 22:15:13 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Checks all combinations from the server and returns an array of
|
|
|
|
// solvers which should get executed in series.
|
2015-06-13 16:37:30 +00:00
|
|
|
func (c *Client) chooseSolvers(auth authorization, domain string) map[int]solver {
|
2015-06-11 22:15:13 +00:00
|
|
|
for _, combination := range auth.Combinations {
|
|
|
|
solvers := make(map[int]solver)
|
2015-06-13 01:55:53 +00:00
|
|
|
for _, idx := range combination {
|
2015-10-25 23:39:24 +00:00
|
|
|
if solver, ok := c.solvers[auth.Challenges[idx].Type]; ok {
|
2015-06-13 01:55:53 +00:00
|
|
|
solvers[idx] = solver
|
|
|
|
} else {
|
2015-12-15 20:13:40 +00:00
|
|
|
logf("[INFO][%s] acme: Could not find solver for: %s", domain, auth.Challenges[idx].Type)
|
2015-06-11 22:15:13 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// If we can solve the whole combination, return the solvers
|
|
|
|
if len(solvers) == len(combination) {
|
|
|
|
return solvers
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return nil
|
2015-06-10 13:11:01 +00:00
|
|
|
}
|
|
|
|
|
2015-06-10 23:11:14 +00:00
|
|
|
// Get the challenges needed to proof our identifier to the ACME server.
|
2015-11-11 00:01:15 +00:00
|
|
|
func (c *Client) getChallenges(domains []string) ([]authorizationResource, map[string]error) {
|
|
|
|
resc, errc := make(chan authorizationResource), make(chan domainError)
|
2015-06-10 13:11:01 +00:00
|
|
|
|
2015-06-08 21:54:15 +00:00
|
|
|
for _, domain := range domains {
|
|
|
|
go func(domain string) {
|
2015-12-05 15:59:15 +00:00
|
|
|
authMsg := authorization{Resource: "new-authz", Identifier: identifier{Type: "dns", Value: domain}}
|
|
|
|
var authz authorization
|
|
|
|
hdr, err := postJSON(c.jws, c.user.GetRegistration().NewAuthzURL, authMsg, &authz)
|
2015-06-08 21:54:15 +00:00
|
|
|
if err != nil {
|
2015-11-02 00:01:00 +00:00
|
|
|
errc <- domainError{Domain: domain, Error: err}
|
2015-06-08 21:54:15 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2015-12-05 15:59:15 +00:00
|
|
|
links := parseLinks(hdr["Link"])
|
2015-06-08 21:54:15 +00:00
|
|
|
if links["next"] == "" {
|
2015-12-15 20:13:40 +00:00
|
|
|
logf("[ERROR][%s] acme: Server did not provide next link to proceed", domain)
|
2015-10-27 23:00:42 +00:00
|
|
|
return
|
2015-06-08 21:54:15 +00:00
|
|
|
}
|
|
|
|
|
2015-12-05 15:59:15 +00:00
|
|
|
resc <- authorizationResource{Body: authz, NewCertURL: links["next"], AuthURL: hdr.Get("Location"), Domain: domain}
|
2015-06-08 21:54:15 +00:00
|
|
|
}(domain)
|
|
|
|
}
|
|
|
|
|
2015-11-18 18:44:47 +00:00
|
|
|
responses := make(map[string]authorizationResource)
|
2015-11-02 00:01:00 +00:00
|
|
|
failures := make(map[string]error)
|
2015-06-08 21:54:15 +00:00
|
|
|
for i := 0; i < len(domains); i++ {
|
|
|
|
select {
|
|
|
|
case res := <-resc:
|
2015-11-18 18:44:47 +00:00
|
|
|
responses[res.Domain] = res
|
2015-06-08 21:54:15 +00:00
|
|
|
case err := <-errc:
|
2015-11-02 00:01:00 +00:00
|
|
|
failures[err.Domain] = err.Error
|
2015-06-08 21:54:15 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2015-11-18 18:44:47 +00:00
|
|
|
challenges := make([]authorizationResource, 0, len(responses))
|
|
|
|
for _, domain := range domains {
|
|
|
|
if challenge, ok := responses[domain]; ok {
|
|
|
|
challenges = append(challenges, challenge)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2015-06-08 21:54:15 +00:00
|
|
|
close(resc)
|
|
|
|
close(errc)
|
|
|
|
|
2015-11-18 18:44:47 +00:00
|
|
|
return challenges, failures
|
2015-06-08 21:54:15 +00:00
|
|
|
}
|
|
|
|
|
2015-11-11 00:01:15 +00:00
|
|
|
func (c *Client) requestCertificate(authz []authorizationResource, bundle bool) (CertificateResource, error) {
|
|
|
|
if len(authz) == 0 {
|
|
|
|
return CertificateResource{}, errors.New("Passed no authorizations to requestCertificate!")
|
|
|
|
}
|
|
|
|
|
|
|
|
commonName := authz[0]
|
2015-10-23 14:24:02 +00:00
|
|
|
privKey, err := generatePrivateKey(rsakey, c.keyBits)
|
2015-10-18 00:16:15 +00:00
|
|
|
if err != nil {
|
2015-11-11 00:01:15 +00:00
|
|
|
return CertificateResource{}, err
|
|
|
|
}
|
|
|
|
|
|
|
|
var san []string
|
|
|
|
var authURLs []string
|
|
|
|
for _, auth := range authz[1:] {
|
|
|
|
san = append(san, auth.Domain)
|
|
|
|
authURLs = append(authURLs, auth.AuthURL)
|
2015-10-18 00:16:15 +00:00
|
|
|
}
|
|
|
|
|
2015-10-24 01:55:18 +00:00
|
|
|
// TODO: should the CSR be customizable?
|
2015-11-11 00:01:15 +00:00
|
|
|
csr, err := generateCsr(privKey.(*rsa.PrivateKey), commonName.Domain, san)
|
2015-10-18 00:16:15 +00:00
|
|
|
if err != nil {
|
2015-11-11 00:01:15 +00:00
|
|
|
return CertificateResource{}, err
|
2015-10-18 00:16:15 +00:00
|
|
|
}
|
2015-06-13 01:55:53 +00:00
|
|
|
|
2015-10-18 00:16:15 +00:00
|
|
|
csrString := base64.URLEncoding.EncodeToString(csr)
|
2015-11-11 00:01:15 +00:00
|
|
|
jsonBytes, err := json.Marshal(csrMessage{Resource: "new-cert", Csr: csrString, Authorizations: authURLs})
|
2015-10-18 00:16:15 +00:00
|
|
|
if err != nil {
|
2015-11-11 00:01:15 +00:00
|
|
|
return CertificateResource{}, err
|
2015-10-18 00:16:15 +00:00
|
|
|
}
|
|
|
|
|
2015-11-11 00:01:15 +00:00
|
|
|
resp, err := c.jws.post(commonName.NewCertURL, jsonBytes)
|
2015-10-18 00:16:15 +00:00
|
|
|
if err != nil {
|
2015-11-11 00:01:15 +00:00
|
|
|
return CertificateResource{}, err
|
2015-10-18 00:16:15 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
privateKeyPem := pemEncode(privKey)
|
|
|
|
cerRes := CertificateResource{
|
2015-11-11 00:01:15 +00:00
|
|
|
Domain: commonName.Domain,
|
2015-10-18 00:16:15 +00:00
|
|
|
CertURL: resp.Header.Get("Location"),
|
|
|
|
PrivateKey: privateKeyPem}
|
|
|
|
|
|
|
|
for {
|
|
|
|
|
|
|
|
switch resp.StatusCode {
|
|
|
|
case 202:
|
|
|
|
case 201:
|
|
|
|
|
2015-12-24 08:57:09 +00:00
|
|
|
cert, err := ioutil.ReadAll(limitReader(resp.Body, 1024*1024))
|
2015-10-22 04:16:29 +00:00
|
|
|
resp.Body.Close()
|
2015-10-18 00:16:15 +00:00
|
|
|
if err != nil {
|
2015-11-11 00:01:15 +00:00
|
|
|
return CertificateResource{}, err
|
2015-10-18 00:16:15 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// The server returns a body with a length of zero if the
|
|
|
|
// certificate was not ready at the time this request completed.
|
|
|
|
// Otherwise the body is the certificate.
|
|
|
|
if len(cert) > 0 {
|
2015-10-24 01:55:18 +00:00
|
|
|
|
2015-10-18 00:16:15 +00:00
|
|
|
cerRes.CertStableURL = resp.Header.Get("Content-Location")
|
2015-10-24 01:55:18 +00:00
|
|
|
|
|
|
|
issuedCert := pemEncode(derCertificateBytes(cert))
|
|
|
|
// If bundle is true, we want to return a certificate bundle.
|
|
|
|
// To do this, we need the issuer certificate.
|
|
|
|
if bundle {
|
|
|
|
// 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 we fail to aquire the issuer cert, return the issued certificate - do not fail.
|
2015-12-15 20:13:40 +00:00
|
|
|
logf("[WARNING][%s] acme: Could not bundle issuer certificate: %v", commonName.Domain, err)
|
2015-10-24 01:55:18 +00:00
|
|
|
} else {
|
2015-10-24 02:31:12 +00:00
|
|
|
// Success - append the issuer cert to the issued cert.
|
2015-10-24 01:55:18 +00:00
|
|
|
issuerCert = pemEncode(derCertificateBytes(issuerCert))
|
2015-10-24 02:31:12 +00:00
|
|
|
issuedCert = append(issuedCert, issuerCert...)
|
2015-10-24 01:55:18 +00:00
|
|
|
}
|
|
|
|
}
|
2015-10-24 02:31:12 +00:00
|
|
|
|
|
|
|
cerRes.Certificate = issuedCert
|
2015-12-15 20:13:40 +00:00
|
|
|
logf("[INFO][%s] Server responded with a certificate.", commonName.Domain)
|
2015-11-11 00:01:15 +00:00
|
|
|
return cerRes, nil
|
2015-10-18 15:27:59 +00:00
|
|
|
}
|
2015-10-18 00:16:15 +00:00
|
|
|
|
2015-10-18 15:27:59 +00:00
|
|
|
// The certificate was granted but is not yet issued.
|
|
|
|
// Check retry-after and loop.
|
|
|
|
ra := resp.Header.Get("Retry-After")
|
|
|
|
retryAfter, err := strconv.Atoi(ra)
|
|
|
|
if err != nil {
|
2015-11-11 00:01:15 +00:00
|
|
|
return CertificateResource{}, err
|
2015-10-18 00:16:15 +00:00
|
|
|
}
|
2015-10-18 15:27:59 +00:00
|
|
|
|
2015-12-15 20:13:40 +00:00
|
|
|
logf("[INFO][%s] acme: Server responded with status 202; retrying after %ds", commonName.Domain, retryAfter)
|
2015-10-18 15:27:59 +00:00
|
|
|
time.Sleep(time.Duration(retryAfter) * time.Second)
|
|
|
|
|
2015-10-18 00:16:15 +00:00
|
|
|
break
|
|
|
|
default:
|
2015-11-11 00:01:15 +00:00
|
|
|
return CertificateResource{}, handleHTTPError(resp)
|
2015-06-13 01:55:53 +00:00
|
|
|
}
|
|
|
|
|
2015-10-18 00:16:15 +00:00
|
|
|
resp, err = http.Get(cerRes.CertURL)
|
2015-06-13 01:55:53 +00:00
|
|
|
if err != nil {
|
2015-11-11 00:01:15 +00:00
|
|
|
return CertificateResource{}, err
|
2015-06-13 01:55:53 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2015-10-24 01:55:18 +00:00
|
|
|
// getIssuerCertificate requests the issuer certificate and caches it for
|
|
|
|
// subsequent requests.
|
|
|
|
func (c *Client) getIssuerCertificate(url string) ([]byte, error) {
|
2015-11-07 06:22:32 +00:00
|
|
|
logf("[INFO] acme: Requesting issuer cert from %s", url)
|
2015-10-24 01:55:18 +00:00
|
|
|
if c.issuerCert != nil {
|
|
|
|
return c.issuerCert, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
resp, err := http.Get(url)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2015-12-21 00:42:33 +00:00
|
|
|
defer resp.Body.Close()
|
2015-10-24 01:55:18 +00:00
|
|
|
|
2015-12-24 08:57:09 +00:00
|
|
|
issuerBytes, err := ioutil.ReadAll(limitReader(resp.Body, 1024*1024))
|
2015-10-24 01:55:18 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
_, err = x509.ParseCertificate(issuerBytes)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
c.issuerCert = issuerBytes
|
|
|
|
return issuerBytes, err
|
|
|
|
}
|
|
|
|
|
2015-06-08 00:36:07 +00:00
|
|
|
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
|
|
|
|
}
|
2015-12-05 11:51:30 +00:00
|
|
|
|
|
|
|
// validate makes the ACME server start validating a
|
|
|
|
// challenge response, only returning once it is done.
|
2015-12-27 18:08:17 +00:00
|
|
|
func validate(j *jws, domain, uri string, chlng challenge) error {
|
2015-12-05 11:51:30 +00:00
|
|
|
var challengeResponse challenge
|
|
|
|
|
2015-12-05 21:32:53 +00:00
|
|
|
hdr, err := postJSON(j, uri, chlng, &challengeResponse)
|
2015-12-05 15:59:15 +00:00
|
|
|
if err != nil {
|
2015-12-05 11:51:30 +00:00
|
|
|
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 challengeResponse.Status {
|
|
|
|
case "valid":
|
2015-12-27 18:08:17 +00:00
|
|
|
logf("[INFO][%s] The server validated our request", domain)
|
2015-12-05 11:51:30 +00:00
|
|
|
return nil
|
|
|
|
case "pending":
|
|
|
|
break
|
|
|
|
case "invalid":
|
2015-12-27 18:08:17 +00:00
|
|
|
return handleChallengeError(challengeResponse)
|
2015-12-05 11:51:30 +00:00
|
|
|
default:
|
|
|
|
return errors.New("The server returned an unexpected state.")
|
|
|
|
}
|
|
|
|
|
2015-12-05 21:32:53 +00:00
|
|
|
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 = 1
|
2015-12-05 11:51:30 +00:00
|
|
|
}
|
2015-12-05 21:32:53 +00:00
|
|
|
time.Sleep(time.Duration(ra) * time.Second)
|
2015-12-05 11:51:30 +00:00
|
|
|
|
2015-12-05 21:32:53 +00:00
|
|
|
hdr, err = getJSON(uri, &challengeResponse)
|
|
|
|
if err != nil {
|
2015-12-05 11:51:30 +00:00
|
|
|
return err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// getJSON performs an HTTP GET request and parses the response body
|
|
|
|
// as JSON, into the provided respBody object.
|
2015-12-05 21:32:53 +00:00
|
|
|
func getJSON(uri string, respBody interface{}) (http.Header, error) {
|
2015-12-05 11:51:30 +00:00
|
|
|
resp, err := http.Get(uri)
|
|
|
|
if err != nil {
|
2015-12-05 21:32:53 +00:00
|
|
|
return nil, fmt.Errorf("failed to get %q: %v", uri, err)
|
2015-12-05 11:51:30 +00:00
|
|
|
}
|
|
|
|
defer resp.Body.Close()
|
|
|
|
|
|
|
|
if resp.StatusCode >= http.StatusBadRequest {
|
2015-12-05 21:32:53 +00:00
|
|
|
return resp.Header, handleHTTPError(resp)
|
2015-12-05 11:51:30 +00:00
|
|
|
}
|
|
|
|
|
2015-12-05 21:32:53 +00:00
|
|
|
return resp.Header, json.NewDecoder(resp.Body).Decode(respBody)
|
2015-12-05 11:51:30 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// postJSON performs an HTTP POST request and parses the response body
|
|
|
|
// as JSON, into the provided respBody object.
|
2015-12-05 15:59:15 +00:00
|
|
|
func postJSON(j *jws, uri string, reqBody, respBody interface{}) (http.Header, error) {
|
2015-12-05 11:51:30 +00:00
|
|
|
jsonBytes, err := json.Marshal(reqBody)
|
|
|
|
if err != nil {
|
2015-12-05 15:59:15 +00:00
|
|
|
return nil, errors.New("Failed to marshal network message...")
|
2015-12-05 11:51:30 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
resp, err := j.post(uri, jsonBytes)
|
|
|
|
if err != nil {
|
2015-12-05 15:59:15 +00:00
|
|
|
return nil, fmt.Errorf("Failed to post JWS message. -> %v", err)
|
2015-12-05 11:51:30 +00:00
|
|
|
}
|
|
|
|
defer resp.Body.Close()
|
|
|
|
|
|
|
|
if resp.StatusCode >= http.StatusBadRequest {
|
2015-12-05 15:59:15 +00:00
|
|
|
return resp.Header, handleHTTPError(resp)
|
2015-12-05 11:51:30 +00:00
|
|
|
}
|
|
|
|
|
2015-12-05 15:59:15 +00:00
|
|
|
if respBody == nil {
|
|
|
|
return resp.Header, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
return resp.Header, json.NewDecoder(resp.Body).Decode(respBody)
|
2015-12-05 11:51:30 +00:00
|
|
|
}
|