Update everything to work with the latest boulder updates.

This commit is contained in:
xenolf 2015-09-26 19:45:52 +02:00
parent 41c2d8cc83
commit 37b20117bf
7 changed files with 141 additions and 56 deletions

View file

@ -51,7 +51,7 @@ type Client struct {
// NewClient creates a new client for the set user. // NewClient creates a new client for the set user.
func NewClient(caURL string, usr User, keyBits int, optPort string) *Client { func NewClient(caURL string, usr User, keyBits int, optPort string) *Client {
if err := usr.GetPrivateKey().Validate(); err != nil { if err := usr.GetPrivateKey().Validate(); err != nil {
logger().Fatalf("Could not validate the private account key of %s -> %v", usr.GetEmail(), err) logger().Fatalf("Could not validate the private account key of %s\n\t%v", usr.GetEmail(), err)
} }
jws := &jws{privKey: usr.GetPrivateKey()} jws := &jws{privKey: usr.GetPrivateKey()}
@ -60,7 +60,7 @@ func NewClient(caURL string, usr User, keyBits int, optPort string) *Client {
// 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[string]solver) solvers := make(map[string]solver)
solvers["simpleHttps"] = &simpleHTTPChallenge{jws: jws, optPort: optPort} solvers["simpleHttp"] = &simpleHTTPChallenge{jws: jws, optPort: optPort}
return &Client{regURL: caURL, user: usr, jws: jws, keyBits: keyBits, solvers: solvers} return &Client{regURL: caURL, user: usr, jws: jws, keyBits: keyBits, solvers: solvers}
} }
@ -68,7 +68,7 @@ func NewClient(caURL string, usr User, keyBits int, optPort string) *Client {
// 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() (*RegistrationResource, error) {
logger().Print("Registering account ... ") logger().Print("Registering account ... ")
jsonBytes, err := json.Marshal(registrationMessage{Contact: []string{"mailto:" + c.user.GetEmail()}}) jsonBytes, err := json.Marshal(registrationMessage{Resource: "new-reg", Contact: []string{"mailto:" + c.user.GetEmail()}})
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -111,6 +111,7 @@ func (c *Client) Register() (*RegistrationResource, error) {
// the server. // the server.
func (c *Client) AgreeToTos() error { func (c *Client) AgreeToTos() error {
c.user.GetRegistration().Body.Agreement = c.user.GetRegistration().TosURL c.user.GetRegistration().Body.Agreement = c.user.GetRegistration().TosURL
c.user.GetRegistration().Body.Resource = "reg"
jsonBytes, err := json.Marshal(&c.user.GetRegistration().Body) jsonBytes, err := json.Marshal(&c.user.GetRegistration().Body)
if err != nil { if err != nil {
return err return err
@ -193,7 +194,7 @@ func (c *Client) getChallenges(domains []string) []*authorizationResource {
for _, domain := range domains { for _, domain := range domains {
go func(domain string) { go func(domain string) {
jsonBytes, err := json.Marshal(authorization{Identifier: identifier{Type: "dns", Value: domain}}) jsonBytes, err := json.Marshal(authorization{Resource: "new-authz", Identifier: identifier{Type: "dns", Value: domain}})
if err != nil { if err != nil {
errc <- err errc <- err
return return
@ -259,7 +260,7 @@ func (c *Client) requestCertificates(challenges []*authorizationResource) ([]Cer
return nil, err return nil, err
} }
csrString := base64.URLEncoding.EncodeToString(csr) csrString := base64.URLEncoding.EncodeToString(csr)
jsonBytes, err := json.Marshal(csrMessage{Csr: csrString, Authorizations: []string{authz.AuthURL}}) jsonBytes, err := json.Marshal(csrMessage{Resource: "new-cert", Csr: csrString, Authorizations: []string{authz.AuthURL}})
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -269,8 +270,6 @@ func (c *Client) requestCertificates(challenges []*authorizationResource) ([]Cer
return nil, err return nil, err
} }
logResponseHeaders(resp)
if resp.Header.Get("Content-Type") != "application/pkix-cert" { if resp.Header.Get("Content-Type") != "application/pkix-cert" {
return nil, fmt.Errorf("The server returned an unexpected content-type header: %s - expected %s", resp.Header.Get("Content-Type"), "application/pkix-cert") return nil, fmt.Errorf("The server returned an unexpected content-type header: %s - expected %s", resp.Header.Get("Content-Type"), "application/pkix-cert")
} }

View file

@ -38,7 +38,7 @@ func TestNewClient(t *testing.T) {
t.Fatal("Expected %d solver(s), got %d", expected, actual) t.Fatal("Expected %d solver(s), got %d", expected, actual)
} }
simphttp, ok := client.solvers["simpleHttps"].(*simpleHTTPChallenge) simphttp, ok := client.solvers["simpleHttp"].(*simpleHTTPChallenge)
if !ok { if !ok {
t.Fatal("Expected simpleHttps solver to be simpleHTTPChallenge type") t.Fatal("Expected simpleHttps solver to be simpleHTTPChallenge type")
} }

View file

@ -3,33 +3,80 @@ package acme
import ( import (
"bytes" "bytes"
"crypto/rsa" "crypto/rsa"
"fmt"
"net/http" "net/http"
"github.com/square/go-jose" "github.com/letsencrypt/go-jose"
) )
type jws struct { type jws struct {
privKey *rsa.PrivateKey privKey *rsa.PrivateKey
nonces []string
} }
// Posts a JWS signed message to the specified URL // Posts a JWS signed message to the specified URL
func (j *jws) post(url string, content []byte) (*http.Response, error) { func (j *jws) post(url string, content []byte) (*http.Response, error) {
if len(j.nonces) == 0 {
err := j.getNonce(url)
if err != nil {
return nil, fmt.Errorf("Could not get a nonce for request: %s\n\t\tError: %v", url, err)
}
}
signedContent, err := j.signContent(content)
if err != nil {
return nil, err
}
resp, err := http.Post(url, "application/json", bytes.NewBuffer([]byte(signedContent.FullSerialize())))
if err != nil {
return nil, err
}
j.getNonceFromResponse(resp)
return resp, err
}
func (j *jws) signContent(content []byte) (*jose.JsonWebSignature, error) {
// TODO: support other algorithms - RS512 // TODO: support other algorithms - RS512
signer, err := jose.NewSigner(jose.RS256, j.privKey) signer, err := jose.NewSigner(jose.RS256, j.privKey)
if err != nil { if err != nil {
return nil, err return nil, err
} }
signed, err := signer.Sign(content) signed, err := signer.Sign(content, j.consumeNonce())
if err != nil { if err != nil {
return nil, err return nil, err
} }
signedContent := signed.FullSerialize() return signed, nil
}
resp, err := http.Post(url, "application/json", bytes.NewBuffer([]byte(signedContent))) func (j *jws) getNonceFromResponse(resp *http.Response) error {
nonce := resp.Header.Get("Replay-Nonce")
if nonce == "" {
return fmt.Errorf("Server did not respond with a proper nonce header.")
}
j.nonces = append(j.nonces, nonce)
return nil
}
func (j *jws) getNonce(url string) error {
resp, err := http.Head(url)
if err != nil { if err != nil {
return nil, err return err
} }
return resp, err return j.getNonceFromResponse(resp)
}
func (j *jws) consumeNonce() string {
nonce := ""
if len(j.nonces) == 0 {
return nonce
}
nonce, j.nonces = j.nonces[len(j.nonces)-1], j.nonces[:len(j.nonces)-1]
return nonce
} }

View file

@ -3,21 +3,24 @@ package acme
import "time" import "time"
type registrationMessage struct { type registrationMessage struct {
Resource string `json:"resource"`
Contact []string `json:"contact"` Contact []string `json:"contact"`
} }
// Registration is returned by the ACME server after the registration // Registration is returned by the ACME server after the registration
// The client implementation should save this registration somewhere. // The client implementation should save this registration somewhere.
type Registration struct { type Registration struct {
Resource string `json:"resource,omitempty"`
ID int `json:"id"` ID int `json:"id"`
Key struct { Key struct {
Kty string `json:"kty"` Kty string `json:"kty"`
N string `json:"n"` N string `json:"n"`
E string `json:"e"` E string `json:"e"`
} `json:"key"` } `json:"key"`
Recoverytoken string `json:"recoveryToken"`
Contact []string `json:"contact"` Contact []string `json:"contact"`
Agreement string `json:"agreement,omitempty"` 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
@ -37,6 +40,7 @@ type authorizationResource struct {
} }
type authorization struct { type authorization struct {
Resource string `json:"resource,omitempty"`
Identifier identifier `json:"identifier"` Identifier identifier `json:"identifier"`
Status string `json:"status,omitempty"` Status string `json:"status,omitempty"`
Expires time.Time `json:"expires,omitempty"` Expires time.Time `json:"expires,omitempty"`
@ -50,14 +54,16 @@ type identifier struct {
} }
type challenge struct { type challenge struct {
Resource string `json:"resource,omitempty"`
Type string `json:"type,omitempty"` Type string `json:"type,omitempty"`
Status string `json:"status,omitempty"` Status string `json:"status,omitempty"`
URI string `json:"uri,omitempty"` URI string `json:"uri,omitempty"`
Token string `json:"token,omitempty"` Token string `json:"token,omitempty"`
Path string `json:"path,omitempty"` Tls bool `json:"tls,omitempty"`
} }
type csrMessage struct { type csrMessage struct {
Resource string `json:"resource,omitempty"`
Csr string `json:"csr"` Csr string `json:"csr"`
Authorizations []string `json:"authorizations"` Authorizations []string `json:"authorizations"`
} }

View file

@ -25,6 +25,7 @@ type simpleHTTPChallenge struct {
// SimpleHTTPS checks for DNS, public IP and port bindings // SimpleHTTPS checks for DNS, public IP and port bindings
func (s *simpleHTTPChallenge) CanSolve(domain string) bool { func (s *simpleHTTPChallenge) CanSolve(domain string) bool {
// determine public ip // determine public ip
resp, err := http.Get("https://icanhazip.com/") resp, err := http.Get("https://icanhazip.com/")
if err != nil { if err != nil {
@ -54,24 +55,23 @@ func (s *simpleHTTPChallenge) CanSolve(domain string) bool {
} }
} }
logger().Printf("SimpleHTTPS: Domain %s does not resolve to the public ip of this server. Determined ip: %s", domain, ipStr) logger().Printf("SimpleHTTP: Domain %s does not resolve to the public ip of this server. Determined IP: %s Resolved IP: %s", domain, ipStr, resolvedIPs[0])
return false return false
} }
func (s *simpleHTTPChallenge) Solve(chlng challenge, domain string) error { func (s *simpleHTTPChallenge) Solve(chlng challenge, domain string) error {
logger().Print("Trying to solve SimpleHTTPS") logger().Print("Trying to solve SimpleHTTP")
// Generate random string for the path. The acme server will // Generate random string for the path. The acme server will
// access this path on the server in order to validate the request // access this path on the server in order to validate the request
responseToken := getRandomString(15) listener, err := s.startHTTPSServer(domain, chlng.Token)
listener, err := s.startHTTPSServer(domain, chlng.Token, responseToken)
if err != nil { if err != nil {
return fmt.Errorf("Could not start HTTPS server for challenge -> %v", err) return fmt.Errorf("Could not start HTTPS server for challenge -> %v", err)
} }
// Tell the server about the generated random path // Tell the server about the generated random path
jsonBytes, err := json.Marshal(challenge{Type: chlng.Type, Path: responseToken}) jsonBytes, err := json.Marshal(challenge{Resource: "challenge", Type: chlng.Type, Token: chlng.Token})
if err != nil { if err != nil {
return errors.New("Failed to marshal network message...") return errors.New("Failed to marshal network message...")
} }
@ -98,9 +98,11 @@ loop:
break break
case "invalid": case "invalid":
listener.Close() listener.Close()
logger().Print("The server could not validate our request.")
return errors.New("The server could not validate our request.") return errors.New("The server could not validate our request.")
default: default:
listener.Close() listener.Close()
logger().Print("The server returned an unexpected state.")
return errors.New("The server returned an unexpected state.") return errors.New("The server returned an unexpected state.")
} }
@ -113,7 +115,7 @@ loop:
// Starts a temporary HTTPS server on port 443. As soon as the challenge passed validation, // Starts a temporary HTTPS server on port 443. As soon as the challenge passed validation,
// this server will get shut down. The certificate generated here is only held in memory. // this server will get shut down. The certificate generated here is only held in memory.
func (s *simpleHTTPChallenge) startHTTPSServer(domain string, token string, responseToken string) (net.Listener, error) { func (s *simpleHTTPChallenge) startHTTPSServer(domain string, token string) (net.Listener, error) {
// Generate a new RSA key and a self-signed certificate. // Generate a new RSA key and a self-signed certificate.
tempPrivKey, err := generatePrivateKey(2048) tempPrivKey, err := generatePrivateKey(2048)
@ -135,7 +137,7 @@ func (s *simpleHTTPChallenge) startHTTPSServer(domain string, token string, resp
tlsConf := new(tls.Config) tlsConf := new(tls.Config)
tlsConf.Certificates = []tls.Certificate{tempKeyPair} tlsConf.Certificates = []tls.Certificate{tempKeyPair}
path := "/.well-known/acme-challenge/" + responseToken path := "/.well-known/acme-challenge/" + token
// Allow for CLI override // Allow for CLI override
port := ":443" port := ":443"
@ -148,11 +150,29 @@ func (s *simpleHTTPChallenge) startHTTPSServer(domain string, token string, resp
return nil, fmt.Errorf("Could not start HTTP listener! -> %v", err) return nil, fmt.Errorf("Could not start HTTP listener! -> %v", err)
} }
jsonBytes, err := json.Marshal(challenge{Type: "simpleHttp", Token: token, Tls: true})
if err != nil {
return nil, errors.New("startHTTPSServer: Failed to marshal network message...")
}
signed, err := s.jws.signContent(jsonBytes)
if err != nil {
return nil, errors.New("startHTTPSServer: Failed to sign message...")
}
signedCompact := signed.FullSerialize()
if err != nil {
return nil, errors.New("startHTTPSServer: Failed to serialize message...")
}
// The handler validates the HOST header and request type. // The handler validates the HOST header and request type.
// For validation it then writes the token the server returned with the challenge // For validation it then writes the token the server returned with the challenge
http.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) { http.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) {
if r.Host == domain && r.Method == "GET" { if strings.HasPrefix(r.Host, domain) && r.Method == "GET" {
w.Write([]byte(token)) w.Header().Add("Content-Type", "application/jose+json")
w.Write([]byte(signedCompact))
logger().Print("Served JWS payload...")
} else {
logger().Printf("Received request for domain %s with method %s", r.Host, r.Method)
w.Write([]byte("TEST"))
} }
}) })

View file

@ -45,10 +45,11 @@ func TestSimpleHTTP(t *testing.T) {
jws := &jws{privKey: privKey} jws := &jws{privKey: privKey}
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Add("Replay-Nonce", "12345")
})) }))
solver := &simpleHTTPChallenge{jws: jws} solver := &simpleHTTPChallenge{jws: jws}
clientChallenge := challenge{Type: "simpleHttps", Status: "pending", URI: ts.URL, Token: "123456789"} clientChallenge := challenge{Type: "simpleHttp", Status: "pending", URI: ts.URL, Token: "123456789"}
// validate error on non-root bind to 443 // validate error on non-root bind to 443
if err = solver.Solve(clientChallenge, "test.domain"); err == nil { if err = solver.Solve(clientChallenge, "test.domain"); err == nil {
@ -63,32 +64,43 @@ func TestSimpleHTTP(t *testing.T) {
// Validate error on invalid status // Validate error on invalid status
ts.Config.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { ts.Config.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
failed := challenge{Type: "simpleHttps", Status: "invalid", URI: ts.URL, Token: "123456789"} w.Header().Add("Replay-Nonce", "12345")
failed := challenge{Type: "simpleHttp", Status: "invalid", URI: ts.URL, Token: "1234567810"}
jsonBytes, _ := json.Marshal(&failed) jsonBytes, _ := json.Marshal(&failed)
w.Write(jsonBytes) w.Write(jsonBytes)
}) })
clientChallenge.Token = "1234567810"
if err = solver.Solve(clientChallenge, "test.domain"); err == nil { if err = solver.Solve(clientChallenge, "test.domain"); err == nil {
t.Error("FAILED: Expected Solve to return an error but the error was nil.") t.Error("FAILED: Expected Solve to return an error but the error was nil.")
} }
// Validate no error on valid response // Validate no error on valid response
ts.Config.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { ts.Config.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
valid := challenge{Type: "simpleHttps", Status: "valid", URI: ts.URL, Token: "123456789"} w.Header().Add("Replay-Nonce", "12345")
valid := challenge{Type: "simpleHttp", Status: "valid", URI: ts.URL, Token: "1234567811"}
jsonBytes, _ := json.Marshal(&valid) jsonBytes, _ := json.Marshal(&valid)
w.Write(jsonBytes) w.Write(jsonBytes)
}) })
clientChallenge.Token = "1234567811"
if err = solver.Solve(clientChallenge, "test.domain"); err != nil { if err = solver.Solve(clientChallenge, "test.domain"); err != nil {
t.Errorf("VALID: Expected Solve to return no error but the error was -> %v", err) t.Errorf("VALID: Expected Solve to return no error but the error was -> %v", err)
} }
// Validate server on port 8080 which responds appropriately // Validate server on port 8080 which responds appropriately
clientChallenge.Token = "1234567812"
ts.Config.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { ts.Config.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var request challenge var request challenge
w.Header().Add("Replay-Nonce", "12345")
if r.Method == "HEAD" {
return
}
clientJws, _ := ioutil.ReadAll(r.Body) clientJws, _ := ioutil.ReadAll(r.Body)
j, err := jose.ParseSigned(string(clientJws)) j, err := jose.ParseSigned(string(clientJws))
if err != nil { if err != nil {
t.Errorf("Client sent invalid JWS to the server. -> %v", err) t.Errorf("Client sent invalid JWS to the server.\n\t%v", err)
return
} }
output, err := j.Verify(&privKey.PublicKey) output, err := j.Verify(&privKey.PublicKey)
if err != nil { if err != nil {
@ -99,7 +111,7 @@ func TestSimpleHTTP(t *testing.T) {
transport := &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: true}} transport := &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: true}}
client := &http.Client{Transport: transport} client := &http.Client{Transport: transport}
reqURL := "https://localhost:8080/.well-known/acme-challenge/" + request.Path reqURL := "https://localhost:8080/.well-known/acme-challenge/" + clientChallenge.Token
t.Logf("Request URL is: %s", reqURL) t.Logf("Request URL is: %s", reqURL)
req, _ := http.NewRequest("GET", reqURL, nil) req, _ := http.NewRequest("GET", reqURL, nil)
req.Host = "test.domain" req.Host = "test.domain"
@ -110,11 +122,17 @@ func TestSimpleHTTP(t *testing.T) {
body, _ := ioutil.ReadAll(resp.Body) body, _ := ioutil.ReadAll(resp.Body)
bodyStr := string(body) bodyStr := string(body)
if bodyStr != "123456789" { clientResponse, err := jose.ParseSigned(bodyStr)
t.Errorf("Expected the solver to return the token %s but instead returned '%s'", "123456789", bodyStr) if err != nil {
t.Errorf("Client answered with invalid JWS.\n\t%v", err)
return
}
_, err = clientResponse.Verify(&privKey.PublicKey)
if err != nil {
t.Errorf("Unable to verify client data -> %v", err)
} }
valid := challenge{Type: "simpleHttps", Status: "valid", URI: ts.URL, Token: "123456789"} valid := challenge{Type: "simpleHttp", Status: "valid", URI: ts.URL, Token: "1234567812"}
jsonBytes, _ := json.Marshal(&valid) jsonBytes, _ := json.Marshal(&valid)
w.Write(jsonBytes) w.Write(jsonBytes)
}) })

View file

@ -36,7 +36,7 @@ func run(c *cli.Context) {
if acc.Registration == nil { if acc.Registration == nil {
reg, err := client.Register() reg, err := client.Register()
if err != nil { if err != nil {
logger().Fatalf("Could not complete registration -> %v", err) logger().Fatalf("Could not complete registration\n\t%v", err)
} }
acc.Registration = reg acc.Registration = reg
@ -49,12 +49,7 @@ func run(c *cli.Context) {
You should make a secure backup of this folder now. This You should make a secure backup of this folder now. This
configuration directory will also contain certificates and configuration directory will also contain certificates and
private keys obtained from Let's Encrypt so making regular private keys obtained from Let's Encrypt so making regular
backups of this folder is ideal. backups of this folder is ideal.`, c.GlobalString("config-dir"))
If you lose your account credentials, you can recover
them using the token
"%s".
You must write that down and put it in a safe place.`, c.GlobalString("config-dir"), reg.Body.Recoverytoken)
} }
@ -96,7 +91,7 @@ func run(c *cli.Context) {
certs, err := client.ObtainCertificates(c.GlobalStringSlice("domains")) certs, err := client.ObtainCertificates(c.GlobalStringSlice("domains"))
if err != nil { if err != nil {
logger().Fatalf("Could not obtain certificates -> %v", err) logger().Fatalf("Could not obtain certificates\n\t%v", err)
} }
err = checkFolder(conf.CertPath()) err = checkFolder(conf.CertPath())
@ -110,12 +105,12 @@ func run(c *cli.Context) {
err = ioutil.WriteFile(certOut, certRes.Certificate, 0700) err = ioutil.WriteFile(certOut, certRes.Certificate, 0700)
if err != nil { if err != nil {
logger().Printf("Unable to save Certificate for domain %s -> %v", certRes.Domain, err) logger().Printf("Unable to save Certificate for domain %s\n\t%v", certRes.Domain, err)
} }
err = ioutil.WriteFile(privOut, certRes.PrivateKey, 0700) err = ioutil.WriteFile(privOut, certRes.PrivateKey, 0700)
if err != nil { if err != nil {
logger().Printf("Unable to save PrivateKey for domain %s -> %v", certRes.Domain, err) logger().Printf("Unable to save PrivateKey for domain %s\n\t%v", certRes.Domain, err)
} }
} }