diff --git a/acme/account.go b/acme/account.go index 330d2839..042cbdd3 100644 --- a/acme/account.go +++ b/acme/account.go @@ -44,12 +44,13 @@ func KeyToID(jwk *jose.JSONWebKey) (string, error) { } type ExternalAccountKey struct { - ID string `json:"id"` - Name string `json:"name"` - AccountID string `json:"-"` - KeyBytes []byte `json:"-"` - CreatedAt time.Time `json:"createdAt"` - BoundAt time.Time `json:"boundAt,omitempty"` + ID string `json:"id"` + ProvisionerName string `json:"provisioner_name"` + Name string `json:"name"` + AccountID string `json:"-"` + KeyBytes []byte `json:"-"` + CreatedAt time.Time `json:"createdAt"` + BoundAt time.Time `json:"boundAt,omitempty"` } func (eak *ExternalAccountKey) AlreadyBound() bool { diff --git a/acme/api/account.go b/acme/api/account.go index 8e9e9b11..6484609f 100644 --- a/acme/api/account.go +++ b/acme/api/account.go @@ -93,6 +93,12 @@ func (h *Handler) NewAccount(w http.ResponseWriter, r *http.Request) { return } + prov, err := acmeProvisionerFromContext(ctx) + if err != nil { + api.WriteError(w, err) + return + } + httpStatus := http.StatusCreated acc, err := accountFromContext(r.Context()) 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 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")) return } @@ -224,7 +230,7 @@ func (h *Handler) GetOrdersByAccountID(w http.ResponseWriter, r *http.Request) { 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) { acmeProv, err := acmeProvisionerFromContext(ctx) 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 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 _, ok := err.(*acme.Error); ok { + return nil, err + } 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 } +// keysAreEqual performs an equality check on two JWKs by comparing +// the (base64 encoding) of the Key IDs. func keysAreEqual(x, y *squarejose.JSONWebKey) bool { if x == nil || y == nil { return false diff --git a/acme/api/account_test.go b/acme/api/account_test.go index fe0f5392..e68999e2 100644 --- a/acme/api/account_test.go +++ b/acme/api/account_test.go @@ -3,11 +3,19 @@ package api import ( "bytes" "context" + "crypto" + "crypto/ecdsa" + "crypto/hmac" + "crypto/rsa" + "crypto/sha256" + "encoding/base64" "encoding/json" "fmt" "io/ioutil" + "math/big" "net/http/httptest" "net/url" + "reflect" "testing" "time" @@ -16,6 +24,7 @@ import ( "github.com/smallstep/certificates/acme" "github.com/smallstep/certificates/authority/provisioner" "go.step.sm/crypto/jose" + squarejose "gopkg.in/square/go-jose.v2" ) var ( @@ -40,6 +49,136 @@ func newProv() acme.Provisioner { 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) { type test struct { nar *NewAccountRequest @@ -377,6 +516,27 @@ func TestHandler_NewAccount(t *testing.T) { 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 { nar := &NewAccountRequest{ Contact: []string{"foo", "bar"}, @@ -456,6 +616,94 @@ func TestHandler_NewAccount(t *testing.T) { 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 { 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) + } + }) + } +} diff --git a/acme/db.go b/acme/db.go index b80e8f81..46e3fc97 100644 --- a/acme/db.go +++ b/acme/db.go @@ -19,9 +19,9 @@ type DB interface { GetAccountByKeyID(ctx context.Context, kid string) (*Account, error) UpdateAccount(ctx context.Context, acc *Account) error - CreateExternalAccountKey(ctx context.Context, name string) (*ExternalAccountKey, error) - GetExternalAccountKey(ctx context.Context, keyID string) (*ExternalAccountKey, error) - UpdateExternalAccountKey(ctx context.Context, eak *ExternalAccountKey) error + CreateExternalAccountKey(ctx context.Context, provisionerName string, name string) (*ExternalAccountKey, error) + GetExternalAccountKey(ctx context.Context, provisionerName string, keyID string) (*ExternalAccountKey, error) + UpdateExternalAccountKey(ctx context.Context, provisionerName string, eak *ExternalAccountKey) error CreateNonce(ctx context.Context) (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) MockUpdateAccount func(ctx context.Context, acc *Account) error - MockCreateExternalAccountKey func(ctx context.Context, name string) (*ExternalAccountKey, error) - MockGetExternalAccountKey func(ctx context.Context, keyID string) (*ExternalAccountKey, error) - MockUpdateExternalAccountKey func(ctx context.Context, eak *ExternalAccountKey) error + MockCreateExternalAccountKey func(ctx context.Context, provisionerName string, name string) (*ExternalAccountKey, error) + MockGetExternalAccountKey func(ctx context.Context, provisionerName string, keyID string) (*ExternalAccountKey, error) + MockUpdateExternalAccountKey func(ctx context.Context, provisionerName string, eak *ExternalAccountKey) error MockCreateNonce func(ctx context.Context) (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 -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 { - return m.MockCreateExternalAccountKey(ctx, name) + return m.MockCreateExternalAccountKey(ctx, provisionerName, name) } else if m.MockError != nil { return nil, m.MockError } @@ -129,18 +129,18 @@ func (m *MockDB) CreateExternalAccountKey(ctx context.Context, name string) (*Ex } // 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 { - return m.MockGetExternalAccountKey(ctx, keyID) + return m.MockGetExternalAccountKey(ctx, provisionerName, keyID) } else if m.MockError != nil { return nil, 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 { - return m.MockUpdateExternalAccountKey(ctx, eak) + return m.MockUpdateExternalAccountKey(ctx, provisionerName, eak) } else if m.MockError != nil { return m.MockError } diff --git a/acme/db/nosql/account.go b/acme/db/nosql/account.go index e38ff425..dc909dde 100644 --- a/acme/db/nosql/account.go +++ b/acme/db/nosql/account.go @@ -28,12 +28,13 @@ func (dba *dbAccount) clone() *dbAccount { } type dbExternalAccountKey struct { - ID string `json:"id"` - Name string `json:"name"` - AccountID string `json:"accountID,omitempty"` - KeyBytes []byte `json:"key"` - CreatedAt time.Time `json:"createdAt"` - BoundAt time.Time `json:"boundAt"` + ID string `json:"id"` + ProvisionerName string `json:"provisioner_name"` + Name string `json:"name"` + AccountID string `json:"accountID,omitempty"` + KeyBytes []byte `json:"key"` + CreatedAt time.Time `json:"createdAt"` + BoundAt time.Time `json:"boundAt"` } 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 -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() if err != nil { return nil, err @@ -177,55 +178,67 @@ func (db *DB) CreateExternalAccountKey(ctx context.Context, name string) (*acme. } dbeak := &dbExternalAccountKey{ - ID: keyID, - Name: name, - KeyBytes: random, - CreatedAt: clock.Now(), + ID: keyID, + ProvisionerName: provisionerName, + Name: name, + KeyBytes: random, + CreatedAt: clock.Now(), } if err = db.save(ctx, keyID, dbeak, nil, "external_account_key", externalAccountKeyTable); err != nil { return nil, err } return &acme.ExternalAccountKey{ - ID: dbeak.ID, - Name: dbeak.Name, - AccountID: dbeak.AccountID, - KeyBytes: dbeak.KeyBytes, - CreatedAt: dbeak.CreatedAt, - BoundAt: dbeak.BoundAt, + ID: dbeak.ID, + ProvisionerName: dbeak.ProvisionerName, + Name: dbeak.Name, + AccountID: dbeak.AccountID, + KeyBytes: dbeak.KeyBytes, + CreatedAt: dbeak.CreatedAt, + BoundAt: dbeak.BoundAt, }, nil } // 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) if err != nil { 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{ - ID: dbeak.ID, - Name: dbeak.Name, - AccountID: dbeak.AccountID, - KeyBytes: dbeak.KeyBytes, - CreatedAt: dbeak.CreatedAt, - BoundAt: dbeak.BoundAt, + ID: dbeak.ID, + ProvisionerName: dbeak.ProvisionerName, + Name: dbeak.Name, + AccountID: dbeak.AccountID, + KeyBytes: dbeak.KeyBytes, + CreatedAt: dbeak.CreatedAt, + BoundAt: dbeak.BoundAt, }, 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) if err != nil { 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{ - ID: eak.ID, - Name: eak.Name, - AccountID: eak.AccountID, - KeyBytes: eak.KeyBytes, - CreatedAt: eak.CreatedAt, - BoundAt: eak.BoundAt, + ID: eak.ID, + ProvisionerName: eak.ProvisionerName, + Name: eak.Name, + AccountID: eak.AccountID, + KeyBytes: eak.KeyBytes, + CreatedAt: eak.CreatedAt, + BoundAt: eak.BoundAt, } return db.save(ctx, nu.ID, nu, old, "external_account_key", externalAccountKeyTable) diff --git a/authority/admin/api/acme.go b/authority/admin/api/acme.go index f193bcb5..36fc10fe 100644 --- a/authority/admin/api/acme.go +++ b/authority/admin/api/acme.go @@ -9,14 +9,16 @@ import ( // CreateExternalAccountKeyRequest is the type for POST /admin/acme/eab requests 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 type CreateExternalAccountKeyResponse struct { - KeyID string `json:"keyID"` - Name string `json:"name"` - Key []byte `json:"key"` + ProvisionerName string `json:"provisioner"` + KeyID string `json:"keyID"` + Name string `json:"name"` + Key []byte `json:"key"` } // 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 - eak, err := h.acmeDB.CreateExternalAccountKey(r.Context(), body.Name) + eak, err := h.acmeDB.CreateExternalAccountKey(r.Context(), body.ProvisionerName, body.Name) if err != nil { api.WriteError(w, admin.WrapErrorISE(err, "error creating external account key %s", body.Name)) return } eakResponse := CreateExternalAccountKeyResponse{ - KeyID: eak.ID, - Name: eak.Name, - Key: eak.KeyBytes, + ProvisionerName: eak.ProvisionerName, + KeyID: eak.ID, + Name: eak.Name, + Key: eak.KeyBytes, } api.JSONStatus(w, eakResponse, http.StatusCreated) // TODO: rewrite into protobuf json (likely)