Copy across WIP acme v2 golang package
This commit is contained in:
parent
b3ddc60331
commit
486cd3ae18
923
third_party/crypto/acme/acme.go
vendored
Normal file
923
third_party/crypto/acme/acme.go
vendored
Normal file
@ -0,0 +1,923 @@
|
||||
// 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 provides an implementation of the
|
||||
// Automatic Certificate Management Environment (ACME) spec.
|
||||
// See https://tools.ietf.org/html/draft-ietf-acme-acme-09 for details.
|
||||
//
|
||||
// Most common scenarios will want to use autocert subdirectory instead,
|
||||
// which provides automatic access to certificates from Let's Encrypt
|
||||
// and any other ACME-based CA.
|
||||
//
|
||||
// This package is a work in progress and makes no API stability promises.
|
||||
package acme
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto"
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"math/big"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// LetsEncryptURL is the Directory endpoint of Let's Encrypt CA.
|
||||
const LetsEncryptURL = "https://acme-v02.api.letsencrypt.org/directory"
|
||||
|
||||
const (
|
||||
// max length of a certificate chain
|
||||
maxChainLen = 5
|
||||
// max size of a certificate chain response, in bytes
|
||||
maxChainSize = (1 << 20) * maxChainLen
|
||||
|
||||
// Max number of collected nonces kept in memory.
|
||||
// Expect usual peak of 1 or 2.
|
||||
maxNonces = 100
|
||||
)
|
||||
|
||||
// Client is an ACME client.
|
||||
// The only required field is Key. An example of creating a client with a new key
|
||||
// is as follows:
|
||||
//
|
||||
// key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||
// if err != nil {
|
||||
// log.Fatal(err)
|
||||
// }
|
||||
// client := &Client{Key: key}
|
||||
//
|
||||
type Client struct {
|
||||
// Key is the account key used to register with a CA and sign requests.
|
||||
// Key.Public() must return a *rsa.PublicKey or *ecdsa.PublicKey.
|
||||
Key crypto.Signer
|
||||
|
||||
// HTTPClient optionally specifies an HTTP client to use
|
||||
// instead of http.DefaultClient.
|
||||
HTTPClient *http.Client
|
||||
|
||||
// DirectoryURL points to the CA directory endpoint.
|
||||
// If empty, LetsEncryptURL is used.
|
||||
// Mutating this value after a successful call of Client's Discover method
|
||||
// will have no effect.
|
||||
DirectoryURL string
|
||||
|
||||
noncesMu sync.Mutex
|
||||
nonces map[string]struct{} // nonces collected from previous responses
|
||||
|
||||
urlMu sync.Mutex // urlMu guards writes to dir, accountURL, ordersURL
|
||||
dir *Directory // cached result of Client's Discover method
|
||||
accountURL string
|
||||
ordersURL string
|
||||
}
|
||||
|
||||
// Discover performs ACME server discovery using c.DirectoryURL.
|
||||
//
|
||||
// It caches successful result. So, subsequent calls will not result in
|
||||
// a network round-trip. This also means mutating c.DirectoryURL after successful call
|
||||
// of this method will have no effect.
|
||||
func (c *Client) Discover(ctx context.Context) (Directory, error) {
|
||||
c.urlMu.Lock()
|
||||
defer c.urlMu.Unlock()
|
||||
if c.dir != nil {
|
||||
return *c.dir, nil
|
||||
}
|
||||
|
||||
dirURL := c.DirectoryURL
|
||||
if dirURL == "" {
|
||||
dirURL = LetsEncryptURL
|
||||
}
|
||||
res, err := c.get(ctx, dirURL)
|
||||
if err != nil {
|
||||
return Directory{}, err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
c.addNonce(res.Header)
|
||||
if res.StatusCode != http.StatusOK {
|
||||
return Directory{}, responseError(res)
|
||||
}
|
||||
|
||||
var v struct {
|
||||
NewNonce string
|
||||
NewAccount string
|
||||
NewOrder string
|
||||
NewAuthz string
|
||||
RevokeCert string
|
||||
KeyChange string
|
||||
Meta struct {
|
||||
TermsOfService string
|
||||
Website string
|
||||
CAAIdentities []string
|
||||
ExternalAccountRequired bool
|
||||
}
|
||||
}
|
||||
if err := json.NewDecoder(res.Body).Decode(&v); err != nil {
|
||||
return Directory{}, err
|
||||
}
|
||||
c.dir = &Directory{
|
||||
NewNonceURL: v.NewNonce,
|
||||
NewAccountURL: v.NewAccount,
|
||||
NewOrderURL: v.NewOrder,
|
||||
NewAuthzURL: v.NewAuthz,
|
||||
RevokeCertURL: v.RevokeCert,
|
||||
KeyChangeURL: v.KeyChange,
|
||||
Terms: v.Meta.TermsOfService,
|
||||
Website: v.Meta.Website,
|
||||
CAA: v.Meta.CAAIdentities,
|
||||
ExternalAccountRequired: v.Meta.ExternalAccountRequired,
|
||||
}
|
||||
return *c.dir, nil
|
||||
}
|
||||
|
||||
// CreateOrder creates a new certificate order. The input order argument is not
|
||||
// modified and can be built using NewOrderWithDomains.
|
||||
func (c *Client) CreateOrder(ctx context.Context, order *Order) (*Order, error) {
|
||||
if _, err := c.Discover(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req := struct {
|
||||
Identifiers []wireAuthzID `json:"identifiers"`
|
||||
NotBefore string `json:"notBefore,omitempty"`
|
||||
NotAfter string `json:"notAfter,omitempty"`
|
||||
}{
|
||||
Identifiers: make([]wireAuthzID, len(order.Identifiers)),
|
||||
}
|
||||
for i, id := range order.Identifiers {
|
||||
req.Identifiers[i] = wireAuthzID{
|
||||
Type: id.Type,
|
||||
Value: id.Value,
|
||||
}
|
||||
}
|
||||
if !order.NotBefore.IsZero() {
|
||||
req.NotBefore = order.NotBefore.Format(time.RFC3339)
|
||||
}
|
||||
if !order.NotAfter.IsZero() {
|
||||
req.NotAfter = order.NotAfter.Format(time.RFC3339)
|
||||
}
|
||||
|
||||
res, err := c.postWithJWSAccount(ctx, c.dir.NewOrderURL, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
if res.StatusCode != http.StatusCreated {
|
||||
return nil, responseError(res)
|
||||
}
|
||||
var v wireOrder
|
||||
if err := json.NewDecoder(res.Body).Decode(&v); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
l, err := resolveLocation(c.dir.NewOrderURL, res.Header)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
o := v.order(l, "")
|
||||
|
||||
if o.Status == StatusInvalid {
|
||||
return nil, OrderInvalidError{o}
|
||||
}
|
||||
return o, nil
|
||||
}
|
||||
|
||||
// FinalizeOrder finalizes an order using the Certificate Signing Request csr
|
||||
// encoded in DER format. If the order has not been fully authorized,
|
||||
// an OrderPendingError will be returned.
|
||||
//
|
||||
// After requesting finalization, FinalizOrder polls the order using WaitOrder
|
||||
// until it is finalized and then fetches the associated certificate and returns
|
||||
// it.
|
||||
//
|
||||
// Callers are encouraged to parse the returned certificate chain to ensure it
|
||||
// is valid and has the expected attributes.
|
||||
func (c *Client) FinalizeOrder(ctx context.Context, finalizeURL string, csr []byte) (der [][]byte, err error) {
|
||||
req := struct {
|
||||
CSR string `json:"csr"`
|
||||
}{
|
||||
CSR: base64.RawURLEncoding.EncodeToString(csr),
|
||||
}
|
||||
|
||||
res, err := c.postWithJWSAccount(ctx, finalizeURL, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
if res.StatusCode != http.StatusOK {
|
||||
return nil, responseError(res)
|
||||
}
|
||||
var v wireOrder
|
||||
if err := json.NewDecoder(res.Body).Decode(&v); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
l, err := resolveLocation(finalizeURL, res.Header)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
o := v.order(l, res.Header.Get("Retry-After"))
|
||||
if o.Status == StatusProcessing || o.Status == StatusPending {
|
||||
o, err = c.WaitOrder(ctx, o.URL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if o.Status != StatusValid {
|
||||
return nil, fmt.Errorf("acme: unexpected order status %q", o.Status)
|
||||
}
|
||||
|
||||
return c.getCert(ctx, o.CertificateURL)
|
||||
}
|
||||
|
||||
// GetOrder retrieves an order identified by url.
|
||||
//
|
||||
// If a caller needs to poll an order until its status is final,
|
||||
// see the WaitOrder method.
|
||||
func (c *Client) GetOrder(ctx context.Context, url string) (*Order, error) {
|
||||
res, err := c.get(ctx, url)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
if res.StatusCode != http.StatusOK {
|
||||
err = responseError(res)
|
||||
return nil, err
|
||||
}
|
||||
var v wireOrder
|
||||
if err := json.NewDecoder(res.Body).Decode(&v); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return v.order(url, res.Header.Get("Retry-After")), nil
|
||||
}
|
||||
|
||||
// WaitOrder waits for an order to transition from StatusProcessing to a final
|
||||
// state (StatusValid/StatusInvalid), it retries the request until the order is
|
||||
// final, ctx is cancelled by the caller, or an error response is received.
|
||||
//
|
||||
// It returns a non-nil Order only if its Status is StatusValid. In all other
|
||||
// cases WaitOrder returns an error. If the Status is StatusInvalid, the
|
||||
// returned error will be of type OrderInvalidError. If the status is
|
||||
// StatusPending, the returned error will be of type OrderPendingError.
|
||||
func (c *Client) WaitOrder(ctx context.Context, url string) (*Order, error) {
|
||||
sleep := timeSleeper(ctx)
|
||||
for {
|
||||
o, err := c.GetOrder(ctx, url)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
switch o.Status {
|
||||
case StatusValid:
|
||||
return o, nil
|
||||
case StatusInvalid:
|
||||
return nil, OrderInvalidError{o}
|
||||
case StatusPending:
|
||||
return nil, OrderPendingError{o}
|
||||
case StatusProcessing: // continue retry loop
|
||||
default:
|
||||
return nil, fmt.Errorf("acme: unexpected order status %q", o.Status)
|
||||
}
|
||||
if err := sleep(o.RetryAfter); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// RevokeCert revokes a previously issued certificate cert, provided in DER
|
||||
// format.
|
||||
//
|
||||
// If key is nil, the account must have been used to issue the certificate or
|
||||
// have valid authorizations for all of the identifiers in the certificate. If
|
||||
// key is provided, it must be the certificate's private key.
|
||||
func (c *Client) RevokeCert(ctx context.Context, key crypto.Signer, cert []byte, reason CRLReasonCode) error {
|
||||
if _, err := c.Discover(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
body := &struct {
|
||||
Cert string `json:"certificate"`
|
||||
Reason int `json:"reason"`
|
||||
}{
|
||||
Cert: base64.RawURLEncoding.EncodeToString(cert),
|
||||
Reason: int(reason),
|
||||
}
|
||||
var res *http.Response
|
||||
var err error
|
||||
if key == nil {
|
||||
res, err = c.postWithJWSAccount(ctx, c.dir.RevokeCertURL, body)
|
||||
} else {
|
||||
res, err = c.postWithJWSKey(ctx, key, c.dir.RevokeCertURL, body)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
if res.StatusCode != http.StatusOK {
|
||||
return responseError(res)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// CreateAccount creates a new account. It returns the account details from the
|
||||
// server and does not modify the account argument that it is called with.
|
||||
func (c *Client) CreateAccount(ctx context.Context, a *Account) (*Account, error) {
|
||||
if _, err := c.Discover(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return c.doAccount(ctx, c.dir.NewAccountURL, false, a)
|
||||
}
|
||||
|
||||
// GetAccount retrieves the account that the client is configured with.
|
||||
func (c *Client) GetAccount(ctx context.Context) (*Account, error) {
|
||||
if _, err := c.Discover(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return c.doAccount(ctx, c.dir.NewAccountURL, true, nil)
|
||||
}
|
||||
|
||||
// UpdateAccount updates an existing account. It returns an updated account
|
||||
// copy. The provided account is not modified.
|
||||
func (c *Client) UpdateAccount(ctx context.Context, a *Account) (*Account, error) {
|
||||
return c.doAccount(ctx, a.URL, false, a)
|
||||
}
|
||||
|
||||
// GetAuthorization retrieves an authorization identified by the given URL.
|
||||
//
|
||||
// If a caller needs to poll an authorization until its status is final,
|
||||
// see the WaitAuthorization method.
|
||||
func (c *Client) GetAuthorization(ctx context.Context, url string) (*Authorization, error) {
|
||||
res, err := c.get(ctx, url)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
if res.StatusCode != http.StatusOK {
|
||||
return nil, responseError(res)
|
||||
}
|
||||
var v wireAuthz
|
||||
if err := json.NewDecoder(res.Body).Decode(&v); err != nil {
|
||||
return nil, fmt.Errorf("acme: invalid response: %v", err)
|
||||
}
|
||||
return v.authorization(url), nil
|
||||
}
|
||||
|
||||
// DeactivateAuthorization relinquishes an existing authorization identified by
|
||||
// the given URL.
|
||||
//
|
||||
// If successful, the caller will be required to obtain a new authorization
|
||||
// before a new certificate for the domain associated with the authorization is
|
||||
// issued.
|
||||
//
|
||||
// It does not revoke existing certificates.
|
||||
func (c *Client) DeactivateAuthorization(ctx context.Context, url string) error {
|
||||
res, err := c.postWithJWSAccount(ctx, url, json.RawMessage(`{"status":"deactivated"}`))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
if res.StatusCode != http.StatusOK {
|
||||
return responseError(res)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// WaitAuthorization retrieves authorization details. If the authorization is not in
|
||||
// a final state (StatusValid/StatusInvalid), it retries the request until the authorization
|
||||
// is final, ctx is cancelled by the caller, or an error response is received.
|
||||
//
|
||||
// It returns a non-nil Authorization only if its Status is StatusValid.
|
||||
// In all other cases WaitAuthorization returns an error.
|
||||
// If the Status is StatusInvalid or StatusDeactivated, the returned error will be of type AuthorizationError.
|
||||
func (c *Client) WaitAuthorization(ctx context.Context, url string) (*Authorization, error) {
|
||||
sleep := sleeper(ctx)
|
||||
for {
|
||||
res, err := c.get(ctx, url)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if res.StatusCode != http.StatusOK {
|
||||
err = responseError(res)
|
||||
res.Body.Close()
|
||||
return nil, err
|
||||
}
|
||||
var raw wireAuthz
|
||||
err = json.NewDecoder(res.Body).Decode(&raw)
|
||||
res.Body.Close()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
switch raw.Status {
|
||||
case StatusValid:
|
||||
return raw.authorization(url), nil
|
||||
case StatusInvalid, StatusDeactivated:
|
||||
return nil, AuthorizationError{raw.authorization(url)}
|
||||
case StatusPending, StatusProcessing: // fall through to sleep
|
||||
default:
|
||||
return nil, fmt.Errorf("acme: unknown authorization status %q", raw.Status)
|
||||
}
|
||||
if err := sleep(res.Header.Get("Retry-After")); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// GetChallenge retrieves the current status of a challenge.
|
||||
//
|
||||
// A client typically polls a challenge status using this method.
|
||||
func (c *Client) GetChallenge(ctx context.Context, url string) (*Challenge, error) {
|
||||
res, err := c.get(ctx, url)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
if res.StatusCode != http.StatusOK {
|
||||
return nil, responseError(res)
|
||||
}
|
||||
v := wireChallenge{URL: url}
|
||||
if err := json.NewDecoder(res.Body).Decode(&v); err != nil {
|
||||
return nil, fmt.Errorf("acme: invalid response: %v", err)
|
||||
}
|
||||
return v.challenge(), nil
|
||||
}
|
||||
|
||||
// AcceptChallenge informs the server that the client accepts one of its
|
||||
// authorization challenges previously obtained with
|
||||
// CreateOrder/GetAuthorization.
|
||||
//
|
||||
// The server will then perform the validation asynchronously.
|
||||
func (c *Client) AcceptChallenge(ctx context.Context, chal *Challenge) (*Challenge, error) {
|
||||
auth, err := keyAuth(c.Key.Public(), chal.Token)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req := struct {
|
||||
Auth string `json:"keyAuthorization"`
|
||||
}{auth}
|
||||
res, err := c.postWithJWSAccount(ctx, chal.URL, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
if res.StatusCode != http.StatusOK {
|
||||
return nil, responseError(res)
|
||||
}
|
||||
|
||||
var v wireChallenge
|
||||
if err := json.NewDecoder(res.Body).Decode(&v); err != nil {
|
||||
return nil, fmt.Errorf("acme: invalid response: %v", err)
|
||||
}
|
||||
return v.challenge(), nil
|
||||
}
|
||||
|
||||
// DNS01ChallengeRecord returns a DNS record value for a dns-01 challenge response.
|
||||
// A TXT record containing the returned value must be provisioned under
|
||||
// "_acme-challenge" name of the domain being validated.
|
||||
//
|
||||
// The token argument is a Challenge.Token value.
|
||||
func (c *Client) DNS01ChallengeRecord(token string) (string, error) {
|
||||
ka, err := keyAuth(c.Key.Public(), token)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
b := sha256.Sum256([]byte(ka))
|
||||
return base64.RawURLEncoding.EncodeToString(b[:]), nil
|
||||
}
|
||||
|
||||
// HTTP01ChallengeResponse returns the response for an http-01 challenge.
|
||||
// Servers should respond with the value to HTTP requests at the URL path
|
||||
// provided by HTTP01ChallengePath to validate the challenge and prove control
|
||||
// over a domain name.
|
||||
//
|
||||
// The token argument is a Challenge.Token value.
|
||||
func (c *Client) HTTP01ChallengeResponse(token string) (string, error) {
|
||||
return keyAuth(c.Key.Public(), token)
|
||||
}
|
||||
|
||||
// HTTP01ChallengePath returns the URL path at which the response for an http-01 challenge
|
||||
// should be provided by the servers.
|
||||
// The response value can be obtained with HTTP01ChallengeResponse.
|
||||
//
|
||||
// The token argument is a Challenge.Token value.
|
||||
func (c *Client) HTTP01ChallengePath(token string) string {
|
||||
return "/.well-known/acme-challenge/" + token
|
||||
}
|
||||
|
||||
// doAccount creates, updates, and reads accounts.
|
||||
//
|
||||
// A non-nil acct argument indicates whether the intention is to mutate data of
|
||||
// the Account. Only the Contact field can be updated.
|
||||
func (c *Client) doAccount(ctx context.Context, url string, getExistingWithKey bool, acct *Account) (*Account, error) {
|
||||
req := struct {
|
||||
Contact []string `json:"contact,omitempty"`
|
||||
TermsAgreed bool `json:"termsOfServiceAgreed,omitempty"`
|
||||
GetExisting bool `json:"onlyReturnExisting,omitempty"`
|
||||
}{
|
||||
GetExisting: getExistingWithKey,
|
||||
}
|
||||
var accountURL string
|
||||
if url != c.dir.NewAccountURL {
|
||||
accountURL = url
|
||||
}
|
||||
if acct != nil {
|
||||
req.Contact = acct.Contact
|
||||
req.TermsAgreed = acct.TermsAgreed
|
||||
}
|
||||
res, err := c.retryPostJWS(ctx, c.Key, accountURL, url, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
if res.StatusCode < 200 || res.StatusCode > 299 {
|
||||
return nil, responseError(res)
|
||||
}
|
||||
|
||||
if getExistingWithKey {
|
||||
l, err := resolveLocation(url, res.Header)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return c.doAccount(ctx, l, false, nil)
|
||||
}
|
||||
|
||||
var v struct {
|
||||
Status string
|
||||
Contact []string
|
||||
Orders string
|
||||
}
|
||||
if err := json.NewDecoder(res.Body).Decode(&v); err != nil {
|
||||
return nil, fmt.Errorf("acme: invalid response: %v", err)
|
||||
}
|
||||
l, err := resolveLocation(url, res.Header)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
a := &Account{
|
||||
URL: l,
|
||||
Status: v.Status,
|
||||
Contact: v.Contact,
|
||||
OrdersURL: v.Orders,
|
||||
}
|
||||
if a.URL == "" {
|
||||
a.URL = url
|
||||
}
|
||||
c.urlMu.Lock()
|
||||
defer c.urlMu.Unlock()
|
||||
c.accountURL = a.URL
|
||||
c.ordersURL = a.OrdersURL
|
||||
return a, nil
|
||||
}
|
||||
|
||||
// cacheAccount ensures that the account URL is cached and returns it.
|
||||
func (c *Client) cacheAccountURL(ctx context.Context) (string, error) {
|
||||
c.urlMu.Lock()
|
||||
defer c.urlMu.Unlock()
|
||||
if c.accountURL != "" {
|
||||
return c.accountURL, nil
|
||||
}
|
||||
res, err := c.postWithJWSKey(ctx, c.Key, c.dir.NewAccountURL, json.RawMessage(`{"onlyReturnExisting":true}`))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
if res.StatusCode != http.StatusOK {
|
||||
return "", responseError(res)
|
||||
}
|
||||
var v struct {
|
||||
Orders string
|
||||
}
|
||||
if err := json.NewDecoder(res.Body).Decode(&v); err != nil {
|
||||
return "", err
|
||||
}
|
||||
l, err := resolveLocation(c.dir.NewAccountURL, res.Header)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
c.accountURL = l
|
||||
c.ordersURL = v.Orders
|
||||
return c.accountURL, nil
|
||||
}
|
||||
|
||||
func (c *Client) postWithJWSKey(ctx context.Context, key crypto.Signer, url string, body interface{}) (*http.Response, error) {
|
||||
return c.retryPostJWS(ctx, key, "", url, body)
|
||||
}
|
||||
|
||||
func (c *Client) postWithJWSAccount(ctx context.Context, url string, body interface{}) (*http.Response, error) {
|
||||
accountURL, err := c.cacheAccountURL(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return c.retryPostJWS(ctx, c.Key, accountURL, url, body)
|
||||
}
|
||||
|
||||
// retryPostJWS will retry calls to postJWS if there is a badNonce error,
|
||||
// clearing the stored nonces after each error.
|
||||
// If the response was 4XX-5XX, then responseError is called on the body,
|
||||
// the body is closed, and the error returned.
|
||||
func (c *Client) retryPostJWS(ctx context.Context, key crypto.Signer, accountURL, url string, body interface{}) (*http.Response, error) {
|
||||
sleep := sleeper(ctx)
|
||||
for {
|
||||
res, err := c.postJWS(ctx, key, accountURL, url, body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// handle errors 4XX-5XX with responseError
|
||||
if res.StatusCode >= 400 && res.StatusCode <= 599 {
|
||||
err := responseError(res)
|
||||
res.Body.Close()
|
||||
if ae, ok := err.(*Error); ok && ae.Type == "urn:ietf:params:acme:error:badNonce" {
|
||||
// clear any nonces that we might've stored that might now be
|
||||
// considered bad
|
||||
c.clearNonces()
|
||||
retry := res.Header.Get("Retry-After")
|
||||
if err := sleep(retry); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
continue
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
}
|
||||
|
||||
// postJWS signs the body with the given key and POSTs it to the provided url.
|
||||
// The body argument must be JSON-serializable.
|
||||
// The accountURL should be empty for account creation and certificate revocation.
|
||||
func (c *Client) postJWS(ctx context.Context, key crypto.Signer, accountURL, url string, body interface{}) (*http.Response, error) {
|
||||
nonce, err := c.popNonce(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
b, err := jwsEncodeJSON(body, key, accountURL, url, nonce)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
res, err := c.post(ctx, url, "application/jose+json", bytes.NewReader(b))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
c.addNonce(res.Header)
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// popNonce returns a nonce value previously stored with c.addNonce
|
||||
// or fetches a fresh one.
|
||||
func (c *Client) popNonce(ctx context.Context) (string, error) {
|
||||
c.noncesMu.Lock()
|
||||
defer c.noncesMu.Unlock()
|
||||
if len(c.nonces) == 0 {
|
||||
return c.fetchNonce(ctx)
|
||||
}
|
||||
var nonce string
|
||||
for nonce = range c.nonces {
|
||||
delete(c.nonces, nonce)
|
||||
break
|
||||
}
|
||||
return nonce, nil
|
||||
}
|
||||
|
||||
// clearNonces clears any stored nonces
|
||||
func (c *Client) clearNonces() {
|
||||
c.noncesMu.Lock()
|
||||
defer c.noncesMu.Unlock()
|
||||
c.nonces = make(map[string]struct{})
|
||||
}
|
||||
|
||||
// addNonce stores a nonce value found in h (if any) for future use.
|
||||
func (c *Client) addNonce(h http.Header) {
|
||||
v := nonceFromHeader(h)
|
||||
if v == "" {
|
||||
return
|
||||
}
|
||||
c.noncesMu.Lock()
|
||||
defer c.noncesMu.Unlock()
|
||||
if len(c.nonces) >= maxNonces {
|
||||
return
|
||||
}
|
||||
if c.nonces == nil {
|
||||
c.nonces = make(map[string]struct{})
|
||||
}
|
||||
c.nonces[v] = struct{}{}
|
||||
}
|
||||
|
||||
func (c *Client) httpClient() *http.Client {
|
||||
if c.HTTPClient != nil {
|
||||
return c.HTTPClient
|
||||
}
|
||||
return http.DefaultClient
|
||||
}
|
||||
|
||||
func (c *Client) get(ctx context.Context, urlStr string) (*http.Response, error) {
|
||||
req, err := http.NewRequest("GET", urlStr, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return c.do(ctx, req)
|
||||
}
|
||||
|
||||
func (c *Client) head(ctx context.Context, urlStr string) (*http.Response, error) {
|
||||
req, err := http.NewRequest("HEAD", urlStr, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return c.do(ctx, req)
|
||||
}
|
||||
|
||||
func (c *Client) post(ctx context.Context, urlStr, contentType string, body io.Reader) (*http.Response, error) {
|
||||
req, err := http.NewRequest("POST", urlStr, body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Header.Set("Content-Type", contentType)
|
||||
return c.do(ctx, req)
|
||||
}
|
||||
|
||||
func (c *Client) do(ctx context.Context, req *http.Request) (*http.Response, error) {
|
||||
res, err := c.httpClient().Do(req.WithContext(ctx))
|
||||
if err != nil {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
// Prefer the unadorned context error.
|
||||
// (The acme package had tests assuming this, previously from ctxhttp's
|
||||
// behavior, predating net/http supporting contexts natively)
|
||||
// TODO(bradfitz): reconsider this in the future. But for now this
|
||||
// requires no test updates.
|
||||
return nil, ctx.Err()
|
||||
default:
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (c *Client) fetchNonce(ctx context.Context) (string, error) {
|
||||
resp, err := c.head(ctx, c.dir.NewNonceURL)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
nonce := nonceFromHeader(resp.Header)
|
||||
if nonce == "" {
|
||||
if resp.StatusCode > 299 {
|
||||
return "", responseError(resp)
|
||||
}
|
||||
return "", errors.New("acme: nonce not found")
|
||||
}
|
||||
return nonce, nil
|
||||
}
|
||||
|
||||
func nonceFromHeader(h http.Header) string {
|
||||
return h.Get("Replay-Nonce")
|
||||
}
|
||||
|
||||
func (c *Client) getCert(ctx context.Context, url string) ([][]byte, error) {
|
||||
res, err := c.get(ctx, url)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
data, err := ioutil.ReadAll(io.LimitReader(res.Body, maxChainSize+1))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("acme: error getting certificate: %v", err)
|
||||
}
|
||||
if len(data) > maxChainSize {
|
||||
return nil, errors.New("acme: certificate chain is too big")
|
||||
}
|
||||
var chain [][]byte
|
||||
for {
|
||||
var p *pem.Block
|
||||
p, data = pem.Decode(data)
|
||||
if p == nil {
|
||||
if len(chain) == 0 {
|
||||
return nil, errors.New("acme: invalid PEM certificate chain")
|
||||
}
|
||||
break
|
||||
}
|
||||
if len(chain) == maxChainLen {
|
||||
return nil, errors.New("acme: certificate chain is too long")
|
||||
}
|
||||
if p.Type != "CERTIFICATE" {
|
||||
return nil, fmt.Errorf("acme: invalid PEM block type %q", p.Type)
|
||||
}
|
||||
chain = append(chain, p.Bytes)
|
||||
}
|
||||
return chain, nil
|
||||
}
|
||||
|
||||
// responseError creates an error of Error type from resp.
|
||||
func responseError(resp *http.Response) error {
|
||||
// don't care if ReadAll returns an error:
|
||||
// json.Unmarshal will fail in that case anyway
|
||||
b, _ := ioutil.ReadAll(resp.Body)
|
||||
e := &wireError{Status: resp.StatusCode}
|
||||
if err := json.Unmarshal(b, e); err != nil {
|
||||
// this is not a regular error response:
|
||||
// populate detail with anything we received,
|
||||
// e.Status will already contain HTTP response code value
|
||||
e.Detail = string(b)
|
||||
if e.Detail == "" {
|
||||
e.Detail = resp.Status
|
||||
}
|
||||
}
|
||||
return e.error(resp.Header)
|
||||
}
|
||||
|
||||
// sleeper returns a function that accepts the Retry-After HTTP header value
|
||||
// and an increment that's used with backoff to increasingly sleep on
|
||||
// consecutive calls until the context is done. If the Retry-After header
|
||||
// cannot be parsed, then backoff is used with a maximum sleep time of 10
|
||||
// seconds.
|
||||
func sleeper(ctx context.Context) func(ra string) error {
|
||||
sleep := timeSleeper(ctx)
|
||||
return func(ra string) error {
|
||||
return sleep(retryAfter(ra))
|
||||
}
|
||||
}
|
||||
|
||||
func timeSleeper(ctx context.Context) func(time.Time) error {
|
||||
var count int
|
||||
return func(t time.Time) error {
|
||||
d := backoff(count, 10*time.Second)
|
||||
count++
|
||||
if !t.IsZero() {
|
||||
d = t.Sub(timeNow())
|
||||
}
|
||||
wakeup := time.NewTimer(d)
|
||||
defer wakeup.Stop()
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
case <-wakeup.C:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// retryAfter parses a Retry-After HTTP header value,
|
||||
// trying to convert v into an int (seconds) or use http.ParseTime otherwise.
|
||||
func retryAfter(v string) time.Time {
|
||||
if i, err := strconv.Atoi(v); err == nil {
|
||||
return timeNow().Add(time.Duration(i) * time.Second)
|
||||
}
|
||||
t, err := http.ParseTime(v)
|
||||
if err != nil {
|
||||
return time.Time{}
|
||||
}
|
||||
return t
|
||||
}
|
||||
|
||||
// backoff computes a duration after which an n+1 retry iteration should occur
|
||||
// using truncated exponential backoff algorithm.
|
||||
//
|
||||
// The n argument is always bounded between 0 and 30.
|
||||
// The max argument defines upper bound for the returned value.
|
||||
func backoff(n int, max time.Duration) time.Duration {
|
||||
if n < 0 {
|
||||
n = 0
|
||||
}
|
||||
if n > 30 {
|
||||
n = 30
|
||||
}
|
||||
var d time.Duration
|
||||
if x, err := rand.Int(rand.Reader, big.NewInt(1000)); err == nil {
|
||||
d = time.Duration(x.Int64()) * time.Millisecond
|
||||
}
|
||||
d += time.Duration(1<<uint(n)) * time.Second
|
||||
if d > max {
|
||||
return max
|
||||
}
|
||||
return d
|
||||
}
|
||||
|
||||
// keyAuth generates a key authorization string for a given token.
|
||||
func keyAuth(pub crypto.PublicKey, token string) (string, error) {
|
||||
th, err := JWKThumbprint(pub)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return fmt.Sprintf("%s.%s", token, th), nil
|
||||
}
|
||||
|
||||
func resolveLocation(base string, h http.Header) (string, error) {
|
||||
u, err := url.Parse(base)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
u, err = u.Parse(h.Get("Location"))
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("acme: error parsing Location: %s", err)
|
||||
}
|
||||
return u.String(), nil
|
||||
}
|
||||
|
||||
// timeNow is useful for testing for fixed current time.
|
||||
var timeNow = time.Now
|
||||
1123
third_party/crypto/acme/acme_test.go
vendored
Normal file
1123
third_party/crypto/acme/acme_test.go
vendored
Normal file
File diff suppressed because it is too large
Load Diff
146
third_party/crypto/acme/integration_test.go
vendored
Normal file
146
third_party/crypto/acme/integration_test.go
vendored
Normal file
@ -0,0 +1,146 @@
|
||||
// Copyright 2018 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.
|
||||
|
||||
// +build integration_test
|
||||
|
||||
package acme_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/ecdsa"
|
||||
"crypto/elliptic"
|
||||
"crypto/rand"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"golang.org/x/crypto/acme"
|
||||
)
|
||||
|
||||
// This test works with Pebble and Let's Encrypt staging.
|
||||
// For pebble use: ACME_DIRECTORY_URL=https://localhost:14000/dir go test
|
||||
// For Let's Encrypt you'll need a publicly accessible HTTP server like `ngrok http 8080` and then
|
||||
// TEST_HOST=xxx.ngrok.io:8080 ACME_DIRECTORY_URL=https://acme-staging-v02.api.letsencrypt.org/directory TEST_ACCOUNT_GET=1 TEST_REVOKE=1 go test
|
||||
func TestIntegration(t *testing.T) {
|
||||
dir := os.Getenv("ACME_DIRECTORY_URL")
|
||||
testAccountGet := os.Getenv("TEST_ACCOUNT_GET") != ""
|
||||
testRevoke := os.Getenv("TEST_REVOKE") != ""
|
||||
testHost := os.Getenv("TEST_HOST")
|
||||
if testHost == "" {
|
||||
testHost = "localhost:5002"
|
||||
}
|
||||
testIdentifier, listenPort, _ := net.SplitHostPort(testHost)
|
||||
if dir == "" {
|
||||
t.Fatal("ACME_DIRECTORY_URL is required")
|
||||
}
|
||||
|
||||
key, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||
c := &acme.Client{
|
||||
Key: key,
|
||||
DirectoryURL: dir,
|
||||
HTTPClient: &http.Client{
|
||||
Transport: &http.Transport{
|
||||
TLSClientConfig: &tls.Config{
|
||||
InsecureSkipVerify: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
a := &acme.Account{
|
||||
Contact: []string{"mailto:user@example.com"},
|
||||
TermsAgreed: true,
|
||||
}
|
||||
na, err := c.CreateAccount(context.Background(), a)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(a.Contact, na.Contact) {
|
||||
t.Errorf("na.Contact = %q; want %q", na.Contact, a.Contact)
|
||||
}
|
||||
if na.URL == "" {
|
||||
t.Fatal("empty na.URL")
|
||||
}
|
||||
|
||||
// this endpoint is not supported by pebble, so put it behind a flag
|
||||
if testAccountGet {
|
||||
na, err = c.GetAccount(context.Background())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !reflect.DeepEqual(a.Contact, na.Contact) {
|
||||
t.Errorf("na.Contact = %q; want %q", na.Contact, a.Contact)
|
||||
}
|
||||
}
|
||||
|
||||
order, err := c.CreateOrder(context.Background(), acme.NewOrder(testIdentifier))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
auth, err := c.GetAuthorization(context.Background(), order.Authorizations[0])
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
var challenge *acme.Challenge
|
||||
for _, ch := range auth.Challenges {
|
||||
if ch.Type == "http-01" {
|
||||
challenge = ch
|
||||
break
|
||||
}
|
||||
}
|
||||
if challenge == nil {
|
||||
t.Fatal("missing http-01 challenge")
|
||||
}
|
||||
|
||||
l, err := net.Listen("tcp", ":"+listenPort)
|
||||
if err != nil {
|
||||
t.Errorf("error listening for challenge: %s", err)
|
||||
}
|
||||
defer l.Close()
|
||||
go http.Serve(l, http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||||
if req.URL.Path != c.HTTP01ChallengePath(challenge.Token) {
|
||||
w.WriteHeader(404)
|
||||
return
|
||||
}
|
||||
res, _ := c.HTTP01ChallengeResponse(challenge.Token)
|
||||
w.Write([]byte(res))
|
||||
}))
|
||||
|
||||
_, err = c.AcceptChallenge(context.Background(), challenge)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
_, err = c.WaitAuthorization(context.Background(), order.Authorizations[0])
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
certKey, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||
csr, _ := x509.CreateCertificateRequest(rand.Reader, &x509.CertificateRequest{DNSNames: []string{testIdentifier}}, certKey)
|
||||
der, err := c.FinalizeOrder(context.Background(), order.FinalizeURL, csr)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
cert, err := x509.ParseCertificate(der[0])
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if cert.DNSNames[0] != testIdentifier {
|
||||
t.Errorf("unexpected DNSNames %v", cert.DNSNames)
|
||||
}
|
||||
|
||||
if testRevoke {
|
||||
if err := c.RevokeCert(context.Background(), certKey, der[0], acme.CRLReasonUnspecified); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
158
third_party/crypto/acme/jws.go
vendored
Normal file
158
third_party/crypto/acme/jws.go
vendored
Normal file
@ -0,0 +1,158 @@
|
||||
// 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/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/sha256"
|
||||
_ "crypto/sha512" // need for EC keys
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"math/big"
|
||||
)
|
||||
|
||||
// jwsEncodeJSON signs claimset using provided key and a nonce.
|
||||
// The result is serialized in JSON format.
|
||||
// See https://tools.ietf.org/html/rfc7515#section-7.
|
||||
func jwsEncodeJSON(claimset interface{}, key crypto.Signer, accountURL, url, nonce string) ([]byte, error) {
|
||||
alg, sha := jwsHasher(key)
|
||||
if alg == "" || !sha.Available() {
|
||||
return nil, ErrUnsupportedKey
|
||||
}
|
||||
var phead string
|
||||
if accountURL == "" {
|
||||
jwk, err := jwkEncode(key.Public())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
phead = fmt.Sprintf(`{"alg":%q,"jwk":%s,"nonce":%q,"url":%q}`, alg, jwk, nonce, url)
|
||||
} else {
|
||||
phead = fmt.Sprintf(`{"alg":%q,"kid":%q,"nonce":%q,"url":%q}`, alg, accountURL, nonce, url)
|
||||
}
|
||||
phead = base64.RawURLEncoding.EncodeToString([]byte(phead))
|
||||
cs, err := json.Marshal(claimset)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
payload := base64.RawURLEncoding.EncodeToString(cs)
|
||||
hash := sha.New()
|
||||
hash.Write([]byte(phead + "." + payload))
|
||||
sig, err := jwsSign(key, sha, hash.Sum(nil))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
enc := struct {
|
||||
Protected string `json:"protected"`
|
||||
Payload string `json:"payload"`
|
||||
Sig string `json:"signature"`
|
||||
}{
|
||||
Protected: phead,
|
||||
Payload: payload,
|
||||
Sig: base64.RawURLEncoding.EncodeToString(sig),
|
||||
}
|
||||
return json.Marshal(&enc)
|
||||
}
|
||||
|
||||
// jwkEncode encodes public part of an RSA or ECDSA key into a JWK.
|
||||
// The result is also suitable for creating a JWK thumbprint.
|
||||
// https://tools.ietf.org/html/rfc7517
|
||||
func jwkEncode(pub crypto.PublicKey) (string, error) {
|
||||
switch pub := pub.(type) {
|
||||
case *rsa.PublicKey:
|
||||
// https://tools.ietf.org/html/rfc7518#section-6.3.1
|
||||
n := pub.N
|
||||
e := big.NewInt(int64(pub.E))
|
||||
// Field order is important.
|
||||
// See https://tools.ietf.org/html/rfc7638#section-3.3 for details.
|
||||
return fmt.Sprintf(`{"e":"%s","kty":"RSA","n":"%s"}`,
|
||||
base64.RawURLEncoding.EncodeToString(e.Bytes()),
|
||||
base64.RawURLEncoding.EncodeToString(n.Bytes()),
|
||||
), nil
|
||||
case *ecdsa.PublicKey:
|
||||
// https://tools.ietf.org/html/rfc7518#section-6.2.1
|
||||
p := pub.Curve.Params()
|
||||
n := p.BitSize / 8
|
||||
if p.BitSize%8 != 0 {
|
||||
n++
|
||||
}
|
||||
x := pub.X.Bytes()
|
||||
if n > len(x) {
|
||||
x = append(make([]byte, n-len(x)), x...)
|
||||
}
|
||||
y := pub.Y.Bytes()
|
||||
if n > len(y) {
|
||||
y = append(make([]byte, n-len(y)), y...)
|
||||
}
|
||||
// Field order is important.
|
||||
// See https://tools.ietf.org/html/rfc7638#section-3.3 for details.
|
||||
return fmt.Sprintf(`{"crv":"%s","kty":"EC","x":"%s","y":"%s"}`,
|
||||
p.Name,
|
||||
base64.RawURLEncoding.EncodeToString(x),
|
||||
base64.RawURLEncoding.EncodeToString(y),
|
||||
), nil
|
||||
}
|
||||
return "", ErrUnsupportedKey
|
||||
}
|
||||
|
||||
// jwsSign signs the digest using the given key.
|
||||
// It returns ErrUnsupportedKey if the key type is unknown.
|
||||
// The hash is used only for RSA keys.
|
||||
func jwsSign(key crypto.Signer, hash crypto.Hash, digest []byte) ([]byte, error) {
|
||||
switch key := key.(type) {
|
||||
case *rsa.PrivateKey:
|
||||
return key.Sign(rand.Reader, digest, hash)
|
||||
case *ecdsa.PrivateKey:
|
||||
r, s, err := ecdsa.Sign(rand.Reader, key, digest)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rb, sb := r.Bytes(), s.Bytes()
|
||||
size := key.Params().BitSize / 8
|
||||
if size%8 > 0 {
|
||||
size++
|
||||
}
|
||||
sig := make([]byte, size*2)
|
||||
copy(sig[size-len(rb):], rb)
|
||||
copy(sig[size*2-len(sb):], sb)
|
||||
return sig, nil
|
||||
}
|
||||
return nil, ErrUnsupportedKey
|
||||
}
|
||||
|
||||
// jwsHasher indicates suitable JWS algorithm name and a hash function
|
||||
// to use for signing a digest with the provided key.
|
||||
// It returns ("", 0) if the key is not supported.
|
||||
func jwsHasher(key crypto.Signer) (string, crypto.Hash) {
|
||||
switch key := key.(type) {
|
||||
case *rsa.PrivateKey:
|
||||
return "RS256", crypto.SHA256
|
||||
case *ecdsa.PrivateKey:
|
||||
switch key.Params().Name {
|
||||
case "P-256":
|
||||
return "ES256", crypto.SHA256
|
||||
case "P-384":
|
||||
return "ES384", crypto.SHA384
|
||||
case "P-521":
|
||||
return "ES512", crypto.SHA512
|
||||
}
|
||||
}
|
||||
return "", 0
|
||||
}
|
||||
|
||||
// JWKThumbprint creates a JWK thumbprint out of pub
|
||||
// as specified in https://tools.ietf.org/html/rfc7638.
|
||||
func JWKThumbprint(pub crypto.PublicKey) (string, error) {
|
||||
jwk, err := jwkEncode(pub)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
b := sha256.Sum256([]byte(jwk))
|
||||
return base64.RawURLEncoding.EncodeToString(b[:]), nil
|
||||
}
|
||||
320
third_party/crypto/acme/jws_test.go
vendored
Normal file
320
third_party/crypto/acme/jws_test.go
vendored
Normal file
@ -0,0 +1,320 @@
|
||||
// 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/ecdsa"
|
||||
"crypto/elliptic"
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"testing"
|
||||
)
|
||||
|
||||
const (
|
||||
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 <val> | xxd -r -p | base64 -w 100 | tr -d '=' | tr '/+' '_-'
|
||||
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":"<testKeyECPubX>","y":"<testKeyECPubY>"}' | \
|
||||
// openssl dgst -binary -sha256 | base64 | tr -d '=' | tr '/+' '_-'
|
||||
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":"..."},"url":"https://example.com","nonce":"nonce"}
|
||||
protected = "eyJhbGciOiJSUzI1NiIsImp3ayI6eyJlIjoiQVFBQiIsImt0eSI6" +
|
||||
"IlJTQSIsIm4iOiI0eGdaM2VSUGt3b1J2eTdxZVJVYm1NRGUwVi14" +
|
||||
"SDllV0xkdTBpaGVlTGxybUQybXFXWGZQOUllU0tBcGJuMzRnOFR1" +
|
||||
"QVM5ZzV6aHE4RUxRM2ttanItS1Y4NkdBTWdJNlZBY0dscTNRcnpw" +
|
||||
"VENmXzMwQWI3LXphd3JmUmFGT05hMUh3RXpQWTFLSG5HVmt4SmM4" +
|
||||
"NWdOa3dZSTlTWTJSSFh0dmxuM3pzNXdJVE5yZG9zcUVYZWFJa1ZZ" +
|
||||
"QkVoYmhOdTU0cHAza3hvNlR1V0xpOWU2cFhlV2V0RXdtbEJ3dFda" +
|
||||
"bFBvaWIyajNUeExCa3NLWmZveUZ5ZWszODBtSGdKQXVtUV9JMmZq" +
|
||||
"ajk4Xzk3bWszaWhPWTRBZ1ZkQ0RqMXpfR0NvWmtHNVJxN25iQ0d5" +
|
||||
"b3N5S1d5RFgwMFpzLW5OcVZob0xlSXZYQzRubldkSk1aNnJvZ3h5" +
|
||||
"UVEifSwibm9uY2UiOiJub25jZSIsInVybCI6Imh0dHBzOi8vZXhh" +
|
||||
"bXBsZS5jb20ifQ"
|
||||
// {"Msg":"Hello JWS"}
|
||||
payload = "eyJNc2ciOiJIZWxsbyBKV1MifQ"
|
||||
signature = "EtedUusG_N7NkuHRs9Ios6V0_VZdjYPut8vqRMDHvZQ3kZO0d5-9" +
|
||||
"BivWINleGajAW29So64s4WYsITx2Y0g3obSw70Xsr8XVsVox2Wsx" +
|
||||
"RJgd6KBNk1SGzqUW7-yEaS0fs0ax5SHPwpS9ek9WPCZ0MphfUH3d" +
|
||||
"qK40x6dYbgY9mInfzf7L11QeRrQdJfGuef_74SJGTp6D4B5UrR2w" +
|
||||
"m-AoSsRXY5A99U7J8YE9LFTg7pUQRSQWqqGZu-U9VDiB8bBvViVH" +
|
||||
"1abbI5xHaSagDb1avfdIXqYv_QVeMXF67Nis8f963FOdX0zwjpob" +
|
||||
"mpi-rsSmLBEtUkLERBIU_8JRdkXMcw"
|
||||
)
|
||||
|
||||
b, err := jwsEncodeJSON(claims, testKey, "", "https://example.com", "nonce")
|
||||
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 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, "", "https://example.com", "nonce")
|
||||
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
|
||||
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.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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
// echo -n '{"crv":"P-521","kty":"EC","x":"<base64X>","y":"<base64Y>"}' | \
|
||||
// openssl dgst -binary -sha256 | \
|
||||
// base64 | \
|
||||
// tr -d '=' | tr '/+' '_-'
|
||||
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)
|
||||
}
|
||||
}
|
||||
404
third_party/crypto/acme/types.go
vendored
Normal file
404
third_party/crypto/acme/types.go
vendored
Normal file
@ -0,0 +1,404 @@
|
||||
// Copyright 2016 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 (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
// ACME server response statuses used to describe Account, Authorization, and Challenge states.
|
||||
const (
|
||||
StatusUnknown = "unknown"
|
||||
StatusPending = "pending"
|
||||
StatusProcessing = "processing"
|
||||
StatusValid = "valid"
|
||||
StatusInvalid = "invalid"
|
||||
StatusRevoked = "revoked"
|
||||
StatusDeactivated = "deactivated"
|
||||
)
|
||||
|
||||
// CRLReasonCode identifies the reason for a certificate revocation.
|
||||
type CRLReasonCode int
|
||||
|
||||
// CRL reason codes as defined in RFC 5280.
|
||||
const (
|
||||
CRLReasonUnspecified CRLReasonCode = 0
|
||||
CRLReasonKeyCompromise CRLReasonCode = 1
|
||||
CRLReasonCACompromise CRLReasonCode = 2
|
||||
CRLReasonAffiliationChanged CRLReasonCode = 3
|
||||
CRLReasonSuperseded CRLReasonCode = 4
|
||||
CRLReasonCessationOfOperation CRLReasonCode = 5
|
||||
CRLReasonCertificateHold CRLReasonCode = 6
|
||||
CRLReasonRemoveFromCRL CRLReasonCode = 8
|
||||
CRLReasonPrivilegeWithdrawn CRLReasonCode = 9
|
||||
CRLReasonAACompromise CRLReasonCode = 10
|
||||
)
|
||||
|
||||
// ErrUnsupportedKey is returned when an unsupported key type is encountered.
|
||||
var ErrUnsupportedKey = errors.New("acme: unknown key type; only RSA and ECDSA are supported")
|
||||
|
||||
// Error is an ACME error as defined in RFC 7807, Problem Details for HTTP APIs.
|
||||
type Error struct {
|
||||
// StatusCode is The HTTP status code generated by the origin server.
|
||||
StatusCode int
|
||||
|
||||
// Type is a URI that identifies the problem type, typically in
|
||||
// a "urn:ietf:params:acme:error:xxx" form.
|
||||
Type string
|
||||
|
||||
// Detail is a human-readable explanation specific to this occurrence of the problem.
|
||||
Detail string
|
||||
|
||||
// Subproblems is an optional list of additional error information, usually
|
||||
// indicating problems with specific identifiers during authorization.
|
||||
Subproblems []Subproblem
|
||||
|
||||
// Header is the original server error response headers.
|
||||
// It may be nil.
|
||||
Header http.Header
|
||||
}
|
||||
|
||||
func (e *Error) Error() string {
|
||||
return fmt.Sprintf("acme: %s: %s", e.Type, e.Detail)
|
||||
}
|
||||
|
||||
// An Subproblem is additional error detail that is included in an Error,
|
||||
// usually indicating a problem with a specific identifier during authorization.
|
||||
type Subproblem struct {
|
||||
// Type is a URI that identifies the subproblem type, typically in
|
||||
// "urn:ietf:params:acme:error:xxx" form.
|
||||
Type string
|
||||
|
||||
// Detail is a human-readable explanation specific to this occurrence of the
|
||||
// subproblem.
|
||||
Detail string
|
||||
|
||||
// Identifier optionally indicates the identifier that this subproblem is about.
|
||||
Identifier *AuthzID
|
||||
}
|
||||
|
||||
// OrderInvalidError is returned when an order is marked as invalid.
|
||||
type OrderInvalidError struct {
|
||||
// Order is the order that is invalid.
|
||||
Order *Order
|
||||
}
|
||||
|
||||
func (e OrderInvalidError) Error() string {
|
||||
if e.Order == nil || e.Order.Error == nil {
|
||||
return "acme: order is invalid"
|
||||
}
|
||||
return fmt.Sprintf("acme: invalid order (%s): %s", e.Order.Error.Type, e.Order.Error.Detail)
|
||||
}
|
||||
|
||||
// OrderPendingError is returned when an order is still pending after an
|
||||
// attempted finalization.
|
||||
type OrderPendingError struct {
|
||||
// Order is the order that is pending.
|
||||
Order *Order
|
||||
}
|
||||
|
||||
func (e OrderPendingError) Error() string {
|
||||
return "acme: order is pending due to incomplete authorization"
|
||||
}
|
||||
|
||||
// AuthorizationError is returned when an authorization is marked as invalid.
|
||||
type AuthorizationError struct {
|
||||
// Authorization is the authorization that is invalid.
|
||||
Authorization *Authorization
|
||||
}
|
||||
|
||||
func (e AuthorizationError) Error() string {
|
||||
if e.Authorization == nil {
|
||||
return "acme: authorization is invalid"
|
||||
}
|
||||
return fmt.Sprintf("acme: authorization for identifier %s is %s", e.Authorization.Identifier.Value, e.Authorization.Status)
|
||||
}
|
||||
|
||||
// RateLimit reports whether err represents a rate limit error and
|
||||
// any Retry-After duration returned by the server.
|
||||
//
|
||||
// See the following for more details on rate limiting:
|
||||
// https://tools.ietf.org/html/draft-ietf-acme-acme-09#section-6.5
|
||||
func RateLimit(err error) (time.Time, bool) {
|
||||
e, ok := err.(*Error)
|
||||
if !ok || e.Type != "urn:ietf:params:acme:error:rateLimited" {
|
||||
return time.Time{}, false
|
||||
}
|
||||
if e.Header == nil {
|
||||
return time.Time{}, true
|
||||
}
|
||||
return retryAfter(e.Header.Get("Retry-After")), true
|
||||
}
|
||||
|
||||
// Account is a user account. It is associated with a private key.
|
||||
type Account struct {
|
||||
// URL uniquely identifies the account.
|
||||
URL string
|
||||
|
||||
// Status is the status of the account. Valid values are StatusValid,
|
||||
// StatusDeactivated, and StatusRevoked.
|
||||
Status string
|
||||
|
||||
// Contact is a list of URLs that the server can use to contact the client
|
||||
// for issues related to this account.
|
||||
Contact []string
|
||||
|
||||
// TermsAgreed indicates agreement with the terms of service. It is not
|
||||
// modifiable after account creation.
|
||||
TermsAgreed bool
|
||||
|
||||
// OrdersURL is the URL used to fetch a list of orders submitted by this
|
||||
// account.
|
||||
OrdersURL string
|
||||
}
|
||||
|
||||
// Directory is ACME server discovery data.
|
||||
type Directory struct {
|
||||
// NewNonceURL is used to retrieve new nonces.
|
||||
NewNonceURL string
|
||||
|
||||
// NewAccountURL is used to create new accounts.
|
||||
NewAccountURL string
|
||||
|
||||
// NewOrderURL is used to create new orders.
|
||||
NewOrderURL string
|
||||
|
||||
// NewAuthzURL is used to create new authorizations.
|
||||
NewAuthzURL string
|
||||
|
||||
// RevokeCertURL is used to revoke a certificate.
|
||||
RevokeCertURL string
|
||||
|
||||
// KeyChangeURL is used to change the account key.
|
||||
KeyChangeURL string
|
||||
|
||||
// Terms is a URL identifying the current terms of service.
|
||||
Terms string
|
||||
|
||||
// Website is an HTTP or HTTPS URL locating a website
|
||||
// providing more information about the ACME server.
|
||||
Website string
|
||||
|
||||
// CAA consists of lowercase hostname elements, which the ACME server
|
||||
// recognises as referring to itself for the purposes of CAA record validation
|
||||
// as defined in RFC6844.
|
||||
CAA []string
|
||||
|
||||
// ExternalAccountRequired, if true, indicates that the CA requires that all
|
||||
// new account requests include an ExternalAccountBinding field associating
|
||||
// the new account with an external account.
|
||||
ExternalAccountRequired bool
|
||||
}
|
||||
|
||||
// NewOrder creates a new order with the domains provided, suitable for creating
|
||||
// a TLS certificate order with CreateOrder.
|
||||
func NewOrder(domains ...string) *Order {
|
||||
o := &Order{Identifiers: make([]AuthzID, len(domains))}
|
||||
for i, d := range domains {
|
||||
o.Identifiers[i] = AuthzID{
|
||||
Type: "dns",
|
||||
Value: d,
|
||||
}
|
||||
}
|
||||
return o
|
||||
}
|
||||
|
||||
// An Order represents a request for a certificate and is used to track the
|
||||
// progress through to issuance.
|
||||
type Order struct {
|
||||
// URL uniquely identifies the order.
|
||||
URL string
|
||||
|
||||
// Status is the status of the order. It will be one of StatusPending,
|
||||
// StatusProcessing, StatusValid, and StatusInvalid.
|
||||
Status string
|
||||
|
||||
// Expires is the teimstamp after which the server will consider the order invalid.
|
||||
Expires time.Time
|
||||
|
||||
// Identifiers is a list of identifiers that the order pertains to.
|
||||
Identifiers []AuthzID
|
||||
|
||||
// NotBefore is an optional requested value of the notBefore field in the certificate.
|
||||
NotBefore time.Time
|
||||
|
||||
// NotAfter is an optional requested value of the notAfter field in the certificate.
|
||||
NotAfter time.Time
|
||||
|
||||
// Error is the error that occurred while processing the order, if any.
|
||||
Error *Error
|
||||
|
||||
// Authorizations is a list of URLs for authorizations that the client needs
|
||||
// to complete before the requested certificate can be issued. For
|
||||
// valid/invalid orders, these are the authorizations that were completed.
|
||||
Authorizations []string
|
||||
|
||||
// FinalizeURL is the URL that is used to finalize the Order.
|
||||
FinalizeURL string
|
||||
|
||||
// CertificateURL is the URL for the certificate that has been issued in
|
||||
// response to this order.
|
||||
CertificateURL string
|
||||
|
||||
// RetryAfter is the timestamp, if any, to wait for before fetching this
|
||||
// order again.
|
||||
RetryAfter time.Time
|
||||
}
|
||||
|
||||
// A Challenge is a CA challenge for an identifier.
|
||||
type Challenge struct {
|
||||
// Type is the challenge type, e.g. "http-01", "tls-sni-02", "dns-01".
|
||||
Type string
|
||||
|
||||
// URL is the URL where a challenge response can be posted.
|
||||
URL string
|
||||
|
||||
// Token is a random value that uniquely identifies the challenge.
|
||||
Token string
|
||||
|
||||
// Validated is the time at which the server validated this challenge.
|
||||
Validated time.Time
|
||||
|
||||
// Status identifies the status of this challenge. Valid values are
|
||||
// StatusPending, StatusValid, and StatusInvalid.
|
||||
Status string
|
||||
|
||||
// Error indicates the errors that occurred while the server was validating
|
||||
// this challenge.
|
||||
Errors []*Error
|
||||
}
|
||||
|
||||
// Authorization encodes an authorization response.
|
||||
type Authorization struct {
|
||||
// URL uniquely identifies the authorization.
|
||||
URL string
|
||||
|
||||
// Status is the status of the authorization. Valid values are
|
||||
// StatusPending, StatusProcessing, StatusValid, StatusInvalid, and
|
||||
// StatusRevoked.
|
||||
Status string
|
||||
|
||||
// Identifier is the identifier that the account is authorized to represent.
|
||||
Identifier AuthzID
|
||||
|
||||
// Expires is the timestamp after which the server will consider this authorization invalid.
|
||||
Expires *time.Time
|
||||
|
||||
// Challenges is the list of challenges that the client can fulfill in order
|
||||
// to prove posession of the identifier. For valid/invalid authorizations,
|
||||
// this is the list of challenges that were used.
|
||||
Challenges []*Challenge
|
||||
}
|
||||
|
||||
// AuthzID is an identifier that an account is authorized to represent.
|
||||
type AuthzID struct {
|
||||
Type string // The type of identifier, e.g. "dns".
|
||||
Value string // The identifier itself, e.g. "example.org".
|
||||
}
|
||||
|
||||
type wireAuthzID struct {
|
||||
Type string `json:"type"`
|
||||
Value string `json:"value"`
|
||||
}
|
||||
|
||||
// wireAuthz is ACME JSON representation of Authorization objects.
|
||||
type wireAuthz struct {
|
||||
Status string
|
||||
Challenges []wireChallenge
|
||||
Expires *time.Time
|
||||
Identifier struct {
|
||||
Type string
|
||||
Value string
|
||||
}
|
||||
}
|
||||
|
||||
func (z *wireAuthz) authorization(url string) *Authorization {
|
||||
a := &Authorization{
|
||||
URL: url,
|
||||
Status: z.Status,
|
||||
Expires: z.Expires,
|
||||
Identifier: AuthzID{Type: z.Identifier.Type, Value: z.Identifier.Value},
|
||||
Challenges: make([]*Challenge, len(z.Challenges)),
|
||||
}
|
||||
for i, v := range z.Challenges {
|
||||
a.Challenges[i] = v.challenge()
|
||||
}
|
||||
return a
|
||||
}
|
||||
|
||||
// wireChallenge is ACME JSON challenge representation.
|
||||
type wireChallenge struct {
|
||||
URL string
|
||||
Type string
|
||||
Token string
|
||||
Status string
|
||||
Validated time.Time
|
||||
Errors []*Error
|
||||
}
|
||||
|
||||
func (c *wireChallenge) challenge() *Challenge {
|
||||
v := &Challenge{
|
||||
URL: c.URL,
|
||||
Type: c.Type,
|
||||
Token: c.Token,
|
||||
Status: c.Status,
|
||||
Validated: c.Validated,
|
||||
Errors: c.Errors,
|
||||
}
|
||||
if v.Status == "" {
|
||||
v.Status = StatusUnknown
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
// wireError is a subset of fields of the Problem Details object
|
||||
// as described in https://tools.ietf.org/html/rfc7807#section-3.1.
|
||||
type wireError struct {
|
||||
Status int
|
||||
Type string
|
||||
Detail string
|
||||
Subproblems []Subproblem
|
||||
}
|
||||
|
||||
func (e *wireError) error(h http.Header) *Error {
|
||||
return &Error{
|
||||
StatusCode: e.Status,
|
||||
Type: e.Type,
|
||||
Detail: e.Detail,
|
||||
Subproblems: e.Subproblems,
|
||||
Header: h,
|
||||
}
|
||||
}
|
||||
|
||||
type wireOrder struct {
|
||||
Status string
|
||||
Expires time.Time
|
||||
Identifiers []AuthzID
|
||||
NotBefore time.Time
|
||||
NotAfter time.Time
|
||||
Error *Error
|
||||
Authorizations []string
|
||||
Finalize string
|
||||
Certificate string
|
||||
}
|
||||
|
||||
func (o *wireOrder) order(url string, retryHeader string) *Order {
|
||||
return &Order{
|
||||
URL: url,
|
||||
Status: o.Status,
|
||||
Expires: o.Expires,
|
||||
Identifiers: o.Identifiers,
|
||||
NotBefore: o.NotBefore,
|
||||
NotAfter: o.NotAfter,
|
||||
Error: o.Error,
|
||||
Authorizations: o.Authorizations,
|
||||
FinalizeURL: o.Finalize,
|
||||
CertificateURL: o.Certificate,
|
||||
RetryAfter: retryAfter(retryHeader),
|
||||
}
|
||||
}
|
||||
63
third_party/crypto/acme/types_test.go
vendored
Normal file
63
third_party/crypto/acme/types_test.go
vendored
Normal file
@ -0,0 +1,63 @@
|
||||
// Copyright 2017 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 (
|
||||
"errors"
|
||||
"net/http"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestRateLimit(t *testing.T) {
|
||||
now := time.Date(2017, 04, 27, 10, 0, 0, 0, time.UTC)
|
||||
f := timeNow
|
||||
defer func() { timeNow = f }()
|
||||
timeNow = func() time.Time { return now }
|
||||
|
||||
h120, hTime := http.Header{}, http.Header{}
|
||||
h120.Set("Retry-After", "120")
|
||||
hTime.Set("Retry-After", "Tue Apr 27 11:00:00 2017")
|
||||
|
||||
err1 := &Error{
|
||||
Type: "urn:ietf:params:acme:error:nolimit",
|
||||
Header: h120,
|
||||
}
|
||||
err2 := &Error{
|
||||
Type: "urn:ietf:params:acme:error:rateLimited",
|
||||
Header: h120,
|
||||
}
|
||||
err3 := &Error{
|
||||
Type: "urn:ietf:params:acme:error:rateLimited",
|
||||
Header: nil,
|
||||
}
|
||||
err4 := &Error{
|
||||
Type: "urn:ietf:params:acme:error:rateLimited",
|
||||
Header: hTime,
|
||||
}
|
||||
|
||||
tt := []struct {
|
||||
err error
|
||||
res time.Time
|
||||
ok bool
|
||||
}{
|
||||
{},
|
||||
{err: errors.New("dummy")},
|
||||
{err: err1},
|
||||
{err: err2, res: now.Add(2 * time.Minute), ok: true},
|
||||
{err: err3, ok: true},
|
||||
{err: err4, res: now.Add(time.Hour), ok: true},
|
||||
}
|
||||
for i, test := range tt {
|
||||
res, ok := RateLimit(test.err)
|
||||
if ok != test.ok {
|
||||
t.Errorf("%d: RateLimit(%+v): ok = %v; want %v", i, test.err, ok, test.ok)
|
||||
continue
|
||||
}
|
||||
if !res.Equal(test.res) {
|
||||
t.Errorf("%d: RateLimit(%+v) = %v; want %v", i, test.err, res, test.res)
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user