pax_global_header00006660000000000000000000000064146257201040014513gustar00rootroot0000000000000052 comment=79ababeef594ad243c4444ab292908aa9c9ba62e acme-3.6.1/000077500000000000000000000000001462572010400124275ustar00rootroot00000000000000acme-3.6.1/.github/000077500000000000000000000000001462572010400137675ustar00rootroot00000000000000acme-3.6.1/.github/workflows/000077500000000000000000000000001462572010400160245ustar00rootroot00000000000000acme-3.6.1/.github/workflows/go.yml000066400000000000000000000034431462572010400171600ustar00rootroot00000000000000name: Go on: push: branches: [ "*" ] schedule: - cron: "0 0 1 * *" jobs: build-examples: runs-on: ubuntu-latest strategy: matrix: go-version: [ '1.11', 'stable' ] steps: - uses: actions/checkout@v3 - name: Set up Go ${{ matrix.go-version }} uses: actions/setup-go@v3 with: go-version: ${{ matrix.go-version }} - name: make examples run: make examples test-pebble: runs-on: ubuntu-latest strategy: matrix: go-version: [ '1.11', 'stable' ] steps: - uses: actions/checkout@v3 - name: Set up Go uses: actions/setup-go@v3 with: go-version: ${{ matrix.go-version }} - name: make pebble ${{ matrix.go-version }} run: make pebble - name: Send pebble coverage ${{ matrix.go-version }} uses: shogo82148/actions-goveralls@v1 with: path-to-profile: coverage-pebble.out flag-name: pebble-${{ matrix.go-version }} parallel: true test-boulder: runs-on: ubuntu-latest strategy: matrix: go-version: [ '1.11', 'stable' ] steps: - uses: actions/checkout@v3 - name: Set up Go uses: actions/setup-go@v3 with: go-version: ${{ matrix.go-version }} - name: make boulder ${{ matrix.go-version }} run: make boulder - name: Send boulder coverage ${{ matrix.go-version }} uses: shogo82148/actions-goveralls@v1 with: path-to-profile: coverage-boulder.out flag-name: boulder-${{ matrix.go-version }} parallel: true finish: needs: [test-pebble, test-boulder] runs-on: ubuntu-latest steps: - uses: shogo82148/actions-goveralls@v1 with: parallel-finished: trueacme-3.6.1/.gitignore000066400000000000000000000000271462572010400144160ustar00rootroot00000000000000.idea/ *.out coverage* acme-3.6.1/LICENSE000066400000000000000000000020461462572010400134360ustar00rootroot00000000000000MIT 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. acme-3.6.1/Makefile000066400000000000000000000052451462572010400140750ustar00rootroot00000000000000 .PHONY: test examples clean test_full pebble pebble_setup pebble_start pebble_wait pebble_stop boulder boulder_setup boulder_start boulder_stop # some variables for path injection, if already set will not override 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/v3 CLIENT ?= unknown # tests the code against an already running ca instance # to actually do a test against pebble or boulder, including , see the 'pebble' or 'boulder' targets test: -go clean -testcache CGO_ENABLED=1 go test -v -race -coverprofile=coverage-$(CLIENT).out -covermode=atomic $(TEST_PATH) examples: go build -o /dev/null examples/certbot/certbot.go go build -o /dev/null examples/autocert/autocert.go go build -o /dev/null examples/zerossl/zerossl.go go build -o /dev/null examples/ari/renewalinfo.go clean: rm -f coverage*.out test_full: clean examples pebble pebble_stop boulder boulder_stop # sets up & runs pebble (in docker), tests, then stops pebble pebble: CLIENT = pebble pebble: pebble_setup pebble_start pebble_wait test pebble_stop pebble_setup: CLIENT=pebble mkdir -p $(PEBBLE_PATH) -git clone --depth 1 https://github.com/letsencrypt/pebble.git $(PEBBLE_PATH) (cd $(PEBBLE_PATH); git checkout -f main && git reset --hard HEAD && git pull -q) make pebble_stop # 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 # sets up & runs boulder (in docker), tests, then stops boulder boulder: CLIENT = boulder 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 main && git reset --hard HEAD && git pull -q) make boulder_stop # runs an instance of boulder boulder_start: docker-compose -f $(BOULDER_PATH)/docker-compose.yml -f $(BOULDER_PATH)/docker-compose.next.yml -f docker-compose.boulder-temp.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 -f $(BOULDER_PATH)/docker-compose.next.yml -f docker-compose.boulder-temp.yml down acme-3.6.1/README.md000066400000000000000000000043551462572010400137150ustar00rootroot00000000000000# eggsampler/acme [![GoDoc](https://godoc.org/github.com/eggsampler/acme?status.svg)](https://godoc.org/github.com/eggsampler/acme) [![Build Status](https://github.com/eggsampler/acme/actions/workflows/go.yml/badge.svg)](https://github.com/eggsampler/acme/actions) [![Coverage Status](https://coveralls.io/repos/github/eggsampler/acme/badge.svg)](https://coveralls.io/github/eggsampler/acme) ## About `eggsampler/acme` is a Go client library implementation for [RFC8555](https://tools.ietf.org/html/rfc8555) (previously ACME v2). This library can be used with the [Let's Encrypt](https://letsencrypt.org/) Certificate Authority (CA), but also other ACME compliant CA's such as [ZeroSSL](https://zerossl.com/). The library is designed to provide a zero external dependency wrapper over exposed directory endpoints and provide objects in easy to use structures. ## Requirements A Go version of at least 1.11 is required as this repository is designed to be imported as a Go module. ## Usage Simply import the module into a project, ```go import "github.com/eggsampler/acme/v3" ``` Note the `/v3` major version at the end. Due to the way modules function, this is the major version as represented in the `go.mod` file and latest git repo [semver](https://semver.org/) tag. All functions are still exported and called using the `acme` package name. ## Examples 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. acme-3.6.1/THIRD-PARTY000066400000000000000000000033761462572010400141320ustar00rootroot00000000000000This 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.acme-3.6.1/account.go000066400000000000000000000102221462572010400144070ustar00rootroot00000000000000package acme import ( "crypto" "encoding/json" "errors" "fmt" "net/http" "reflect" ) // NewAccount registers a new account with the acme service // Note this function is essentially deprecated and only present for backwards compatibility. // New programs should implement NewAccountOptions instead. func (c Client) NewAccount(privateKey crypto.Signer, onlyReturnExisting, termsOfServiceAgreed bool, contact ...string) (Account, error) { var opts []NewAccountOptionFunc if onlyReturnExisting { opts = append(opts, NewAcctOptOnlyReturnExisting()) } if termsOfServiceAgreed { opts = append(opts, NewAcctOptAgreeTOS()) } if len(contact) > 0 { opts = append(opts, NewAcctOptWithContacts(contact...)) } return c.NewAccountOptions(privateKey, opts...) } // NewAccountOptions registers an account with an acme server with the provided options. func (c Client) NewAccountOptions(privateKey crypto.Signer, options ...NewAccountOptionFunc) (Account, error) { newAccountReq := NewAccountRequest{} account := Account{} for _, opt := range options { if err := opt(privateKey, &account, &newAccountReq, c); err != nil { return account, err } } 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, contact ...string) (Account, error) { var updateAccountReq interface{} if !reflect.DeepEqual(account.Contact, contact) { // Only provide a non-nil updateAccountReq when there is an update to be made. updateAccountReq = struct { Contact []string `json:"contact,omitempty"` }{ Contact: contact, } } else { // Otherwise use "" to trigger a POST-as-GET to fetch up-to-date account // information from the acme service. updateAccountReq = "" } _, 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) { 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 } // FetchOrderList fetches a list of orders from the account url provided in the account Orders field func (c Client) FetchOrderList(account Account) (OrderList, error) { orderList := OrderList{} if account.Orders == "" { return orderList, errors.New("no order list for account") } _, err := c.post(account.Orders, account.URL, account.PrivateKey, "", &orderList, http.StatusOK) return orderList, err } acme-3.6.1/account_test.go000066400000000000000000000174631462572010400154640ustar00rootroot00000000000000package acme import ( "crypto" "crypto/ecdsa" "crypto/elliptic" "crypto/rand" "errors" "io" "reflect" "strings" "testing" ) func TestClient_NewAccount(t *testing.T) { errorTests := []struct { Name string OnlyReturnExisting bool TermsOfServiceAgreed bool Contact []string }{ { Name: "fetching non-existing account", OnlyReturnExisting: true, TermsOfServiceAgreed: true, }, { Name: "not agreeing to terms of service", OnlyReturnExisting: false, TermsOfServiceAgreed: false, }, { Name: "bad contacts", OnlyReturnExisting: false, TermsOfServiceAgreed: true, Contact: []string{"this will fail"}, }, } for _, currentTest := range errorTests { key := makePrivateKey(t) _, err := testClient.NewAccount(key, currentTest.OnlyReturnExisting, currentTest.TermsOfServiceAgreed, currentTest.Contact...) if err == nil { t.Fatalf("expected error %s, got none", currentTest.Name) } acmeErr, ok := err.(Problem) if !ok { t.Fatalf("unknown error %s: %v", currentTest.Name, err) } if acmeErr.Type == "" { t.Fatalf("%s no acme error type present: %+v", currentTest.Name, acmeErr) } } } func TestClient_NewAccount2(t *testing.T) { existingKey := makePrivateKey(t) successTests := []struct { Name string Existing bool Key crypto.Signer Contact []string }{ { Name: "new account without contact", }, { Name: "new account with contact", Contact: []string{"mailto:test@test.com"}, }, { Name: "new account for fetching existing", Key: existingKey, }, { Name: "fetching existing account", Key: existingKey, Existing: true, }, } for _, currentTest := range successTests { var key crypto.Signer if currentTest.Key != nil { key = currentTest.Key } else { key = makePrivateKey(t) } if _, err := testClient.NewAccount(key, currentTest.Existing, true, currentTest.Contact...); err != nil { t.Fatalf("unexpected error %s: %v", currentTest.Name, err) } } } func TestClient_UpdateAccount(t *testing.T) { account := makeAccount(t) contact := []string{"mailto:test@test.com"} updatedAccount, err := testClient.UpdateAccount(account, contact...) if err != nil { t.Fatalf("unexpected error: %v", err) } if !reflect.DeepEqual(updatedAccount.Contact, contact) { t.Fatalf("contact mismatch, expected: %v, got: %v", contact, updatedAccount.Contact) } } func TestClient_UpdateAccount2(t *testing.T) { account := makeAccount(t) updatedAccount, err := testClient.UpdateAccount(Account{PrivateKey: account.PrivateKey, URL: account.URL}) if err != nil { t.Fatalf("unexpected error: %v", err) } if !reflect.DeepEqual(account, updatedAccount) { t.Fatalf("account and updated account mismatch, expected: %+v, got: %+v", account, updatedAccount) } _, err = testClient.UpdateAccount(Account{PrivateKey: account.PrivateKey}) if err == nil { t.Fatalf("expected error, got none") } } type errSigner struct{} func (es errSigner) Public() crypto.PublicKey { privKey, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) return privKey } func (es errSigner) Sign(io.Reader, []byte, crypto.SignerOpts) ([]byte, error) { return nil, errors.New("cannot sign key okeydokey") } func TestClient_AccountKeyChange(t *testing.T) { tests := []struct { name string account func() Account newKey func() crypto.Signer expectsError bool errorStr string }{ { name: "bad key", account: func() Account { return Account{ PrivateKey: errSigner{}, } }, newKey: func() crypto.Signer { return nil }, expectsError: true, errorStr: "unknown key type", }, { name: "success", account: func() Account { return makeAccount(t) }, newKey: func() crypto.Signer { return makePrivateKey(t) }, }, { name: "bad signer", account: func() Account { return makeAccount(t) }, newKey: func() crypto.Signer { return errSigner{} }, expectsError: true, errorStr: "inner jws", }, { name: "bad post", account: func() Account { acct := makeAccount(t) acct.URL = "invalid" return acct }, newKey: func() crypto.Signer { return makePrivateKey(t) }, expectsError: true, errorStr: "malformed", }, } for i, ct := range tests { account := ct.account() newKey := ct.newKey() accountNewKey, err := testClient.AccountKeyChange(account, newKey) if ct.expectsError && err == nil { t.Errorf("AccountKeyChange test %d %q expected error, got none", i, ct.name) } if !ct.expectsError && err != nil { t.Errorf("AccountKeyChange test %d %q expected no error, got: %v", i, ct.name, err) } if err != nil && ct.errorStr != "" && !strings.Contains(err.Error(), ct.errorStr) { t.Errorf("AccountKeyChange test %d %q error doesnt contain %q: %s", i, ct.name, ct.errorStr, err.Error()) } if err != nil { continue } if accountNewKey.PrivateKey == account.PrivateKey { t.Fatalf("UpdateAccount test %d %q account key didnt change", i, ct.name) } if accountNewKey.PrivateKey != newKey { t.Fatalf("UpdateAccount test %d %q new key isnt set", i, ct.name) } } } func TestClient_DeactivateAccount(t *testing.T) { account := makeAccount(t) var err error account, err = testClient.DeactivateAccount(account) if err != nil { t.Fatalf("expected no error, got: %v", err) } if account.Status != "deactivated" { t.Fatalf("expected account deactivated, got: %s", account.Status) } } func TestClient_FetchOrderList(t *testing.T) { if testClientMeta.Software == clientBoulder { t.Skip("boulder doesnt support orders list: https://github.com/letsencrypt/boulder/issues/3335") return } tests := []struct { pre func(acct *Account) bool post func(*testing.T, Account, OrderList) expectsError bool errorStr string }{ { pre: func(acct *Account) bool { acct.Orders = "" return false }, expectsError: true, errorStr: "no order", }, { pre: func(acct *Account) bool { *acct, _, _ = makeOrderFinalised(t, nil) return true }, post: func(st *testing.T, account Account, list OrderList) { if len(list.Orders) != 1 { st.Fatalf("expected 1 orders, got: %d", len(list.Orders)) } }, expectsError: false, }, } for i, ct := range tests { acct := makeAccount(t) if ct.pre != nil { update := ct.pre(&acct) if update { var err error acct, err = testClient.UpdateAccount(acct) if err != nil { panic(err) } } } list, err := testClient.FetchOrderList(acct) if ct.expectsError && err == nil { t.Errorf("order list test %d expected error, got none", i) } if !ct.expectsError && err != nil { t.Errorf("order list test %d expected no error, got: %v", i, err) } if err != nil && ct.errorStr != "" && !strings.Contains(err.Error(), ct.errorStr) { t.Errorf("order list test %d error doesnt contain %q: %s", i, ct.errorStr, err.Error()) } if ct.post != nil { ct.post(t, acct, list) } } } func TestClient_NewAccountOptions(t *testing.T) { tests := []struct { name string options []NewAccountOptionFunc expectsError bool errorStr string }{ { name: "opt error func", options: []NewAccountOptionFunc{ func(signer crypto.Signer, account *Account, request *NewAccountRequest, client Client) error { return errors.New("ALWAYS ERRORS") }, }, expectsError: true, errorStr: "ALWAYS", }, } for i, ct := range tests { key := makePrivateKey(t) _, err := testClient.NewAccountOptions(key, ct.options...) if ct.expectsError && err == nil { t.Errorf("NewAccountOptions test %d %q expected error, got none", i, ct.name) } if !ct.expectsError && err != nil { t.Errorf("NewAccountOptions test %d %q expected no error, got: %v", i, ct.name, err) } } } acme-3.6.1/acme.go000066400000000000000000000201761462572010400136710ustar00rootroot00000000000000package acme import ( "bytes" "crypto" "encoding/json" "errors" "fmt" "io/ioutil" "net/http" "os" "regexp" "strings" "time" ) 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" // ZeroSSLProduction holds the ZeroSSL directory url ZeroSSLProduction = "https://acme.zerossl.com/v2/DV90" userAgentString = "eggsampler-acme/v3 Go-http-client/1.1" ) // NewClient creates a new acme client given a valid directory url. func NewClient(directoryURL string, options ...OptionFunc) (Client, error) { // Set a default http timeout of 60 seconds, this can be overridden // via an OptionFunc eg: acme.NewClient(url, WithHTTPTimeout(10 * time.Second)) httpClient := &http.Client{ 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 } // Directory is the 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. The caller is responsible for closing the // body so they can read the response. 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. The caller // is responsible for closing the body so they can read the response. 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. The caller is // responsible for closing the body so they can read the response. 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. The caller is responsible for closing // the body so they can read the response. func (c Client) postRaw(retryCount int, requestURL, kid 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, KeyID(kid), nonce, requestURL) 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, kid, 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. The caller is // responsible for closing the body so they can read the response. 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 _, b := os.LookupEnv("ACME_DEBUG_POST"); b { fmt.Println() fmt.Println("========= " + requestURL) fmt.Println(string(body)) fmt.Println() } 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 an http response and closes the body. 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 "" } // Fetch is a helper function to assist with POST-AS-GET requests func (c Client) Fetch(account Account, requestURL string, result interface{}, expectedStatus ...int) error { if len(expectedStatus) == 0 { expectedStatus = []int{http.StatusOK} } _, err := c.post(requestURL, account.URL, account.PrivateKey, "", result, expectedStatus...) return err } // Fetches all http Link header from a http response func fetchLinks(resp *http.Response, wantedLink string) []string { if resp == nil { return nil } linkHeader := resp.Header["Link"] if len(linkHeader) == 0 { return nil } var links []string for _, l := range linkHeader { matches := regLink.FindAllStringSubmatch(l, -1) for _, m := range matches { if len(m) != 3 { continue } if m[2] == wantedLink { links = append(links, m[1]) } } } return links } acme-3.6.1/acme_test.go000066400000000000000000000066631462572010400147350ustar00rootroot00000000000000package acme import ( "encoding/json" "net/http" "reflect" "testing" ) func TestNewClient(t *testing.T) { if _, err := NewClient("http://fake"); err == nil { t.Fatal("expected error, got none") } } func TestFetchLink(t *testing.T) { linkTests := []struct { Name string LinkHeaders []string WantedLink string ExpectedURL string }{ { Name: "no links", WantedLink: "fail", ExpectedURL: "", }, {Name: "joined links", LinkHeaders: []string{`; rel="next", ; rel="up"`}, WantedLink: "up", ExpectedURL: "http://url/path?query", }, { Name: "separate links", LinkHeaders: []string{`; rel="next"`, `; rel="up"`}, WantedLink: "up", ExpectedURL: "http://url/path?query", }, } for _, currentTest := range linkTests { linkURL := fetchLink(&http.Response{Header: http.Header{"Link": currentTest.LinkHeaders}}, currentTest.WantedLink) if linkURL != currentTest.ExpectedURL { t.Fatalf("%s: links not equal, expected: %s, got: %s", currentTest.Name, currentTest.ExpectedURL, linkURL) } } } func stringSliceEqual(a, b []string) bool { if len(a) != len(b) { return false } for i := 0; i < len(a); i++ { if a[i] != b[i] { return false } } return true } func TestFetchLinks(t *testing.T) { linkTests := []struct { Name string LinkHeaders []string WantedLink string ExpectedURLs []string }{ { Name: "no links", WantedLink: "fail", ExpectedURLs: nil, }, {Name: "joined links", LinkHeaders: []string{`; rel="next", ; rel="up"`}, WantedLink: "up", ExpectedURLs: []string{"http://url/path?query"}, }, { Name: "separate links", LinkHeaders: []string{`; rel="next"`, `; rel="up"`}, WantedLink: "up", ExpectedURLs: []string{"http://url/path?query"}, }, { Name: "multiple links", LinkHeaders: []string{`; rel="up"`, `; rel="up"`}, WantedLink: "up", ExpectedURLs: []string{"https://url/path", "http://url/path?query"}, }, } for _, currentTest := range linkTests { linkURLs := fetchLinks(&http.Response{Header: http.Header{"Link": currentTest.LinkHeaders}}, currentTest.WantedLink) if !stringSliceEqual(linkURLs, currentTest.ExpectedURLs) { t.Fatalf("%s: links not equal, expected: %s, got: %s", currentTest.Name, currentTest.ExpectedURLs, linkURLs) } } } func TestClient_Directory(t *testing.T) { if !reflect.DeepEqual(testClient.dir, testClient.Directory()) { t.Fatalf("directory mismatch, expected: %+v, got: %+v", testClient.dir, testClient.Directory()) } } func TestClient_Fetch(t *testing.T) { _, account1order, _ := makeOrderFinalised(t, []string{ChallengeTypeDNS01}, Identifier{"dns", randString() + ".com"}) account2 := makeAccount(t) err := testClient.Fetch(account2, account1order.URL, &Account{}) if err == nil { t.Error("expected error accessing different account post-as-get, got none") } account := makeAccount(t) b := json.RawMessage{} if err := testClient.Fetch(account, testClient.Directory().URL, &b); err != nil { t.Errorf("error post-as-get directory url: %v", err) } if err := testClient.Fetch(account, testClient.Directory().NewNonce, &b, http.StatusNoContent, http.StatusOK); err != nil { t.Errorf("error post-as-get newnonce url: %v", err) } } acme-3.6.1/ari.go000066400000000000000000000066671462572010400135500ustar00rootroot00000000000000package acme import ( _ "crypto/sha1" _ "crypto/sha256" _ "crypto/sha512" "crypto/x509" "encoding/asn1" "encoding/base64" "fmt" "math/rand" "net/http" "strconv" "strings" "time" ) // GetRenewalInfo returns the renewal information (if present and supported by // the ACME server), and a Retry-After time if indicated in the http response // header. func (c Client) GetRenewalInfo(cert *x509.Certificate) (RenewalInfo, error) { if c.dir.RenewalInfo == "" { return RenewalInfo{}, ErrRenewalInfoNotSupported } certID, err := GenerateARICertID(cert) if err != nil { return RenewalInfo{}, fmt.Errorf("acme: error generating certificate id: %v", err) } renewalURL := c.dir.RenewalInfo if !strings.HasSuffix(renewalURL, "/") { renewalURL += "/" } renewalURL += certID var ri RenewalInfo resp, err := c.get(renewalURL, &ri, http.StatusOK) if err != nil { return ri, err } defer resp.Body.Close() ri.RetryAfter, err = parseRetryAfter(resp.Header.Get("Retry-After")) return ri, err } // GenerateARICertID constructs a certificate identifier as described in // draft-ietf-acme-ari-03, section 4.1. func GenerateARICertID(cert *x509.Certificate) (string, error) { if cert == nil { return "", fmt.Errorf("certificate not found") } derBytes, err := asn1.Marshal(cert.SerialNumber) if err != nil { return "", err } if len(derBytes) < 3 { return "", fmt.Errorf("invalid DER encoding of serial number") } // Extract only the integer bytes from the DER encoded Serial Number // Skipping the first 2 bytes (tag and length). The result is base64url // encoded without padding. serial := base64.RawURLEncoding.EncodeToString(derBytes[2:]) // Convert the Authority Key Identifier to base64url encoding without // padding. aki := base64.RawURLEncoding.EncodeToString(cert.AuthorityKeyId) // Construct the final identifier by concatenating AKI and Serial Number. return fmt.Sprintf("%s.%s", aki, serial), nil } func (r RenewalInfo) ShouldRenewAt(now time.Time, willingToSleep time.Duration) *time.Time { // Explicitly convert all times to UTC. now = now.UTC() start := r.SuggestedWindow.Start.UTC() end := r.SuggestedWindow.End.UTC() // Select a uniform random time within the suggested window. window := end.Sub(start) randomDuration := time.Duration(rand.Int63n(int64(window))) randomTime := start.Add(randomDuration) // If the selected time is in the past, attempt renewal immediately. if randomTime.Before(now) { return &now } // Otherwise, if the client can schedule itself to attempt renewal at // exactly the selected time, do so. willingToSleepUntil := now.Add(willingToSleep) if willingToSleepUntil.After(randomTime) || willingToSleepUntil.Equal(randomTime) { return &randomTime } return nil } // timeNow and implementations support testing type timeNow interface { Now() time.Time } type currentTimeNow struct{} func (currentTimeNow) Now() time.Time { return time.Now() } var systemTime timeNow = currentTimeNow{} func parseRetryAfter(ra string) (time.Time, error) { retryAfterString := strings.TrimSpace(ra) if len(retryAfterString) == 0 { return time.Time{}, nil } if retryAfterTime, err := time.Parse(time.RFC1123, retryAfterString); err == nil { return retryAfterTime, nil } if retryAfterInt, err := strconv.Atoi(retryAfterString); err == nil { return systemTime.Now().Add(time.Second * time.Duration(retryAfterInt)), nil } return time.Time{}, fmt.Errorf("invalid time format: %s", retryAfterString) } acme-3.6.1/ari_test.go000066400000000000000000000157041462572010400145770ustar00rootroot00000000000000package acme import ( "crypto/x509" "encoding/pem" "testing" "time" ) func TestClient_GetRenewalInfo(t *testing.T) { if testClient.dir.RenewalInfo == "" { t.Skip("acme server does not support ari renewals") return } account, order, _ := makeOrderFinalised(t, nil) if order.Certificate == "" { t.Fatalf("no certificate: %+v", order) } certs, err := testClient.FetchCertificates(account, order.Certificate) t.Logf("Issued serial %s\n", certs[0].SerialNumber.String()) if err != nil { t.Fatalf("expected no error, got: %v", err) } if len(certs) < 2 { t.Fatalf("no certs") } renewalInfo, err := testClient.GetRenewalInfo(certs[0]) t.Logf("Suggested renewal window for new issuance: %v\n", renewalInfo.SuggestedWindow) if err != nil { t.Fatalf("expected no error, got: %v", err) } if renewalInfo.RetryAfter.IsZero() { t.Fatalf("no retry after provided") } if renewalInfo.SuggestedWindow.Start.Before(time.Now()) { t.Fatalf("suggested window start is in the past?") } if renewalInfo.SuggestedWindow.End.Before(time.Now()) { t.Fatalf("suggested window start is in the past?") } if renewalInfo.SuggestedWindow.End.Before(renewalInfo.SuggestedWindow.Start) { t.Fatalf("suggested window end is before start?") } err = testClient.RevokeCertificate(account, certs[0], account.PrivateKey, ReasonUnspecified) if err != nil { t.Fatalf("failed to revoke certificate: %v", err) } // The renewal window should adjust to allow immediate renewal renewalInfo, err = testClient.GetRenewalInfo(certs[0]) t.Logf("Suggested renewal window for revoked certificate: %v\n", renewalInfo.SuggestedWindow) if err != nil { t.Fatalf("expected no error, got: %v", err) } if !renewalInfo.SuggestedWindow.Start.Before(time.Now()) { t.Fatalf("suggested window start is in the past?") } if !renewalInfo.SuggestedWindow.End.Before(time.Now()) { t.Fatalf("suggested window start is in the past?") } if renewalInfo.SuggestedWindow.End.Before(renewalInfo.SuggestedWindow.Start) { t.Fatalf("suggested window end is before start?") } } func TestClient_IssueReplacementCert(t *testing.T) { if testClient.dir.RenewalInfo == "" { t.Skip("acme server does not support ari renewals") return } t.Log("Issuing initial order") account, order, _ := makeOrderFinalised(t, nil) if order.Certificate == "" { t.Fatalf("no certificate: %+v", order) } // Replacing the original order should work t.Log("Issuing first replacement order") replacementOrder, err := makeReplacementOrderFinalized(t, order, account, nil, false) if err != nil { t.Fatal(err) } // Replacing the replacement should work t.Log("Issuing second replacement order") _, err = makeReplacementOrderFinalized(t, replacementOrder, account, nil, false) if err != nil { t.Fatal(err) } // Attempting to replace a previously replacement order should fail. t.Log("Should not be able to create a duplicate replacement") _, err = makeReplacementOrderFinalized(t, replacementOrder, account, nil, false) if err == nil { t.Fatal(err) } } func TestClient_FailedReplacementOrderAllowsAnotherReplacement(t *testing.T) { if testClient.dir.RenewalInfo == "" { t.Skip("acme server does not support ari renewals") return } t.Log("Issuing initial order") account, order, _ := makeOrderFinalised(t, nil) if order.Certificate == "" { t.Fatalf("no certificate: %+v", order) } // Explicitly deactivate the previous authorization so the VA has to // re-validate the order and encounter a failure. Upon receiving a // validation failure, Pebble marks an order as invalid which is what we // need. auth, err := testClient.DeactivateAuthorization(account, order.Authorizations[0]) if err != nil { t.Fatalf("expected no error, got: %v", err) } if auth.Status != "deactivated" { t.Fatalf("expected deactivated status, got: %s", auth.Status) } t.Log("Issuing replacement order which will intentionally fail") _, err = makeReplacementOrderFinalized(t, order, account, nil, true) if err == nil { t.Fatal(err) } // Attempting to replace a previously failed replacement order should pass t.Log("Issuing replacement order for a parent order who previously had a failed replacement order") _, err = makeReplacementOrderFinalized(t, order, account, nil, false) if err != nil { t.Fatal(err) } } func Test_generateCertID(t *testing.T) { type args struct { cert *x509.Certificate } tests := []struct { name string args args want string wantErr bool }{ { name: "ari example", args: args{ // certificate taken from draft-ietf-acme-ari-03 appendix A.1. Example Certificate cert: pem2cert(t, `-----BEGIN CERTIFICATE----- MIIBQzCB66ADAgECAgUAh2VDITAKBggqhkjOPQQDAjAVMRMwEQYDVQQDEwpFeGFt cGxlIENBMCIYDzAwMDEwMTAxMDAwMDAwWhgPMDAwMTAxMDEwMDAwMDBaMBYxFDAS BgNVBAMTC2V4YW1wbGUuY29tMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEeBZu 7cbpAYNXZLbbh8rNIzuOoqOOtmxA1v7cRm//AwyMwWxyHz4zfwmBhcSrf47NUAFf qzLQ2PPQxdTXREYEnKMjMCEwHwYDVR0jBBgwFoAUaYhba4dGQEHhs3uEe6CuLN4B yNQwCgYIKoZIzj0EAwIDRwAwRAIge09+S5TZAlw5tgtiVvuERV6cT4mfutXIlwTb +FYN/8oCIClDsqBklhB9KAelFiYt9+6FDj3z4KGVelYM5MdsO3pK -----END CERTIFICATE-----`), }, want: `aYhba4dGQEHhs3uEe6CuLN4ByNQ.AIdlQyE`, wantErr: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got, err := GenerateARICertID(tt.args.cert) if (err != nil) != tt.wantErr { t.Errorf("GenerateARICertID() error = %v, wantErr %v", err, tt.wantErr) return } if got != tt.want { t.Errorf("GenerateARICertID() error\n got = %v\n want = %v", got, tt.want) } }) } } func pem2cert(t *testing.T, s string) *x509.Certificate { block, _ := pem.Decode([]byte(s)) cert, err := x509.ParseCertificate(block.Bytes) if err != nil { t.Fatalf("error parsing certificate: %v", err) } return cert } type zeroTimeNow struct{} func (zeroTimeNow) Now() time.Time { return time.Time{} } func Test_parseRetryAfter(t *testing.T) { systemTime = zeroTimeNow{} currentTime := time.Now().Round(time.Second) currentTimeRFC1123 := currentTime.Format(time.RFC1123) type args struct { ra string } tests := []struct { name string args args want time.Time wantErr bool }{ { name: "simple", args: args{ ra: "123", }, want: time.Time{}.Add(123 * time.Second), wantErr: false, }, { name: "date", args: args{ ra: "Wed, 21 Oct 2015 07:28:00 GMT", }, want: time.Date(2015, 10, 21, 7, 28, 0, 0, time.FixedZone("GMT", 0)), wantErr: false, }, { name: "bad", args: args{ ra: "hello, world", }, want: time.Time{}, wantErr: true, }, { name: "dynamic", args: args{ ra: currentTimeRFC1123, }, want: currentTime, wantErr: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got, err := parseRetryAfter(tt.args.ra) if (err != nil) != tt.wantErr { t.Errorf("parseRetryAfter() error = %v, wantErr %v", err, tt.wantErr) return } if !got.Equal(tt.want) { t.Errorf("parseRetryAfter() got = %v, want %v", got, tt.want) } }) } } acme-3.6.1/authorization.go000066400000000000000000000024551462572010400156640ustar00rootroot00000000000000package 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 } acme-3.6.1/authorization_test.go000066400000000000000000000014151462572010400167160ustar00rootroot00000000000000package acme import "testing" func TestClient_FetchAuthorization(t *testing.T) { account, order := makeOrder(t) auth, err := testClient.FetchAuthorization(account, order.Authorizations[0]) if err != nil { t.Fatalf("unexpected error fetching authorization: %v", err) } if auth.Status != "pending" { t.Fatalf("unexpected auth status: %s", auth.Status) } if len(auth.Challenges) == 0 { t.Fatalf("no challenges on auth") } } func TestClient_DeactivateAuthorization(t *testing.T) { account, order := makeOrder(t) auth, err := testClient.DeactivateAuthorization(account, order.Authorizations[0]) if err != nil { t.Fatalf("expected no error, got: %v", err) } if auth.Status != "deactivated" { t.Fatalf("expected deactivated status, got: %s", auth.Status) } } acme-3.6.1/autocert.go000066400000000000000000000260161462572010400146110ustar00rootroot00000000000000package 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, error) { // check for a stored cert certData := m.getCache("cert", name) if len(certData) == 0 { return nil, errors.New("autocert: no existing certificate") } privBlock, pubData := pem.Decode(certData) if len(pubData) == 0 { return nil, errors.New("autocert: no public key data (cert/issuer)") } // 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 { return nil, errors.New("autocert: leftover data in file - possibly corrupt") } certs, err := x509.ParseCertificates(pub) if err != nil { return nil, fmt.Errorf("autocert: bad certificate: %v", err) } 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 != "" { block, rest := pem.Decode([]byte(m.RootCert)) for block != nil { rootCert, err := x509.ParseCertificate(block.Bytes) if err != nil { return nil, errors.New("autocert: error parsing root certificate") } if roots == nil { roots = x509.NewCertPool() } roots.AddCert(rootCert) block, rest = pem.Decode(rest) } } opts := x509.VerifyOptions{ DNSName: name, Intermediates: intermediates, Roots: roots, } if _, err := leaf.Verify(opts); err != nil { return nil, fmt.Errorf("autocert: unable to verify: %v", err) } privKey, err := x509.ParseECPrivateKey(privBlock.Bytes) if err != nil { return nil, errors.New("autocert: invalid private key") } return &tls.Certificate{ Certificate: pubDER, PrivateKey: privKey, Leaf: leaf, }, nil } 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) } acme-3.6.1/autocert_test.go000066400000000000000000000107551462572010400156530ustar00rootroot00000000000000package acme import ( "bytes" "crypto/tls" "encoding/json" "fmt" "net/http" "net/http/httptest" "os" "reflect" "strings" "testing" ) func TestWhitelistHosts(t *testing.T) { w := WhitelistHosts("hello") if err := w("no"); err == nil { t.Fatal("expected error, got none") } if err := w("hello"); err != nil { t.Fatalf("expected no error, got: %v", err) } } func TestAutoCert_HTTPHandler(t *testing.T) { a := AutoCert{} handler := a.HTTPHandler(nil) r := httptest.NewRequest("GET", "/", nil) w := httptest.NewRecorder() handler.ServeHTTP(w, r) if w.Result().StatusCode != http.StatusMovedPermanently { t.Fatalf("expected status %d, got: %d", http.StatusMovedPermanently, w.Result().StatusCode) } } func TestAutoCert_GetCertificate(t *testing.T) { tests := []struct { ac AutoCert helo *tls.ClientHelloInfo err bool errStr string }{ { ac: AutoCert{}, helo: &tls.ClientHelloInfo{}, err: true, errStr: "missing", }, { ac: AutoCert{}, helo: &tls.ClientHelloInfo{ServerName: "simple"}, err: true, errStr: "count invalid", }, { ac: AutoCert{}, helo: &tls.ClientHelloInfo{ServerName: `inva.lid\`}, err: true, errStr: "invalid character", }, { ac: AutoCert{}, helo: &tls.ClientHelloInfo{ServerName: `inva.lid/`}, err: true, errStr: "invalid character", }, { ac: AutoCert{HostCheck: WhitelistHosts("no.no")}, helo: &tls.ClientHelloInfo{ServerName: `va.lid`}, err: true, errStr: "not whitelisted", }, } for k := range tests { _, err := tests[k].ac.GetCertificate(tests[k].helo) if tests[k].err && err == nil { t.Fatalf("expected error, got none") } if !tests[k].err && err != nil { t.Fatalf("expected no error, got: %v", err) } if !strings.Contains(err.Error(), tests[k].errStr) { t.Fatalf("missing %q in error: %v", tests[k].errStr, err) } } } func TestAutoCert_getDirectoryURL(t *testing.T) { ac := AutoCert{} if dir := ac.getDirectoryURL(); dir != LetsEncryptStaging { t.Fatalf("Expected staging url, got: %s", dir) } ac.DirectoryURL = "blah" if dir := ac.getDirectoryURL(); dir != "blah" { t.Fatalf("expected blah, got: %s", dir) } } func TestAutoCert_Cache(t *testing.T) { ac := AutoCert{} data := []byte{1, 2, 3} ac.putCache(data, "hello", "world") if b := ac.getCache("hello", "world"); !reflect.DeepEqual(data, b) { t.Fatalf("expected: %+v, got: %+v", data, b) } if b := ac.getCache("non", "existent"); b != nil { t.Fatalf("expected: nil, got: %+v", b) } } func TestAutoCert_Cache2(t *testing.T) { ac := AutoCert{CacheDir: os.TempDir()} data := []byte{1, 2, 3} ctx := ac.putCache(data, "hello", "world") <-ctx.Done() ac2 := AutoCert{CacheDir: os.TempDir()} if b := ac2.getCache("hello", "world"); !reflect.DeepEqual(data, b) { t.Fatalf("expected: %+v, got: %+v", data, b) } ac3 := AutoCert{CacheDir: "fake"} if b := ac3.getCache("hello", "world"); b != nil { t.Fatalf("expected: nil, got: %+v", b) } } func TestAutoCert_checkHost(t *testing.T) { ac := AutoCert{} if err := ac.checkHost("ok"); err != nil { t.Fatalf("expected nil, got: %v", err) } ac2 := AutoCert{HostCheck: WhitelistHosts("host")} if err := ac2.checkHost("ok"); err == nil { t.Fatal("expected error, got: nil") } } func TestAutoCert_getExistingCert(t *testing.T) { ac := AutoCert{} if cert, _ := ac.getExistingCert("fake"); cert != nil { t.Fatalf("expected nil cert, got: %+v", cert) } } func TestAutoCert_GetCertificate2(t *testing.T) { root := fetchRoot() doPost := func(name string, req interface{}) { reqJSON, err := json.Marshal(req) if err != nil { panic(fmt.Sprintf("error marshalling boulder %s: %v", name, err)) } if _, err := http.Post("http://localhost:8055/"+name, "application/json", bytes.NewReader(reqJSON)); err != nil { panic(fmt.Sprintf("error posting boulder %s: %v", name, err)) } } ac := AutoCert{ DirectoryURL: testClient.Directory().URL, Options: []OptionFunc{WithInsecureSkipVerify()}, RootCert: string(root), PreUpdateChallengeHook: func(account Account, challenge Challenge) { addReq := struct { Token string `json:"token"` Content string `json:"content"` }{ Token: challenge.Token, Content: challenge.KeyAuthorization, } doPost("add-http01", addReq) }, } cert, err := ac.GetCertificate(&tls.ClientHelloInfo{ServerName: randString() + ".com"}) if err != nil { t.Fatalf("error getting certificate: %v", err) } if cert == nil { t.Fatalf("NO CERT") } } acme-3.6.1/certificate.go000066400000000000000000000057021462572010400152440ustar00rootroot00000000000000package acme import ( "crypto" "crypto/x509" "encoding/base64" "encoding/pem" "fmt" "net/http" ) func (c Client) decodeCertificateChain(body []byte, resp *http.Response, account Account) ([]*x509.Certificate, error) { 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 } // 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 } return c.decodeCertificateChain(body, resp, account) } // FetchAllCertificates downloads a certificate chain from a url given in an order certificate, as well as any alternate certificates if provided. // Returns a mapping of certificate urls to the certificate chain. func (c Client) FetchAllCertificates(account Account, certificateURL string) (map[string][]*x509.Certificate, error) { resp, body, err := c.postRaw(0, certificateURL, account.URL, account.PrivateKey, "", []int{http.StatusOK}) if err != nil { return nil, err } certChain, err := c.decodeCertificateChain(body, resp, account) if err != nil { return nil, err } certs := map[string][]*x509.Certificate{ certificateURL: certChain, } alternates := fetchLinks(resp, "alternate") for _, altURL := range alternates { altResp, altBody, err := c.postRaw(0, altURL, account.URL, account.PrivateKey, "", []int{http.StatusOK}) if err != nil { return certs, fmt.Errorf("acme: error fetching alt cert chain at %q - %v", altURL, err) } altCertChain, err := c.decodeCertificateChain(altBody, altResp, account) if err != nil { return certs, fmt.Errorf("acme: error decoding alt cert chain at %q - %v", altURL, err) } certs[altURL] = altCertChain } 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 } acme-3.6.1/certificate_test.go000066400000000000000000000074461462572010400163120ustar00rootroot00000000000000package acme import ( "bytes" "encoding/pem" "net/http" "strings" "testing" ) func Test_decodeCertificateChain(t *testing.T) { account, order, _ := makeOrderFinalised(t, nil) tests := []struct { name string body func() []byte resp func() *http.Response expectsError bool errorStr string }{ { name: "invalid certificate", body: func() []byte { var b []byte block := &pem.Block{ Type: "MESSAGE", Headers: map[string]string{ "Animal": "Gopher", }, Bytes: []byte("test"), } if err := pem.Encode(bytes.NewBuffer(b), block); err != nil { t.Fatal(err) } return b }, resp: func() *http.Response { return nil }, }, { name: "invalid link", body: func() []byte { return nil }, resp: func() *http.Response { return &http.Response{ Header: map[string][]string{ "Link": {`; rel="up"`}, }, } }, expectsError: true, errorStr: "bogus.fakedomain", }, { name: "valid link", body: func() []byte { return nil }, resp: func() *http.Response { return &http.Response{ Header: map[string][]string{ "Link": {`<` + order.Certificate + `>; rel="up"`}, }, } }, }, } for i, ct := range tests { body := ct.body() resp := ct.resp() _, err := testClient.decodeCertificateChain(body, resp, account) if ct.expectsError && err == nil { t.Errorf("decodeCertificateChain test %d %q expected error, got none", i, ct.name) } if !ct.expectsError && err != nil { t.Errorf("decodeCertificateChain test %d %q expected no error, got: %v", i, ct.name, err) } if err != nil && ct.errorStr != "" && !strings.Contains(err.Error(), ct.errorStr) { t.Errorf("AccoudecodeCertificateChainntKeyChange test %d %q error doesnt contain %q: %s", i, ct.name, ct.errorStr, err.Error()) } } } func TestClient_FetchCertificates(t *testing.T) { account, order, _ := makeOrderFinalised(t, nil) if order.Certificate == "" { t.Fatalf("no certificate: %+v", order) } certs, err := testClient.FetchCertificates(account, order.Certificate) if err != nil { t.Fatalf("expeceted no error, got: %v", err) } if len(certs) == 0 { t.Fatal("no certs returned") } for _, d := range order.Identifiers { if err := certs[0].VerifyHostname(d.Value); err != nil { t.Fatalf("cert not verified for %s: %v - %+v", d, err, certs[0]) } } } func TestClient_FetchAllCertificates(t *testing.T) { account, order, _ := makeOrderFinalised(t, nil) if order.Certificate == "" { t.Fatalf("no certificate: %+v", order) } certs, err := testClient.FetchAllCertificates(account, order.Certificate) if err != nil { t.Fatalf("expeceted no error, got: %v", err) } if len(certs) == 1 { t.Skip("no alternative root certificates") } } func TestClient_RevokeCertificate(t *testing.T) { // test revoking cert with cert key account, order, privKey := makeOrderFinalised(t, nil) if order.Certificate == "" { t.Fatalf("no certificate: %+v", order) } certs, err := testClient.FetchCertificates(account, order.Certificate) if err != nil { t.Fatalf("expeceted no error, got: %v", err) } if err := testClient.RevokeCertificate(account, certs[0], privKey, ReasonUnspecified); err != nil { t.Fatalf("expected no error, got: %v", err) } } func TestClient_RevokeCertificate2(t *testing.T) { // test revoking cert with account key account, order, _ := makeOrderFinalised(t, nil) if order.Certificate == "" { t.Fatalf("no certificate: %+v", order) } certs, err := testClient.FetchCertificates(account, order.Certificate) if err != nil { t.Fatalf("expeceted no error, got: %v", err) } if err := testClient.RevokeCertificate(account, certs[0], account.PrivateKey, ReasonUnspecified); err != nil { t.Fatalf("expected no error, got: %v", err) } } acme-3.6.1/challenge.go000066400000000000000000000062531462572010400147060ustar00rootroot00000000000000package 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 its 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 } acme-3.6.1/challenge_test.go000066400000000000000000000045721462572010400157470ustar00rootroot00000000000000package acme import ( "testing" ) func TestEncodeDns01KeyAuthorization(t *testing.T) { tests := []struct { KeyAuth string Encoded string }{ { "YLhavngUj1w8B79rUzxB5imUvO8DPyLDHgce89NuMfw.4fqGG7OQog-EV3ovi0b_amhdzVNWxxswDUN9ypYhWpE", "vKcNRAl8IQoDxFFQbEmXHgZ8O1rYk3JTFooIfYJDEEU", }, } for _, currentTest := range tests { e := EncodeDNS01KeyAuthorization(currentTest.KeyAuth) if e != currentTest.Encoded { t.Fatalf("expected: %s, got: %s", currentTest.Encoded, e) } } } func TestClient_UpdateChallenge(t *testing.T) { account, order := makeOrder(t) auth, err := testClient.FetchAuthorization(account, order.Authorizations[0]) if err != nil { t.Fatalf("unexpected error fetching authorization: %v", err) } chal := auth.ChallengeMap[ChallengeTypeDNS01] preChallenge(account, auth, chal) defer postChallenge(account, auth, chal) updatedChal, err := testClient.UpdateChallenge(account, chal) if err != nil { t.Fatalf("expected no error, got: %v", err) } if updatedChal.Status != "valid" { t.Fatalf("expected valid challenge, got: %s", chal.Status) } } func TestClient_FetchChallenge(t *testing.T) { account, order := makeOrder(t) auth, err := testClient.FetchAuthorization(account, order.Authorizations[0]) if err != nil { t.Fatalf("unexpected error fetching authorization: %v", err) } chal := auth.Challenges[0] fetchedChal, err := testClient.FetchChallenge(account, chal.URL) if err != nil { t.Fatalf("expected no error, got: %v", err) } if chal.Token != fetchedChal.Token { t.Fatalf("tokens different") } } func Test_checkUpdatedChallengeStatus(t *testing.T) { tests := []struct { Status string Finished bool HasError bool }{ { Status: "pending", }, { Status: "processing", }, { Status: "valid", Finished: true, }, { Status: "invalid", Finished: true, HasError: true, }, { Status: "blah", Finished: true, HasError: true, }, } for _, ct := range tests { finished, err := checkUpdatedChallengeStatus(Challenge{ Status: ct.Status, }) if ct.Finished != finished { t.Fatalf("Finished mismatch on status %s, expected: %t got: %t", ct.Status, ct.Finished, finished) } if ct.HasError && err == nil { t.Fatalf("status %s expected error, got none", ct.Status) } if !ct.HasError && err != nil { t.Fatalf("status %s expected no error, got: %v", ct.Status, err) } } } acme-3.6.1/docker-compose.boulder-temp.yml000066400000000000000000000001001462572010400204510ustar00rootroot00000000000000version: '3' services: boulder: ports: - "8055:8055"acme-3.6.1/examples/000077500000000000000000000000001462572010400142455ustar00rootroot00000000000000acme-3.6.1/examples/.gitignore000066400000000000000000000000231462572010400162300ustar00rootroot00000000000000.well-known *.json acme-3.6.1/examples/ari/000077500000000000000000000000001462572010400150205ustar00rootroot00000000000000acme-3.6.1/examples/ari/renewalinfo.go000066400000000000000000000063041462572010400176630ustar00rootroot00000000000000//go:build ignore // +build ignore // this example tests the asynchronous order status and fetching renewal information // has been tested using the following, // $ cloudflared tunnel --url http://192.168.2.178:9999 // $ go run renewalinfo.go -domain [tunnel domain] // output - // Renewal info: // - Start: 2023-06-08 00:37:21 +0000 UTC // - End: 2023-06-10 00:37:21 +0000 UTC // - URL: // - Retry-After: 2023-04-10 17:17:25.6274661 +1000 AEST m=+21608.774701601 package main import ( "crypto/ecdsa" "crypto/elliptic" "crypto/rand" "crypto/x509" "crypto/x509/pkix" "flag" "fmt" "log" "net/http" "time" "github.com/eggsampler/acme/v3" ) var ( domain string keyAuth string ) func main() { flag.StringVar(&domain, "domain", "", "domain to use for testing") flag.Parse() if domain == "" { panic("no domain") } http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { _, _ = w.Write([]byte(keyAuth)) }) go func() { err := http.ListenAndServe(":9999", nil) ifpanic(err) }() <-time.After(2 * time.Second) client, err := acme.NewClient(acme.LetsEncryptStaging) ifpanic(err) privKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) ifpanic(err) account, err := client.NewAccount(privKey, false, true, "mailto:test@eggsampler.com") ifpanic(err) order, err := client.NewOrder(account, []acme.Identifier{{Type: "dns", Value: domain}}) ifpanic(err) auth, err := client.FetchAuthorization(account, order.Authorizations[0]) ifpanic(err) chal, ok := auth.ChallengeMap[acme.ChallengeTypeHTTP01] if !ok { panic("no challenge") } keyAuth = chal.KeyAuthorization chal, err = client.UpdateChallenge(account, chal) ifpanic(err) certKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) ifpanic(err) tpl := &x509.CertificateRequest{ SignatureAlgorithm: x509.ECDSAWithSHA256, PublicKeyAlgorithm: x509.ECDSA, PublicKey: certKey.Public(), Subject: pkix.Name{CommonName: domain}, DNSNames: []string{domain}, } csrDer, err := x509.CreateCertificateRequest(rand.Reader, tpl, certKey) if err != nil { log.Fatalf("Error creating certificate request: %v", err) } csr, err := x509.ParseCertificateRequest(csrDer) if err != nil { log.Fatalf("Error parsing certificate request: %v", err) } client.IgnoreRetryAfter = true client.IgnorePolling = true order, err = client.FinalizeOrder(account, order, csr) ifpanic(err) if order.Status != "processing" { panic("expected async processing order") } for { <-time.After(time.Until(order.RetryAfter) + 5*time.Second) order, err = client.FetchOrder(account, order.URL) ifpanic(err) if order.Status == "valid" { break } } cert, err := client.FetchCertificates(account, order.Certificate) ifpanic(err) ri, err := client.GetRenewalInfo(cert[0]) ifpanic(err) shouldRenewAt := ri.ShouldRenewAt(time.Now(), time.Duration(1*time.Second)) fmt.Println("Renewal info:") fmt.Printf(" - Start: %s\n", ri.SuggestedWindow.Start) fmt.Printf(" - End: %s\n", ri.SuggestedWindow.End) fmt.Printf(" - URL: %s\n", ri.ExplanationURL) fmt.Printf(" - Retry-After: %s\n", ri.RetryAfter) fmt.Printf(" - Renew-At: %s\n", shouldRenewAt) } func ifpanic(err error) { if err != nil { panic(err) } } acme-3.6.1/examples/autocert/000077500000000000000000000000001462572010400160735ustar00rootroot00000000000000acme-3.6.1/examples/autocert/autocert.go000066400000000000000000000050361462572010400202540ustar00rootroot00000000000000//go:build ignore // +build ignore package main // An example which uses autocert to issue certificates for connecting hosts // Uses the Let's Encrypt staging environment and fake root certificate // // Can be tested like the following, // - go run autocert.go // - ngrok http 80 // - openssl s_client -connect localhost:443 -servername [NGROK FORWARDING HOSTNAME] import ( "crypto/tls" "log" "net/http" "github.com/eggsampler/acme/v3" ) func main() { autoCert := acme.AutoCert{ DirectoryURL: acme.LetsEncryptStaging, RootCert: `-----BEGIN CERTIFICATE----- MIIEqzCCApOgAwIBAgIRAIvhKg5ZRO08VGQx8JdhT+UwDQYJKoZIhvcNAQELBQAw GjEYMBYGA1UEAwwPRmFrZSBMRSBSb290IFgxMB4XDTE2MDUyMzIyMDc1OVoXDTM2 MDUyMzIyMDc1OVowIjEgMB4GA1UEAwwXRmFrZSBMRSBJbnRlcm1lZGlhdGUgWDEw ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDtWKySDn7rWZc5ggjz3ZB0 8jO4xti3uzINfD5sQ7Lj7hzetUT+wQob+iXSZkhnvx+IvdbXF5/yt8aWPpUKnPym oLxsYiI5gQBLxNDzIec0OIaflWqAr29m7J8+NNtApEN8nZFnf3bhehZW7AxmS1m0 ZnSsdHw0Fw+bgixPg2MQ9k9oefFeqa+7Kqdlz5bbrUYV2volxhDFtnI4Mh8BiWCN xDH1Hizq+GKCcHsinDZWurCqder/afJBnQs+SBSL6MVApHt+d35zjBD92fO2Je56 dhMfzCgOKXeJ340WhW3TjD1zqLZXeaCyUNRnfOmWZV8nEhtHOFbUCU7r/KkjMZO9 AgMBAAGjgeMwgeAwDgYDVR0PAQH/BAQDAgGGMBIGA1UdEwEB/wQIMAYBAf8CAQAw HQYDVR0OBBYEFMDMA0a5WCDMXHJw8+EuyyCm9Wg6MHoGCCsGAQUFBwEBBG4wbDA0 BggrBgEFBQcwAYYoaHR0cDovL29jc3Auc3RnLXJvb3QteDEubGV0c2VuY3J5cHQu b3JnLzA0BggrBgEFBQcwAoYoaHR0cDovL2NlcnQuc3RnLXJvb3QteDEubGV0c2Vu Y3J5cHQub3JnLzAfBgNVHSMEGDAWgBTBJnSkikSg5vogKNhcI5pFiBh54DANBgkq hkiG9w0BAQsFAAOCAgEABYSu4Il+fI0MYU42OTmEj+1HqQ5DvyAeyCA6sGuZdwjF UGeVOv3NnLyfofuUOjEbY5irFCDtnv+0ckukUZN9lz4Q2YjWGUpW4TTu3ieTsaC9 AFvCSgNHJyWSVtWvB5XDxsqawl1KzHzzwr132bF2rtGtazSqVqK9E07sGHMCf+zp DQVDVVGtqZPHwX3KqUtefE621b8RI6VCl4oD30Olf8pjuzG4JKBFRFclzLRjo/h7 IkkfjZ8wDa7faOjVXx6n+eUQ29cIMCzr8/rNWHS9pYGGQKJiY2xmVC9h12H99Xyf zWE9vb5zKP3MVG6neX1hSdo7PEAb9fqRhHkqVsqUvJlIRmvXvVKTwNCP3eCjRCCI PTAvjV+4ni786iXwwFYNz8l3PmPLCyQXWGohnJ8iBm+5nk7O2ynaPVW0U2W+pt2w SVuvdDM5zGv2f9ltNWUiYZHJ1mmO97jSY/6YfdOUH66iRtQtDkHBRdkNBsMbD+Em 2TgBldtHNSJBfB3pm9FblgOcJ0FSWcUDWJ7vO0+NTXlgrRofRT6pVywzxVo6dND0 WzYlTWeUVsO40xJqhgUQRER9YLOLxJ0O6C8i0xFxAMKOtSdodMB3RIwt7RFQ0uyt n5Z5MqkYhlMI3J1tPRTp1nEt9fyGspBOO05gi148Qasp+3N+svqKomoQglNoAxU= -----END CERTIFICATE-----`, } http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { w.Write([]byte("Hello, World!")) }) go func() { log.Fatal(http.ListenAndServe(":80", autoCert.HTTPHandler(nil))) }() server := &http.Server{ Addr: ":443", TLSConfig: &tls.Config{ GetCertificate: autoCert.GetCertificate, }, } log.Fatal(server.ListenAndServeTLS("", "")) } acme-3.6.1/examples/certbot/000077500000000000000000000000001462572010400157075ustar00rootroot00000000000000acme-3.6.1/examples/certbot/certbot.go000066400000000000000000000231251462572010400177030ustar00rootroot00000000000000//go:build ignore // +build ignore package main // An example of the acme library to create a simple certbot-like clone. Takes a few command line parameters and issues // a certificate using the http-01 challenge method. import ( "crypto/ecdsa" "crypto/elliptic" "crypto/rand" "crypto/x509" "crypto/x509/pkix" "encoding/json" "encoding/pem" "flag" "fmt" "io/ioutil" "log" "os" "path/filepath" "strings" "github.com/eggsampler/acme/v3" ) var ( webroot string domains string directoryUrl string contactsList string accountFile string certFile string keyFile string ) type acmeAccountFile struct { PrivateKey string `json:"privateKey"` Url string `json:"url"` } func main() { flag.StringVar(&directoryUrl, "dirurl", acme.LetsEncryptStaging, "acme directory url - defaults to lets encrypt v2 staging url if not provided") flag.StringVar(&contactsList, "contact", "", "a list of comma separated contact emails to use when creating a new account (optional, dont include 'mailto:' prefix)") flag.StringVar(&webroot, "webroot", "/var/www/html", "a webroot that the acme key authorization file will be written to (don't include /.acme-challenge/well-known/ suffix)") flag.StringVar(&domains, "domains", "", "a comma separated list of domains to issue a certificate for") flag.StringVar(&accountFile, "accountfile", "account.json", "the file that the account json data will be saved to/loaded from (will create new file if not exists)") flag.StringVar(&certFile, "certfile", "cert.pem", "the file that the pem encoded certificate chain will be saved to") flag.StringVar(&keyFile, "keyfile", "privkey.pem", "the file that the pem encoded certificate private key will be saved to") flag.Parse() // check domains are provided if domains == "" { log.Fatal("No domains provided") } // make sure a webroot directory exists if _, err := os.Stat(webroot); os.IsNotExist(err) { log.Fatalf("Webroot does not exist: %q", webroot) } // create a new acme client given a provided (or default) directory url log.Printf("Connecting to acme directory url: %s", directoryUrl) client, err := acme.NewClient(directoryUrl) if err != nil { log.Fatalf("Error connecting to acme directory: %v", err) } // attempt to load an existing account from file log.Printf("Loading account file %s", accountFile) account, err := loadAccount(client) if err != nil { log.Printf("Error loading existing account: %v", err) // if there was an error loading an account, just create a new one log.Printf("Creating new account") account, err = createAccount(client) if err != nil { log.Fatalf("Error creaing new account: %v", err) } } log.Printf("Account url: %s", account.URL) // prepend the .well-known/acme-challenge path to the webroot path webroot = filepath.Join(webroot, ".well-known", "acme-challenge") if _, err := os.Stat(webroot); os.IsNotExist(err) { log.Printf("Making directory path: %s", webroot) if err := os.MkdirAll(webroot, 0755); err != nil { log.Fatalf("Error creating webroot path %q: %v", webroot, err) } } // collect the comma separated domains into acme identifiers domainList := strings.Split(domains, ",") var ids []acme.Identifier for _, domain := range domainList { ids = append(ids, acme.Identifier{Type: "dns", Value: domain}) } // create a new order with the acme service given the provided identifiers log.Printf("Creating new order for domains: %s", domainList) order, err := client.NewOrder(account, ids) if err != nil { log.Fatalf("Error creating new order: %v", err) } log.Printf("Order created: %s", order.URL) // loop through each of the provided authorization urls for _, authUrl := range order.Authorizations { // fetch the authorization data from the acme service given the provided authorization url log.Printf("Fetching authorization: %s", authUrl) auth, err := client.FetchAuthorization(account, authUrl) if err != nil { log.Fatalf("Error fetching authorization url %q: %v", authUrl, err) } log.Printf("Fetched authorization: %s", auth.Identifier.Value) // grab a http-01 challenge from the authorization if it exists chal, ok := auth.ChallengeMap[acme.ChallengeTypeHTTP01] if !ok { log.Fatalf("Unable to find http challenge for auth %s", auth.Identifier.Value) } // create the challenge token file with the key authorization from the challenge tokenFile := filepath.Join(webroot, chal.Token) log.Printf("Creating challenge token file: %s", tokenFile) defer os.Remove(tokenFile) if err := ioutil.WriteFile(tokenFile, []byte(chal.KeyAuthorization), 0644); err != nil { log.Fatalf("Error writing authorization %s challenge file %q: %v", auth.Identifier.Value, tokenFile, err) } /* If you wanted to use a DNS-01 challenge you would extract the challenge object, chal, ok: = auth.ChallengeMap[acme.ChallengeTypeDNS01] You then need to base64 encode the challenge key authorisation for which a helper function is included, txt := acme.EncodeDNS01KeyAuthorization(chal.KeyAuthorization) This txt value is what you then place in the DNS TXT record for "_acme-challenge.[YOURDOMAIN]" before continuing to update the challenge. */ // update the acme server that the challenge file is ready to be queried log.Printf("Updating challenge for authorization %s: %s", auth.Identifier.Value, chal.URL) chal, err = client.UpdateChallenge(account, chal) if err != nil { log.Fatalf("Error updating authorization %s challenge: %v", auth.Identifier.Value, err) } log.Printf("Challenge updated") } // all the challenges should now be completed // create a csr for the new certificate log.Printf("Generating certificate private key") certKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) if err != nil { log.Fatalf("Error generating certificate key: %v", err) } b := key2pem(certKey) // write the key to the key file as a pem encoded key log.Printf("Writing key file: %s", keyFile) if err := ioutil.WriteFile(keyFile, b, 0600); err != nil { log.Fatalf("Error writing key file %q: %v", keyFile, err) } // create the new csr template log.Printf("Creating csr") tpl := &x509.CertificateRequest{ SignatureAlgorithm: x509.ECDSAWithSHA256, PublicKeyAlgorithm: x509.ECDSA, PublicKey: certKey.Public(), Subject: pkix.Name{CommonName: domainList[0]}, DNSNames: domainList, } csrDer, err := x509.CreateCertificateRequest(rand.Reader, tpl, certKey) if err != nil { log.Fatalf("Error creating certificate request: %v", err) } csr, err := x509.ParseCertificateRequest(csrDer) if err != nil { log.Fatalf("Error parsing certificate request: %v", err) } // finalize the order with the acme server given a csr log.Printf("Finalising order: %s", order.URL) order, err = client.FinalizeOrder(account, order, csr) if err != nil { log.Fatalf("Error finalizing order: %v", err) } // fetch the certificate chain from the finalized order provided by the acme server log.Printf("Fetching certificate: %s", order.Certificate) certs, err := client.FetchCertificates(account, order.Certificate) if err != nil { log.Fatalf("Error fetching order certificates: %v", err) } // write the pem encoded certificate chain to file log.Printf("Saving certificate to: %s", certFile) var pemData []string for _, c := range certs { pemData = append(pemData, strings.TrimSpace(string(pem.EncodeToMemory(&pem.Block{ Type: "CERTIFICATE", Bytes: c.Raw, })))) } if err := ioutil.WriteFile(certFile, []byte(strings.Join(pemData, "\n")), 0600); err != nil { log.Fatalf("Error writing certificate file %q: %v", certFile, err) } log.Printf("Done.") } func loadAccount(client acme.Client) (acme.Account, error) { raw, err := ioutil.ReadFile(accountFile) if err != nil { return acme.Account{}, fmt.Errorf("error reading account file %q: %v", accountFile, err) } var aaf acmeAccountFile if err := json.Unmarshal(raw, &aaf); err != nil { return acme.Account{}, fmt.Errorf("error parsing account file %q: %v", accountFile, err) } account, err := client.UpdateAccount(acme.Account{PrivateKey: pem2key([]byte(aaf.PrivateKey)), URL: aaf.Url}, getContacts()...) if err != nil { return acme.Account{}, fmt.Errorf("error updating existing account: %v", err) } return account, nil } func createAccount(client acme.Client) (acme.Account, error) { privKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) if err != nil { return acme.Account{}, fmt.Errorf("error creating private key: %v", err) } account, err := client.NewAccount(privKey, false, true, getContacts()...) if err != nil { return acme.Account{}, fmt.Errorf("error creating new account: %v", err) } raw, err := json.Marshal(acmeAccountFile{PrivateKey: string(key2pem(privKey)), Url: account.URL}) if err != nil { return acme.Account{}, fmt.Errorf("error parsing new account: %v", err) } if err := ioutil.WriteFile(accountFile, raw, 0600); err != nil { return acme.Account{}, fmt.Errorf("error creating account file: %v", err) } return account, nil } func getContacts() []string { var contacts []string if contactsList != "" { contacts = strings.Split(contactsList, ",") for i := 0; i < len(contacts); i++ { contacts[i] = "mailto:" + contacts[i] } } return contacts } func key2pem(certKey *ecdsa.PrivateKey) []byte { certKeyEnc, err := x509.MarshalECPrivateKey(certKey) if err != nil { log.Fatalf("Error encoding key: %v", err) } return pem.EncodeToMemory(&pem.Block{ Type: "EC PRIVATE KEY", Bytes: certKeyEnc, }) } func pem2key(data []byte) *ecdsa.PrivateKey { b, _ := pem.Decode(data) key, err := x509.ParseECPrivateKey(b.Bytes) if err != nil { log.Fatalf("Error decoding key: %v", err) } return key } acme-3.6.1/examples/zerossl/000077500000000000000000000000001462572010400157465ustar00rootroot00000000000000acme-3.6.1/examples/zerossl/zerossl.go000066400000000000000000000056121462572010400200020ustar00rootroot00000000000000//go:build ignore // +build ignore package main import ( "crypto" "crypto/ecdsa" "crypto/elliptic" "crypto/rand" "crypto/x509" "encoding/json" "encoding/pem" "fmt" "io/ioutil" "log" "os" "github.com/eggsampler/acme/v3" ) type acmeAccountFile struct { PrivateKey string `json:"privateKey"` Url string `json:"url"` EABKID string `json:"eab_kid"` EABMAC string `json:"eab_mac"` EABAlgo string `json:"eab_algo"` } const accountFile = "account.json" func main() { client, err := acme.NewClient("https://acme.zerossl.com/v2/DV90") iferr(err, "creating client") if !client.Directory().Meta.ExternalAccountRequired { log.Fatalf("Expected ExternalAccountRequired") } account, err := loadAccount(client) if err != nil { account = createAccount(client) } log.Printf("account: %+v", account) orders, err := client.FetchOrderList(account) iferr(err, "fetching order list") for _, v := range orders.Orders { log.Printf("Order: %+v", v) } } func loadAccount(client acme.Client) (acme.Account, error) { raw, err := ioutil.ReadFile(accountFile) if err != nil { return acme.Account{}, err } var aaf acmeAccountFile if err := json.Unmarshal(raw, &aaf); err != nil { return acme.Account{}, err } account, err := client.UpdateAccount(acme.Account{PrivateKey: pem2key([]byte(aaf.PrivateKey)), URL: aaf.Url}) if err != nil { return acme.Account{}, err } return account, nil } func createAccount(client acme.Client) acme.Account { privKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) iferr(err, "generating priv key") // TODO: Enter EAB Credentials as generated at https://app.zerossl.com/developer eab := acme.ExternalAccountBinding{ KeyIdentifier: os.Getenv("EAB_KID"), MacKey: os.Getenv("EAB_HMAC_KEY"), Algorithm: "HS256", HashFunc: crypto.SHA256, } log.Printf("EAB: %+v", eab) account, err := client.NewAccountOptions(privKey, acme.NewAcctOptAgreeTOS(), acme.NewAcctOptExternalAccountBinding(eab)) iferr(err, "creating new account") acc := acmeAccountFile{ PrivateKey: string(key2pem(privKey)), Url: account.URL, EABKID: eab.KeyIdentifier, EABMAC: eab.MacKey, EABAlgo: fmt.Sprintf("%s", eab.HashFunc), } raw, err := json.Marshal(acc) iferr(err, "marshalling acc") err = ioutil.WriteFile(accountFile, raw, 0600) iferr(err, "writing account file") return account } func key2pem(certKey *ecdsa.PrivateKey) []byte { certKeyEnc, err := x509.MarshalECPrivateKey(certKey) if err != nil { log.Fatalf("Error encoding key: %v", err) } return pem.EncodeToMemory(&pem.Block{ Type: "EC PRIVATE KEY", Bytes: certKeyEnc, }) } func pem2key(data []byte) *ecdsa.PrivateKey { b, _ := pem.Decode(data) key, err := x509.ParseECPrivateKey(b.Bytes) if err != nil { log.Fatalf("Error decoding key: %v", err) } return key } func iferr(err error, s string) { if err != nil { log.Fatalf("%s: %v", s, err) } } acme-3.6.1/go.mod000066400000000000000000000000561462572010400135360ustar00rootroot00000000000000module github.com/eggsampler/acme/v3 go 1.11 acme-3.6.1/jws.go000066400000000000000000000164371462572010400135740ustar00rootroot00000000000000// 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 LICENSE file. package acme import ( "crypto" "crypto/ecdsa" "crypto/hmac" "crypto/rand" "crypto/rsa" "crypto/sha256" _ "crypto/sha512" // need for EC keys "encoding/asn1" "encoding/base64" "encoding/json" "errors" "fmt" "math/big" ) // KeyID is the account key identity provided by a CA during registration. type KeyID string // noKeyID indicates that jwsEncodeJSON should compute and use JWK instead of a KID. // See jwsEncodeJSON for details. const noKeyID = KeyID("") // noPayload indicates jwsEncodeJSON will encode zero-length octet string // in a JWS request. This is called POST-as-GET in RFC 8555 and is used to make // authenticated GET requests via POSTing with an empty payload. // See https://tools.ietf.org/html/rfc8555#section-6.3 for more details. const noPayload = "" // noNonce indicates that the nonce should be omitted from the protected header. // See jwsEncodeJSON for details. const noNonce = "" // jsonWebSignature can be easily serialized into a JWS following // https://tools.ietf.org/html/rfc7515#section-3.2. type jsonWebSignature struct { Protected string `json:"protected"` Payload string `json:"payload"` Sig string `json:"signature"` } // jwsEncodeJSON signs claimset using provided key and a nonce. // The result is serialized in JSON format containing either kid or jwk // fields based on the provided KeyID value. // // The claimset is marshalled using json.Marshal unless it is a string. // In which case it is inserted directly into the message. // // If kid is non-empty, its quoted value is inserted in the protected header // as "kid" field value. Otherwise, JWK is computed using jwkEncode and inserted // as "jwk" field value. The "jwk" and "kid" fields are mutually exclusive. // // If nonce is non-empty, its quoted value is inserted in the protected header. // // See https://tools.ietf.org/html/rfc7515#section-7. func jwsEncodeJSON(claimset interface{}, key crypto.Signer, kid KeyID, nonce, url string) ([]byte, error) { if key == nil { return nil, errors.New("nil key") } alg, sha := jwsHasher(key.Public()) if alg == "" || !sha.Available() { return nil, ErrUnsupportedKey } headers := struct { Alg string `json:"alg"` KID string `json:"kid,omitempty"` JWK json.RawMessage `json:"jwk,omitempty"` Nonce string `json:"nonce,omitempty"` URL string `json:"url"` }{ Alg: alg, Nonce: nonce, URL: url, } switch kid { case noKeyID: jwk, err := jwkEncode(key.Public()) if err != nil { return nil, err } headers.JWK = json.RawMessage(jwk) default: headers.KID = string(kid) } phJSON, err := json.Marshal(headers) if err != nil { return nil, err } phead := base64.RawURLEncoding.EncodeToString([]byte(phJSON)) var payload string if val, ok := claimset.(string); ok { payload = val } else { 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 := jsonWebSignature{ Protected: phead, Payload: payload, Sig: base64.RawURLEncoding.EncodeToString(sig), } return json.Marshal(&enc) } // jwsWithMAC creates and signs a JWS using the given key and the HS256 // algorithm. kid and url are included in the protected header. rawPayload // should not be base64-URL-encoded. func jwsWithMAC(key []byte, kid, url string, rawPayload []byte) (*jsonWebSignature, error) { if len(key) == 0 { return nil, errors.New("acme: cannot sign JWS with an empty MAC key") } header := struct { Algorithm string `json:"alg"` KID string `json:"kid"` URL string `json:"url,omitempty"` }{ // Only HMAC-SHA256 is supported. Algorithm: "HS256", KID: kid, URL: url, } rawProtected, err := json.Marshal(header) if err != nil { return nil, err } protected := base64.RawURLEncoding.EncodeToString(rawProtected) payload := base64.RawURLEncoding.EncodeToString(rawPayload) h := hmac.New(sha256.New, key) if _, err := h.Write([]byte(protected + "." + payload)); err != nil { return nil, err } mac := h.Sum(nil) return &jsonWebSignature{ Protected: protected, Payload: payload, Sig: base64.RawURLEncoding.EncodeToString(mac), }, nil } // 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. // The hash is unused for ECDSA keys. func jwsSign(key crypto.Signer, hash crypto.Hash, digest []byte) ([]byte, error) { switch pub := key.Public().(type) { case *rsa.PublicKey: return key.Sign(rand.Reader, digest, hash) case *ecdsa.PublicKey: sigASN1, err := key.Sign(rand.Reader, digest, hash) if err != nil { return nil, err } var rs struct{ R, S *big.Int } if _, err := asn1.Unmarshal(sigASN1, &rs); err != nil { return nil, err } rb, sb := rs.R.Bytes(), rs.S.Bytes() size := pub.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(pub crypto.PublicKey) (string, crypto.Hash) { switch pub := pub.(type) { case *rsa.PublicKey: return "RS256", crypto.SHA256 case *ecdsa.PublicKey: switch pub.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 } acme-3.6.1/jws_test.go000066400000000000000000000450641462572010400146310ustar00rootroot00000000000000// 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 LICENSE file. package acme import ( "crypto" "crypto/ecdsa" "crypto/elliptic" "crypto/rsa" "crypto/sha256" "crypto/x509" "encoding/base64" "encoding/json" "encoding/pem" "fmt" "io" "math/big" "testing" ) // The following shell command alias is used in the comments // throughout this file: // alias b64raw="base64 -w0 | tr -d '=' | tr '/+' '_-'" const ( // Modulus in raw base64: // 4xgZ3eRPkwoRvy7qeRUbmMDe0V-xH9eWLdu0iheeLlrmD2mqWXfP9IeSKApbn34 // g8TuAS9g5zhq8ELQ3kmjr-KV86GAMgI6VAcGlq3QrzpTCf_30Ab7-zawrfRaFON // a1HwEzPY1KHnGVkxJc85gNkwYI9SY2RHXtvln3zs5wITNrdosqEXeaIkVYBEhbh // Nu54pp3kxo6TuWLi9e6pXeWetEwmlBwtWZlPoib2j3TxLBksKZfoyFyek380mHg // JAumQ_I2fjj98_97mk3ihOY4AgVdCDj1z_GCoZkG5Rq7nbCGyosyKWyDX00Zs-n // NqVhoLeIvXC4nnWdJMZ6rogxyQQ testKeyPEM = ` -----BEGIN RSA PRIVATE KEY----- MIIEowIBAAKCAQEA4xgZ3eRPkwoRvy7qeRUbmMDe0V+xH9eWLdu0iheeLlrmD2mq WXfP9IeSKApbn34g8TuAS9g5zhq8ELQ3kmjr+KV86GAMgI6VAcGlq3QrzpTCf/30 Ab7+zawrfRaFONa1HwEzPY1KHnGVkxJc85gNkwYI9SY2RHXtvln3zs5wITNrdosq EXeaIkVYBEhbhNu54pp3kxo6TuWLi9e6pXeWetEwmlBwtWZlPoib2j3TxLBksKZf oyFyek380mHgJAumQ/I2fjj98/97mk3ihOY4AgVdCDj1z/GCoZkG5Rq7nbCGyosy KWyDX00Zs+nNqVhoLeIvXC4nnWdJMZ6rogxyQQIDAQABAoIBACIEZTOI1Kao9nmV 9IeIsuaR1Y61b9neOF/MLmIVIZu+AAJFCMB4Iw11FV6sFodwpEyeZhx2WkpWVN+H r19eGiLX3zsL0DOdqBJoSIHDWCCMxgnYJ6nvS0nRxX3qVrBp8R2g12Ub+gNPbmFm ecf/eeERIVxfifd9VsyRu34eDEvcmKFuLYbElFcPh62xE3x12UZvV/sN7gXbawpP G+w255vbE5MoaKdnnO83cTFlcHvhn24M/78qP7Te5OAeelr1R89kYxQLpuGe4fbS zc6E3ym5Td6urDetGGrSY1Eu10/8sMusX+KNWkm+RsBRbkyKq72ks/qKpOxOa+c6 9gm+Y8ECgYEA/iNUyg1ubRdH11p82l8KHtFC1DPE0V1gSZsX29TpM5jS4qv46K+s 8Ym1zmrORM8x+cynfPx1VQZQ34EYeCMIX212ryJ+zDATl4NE0I4muMvSiH9vx6Xc 7FmhNnaYzPsBL5Tm9nmtQuP09YEn8poiOJFiDs/4olnD5ogA5O4THGkCgYEA5MIL qWYBUuqbEWLRtMruUtpASclrBqNNsJEsMGbeqBJmoMxdHeSZckbLOrqm7GlMyNRJ Ne/5uWRGSzaMYuGmwsPpERzqEvYFnSrpjW5YtXZ+JtxFXNVfm9Z1gLLgvGpOUCIU RbpoDckDe1vgUuk3y5+DjZihs+rqIJ45XzXTzBkCgYBWuf3segruJZy5rEKhTv+o JqeUvRn0jNYYKFpLBeyTVBrbie6GkbUGNIWbrK05pC+c3K9nosvzuRUOQQL1tJbd 4gA3oiD9U4bMFNr+BRTHyZ7OQBcIXdz3t1qhuHVKtnngIAN1p25uPlbRFUNpshnt jgeVoHlsBhApcs5DUc+pyQKBgDzeHPg/+g4z+nrPznjKnktRY1W+0El93kgi+J0Q YiJacxBKEGTJ1MKBb8X6sDurcRDm22wMpGfd9I5Cv2v4GsUsF7HD/cx5xdih+G73 c4clNj/k0Ff5Nm1izPUno4C+0IOl7br39IPmfpSuR6wH/h6iHQDqIeybjxyKvT1G N0rRAoGBAKGD+4ZI/E1MoJ5CXB8cDDMHagbE3cq/DtmYzE2v1DFpQYu5I4PCm5c7 EQeIP6dZtv8IMgtGIb91QX9pXvP0aznzQKwYIA8nZgoENCPfiMTPiEDT9e/0lObO 9XWsXpbSTsRPj0sv1rB+UzBJ0PgjK4q2zOF0sNo7b1+6nlM3BWPx -----END RSA PRIVATE KEY----- ` // This thumbprint is for the testKey defined above. testKeyThumbprint = "6nicxzh6WETQlrvdchkz-U3e3DOQZ4heJKU63rfqMqQ" // openssl ecparam -name secp256k1 -genkey -noout testKeyECPEM = ` -----BEGIN EC PRIVATE KEY----- MHcCAQEEIK07hGLr0RwyUdYJ8wbIiBS55CjnkMD23DWr+ccnypWLoAoGCCqGSM49 AwEHoUQDQgAE5lhEug5xK4xBDZ2nAbaxLtaLiv85bxJ7ePd1dkO23HThqIrvawF5 QAaS/RNouybCiRhRjI3EaxLkQwgrCw0gqQ== -----END EC PRIVATE KEY----- ` // openssl ecparam -name secp384r1 -genkey -noout testKeyEC384PEM = ` -----BEGIN EC PRIVATE KEY----- MIGkAgEBBDAQ4lNtXRORWr1bgKR1CGysr9AJ9SyEk4jiVnlUWWUChmSNL+i9SLSD Oe/naPqXJ6CgBwYFK4EEACKhZANiAAQzKtj+Ms0vHoTX5dzv3/L5YMXOWuI5UKRj JigpahYCqXD2BA1j0E/2xt5vlPf+gm0PL+UHSQsCokGnIGuaHCsJAp3ry0gHQEke WYXapUUFdvaK1R2/2hn5O+eiQM8YzCg= -----END EC PRIVATE KEY----- ` // openssl ecparam -name secp521r1 -genkey -noout testKeyEC512PEM = ` -----BEGIN EC PRIVATE KEY----- MIHcAgEBBEIBSNZKFcWzXzB/aJClAb305ibalKgtDA7+70eEkdPt28/3LZMM935Z KqYHh/COcxuu3Kt8azRAUz3gyr4zZKhlKUSgBwYFK4EEACOhgYkDgYYABAHUNKbx 7JwC7H6pa2sV0tERWhHhB3JmW+OP6SUgMWryvIKajlx73eS24dy4QPGrWO9/ABsD FqcRSkNVTXnIv6+0mAF25knqIBIg5Q8M9BnOu9GGAchcwt3O7RDHmqewnJJDrbjd GGnm6rb+NnWR9DIopM0nKNkToWoF/hzopxu4Ae/GsQ== -----END EC PRIVATE KEY----- ` // 1. openssl ec -in key.pem -noout -text // 2. remove first byte, 04 (the header); the rest is X and Y // 3. convert each with: echo | xxd -r -p | b64raw testKeyECPubX = "5lhEug5xK4xBDZ2nAbaxLtaLiv85bxJ7ePd1dkO23HQ" testKeyECPubY = "4aiK72sBeUAGkv0TaLsmwokYUYyNxGsS5EMIKwsNIKk" testKeyEC384PubX = "MyrY_jLNLx6E1-Xc79_y-WDFzlriOVCkYyYoKWoWAqlw9gQNY9BP9sbeb5T3_oJt" testKeyEC384PubY = "Dy_lB0kLAqJBpyBrmhwrCQKd68tIB0BJHlmF2qVFBXb2itUdv9oZ-TvnokDPGMwo" testKeyEC512PubX = "AdQ0pvHsnALsfqlraxXS0RFaEeEHcmZb44_pJSAxavK8gpqOXHvd5Lbh3LhA8atY738AGwMWpxFKQ1VNeci_r7SY" testKeyEC512PubY = "AXbmSeogEiDlDwz0Gc670YYByFzC3c7tEMeap7CckkOtuN0Yaebqtv42dZH0MiikzSco2ROhagX-HOinG7gB78ax" // echo -n '{"crv":"P-256","kty":"EC","x":"","y":""}' | \ // openssl dgst -binary -sha256 | b64raw testKeyECThumbprint = "zedj-Bd1Zshp8KLePv2MB-lJ_Hagp7wAwdkA0NUTniU" ) var ( testKey *rsa.PrivateKey testKeyEC *ecdsa.PrivateKey testKeyEC384 *ecdsa.PrivateKey testKeyEC512 *ecdsa.PrivateKey ) func init() { testKey = parseRSA(testKeyPEM, "testKeyPEM") testKeyEC = parseEC(testKeyECPEM, "testKeyECPEM") testKeyEC384 = parseEC(testKeyEC384PEM, "testKeyEC384PEM") testKeyEC512 = parseEC(testKeyEC512PEM, "testKeyEC512PEM") } func decodePEM(s, name string) []byte { d, _ := pem.Decode([]byte(s)) if d == nil { panic("no block found in " + name) } return d.Bytes } func parseRSA(s, name string) *rsa.PrivateKey { b := decodePEM(s, name) k, err := x509.ParsePKCS1PrivateKey(b) if err != nil { panic(fmt.Sprintf("%s: %v", name, err)) } return k } func parseEC(s, name string) *ecdsa.PrivateKey { b := decodePEM(s, name) k, err := x509.ParseECPrivateKey(b) if err != nil { panic(fmt.Sprintf("%s: %v", name, err)) } return k } func TestJWSEncodeJSON(t *testing.T) { claims := struct{ Msg string }{"Hello JWS"} // JWS signed with testKey and "nonce" as the nonce value // JSON-serialized JWS fields are split for easier testing const ( // {"alg":"RS256","jwk":{"e":"AQAB","kty":"RSA","n":"..."},"nonce":"nonce","url":"url"} protected = "eyJhbGciOiJSUzI1NiIsImp3ayI6eyJlIjoiQVFBQiIsImt0eSI6" + "IlJTQSIsIm4iOiI0eGdaM2VSUGt3b1J2eTdxZVJVYm1NRGUwVi14" + "SDllV0xkdTBpaGVlTGxybUQybXFXWGZQOUllU0tBcGJuMzRnOFR1" + "QVM5ZzV6aHE4RUxRM2ttanItS1Y4NkdBTWdJNlZBY0dscTNRcnpw" + "VENmXzMwQWI3LXphd3JmUmFGT05hMUh3RXpQWTFLSG5HVmt4SmM4" + "NWdOa3dZSTlTWTJSSFh0dmxuM3pzNXdJVE5yZG9zcUVYZWFJa1ZZ" + "QkVoYmhOdTU0cHAza3hvNlR1V0xpOWU2cFhlV2V0RXdtbEJ3dFda" + "bFBvaWIyajNUeExCa3NLWmZveUZ5ZWszODBtSGdKQXVtUV9JMmZq" + "ajk4Xzk3bWszaWhPWTRBZ1ZkQ0RqMXpfR0NvWmtHNVJxN25iQ0d5" + "b3N5S1d5RFgwMFpzLW5OcVZob0xlSXZYQzRubldkSk1aNnJvZ3h5" + "UVEifSwibm9uY2UiOiJub25jZSIsInVybCI6InVybCJ9" // {"Msg":"Hello JWS"} payload = "eyJNc2ciOiJIZWxsbyBKV1MifQ" // printf '.' | openssl dgst -binary -sha256 -sign testKey | b64raw signature = "YFyl_xz1E7TR-3E1bIuASTr424EgCvBHjt25WUFC2VaDjXYV0Rj_" + "Hd3dJ_2IRqBrXDZZ2n4ZeA_4mm3QFwmwyeDwe2sWElhb82lCZ8iX" + "uFnjeOmSOjx-nWwPa5ibCXzLq13zZ-OBV1Z4oN_TuailQeRoSfA3" + "nO8gG52mv1x2OMQ5MAFtt8jcngBLzts4AyhI6mBJ2w7Yaj3ZCriq" + "DWA3GLFvvHdW1Ba9Z01wtGT2CuZI7DUk_6Qj1b3BkBGcoKur5C9i" + "bUJtCkABwBMvBQNyD3MmXsrRFRTgvVlyU_yMaucYm7nmzEr_2PaQ" + "50rFt_9qOfJ4sfbLtG1Wwae57BQx1g" ) b, err := jwsEncodeJSON(claims, testKey, noKeyID, "nonce", "url") if err != nil { t.Fatal(err) } var jws struct{ Protected, Payload, Signature string } if err := json.Unmarshal(b, &jws); err != nil { t.Fatal(err) } if jws.Protected != protected { t.Errorf("protected:\n%s\nwant:\n%s", jws.Protected, protected) } if jws.Payload != payload { t.Errorf("payload:\n%s\nwant:\n%s", jws.Payload, payload) } if jws.Signature != signature { t.Errorf("signature:\n%s\nwant:\n%s", jws.Signature, signature) } } func TestJWSEncodeNoNonce(t *testing.T) { kid := KeyID("https://example.org/account/1") claims := "RawString" const ( // {"alg":"ES256","kid":"https://example.org/account/1","nonce":"nonce","url":"url"} protected = "eyJhbGciOiJFUzI1NiIsImtpZCI6Imh0dHBzOi8vZXhhbXBsZS5vcmcvYWNjb3VudC8xIiwidXJsIjoidXJsIn0" // "Raw String" payload = "RawString" ) b, err := jwsEncodeJSON(claims, testKeyEC, kid, "", "url") if err != nil { t.Fatal(err) } var jws struct{ Protected, Payload, Signature string } if err := json.Unmarshal(b, &jws); err != nil { t.Fatal(err) } if jws.Protected != protected { t.Errorf("protected:\n%s\nwant:\n%s", jws.Protected, protected) } if jws.Payload != payload { t.Errorf("payload:\n%s\nwant:\n%s", jws.Payload, payload) } sig, err := base64.RawURLEncoding.DecodeString(jws.Signature) if err != nil { t.Fatalf("jws.Signature: %v", err) } r, s := big.NewInt(0), big.NewInt(0) r.SetBytes(sig[:len(sig)/2]) s.SetBytes(sig[len(sig)/2:]) h := sha256.Sum256([]byte(protected + "." + payload)) if !ecdsa.Verify(testKeyEC.Public().(*ecdsa.PublicKey), h[:], r, s) { t.Error("invalid signature") } } func TestJWSEncodeKID(t *testing.T) { kid := KeyID("https://example.org/account/1") claims := struct{ Msg string }{"Hello JWS"} // JWS signed with testKeyEC const ( // {"alg":"ES256","kid":"https://example.org/account/1","nonce":"nonce","url":"url"} protected = "eyJhbGciOiJFUzI1NiIsImtpZCI6Imh0dHBzOi8vZXhhbXBsZS5" + "vcmcvYWNjb3VudC8xIiwibm9uY2UiOiJub25jZSIsInVybCI6InVybCJ9" // {"Msg":"Hello JWS"} payload = "eyJNc2ciOiJIZWxsbyBKV1MifQ" ) b, err := jwsEncodeJSON(claims, testKeyEC, kid, "nonce", "url") if err != nil { t.Fatal(err) } var jws struct{ Protected, Payload, Signature string } if err := json.Unmarshal(b, &jws); err != nil { t.Fatal(err) } if jws.Protected != protected { t.Errorf("protected:\n%s\nwant:\n%s", jws.Protected, protected) } if jws.Payload != payload { t.Errorf("payload:\n%s\nwant:\n%s", jws.Payload, payload) } sig, err := base64.RawURLEncoding.DecodeString(jws.Signature) if err != nil { t.Fatalf("jws.Signature: %v", err) } r, s := big.NewInt(0), big.NewInt(0) r.SetBytes(sig[:len(sig)/2]) s.SetBytes(sig[len(sig)/2:]) h := sha256.Sum256([]byte(protected + "." + payload)) if !ecdsa.Verify(testKeyEC.Public().(*ecdsa.PublicKey), h[:], r, s) { t.Error("invalid signature") } } func TestJWSEncodeJSONEC(t *testing.T) { tt := []struct { key *ecdsa.PrivateKey x, y string alg, crv string }{ {testKeyEC, testKeyECPubX, testKeyECPubY, "ES256", "P-256"}, {testKeyEC384, testKeyEC384PubX, testKeyEC384PubY, "ES384", "P-384"}, {testKeyEC512, testKeyEC512PubX, testKeyEC512PubY, "ES512", "P-521"}, } for i, test := range tt { claims := struct{ Msg string }{"Hello JWS"} b, err := jwsEncodeJSON(claims, test.key, noKeyID, "nonce", "url") if err != nil { t.Errorf("%d: %v", i, err) continue } var jws struct{ Protected, Payload, Signature string } if err := json.Unmarshal(b, &jws); err != nil { t.Errorf("%d: %v", i, err) continue } b, err = base64.RawURLEncoding.DecodeString(jws.Protected) if err != nil { t.Errorf("%d: jws.Protected: %v", i, err) } var head struct { Alg string Nonce string URL string `json:"url"` KID string `json:"kid"` JWK struct { Crv string Kty string X string Y string } `json:"jwk"` } if err := json.Unmarshal(b, &head); err != nil { t.Errorf("%d: jws.Protected: %v", i, err) } if head.Alg != test.alg { t.Errorf("%d: head.Alg = %q; want %q", i, head.Alg, test.alg) } if head.Nonce != "nonce" { t.Errorf("%d: head.Nonce = %q; want nonce", i, head.Nonce) } if head.URL != "url" { t.Errorf("%d: head.URL = %q; want 'url'", i, head.URL) } if head.KID != "" { // We used noKeyID in jwsEncodeJSON: expect no kid value. t.Errorf("%d: head.KID = %q; want empty", i, head.KID) } if head.JWK.Crv != test.crv { t.Errorf("%d: head.JWK.Crv = %q; want %q", i, head.JWK.Crv, test.crv) } if head.JWK.Kty != "EC" { t.Errorf("%d: head.JWK.Kty = %q; want EC", i, head.JWK.Kty) } if head.JWK.X != test.x { t.Errorf("%d: head.JWK.X = %q; want %q", i, head.JWK.X, test.x) } if head.JWK.Y != test.y { t.Errorf("%d: head.JWK.Y = %q; want %q", i, head.JWK.Y, test.y) } } } type customTestSigner struct { sig []byte pub crypto.PublicKey } func (s *customTestSigner) Public() crypto.PublicKey { return s.pub } func (s *customTestSigner) Sign(io.Reader, []byte, crypto.SignerOpts) ([]byte, error) { return s.sig, nil } func TestJWSEncodeJSONCustom(t *testing.T) { claims := struct{ Msg string }{"hello"} const ( // printf '{"Msg":"hello"}' | b64raw payload = "eyJNc2ciOiJoZWxsbyJ9" // printf 'testsig' | b64raw testsig = "dGVzdHNpZw" // the example P256 curve point from https://tools.ietf.org/html/rfc7515#appendix-A.3.1 // encoded as ASN.1… es256stdsig = "MEUCIA7RIVN5Y2xIPC9/FVgH1AKjsigDOvl8fheBmsMWnqZlAiEA" + "xQoH04w8cOXY8S2vCEpUgKZlkMXyk1Cajz9/ioOjVNU" // …and RFC7518 (https://tools.ietf.org/html/rfc7518#section-3.4) es256jwsig = "DtEhU3ljbEg8L38VWAfUAqOyKAM6-Xx-F4GawxaepmXFCgfTjDxw" + "5djxLa8ISlSApmWQxfKTUJqPP3-Kg6NU1Q" // printf '{"alg":"ES256","jwk":{"crv":"P-256","kty":"EC","x":,"y":},"nonce":"nonce","url":"url"}' | b64raw es256phead = "eyJhbGciOiJFUzI1NiIsImp3ayI6eyJjcnYiOiJQLTI1NiIsImt0" + "eSI6IkVDIiwieCI6IjVsaEV1ZzV4SzR4QkRaMm5BYmF4THRhTGl2" + "ODVieEo3ZVBkMWRrTzIzSFEiLCJ5IjoiNGFpSzcyc0JlVUFHa3Yw" + "VGFMc213b2tZVVl5TnhHc1M1RU1JS3dzTklLayJ9LCJub25jZSI6" + "Im5vbmNlIiwidXJsIjoidXJsIn0" // {"alg":"RS256","jwk":{"e":"AQAB","kty":"RSA","n":"..."},"nonce":"nonce","url":"url"} rs256phead = "eyJhbGciOiJSUzI1NiIsImp3ayI6eyJlIjoiQVFBQiIsImt0eSI6" + "IlJTQSIsIm4iOiI0eGdaM2VSUGt3b1J2eTdxZVJVYm1NRGUwVi14" + "SDllV0xkdTBpaGVlTGxybUQybXFXWGZQOUllU0tBcGJuMzRnOFR1" + "QVM5ZzV6aHE4RUxRM2ttanItS1Y4NkdBTWdJNlZBY0dscTNRcnpw" + "VENmXzMwQWI3LXphd3JmUmFGT05hMUh3RXpQWTFLSG5HVmt4SmM4" + "NWdOa3dZSTlTWTJSSFh0dmxuM3pzNXdJVE5yZG9zcUVYZWFJa1ZZ" + "QkVoYmhOdTU0cHAza3hvNlR1V0xpOWU2cFhlV2V0RXdtbEJ3dFda" + "bFBvaWIyajNUeExCa3NLWmZveUZ5ZWszODBtSGdKQXVtUV9JMmZq" + "ajk4Xzk3bWszaWhPWTRBZ1ZkQ0RqMXpfR0NvWmtHNVJxN25iQ0d5" + "b3N5S1d5RFgwMFpzLW5OcVZob0xlSXZYQzRubldkSk1aNnJvZ3h5" + "UVEifSwibm9uY2UiOiJub25jZSIsInVybCI6InVybCJ9" ) tt := []struct { alg, phead string pub crypto.PublicKey stdsig, jwsig string }{ {"ES256", es256phead, testKeyEC.Public(), es256stdsig, es256jwsig}, {"RS256", rs256phead, testKey.Public(), testsig, testsig}, } for _, tc := range tt { tc := tc t.Run(tc.alg, func(t *testing.T) { stdsig, err := base64.RawStdEncoding.DecodeString(tc.stdsig) if err != nil { t.Errorf("couldn't decode test vector: %v", err) } signer := &customTestSigner{ sig: stdsig, pub: tc.pub, } b, err := jwsEncodeJSON(claims, signer, noKeyID, "nonce", "url") if err != nil { t.Fatal(err) } var j jsonWebSignature if err := json.Unmarshal(b, &j); err != nil { t.Fatal(err) } if j.Protected != tc.phead { t.Errorf("j.Protected = %q\nwant %q", j.Protected, tc.phead) } if j.Payload != payload { t.Errorf("j.Payload = %q\nwant %q", j.Payload, payload) } if j.Sig != tc.jwsig { t.Errorf("j.Sig = %q\nwant %q", j.Sig, tc.jwsig) } }) } } func TestJWSWithMAC(t *testing.T) { // Example from RFC 7520 Section 4.4.3. // https://tools.ietf.org/html/rfc7520#section-4.4.3 b64Key := "hJtXIZ2uSN5kbQfbtTNWbpdmhkV8FJG-Onbc6mxCcYg" rawPayload := []byte("It\xe2\x80\x99s a dangerous business, Frodo, going out your " + "door. You step onto the road, and if you don't keep your feet, " + "there\xe2\x80\x99s no knowing where you might be swept off " + "to.") protected := "eyJhbGciOiJIUzI1NiIsImtpZCI6IjAxOGMwYWU1LTRkOWItNDcxYi1iZmQ2LW" + "VlZjMxNGJjNzAzNyJ9" payload := "SXTigJlzIGEgZGFuZ2Vyb3VzIGJ1c2luZXNzLCBGcm9kbywg" + "Z29pbmcgb3V0IHlvdXIgZG9vci4gWW91IHN0ZXAgb250byB0aGUgcm9h" + "ZCwgYW5kIGlmIHlvdSBkb24ndCBrZWVwIHlvdXIgZmVldCwgdGhlcmXi" + "gJlzIG5vIGtub3dpbmcgd2hlcmUgeW91IG1pZ2h0IGJlIHN3ZXB0IG9m" + "ZiB0by4" sig := "s0h6KThzkfBBBkLspW1h84VsJZFTsPPqMDA7g1Md7p0" key, err := base64.RawURLEncoding.DecodeString(b64Key) if err != nil { t.Fatalf("unable to decode key: %q", b64Key) } got, err := jwsWithMAC(key, "018c0ae5-4d9b-471b-bfd6-eef314bc7037", "", rawPayload) if err != nil { t.Fatalf("jwsWithMAC() = %q", err) } if got.Protected != protected { t.Errorf("got.Protected = %q\nwant %q", got.Protected, protected) } if got.Payload != payload { t.Errorf("got.Payload = %q\nwant %q", got.Payload, payload) } if got.Sig != sig { t.Errorf("got.Signature = %q\nwant %q", got.Sig, sig) } } func TestJWSWithMACError(t *testing.T) { p := "{}" if _, err := jwsWithMAC(nil, "", "", []byte(p)); err == nil { t.Errorf("jwsWithMAC(nil, ...) = success; want err") } } func TestJWKThumbprintRSA(t *testing.T) { // Key example from RFC 7638 const base64N = "0vx7agoebGcQSuuPiLJXZptN9nndrQmbXEps2aiAFbWhM78LhWx4cbbfAAt" + "VT86zwu1RK7aPFFxuhDR1L6tSoc_BJECPebWKRXjBZCiFV4n3oknjhMstn6" + "4tZ_2W-5JsGY4Hc5n9yBXArwl93lqt7_RN5w6Cf0h4QyQ5v-65YGjQR0_FD" + "W2QvzqY368QQMicAtaSqzs8KJZgnYb9c7d0zgdAZHzu6qMQvRL5hajrn1n9" + "1CbOpbISD08qNLyrdkt-bFTWhAI4vMQFh6WeZu0fM4lFd2NcRwr3XPksINH" + "aQ-G_xBniIqbw0Ls1jF44-csFCur-kEgU8awapJzKnqDKgw" const base64E = "AQAB" const expected = "NzbLsXh8uDCcd-6MNwXF4W_7noWXFZAfHkxZsRGC9Xs" b, err := base64.RawURLEncoding.DecodeString(base64N) if err != nil { t.Fatalf("Error parsing example key N: %v", err) } n := new(big.Int).SetBytes(b) b, err = base64.RawURLEncoding.DecodeString(base64E) if err != nil { t.Fatalf("Error parsing example key E: %v", err) } e := new(big.Int).SetBytes(b) pub := &rsa.PublicKey{N: n, E: int(e.Uint64())} th, err := JWKThumbprint(pub) if err != nil { t.Error(err) } if th != expected { t.Errorf("thumbprint = %q; want %q", th, expected) } } func TestJWKThumbprintEC(t *testing.T) { // Key example from RFC 7520 // expected was computed with // printf '{"crv":"P-521","kty":"EC","x":"","y":""}' | \ // openssl dgst -binary -sha256 | b64raw const ( base64X = "AHKZLLOsCOzz5cY97ewNUajB957y-C-U88c3v13nmGZx6sYl_oJXu9A5RkT" + "KqjqvjyekWF-7ytDyRXYgCF5cj0Kt" base64Y = "AdymlHvOiLxXkEhayXQnNCvDX4h9htZaCJN34kfmC6pV5OhQHiraVySsUda" + "QkAgDPrwQrJmbnX9cwlGfP-HqHZR1" expected = "dHri3SADZkrush5HU_50AoRhcKFryN-PI6jPBtPL55M" ) b, err := base64.RawURLEncoding.DecodeString(base64X) if err != nil { t.Fatalf("Error parsing example key X: %v", err) } x := new(big.Int).SetBytes(b) b, err = base64.RawURLEncoding.DecodeString(base64Y) if err != nil { t.Fatalf("Error parsing example key Y: %v", err) } y := new(big.Int).SetBytes(b) pub := &ecdsa.PublicKey{Curve: elliptic.P521(), X: x, Y: y} th, err := JWKThumbprint(pub) if err != nil { t.Error(err) } if th != expected { t.Errorf("thumbprint = %q; want %q", th, expected) } } func TestJWKThumbprintErrUnsupportedKey(t *testing.T) { _, err := JWKThumbprint(struct{}{}) if err != ErrUnsupportedKey { t.Errorf("err = %q; want %q", err, ErrUnsupportedKey) } } acme-3.6.1/misc_test.go000066400000000000000000000017361462572010400147570ustar00rootroot00000000000000package acme import "testing" func TestWildcard(t *testing.T) { d := "*." + randString() + ".com" account, order, _ := makeOrderFinalised(t, []string{ChallengeTypeDNS01}, Identifier{Type: "dns", Value: d}) certs, err := testClient.FetchCertificates(account, order.Certificate) if err != nil { t.Fatalf("error fetch cert: %v", err) } if len(certs) == 0 { t.Fatal("no certs") } if err := certs[0].VerifyHostname(d); err != nil { t.Fatalf("error verifying hostname %s: %v", d, err) } } func TestWildcardDNSAccount(t *testing.T) { d := "*." + randString() + ".com" account, order, _ := makeOrderFinalised(t, []string{ChallengeTypeDNSAccount01}, Identifier{Type: "dns", Value: d}) certs, err := testClient.FetchCertificates(account, order.Certificate) if err != nil { t.Fatalf("error fetch cert: %v", err) } if len(certs) == 0 { t.Fatal("no certs") } if err := certs[0].VerifyHostname(d); err != nil { t.Fatalf("error verifying hostname %s: %v", d, err) } } acme-3.6.1/nonce.go000066400000000000000000000012561462572010400140640ustar00rootroot00000000000000package 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 } acme-3.6.1/nonce_test.go000066400000000000000000000007401462572010400151200ustar00rootroot00000000000000package acme import ( "testing" ) func TestNonceStack(t *testing.T) { ns := nonceStack{} ns.push("test") if len(ns.stack) != 1 { t.Fatalf("expected stack size of 1, got: %d", len(ns.stack)) } nonce := ns.pop() if nonce != "test" { t.Fatalf("bad nonce returned from stack, expected %q got %q", "test", nonce) } if nonce := ns.pop(); nonce != "" { t.Fatalf("expected no nonce, got: %v", nonce) } if len(ns.stack) != 0 { t.Fatal("expected empty stack") } } acme-3.6.1/options.go000066400000000000000000000124321462572010400144530ustar00rootroot00000000000000package acme import ( "crypto" "crypto/hmac" "crypto/tls" "crypto/x509" "encoding/base64" "encoding/json" "errors" "fmt" "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 } } // WithRootCerts sets the httpclient transport to use a given certpool for root certs func WithRootCerts(pool *x509.CertPool) OptionFunc { return func(client *Client) error { client.httpClient.Transport = &http.Transport{ TLSClientConfig: &tls.Config{ RootCAs: pool, }, } return nil } } // NewAccountOptionFunc function prototype for passing options to NewClient type NewAccountOptionFunc func(crypto.Signer, *Account, *NewAccountRequest, Client) error // NewAcctOptOnlyReturnExisting sets the new client request to only return existing accounts func NewAcctOptOnlyReturnExisting() NewAccountOptionFunc { return func(privateKey crypto.Signer, account *Account, request *NewAccountRequest, client Client) error { request.OnlyReturnExisting = true return nil } } // NewAcctOptAgreeTOS sets the new account request as agreeing to the terms of service func NewAcctOptAgreeTOS() NewAccountOptionFunc { return func(privateKey crypto.Signer, account *Account, request *NewAccountRequest, client Client) error { request.TermsOfServiceAgreed = true return nil } } // NewAcctOptWithContacts adds contacts to a new account request func NewAcctOptWithContacts(contacts ...string) NewAccountOptionFunc { return func(privateKey crypto.Signer, account *Account, request *NewAccountRequest, client Client) error { request.Contact = contacts return nil } } // NewAcctOptExternalAccountBinding adds an external account binding to the new account request // Code adopted from jwsEncodeJSON func NewAcctOptExternalAccountBinding(binding ExternalAccountBinding) NewAccountOptionFunc { return func(privateKey crypto.Signer, account *Account, request *NewAccountRequest, client Client) error { if binding.KeyIdentifier == "" { return errors.New("acme: NewAcctOptExternalAccountBinding has no KeyIdentifier set") } if binding.MacKey == "" { return errors.New("acme: NewAcctOptExternalAccountBinding has no MacKey set") } if binding.Algorithm == "" { return errors.New("acme: NewAcctOptExternalAccountBinding has no Algorithm set") } if binding.HashFunc == 0 { return errors.New("acme: NewAcctOptExternalAccountBinding has no HashFunc set") } jwk, err := jwkEncode(privateKey.Public()) if err != nil { return fmt.Errorf("acme: external account binding error encoding public key: %v", err) } payload := base64.RawURLEncoding.EncodeToString([]byte(jwk)) phead := fmt.Sprintf(`{"alg":%q,"kid":%q,"url":%q}`, binding.Algorithm, binding.KeyIdentifier, client.Directory().NewAccount) phead = base64.RawURLEncoding.EncodeToString([]byte(phead)) decodedAccountMac, err := base64.RawURLEncoding.DecodeString(binding.MacKey) if err != nil { return fmt.Errorf("acme: external account binding error decoding mac key: %v", err) } macHash := hmac.New(binding.HashFunc.New, decodedAccountMac) if _, err := macHash.Write([]byte(phead + "." + payload)); err != nil { return err } enc := struct { Protected string `json:"protected"` Payload string `json:"payload"` Sig string `json:"signature"` }{ Protected: phead, Payload: payload, Sig: base64.RawURLEncoding.EncodeToString(macHash.Sum(nil)), } jwsEab, err := json.Marshal(&enc) if err != nil { return fmt.Errorf("acme: external account binding error marshalling struct: %v", err) } request.ExternalAccountBinding = jwsEab account.ExternalAccountBinding = binding return nil } } acme-3.6.1/options_test.go000066400000000000000000000135751462572010400155230ustar00rootroot00000000000000package acme import ( "crypto" "net/http" "reflect" "strings" "testing" "time" ) func TestWithHTTPTimeout(t *testing.T) { acmeClient := Client{httpClient: http.DefaultClient} timeout := 30 * time.Second opt := WithHTTPTimeout(timeout) if err := opt(&acmeClient); err != nil { t.Fatalf("unexpected error: %v", err) } if timeout != acmeClient.httpClient.Timeout { t.Fatalf("timeout not set, expected %v, got %v", timeout, acmeClient.httpClient.Timeout) } } func TestWithInsecureSkipVerify(t *testing.T) { acmeClient := Client{httpClient: http.DefaultClient} opt := WithInsecureSkipVerify() if err := opt(&acmeClient); err != nil { t.Fatalf("unexpected error: %v", err) } tr := acmeClient.httpClient.Transport.(*http.Transport) if !tr.TLSClientConfig.InsecureSkipVerify { t.Fatalf("InsecureSkipVerify not set") } } func TestWithAcceptLanguage(t *testing.T) { acmeClient := Client{httpClient: http.DefaultClient} acceptLanguage := "de" opt := WithAcceptLanguage(acceptLanguage) if err := opt(&acmeClient); err != nil { t.Fatalf("unexpected error: %v", err) } if acceptLanguage != acmeClient.acceptLanguage { t.Fatalf("accept language not set, expected %v, got %v", acceptLanguage, acmeClient.acceptLanguage) } } func TestWithRetryCount(t *testing.T) { acmeClient := Client{httpClient: http.DefaultClient} retryCount := 10 opt := WithRetryCount(retryCount) if err := opt(&acmeClient); err != nil { t.Fatalf("unexpected error: %v", err) } if retryCount != acmeClient.retryCount { t.Fatalf("retry count not set, expected %v, got %v", retryCount, acmeClient.retryCount) } opt2 := WithRetryCount(-100) if err := opt2(&acmeClient); err == nil { t.Fatal("expected error, got none") } } func TestWithUserAgentSuffix(t *testing.T) { acmeClient := Client{httpClient: http.DefaultClient} suffix := "hi2u" opt := WithUserAgentSuffix(suffix) if err := opt(&acmeClient); err != nil { t.Fatalf("unexpected error: %v", err) } if suffix != acmeClient.userAgentSuffix { t.Fatalf("user agent suffix not set, expected %v, got %v", suffix, acmeClient.userAgentSuffix) } } func TestWithHTTPClient(t *testing.T) { acmeClient := Client{} opt1 := WithHTTPClient(nil) if err := opt1(&acmeClient); err == nil { t.Fatal("expected error, got none") } opt2 := WithHTTPClient(http.DefaultClient) suffix := &http.Client{} if err := opt2(&acmeClient); err != nil { t.Fatalf("unexpected error: %v", err) } if reflect.TypeOf(suffix).Kind() != reflect.TypeOf(acmeClient.httpClient).Kind() { t.Fatalf("http client suffix not set, expected %v, got %v", suffix, acmeClient.httpClient) } } func TestNewAcctOptOnlyReturnExisting(t *testing.T) { r := NewAccountRequest{} f := NewAcctOptOnlyReturnExisting() err := f(nil, nil, &r, Client{}) if err != nil { t.Fatalf("expected no error, got: %v", err) } if !r.OnlyReturnExisting { t.Fatal("OnlyReturnExisting not set") } } func TestNewAcctOptAgreeTOS(t *testing.T) { r := NewAccountRequest{} f := NewAcctOptAgreeTOS() err := f(nil, nil, &r, Client{}) if err != nil { t.Fatalf("expected no error, got: %v", err) } if !r.TermsOfServiceAgreed { t.Fatal("TermsOfServiceAgreed not set") } } func TestNewAcctOptWithContacts(t *testing.T) { r := NewAccountRequest{} f := NewAcctOptWithContacts("hello") err := f(nil, nil, &r, Client{}) if err != nil { t.Fatalf("expected no error, got: %v", err) } if len(r.Contact) != 1 && r.Contact[0] != "hello" { t.Fatalf(`expected contact "hello" got: %v`, r.Contact[0]) } } func TestNewAcctOptExternalAccountBinding(t *testing.T) { tests := []struct { name string binding ExternalAccountBinding signer crypto.Signer account *Account request *NewAccountRequest client Client expectsError bool errorStr string }{ { name: "empty binding", expectsError: true, errorStr: "no KeyIdentifier set", }, { name: "empty mac", binding: ExternalAccountBinding{ KeyIdentifier: "rubbish", }, expectsError: true, errorStr: "no MacKey set", }, { name: "empty algo", binding: ExternalAccountBinding{ KeyIdentifier: "rubbish", MacKey: "rubbish", }, expectsError: true, errorStr: "no Algorithm set", }, { name: "empty hashfunc", binding: ExternalAccountBinding{ KeyIdentifier: "rubbish", MacKey: "rubbish", Algorithm: "rubbish", }, expectsError: true, errorStr: "no HashFunc set", }, { name: "unknown key type", binding: ExternalAccountBinding{ KeyIdentifier: "rubbish", MacKey: "rubbish", Algorithm: "rubbish", HashFunc: crypto.SHA256, }, signer: errSigner{}, expectsError: true, errorStr: "unknown key type", }, { name: "invalid mac", binding: ExternalAccountBinding{ KeyIdentifier: "rubbish", MacKey: "!!!!", Algorithm: "rubbish", HashFunc: crypto.SHA256, }, signer: makePrivateKey(t), expectsError: true, errorStr: "error decoding mac", }, { name: "ok", binding: ExternalAccountBinding{ KeyIdentifier: "rubbish", MacKey: "rubbish", Algorithm: "rubbish", HashFunc: crypto.SHA256, }, signer: makePrivateKey(t), account: &Account{}, request: &NewAccountRequest{}, }, } for i, ct := range tests { f := NewAcctOptExternalAccountBinding(ct.binding) err := f(ct.signer, ct.account, ct.request, ct.client) if ct.expectsError && err == nil { t.Errorf("decodeCertificateChain test %d %q expected error, got none", i, ct.name) } if !ct.expectsError && err != nil { t.Errorf("decodeCertificateChain test %d %q expected no error, got: %v", i, ct.name, err) } if err != nil && ct.errorStr != "" && !strings.Contains(err.Error(), ct.errorStr) { t.Errorf("AccoudecodeCertificateChainntKeyChange test %d %q error doesnt contain %q: %s", i, ct.name, ct.errorStr, err.Error()) } } } acme-3.6.1/order.go000066400000000000000000000152611462572010400140760ustar00rootroot00000000000000package acme import ( "crypto/x509" "encoding/base64" "errors" "fmt" "net/http" "time" ) // NewOrder initiates a new order for a new certificate. This method does not use ACME Renewal Info. func (c Client) NewOrder(account Account, identifiers []Identifier) (Order, error) { return c.ReplacementOrder(account, nil, identifiers) } // NewOrderDomains takes a list of domain dns identifiers for a new certificate. Essentially a helper function. func (c Client) NewOrderDomains(account Account, domains ...string) (Order, error) { var identifiers []Identifier for _, d := range domains { identifiers = append(identifiers, Identifier{Type: "dns", Value: d}) } return c.ReplacementOrder(account, nil, identifiers) } // ReplacementOrder takes an existing *x509.Certificate and initiates a new // order for a new certificate, but with the order being marked as a // replacement. Replacement orders which are valid replacements are (currently) // exempt from Let's Encrypt NewOrder rate limits, but may not be exempt from // other ACME CAs ACME Renewal Info implementations. At least one identifier // must match the list of identifiers from the parent order to be considered as // a valid replacement order. // See https://datatracker.ietf.org/doc/html/draft-ietf-acme-ari-03#section-5 func (c Client) ReplacementOrder(account Account, oldCert *x509.Certificate, identifiers []Identifier) (Order, error) { // If an old cert being replaced is present and the acme directory doesn't list a RenewalInfo endpoint, // throw an error. This endpoint being present indicates support for ARI. if oldCert != nil && c.dir.RenewalInfo == "" { return Order{}, ErrRenewalInfoNotSupported } // 'replaces' is specifically listed as 'omitempty' so the json encoder doesn't include this key // if the ari oldCert is nil newOrderReq := struct { Identifiers []Identifier `json:"identifiers"` Replaces string `json:"replaces,omitempty"` }{ Identifiers: identifiers, } newOrderResp := Order{} // If present, add the ari cert ID from the original/old certificate if oldCert != nil { replacesCertID, err := GenerateARICertID(oldCert) if err != nil { return Order{}, fmt.Errorf("acme: error generating replacement certificate id: %v", err) } newOrderReq.Replaces = replacesCertID newOrderResp.Replaces = replacesCertID // server does not appear to set this currently? } // Submit the order resp, err := c.post(c.dir.NewOrder, account.URL, account.PrivateKey, newOrderReq, &newOrderResp, http.StatusCreated) if err != nil { return newOrderResp, err } defer resp.Body.Close() newOrderResp.URL = resp.Header.Get("Location") return newOrderResp, nil } // 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 its 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") updateOrder := func(resp *http.Response) (bool, error) { if finished, err := checkFinalizedOrderStatus(order); finished { return true, err } retryAfter, err := parseRetryAfter(resp.Header.Get("Retry-After")) if err != nil { return false, fmt.Errorf("acme: error parsing retry-after header: %v", err) } order.RetryAfter = retryAfter return false, nil } if finished, err := updateOrder(resp); finished || err != nil { return order, err } fetchOrder := func() (bool, error) { resp, err := c.post(order.URL, account.URL, account.PrivateKey, "", &order, http.StatusOK) if err != nil { return false, nil } return updateOrder(resp) } if !c.IgnoreRetryAfter && !order.RetryAfter.IsZero() { _, pollTimeout := c.getPollingDurations() end := time.Now().Add(pollTimeout) for { if time.Now().After(end) { return order, errors.New("acme: finalized order timeout") } diff := time.Until(order.RetryAfter) _, pollTimeout := c.getPollingDurations() if diff > pollTimeout { return order, fmt.Errorf("acme: Retry-After (%v) longer than poll timeout (%v)", diff, c.PollTimeout) } if diff > 0 { time.Sleep(diff) } if finished, err := fetchOrder(); finished || err != nil { return order, err } } } if !c.IgnoreRetryAfter { 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 finished, err := fetchOrder(); finished || err != nil { return order, err } } } return order, err } acme-3.6.1/order_test.go000066400000000000000000000110041462572010400151240ustar00rootroot00000000000000package acme import ( "crypto/x509" "reflect" "strings" "testing" ) func TestClient_NewOrder(t *testing.T) { key := makePrivateKey(t) account, err := testClient.NewAccount(key, false, true) if err != nil { t.Fatalf("unexpected error making account: %v", err) } identifiers := []Identifier{{"dns", randString() + ".com"}} order, err := testClient.NewOrder(account, identifiers) if err != nil { t.Fatalf("unexpected error making order: %v", err) } if !reflect.DeepEqual(order.Identifiers, identifiers) { t.Fatalf("order identifiers mismatch, identifiers: %+v, order identifiers: %+v", identifiers, order.Identifiers) } badIdentifiers := []Identifier{{"bad", randString() + ".com"}} _, err = testClient.NewOrder(account, badIdentifiers) if err == nil { t.Fatal("expected error, got none") } if _, ok := err.(Problem); !ok { t.Fatalf("expected Problem, got: %v - %v", reflect.TypeOf(err), err) } } func TestClient_FetchOrder(t *testing.T) { account, order := makeOrder(t) if _, err := testClient.FetchOrder(account, testClient.Directory().URL+"/asdasdasd"); err == nil { t.Fatal("expected error, got none") } fetchedOrder, err := testClient.FetchOrder(account, order.URL) if err != nil { t.Fatalf("unexpected error fetching order: %v", err) } // boulder seems to return slightly different expiry times, workaround for deepequal check fetchedOrder.Expires = order.Expires if !reflect.DeepEqual(order, fetchedOrder) { t.Fatalf("fetched order different to order, order: %+v, fetchedOrder: %+v", order, fetchedOrder) } } func TestClient_FinalizeOrder(t *testing.T) { makeOrderFinalised(t, nil) } func Test_checkFinalizedOrderStatus(t *testing.T) { tests := []struct { Order Order Finished bool HasError bool ErrorString string }{ { Order: Order{Status: "invalid"}, Finished: true, HasError: true, ErrorString: "no error provided", }, { Order: Order{Status: "invalid", Error: Problem{Type: "blahblahblah"}}, Finished: true, HasError: true, ErrorString: "blahblahblah", }, { Order: Order{Status: "pending"}, Finished: true, HasError: true, ErrorString: "not fulfilled", }, { Order: Order{Status: "ready"}, Finished: true, HasError: true, ErrorString: "unexpected", }, { Order: Order{Status: "processing"}, Finished: false, HasError: false, }, { Order: Order{Status: "valid"}, Finished: true, HasError: false, }, { Order: Order{Status: "asdfasdf"}, Finished: true, HasError: true, ErrorString: "unknown order status", }, { Order: Order{}, Finished: true, HasError: true, ErrorString: "unknown order status", }, } for _, ct := range tests { finished, err := checkFinalizedOrderStatus(ct.Order) if ct.Finished != finished { t.Errorf("finished mismatched, expected %t, got %t", ct.Finished, finished) } if ct.HasError && err == nil { t.Errorf("order %v expected error, got none", ct.Order) } if !ct.HasError && err != nil { t.Errorf("order %v expected no error, got: %v", ct.Order, err) } if len(ct.ErrorString) > 0 { if err == nil { t.Fatalf("expected error, got none") } if !strings.Contains(err.Error(), ct.ErrorString) { t.Fatalf("expected error string %q not found in: %s", ct.ErrorString, err.Error()) } } } } func TestClient_ReplacementOrder(t *testing.T) { if testClient.dir.RenewalInfo == "" { t.Skip("acme server does not support ari renewals") return } account, order, _ := makeOrderFinalised(t, nil) tc2 := testClient tc2.dir.RenewalInfo = "" certs, err := tc2.FetchCertificates(account, order.Certificate) if err != nil { t.Fatalf("unexpected error fetching certificates: %v", err) } if _, err := tc2.ReplacementOrder(account, certs[0], order.Identifiers); err == nil { t.Fatalf("expected error, got none") } else if err != ErrRenewalInfoNotSupported { t.Fatalf("unexpected error replacing order: %v", err) } if _, err := testClient.ReplacementOrder(account, &x509.Certificate{Raw: []byte{1}}, order.Identifiers); err == nil { t.Fatalf("expected error, got none") } newOrder, err := testClient.ReplacementOrder(account, certs[0], order.Identifiers) if err != nil { t.Fatalf("unexpected error replacing certificates: %v", err) } if !reflect.DeepEqual(newOrder.Identifiers, order.Identifiers) { t.Fatalf("unexpected difference in replaced order identifiers") } if newOrder.Replaces == "" { t.Fatalf("replace order identifier is empty") } } acme-3.6.1/problem.go000066400000000000000000000033501462572010400144170ustar00rootroot00000000000000package acme import ( "encoding/json" "fmt" "io/ioutil" "net/http" ) // Problem document as defined in, // https://tools.ietf.org/html/rfc7807 // Problem represents an error returned by an acme server. type Problem struct { Type string `json:"type"` Detail string `json:"detail,omitempty"` Status int `json:"status,omitempty"` Instance string `json:"instance,omitempty"` SubProblems []SubProblem `json:"subproblems,omitempty"` } type SubProblem struct { Type string `json:"type"` Detail string `json:"detail"` Identifier Identifier `json:"identifier"` } // 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 } acme-3.6.1/problem_test.go000066400000000000000000000031641462572010400154610ustar00rootroot00000000000000package acme import ( "net/http" "strings" "testing" ) func TestCheckError(t *testing.T) { errorTests := []struct { Name string URL string ExpectedStatus []int }{ { Name: "test expecting http 202, but got 200", URL: testClient.Directory().URL, ExpectedStatus: []int{202}, }, { Name: "test acme error expecting ok", URL: testClient.Directory().NewAccount, ExpectedStatus: []int{http.StatusOK}, }, { Name: "test http error expecting ok", URL: testClient.Directory().NewAccount + "/asdasdasdasdasd", ExpectedStatus: []int{http.StatusOK}, }, } for _, currentTest := range errorTests { resp, err := http.Get(currentTest.URL) if err != nil { t.Fatalf("error %s: expected no error, got: %v", currentTest.Name, err) } if err := checkError(resp, currentTest.ExpectedStatus...); err == nil { t.Fatalf("error %s: expected error, got none", currentTest.Name) } } resp, err := http.Get(testClient.Directory().URL) if err != nil { t.Fatalf("expected no error, got: %v", err) } if err := checkError(resp, http.StatusOK); err != nil { t.Fatalf("expected no error, got: %v", err) } } func TestProblem_Error(t *testing.T) { err := Problem{ Status: 123, Type: "type", Detail: "detail", Instance: "instance", SubProblems: []SubProblem{ { Type: "type2", Detail: "detail", Identifier: Identifier{"DNS", randString() + ".com"}, }, }, } s := error(err).Error() if !strings.HasPrefix(s, "acme: error code") { t.Fatalf("unexpected acme error: %v", err) } } acme-3.6.1/types.go000066400000000000000000000210411462572010400141200ustar00rootroot00000000000000package acme import ( "crypto" "encoding/json" "errors" "net/http" "time" ) var ( // ErrUnsupportedKey is returned when an unsupported key type is encountered. ErrUnsupportedKey = errors.New("acme: unknown key type; only RSA and ECDSA are supported") // ErrRenewalInfoNotSupported is returned by Client.GetRenewalInfo if the // renewal info entry isn't present on the acme directory (ie, it's not // supported by the acme server) ErrRenewalInfoNotSupported = errors.New("renewal information endpoint not supported") ) // Different possible challenge types provided by an ACME server. // See https://tools.ietf.org/html/rfc8555#section-9.7.8 const ( ChallengeTypeDNS01 = "dns-01" ChallengeTypeDNSAccount01 = "dns-account-01" ChallengeTypeHTTP01 = "http-01" ChallengeTypeTLSALPN01 = "tls-alpn-01" // ChallengeTypeTLSSNI01 is deprecated and should not be used. // See: https://community.letsencrypt.org/t/important-what-you-need-to-know-about-tls-sni-validation-issues/50811 ChallengeTypeTLSSNI01 = "tls-sni-01" ) // Constants used for certificate revocation, used for RevokeCertificate // See https://tools.ietf.org/html/rfc5280#section-5.3.1 const ( ReasonUnspecified = iota // 0 ReasonKeyCompromise // 1 ReasonCaCompromise // 2 ReasonAffiliationChanged // 3 ReasonSuperseded // 4 ReasonCessationOfOperation // 5 ReasonCertificateHold // 6 _ // 7 - Unused ReasonRemoveFromCRL // 8 ReasonPrivilegeWithdrawn // 9 ReasonAaCompromise // 10 ) // Directory object as returned from the client's directory url upon creation of client. // See https://tools.ietf.org/html/rfc8555#section-7.1.1 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 // https://datatracker.ietf.org/doc/html/draft-ietf-acme-ari-03 RenewalInfo string `json:"renewalInfo"` // url to renewal info 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 // IgnorePolling does not use any simple polling in order finalisation IgnorePolling bool // IgnoreRetryAfter does not use the retry-after header in order finalisation IgnoreRetryAfter bool } // Account structure representing fields in an account object. // See https://tools.ietf.org/html/rfc8555#section-7.1.2 // See also https://tools.ietf.org/html/rfc8555#section-9.7.1 type Account struct { Status string `json:"status"` Contact []string `json:"contact"` 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:"-"` // Thumbprint is the SHA-256 digest JWK_Thumbprint of the account key. // See https://tools.ietf.org/html/rfc8555#section-8.1 Thumbprint string `json:"-"` // ExternalAccountBinding is populated when using the NewAcctOptExternalAccountBinding option for NewAccountOption // and is otherwise empty. Not populated when account is fetched or created otherwise. ExternalAccountBinding ExternalAccountBinding `json:"-"` } // ExternalAccountBinding holds the key identifier and mac key provided for use in servers that support/require // external account binding. // The MacKey is a base64url-encoded string. // Algorithm is a "MAC-based algorithm" as per RFC8555. Typically this is either, // - "HS256" for HashFunc: crypto.SHA256 // - "HS384" for HashFunc: crypto.SHA384 // - "HS512" for HashFunc: crypto.SHA512 // // However this is dependent on the acme server in question and is provided here to give more options for future compatibility. type ExternalAccountBinding struct { KeyIdentifier string `json:"-"` MacKey string `json:"-"` Algorithm string `json:"-"` HashFunc crypto.Hash `json:"-"` } // Identifier object used in order and authorization objects // See https://tools.ietf.org/html/rfc8555#section-7.1.4 type Identifier struct { Type string `json:"type"` Value string `json:"value"` } // Order object returned when fetching or creating a new order. // See https://tools.ietf.org/html/rfc8555#section-7.1.3 type Order struct { Status string `json:"status"` Expires time.Time `json:"expires"` Identifiers []Identifier `json:"identifiers"` NotBefore time.Time `json:"notBefore"` NotAfter time.Time `json:"notAfter"` Error Problem `json:"error"` Authorizations []string `json:"authorizations"` Finalize string `json:"finalize"` Certificate string `json:"certificate"` // URL for the order object. // Provided by the rel="Location" Link http header URL string `json:"-"` // RetryAfter is the http Retry-After header from the order response RetryAfter time.Time `json:"-"` // Replaces (optional, string): A string uniquely identifying a // previously-issued certificate which this order is intended to replace. // See https://datatracker.ietf.org/doc/html/draft-ietf-acme-ari-03#section-5 Replaces string `json:"replaces,omitempty"` } // Authorization object returned when fetching an authorization in an order. // See https://tools.ietf.org/html/rfc8555#section-7.1.4 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. // See https://tools.ietf.org/html/rfc8555#section-7.1.5 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:"-"` } // NewAccountRequest object used for submitting a request for a new account. // Primarily used with NewAccountOptionFunc type NewAccountRequest struct { OnlyReturnExisting bool `json:"onlyReturnExisting"` TermsOfServiceAgreed bool `json:"termsOfServiceAgreed"` Contact []string `json:"contact,omitempty"` ExternalAccountBinding json.RawMessage `json:"externalAccountBinding"` } // RenewalInfo stores the server-provided suggestions on when to renew // certificates. type RenewalInfo struct { SuggestedWindow struct { Start time.Time `json:"start"` End time.Time `json:"end"` } `json:"suggestedWindow"` ExplanationURL string `json:"explanationURL,omitempty"` RetryAfter time.Time `json:"-"` } acme-3.6.1/utility_test.go000066400000000000000000000370451462572010400155310ustar00rootroot00000000000000package acme import ( "bytes" "crypto" "crypto/ecdsa" "crypto/elliptic" crand "crypto/rand" "crypto/sha256" "crypto/tls" "crypto/x509" "crypto/x509/pkix" "encoding/base32" "encoding/json" "encoding/pem" "fmt" "go/build" "io/ioutil" "log" mrand "math/rand" "mime" "net/http" "os" "path/filepath" "strings" "testing" "time" ) type clientMeta struct { Software string Options []OptionFunc } const ( clientBoulder = "boulder" clientPebble = "pebble" ) var ( testClient Client testClientMeta clientMeta ) func TestMain(m *testing.M) { mrand.Seed(time.Now().UnixNano()) var err error // attempt to use manually supplied directory url first if dir := os.Getenv("ACME_DIRECTORY"); dir != "" { var opts []OptionFunc if os.Getenv("ACME_STRICT") == "" { opts = append(opts, WithInsecureSkipVerify()) } testClient, err = NewClient(dir, opts...) if err != nil { panic("error creating manual test client at '" + dir + "' - " + err.Error()) } return } roots := fetchRoot() pool := x509.NewCertPool() pool.AppendCertsFromPEM(roots) directories := map[string]clientMeta{ "https://localhost:14000/dir": { Software: clientPebble, Options: []OptionFunc{WithRootCerts(pool)}, }, "https://localhost:4431/directory": { Software: clientBoulder, Options: []OptionFunc{WithRootCerts(pool)}, }, "http://localhost:4001/directory": { Software: clientBoulder, }, } for k, v := range directories { testClient, err = NewClient(k, v.Options...) if err != nil { log.Printf("error creating client for %s - %v", k, err) continue } testClientMeta = v log.Printf("using %s directory at: %s", v.Software, k) os.Exit(m.Run()) } log.Fatal("no acme ca available") } func randString() string { min := int('a') max := int('z') n := mrand.Intn(10) + 10 b := make([]byte, n) for i := 0; i < n; i++ { b[i] = byte(mrand.Intn(max-min) + min) } return string(b) } func makePrivateKey(t *testing.T) crypto.Signer { privKey, err := ecdsa.GenerateKey(elliptic.P256(), crand.Reader) if err != nil { t.Fatalf("error creating account private key: %v", err) } return privKey } func makeAccount(t *testing.T) Account { key := makePrivateKey(t) account, err := testClient.NewAccount(key, false, true) if err != nil { t.Fatalf("error creating new account: %v", err) } return account } func makeOrder(t *testing.T, identifiers ...Identifier) (Account, Order) { if len(identifiers) == 0 { identifiers = []Identifier{{Type: "dns", Value: randString() + ".com"}} } account := makeAccount(t) order, err := testClient.NewOrder(account, identifiers) if err != nil { t.Fatalf("error making order: %v", err) } if order.Status != "pending" { t.Fatalf("expected pending order status, got: %s", order.Status) } if len(order.Authorizations) != len(identifiers) { t.Fatalf("expected %d authorizations, got: %d", len(identifiers), len(order.Authorizations)) } return account, order } func makeOrderFinalised(t *testing.T, supportedChalTypes []string, identifiers ...Identifier) (Account, Order, crypto.Signer) { if len(supportedChalTypes) == 0 { supportedChalTypes = []string{ChallengeTypeDNS01, ChallengeTypeHTTP01} } acct, order := makeOrder(t, identifiers...) err := validateChallenges(t, order, acct, supportedChalTypes, false) if err != nil { t.Fatalf("unexpected error: %v", err) } updatedOrder, err := testClient.FetchOrder(acct, order.URL) if err != nil { t.Fatalf("unexpected error: %v", err) } if updatedOrder.Status != "ready" { t.Fatal("order not ready") } var domains []string for _, id := range order.Identifiers { domains = append(domains, id.Value) } csr, privKey := makeCSR(t, domains) finalizedOrder, err := testClient.FinalizeOrder(acct, order, csr) if err != nil { t.Fatalf("unexpected error: %v", err) } if finalizedOrder.Status != "valid" { t.Fatal("order not valid") } return acct, finalizedOrder, privKey } // makeReplacementOrderFinalized is a helper that fetches a certificate from the // given order, creates a replacement order for the given order, and finalizes // it. It differs from makeOrder and makeOrderFinalized in that it does not // create a new Account object each time it's called. func makeReplacementOrderFinalized(t *testing.T, order Order, account Account, supportedChalTypes []string, challengesShouldFail bool) (Order, error) { certs, err := testClient.FetchCertificates(account, order.Certificate) if err != nil { return Order{}, fmt.Errorf("expected no error, got: %v", err) } if len(certs) < 2 { return Order{}, fmt.Errorf("no certs") } replacementOrder, err := testClient.ReplacementOrder(account, certs[0], order.Identifiers) if err != nil { return Order{}, fmt.Errorf("expected no error, got: %v", err) } err = validateChallenges(t, replacementOrder, account, supportedChalTypes, challengesShouldFail) if err != nil { return Order{}, err } updatedOrder, err := testClient.FetchOrder(account, replacementOrder.URL) if err != nil { t.Fatalf("unexpected error: %v", err) } // We can ignore updatedOrder after this. if updatedOrder.Status != "ready" { t.Fatal("order not ready") } // Check that the replacement order Replaces field is populated with the // expected value. ariCertID, err := GenerateARICertID(certs[0]) if err != nil { return Order{}, fmt.Errorf("expected no error, got: %v", err) } if replacementOrder.Replaces != ariCertID { return Order{}, fmt.Errorf("%s != %s", replacementOrder.Replaces, ariCertID) } // Make sure that the replacement order shares at least one identifier as // the order it is replacing. We'll do this by sending the exact identifiers // as the original order. // See: https://datatracker.ietf.org/doc/html/draft-ietf-acme-ari-03#section-5 var domains []string for _, id := range replacementOrder.Identifiers { domains = append(domains, id.Value) } csr, _ := makeCSR(t, domains) // Issue a certificate for the replacement order. replacementOrder, err = testClient.FinalizeOrder(account, replacementOrder, csr) if err != nil { return Order{}, fmt.Errorf("unexpected error: %v", err) } if replacementOrder.Status != "valid" { return Order{}, fmt.Errorf("order not valid") } return replacementOrder, nil } // validateChallenges validates all the challenges for each authorization on the // given order or returns an error. func validateChallenges(t *testing.T, order Order, account Account, supportedChalTypes []string, challengesShouldFail bool) error { if supportedChalTypes == nil { supportedChalTypes = []string{ChallengeTypeDNS01, ChallengeTypeHTTP01} } for _, authURL := range order.Authorizations { auth, err := testClient.FetchAuthorization(account, authURL) if err != nil { return fmt.Errorf("unexpected error fetching authorization: %v", err) } if auth.Status == "valid" { continue } if auth.Status != "pending" { return fmt.Errorf("expected auth status pending, got: %v", auth.Status) } chalType := supportedChalTypes[mrand.Intn(len(supportedChalTypes))] chal, ok := auth.ChallengeMap[chalType] if !ok { t.Skipf("skipping, no supported challenge %q (%v) in challenges: %v", chalType, supportedChalTypes, auth.ChallengeTypes) } if chal.Status == "valid" { continue } if chal.Status != "pending" { return fmt.Errorf("unexpected status %q on challenge: %+v", chal.Status, chal) } if challengesShouldFail { // We want to trigger the VA's DNS resolver to return SERVFAIL or // some other type of DNS badness so we can see how the client // reacts to VA errors. for _, id := range order.Identifiers { failPreChallenge(chal, id.Value) defer failPostChallenge(chal, id.Value) } } else { preChallenge(account, auth, chal) defer postChallenge(account, auth, chal) } updatedChal, err := testClient.UpdateChallenge(account, chal) if err != nil { return fmt.Errorf("error updating challenge %s : %v", chal.URL, err) } if updatedChal.Status != "valid" { return fmt.Errorf("unexpected updated challenge status %q on challenge: %+v", updatedChal.Status, updatedChal) } } return nil } func makeCSR(t *testing.T, domains []string) (*x509.CertificateRequest, crypto.Signer) { privKey, err := ecdsa.GenerateKey(elliptic.P256(), crand.Reader) if err != nil { t.Fatalf("error generating private key: %v", err) } tpl := &x509.CertificateRequest{ SignatureAlgorithm: x509.ECDSAWithSHA256, PublicKeyAlgorithm: x509.ECDSA, PublicKey: privKey.Public(), Subject: pkix.Name{CommonName: domains[0]}, DNSNames: domains, } csrDer, err := x509.CreateCertificateRequest(crand.Reader, tpl, privKey) if err != nil { t.Fatalf("error creating certificate request: %v", err) } csr, err := x509.ParseCertificateRequest(csrDer) if err != nil { t.Fatalf("error parsing certificate request: %v", err) } return csr, privKey } func doPost(name string, req interface{}) { reqJSON, err := json.Marshal(req) if err != nil { panic(fmt.Sprintf("error marshalling boulder %s: %v", name, err)) } if _, err := http.Post("http://localhost:8055/"+name, "application/json", bytes.NewReader(reqJSON)); err != nil { panic(fmt.Sprintf("error posting boulder %s: %v", name, err)) } } // failPreChallenge causes challtestsrv to respond with bogus data/SERVFAIL for // the given domain which will trigger the VA to return a validation failure. func failPreChallenge(chal Challenge, domain string) { switch chal.Type { case ChallengeTypeDNS01: records := []string{domain, "_acme-challenge." + domain} for _, r := range records { setReq := struct { Host string `json:"host"` }{ Host: r, } doPost("set-servfail", setReq) } case ChallengeTypeHTTP01: addReq := struct { Token string `json:"token"` Content string `json:"content"` }{ Token: "bad", Content: "chall", } doPost("add-http01", addReq) addReq2 := struct { Host string `json:"host"` }{ Host: domain, } doPost("set-servfail", addReq2) default: panic("post: unsupported challenge type: " + chal.Type) } } // failPostChallenge cleans up after failPreChallenge and resets challtestsrv. func failPostChallenge(chal Challenge, domain string) { switch chal.Type { case ChallengeTypeDNS01: records := []string{domain, "_acme-challenge." + domain} for _, r := range records { setReq := struct { Host string `json:"host"` }{ Host: r, } doPost("clear-servfail", setReq) } case ChallengeTypeHTTP01: addReq := struct { Token string `json:"token"` }{ Token: "bad", } doPost("del-http01", addReq) addReq2 := struct { Host string `json:"host"` }{ Host: domain, } doPost("clear-servfail", addReq2) default: panic("post: unsupported challenge type: " + chal.Type) } } func preChallenge(acct Account, auth Authorization, chal Challenge) { switch chal.Type { case ChallengeTypeDNS01: setReq := struct { Host string `json:"host"` Value string `json:"value"` }{ Host: "_acme-challenge." + auth.Identifier.Value + ".", Value: EncodeDNS01KeyAuthorization(chal.KeyAuthorization), } doPost("set-txt", setReq) case ChallengeTypeDNSAccount01: acctHash := sha256.Sum256([]byte(acct.URL)) acctLabel := strings.ToLower(base32.StdEncoding.EncodeToString(acctHash[0:10])) scope := "host" if auth.Wildcard { scope = "wildcard" } setReq := struct { Host string `json:"host"` Value string `json:"value"` }{ Host: "_" + acctLabel + "._acme-" + scope + "-challenge." + auth.Identifier.Value + ".", Value: EncodeDNS01KeyAuthorization(chal.KeyAuthorization), } doPost("set-txt", setReq) case ChallengeTypeHTTP01: addReq := struct { Token string `json:"token"` Content string `json:"content"` }{ Token: chal.Token, Content: chal.KeyAuthorization, } doPost("add-http01", addReq) case ChallengeTypeTLSALPN01: addReq := struct { Host string `json:"host"` Content string `json:"content"` }{ Host: auth.Identifier.Value, Content: chal.KeyAuthorization, } doPost("add-tlsalpn01", addReq) default: panic("pre: unsupported challenge type: " + chal.Type) } } func postChallenge(acct Account, auth Authorization, chal Challenge) { switch chal.Type { case ChallengeTypeDNS01: host := "_acme-challenge." + auth.Identifier.Value + "." clearReq := struct { Host string `json:"host"` }{ Host: host, } doPost("clear-txt", clearReq) case ChallengeTypeDNSAccount01: acctHash := sha256.Sum256([]byte(acct.URL)) acctLabel := strings.ToLower(base32.StdEncoding.EncodeToString(acctHash[0:10])) scope := "host" if auth.Wildcard { scope = "wildcard" } host := "_" + acctLabel + "._acme-" + scope + "-challenge." + auth.Identifier.Value + "." clearReq := struct { Host string `json:"host"` }{ Host: host, } doPost("clear-txt", clearReq) case ChallengeTypeHTTP01: delReq := struct { Token string `json:"token"` }{ Token: chal.Token, } doPost("del-http01", delReq) case ChallengeTypeTLSALPN01: delReq := struct { Host string `json:"token"` }{ Host: auth.Identifier.Value, } doPost("del-tlsalpn01", delReq) default: panic("post: unsupported challenge type: " + chal.Type) } } func getPath(env, folder string) string { p := os.Getenv(env) if p != "" { return p } p = os.Getenv("GOPATH") if p != "" { return filepath.Join(p, "src", "github.com", "letsencrypt", folder) } p = build.Default.GOPATH if p != "" { return filepath.Join(p, "src", "github.com", "letsencrypt", folder) } p = os.Getenv("HOME") if p != "" { return filepath.Join(p, "go", "src", "github.com", "letsencrypt", folder) } return "" } func fetchRoot() []byte { var certPaths []string var certsPem []string boulderPath := getPath("BOULDER_PATH", "boulder") certPaths = append(certPaths, filepath.Join(boulderPath, ".hierarchy", "root-ecdsa.cert.pem")) certPaths = append(certPaths, filepath.Join(boulderPath, ".hierarchy", "root-cert-ecdsa.pem")) certPaths = append(certPaths, filepath.Join(boulderPath, ".hierarchy", "root-cert-rsa.pem")) certPaths = append(certPaths, filepath.Join(boulderPath, "test", "wfe-tls", "minica.pem")) pebblePath := getPath("PEBBLE_PATH", "pebble") // these certs are the ones used for the web server, not signing certPaths = append(certPaths, filepath.Join(pebblePath, "test", "certs", "pebble.minica.pem")) certPaths = append(certPaths, filepath.Join(pebblePath, "test", "certs", "localhost", "cert.pem")) for _, v := range certPaths { bPem, err := ioutil.ReadFile(v) if err != nil { log.Printf("error reading: %s - %v", v, err) continue } certsPem = append(certsPem, "# "+v+"\n"+strings.TrimSpace(string(bPem))) } tr := &http.Transport{ TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, } httpClient := &http.Client{Transport: tr} i := 0 for { // these are the signing roots pebbleRootURL := fmt.Sprintf("https://localhost:15000/roots/%d", i) i++ resp, err := httpClient.Get(pebbleRootURL) if err != nil { break } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { break } body, err := ioutil.ReadAll(resp.Body) if err != nil { break } mediaType, _, err := mime.ParseMediaType(resp.Header.Get("Content-Type")) if err != nil { panic(err) } switch mediaType { case "application/pem-certificate-chain": certsPem = append(certsPem, strings.TrimSpace(string(body))) case "application/pkix-cert": bPem := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: body}) certsPem = append(certsPem, strings.TrimSpace(string(bPem))) default: panic(pebbleRootURL + " unsupported content type: " + mediaType) } } if len(certsPem) == 0 { return nil } return []byte(strings.Join(certsPem, "\n")) }