forked from TrueCloudLab/lego
Initial version of the SimpleHTTPS challenge
This commit is contained in:
parent
8f992218b9
commit
a2d9bf4cc3
4 changed files with 236 additions and 31 deletions
|
@ -2,6 +2,7 @@ package acme
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/rsa"
|
"crypto/rsa"
|
||||||
|
"encoding/base64"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
@ -34,7 +35,7 @@ type User interface {
|
||||||
|
|
||||||
type solver interface {
|
type solver interface {
|
||||||
CanSolve() bool
|
CanSolve() bool
|
||||||
Solve(challenge challenge, domain string)
|
Solve(challenge challenge, domain string) error
|
||||||
}
|
}
|
||||||
|
|
||||||
// Client is the user-friendy way to ACME
|
// Client is the user-friendy way to ACME
|
||||||
|
@ -42,11 +43,12 @@ type Client struct {
|
||||||
regURL string
|
regURL string
|
||||||
user User
|
user User
|
||||||
jws *jws
|
jws *jws
|
||||||
|
keyBits int
|
||||||
Solvers map[string]solver
|
Solvers map[string]solver
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewClient creates a new client for the set user.
|
// NewClient creates a new client for the set user.
|
||||||
func NewClient(caURL string, usr User, 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 -> %v", usr.GetEmail(), err)
|
||||||
}
|
}
|
||||||
|
@ -55,10 +57,9 @@ func NewClient(caURL string, usr User, optPort string) *Client {
|
||||||
|
|
||||||
// REVIEW: best possibility?
|
// REVIEW: best possibility?
|
||||||
solvers := make(map[string]solver)
|
solvers := make(map[string]solver)
|
||||||
solvers["simpleHttp"] = &simpleHTTPChallenge{jws: jws}
|
solvers["simpleHttps"] = &simpleHTTPChallenge{jws: jws, optPort: optPort}
|
||||||
solvers["dvsni"] = &dvsniChallenge{}
|
|
||||||
|
|
||||||
return &Client{regURL: caURL, user: usr, jws: jws}
|
return &Client{regURL: caURL, user: usr, jws: jws, keyBits: keyBits, Solvers: solvers}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Register the current account to the ACME server.
|
// Register the current account to the ACME server.
|
||||||
|
@ -112,33 +113,32 @@ func (c *Client) AgreeToTos() error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
logger().Printf("Agreement: %s", string(jsonBytes))
|
|
||||||
|
|
||||||
resp, err := c.jws.post(c.user.GetRegistration().URI, jsonBytes)
|
resp, err := c.jws.post(c.user.GetRegistration().URI, jsonBytes)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
logResponseBody(resp)
|
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusAccepted {
|
if resp.StatusCode != http.StatusAccepted {
|
||||||
return fmt.Errorf("The server returned %d but we expected %d", resp.StatusCode, http.StatusAccepted)
|
return fmt.Errorf("The server returned %d but we expected %d", resp.StatusCode, http.StatusAccepted)
|
||||||
}
|
}
|
||||||
|
|
||||||
logResponseHeaders(resp)
|
|
||||||
logResponseBody(resp)
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ObtainCertificates tries to obtain certificates from the CA server
|
// ObtainCertificates tries to obtain certificates from the CA server
|
||||||
// using the challenges it has configured. It also tries to do multiple
|
// using the challenges it has configured. The returned certificates are
|
||||||
// certificate processings at the same time in parallel.
|
// DER encoded byte slices.
|
||||||
func (c *Client) ObtainCertificates(domains []string) error {
|
func (c *Client) ObtainCertificates(domains []string) ([]CertificateResource, error) {
|
||||||
|
logger().Print("Obtaining certificates...")
|
||||||
challenges := c.getChallenges(domains)
|
challenges := c.getChallenges(domains)
|
||||||
c.solveChallenges(challenges)
|
err := c.solveChallenges(challenges)
|
||||||
return nil
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
logger().Print("Validations succeeded. Getting certificates")
|
||||||
|
|
||||||
|
return c.requestCertificates(challenges)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Looks through the challenge combinations to find a solvable match.
|
// Looks through the challenge combinations to find a solvable match.
|
||||||
|
@ -149,7 +149,11 @@ func (c *Client) solveChallenges(challenges []*authorizationResource) error {
|
||||||
// no solvers - no solving
|
// no solvers - no solving
|
||||||
if solvers := c.chooseSolvers(authz.Body); solvers != nil {
|
if solvers := c.chooseSolvers(authz.Body); solvers != nil {
|
||||||
for i, solver := range solvers {
|
for i, solver := range solvers {
|
||||||
solver.Solve(authz.Body.Challenges[i], authz.Domain)
|
// TODO: do not immediately fail if one domain fails to validate.
|
||||||
|
err := solver.Solve(authz.Body.Challenges[i], authz.Domain)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return fmt.Errorf("Could not determine solvers for %s", authz.Domain)
|
return fmt.Errorf("Could not determine solvers for %s", authz.Domain)
|
||||||
|
@ -164,9 +168,11 @@ func (c *Client) solveChallenges(challenges []*authorizationResource) error {
|
||||||
func (c *Client) chooseSolvers(auth authorization) map[int]solver {
|
func (c *Client) chooseSolvers(auth authorization) map[int]solver {
|
||||||
for _, combination := range auth.Combinations {
|
for _, combination := range auth.Combinations {
|
||||||
solvers := make(map[int]solver)
|
solvers := make(map[int]solver)
|
||||||
for i := range combination {
|
for _, idx := range combination {
|
||||||
if solver, ok := c.Solvers[auth.Challenges[i].Type]; ok {
|
if solver, ok := c.Solvers[auth.Challenges[idx].Type]; ok {
|
||||||
solvers[i] = solver
|
solvers[idx] = solver
|
||||||
|
} else {
|
||||||
|
logger().Printf("Could not find solver for: %s", auth.Challenges[idx].Type)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -213,7 +219,7 @@ func (c *Client) getChallenges(domains []string) []*authorizationResource {
|
||||||
errc <- err
|
errc <- err
|
||||||
}
|
}
|
||||||
|
|
||||||
resc <- &authorizationResource{Body: authz, NewCertURL: links["next"], Domain: domain}
|
resc <- &authorizationResource{Body: authz, NewCertURL: links["next"], AuthURL: resp.Header.Get("Location"), Domain: domain}
|
||||||
|
|
||||||
}(domain)
|
}(domain)
|
||||||
}
|
}
|
||||||
|
@ -234,6 +240,47 @@ func (c *Client) getChallenges(domains []string) []*authorizationResource {
|
||||||
return responses
|
return responses
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *Client) requestCertificates(challenges []*authorizationResource) ([]CertificateResource, error) {
|
||||||
|
var certs []CertificateResource
|
||||||
|
for _, authz := range challenges {
|
||||||
|
privKey, err := generatePrivateKey(c.keyBits)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
csr, err := generateCsr(privKey, authz.Domain)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
csrString := base64.URLEncoding.EncodeToString(csr)
|
||||||
|
jsonBytes, err := json.Marshal(csrMessage{Csr: csrString})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := c.jws.post(authz.NewCertURL, jsonBytes)
|
||||||
|
if err != nil {
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
|
||||||
|
cert, err := ioutil.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
privateKeyPem := pemEncode(privKey)
|
||||||
|
|
||||||
|
certs = append(certs, CertificateResource{Domain: authz.Domain, CertURL: resp.Header.Get("Location"), PrivateKey: privateKeyPem, Certificate: cert})
|
||||||
|
}
|
||||||
|
return certs, nil
|
||||||
|
}
|
||||||
|
|
||||||
func logResponseHeaders(resp *http.Response) {
|
func logResponseHeaders(resp *http.Response) {
|
||||||
logger().Println(resp.Status)
|
logger().Println(resp.Status)
|
||||||
for k, v := range resp.Header {
|
for k, v := range resp.Header {
|
||||||
|
|
|
@ -49,8 +49,23 @@ type identifier struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
type challenge struct {
|
type challenge struct {
|
||||||
Type string `json:"type"`
|
Type string `json:"type,omitempty"`
|
||||||
Status string `json:"status"`
|
Status string `json:"status,omitempty"`
|
||||||
URI string `json:"uri"`
|
URI string `json:"uri,omitempty"`
|
||||||
Token string `json:"token"`
|
Token string `json:"token,omitempty"`
|
||||||
|
Path string `json:"path,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type csrMessage struct {
|
||||||
|
Csr string `json:"csr"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// CertificateResource represents a CA issued certificate.
|
||||||
|
// PrivateKey and Certificate are both already PEM encoded
|
||||||
|
// and can be directly written to disk.
|
||||||
|
type CertificateResource struct {
|
||||||
|
Domain string
|
||||||
|
CertURL string
|
||||||
|
PrivateKey []byte
|
||||||
|
Certificate []byte
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,12 +1,155 @@
|
||||||
package acme
|
package acme
|
||||||
|
|
||||||
type simpleHTTPChallenge struct{}
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/rsa"
|
||||||
|
"crypto/tls"
|
||||||
|
"crypto/x509"
|
||||||
|
"crypto/x509/pkix"
|
||||||
|
"encoding/json"
|
||||||
|
"encoding/pem"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"math/big"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type simpleHTTPChallenge struct {
|
||||||
|
jws *jws
|
||||||
|
optPort string
|
||||||
|
}
|
||||||
|
|
||||||
func (s *simpleHTTPChallenge) CanSolve() bool {
|
func (s *simpleHTTPChallenge) CanSolve() bool {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *simpleHTTPChallenge) Solve(challenge challenge) {
|
func (s *simpleHTTPChallenge) Solve(chlng challenge, domain string) error {
|
||||||
func (s *simpleHTTPChallenge) Solve(chlng challenge, domain string) {
|
|
||||||
|
|
||||||
|
logger().Print("Trying to solve SimpleHTTPS")
|
||||||
|
|
||||||
|
responseToken := getRandomString(15)
|
||||||
|
listener, err := s.startHTTPSServer(domain, chlng.Token, responseToken)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Could not start HTTPS server for challenge -> %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
jsonBytes, err := json.Marshal(challenge{Type: chlng.Type, Path: responseToken})
|
||||||
|
if err != nil {
|
||||||
|
return errors.New("Failed to marshal network message...")
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := s.jws.post(chlng.URI, jsonBytes)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Failed to post JWS message. -> %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var challengeResponse challenge
|
||||||
|
loop:
|
||||||
|
for {
|
||||||
|
decoder := json.NewDecoder(resp.Body)
|
||||||
|
decoder.Decode(&challengeResponse)
|
||||||
|
|
||||||
|
switch challengeResponse.Status {
|
||||||
|
case "valid":
|
||||||
|
logger().Print("The server validated our request")
|
||||||
|
listener.Close()
|
||||||
|
break loop
|
||||||
|
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.")
|
||||||
|
}
|
||||||
|
|
||||||
|
time.Sleep(1 * time.Second)
|
||||||
|
resp, err = http.Get(chlng.URI)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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) {
|
||||||
|
tempPrivKey, err := generatePrivateKey(2048)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
tempCertPEM, err := generateCert(tempPrivKey, domain)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
pemBytes := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(tempPrivKey)})
|
||||||
|
tempKeyPair, err := tls.X509KeyPair(
|
||||||
|
tempCertPEM,
|
||||||
|
pemBytes)
|
||||||
|
if err != nil {
|
||||||
|
logger().Print("error here!")
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
tlsConf := new(tls.Config)
|
||||||
|
tlsConf.Certificates = []tls.Certificate{tempKeyPair}
|
||||||
|
|
||||||
|
path := "/.well-known/acme-challenge/" + responseToken
|
||||||
|
|
||||||
|
port := ":443"
|
||||||
|
if s.optPort != "" {
|
||||||
|
port = ":" + s.optPort
|
||||||
|
}
|
||||||
|
tlsListener, err := tls.Listen("tcp", port, tlsConf)
|
||||||
|
if err != nil {
|
||||||
|
logger().Fatalf("Could not start HTTP listener! -> %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
http.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Host == domain && r.Method == "GET" {
|
||||||
|
w.Write([]byte(token))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
go http.Serve(tlsListener, nil)
|
||||||
|
|
||||||
|
return tlsListener, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getRandomString(length int) string {
|
||||||
|
const alphanum = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
|
||||||
|
var bytes = make([]byte, length)
|
||||||
|
rand.Read(bytes)
|
||||||
|
for i, b := range bytes {
|
||||||
|
bytes[i] = alphanum[b%byte(len(alphanum))]
|
||||||
|
}
|
||||||
|
return string(bytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
func generateCert(privKey *rsa.PrivateKey, domain string) ([]byte, error) {
|
||||||
|
serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
|
||||||
|
serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
template := x509.Certificate{
|
||||||
|
SerialNumber: serialNumber,
|
||||||
|
Subject: pkix.Name{
|
||||||
|
CommonName: "ACME Challenge TEMP",
|
||||||
|
},
|
||||||
|
NotBefore: time.Now(),
|
||||||
|
NotAfter: time.Now().Add(365),
|
||||||
|
|
||||||
|
KeyUsage: x509.KeyUsageKeyEncipherment,
|
||||||
|
BasicConstraintsValid: true,
|
||||||
|
DNSNames: []string{domain},
|
||||||
|
}
|
||||||
|
|
||||||
|
derBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, &privKey.PublicKey, privKey)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: derBytes}), nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,7 +30,7 @@ func run(c *cli.Context) {
|
||||||
}
|
}
|
||||||
|
|
||||||
acc := NewAccount(c.GlobalString("email"), conf)
|
acc := NewAccount(c.GlobalString("email"), conf)
|
||||||
client := acme.NewClient(c.GlobalString("server"), acc, conf.OptPort())
|
client := acme.NewClient(c.GlobalString("server"), acc, conf.RsaBits(), conf.OptPort())
|
||||||
if acc.Registration == nil {
|
if acc.Registration == nil {
|
||||||
reg, err := client.Register()
|
reg, err := client.Register()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
Loading…
Reference in a new issue