forked from TrueCloudLab/certificates
Add first working version of External Account Binding
This commit is contained in:
parent
b9743b36e1
commit
f81d49d963
13 changed files with 353 additions and 22 deletions
|
@ -4,6 +4,7 @@ import (
|
||||||
"crypto"
|
"crypto"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"time"
|
||||||
|
|
||||||
"go.step.sm/crypto/jose"
|
"go.step.sm/crypto/jose"
|
||||||
)
|
)
|
||||||
|
@ -40,3 +41,12 @@ func KeyToID(jwk *jose.JSONWebKey) (string, error) {
|
||||||
}
|
}
|
||||||
return base64.RawURLEncoding.EncodeToString(kid), nil
|
return base64.RawURLEncoding.EncodeToString(kid), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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"`
|
||||||
|
}
|
||||||
|
|
|
@ -1,20 +1,26 @@
|
||||||
package api
|
package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/go-chi/chi"
|
"github.com/go-chi/chi"
|
||||||
"github.com/smallstep/certificates/acme"
|
"github.com/smallstep/certificates/acme"
|
||||||
"github.com/smallstep/certificates/api"
|
"github.com/smallstep/certificates/api"
|
||||||
|
"github.com/smallstep/certificates/authority/provisioner"
|
||||||
"github.com/smallstep/certificates/logging"
|
"github.com/smallstep/certificates/logging"
|
||||||
|
|
||||||
|
squarejose "gopkg.in/square/go-jose.v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
// NewAccountRequest represents the payload for a new account request.
|
// NewAccountRequest represents the payload for a new account request.
|
||||||
type NewAccountRequest struct {
|
type NewAccountRequest struct {
|
||||||
Contact []string `json:"contact"`
|
Contact []string `json:"contact"`
|
||||||
OnlyReturnExisting bool `json:"onlyReturnExisting"`
|
OnlyReturnExisting bool `json:"onlyReturnExisting"`
|
||||||
TermsOfServiceAgreed bool `json:"termsOfServiceAgreed"`
|
TermsOfServiceAgreed bool `json:"termsOfServiceAgreed"`
|
||||||
|
ExternalAccountBinding interface{} `json:"externalAccountBinding,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func validateContacts(cs []string) error {
|
func validateContacts(cs []string) error {
|
||||||
|
@ -83,6 +89,14 @@ func (h *Handler) NewAccount(w http.ResponseWriter, r *http.Request) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := h.validateExternalAccountBinding(ctx, &nar); err != nil {
|
||||||
|
api.WriteError(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: link account to the key when created; mark boundat timestamp
|
||||||
|
// TODO: return the externalAccountBinding field (should contain same info) if new account created
|
||||||
|
|
||||||
httpStatus := http.StatusCreated
|
httpStatus := http.StatusCreated
|
||||||
acc, err := accountFromContext(r.Context())
|
acc, err := accountFromContext(r.Context())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -205,3 +219,66 @@ func (h *Handler) GetOrdersByAccountID(w http.ResponseWriter, r *http.Request) {
|
||||||
api.JSON(w, orders)
|
api.JSON(w, orders)
|
||||||
logOrdersByAccount(w, orders)
|
logOrdersByAccount(w, orders)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// validateExternalAccountBinding validates the externalAccountBinding property in a call to new-account
|
||||||
|
func (h *Handler) validateExternalAccountBinding(ctx context.Context, nar *NewAccountRequest) error {
|
||||||
|
|
||||||
|
prov, err := provisionerFromContext(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
acmeProv, ok := prov.(*provisioner.ACME) // TODO: rewrite into providing configuration via function on acme.Provisioner
|
||||||
|
if !ok || acmeProv == nil {
|
||||||
|
return acme.NewErrorISE("provisioner in context is not an ACME provisioner")
|
||||||
|
}
|
||||||
|
|
||||||
|
shouldSkipAccountBindingValidation := !acmeProv.RequireEAB
|
||||||
|
if shouldSkipAccountBindingValidation {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if nar.ExternalAccountBinding == nil {
|
||||||
|
return acme.NewError(acme.ErrorExternalAccountRequiredType, "no external account binding provided")
|
||||||
|
}
|
||||||
|
|
||||||
|
eabJSONBytes, err := json.Marshal(nar.ExternalAccountBinding)
|
||||||
|
if err != nil {
|
||||||
|
return acme.WrapErrorISE(err, "error marshalling externalAccountBinding")
|
||||||
|
}
|
||||||
|
|
||||||
|
eabJWS, err := squarejose.ParseSigned(string(eabJSONBytes))
|
||||||
|
if err != nil {
|
||||||
|
return acme.WrapErrorISE(err, "error parsing externalAccountBinding jws")
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: verify supported algorithms against the incoming alg (and corresponding settings)?
|
||||||
|
// 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)
|
||||||
|
if err != nil {
|
||||||
|
return acme.WrapErrorISE(err, "error retrieving external account key")
|
||||||
|
}
|
||||||
|
|
||||||
|
payload, err := eabJWS.Verify(externalAccountKey.KeyBytes)
|
||||||
|
if err != nil {
|
||||||
|
return acme.WrapErrorISE(err, "error verifying externalAccountBinding signature")
|
||||||
|
}
|
||||||
|
|
||||||
|
jwk, err := jwkFromContext(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
jwkJSONBytes, err := jwk.MarshalJSON()
|
||||||
|
if err != nil {
|
||||||
|
return acme.WrapErrorISE(err, "error marshaling jwk")
|
||||||
|
}
|
||||||
|
|
||||||
|
if bytes.Equal(payload, jwkJSONBytes) {
|
||||||
|
acme.NewError(acme.ErrorMalformedType, "keys in jws and eab payload do not match") // TODO: decide ACME error type to use
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
|
@ -123,6 +123,13 @@ func (h *Handler) GetNonce(w http.ResponseWriter, r *http.Request) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Meta struct {
|
||||||
|
TermsOfService string `json:"termsOfService,omitempty"`
|
||||||
|
Website string `json:"website,omitempty"`
|
||||||
|
CaaIdentities []string `json:"caaIdentities,omitempty"`
|
||||||
|
ExternalAccountRequired bool `json:"externalAccountRequired,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
// Directory represents an ACME directory for configuring clients.
|
// Directory represents an ACME directory for configuring clients.
|
||||||
type Directory struct {
|
type Directory struct {
|
||||||
NewNonce string `json:"newNonce"`
|
NewNonce string `json:"newNonce"`
|
||||||
|
@ -130,6 +137,7 @@ type Directory struct {
|
||||||
NewOrder string `json:"newOrder"`
|
NewOrder string `json:"newOrder"`
|
||||||
RevokeCert string `json:"revokeCert"`
|
RevokeCert string `json:"revokeCert"`
|
||||||
KeyChange string `json:"keyChange"`
|
KeyChange string `json:"keyChange"`
|
||||||
|
Meta Meta `json:"meta"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ToLog enables response logging for the Directory type.
|
// ToLog enables response logging for the Directory type.
|
||||||
|
@ -145,12 +153,27 @@ func (d *Directory) ToLog() (interface{}, error) {
|
||||||
// for client configuration.
|
// for client configuration.
|
||||||
func (h *Handler) GetDirectory(w http.ResponseWriter, r *http.Request) {
|
func (h *Handler) GetDirectory(w http.ResponseWriter, r *http.Request) {
|
||||||
ctx := r.Context()
|
ctx := r.Context()
|
||||||
|
prov, err := provisionerFromContext(ctx)
|
||||||
|
if err != nil {
|
||||||
|
api.WriteError(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
acmeProv, ok := prov.(*provisioner.ACME) // TODO: rewrite into providing configuration via function on acme.Provisioner
|
||||||
|
if !ok || acmeProv == nil {
|
||||||
|
api.WriteError(w, acme.NewErrorISE("provisioner in context is not an ACME provisioner"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
api.JSON(w, &Directory{
|
api.JSON(w, &Directory{
|
||||||
NewNonce: h.linker.GetLink(ctx, NewNonceLinkType),
|
NewNonce: h.linker.GetLink(ctx, NewNonceLinkType),
|
||||||
NewAccount: h.linker.GetLink(ctx, NewAccountLinkType),
|
NewAccount: h.linker.GetLink(ctx, NewAccountLinkType),
|
||||||
NewOrder: h.linker.GetLink(ctx, NewOrderLinkType),
|
NewOrder: h.linker.GetLink(ctx, NewOrderLinkType),
|
||||||
RevokeCert: h.linker.GetLink(ctx, RevokeCertLinkType),
|
RevokeCert: h.linker.GetLink(ctx, RevokeCertLinkType),
|
||||||
KeyChange: h.linker.GetLink(ctx, KeyChangeLinkType),
|
KeyChange: h.linker.GetLink(ctx, KeyChangeLinkType),
|
||||||
|
Meta: Meta{
|
||||||
|
ExternalAccountRequired: acmeProv.RequireEAB,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
26
acme/db.go
26
acme/db.go
|
@ -19,6 +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)
|
||||||
|
GetExternalAccountKey(ctx context.Context, keyID string) (*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
|
||||||
|
|
||||||
|
@ -47,6 +50,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)
|
||||||
|
MockGetExternalAccountKey func(ctx context.Context, keyID string) (*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
|
||||||
|
|
||||||
|
@ -110,6 +116,26 @@ func (m *MockDB) UpdateAccount(ctx context.Context, acc *Account) error {
|
||||||
return m.MockError
|
return m.MockError
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CreateExternalAccountKey mock
|
||||||
|
func (m *MockDB) CreateExternalAccountKey(ctx context.Context, name string) (*ExternalAccountKey, error) {
|
||||||
|
if m.MockCreateExternalAccountKey != nil {
|
||||||
|
return m.MockCreateExternalAccountKey(ctx, name)
|
||||||
|
} else if m.MockError != nil {
|
||||||
|
return nil, m.MockError
|
||||||
|
}
|
||||||
|
return m.MockRet1.(*ExternalAccountKey), m.MockError
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetExternalAccountKey mock
|
||||||
|
func (m *MockDB) GetExternalAccountKey(ctx context.Context, keyID string) (*ExternalAccountKey, error) {
|
||||||
|
if m.MockGetExternalAccountKey != nil {
|
||||||
|
return m.MockGetExternalAccountKey(ctx, keyID)
|
||||||
|
} else if m.MockError != nil {
|
||||||
|
return nil, m.MockError
|
||||||
|
}
|
||||||
|
return m.MockRet1.(*ExternalAccountKey), m.MockError
|
||||||
|
}
|
||||||
|
|
||||||
// CreateNonce mock
|
// CreateNonce mock
|
||||||
func (m *MockDB) CreateNonce(ctx context.Context) (Nonce, error) {
|
func (m *MockDB) CreateNonce(ctx context.Context) (Nonce, error) {
|
||||||
if m.MockCreateNonce != nil {
|
if m.MockCreateNonce != nil {
|
||||||
|
|
|
@ -2,6 +2,7 @@ package nosql
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"crypto/rand"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
@ -26,6 +27,15 @@ func (dba *dbAccount) clone() *dbAccount {
|
||||||
return &nu
|
return &nu
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type dbExternalAccountKey struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
AccountID string `json:"accountID,omitempty"`
|
||||||
|
KeyBytes []byte `json:"key,omitempty"`
|
||||||
|
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) {
|
||||||
id, err := db.db.Get(accountByKeyIDTable, []byte(kid))
|
id, err := db.db.Get(accountByKeyIDTable, []byte(kid))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -134,3 +144,61 @@ func (db *DB) UpdateAccount(ctx context.Context, acc *acme.Account) error {
|
||||||
|
|
||||||
return db.save(ctx, old.ID, nu, old, "account", accountTable)
|
return db.save(ctx, old.ID, nu, old, "account", accountTable)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CreateExternalAccountKey creates a new External Account Binding key with a name
|
||||||
|
func (db *DB) CreateExternalAccountKey(ctx context.Context, name string) (*acme.ExternalAccountKey, error) {
|
||||||
|
keyID, err := randID()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
random := make([]byte, 32)
|
||||||
|
_, err = rand.Read(random)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
dbeak := &dbExternalAccountKey{
|
||||||
|
ID: keyID,
|
||||||
|
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,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetExternalAccountKey retrieves an External Account Binding key by KeyID
|
||||||
|
func (db *DB) GetExternalAccountKey(ctx context.Context, keyID string) (*acme.ExternalAccountKey, error) {
|
||||||
|
data, err := db.db.Get(externalAccountKeyTable, []byte(keyID))
|
||||||
|
if err != nil {
|
||||||
|
if nosqlDB.IsErrNotFound(err) {
|
||||||
|
return nil, acme.ErrNotFound
|
||||||
|
}
|
||||||
|
return nil, errors.Wrapf(err, "error loading external account key %s", keyID)
|
||||||
|
}
|
||||||
|
|
||||||
|
dbeak := new(dbExternalAccountKey)
|
||||||
|
if err = json.Unmarshal(data, dbeak); err != nil {
|
||||||
|
return nil, errors.Wrapf(err, "error unmarshaling external account key %s into dbExternalAccountKey", keyID)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &acme.ExternalAccountKey{
|
||||||
|
ID: dbeak.ID,
|
||||||
|
Name: dbeak.Name,
|
||||||
|
AccountID: dbeak.AccountID,
|
||||||
|
KeyBytes: dbeak.KeyBytes,
|
||||||
|
CreatedAt: dbeak.CreatedAt,
|
||||||
|
BoundAt: dbeak.BoundAt,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
|
@ -11,14 +11,15 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
accountTable = []byte("acme_accounts")
|
accountTable = []byte("acme_accounts")
|
||||||
accountByKeyIDTable = []byte("acme_keyID_accountID_index")
|
accountByKeyIDTable = []byte("acme_keyID_accountID_index")
|
||||||
authzTable = []byte("acme_authzs")
|
authzTable = []byte("acme_authzs")
|
||||||
challengeTable = []byte("acme_challenges")
|
challengeTable = []byte("acme_challenges")
|
||||||
nonceTable = []byte("nonces")
|
nonceTable = []byte("nonces")
|
||||||
orderTable = []byte("acme_orders")
|
orderTable = []byte("acme_orders")
|
||||||
ordersByAccountIDTable = []byte("acme_account_orders_index")
|
ordersByAccountIDTable = []byte("acme_account_orders_index")
|
||||||
certTable = []byte("acme_certs")
|
certTable = []byte("acme_certs")
|
||||||
|
externalAccountKeyTable = []byte("acme_external_account_keys")
|
||||||
)
|
)
|
||||||
|
|
||||||
// DB is a struct that implements the AcmeDB interface.
|
// DB is a struct that implements the AcmeDB interface.
|
||||||
|
@ -29,7 +30,7 @@ type DB struct {
|
||||||
// New configures and returns a new ACME DB backend implemented using a nosql DB.
|
// New configures and returns a new ACME DB backend implemented using a nosql DB.
|
||||||
func New(db nosqlDB.DB) (*DB, error) {
|
func New(db nosqlDB.DB) (*DB, error) {
|
||||||
tables := [][]byte{accountTable, accountByKeyIDTable, authzTable,
|
tables := [][]byte{accountTable, accountByKeyIDTable, authzTable,
|
||||||
challengeTable, nonceTable, orderTable, ordersByAccountIDTable, certTable}
|
challengeTable, nonceTable, orderTable, ordersByAccountIDTable, certTable, externalAccountKeyTable}
|
||||||
for _, b := range tables {
|
for _, b := range tables {
|
||||||
if err := db.CreateTable(b); err != nil {
|
if err := db.CreateTable(b); err != nil {
|
||||||
return nil, errors.Wrapf(err, "error creating table %s",
|
return nil, errors.Wrapf(err, "error creating table %s",
|
||||||
|
|
45
authority/admin/api/eak.go
Normal file
45
authority/admin/api/eak.go
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/smallstep/certificates/api"
|
||||||
|
"github.com/smallstep/certificates/authority/admin"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CreateExternalAccountKeyRequest is the type for GET /admin/eak requests
|
||||||
|
type CreateExternalAccountKeyRequest struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateExternalAccountKeyResponse is the type for GET /admin/eak responses
|
||||||
|
type CreateExternalAccountKeyResponse struct {
|
||||||
|
KeyID string `json:"keyID"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Key []byte `json:"key"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateExternalAccountKey creates a new External Account Binding key
|
||||||
|
func (h *Handler) CreateExternalAccountKey(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var eakRequest = new(CreateExternalAccountKeyRequest)
|
||||||
|
if err := api.ReadJSON(r.Body, eakRequest); err != nil { // TODO: rewrite into protobuf json (likely)
|
||||||
|
api.WriteError(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Validate input
|
||||||
|
|
||||||
|
eak, err := h.db.CreateExternalAccountKey(r.Context(), eakRequest.Name)
|
||||||
|
if err != nil {
|
||||||
|
api.WriteError(w, admin.WrapErrorISE(err, "error creating external account key %s", eakRequest.Name))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
eakResponse := CreateExternalAccountKeyResponse{
|
||||||
|
KeyID: eak.ID,
|
||||||
|
Name: eak.Name,
|
||||||
|
Key: eak.KeyBytes,
|
||||||
|
}
|
||||||
|
|
||||||
|
api.JSONStatus(w, eakResponse, http.StatusCreated) // TODO: rewrite into protobuf json (likely)
|
||||||
|
}
|
|
@ -38,4 +38,7 @@ func (h *Handler) Route(r api.Router) {
|
||||||
r.MethodFunc("POST", "/admins", authnz(h.CreateAdmin))
|
r.MethodFunc("POST", "/admins", authnz(h.CreateAdmin))
|
||||||
r.MethodFunc("PATCH", "/admins/{id}", authnz(h.UpdateAdmin))
|
r.MethodFunc("PATCH", "/admins/{id}", authnz(h.UpdateAdmin))
|
||||||
r.MethodFunc("DELETE", "/admins/{id}", authnz(h.DeleteAdmin))
|
r.MethodFunc("DELETE", "/admins/{id}", authnz(h.DeleteAdmin))
|
||||||
|
|
||||||
|
// External Account Binding Keys
|
||||||
|
r.MethodFunc("POST", "/eak", h.CreateExternalAccountKey) // TODO: authnz
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,6 +7,8 @@ import (
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"go.step.sm/linkedca"
|
"go.step.sm/linkedca"
|
||||||
|
|
||||||
|
"github.com/smallstep/certificates/authority/admin/eak"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@ -67,6 +69,8 @@ type DB interface {
|
||||||
GetAdmins(ctx context.Context) ([]*linkedca.Admin, error)
|
GetAdmins(ctx context.Context) ([]*linkedca.Admin, error)
|
||||||
UpdateAdmin(ctx context.Context, admin *linkedca.Admin) error
|
UpdateAdmin(ctx context.Context, admin *linkedca.Admin) error
|
||||||
DeleteAdmin(ctx context.Context, id string) error
|
DeleteAdmin(ctx context.Context, id string) error
|
||||||
|
|
||||||
|
CreateExternalAccountKey(ctx context.Context, name string) (*eak.ExternalAccountKey, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MockDB is an implementation of the DB interface that should only be used as
|
// MockDB is an implementation of the DB interface that should only be used as
|
||||||
|
@ -84,6 +88,8 @@ type MockDB struct {
|
||||||
MockUpdateAdmin func(ctx context.Context, adm *linkedca.Admin) error
|
MockUpdateAdmin func(ctx context.Context, adm *linkedca.Admin) error
|
||||||
MockDeleteAdmin func(ctx context.Context, id string) error
|
MockDeleteAdmin func(ctx context.Context, id string) error
|
||||||
|
|
||||||
|
MockCreateExternalAccountKey func(ctx context.Context, name string) (*eak.ExternalAccountKey, error)
|
||||||
|
|
||||||
MockError error
|
MockError error
|
||||||
MockRet1 interface{}
|
MockRet1 interface{}
|
||||||
}
|
}
|
||||||
|
@ -177,3 +183,10 @@ func (m *MockDB) DeleteAdmin(ctx context.Context, id string) error {
|
||||||
}
|
}
|
||||||
return m.MockError
|
return m.MockError
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *MockDB) CreateExternalAccountKey(ctx context.Context, name string) (*eak.ExternalAccountKey, error) {
|
||||||
|
if m.MockCreateExternalAccountKey != nil {
|
||||||
|
return m.MockCreateExternalAccountKey(ctx, name)
|
||||||
|
}
|
||||||
|
return m.MockRet1.(*eak.ExternalAccountKey), m.MockError
|
||||||
|
}
|
||||||
|
|
51
authority/admin/db/nosql/eak.go
Normal file
51
authority/admin/db/nosql/eak.go
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
package nosql
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/rand"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/smallstep/certificates/authority/admin/eak"
|
||||||
|
)
|
||||||
|
|
||||||
|
type dbExternalAccountKey struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
AccountID string `json:"accountID,omitempty"`
|
||||||
|
KeyBytes []byte `json:"key,omitempty"`
|
||||||
|
CreatedAt time.Time `json:"createdAt"`
|
||||||
|
BoundAt time.Time `json:"boundAt"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateExternalAccountKey creates a new External Account Binding key
|
||||||
|
func (db *DB) CreateExternalAccountKey(ctx context.Context, name string) (*eak.ExternalAccountKey, error) {
|
||||||
|
keyID, err := randID()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
random := make([]byte, 32)
|
||||||
|
_, err = rand.Read(random)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
dbeak := &dbExternalAccountKey{
|
||||||
|
ID: keyID,
|
||||||
|
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 &eak.ExternalAccountKey{
|
||||||
|
ID: dbeak.ID,
|
||||||
|
Name: dbeak.Name,
|
||||||
|
AccountID: dbeak.AccountID,
|
||||||
|
KeyBytes: dbeak.KeyBytes,
|
||||||
|
CreatedAt: dbeak.CreatedAt,
|
||||||
|
BoundAt: dbeak.BoundAt,
|
||||||
|
}, nil
|
||||||
|
}
|
|
@ -11,8 +11,9 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
adminsTable = []byte("admins")
|
adminsTable = []byte("admins")
|
||||||
provisionersTable = []byte("provisioners")
|
provisionersTable = []byte("provisioners")
|
||||||
|
externalAccountKeyTable = []byte("acme_external_account_keys")
|
||||||
)
|
)
|
||||||
|
|
||||||
// DB is a struct that implements the AdminDB interface.
|
// DB is a struct that implements the AdminDB interface.
|
||||||
|
@ -23,7 +24,7 @@ type DB struct {
|
||||||
|
|
||||||
// New configures and returns a new Authority DB backend implemented using a nosql DB.
|
// New configures and returns a new Authority DB backend implemented using a nosql DB.
|
||||||
func New(db nosqlDB.DB, authorityID string) (*DB, error) {
|
func New(db nosqlDB.DB, authorityID string) (*DB, error) {
|
||||||
tables := [][]byte{adminsTable, provisionersTable}
|
tables := [][]byte{adminsTable, provisionersTable, externalAccountKeyTable}
|
||||||
for _, b := range tables {
|
for _, b := range tables {
|
||||||
if err := db.CreateTable(b); err != nil {
|
if err := db.CreateTable(b); err != nil {
|
||||||
return nil, errors.Wrapf(err, "error creating table %s",
|
return nil, errors.Wrapf(err, "error creating table %s",
|
||||||
|
|
12
authority/admin/eak/eak.go
Normal file
12
authority/admin/eak/eak.go
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
package eak
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
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"`
|
||||||
|
}
|
|
@ -13,13 +13,14 @@ import (
|
||||||
// provisioning flow.
|
// provisioning flow.
|
||||||
type ACME struct {
|
type ACME struct {
|
||||||
*base
|
*base
|
||||||
ID string `json:"-"`
|
ID string `json:"-"`
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
ForceCN bool `json:"forceCN,omitempty"`
|
ForceCN bool `json:"forceCN,omitempty"`
|
||||||
Claims *Claims `json:"claims,omitempty"`
|
RequireEAB bool `json:"requireEAB,omitempty"`
|
||||||
Options *Options `json:"options,omitempty"`
|
Claims *Claims `json:"claims,omitempty"`
|
||||||
claimer *Claimer
|
Options *Options `json:"options,omitempty"`
|
||||||
|
claimer *Claimer
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetID returns the provisioner unique identifier.
|
// GetID returns the provisioner unique identifier.
|
||||||
|
|
Loading…
Reference in a new issue