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:
Herman Slatman 2021-08-09 10:26:31 +02:00
parent 71b3f65df1
commit 492256f2d7
No known key found for this signature in database
GPG key ID: F4D8A44EA0A75A4F
6 changed files with 426 additions and 60 deletions

View file

@ -45,6 +45,7 @@ func KeyToID(jwk *jose.JSONWebKey) (string, error) {
type ExternalAccountKey struct { type ExternalAccountKey struct {
ID string `json:"id"` ID string `json:"id"`
ProvisionerName string `json:"provisioner_name"`
Name string `json:"name"` Name string `json:"name"`
AccountID string `json:"-"` AccountID string `json:"-"`
KeyBytes []byte `json:"-"` KeyBytes []byte `json:"-"`

View file

@ -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

View file

@ -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)
}
})
}
}

View file

@ -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
} }

View file

@ -29,6 +29,7 @@ func (dba *dbAccount) clone() *dbAccount {
type dbExternalAccountKey struct { type dbExternalAccountKey struct {
ID string `json:"id"` ID string `json:"id"`
ProvisionerName string `json:"provisioner_name"`
Name string `json:"name"` Name string `json:"name"`
AccountID string `json:"accountID,omitempty"` AccountID string `json:"accountID,omitempty"`
KeyBytes []byte `json:"key"` KeyBytes []byte `json:"key"`
@ -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
@ -178,6 +179,7 @@ func (db *DB) CreateExternalAccountKey(ctx context.Context, name string) (*acme.
dbeak := &dbExternalAccountKey{ dbeak := &dbExternalAccountKey{
ID: keyID, ID: keyID,
ProvisionerName: provisionerName,
Name: name, Name: name,
KeyBytes: random, KeyBytes: random,
CreatedAt: clock.Now(), CreatedAt: clock.Now(),
@ -188,6 +190,7 @@ func (db *DB) CreateExternalAccountKey(ctx context.Context, name string) (*acme.
} }
return &acme.ExternalAccountKey{ return &acme.ExternalAccountKey{
ID: dbeak.ID, ID: dbeak.ID,
ProvisionerName: dbeak.ProvisionerName,
Name: dbeak.Name, Name: dbeak.Name,
AccountID: dbeak.AccountID, AccountID: dbeak.AccountID,
KeyBytes: dbeak.KeyBytes, KeyBytes: dbeak.KeyBytes,
@ -197,14 +200,19 @@ func (db *DB) CreateExternalAccountKey(ctx context.Context, name string) (*acme.
} }
// 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,
ProvisionerName: dbeak.ProvisionerName,
Name: dbeak.Name, Name: dbeak.Name,
AccountID: dbeak.AccountID, AccountID: dbeak.AccountID,
KeyBytes: dbeak.KeyBytes, KeyBytes: dbeak.KeyBytes,
@ -213,14 +221,19 @@ func (db *DB) GetExternalAccountKey(ctx context.Context, keyID string) (*acme.Ex
}, 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,
ProvisionerName: eak.ProvisionerName,
Name: eak.Name, Name: eak.Name,
AccountID: eak.AccountID, AccountID: eak.AccountID,
KeyBytes: eak.KeyBytes, KeyBytes: eak.KeyBytes,

View file

@ -9,11 +9,13 @@ 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 {
ProvisionerName string `json:"provisioner"`
Name string `json:"name"` 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 {
ProvisionerName string `json:"provisioner"`
KeyID string `json:"keyID"` KeyID string `json:"keyID"`
Name string `json:"name"` Name string `json:"name"`
Key []byte `json:"key"` Key []byte `json:"key"`
@ -35,13 +37,14 @@ 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{
ProvisionerName: eak.ProvisionerName,
KeyID: eak.ID, KeyID: eak.ID,
Name: eak.Name, Name: eak.Name,
Key: eak.KeyBytes, Key: eak.KeyBytes,