forked from TrueCloudLab/certificates
Merge branch 'master' into herman/normalize-ipv6-dns-names
This commit is contained in:
commit
bfa2245abb
48 changed files with 8368 additions and 429 deletions
|
@ -7,6 +7,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
|
|||
## [Unreleased - 0.18.1] - DATE
|
||||
### Added
|
||||
- Support for ACME revocation.
|
||||
- Replace hash function with an RSA SSH CA to "rsa-sha2-256".
|
||||
### Changed
|
||||
### Deprecated
|
||||
### Removed
|
||||
|
|
|
@ -4,6 +4,7 @@ import (
|
|||
"crypto"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
"go.step.sm/crypto/jose"
|
||||
)
|
||||
|
@ -11,11 +12,12 @@ import (
|
|||
// Account is a subset of the internal account type containing only those
|
||||
// attributes required for responses in the ACME protocol.
|
||||
type Account struct {
|
||||
ID string `json:"-"`
|
||||
Key *jose.JSONWebKey `json:"-"`
|
||||
Contact []string `json:"contact,omitempty"`
|
||||
Status Status `json:"status"`
|
||||
OrdersURL string `json:"orders"`
|
||||
ID string `json:"-"`
|
||||
Key *jose.JSONWebKey `json:"-"`
|
||||
Contact []string `json:"contact,omitempty"`
|
||||
Status Status `json:"status"`
|
||||
OrdersURL string `json:"orders"`
|
||||
ExternalAccountBinding interface{} `json:"externalAccountBinding,omitempty"`
|
||||
}
|
||||
|
||||
// ToLog enables response logging.
|
||||
|
@ -40,3 +42,32 @@ func KeyToID(jwk *jose.JSONWebKey) (string, error) {
|
|||
}
|
||||
return base64.RawURLEncoding.EncodeToString(kid), nil
|
||||
}
|
||||
|
||||
// ExternalAccountKey is an ACME External Account Binding key.
|
||||
type ExternalAccountKey struct {
|
||||
ID string `json:"id"`
|
||||
ProvisionerID string `json:"provisionerID"`
|
||||
Reference string `json:"reference"`
|
||||
AccountID string `json:"-"`
|
||||
KeyBytes []byte `json:"-"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
BoundAt time.Time `json:"boundAt,omitempty"`
|
||||
}
|
||||
|
||||
// AlreadyBound returns whether this EAK is already bound to
|
||||
// an ACME Account or not.
|
||||
func (eak *ExternalAccountKey) AlreadyBound() bool {
|
||||
return !eak.BoundAt.IsZero()
|
||||
}
|
||||
|
||||
// BindTo binds the EAK to an Account.
|
||||
// It returns an error if it's already bound.
|
||||
func (eak *ExternalAccountKey) BindTo(account *Account) error {
|
||||
if eak.AlreadyBound() {
|
||||
return NewError(ErrorUnauthorizedType, "external account binding key with id '%s' was already bound to account '%s' on %s", eak.ID, eak.AccountID, eak.BoundAt)
|
||||
}
|
||||
eak.AccountID = account.ID
|
||||
eak.BoundAt = time.Now()
|
||||
eak.KeyBytes = []byte{} // clearing the key bytes; can only be used once
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@ import (
|
|||
"crypto"
|
||||
"encoding/base64"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/smallstep/assert"
|
||||
|
@ -79,3 +80,67 @@ func TestAccount_IsValid(t *testing.T) {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestExternalAccountKey_BindTo(t *testing.T) {
|
||||
boundAt := time.Now()
|
||||
tests := []struct {
|
||||
name string
|
||||
eak *ExternalAccountKey
|
||||
acct *Account
|
||||
err *Error
|
||||
}{
|
||||
{
|
||||
name: "ok",
|
||||
eak: &ExternalAccountKey{
|
||||
ID: "eakID",
|
||||
ProvisionerID: "provID",
|
||||
Reference: "ref",
|
||||
KeyBytes: []byte{1, 3, 3, 7},
|
||||
},
|
||||
acct: &Account{
|
||||
ID: "accountID",
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
name: "fail/already-bound",
|
||||
eak: &ExternalAccountKey{
|
||||
ID: "eakID",
|
||||
ProvisionerID: "provID",
|
||||
Reference: "ref",
|
||||
KeyBytes: []byte{1, 3, 3, 7},
|
||||
AccountID: "someAccountID",
|
||||
BoundAt: boundAt,
|
||||
},
|
||||
acct: &Account{
|
||||
ID: "accountID",
|
||||
},
|
||||
err: NewError(ErrorUnauthorizedType, "external account binding key with id '%s' was already bound to account '%s' on %s", "eakID", "someAccountID", boundAt),
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
eak := tt.eak
|
||||
acct := tt.acct
|
||||
err := eak.BindTo(acct)
|
||||
wantErr := tt.err != nil
|
||||
gotErr := err != nil
|
||||
if wantErr != gotErr {
|
||||
t.Errorf("ExternalAccountKey.BindTo() error = %v, wantErr %v", err, tt.err)
|
||||
}
|
||||
if wantErr {
|
||||
assert.NotNil(t, err)
|
||||
assert.Type(t, &Error{}, err)
|
||||
ae, _ := err.(*Error)
|
||||
assert.Equals(t, ae.Type, tt.err.Type)
|
||||
assert.Equals(t, ae.Detail, tt.err.Detail)
|
||||
assert.Equals(t, ae.Identifier, tt.err.Identifier)
|
||||
assert.Equals(t, ae.Subproblems, tt.err.Subproblems)
|
||||
} else {
|
||||
assert.Equals(t, eak.AccountID, acct.ID)
|
||||
assert.Equals(t, eak.KeyBytes, []byte{})
|
||||
assert.NotNil(t, eak.BoundAt)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,9 +12,10 @@ import (
|
|||
|
||||
// NewAccountRequest represents the payload for a new account request.
|
||||
type NewAccountRequest struct {
|
||||
Contact []string `json:"contact"`
|
||||
OnlyReturnExisting bool `json:"onlyReturnExisting"`
|
||||
TermsOfServiceAgreed bool `json:"termsOfServiceAgreed"`
|
||||
Contact []string `json:"contact"`
|
||||
OnlyReturnExisting bool `json:"onlyReturnExisting"`
|
||||
TermsOfServiceAgreed bool `json:"termsOfServiceAgreed"`
|
||||
ExternalAccountBinding *ExternalAccountBinding `json:"externalAccountBinding,omitempty"`
|
||||
}
|
||||
|
||||
func validateContacts(cs []string) error {
|
||||
|
@ -83,8 +84,14 @@ 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())
|
||||
acc, err := accountFromContext(ctx)
|
||||
if err != nil {
|
||||
acmeErr, ok := err.(*acme.Error)
|
||||
if !ok || acmeErr.Status != http.StatusBadRequest {
|
||||
|
@ -99,12 +106,19 @@ func (h *Handler) NewAccount(w http.ResponseWriter, r *http.Request) {
|
|||
"account does not exist"))
|
||||
return
|
||||
}
|
||||
|
||||
jwk, err := jwkFromContext(ctx)
|
||||
if err != nil {
|
||||
api.WriteError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
eak, err := h.validateExternalAccountBinding(ctx, &nar)
|
||||
if err != nil {
|
||||
api.WriteError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
acc = &acme.Account{
|
||||
Key: jwk,
|
||||
Contact: nar.Contact,
|
||||
|
@ -114,8 +128,21 @@ func (h *Handler) NewAccount(w http.ResponseWriter, r *http.Request) {
|
|||
api.WriteError(w, acme.WrapErrorISE(err, "error creating account"))
|
||||
return
|
||||
}
|
||||
|
||||
if eak != nil { // means that we have a (valid) External Account Binding key that should be bound, updated and sent in the response
|
||||
err := eak.BindTo(acc)
|
||||
if err != nil {
|
||||
api.WriteError(w, err)
|
||||
return
|
||||
}
|
||||
if err := h.db.UpdateExternalAccountKey(ctx, prov.ID, eak); err != nil {
|
||||
api.WriteError(w, acme.WrapErrorISE(err, "error updating external account binding key"))
|
||||
return
|
||||
}
|
||||
acc.ExternalAccountBinding = nar.ExternalAccountBinding
|
||||
}
|
||||
} else {
|
||||
// Account exists //
|
||||
// Account exists
|
||||
httpStatus = http.StatusOK
|
||||
}
|
||||
|
||||
|
|
|
@ -12,6 +12,7 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/go-chi/chi"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/smallstep/assert"
|
||||
"github.com/smallstep/certificates/acme"
|
||||
"github.com/smallstep/certificates/authority/provisioner"
|
||||
|
@ -40,6 +41,66 @@ 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
|
||||
}
|
||||
|
||||
func createEABJWS(jwk *jose.JSONWebKey, hmacKey []byte, keyID, u string) (*jose.JSONWebSignature, error) {
|
||||
signer, err := jose.NewSigner(
|
||||
jose.SigningKey{
|
||||
Algorithm: jose.SignatureAlgorithm("HS256"),
|
||||
Key: hmacKey,
|
||||
},
|
||||
&jose.SignerOptions{
|
||||
ExtraHeaders: map[jose.HeaderKey]interface{}{
|
||||
"kid": keyID,
|
||||
"url": u,
|
||||
},
|
||||
EmbedJWK: false,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
jwkJSONBytes, err := jwk.Public().MarshalJSON()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
jws, err := signer.Sign(jwkJSONBytes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
raw, err := jws.CompactSerialize()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
parsedJWS, err := jose.ParseJWS(raw)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return parsedJWS, nil
|
||||
}
|
||||
|
||||
func createRawEABJWS(jwk *jose.JSONWebKey, hmacKey []byte, keyID, u string) ([]byte, error) {
|
||||
jws, err := createEABJWS(jwk, hmacKey, keyID, u)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
rawJWS := jws.FullSerialize()
|
||||
return []byte(rawJWS), nil
|
||||
}
|
||||
|
||||
func TestNewAccountRequest_Validate(t *testing.T) {
|
||||
type test struct {
|
||||
nar *NewAccountRequest
|
||||
|
@ -290,6 +351,7 @@ func TestHandler_NewAccount(t *testing.T) {
|
|||
prov := newProv()
|
||||
escProvName := url.PathEscape(prov.GetName())
|
||||
baseURL := &url.URL{Scheme: "https", Host: "test.ca.smallstep.com"}
|
||||
provID := prov.GetID()
|
||||
|
||||
type test struct {
|
||||
db acme.DB
|
||||
|
@ -343,6 +405,7 @@ func TestHandler_NewAccount(t *testing.T) {
|
|||
b, err := json.Marshal(nar)
|
||||
assert.FatalError(t, err)
|
||||
ctx := context.WithValue(context.Background(), payloadContextKey, &payloadInfo{value: b})
|
||||
ctx = context.WithValue(ctx, provisionerContextKey, prov)
|
||||
return test{
|
||||
ctx: ctx,
|
||||
statusCode: 400,
|
||||
|
@ -355,7 +418,8 @@ func TestHandler_NewAccount(t *testing.T) {
|
|||
}
|
||||
b, err := json.Marshal(nar)
|
||||
assert.FatalError(t, err)
|
||||
ctx := context.WithValue(context.Background(), payloadContextKey, &payloadInfo{value: b})
|
||||
ctx := context.WithValue(context.Background(), provisionerContextKey, prov)
|
||||
ctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{value: b})
|
||||
return test{
|
||||
ctx: ctx,
|
||||
statusCode: 500,
|
||||
|
@ -368,7 +432,8 @@ func TestHandler_NewAccount(t *testing.T) {
|
|||
}
|
||||
b, err := json.Marshal(nar)
|
||||
assert.FatalError(t, err)
|
||||
ctx := context.WithValue(context.Background(), payloadContextKey, &payloadInfo{value: b})
|
||||
ctx := context.WithValue(context.Background(), provisionerContextKey, prov)
|
||||
ctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{value: b})
|
||||
ctx = context.WithValue(ctx, jwkContextKey, nil)
|
||||
return test{
|
||||
ctx: ctx,
|
||||
|
@ -376,6 +441,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"},
|
||||
|
@ -385,6 +471,7 @@ func TestHandler_NewAccount(t *testing.T) {
|
|||
jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0)
|
||||
assert.FatalError(t, err)
|
||||
ctx := context.WithValue(context.Background(), payloadContextKey, &payloadInfo{value: b})
|
||||
ctx = context.WithValue(ctx, provisionerContextKey, prov)
|
||||
ctx = context.WithValue(ctx, jwkContextKey, jwk)
|
||||
return test{
|
||||
db: &acme.MockDB{
|
||||
|
@ -399,6 +486,109 @@ func TestHandler_NewAccount(t *testing.T) {
|
|||
err: acme.NewErrorISE("force"),
|
||||
}
|
||||
},
|
||||
"fail/acmeProvisionerFromContext": func(t *testing.T) test {
|
||||
jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0)
|
||||
assert.FatalError(t, err)
|
||||
url := fmt.Sprintf("%s/acme/%s/account/new-account", baseURL.String(), escProvName)
|
||||
rawEABJWS, err := createRawEABJWS(jwk, []byte{1, 3, 3, 7}, "eakID", url)
|
||||
assert.FatalError(t, err)
|
||||
eab := &ExternalAccountBinding{}
|
||||
err = json.Unmarshal(rawEABJWS, &eab)
|
||||
assert.FatalError(t, err)
|
||||
nar := &NewAccountRequest{
|
||||
Contact: []string{"foo", "bar"},
|
||||
ExternalAccountBinding: eab,
|
||||
}
|
||||
b, err := json.Marshal(nar)
|
||||
assert.FatalError(t, err)
|
||||
scepProvisioner := &provisioner.SCEP{
|
||||
Type: "SCEP",
|
||||
Name: "test@scep-<test>provisioner.com",
|
||||
}
|
||||
if err := scepProvisioner.Init(provisioner.Config{Claims: globalProvisionerClaims}); err != nil {
|
||||
assert.FatalError(t, err)
|
||||
}
|
||||
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, scepProvisioner)
|
||||
return test{
|
||||
ctx: ctx,
|
||||
statusCode: 500,
|
||||
err: acme.NewError(acme.ErrorServerInternalType, "provisioner in context is not an ACME provisioner"),
|
||||
}
|
||||
},
|
||||
"fail/db.UpdateExternalAccountKey-error": func(t *testing.T) test {
|
||||
jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0)
|
||||
assert.FatalError(t, err)
|
||||
url := fmt.Sprintf("%s/acme/%s/account/new-account", baseURL.String(), escProvName)
|
||||
rawEABJWS, err := createRawEABJWS(jwk, []byte{1, 3, 3, 7}, "eakID", url)
|
||||
assert.FatalError(t, err)
|
||||
eab := &ExternalAccountBinding{}
|
||||
err = json.Unmarshal(rawEABJWS, &eab)
|
||||
assert.FatalError(t, err)
|
||||
nar := &NewAccountRequest{
|
||||
Contact: []string{"foo", "bar"},
|
||||
ExternalAccountBinding: eab,
|
||||
}
|
||||
payloadBytes, err := json.Marshal(nar)
|
||||
assert.FatalError(t, err)
|
||||
so := new(jose.SignerOptions)
|
||||
so.WithHeader("alg", jose.SignatureAlgorithm(jwk.Algorithm))
|
||||
so.WithHeader("url", url)
|
||||
signer, err := jose.NewSigner(jose.SigningKey{
|
||||
Algorithm: jose.SignatureAlgorithm(jwk.Algorithm),
|
||||
Key: jwk.Key,
|
||||
}, so)
|
||||
assert.FatalError(t, err)
|
||||
jws, err := signer.Sign(payloadBytes)
|
||||
assert.FatalError(t, err)
|
||||
raw, err := jws.CompactSerialize()
|
||||
assert.FatalError(t, err)
|
||||
parsedJWS, err := jose.ParseJWS(raw)
|
||||
assert.FatalError(t, err)
|
||||
prov := newACMEProv(t)
|
||||
prov.RequireEAB = true
|
||||
ctx := context.WithValue(context.Background(), payloadContextKey, &payloadInfo{value: payloadBytes})
|
||||
ctx = context.WithValue(ctx, jwkContextKey, jwk)
|
||||
ctx = context.WithValue(ctx, baseURLContextKey, baseURL)
|
||||
ctx = context.WithValue(ctx, provisionerContextKey, prov)
|
||||
ctx = context.WithValue(ctx, jwsContextKey, parsedJWS)
|
||||
eak := &acme.ExternalAccountKey{
|
||||
ID: "eakID",
|
||||
ProvisionerID: provID,
|
||||
Reference: "testeak",
|
||||
KeyBytes: []byte{1, 3, 3, 7},
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
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, keyID string) (*acme.ExternalAccountKey, error) {
|
||||
return eak, nil
|
||||
},
|
||||
MockUpdateExternalAccountKey: func(ctx context.Context, provisionerName string, eak *acme.ExternalAccountKey) error {
|
||||
return errors.New("force")
|
||||
},
|
||||
},
|
||||
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: eab,
|
||||
},
|
||||
ctx: ctx,
|
||||
statusCode: 500,
|
||||
err: acme.NewError(acme.ErrorServerInternalType, "error updating external account binding key"),
|
||||
}
|
||||
},
|
||||
"ok/new-account": func(t *testing.T) test {
|
||||
nar := &NewAccountRequest{
|
||||
Contact: []string{"foo", "bar"},
|
||||
|
@ -455,6 +645,116 @@ func TestHandler_NewAccount(t *testing.T) {
|
|||
statusCode: 200,
|
||||
}
|
||||
},
|
||||
"ok/new-account-no-eab-required": func(t *testing.T) test {
|
||||
jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0)
|
||||
assert.FatalError(t, err)
|
||||
url := fmt.Sprintf("%s/acme/%s/account/new-account", baseURL.String(), escProvName)
|
||||
rawEABJWS, err := createRawEABJWS(jwk, []byte{1, 3, 3, 7}, "eakID", url)
|
||||
assert.FatalError(t, err)
|
||||
eab := &ExternalAccountBinding{}
|
||||
err = json.Unmarshal(rawEABJWS, &eab)
|
||||
assert.FatalError(t, err)
|
||||
nar := &NewAccountRequest{
|
||||
Contact: []string{"foo", "bar"},
|
||||
ExternalAccountBinding: eab,
|
||||
}
|
||||
b, err := json.Marshal(nar)
|
||||
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)
|
||||
url := fmt.Sprintf("%s/acme/%s/account/new-account", baseURL.String(), escProvName)
|
||||
rawEABJWS, err := createRawEABJWS(jwk, []byte{1, 3, 3, 7}, "eakID", url)
|
||||
assert.FatalError(t, err)
|
||||
eab := &ExternalAccountBinding{}
|
||||
err = json.Unmarshal(rawEABJWS, &eab)
|
||||
assert.FatalError(t, err)
|
||||
nar := &NewAccountRequest{
|
||||
Contact: []string{"foo", "bar"},
|
||||
ExternalAccountBinding: eab,
|
||||
}
|
||||
payloadBytes, err := json.Marshal(nar)
|
||||
assert.FatalError(t, err)
|
||||
so := new(jose.SignerOptions)
|
||||
so.WithHeader("alg", jose.SignatureAlgorithm(jwk.Algorithm))
|
||||
so.WithHeader("url", url)
|
||||
signer, err := jose.NewSigner(jose.SigningKey{
|
||||
Algorithm: jose.SignatureAlgorithm(jwk.Algorithm),
|
||||
Key: jwk.Key,
|
||||
}, so)
|
||||
assert.FatalError(t, err)
|
||||
jws, err := signer.Sign(payloadBytes)
|
||||
assert.FatalError(t, err)
|
||||
raw, err := jws.CompactSerialize()
|
||||
assert.FatalError(t, err)
|
||||
parsedJWS, err := jose.ParseJWS(raw)
|
||||
assert.FatalError(t, err)
|
||||
prov := newACMEProv(t)
|
||||
prov.RequireEAB = true
|
||||
ctx := context.WithValue(context.Background(), payloadContextKey, &payloadInfo{value: payloadBytes})
|
||||
ctx = context.WithValue(ctx, jwkContextKey, jwk)
|
||||
ctx = context.WithValue(ctx, baseURLContextKey, baseURL)
|
||||
ctx = context.WithValue(ctx, provisionerContextKey, prov)
|
||||
ctx = context.WithValue(ctx, jwsContextKey, parsedJWS)
|
||||
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, keyID string) (*acme.ExternalAccountKey, error) {
|
||||
return &acme.ExternalAccountKey{
|
||||
ID: "eakID",
|
||||
ProvisionerID: provID,
|
||||
Reference: "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: eab,
|
||||
},
|
||||
ctx: ctx,
|
||||
statusCode: 201,
|
||||
}
|
||||
},
|
||||
}
|
||||
for name, run := range tests {
|
||||
tc := run(t)
|
||||
|
|
155
acme/api/eab.go
Normal file
155
acme/api/eab.go
Normal file
|
@ -0,0 +1,155 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
|
||||
"github.com/smallstep/certificates/acme"
|
||||
"go.step.sm/crypto/jose"
|
||||
)
|
||||
|
||||
// ExternalAccountBinding represents the ACME externalAccountBinding JWS
|
||||
type ExternalAccountBinding struct {
|
||||
Protected string `json:"protected"`
|
||||
Payload string `json:"payload"`
|
||||
Sig string `json:"signature"`
|
||||
}
|
||||
|
||||
// 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 {
|
||||
return nil, acme.WrapErrorISE(err, "could not load ACME provisioner from context")
|
||||
}
|
||||
|
||||
if !acmeProv.RequireEAB {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
if nar.ExternalAccountBinding == nil {
|
||||
return nil, acme.NewError(acme.ErrorExternalAccountRequiredType, "no external account binding provided")
|
||||
}
|
||||
|
||||
eabJSONBytes, err := json.Marshal(nar.ExternalAccountBinding)
|
||||
if err != nil {
|
||||
return nil, acme.WrapErrorISE(err, "error marshaling externalAccountBinding into bytes")
|
||||
}
|
||||
|
||||
eabJWS, err := jose.ParseJWS(string(eabJSONBytes))
|
||||
if err != nil {
|
||||
return nil, acme.WrapErrorISE(err, "error parsing externalAccountBinding jws")
|
||||
}
|
||||
|
||||
// TODO(hs): implement strategy pattern to allow for different ways of verification (i.e. webhook call) based on configuration?
|
||||
|
||||
keyID, acmeErr := validateEABJWS(ctx, eabJWS)
|
||||
if acmeErr != nil {
|
||||
return nil, acmeErr
|
||||
}
|
||||
|
||||
externalAccountKey, err := h.db.GetExternalAccountKey(ctx, acmeProv.ID, keyID)
|
||||
if err != nil {
|
||||
if _, ok := err.(*acme.Error); ok {
|
||||
return nil, acme.WrapError(acme.ErrorUnauthorizedType, err, "the field 'kid' references an unknown key")
|
||||
}
|
||||
return nil, acme.WrapErrorISE(err, "error retrieving external account key")
|
||||
}
|
||||
|
||||
if externalAccountKey.AlreadyBound() {
|
||||
return nil, acme.NewError(acme.ErrorUnauthorizedType, "external account binding key with id '%s' was already bound to account '%s' on %s", keyID, externalAccountKey.AccountID, externalAccountKey.BoundAt)
|
||||
}
|
||||
|
||||
payload, err := eabJWS.Verify(externalAccountKey.KeyBytes)
|
||||
if err != nil {
|
||||
return nil, acme.WrapErrorISE(err, "error verifying externalAccountBinding signature")
|
||||
}
|
||||
|
||||
jwk, err := jwkFromContext(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var payloadJWK *jose.JSONWebKey
|
||||
if err = json.Unmarshal(payload, &payloadJWK); err != nil {
|
||||
return nil, acme.WrapError(acme.ErrorMalformedType, err, "error unmarshaling payload into jwk")
|
||||
}
|
||||
|
||||
if !keysAreEqual(jwk, payloadJWK) {
|
||||
return nil, acme.NewError(acme.ErrorUnauthorizedType, "keys in jws and eab payload do not match")
|
||||
}
|
||||
|
||||
return externalAccountKey, nil
|
||||
}
|
||||
|
||||
// keysAreEqual performs an equality check on two JWKs by comparing
|
||||
// the (base64 encoding) of the Key IDs.
|
||||
func keysAreEqual(x, y *jose.JSONWebKey) bool {
|
||||
if x == nil || y == nil {
|
||||
return false
|
||||
}
|
||||
digestX, errX := acme.KeyToID(x)
|
||||
digestY, errY := acme.KeyToID(y)
|
||||
if errX != nil || errY != nil {
|
||||
return false
|
||||
}
|
||||
return digestX == digestY
|
||||
}
|
||||
|
||||
// validateEABJWS verifies the contents of the External Account Binding JWS.
|
||||
// The protected header of the JWS MUST meet the following criteria:
|
||||
// o The "alg" field MUST indicate a MAC-based algorithm
|
||||
// o The "kid" field MUST contain the key identifier provided by the CA
|
||||
// o The "nonce" field MUST NOT be present
|
||||
// o The "url" field MUST be set to the same value as the outer JWS
|
||||
func validateEABJWS(ctx context.Context, jws *jose.JSONWebSignature) (string, *acme.Error) {
|
||||
|
||||
if jws == nil {
|
||||
return "", acme.NewErrorISE("no JWS provided")
|
||||
}
|
||||
|
||||
if len(jws.Signatures) != 1 {
|
||||
return "", acme.NewError(acme.ErrorMalformedType, "JWS must have one signature")
|
||||
}
|
||||
|
||||
header := jws.Signatures[0].Protected
|
||||
algorithm := header.Algorithm
|
||||
keyID := header.KeyID
|
||||
nonce := header.Nonce
|
||||
|
||||
if !(algorithm == jose.HS256 || algorithm == jose.HS384 || algorithm == jose.HS512) {
|
||||
return "", acme.NewError(acme.ErrorMalformedType, "'alg' field set to invalid algorithm '%s'", algorithm)
|
||||
}
|
||||
|
||||
if keyID == "" {
|
||||
return "", acme.NewError(acme.ErrorMalformedType, "'kid' field is required")
|
||||
}
|
||||
|
||||
if nonce != "" {
|
||||
return "", acme.NewError(acme.ErrorMalformedType, "'nonce' must not be present")
|
||||
}
|
||||
|
||||
jwsURL, ok := header.ExtraHeaders["url"]
|
||||
if !ok {
|
||||
return "", acme.NewError(acme.ErrorMalformedType, "'url' field is required")
|
||||
}
|
||||
|
||||
outerJWS, err := jwsFromContext(ctx)
|
||||
if err != nil {
|
||||
return "", acme.WrapErrorISE(err, "could not retrieve outer JWS from context")
|
||||
}
|
||||
|
||||
if len(outerJWS.Signatures) != 1 {
|
||||
return "", acme.NewError(acme.ErrorMalformedType, "outer JWS must have one signature")
|
||||
}
|
||||
|
||||
outerJWSURL, ok := outerJWS.Signatures[0].Protected.ExtraHeaders["url"]
|
||||
if !ok {
|
||||
return "", acme.NewError(acme.ErrorMalformedType, "'url' field must be set in outer JWS")
|
||||
}
|
||||
|
||||
if jwsURL != outerJWSURL {
|
||||
return "", acme.NewError(acme.ErrorMalformedType, "'url' field is not the same value as the outer JWS")
|
||||
}
|
||||
|
||||
return keyID, nil
|
||||
}
|
1068
acme/api/eab_test.go
Normal file
1068
acme/api/eab_test.go
Normal file
File diff suppressed because it is too large
Load diff
|
@ -136,6 +136,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.
|
||||
type Directory struct {
|
||||
NewNonce string `json:"newNonce"`
|
||||
|
@ -143,6 +150,7 @@ type Directory struct {
|
|||
NewOrder string `json:"newOrder"`
|
||||
RevokeCert string `json:"revokeCert"`
|
||||
KeyChange string `json:"keyChange"`
|
||||
Meta Meta `json:"meta"`
|
||||
}
|
||||
|
||||
// ToLog enables response logging for the Directory type.
|
||||
|
@ -158,12 +166,21 @@ func (d *Directory) ToLog() (interface{}, error) {
|
|||
// for client configuration.
|
||||
func (h *Handler) GetDirectory(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
acmeProv, err := acmeProvisionerFromContext(ctx)
|
||||
if err != nil {
|
||||
api.WriteError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
api.JSON(w, &Directory{
|
||||
NewNonce: h.linker.GetLink(ctx, NewNonceLinkType),
|
||||
NewAccount: h.linker.GetLink(ctx, NewAccountLinkType),
|
||||
NewOrder: h.linker.GetLink(ctx, NewOrderLinkType),
|
||||
RevokeCert: h.linker.GetLink(ctx, RevokeCertLinkType),
|
||||
KeyChange: h.linker.GetLink(ctx, KeyChangeLinkType),
|
||||
Meta: Meta{
|
||||
ExternalAccountRequired: acmeProv.RequireEAB,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -15,9 +15,11 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/go-chi/chi"
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/smallstep/assert"
|
||||
"github.com/smallstep/certificates/acme"
|
||||
"github.com/smallstep/certificates/authority/provisioner"
|
||||
"go.step.sm/crypto/jose"
|
||||
"go.step.sm/crypto/pemutil"
|
||||
)
|
||||
|
@ -51,28 +53,76 @@ func TestHandler_GetNonce(t *testing.T) {
|
|||
|
||||
func TestHandler_GetDirectory(t *testing.T) {
|
||||
linker := NewLinker("ca.smallstep.com", "acme")
|
||||
|
||||
prov := newProv()
|
||||
provName := url.PathEscape(prov.GetName())
|
||||
baseURL := &url.URL{Scheme: "https", Host: "test.ca.smallstep.com"}
|
||||
ctx := context.WithValue(context.Background(), provisionerContextKey, prov)
|
||||
ctx = context.WithValue(ctx, baseURLContextKey, baseURL)
|
||||
|
||||
expDir := Directory{
|
||||
NewNonce: fmt.Sprintf("%s/acme/%s/new-nonce", baseURL.String(), provName),
|
||||
NewAccount: fmt.Sprintf("%s/acme/%s/new-account", baseURL.String(), provName),
|
||||
NewOrder: fmt.Sprintf("%s/acme/%s/new-order", baseURL.String(), provName),
|
||||
RevokeCert: fmt.Sprintf("%s/acme/%s/revoke-cert", baseURL.String(), provName),
|
||||
KeyChange: fmt.Sprintf("%s/acme/%s/key-change", baseURL.String(), provName),
|
||||
}
|
||||
|
||||
type test struct {
|
||||
ctx context.Context
|
||||
statusCode int
|
||||
dir Directory
|
||||
err *acme.Error
|
||||
}
|
||||
var tests = map[string]func(t *testing.T) test{
|
||||
"ok": func(t *testing.T) test {
|
||||
"fail/no-provisioner": func(t *testing.T) test {
|
||||
baseURL := &url.URL{Scheme: "https", Host: "test.ca.smallstep.com"}
|
||||
ctx := context.WithValue(context.Background(), provisionerContextKey, nil)
|
||||
ctx = context.WithValue(ctx, baseURLContextKey, baseURL)
|
||||
return test{
|
||||
ctx: ctx,
|
||||
statusCode: 500,
|
||||
err: acme.NewErrorISE("provisioner in context is not an ACME provisioner"),
|
||||
}
|
||||
},
|
||||
"fail/different-provisioner": func(t *testing.T) test {
|
||||
prov := &provisioner.SCEP{
|
||||
Type: "SCEP",
|
||||
Name: "test@scep-<test>provisioner.com",
|
||||
}
|
||||
baseURL := &url.URL{Scheme: "https", Host: "test.ca.smallstep.com"}
|
||||
ctx := context.WithValue(context.Background(), provisionerContextKey, prov)
|
||||
ctx = context.WithValue(ctx, baseURLContextKey, baseURL)
|
||||
return test{
|
||||
ctx: ctx,
|
||||
statusCode: 500,
|
||||
err: acme.NewErrorISE("provisioner in context is not an ACME provisioner"),
|
||||
}
|
||||
},
|
||||
"ok": func(t *testing.T) test {
|
||||
prov := newProv()
|
||||
provName := url.PathEscape(prov.GetName())
|
||||
baseURL := &url.URL{Scheme: "https", Host: "test.ca.smallstep.com"}
|
||||
ctx := context.WithValue(context.Background(), provisionerContextKey, prov)
|
||||
ctx = context.WithValue(ctx, baseURLContextKey, baseURL)
|
||||
expDir := Directory{
|
||||
NewNonce: fmt.Sprintf("%s/acme/%s/new-nonce", baseURL.String(), provName),
|
||||
NewAccount: fmt.Sprintf("%s/acme/%s/new-account", baseURL.String(), provName),
|
||||
NewOrder: fmt.Sprintf("%s/acme/%s/new-order", baseURL.String(), provName),
|
||||
RevokeCert: fmt.Sprintf("%s/acme/%s/revoke-cert", baseURL.String(), provName),
|
||||
KeyChange: fmt.Sprintf("%s/acme/%s/key-change", baseURL.String(), provName),
|
||||
}
|
||||
return test{
|
||||
ctx: ctx,
|
||||
dir: expDir,
|
||||
statusCode: 200,
|
||||
}
|
||||
},
|
||||
"ok/eab-required": func(t *testing.T) test {
|
||||
prov := newACMEProv(t)
|
||||
prov.RequireEAB = true
|
||||
provName := url.PathEscape(prov.GetName())
|
||||
baseURL := &url.URL{Scheme: "https", Host: "test.ca.smallstep.com"}
|
||||
ctx := context.WithValue(context.Background(), provisionerContextKey, prov)
|
||||
ctx = context.WithValue(ctx, baseURLContextKey, baseURL)
|
||||
expDir := Directory{
|
||||
NewNonce: fmt.Sprintf("%s/acme/%s/new-nonce", baseURL.String(), provName),
|
||||
NewAccount: fmt.Sprintf("%s/acme/%s/new-account", baseURL.String(), provName),
|
||||
NewOrder: fmt.Sprintf("%s/acme/%s/new-order", baseURL.String(), provName),
|
||||
RevokeCert: fmt.Sprintf("%s/acme/%s/revoke-cert", baseURL.String(), provName),
|
||||
KeyChange: fmt.Sprintf("%s/acme/%s/key-change", baseURL.String(), provName),
|
||||
Meta: Meta{
|
||||
ExternalAccountRequired: true,
|
||||
},
|
||||
}
|
||||
return test{
|
||||
ctx: ctx,
|
||||
dir: expDir,
|
||||
statusCode: 200,
|
||||
}
|
||||
},
|
||||
|
@ -82,7 +132,7 @@ func TestHandler_GetDirectory(t *testing.T) {
|
|||
t.Run(name, func(t *testing.T) {
|
||||
h := &Handler{linker: linker}
|
||||
req := httptest.NewRequest("GET", "/foo/bar", nil)
|
||||
req = req.WithContext(ctx)
|
||||
req = req.WithContext(tc.ctx)
|
||||
w := httptest.NewRecorder()
|
||||
h.GetDirectory(w, req)
|
||||
res := w.Result()
|
||||
|
@ -105,7 +155,9 @@ func TestHandler_GetDirectory(t *testing.T) {
|
|||
} else {
|
||||
var dir Directory
|
||||
json.Unmarshal(bytes.TrimSpace(body), &dir)
|
||||
assert.Equals(t, dir, expDir)
|
||||
if !cmp.Equal(tc.dir, dir) {
|
||||
t.Errorf("GetDirectory() diff =\n%s", cmp.Diff(tc.dir, dir))
|
||||
}
|
||||
assert.Equals(t, res.Header["Content-Type"], []string{"application/json"})
|
||||
}
|
||||
})
|
||||
|
|
|
@ -507,6 +507,20 @@ func provisionerFromContext(ctx context.Context) (acme.Provisioner, error) {
|
|||
return pval, nil
|
||||
}
|
||||
|
||||
// acmeProvisionerFromContext searches the context for an ACME provisioner. Returns
|
||||
// pointer to an ACME provisioner or an error.
|
||||
func acmeProvisionerFromContext(ctx context.Context) (*provisioner.ACME, error) {
|
||||
prov, err := provisionerFromContext(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
acmeProv, ok := prov.(*provisioner.ACME)
|
||||
if !ok || acmeProv == nil {
|
||||
return nil, acme.NewErrorISE("provisioner in context is not an ACME provisioner")
|
||||
}
|
||||
return acmeProv, nil
|
||||
}
|
||||
|
||||
// payloadFromContext searches the context for a payload. Returns the payload
|
||||
// or an error.
|
||||
func payloadFromContext(ctx context.Context) (*payloadInfo, error) {
|
||||
|
|
74
acme/db.go
74
acme/db.go
|
@ -19,6 +19,13 @@ type DB interface {
|
|||
GetAccountByKeyID(ctx context.Context, kid string) (*Account, error)
|
||||
UpdateAccount(ctx context.Context, acc *Account) error
|
||||
|
||||
CreateExternalAccountKey(ctx context.Context, provisionerID, reference string) (*ExternalAccountKey, error)
|
||||
GetExternalAccountKey(ctx context.Context, provisionerID, keyID string) (*ExternalAccountKey, error)
|
||||
GetExternalAccountKeys(ctx context.Context, provisionerID, cursor string, limit int) ([]*ExternalAccountKey, string, error)
|
||||
GetExternalAccountKeyByReference(ctx context.Context, provisionerID, reference string) (*ExternalAccountKey, error)
|
||||
DeleteExternalAccountKey(ctx context.Context, provisionerID, keyID string) error
|
||||
UpdateExternalAccountKey(ctx context.Context, provisionerID string, eak *ExternalAccountKey) error
|
||||
|
||||
CreateNonce(ctx context.Context) (Nonce, error)
|
||||
DeleteNonce(ctx context.Context, nonce Nonce) error
|
||||
|
||||
|
@ -49,6 +56,13 @@ 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, provisionerID, reference string) (*ExternalAccountKey, error)
|
||||
MockGetExternalAccountKey func(ctx context.Context, provisionerID, keyID string) (*ExternalAccountKey, error)
|
||||
MockGetExternalAccountKeys func(ctx context.Context, provisionerID, cursor string, limit int) ([]*ExternalAccountKey, string, error)
|
||||
MockGetExternalAccountKeyByReference func(ctx context.Context, provisionerID, reference string) (*ExternalAccountKey, error)
|
||||
MockDeleteExternalAccountKey func(ctx context.Context, provisionerID, keyID string) error
|
||||
MockUpdateExternalAccountKey func(ctx context.Context, provisionerID string, eak *ExternalAccountKey) error
|
||||
|
||||
MockCreateNonce func(ctx context.Context) (Nonce, error)
|
||||
MockDeleteNonce func(ctx context.Context, nonce Nonce) error
|
||||
|
||||
|
@ -114,6 +128,66 @@ func (m *MockDB) UpdateAccount(ctx context.Context, acc *Account) error {
|
|||
return m.MockError
|
||||
}
|
||||
|
||||
// CreateExternalAccountKey mock
|
||||
func (m *MockDB) CreateExternalAccountKey(ctx context.Context, provisionerID, reference string) (*ExternalAccountKey, error) {
|
||||
if m.MockCreateExternalAccountKey != nil {
|
||||
return m.MockCreateExternalAccountKey(ctx, provisionerID, reference)
|
||||
} else if m.MockError != nil {
|
||||
return nil, m.MockError
|
||||
}
|
||||
return m.MockRet1.(*ExternalAccountKey), m.MockError
|
||||
}
|
||||
|
||||
// GetExternalAccountKey mock
|
||||
func (m *MockDB) GetExternalAccountKey(ctx context.Context, provisionerID, keyID string) (*ExternalAccountKey, error) {
|
||||
if m.MockGetExternalAccountKey != nil {
|
||||
return m.MockGetExternalAccountKey(ctx, provisionerID, keyID)
|
||||
} else if m.MockError != nil {
|
||||
return nil, m.MockError
|
||||
}
|
||||
return m.MockRet1.(*ExternalAccountKey), m.MockError
|
||||
}
|
||||
|
||||
// GetExternalAccountKeys mock
|
||||
func (m *MockDB) GetExternalAccountKeys(ctx context.Context, provisionerID, cursor string, limit int) ([]*ExternalAccountKey, string, error) {
|
||||
if m.MockGetExternalAccountKeys != nil {
|
||||
return m.MockGetExternalAccountKeys(ctx, provisionerID, cursor, limit)
|
||||
} else if m.MockError != nil {
|
||||
return nil, "", m.MockError
|
||||
}
|
||||
return m.MockRet1.([]*ExternalAccountKey), "", m.MockError
|
||||
}
|
||||
|
||||
// GetExternalAccountKeyByReference mock
|
||||
func (m *MockDB) GetExternalAccountKeyByReference(ctx context.Context, provisionerID, reference string) (*ExternalAccountKey, error) {
|
||||
if m.MockGetExternalAccountKeyByReference != nil {
|
||||
return m.MockGetExternalAccountKeyByReference(ctx, provisionerID, reference)
|
||||
} else if m.MockError != nil {
|
||||
return nil, m.MockError
|
||||
}
|
||||
return m.MockRet1.(*ExternalAccountKey), m.MockError
|
||||
}
|
||||
|
||||
// DeleteExternalAccountKey mock
|
||||
func (m *MockDB) DeleteExternalAccountKey(ctx context.Context, provisionerID, keyID string) error {
|
||||
if m.MockDeleteExternalAccountKey != nil {
|
||||
return m.MockDeleteExternalAccountKey(ctx, provisionerID, keyID)
|
||||
} else if m.MockError != nil {
|
||||
return m.MockError
|
||||
}
|
||||
return m.MockError
|
||||
}
|
||||
|
||||
// UpdateExternalAccountKey mock
|
||||
func (m *MockDB) UpdateExternalAccountKey(ctx context.Context, provisionerID string, eak *ExternalAccountKey) error {
|
||||
if m.MockUpdateExternalAccountKey != nil {
|
||||
return m.MockUpdateExternalAccountKey(ctx, provisionerID, eak)
|
||||
} else if m.MockError != nil {
|
||||
return m.MockError
|
||||
}
|
||||
return m.MockError
|
||||
}
|
||||
|
||||
// CreateNonce mock
|
||||
func (m *MockDB) CreateNonce(ctx context.Context) (Nonce, error) {
|
||||
if m.MockCreateNonce != nil {
|
||||
|
|
|
@ -307,7 +307,7 @@ func TestDB_GetAccountByKeyID(t *testing.T) {
|
|||
assert.Equals(t, string(key), accID)
|
||||
return nil, errors.New("force")
|
||||
default:
|
||||
assert.FatalError(t, errors.Errorf("unrecognized bucket %s", string(bucket)))
|
||||
assert.FatalError(t, errors.Errorf("unexpected bucket %s", string(bucket)))
|
||||
return nil, errors.New("force")
|
||||
}
|
||||
},
|
||||
|
@ -340,7 +340,7 @@ func TestDB_GetAccountByKeyID(t *testing.T) {
|
|||
assert.Equals(t, string(key), accID)
|
||||
return b, nil
|
||||
default:
|
||||
assert.FatalError(t, errors.Errorf("unrecognized bucket %s", string(bucket)))
|
||||
assert.FatalError(t, errors.Errorf("unexpected bucket %s", string(bucket)))
|
||||
return nil, errors.New("force")
|
||||
}
|
||||
},
|
||||
|
@ -462,7 +462,7 @@ func TestDB_CreateAccount(t *testing.T) {
|
|||
assert.True(t, dbacc.DeactivatedAt.IsZero())
|
||||
return nil, false, errors.New("force")
|
||||
default:
|
||||
assert.FatalError(t, errors.Errorf("unrecognized bucket %s", string(bucket)))
|
||||
assert.FatalError(t, errors.Errorf("unexpected bucket %s", string(bucket)))
|
||||
return nil, false, errors.New("force")
|
||||
}
|
||||
},
|
||||
|
@ -506,7 +506,7 @@ func TestDB_CreateAccount(t *testing.T) {
|
|||
assert.True(t, dbacc.DeactivatedAt.IsZero())
|
||||
return nu, true, nil
|
||||
default:
|
||||
assert.FatalError(t, errors.Errorf("unrecognized bucket %s", string(bucket)))
|
||||
assert.FatalError(t, errors.Errorf("unexpected bucket %s", string(bucket)))
|
||||
return nil, false, errors.New("force")
|
||||
}
|
||||
},
|
||||
|
|
380
acme/db/nosql/eab.go
Normal file
380
acme/db/nosql/eab.go
Normal file
|
@ -0,0 +1,380 @@
|
|||
package nosql
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"encoding/json"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/smallstep/certificates/acme"
|
||||
nosqlDB "github.com/smallstep/nosql"
|
||||
)
|
||||
|
||||
// externalAccountKeyMutex for read/write locking of EAK operations.
|
||||
var externalAccountKeyMutex sync.RWMutex
|
||||
|
||||
// referencesByProvisionerIndexMutex for locking referencesByProvisioner index operations.
|
||||
var referencesByProvisionerIndexMutex sync.Mutex
|
||||
|
||||
type dbExternalAccountKey struct {
|
||||
ID string `json:"id"`
|
||||
ProvisionerID string `json:"provisionerID"`
|
||||
Reference string `json:"reference"`
|
||||
AccountID string `json:"accountID,omitempty"`
|
||||
KeyBytes []byte `json:"key"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
BoundAt time.Time `json:"boundAt"`
|
||||
}
|
||||
|
||||
type dbExternalAccountKeyReference struct {
|
||||
Reference string `json:"reference"`
|
||||
ExternalAccountKeyID string `json:"externalAccountKeyID"`
|
||||
}
|
||||
|
||||
// getDBExternalAccountKey retrieves and unmarshals dbExternalAccountKey.
|
||||
func (db *DB) getDBExternalAccountKey(ctx context.Context, id string) (*dbExternalAccountKey, error) {
|
||||
data, err := db.db.Get(externalAccountKeyTable, []byte(id))
|
||||
if err != nil {
|
||||
if nosqlDB.IsErrNotFound(err) {
|
||||
return nil, acme.ErrNotFound
|
||||
}
|
||||
return nil, errors.Wrapf(err, "error loading external account key %s", id)
|
||||
}
|
||||
|
||||
dbeak := new(dbExternalAccountKey)
|
||||
if err = json.Unmarshal(data, dbeak); err != nil {
|
||||
return nil, errors.Wrapf(err, "error unmarshaling external account key %s into dbExternalAccountKey", id)
|
||||
}
|
||||
|
||||
return dbeak, nil
|
||||
}
|
||||
|
||||
// CreateExternalAccountKey creates a new External Account Binding key with a name
|
||||
func (db *DB) CreateExternalAccountKey(ctx context.Context, provisionerID, reference string) (*acme.ExternalAccountKey, error) {
|
||||
|
||||
externalAccountKeyMutex.Lock()
|
||||
defer externalAccountKeyMutex.Unlock()
|
||||
|
||||
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,
|
||||
ProvisionerID: provisionerID,
|
||||
Reference: reference,
|
||||
KeyBytes: random,
|
||||
CreatedAt: clock.Now(),
|
||||
}
|
||||
|
||||
if err := db.save(ctx, keyID, dbeak, nil, "external_account_key", externalAccountKeyTable); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := db.addEAKID(ctx, provisionerID, dbeak.ID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if dbeak.Reference != "" {
|
||||
dbExternalAccountKeyReference := &dbExternalAccountKeyReference{
|
||||
Reference: dbeak.Reference,
|
||||
ExternalAccountKeyID: dbeak.ID,
|
||||
}
|
||||
if err := db.save(ctx, referenceKey(provisionerID, dbeak.Reference), dbExternalAccountKeyReference, nil, "external_account_key_reference", externalAccountKeyIDsByReferenceTable); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return &acme.ExternalAccountKey{
|
||||
ID: dbeak.ID,
|
||||
ProvisionerID: dbeak.ProvisionerID,
|
||||
Reference: dbeak.Reference,
|
||||
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, provisionerID, keyID string) (*acme.ExternalAccountKey, error) {
|
||||
externalAccountKeyMutex.RLock()
|
||||
defer externalAccountKeyMutex.RUnlock()
|
||||
|
||||
dbeak, err := db.getDBExternalAccountKey(ctx, keyID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if dbeak.ProvisionerID != provisionerID {
|
||||
return nil, acme.NewError(acme.ErrorUnauthorizedType, "provisioner does not match provisioner for which the EAB key was created")
|
||||
}
|
||||
|
||||
return &acme.ExternalAccountKey{
|
||||
ID: dbeak.ID,
|
||||
ProvisionerID: dbeak.ProvisionerID,
|
||||
Reference: dbeak.Reference,
|
||||
AccountID: dbeak.AccountID,
|
||||
KeyBytes: dbeak.KeyBytes,
|
||||
CreatedAt: dbeak.CreatedAt,
|
||||
BoundAt: dbeak.BoundAt,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (db *DB) DeleteExternalAccountKey(ctx context.Context, provisionerID, keyID string) error {
|
||||
externalAccountKeyMutex.Lock()
|
||||
defer externalAccountKeyMutex.Unlock()
|
||||
|
||||
dbeak, err := db.getDBExternalAccountKey(ctx, keyID)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "error loading ACME EAB Key with Key ID %s", keyID)
|
||||
}
|
||||
|
||||
if dbeak.ProvisionerID != provisionerID {
|
||||
return errors.New("provisioner does not match provisioner for which the EAB key was created")
|
||||
}
|
||||
|
||||
if dbeak.Reference != "" {
|
||||
if err := db.db.Del(externalAccountKeyIDsByReferenceTable, []byte(referenceKey(provisionerID, dbeak.Reference))); err != nil {
|
||||
return errors.Wrapf(err, "error deleting ACME EAB Key reference with Key ID %s and reference %s", keyID, dbeak.Reference)
|
||||
}
|
||||
}
|
||||
if err := db.db.Del(externalAccountKeyTable, []byte(keyID)); err != nil {
|
||||
return errors.Wrapf(err, "error deleting ACME EAB Key with Key ID %s", keyID)
|
||||
}
|
||||
if err := db.deleteEAKID(ctx, provisionerID, keyID); err != nil {
|
||||
return errors.Wrapf(err, "error removing ACME EAB Key ID %s", keyID)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetExternalAccountKeys retrieves all External Account Binding keys for a provisioner
|
||||
func (db *DB) GetExternalAccountKeys(ctx context.Context, provisionerID, cursor string, limit int) ([]*acme.ExternalAccountKey, string, error) {
|
||||
externalAccountKeyMutex.RLock()
|
||||
defer externalAccountKeyMutex.RUnlock()
|
||||
|
||||
// cursor and limit are ignored in open source, at least for now.
|
||||
|
||||
var eakIDs []string
|
||||
r, err := db.db.Get(externalAccountKeyIDsByProvisionerIDTable, []byte(provisionerID))
|
||||
if err != nil {
|
||||
if !nosqlDB.IsErrNotFound(err) {
|
||||
return nil, "", errors.Wrapf(err, "error loading ACME EAB Key IDs for provisioner %s", provisionerID)
|
||||
}
|
||||
// it may happen that no record is found; we'll continue with an empty slice
|
||||
} else {
|
||||
if err := json.Unmarshal(r, &eakIDs); err != nil {
|
||||
return nil, "", errors.Wrapf(err, "error unmarshaling ACME EAB Key IDs for provisioner %s", provisionerID)
|
||||
}
|
||||
}
|
||||
|
||||
keys := []*acme.ExternalAccountKey{}
|
||||
for _, eakID := range eakIDs {
|
||||
if eakID == "" {
|
||||
continue // shouldn't happen; just in case
|
||||
}
|
||||
eak, err := db.getDBExternalAccountKey(ctx, eakID)
|
||||
if err != nil {
|
||||
if !nosqlDB.IsErrNotFound(err) {
|
||||
return nil, "", errors.Wrapf(err, "error retrieving ACME EAB Key for provisioner %s and keyID %s", provisionerID, eakID)
|
||||
}
|
||||
}
|
||||
keys = append(keys, &acme.ExternalAccountKey{
|
||||
ID: eak.ID,
|
||||
KeyBytes: eak.KeyBytes,
|
||||
ProvisionerID: eak.ProvisionerID,
|
||||
Reference: eak.Reference,
|
||||
AccountID: eak.AccountID,
|
||||
CreatedAt: eak.CreatedAt,
|
||||
BoundAt: eak.BoundAt,
|
||||
})
|
||||
}
|
||||
|
||||
return keys, "", nil
|
||||
}
|
||||
|
||||
// GetExternalAccountKeyByReference retrieves an External Account Binding key with unique reference
|
||||
func (db *DB) GetExternalAccountKeyByReference(ctx context.Context, provisionerID, reference string) (*acme.ExternalAccountKey, error) {
|
||||
externalAccountKeyMutex.RLock()
|
||||
defer externalAccountKeyMutex.RUnlock()
|
||||
|
||||
if reference == "" {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
k, err := db.db.Get(externalAccountKeyIDsByReferenceTable, []byte(referenceKey(provisionerID, reference)))
|
||||
if nosqlDB.IsErrNotFound(err) {
|
||||
return nil, acme.ErrNotFound
|
||||
} else if err != nil {
|
||||
return nil, errors.Wrapf(err, "error loading ACME EAB key for reference %s", reference)
|
||||
}
|
||||
dbExternalAccountKeyReference := new(dbExternalAccountKeyReference)
|
||||
if err := json.Unmarshal(k, dbExternalAccountKeyReference); err != nil {
|
||||
return nil, errors.Wrapf(err, "error unmarshaling ACME EAB key for reference %s", reference)
|
||||
}
|
||||
|
||||
return db.GetExternalAccountKey(ctx, provisionerID, dbExternalAccountKeyReference.ExternalAccountKeyID)
|
||||
}
|
||||
|
||||
func (db *DB) UpdateExternalAccountKey(ctx context.Context, provisionerID string, eak *acme.ExternalAccountKey) error {
|
||||
externalAccountKeyMutex.Lock()
|
||||
defer externalAccountKeyMutex.Unlock()
|
||||
|
||||
old, err := db.getDBExternalAccountKey(ctx, eak.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if old.ProvisionerID != provisionerID {
|
||||
return errors.New("provisioner does not match provisioner for which the EAB key was created")
|
||||
}
|
||||
|
||||
if old.ProvisionerID != eak.ProvisionerID {
|
||||
return errors.New("cannot change provisioner for an existing ACME EAB Key")
|
||||
}
|
||||
|
||||
if old.Reference != eak.Reference {
|
||||
return errors.New("cannot change reference for an existing ACME EAB Key")
|
||||
}
|
||||
|
||||
nu := dbExternalAccountKey{
|
||||
ID: eak.ID,
|
||||
ProvisionerID: eak.ProvisionerID,
|
||||
Reference: eak.Reference,
|
||||
AccountID: eak.AccountID,
|
||||
KeyBytes: eak.KeyBytes,
|
||||
CreatedAt: eak.CreatedAt,
|
||||
BoundAt: eak.BoundAt,
|
||||
}
|
||||
|
||||
return db.save(ctx, nu.ID, nu, old, "external_account_key", externalAccountKeyTable)
|
||||
}
|
||||
|
||||
func (db *DB) addEAKID(ctx context.Context, provisionerID, eakID string) error {
|
||||
referencesByProvisionerIndexMutex.Lock()
|
||||
defer referencesByProvisionerIndexMutex.Unlock()
|
||||
|
||||
if eakID == "" {
|
||||
return errors.Errorf("can't add empty eakID for provisioner %s", provisionerID)
|
||||
}
|
||||
|
||||
var eakIDs []string
|
||||
b, err := db.db.Get(externalAccountKeyIDsByProvisionerIDTable, []byte(provisionerID))
|
||||
if err != nil {
|
||||
if !nosqlDB.IsErrNotFound(err) {
|
||||
return errors.Wrapf(err, "error loading eakIDs for provisioner %s", provisionerID)
|
||||
}
|
||||
// it may happen that no record is found; we'll continue with an empty slice
|
||||
} else {
|
||||
if err := json.Unmarshal(b, &eakIDs); err != nil {
|
||||
return errors.Wrapf(err, "error unmarshaling eakIDs for provisioner %s", provisionerID)
|
||||
}
|
||||
}
|
||||
|
||||
for _, id := range eakIDs {
|
||||
if id == eakID {
|
||||
// return an error when a duplicate ID is found
|
||||
return errors.Errorf("eakID %s already exists for provisioner %s", eakID, provisionerID)
|
||||
}
|
||||
}
|
||||
|
||||
var newEAKIDs []string
|
||||
newEAKIDs = append(newEAKIDs, eakIDs...)
|
||||
newEAKIDs = append(newEAKIDs, eakID)
|
||||
|
||||
var (
|
||||
_old interface{} = eakIDs
|
||||
_new interface{} = newEAKIDs
|
||||
)
|
||||
|
||||
// ensure that the DB gets the expected value when the slice is empty; otherwise
|
||||
// it'll return with an error that indicates that the DBs view of the data is
|
||||
// different from the last read (i.e. _old is different from what the DB has).
|
||||
if len(eakIDs) == 0 {
|
||||
_old = nil
|
||||
}
|
||||
|
||||
if err = db.save(ctx, provisionerID, _new, _old, "externalAccountKeyIDsByProvisionerID", externalAccountKeyIDsByProvisionerIDTable); err != nil {
|
||||
return errors.Wrapf(err, "error saving eakIDs index for provisioner %s", provisionerID)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (db *DB) deleteEAKID(ctx context.Context, provisionerID, eakID string) error {
|
||||
referencesByProvisionerIndexMutex.Lock()
|
||||
defer referencesByProvisionerIndexMutex.Unlock()
|
||||
|
||||
var eakIDs []string
|
||||
b, err := db.db.Get(externalAccountKeyIDsByProvisionerIDTable, []byte(provisionerID))
|
||||
if err != nil {
|
||||
if !nosqlDB.IsErrNotFound(err) {
|
||||
return errors.Wrapf(err, "error loading eakIDs for provisioner %s", provisionerID)
|
||||
}
|
||||
// it may happen that no record is found; we'll continue with an empty slice
|
||||
} else {
|
||||
if err := json.Unmarshal(b, &eakIDs); err != nil {
|
||||
return errors.Wrapf(err, "error unmarshaling eakIDs for provisioner %s", provisionerID)
|
||||
}
|
||||
}
|
||||
|
||||
newEAKIDs := removeElement(eakIDs, eakID)
|
||||
var (
|
||||
_old interface{} = eakIDs
|
||||
_new interface{} = newEAKIDs
|
||||
)
|
||||
|
||||
// ensure that the DB gets the expected value when the slice is empty; otherwise
|
||||
// it'll return with an error that indicates that the DBs view of the data is
|
||||
// different from the last read (i.e. _old is different from what the DB has).
|
||||
if len(eakIDs) == 0 {
|
||||
_old = nil
|
||||
}
|
||||
|
||||
if err = db.save(ctx, provisionerID, _new, _old, "externalAccountKeyIDsByProvisionerID", externalAccountKeyIDsByProvisionerIDTable); err != nil {
|
||||
return errors.Wrapf(err, "error saving eakIDs index for provisioner %s", provisionerID)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// referenceKey returns a unique key for a reference per provisioner
|
||||
func referenceKey(provisionerID, reference string) string {
|
||||
return provisionerID + "." + reference
|
||||
}
|
||||
|
||||
// sliceIndex finds the index of item in slice
|
||||
func sliceIndex(slice []string, item string) int {
|
||||
for i := range slice {
|
||||
if slice[i] == item {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
// removeElement deletes the item if it exists in the
|
||||
// slice. It returns a new slice, keeping the old one intact.
|
||||
func removeElement(slice []string, item string) []string {
|
||||
|
||||
newSlice := make([]string, 0)
|
||||
index := sliceIndex(slice, item)
|
||||
if index < 0 {
|
||||
newSlice = append(newSlice, slice...)
|
||||
return newSlice
|
||||
}
|
||||
|
||||
newSlice = append(newSlice, slice[:index]...)
|
||||
|
||||
return append(newSlice, slice[index+1:]...)
|
||||
}
|
1712
acme/db/nosql/eab_test.go
Normal file
1712
acme/db/nosql/eab_test.go
Normal file
File diff suppressed because it is too large
Load diff
|
@ -11,15 +11,18 @@ import (
|
|||
)
|
||||
|
||||
var (
|
||||
accountTable = []byte("acme_accounts")
|
||||
accountByKeyIDTable = []byte("acme_keyID_accountID_index")
|
||||
authzTable = []byte("acme_authzs")
|
||||
challengeTable = []byte("acme_challenges")
|
||||
nonceTable = []byte("nonces")
|
||||
orderTable = []byte("acme_orders")
|
||||
ordersByAccountIDTable = []byte("acme_account_orders_index")
|
||||
certTable = []byte("acme_certs")
|
||||
certBySerialTable = []byte("acme_serial_certs_index")
|
||||
accountTable = []byte("acme_accounts")
|
||||
accountByKeyIDTable = []byte("acme_keyID_accountID_index")
|
||||
authzTable = []byte("acme_authzs")
|
||||
challengeTable = []byte("acme_challenges")
|
||||
nonceTable = []byte("nonces")
|
||||
orderTable = []byte("acme_orders")
|
||||
ordersByAccountIDTable = []byte("acme_account_orders_index")
|
||||
certTable = []byte("acme_certs")
|
||||
certBySerialTable = []byte("acme_serial_certs_index")
|
||||
externalAccountKeyTable = []byte("acme_external_account_keys")
|
||||
externalAccountKeyIDsByReferenceTable = []byte("acme_external_account_keyID_reference_index")
|
||||
externalAccountKeyIDsByProvisionerIDTable = []byte("acme_external_account_keyID_provisionerID_index")
|
||||
)
|
||||
|
||||
// DB is a struct that implements the AcmeDB interface.
|
||||
|
@ -30,7 +33,10 @@ type DB struct {
|
|||
// New configures and returns a new ACME DB backend implemented using a nosql DB.
|
||||
func New(db nosqlDB.DB) (*DB, error) {
|
||||
tables := [][]byte{accountTable, accountByKeyIDTable, authzTable,
|
||||
challengeTable, nonceTable, orderTable, ordersByAccountIDTable, certTable, certBySerialTable}
|
||||
challengeTable, nonceTable, orderTable, ordersByAccountIDTable,
|
||||
certTable, certBySerialTable, externalAccountKeyTable,
|
||||
externalAccountKeyIDsByReferenceTable, externalAccountKeyIDsByProvisionerIDTable,
|
||||
}
|
||||
for _, b := range tables {
|
||||
if err := db.CreateTable(b); err != nil {
|
||||
return nil, errors.Wrapf(err, "error creating table %s",
|
||||
|
|
404
api/api_test.go
404
api/api_test.go
|
@ -167,6 +167,208 @@ func parseCertificateRequest(data string) *x509.CertificateRequest {
|
|||
return csr
|
||||
}
|
||||
|
||||
type mockAuthority struct {
|
||||
ret1, ret2 interface{}
|
||||
err error
|
||||
authorizeSign func(ott string) ([]provisioner.SignOption, error)
|
||||
getTLSOptions func() *authority.TLSOptions
|
||||
root func(shasum string) (*x509.Certificate, error)
|
||||
sign func(cr *x509.CertificateRequest, opts provisioner.SignOptions, signOpts ...provisioner.SignOption) ([]*x509.Certificate, error)
|
||||
renew func(cert *x509.Certificate) ([]*x509.Certificate, error)
|
||||
rekey func(oldCert *x509.Certificate, pk crypto.PublicKey) ([]*x509.Certificate, error)
|
||||
loadProvisionerByCertificate func(cert *x509.Certificate) (provisioner.Interface, error)
|
||||
loadProvisionerByName func(name string) (provisioner.Interface, error)
|
||||
getProvisioners func(nextCursor string, limit int) (provisioner.List, string, error)
|
||||
revoke func(context.Context, *authority.RevokeOptions) error
|
||||
getEncryptedKey func(kid string) (string, error)
|
||||
getRoots func() ([]*x509.Certificate, error)
|
||||
getFederation func() ([]*x509.Certificate, error)
|
||||
signSSH func(ctx context.Context, key ssh.PublicKey, opts provisioner.SignSSHOptions, signOpts ...provisioner.SignOption) (*ssh.Certificate, error)
|
||||
signSSHAddUser func(ctx context.Context, key ssh.PublicKey, cert *ssh.Certificate) (*ssh.Certificate, error)
|
||||
renewSSH func(ctx context.Context, cert *ssh.Certificate) (*ssh.Certificate, error)
|
||||
rekeySSH func(ctx context.Context, cert *ssh.Certificate, key ssh.PublicKey, signOpts ...provisioner.SignOption) (*ssh.Certificate, error)
|
||||
getSSHHosts func(ctx context.Context, cert *x509.Certificate) ([]authority.Host, error)
|
||||
getSSHRoots func(ctx context.Context) (*authority.SSHKeys, error)
|
||||
getSSHFederation func(ctx context.Context) (*authority.SSHKeys, error)
|
||||
getSSHConfig func(ctx context.Context, typ string, data map[string]string) ([]templates.Output, error)
|
||||
checkSSHHost func(ctx context.Context, principal, token string) (bool, error)
|
||||
getSSHBastion func(ctx context.Context, user string, hostname string) (*authority.Bastion, error)
|
||||
version func() authority.Version
|
||||
}
|
||||
|
||||
// TODO: remove once Authorize is deprecated.
|
||||
func (m *mockAuthority) Authorize(ctx context.Context, ott string) ([]provisioner.SignOption, error) {
|
||||
return m.AuthorizeSign(ott)
|
||||
}
|
||||
|
||||
func (m *mockAuthority) AuthorizeSign(ott string) ([]provisioner.SignOption, error) {
|
||||
if m.authorizeSign != nil {
|
||||
return m.authorizeSign(ott)
|
||||
}
|
||||
return m.ret1.([]provisioner.SignOption), m.err
|
||||
}
|
||||
|
||||
func (m *mockAuthority) GetTLSOptions() *authority.TLSOptions {
|
||||
if m.getTLSOptions != nil {
|
||||
return m.getTLSOptions()
|
||||
}
|
||||
return m.ret1.(*authority.TLSOptions)
|
||||
}
|
||||
|
||||
func (m *mockAuthority) Root(shasum string) (*x509.Certificate, error) {
|
||||
if m.root != nil {
|
||||
return m.root(shasum)
|
||||
}
|
||||
return m.ret1.(*x509.Certificate), m.err
|
||||
}
|
||||
|
||||
func (m *mockAuthority) Sign(cr *x509.CertificateRequest, opts provisioner.SignOptions, signOpts ...provisioner.SignOption) ([]*x509.Certificate, error) {
|
||||
if m.sign != nil {
|
||||
return m.sign(cr, opts, signOpts...)
|
||||
}
|
||||
return []*x509.Certificate{m.ret1.(*x509.Certificate), m.ret2.(*x509.Certificate)}, m.err
|
||||
}
|
||||
|
||||
func (m *mockAuthority) Renew(cert *x509.Certificate) ([]*x509.Certificate, error) {
|
||||
if m.renew != nil {
|
||||
return m.renew(cert)
|
||||
}
|
||||
return []*x509.Certificate{m.ret1.(*x509.Certificate), m.ret2.(*x509.Certificate)}, m.err
|
||||
}
|
||||
|
||||
func (m *mockAuthority) Rekey(oldcert *x509.Certificate, pk crypto.PublicKey) ([]*x509.Certificate, error) {
|
||||
if m.rekey != nil {
|
||||
return m.rekey(oldcert, pk)
|
||||
}
|
||||
return []*x509.Certificate{m.ret1.(*x509.Certificate), m.ret2.(*x509.Certificate)}, m.err
|
||||
}
|
||||
|
||||
func (m *mockAuthority) GetProvisioners(nextCursor string, limit int) (provisioner.List, string, error) {
|
||||
if m.getProvisioners != nil {
|
||||
return m.getProvisioners(nextCursor, limit)
|
||||
}
|
||||
return m.ret1.(provisioner.List), m.ret2.(string), m.err
|
||||
}
|
||||
|
||||
func (m *mockAuthority) LoadProvisionerByCertificate(cert *x509.Certificate) (provisioner.Interface, error) {
|
||||
if m.loadProvisionerByCertificate != nil {
|
||||
return m.loadProvisionerByCertificate(cert)
|
||||
}
|
||||
return m.ret1.(provisioner.Interface), m.err
|
||||
}
|
||||
|
||||
func (m *mockAuthority) LoadProvisionerByName(name string) (provisioner.Interface, error) {
|
||||
if m.loadProvisionerByName != nil {
|
||||
return m.loadProvisionerByName(name)
|
||||
}
|
||||
return m.ret1.(provisioner.Interface), m.err
|
||||
}
|
||||
|
||||
func (m *mockAuthority) Revoke(ctx context.Context, opts *authority.RevokeOptions) error {
|
||||
if m.revoke != nil {
|
||||
return m.revoke(ctx, opts)
|
||||
}
|
||||
return m.err
|
||||
}
|
||||
|
||||
func (m *mockAuthority) GetEncryptedKey(kid string) (string, error) {
|
||||
if m.getEncryptedKey != nil {
|
||||
return m.getEncryptedKey(kid)
|
||||
}
|
||||
return m.ret1.(string), m.err
|
||||
}
|
||||
|
||||
func (m *mockAuthority) GetRoots() ([]*x509.Certificate, error) {
|
||||
if m.getRoots != nil {
|
||||
return m.getRoots()
|
||||
}
|
||||
return m.ret1.([]*x509.Certificate), m.err
|
||||
}
|
||||
|
||||
func (m *mockAuthority) GetFederation() ([]*x509.Certificate, error) {
|
||||
if m.getFederation != nil {
|
||||
return m.getFederation()
|
||||
}
|
||||
return m.ret1.([]*x509.Certificate), m.err
|
||||
}
|
||||
|
||||
func (m *mockAuthority) SignSSH(ctx context.Context, key ssh.PublicKey, opts provisioner.SignSSHOptions, signOpts ...provisioner.SignOption) (*ssh.Certificate, error) {
|
||||
if m.signSSH != nil {
|
||||
return m.signSSH(ctx, key, opts, signOpts...)
|
||||
}
|
||||
return m.ret1.(*ssh.Certificate), m.err
|
||||
}
|
||||
|
||||
func (m *mockAuthority) SignSSHAddUser(ctx context.Context, key ssh.PublicKey, cert *ssh.Certificate) (*ssh.Certificate, error) {
|
||||
if m.signSSHAddUser != nil {
|
||||
return m.signSSHAddUser(ctx, key, cert)
|
||||
}
|
||||
return m.ret1.(*ssh.Certificate), m.err
|
||||
}
|
||||
|
||||
func (m *mockAuthority) RenewSSH(ctx context.Context, cert *ssh.Certificate) (*ssh.Certificate, error) {
|
||||
if m.renewSSH != nil {
|
||||
return m.renewSSH(ctx, cert)
|
||||
}
|
||||
return m.ret1.(*ssh.Certificate), m.err
|
||||
}
|
||||
|
||||
func (m *mockAuthority) RekeySSH(ctx context.Context, cert *ssh.Certificate, key ssh.PublicKey, signOpts ...provisioner.SignOption) (*ssh.Certificate, error) {
|
||||
if m.rekeySSH != nil {
|
||||
return m.rekeySSH(ctx, cert, key, signOpts...)
|
||||
}
|
||||
return m.ret1.(*ssh.Certificate), m.err
|
||||
}
|
||||
|
||||
func (m *mockAuthority) GetSSHHosts(ctx context.Context, cert *x509.Certificate) ([]authority.Host, error) {
|
||||
if m.getSSHHosts != nil {
|
||||
return m.getSSHHosts(ctx, cert)
|
||||
}
|
||||
return m.ret1.([]authority.Host), m.err
|
||||
}
|
||||
|
||||
func (m *mockAuthority) GetSSHRoots(ctx context.Context) (*authority.SSHKeys, error) {
|
||||
if m.getSSHRoots != nil {
|
||||
return m.getSSHRoots(ctx)
|
||||
}
|
||||
return m.ret1.(*authority.SSHKeys), m.err
|
||||
}
|
||||
|
||||
func (m *mockAuthority) GetSSHFederation(ctx context.Context) (*authority.SSHKeys, error) {
|
||||
if m.getSSHFederation != nil {
|
||||
return m.getSSHFederation(ctx)
|
||||
}
|
||||
return m.ret1.(*authority.SSHKeys), m.err
|
||||
}
|
||||
|
||||
func (m *mockAuthority) GetSSHConfig(ctx context.Context, typ string, data map[string]string) ([]templates.Output, error) {
|
||||
if m.getSSHConfig != nil {
|
||||
return m.getSSHConfig(ctx, typ, data)
|
||||
}
|
||||
return m.ret1.([]templates.Output), m.err
|
||||
}
|
||||
|
||||
func (m *mockAuthority) CheckSSHHost(ctx context.Context, principal, token string) (bool, error) {
|
||||
if m.checkSSHHost != nil {
|
||||
return m.checkSSHHost(ctx, principal, token)
|
||||
}
|
||||
return m.ret1.(bool), m.err
|
||||
}
|
||||
|
||||
func (m *mockAuthority) GetSSHBastion(ctx context.Context, user, hostname string) (*authority.Bastion, error) {
|
||||
if m.getSSHBastion != nil {
|
||||
return m.getSSHBastion(ctx, user, hostname)
|
||||
}
|
||||
return m.ret1.(*authority.Bastion), m.err
|
||||
}
|
||||
|
||||
func (m *mockAuthority) Version() authority.Version {
|
||||
if m.version != nil {
|
||||
return m.version()
|
||||
}
|
||||
return m.ret1.(authority.Version)
|
||||
}
|
||||
|
||||
func TestNewCertificate(t *testing.T) {
|
||||
cert := parseCertificate(rootPEM)
|
||||
if !reflect.DeepEqual(Certificate{Certificate: cert}, NewCertificate(cert)) {
|
||||
|
@ -551,208 +753,6 @@ func (m *mockProvisioner) AuthorizeSSHRekey(ctx context.Context, token string) (
|
|||
return m.ret1.(*ssh.Certificate), m.ret2.([]provisioner.SignOption), m.err
|
||||
}
|
||||
|
||||
type mockAuthority struct {
|
||||
ret1, ret2 interface{}
|
||||
err error
|
||||
authorizeSign func(ott string) ([]provisioner.SignOption, error)
|
||||
getTLSOptions func() *authority.TLSOptions
|
||||
root func(shasum string) (*x509.Certificate, error)
|
||||
sign func(cr *x509.CertificateRequest, opts provisioner.SignOptions, signOpts ...provisioner.SignOption) ([]*x509.Certificate, error)
|
||||
renew func(cert *x509.Certificate) ([]*x509.Certificate, error)
|
||||
rekey func(oldCert *x509.Certificate, pk crypto.PublicKey) ([]*x509.Certificate, error)
|
||||
loadProvisionerByCertificate func(cert *x509.Certificate) (provisioner.Interface, error)
|
||||
loadProvisionerByName func(name string) (provisioner.Interface, error)
|
||||
getProvisioners func(nextCursor string, limit int) (provisioner.List, string, error)
|
||||
revoke func(context.Context, *authority.RevokeOptions) error
|
||||
getEncryptedKey func(kid string) (string, error)
|
||||
getRoots func() ([]*x509.Certificate, error)
|
||||
getFederation func() ([]*x509.Certificate, error)
|
||||
signSSH func(ctx context.Context, key ssh.PublicKey, opts provisioner.SignSSHOptions, signOpts ...provisioner.SignOption) (*ssh.Certificate, error)
|
||||
signSSHAddUser func(ctx context.Context, key ssh.PublicKey, cert *ssh.Certificate) (*ssh.Certificate, error)
|
||||
renewSSH func(ctx context.Context, cert *ssh.Certificate) (*ssh.Certificate, error)
|
||||
rekeySSH func(ctx context.Context, cert *ssh.Certificate, key ssh.PublicKey, signOpts ...provisioner.SignOption) (*ssh.Certificate, error)
|
||||
getSSHHosts func(ctx context.Context, cert *x509.Certificate) ([]authority.Host, error)
|
||||
getSSHRoots func(ctx context.Context) (*authority.SSHKeys, error)
|
||||
getSSHFederation func(ctx context.Context) (*authority.SSHKeys, error)
|
||||
getSSHConfig func(ctx context.Context, typ string, data map[string]string) ([]templates.Output, error)
|
||||
checkSSHHost func(ctx context.Context, principal, token string) (bool, error)
|
||||
getSSHBastion func(ctx context.Context, user string, hostname string) (*authority.Bastion, error)
|
||||
version func() authority.Version
|
||||
}
|
||||
|
||||
// TODO: remove once Authorize is deprecated.
|
||||
func (m *mockAuthority) Authorize(ctx context.Context, ott string) ([]provisioner.SignOption, error) {
|
||||
return m.AuthorizeSign(ott)
|
||||
}
|
||||
|
||||
func (m *mockAuthority) AuthorizeSign(ott string) ([]provisioner.SignOption, error) {
|
||||
if m.authorizeSign != nil {
|
||||
return m.authorizeSign(ott)
|
||||
}
|
||||
return m.ret1.([]provisioner.SignOption), m.err
|
||||
}
|
||||
|
||||
func (m *mockAuthority) GetTLSOptions() *authority.TLSOptions {
|
||||
if m.getTLSOptions != nil {
|
||||
return m.getTLSOptions()
|
||||
}
|
||||
return m.ret1.(*authority.TLSOptions)
|
||||
}
|
||||
|
||||
func (m *mockAuthority) Root(shasum string) (*x509.Certificate, error) {
|
||||
if m.root != nil {
|
||||
return m.root(shasum)
|
||||
}
|
||||
return m.ret1.(*x509.Certificate), m.err
|
||||
}
|
||||
|
||||
func (m *mockAuthority) Sign(cr *x509.CertificateRequest, opts provisioner.SignOptions, signOpts ...provisioner.SignOption) ([]*x509.Certificate, error) {
|
||||
if m.sign != nil {
|
||||
return m.sign(cr, opts, signOpts...)
|
||||
}
|
||||
return []*x509.Certificate{m.ret1.(*x509.Certificate), m.ret2.(*x509.Certificate)}, m.err
|
||||
}
|
||||
|
||||
func (m *mockAuthority) Renew(cert *x509.Certificate) ([]*x509.Certificate, error) {
|
||||
if m.renew != nil {
|
||||
return m.renew(cert)
|
||||
}
|
||||
return []*x509.Certificate{m.ret1.(*x509.Certificate), m.ret2.(*x509.Certificate)}, m.err
|
||||
}
|
||||
|
||||
func (m *mockAuthority) Rekey(oldcert *x509.Certificate, pk crypto.PublicKey) ([]*x509.Certificate, error) {
|
||||
if m.rekey != nil {
|
||||
return m.rekey(oldcert, pk)
|
||||
}
|
||||
return []*x509.Certificate{m.ret1.(*x509.Certificate), m.ret2.(*x509.Certificate)}, m.err
|
||||
}
|
||||
|
||||
func (m *mockAuthority) GetProvisioners(nextCursor string, limit int) (provisioner.List, string, error) {
|
||||
if m.getProvisioners != nil {
|
||||
return m.getProvisioners(nextCursor, limit)
|
||||
}
|
||||
return m.ret1.(provisioner.List), m.ret2.(string), m.err
|
||||
}
|
||||
|
||||
func (m *mockAuthority) LoadProvisionerByCertificate(cert *x509.Certificate) (provisioner.Interface, error) {
|
||||
if m.loadProvisionerByCertificate != nil {
|
||||
return m.loadProvisionerByCertificate(cert)
|
||||
}
|
||||
return m.ret1.(provisioner.Interface), m.err
|
||||
}
|
||||
|
||||
func (m *mockAuthority) LoadProvisionerByName(name string) (provisioner.Interface, error) {
|
||||
if m.loadProvisionerByName != nil {
|
||||
return m.loadProvisionerByName(name)
|
||||
}
|
||||
return m.ret1.(provisioner.Interface), m.err
|
||||
}
|
||||
|
||||
func (m *mockAuthority) Revoke(ctx context.Context, opts *authority.RevokeOptions) error {
|
||||
if m.revoke != nil {
|
||||
return m.revoke(ctx, opts)
|
||||
}
|
||||
return m.err
|
||||
}
|
||||
|
||||
func (m *mockAuthority) GetEncryptedKey(kid string) (string, error) {
|
||||
if m.getEncryptedKey != nil {
|
||||
return m.getEncryptedKey(kid)
|
||||
}
|
||||
return m.ret1.(string), m.err
|
||||
}
|
||||
|
||||
func (m *mockAuthority) GetRoots() ([]*x509.Certificate, error) {
|
||||
if m.getRoots != nil {
|
||||
return m.getRoots()
|
||||
}
|
||||
return m.ret1.([]*x509.Certificate), m.err
|
||||
}
|
||||
|
||||
func (m *mockAuthority) GetFederation() ([]*x509.Certificate, error) {
|
||||
if m.getFederation != nil {
|
||||
return m.getFederation()
|
||||
}
|
||||
return m.ret1.([]*x509.Certificate), m.err
|
||||
}
|
||||
|
||||
func (m *mockAuthority) SignSSH(ctx context.Context, key ssh.PublicKey, opts provisioner.SignSSHOptions, signOpts ...provisioner.SignOption) (*ssh.Certificate, error) {
|
||||
if m.signSSH != nil {
|
||||
return m.signSSH(ctx, key, opts, signOpts...)
|
||||
}
|
||||
return m.ret1.(*ssh.Certificate), m.err
|
||||
}
|
||||
|
||||
func (m *mockAuthority) SignSSHAddUser(ctx context.Context, key ssh.PublicKey, cert *ssh.Certificate) (*ssh.Certificate, error) {
|
||||
if m.signSSHAddUser != nil {
|
||||
return m.signSSHAddUser(ctx, key, cert)
|
||||
}
|
||||
return m.ret1.(*ssh.Certificate), m.err
|
||||
}
|
||||
|
||||
func (m *mockAuthority) RenewSSH(ctx context.Context, cert *ssh.Certificate) (*ssh.Certificate, error) {
|
||||
if m.renewSSH != nil {
|
||||
return m.renewSSH(ctx, cert)
|
||||
}
|
||||
return m.ret1.(*ssh.Certificate), m.err
|
||||
}
|
||||
|
||||
func (m *mockAuthority) RekeySSH(ctx context.Context, cert *ssh.Certificate, key ssh.PublicKey, signOpts ...provisioner.SignOption) (*ssh.Certificate, error) {
|
||||
if m.rekeySSH != nil {
|
||||
return m.rekeySSH(ctx, cert, key, signOpts...)
|
||||
}
|
||||
return m.ret1.(*ssh.Certificate), m.err
|
||||
}
|
||||
|
||||
func (m *mockAuthority) GetSSHHosts(ctx context.Context, cert *x509.Certificate) ([]authority.Host, error) {
|
||||
if m.getSSHHosts != nil {
|
||||
return m.getSSHHosts(ctx, cert)
|
||||
}
|
||||
return m.ret1.([]authority.Host), m.err
|
||||
}
|
||||
|
||||
func (m *mockAuthority) GetSSHRoots(ctx context.Context) (*authority.SSHKeys, error) {
|
||||
if m.getSSHRoots != nil {
|
||||
return m.getSSHRoots(ctx)
|
||||
}
|
||||
return m.ret1.(*authority.SSHKeys), m.err
|
||||
}
|
||||
|
||||
func (m *mockAuthority) GetSSHFederation(ctx context.Context) (*authority.SSHKeys, error) {
|
||||
if m.getSSHFederation != nil {
|
||||
return m.getSSHFederation(ctx)
|
||||
}
|
||||
return m.ret1.(*authority.SSHKeys), m.err
|
||||
}
|
||||
|
||||
func (m *mockAuthority) GetSSHConfig(ctx context.Context, typ string, data map[string]string) ([]templates.Output, error) {
|
||||
if m.getSSHConfig != nil {
|
||||
return m.getSSHConfig(ctx, typ, data)
|
||||
}
|
||||
return m.ret1.([]templates.Output), m.err
|
||||
}
|
||||
|
||||
func (m *mockAuthority) CheckSSHHost(ctx context.Context, principal, token string) (bool, error) {
|
||||
if m.checkSSHHost != nil {
|
||||
return m.checkSSHHost(ctx, principal, token)
|
||||
}
|
||||
return m.ret1.(bool), m.err
|
||||
}
|
||||
|
||||
func (m *mockAuthority) GetSSHBastion(ctx context.Context, user, hostname string) (*authority.Bastion, error) {
|
||||
if m.getSSHBastion != nil {
|
||||
return m.getSSHBastion(ctx, user, hostname)
|
||||
}
|
||||
return m.ret1.(*authority.Bastion), m.err
|
||||
}
|
||||
|
||||
func (m *mockAuthority) Version() authority.Version {
|
||||
if m.version != nil {
|
||||
return m.version()
|
||||
}
|
||||
return m.ret1.(authority.Version)
|
||||
}
|
||||
|
||||
func Test_caHandler_Route(t *testing.T) {
|
||||
type fields struct {
|
||||
Authority Authority
|
||||
|
|
246
authority/admin/api/acme.go
Normal file
246
authority/admin/api/acme.go
Normal file
|
@ -0,0 +1,246 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/go-chi/chi"
|
||||
"github.com/smallstep/certificates/acme"
|
||||
"github.com/smallstep/certificates/api"
|
||||
"github.com/smallstep/certificates/authority/admin"
|
||||
"github.com/smallstep/certificates/authority/provisioner"
|
||||
"go.step.sm/linkedca"
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
)
|
||||
|
||||
const (
|
||||
// provisionerContextKey provisioner key
|
||||
provisionerContextKey = ContextKey("provisioner")
|
||||
)
|
||||
|
||||
// CreateExternalAccountKeyRequest is the type for POST /admin/acme/eab requests
|
||||
type CreateExternalAccountKeyRequest struct {
|
||||
Reference string `json:"reference"`
|
||||
}
|
||||
|
||||
// Validate validates a new ACME EAB Key request body.
|
||||
func (r *CreateExternalAccountKeyRequest) Validate() error {
|
||||
if len(r.Reference) > 256 { // an arbitrary, but sensible (IMO), limit
|
||||
return fmt.Errorf("reference length %d exceeds the maximum (256)", len(r.Reference))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetExternalAccountKeysResponse is the type for GET /admin/acme/eab responses
|
||||
type GetExternalAccountKeysResponse struct {
|
||||
EAKs []*linkedca.EABKey `json:"eaks"`
|
||||
NextCursor string `json:"nextCursor"`
|
||||
}
|
||||
|
||||
// requireEABEnabled is a middleware that ensures ACME EAB is enabled
|
||||
// before serving requests that act on ACME EAB credentials.
|
||||
func (h *Handler) requireEABEnabled(next nextHTTP) nextHTTP {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
provName := chi.URLParam(r, "provisionerName")
|
||||
eabEnabled, prov, err := h.provisionerHasEABEnabled(ctx, provName)
|
||||
if err != nil {
|
||||
api.WriteError(w, err)
|
||||
return
|
||||
}
|
||||
if !eabEnabled {
|
||||
api.WriteError(w, admin.NewError(admin.ErrorBadRequestType, "ACME EAB not enabled for provisioner %s", prov.GetName()))
|
||||
return
|
||||
}
|
||||
ctx = context.WithValue(ctx, provisionerContextKey, prov)
|
||||
next(w, r.WithContext(ctx))
|
||||
}
|
||||
}
|
||||
|
||||
// provisionerHasEABEnabled determines if the "requireEAB" setting for an ACME
|
||||
// provisioner is set to true and thus has EAB enabled.
|
||||
func (h *Handler) provisionerHasEABEnabled(ctx context.Context, provisionerName string) (bool, *linkedca.Provisioner, error) {
|
||||
var (
|
||||
p provisioner.Interface
|
||||
err error
|
||||
)
|
||||
if p, err = h.auth.LoadProvisionerByName(provisionerName); err != nil {
|
||||
return false, nil, admin.WrapErrorISE(err, "error loading provisioner %s", provisionerName)
|
||||
}
|
||||
|
||||
prov, err := h.db.GetProvisioner(ctx, p.GetID())
|
||||
if err != nil {
|
||||
return false, nil, admin.WrapErrorISE(err, "error getting provisioner with ID: %s", p.GetID())
|
||||
}
|
||||
|
||||
details := prov.GetDetails()
|
||||
if details == nil {
|
||||
return false, nil, admin.NewErrorISE("error getting details for provisioner with ID: %s", p.GetID())
|
||||
}
|
||||
|
||||
acmeProvisioner := details.GetACME()
|
||||
if acmeProvisioner == nil {
|
||||
return false, nil, admin.NewErrorISE("error getting ACME details for provisioner with ID: %s", p.GetID())
|
||||
}
|
||||
|
||||
return acmeProvisioner.GetRequireEab(), prov, nil
|
||||
}
|
||||
|
||||
// provisionerFromContext searches the context for a provisioner. Returns the
|
||||
// provisioner or an error.
|
||||
func provisionerFromContext(ctx context.Context) (*linkedca.Provisioner, error) {
|
||||
val := ctx.Value(provisionerContextKey)
|
||||
if val == nil {
|
||||
return nil, admin.NewErrorISE("provisioner expected in request context")
|
||||
}
|
||||
pval, ok := val.(*linkedca.Provisioner)
|
||||
if !ok || pval == nil {
|
||||
return nil, admin.NewErrorISE("provisioner in context is not a linkedca.Provisioner")
|
||||
}
|
||||
return pval, nil
|
||||
}
|
||||
|
||||
// CreateExternalAccountKey creates a new External Account Binding key
|
||||
func (h *Handler) CreateExternalAccountKey(w http.ResponseWriter, r *http.Request) {
|
||||
var body CreateExternalAccountKeyRequest
|
||||
if err := api.ReadJSON(r.Body, &body); err != nil {
|
||||
api.WriteError(w, admin.WrapError(admin.ErrorBadRequestType, err, "error reading request body"))
|
||||
return
|
||||
}
|
||||
|
||||
if err := body.Validate(); err != nil {
|
||||
api.WriteError(w, admin.WrapError(admin.ErrorBadRequestType, err, "error validating request body"))
|
||||
return
|
||||
}
|
||||
|
||||
ctx := r.Context()
|
||||
prov, err := provisionerFromContext(ctx)
|
||||
if err != nil {
|
||||
api.WriteError(w, admin.WrapErrorISE(err, "error getting provisioner from context"))
|
||||
return
|
||||
}
|
||||
|
||||
// check if a key with the reference does not exist (only when a reference was in the request)
|
||||
reference := body.Reference
|
||||
if reference != "" {
|
||||
k, err := h.acmeDB.GetExternalAccountKeyByReference(ctx, prov.GetId(), reference)
|
||||
// retrieving an EAB key from DB results in an error if it doesn't exist, which is what we're looking for,
|
||||
// but other errors can also happen. Return early if that happens; continuing if it was acme.ErrNotFound.
|
||||
if shouldWriteError := err != nil && !errors.Is(err, acme.ErrNotFound); shouldWriteError {
|
||||
api.WriteError(w, admin.WrapErrorISE(err, "could not lookup external account key by reference"))
|
||||
return
|
||||
}
|
||||
// if a key was found, return HTTP 409 conflict
|
||||
if k != nil {
|
||||
err := admin.NewError(admin.ErrorBadRequestType, "an ACME EAB key for provisioner '%s' with reference '%s' already exists", prov.GetName(), reference)
|
||||
err.Status = 409
|
||||
api.WriteError(w, err)
|
||||
return
|
||||
}
|
||||
// continue execution if no key was found for the reference
|
||||
}
|
||||
|
||||
eak, err := h.acmeDB.CreateExternalAccountKey(ctx, prov.GetId(), reference)
|
||||
if err != nil {
|
||||
msg := fmt.Sprintf("error creating ACME EAB key for provisioner '%s'", prov.GetName())
|
||||
if reference != "" {
|
||||
msg += fmt.Sprintf(" and reference '%s'", reference)
|
||||
}
|
||||
api.WriteError(w, admin.WrapErrorISE(err, msg))
|
||||
return
|
||||
}
|
||||
|
||||
response := &linkedca.EABKey{
|
||||
Id: eak.ID,
|
||||
HmacKey: eak.KeyBytes,
|
||||
Provisioner: prov.GetName(),
|
||||
Reference: eak.Reference,
|
||||
}
|
||||
|
||||
api.ProtoJSONStatus(w, response, http.StatusCreated)
|
||||
}
|
||||
|
||||
// DeleteExternalAccountKey deletes an ACME External Account Key.
|
||||
func (h *Handler) DeleteExternalAccountKey(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
keyID := chi.URLParam(r, "id")
|
||||
|
||||
ctx := r.Context()
|
||||
prov, err := provisionerFromContext(ctx)
|
||||
if err != nil {
|
||||
api.WriteError(w, admin.WrapErrorISE(err, "error getting provisioner from context"))
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.acmeDB.DeleteExternalAccountKey(ctx, prov.GetId(), keyID); err != nil {
|
||||
api.WriteError(w, admin.WrapErrorISE(err, "error deleting ACME EAB Key '%s'", keyID))
|
||||
return
|
||||
}
|
||||
|
||||
api.JSON(w, &DeleteResponse{Status: "ok"})
|
||||
}
|
||||
|
||||
// GetExternalAccountKeys returns ACME EAB Keys. If a reference is specified,
|
||||
// only the ExternalAccountKey with that reference is returned. Otherwise all
|
||||
// ExternalAccountKeys in the system for a specific provisioner are returned.
|
||||
func (h *Handler) GetExternalAccountKeys(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
var (
|
||||
key *acme.ExternalAccountKey
|
||||
keys []*acme.ExternalAccountKey
|
||||
err error
|
||||
cursor string
|
||||
nextCursor string
|
||||
limit int
|
||||
)
|
||||
|
||||
ctx := r.Context()
|
||||
prov, err := provisionerFromContext(ctx)
|
||||
if err != nil {
|
||||
api.WriteError(w, admin.WrapErrorISE(err, "error getting provisioner from context"))
|
||||
return
|
||||
}
|
||||
|
||||
if cursor, limit, err = api.ParseCursor(r); err != nil {
|
||||
api.WriteError(w, admin.WrapError(admin.ErrorBadRequestType, err,
|
||||
"error parsing cursor and limit from query params"))
|
||||
return
|
||||
}
|
||||
|
||||
reference := chi.URLParam(r, "reference")
|
||||
if reference != "" {
|
||||
if key, err = h.acmeDB.GetExternalAccountKeyByReference(ctx, prov.GetId(), reference); err != nil {
|
||||
api.WriteError(w, admin.WrapErrorISE(err, "error retrieving external account key with reference '%s'", reference))
|
||||
return
|
||||
}
|
||||
if key != nil {
|
||||
keys = []*acme.ExternalAccountKey{key}
|
||||
}
|
||||
} else {
|
||||
if keys, nextCursor, err = h.acmeDB.GetExternalAccountKeys(ctx, prov.GetId(), cursor, limit); err != nil {
|
||||
api.WriteError(w, admin.WrapErrorISE(err, "error retrieving external account keys"))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
provisionerName := prov.GetName()
|
||||
eaks := make([]*linkedca.EABKey, len(keys))
|
||||
for i, k := range keys {
|
||||
eaks[i] = &linkedca.EABKey{
|
||||
Id: k.ID,
|
||||
HmacKey: []byte{},
|
||||
Provisioner: provisionerName,
|
||||
Reference: k.Reference,
|
||||
Account: k.AccountID,
|
||||
CreatedAt: timestamppb.New(k.CreatedAt),
|
||||
BoundAt: timestamppb.New(k.BoundAt),
|
||||
}
|
||||
}
|
||||
|
||||
api.JSON(w, &GetExternalAccountKeysResponse{
|
||||
EAKs: eaks,
|
||||
NextCursor: nextCursor,
|
||||
})
|
||||
}
|
1222
authority/admin/api/acme_test.go
Normal file
1222
authority/admin/api/acme_test.go
Normal file
File diff suppressed because it is too large
Load diff
|
@ -1,14 +1,32 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
"github.com/go-chi/chi"
|
||||
"github.com/smallstep/certificates/api"
|
||||
"github.com/smallstep/certificates/authority/admin"
|
||||
"github.com/smallstep/certificates/authority/provisioner"
|
||||
"go.step.sm/linkedca"
|
||||
)
|
||||
|
||||
type adminAuthority interface {
|
||||
LoadProvisionerByName(string) (provisioner.Interface, error)
|
||||
GetProvisioners(cursor string, limit int) (provisioner.List, string, error)
|
||||
IsAdminAPIEnabled() bool
|
||||
LoadAdminByID(id string) (*linkedca.Admin, bool)
|
||||
GetAdmins(cursor string, limit int) ([]*linkedca.Admin, string, error)
|
||||
StoreAdmin(ctx context.Context, adm *linkedca.Admin, prov provisioner.Interface) error
|
||||
UpdateAdmin(ctx context.Context, id string, nu *linkedca.Admin) (*linkedca.Admin, error)
|
||||
RemoveAdmin(ctx context.Context, id string) error
|
||||
AuthorizeAdminToken(r *http.Request, token string) (*linkedca.Admin, error)
|
||||
StoreProvisioner(ctx context.Context, prov *linkedca.Provisioner) error
|
||||
LoadProvisionerByID(id string) (provisioner.Interface, error)
|
||||
UpdateProvisioner(ctx context.Context, nu *linkedca.Provisioner) error
|
||||
RemoveProvisioner(ctx context.Context, id string) error
|
||||
}
|
||||
|
||||
// CreateAdminRequest represents the body for a CreateAdmin request.
|
||||
type CreateAdminRequest struct {
|
||||
Subject string `json:"subject"`
|
||||
|
|
919
authority/admin/api/admin_test.go
Normal file
919
authority/admin/api/admin_test.go
Normal file
|
@ -0,0 +1,919 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/go-chi/chi"
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/google/go-cmp/cmp/cmpopts"
|
||||
"github.com/smallstep/assert"
|
||||
"github.com/smallstep/certificates/authority/admin"
|
||||
"github.com/smallstep/certificates/authority/provisioner"
|
||||
"go.step.sm/linkedca"
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
)
|
||||
|
||||
type mockAdminAuthority struct {
|
||||
MockLoadProvisionerByName func(name string) (provisioner.Interface, error)
|
||||
MockGetProvisioners func(nextCursor string, limit int) (provisioner.List, string, error)
|
||||
MockRet1, MockRet2 interface{} // TODO: refactor the ret1/ret2 into those two
|
||||
MockErr error
|
||||
MockIsAdminAPIEnabled func() bool
|
||||
MockLoadAdminByID func(id string) (*linkedca.Admin, bool)
|
||||
MockGetAdmins func(cursor string, limit int) ([]*linkedca.Admin, string, error)
|
||||
MockStoreAdmin func(ctx context.Context, adm *linkedca.Admin, prov provisioner.Interface) error
|
||||
MockUpdateAdmin func(ctx context.Context, id string, nu *linkedca.Admin) (*linkedca.Admin, error)
|
||||
MockRemoveAdmin func(ctx context.Context, id string) error
|
||||
MockAuthorizeAdminToken func(r *http.Request, token string) (*linkedca.Admin, error)
|
||||
MockStoreProvisioner func(ctx context.Context, prov *linkedca.Provisioner) error
|
||||
MockLoadProvisionerByID func(id string) (provisioner.Interface, error)
|
||||
MockUpdateProvisioner func(ctx context.Context, nu *linkedca.Provisioner) error
|
||||
MockRemoveProvisioner func(ctx context.Context, id string) error
|
||||
}
|
||||
|
||||
func (m *mockAdminAuthority) IsAdminAPIEnabled() bool {
|
||||
if m.MockIsAdminAPIEnabled != nil {
|
||||
return m.MockIsAdminAPIEnabled()
|
||||
}
|
||||
return m.MockRet1.(bool)
|
||||
}
|
||||
|
||||
func (m *mockAdminAuthority) LoadProvisionerByName(name string) (provisioner.Interface, error) {
|
||||
if m.MockLoadProvisionerByName != nil {
|
||||
return m.MockLoadProvisionerByName(name)
|
||||
}
|
||||
return m.MockRet1.(provisioner.Interface), m.MockErr
|
||||
}
|
||||
|
||||
func (m *mockAdminAuthority) GetProvisioners(nextCursor string, limit int) (provisioner.List, string, error) {
|
||||
if m.MockGetProvisioners != nil {
|
||||
return m.MockGetProvisioners(nextCursor, limit)
|
||||
}
|
||||
return m.MockRet1.(provisioner.List), m.MockRet2.(string), m.MockErr
|
||||
}
|
||||
|
||||
func (m *mockAdminAuthority) LoadAdminByID(id string) (*linkedca.Admin, bool) {
|
||||
if m.MockLoadAdminByID != nil {
|
||||
return m.MockLoadAdminByID(id)
|
||||
}
|
||||
return m.MockRet1.(*linkedca.Admin), m.MockRet2.(bool)
|
||||
}
|
||||
|
||||
func (m *mockAdminAuthority) GetAdmins(cursor string, limit int) ([]*linkedca.Admin, string, error) {
|
||||
if m.MockGetAdmins != nil {
|
||||
return m.MockGetAdmins(cursor, limit)
|
||||
}
|
||||
return m.MockRet1.([]*linkedca.Admin), m.MockRet2.(string), m.MockErr
|
||||
}
|
||||
|
||||
func (m *mockAdminAuthority) StoreAdmin(ctx context.Context, adm *linkedca.Admin, prov provisioner.Interface) error {
|
||||
if m.MockStoreAdmin != nil {
|
||||
return m.MockStoreAdmin(ctx, adm, prov)
|
||||
}
|
||||
return m.MockErr
|
||||
}
|
||||
|
||||
func (m *mockAdminAuthority) UpdateAdmin(ctx context.Context, id string, nu *linkedca.Admin) (*linkedca.Admin, error) {
|
||||
if m.MockUpdateAdmin != nil {
|
||||
return m.MockUpdateAdmin(ctx, id, nu)
|
||||
}
|
||||
return m.MockRet1.(*linkedca.Admin), m.MockErr
|
||||
}
|
||||
|
||||
func (m *mockAdminAuthority) RemoveAdmin(ctx context.Context, id string) error {
|
||||
if m.MockRemoveAdmin != nil {
|
||||
return m.MockRemoveAdmin(ctx, id)
|
||||
}
|
||||
return m.MockErr
|
||||
}
|
||||
|
||||
func (m *mockAdminAuthority) AuthorizeAdminToken(r *http.Request, token string) (*linkedca.Admin, error) {
|
||||
if m.MockAuthorizeAdminToken != nil {
|
||||
return m.MockAuthorizeAdminToken(r, token)
|
||||
}
|
||||
return m.MockRet1.(*linkedca.Admin), m.MockErr
|
||||
}
|
||||
|
||||
func (m *mockAdminAuthority) StoreProvisioner(ctx context.Context, prov *linkedca.Provisioner) error {
|
||||
if m.MockStoreProvisioner != nil {
|
||||
return m.MockStoreProvisioner(ctx, prov)
|
||||
}
|
||||
return m.MockErr
|
||||
}
|
||||
|
||||
func (m *mockAdminAuthority) LoadProvisionerByID(id string) (provisioner.Interface, error) {
|
||||
if m.MockLoadProvisionerByID != nil {
|
||||
return m.MockLoadProvisionerByID(id)
|
||||
}
|
||||
return m.MockRet1.(provisioner.Interface), m.MockErr
|
||||
}
|
||||
|
||||
func (m *mockAdminAuthority) UpdateProvisioner(ctx context.Context, nu *linkedca.Provisioner) error {
|
||||
if m.MockUpdateProvisioner != nil {
|
||||
return m.MockUpdateProvisioner(ctx, nu)
|
||||
}
|
||||
return m.MockErr
|
||||
}
|
||||
|
||||
func (m *mockAdminAuthority) RemoveProvisioner(ctx context.Context, id string) error {
|
||||
if m.MockRemoveProvisioner != nil {
|
||||
return m.MockRemoveProvisioner(ctx, id)
|
||||
}
|
||||
return m.MockErr
|
||||
}
|
||||
|
||||
func TestCreateAdminRequest_Validate(t *testing.T) {
|
||||
type fields struct {
|
||||
Subject string
|
||||
Provisioner string
|
||||
Type linkedca.Admin_Type
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
err *admin.Error
|
||||
}{
|
||||
{
|
||||
name: "fail/subject-empty",
|
||||
fields: fields{
|
||||
Subject: "",
|
||||
Provisioner: "",
|
||||
Type: 0,
|
||||
},
|
||||
err: admin.NewError(admin.ErrorBadRequestType, "subject cannot be empty"),
|
||||
},
|
||||
{
|
||||
name: "fail/provisioner-empty",
|
||||
fields: fields{
|
||||
Subject: "admin",
|
||||
Provisioner: "",
|
||||
Type: 0,
|
||||
},
|
||||
err: admin.NewError(admin.ErrorBadRequestType, "provisioner cannot be empty"),
|
||||
},
|
||||
{
|
||||
name: "fail/invalid-type",
|
||||
fields: fields{
|
||||
Subject: "admin",
|
||||
Provisioner: "prov",
|
||||
Type: -1,
|
||||
},
|
||||
err: admin.NewError(admin.ErrorBadRequestType, "invalid value for admin type"),
|
||||
},
|
||||
{
|
||||
name: "ok",
|
||||
fields: fields{
|
||||
Subject: "admin",
|
||||
Provisioner: "prov",
|
||||
Type: linkedca.Admin_SUPER_ADMIN,
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
car := &CreateAdminRequest{
|
||||
Subject: tt.fields.Subject,
|
||||
Provisioner: tt.fields.Provisioner,
|
||||
Type: tt.fields.Type,
|
||||
}
|
||||
err := car.Validate()
|
||||
|
||||
if (err != nil) != (tt.err != nil) {
|
||||
t.Errorf("CreateAdminRequest.Validate() error = %v, wantErr %v", err, (tt.err != nil))
|
||||
return
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
assert.Type(t, &admin.Error{}, err)
|
||||
adminErr, _ := err.(*admin.Error)
|
||||
assert.Equals(t, tt.err.Type, adminErr.Type)
|
||||
assert.Equals(t, tt.err.Detail, adminErr.Detail)
|
||||
assert.Equals(t, tt.err.Status, adminErr.Status)
|
||||
assert.Equals(t, tt.err.Message, adminErr.Message)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateAdminRequest_Validate(t *testing.T) {
|
||||
type fields struct {
|
||||
Type linkedca.Admin_Type
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
err *admin.Error
|
||||
}{
|
||||
{
|
||||
name: "fail/invalid-type",
|
||||
fields: fields{
|
||||
Type: -1,
|
||||
},
|
||||
err: admin.NewError(admin.ErrorBadRequestType, "invalid value for admin type"),
|
||||
},
|
||||
{
|
||||
name: "ok",
|
||||
fields: fields{
|
||||
Type: linkedca.Admin_SUPER_ADMIN,
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
uar := &UpdateAdminRequest{
|
||||
Type: tt.fields.Type,
|
||||
}
|
||||
|
||||
err := uar.Validate()
|
||||
|
||||
if (err != nil) != (tt.err != nil) {
|
||||
t.Errorf("CreateAdminRequest.Validate() error = %v, wantErr %v", err, (tt.err != nil))
|
||||
return
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
assert.Type(t, &admin.Error{}, err)
|
||||
adminErr, _ := err.(*admin.Error)
|
||||
assert.Equals(t, tt.err.Type, adminErr.Type)
|
||||
assert.Equals(t, tt.err.Detail, adminErr.Detail)
|
||||
assert.Equals(t, tt.err.Status, adminErr.Status)
|
||||
assert.Equals(t, tt.err.Message, adminErr.Message)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandler_GetAdmin(t *testing.T) {
|
||||
type test struct {
|
||||
ctx context.Context
|
||||
auth adminAuthority
|
||||
statusCode int
|
||||
err *admin.Error
|
||||
adm *linkedca.Admin
|
||||
}
|
||||
var tests = map[string]func(t *testing.T) test{
|
||||
"fail/auth.LoadAdminByID-not-found": func(t *testing.T) test {
|
||||
chiCtx := chi.NewRouteContext()
|
||||
chiCtx.URLParams.Add("id", "adminID")
|
||||
ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx)
|
||||
auth := &mockAdminAuthority{
|
||||
MockLoadAdminByID: func(id string) (*linkedca.Admin, bool) {
|
||||
assert.Equals(t, "adminID", id)
|
||||
return nil, false
|
||||
},
|
||||
}
|
||||
return test{
|
||||
ctx: ctx,
|
||||
auth: auth,
|
||||
statusCode: 404,
|
||||
err: &admin.Error{
|
||||
Type: admin.ErrorNotFoundType.String(),
|
||||
Status: 404,
|
||||
Detail: "resource not found",
|
||||
Message: "admin adminID not found",
|
||||
},
|
||||
}
|
||||
},
|
||||
"ok": func(t *testing.T) test {
|
||||
chiCtx := chi.NewRouteContext()
|
||||
chiCtx.URLParams.Add("id", "adminID")
|
||||
ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx)
|
||||
createdAt := time.Now()
|
||||
var deletedAt time.Time
|
||||
adm := &linkedca.Admin{
|
||||
Id: "adminID",
|
||||
AuthorityId: "authorityID",
|
||||
Subject: "admin",
|
||||
ProvisionerId: "provID",
|
||||
Type: linkedca.Admin_SUPER_ADMIN,
|
||||
CreatedAt: timestamppb.New(createdAt),
|
||||
DeletedAt: timestamppb.New(deletedAt),
|
||||
}
|
||||
auth := &mockAdminAuthority{
|
||||
MockLoadAdminByID: func(id string) (*linkedca.Admin, bool) {
|
||||
assert.Equals(t, "adminID", id)
|
||||
return adm, true
|
||||
},
|
||||
}
|
||||
return test{
|
||||
ctx: ctx,
|
||||
auth: auth,
|
||||
statusCode: 200,
|
||||
err: nil,
|
||||
adm: adm,
|
||||
}
|
||||
},
|
||||
}
|
||||
for name, prep := range tests {
|
||||
tc := prep(t)
|
||||
t.Run(name, func(t *testing.T) {
|
||||
h := &Handler{
|
||||
auth: tc.auth,
|
||||
}
|
||||
|
||||
req := httptest.NewRequest("GET", "/foo", nil) // chi routing is prepared in test setup
|
||||
req = req.WithContext(tc.ctx)
|
||||
w := httptest.NewRecorder()
|
||||
h.GetAdmin(w, req)
|
||||
res := w.Result()
|
||||
|
||||
assert.Equals(t, tc.statusCode, res.StatusCode)
|
||||
|
||||
if res.StatusCode >= 400 {
|
||||
|
||||
body, err := io.ReadAll(res.Body)
|
||||
res.Body.Close()
|
||||
assert.FatalError(t, err)
|
||||
|
||||
adminErr := admin.Error{}
|
||||
assert.FatalError(t, json.Unmarshal(bytes.TrimSpace(body), &adminErr))
|
||||
|
||||
assert.Equals(t, tc.err.Type, adminErr.Type)
|
||||
assert.Equals(t, tc.err.Message, adminErr.Message)
|
||||
assert.Equals(t, tc.err.Detail, adminErr.Detail)
|
||||
assert.Equals(t, []string{"application/json"}, res.Header["Content-Type"])
|
||||
return
|
||||
}
|
||||
|
||||
adm := &linkedca.Admin{}
|
||||
err := readProtoJSON(res.Body, adm)
|
||||
assert.FatalError(t, err)
|
||||
|
||||
assert.Equals(t, []string{"application/json"}, res.Header["Content-Type"])
|
||||
|
||||
opts := []cmp.Option{cmpopts.IgnoreUnexported(linkedca.Admin{}, timestamppb.Timestamp{})}
|
||||
if !cmp.Equal(tc.adm, adm, opts...) {
|
||||
t.Errorf("linkedca.Admin diff =\n%s", cmp.Diff(tc.adm, adm, opts...))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandler_GetAdmins(t *testing.T) {
|
||||
type test struct {
|
||||
ctx context.Context
|
||||
auth adminAuthority
|
||||
req *http.Request
|
||||
statusCode int
|
||||
err *admin.Error
|
||||
resp GetAdminsResponse
|
||||
}
|
||||
var tests = map[string]func(t *testing.T) test{
|
||||
"fail/parse-cursor": func(t *testing.T) test {
|
||||
req := httptest.NewRequest("GET", "/foo?limit=A", nil)
|
||||
return test{
|
||||
ctx: context.Background(),
|
||||
req: req,
|
||||
statusCode: 400,
|
||||
err: &admin.Error{
|
||||
Status: 400,
|
||||
Type: admin.ErrorBadRequestType.String(),
|
||||
Detail: "bad request",
|
||||
Message: "error parsing cursor and limit from query params: limit 'A' is not an integer: strconv.Atoi: parsing \"A\": invalid syntax",
|
||||
},
|
||||
}
|
||||
},
|
||||
"fail/auth.GetAdmins": func(t *testing.T) test {
|
||||
req := httptest.NewRequest("GET", "/foo", nil)
|
||||
auth := &mockAdminAuthority{
|
||||
MockGetAdmins: func(cursor string, limit int) ([]*linkedca.Admin, string, error) {
|
||||
assert.Equals(t, "", cursor)
|
||||
assert.Equals(t, 0, limit)
|
||||
return nil, "", errors.New("force")
|
||||
},
|
||||
}
|
||||
return test{
|
||||
ctx: context.Background(),
|
||||
req: req,
|
||||
auth: auth,
|
||||
statusCode: 500,
|
||||
err: &admin.Error{
|
||||
Status: 500,
|
||||
Type: admin.ErrorServerInternalType.String(),
|
||||
Detail: "the server experienced an internal error",
|
||||
Message: "error retrieving paginated admins: force",
|
||||
},
|
||||
}
|
||||
},
|
||||
"ok": func(t *testing.T) test {
|
||||
req := httptest.NewRequest("GET", "/foo", nil)
|
||||
createdAt := time.Now()
|
||||
var deletedAt time.Time
|
||||
adm1 := &linkedca.Admin{
|
||||
Id: "adminID1",
|
||||
AuthorityId: "authorityID1",
|
||||
Subject: "admin1",
|
||||
ProvisionerId: "provID",
|
||||
Type: linkedca.Admin_SUPER_ADMIN,
|
||||
CreatedAt: timestamppb.New(createdAt),
|
||||
DeletedAt: timestamppb.New(deletedAt),
|
||||
}
|
||||
adm2 := &linkedca.Admin{
|
||||
Id: "adminID2",
|
||||
AuthorityId: "authorityID",
|
||||
Subject: "admin2",
|
||||
ProvisionerId: "provID",
|
||||
Type: linkedca.Admin_ADMIN,
|
||||
CreatedAt: timestamppb.New(createdAt),
|
||||
DeletedAt: timestamppb.New(deletedAt),
|
||||
}
|
||||
auth := &mockAdminAuthority{
|
||||
MockGetAdmins: func(cursor string, limit int) ([]*linkedca.Admin, string, error) {
|
||||
assert.Equals(t, "", cursor)
|
||||
assert.Equals(t, 0, limit)
|
||||
return []*linkedca.Admin{
|
||||
adm1,
|
||||
adm2,
|
||||
}, "nextCursorValue", nil
|
||||
},
|
||||
}
|
||||
return test{
|
||||
ctx: context.Background(),
|
||||
req: req,
|
||||
auth: auth,
|
||||
statusCode: 200,
|
||||
err: nil,
|
||||
resp: GetAdminsResponse{
|
||||
Admins: []*linkedca.Admin{
|
||||
adm1,
|
||||
adm2,
|
||||
},
|
||||
NextCursor: "nextCursorValue",
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
for name, prep := range tests {
|
||||
tc := prep(t)
|
||||
t.Run(name, func(t *testing.T) {
|
||||
h := &Handler{
|
||||
auth: tc.auth,
|
||||
}
|
||||
|
||||
req := tc.req.WithContext(tc.ctx)
|
||||
w := httptest.NewRecorder()
|
||||
h.GetAdmins(w, req)
|
||||
res := w.Result()
|
||||
|
||||
assert.Equals(t, tc.statusCode, res.StatusCode)
|
||||
|
||||
body, err := io.ReadAll(res.Body)
|
||||
res.Body.Close()
|
||||
assert.FatalError(t, err)
|
||||
|
||||
if res.StatusCode >= 400 {
|
||||
|
||||
adminErr := admin.Error{}
|
||||
assert.FatalError(t, json.Unmarshal(bytes.TrimSpace(body), &adminErr))
|
||||
|
||||
assert.Equals(t, tc.err.Type, adminErr.Type)
|
||||
assert.Equals(t, tc.err.Message, adminErr.Message)
|
||||
assert.Equals(t, tc.err.Detail, adminErr.Detail)
|
||||
assert.Equals(t, []string{"application/json"}, res.Header["Content-Type"])
|
||||
return
|
||||
}
|
||||
|
||||
response := GetAdminsResponse{}
|
||||
assert.FatalError(t, json.Unmarshal(bytes.TrimSpace(body), &response))
|
||||
assert.Equals(t, []string{"application/json"}, res.Header["Content-Type"])
|
||||
|
||||
opts := []cmp.Option{cmpopts.IgnoreUnexported(linkedca.Admin{}, timestamppb.Timestamp{})}
|
||||
if !cmp.Equal(tc.resp, response, opts...) {
|
||||
t.Errorf("GetAdmins diff =\n%s", cmp.Diff(tc.resp, response, opts...))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandler_CreateAdmin(t *testing.T) {
|
||||
type test struct {
|
||||
ctx context.Context
|
||||
auth adminAuthority
|
||||
body []byte
|
||||
statusCode int
|
||||
err *admin.Error
|
||||
adm *linkedca.Admin
|
||||
}
|
||||
var tests = map[string]func(t *testing.T) test{
|
||||
"fail/ReadJSON": func(t *testing.T) test {
|
||||
body := []byte("{!?}")
|
||||
return test{
|
||||
ctx: context.Background(),
|
||||
body: body,
|
||||
statusCode: 400,
|
||||
err: &admin.Error{
|
||||
Type: admin.ErrorBadRequestType.String(),
|
||||
Status: 400,
|
||||
Detail: "bad request",
|
||||
Message: "error reading request body: error decoding json: invalid character '!' looking for beginning of object key string",
|
||||
},
|
||||
}
|
||||
},
|
||||
"fail/validate": func(t *testing.T) test {
|
||||
req := CreateAdminRequest{
|
||||
Subject: "",
|
||||
Provisioner: "",
|
||||
Type: -1,
|
||||
}
|
||||
body, err := json.Marshal(req)
|
||||
assert.FatalError(t, err)
|
||||
return test{
|
||||
ctx: context.Background(),
|
||||
body: body,
|
||||
statusCode: 400,
|
||||
err: &admin.Error{
|
||||
Type: admin.ErrorBadRequestType.String(),
|
||||
Status: 400,
|
||||
Detail: "bad request",
|
||||
Message: "subject cannot be empty",
|
||||
},
|
||||
}
|
||||
},
|
||||
"fail/auth.LoadProvisionerByName": func(t *testing.T) test {
|
||||
req := CreateAdminRequest{
|
||||
Subject: "admin",
|
||||
Provisioner: "prov",
|
||||
Type: linkedca.Admin_SUPER_ADMIN,
|
||||
}
|
||||
body, err := json.Marshal(req)
|
||||
assert.FatalError(t, err)
|
||||
auth := &mockAdminAuthority{
|
||||
MockLoadProvisionerByName: func(name string) (provisioner.Interface, error) {
|
||||
assert.Equals(t, "prov", name)
|
||||
return nil, errors.New("force")
|
||||
},
|
||||
}
|
||||
return test{
|
||||
ctx: context.Background(),
|
||||
body: body,
|
||||
auth: auth,
|
||||
statusCode: 500,
|
||||
err: &admin.Error{
|
||||
Type: admin.ErrorServerInternalType.String(),
|
||||
Status: 500,
|
||||
Detail: "the server experienced an internal error",
|
||||
Message: "error loading provisioner prov: force",
|
||||
},
|
||||
}
|
||||
},
|
||||
"fail/auth.StoreAdmin": func(t *testing.T) test {
|
||||
req := CreateAdminRequest{
|
||||
Subject: "admin",
|
||||
Provisioner: "prov",
|
||||
Type: linkedca.Admin_SUPER_ADMIN,
|
||||
}
|
||||
body, err := json.Marshal(req)
|
||||
assert.FatalError(t, err)
|
||||
auth := &mockAdminAuthority{
|
||||
MockLoadProvisionerByName: func(name string) (provisioner.Interface, error) {
|
||||
assert.Equals(t, "prov", name)
|
||||
return &provisioner.ACME{
|
||||
ID: "provID",
|
||||
Name: "prov",
|
||||
}, nil
|
||||
},
|
||||
MockStoreAdmin: func(ctx context.Context, adm *linkedca.Admin, prov provisioner.Interface) error {
|
||||
assert.Equals(t, "admin", adm.Subject)
|
||||
assert.Equals(t, "provID", prov.GetID())
|
||||
return errors.New("force")
|
||||
},
|
||||
}
|
||||
return test{
|
||||
ctx: context.Background(),
|
||||
body: body,
|
||||
auth: auth,
|
||||
statusCode: 500,
|
||||
err: &admin.Error{
|
||||
Type: admin.ErrorServerInternalType.String(),
|
||||
Status: 500,
|
||||
Detail: "the server experienced an internal error",
|
||||
Message: "error storing admin: force",
|
||||
},
|
||||
}
|
||||
},
|
||||
"ok": func(t *testing.T) test {
|
||||
req := CreateAdminRequest{
|
||||
Subject: "admin",
|
||||
Provisioner: "prov",
|
||||
Type: linkedca.Admin_SUPER_ADMIN,
|
||||
}
|
||||
body, err := json.Marshal(req)
|
||||
assert.FatalError(t, err)
|
||||
auth := &mockAdminAuthority{
|
||||
MockLoadProvisionerByName: func(name string) (provisioner.Interface, error) {
|
||||
assert.Equals(t, "prov", name)
|
||||
return &provisioner.ACME{
|
||||
ID: "provID",
|
||||
Name: "prov",
|
||||
}, nil
|
||||
},
|
||||
MockStoreAdmin: func(ctx context.Context, adm *linkedca.Admin, prov provisioner.Interface) error {
|
||||
assert.Equals(t, "admin", adm.Subject)
|
||||
assert.Equals(t, "provID", prov.GetID())
|
||||
return nil
|
||||
},
|
||||
}
|
||||
return test{
|
||||
ctx: context.Background(),
|
||||
body: body,
|
||||
auth: auth,
|
||||
statusCode: 201,
|
||||
err: nil,
|
||||
adm: &linkedca.Admin{
|
||||
ProvisionerId: "provID",
|
||||
Subject: "admin",
|
||||
Type: linkedca.Admin_SUPER_ADMIN,
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
for name, prep := range tests {
|
||||
tc := prep(t)
|
||||
t.Run(name, func(t *testing.T) {
|
||||
h := &Handler{
|
||||
auth: tc.auth,
|
||||
}
|
||||
req := httptest.NewRequest("GET", "/foo", io.NopCloser(bytes.NewBuffer(tc.body)))
|
||||
req = req.WithContext(tc.ctx)
|
||||
w := httptest.NewRecorder()
|
||||
h.CreateAdmin(w, req)
|
||||
res := w.Result()
|
||||
|
||||
assert.Equals(t, tc.statusCode, res.StatusCode)
|
||||
|
||||
if res.StatusCode >= 400 {
|
||||
|
||||
body, err := io.ReadAll(res.Body)
|
||||
res.Body.Close()
|
||||
assert.FatalError(t, err)
|
||||
|
||||
adminErr := admin.Error{}
|
||||
assert.FatalError(t, json.Unmarshal(bytes.TrimSpace(body), &adminErr))
|
||||
|
||||
assert.Equals(t, tc.err.Type, adminErr.Type)
|
||||
assert.Equals(t, tc.err.Message, adminErr.Message)
|
||||
assert.Equals(t, tc.err.Detail, adminErr.Detail)
|
||||
assert.Equals(t, []string{"application/json"}, res.Header["Content-Type"])
|
||||
return
|
||||
}
|
||||
|
||||
adm := &linkedca.Admin{}
|
||||
err := readProtoJSON(res.Body, adm)
|
||||
assert.FatalError(t, err)
|
||||
|
||||
assert.Equals(t, []string{"application/json"}, res.Header["Content-Type"])
|
||||
|
||||
opts := []cmp.Option{cmpopts.IgnoreUnexported(linkedca.Admin{}, timestamppb.Timestamp{})}
|
||||
if !cmp.Equal(tc.adm, adm, opts...) {
|
||||
t.Errorf("h.CreateAdmin diff =\n%s", cmp.Diff(tc.adm, adm, opts...))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandler_DeleteAdmin(t *testing.T) {
|
||||
type test struct {
|
||||
ctx context.Context
|
||||
auth adminAuthority
|
||||
statusCode int
|
||||
err *admin.Error
|
||||
}
|
||||
var tests = map[string]func(t *testing.T) test{
|
||||
"fail/auth.RemoveAdmin": func(t *testing.T) test {
|
||||
chiCtx := chi.NewRouteContext()
|
||||
chiCtx.URLParams.Add("id", "adminID")
|
||||
ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx)
|
||||
auth := &mockAdminAuthority{
|
||||
MockRemoveAdmin: func(ctx context.Context, id string) error {
|
||||
assert.Equals(t, "adminID", id)
|
||||
return errors.New("force")
|
||||
},
|
||||
}
|
||||
return test{
|
||||
ctx: ctx,
|
||||
auth: auth,
|
||||
statusCode: 500,
|
||||
err: &admin.Error{
|
||||
Type: admin.ErrorServerInternalType.String(),
|
||||
Status: 500,
|
||||
Detail: "the server experienced an internal error",
|
||||
Message: "error deleting admin adminID: force",
|
||||
},
|
||||
}
|
||||
},
|
||||
"ok": func(t *testing.T) test {
|
||||
chiCtx := chi.NewRouteContext()
|
||||
chiCtx.URLParams.Add("id", "adminID")
|
||||
ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx)
|
||||
auth := &mockAdminAuthority{
|
||||
MockRemoveAdmin: func(ctx context.Context, id string) error {
|
||||
assert.Equals(t, "adminID", id)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
return test{
|
||||
ctx: ctx,
|
||||
auth: auth,
|
||||
statusCode: 200,
|
||||
err: nil,
|
||||
}
|
||||
},
|
||||
}
|
||||
for name, prep := range tests {
|
||||
tc := prep(t)
|
||||
t.Run(name, func(t *testing.T) {
|
||||
h := &Handler{
|
||||
auth: tc.auth,
|
||||
}
|
||||
req := httptest.NewRequest("DELETE", "/foo", nil) // chi routing is prepared in test setup
|
||||
req = req.WithContext(tc.ctx)
|
||||
w := httptest.NewRecorder()
|
||||
h.DeleteAdmin(w, req)
|
||||
res := w.Result()
|
||||
assert.Equals(t, tc.statusCode, res.StatusCode)
|
||||
|
||||
if res.StatusCode >= 400 {
|
||||
body, err := io.ReadAll(res.Body)
|
||||
res.Body.Close()
|
||||
assert.FatalError(t, err)
|
||||
|
||||
adminErr := admin.Error{}
|
||||
assert.FatalError(t, json.Unmarshal(bytes.TrimSpace(body), &adminErr))
|
||||
|
||||
assert.Equals(t, tc.err.Type, adminErr.Type)
|
||||
assert.Equals(t, tc.err.Message, adminErr.Message)
|
||||
assert.Equals(t, tc.err.StatusCode(), res.StatusCode)
|
||||
assert.Equals(t, tc.err.Detail, adminErr.Detail)
|
||||
assert.Equals(t, []string{"application/json"}, res.Header["Content-Type"])
|
||||
return
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(res.Body)
|
||||
res.Body.Close()
|
||||
assert.FatalError(t, err)
|
||||
|
||||
response := DeleteResponse{}
|
||||
assert.FatalError(t, json.Unmarshal(bytes.TrimSpace(body), &response))
|
||||
assert.Equals(t, "ok", response.Status)
|
||||
assert.Equals(t, []string{"application/json"}, res.Header["Content-Type"])
|
||||
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandler_UpdateAdmin(t *testing.T) {
|
||||
type test struct {
|
||||
ctx context.Context
|
||||
auth adminAuthority
|
||||
body []byte
|
||||
statusCode int
|
||||
err *admin.Error
|
||||
adm *linkedca.Admin
|
||||
}
|
||||
var tests = map[string]func(t *testing.T) test{
|
||||
"fail/ReadJSON": func(t *testing.T) test {
|
||||
body := []byte("{!?}")
|
||||
return test{
|
||||
ctx: context.Background(),
|
||||
body: body,
|
||||
statusCode: 400,
|
||||
err: &admin.Error{
|
||||
Type: admin.ErrorBadRequestType.String(),
|
||||
Status: 400,
|
||||
Detail: "bad request",
|
||||
Message: "error reading request body: error decoding json: invalid character '!' looking for beginning of object key string",
|
||||
},
|
||||
}
|
||||
},
|
||||
"fail/validate": func(t *testing.T) test {
|
||||
req := UpdateAdminRequest{
|
||||
Type: -1,
|
||||
}
|
||||
body, err := json.Marshal(req)
|
||||
assert.FatalError(t, err)
|
||||
return test{
|
||||
ctx: context.Background(),
|
||||
body: body,
|
||||
statusCode: 400,
|
||||
err: &admin.Error{
|
||||
Type: admin.ErrorBadRequestType.String(),
|
||||
Status: 400,
|
||||
Detail: "bad request",
|
||||
Message: "invalid value for admin type",
|
||||
},
|
||||
}
|
||||
},
|
||||
"fail/auth.UpdateAdmin": func(t *testing.T) test {
|
||||
req := UpdateAdminRequest{
|
||||
Type: linkedca.Admin_ADMIN,
|
||||
}
|
||||
body, err := json.Marshal(req)
|
||||
assert.FatalError(t, err)
|
||||
chiCtx := chi.NewRouteContext()
|
||||
chiCtx.URLParams.Add("id", "adminID")
|
||||
ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx)
|
||||
auth := &mockAdminAuthority{
|
||||
MockUpdateAdmin: func(ctx context.Context, id string, nu *linkedca.Admin) (*linkedca.Admin, error) {
|
||||
assert.Equals(t, "adminID", id)
|
||||
assert.Equals(t, linkedca.Admin_ADMIN, nu.Type)
|
||||
return nil, errors.New("force")
|
||||
},
|
||||
}
|
||||
return test{
|
||||
ctx: ctx,
|
||||
body: body,
|
||||
auth: auth,
|
||||
statusCode: 500,
|
||||
err: &admin.Error{
|
||||
Type: admin.ErrorServerInternalType.String(),
|
||||
Status: 500,
|
||||
Detail: "the server experienced an internal error",
|
||||
Message: "error updating admin adminID: force",
|
||||
},
|
||||
}
|
||||
},
|
||||
"ok": func(t *testing.T) test {
|
||||
req := UpdateAdminRequest{
|
||||
Type: linkedca.Admin_ADMIN,
|
||||
}
|
||||
body, err := json.Marshal(req)
|
||||
assert.FatalError(t, err)
|
||||
chiCtx := chi.NewRouteContext()
|
||||
chiCtx.URLParams.Add("id", "adminID")
|
||||
ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx)
|
||||
adm := &linkedca.Admin{
|
||||
Id: "adminID",
|
||||
ProvisionerId: "provID",
|
||||
Subject: "admin",
|
||||
Type: linkedca.Admin_SUPER_ADMIN,
|
||||
}
|
||||
auth := &mockAdminAuthority{
|
||||
MockUpdateAdmin: func(ctx context.Context, id string, nu *linkedca.Admin) (*linkedca.Admin, error) {
|
||||
assert.Equals(t, "adminID", id)
|
||||
assert.Equals(t, linkedca.Admin_ADMIN, nu.Type)
|
||||
return adm, nil
|
||||
},
|
||||
}
|
||||
return test{
|
||||
ctx: ctx,
|
||||
body: body,
|
||||
auth: auth,
|
||||
statusCode: 200,
|
||||
err: nil,
|
||||
adm: adm,
|
||||
}
|
||||
},
|
||||
}
|
||||
for name, prep := range tests {
|
||||
tc := prep(t)
|
||||
t.Run(name, func(t *testing.T) {
|
||||
h := &Handler{
|
||||
auth: tc.auth,
|
||||
}
|
||||
req := httptest.NewRequest("GET", "/foo", io.NopCloser(bytes.NewBuffer(tc.body)))
|
||||
req = req.WithContext(tc.ctx)
|
||||
w := httptest.NewRecorder()
|
||||
h.UpdateAdmin(w, req)
|
||||
res := w.Result()
|
||||
|
||||
assert.Equals(t, tc.statusCode, res.StatusCode)
|
||||
|
||||
if res.StatusCode >= 400 {
|
||||
|
||||
body, err := io.ReadAll(res.Body)
|
||||
res.Body.Close()
|
||||
assert.FatalError(t, err)
|
||||
|
||||
adminErr := admin.Error{}
|
||||
assert.FatalError(t, json.Unmarshal(bytes.TrimSpace(body), &adminErr))
|
||||
|
||||
assert.Equals(t, tc.err.Type, adminErr.Type)
|
||||
assert.Equals(t, tc.err.Message, adminErr.Message)
|
||||
assert.Equals(t, tc.err.Detail, adminErr.Detail)
|
||||
assert.Equals(t, []string{"application/json"}, res.Header["Content-Type"])
|
||||
return
|
||||
}
|
||||
|
||||
adm := &linkedca.Admin{}
|
||||
err := readProtoJSON(res.Body, adm)
|
||||
assert.FatalError(t, err)
|
||||
|
||||
assert.Equals(t, []string{"application/json"}, res.Header["Content-Type"])
|
||||
|
||||
opts := []cmp.Option{cmpopts.IgnoreUnexported(linkedca.Admin{}, timestamppb.Timestamp{})}
|
||||
if !cmp.Equal(tc.adm, adm, opts...) {
|
||||
t.Errorf("h.UpdateAdmin diff =\n%s", cmp.Diff(tc.adm, adm, opts...))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -1,22 +1,25 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"github.com/smallstep/certificates/acme"
|
||||
"github.com/smallstep/certificates/api"
|
||||
"github.com/smallstep/certificates/authority"
|
||||
"github.com/smallstep/certificates/authority/admin"
|
||||
)
|
||||
|
||||
// Handler is the ACME API request handler.
|
||||
// Handler is the Admin API request handler.
|
||||
type Handler struct {
|
||||
db admin.DB
|
||||
auth *authority.Authority
|
||||
db admin.DB
|
||||
auth adminAuthority
|
||||
acmeDB acme.DB
|
||||
}
|
||||
|
||||
// NewHandler returns a new Authority Config Handler.
|
||||
func NewHandler(auth *authority.Authority) api.RouterHandler {
|
||||
h := &Handler{db: auth.GetAdminDatabase(), auth: auth}
|
||||
|
||||
return h
|
||||
func NewHandler(auth adminAuthority, adminDB admin.DB, acmeDB acme.DB) api.RouterHandler {
|
||||
return &Handler{
|
||||
db: adminDB,
|
||||
auth: auth,
|
||||
acmeDB: acmeDB,
|
||||
}
|
||||
}
|
||||
|
||||
// Route traffic and implement the Router interface.
|
||||
|
@ -25,6 +28,10 @@ func (h *Handler) Route(r api.Router) {
|
|||
return h.extractAuthorizeTokenAdmin(h.requireAPIEnabled(next))
|
||||
}
|
||||
|
||||
requireEABEnabled := func(next nextHTTP) nextHTTP {
|
||||
return h.requireEABEnabled(next)
|
||||
}
|
||||
|
||||
// Provisioners
|
||||
r.MethodFunc("GET", "/provisioners/{name}", authnz(h.GetProvisioner))
|
||||
r.MethodFunc("GET", "/provisioners", authnz(h.GetProvisioners))
|
||||
|
@ -38,4 +45,10 @@ func (h *Handler) Route(r api.Router) {
|
|||
r.MethodFunc("POST", "/admins", authnz(h.CreateAdmin))
|
||||
r.MethodFunc("PATCH", "/admins/{id}", authnz(h.UpdateAdmin))
|
||||
r.MethodFunc("DELETE", "/admins/{id}", authnz(h.DeleteAdmin))
|
||||
|
||||
// ACME External Account Binding Keys
|
||||
r.MethodFunc("GET", "/acme/eab/{provisionerName}/{reference}", authnz(requireEABEnabled(h.GetExternalAccountKeys)))
|
||||
r.MethodFunc("GET", "/acme/eab/{provisionerName}", authnz(requireEABEnabled(h.GetExternalAccountKeys)))
|
||||
r.MethodFunc("POST", "/acme/eab/{provisionerName}", authnz(requireEABEnabled(h.CreateExternalAccountKey)))
|
||||
r.MethodFunc("DELETE", "/acme/eab/{provisionerName}/{id}", authnz(requireEABEnabled(h.DeleteExternalAccountKey)))
|
||||
}
|
||||
|
|
225
authority/admin/api/middleware_test.go
Normal file
225
authority/admin/api/middleware_test.go
Normal file
|
@ -0,0 +1,225 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/google/go-cmp/cmp/cmpopts"
|
||||
"github.com/smallstep/assert"
|
||||
"github.com/smallstep/certificates/authority/admin"
|
||||
"go.step.sm/linkedca"
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
)
|
||||
|
||||
func TestHandler_requireAPIEnabled(t *testing.T) {
|
||||
type test struct {
|
||||
ctx context.Context
|
||||
auth adminAuthority
|
||||
next nextHTTP
|
||||
err *admin.Error
|
||||
statusCode int
|
||||
}
|
||||
var tests = map[string]func(t *testing.T) test{
|
||||
"fail/auth.IsAdminAPIEnabled": func(t *testing.T) test {
|
||||
return test{
|
||||
ctx: context.Background(),
|
||||
auth: &mockAdminAuthority{
|
||||
MockIsAdminAPIEnabled: func() bool {
|
||||
return false
|
||||
},
|
||||
},
|
||||
err: &admin.Error{
|
||||
Type: admin.ErrorNotImplementedType.String(),
|
||||
Status: 501,
|
||||
Detail: "not implemented",
|
||||
Message: "administration API not enabled",
|
||||
},
|
||||
statusCode: 501,
|
||||
}
|
||||
},
|
||||
"ok": func(t *testing.T) test {
|
||||
auth := &mockAdminAuthority{
|
||||
MockIsAdminAPIEnabled: func() bool {
|
||||
return true
|
||||
},
|
||||
}
|
||||
next := func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Write(nil) // mock response with status 200
|
||||
}
|
||||
return test{
|
||||
ctx: context.Background(),
|
||||
auth: auth,
|
||||
next: next,
|
||||
statusCode: 200,
|
||||
}
|
||||
},
|
||||
}
|
||||
for name, prep := range tests {
|
||||
tc := prep(t)
|
||||
t.Run(name, func(t *testing.T) {
|
||||
h := &Handler{
|
||||
auth: tc.auth,
|
||||
}
|
||||
req := httptest.NewRequest("GET", "/foo", nil) // chi routing is prepared in test setup
|
||||
req = req.WithContext(tc.ctx)
|
||||
w := httptest.NewRecorder()
|
||||
h.requireAPIEnabled(tc.next)(w, req)
|
||||
res := w.Result()
|
||||
|
||||
assert.Equals(t, tc.statusCode, res.StatusCode)
|
||||
|
||||
body, err := io.ReadAll(res.Body)
|
||||
res.Body.Close()
|
||||
assert.FatalError(t, err)
|
||||
|
||||
if res.StatusCode >= 400 {
|
||||
err := admin.Error{}
|
||||
assert.FatalError(t, json.Unmarshal(bytes.TrimSpace(body), &err))
|
||||
|
||||
assert.Equals(t, tc.err.Type, err.Type)
|
||||
assert.Equals(t, tc.err.Message, err.Message)
|
||||
assert.Equals(t, tc.err.StatusCode(), res.StatusCode)
|
||||
assert.Equals(t, tc.err.Detail, err.Detail)
|
||||
assert.Equals(t, []string{"application/json"}, res.Header["Content-Type"])
|
||||
return
|
||||
}
|
||||
|
||||
// nothing to test when the requireAPIEnabled middleware succeeds, currently
|
||||
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandler_extractAuthorizeTokenAdmin(t *testing.T) {
|
||||
type test struct {
|
||||
ctx context.Context
|
||||
auth adminAuthority
|
||||
req *http.Request
|
||||
next nextHTTP
|
||||
err *admin.Error
|
||||
statusCode int
|
||||
}
|
||||
var tests = map[string]func(t *testing.T) test{
|
||||
"fail/missing-authorization-token": func(t *testing.T) test {
|
||||
req := httptest.NewRequest("GET", "/foo", nil)
|
||||
req.Header["Authorization"] = []string{""}
|
||||
return test{
|
||||
ctx: context.Background(),
|
||||
req: req,
|
||||
statusCode: 401,
|
||||
err: &admin.Error{
|
||||
Type: admin.ErrorUnauthorizedType.String(),
|
||||
Status: 401,
|
||||
Detail: "unauthorized",
|
||||
Message: "missing authorization header token",
|
||||
},
|
||||
}
|
||||
},
|
||||
"fail/auth.AuthorizeAdminToken": func(t *testing.T) test {
|
||||
req := httptest.NewRequest("GET", "/foo", nil)
|
||||
req.Header["Authorization"] = []string{"token"}
|
||||
auth := &mockAdminAuthority{
|
||||
MockAuthorizeAdminToken: func(r *http.Request, token string) (*linkedca.Admin, error) {
|
||||
assert.Equals(t, "token", token)
|
||||
return nil, admin.NewError(
|
||||
admin.ErrorUnauthorizedType,
|
||||
"not authorized",
|
||||
)
|
||||
},
|
||||
}
|
||||
return test{
|
||||
ctx: context.Background(),
|
||||
auth: auth,
|
||||
req: req,
|
||||
statusCode: 401,
|
||||
err: &admin.Error{
|
||||
Type: admin.ErrorUnauthorizedType.String(),
|
||||
Status: 401,
|
||||
Detail: "unauthorized",
|
||||
Message: "not authorized",
|
||||
},
|
||||
}
|
||||
},
|
||||
"ok": func(t *testing.T) test {
|
||||
req := httptest.NewRequest("GET", "/foo", nil)
|
||||
req.Header["Authorization"] = []string{"token"}
|
||||
createdAt := time.Now()
|
||||
var deletedAt time.Time
|
||||
admin := &linkedca.Admin{
|
||||
Id: "adminID",
|
||||
AuthorityId: "authorityID",
|
||||
Subject: "admin",
|
||||
ProvisionerId: "provID",
|
||||
Type: linkedca.Admin_SUPER_ADMIN,
|
||||
CreatedAt: timestamppb.New(createdAt),
|
||||
DeletedAt: timestamppb.New(deletedAt),
|
||||
}
|
||||
auth := &mockAdminAuthority{
|
||||
MockAuthorizeAdminToken: func(r *http.Request, token string) (*linkedca.Admin, error) {
|
||||
assert.Equals(t, "token", token)
|
||||
return admin, nil
|
||||
},
|
||||
}
|
||||
next := func(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
a := ctx.Value(adminContextKey) // verifying that the context now has a linkedca.Admin
|
||||
adm, ok := a.(*linkedca.Admin)
|
||||
if !ok {
|
||||
t.Errorf("expected *linkedca.Admin; got %T", a)
|
||||
return
|
||||
}
|
||||
opts := []cmp.Option{cmpopts.IgnoreUnexported(linkedca.Admin{}, timestamppb.Timestamp{})}
|
||||
if !cmp.Equal(admin, adm, opts...) {
|
||||
t.Errorf("linkedca.Admin diff =\n%s", cmp.Diff(admin, adm, opts...))
|
||||
}
|
||||
w.Write(nil) // mock response with status 200
|
||||
}
|
||||
return test{
|
||||
ctx: context.Background(),
|
||||
auth: auth,
|
||||
req: req,
|
||||
next: next,
|
||||
statusCode: 200,
|
||||
err: nil,
|
||||
}
|
||||
},
|
||||
}
|
||||
for name, prep := range tests {
|
||||
tc := prep(t)
|
||||
t.Run(name, func(t *testing.T) {
|
||||
h := &Handler{
|
||||
auth: tc.auth,
|
||||
}
|
||||
|
||||
req := tc.req.WithContext(tc.ctx)
|
||||
w := httptest.NewRecorder()
|
||||
h.extractAuthorizeTokenAdmin(tc.next)(w, req)
|
||||
res := w.Result()
|
||||
|
||||
assert.Equals(t, tc.statusCode, res.StatusCode)
|
||||
|
||||
body, err := io.ReadAll(res.Body)
|
||||
res.Body.Close()
|
||||
assert.FatalError(t, err)
|
||||
|
||||
if res.StatusCode >= 400 {
|
||||
err := admin.Error{}
|
||||
assert.FatalError(t, json.Unmarshal(bytes.TrimSpace(body), &err))
|
||||
|
||||
assert.Equals(t, tc.err.Type, err.Type)
|
||||
assert.Equals(t, tc.err.Message, err.Message)
|
||||
assert.Equals(t, tc.err.StatusCode(), res.StatusCode)
|
||||
assert.Equals(t, tc.err.Detail, err.Detail)
|
||||
assert.Equals(t, []string{"application/json"}, res.Header["Content-Type"])
|
||||
return
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -54,7 +54,7 @@ func (h *Handler) GetProvisioners(w http.ResponseWriter, r *http.Request) {
|
|||
cursor, limit, err := api.ParseCursor(r)
|
||||
if err != nil {
|
||||
api.WriteError(w, admin.WrapError(admin.ErrorBadRequestType, err,
|
||||
"error parsing cursor & limit query params"))
|
||||
"error parsing cursor and limit from query params"))
|
||||
return
|
||||
}
|
||||
|
||||
|
|
1100
authority/admin/api/provisioner_test.go
Normal file
1100
authority/admin/api/provisioner_test.go
Normal file
File diff suppressed because it is too large
Load diff
|
@ -11,7 +11,7 @@ import (
|
|||
"github.com/smallstep/certificates/authority/admin"
|
||||
"github.com/smallstep/certificates/db"
|
||||
"github.com/smallstep/nosql"
|
||||
"github.com/smallstep/nosql/database"
|
||||
nosqldb "github.com/smallstep/nosql/database"
|
||||
"go.step.sm/linkedca"
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
)
|
||||
|
@ -31,7 +31,7 @@ func TestDB_getDBAdminBytes(t *testing.T) {
|
|||
assert.Equals(t, bucket, adminsTable)
|
||||
assert.Equals(t, string(key), adminID)
|
||||
|
||||
return nil, database.ErrNotFound
|
||||
return nil, nosqldb.ErrNotFound
|
||||
},
|
||||
},
|
||||
adminErr: admin.NewError(admin.ErrorNotFoundType, "admin adminID not found"),
|
||||
|
@ -105,7 +105,7 @@ func TestDB_getDBAdmin(t *testing.T) {
|
|||
assert.Equals(t, bucket, adminsTable)
|
||||
assert.Equals(t, string(key), adminID)
|
||||
|
||||
return nil, database.ErrNotFound
|
||||
return nil, nosqldb.ErrNotFound
|
||||
},
|
||||
},
|
||||
adminErr: admin.NewError(admin.ErrorNotFoundType, "admin adminID not found"),
|
||||
|
@ -398,7 +398,7 @@ func TestDB_GetAdmin(t *testing.T) {
|
|||
assert.Equals(t, bucket, adminsTable)
|
||||
assert.Equals(t, string(key), adminID)
|
||||
|
||||
return nil, database.ErrNotFound
|
||||
return nil, nosqldb.ErrNotFound
|
||||
},
|
||||
},
|
||||
adminErr: admin.NewError(admin.ErrorNotFoundType, "admin adminID not found"),
|
||||
|
@ -551,7 +551,7 @@ func TestDB_DeleteAdmin(t *testing.T) {
|
|||
assert.Equals(t, bucket, adminsTable)
|
||||
assert.Equals(t, string(key), adminID)
|
||||
|
||||
return nil, database.ErrNotFound
|
||||
return nil, nosqldb.ErrNotFound
|
||||
},
|
||||
},
|
||||
adminErr: admin.NewError(admin.ErrorNotFoundType, "admin adminID not found"),
|
||||
|
@ -697,7 +697,7 @@ func TestDB_UpdateAdmin(t *testing.T) {
|
|||
assert.Equals(t, bucket, adminsTable)
|
||||
assert.Equals(t, string(key), adminID)
|
||||
|
||||
return nil, database.ErrNotFound
|
||||
return nil, nosqldb.ErrNotFound
|
||||
},
|
||||
},
|
||||
adminErr: admin.NewError(admin.ErrorNotFoundType, "admin adminID not found"),
|
||||
|
@ -985,7 +985,7 @@ func TestDB_GetAdmins(t *testing.T) {
|
|||
"fail/db.List-error": func(t *testing.T) test {
|
||||
return test{
|
||||
db: &db.MockNoSQLDB{
|
||||
MList: func(bucket []byte) ([]*database.Entry, error) {
|
||||
MList: func(bucket []byte) ([]*nosqldb.Entry, error) {
|
||||
assert.Equals(t, bucket, adminsTable)
|
||||
|
||||
return nil, errors.New("force")
|
||||
|
@ -995,14 +995,14 @@ func TestDB_GetAdmins(t *testing.T) {
|
|||
}
|
||||
},
|
||||
"fail/unmarshal-error": func(t *testing.T) test {
|
||||
ret := []*database.Entry{
|
||||
ret := []*nosqldb.Entry{
|
||||
{Bucket: adminsTable, Key: []byte("foo"), Value: foob},
|
||||
{Bucket: adminsTable, Key: []byte("bar"), Value: barb},
|
||||
{Bucket: adminsTable, Key: []byte("zap"), Value: []byte("zap")},
|
||||
}
|
||||
return test{
|
||||
db: &db.MockNoSQLDB{
|
||||
MList: func(bucket []byte) ([]*database.Entry, error) {
|
||||
MList: func(bucket []byte) ([]*nosqldb.Entry, error) {
|
||||
assert.Equals(t, bucket, adminsTable)
|
||||
|
||||
return ret, nil
|
||||
|
@ -1012,10 +1012,10 @@ func TestDB_GetAdmins(t *testing.T) {
|
|||
}
|
||||
},
|
||||
"ok/none": func(t *testing.T) test {
|
||||
ret := []*database.Entry{}
|
||||
ret := []*nosqldb.Entry{}
|
||||
return test{
|
||||
db: &db.MockNoSQLDB{
|
||||
MList: func(bucket []byte) ([]*database.Entry, error) {
|
||||
MList: func(bucket []byte) ([]*nosqldb.Entry, error) {
|
||||
assert.Equals(t, bucket, adminsTable)
|
||||
|
||||
return ret, nil
|
||||
|
@ -1027,13 +1027,13 @@ func TestDB_GetAdmins(t *testing.T) {
|
|||
}
|
||||
},
|
||||
"ok/only-invalid": func(t *testing.T) test {
|
||||
ret := []*database.Entry{
|
||||
ret := []*nosqldb.Entry{
|
||||
{Bucket: adminsTable, Key: []byte("bar"), Value: barb},
|
||||
{Bucket: adminsTable, Key: []byte("baz"), Value: bazb},
|
||||
}
|
||||
return test{
|
||||
db: &db.MockNoSQLDB{
|
||||
MList: func(bucket []byte) ([]*database.Entry, error) {
|
||||
MList: func(bucket []byte) ([]*nosqldb.Entry, error) {
|
||||
assert.Equals(t, bucket, adminsTable)
|
||||
|
||||
return ret, nil
|
||||
|
@ -1045,7 +1045,7 @@ func TestDB_GetAdmins(t *testing.T) {
|
|||
}
|
||||
},
|
||||
"ok": func(t *testing.T) test {
|
||||
ret := []*database.Entry{
|
||||
ret := []*nosqldb.Entry{
|
||||
{Bucket: adminsTable, Key: []byte("foo"), Value: foob},
|
||||
{Bucket: adminsTable, Key: []byte("bar"), Value: barb},
|
||||
{Bucket: adminsTable, Key: []byte("baz"), Value: bazb},
|
||||
|
@ -1053,7 +1053,7 @@ func TestDB_GetAdmins(t *testing.T) {
|
|||
}
|
||||
return test{
|
||||
db: &db.MockNoSQLDB{
|
||||
MList: func(bucket []byte) ([]*database.Entry, error) {
|
||||
MList: func(bucket []byte) ([]*nosqldb.Entry, error) {
|
||||
assert.Equals(t, bucket, adminsTable)
|
||||
|
||||
return ret, nil
|
||||
|
|
|
@ -11,7 +11,7 @@ import (
|
|||
"github.com/smallstep/certificates/authority/admin"
|
||||
"github.com/smallstep/certificates/db"
|
||||
"github.com/smallstep/nosql"
|
||||
"github.com/smallstep/nosql/database"
|
||||
nosqldb "github.com/smallstep/nosql/database"
|
||||
"go.step.sm/linkedca"
|
||||
)
|
||||
|
||||
|
@ -30,7 +30,7 @@ func TestDB_getDBProvisionerBytes(t *testing.T) {
|
|||
assert.Equals(t, bucket, provisionersTable)
|
||||
assert.Equals(t, string(key), provID)
|
||||
|
||||
return nil, database.ErrNotFound
|
||||
return nil, nosqldb.ErrNotFound
|
||||
},
|
||||
},
|
||||
adminErr: admin.NewError(admin.ErrorNotFoundType, "provisioner provID not found"),
|
||||
|
@ -104,7 +104,7 @@ func TestDB_getDBProvisioner(t *testing.T) {
|
|||
assert.Equals(t, bucket, provisionersTable)
|
||||
assert.Equals(t, string(key), provID)
|
||||
|
||||
return nil, database.ErrNotFound
|
||||
return nil, nosqldb.ErrNotFound
|
||||
},
|
||||
},
|
||||
adminErr: admin.NewError(admin.ErrorNotFoundType, "provisioner provID not found"),
|
||||
|
@ -444,7 +444,7 @@ func TestDB_GetProvisioner(t *testing.T) {
|
|||
assert.Equals(t, bucket, provisionersTable)
|
||||
assert.Equals(t, string(key), provID)
|
||||
|
||||
return nil, database.ErrNotFound
|
||||
return nil, nosqldb.ErrNotFound
|
||||
},
|
||||
},
|
||||
adminErr: admin.NewError(admin.ErrorNotFoundType, "provisioner provID not found"),
|
||||
|
@ -581,7 +581,7 @@ func TestDB_DeleteProvisioner(t *testing.T) {
|
|||
assert.Equals(t, bucket, provisionersTable)
|
||||
assert.Equals(t, string(key), provID)
|
||||
|
||||
return nil, database.ErrNotFound
|
||||
return nil, nosqldb.ErrNotFound
|
||||
},
|
||||
},
|
||||
adminErr: admin.NewError(admin.ErrorNotFoundType, "provisioner provID not found"),
|
||||
|
@ -735,7 +735,7 @@ func TestDB_GetProvisioners(t *testing.T) {
|
|||
"fail/db.List-error": func(t *testing.T) test {
|
||||
return test{
|
||||
db: &db.MockNoSQLDB{
|
||||
MList: func(bucket []byte) ([]*database.Entry, error) {
|
||||
MList: func(bucket []byte) ([]*nosqldb.Entry, error) {
|
||||
assert.Equals(t, bucket, provisionersTable)
|
||||
|
||||
return nil, errors.New("force")
|
||||
|
@ -745,14 +745,14 @@ func TestDB_GetProvisioners(t *testing.T) {
|
|||
}
|
||||
},
|
||||
"fail/unmarshal-error": func(t *testing.T) test {
|
||||
ret := []*database.Entry{
|
||||
ret := []*nosqldb.Entry{
|
||||
{Bucket: provisionersTable, Key: []byte("foo"), Value: foob},
|
||||
{Bucket: provisionersTable, Key: []byte("bar"), Value: barb},
|
||||
{Bucket: provisionersTable, Key: []byte("zap"), Value: []byte("zap")},
|
||||
}
|
||||
return test{
|
||||
db: &db.MockNoSQLDB{
|
||||
MList: func(bucket []byte) ([]*database.Entry, error) {
|
||||
MList: func(bucket []byte) ([]*nosqldb.Entry, error) {
|
||||
assert.Equals(t, bucket, provisionersTable)
|
||||
|
||||
return ret, nil
|
||||
|
@ -762,10 +762,10 @@ func TestDB_GetProvisioners(t *testing.T) {
|
|||
}
|
||||
},
|
||||
"ok/none": func(t *testing.T) test {
|
||||
ret := []*database.Entry{}
|
||||
ret := []*nosqldb.Entry{}
|
||||
return test{
|
||||
db: &db.MockNoSQLDB{
|
||||
MList: func(bucket []byte) ([]*database.Entry, error) {
|
||||
MList: func(bucket []byte) ([]*nosqldb.Entry, error) {
|
||||
assert.Equals(t, bucket, provisionersTable)
|
||||
|
||||
return ret, nil
|
||||
|
@ -777,13 +777,13 @@ func TestDB_GetProvisioners(t *testing.T) {
|
|||
}
|
||||
},
|
||||
"ok/only-invalid": func(t *testing.T) test {
|
||||
ret := []*database.Entry{
|
||||
ret := []*nosqldb.Entry{
|
||||
{Bucket: provisionersTable, Key: []byte("bar"), Value: barb},
|
||||
{Bucket: provisionersTable, Key: []byte("baz"), Value: bazb},
|
||||
}
|
||||
return test{
|
||||
db: &db.MockNoSQLDB{
|
||||
MList: func(bucket []byte) ([]*database.Entry, error) {
|
||||
MList: func(bucket []byte) ([]*nosqldb.Entry, error) {
|
||||
assert.Equals(t, bucket, provisionersTable)
|
||||
|
||||
return ret, nil
|
||||
|
@ -795,7 +795,7 @@ func TestDB_GetProvisioners(t *testing.T) {
|
|||
}
|
||||
},
|
||||
"ok": func(t *testing.T) test {
|
||||
ret := []*database.Entry{
|
||||
ret := []*nosqldb.Entry{
|
||||
{Bucket: provisionersTable, Key: []byte("foo"), Value: foob},
|
||||
{Bucket: provisionersTable, Key: []byte("bar"), Value: barb},
|
||||
{Bucket: provisionersTable, Key: []byte("baz"), Value: bazb},
|
||||
|
@ -803,7 +803,7 @@ func TestDB_GetProvisioners(t *testing.T) {
|
|||
}
|
||||
return test{
|
||||
db: &db.MockNoSQLDB{
|
||||
MList: func(bucket []byte) ([]*database.Entry, error) {
|
||||
MList: func(bucket []byte) ([]*nosqldb.Entry, error) {
|
||||
assert.Equals(t, bucket, provisionersTable)
|
||||
|
||||
return ret, nil
|
||||
|
@ -988,7 +988,7 @@ func TestDB_UpdateProvisioner(t *testing.T) {
|
|||
assert.Equals(t, bucket, provisionersTable)
|
||||
assert.Equals(t, string(key), provID)
|
||||
|
||||
return nil, database.ErrNotFound
|
||||
return nil, nosqldb.ErrNotFound
|
||||
},
|
||||
},
|
||||
adminErr: admin.NewError(admin.ErrorNotFoundType, "provisioner provID not found"),
|
||||
|
|
|
@ -104,7 +104,7 @@ var (
|
|||
}
|
||||
)
|
||||
|
||||
// Error represents an Admin
|
||||
// Error represents an Admin error
|
||||
type Error struct {
|
||||
Type string `json:"type"`
|
||||
Detail string `json:"detail"`
|
||||
|
|
|
@ -437,13 +437,6 @@ func (a *Authority) init() error {
|
|||
}
|
||||
}
|
||||
|
||||
// Check if a KMS with decryption capability is required and available
|
||||
if a.requiresDecrypter() {
|
||||
if _, ok := a.keyManager.(kmsapi.Decrypter); !ok {
|
||||
return errors.New("keymanager doesn't provide crypto.Decrypter")
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: decide if this is a good approach for providing the SCEP functionality
|
||||
// It currently mirrors the logic for the x509CAService
|
||||
if a.requiresSCEPService() && a.scepService == nil {
|
||||
|
@ -454,6 +447,7 @@ func (a *Authority) init() error {
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
options.CertificateChain = append(options.CertificateChain, a.rootX509Certs...)
|
||||
options.Signer, err = a.keyManager.CreateSigner(&kmsapi.CreateSignerRequest{
|
||||
SigningKey: a.config.IntermediateKey,
|
||||
Password: []byte(a.password),
|
||||
|
|
|
@ -6,7 +6,6 @@ import (
|
|||
"crypto/sha256"
|
||||
"crypto/x509"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"reflect"
|
||||
|
@ -328,7 +327,6 @@ func TestAuthority_CloseForReload(t *testing.T) {
|
|||
}
|
||||
|
||||
func testScepAuthority(t *testing.T, opts ...Option) *Authority {
|
||||
|
||||
p := provisioner.List{
|
||||
&provisioner.SCEP{
|
||||
Name: "scep1",
|
||||
|
@ -353,39 +351,15 @@ func testScepAuthority(t *testing.T, opts ...Option) *Authority {
|
|||
}
|
||||
|
||||
func TestAuthority_GetSCEPService(t *testing.T) {
|
||||
auth := testScepAuthority(t)
|
||||
fmt.Println(auth)
|
||||
|
||||
_ = testScepAuthority(t)
|
||||
p := provisioner.List{
|
||||
&provisioner.SCEP{
|
||||
Name: "scep1",
|
||||
Type: "SCEP",
|
||||
},
|
||||
}
|
||||
|
||||
type fields struct {
|
||||
config *Config
|
||||
// keyManager kms.KeyManager
|
||||
// provisioners *provisioner.Collection
|
||||
// db db.AuthDB
|
||||
// templates *templates.Templates
|
||||
// x509CAService cas.CertificateAuthorityService
|
||||
// rootX509Certs []*x509.Certificate
|
||||
// federatedX509Certs []*x509.Certificate
|
||||
// certificates *sync.Map
|
||||
// scepService *scep.Service
|
||||
// sshCAUserCertSignKey ssh.Signer
|
||||
// sshCAHostCertSignKey ssh.Signer
|
||||
// sshCAUserCerts []ssh.PublicKey
|
||||
// sshCAHostCerts []ssh.PublicKey
|
||||
// sshCAUserFederatedCerts []ssh.PublicKey
|
||||
// sshCAHostFederatedCerts []ssh.PublicKey
|
||||
// initOnce bool
|
||||
// startTime time.Time
|
||||
// sshBastionFunc func(ctx context.Context, user, hostname string) (*Bastion, error)
|
||||
// sshCheckHostFunc func(ctx context.Context, principal string, tok string, roots []*x509.Certificate) (bool, error)
|
||||
// sshGetHostsFunc func(ctx context.Context, cert *x509.Certificate) ([]Host, error)
|
||||
// getIdentityFunc provisioner.GetIdentityFunc
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
|
@ -434,30 +408,6 @@ func TestAuthority_GetSCEPService(t *testing.T) {
|
|||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// a := &Authority{
|
||||
// config: tt.fields.config,
|
||||
// keyManager: tt.fields.keyManager,
|
||||
// provisioners: tt.fields.provisioners,
|
||||
// db: tt.fields.db,
|
||||
// templates: tt.fields.templates,
|
||||
// x509CAService: tt.fields.x509CAService,
|
||||
// rootX509Certs: tt.fields.rootX509Certs,
|
||||
// federatedX509Certs: tt.fields.federatedX509Certs,
|
||||
// certificates: tt.fields.certificates,
|
||||
// scepService: tt.fields.scepService,
|
||||
// sshCAUserCertSignKey: tt.fields.sshCAUserCertSignKey,
|
||||
// sshCAHostCertSignKey: tt.fields.sshCAHostCertSignKey,
|
||||
// sshCAUserCerts: tt.fields.sshCAUserCerts,
|
||||
// sshCAHostCerts: tt.fields.sshCAHostCerts,
|
||||
// sshCAUserFederatedCerts: tt.fields.sshCAUserFederatedCerts,
|
||||
// sshCAHostFederatedCerts: tt.fields.sshCAHostFederatedCerts,
|
||||
// initOnce: tt.fields.initOnce,
|
||||
// startTime: tt.fields.startTime,
|
||||
// sshBastionFunc: tt.fields.sshBastionFunc,
|
||||
// sshCheckHostFunc: tt.fields.sshCheckHostFunc,
|
||||
// sshGetHostsFunc: tt.fields.sshGetHostsFunc,
|
||||
// getIdentityFunc: tt.fields.getIdentityFunc,
|
||||
// }
|
||||
a, err := New(tt.fields.config)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("Authority.New(), error = %v, wantErr %v", err, tt.wantErr)
|
||||
|
|
|
@ -13,13 +13,18 @@ import (
|
|||
// provisioning flow.
|
||||
type ACME struct {
|
||||
*base
|
||||
ID string `json:"-"`
|
||||
Type string `json:"type"`
|
||||
Name string `json:"name"`
|
||||
ForceCN bool `json:"forceCN,omitempty"`
|
||||
Claims *Claims `json:"claims,omitempty"`
|
||||
Options *Options `json:"options,omitempty"`
|
||||
claimer *Claimer
|
||||
ID string `json:"-"`
|
||||
Type string `json:"type"`
|
||||
Name string `json:"name"`
|
||||
ForceCN bool `json:"forceCN,omitempty"`
|
||||
// RequireEAB makes the provisioner require ACME EAB to be provided
|
||||
// by clients when creating a new Account. If set to true, the provided
|
||||
// EAB will be verified. If set to false and an EAB is provided, it is
|
||||
// not verified. Defaults to false.
|
||||
RequireEAB bool `json:"requireEAB,omitempty"`
|
||||
Claims *Claims `json:"claims,omitempty"`
|
||||
Options *Options `json:"options,omitempty"`
|
||||
claimer *Claimer
|
||||
}
|
||||
|
||||
// GetID returns the provisioner unique identifier.
|
||||
|
|
|
@ -18,13 +18,21 @@ type SCEP struct {
|
|||
ForceCN bool `json:"forceCN,omitempty"`
|
||||
ChallengePassword string `json:"challenge,omitempty"`
|
||||
Capabilities []string `json:"capabilities,omitempty"`
|
||||
// IncludeRoot makes the provisioner return the CA root in addition to the
|
||||
// intermediate in the GetCACerts response
|
||||
IncludeRoot bool `json:"includeRoot,omitempty"`
|
||||
// MinimumPublicKeyLength is the minimum length for public keys in CSRs
|
||||
MinimumPublicKeyLength int `json:"minimumPublicKeyLength,omitempty"`
|
||||
Options *Options `json:"options,omitempty"`
|
||||
Claims *Claims `json:"claims,omitempty"`
|
||||
claimer *Claimer
|
||||
MinimumPublicKeyLength int `json:"minimumPublicKeyLength,omitempty"`
|
||||
// Numerical identifier for the ContentEncryptionAlgorithm as defined in github.com/mozilla-services/pkcs7
|
||||
// at https://github.com/mozilla-services/pkcs7/blob/33d05740a3526e382af6395d3513e73d4e66d1cb/encrypt.go#L63
|
||||
// Defaults to 0, being DES-CBC
|
||||
EncryptionAlgorithmIdentifier int `json:"encryptionAlgorithmIdentifier,omitempty"`
|
||||
Options *Options `json:"options,omitempty"`
|
||||
Claims *Claims `json:"claims,omitempty"`
|
||||
claimer *Claimer
|
||||
|
||||
secretChallengePassword string
|
||||
encryptionAlgorithm int
|
||||
}
|
||||
|
||||
// GetID returns the provisioner unique identifier.
|
||||
|
@ -97,7 +105,12 @@ func (s *SCEP) Init(config Config) (err error) {
|
|||
}
|
||||
|
||||
if s.MinimumPublicKeyLength%8 != 0 {
|
||||
return errors.Errorf("only minimum public keys exactly divisible by 8 are supported; %d is not exactly divisible by 8", s.MinimumPublicKeyLength)
|
||||
return errors.Errorf("%d bits is not exactly divisible by 8", s.MinimumPublicKeyLength)
|
||||
}
|
||||
|
||||
s.encryptionAlgorithm = s.EncryptionAlgorithmIdentifier // TODO(hs): we might want to upgrade the default security to AES-CBC?
|
||||
if s.encryptionAlgorithm < 0 || s.encryptionAlgorithm > 4 {
|
||||
return errors.New("only encryption algorithm identifiers from 0 to 4 are valid")
|
||||
}
|
||||
|
||||
// TODO: add other, SCEP specific, options?
|
||||
|
@ -129,3 +142,17 @@ func (s *SCEP) GetChallengePassword() string {
|
|||
func (s *SCEP) GetCapabilities() []string {
|
||||
return s.Capabilities
|
||||
}
|
||||
|
||||
// ShouldIncludeRootInChain indicates if the CA should
|
||||
// return its intermediate, which is currently used for
|
||||
// both signing and decryption, as well as the root in
|
||||
// its chain.
|
||||
func (s *SCEP) ShouldIncludeRootInChain() bool {
|
||||
return s.IncludeRoot
|
||||
}
|
||||
|
||||
// GetContentEncryptionAlgorithm returns the numeric identifier
|
||||
// for the pkcs7 package encryption algorithm to use.
|
||||
func (s *SCEP) GetContentEncryptionAlgorithm() int {
|
||||
return s.encryptionAlgorithm
|
||||
}
|
||||
|
|
|
@ -638,12 +638,13 @@ func ProvisionerToCertificates(p *linkedca.Provisioner) (provisioner.Interface,
|
|||
case *linkedca.ProvisionerDetails_ACME:
|
||||
cfg := d.ACME
|
||||
return &provisioner.ACME{
|
||||
ID: p.Id,
|
||||
Type: p.Type.String(),
|
||||
Name: p.Name,
|
||||
ForceCN: cfg.ForceCn,
|
||||
Claims: claims,
|
||||
Options: options,
|
||||
ID: p.Id,
|
||||
Type: p.Type.String(),
|
||||
Name: p.Name,
|
||||
ForceCN: cfg.ForceCn,
|
||||
RequireEAB: cfg.RequireEab,
|
||||
Claims: claims,
|
||||
Options: options,
|
||||
}, nil
|
||||
case *linkedca.ProvisionerDetails_OIDC:
|
||||
cfg := d.OIDC
|
||||
|
@ -711,6 +712,21 @@ func ProvisionerToCertificates(p *linkedca.Provisioner) (provisioner.Interface,
|
|||
Claims: claims,
|
||||
Options: options,
|
||||
}, nil
|
||||
case *linkedca.ProvisionerDetails_SCEP:
|
||||
cfg := d.SCEP
|
||||
return &provisioner.SCEP{
|
||||
ID: p.Id,
|
||||
Type: p.Type.String(),
|
||||
Name: p.Name,
|
||||
ForceCN: cfg.ForceCn,
|
||||
ChallengePassword: cfg.Challenge,
|
||||
Capabilities: cfg.Capabilities,
|
||||
IncludeRoot: cfg.IncludeRoot,
|
||||
MinimumPublicKeyLength: int(cfg.MinimumPublicKeyLength),
|
||||
EncryptionAlgorithmIdentifier: int(cfg.EncryptionAlgorithmIdentifier),
|
||||
Claims: claims,
|
||||
Options: options,
|
||||
}, nil
|
||||
case *linkedca.ProvisionerDetails_Nebula:
|
||||
var roots []byte
|
||||
for i, root := range d.Nebula.GetRoots() {
|
||||
|
@ -943,10 +959,12 @@ func ProvisionerToLinkedca(p provisioner.Interface) (*linkedca.Provisioner, erro
|
|||
Details: &linkedca.ProvisionerDetails{
|
||||
Data: &linkedca.ProvisionerDetails_SCEP{
|
||||
SCEP: &linkedca.SCEPProvisioner{
|
||||
ForceCn: p.ForceCN,
|
||||
Challenge: p.GetChallengePassword(),
|
||||
Capabilities: p.Capabilities,
|
||||
MinimumPublicKeyLength: int32(p.MinimumPublicKeyLength),
|
||||
ForceCn: p.ForceCN,
|
||||
Challenge: p.GetChallengePassword(),
|
||||
Capabilities: p.Capabilities,
|
||||
MinimumPublicKeyLength: int32(p.MinimumPublicKeyLength),
|
||||
IncludeRoot: p.IncludeRoot,
|
||||
EncryptionAlgorithmIdentifier: int32(p.EncryptionAlgorithmIdentifier),
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
@ -560,7 +560,115 @@ retry:
|
|||
return nil
|
||||
}
|
||||
|
||||
// GetExternalAccountKeysPaginate returns a page from the GET /admin/acme/eab request to the CA.
|
||||
func (c *AdminClient) GetExternalAccountKeysPaginate(provisionerName, reference string, opts ...AdminOption) (*adminAPI.GetExternalAccountKeysResponse, error) {
|
||||
var retried bool
|
||||
o := new(adminOptions)
|
||||
if err := o.apply(opts); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
p := path.Join(adminURLPrefix, "acme/eab", provisionerName)
|
||||
if reference != "" {
|
||||
p = path.Join(p, "/", reference)
|
||||
}
|
||||
u := c.endpoint.ResolveReference(&url.URL{
|
||||
Path: p,
|
||||
RawQuery: o.rawQuery(),
|
||||
})
|
||||
tok, err := c.generateAdminToken(u.Path)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "error generating admin token")
|
||||
}
|
||||
req, err := http.NewRequest("GET", u.String(), http.NoBody)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "create GET %s request failed", u)
|
||||
}
|
||||
req.Header.Add("Authorization", tok)
|
||||
retry:
|
||||
resp, err := c.client.Do(req)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "client GET %s failed", u)
|
||||
}
|
||||
if resp.StatusCode >= 400 {
|
||||
if !retried && c.retryOnError(resp) {
|
||||
retried = true
|
||||
goto retry
|
||||
}
|
||||
return nil, readAdminError(resp.Body)
|
||||
}
|
||||
var body = new(adminAPI.GetExternalAccountKeysResponse)
|
||||
if err := readJSON(resp.Body, body); err != nil {
|
||||
return nil, errors.Wrapf(err, "error reading %s", u)
|
||||
}
|
||||
return body, nil
|
||||
}
|
||||
|
||||
// CreateExternalAccountKey performs the POST /admin/acme/eab request to the CA.
|
||||
func (c *AdminClient) CreateExternalAccountKey(provisionerName string, eakRequest *adminAPI.CreateExternalAccountKeyRequest) (*linkedca.EABKey, error) {
|
||||
var retried bool
|
||||
body, err := json.Marshal(eakRequest)
|
||||
if err != nil {
|
||||
return nil, errs.Wrap(http.StatusInternalServerError, err, "error marshaling request")
|
||||
}
|
||||
u := c.endpoint.ResolveReference(&url.URL{Path: path.Join(adminURLPrefix, "acme/eab/", provisionerName)})
|
||||
tok, err := c.generateAdminToken(u.Path)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "error generating admin token")
|
||||
}
|
||||
req, err := http.NewRequest("POST", u.String(), bytes.NewReader(body))
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "create POST %s request failed", u)
|
||||
}
|
||||
req.Header.Add("Authorization", tok)
|
||||
retry:
|
||||
resp, err := c.client.Do(req)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "client POST %s failed", u)
|
||||
}
|
||||
if resp.StatusCode >= 400 {
|
||||
if !retried && c.retryOnError(resp) {
|
||||
retried = true
|
||||
goto retry
|
||||
}
|
||||
return nil, readAdminError(resp.Body)
|
||||
}
|
||||
var eabKey = new(linkedca.EABKey)
|
||||
if err := readProtoJSON(resp.Body, eabKey); err != nil {
|
||||
return nil, errors.Wrapf(err, "error reading %s", u)
|
||||
}
|
||||
return eabKey, nil
|
||||
}
|
||||
|
||||
// RemoveExternalAccountKey performs the DELETE /admin/acme/eab/{prov}/{key_id} request to the CA.
|
||||
func (c *AdminClient) RemoveExternalAccountKey(provisionerName, keyID string) error {
|
||||
var retried bool
|
||||
u := c.endpoint.ResolveReference(&url.URL{Path: path.Join(adminURLPrefix, "acme/eab", provisionerName, "/", keyID)})
|
||||
tok, err := c.generateAdminToken(u.Path)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "error generating admin token")
|
||||
}
|
||||
req, err := http.NewRequest("DELETE", u.String(), http.NoBody)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "create DELETE %s request failed", u)
|
||||
}
|
||||
req.Header.Add("Authorization", tok)
|
||||
retry:
|
||||
resp, err := c.client.Do(req)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "client DELETE %s failed", u)
|
||||
}
|
||||
if resp.StatusCode >= 400 {
|
||||
if !retried && c.retryOnError(resp) {
|
||||
retried = true
|
||||
goto retry
|
||||
}
|
||||
return readAdminError(resp.Body)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func readAdminError(r io.ReadCloser) error {
|
||||
// TODO: not all errors can be read (i.e. 404); seems to be a bigger issue
|
||||
defer r.Close()
|
||||
adminErr := new(admin.Error)
|
||||
if err := json.NewDecoder(r).Decode(adminErr); err != nil {
|
||||
|
|
25
ca/ca.go
25
ca/ca.go
|
@ -207,7 +207,7 @@ func (ca *CA) Init(cfg *config.Config) (*CA, error) {
|
|||
if cfg.AuthorityConfig.EnableAdmin {
|
||||
adminDB := auth.GetAdminDatabase()
|
||||
if adminDB != nil {
|
||||
adminHandler := adminAPI.NewHandler(auth)
|
||||
adminHandler := adminAPI.NewHandler(auth, adminDB, acmeDB)
|
||||
mux.Route("/admin", func(r chi.Router) {
|
||||
adminHandler.Route(r)
|
||||
})
|
||||
|
@ -417,11 +417,6 @@ func (ca *CA) getTLSConfig(auth *authority.Authority) (*tls.Config, error) {
|
|||
}
|
||||
}
|
||||
|
||||
certPool := x509.NewCertPool()
|
||||
for _, crt := range auth.GetRootCertificates() {
|
||||
certPool.AddCert(crt)
|
||||
}
|
||||
|
||||
// GetCertificate will only be called if the client supplies SNI
|
||||
// information or if tlsConfig.Certificates is empty.
|
||||
// When client requests are made using an IP address (as opposed to a domain
|
||||
|
@ -432,6 +427,24 @@ func (ca *CA) getTLSConfig(auth *authority.Authority) (*tls.Config, error) {
|
|||
tlsConfig.Certificates = []tls.Certificate{}
|
||||
tlsConfig.GetCertificate = ca.renewer.GetCertificateForCA
|
||||
|
||||
// initialize a certificate pool with root CA certificates to trust when doing mTLS.
|
||||
certPool := x509.NewCertPool()
|
||||
for _, crt := range auth.GetRootCertificates() {
|
||||
certPool.AddCert(crt)
|
||||
}
|
||||
|
||||
// adding the intermediate CA certificates to the pool will allow clients that
|
||||
// do mTLS but don't send an intermediate to successfully connect. The intermediates
|
||||
// added here are used when building a certificate chain.
|
||||
intermediates := tlsCrt.Certificate[1:]
|
||||
for _, certBytes := range intermediates {
|
||||
cert, err := x509.ParseCertificate(certBytes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
certPool.AddCert(cert)
|
||||
}
|
||||
|
||||
// Add support for mutual tls to renew certificates
|
||||
tlsConfig.ClientAuth = tls.VerifyClientCertIfGiven
|
||||
tlsConfig.ClientCAs = certPool
|
||||
|
|
|
@ -31,6 +31,7 @@ import (
|
|||
longrunningpb "google.golang.org/genproto/googleapis/longrunning"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/credentials/insecure"
|
||||
"google.golang.org/grpc/status"
|
||||
"google.golang.org/grpc/test/bufconn"
|
||||
"google.golang.org/protobuf/types/known/anypb"
|
||||
|
@ -852,7 +853,7 @@ func TestCloudCAS_CreateCertificateAuthority(t *testing.T) {
|
|||
defer srv.Stop()
|
||||
|
||||
// Create fake privateca client
|
||||
conn, err := grpc.DialContext(context.Background(), "", grpc.WithInsecure(),
|
||||
conn, err := grpc.DialContext(context.Background(), "", grpc.WithTransportCredentials(insecure.NewCredentials()),
|
||||
grpc.WithContextDialer(func(context.Context, string) (net.Conn, error) {
|
||||
return lis.Dial()
|
||||
}))
|
||||
|
|
|
@ -346,6 +346,7 @@ Below is an example of an ACME provisioner in the `ca.json`:
|
|||
"type": "ACME",
|
||||
"name": "my-acme-provisioner",
|
||||
"forceCN": true,
|
||||
"requireEAB": false,
|
||||
"claims": {
|
||||
"maxTLSCertDuration": "8h",
|
||||
"defaultTLSCertDuration": "2h",
|
||||
|
@ -361,6 +362,9 @@ Below is an example of an ACME provisioner in the `ca.json`:
|
|||
* `forceCN` (optional): force one of the SANs to become the Common Name, if a
|
||||
common name is not provided.
|
||||
|
||||
* `requireEAB` (optional): require clients to provide External Account Binding
|
||||
credentials when creating an ACME Account.
|
||||
|
||||
* `claims` (optional): overwrites the default claims set in the authority, see
|
||||
the [top](#provisioners) section for all the options.
|
||||
|
||||
|
|
18
examples/ansible/smallstep-certs/defaults/main.yml
Normal file
18
examples/ansible/smallstep-certs/defaults/main.yml
Normal file
|
@ -0,0 +1,18 @@
|
|||
|
||||
|
||||
|
||||
# Root cert for each will be saved in /etc/ssl/smallstep/ca/{{ ca_name }}/certs/root_ca.crt
|
||||
smallstep_root_certs: []
|
||||
# -
|
||||
# ca_name: your_ca
|
||||
# ca_url: "https://certs.your_ca.ca.smallstep.com"
|
||||
# ca_fingerprint: "56092...2200"
|
||||
|
||||
# Each leaf cert will be saved in /etc/ssl/smallstep/leaf/{{ cert_subject }}/{{ cert_subject }}.crt|key
|
||||
smallstep_leaf_certs: []
|
||||
# -
|
||||
# ca_name: your_ca
|
||||
# cert_subject: "{{ inventory_hostname }}"
|
||||
# provisioner_name: "admin"
|
||||
# provisioner_password: "{{ smallstep_ssh_provisioner_password }}"
|
||||
|
44
examples/ansible/smallstep-certs/tasks/main.yml
Normal file
44
examples/ansible/smallstep-certs/tasks/main.yml
Normal file
|
@ -0,0 +1,44 @@
|
|||
|
||||
- name: "Ensure provisioners directories exist"
|
||||
file:
|
||||
path: "/etc/ssl/smallstep/provisioners/{{ item.context }}/{{ item.provisioner_name }}"
|
||||
state: directory
|
||||
mode: 0600
|
||||
owner: root
|
||||
group: root
|
||||
with_items: "{{ smallstep_leaf_certs }}"
|
||||
no_log: true
|
||||
|
||||
- name: "Ensure provisioner passwords are up to date"
|
||||
copy:
|
||||
dest: "/etc/ssl/smallstep/provisioners/{{ item.context }}/{{ item.provisioner_name }}/provisioner-pass.txt"
|
||||
content: "{{ item.provisioner_password }}"
|
||||
mode: 0700
|
||||
owner: root
|
||||
group: root
|
||||
with_items: "{{ smallstep_leaf_certs }}"
|
||||
no_log: true
|
||||
|
||||
- name: "Get root certs for CAs"
|
||||
command:
|
||||
cmd: "step ca bootstrap --context {{ item.context }} --ca-url {{ item.ca_url }} --fingerprint {{ item.ca_fingerprint }}"
|
||||
with_items: "{{ smallstep_root_certs }}"
|
||||
no_log: true
|
||||
|
||||
- name: "Get leaf certs"
|
||||
command:
|
||||
cmd: "step ca certificate --context {{ item.context }} {{ item.cert_subject }} {{ item.cert_path }} {{ item.key_path }} --force --console --provisioner {{ item.provisioner_name }} --provisioner-password-file /etc/ssl/smallstep/provisioners/{{ item.context }}/{{ item.provisioner_name }}/provisioner-pass.txt"
|
||||
with_items: "{{ smallstep_leaf_certs }}"
|
||||
no_log: true
|
||||
|
||||
- name: Ensure cron to renew leaf certs is up to date
|
||||
cron:
|
||||
user: "root"
|
||||
name: "renew leaf cert {{ item.cert_subject }}"
|
||||
cron_file: smallstep
|
||||
job: "step ca renew --context {{ item.context }} {{ item.cert_path }} {{ item.key_path }} --expires-in 6h --force >> /var/log/smallstep-{{ item.cert_subject }}.log 2>&1"
|
||||
state: present
|
||||
minute: "*/30"
|
||||
with_items: "{{ smallstep_leaf_certs }}"
|
||||
when: "{{ item.cron_renew }}"
|
||||
no_log: true
|
2
examples/ansible/smallstep-install/defaults/main.yml
Normal file
2
examples/ansible/smallstep-install/defaults/main.yml
Normal file
|
@ -0,0 +1,2 @@
|
|||
smallstep_install_step_version: 0.15.3
|
||||
smallstep_install_step_ssh_version: 0.19.1-1
|
29
examples/ansible/smallstep-install/tasks/main.yml
Normal file
29
examples/ansible/smallstep-install/tasks/main.yml
Normal file
|
@ -0,0 +1,29 @@
|
|||
|
||||
# These steps automate the installation guide here:
|
||||
# https://smallstep.com/docs/sso-ssh/hosts/
|
||||
|
||||
- name: Download step binary
|
||||
get_url:
|
||||
url: "https://files.smallstep.com/step-linux-{{ smallstep_install_step_version }}"
|
||||
dest: "/usr/local/bin/step-{{ smallstep_install_step_version }}"
|
||||
mode: '0755'
|
||||
|
||||
- name: Link binaries to correct version
|
||||
file:
|
||||
src: "/usr/local/bin/step-{{ smallstep_install_step_version }}"
|
||||
dest: "{{ item }}"
|
||||
state: link
|
||||
with_items:
|
||||
- /usr/bin/step
|
||||
- /usr/local/bin/step
|
||||
|
||||
- name: Link /usr/local/bin/step to correct binary version
|
||||
file:
|
||||
src: "/usr/local/bin/step-{{ smallstep_install_step_version }}"
|
||||
dest: /usr/local/bin/step
|
||||
state: link
|
||||
|
||||
- name: Ensure step-ssh is installed
|
||||
apt:
|
||||
deb: "https://files.smallstep.com/step-ssh_{{ smallstep_install_step_ssh_version }}_amd64.deb"
|
||||
state: present
|
8
examples/ansible/smallstep-ssh/defaults/main.yml
Normal file
8
examples/ansible/smallstep-ssh/defaults/main.yml
Normal file
|
@ -0,0 +1,8 @@
|
|||
# If this host is behind a bastion this variable should contain the hostname of the bastion
|
||||
smallstep_ssh_host_behind_bastion_name: ""
|
||||
smallstep_ssh_host_is_bastion: false
|
||||
smallstep_ssh_ca_url: "https://ssh.mycompany.ca.smallstep.com"
|
||||
smallstep_ssh_ca_fingerprint: "XXXXXXXXXXXXXXX"
|
||||
|
||||
# Whether or not to reinitialize the host even if it's already been installed
|
||||
smallstep_ssh_force_reinit: true
|
41
examples/ansible/smallstep-ssh/tasks/main.yml
Normal file
41
examples/ansible/smallstep-ssh/tasks/main.yml
Normal file
|
@ -0,0 +1,41 @@
|
|||
|
||||
# These steps automate the installation guide here:
|
||||
# https://smallstep.com/docs/sso-ssh/hosts/
|
||||
|
||||
# TODO: Figure out how to make this idempotent instead of reinstalling on each run
|
||||
|
||||
- name: Bootstrap node to connect to CA
|
||||
command: "step ca bootstrap --context ssh --ca-url {{ smallstep_ssh_ca_url }} --fingerprint {{ smallstep_ssh_ca_fingerprint }} --force"
|
||||
# when: smallstep_ssh_installed.changed or smallstep_ssh_force_reinit
|
||||
|
||||
- name: Get a host SSH certificate
|
||||
command: "step ssh certificate --context ssh {{ inventory_hostname }} /etc/ssh/ssh_host_ecdsa_key.pub --host --sign --provisioner=\"Service Account\" --token=\"{{ smallstep_ssh_enrollment_token }}\" --force"
|
||||
# when: smallstep_ssh_installed.changed or smallstep_ssh_force_reinit
|
||||
|
||||
- name: Configure SSHD (will be overwriten by the sshd template in Ansible later)
|
||||
command: "step ssh config --context ssh --host --set Certificate=ssh_host_ecdsa_key-cert.pub --set Key=ssh_host_ecdsa_key"
|
||||
# when: smallstep_ssh_installed.changed or smallstep_ssh_force_reinit
|
||||
|
||||
- name: Activate SmallStep PAM/NSS modules and nohup sshd
|
||||
command: "step-ssh activate {{ inventory_hostname }}"
|
||||
# when: smallstep_ssh_installed.changed or smallstep_ssh_force_reinit
|
||||
|
||||
- name: Generate host tags list
|
||||
set_fact:
|
||||
smallstep_ssh_host_tags_string: "{{ smallstep_ssh_host_tags | to_json | regex_replace('\\:\\ ','=') | regex_replace('\\{\\\"|,\\ \\\"', ' --tag \"') | regex_replace('[\\[\\]{}]') }}"
|
||||
|
||||
- name: Generate command to register
|
||||
set_fact:
|
||||
smallstep_ssh_register_string: |
|
||||
step-ssh-ctl register
|
||||
--hostname {{ inventory_hostname }}
|
||||
{% if not smallstep_ssh_host_is_bastion %}--bastion '{{ smallstep_ssh_host_behind_bastion_name|default("") }}'{% endif %}
|
||||
{% if smallstep_ssh_host_is_bastion %}--is-bastion{% endif %}
|
||||
{{ smallstep_ssh_host_tags_string }}
|
||||
|
||||
- debug: var=smallstep_ssh_register_string
|
||||
|
||||
- name: Register host with smallstep
|
||||
command: "{{ smallstep_ssh_register_string }}"
|
||||
# when: smallstep_ssh_installed.changed or smallstep_ssh_force_reinit
|
||||
|
11
go.mod
11
go.mod
|
@ -34,13 +34,14 @@ require (
|
|||
github.com/urfave/cli v1.22.4
|
||||
go.mozilla.org/pkcs7 v0.0.0-20210826202110-33d05740a352
|
||||
go.step.sm/cli-utils v0.7.0
|
||||
go.step.sm/crypto v0.14.0
|
||||
go.step.sm/linkedca v0.9.0
|
||||
go.step.sm/crypto v0.15.0
|
||||
go.step.sm/linkedca v0.9.2
|
||||
golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3
|
||||
golang.org/x/net v0.0.0-20211216030914-fe4d6282115f
|
||||
golang.org/x/net v0.0.0-20220114011407-0dd24b26b47d
|
||||
golang.org/x/sys v0.0.0-20220114195835-da31bd327af9 // indirect
|
||||
google.golang.org/api v0.47.0
|
||||
google.golang.org/genproto v0.0.0-20210719143636-1d5a45f8e492
|
||||
google.golang.org/grpc v1.39.0
|
||||
google.golang.org/genproto v0.0.0-20220118154757-00ab72f36ad5
|
||||
google.golang.org/grpc v1.43.0
|
||||
google.golang.org/protobuf v1.27.1
|
||||
gopkg.in/square/go-jose.v2 v2.6.0
|
||||
)
|
||||
|
|
29
go.sum
29
go.sum
|
@ -148,7 +148,11 @@ github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDk
|
|||
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
|
||||
github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
|
||||
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
|
||||
github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI=
|
||||
github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
|
||||
github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
|
||||
github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
|
||||
github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
|
||||
github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8=
|
||||
github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd/go.mod h1:sE/e/2PUdi/liOCUjSTXgM1o87ZssimdTWN964YiIeI=
|
||||
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
|
||||
|
@ -196,6 +200,7 @@ github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5y
|
|||
github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
|
||||
github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
|
||||
github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ=
|
||||
github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0=
|
||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
|
||||
github.com/flynn/noise v1.0.0/go.mod h1:xbMo+0i6+IGbYdJhF31t2eR1BIU0CYc12+BNAKwUTag=
|
||||
|
@ -605,10 +610,10 @@ go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqe
|
|||
go.step.sm/cli-utils v0.7.0 h1:2GvY5Muid1yzp7YQbfCCS+gK3q7zlHjjLL5Z0DXz8ds=
|
||||
go.step.sm/cli-utils v0.7.0/go.mod h1:Ur6bqA/yl636kCUJbp30J7Unv5JJ226eW2KqXPDwF/E=
|
||||
go.step.sm/crypto v0.9.0/go.mod h1:+CYG05Mek1YDqi5WK0ERc6cOpKly2i/a5aZmU1sfGj0=
|
||||
go.step.sm/crypto v0.14.0 h1:HzSkUDwqKhODKpsTxevJz956U2xVDZ3sDdGQVwR6Ttw=
|
||||
go.step.sm/crypto v0.14.0/go.mod h1:3G0yQr5lQqfEG0CMYz8apC/qMtjLRQlzflL2AxkcN+g=
|
||||
go.step.sm/linkedca v0.9.0 h1:xKXZoRXy4B7LeGBZozq62IQ0p3v8dT33O9UOMpVtRtI=
|
||||
go.step.sm/linkedca v0.9.0/go.mod h1:5uTRjozEGSPAZal9xJqlaD38cvJcLe3o1VAFVjqcORo=
|
||||
go.step.sm/crypto v0.15.0 h1:VioBln+x3+RoejgeBhvxkLGVYdWRy6PFiAaUUN29/E0=
|
||||
go.step.sm/crypto v0.15.0/go.mod h1:3G0yQr5lQqfEG0CMYz8apC/qMtjLRQlzflL2AxkcN+g=
|
||||
go.step.sm/linkedca v0.9.2 h1:CpAkd174sLXFfrOZrbPEiTzik91QRj3+L0omsiwsiok=
|
||||
go.step.sm/linkedca v0.9.2/go.mod h1:5uTRjozEGSPAZal9xJqlaD38cvJcLe3o1VAFVjqcORo=
|
||||
go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
|
||||
go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
|
||||
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
|
||||
|
@ -717,8 +722,9 @@ golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qx
|
|||
golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20211020060615-d418f374d309/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20211216030914-fe4d6282115f h1:hEYJvxw1lSnWIl8X9ofsYMklzaDs90JI2az5YMd4fPM=
|
||||
golang.org/x/net v0.0.0-20211216030914-fe4d6282115f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20220114011407-0dd24b26b47d h1:1n1fc535VhN8SYtD4cDUyNlfpAF2ROMM9+11equK3hs=
|
||||
golang.org/x/net v0.0.0-20220114011407-0dd24b26b47d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
|
@ -813,8 +819,9 @@ golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBc
|
|||
golang.org/x/sys v0.0.0-20210915083310-ed5796bab164/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211031064116-611d5d643895/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211103235746-7861aae1554b h1:1VkfZQv42XQlA/jchYumAnv1UPo6RgF9rJFkTgZIxO4=
|
||||
golang.org/x/sys v0.0.0-20211103235746-7861aae1554b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220114195835-da31bd327af9 h1:XfKQ4OlFl8okEOr5UvAqFRVj8pY/4yfcXrddB8qAbU0=
|
||||
golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
|
@ -889,7 +896,6 @@ golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4f
|
|||
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
|
||||
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
golang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
|
@ -974,8 +980,8 @@ google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6D
|
|||
google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A=
|
||||
google.golang.org/genproto v0.0.0-20210513213006-bf773b8c8384/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A=
|
||||
google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=
|
||||
google.golang.org/genproto v0.0.0-20210719143636-1d5a45f8e492 h1:7yQQsvnwjfEahbNNEKcBHv3mR+HnB1ctGY/z1JXzx8M=
|
||||
google.golang.org/genproto v0.0.0-20210719143636-1d5a45f8e492/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48=
|
||||
google.golang.org/genproto v0.0.0-20220118154757-00ab72f36ad5 h1:zzNejm+EgrbLfDZ6lu9Uud2IVvHySPl8vQzf04laR5Q=
|
||||
google.golang.org/genproto v0.0.0-20220118154757-00ab72f36ad5/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
|
||||
google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/grpc v1.20.0/go.mod h1:chYK+tFQF0nDUGJgXMSgLCQk3phJEuONr2DCgLDdAQM=
|
||||
|
@ -1003,8 +1009,9 @@ google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAG
|
|||
google.golang.org/grpc v1.37.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=
|
||||
google.golang.org/grpc v1.37.1/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=
|
||||
google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=
|
||||
google.golang.org/grpc v1.39.0 h1:Klz8I9kdtkIN6EpHHUOMLCYhTn/2WAe5a0s1hcBkdTI=
|
||||
google.golang.org/grpc v1.39.0/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE=
|
||||
google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34=
|
||||
google.golang.org/grpc v1.43.0 h1:Eeu7bZtDZ2DpRCsLhUlcrLnvYaMK1Gz86a+hMVvELmM=
|
||||
google.golang.org/grpc v1.43.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU=
|
||||
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw=
|
||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||
|
|
|
@ -66,8 +66,8 @@ func New(scepAuth scep.Interface) api.RouterHandler {
|
|||
// Route traffic and implement the Router interface.
|
||||
func (h *Handler) Route(r api.Router) {
|
||||
getLink := h.Auth.GetLinkExplicit
|
||||
r.MethodFunc(http.MethodGet, getLink("{provisionerID}", false, nil), h.lookupProvisioner(h.Get))
|
||||
r.MethodFunc(http.MethodPost, getLink("{provisionerID}", false, nil), h.lookupProvisioner(h.Post))
|
||||
r.MethodFunc(http.MethodGet, getLink("{provisionerName}", false, nil), h.lookupProvisioner(h.Get))
|
||||
r.MethodFunc(http.MethodPost, getLink("{provisionerName}", false, nil), h.lookupProvisioner(h.Post))
|
||||
}
|
||||
|
||||
// Get handles all SCEP GET requests
|
||||
|
@ -184,14 +184,14 @@ func decodeSCEPRequest(r *http.Request) (SCEPRequest, error) {
|
|||
func (h *Handler) lookupProvisioner(next nextHTTP) nextHTTP {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
name := chi.URLParam(r, "provisionerID")
|
||||
provisionerID, err := url.PathUnescape(name)
|
||||
name := chi.URLParam(r, "provisionerName")
|
||||
provisionerName, err := url.PathUnescape(name)
|
||||
if err != nil {
|
||||
api.WriteError(w, errors.Errorf("error url unescaping provisioner id '%s'", name))
|
||||
api.WriteError(w, errors.Errorf("error url unescaping provisioner name '%s'", name))
|
||||
return
|
||||
}
|
||||
|
||||
p, err := h.Auth.LoadProvisionerByID("scep/" + provisionerID)
|
||||
p, err := h.Auth.LoadProvisionerByName(provisionerName)
|
||||
if err != nil {
|
||||
api.WriteError(w, err)
|
||||
return
|
||||
|
@ -212,7 +212,7 @@ func (h *Handler) lookupProvisioner(next nextHTTP) nextHTTP {
|
|||
// GetCACert returns the CA certificates in a SCEP response
|
||||
func (h *Handler) GetCACert(ctx context.Context) (SCEPResponse, error) {
|
||||
|
||||
certs, err := h.Auth.GetCACertificates()
|
||||
certs, err := h.Auth.GetCACertificates(ctx)
|
||||
if err != nil {
|
||||
return SCEPResponse{}, err
|
||||
}
|
||||
|
@ -289,20 +289,29 @@ func (h *Handler) PKIOperation(ctx context.Context, request SCEPRequest) (SCEPRe
|
|||
// NOTE: at this point we have sufficient information for returning nicely signed CertReps
|
||||
csr := msg.CSRReqMessage.CSR
|
||||
|
||||
if msg.MessageType == microscep.PKCSReq {
|
||||
|
||||
// NOTE: we're blocking the RenewalReq if the challenge does not match, because otherwise we don't have any authentication.
|
||||
// The macOS SCEP client performs renewals using PKCSreq. The CertNanny SCEP client will use PKCSreq with challenge too, it seems,
|
||||
// even if using the renewal flow as described in the README.md. MicroMDM SCEP client also only does PKCSreq by default, unless
|
||||
// a certificate exists; then it will use RenewalReq. Adding the challenge check here may be a small breaking change for clients.
|
||||
// We'll have to see how it works out.
|
||||
if msg.MessageType == microscep.PKCSReq || msg.MessageType == microscep.RenewalReq {
|
||||
challengeMatches, err := h.Auth.MatchChallengePassword(ctx, msg.CSRReqMessage.ChallengePassword)
|
||||
if err != nil {
|
||||
return h.createFailureResponse(ctx, csr, msg, microscep.BadRequest, errors.New("error when checking password"))
|
||||
}
|
||||
|
||||
if !challengeMatches {
|
||||
// TODO: can this be returned safely to the client? In the end, if the password was correct, that gains a bit of info too.
|
||||
return h.createFailureResponse(ctx, csr, msg, microscep.BadRequest, errors.New("wrong password provided"))
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: check if CN already exists, if renewal is allowed and if existing should be revoked; fail if not
|
||||
// TODO: authorize renewal: we can authorize renewals with the challenge password (if reusable secrets are used).
|
||||
// Renewals OPTIONALLY include the challenge if the existing cert is used as authentication, but client SHOULD omit the challenge.
|
||||
// This means that for renewal requests we should check the certificate provided to be signed before by the CA. We could
|
||||
// enforce use of the challenge if we want too. That way we could be more flexible in terms of authentication scheme (i.e. reusing
|
||||
// tokens from other provisioners, calling a webhook, storing multiple secrets, allowing them to be multi-use, etc).
|
||||
// Authentication by the (self-signed) certificate with an optional challenge is required; supporting renewals incl. verification
|
||||
// of the client cert is not.
|
||||
|
||||
certRep, err := h.Auth.SignCSR(ctx, csr, msg)
|
||||
if err != nil {
|
||||
|
|
|
@ -20,10 +20,10 @@ import (
|
|||
|
||||
// Interface is the SCEP authority interface.
|
||||
type Interface interface {
|
||||
LoadProvisionerByID(string) (provisioner.Interface, error)
|
||||
LoadProvisionerByName(string) (provisioner.Interface, error)
|
||||
GetLinkExplicit(provName string, absoluteLink bool, baseURL *url.URL, inputs ...string) string
|
||||
|
||||
GetCACertificates() ([]*x509.Certificate, error)
|
||||
GetCACertificates(ctx context.Context) ([]*x509.Certificate, error)
|
||||
DecryptPKIEnvelope(ctx context.Context, msg *PKIMessage) error
|
||||
SignCSR(ctx context.Context, csr *x509.CertificateRequest, msg *PKIMessage) (*PKIMessage, error)
|
||||
CreateFailureResponse(ctx context.Context, csr *x509.CertificateRequest, msg *PKIMessage, info FailInfoName, infoText string) (*PKIMessage, error)
|
||||
|
@ -36,6 +36,7 @@ type Authority struct {
|
|||
prefix string
|
||||
dns string
|
||||
intermediateCertificate *x509.Certificate
|
||||
caCerts []*x509.Certificate // TODO(hs): change to use these instead of root and intermediate
|
||||
service *Service
|
||||
signAuth SignAuthority
|
||||
}
|
||||
|
@ -56,7 +57,7 @@ type AuthorityOptions struct {
|
|||
// SignAuthority is the interface for a signing authority
|
||||
type SignAuthority interface {
|
||||
Sign(cr *x509.CertificateRequest, opts provisioner.SignOptions, signOpts ...provisioner.SignOption) ([]*x509.Certificate, error)
|
||||
LoadProvisionerByID(string) (provisioner.Interface, error)
|
||||
LoadProvisionerByName(string) (provisioner.Interface, error)
|
||||
}
|
||||
|
||||
// New returns a new Authority that implements the SCEP interface.
|
||||
|
@ -72,6 +73,8 @@ func New(signAuth SignAuthority, ops AuthorityOptions) (*Authority, error) {
|
|||
// in its entirety to make this more interoperable with the rest of
|
||||
// step-ca, I think.
|
||||
if ops.Service != nil {
|
||||
authority.caCerts = ops.Service.certificateChain
|
||||
// TODO(hs): look into refactoring SCEP into using just caCerts everywhere, if it makes sense for more elaborate SCEP configuration. Keeping it like this for clarity (for now).
|
||||
authority.intermediateCertificate = ops.Service.certificateChain[0]
|
||||
authority.service = ops.Service
|
||||
}
|
||||
|
@ -82,7 +85,7 @@ func New(signAuth SignAuthority, ops AuthorityOptions) (*Authority, error) {
|
|||
var (
|
||||
// TODO: check the default capabilities; https://tools.ietf.org/html/rfc8894#section-3.5.2
|
||||
defaultCapabilities = []string{
|
||||
"Renewal",
|
||||
"Renewal", // NOTE: removing this will result in macOS SCEP client stating the server doesn't support renewal, but it uses PKCSreq to do so.
|
||||
"SHA-1",
|
||||
"SHA-256",
|
||||
"AES",
|
||||
|
@ -92,26 +95,21 @@ var (
|
|||
}
|
||||
)
|
||||
|
||||
// LoadProvisionerByID calls out to the SignAuthority interface to load a
|
||||
// provisioner by ID.
|
||||
func (a *Authority) LoadProvisionerByID(id string) (provisioner.Interface, error) {
|
||||
return a.signAuth.LoadProvisionerByID(id)
|
||||
// LoadProvisionerByName calls out to the SignAuthority interface to load a
|
||||
// provisioner by name.
|
||||
func (a *Authority) LoadProvisionerByName(name string) (provisioner.Interface, error) {
|
||||
return a.signAuth.LoadProvisionerByName(name)
|
||||
}
|
||||
|
||||
// GetLinkExplicit returns the requested link from the directory.
|
||||
func (a *Authority) GetLinkExplicit(provName string, abs bool, baseURL *url.URL, inputs ...string) string {
|
||||
// TODO: taken from ACME; move it to directory (if we need a directory in SCEP)?
|
||||
return a.getLinkExplicit(provName, abs, baseURL, inputs...)
|
||||
}
|
||||
|
||||
// getLinkExplicit returns an absolute or partial path to the given resource and a base
|
||||
// URL dynamically obtained from the request for which the link is being calculated.
|
||||
func (a *Authority) getLinkExplicit(provisionerName string, abs bool, baseURL *url.URL, inputs ...string) string {
|
||||
|
||||
// TODO: do we need to provide a way to provide a different suffix?
|
||||
// Like "/cgi-bin/pkiclient.exe"? Or would it be enough to have that as the name?
|
||||
link := "/" + provisionerName
|
||||
|
||||
if abs {
|
||||
// Copy the baseURL value from the pointer. https://github.com/golang/go/issues/38351
|
||||
u := url.URL{}
|
||||
|
@ -137,7 +135,7 @@ func (a *Authority) getLinkExplicit(provisionerName string, abs bool, baseURL *u
|
|||
}
|
||||
|
||||
// GetCACertificates returns the certificate (chain) for the CA
|
||||
func (a *Authority) GetCACertificates() ([]*x509.Certificate, error) {
|
||||
func (a *Authority) GetCACertificates(ctx context.Context) ([]*x509.Certificate, error) {
|
||||
|
||||
// TODO: this should return: the "SCEP Server (RA)" certificate, the issuing CA up to and excl. the root
|
||||
// Some clients do need the root certificate however; also see: https://github.com/openxpki/openxpki/issues/73
|
||||
|
@ -153,14 +151,28 @@ func (a *Authority) GetCACertificates() ([]*x509.Certificate, error) {
|
|||
// Using an RA does not seem to exist in https://tools.ietf.org/html/rfc8894, but is mentioned in
|
||||
// https://tools.ietf.org/id/draft-nourse-scep-21.html. Will continue using the CA directly for now.
|
||||
//
|
||||
// The certificate to use should probably depend on the (configured) Provisioner and may
|
||||
// The certificate to use should probably depend on the (configured) provisioner and may
|
||||
// use a distinct certificate, apart from the intermediate.
|
||||
|
||||
if a.intermediateCertificate == nil {
|
||||
p, err := provisionerFromContext(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(a.caCerts) == 0 {
|
||||
return nil, errors.New("no intermediate certificate available in SCEP authority")
|
||||
}
|
||||
|
||||
return []*x509.Certificate{a.intermediateCertificate}, nil
|
||||
certs := []*x509.Certificate{}
|
||||
certs = append(certs, a.caCerts[0])
|
||||
|
||||
// NOTE: we're adding the CA roots here, but they are (highly likely) different than what the RFC means.
|
||||
// Clients are responsible to select the right cert(s) to use, though.
|
||||
if p.ShouldIncludeRootInChain() && len(a.caCerts) > 1 {
|
||||
certs = append(certs, a.caCerts[1])
|
||||
}
|
||||
|
||||
return certs, nil
|
||||
}
|
||||
|
||||
// DecryptPKIEnvelope decrypts an enveloped message
|
||||
|
@ -211,8 +223,6 @@ func (a *Authority) DecryptPKIEnvelope(ctx context.Context, msg *PKIMessage) err
|
|||
|
||||
// SignCSR creates an x509.Certificate based on a CSR template and Cert Authority credentials
|
||||
// returns a new PKIMessage with CertRep data
|
||||
//func (msg *PKIMessage) SignCSR(crtAuth *x509.Certificate, keyAuth *rsa.PrivateKey, template *x509.Certificate) (*PKIMessage, error) {
|
||||
//func (a *Authority) SignCSR(ctx context.Context, msg *PKIMessage, template *x509.Certificate) (*PKIMessage, error) {
|
||||
func (a *Authority) SignCSR(ctx context.Context, csr *x509.CertificateRequest, msg *PKIMessage) (*PKIMessage, error) {
|
||||
|
||||
// TODO: intermediate storage of the request? In SCEP it's possible to request a csr/certificate
|
||||
|
@ -220,7 +230,7 @@ func (a *Authority) SignCSR(ctx context.Context, csr *x509.CertificateRequest, m
|
|||
// poll for the status. It seems to be similar as what can happen in ACME, so might want to model
|
||||
// the implementation after the one in the ACME authority. Requires storage, etc.
|
||||
|
||||
p, err := ProvisionerFromContext(ctx)
|
||||
p, err := provisionerFromContext(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -267,11 +277,7 @@ func (a *Authority) SignCSR(ctx context.Context, csr *x509.CertificateRequest, m
|
|||
return nil, errors.Wrap(err, "error retrieving authorization options from SCEP provisioner")
|
||||
}
|
||||
|
||||
opts := provisioner.SignOptions{
|
||||
// NotBefore: provisioner.NewTimeDuration(o.NotBefore),
|
||||
// NotAfter: provisioner.NewTimeDuration(o.NotAfter),
|
||||
}
|
||||
|
||||
opts := provisioner.SignOptions{}
|
||||
templateOptions, err := provisioner.TemplateOptions(p.GetOptions(), data)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "error creating template options from SCEP provisioner")
|
||||
|
@ -292,10 +298,17 @@ func (a *Authority) SignCSR(ctx context.Context, csr *x509.CertificateRequest, m
|
|||
return nil, err
|
||||
}
|
||||
|
||||
// apparently the pkcs7 library uses a global default setting for the content encryption
|
||||
// algorithm to use when en- or decrypting data. We need to restore the current setting after
|
||||
// the cryptographic operation, so that other usages of the library are not influenced by
|
||||
// this call to Encrypt(). We are not required to use the same algorithm the SCEP client uses.
|
||||
encryptionAlgorithmToRestore := pkcs7.ContentEncryptionAlgorithm
|
||||
pkcs7.ContentEncryptionAlgorithm = p.GetContentEncryptionAlgorithm()
|
||||
e7, err := pkcs7.Encrypt(deg, msg.P7.Certificates)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
pkcs7.ContentEncryptionAlgorithm = encryptionAlgorithmToRestore
|
||||
|
||||
// PKIMessageAttributes to be signed
|
||||
config := pkcs7.SignerInfoConfig{
|
||||
|
@ -434,7 +447,7 @@ func (a *Authority) CreateFailureResponse(ctx context.Context, csr *x509.Certifi
|
|||
// MatchChallengePassword verifies a SCEP challenge password
|
||||
func (a *Authority) MatchChallengePassword(ctx context.Context, password string) (bool, error) {
|
||||
|
||||
p, err := ProvisionerFromContext(ctx)
|
||||
p, err := provisionerFromContext(ctx)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
@ -453,7 +466,7 @@ func (a *Authority) MatchChallengePassword(ctx context.Context, password string)
|
|||
// GetCACaps returns the CA capabilities
|
||||
func (a *Authority) GetCACaps(ctx context.Context) []string {
|
||||
|
||||
p, err := ProvisionerFromContext(ctx)
|
||||
p, err := provisionerFromContext(ctx)
|
||||
if err != nil {
|
||||
return defaultCapabilities
|
||||
}
|
||||
|
|
|
@ -14,9 +14,9 @@ const (
|
|||
ProvisionerContextKey = ContextKey("provisioner")
|
||||
)
|
||||
|
||||
// ProvisionerFromContext searches the context for a SCEP provisioner.
|
||||
// provisionerFromContext searches the context for a SCEP provisioner.
|
||||
// Returns the provisioner or an error.
|
||||
func ProvisionerFromContext(ctx context.Context) (Provisioner, error) {
|
||||
func provisionerFromContext(ctx context.Context) (Provisioner, error) {
|
||||
val := ctx.Value(ProvisionerContextKey)
|
||||
if val == nil {
|
||||
return nil, errors.New("provisioner expected in request context")
|
||||
|
|
|
@ -16,4 +16,6 @@ type Provisioner interface {
|
|||
GetOptions() *provisioner.Options
|
||||
GetChallengePassword() string
|
||||
GetCapabilities() []string
|
||||
ShouldIncludeRootInChain() bool
|
||||
GetContentEncryptionAlgorithm() int
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue