Add ACME CA capabilities
This commit is contained in:
parent
68ab03dc1b
commit
e3826dd1c3
54 changed files with 15687 additions and 184 deletions
354
ca/acmeClient.go
Normal file
354
ca/acmeClient.go
Normal file
|
@ -0,0 +1,354 @@
|
|||
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
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue