forked from TrueCloudLab/certificates
Add first test cases for EAB and make provisioner unique per EAB
Before this commit, EAB keys could be used CA-wide, meaning that an EAB credential could be used at any ACME provisioner. This commit changes that behavior, so that EAB credentials are now intended to be used with a specific ACME provisioner. I think that makes sense, because from the perspective of an ACME client the provisioner is like a distinct CA. Besides that this commit also includes the first tests for EAB. The logic for creating the EAB JWS as a client has been taken from github.com/mholt/acmez. This logic may be moved or otherwise sourced (i.e. from a vendor) as soon as the step client also (needs to) support(s) EAB with ACME.
This commit is contained in:
parent
71b3f65df1
commit
492256f2d7
6 changed files with 426 additions and 60 deletions
|
@ -44,12 +44,13 @@ func KeyToID(jwk *jose.JSONWebKey) (string, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
type ExternalAccountKey struct {
|
type ExternalAccountKey struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
Name string `json:"name"`
|
ProvisionerName string `json:"provisioner_name"`
|
||||||
AccountID string `json:"-"`
|
Name string `json:"name"`
|
||||||
KeyBytes []byte `json:"-"`
|
AccountID string `json:"-"`
|
||||||
CreatedAt time.Time `json:"createdAt"`
|
KeyBytes []byte `json:"-"`
|
||||||
BoundAt time.Time `json:"boundAt,omitempty"`
|
CreatedAt time.Time `json:"createdAt"`
|
||||||
|
BoundAt time.Time `json:"boundAt,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (eak *ExternalAccountKey) AlreadyBound() bool {
|
func (eak *ExternalAccountKey) AlreadyBound() bool {
|
||||||
|
|
|
@ -93,6 +93,12 @@ func (h *Handler) NewAccount(w http.ResponseWriter, r *http.Request) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
prov, err := acmeProvisionerFromContext(ctx)
|
||||||
|
if err != nil {
|
||||||
|
api.WriteError(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
httpStatus := http.StatusCreated
|
httpStatus := http.StatusCreated
|
||||||
acc, err := accountFromContext(r.Context())
|
acc, err := accountFromContext(r.Context())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -126,7 +132,7 @@ func (h *Handler) NewAccount(w http.ResponseWriter, r *http.Request) {
|
||||||
}
|
}
|
||||||
if eak != nil { // means that we have a (valid) External Account Binding key that should be bound, updated and sent in the response
|
if eak != nil { // means that we have a (valid) External Account Binding key that should be bound, updated and sent in the response
|
||||||
eak.BindTo(acc)
|
eak.BindTo(acc)
|
||||||
if err := h.db.UpdateExternalAccountKey(ctx, eak); err != nil {
|
if err := h.db.UpdateExternalAccountKey(ctx, prov.Name, eak); err != nil {
|
||||||
api.WriteError(w, acme.WrapErrorISE(err, "error updating external account binding key"))
|
api.WriteError(w, acme.WrapErrorISE(err, "error updating external account binding key"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -224,7 +230,7 @@ func (h *Handler) GetOrdersByAccountID(w http.ResponseWriter, r *http.Request) {
|
||||||
logOrdersByAccount(w, orders)
|
logOrdersByAccount(w, orders)
|
||||||
}
|
}
|
||||||
|
|
||||||
// validateExternalAccountBinding validates the externalAccountBinding property in a call to new-account
|
// validateExternalAccountBinding validates the externalAccountBinding property in a call to new-account.
|
||||||
func (h *Handler) validateExternalAccountBinding(ctx context.Context, nar *NewAccountRequest) (*acme.ExternalAccountKey, error) {
|
func (h *Handler) validateExternalAccountBinding(ctx context.Context, nar *NewAccountRequest) (*acme.ExternalAccountKey, error) {
|
||||||
acmeProv, err := acmeProvisionerFromContext(ctx)
|
acmeProv, err := acmeProvisionerFromContext(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -253,8 +259,11 @@ func (h *Handler) validateExternalAccountBinding(ctx context.Context, nar *NewAc
|
||||||
// TODO: implement strategy pattern to allow for different ways of verification (i.e. webhook call) based on configuration
|
// TODO: implement strategy pattern to allow for different ways of verification (i.e. webhook call) based on configuration
|
||||||
|
|
||||||
keyID := eabJWS.Signatures[0].Protected.KeyID
|
keyID := eabJWS.Signatures[0].Protected.KeyID
|
||||||
externalAccountKey, err := h.db.GetExternalAccountKey(ctx, keyID)
|
externalAccountKey, err := h.db.GetExternalAccountKey(ctx, acmeProv.Name, keyID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
if _, ok := err.(*acme.Error); ok {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
return nil, acme.WrapErrorISE(err, "error retrieving external account key")
|
return nil, acme.WrapErrorISE(err, "error retrieving external account key")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -285,6 +294,8 @@ func (h *Handler) validateExternalAccountBinding(ctx context.Context, nar *NewAc
|
||||||
return externalAccountKey, nil
|
return externalAccountKey, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// keysAreEqual performs an equality check on two JWKs by comparing
|
||||||
|
// the (base64 encoding) of the Key IDs.
|
||||||
func keysAreEqual(x, y *squarejose.JSONWebKey) bool {
|
func keysAreEqual(x, y *squarejose.JSONWebKey) bool {
|
||||||
if x == nil || y == nil {
|
if x == nil || y == nil {
|
||||||
return false
|
return false
|
||||||
|
|
|
@ -3,11 +3,19 @@ package api
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
|
"crypto"
|
||||||
|
"crypto/ecdsa"
|
||||||
|
"crypto/hmac"
|
||||||
|
"crypto/rsa"
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/base64"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
|
"math/big"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"reflect"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
@ -16,6 +24,7 @@ import (
|
||||||
"github.com/smallstep/certificates/acme"
|
"github.com/smallstep/certificates/acme"
|
||||||
"github.com/smallstep/certificates/authority/provisioner"
|
"github.com/smallstep/certificates/authority/provisioner"
|
||||||
"go.step.sm/crypto/jose"
|
"go.step.sm/crypto/jose"
|
||||||
|
squarejose "gopkg.in/square/go-jose.v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
@ -40,6 +49,136 @@ func newProv() acme.Provisioner {
|
||||||
return p
|
return p
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func newACMEProv(t *testing.T) *provisioner.ACME {
|
||||||
|
p := newProv()
|
||||||
|
a, ok := p.(*provisioner.ACME)
|
||||||
|
if !ok {
|
||||||
|
t.Fatal("not a valid ACME provisioner")
|
||||||
|
}
|
||||||
|
return a
|
||||||
|
}
|
||||||
|
|
||||||
|
var errUnsupportedKey = fmt.Errorf("unknown key type; only RSA and ECDSA are supported")
|
||||||
|
|
||||||
|
// keyID is the account identity provided by a CA during registration.
|
||||||
|
type keyID string
|
||||||
|
|
||||||
|
// noKeyID indicates that jwsEncodeJSON should compute and use JWK instead of a KID.
|
||||||
|
// See jwsEncodeJSON for details.
|
||||||
|
const noKeyID = keyID("")
|
||||||
|
|
||||||
|
// jwsEncodeEAB creates a JWS payload for External Account Binding according to RFC 8555 §7.3.4.
|
||||||
|
// Implementation taken from github.com/mholt/acmez
|
||||||
|
func jwsEncodeEAB(accountKey crypto.PublicKey, hmacKey []byte, kid keyID, url string) ([]byte, error) {
|
||||||
|
// §7.3.4: "The 'alg' field MUST indicate a MAC-based algorithm"
|
||||||
|
alg, sha := "HS256", crypto.SHA256
|
||||||
|
|
||||||
|
// §7.3.4: "The 'nonce' field MUST NOT be present"
|
||||||
|
phead, err := jwsHead(alg, "", url, kid, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
encodedKey, err := jwkEncode(accountKey)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
payload := base64.RawURLEncoding.EncodeToString([]byte(encodedKey))
|
||||||
|
|
||||||
|
payloadToSign := []byte(phead + "." + payload)
|
||||||
|
|
||||||
|
h := hmac.New(sha256.New, hmacKey)
|
||||||
|
h.Write(payloadToSign)
|
||||||
|
sig := h.Sum(nil)
|
||||||
|
|
||||||
|
return jwsFinal(sha, sig, phead, payload)
|
||||||
|
}
|
||||||
|
|
||||||
|
// jwsHead constructs the protected JWS header for the given fields.
|
||||||
|
// Since jwk and kid are mutually-exclusive, the jwk will be encoded
|
||||||
|
// only if kid is empty. If nonce is empty, it will not be encoded.
|
||||||
|
// Implementation taken from github.com/mholt/acmez
|
||||||
|
func jwsHead(alg, nonce, url string, kid keyID, key crypto.Signer) (string, error) {
|
||||||
|
phead := fmt.Sprintf(`{"alg":%q`, alg)
|
||||||
|
if kid == noKeyID {
|
||||||
|
jwk, err := jwkEncode(key.Public())
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
phead += fmt.Sprintf(`,"jwk":%s`, jwk)
|
||||||
|
} else {
|
||||||
|
phead += fmt.Sprintf(`,"kid":%q`, kid)
|
||||||
|
}
|
||||||
|
if nonce != "" {
|
||||||
|
phead += fmt.Sprintf(`,"nonce":%q`, nonce)
|
||||||
|
}
|
||||||
|
phead += fmt.Sprintf(`,"url":%q}`, url)
|
||||||
|
phead = base64.RawURLEncoding.EncodeToString([]byte(phead))
|
||||||
|
return phead, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// jwkEncode encodes public part of an RSA or ECDSA key into a JWK.
|
||||||
|
// The result is also suitable for creating a JWK thumbprint.
|
||||||
|
// https://tools.ietf.org/html/rfc7517
|
||||||
|
// Implementation taken from github.com/mholt/acmez
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
// jwsFinal constructs the final JWS object.
|
||||||
|
// Implementation taken from github.com/mholt/acmez
|
||||||
|
func jwsFinal(sha crypto.Hash, sig []byte, phead, payload string) ([]byte, error) {
|
||||||
|
enc := struct {
|
||||||
|
Protected string `json:"protected"`
|
||||||
|
Payload string `json:"payload"`
|
||||||
|
Sig string `json:"signature"`
|
||||||
|
}{
|
||||||
|
Protected: phead,
|
||||||
|
Payload: payload,
|
||||||
|
Sig: base64.RawURLEncoding.EncodeToString(sig),
|
||||||
|
}
|
||||||
|
result, err := json.Marshal(&enc)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
func TestNewAccountRequest_Validate(t *testing.T) {
|
func TestNewAccountRequest_Validate(t *testing.T) {
|
||||||
type test struct {
|
type test struct {
|
||||||
nar *NewAccountRequest
|
nar *NewAccountRequest
|
||||||
|
@ -377,6 +516,27 @@ func TestHandler_NewAccount(t *testing.T) {
|
||||||
err: acme.NewErrorISE("jwk expected in request context"),
|
err: acme.NewErrorISE("jwk expected in request context"),
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"fail/new-account-no-eab-provided": func(t *testing.T) test {
|
||||||
|
nar := &NewAccountRequest{
|
||||||
|
Contact: []string{"foo", "bar"},
|
||||||
|
ExternalAccountBinding: nil,
|
||||||
|
}
|
||||||
|
b, err := json.Marshal(nar)
|
||||||
|
assert.FatalError(t, err)
|
||||||
|
jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0)
|
||||||
|
assert.FatalError(t, err)
|
||||||
|
prov := newACMEProv(t)
|
||||||
|
prov.RequireEAB = true
|
||||||
|
ctx := context.WithValue(context.Background(), payloadContextKey, &payloadInfo{value: b})
|
||||||
|
ctx = context.WithValue(ctx, jwkContextKey, jwk)
|
||||||
|
ctx = context.WithValue(ctx, baseURLContextKey, baseURL)
|
||||||
|
ctx = context.WithValue(ctx, provisionerContextKey, prov)
|
||||||
|
return test{
|
||||||
|
ctx: ctx,
|
||||||
|
statusCode: 400,
|
||||||
|
err: acme.NewError(acme.ErrorExternalAccountRequiredType, "no external account binding provided"),
|
||||||
|
}
|
||||||
|
},
|
||||||
"fail/db.CreateAccount-error": func(t *testing.T) test {
|
"fail/db.CreateAccount-error": func(t *testing.T) test {
|
||||||
nar := &NewAccountRequest{
|
nar := &NewAccountRequest{
|
||||||
Contact: []string{"foo", "bar"},
|
Contact: []string{"foo", "bar"},
|
||||||
|
@ -456,6 +616,94 @@ func TestHandler_NewAccount(t *testing.T) {
|
||||||
statusCode: 200,
|
statusCode: 200,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"ok/new-account-no-eab-required": func(t *testing.T) test {
|
||||||
|
nar := &NewAccountRequest{
|
||||||
|
Contact: []string{"foo", "bar"},
|
||||||
|
ExternalAccountBinding: struct{}{},
|
||||||
|
}
|
||||||
|
b, err := json.Marshal(nar)
|
||||||
|
assert.FatalError(t, err)
|
||||||
|
jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0)
|
||||||
|
assert.FatalError(t, err)
|
||||||
|
prov := newACMEProv(t)
|
||||||
|
prov.RequireEAB = false
|
||||||
|
ctx := context.WithValue(context.Background(), payloadContextKey, &payloadInfo{value: b})
|
||||||
|
ctx = context.WithValue(ctx, jwkContextKey, jwk)
|
||||||
|
ctx = context.WithValue(ctx, baseURLContextKey, baseURL)
|
||||||
|
ctx = context.WithValue(ctx, provisionerContextKey, prov)
|
||||||
|
return test{
|
||||||
|
db: &acme.MockDB{
|
||||||
|
MockCreateAccount: func(ctx context.Context, acc *acme.Account) error {
|
||||||
|
acc.ID = "accountID"
|
||||||
|
assert.Equals(t, acc.Contact, nar.Contact)
|
||||||
|
assert.Equals(t, acc.Key, jwk)
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
acc: &acme.Account{
|
||||||
|
ID: "accountID",
|
||||||
|
Key: jwk,
|
||||||
|
Status: acme.StatusValid,
|
||||||
|
Contact: []string{"foo", "bar"},
|
||||||
|
OrdersURL: fmt.Sprintf("%s/acme/%s/account/accountID/orders", baseURL.String(), escProvName),
|
||||||
|
},
|
||||||
|
ctx: ctx,
|
||||||
|
statusCode: 201,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ok/new-account-with-eab": func(t *testing.T) test {
|
||||||
|
jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0)
|
||||||
|
assert.FatalError(t, err)
|
||||||
|
eabJWS, err := jwsEncodeEAB(jwk.Public().Key, []byte{1, 3, 3, 7}, "eakID", fmt.Sprintf("%s/acme/%s/account/new-account", baseURL.String(), escProvName))
|
||||||
|
assert.FatalError(t, err)
|
||||||
|
mappedEAB := make(map[string]interface{})
|
||||||
|
err = json.Unmarshal(eabJWS, &mappedEAB)
|
||||||
|
assert.FatalError(t, err)
|
||||||
|
nar := &NewAccountRequest{
|
||||||
|
Contact: []string{"foo", "bar"},
|
||||||
|
ExternalAccountBinding: mappedEAB,
|
||||||
|
}
|
||||||
|
b, err := json.Marshal(nar)
|
||||||
|
assert.FatalError(t, err)
|
||||||
|
prov := newACMEProv(t)
|
||||||
|
prov.RequireEAB = true
|
||||||
|
ctx := context.WithValue(context.Background(), payloadContextKey, &payloadInfo{value: b})
|
||||||
|
ctx = context.WithValue(ctx, jwkContextKey, jwk)
|
||||||
|
ctx = context.WithValue(ctx, baseURLContextKey, baseURL)
|
||||||
|
ctx = context.WithValue(ctx, provisionerContextKey, prov)
|
||||||
|
return test{
|
||||||
|
db: &acme.MockDB{
|
||||||
|
MockCreateAccount: func(ctx context.Context, acc *acme.Account) error {
|
||||||
|
acc.ID = "accountID"
|
||||||
|
assert.Equals(t, acc.Contact, nar.Contact)
|
||||||
|
assert.Equals(t, acc.Key, jwk)
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
MockGetExternalAccountKey: func(ctx context.Context, provisionerName string, keyID string) (*acme.ExternalAccountKey, error) {
|
||||||
|
return &acme.ExternalAccountKey{
|
||||||
|
ID: "eakID",
|
||||||
|
ProvisionerName: escProvName,
|
||||||
|
Name: "testeak",
|
||||||
|
KeyBytes: []byte{1, 3, 3, 7},
|
||||||
|
CreatedAt: time.Now(),
|
||||||
|
}, nil
|
||||||
|
},
|
||||||
|
MockUpdateExternalAccountKey: func(ctx context.Context, provisionerName string, eak *acme.ExternalAccountKey) error {
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
acc: &acme.Account{
|
||||||
|
ID: "accountID",
|
||||||
|
Key: jwk,
|
||||||
|
Status: acme.StatusValid,
|
||||||
|
Contact: []string{"foo", "bar"},
|
||||||
|
OrdersURL: fmt.Sprintf("%s/acme/%s/account/accountID/orders", baseURL.String(), escProvName),
|
||||||
|
ExternalAccountBinding: mappedEAB,
|
||||||
|
},
|
||||||
|
ctx: ctx,
|
||||||
|
statusCode: 201,
|
||||||
|
}
|
||||||
|
},
|
||||||
}
|
}
|
||||||
for name, run := range tests {
|
for name, run := range tests {
|
||||||
tc := run(t)
|
tc := run(t)
|
||||||
|
@ -694,3 +942,93 @@ func TestHandler_GetOrUpdateAccount(t *testing.T) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func Test_keysAreEqual(t *testing.T) {
|
||||||
|
jwkX, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0)
|
||||||
|
assert.FatalError(t, err)
|
||||||
|
jwkY, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0)
|
||||||
|
assert.FatalError(t, err)
|
||||||
|
type args struct {
|
||||||
|
x *squarejose.JSONWebKey
|
||||||
|
y *squarejose.JSONWebKey
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
args args
|
||||||
|
want bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "ok/nil",
|
||||||
|
args: args{
|
||||||
|
x: jwkX,
|
||||||
|
y: nil,
|
||||||
|
},
|
||||||
|
want: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ok/equal",
|
||||||
|
args: args{
|
||||||
|
x: jwkX,
|
||||||
|
y: jwkX,
|
||||||
|
},
|
||||||
|
want: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ok/not-equal",
|
||||||
|
args: args{
|
||||||
|
x: jwkX,
|
||||||
|
y: jwkY,
|
||||||
|
},
|
||||||
|
want: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
if got := keysAreEqual(tt.args.x, tt.args.y); got != tt.want {
|
||||||
|
t.Errorf("keysAreEqual() = %v, want %v", got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHandler_validateExternalAccountBinding(t *testing.T) {
|
||||||
|
type fields struct {
|
||||||
|
db acme.DB
|
||||||
|
backdate provisioner.Duration
|
||||||
|
ca acme.CertificateAuthority
|
||||||
|
linker Linker
|
||||||
|
validateChallengeOptions *acme.ValidateChallengeOptions
|
||||||
|
}
|
||||||
|
type args struct {
|
||||||
|
ctx context.Context
|
||||||
|
nar *NewAccountRequest
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
fields fields
|
||||||
|
args args
|
||||||
|
want *acme.ExternalAccountKey
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
// TODO: Add test cases.
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
h := &Handler{
|
||||||
|
db: tt.fields.db,
|
||||||
|
backdate: tt.fields.backdate,
|
||||||
|
ca: tt.fields.ca,
|
||||||
|
linker: tt.fields.linker,
|
||||||
|
validateChallengeOptions: tt.fields.validateChallengeOptions,
|
||||||
|
}
|
||||||
|
got, err := h.validateExternalAccountBinding(tt.args.ctx, tt.args.nar)
|
||||||
|
if (err != nil) != tt.wantErr {
|
||||||
|
t.Errorf("Handler.validateExternalAccountBinding() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(got, tt.want) {
|
||||||
|
t.Errorf("Handler.validateExternalAccountBinding() = %v, want %v", got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
24
acme/db.go
24
acme/db.go
|
@ -19,9 +19,9 @@ type DB interface {
|
||||||
GetAccountByKeyID(ctx context.Context, kid string) (*Account, error)
|
GetAccountByKeyID(ctx context.Context, kid string) (*Account, error)
|
||||||
UpdateAccount(ctx context.Context, acc *Account) error
|
UpdateAccount(ctx context.Context, acc *Account) error
|
||||||
|
|
||||||
CreateExternalAccountKey(ctx context.Context, name string) (*ExternalAccountKey, error)
|
CreateExternalAccountKey(ctx context.Context, provisionerName string, name string) (*ExternalAccountKey, error)
|
||||||
GetExternalAccountKey(ctx context.Context, keyID string) (*ExternalAccountKey, error)
|
GetExternalAccountKey(ctx context.Context, provisionerName string, keyID string) (*ExternalAccountKey, error)
|
||||||
UpdateExternalAccountKey(ctx context.Context, eak *ExternalAccountKey) error
|
UpdateExternalAccountKey(ctx context.Context, provisionerName string, eak *ExternalAccountKey) error
|
||||||
|
|
||||||
CreateNonce(ctx context.Context) (Nonce, error)
|
CreateNonce(ctx context.Context) (Nonce, error)
|
||||||
DeleteNonce(ctx context.Context, nonce Nonce) error
|
DeleteNonce(ctx context.Context, nonce Nonce) error
|
||||||
|
@ -51,9 +51,9 @@ type MockDB struct {
|
||||||
MockGetAccountByKeyID func(ctx context.Context, kid string) (*Account, error)
|
MockGetAccountByKeyID func(ctx context.Context, kid string) (*Account, error)
|
||||||
MockUpdateAccount func(ctx context.Context, acc *Account) error
|
MockUpdateAccount func(ctx context.Context, acc *Account) error
|
||||||
|
|
||||||
MockCreateExternalAccountKey func(ctx context.Context, name string) (*ExternalAccountKey, error)
|
MockCreateExternalAccountKey func(ctx context.Context, provisionerName string, name string) (*ExternalAccountKey, error)
|
||||||
MockGetExternalAccountKey func(ctx context.Context, keyID string) (*ExternalAccountKey, error)
|
MockGetExternalAccountKey func(ctx context.Context, provisionerName string, keyID string) (*ExternalAccountKey, error)
|
||||||
MockUpdateExternalAccountKey func(ctx context.Context, eak *ExternalAccountKey) error
|
MockUpdateExternalAccountKey func(ctx context.Context, provisionerName string, eak *ExternalAccountKey) error
|
||||||
|
|
||||||
MockCreateNonce func(ctx context.Context) (Nonce, error)
|
MockCreateNonce func(ctx context.Context) (Nonce, error)
|
||||||
MockDeleteNonce func(ctx context.Context, nonce Nonce) error
|
MockDeleteNonce func(ctx context.Context, nonce Nonce) error
|
||||||
|
@ -119,9 +119,9 @@ func (m *MockDB) UpdateAccount(ctx context.Context, acc *Account) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateExternalAccountKey mock
|
// CreateExternalAccountKey mock
|
||||||
func (m *MockDB) CreateExternalAccountKey(ctx context.Context, name string) (*ExternalAccountKey, error) {
|
func (m *MockDB) CreateExternalAccountKey(ctx context.Context, provisionerName string, name string) (*ExternalAccountKey, error) {
|
||||||
if m.MockCreateExternalAccountKey != nil {
|
if m.MockCreateExternalAccountKey != nil {
|
||||||
return m.MockCreateExternalAccountKey(ctx, name)
|
return m.MockCreateExternalAccountKey(ctx, provisionerName, name)
|
||||||
} else if m.MockError != nil {
|
} else if m.MockError != nil {
|
||||||
return nil, m.MockError
|
return nil, m.MockError
|
||||||
}
|
}
|
||||||
|
@ -129,18 +129,18 @@ func (m *MockDB) CreateExternalAccountKey(ctx context.Context, name string) (*Ex
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetExternalAccountKey mock
|
// GetExternalAccountKey mock
|
||||||
func (m *MockDB) GetExternalAccountKey(ctx context.Context, keyID string) (*ExternalAccountKey, error) {
|
func (m *MockDB) GetExternalAccountKey(ctx context.Context, provisionerName string, keyID string) (*ExternalAccountKey, error) {
|
||||||
if m.MockGetExternalAccountKey != nil {
|
if m.MockGetExternalAccountKey != nil {
|
||||||
return m.MockGetExternalAccountKey(ctx, keyID)
|
return m.MockGetExternalAccountKey(ctx, provisionerName, keyID)
|
||||||
} else if m.MockError != nil {
|
} else if m.MockError != nil {
|
||||||
return nil, m.MockError
|
return nil, m.MockError
|
||||||
}
|
}
|
||||||
return m.MockRet1.(*ExternalAccountKey), m.MockError
|
return m.MockRet1.(*ExternalAccountKey), m.MockError
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *MockDB) UpdateExternalAccountKey(ctx context.Context, eak *ExternalAccountKey) error {
|
func (m *MockDB) UpdateExternalAccountKey(ctx context.Context, provisionerName string, eak *ExternalAccountKey) error {
|
||||||
if m.MockUpdateExternalAccountKey != nil {
|
if m.MockUpdateExternalAccountKey != nil {
|
||||||
return m.MockUpdateExternalAccountKey(ctx, eak)
|
return m.MockUpdateExternalAccountKey(ctx, provisionerName, eak)
|
||||||
} else if m.MockError != nil {
|
} else if m.MockError != nil {
|
||||||
return m.MockError
|
return m.MockError
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,12 +28,13 @@ func (dba *dbAccount) clone() *dbAccount {
|
||||||
}
|
}
|
||||||
|
|
||||||
type dbExternalAccountKey struct {
|
type dbExternalAccountKey struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
Name string `json:"name"`
|
ProvisionerName string `json:"provisioner_name"`
|
||||||
AccountID string `json:"accountID,omitempty"`
|
Name string `json:"name"`
|
||||||
KeyBytes []byte `json:"key"`
|
AccountID string `json:"accountID,omitempty"`
|
||||||
CreatedAt time.Time `json:"createdAt"`
|
KeyBytes []byte `json:"key"`
|
||||||
BoundAt time.Time `json:"boundAt"`
|
CreatedAt time.Time `json:"createdAt"`
|
||||||
|
BoundAt time.Time `json:"boundAt"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (db *DB) getAccountIDByKeyID(ctx context.Context, kid string) (string, error) {
|
func (db *DB) getAccountIDByKeyID(ctx context.Context, kid string) (string, error) {
|
||||||
|
@ -164,7 +165,7 @@ func (db *DB) UpdateAccount(ctx context.Context, acc *acme.Account) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateExternalAccountKey creates a new External Account Binding key with a name
|
// CreateExternalAccountKey creates a new External Account Binding key with a name
|
||||||
func (db *DB) CreateExternalAccountKey(ctx context.Context, name string) (*acme.ExternalAccountKey, error) {
|
func (db *DB) CreateExternalAccountKey(ctx context.Context, provisionerName string, name string) (*acme.ExternalAccountKey, error) {
|
||||||
keyID, err := randID()
|
keyID, err := randID()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -177,55 +178,67 @@ func (db *DB) CreateExternalAccountKey(ctx context.Context, name string) (*acme.
|
||||||
}
|
}
|
||||||
|
|
||||||
dbeak := &dbExternalAccountKey{
|
dbeak := &dbExternalAccountKey{
|
||||||
ID: keyID,
|
ID: keyID,
|
||||||
Name: name,
|
ProvisionerName: provisionerName,
|
||||||
KeyBytes: random,
|
Name: name,
|
||||||
CreatedAt: clock.Now(),
|
KeyBytes: random,
|
||||||
|
CreatedAt: clock.Now(),
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = db.save(ctx, keyID, dbeak, nil, "external_account_key", externalAccountKeyTable); err != nil {
|
if err = db.save(ctx, keyID, dbeak, nil, "external_account_key", externalAccountKeyTable); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return &acme.ExternalAccountKey{
|
return &acme.ExternalAccountKey{
|
||||||
ID: dbeak.ID,
|
ID: dbeak.ID,
|
||||||
Name: dbeak.Name,
|
ProvisionerName: dbeak.ProvisionerName,
|
||||||
AccountID: dbeak.AccountID,
|
Name: dbeak.Name,
|
||||||
KeyBytes: dbeak.KeyBytes,
|
AccountID: dbeak.AccountID,
|
||||||
CreatedAt: dbeak.CreatedAt,
|
KeyBytes: dbeak.KeyBytes,
|
||||||
BoundAt: dbeak.BoundAt,
|
CreatedAt: dbeak.CreatedAt,
|
||||||
|
BoundAt: dbeak.BoundAt,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetExternalAccountKey retrieves an External Account Binding key by KeyID
|
// GetExternalAccountKey retrieves an External Account Binding key by KeyID
|
||||||
func (db *DB) GetExternalAccountKey(ctx context.Context, keyID string) (*acme.ExternalAccountKey, error) {
|
func (db *DB) GetExternalAccountKey(ctx context.Context, provisionerName string, keyID string) (*acme.ExternalAccountKey, error) {
|
||||||
dbeak, err := db.getDBExternalAccountKey(ctx, keyID)
|
dbeak, err := db.getDBExternalAccountKey(ctx, keyID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if dbeak.ProvisionerName != provisionerName {
|
||||||
|
return nil, acme.NewError(acme.ErrorUnauthorizedType, "name of provisioner does not match provisioner for which the EAB key was created")
|
||||||
|
}
|
||||||
|
|
||||||
return &acme.ExternalAccountKey{
|
return &acme.ExternalAccountKey{
|
||||||
ID: dbeak.ID,
|
ID: dbeak.ID,
|
||||||
Name: dbeak.Name,
|
ProvisionerName: dbeak.ProvisionerName,
|
||||||
AccountID: dbeak.AccountID,
|
Name: dbeak.Name,
|
||||||
KeyBytes: dbeak.KeyBytes,
|
AccountID: dbeak.AccountID,
|
||||||
CreatedAt: dbeak.CreatedAt,
|
KeyBytes: dbeak.KeyBytes,
|
||||||
BoundAt: dbeak.BoundAt,
|
CreatedAt: dbeak.CreatedAt,
|
||||||
|
BoundAt: dbeak.BoundAt,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (db *DB) UpdateExternalAccountKey(ctx context.Context, eak *acme.ExternalAccountKey) error {
|
func (db *DB) UpdateExternalAccountKey(ctx context.Context, provisionerName string, eak *acme.ExternalAccountKey) error {
|
||||||
old, err := db.getDBExternalAccountKey(ctx, eak.ID)
|
old, err := db.getDBExternalAccountKey(ctx, eak.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if old.ProvisionerName != provisionerName {
|
||||||
|
return acme.NewError(acme.ErrorUnauthorizedType, "name of provisioner does not match provisioner for which the EAB key was created")
|
||||||
|
}
|
||||||
|
|
||||||
nu := dbExternalAccountKey{
|
nu := dbExternalAccountKey{
|
||||||
ID: eak.ID,
|
ID: eak.ID,
|
||||||
Name: eak.Name,
|
ProvisionerName: eak.ProvisionerName,
|
||||||
AccountID: eak.AccountID,
|
Name: eak.Name,
|
||||||
KeyBytes: eak.KeyBytes,
|
AccountID: eak.AccountID,
|
||||||
CreatedAt: eak.CreatedAt,
|
KeyBytes: eak.KeyBytes,
|
||||||
BoundAt: eak.BoundAt,
|
CreatedAt: eak.CreatedAt,
|
||||||
|
BoundAt: eak.BoundAt,
|
||||||
}
|
}
|
||||||
|
|
||||||
return db.save(ctx, nu.ID, nu, old, "external_account_key", externalAccountKeyTable)
|
return db.save(ctx, nu.ID, nu, old, "external_account_key", externalAccountKeyTable)
|
||||||
|
|
|
@ -9,14 +9,16 @@ import (
|
||||||
|
|
||||||
// CreateExternalAccountKeyRequest is the type for POST /admin/acme/eab requests
|
// CreateExternalAccountKeyRequest is the type for POST /admin/acme/eab requests
|
||||||
type CreateExternalAccountKeyRequest struct {
|
type CreateExternalAccountKeyRequest struct {
|
||||||
Name string `json:"name"`
|
ProvisionerName string `json:"provisioner"`
|
||||||
|
Name string `json:"name"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateExternalAccountKeyResponse is the type for POST /admin/acme/eab responses
|
// CreateExternalAccountKeyResponse is the type for POST /admin/acme/eab responses
|
||||||
type CreateExternalAccountKeyResponse struct {
|
type CreateExternalAccountKeyResponse struct {
|
||||||
KeyID string `json:"keyID"`
|
ProvisionerName string `json:"provisioner"`
|
||||||
Name string `json:"name"`
|
KeyID string `json:"keyID"`
|
||||||
Key []byte `json:"key"`
|
Name string `json:"name"`
|
||||||
|
Key []byte `json:"key"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetExternalAccountKeysResponse is the type for GET /admin/acme/eab responses
|
// GetExternalAccountKeysResponse is the type for GET /admin/acme/eab responses
|
||||||
|
@ -35,16 +37,17 @@ func (h *Handler) CreateExternalAccountKey(w http.ResponseWriter, r *http.Reques
|
||||||
|
|
||||||
// TODO: Validate input
|
// TODO: Validate input
|
||||||
|
|
||||||
eak, err := h.acmeDB.CreateExternalAccountKey(r.Context(), body.Name)
|
eak, err := h.acmeDB.CreateExternalAccountKey(r.Context(), body.ProvisionerName, body.Name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
api.WriteError(w, admin.WrapErrorISE(err, "error creating external account key %s", body.Name))
|
api.WriteError(w, admin.WrapErrorISE(err, "error creating external account key %s", body.Name))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
eakResponse := CreateExternalAccountKeyResponse{
|
eakResponse := CreateExternalAccountKeyResponse{
|
||||||
KeyID: eak.ID,
|
ProvisionerName: eak.ProvisionerName,
|
||||||
Name: eak.Name,
|
KeyID: eak.ID,
|
||||||
Key: eak.KeyBytes,
|
Name: eak.Name,
|
||||||
|
Key: eak.KeyBytes,
|
||||||
}
|
}
|
||||||
|
|
||||||
api.JSONStatus(w, eakResponse, http.StatusCreated) // TODO: rewrite into protobuf json (likely)
|
api.JSONStatus(w, eakResponse, http.StatusCreated) // TODO: rewrite into protobuf json (likely)
|
||||||
|
|
Loading…
Reference in a new issue