forked from TrueCloudLab/certificates
355 lines
9.1 KiB
Go
355 lines
9.1 KiB
Go
|
package ca
|
||
|
|
||
|
import (
|
||
|
"crypto/x509"
|
||
|
"encoding/base64"
|
||
|
"encoding/json"
|
||
|
"encoding/pem"
|
||
|
"fmt"
|
||
|
"io"
|
||
|
"io/ioutil"
|
||
|
"net/http"
|
||
|
"strings"
|
||
|
|
||
|
"github.com/pkg/errors"
|
||
|
"github.com/smallstep/certificates/acme"
|
||
|
acmeAPI "github.com/smallstep/certificates/acme/api"
|
||
|
"github.com/smallstep/cli/jose"
|
||
|
)
|
||
|
|
||
|
// ACMEClient implements an HTTP client to an ACME API.
|
||
|
type ACMEClient struct {
|
||
|
client *http.Client
|
||
|
dirLoc string
|
||
|
dir *acme.Directory
|
||
|
acc *acme.Account
|
||
|
Key *jose.JSONWebKey
|
||
|
kid string
|
||
|
}
|
||
|
|
||
|
// NewACMEClient initializes a new ACMEClient.
|
||
|
func NewACMEClient(endpoint string, contact []string, opts ...ClientOption) (*ACMEClient, error) {
|
||
|
// Retrieve transport from options.
|
||
|
o := new(clientOptions)
|
||
|
if err := o.apply(opts); err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
tr, err := o.getTransport(endpoint)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
ac := &ACMEClient{
|
||
|
client: &http.Client{
|
||
|
Transport: tr,
|
||
|
},
|
||
|
dirLoc: endpoint,
|
||
|
}
|
||
|
|
||
|
resp, err := ac.client.Get(endpoint)
|
||
|
if err != nil {
|
||
|
return nil, errors.Wrapf(err, "client GET %s failed", endpoint)
|
||
|
}
|
||
|
if resp.StatusCode >= 400 {
|
||
|
return nil, readACMEError(resp.Body)
|
||
|
}
|
||
|
var dir acme.Directory
|
||
|
if err := readJSON(resp.Body, &dir); err != nil {
|
||
|
return nil, errors.Wrapf(err, "error reading %s", endpoint)
|
||
|
}
|
||
|
|
||
|
ac.dir = &dir
|
||
|
|
||
|
ac.Key, err = jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
nar := &acmeAPI.NewAccountRequest{
|
||
|
Contact: contact,
|
||
|
TermsOfServiceAgreed: true,
|
||
|
}
|
||
|
payload, err := json.Marshal(nar)
|
||
|
if err != nil {
|
||
|
return nil, errors.Wrap(err, "error marshaling new account request")
|
||
|
}
|
||
|
|
||
|
resp, err = ac.post(payload, ac.dir.NewAccount, withJWK(ac))
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
if resp.StatusCode >= 400 {
|
||
|
return nil, readACMEError(resp.Body)
|
||
|
}
|
||
|
var acc acme.Account
|
||
|
if err := readJSON(resp.Body, &acc); err != nil {
|
||
|
return nil, errors.Wrapf(err, "error reading %s", dir.NewAccount)
|
||
|
}
|
||
|
ac.acc = &acc
|
||
|
ac.kid = resp.Header.Get("Location")
|
||
|
|
||
|
return ac, nil
|
||
|
}
|
||
|
|
||
|
// GetDirectory makes a directory request to the ACME api and returns an
|
||
|
// ACME directory object.
|
||
|
func (c *ACMEClient) GetDirectory() (*acme.Directory, error) {
|
||
|
return c.dir, nil
|
||
|
}
|
||
|
|
||
|
// GetNonce makes a nonce request to the ACME api and returns an
|
||
|
// ACME directory object.
|
||
|
func (c *ACMEClient) GetNonce() (string, error) {
|
||
|
resp, err := c.client.Get(c.dir.NewNonce)
|
||
|
if err != nil {
|
||
|
return "", errors.Wrapf(err, "client GET %s failed", c.dir.NewNonce)
|
||
|
}
|
||
|
if resp.StatusCode >= 400 {
|
||
|
return "", readACMEError(resp.Body)
|
||
|
}
|
||
|
return resp.Header.Get("Replay-Nonce"), nil
|
||
|
}
|
||
|
|
||
|
type withHeaderOption func(so *jose.SignerOptions)
|
||
|
|
||
|
func withJWK(c *ACMEClient) withHeaderOption {
|
||
|
return func(so *jose.SignerOptions) {
|
||
|
so.WithHeader("jwk", c.Key.Public())
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func withKid(c *ACMEClient) withHeaderOption {
|
||
|
return func(so *jose.SignerOptions) {
|
||
|
so.WithHeader("kid", c.kid)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// serialize serializes a json web signature and doesn't omit empty fields.
|
||
|
func serialize(obj *jose.JSONWebSignature) (string, error) {
|
||
|
raw, err := obj.CompactSerialize()
|
||
|
if err != nil {
|
||
|
return "", errors.Wrap(err, "error serializing JWS")
|
||
|
}
|
||
|
parts := strings.Split(raw, ".")
|
||
|
msg := struct {
|
||
|
Protected string `json:"protected"`
|
||
|
Payload string `json:"payload"`
|
||
|
Signature string `json:"signature"`
|
||
|
}{Protected: parts[0], Payload: parts[1], Signature: parts[2]}
|
||
|
b, err := json.Marshal(msg)
|
||
|
if err != nil {
|
||
|
return "", errors.Wrap(err, "error marshaling jws message")
|
||
|
}
|
||
|
return string(b), nil
|
||
|
}
|
||
|
|
||
|
func (c *ACMEClient) post(payload []byte, url string, headerOps ...withHeaderOption) (*http.Response, error) {
|
||
|
if c.Key == nil {
|
||
|
return nil, errors.New("acme client not configured with account")
|
||
|
}
|
||
|
nonce, err := c.GetNonce()
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
so := new(jose.SignerOptions)
|
||
|
so.WithHeader("nonce", nonce)
|
||
|
so.WithHeader("url", url)
|
||
|
for _, hop := range headerOps {
|
||
|
hop(so)
|
||
|
}
|
||
|
signer, err := jose.NewSigner(jose.SigningKey{
|
||
|
Algorithm: jose.SignatureAlgorithm(c.Key.Algorithm),
|
||
|
Key: c.Key.Key,
|
||
|
}, so)
|
||
|
if err != nil {
|
||
|
return nil, errors.Wrap(err, "error creating JWS signer")
|
||
|
}
|
||
|
signed, err := signer.Sign(payload)
|
||
|
if err != nil {
|
||
|
return nil, errors.Errorf("error signing payload: %s", strings.TrimPrefix(err.Error(), "square/go-jose: "))
|
||
|
}
|
||
|
raw, err := serialize(signed)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
resp, err := c.client.Post(url, "application/jose+json", strings.NewReader(raw))
|
||
|
if err != nil {
|
||
|
return nil, errors.Wrapf(err, "client GET %s failed", c.dir.NewOrder)
|
||
|
}
|
||
|
return resp, nil
|
||
|
}
|
||
|
|
||
|
// NewOrder creates and returns the information for a new ACME order.
|
||
|
func (c *ACMEClient) NewOrder(payload []byte) (*acme.Order, error) {
|
||
|
resp, err := c.post(payload, c.dir.NewOrder, withKid(c))
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
if resp.StatusCode >= 400 {
|
||
|
return nil, readACMEError(resp.Body)
|
||
|
}
|
||
|
|
||
|
var o acme.Order
|
||
|
if err := readJSON(resp.Body, &o); err != nil {
|
||
|
return nil, errors.Wrapf(err, "error reading %s", c.dir.NewOrder)
|
||
|
}
|
||
|
o.ID = resp.Header.Get("Location")
|
||
|
return &o, nil
|
||
|
}
|
||
|
|
||
|
// GetChallenge returns the Challenge at the given path.
|
||
|
// With the validate parameter set to True this method will attempt to validate the
|
||
|
// challenge before returning it.
|
||
|
func (c *ACMEClient) GetChallenge(url string) (*acme.Challenge, error) {
|
||
|
resp, err := c.post(nil, url, withKid(c))
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
if resp.StatusCode >= 400 {
|
||
|
return nil, readACMEError(resp.Body)
|
||
|
}
|
||
|
|
||
|
var ch acme.Challenge
|
||
|
if err := readJSON(resp.Body, &ch); err != nil {
|
||
|
return nil, errors.Wrapf(err, "error reading %s", url)
|
||
|
}
|
||
|
return &ch, nil
|
||
|
}
|
||
|
|
||
|
// ValidateChallenge returns the Challenge at the given path.
|
||
|
// With the validate parameter set to True this method will attempt to validate the
|
||
|
// challenge before returning it.
|
||
|
func (c *ACMEClient) ValidateChallenge(url string) error {
|
||
|
resp, err := c.post([]byte("{}"), url, withKid(c))
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
if resp.StatusCode >= 400 {
|
||
|
return readACMEError(resp.Body)
|
||
|
}
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
// GetAuthz returns the Authz at the given path.
|
||
|
func (c *ACMEClient) GetAuthz(url string) (*acme.Authz, error) {
|
||
|
resp, err := c.post(nil, url, withKid(c))
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
if resp.StatusCode >= 400 {
|
||
|
return nil, readACMEError(resp.Body)
|
||
|
}
|
||
|
|
||
|
var az acme.Authz
|
||
|
if err := readJSON(resp.Body, &az); err != nil {
|
||
|
return nil, errors.Wrapf(err, "error reading %s", url)
|
||
|
}
|
||
|
return &az, nil
|
||
|
}
|
||
|
|
||
|
// GetOrder returns the Order at the given path.
|
||
|
func (c *ACMEClient) GetOrder(url string) (*acme.Order, error) {
|
||
|
resp, err := c.post(nil, url, withKid(c))
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
if resp.StatusCode >= 400 {
|
||
|
return nil, readACMEError(resp.Body)
|
||
|
}
|
||
|
|
||
|
var o acme.Order
|
||
|
if err := readJSON(resp.Body, &o); err != nil {
|
||
|
return nil, errors.Wrapf(err, "error reading %s", url)
|
||
|
}
|
||
|
return &o, nil
|
||
|
}
|
||
|
|
||
|
// FinalizeOrder makes a finalize request to the ACME api.
|
||
|
func (c *ACMEClient) FinalizeOrder(url string, csr *x509.CertificateRequest) error {
|
||
|
payload, err := json.Marshal(acmeAPI.FinalizeRequest{
|
||
|
CSR: base64.RawURLEncoding.EncodeToString(csr.Raw),
|
||
|
})
|
||
|
if err != nil {
|
||
|
return errors.Wrap(err, "error marshaling finalize request")
|
||
|
}
|
||
|
resp, err := c.post(payload, url, withKid(c))
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
if resp.StatusCode >= 400 {
|
||
|
return readACMEError(resp.Body)
|
||
|
}
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
// GetCertificate retrieves the certificate along with all intermediates.
|
||
|
func (c *ACMEClient) GetCertificate(url string) (*x509.Certificate, []*x509.Certificate, error) {
|
||
|
resp, err := c.post(nil, url, withKid(c))
|
||
|
if err != nil {
|
||
|
return nil, nil, err
|
||
|
}
|
||
|
if resp.StatusCode >= 400 {
|
||
|
return nil, nil, readACMEError(resp.Body)
|
||
|
}
|
||
|
defer resp.Body.Close()
|
||
|
bodyBytes, err := ioutil.ReadAll(resp.Body)
|
||
|
if err != nil {
|
||
|
return nil, nil, errors.Wrap(err, "error reading GET certificate response")
|
||
|
}
|
||
|
|
||
|
var certs []*x509.Certificate
|
||
|
|
||
|
block, rest := pem.Decode(bodyBytes)
|
||
|
if block == nil {
|
||
|
return nil, nil, errors.New("failed to parse any certificates from response")
|
||
|
}
|
||
|
for block != nil {
|
||
|
cert, err := x509.ParseCertificate(block.Bytes)
|
||
|
if err != nil {
|
||
|
return nil, nil, errors.Wrap(err, "error parsing certificate pem response")
|
||
|
}
|
||
|
certs = append(certs, cert)
|
||
|
block, rest = pem.Decode(rest)
|
||
|
}
|
||
|
|
||
|
return certs[0], certs[1:], nil
|
||
|
}
|
||
|
|
||
|
// GetAccountOrders retrieves the orders belonging to the given account.
|
||
|
func (c *ACMEClient) GetAccountOrders() ([]string, error) {
|
||
|
if c.acc == nil {
|
||
|
return nil, errors.New("acme client not configured with account")
|
||
|
}
|
||
|
resp, err := c.post(nil, c.acc.Orders, withKid(c))
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
if resp.StatusCode >= 400 {
|
||
|
return nil, readACMEError(resp.Body)
|
||
|
}
|
||
|
|
||
|
var orders []string
|
||
|
if err := readJSON(resp.Body, &orders); err != nil {
|
||
|
return nil, errors.Wrapf(err, "error reading %s", c.acc.Orders)
|
||
|
}
|
||
|
|
||
|
return orders, nil
|
||
|
}
|
||
|
|
||
|
func readACMEError(r io.ReadCloser) error {
|
||
|
defer r.Close()
|
||
|
b, err := ioutil.ReadAll(r)
|
||
|
if err != nil {
|
||
|
return errors.Wrap(err, "error reading from body")
|
||
|
}
|
||
|
ae := new(acme.AError)
|
||
|
err = json.Unmarshal(b, &ae)
|
||
|
// If we successfully marshaled to an ACMEError then return the ACMEError.
|
||
|
if err != nil || len(ae.Error()) == 0 {
|
||
|
fmt.Printf("b = %s\n", b)
|
||
|
// Throw up our hands.
|
||
|
return errors.Errorf("%s", b)
|
||
|
}
|
||
|
return ae
|
||
|
}
|