Add integration test for precertificate OCSP. (#4417)

This test adds support in ct-test-srv for rejecting precertificates by
hostname, in order to artificially generate a condition where a
precertificate is issued but no final certificate can be issued. Right
now the final check in the test is temporarily disabled until the
feature is fixed.

Also, as our first Go-based integration test, this pulls in the
eggsampler/acme Go client, and adds some suport in integration-test.py.

This also refactors ct-test-srv slightly to use a ServeMux, and fixes
a couple of cases of not returning immediately on error.
This commit is contained in:
Jacob Hoffman-Andrews 2019-09-06 13:35:08 -07:00 committed by GitHub
parent b905691211
commit a8586d05cd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
27 changed files with 2200 additions and 64 deletions

1
go.mod
View File

@ -8,6 +8,7 @@ require (
github.com/beeker1121/goque v0.0.0-20170321141813-4044bc29b280
github.com/beorn7/perks v0.0.0-20160229213445-3ac7bf7a47d1 // indirect
github.com/cloudflare/cfssl v0.0.0-20190716004220-2185c182e6ba
github.com/eggsampler/acme/v2 v2.0.1
github.com/go-gorp/gorp v2.0.0+incompatible // indirect
github.com/go-sql-driver/mysql v0.0.0-20170715192408-3955978caca4
github.com/golang/mock v1.2.0

2
go.sum
View File

@ -21,6 +21,8 @@ github.com/cloudflare/cfssl v0.0.0-20190726000631-633726f6bcb7 h1:Puu1hUwfps3+1C
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/eggsampler/acme/v2 v2.0.1 h1:SfhaHP/6jCWOEMdzWI/9pmNDm2yuCHd4agW7u29fEJY=
github.com/eggsampler/acme/v2 v2.0.1/go.mod h1:kMR4S+ZCJtXb0WCg8MJkUKFQ0pyLEMQ9l5JA+CHvK2Y=
github.com/envoyproxy/go-control-plane v0.6.9/go.mod h1:SBwIajubJHhxtWwsL9s8ss4safvEdbitLhGGK48rN6g=
github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=

View File

@ -28,16 +28,75 @@ type ctSubmissionRequest struct {
type integrationSrv struct {
sync.Mutex
submissions map[string]int64
// Hostnames where we refuse to provide an SCT. This is to exercise the code
// path where all CT servers fail.
rejectHosts map[string]bool
// A list of entries that we rejected based on rejectHosts.
rejected []string
key *ecdsa.PrivateKey
latencySchedule []float64
latencyItem int
}
func (is *integrationSrv) handler(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/ct/v1/add-pre-chain":
fallthrough
case "/ct/v1/add-chain":
func readJSON(w http.ResponseWriter, r *http.Request, output interface{}) error {
if r.Method != "POST" {
return fmt.Errorf("incorrect method; only POST allowed")
}
bodyBytes, err := ioutil.ReadAll(r.Body)
if err != nil {
return err
}
err = json.Unmarshal(bodyBytes, output)
if err != nil {
return err
}
return nil
}
func (is *integrationSrv) addChain(w http.ResponseWriter, r *http.Request) {
is.addChainOrPre(w, r, false)
}
// addRejectHost takes a JSON POST with a "host" field; any subsequent
// submissions for that host will get a 400 error.
func (is *integrationSrv) addRejectHost(w http.ResponseWriter, r *http.Request) {
var rejectHostReq struct {
Host string
}
err := readJSON(w, r, &rejectHostReq)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
is.Lock()
defer is.Unlock()
is.rejectHosts[rejectHostReq.Host] = true
w.Write([]byte{})
}
// getRejections returns a JSON array containing strings; those strings are
// base64 encodings of certificates or precertificates that were rejected due to
// the rejectHosts mechanism.
func (is *integrationSrv) getRejections(w http.ResponseWriter, r *http.Request) {
is.Lock()
defer is.Unlock()
output, err := json.Marshal(is.rejected)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
w.WriteHeader(http.StatusOK)
w.Write(output)
}
func (is *integrationSrv) addPreChain(w http.ResponseWriter, r *http.Request) {
is.addChainOrPre(w, r, true)
}
func (is *integrationSrv) addChainOrPre(w http.ResponseWriter, r *http.Request, precert bool) {
if r.Method != "POST" {
http.NotFound(w, r)
return
@ -45,23 +104,20 @@ func (is *integrationSrv) handler(w http.ResponseWriter, r *http.Request) {
bodyBytes, err := ioutil.ReadAll(r.Body)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
var addChainReq ctSubmissionRequest
err = json.Unmarshal(bodyBytes, &addChainReq)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
if len(addChainReq.Chain) == 0 {
w.WriteHeader(400)
return
}
precert := false
if r.URL.Path == "/ct/v1/add-pre-chain" {
precert = true
}
b, err := base64.StdEncoding.DecodeString(addChainReq.Chain[0])
if err != nil {
w.WriteHeader(400)
@ -75,6 +131,15 @@ func (is *integrationSrv) handler(w http.ResponseWriter, r *http.Request) {
hostnames := strings.Join(cert.DNSNames, ",")
is.Lock()
for _, h := range cert.DNSNames {
if is.rejectHosts[h] {
is.Unlock()
is.rejected = append(is.rejected, addChainReq.Chain[0])
w.WriteHeader(400)
return
}
}
is.submissions[hostnames]++
is.Unlock()
@ -87,7 +152,9 @@ func (is *integrationSrv) handler(w http.ResponseWriter, r *http.Request) {
}
w.WriteHeader(http.StatusOK)
w.Write(publisher.CreateTestingSignedSCT(addChainReq.Chain, is.key, precert, time.Now()))
case "/submissions":
}
func (is *integrationSrv) getSubmissions(w http.ResponseWriter, r *http.Request) {
if r.Method != "GET" {
http.NotFound(w, r)
return
@ -100,10 +167,6 @@ func (is *integrationSrv) handler(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
fmt.Fprintf(w, "%d", submissions)
default:
http.NotFound(w, r)
return
}
}
type config struct {
@ -139,10 +202,17 @@ func runPersonality(p Personality) {
key: key,
latencySchedule: p.LatencySchedule,
submissions: make(map[string]int64),
rejectHosts: make(map[string]bool),
}
m := http.NewServeMux()
m.HandleFunc("/submissions", is.getSubmissions)
m.HandleFunc("/ct/v1/add-pre-chain", is.addPreChain)
m.HandleFunc("/ct/v1/add-chain", is.addChain)
m.HandleFunc("/add-reject-host", is.addRejectHost)
m.HandleFunc("/get-rejections", is.getRejections)
srv := &http.Server{
Addr: p.Addr,
Handler: http.HandlerFunc(is.handler),
Handler: m,
}
log.Printf("ct-test-srv on %s with pubkey %s", p.Addr,
base64.StdEncoding.EncodeToString(pubKeyBytes))

View File

@ -38,6 +38,9 @@ def run_client_tests():
cmd = os.path.join(root, 'tests', 'boulder-integration.sh')
run(cmd, cwd=root)
def run_go_tests():
run("go test -tags integration -count=1 ./test/integration")
def run_expired_authz_purger():
# Note: This test must be run after all other tests that depend on
# authorizations added to the database during setup
@ -256,6 +259,8 @@ def main():
if args.run_certbot:
run_client_tests()
run_go_tests()
if args.custom:
run(args.custom)

View File

@ -0,0 +1,156 @@
// +build integration
package integration
import (
"bytes"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/x509"
"crypto/x509/pkix"
"encoding/hex"
"fmt"
"net/http"
"os"
"github.com/eggsampler/acme/v2"
)
func random_domain() string {
var bytes [3]byte
rand.Read(bytes[:])
return hex.EncodeToString(bytes[:]) + ".com"
}
type client struct {
acme.Account
acme.Client
}
func makeClient() (*client, error) {
c, err := acme.NewClient(os.Getenv("DIRECTORY"))
if err != nil {
return nil, fmt.Errorf("Error connecting to acme directory: %v", err)
}
privKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
return nil, fmt.Errorf("error creating private key: %v", err)
}
account, err := c.NewAccount(privKey, false, true, "mailto:example@letsencrypt.org")
if err != nil {
return nil, fmt.Errorf("error creating new account: %v", err)
}
return &client{account, c}, nil
}
func addHTTP01Response(token, keyAuthorization string) error {
resp, err := http.Post("http://boulder:8055/add-http01", "",
bytes.NewBufferString(fmt.Sprintf(`{
"token": "%s",
"content": "%s"
}`, token, keyAuthorization)))
if err != nil {
return fmt.Errorf("adding http-01 response: %s", err)
}
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("adding http-01 response: status %d", resp.StatusCode)
}
resp.Body.Close()
return nil
}
func delHTTP01Response(token string) error {
resp, err := http.Post("http://boulder:8055/del-http01", "",
bytes.NewBufferString(fmt.Sprintf(`{
"token": "%s"
}`, token)))
if err != nil {
return fmt.Errorf("deleting http-01 response: %s", err)
}
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("deleting http-01 response: status %d", resp.StatusCode)
}
resp.Body.Close()
return nil
}
type issuanceResult struct {
acme.Order
certs []*x509.Certificate
}
func authAndIssue(domains []string) (*issuanceResult, error) {
c, err := makeClient()
if err != nil {
return nil, err
}
var ids []acme.Identifier
for _, domain := range domains {
ids = append(ids, acme.Identifier{Type: "dns", Value: domain})
}
order, err := c.Client.NewOrder(c.Account, ids)
if err != nil {
return nil, fmt.Errorf("making order: %s", err)
}
for _, authUrl := range order.Authorizations {
auth, err := c.Client.FetchAuthorization(c.Account, authUrl)
if err != nil {
return nil, fmt.Errorf("fetching authorization at %s: %s", authUrl, err)
}
chal, ok := auth.ChallengeMap[acme.ChallengeTypeHTTP01]
if !ok {
return nil, fmt.Errorf("no HTTP challenge at %s", authUrl)
}
err = addHTTP01Response(chal.Token, chal.KeyAuthorization)
if err != nil {
return nil, fmt.Errorf("adding HTTP-01 response: %s", err)
}
defer delHTTP01Response(chal.Token)
chal, err = c.Client.UpdateChallenge(c.Account, chal)
if err != nil {
return nil, fmt.Errorf("updating challenge: %s", err)
}
}
csr, err := makeCSR(domains)
if err != nil {
return nil, err
}
order, err = c.Client.FinalizeOrder(c.Account, order, csr)
if err != nil {
return nil, fmt.Errorf("finalizing order: %s", err)
}
certs, err := c.Client.FetchCertificates(c.Account, order.Certificate)
if err != nil {
return nil, fmt.Errorf("fetching certificates: %s", err)
}
return &issuanceResult{order, certs}, nil
}
func makeCSR(domains []string) (*x509.CertificateRequest, error) {
certKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
return nil, fmt.Errorf("generating certificate key: %s", err)
}
csrDer, err := x509.CreateCertificateRequest(rand.Reader, &x509.CertificateRequest{
SignatureAlgorithm: x509.ECDSAWithSHA256,
PublicKeyAlgorithm: x509.ECDSA,
PublicKey: certKey.Public(),
Subject: pkix.Name{CommonName: domains[0]},
DNSNames: domains,
}, certKey)
if err != nil {
return nil, fmt.Errorf("making csr: %s", err)
}
csr, err := x509.ParseCertificateRequest(csrDer)
if err != nil {
return nil, fmt.Errorf("parsing csr: %s", err)
}
return csr, nil
}

View File

@ -0,0 +1,71 @@
// +build integration
package integration
import (
"bytes"
"encoding/base64"
"encoding/json"
"fmt"
"net/http"
"os"
"strings"
"testing"
ocsp_helper "github.com/letsencrypt/boulder/test/ocsp/helper"
)
func TestPrecertificateOCSP(t *testing.T) {
domain := random_domain()
for _, port := range []int{4500, 4501, 4510, 4511} {
url := fmt.Sprintf("http://boulder:%d/add-reject-host", port)
body := []byte(fmt.Sprintf(`{"host": "%s"}`, domain))
resp, err := http.Post(url, "", bytes.NewBuffer(body))
if err != nil {
t.Fatalf("adding reject host: %s", err)
}
if resp.StatusCode != http.StatusOK {
t.Fatalf("adding reject host: %d", resp.StatusCode)
}
resp.Body.Close()
}
os.Setenv("DIRECTORY", "http://boulder:4001/directory")
_, err := authAndIssue([]string{domain})
if err != nil {
if strings.Contains(err.Error(), "urn:ietf:params:acme:error:serverInternal") &&
strings.Contains(err.Error(), "SCT embedding") {
} else {
t.Fatal(err)
}
}
if err == nil {
t.Fatal("expected error issuing for domain rejected by CT servers; got none")
}
resp, err := http.Get("http://boulder:4500/get-rejections")
if err != nil {
t.Fatalf("getting rejections: %s", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
t.Fatalf("getting rejections: status %d", resp.StatusCode)
}
var rejections []string
err = json.NewDecoder(resp.Body).Decode(&rejections)
if err != nil {
t.Fatalf("parsing rejections: %s", err)
}
for _, r := range rejections {
rejectedCertBytes, err := base64.StdEncoding.DecodeString(r)
if err != nil {
t.Fatalf("decoding rejected cert: %s", err)
}
_, err = ocsp_helper.ReqDER(rejectedCertBytes)
if err != nil {
// TODO(#4412): This should become a `t.Errorf`
t.Logf("requesting OCSP for rejected precertificate: %s", err)
}
}
}

View File

@ -96,7 +96,11 @@ func Req(fileName string) (*ocsp.Response, error) {
if err != nil {
return nil, err
}
cert, err := parse(contents)
return ReqDER(contents)
}
func ReqDER(der []byte) (*ocsp.Response, error) {
cert, err := parse(der)
if err != nil {
return nil, fmt.Errorf("parsing certificate: %s", err)
}

3
vendor/github.com/eggsampler/acme/v2/.gitignore generated vendored Normal file
View File

@ -0,0 +1,3 @@
.idea/
*.out
coverage_*.txt

18
vendor/github.com/eggsampler/acme/v2/.travis.yml generated vendored Normal file
View File

@ -0,0 +1,18 @@
language: go
go:
- "1.x"
sudo: required
services:
- docker
script:
- unset TRAVIS_GO_VERSION
- make clean
- make pebble
- make boulder
after_success:
- bash <(curl -s https://codecov.io/bash)

21
vendor/github.com/eggsampler/acme/v2/LICENSE generated vendored Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2018 Isaac
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

59
vendor/github.com/eggsampler/acme/v2/Makefile generated vendored Normal file
View File

@ -0,0 +1,59 @@
.PHONY: test pebble pebble_setup pebble_start pebble_wait pebble_stop boulder boulder_setup boulder_start boulder_stop
GOPATH ?= $(HOME)/go
BOULDER_PATH ?= $(GOPATH)/src/github.com/letsencrypt/boulder
PEBBLE_PATH ?= $(GOPATH)/src/github.com/letsencrypt/pebble
TEST_PATH ?= github.com/eggsampler/acme
# tests the code against a running ca instance
test:
$(eval COVERAGE = coverage_$(strip $(shell ls coverage* 2>/dev/null | wc -l)).txt)
-go clean -testcache
go test -race -coverprofile=$(COVERAGE) -covermode=atomic $(TEST_PATH)
clean:
rm -f coverage_*.txt
pebble: pebble_setup pebble_start pebble_wait test pebble_stop
pebble_setup:
mkdir -p $(PEBBLE_PATH)
git clone --depth 1 https://github.com/letsencrypt/pebble.git $(PEBBLE_PATH) \
|| (cd $(PEBBLE_PATH); git checkout -f master && git reset --hard HEAD && git pull -q)
# runs an instance of pebble using docker
pebble_start:
docker-compose -f $(PEBBLE_PATH)/docker-compose.yml up -d
# waits until pebble responds
pebble_wait:
while ! wget --delete-after -q --no-check-certificate "https://localhost:14000/dir" ; do sleep 1 ; done
# stops the running pebble instance
pebble_stop:
docker-compose -f $(PEBBLE_PATH)/docker-compose.yml down
boulder: boulder_setup boulder_start boulder_wait test boulder_stop
# NB: this edits docker-compose.yml
boulder_setup:
mkdir -p $(BOULDER_PATH)
git clone --depth 1 https://github.com/letsencrypt/boulder.git $(BOULDER_PATH) \
|| (cd $(BOULDER_PATH); git checkout -f master && git reset --hard HEAD && git pull -q)
# runs an instance of boulder
boulder_start:
docker-compose -f $(BOULDER_PATH)/docker-compose.yml up -d
# waits until boulder responds
boulder_wait:
while ! wget --delete-after -q --no-check-certificate "http://localhost:4001/directory" ; do sleep 1 ; done
# stops the running docker instance
boulder_stop:
docker-compose -f $(BOULDER_PATH)/docker-compose.yml down

25
vendor/github.com/eggsampler/acme/v2/README.md generated vendored Normal file
View File

@ -0,0 +1,25 @@
# eggsampler/acme
[![GoDoc](https://godoc.org/github.com/eggsampler/acme?status.svg)](https://godoc.org/github.com/eggsampler/acme) [![Build Status](https://travis-ci.com/eggsampler/acme.svg?branch=master)](https://travis-ci.com/eggsampler/acme) [![codecov.io](https://codecov.io/gh/eggsampler/acme/branch/master/graph/badge.svg)](https://codecov.io/gh/eggsampler/acme/branch/master)
## About
`eggsampler/acme` is a Go client library implementation for [RFC8555](https://tools.ietf.org/html/rfc8555) (previously ACME), specifically for use with the [Let's Encrypt](https://letsencrypt.org/) service.
The library is designed to provide a zero external dependency wrapper over exposed directory endpoints and provide objects in easy to use structures.
## Example
A simple [certbot](https://certbot.eff.org/)-like example is provided in the examples/certbot directory. This code demonstrates account registration, new order submission, fulfilling challenges, finalising an order and fetching the issued certificate chain.
An example of how to use the autocert package is also provided in examples/autocert.
## Tests
The tests can be run against an instance of [boulder](https://github.com/letsencrypt/boulder) or [pebble](https://github.com/letsencrypt/pebble).
Challenge fulfilment is designed to use the new `challtestsrv` server present inside boulder and pebble which responds to dns queries and challenges as required.
To run tests against an already running instance of boulder or pebble, use the `test` target in the Makefile.
Some convenience targets for launching pebble/boulder using their respective docker compose files have also been included in the Makefile.

35
vendor/github.com/eggsampler/acme/v2/THIRD-PARTY generated vendored Normal file
View File

@ -0,0 +1,35 @@
This document contains Third Party Software Notices and/or Additional Terms and Conditions for licensed third party software components included within this product.
==
https://github.com/golang/crypto/blob/master/acme/jws.go
https://github.com/golang/crypto/blob/master/acme/jws_test.go
(with modifications)
Copyright (c) 2009 The Go Authors. All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following disclaimer
in the documentation and/or other materials provided with the
distribution.
* Neither the name of Google Inc. nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

124
vendor/github.com/eggsampler/acme/v2/account.go generated vendored Normal file
View File

@ -0,0 +1,124 @@
package acme
import (
"net/http"
"fmt"
"crypto"
"encoding/json"
)
// NewAccount registers a new account with the acme service
func (c Client) NewAccount(privateKey crypto.Signer, onlyReturnExisting, termsOfServiceAgreed bool, contact ...string) (Account, error) {
if contact == nil {
// workaround for json marshalling {"contact":null}
contact = []string{}
// should now be {"contact":[]}
}
newAccountReq := struct {
OnlyReturnExisting bool `json:"onlyReturnExisting"`
TermsOfServiceAgreed bool `json:"termsOfServiceAgreed"`
Contact []string `json:"contact"`
}{
OnlyReturnExisting: onlyReturnExisting,
TermsOfServiceAgreed: termsOfServiceAgreed,
Contact: contact,
}
account := Account{}
resp, err := c.post(c.dir.NewAccount, "", privateKey, newAccountReq, &account, http.StatusOK, http.StatusCreated)
if err != nil {
return account, err
}
account.URL = resp.Header.Get("Location")
account.PrivateKey = privateKey
if account.Thumbprint == "" {
account.Thumbprint, err = JWKThumbprint(account.PrivateKey.Public())
if err != nil {
return account, fmt.Errorf("acme: error computing account thumbprint: %v", err)
}
}
return account, nil
}
// UpdateAccount updates an existing account with the acme service.
func (c Client) UpdateAccount(account Account, termsOfServiceAgreed bool, contact ...string) (Account, error) {
if contact == nil {
// workaround for json marshalling {"contact":null}
contact = []string{}
// should now be {"contact":[]}
}
updateAccountReq := struct {
TermsOfServiceAgreed bool `json:"termsOfServiceAgreed"`
Contact []string `json:"contact"`
}{
TermsOfServiceAgreed: termsOfServiceAgreed,
Contact: contact,
}
_, err := c.post(account.URL, account.URL, account.PrivateKey, updateAccountReq, &account, http.StatusOK)
if err != nil {
return account, err
}
if account.Thumbprint == "" {
account.Thumbprint, err = JWKThumbprint(account.PrivateKey.Public())
if err != nil {
return account, fmt.Errorf("acme: error computing account thumbprint: %v", err)
}
}
return account, nil
}
// AccountKeyChange rolls over an account to a new key.
func (c Client) AccountKeyChange(account Account, newPrivateKey crypto.Signer) (Account, error) {
if c.dir.KeyChange == "" {
return account, ErrUnsupported
}
oldJwkKeyPub, err := jwkEncode(account.PrivateKey.Public())
if err != nil {
return account, fmt.Errorf("acme: error encoding new private key: %v", err)
}
keyChangeReq := struct {
Account string `json:"account"`
OldKey json.RawMessage `json:"oldKey"`
}{
Account: account.URL,
OldKey: []byte(oldJwkKeyPub),
}
innerJws, err := jwsEncodeJSON(keyChangeReq, newPrivateKey, c.dir.KeyChange, "", "")
if err != nil {
return account, fmt.Errorf("acme: error encoding inner jws: %v", err)
}
if _, err := c.post(c.dir.KeyChange, account.URL, account.PrivateKey, json.RawMessage(innerJws), nil, http.StatusOK); err != nil {
return account, err
}
account.PrivateKey = newPrivateKey
return account, nil
}
// DeactivateAccount deactivates a given account.
func (c Client) DeactivateAccount(account Account) (Account, error) {
deactivateReq := struct {
Status string `json:"status"`
}{
Status: "deactivated",
}
_, err := c.post(account.URL, account.URL, account.PrivateKey, deactivateReq, &account, http.StatusOK)
return account, err
}

256
vendor/github.com/eggsampler/acme/v2/acme.go generated vendored Normal file
View File

@ -0,0 +1,256 @@
package acme
import (
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"time"
"regexp"
"strings"
"bytes"
"crypto"
"errors"
)
const (
// LetsEncryptProduction holds the production directory url
LetsEncryptProduction = "https://acme-v02.api.letsencrypt.org/directory"
// LetsEncryptStaging holds the staging directory url
LetsEncryptStaging = "https://acme-staging-v02.api.letsencrypt.org/directory"
userAgentString = "eggsampler-acme/1.0 Go-http-client/1.1"
)
// NewClient creates a new acme client given a valid directory url.
func NewClient(directoryURL string, options ...OptionFunc) (Client, error) {
httpClient := http.DefaultClient
// Set a default http timeout of 60 seconds
// can be overridden via OptionFunc eg: acme.NewClient(url, WithHTTPTimeout(10 * time.Second))
httpClient.Timeout = 60 * time.Second
acmeClient := Client{
httpClient: httpClient,
nonces: &nonceStack{},
retryCount: 5,
}
acmeClient.dir.URL = directoryURL
for _, opt := range options {
if err := opt(&acmeClient); err != nil {
return acmeClient, fmt.Errorf("acme: error setting option: %v", err)
}
}
if _, err := acmeClient.get(directoryURL, &acmeClient.dir, http.StatusOK); err != nil {
return acmeClient, err
}
return acmeClient, nil
}
// The directory object returned by the client connecting to a directory url.
func (c Client) Directory() Directory {
return c.dir
}
// Helper function to get the poll interval and poll timeout, defaulting if 0
func (c Client) getPollingDurations() (time.Duration, time.Duration) {
pollInterval := c.PollInterval
if pollInterval == 0 {
pollInterval = 500 * time.Millisecond
}
pollTimeout := c.PollTimeout
if pollTimeout == 0 {
pollTimeout = 30 * time.Second
}
return pollInterval, pollTimeout
}
// Helper function to have a central point for performing http requests.
// Stores any returned nonces in the stack.
func (c Client) do(req *http.Request, addNonce bool) (*http.Response, error) {
// identifier for this client, as well as the default go user agent
if c.userAgentSuffix != "" {
req.Header.Set("User-Agent", userAgentString+" "+c.userAgentSuffix)
} else {
req.Header.Set("User-Agent", userAgentString)
}
if c.acceptLanguage != "" {
req.Header.Set("Accept-Language", c.acceptLanguage)
}
resp, err := c.httpClient.Do(req)
if err != nil {
return resp, err
}
if addNonce {
c.nonces.push(resp.Header.Get("Replay-Nonce"))
}
return resp, nil
}
// Helper function to perform an http get request and read the body.
func (c Client) getRaw(url string, expectedStatus ...int) (*http.Response, []byte, error) {
req, err := http.NewRequest(http.MethodGet, url, nil)
if err != nil {
return nil, nil, fmt.Errorf("acme: error creating request: %v", err)
}
resp, err := c.do(req, true)
if err != nil {
return resp, nil, fmt.Errorf("acme: error fetching response: %v", err)
}
defer resp.Body.Close()
if err := checkError(resp, expectedStatus...); err != nil {
return resp, nil, err
}
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return resp, body, fmt.Errorf("acme: error reading response body: %v", err)
}
return resp, body, nil
}
// Helper function for performing a http get on an acme resource.
func (c Client) get(url string, out interface{}, expectedStatus ...int) (*http.Response, error) {
resp, body, err := c.getRaw(url, expectedStatus...)
if err != nil {
return resp, err
}
if len(body) > 0 && out != nil {
if err := json.Unmarshal(body, out); err != nil {
return resp, fmt.Errorf("acme: error parsing response body: %v", err)
}
}
return resp, nil
}
func (c Client) nonce() (string, error) {
nonce := c.nonces.pop()
if nonce != "" {
return nonce, nil
}
if c.dir.NewNonce == "" {
return "", errors.New("acme: no new nonce url")
}
req, err := http.NewRequest("HEAD", c.dir.NewNonce, nil)
if err != nil {
return "", fmt.Errorf("acme: error creating new nonce request: %v", err)
}
resp, err := c.do(req, false)
if err != nil {
return "", fmt.Errorf("acme: error fetching new nonce: %v", err)
}
nonce = resp.Header.Get("Replay-Nonce")
return nonce, nil
}
// Helper function to perform an http post request and read the body.
// Will attempt to retry if error is badNonce
func (c Client) postRaw(retryCount int, requestURL, keyID string, privateKey crypto.Signer, payload interface{}, expectedStatus []int) (*http.Response, []byte, error) {
nonce, err := c.nonce()
if err != nil {
return nil, nil, err
}
data, err := jwsEncodeJSON(payload, privateKey, requestURL, keyID, nonce)
if err != nil {
return nil, nil, fmt.Errorf("acme: error encoding json payload: %v", err)
}
req, err := http.NewRequest(http.MethodPost, requestURL, bytes.NewReader(data))
if err != nil {
return nil, nil, fmt.Errorf("acme: error creating request: %v", err)
}
req.Header.Set("Content-Type", "application/jose+json")
resp, err := c.do(req, true)
if err != nil {
return resp, nil, fmt.Errorf("acme: error sending request: %v", err)
}
defer resp.Body.Close()
if err := checkError(resp, expectedStatus...); err != nil {
prob, ok := err.(Problem)
if !ok {
// don't retry for an error we don't know about
return resp, nil, err
}
if retryCount >= c.retryCount {
// don't attempt to retry if too many retries
return resp, nil, err
}
if strings.HasSuffix(prob.Type, ":badNonce") {
// only retry if error is badNonce
return c.postRaw(retryCount+1, requestURL, keyID, privateKey, payload, expectedStatus)
}
return resp, nil, err
}
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return resp, body, fmt.Errorf("acme: error reading response body: %v", err)
}
return resp, body, nil
}
// Helper function for performing a http post to an acme resource.
func (c Client) post(requestURL, keyID string, privateKey crypto.Signer, payload interface{}, out interface{}, expectedStatus ...int) (*http.Response, error) {
resp, body, err := c.postRaw(0, requestURL, keyID, privateKey, payload, expectedStatus)
if err != nil {
return resp, err
}
if len(body) > 0 && out != nil {
if err := json.Unmarshal(body, out); err != nil {
return resp, fmt.Errorf("acme: error parsing response: %v - %s", err, string(body))
}
}
return resp, nil
}
var regLink = regexp.MustCompile(`<(.+?)>;\s*rel="(.+?)"`)
// Fetches a http Link header from a http response
func fetchLink(resp *http.Response, wantedLink string) string {
if resp == nil {
return ""
}
linkHeader := resp.Header["Link"]
if len(linkHeader) == 0 {
return ""
}
for _, l := range linkHeader {
matches := regLink.FindAllStringSubmatch(l, -1)
for _, m := range matches {
if len(m) != 3 {
continue
}
if m[2] == wantedLink {
return m[1]
}
}
}
return ""
}

43
vendor/github.com/eggsampler/acme/v2/authorization.go generated vendored Normal file
View File

@ -0,0 +1,43 @@
package acme
import "net/http"
// FetchAuthorization fetches an authorization from an authorization url provided in an order.
func (c Client) FetchAuthorization(account Account, authURL string) (Authorization, error) {
authResp := Authorization{}
_, err := c.post(authURL, account.URL, account.PrivateKey, "", &authResp, http.StatusOK)
if err != nil {
return authResp, err
}
for i := 0; i < len(authResp.Challenges); i++ {
if authResp.Challenges[i].KeyAuthorization == "" {
authResp.Challenges[i].KeyAuthorization = authResp.Challenges[i].Token + "." + account.Thumbprint
}
}
authResp.ChallengeMap = map[string]Challenge{}
authResp.ChallengeTypes = []string{}
for _, c := range authResp.Challenges {
authResp.ChallengeMap[c.Type] = c
authResp.ChallengeTypes = append(authResp.ChallengeTypes, c.Type)
}
authResp.URL = authURL
return authResp, nil
}
// DeactivateAuthorization deactivate a provided authorization url from an order.
func (c Client) DeactivateAuthorization(account Account, authURL string) (Authorization, error) {
deactivateReq := struct {
Status string `json:"status"`
}{
Status: "deactivated",
}
deactivateResp := Authorization{}
_, err := c.post(authURL, account.URL, account.PrivateKey, deactivateReq, &deactivateResp, http.StatusOK)
return deactivateResp, err
}

430
vendor/github.com/eggsampler/acme/v2/autocert.go generated vendored Normal file
View File

@ -0,0 +1,430 @@
package acme
// Similar to golang.org/x/crypto/acme/autocert
import (
"context"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/tls"
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"errors"
"fmt"
"io/ioutil"
"net/http"
"path"
"strings"
"sync"
)
// HostCheck function prototype to implement for checking hosts against before issuing certificates
type HostCheck func(host string) error
// WhitelistHosts implements a simple whitelist HostCheck
func WhitelistHosts(hosts ...string) HostCheck {
m := map[string]bool{}
for _, v := range hosts {
m[v] = true
}
return func(host string) error {
if !m[host] {
return errors.New("autocert: host not whitelisted")
}
return nil
}
}
// AutoCert is a stateful certificate manager for issuing certificates on connecting hosts
type AutoCert struct {
// Acme directory Url
// If nil, uses `LetsEncryptStaging`
DirectoryURL string
// Options contains the options used for creating the acme client
Options []OptionFunc
// A function to check whether a host is allowed or not
// If nil, all hosts allowed
// Use `WhitelistHosts(hosts ...string)` for a simple white list of hostnames
HostCheck HostCheck
// Cache dir to store account data and certificates
// If nil, does not write cache data to file
CacheDir string
// When using a staging environment, include a root certificate for verification purposes
RootCert string
// Called before updating challenges
PreUpdateChallengeHook func(Account, Challenge)
// Mapping of token -> keyauth
// Protected by a mutex, but not rwmutex because tokens are deleted once read
tokensLock sync.RWMutex
tokens map[string][]byte
// Mapping of cache key -> value
cacheLock sync.Mutex
cache map[string][]byte
// read lock around getting existing certs
// write lock around issuing new certificate
certLock sync.RWMutex
client Client
}
// HTTPHandler Wraps a handler and provides serving of http-01 challenge tokens from /.well-known/acme-challenge/
// If handler is nil, will redirect all traffic otherwise to https
func (m *AutoCert) HTTPHandler(handler http.Handler) http.Handler {
if handler == nil {
handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, "https://"+r.Host+r.URL.RequestURI(), http.StatusMovedPermanently)
})
}
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if !strings.HasPrefix(r.URL.Path, "/.well-known/acme-challenge/") {
handler.ServeHTTP(w, r)
return
}
if err := m.checkHost(r.Host); err != nil {
http.Error(w, err.Error(), http.StatusForbidden)
return
}
token := path.Base(r.URL.Path)
m.tokensLock.RLock()
defer m.tokensLock.RUnlock()
keyAuth := m.tokens[token]
if len(keyAuth) == 0 {
http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
return
}
w.Write(keyAuth)
})
}
// GetCertificate implements a tls.Config.GetCertificate hook
func (m *AutoCert) GetCertificate(hello *tls.ClientHelloInfo) (*tls.Certificate, error) {
name := strings.TrimSuffix(hello.ServerName, ".")
if name == "" {
return nil, errors.New("autocert: missing server name")
}
if !strings.Contains(strings.Trim(name, "."), ".") {
return nil, errors.New("autocert: server name component count invalid")
}
if strings.ContainsAny(name, `/\`) {
return nil, errors.New("autocert: server name contains invalid character")
}
// check the hostname is allowed
if err := m.checkHost(name); err != nil {
return nil, err
}
// check if there's an existing cert
m.certLock.RLock()
existingCert := m.getExistingCert(name)
m.certLock.RUnlock()
if existingCert != nil {
return existingCert, nil
}
// if not, attempt to issue a new cert
m.certLock.Lock()
defer m.certLock.Unlock()
return m.issueCert(name)
}
func (m *AutoCert) getDirectoryURL() string {
if m.DirectoryURL != "" {
return m.DirectoryURL
}
return LetsEncryptStaging
}
func (m *AutoCert) getCache(keys ...string) []byte {
key := strings.Join(keys, "-")
m.cacheLock.Lock()
defer m.cacheLock.Unlock()
b := m.cache[key]
if len(b) > 0 {
return b
}
if m.CacheDir == "" {
return nil
}
b, _ = ioutil.ReadFile(path.Join(m.CacheDir, key))
if len(b) == 0 {
return nil
}
if m.cache == nil {
m.cache = map[string][]byte{}
}
m.cache[key] = b
return b
}
func (m *AutoCert) putCache(data []byte, keys ...string) context.Context {
ctx, cancel := context.WithCancel(context.Background())
key := strings.Join(keys, "-")
m.cacheLock.Lock()
defer m.cacheLock.Unlock()
if m.cache == nil {
m.cache = map[string][]byte{}
}
m.cache[key] = data
if m.CacheDir == "" {
cancel()
return ctx
}
go func() {
ioutil.WriteFile(path.Join(m.CacheDir, key), data, 0700)
cancel()
}()
return ctx
}
func (m *AutoCert) checkHost(name string) error {
if m.HostCheck == nil {
return nil
}
return m.HostCheck(name)
}
func (m *AutoCert) getExistingCert(name string) *tls.Certificate {
// check for a stored cert
certData := m.getCache("cert", name)
if len(certData) == 0 {
// no cert
return nil
}
privBlock, pubData := pem.Decode(certData)
if len(pubData) == 0 {
// no public key data (cert/issuer), ignore
return nil
}
// decode pub chain
var pubDER [][]byte
var pub []byte
for len(pubData) > 0 {
var b *pem.Block
b, pubData = pem.Decode(pubData)
if b == nil {
break
}
pubDER = append(pubDER, b.Bytes)
pub = append(pub, b.Bytes...)
}
if len(pubData) > 0 {
// leftover data in file - possibly corrupt, ignore
return nil
}
certs, err := x509.ParseCertificates(pub)
if err != nil {
// bad certificates, ignore
return nil
}
leaf := certs[0]
// add any intermediate certs if present
var intermediates *x509.CertPool
if len(certs) > 1 {
intermediates = x509.NewCertPool()
for i := 1; i < len(certs); i++ {
intermediates.AddCert(certs[i])
}
}
// add a root certificate if present
var roots *x509.CertPool
if m.RootCert != "" {
roots = x509.NewCertPool()
rootBlock, _ := pem.Decode([]byte(m.RootCert))
rootCert, err := x509.ParseCertificate(rootBlock.Bytes)
if err != nil {
return nil
}
roots.AddCert(rootCert)
}
if _, err := leaf.Verify(x509.VerifyOptions{DNSName: name, Intermediates: intermediates, Roots: roots}); err != nil {
// invalid certificates , ignore
return nil
}
privKey, err := x509.ParseECPrivateKey(privBlock.Bytes)
if err != nil {
// invalid private key, ignore
return nil
}
return &tls.Certificate{
Certificate: pubDER,
PrivateKey: privKey,
Leaf: leaf,
}
}
func (m *AutoCert) issueCert(domainName string) (*tls.Certificate, error) {
// attempt to load an existing account key
var privKey *ecdsa.PrivateKey
if keyData := m.getCache("account"); len(keyData) > 0 {
block, _ := pem.Decode(keyData)
x509Encoded := block.Bytes
privKey, _ = x509.ParseECPrivateKey(x509Encoded)
}
// otherwise generate a new one
if privKey == nil {
var err error
privKey, err = ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
return nil, fmt.Errorf("autocert: error generating new account key: %v", err)
}
x509Encoded, _ := x509.MarshalECPrivateKey(privKey)
pemEncoded := pem.EncodeToMemory(&pem.Block{Type: "EC PRIVATE KEY", Bytes: x509Encoded})
m.putCache(pemEncoded, "account")
}
// create a new client if one doesn't exist
if m.client.Directory().URL == "" {
var err error
m.client, err = NewClient(m.getDirectoryURL(), m.Options...)
if err != nil {
return nil, err
}
}
// create/fetch acme account
account, err := m.client.NewAccount(privKey, false, true)
if err != nil {
return nil, fmt.Errorf("autocert: error creating/fetching account: %v", err)
}
// start a new order process
order, err := m.client.NewOrderDomains(account, domainName)
if err != nil {
return nil, fmt.Errorf("autocert: error creating new order for domain %s: %v", domainName, err)
}
// loop through each of the provided authorization Urls
for _, authURL := range order.Authorizations {
auth, err := m.client.FetchAuthorization(account, authURL)
if err != nil {
return nil, fmt.Errorf("autocert: error fetching authorization Url %q: %v", authURL, err)
}
if auth.Status == "valid" {
continue
}
chal, ok := auth.ChallengeMap[ChallengeTypeHTTP01]
if !ok {
return nil, fmt.Errorf("autocert: unable to find http-01 challenge for auth %s, Url: %s", auth.Identifier.Value, authURL)
}
m.tokensLock.Lock()
if m.tokens == nil {
m.tokens = map[string][]byte{}
}
m.tokens[chal.Token] = []byte(chal.KeyAuthorization)
m.tokensLock.Unlock()
if m.PreUpdateChallengeHook != nil {
m.PreUpdateChallengeHook(account, chal)
}
chal, err = m.client.UpdateChallenge(account, chal)
if err != nil {
return nil, fmt.Errorf("autocert: error updating authorization %s challenge (Url: %s) : %v", auth.Identifier.Value, authURL, err)
}
m.tokensLock.Lock()
delete(m.tokens, chal.Token)
m.tokensLock.Unlock()
}
// generate private key for cert
certKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
return nil, fmt.Errorf("autocert: error generating certificate key for %s: %v", domainName, err)
}
certKeyEnc, err := x509.MarshalECPrivateKey(certKey)
if err != nil {
return nil, fmt.Errorf("autocert: error encoding certificate key for %s: %v", domainName, err)
}
certKeyPem := pem.EncodeToMemory(&pem.Block{
Type: "EC PRIVATE KEY",
Bytes: certKeyEnc,
})
// create the new csr template
tpl := &x509.CertificateRequest{
SignatureAlgorithm: x509.ECDSAWithSHA256,
PublicKeyAlgorithm: x509.ECDSA,
PublicKey: certKey.Public(),
Subject: pkix.Name{CommonName: domainName},
DNSNames: []string{domainName},
}
csrDer, err := x509.CreateCertificateRequest(rand.Reader, tpl, certKey)
if err != nil {
return nil, fmt.Errorf("autocert: error creating certificate request for %s: %v", domainName, err)
}
csr, err := x509.ParseCertificateRequest(csrDer)
if err != nil {
return nil, fmt.Errorf("autocert: error parsing certificate request for %s: %v", domainName, err)
}
// finalize the order with the acme server given a csr
order, err = m.client.FinalizeOrder(account, order, csr)
if err != nil {
return nil, fmt.Errorf("autocert: error finalizing order for %s: %v", domainName, err)
}
// fetch the certificate chain from the finalized order provided by the acme server
certs, err := m.client.FetchCertificates(account, order.Certificate)
if err != nil {
return nil, fmt.Errorf("autocert: error fetching order certificates for %s: %v", domainName, err)
}
certPem := certKeyPem
var certDer [][]byte
for _, c := range certs {
b := pem.EncodeToMemory(&pem.Block{
Type: "CERTIFICATE",
Bytes: c.Raw,
})
certPem = append(certPem, b...)
certDer = append(certDer, c.Raw)
}
m.putCache(certPem, "cert", domainName)
return m.getExistingCert(domainName), nil
}

67
vendor/github.com/eggsampler/acme/v2/certificate.go generated vendored Normal file
View File

@ -0,0 +1,67 @@
package acme
import (
"crypto"
"crypto/x509"
"encoding/base64"
"encoding/pem"
"fmt"
"net/http"
)
// FetchCertificates downloads a certificate chain from a url given in an order certificate.
func (c Client) FetchCertificates(account Account, certificateURL string) ([]*x509.Certificate, error) {
resp, body, err := c.postRaw(0, certificateURL, account.URL, account.PrivateKey, "", []int{http.StatusOK})
if err != nil {
return nil, err
}
var certs []*x509.Certificate
for {
var p *pem.Block
p, body = pem.Decode(body)
if p == nil {
break
}
cert, err := x509.ParseCertificate(p.Bytes)
if err != nil {
return certs, fmt.Errorf("acme: error parsing certificate: %v", err)
}
certs = append(certs, cert)
}
up := fetchLink(resp, "up")
if up != "" {
upCerts, err := c.FetchCertificates(account, up)
if err != nil {
return certs, fmt.Errorf("acme: error fetching up cert: %v", err)
}
if len(upCerts) != 0 {
certs = append(certs, upCerts...)
}
}
return certs, nil
}
// RevokeCertificate revokes a given certificate given the certificate key or account key, and a reason.
func (c Client) RevokeCertificate(account Account, cert *x509.Certificate, key crypto.Signer, reason int) error {
revokeReq := struct {
Certificate string `json:"certificate"`
Reason int `json:"reason"`
}{
Certificate: base64.RawURLEncoding.EncodeToString(cert.Raw),
Reason: reason,
}
kid := ""
if key == account.PrivateKey {
kid = account.URL
}
if _, err := c.post(c.dir.RevokeCert, kid, key, revokeReq, nil, http.StatusOK); err != nil {
return err
}
return nil
}

102
vendor/github.com/eggsampler/acme/v2/challenge.go generated vendored Normal file
View File

@ -0,0 +1,102 @@
package acme
import (
"crypto/sha256"
"encoding/base64"
"errors"
"fmt"
"net/http"
"time"
)
// EncodeDNS01KeyAuthorization encodes a key authorization and provides a value to be put in the TXT record for the _acme-challenge DNS entry.
func EncodeDNS01KeyAuthorization(keyAuth string) string {
h := sha256.Sum256([]byte(keyAuth))
return base64.RawURLEncoding.EncodeToString(h[:])
}
// Helper function to determine whether a challenge is "finished" by it's status.
func checkUpdatedChallengeStatus(challenge Challenge) (bool, error) {
switch challenge.Status {
case "pending":
// Challenge objects are created in the "pending" state.
// TODO: https://github.com/letsencrypt/boulder/issues/3346
// return true, errors.New("acme: unexpected 'pending' challenge state")
return false, nil
case "processing":
// They transition to the "processing" state when the client responds to the
// challenge and the server begins attempting to validate that the client has completed the challenge.
return false, nil
case "valid":
// If validation is successful, the challenge moves to the "valid" state
return true, nil
case "invalid":
// if there is an error, the challenge moves to the "invalid" state.
if challenge.Error.Type != "" {
return true, challenge.Error
}
return true, errors.New("acme: challenge is invalid, no error provided")
default:
return true, fmt.Errorf("acme: unknown challenge status: %s", challenge.Status)
}
}
// UpdateChallenge responds to a challenge to indicate to the server to complete the challenge.
func (c Client) UpdateChallenge(account Account, challenge Challenge) (Challenge, error) {
resp, err := c.post(challenge.URL, account.URL, account.PrivateKey, struct{}{}, &challenge, http.StatusOK)
if err != nil {
return challenge, err
}
if loc := resp.Header.Get("Location"); loc != "" {
challenge.URL = loc
}
challenge.AuthorizationURL = fetchLink(resp, "up")
if finished, err := checkUpdatedChallengeStatus(challenge); finished {
return challenge, err
}
pollInterval, pollTimeout := c.getPollingDurations()
end := time.Now().Add(pollTimeout)
for {
if time.Now().After(end) {
return challenge, errors.New("acme: challenge update timeout")
}
time.Sleep(pollInterval)
resp, err := c.post(challenge.URL, account.URL, account.PrivateKey, "", &challenge, http.StatusOK)
if err != nil {
// i don't think it's worth exiting the loop on this error
// it could just be connectivity issue that's resolved before the timeout duration
continue
}
if loc := resp.Header.Get("Location"); loc != "" {
challenge.URL = loc
}
challenge.AuthorizationURL = fetchLink(resp, "up")
if finished, err := checkUpdatedChallengeStatus(challenge); finished {
return challenge, err
}
}
}
// FetchChallenge fetches an existing challenge from the given url.
func (c Client) FetchChallenge(account Account, challengeURL string) (Challenge, error) {
challenge := Challenge{}
resp, err := c.post(challengeURL, account.URL, account.PrivateKey, "", &challenge, http.StatusOK)
if err != nil {
return challenge, err
}
challenge.URL = resp.Header.Get("Location")
challenge.AuthorizationURL = fetchLink(resp, "up")
return challenge, nil
}

3
vendor/github.com/eggsampler/acme/v2/go.mod generated vendored Normal file
View File

@ -0,0 +1,3 @@
module github.com/eggsampler/acme/v2
go 1.12

167
vendor/github.com/eggsampler/acme/v2/jws.go generated vendored Normal file
View File

@ -0,0 +1,167 @@
// Copyright 2015 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the THIRD-PARTY file.
package acme
import (
"crypto"
"crypto/ecdsa"
"crypto/rand"
"crypto/rsa"
"crypto/sha256"
_ "crypto/sha512" // need for EC keys
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"math/big"
)
var errUnsupportedKey = errors.New("acme: unknown key type; only RSA and ECDSA are supported")
// jwsEncodeJSON signs claimset using provided key and a nonce.
// The result is serialized in JSON format.
// See https://tools.ietf.org/html/rfc7515#section-7.
func jwsEncodeJSON(claimset interface{}, key crypto.Signer, requestURL, keyID, nonce string) ([]byte, error) {
jwk, err := jwkEncode(key.Public())
if err != nil {
return nil, err
}
alg, sha := jwsHasher(key)
if alg == "" || !sha.Available() {
return nil, errUnsupportedKey
}
var phead string
if keyID != "" {
phead = fmt.Sprintf(`{"alg":%q,"kid":%q,"nonce":%q,"url":%q}`, alg, keyID, nonce, requestURL)
} else {
phead = fmt.Sprintf(`{"alg":%q,"jwk":%s,"nonce":%q,"url":%q}`, alg, jwk, nonce, requestURL)
}
phead = base64.RawURLEncoding.EncodeToString([]byte(phead))
var payload string
csString, ok := claimset.(string)
if !ok || csString != "" {
cs, err := json.Marshal(claimset)
if err != nil {
return nil, err
}
payload = base64.RawURLEncoding.EncodeToString(cs)
}
hash := sha.New()
hash.Write([]byte(phead + "." + payload))
sig, err := jwsSign(key, sha, hash.Sum(nil))
if err != nil {
return nil, err
}
enc := struct {
Protected string `json:"protected"`
Payload string `json:"payload"`
Sig string `json:"signature"`
}{
Protected: phead,
Payload: payload,
Sig: base64.RawURLEncoding.EncodeToString(sig),
}
return json.Marshal(&enc)
}
// jwkEncode encodes public part of an RSA or ECDSA key into a JWK.
// The result is also suitable for creating a JWK thumbprint.
// https://tools.ietf.org/html/rfc7517
func jwkEncode(pub crypto.PublicKey) (string, error) {
switch pub := pub.(type) {
case *rsa.PublicKey:
// https://tools.ietf.org/html/rfc7518#section-6.3.1
n := pub.N
e := big.NewInt(int64(pub.E))
// Field order is important.
// See https://tools.ietf.org/html/rfc7638#section-3.3 for details.
return fmt.Sprintf(`{"e":"%s","kty":"RSA","n":"%s"}`,
base64.RawURLEncoding.EncodeToString(e.Bytes()),
base64.RawURLEncoding.EncodeToString(n.Bytes()),
), nil
case *ecdsa.PublicKey:
// https://tools.ietf.org/html/rfc7518#section-6.2.1
p := pub.Curve.Params()
n := p.BitSize / 8
if p.BitSize%8 != 0 {
n++
}
x := pub.X.Bytes()
if n > len(x) {
x = append(make([]byte, n-len(x)), x...)
}
y := pub.Y.Bytes()
if n > len(y) {
y = append(make([]byte, n-len(y)), y...)
}
// Field order is important.
// See https://tools.ietf.org/html/rfc7638#section-3.3 for details.
return fmt.Sprintf(`{"crv":"%s","kty":"EC","x":"%s","y":"%s"}`,
p.Name,
base64.RawURLEncoding.EncodeToString(x),
base64.RawURLEncoding.EncodeToString(y),
), nil
}
return "", errUnsupportedKey
}
// jwsSign signs the digest using the given key.
// It returns ErrUnsupportedKey if the key type is unknown.
// The hash is used only for RSA keys.
func jwsSign(key crypto.Signer, hash crypto.Hash, digest []byte) ([]byte, error) {
switch key := key.(type) {
case *rsa.PrivateKey:
return key.Sign(rand.Reader, digest, hash)
case *ecdsa.PrivateKey:
r, s, err := ecdsa.Sign(rand.Reader, key, digest)
if err != nil {
return nil, err
}
rb, sb := r.Bytes(), s.Bytes()
size := key.Params().BitSize / 8
if size%8 > 0 {
size++
}
sig := make([]byte, size*2)
copy(sig[size-len(rb):], rb)
copy(sig[size*2-len(sb):], sb)
return sig, nil
}
return nil, errUnsupportedKey
}
// jwsHasher indicates suitable JWS algorithm name and a hash function
// to use for signing a digest with the provided key.
// It returns ("", 0) if the key is not supported.
func jwsHasher(key crypto.Signer) (string, crypto.Hash) {
switch key := key.(type) {
case *rsa.PrivateKey:
return "RS256", crypto.SHA256
case *ecdsa.PrivateKey:
switch key.Params().Name {
case "P-256":
return "ES256", crypto.SHA256
case "P-384":
return "ES384", crypto.SHA384
case "P-521":
return "ES512", crypto.SHA512
}
}
return "", 0
}
// JWKThumbprint creates a JWK thumbprint out of pub
// as specified in https://tools.ietf.org/html/rfc7638.
func JWKThumbprint(pub crypto.PublicKey) (string, error) {
jwk, err := jwkEncode(pub)
if err != nil {
return "", err
}
b := sha256.Sum256([]byte(jwk))
return base64.RawURLEncoding.EncodeToString(b[:]), nil
}

45
vendor/github.com/eggsampler/acme/v2/nonce.go generated vendored Normal file
View File

@ -0,0 +1,45 @@
package acme
import (
"sync"
)
// Simple thread-safe stack impl
type nonceStack struct {
lock sync.Mutex
stack []string
}
// Pushes a nonce to the stack.
// Doesn't push empty nonces, or if there's more than 100 nonces on the stack
func (ns *nonceStack) push(v string) {
if v == "" {
return
}
ns.lock.Lock()
defer ns.lock.Unlock()
if len(ns.stack) > 100 {
return
}
ns.stack = append(ns.stack, v)
}
// Pops a nonce from the stack.
// Returns empty string if there are no nonces
func (ns *nonceStack) pop() string {
ns.lock.Lock()
defer ns.lock.Unlock()
n := len(ns.stack)
if n == 0 {
return ""
}
v := ns.stack[n-1]
ns.stack = ns.stack[:n-1]
return v
}

70
vendor/github.com/eggsampler/acme/v2/options.go generated vendored Normal file
View File

@ -0,0 +1,70 @@
package acme
import (
"crypto/tls"
"errors"
"net/http"
"time"
)
// OptionFunc function prototype for passing options to NewClient
type OptionFunc func(client *Client) error
// WithHTTPTimeout sets a timeout on the http client used by the Client
func WithHTTPTimeout(duration time.Duration) OptionFunc {
return func(client *Client) error {
client.httpClient.Timeout = duration
return nil
}
}
// WithInsecureSkipVerify sets InsecureSkipVerify on the http client transport tls client config used by the Client
func WithInsecureSkipVerify() OptionFunc {
return func(client *Client) error {
client.httpClient.Transport = &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true,
},
}
return nil
}
}
// WithUserAgentSuffix appends a user agent suffix for http requests to acme resources
func WithUserAgentSuffix(userAgentSuffix string) OptionFunc {
return func(client *Client) error {
client.userAgentSuffix = userAgentSuffix
return nil
}
}
// WithAcceptLanguage sets an Accept-Language header on http requests
func WithAcceptLanguage(acceptLanguage string) OptionFunc {
return func(client *Client) error {
client.acceptLanguage = acceptLanguage
return nil
}
}
// WithRetryCount sets the number of times the acme client retries when receiving an api error (eg, nonce failures, etc).
// Default: 5
func WithRetryCount(retryCount int) OptionFunc {
return func(client *Client) error {
if retryCount < 1 {
return errors.New("retryCount must be > 0")
}
client.retryCount = retryCount
return nil
}
}
// WithHTTPClient Allows setting a custom http client for acme connections
func WithHTTPClient(httpClient *http.Client) OptionFunc {
return func(client *Client) error {
if httpClient == nil {
return errors.New("client must not be nil")
}
client.httpClient = httpClient
return nil
}
}

141
vendor/github.com/eggsampler/acme/v2/order.go generated vendored Normal file
View File

@ -0,0 +1,141 @@
package acme
import (
"net/http"
"encoding/base64"
"fmt"
"time"
"errors"
"crypto/x509"
)
// NewOrder initiates a new order for a new certificate.
func (c Client) NewOrder(account Account, identifiers []Identifier) (Order, error) {
newOrderReq := struct {
Identifiers []Identifier `json:"identifiers"`
}{
Identifiers: identifiers,
}
newOrderResp := Order{}
resp, err := c.post(c.dir.NewOrder, account.URL, account.PrivateKey, newOrderReq, &newOrderResp, http.StatusCreated)
if err != nil {
return newOrderResp, err
}
newOrderResp.URL = resp.Header.Get("Location")
return newOrderResp, nil
}
// NewOrderDomains is a wrapper for NewOrder(AcmeAccount, []AcmeIdentifiers)
// Creates a dns identifier for each provided domain
func (c Client) NewOrderDomains(account Account, domains ...string) (Order, error) {
if len(domains) == 0 {
return Order{}, errors.New("acme: no domains provided")
}
var ids []Identifier
for _, d := range domains {
ids = append(ids, Identifier{Type: "dns", Value: d})
}
return c.NewOrder(account, ids)
}
// FetchOrder fetches an existing order given an order url.
func (c Client) FetchOrder(account Account, orderURL string) (Order, error) {
orderResp := Order{
URL: orderURL, // boulder response doesn't seem to contain location header for this request
}
_, err := c.post(orderURL, account.URL, account.PrivateKey, "", &orderResp, http.StatusOK)
return orderResp, err
}
// Helper function to determine whether an order is "finished" by it's status.
func checkFinalizedOrderStatus(order Order) (bool, error) {
switch order.Status {
case "invalid":
// "invalid": The certificate will not be issued. Consider this
// order process abandoned.
if order.Error.Type != "" {
return true, order.Error
}
return true, errors.New("acme: finalized order is invalid, no error provided")
case "pending":
// "pending": The server does not believe that the client has
// fulfilled the requirements. Check the "authorizations" array for
// entries that are still pending.
return true, errors.New("acme: authorizations not fulfilled")
case "ready":
// "ready": The server agrees that the requirements have been
// fulfilled, and is awaiting finalization. Submit a finalization
// request.
return true, errors.New("acme: unexpected 'ready' state")
case "processing":
// "processing": The certificate is being issued. Send a GET request
// after the time given in the "Retry-After" header field of the
// response, if any.
return false, nil
case "valid":
// "valid": The server has issued the certificate and provisioned its
// URL to the "certificate" field of the order. Download the
// certificate.
return true, nil
default:
return true, fmt.Errorf("acme: unknown order status: %s", order.Status)
}
}
// FinalizeOrder indicates to the acme server that the client considers an order complete and "finalizes" it.
// If the server believes the authorizations have been filled successfully, a certificate should then be available.
// This function assumes that the order status is "ready".
func (c Client) FinalizeOrder(account Account, order Order, csr *x509.CertificateRequest) (Order, error) {
finaliseReq := struct {
Csr string `json:"csr"`
}{
Csr: base64.RawURLEncoding.EncodeToString(csr.Raw),
}
resp, err := c.post(order.Finalize, account.URL, account.PrivateKey, finaliseReq, &order, http.StatusOK)
if err != nil {
return order, err
}
order.URL = resp.Header.Get("Location")
if finished, err := checkFinalizedOrderStatus(order); finished {
return order, err
}
pollInterval, pollTimeout := c.getPollingDurations()
end := time.Now().Add(pollTimeout)
for {
if time.Now().After(end) {
return order, errors.New("acme: finalized order timeout")
}
time.Sleep(pollInterval)
if _, err := c.post(order.URL, account.URL, account.PrivateKey, "", &order, http.StatusOK); err != nil {
// i dont think it's worth exiting the loop on this error
// it could just be connectivity issue thats resolved before the timeout duration
continue
}
order.URL = resp.Header.Get("Location")
if finished, err := checkFinalizedOrderStatus(order); finished {
return order, err
}
}
}

61
vendor/github.com/eggsampler/acme/v2/problem.go generated vendored Normal file
View File

@ -0,0 +1,61 @@
package acme
import (
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
)
// Problem represents an error returned by an acme server.
type Problem struct {
Status int `json:"status"`
Type string `json:"type"`
Detail string `json:"detail"`
Instance string `json:"instance"`
SubProblems []struct {
Type string `json:"type"`
Detail string `json:"detail"`
Identifier Identifier
} `json:"subproblems"`
}
// Returns a human readable error string.
func (err Problem) Error() string {
s := fmt.Sprintf("acme: error code %d %q: %s", err.Status, err.Type, err.Detail)
if len(err.SubProblems) > 0 {
for _, v := range err.SubProblems {
s += fmt.Sprintf(", problem %q: %s", v.Type, v.Detail)
}
}
if err.Instance != "" {
s += ", url: " + err.Instance
}
return s
}
// Helper function to determine if a response contains an expected status code, or otherwise an error object.
func checkError(resp *http.Response, expectedStatuses ...int) error {
for _, statusCode := range expectedStatuses {
if resp.StatusCode == statusCode {
return nil
}
}
if resp.StatusCode < 400 || resp.StatusCode >= 600 {
return fmt.Errorf("acme: expected status codes: %d, got: %d %s", expectedStatuses, resp.StatusCode, resp.Status)
}
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return fmt.Errorf("acme: error reading error body: %v", err)
}
acmeError := Problem{}
if err := json.Unmarshal(body, &acmeError); err != nil {
return fmt.Errorf("acme: parsing error body: %v - %s", err, string(body))
}
return acmeError
}

155
vendor/github.com/eggsampler/acme/v2/types.go generated vendored Normal file
View File

@ -0,0 +1,155 @@
package acme
import (
"crypto"
"errors"
"net/http"
"time"
)
// Different possible challenge types provided by an ACME server.
const (
ChallengeTypeDNS01 = "dns-01"
ChallengeTypeHTTP01 = "http-01"
ChallengeTypeTLSALPN01 = "tls-alpn-01"
ChallengeTypeTLSSNI01 = "tls-sni-01"
)
// Constants used for certificate revocation, used for RevokeCertificate
const (
ReasonUnspecified = iota // 0
ReasonKeyCompromise // 1
ReasonCaCompromise // 2
ReasonAffiliationChanged // 3
ReasonSuperseded // 4
ReasonCessationOfOperation // 5
ReasonCertificateHold // 6
_ // 7 - Unused
ReasonRemoveFromCRL // 8
ReasonPrivilegeWithdrawn // 9
ReasonAaCompromise // 10
)
var (
ErrUnsupported = errors.New("acme: unsupported")
)
// Directory object as returned from the client's directory url upon creation of client.
type Directory struct {
NewNonce string `json:"newNonce"` // url to new nonce endpoint
NewAccount string `json:"newAccount"` // url to new account endpoint
NewOrder string `json:"newOrder"` // url to new order endpoint
NewAuthz string `json:"newAuthz"` // url to new authz endpoint
RevokeCert string `json:"revokeCert"` // url to revoke cert endpoint
KeyChange string `json:"keyChange"` // url to key change endpoint
// meta object containing directory metadata
Meta struct {
TermsOfService string `json:"termsOfService"`
Website string `json:"website"`
CaaIdentities []string `json:"caaIdentities"`
ExternalAccountRequired bool `json:"externalAccountRequired"`
} `json:"meta"`
// Directory url provided when creating a new acme client.
URL string `json:"-"`
}
// Client structure to interact with an ACME server.
// This is typically how most, if not all, of the communication between the client and server occurs.
type Client struct {
httpClient *http.Client
nonces *nonceStack
dir Directory
userAgentSuffix string
acceptLanguage string
retryCount int
// The amount of total time the Client will wait at most for a challenge to be updated or a certificate to be issued.
// Default 30 seconds if duration is not set or if set to 0.
PollTimeout time.Duration
// The time between checking if a challenge has been updated or a certificate has been issued.
// Default 0.5 seconds if duration is not set or if set to 0.
PollInterval time.Duration
}
// Account structure representing fields in an account object.
type Account struct {
Status string `json:"status"`
Contact []string `json:"contact"`
TermsOfServiceAgreed bool `json:"onlyReturnExisting"`
Orders string `json:"orders"`
// Provided by the Location http header when creating a new account or fetching an existing account.
URL string `json:"-"`
// The private key used to create or fetch the account.
// Not fetched from server.
PrivateKey crypto.Signer `json:"-"`
// SHA-256 digest JWK_Thumbprint of the account key.
// Used in updating challenges, see: https://tools.ietf.org/html/draft-ietf-acme-acme-10#section-8.1
Thumbprint string `json:"-"`
}
// Identifier object used in order and authorization objects
type Identifier struct {
Type string `json:"type"`
Value string `json:"value"`
}
// Order object returned when fetching or creating a new order.
type Order struct {
Status string `json:"status"`
Expires time.Time `json:"expires"`
Identifiers []Identifier `json:"identifiers"`
Authorizations []string `json:"authorizations"`
Error Problem `json:"error"`
Finalize string `json:"finalize"`
Certificate string `json:"certificate"`
// URL for the order object.
// Provided by the rel="Location" Link http header
URL string `json:"-"`
}
// Authorization object returned when fetching an authorization in an order.
type Authorization struct {
Identifier Identifier `json:"identifier"`
Status string `json:"status"`
Expires time.Time `json:"expires"`
Challenges []Challenge `json:"challenges"`
Wildcard bool `json:"wildcard"`
// For convenience access to the provided challenges
ChallengeMap map[string]Challenge `json:"-"`
ChallengeTypes []string `json:"-"`
URL string `json:"-"`
}
// Challenge object fetched in an authorization or directly from the challenge url.
type Challenge struct {
Type string `json:"type"`
URL string `json:"url"`
Status string `json:"status"`
Validated string `json:"validated"`
Error Problem `json:"error"`
// Based on the challenge used
Token string `json:"token"`
KeyAuthorization string `json:"keyAuthorization"`
// Authorization url provided by the rel="up" Link http header
AuthorizationURL string `json:"-"`
}
// OrderList of challenge objects.
type OrderList struct {
Orders []string `json:"orders"`
// Order list pagination, url to next orders.
// Provided by the rel="next" Link http header
Next string `json:"-"`
}

2
vendor/modules.txt vendored
View File

@ -19,6 +19,8 @@ github.com/cloudflare/cfssl/csr
github.com/cloudflare/cfssl/info
github.com/cloudflare/cfssl/crypto/pkcs7
github.com/cloudflare/cfssl/helpers/derhelpers
# github.com/eggsampler/acme/v2 v2.0.1
github.com/eggsampler/acme/v2
# github.com/go-sql-driver/mysql v0.0.0-20170715192408-3955978caca4
github.com/go-sql-driver/mysql
# github.com/golang/mock v1.2.0