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.
func NewClient(caURL string, usr User, keyBits int, optPort string) *Client {
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()}
@ -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
// spec to this map. Otherwise they won`t be found.
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}
}
@ -68,7 +68,7 @@ func NewClient(caURL string, usr User, keyBits int, optPort string) *Client {
// Register the current account to the ACME server.
func (c *Client) Register() (*RegistrationResource, error) {
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 {
return nil, err
}
@ -111,6 +111,7 @@ func (c *Client) Register() (*RegistrationResource, error) {
// the server.
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
@ -193,7 +194,7 @@ func (c *Client) getChallenges(domains []string) []*authorizationResource {
for _, domain := range domains {
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 {
errc <- err
return
@ -259,7 +260,7 @@ func (c *Client) requestCertificates(challenges []*authorizationResource) ([]Cer
return nil, err
}
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 {
return nil, err
}
@ -269,8 +270,6 @@ func (c *Client) requestCertificates(challenges []*authorizationResource) ([]Cer
return nil, err
}
logResponseHeaders(resp)
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")
}

View file

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

View file

@ -3,33 +3,80 @@ package acme
import (
"bytes"
"crypto/rsa"
"fmt"
"net/http"
"github.com/square/go-jose"
"github.com/letsencrypt/go-jose"
)
type jws struct {
privKey *rsa.PrivateKey
nonces []string
}
// Posts a JWS signed message to the specified URL
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
signer, err := jose.NewSigner(jose.RS256, j.privKey)
if err != nil {
return nil, err
}
signed, err := signer.Sign(content)
signed, err := signer.Sign(content, j.consumeNonce())
if err != nil {
return nil, err
}
signedContent := signed.FullSerialize()
resp, err := http.Post(url, "application/json", bytes.NewBuffer([]byte(signedContent)))
if err != nil {
return nil, err
}
return resp, err
return signed, nil
}
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 {
return 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"
type registrationMessage struct {
Contact []string `json:"contact"`
Resource string `json:"resource"`
Contact []string `json:"contact"`
}
// Registration is returned by the ACME server after the registration
// The client implementation should save this registration somewhere.
type Registration struct {
ID int `json:"id"`
Key struct {
Resource string `json:"resource,omitempty"`
ID int `json:"id"`
Key struct {
Kty string `json:"kty"`
N string `json:"n"`
E string `json:"e"`
} `json:"key"`
Recoverytoken string `json:"recoveryToken"`
Contact []string `json:"contact"`
Agreement string `json:"agreement,omitempty"`
Contact []string `json:"contact"`
Agreement string `json:"agreement,omitempty"`
Authorizations string `json:"authorizations,omitempty"`
Certificates string `json:"certificates,omitempty"`
}
// RegistrationResource represents all important informations about a registration
@ -37,6 +40,7 @@ type authorizationResource struct {
}
type authorization struct {
Resource string `json:"resource,omitempty"`
Identifier identifier `json:"identifier"`
Status string `json:"status,omitempty"`
Expires time.Time `json:"expires,omitempty"`
@ -50,14 +54,16 @@ type identifier struct {
}
type challenge struct {
Type string `json:"type,omitempty"`
Status string `json:"status,omitempty"`
URI string `json:"uri,omitempty"`
Token string `json:"token,omitempty"`
Path string `json:"path,omitempty"`
Resource string `json:"resource,omitempty"`
Type string `json:"type,omitempty"`
Status string `json:"status,omitempty"`
URI string `json:"uri,omitempty"`
Token string `json:"token,omitempty"`
Tls bool `json:"tls,omitempty"`
}
type csrMessage struct {
Resource string `json:"resource,omitempty"`
Csr string `json:"csr"`
Authorizations []string `json:"authorizations"`
}

View file

@ -25,6 +25,7 @@ type simpleHTTPChallenge struct {
// SimpleHTTPS checks for DNS, public IP and port bindings
func (s *simpleHTTPChallenge) CanSolve(domain string) bool {
// determine public ip
resp, err := http.Get("https://icanhazip.com/")
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
}
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
// access this path on the server in order to validate the request
responseToken := getRandomString(15)
listener, err := s.startHTTPSServer(domain, chlng.Token, responseToken)
listener, err := s.startHTTPSServer(domain, chlng.Token)
if err != nil {
return fmt.Errorf("Could not start HTTPS server for challenge -> %v", err)
}
// 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 {
return errors.New("Failed to marshal network message...")
}
@ -98,9 +98,11 @@ loop:
break
case "invalid":
listener.Close()
logger().Print("The server could not validate our request.")
return errors.New("The server could not validate our request.")
default:
listener.Close()
logger().Print("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,
// 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.
tempPrivKey, err := generatePrivateKey(2048)
@ -135,7 +137,7 @@ func (s *simpleHTTPChallenge) startHTTPSServer(domain string, token string, resp
tlsConf := new(tls.Config)
tlsConf.Certificates = []tls.Certificate{tempKeyPair}
path := "/.well-known/acme-challenge/" + responseToken
path := "/.well-known/acme-challenge/" + token
// Allow for CLI override
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)
}
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.
// For validation it then writes the token the server returned with the challenge
http.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) {
if r.Host == domain && r.Method == "GET" {
w.Write([]byte(token))
if strings.HasPrefix(r.Host, domain) && r.Method == "GET" {
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}
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Add("Replay-Nonce", "12345")
}))
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
if err = solver.Solve(clientChallenge, "test.domain"); err == nil {
@ -63,32 +64,43 @@ func TestSimpleHTTP(t *testing.T) {
// Validate error on invalid status
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)
w.Write(jsonBytes)
})
clientChallenge.Token = "1234567810"
if err = solver.Solve(clientChallenge, "test.domain"); err == nil {
t.Error("FAILED: Expected Solve to return an error but the error was nil.")
}
// Validate no error on valid response
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)
w.Write(jsonBytes)
})
clientChallenge.Token = "1234567811"
if err = solver.Solve(clientChallenge, "test.domain"); err != nil {
t.Errorf("VALID: Expected Solve to return no error but the error was -> %v", err)
}
// Validate server on port 8080 which responds appropriately
clientChallenge.Token = "1234567812"
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. -> %v", err)
t.Errorf("Client sent invalid JWS to the server.\n\t%v", err)
return
}
output, err := j.Verify(&privKey.PublicKey)
if err != nil {
@ -99,7 +111,7 @@ func TestSimpleHTTP(t *testing.T) {
transport := &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: true}}
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)
req, _ := http.NewRequest("GET", reqURL, nil)
req.Host = "test.domain"
@ -110,11 +122,17 @@ func TestSimpleHTTP(t *testing.T) {
body, _ := ioutil.ReadAll(resp.Body)
bodyStr := string(body)
if bodyStr != "123456789" {
t.Errorf("Expected the solver to return the token %s but instead returned '%s'", "123456789", bodyStr)
clientResponse, err := jose.ParseSigned(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)
w.Write(jsonBytes)
})

View file

@ -36,7 +36,7 @@ func run(c *cli.Context) {
if acc.Registration == nil {
reg, err := client.Register()
if err != nil {
logger().Fatalf("Could not complete registration -> %v", err)
logger().Fatalf("Could not complete registration\n\t%v", err)
}
acc.Registration = reg
@ -49,12 +49,7 @@ func run(c *cli.Context) {
You should make a secure backup of this folder now. This
configuration directory will also contain certificates and
private keys obtained from Let's Encrypt so making regular
backups of this folder is ideal.
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)
backups of this folder is ideal.`, c.GlobalString("config-dir"))
}
@ -96,7 +91,7 @@ func run(c *cli.Context) {
certs, err := client.ObtainCertificates(c.GlobalStringSlice("domains"))
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())
@ -110,12 +105,12 @@ func run(c *cli.Context) {
err = ioutil.WriteFile(certOut, certRes.Certificate, 0700)
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)
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)
}
}