Merge branch 'master' of https://github.com/tommie/lego into refactor-client

# Conflicts:
#	acme/client.go
#	acme/http_challenge.go
#	acme/http_challenge_test.go
#	acme/tls_sni_challenge.go
#	cli.go
#	cli_handlers.go
This commit is contained in:
xenolf 2015-12-27 17:38:49 +01:00
commit 595f684e27
9 changed files with 330 additions and 686 deletions

View file

@ -16,8 +16,13 @@ import (
"time"
)
// Logger is an optional custom logger.
var Logger *log.Logger
var (
// DefaultSolvers is the set of solvers to use if none is given to NewClient.
DefaultSolvers = []string{"http-01", "tls-sni-01"}
// Logger is an optional custom logger.
Logger *log.Logger
)
// logf writes a log entry. It uses Logger if not
// nil, otherwise it uses the default log.Logger.
@ -56,9 +61,12 @@ type Client struct {
// 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.
// If optPort is blank, the port required by the spec will be used, but you must
// forward the required port to optPort for the challenge to succeed.
func NewClient(caDirURL string, user User, keyBits int, optPort string) (*Client, error) {
//
// 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.
func NewClient(caDirURL string, user User, keyBits int, optSolvers []string) (*Client, error) {
privKey := user.GetPrivateKey()
if privKey == nil {
return nil, errors.New("private key was nil")
@ -68,16 +76,9 @@ func NewClient(caDirURL string, user User, keyBits int, optPort string) (*Client
return nil, fmt.Errorf("invalid private key: %v", err)
}
dirResp, err := http.Get(caDirURL)
if err != nil {
return nil, fmt.Errorf("get directory at '%s': %v", caDirURL, err)
}
defer dirResp.Body.Close()
var dir directory
err = json.NewDecoder(dirResp.Body).Decode(&dir)
if err != nil {
return nil, fmt.Errorf("decode directory: %v", err)
if _, err := getJSON(caDirURL, &dir); err != nil {
return nil, fmt.Errorf("get directory at '%s': %v", caDirURL, err)
}
if dir.NewRegURL == "" {
@ -99,8 +100,30 @@ func NewClient(caDirURL string, user User, keyBits int, optPort string) (*Client
// 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)
solvers["http-01"] = &httpChallenge{jws: jws, optPort: optPort}
solvers["tls-sni-01"] = &tlsSNIChallenge{jws: jws, optPort: optPort}
if optSolvers == nil {
optSolvers = DefaultSolvers
}
for _, s := range optSolvers {
ss := strings.SplitN(s, ":", 2)
switch ss[0] {
case "http-01":
optPort := ""
if len(ss) > 1 {
optPort = ss[1]
}
solvers["http-01"] = &httpChallenge{jws: jws, validate: validate, optPort: optPort}
case "tls-sni-01":
optPort := ""
if len(ss) > 1 {
optPort = ss[1]
}
solvers["tls-sni-01"] = &tlsSNIChallenge{jws: jws, validate: validate, optPort: optPort}
default:
return nil, fmt.Errorf("unknown solver: %s", s)
}
}
return &Client{directory: dir, user: user, jws: jws, keyBits: keyBits, solvers: solvers}, nil
}
@ -121,32 +144,16 @@ func (c *Client) Register() (*RegistrationResource, error) {
regMsg.Contact = []string{}
}
jsonBytes, err := json.Marshal(regMsg)
if err != nil {
return nil, err
}
resp, err := c.jws.post(c.directory.NewRegURL, jsonBytes)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode >= http.StatusBadRequest {
return nil, handleHTTPError(resp)
}
var serverReg Registration
decoder := json.NewDecoder(resp.Body)
err = decoder.Decode(&serverReg)
hdr, err := postJSON(c.jws, c.directory.NewRegURL, regMsg, &serverReg)
if err != nil {
return nil, err
}
reg := &RegistrationResource{Body: serverReg}
links := parseLinks(resp.Header["Link"])
reg.URI = resp.Header.Get("Location")
links := parseLinks(hdr["Link"])
reg.URI = hdr.Get("Location")
if links["terms-of-service"] != "" {
reg.TosURL = links["terms-of-service"]
}
@ -165,76 +172,16 @@ func (c *Client) Register() (*RegistrationResource, error) {
func (c *Client) AgreeToTOS() error {
c.user.GetRegistration().Body.Agreement = c.user.GetRegistration().TosURL
c.user.GetRegistration().Body.Resource = "reg"
jsonBytes, err := json.Marshal(&c.user.GetRegistration().Body)
if err != nil {
return err
}
resp, err := c.jws.post(c.user.GetRegistration().URI, jsonBytes)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusAccepted {
return handleHTTPError(resp)
}
return nil
_, err := postJSON(c.jws, c.user.GetRegistration().URI, c.user.GetRegistration().Body, nil)
return err
}
// ObtainCertificates tries to obtain certificates from the CA server
// using the challenges it has configured. The returned certificates are
// PEM encoded byte slices.
// If bundle is true, the []byte contains both the issuer certificate and
// your issued certificate as a bundle.
func (c *Client) ObtainCertificates(domains []string, bundle bool) ([]CertificateResource, map[string]error) {
if bundle {
logf("[INFO][%s] acme: Obtaining bundled certificates", strings.Join(domains, ", "))
} else {
logf("[INFO][%s] acme: Obtaining certificates", strings.Join(domains, ", "))
}
challenges, failures := c.getChallenges(domains)
if len(challenges) == 0 {
return nil, failures
}
err := c.solveChallenges(challenges)
for k, v := range err {
failures[k] = v
}
if len(failures) == len(domains) {
return nil, failures
}
// remove failed challenges from slice
var succeededChallenges []authorizationResource
for _, chln := range challenges {
if failures[chln.Domain] == nil {
succeededChallenges = append(succeededChallenges, chln)
}
}
logf("[INFO][%s] acme: Validations succeeded; requesting certificates", strings.Join(domains, ", "))
certs, err := c.requestCertificates(succeededChallenges, bundle)
for k, v := range err {
failures[k] = v
}
return certs, failures
}
// ObtainSANCertificate 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.
// 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.
// This function will never return a partial certificate. If one domain in the list fails,
// the whole certificate will fail.
func (c *Client) ObtainSANCertificate(domains []string, bundle bool) (CertificateResource, map[string]error) {
func (c *Client) ObtainCertificate(domains []string, bundle bool) (CertificateResource, map[string]error) {
if bundle {
logf("[INFO][%s] acme: Obtaining bundled SAN certificate", strings.Join(domains, ", "))
} else {
@ -279,22 +226,8 @@ func (c *Client) RevokeCertificate(certificate []byte) error {
encodedCert := base64.URLEncoding.EncodeToString(x509Cert.Raw)
jsonBytes, err := json.Marshal(revokeCertMessage{Resource: "revoke-cert", Certificate: encodedCert})
if err != nil {
return err
}
resp, err := c.jws.post(c.directory.RevokeCertURL, jsonBytes)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return handleHTTPError(resp)
}
return nil
_, err = postJSON(c.jws, c.directory.RevokeCertURL, revokeCertMessage{Resource: "revoke-cert", Certificate: encodedCert}, nil)
return err
}
// RenewCertificate takes a CertificateResource and tries to renew the certificate.
@ -304,7 +237,7 @@ func (c *Client) RevokeCertificate(certificate []byte) error {
// 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.
func (c *Client) RenewCertificate(cert CertificateResource, revokeOld bool, bundle bool) (CertificateResource, error) {
func (c *Client) RenewCertificate(cert CertificateResource, bundle 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)
@ -342,9 +275,6 @@ func (c *Client) RenewCertificate(cert CertificateResource, revokeOld bool, bund
// TODO: Further test if we can actually use the new certificate (Our private key works)
if !x509Cert.Equal(serverCert) {
logf("[INFO][%s] acme: Server responded with renewed certificate", cert.Domain)
if revokeOld {
c.RevokeCertificate(cert.Certificate)
}
issuedCert := pemEncode(derCertificateBytes(serverCertBytes))
// If bundle is true, we want to return a certificate bundle.
// To do this, we need the issuer certificate.
@ -368,33 +298,8 @@ func (c *Client) RenewCertificate(cert CertificateResource, revokeOld bool, bund
return cert, nil
}
var domains []string
newCerts := make([]CertificateResource, 1)
var failures map[string]error
// 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)
}
newCerts[0], failures = c.ObtainSANCertificate(domains, bundle)
} else {
domains = append(domains, x509Cert.Subject.CommonName)
newCerts, failures = c.ObtainCertificates(domains, bundle)
}
if len(failures) > 0 {
return CertificateResource{}, failures[cert.Domain]
}
if revokeOld {
c.RevokeCertificate(cert.Certificate)
}
return newCerts[0], nil
newCert, failures := c.ObtainCertificate([]string{cert.Domain}, bundle)
return newCert, failures[cert.Domain]
}
// Looks through the challenge combinations to find a solvable match.
@ -447,37 +352,21 @@ func (c *Client) getChallenges(domains []string) ([]authorizationResource, map[s
for _, domain := range domains {
go func(domain string) {
jsonBytes, err := json.Marshal(authorization{Resource: "new-authz", Identifier: identifier{Type: "dns", Value: domain}})
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)
if err != nil {
errc <- domainError{Domain: domain, Error: err}
return
}
resp, err := c.jws.post(c.user.GetRegistration().NewAuthzURL, jsonBytes)
if err != nil {
errc <- domainError{Domain: domain, Error: err}
return
}
if resp.StatusCode != http.StatusCreated {
errc <- domainError{Domain: domain, Error: handleHTTPError(resp)}
}
links := parseLinks(resp.Header["Link"])
links := parseLinks(hdr["Link"])
if links["next"] == "" {
logf("[ERROR][%s] acme: Server did not provide next link to proceed", domain)
return
}
var authz authorization
decoder := json.NewDecoder(resp.Body)
err = decoder.Decode(&authz)
if err != nil {
errc <- domainError{Domain: domain, Error: err}
}
resp.Body.Close()
resc <- authorizationResource{Body: authz, NewCertURL: links["next"], AuthURL: resp.Header.Get("Location"), Domain: domain}
resc <- authorizationResource{Body: authz, NewCertURL: links["next"], AuthURL: hdr.Get("Location"), Domain: domain}
}(domain)
}
@ -505,39 +394,6 @@ func (c *Client) getChallenges(domains []string) ([]authorizationResource, map[s
return challenges, failures
}
// requestCertificates iterates all granted authorizations, creates RSA private keys and CSRs.
// It then uses these to request a certificate from the CA and returns the list of successfully
// granted certificates.
func (c *Client) requestCertificates(challenges []authorizationResource, bundle bool) ([]CertificateResource, map[string]error) {
resc, errc := make(chan CertificateResource), make(chan domainError)
for _, authz := range challenges {
go func(authz authorizationResource, resc chan CertificateResource, errc chan domainError) {
certRes, err := c.requestCertificate([]authorizationResource{authz}, bundle)
if err != nil {
errc <- domainError{Domain: authz.Domain, Error: err}
} else {
resc <- certRes
}
}(authz, resc, errc)
}
var certs []CertificateResource
failures := make(map[string]error)
for i := 0; i < len(challenges); i++ {
select {
case res := <-resc:
certs = append(certs, res)
case err := <-errc:
failures[err.Domain] = err.Error
}
}
close(resc)
close(errc)
return certs, failures
}
func (c *Client) requestCertificate(authz []authorizationResource, bundle bool) (CertificateResource, error) {
if len(authz) == 0 {
return CertificateResource{}, errors.New("Passed no authorizations to requestCertificate!")
@ -690,3 +546,86 @@ func parseLinks(links []string) map[string]string {
return linkMap
}
// validate makes the ACME server start validating a
// challenge response, only returning once it is done.
func validate(j *jws, uri string, chlng challenge) error {
var challengeResponse challenge
hdr, err := postJSON(j, uri, chlng, &challengeResponse)
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 challengeResponse.Status {
case "valid":
logf("The server validated our request")
return nil
case "pending":
break
case "invalid":
return errors.New("The server could not validate our request.")
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 = 1
}
time.Sleep(time.Duration(ra) * time.Second)
hdr, err = getJSON(uri, &challengeResponse)
if err != nil {
return err
}
}
return nil
}
// 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 := http.Get(uri)
if err != nil {
return nil, fmt.Errorf("failed to get %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 {
return resp.Header, handleHTTPError(resp)
}
if respBody == nil {
return resp.Header, nil
}
return resp.Header, json.NewDecoder(resp.Body).Decode(respBody)
}

View file

@ -6,6 +6,7 @@ import (
"encoding/json"
"net/http"
"net/http/httptest"
"strings"
"testing"
)
@ -26,8 +27,7 @@ func TestNewClient(t *testing.T) {
w.Write(data)
}))
caURL, optPort := ts.URL, "1234"
client, err := NewClient(caURL, user, keyBits, optPort)
client, err := NewClient(ts.URL, user, keyBits, nil)
if err != nil {
t.Fatalf("Could not create client: %v", err)
}
@ -46,6 +46,30 @@ func TestNewClient(t *testing.T) {
if expected, actual := 2, len(client.solvers); actual != expected {
t.Fatalf("Expected %d solver(s), got %d", expected, actual)
}
}
func TestNewClientOptPort(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{NewAuthzURL: "http://test", NewCertURL: "http://test", NewRegURL: "http://test", RevokeCertURL: "http://test"})
w.Write(data)
}))
optPort := "1234"
client, err := NewClient(ts.URL, user, keyBits, []string{"http-01:" + optPort})
if err != nil {
t.Fatalf("Could not create client: %v", err)
}
httpSolver, ok := client.solvers["http-01"].(*httpChallenge)
if !ok {
@ -59,6 +83,75 @@ func TestNewClient(t *testing.T) {
}
}
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, URI: "http://example.com/", Token: "token"})
case "GET":
st := statuses[0]
statuses = statuses[1:]
writeJSONResponse(w, &challenge{Type: "http-01", Status: st, URI: "http://example.com/", Token: "token"})
default:
http.Error(w, r.Method, http.StatusMethodNotAllowed)
}
}))
defer ts.Close()
privKey, _ := generatePrivateKey(rsakey, 512)
j := &jws{privKey: privKey.(*rsa.PrivateKey), directoryURL: ts.URL}
tsts := []struct {
name string
statuses []string
want string
}{
{"POST-unexpected", []string{"weird"}, "unexpected"},
{"POST-valid", []string{"valid"}, ""},
{"POST-invalid", []string{"invalid"}, "not validate"},
{"GET-unexpected", []string{"pending", "weird"}, "unexpected"},
{"GET-valid", []string{"pending", "valid"}, ""},
{"GET-invalid", []string{"pending", "invalid"}, "not validate"},
}
for _, tst := range tsts {
statuses = tst.statuses
if err := validate(j, 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)
}
}
}
// 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, uri string, chlng challenge) error {
return nil
}
type mockUser struct {
email string
regres *RegistrationResource

View file

@ -1,99 +1,28 @@
package acme
import (
"encoding/json"
"errors"
"fmt"
"net"
"net/http"
"strings"
"time"
)
type httpChallenge struct {
jws *jws
optPort string
start chan net.Listener
end chan error
jws *jws
validate func(j *jws, uri string, chlng challenge) error
optPort string
}
func (s *httpChallenge) Solve(chlng challenge, domain string) error {
logf("[INFO][%s] acme: Trying to solve HTTP-01", domain)
s.start = make(chan net.Listener)
s.end = make(chan error)
// Generate the Key Authorization for the challenge
keyAuth, err := getKeyAuthorization(chlng.Token, &s.jws.privKey.PublicKey)
if err != nil {
return err
}
go s.startHTTPServer(domain, chlng.Token, keyAuth)
var listener net.Listener
select {
case listener = <-s.start:
break
case err := <-s.end:
return fmt.Errorf("Could not start HTTP server for challenge -> %v", err)
}
// Make sure we properly close the HTTP server before we return
defer func() {
listener.Close()
err = <-s.end
close(s.start)
close(s.end)
}()
jsonBytes, err := json.Marshal(challenge{Resource: "challenge", Type: chlng.Type, Token: chlng.Token, KeyAuthorization: keyAuth})
if err != nil {
return errors.New("Failed to marshal network message...")
}
// Tell the server we handle HTTP-01
resp, err := s.jws.post(chlng.URI, jsonBytes)
if err != nil {
return fmt.Errorf("Failed to post JWS message. -> %v", err)
}
// After the path is sent, the ACME server will access our server.
// Repeatedly check the server for an updated status on our request.
var challengeResponse challenge
Loop:
for {
if resp.StatusCode >= http.StatusBadRequest {
return handleHTTPError(resp)
}
err = json.NewDecoder(resp.Body).Decode(&challengeResponse)
resp.Body.Close()
if err != nil {
return err
}
switch challengeResponse.Status {
case "valid":
logf("[INFO][%s] The server validated our request", domain)
break Loop
case "pending":
break
case "invalid":
return handleChallengeError(challengeResponse)
default:
return errors.New("The server returned an unexpected state.")
}
time.Sleep(1 * time.Second)
resp, err = http.Get(chlng.URI)
}
return nil
}
func (s *httpChallenge) startHTTPServer(domain string, token string, keyAuth string) {
// Allow for CLI port override
port := ":80"
if s.optPort != "" {
@ -105,17 +34,17 @@ func (s *httpChallenge) startHTTPServer(domain string, token string, keyAuth str
// if the domain:port bind failed, fall back to :port bind and try that instead.
listener, err = net.Listen("tcp", port)
if err != nil {
s.end <- err
return fmt.Errorf("Could not start HTTP server for challenge -> %v", err)
}
}
// Signal successfull start
s.start <- listener
defer listener.Close()
path := "/.well-known/acme-challenge/" + token
path := "/.well-known/acme-challenge/" + chlng.Token
// The handler validates the HOST header and request type.
// For validation it then writes the token the server returned with the challenge
http.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) {
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))
@ -126,8 +55,7 @@ func (s *httpChallenge) startHTTPServer(domain string, token string, keyAuth str
}
})
http.Serve(listener, nil)
go http.Serve(listener, mux)
// Signal that the server was shut down
s.end <- nil
return s.validate(s.jws, chlng.URI, challenge{Resource: "challenge", Type: chlng.Type, Token: chlng.Token, KeyAuthorization: keyAuth})
}

View file

@ -2,263 +2,56 @@ package acme
import (
"crypto/rsa"
"crypto/tls"
"encoding/json"
"io/ioutil"
"net/http"
"net/http/httptest"
"regexp"
"strings"
"testing"
"github.com/square/go-jose"
)
func TestHTTPNonRootBind(t *testing.T) {
privKey, _ := generatePrivateKey(rsakey, 128)
jws := &jws{privKey: privKey.(*rsa.PrivateKey)}
solver := &httpChallenge{jws: jws}
clientChallenge := challenge{Type: "http01", Status: "pending", URI: "localhost:4000", Token: "http1"}
// validate error on non-root bind to 80
if err := solver.Solve(clientChallenge, "127.0.0.1"); err == nil {
t.Error("BIND: Expected Solve to return an error but the error was nil.")
} else {
expectedError := "Could not start HTTP server for challenge -> listen tcp :80: bind: permission denied"
if err.Error() != expectedError {
t.Errorf("Expected error \"%s\" but instead got \"%s\"", expectedError, err.Error())
}
}
}
func TestHTTPShortRSA(t *testing.T) {
privKey, _ := generatePrivateKey(rsakey, 128)
jws := &jws{privKey: privKey.(*rsa.PrivateKey), nonces: []string{"test1", "test2"}}
solver := &httpChallenge{jws: jws, optPort: "23456"}
clientChallenge := challenge{Type: "http01", Status: "pending", URI: "http://localhost:4000", Token: "http2"}
if err := solver.Solve(clientChallenge, "127.0.0.1"); err == nil {
t.Error("UNEXPECTED: Expected Solve to return an error but the error was nil.")
} else {
expectedError := "Failed to post JWS message. -> crypto/rsa: message too long for RSA public key size"
if err.Error() != expectedError {
t.Errorf("Expected error %s but instead got %s", expectedError, err.Error())
}
}
}
func TestHTTPConnectionRefusal(t *testing.T) {
func TestHTTPChallenge(t *testing.T) {
privKey, _ := generatePrivateKey(rsakey, 512)
jws := &jws{privKey: privKey.(*rsa.PrivateKey), nonces: []string{"test1", "test2"}}
solver := &httpChallenge{jws: jws, optPort: "23456"}
clientChallenge := challenge{Type: "http01", Status: "pending", URI: "http://localhost:4000", Token: "http3"}
if err := solver.Solve(clientChallenge, "127.0.0.1"); err == nil {
t.Error("UNEXPECTED: Expected Solve to return an error but the error was nil.")
} else {
reg := "/Failed to post JWS message\\. -> Post http:\\/\\/localhost:4000: dial tcp 127\\.0\\.0\\.1:4000: (getsockopt: )?connection refused/g"
test2 := "Failed to post JWS message. -> Post http://localhost:4000: dial tcp 127.0.0.1:4000: connection refused"
r, _ := regexp.Compile(reg)
if r.MatchString(err.Error()) && r.MatchString(test2) {
t.Errorf("Expected \"%s\" to match %s", err.Error(), reg)
}
}
}
func TestHTTPUnexpectedServerState(t *testing.T) {
privKey, _ := generatePrivateKey(rsakey, 512)
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Add("Replay-Nonce", "12345")
w.Write([]byte("{\"type\":\"http01\",\"status\":\"what\",\"uri\":\"http://some.url\",\"token\":\"http4\"}"))
}))
jws := &jws{privKey: privKey.(*rsa.PrivateKey), directoryURL: ts.URL}
solver := &httpChallenge{jws: jws, optPort: "23456"}
clientChallenge := challenge{Type: "http01", Status: "pending", URI: ts.URL, Token: "http4"}
if err := solver.Solve(clientChallenge, "127.0.0.1"); err == nil {
t.Error("UNEXPECTED: Expected Solve to return an error but the error was nil.")
} else {
expectedError := "The server returned an unexpected state."
if err.Error() != expectedError {
t.Errorf("Expected error %s but instead got %s", expectedError, err.Error())
}
}
}
func TestHTTPChallengeServerUnexpectedDomain(t *testing.T) {
privKey, _ := generatePrivateKey(rsakey, 512)
jws := &jws{privKey: privKey.(*rsa.PrivateKey)}
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method == "POST" {
tr := &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
}
client := &http.Client{Transport: tr}
req, _ := client.Get("https://localhost:23456/.well-known/acme-challenge/" + "htto5")
reqBytes, _ := ioutil.ReadAll(req.Body)
if string(reqBytes) != "TEST" {
t.Error("Expected http01 server to return string TEST on unexpected domain.")
}
}
w.Header().Add("Replay-Nonce", "12345")
w.Write([]byte("{\"type\":\"http01\",\"status\":\"invalid\",\"uri\":\"http://some.url\",\"token\":\"http5\"}"))
}))
solver := &httpChallenge{jws: jws, optPort: "23456"}
clientChallenge := challenge{Type: "http01", Status: "pending", URI: ts.URL, Token: "http5"}
if err := solver.Solve(clientChallenge, "127.0.0.1"); err == nil {
t.Error("UNEXPECTED: Expected Solve to return an error but the error was nil.")
}
}
func TestHTTPServerError(t *testing.T) {
privKey, _ := generatePrivateKey(rsakey, 512)
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method == "HEAD" {
w.Header().Add("Replay-Nonce", "12345")
} else {
w.WriteHeader(http.StatusInternalServerError)
w.Header().Add("Replay-Nonce", "12345")
w.Write([]byte("{\"type\":\"urn:acme:error:unauthorized\",\"detail\":\"Error creating new authz :: Syntax error\"}"))
}
}))
jws := &jws{privKey: privKey.(*rsa.PrivateKey), directoryURL: ts.URL}
solver := &httpChallenge{jws: jws, optPort: "23456"}
clientChallenge := challenge{Type: "http01", Status: "pending", URI: ts.URL, Token: "http6"}
if err := solver.Solve(clientChallenge, "127.0.0.1"); err == nil {
t.Error("UNEXPECTED: Expected Solve to return an error but the error was nil.")
} else {
expectedError := "acme: Error 500 - urn:acme:error:unauthorized - Error creating new authz :: Syntax error"
if err.Error() != expectedError {
t.Errorf("Expected error |%s| but instead got |%s|", expectedError, err.Error())
}
}
}
func TestHTTPInvalidServerState(t *testing.T) {
privKey, _ := generatePrivateKey(rsakey, 512)
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Add("Replay-Nonce", "12345")
w.Write([]byte("{\"type\":\"http01\",\"status\":\"invalid\",\"uri\":\"http://some.url\",\"token\":\"http7\"}"))
}))
jws := &jws{privKey: privKey.(*rsa.PrivateKey), directoryURL: ts.URL}
solver := &httpChallenge{jws: jws, optPort: "23456"}
clientChallenge := challenge{Type: "http01", Status: "pending", URI: ts.URL, Token: "http7"}
if err := solver.Solve(clientChallenge, "127.0.0.1"); err == nil {
t.Error("UNEXPECTED: Expected Solve to return an error but the error was nil.")
} else {
expectedError := "acme: Error 0 - - \nError Detail:\n"
if err.Error() != expectedError {
t.Errorf("Expected error |%s| but instead got |%s|", expectedError, err.Error())
}
}
}
func TestHTTPValidServerResponse(t *testing.T) {
privKey, _ := generatePrivateKey(rsakey, 512)
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Add("Replay-Nonce", "12345")
w.Write([]byte("{\"type\":\"http01\",\"status\":\"valid\",\"uri\":\"http://some.url\",\"token\":\"http8\"}"))
}))
jws := &jws{privKey: privKey.(*rsa.PrivateKey), directoryURL: ts.URL}
solver := &httpChallenge{jws: jws, optPort: "23456"}
clientChallenge := challenge{Type: "http01", Status: "pending", URI: ts.URL, Token: "http8"}
if err := solver.Solve(clientChallenge, "127.0.0.1"); err != nil {
t.Errorf("VALID: Expected Solve to return no error but the error was -> %v", err)
}
}
func TestHTTPValidFull(t *testing.T) {
privKey, _ := generatePrivateKey(rsakey, 512)
ts := httptest.NewServer(nil)
jws := &jws{privKey: privKey.(*rsa.PrivateKey), directoryURL: ts.URL}
solver := &httpChallenge{jws: jws, optPort: "23457"}
clientChallenge := challenge{Type: "http01", Status: "pending", URI: ts.URL, Token: "http9"}
// Validate server on port 23456 which responds appropriately
ts.Config.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var request challenge
w.Header().Add("Replay-Nonce", "12345")
if r.Method == "HEAD" {
return
}
clientJws, _ := ioutil.ReadAll(r.Body)
j, err := jose.ParseSigned(string(clientJws))
j := &jws{privKey: privKey.(*rsa.PrivateKey)}
clientChallenge := challenge{Type: "http-01", Token: "http1"}
mockValidate := func(_ *jws, _ string, chlng challenge) error {
uri := "http://localhost:23457/.well-known/acme-challenge/" + chlng.Token
resp, err := http.Get(uri)
if err != nil {
t.Errorf("Client sent invalid JWS to the server.\n\t%v", err)
return
}
output, err := j.Verify(&privKey.(*rsa.PrivateKey).PublicKey)
if err != nil {
t.Errorf("Unable to verify client data -> %v", err)
}
json.Unmarshal(output, &request)
transport := &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: true}}
client := &http.Client{Transport: transport}
reqURL := "http://localhost:23457/.well-known/acme-challenge/" + clientChallenge.Token
t.Logf("Request URL is: %s", reqURL)
req, err := http.NewRequest("GET", reqURL, nil)
if err != nil {
t.Error(err)
}
req.Host = "127.0.0.1"
resp, err := client.Do(req)
if err != nil {
t.Errorf("Expected the solver to listen on port 23457 -> %v", err)
return err
}
defer resp.Body.Close()
body, _ := ioutil.ReadAll(resp.Body)
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 resp.Header.Get("Content-Type") != "text/plain" {
t.Errorf("Expected server to respond with content type text/plain.")
if bodyStr != chlng.KeyAuthorization {
t.Errorf("Get(%q) Body: got %q, want %q", uri, bodyStr, chlng.KeyAuthorization)
}
tokenRegex := regexp.MustCompile("^[\\w-]{43}$")
parts := strings.Split(bodyStr, ".")
return nil
}
solver := &httpChallenge{jws: j, validate: mockValidate, optPort: "23457"}
if len(parts) != 2 {
t.Errorf("Expected server token to be a composite of two strings, seperated by a dot")
}
if parts[0] != clientChallenge.Token {
t.Errorf("Expected the first part of the server token to be the challenge token.")
}
if !tokenRegex.MatchString(parts[1]) {
t.Errorf("Expected the second part of the server token to be a properly formatted key authorization")
}
valid := challenge{Type: "http01", Status: "valid", URI: ts.URL, Token: "1234567812"}
jsonBytes, _ := json.Marshal(&valid)
w.Write(jsonBytes)
})
if err := solver.Solve(clientChallenge, "127.0.0.1"); err != nil {
t.Errorf("VALID: Expected Solve to return no error but the error was -> %v", err)
if err := solver.Solve(clientChallenge, "localhost:23457"); err != nil {
t.Errorf("Solve error: got %v, want nil", err)
}
}
func TestHTTPChallengeInvalidPort(t *testing.T) {
privKey, _ := generatePrivateKey(rsakey, 128)
j := &jws{privKey: privKey.(*rsa.PrivateKey)}
clientChallenge := challenge{Type: "http-01", Token: "http2"}
solver := &httpChallenge{jws: j, validate: stubValidate, optPort: "123456"}
if err := solver.Solve(clientChallenge, "localhost:123456"); err == nil {
t.Error("Solve error: got %v, want error", err)
} else if want := "invalid port 123456"; !strings.HasSuffix(err.Error(), want) {
t.Errorf("Solve error: got %q, want suffix %q", err.Error(), want)
}
}

View file

@ -5,19 +5,14 @@ import (
"crypto/sha256"
"crypto/tls"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"net"
"net/http"
"time"
)
type tlsSNIChallenge struct {
jws *jws
optPort string
start chan net.Listener
end chan error
jws *jws
validate func(j *jws, uri string, chlng challenge) error
optPort string
}
func (t *tlsSNIChallenge) Solve(chlng challenge, domain string) error {
@ -26,80 +21,35 @@ func (t *tlsSNIChallenge) Solve(chlng challenge, domain string) error {
logf("[INFO][%s] acme: Trying to solve TLS-SNI-01", domain)
t.start = make(chan net.Listener)
t.end = make(chan error)
// Generate the Key Authorization for the challenge
keyAuth, err := getKeyAuthorization(chlng.Token, &t.jws.privKey.PublicKey)
if err != nil {
return err
}
certificate, err := t.generateCertificate(keyAuth)
cert, err := t.generateCertificate(keyAuth)
if err != nil {
return err
}
go t.startSNITLSServer(certificate)
var listener net.Listener
select {
case listener = <-t.start:
break
case err := <-t.end:
// Allow for CLI port override
port := ":443"
if t.optPort != "" {
port = ":" + t.optPort
}
tlsConf := new(tls.Config)
tlsConf.Certificates = []tls.Certificate{cert}
listener, err := tls.Listen("tcp", port, tlsConf)
if err != nil {
return fmt.Errorf("Could not start HTTPS server for challenge -> %v", err)
}
defer listener.Close()
// Make sure we properly close the HTTP server before we return
defer func() {
listener.Close()
err = <-t.end
close(t.start)
close(t.end)
}()
go http.Serve(listener, nil)
jsonBytes, err := json.Marshal(challenge{Resource: "challenge", Type: chlng.Type, Token: chlng.Token, KeyAuthorization: keyAuth})
if err != nil {
return errors.New("Failed to marshal network message...")
}
// Tell the server we handle TLS-SNI-01
resp, err := t.jws.post(chlng.URI, jsonBytes)
if err != nil {
return fmt.Errorf("Failed to post JWS message. -> %v", err)
}
// After the path is sent, the ACME server will access our server.
// Repeatedly check the server for an updated status on our request.
var challengeResponse challenge
Loop:
for {
if resp.StatusCode >= http.StatusBadRequest {
return handleHTTPError(resp)
}
err = json.NewDecoder(resp.Body).Decode(&challengeResponse)
resp.Body.Close()
if err != nil {
return err
}
switch challengeResponse.Status {
case "valid":
logf("[INFO][%s] The server validated our request", domain)
break Loop
case "pending":
break
case "invalid":
return handleChallengeError(challengeResponse)
default:
return errors.New("The server returned an unexpected state.")
}
time.Sleep(1 * time.Second)
resp, err = http.Get(chlng.URI)
}
return nil
return t.validate(t.jws, chlng.URI, challenge{Resource: "challenge", Type: chlng.Type, Token: chlng.Token, KeyAuthorization: keyAuth})
}
func (t *tlsSNIChallenge) generateCertificate(keyAuth string) (tls.Certificate, error) {
@ -128,26 +78,3 @@ func (t *tlsSNIChallenge) generateCertificate(keyAuth string) (tls.Certificate,
return certificate, nil
}
func (t *tlsSNIChallenge) startSNITLSServer(cert tls.Certificate) {
// Allow for CLI port override
port := ":443"
if t.optPort != "" {
port = ":" + t.optPort
}
tlsConf := new(tls.Config)
tlsConf.Certificates = []tls.Certificate{cert}
tlsListener, err := tls.Listen("tcp", port, tlsConf)
if err != nil {
t.end <- err
}
// Signal successfull start
t.start <- tlsListener
http.Serve(tlsListener, nil)
t.end <- nil
}

View file

@ -5,61 +5,17 @@ import (
"crypto/sha256"
"crypto/tls"
"encoding/hex"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"net/http/httptest"
"strings"
"testing"
"github.com/square/go-jose"
)
func TestTLSSNINonRootBind(t *testing.T) {
privKey, _ := generatePrivateKey(rsakey, 128)
jws := &jws{privKey: privKey.(*rsa.PrivateKey)}
solver := &tlsSNIChallenge{jws: jws}
clientChallenge := challenge{Type: "tls-sni-01", Status: "pending", URI: "localhost:4000", Token: "tls1"}
// validate error on non-root bind to 443
if err := solver.Solve(clientChallenge, "127.0.0.1"); err == nil {
t.Error("BIND: Expected Solve to return an error but the error was nil.")
} else {
expectedError := "Could not start HTTPS server for challenge -> listen tcp :443: bind: permission denied"
if err.Error() != expectedError {
t.Errorf("Expected error \"%s\" but instead got \"%s\"", expectedError, err.Error())
}
}
}
func TestTLSSNI(t *testing.T) {
func TestTLSSNIChallenge(t *testing.T) {
privKey, _ := generatePrivateKey(rsakey, 512)
optPort := "5001"
ts := httptest.NewServer(nil)
ts.Config.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var request challenge
w.Header().Add("Replay-Nonce", "12345")
if r.Method == "HEAD" {
return
}
clientJws, _ := ioutil.ReadAll(r.Body)
j, err := jose.ParseSigned(string(clientJws))
if err != nil {
t.Errorf("Client sent invalid JWS to the server.\n\t%v", err)
return
}
output, err := j.Verify(&privKey.(*rsa.PrivateKey).PublicKey)
if err != nil {
t.Errorf("Unable to verify client data -> %v", err)
}
json.Unmarshal(output, &request)
conn, err := tls.Dial("tcp", "localhost:"+optPort, &tls.Config{
j := &jws{privKey: privKey.(*rsa.PrivateKey)}
clientChallenge := challenge{Type: "tls-sni-01", Token: "tlssni1"}
mockValidate := func(_ *jws, _ string, chlng challenge) error {
conn, err := tls.Dial("tcp", "localhost:23457", &tls.Config{
InsecureSkipVerify: true,
})
if err != nil {
@ -77,7 +33,7 @@ func TestTLSSNI(t *testing.T) {
t.Errorf("Expected the challenge certificate to have exactly one DNSNames entry but had %d", count)
}
zBytes := sha256.Sum256([]byte(request.KeyAuthorization))
zBytes := sha256.Sum256([]byte(chlng.KeyAuthorization))
z := hex.EncodeToString(zBytes[:sha256.Size])
domain := fmt.Sprintf("%s.%s.acme.invalid", z[:32], z[32:])
@ -85,16 +41,24 @@ func TestTLSSNI(t *testing.T) {
t.Errorf("Expected the challenge certificate DNSName to match %s but was %s", domain, remoteCert.DNSNames[0])
}
valid := challenge{Type: "tls-sni-01", Status: "valid", URI: ts.URL, Token: "tls1"}
jsonBytes, _ := json.Marshal(&valid)
w.Write(jsonBytes)
})
return nil
}
solver := &tlsSNIChallenge{jws: j, validate: mockValidate, optPort: "23457"}
jws := &jws{privKey: privKey.(*rsa.PrivateKey), directoryURL: ts.URL}
solver := &tlsSNIChallenge{jws: jws, optPort: optPort}
clientChallenge := challenge{Type: "tls-sni-01", Status: "pending", URI: ts.URL, Token: "tls1"}
if err := solver.Solve(clientChallenge, "127.0.0.1"); err != nil {
t.Error("UNEXPECTED: Expected Solve to return no error but the error was %s.", err.Error())
if err := solver.Solve(clientChallenge, "localhost:23457"); err != nil {
t.Errorf("Solve error: got %v, want nil", err)
}
}
func TestTLSSNIChallengeInvalidPort(t *testing.T) {
privKey, _ := generatePrivateKey(rsakey, 128)
j := &jws{privKey: privKey.(*rsa.PrivateKey)}
clientChallenge := challenge{Type: "tls-sni-01", Token: "tlssni2"}
solver := &tlsSNIChallenge{jws: j, validate: stubValidate, optPort: "123456"}
if err := solver.Solve(clientChallenge, "localhost:123456"); err == nil {
t.Error("Solve error: got %v, want error", err)
} else if want := "invalid port 123456"; !strings.HasSuffix(err.Error(), want) {
t.Errorf("Solve error: got %q, want suffix %q", err.Error(), want)
}
}

6
cli.go
View file

@ -81,9 +81,9 @@ func main() {
Usage: "Directory to use for storing the data",
Value: defaultPath,
},
cli.StringFlag{
Name: "port",
Usage: "Challenges will use this port to listen on. Please make sure to forward port 80 and 443 to this port on your machine. Otherwise use setcap on the binary",
cli.StringSliceFlag{
Name: "solvers, S",
Usage: "Add an explicit solver for challenges. Solvers: \"http-01[:port]\", \"tls-sni-01[:port]\".",
},
}

View file

@ -34,7 +34,7 @@ func setup(c *cli.Context) (*Configuration, *Account, *acme.Client) {
//TODO: move to account struct? Currently MUST pass email.
acc := NewAccount(c.GlobalString("email"), conf)
client, err := acme.NewClient(c.GlobalString("server"), acc, conf.RsaBits(), conf.OptPort())
client, err := acme.NewClient(c.GlobalString("server"), acc, conf.RsaBits(), conf.Solvers())
if err != nil {
logger().Fatalf("Could not create client: %s", err.Error())
}
@ -126,7 +126,7 @@ func run(c *cli.Context) {
logger().Fatal("Please specify --domains or -d")
}
cert, failures := client.ObtainSANCertificate(c.GlobalStringSlice("domains"), true)
cert, failures := client.ObtainCertificate(c.GlobalStringSlice("domains"), true)
if len(failures) > 0 {
for k, v := range failures {
logger().Printf("[%s] Could not obtain certificates\n\t%s", k, v.Error())
@ -221,7 +221,7 @@ func renew(c *cli.Context) {
certRes.PrivateKey = keyBytes
certRes.Certificate = certBytes
newCert, err := client.RenewCertificate(certRes, true, true)
newCert, err := client.RenewCertificate(certRes, true)
if err != nil {
logger().Fatalf("%s", err.Error())
}

View file

@ -24,8 +24,8 @@ func (c *Configuration) RsaBits() int {
return c.context.GlobalInt("rsa-key-size")
}
func (c *Configuration) OptPort() string {
return c.context.GlobalString("port")
func (c *Configuration) Solvers() []string {
return c.context.GlobalStringSlice("solvers")
}
// ServerPath returns the OS dependent path to the data for a specific CA