forked from TrueCloudLab/certificates
Merge branch 'master' into context-authority
This commit is contained in:
commit
d461918eb0
94 changed files with 16443 additions and 680 deletions
20
.github/PULL_REQUEST_TEMPLATE
vendored
20
.github/PULL_REQUEST_TEMPLATE
vendored
|
@ -1,4 +1,20 @@
|
||||||
### Description
|
<!---
|
||||||
Please describe your pull request.
|
Please provide answers in the spaces below each prompt, where applicable.
|
||||||
|
Not every PR requires responses for each prompt.
|
||||||
|
Use your discretion.
|
||||||
|
-->
|
||||||
|
#### Name of feature:
|
||||||
|
|
||||||
|
#### Pain or issue this feature alleviates:
|
||||||
|
|
||||||
|
#### Why is this important to the project (if not answered above):
|
||||||
|
|
||||||
|
#### Is there documentation on how to use this feature? If so, where?
|
||||||
|
|
||||||
|
#### In what environments or workflows is this feature supported?
|
||||||
|
|
||||||
|
#### In what environments or workflows is this feature explicitly NOT supported (if any)?
|
||||||
|
|
||||||
|
#### Supporting links/other PRs/issues:
|
||||||
|
|
||||||
💔Thank you!
|
💔Thank you!
|
||||||
|
|
|
@ -7,6 +7,8 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"go.step.sm/crypto/jose"
|
"go.step.sm/crypto/jose"
|
||||||
|
|
||||||
|
"github.com/smallstep/certificates/authority/policy"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Account is a subset of the internal account type containing only those
|
// Account is a subset of the internal account type containing only those
|
||||||
|
@ -43,15 +45,63 @@ func KeyToID(jwk *jose.JSONWebKey) (string, error) {
|
||||||
return base64.RawURLEncoding.EncodeToString(kid), nil
|
return base64.RawURLEncoding.EncodeToString(kid), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PolicyNames contains ACME account level policy names
|
||||||
|
type PolicyNames struct {
|
||||||
|
DNSNames []string `json:"dns"`
|
||||||
|
IPRanges []string `json:"ips"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// X509Policy contains ACME account level X.509 policy
|
||||||
|
type X509Policy struct {
|
||||||
|
Allowed PolicyNames `json:"allow"`
|
||||||
|
Denied PolicyNames `json:"deny"`
|
||||||
|
AllowWildcardNames bool `json:"allowWildcardNames"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Policy is an ACME Account level policy
|
||||||
|
type Policy struct {
|
||||||
|
X509 X509Policy `json:"x509"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Policy) GetAllowedNameOptions() *policy.X509NameOptions {
|
||||||
|
if p == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return &policy.X509NameOptions{
|
||||||
|
DNSDomains: p.X509.Allowed.DNSNames,
|
||||||
|
IPRanges: p.X509.Allowed.IPRanges,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
func (p *Policy) GetDeniedNameOptions() *policy.X509NameOptions {
|
||||||
|
if p == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return &policy.X509NameOptions{
|
||||||
|
DNSDomains: p.X509.Denied.DNSNames,
|
||||||
|
IPRanges: p.X509.Denied.IPRanges,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// AreWildcardNamesAllowed returns if wildcard names
|
||||||
|
// like *.example.com are allowed to be signed.
|
||||||
|
// Defaults to false.
|
||||||
|
func (p *Policy) AreWildcardNamesAllowed() bool {
|
||||||
|
if p == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return p.X509.AllowWildcardNames
|
||||||
|
}
|
||||||
|
|
||||||
// ExternalAccountKey is an ACME External Account Binding key.
|
// ExternalAccountKey is an ACME External Account Binding key.
|
||||||
type ExternalAccountKey struct {
|
type ExternalAccountKey struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
ProvisionerID string `json:"provisionerID"`
|
ProvisionerID string `json:"provisionerID"`
|
||||||
Reference string `json:"reference"`
|
Reference string `json:"reference"`
|
||||||
AccountID string `json:"-"`
|
AccountID string `json:"-"`
|
||||||
KeyBytes []byte `json:"-"`
|
HmacKey []byte `json:"-"`
|
||||||
CreatedAt time.Time `json:"createdAt"`
|
CreatedAt time.Time `json:"createdAt"`
|
||||||
BoundAt time.Time `json:"boundAt,omitempty"`
|
BoundAt time.Time `json:"boundAt,omitempty"`
|
||||||
|
Policy *Policy `json:"policy,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// AlreadyBound returns whether this EAK is already bound to
|
// AlreadyBound returns whether this EAK is already bound to
|
||||||
|
@ -68,6 +118,6 @@ func (eak *ExternalAccountKey) BindTo(account *Account) error {
|
||||||
}
|
}
|
||||||
eak.AccountID = account.ID
|
eak.AccountID = account.ID
|
||||||
eak.BoundAt = time.Now()
|
eak.BoundAt = time.Now()
|
||||||
eak.KeyBytes = []byte{} // clearing the key bytes; can only be used once
|
eak.HmacKey = []byte{} // clearing the key bytes; can only be used once
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,8 +7,9 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/smallstep/assert"
|
|
||||||
"go.step.sm/crypto/jose"
|
"go.step.sm/crypto/jose"
|
||||||
|
|
||||||
|
"github.com/smallstep/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestKeyToID(t *testing.T) {
|
func TestKeyToID(t *testing.T) {
|
||||||
|
@ -95,7 +96,7 @@ func TestExternalAccountKey_BindTo(t *testing.T) {
|
||||||
ID: "eakID",
|
ID: "eakID",
|
||||||
ProvisionerID: "provID",
|
ProvisionerID: "provID",
|
||||||
Reference: "ref",
|
Reference: "ref",
|
||||||
KeyBytes: []byte{1, 3, 3, 7},
|
HmacKey: []byte{1, 3, 3, 7},
|
||||||
},
|
},
|
||||||
acct: &Account{
|
acct: &Account{
|
||||||
ID: "accountID",
|
ID: "accountID",
|
||||||
|
@ -108,7 +109,7 @@ func TestExternalAccountKey_BindTo(t *testing.T) {
|
||||||
ID: "eakID",
|
ID: "eakID",
|
||||||
ProvisionerID: "provID",
|
ProvisionerID: "provID",
|
||||||
Reference: "ref",
|
Reference: "ref",
|
||||||
KeyBytes: []byte{1, 3, 3, 7},
|
HmacKey: []byte{1, 3, 3, 7},
|
||||||
AccountID: "someAccountID",
|
AccountID: "someAccountID",
|
||||||
BoundAt: boundAt,
|
BoundAt: boundAt,
|
||||||
},
|
},
|
||||||
|
@ -138,7 +139,7 @@ func TestExternalAccountKey_BindTo(t *testing.T) {
|
||||||
assert.Equals(t, ae.Subproblems, tt.err.Subproblems)
|
assert.Equals(t, ae.Subproblems, tt.err.Subproblems)
|
||||||
} else {
|
} else {
|
||||||
assert.Equals(t, eak.AccountID, acct.ID)
|
assert.Equals(t, eak.AccountID, acct.ID)
|
||||||
assert.Equals(t, eak.KeyBytes, []byte{})
|
assert.Equals(t, eak.HmacKey, []byte{})
|
||||||
assert.NotNil(t, eak.BoundAt)
|
assert.NotNil(t, eak.BoundAt)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
@ -134,8 +134,7 @@ func NewAccount(w http.ResponseWriter, r *http.Request) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if eak != nil { // means that we have a (valid) External Account Binding key that should be bound, updated and sent in the response
|
if eak != nil { // means that we have a (valid) External Account Binding key that should be bound, updated and sent in the response
|
||||||
err := eak.BindTo(acc)
|
if err := eak.BindTo(acc); err != nil {
|
||||||
if err != nil {
|
|
||||||
render.Error(w, err)
|
render.Error(w, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,10 +13,12 @@ import (
|
||||||
|
|
||||||
"github.com/go-chi/chi"
|
"github.com/go-chi/chi"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
|
|
||||||
|
"go.step.sm/crypto/jose"
|
||||||
|
|
||||||
"github.com/smallstep/assert"
|
"github.com/smallstep/assert"
|
||||||
"github.com/smallstep/certificates/acme"
|
"github.com/smallstep/certificates/acme"
|
||||||
"github.com/smallstep/certificates/authority/provisioner"
|
"github.com/smallstep/certificates/authority/provisioner"
|
||||||
"go.step.sm/crypto/jose"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
@ -29,6 +31,22 @@ var (
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type fakeProvisioner struct{}
|
||||||
|
|
||||||
|
func (*fakeProvisioner) AuthorizeOrderIdentifier(ctx context.Context, identifier provisioner.ACMEIdentifier) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*fakeProvisioner) AuthorizeSign(ctx context.Context, token string) ([]provisioner.SignOption, error) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*fakeProvisioner) AuthorizeRevoke(ctx context.Context, token string) error { return nil }
|
||||||
|
func (*fakeProvisioner) GetID() string { return "" }
|
||||||
|
func (*fakeProvisioner) GetName() string { return "" }
|
||||||
|
func (*fakeProvisioner) DefaultTLSCertDuration() time.Duration { return 0 }
|
||||||
|
func (*fakeProvisioner) GetOptions() *provisioner.Options { return nil }
|
||||||
|
|
||||||
func newProv() acme.Provisioner {
|
func newProv() acme.Provisioner {
|
||||||
// Initialize provisioners
|
// Initialize provisioners
|
||||||
p := &provisioner.ACME{
|
p := &provisioner.ACME{
|
||||||
|
@ -41,6 +59,19 @@ func newProv() acme.Provisioner {
|
||||||
return p
|
return p
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func newProvWithOptions(options *provisioner.Options) acme.Provisioner {
|
||||||
|
// Initialize provisioners
|
||||||
|
p := &provisioner.ACME{
|
||||||
|
Type: "ACME",
|
||||||
|
Name: "test@acme-<test>provisioner.com",
|
||||||
|
Options: options,
|
||||||
|
}
|
||||||
|
if err := p.Init(provisioner.Config{Claims: globalProvisionerClaims}); err != nil {
|
||||||
|
fmt.Printf("%v", err)
|
||||||
|
}
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
func newACMEProv(t *testing.T) *provisioner.ACME {
|
func newACMEProv(t *testing.T) *provisioner.ACME {
|
||||||
p := newProv()
|
p := newProv()
|
||||||
a, ok := p.(*provisioner.ACME)
|
a, ok := p.(*provisioner.ACME)
|
||||||
|
@ -50,6 +81,15 @@ func newACMEProv(t *testing.T) *provisioner.ACME {
|
||||||
return a
|
return a
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func newACMEProvWithOptions(t *testing.T, options *provisioner.Options) *provisioner.ACME {
|
||||||
|
p := newProvWithOptions(options)
|
||||||
|
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) {
|
func createEABJWS(jwk *jose.JSONWebKey, hmacKey []byte, keyID, u string) (*jose.JSONWebSignature, error) {
|
||||||
signer, err := jose.NewSigner(
|
signer, err := jose.NewSigner(
|
||||||
jose.SigningKey{
|
jose.SigningKey{
|
||||||
|
@ -507,16 +547,9 @@ func TestHandler_NewAccount(t *testing.T) {
|
||||||
}
|
}
|
||||||
b, err := json.Marshal(nar)
|
b, err := json.Marshal(nar)
|
||||||
assert.FatalError(t, err)
|
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(context.Background(), payloadContextKey, &payloadInfo{value: b})
|
||||||
ctx = context.WithValue(ctx, jwkContextKey, jwk)
|
ctx = context.WithValue(ctx, jwkContextKey, jwk)
|
||||||
ctx = acme.NewProvisionerContext(ctx, scepProvisioner)
|
ctx = acme.NewProvisionerContext(ctx, &fakeProvisioner{})
|
||||||
return test{
|
return test{
|
||||||
db: &acme.MockDB{},
|
db: &acme.MockDB{},
|
||||||
ctx: ctx,
|
ctx: ctx,
|
||||||
|
@ -563,7 +596,7 @@ func TestHandler_NewAccount(t *testing.T) {
|
||||||
ID: "eakID",
|
ID: "eakID",
|
||||||
ProvisionerID: provID,
|
ProvisionerID: provID,
|
||||||
Reference: "testeak",
|
Reference: "testeak",
|
||||||
KeyBytes: []byte{1, 3, 3, 7},
|
HmacKey: []byte{1, 3, 3, 7},
|
||||||
CreatedAt: time.Now(),
|
CreatedAt: time.Now(),
|
||||||
}
|
}
|
||||||
return test{
|
return test{
|
||||||
|
@ -737,7 +770,7 @@ func TestHandler_NewAccount(t *testing.T) {
|
||||||
ID: "eakID",
|
ID: "eakID",
|
||||||
ProvisionerID: provID,
|
ProvisionerID: provID,
|
||||||
Reference: "testeak",
|
Reference: "testeak",
|
||||||
KeyBytes: []byte{1, 3, 3, 7},
|
HmacKey: []byte{1, 3, 3, 7},
|
||||||
CreatedAt: time.Now(),
|
CreatedAt: time.Now(),
|
||||||
}, nil
|
}, nil
|
||||||
},
|
},
|
||||||
|
|
|
@ -4,8 +4,9 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
|
||||||
"github.com/smallstep/certificates/acme"
|
|
||||||
"go.step.sm/crypto/jose"
|
"go.step.sm/crypto/jose"
|
||||||
|
|
||||||
|
"github.com/smallstep/certificates/acme"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ExternalAccountBinding represents the ACME externalAccountBinding JWS
|
// ExternalAccountBinding represents the ACME externalAccountBinding JWS
|
||||||
|
@ -56,11 +57,19 @@ func validateExternalAccountBinding(ctx context.Context, nar *NewAccountRequest)
|
||||||
return nil, acme.WrapErrorISE(err, "error retrieving external account key")
|
return nil, acme.WrapErrorISE(err, "error retrieving external account key")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if externalAccountKey == nil {
|
||||||
|
return nil, acme.NewError(acme.ErrorUnauthorizedType, "the field 'kid' references an unknown key")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(externalAccountKey.HmacKey) == 0 {
|
||||||
|
return nil, acme.NewError(acme.ErrorServerInternalType, "external account binding key with id '%s' does not have secret bytes", keyID)
|
||||||
|
}
|
||||||
|
|
||||||
if externalAccountKey.AlreadyBound() {
|
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)
|
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)
|
payload, err := eabJWS.Verify(externalAccountKey.HmacKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, acme.WrapErrorISE(err, "error verifying externalAccountBinding signature")
|
return nil, acme.WrapErrorISE(err, "error verifying externalAccountBinding signature")
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,10 +9,11 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
|
|
||||||
|
"go.step.sm/crypto/jose"
|
||||||
|
|
||||||
"github.com/smallstep/assert"
|
"github.com/smallstep/assert"
|
||||||
"github.com/smallstep/certificates/acme"
|
"github.com/smallstep/certificates/acme"
|
||||||
"github.com/smallstep/certificates/authority/provisioner"
|
|
||||||
"go.step.sm/crypto/jose"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func Test_keysAreEqual(t *testing.T) {
|
func Test_keysAreEqual(t *testing.T) {
|
||||||
|
@ -152,7 +153,7 @@ func TestHandler_validateExternalAccountBinding(t *testing.T) {
|
||||||
ID: "eakID",
|
ID: "eakID",
|
||||||
ProvisionerID: provID,
|
ProvisionerID: provID,
|
||||||
Reference: "testeak",
|
Reference: "testeak",
|
||||||
KeyBytes: []byte{1, 3, 3, 7},
|
HmacKey: []byte{1, 3, 3, 7},
|
||||||
CreatedAt: createdAt,
|
CreatedAt: createdAt,
|
||||||
}, nil
|
}, nil
|
||||||
},
|
},
|
||||||
|
@ -166,7 +167,7 @@ func TestHandler_validateExternalAccountBinding(t *testing.T) {
|
||||||
ID: "eakID",
|
ID: "eakID",
|
||||||
ProvisionerID: provID,
|
ProvisionerID: provID,
|
||||||
Reference: "testeak",
|
Reference: "testeak",
|
||||||
KeyBytes: []byte{1, 3, 3, 7},
|
HmacKey: []byte{1, 3, 3, 7},
|
||||||
CreatedAt: createdAt,
|
CreatedAt: createdAt,
|
||||||
},
|
},
|
||||||
err: nil,
|
err: nil,
|
||||||
|
@ -187,16 +188,10 @@ func TestHandler_validateExternalAccountBinding(t *testing.T) {
|
||||||
}
|
}
|
||||||
b, err := json.Marshal(nar)
|
b, err := json.Marshal(nar)
|
||||||
assert.FatalError(t, err)
|
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(context.Background(), payloadContextKey, &payloadInfo{value: b})
|
||||||
ctx = context.WithValue(ctx, jwkContextKey, jwk)
|
ctx = context.WithValue(ctx, jwkContextKey, jwk)
|
||||||
ctx = acme.NewProvisionerContext(ctx, scepProvisioner)
|
ctx = acme.NewProvisionerContext(ctx, &fakeProvisioner{})
|
||||||
return test{
|
return test{
|
||||||
ctx: ctx,
|
ctx: ctx,
|
||||||
err: acme.NewError(acme.ErrorServerInternalType, "could not load ACME provisioner from context: provisioner in context is not an ACME provisioner"),
|
err: acme.NewError(acme.ErrorServerInternalType, "could not load ACME provisioner from context: provisioner in context is not an ACME provisioner"),
|
||||||
|
@ -418,6 +413,112 @@ func TestHandler_validateExternalAccountBinding(t *testing.T) {
|
||||||
err: acme.NewErrorISE("error retrieving external account key"),
|
err: acme.NewErrorISE("error retrieving external account key"),
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"fail/db.GetExternalAccountKey-nil": 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(), jwkContextKey, jwk)
|
||||||
|
ctx = acme.NewProvisionerContext(ctx, prov)
|
||||||
|
ctx = context.WithValue(ctx, jwsContextKey, parsedJWS)
|
||||||
|
return test{
|
||||||
|
db: &acme.MockDB{
|
||||||
|
MockGetExternalAccountKey: func(ctx context.Context, provisionerName, keyID string) (*acme.ExternalAccountKey, error) {
|
||||||
|
return nil, nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ctx: ctx,
|
||||||
|
nar: &NewAccountRequest{
|
||||||
|
Contact: []string{"foo", "bar"},
|
||||||
|
ExternalAccountBinding: eab,
|
||||||
|
},
|
||||||
|
eak: nil,
|
||||||
|
err: acme.NewError(acme.ErrorUnauthorizedType, "the field 'kid' references an unknown key"),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"fail/db.GetExternalAccountKey-no-keybytes": 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(), jwkContextKey, jwk)
|
||||||
|
ctx = acme.NewProvisionerContext(ctx, prov)
|
||||||
|
ctx = context.WithValue(ctx, jwsContextKey, parsedJWS)
|
||||||
|
createdAt := time.Now()
|
||||||
|
return test{
|
||||||
|
db: &acme.MockDB{
|
||||||
|
MockGetExternalAccountKey: func(ctx context.Context, provisionerName, keyID string) (*acme.ExternalAccountKey, error) {
|
||||||
|
return &acme.ExternalAccountKey{
|
||||||
|
ID: "eakID",
|
||||||
|
ProvisionerID: provID,
|
||||||
|
Reference: "testeak",
|
||||||
|
CreatedAt: createdAt,
|
||||||
|
AccountID: "some-account-id",
|
||||||
|
HmacKey: []byte{},
|
||||||
|
}, nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ctx: ctx,
|
||||||
|
nar: &NewAccountRequest{
|
||||||
|
Contact: []string{"foo", "bar"},
|
||||||
|
ExternalAccountBinding: eab,
|
||||||
|
},
|
||||||
|
eak: nil,
|
||||||
|
err: acme.NewError(acme.ErrorServerInternalType, "external account binding key with id 'eakID' does not have secret bytes"),
|
||||||
|
}
|
||||||
|
},
|
||||||
"fail/db.GetExternalAccountKey-wrong-provisioner": func(t *testing.T) test {
|
"fail/db.GetExternalAccountKey-wrong-provisioner": func(t *testing.T) test {
|
||||||
jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0)
|
jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0)
|
||||||
assert.FatalError(t, err)
|
assert.FatalError(t, err)
|
||||||
|
@ -510,6 +611,7 @@ func TestHandler_validateExternalAccountBinding(t *testing.T) {
|
||||||
Reference: "testeak",
|
Reference: "testeak",
|
||||||
CreatedAt: createdAt,
|
CreatedAt: createdAt,
|
||||||
AccountID: "some-account-id",
|
AccountID: "some-account-id",
|
||||||
|
HmacKey: []byte{1, 3, 3, 7},
|
||||||
BoundAt: boundAt,
|
BoundAt: boundAt,
|
||||||
}, nil
|
}, nil
|
||||||
},
|
},
|
||||||
|
@ -564,7 +666,7 @@ func TestHandler_validateExternalAccountBinding(t *testing.T) {
|
||||||
ID: "eakID",
|
ID: "eakID",
|
||||||
ProvisionerID: provID,
|
ProvisionerID: provID,
|
||||||
Reference: "testeak",
|
Reference: "testeak",
|
||||||
KeyBytes: []byte{1, 2, 3, 4},
|
HmacKey: []byte{1, 2, 3, 4},
|
||||||
CreatedAt: time.Now(),
|
CreatedAt: time.Now(),
|
||||||
}, nil
|
}, nil
|
||||||
},
|
},
|
||||||
|
@ -621,7 +723,7 @@ func TestHandler_validateExternalAccountBinding(t *testing.T) {
|
||||||
ID: "eakID",
|
ID: "eakID",
|
||||||
ProvisionerID: provID,
|
ProvisionerID: provID,
|
||||||
Reference: "testeak",
|
Reference: "testeak",
|
||||||
KeyBytes: []byte{1, 3, 3, 7},
|
HmacKey: []byte{1, 3, 3, 7},
|
||||||
CreatedAt: time.Now(),
|
CreatedAt: time.Now(),
|
||||||
}, nil
|
}, nil
|
||||||
},
|
},
|
||||||
|
@ -675,7 +777,7 @@ func TestHandler_validateExternalAccountBinding(t *testing.T) {
|
||||||
ID: "eakID",
|
ID: "eakID",
|
||||||
ProvisionerID: provID,
|
ProvisionerID: provID,
|
||||||
Reference: "testeak",
|
Reference: "testeak",
|
||||||
KeyBytes: []byte{1, 3, 3, 7},
|
HmacKey: []byte{1, 3, 3, 7},
|
||||||
CreatedAt: time.Now(),
|
CreatedAt: time.Now(),
|
||||||
}, nil
|
}, nil
|
||||||
},
|
},
|
||||||
|
@ -730,7 +832,7 @@ func TestHandler_validateExternalAccountBinding(t *testing.T) {
|
||||||
ID: "eakID",
|
ID: "eakID",
|
||||||
ProvisionerID: provID,
|
ProvisionerID: provID,
|
||||||
Reference: "testeak",
|
Reference: "testeak",
|
||||||
KeyBytes: []byte{1, 3, 3, 7},
|
HmacKey: []byte{1, 3, 3, 7},
|
||||||
CreatedAt: time.Now(),
|
CreatedAt: time.Now(),
|
||||||
}, nil
|
}, nil
|
||||||
},
|
},
|
||||||
|
@ -771,7 +873,7 @@ func TestHandler_validateExternalAccountBinding(t *testing.T) {
|
||||||
} else {
|
} else {
|
||||||
assert.NotNil(t, tc.eak)
|
assert.NotNil(t, tc.eak)
|
||||||
assert.Equals(t, got.ID, tc.eak.ID)
|
assert.Equals(t, got.ID, tc.eak.ID)
|
||||||
assert.Equals(t, got.KeyBytes, tc.eak.KeyBytes)
|
assert.Equals(t, got.HmacKey, tc.eak.HmacKey)
|
||||||
assert.Equals(t, got.ProvisionerID, tc.eak.ProvisionerID)
|
assert.Equals(t, got.ProvisionerID, tc.eak.ProvisionerID)
|
||||||
assert.Equals(t, got.Reference, tc.eak.Reference)
|
assert.Equals(t, got.Reference, tc.eak.Reference)
|
||||||
assert.Equals(t, got.CreatedAt, tc.eak.CreatedAt)
|
assert.Equals(t, got.CreatedAt, tc.eak.CreatedAt)
|
||||||
|
|
|
@ -20,7 +20,6 @@ import (
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/smallstep/assert"
|
"github.com/smallstep/assert"
|
||||||
"github.com/smallstep/certificates/acme"
|
"github.com/smallstep/certificates/acme"
|
||||||
"github.com/smallstep/certificates/authority/provisioner"
|
|
||||||
"go.step.sm/crypto/jose"
|
"go.step.sm/crypto/jose"
|
||||||
"go.step.sm/crypto/pemutil"
|
"go.step.sm/crypto/pemutil"
|
||||||
)
|
)
|
||||||
|
@ -93,11 +92,7 @@ func TestHandler_GetDirectory(t *testing.T) {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"fail/different-provisioner": func(t *testing.T) test {
|
"fail/different-provisioner": func(t *testing.T) test {
|
||||||
prov := &provisioner.SCEP{
|
ctx := acme.NewProvisionerContext(context.Background(), &fakeProvisioner{})
|
||||||
Type: "SCEP",
|
|
||||||
Name: "test@scep-<test>provisioner.com",
|
|
||||||
}
|
|
||||||
ctx := acme.NewProvisionerContext(context.Background(), prov)
|
|
||||||
return test{
|
return test{
|
||||||
ctx: ctx,
|
ctx: ctx,
|
||||||
statusCode: 500,
|
statusCode: 500,
|
||||||
|
|
|
@ -16,6 +16,8 @@ import (
|
||||||
|
|
||||||
"github.com/smallstep/certificates/acme"
|
"github.com/smallstep/certificates/acme"
|
||||||
"github.com/smallstep/certificates/api/render"
|
"github.com/smallstep/certificates/api/render"
|
||||||
|
"github.com/smallstep/certificates/authority/policy"
|
||||||
|
"github.com/smallstep/certificates/authority/provisioner"
|
||||||
)
|
)
|
||||||
|
|
||||||
// NewOrderRequest represents the body for a NewOrder request.
|
// NewOrderRequest represents the body for a NewOrder request.
|
||||||
|
@ -37,6 +39,8 @@ func (n *NewOrderRequest) Validate() error {
|
||||||
if id.Type == acme.IP && net.ParseIP(id.Value) == nil {
|
if id.Type == acme.IP && net.ParseIP(id.Value) == nil {
|
||||||
return acme.NewError(acme.ErrorMalformedType, "invalid IP address: %s", id.Value)
|
return acme.NewError(acme.ErrorMalformedType, "invalid IP address: %s", id.Value)
|
||||||
}
|
}
|
||||||
|
// TODO(hs): add some validations for DNS domains?
|
||||||
|
// TODO(hs): combine the errors from this with allow/deny policy, like example error in https://datatracker.ietf.org/doc/html/rfc8555#section-6.7.1
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -70,6 +74,7 @@ var defaultOrderBackdate = time.Minute
|
||||||
// NewOrder ACME api for creating a new order.
|
// NewOrder ACME api for creating a new order.
|
||||||
func NewOrder(w http.ResponseWriter, r *http.Request) {
|
func NewOrder(w http.ResponseWriter, r *http.Request) {
|
||||||
ctx := r.Context()
|
ctx := r.Context()
|
||||||
|
ca := mustAuthority(ctx)
|
||||||
db := acme.MustDatabaseFromContext(ctx)
|
db := acme.MustDatabaseFromContext(ctx)
|
||||||
linker := acme.MustLinkerFromContext(ctx)
|
linker := acme.MustLinkerFromContext(ctx)
|
||||||
|
|
||||||
|
@ -88,6 +93,7 @@ func NewOrder(w http.ResponseWriter, r *http.Request) {
|
||||||
render.Error(w, err)
|
render.Error(w, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var nor NewOrderRequest
|
var nor NewOrderRequest
|
||||||
if err := json.Unmarshal(payload.value, &nor); err != nil {
|
if err := json.Unmarshal(payload.value, &nor); err != nil {
|
||||||
render.Error(w, acme.WrapError(acme.ErrorMalformedType, err,
|
render.Error(w, acme.WrapError(acme.ErrorMalformedType, err,
|
||||||
|
@ -100,6 +106,48 @@ func NewOrder(w http.ResponseWriter, r *http.Request) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO(hs): gather all errors, so that we can build one response with ACME subproblems
|
||||||
|
// include the nor.Validate() error here too, like in the example in the ACME RFC?
|
||||||
|
|
||||||
|
acmeProv, err := acmeProvisionerFromContext(ctx)
|
||||||
|
if err != nil {
|
||||||
|
render.Error(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var eak *acme.ExternalAccountKey
|
||||||
|
if acmeProv.RequireEAB {
|
||||||
|
if eak, err = db.GetExternalAccountKeyByAccountID(ctx, prov.GetID(), acc.ID); err != nil {
|
||||||
|
render.Error(w, acme.WrapErrorISE(err, "error retrieving external account binding key"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
acmePolicy, err := newACMEPolicyEngine(eak)
|
||||||
|
if err != nil {
|
||||||
|
render.Error(w, acme.WrapErrorISE(err, "error creating ACME policy engine"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, identifier := range nor.Identifiers {
|
||||||
|
// evaluate the ACME account level policy
|
||||||
|
if err = isIdentifierAllowed(acmePolicy, identifier); err != nil {
|
||||||
|
render.Error(w, acme.WrapError(acme.ErrorRejectedIdentifierType, err, "not authorized"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// evaluate the provisioner level policy
|
||||||
|
orderIdentifier := provisioner.ACMEIdentifier{Type: provisioner.ACMEIdentifierType(identifier.Type), Value: identifier.Value}
|
||||||
|
if err = prov.AuthorizeOrderIdentifier(ctx, orderIdentifier); err != nil {
|
||||||
|
render.Error(w, acme.WrapError(acme.ErrorRejectedIdentifierType, err, "not authorized"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// evaluate the authority level policy
|
||||||
|
if err = ca.AreSANsAllowed(ctx, []string{identifier.Value}); err != nil {
|
||||||
|
render.Error(w, acme.WrapError(acme.ErrorRejectedIdentifierType, err, "not authorized"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
now := clock.Now()
|
now := clock.Now()
|
||||||
// New order.
|
// New order.
|
||||||
o := &acme.Order{
|
o := &acme.Order{
|
||||||
|
@ -150,6 +198,20 @@ func NewOrder(w http.ResponseWriter, r *http.Request) {
|
||||||
render.JSONStatus(w, o, http.StatusCreated)
|
render.JSONStatus(w, o, http.StatusCreated)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func isIdentifierAllowed(acmePolicy policy.X509Policy, identifier acme.Identifier) error {
|
||||||
|
if acmePolicy == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return acmePolicy.AreSANsAllowed([]string{identifier.Value})
|
||||||
|
}
|
||||||
|
|
||||||
|
func newACMEPolicyEngine(eak *acme.ExternalAccountKey) (policy.X509Policy, error) {
|
||||||
|
if eak == nil {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return policy.NewX509PolicyEngine(eak.Policy)
|
||||||
|
}
|
||||||
|
|
||||||
func newAuthorization(ctx context.Context, az *acme.Authorization) error {
|
func newAuthorization(ctx context.Context, az *acme.Authorization) error {
|
||||||
if strings.HasPrefix(az.Identifier.Value, "*.") {
|
if strings.HasPrefix(az.Identifier.Value, "*.") {
|
||||||
az.Wildcard = true
|
az.Wildcard = true
|
||||||
|
@ -231,7 +293,7 @@ func GetOrder(w http.ResponseWriter, r *http.Request) {
|
||||||
render.JSON(w, o)
|
render.JSON(w, o)
|
||||||
}
|
}
|
||||||
|
|
||||||
// FinalizeOrder attemptst to finalize an order and create a certificate.
|
// FinalizeOrder attempts to finalize an order and create a certificate.
|
||||||
func FinalizeOrder(w http.ResponseWriter, r *http.Request) {
|
func FinalizeOrder(w http.ResponseWriter, r *http.Request) {
|
||||||
ctx := r.Context()
|
ctx := r.Context()
|
||||||
db := acme.MustDatabaseFromContext(ctx)
|
db := acme.MustDatabaseFromContext(ctx)
|
||||||
|
|
|
@ -16,9 +16,13 @@ import (
|
||||||
|
|
||||||
"github.com/go-chi/chi"
|
"github.com/go-chi/chi"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
|
|
||||||
|
"go.step.sm/crypto/pemutil"
|
||||||
|
|
||||||
"github.com/smallstep/assert"
|
"github.com/smallstep/assert"
|
||||||
"github.com/smallstep/certificates/acme"
|
"github.com/smallstep/certificates/acme"
|
||||||
"go.step.sm/crypto/pemutil"
|
"github.com/smallstep/certificates/authority/policy"
|
||||||
|
"github.com/smallstep/certificates/authority/provisioner"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestNewOrderRequest_Validate(t *testing.T) {
|
func TestNewOrderRequest_Validate(t *testing.T) {
|
||||||
|
@ -670,6 +674,7 @@ func TestHandler_NewOrder(t *testing.T) {
|
||||||
baseURL.String(), escProvName)
|
baseURL.String(), escProvName)
|
||||||
|
|
||||||
type test struct {
|
type test struct {
|
||||||
|
ca acme.CertificateAuthority
|
||||||
db acme.DB
|
db acme.DB
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
nor *NewOrderRequest
|
nor *NewOrderRequest
|
||||||
|
@ -737,7 +742,7 @@ func TestHandler_NewOrder(t *testing.T) {
|
||||||
db: &acme.MockDB{},
|
db: &acme.MockDB{},
|
||||||
ctx: ctx,
|
ctx: ctx,
|
||||||
statusCode: 500,
|
statusCode: 500,
|
||||||
err: acme.NewErrorISE("paylod does not exist"),
|
err: acme.NewErrorISE("payload does not exist"),
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"fail/unmarshal-payload-error": func(t *testing.T) test {
|
"fail/unmarshal-payload-error": func(t *testing.T) test {
|
||||||
|
@ -767,6 +772,222 @@ func TestHandler_NewOrder(t *testing.T) {
|
||||||
err: acme.NewError(acme.ErrorMalformedType, "identifiers list cannot be empty"),
|
err: acme.NewError(acme.ErrorMalformedType, "identifiers list cannot be empty"),
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"fail/acmeProvisionerFromContext-error": func(t *testing.T) test {
|
||||||
|
acc := &acme.Account{ID: "accID"}
|
||||||
|
fr := &NewOrderRequest{
|
||||||
|
Identifiers: []acme.Identifier{
|
||||||
|
{Type: "dns", Value: "zap.internal"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
b, err := json.Marshal(fr)
|
||||||
|
assert.FatalError(t, err)
|
||||||
|
ctx := acme.NewProvisionerContext(context.Background(), &acme.MockProvisioner{})
|
||||||
|
ctx = context.WithValue(ctx, accContextKey, acc)
|
||||||
|
ctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{value: b})
|
||||||
|
return test{
|
||||||
|
ctx: ctx,
|
||||||
|
statusCode: 500,
|
||||||
|
ca: &mockCA{},
|
||||||
|
db: &acme.MockDB{
|
||||||
|
MockGetExternalAccountKeyByAccountID: func(ctx context.Context, provisionerID, accountID string) (*acme.ExternalAccountKey, error) {
|
||||||
|
assert.Equals(t, prov.GetID(), provisionerID)
|
||||||
|
assert.Equals(t, "accID", accountID)
|
||||||
|
return nil, errors.New("force")
|
||||||
|
},
|
||||||
|
},
|
||||||
|
err: acme.NewErrorISE("error retrieving external account binding key: force"),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"fail/db.GetExternalAccountKeyByAccountID-error": func(t *testing.T) test {
|
||||||
|
acmeProv := newACMEProv(t)
|
||||||
|
acmeProv.RequireEAB = true
|
||||||
|
acc := &acme.Account{ID: "accID"}
|
||||||
|
fr := &NewOrderRequest{
|
||||||
|
Identifiers: []acme.Identifier{
|
||||||
|
{Type: "dns", Value: "zap.internal"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
b, err := json.Marshal(fr)
|
||||||
|
assert.FatalError(t, err)
|
||||||
|
ctx := acme.NewProvisionerContext(context.Background(), acmeProv)
|
||||||
|
ctx = context.WithValue(ctx, accContextKey, acc)
|
||||||
|
ctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{value: b})
|
||||||
|
return test{
|
||||||
|
ctx: ctx,
|
||||||
|
statusCode: 500,
|
||||||
|
ca: &mockCA{},
|
||||||
|
db: &acme.MockDB{
|
||||||
|
MockGetExternalAccountKeyByAccountID: func(ctx context.Context, provisionerID, accountID string) (*acme.ExternalAccountKey, error) {
|
||||||
|
assert.Equals(t, prov.GetID(), provisionerID)
|
||||||
|
assert.Equals(t, "accID", accountID)
|
||||||
|
return nil, errors.New("force")
|
||||||
|
},
|
||||||
|
},
|
||||||
|
err: acme.NewErrorISE("error retrieving external account binding key: force"),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"fail/newACMEPolicyEngine-error": func(t *testing.T) test {
|
||||||
|
acmeProv := newACMEProv(t)
|
||||||
|
acmeProv.RequireEAB = true
|
||||||
|
acc := &acme.Account{ID: "accID"}
|
||||||
|
fr := &NewOrderRequest{
|
||||||
|
Identifiers: []acme.Identifier{
|
||||||
|
{Type: "dns", Value: "zap.internal"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
b, err := json.Marshal(fr)
|
||||||
|
assert.FatalError(t, err)
|
||||||
|
ctx := acme.NewProvisionerContext(context.Background(), acmeProv)
|
||||||
|
ctx = context.WithValue(ctx, accContextKey, acc)
|
||||||
|
ctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{value: b})
|
||||||
|
return test{
|
||||||
|
ctx: ctx,
|
||||||
|
statusCode: 500,
|
||||||
|
ca: &mockCA{},
|
||||||
|
db: &acme.MockDB{
|
||||||
|
MockGetExternalAccountKeyByAccountID: func(ctx context.Context, provisionerID, accountID string) (*acme.ExternalAccountKey, error) {
|
||||||
|
assert.Equals(t, prov.GetID(), provisionerID)
|
||||||
|
assert.Equals(t, "accID", accountID)
|
||||||
|
return &acme.ExternalAccountKey{
|
||||||
|
Policy: &acme.Policy{
|
||||||
|
X509: acme.X509Policy{
|
||||||
|
Allowed: acme.PolicyNames{
|
||||||
|
DNSNames: []string{"**.local"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
err: acme.NewErrorISE("error creating ACME policy engine"),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"fail/isIdentifierAllowed-error": func(t *testing.T) test {
|
||||||
|
acmeProv := newACMEProv(t)
|
||||||
|
acmeProv.RequireEAB = true
|
||||||
|
acc := &acme.Account{ID: "accID"}
|
||||||
|
fr := &NewOrderRequest{
|
||||||
|
Identifiers: []acme.Identifier{
|
||||||
|
{Type: "dns", Value: "zap.internal"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
b, err := json.Marshal(fr)
|
||||||
|
assert.FatalError(t, err)
|
||||||
|
ctx := acme.NewProvisionerContext(context.Background(), acmeProv)
|
||||||
|
ctx = context.WithValue(ctx, accContextKey, acc)
|
||||||
|
ctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{value: b})
|
||||||
|
return test{
|
||||||
|
ctx: ctx,
|
||||||
|
statusCode: 400,
|
||||||
|
ca: &mockCA{},
|
||||||
|
db: &acme.MockDB{
|
||||||
|
MockGetExternalAccountKeyByAccountID: func(ctx context.Context, provisionerID, accountID string) (*acme.ExternalAccountKey, error) {
|
||||||
|
assert.Equals(t, prov.GetID(), provisionerID)
|
||||||
|
assert.Equals(t, "accID", accountID)
|
||||||
|
return &acme.ExternalAccountKey{
|
||||||
|
Policy: &acme.Policy{
|
||||||
|
X509: acme.X509Policy{
|
||||||
|
Allowed: acme.PolicyNames{
|
||||||
|
DNSNames: []string{"*.local"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
err: acme.NewError(acme.ErrorRejectedIdentifierType, "not authorized"),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"fail/prov.AuthorizeOrderIdentifier-error": func(t *testing.T) test {
|
||||||
|
options := &provisioner.Options{
|
||||||
|
X509: &provisioner.X509Options{
|
||||||
|
AllowedNames: &policy.X509NameOptions{
|
||||||
|
DNSDomains: []string{"*.local"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
provWithPolicy := newACMEProvWithOptions(t, options)
|
||||||
|
provWithPolicy.RequireEAB = true
|
||||||
|
acc := &acme.Account{ID: "accID"}
|
||||||
|
fr := &NewOrderRequest{
|
||||||
|
Identifiers: []acme.Identifier{
|
||||||
|
{Type: "dns", Value: "zap.internal"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
b, err := json.Marshal(fr)
|
||||||
|
assert.FatalError(t, err)
|
||||||
|
ctx := acme.NewProvisionerContext(context.Background(), provWithPolicy)
|
||||||
|
ctx = context.WithValue(ctx, accContextKey, acc)
|
||||||
|
ctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{value: b})
|
||||||
|
return test{
|
||||||
|
ctx: ctx,
|
||||||
|
statusCode: 400,
|
||||||
|
ca: &mockCA{},
|
||||||
|
db: &acme.MockDB{
|
||||||
|
MockGetExternalAccountKeyByAccountID: func(ctx context.Context, provisionerID, accountID string) (*acme.ExternalAccountKey, error) {
|
||||||
|
assert.Equals(t, prov.GetID(), provisionerID)
|
||||||
|
assert.Equals(t, "accID", accountID)
|
||||||
|
return &acme.ExternalAccountKey{
|
||||||
|
Policy: &acme.Policy{
|
||||||
|
X509: acme.X509Policy{
|
||||||
|
Allowed: acme.PolicyNames{
|
||||||
|
DNSNames: []string{"*.internal"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
err: acme.NewError(acme.ErrorRejectedIdentifierType, "not authorized"),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"fail/ca.AreSANsAllowed-error": func(t *testing.T) test {
|
||||||
|
options := &provisioner.Options{
|
||||||
|
X509: &provisioner.X509Options{
|
||||||
|
AllowedNames: &policy.X509NameOptions{
|
||||||
|
DNSDomains: []string{"*.internal"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
provWithPolicy := newACMEProvWithOptions(t, options)
|
||||||
|
provWithPolicy.RequireEAB = true
|
||||||
|
acc := &acme.Account{ID: "accID"}
|
||||||
|
fr := &NewOrderRequest{
|
||||||
|
Identifiers: []acme.Identifier{
|
||||||
|
{Type: "dns", Value: "zap.internal"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
b, err := json.Marshal(fr)
|
||||||
|
assert.FatalError(t, err)
|
||||||
|
ctx := acme.NewProvisionerContext(context.Background(), provWithPolicy)
|
||||||
|
ctx = context.WithValue(ctx, accContextKey, acc)
|
||||||
|
ctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{value: b})
|
||||||
|
return test{
|
||||||
|
ctx: ctx,
|
||||||
|
statusCode: 400,
|
||||||
|
ca: &mockCA{
|
||||||
|
MockAreSANsallowed: func(ctx context.Context, sans []string) error {
|
||||||
|
return errors.New("force: not authorized by authority")
|
||||||
|
},
|
||||||
|
},
|
||||||
|
db: &acme.MockDB{
|
||||||
|
MockGetExternalAccountKeyByAccountID: func(ctx context.Context, provisionerID, accountID string) (*acme.ExternalAccountKey, error) {
|
||||||
|
assert.Equals(t, prov.GetID(), provisionerID)
|
||||||
|
assert.Equals(t, "accID", accountID)
|
||||||
|
return &acme.ExternalAccountKey{
|
||||||
|
Policy: &acme.Policy{
|
||||||
|
X509: acme.X509Policy{
|
||||||
|
Allowed: acme.PolicyNames{
|
||||||
|
DNSNames: []string{"*.internal"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
err: acme.NewError(acme.ErrorRejectedIdentifierType, "not authorized"),
|
||||||
|
}
|
||||||
|
},
|
||||||
"fail/error-h.newAuthorization": func(t *testing.T) test {
|
"fail/error-h.newAuthorization": func(t *testing.T) test {
|
||||||
acc := &acme.Account{ID: "accID"}
|
acc := &acme.Account{ID: "accID"}
|
||||||
fr := &NewOrderRequest{
|
fr := &NewOrderRequest{
|
||||||
|
@ -782,6 +1003,7 @@ func TestHandler_NewOrder(t *testing.T) {
|
||||||
return test{
|
return test{
|
||||||
ctx: ctx,
|
ctx: ctx,
|
||||||
statusCode: 500,
|
statusCode: 500,
|
||||||
|
ca: &mockCA{},
|
||||||
db: &acme.MockDB{
|
db: &acme.MockDB{
|
||||||
MockCreateChallenge: func(ctx context.Context, ch *acme.Challenge) error {
|
MockCreateChallenge: func(ctx context.Context, ch *acme.Challenge) error {
|
||||||
assert.Equals(t, ch.AccountID, "accID")
|
assert.Equals(t, ch.AccountID, "accID")
|
||||||
|
@ -791,6 +1013,11 @@ func TestHandler_NewOrder(t *testing.T) {
|
||||||
assert.Equals(t, ch.Value, "zap.internal")
|
assert.Equals(t, ch.Value, "zap.internal")
|
||||||
return errors.New("force")
|
return errors.New("force")
|
||||||
},
|
},
|
||||||
|
MockGetExternalAccountKeyByAccountID: func(ctx context.Context, provisionerID, accountID string) (*acme.ExternalAccountKey, error) {
|
||||||
|
assert.Equals(t, prov.GetID(), provisionerID)
|
||||||
|
assert.Equals(t, "accID", accountID)
|
||||||
|
return nil, nil
|
||||||
|
},
|
||||||
},
|
},
|
||||||
err: acme.NewErrorISE("error creating challenge: force"),
|
err: acme.NewErrorISE("error creating challenge: force"),
|
||||||
}
|
}
|
||||||
|
@ -815,6 +1042,7 @@ func TestHandler_NewOrder(t *testing.T) {
|
||||||
return test{
|
return test{
|
||||||
ctx: ctx,
|
ctx: ctx,
|
||||||
statusCode: 500,
|
statusCode: 500,
|
||||||
|
ca: &mockCA{},
|
||||||
db: &acme.MockDB{
|
db: &acme.MockDB{
|
||||||
MockCreateChallenge: func(ctx context.Context, ch *acme.Challenge) error {
|
MockCreateChallenge: func(ctx context.Context, ch *acme.Challenge) error {
|
||||||
switch count {
|
switch count {
|
||||||
|
@ -860,6 +1088,11 @@ func TestHandler_NewOrder(t *testing.T) {
|
||||||
assert.Equals(t, o.AuthorizationIDs, []string{*az1ID})
|
assert.Equals(t, o.AuthorizationIDs, []string{*az1ID})
|
||||||
return errors.New("force")
|
return errors.New("force")
|
||||||
},
|
},
|
||||||
|
MockGetExternalAccountKeyByAccountID: func(ctx context.Context, provisionerID, accountID string) (*acme.ExternalAccountKey, error) {
|
||||||
|
assert.Equals(t, prov.GetID(), provisionerID)
|
||||||
|
assert.Equals(t, "accID", accountID)
|
||||||
|
return nil, nil
|
||||||
|
},
|
||||||
},
|
},
|
||||||
err: acme.NewErrorISE("error creating order: force"),
|
err: acme.NewErrorISE("error creating order: force"),
|
||||||
}
|
}
|
||||||
|
@ -886,6 +1119,7 @@ func TestHandler_NewOrder(t *testing.T) {
|
||||||
ctx: ctx,
|
ctx: ctx,
|
||||||
statusCode: 201,
|
statusCode: 201,
|
||||||
nor: nor,
|
nor: nor,
|
||||||
|
ca: &mockCA{},
|
||||||
db: &acme.MockDB{
|
db: &acme.MockDB{
|
||||||
MockCreateChallenge: func(ctx context.Context, ch *acme.Challenge) error {
|
MockCreateChallenge: func(ctx context.Context, ch *acme.Challenge) error {
|
||||||
switch chCount {
|
switch chCount {
|
||||||
|
@ -955,6 +1189,11 @@ func TestHandler_NewOrder(t *testing.T) {
|
||||||
assert.Equals(t, o.AuthorizationIDs, []string{*az1ID, *az2ID})
|
assert.Equals(t, o.AuthorizationIDs, []string{*az1ID, *az2ID})
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
|
MockGetExternalAccountKeyByAccountID: func(ctx context.Context, provisionerID, accountID string) (*acme.ExternalAccountKey, error) {
|
||||||
|
assert.Equals(t, prov.GetID(), provisionerID)
|
||||||
|
assert.Equals(t, "accID", accountID)
|
||||||
|
return nil, nil
|
||||||
|
},
|
||||||
},
|
},
|
||||||
vr: func(t *testing.T, o *acme.Order) {
|
vr: func(t *testing.T, o *acme.Order) {
|
||||||
now := clock.Now()
|
now := clock.Now()
|
||||||
|
@ -1000,6 +1239,7 @@ func TestHandler_NewOrder(t *testing.T) {
|
||||||
ctx: ctx,
|
ctx: ctx,
|
||||||
statusCode: 201,
|
statusCode: 201,
|
||||||
nor: nor,
|
nor: nor,
|
||||||
|
ca: &mockCA{},
|
||||||
db: &acme.MockDB{
|
db: &acme.MockDB{
|
||||||
MockCreateChallenge: func(ctx context.Context, ch *acme.Challenge) error {
|
MockCreateChallenge: func(ctx context.Context, ch *acme.Challenge) error {
|
||||||
switch count {
|
switch count {
|
||||||
|
@ -1046,6 +1286,11 @@ func TestHandler_NewOrder(t *testing.T) {
|
||||||
assert.Equals(t, o.AuthorizationIDs, []string{*az1ID})
|
assert.Equals(t, o.AuthorizationIDs, []string{*az1ID})
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
|
MockGetExternalAccountKeyByAccountID: func(ctx context.Context, provisionerID, accountID string) (*acme.ExternalAccountKey, error) {
|
||||||
|
assert.Equals(t, prov.GetID(), provisionerID)
|
||||||
|
assert.Equals(t, "accID", accountID)
|
||||||
|
return nil, nil
|
||||||
|
},
|
||||||
},
|
},
|
||||||
vr: func(t *testing.T, o *acme.Order) {
|
vr: func(t *testing.T, o *acme.Order) {
|
||||||
now := clock.Now()
|
now := clock.Now()
|
||||||
|
@ -1091,6 +1336,7 @@ func TestHandler_NewOrder(t *testing.T) {
|
||||||
ctx: ctx,
|
ctx: ctx,
|
||||||
statusCode: 201,
|
statusCode: 201,
|
||||||
nor: nor,
|
nor: nor,
|
||||||
|
ca: &mockCA{},
|
||||||
db: &acme.MockDB{
|
db: &acme.MockDB{
|
||||||
MockCreateChallenge: func(ctx context.Context, ch *acme.Challenge) error {
|
MockCreateChallenge: func(ctx context.Context, ch *acme.Challenge) error {
|
||||||
switch count {
|
switch count {
|
||||||
|
@ -1137,6 +1383,11 @@ func TestHandler_NewOrder(t *testing.T) {
|
||||||
assert.Equals(t, o.AuthorizationIDs, []string{*az1ID})
|
assert.Equals(t, o.AuthorizationIDs, []string{*az1ID})
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
|
MockGetExternalAccountKeyByAccountID: func(ctx context.Context, provisionerID, accountID string) (*acme.ExternalAccountKey, error) {
|
||||||
|
assert.Equals(t, prov.GetID(), provisionerID)
|
||||||
|
assert.Equals(t, "accID", accountID)
|
||||||
|
return nil, nil
|
||||||
|
},
|
||||||
},
|
},
|
||||||
vr: func(t *testing.T, o *acme.Order) {
|
vr: func(t *testing.T, o *acme.Order) {
|
||||||
now := clock.Now()
|
now := clock.Now()
|
||||||
|
@ -1181,6 +1432,7 @@ func TestHandler_NewOrder(t *testing.T) {
|
||||||
ctx: ctx,
|
ctx: ctx,
|
||||||
statusCode: 201,
|
statusCode: 201,
|
||||||
nor: nor,
|
nor: nor,
|
||||||
|
ca: &mockCA{},
|
||||||
db: &acme.MockDB{
|
db: &acme.MockDB{
|
||||||
MockCreateChallenge: func(ctx context.Context, ch *acme.Challenge) error {
|
MockCreateChallenge: func(ctx context.Context, ch *acme.Challenge) error {
|
||||||
switch count {
|
switch count {
|
||||||
|
@ -1227,6 +1479,11 @@ func TestHandler_NewOrder(t *testing.T) {
|
||||||
assert.Equals(t, o.AuthorizationIDs, []string{*az1ID})
|
assert.Equals(t, o.AuthorizationIDs, []string{*az1ID})
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
|
MockGetExternalAccountKeyByAccountID: func(ctx context.Context, provisionerID, accountID string) (*acme.ExternalAccountKey, error) {
|
||||||
|
assert.Equals(t, prov.GetID(), provisionerID)
|
||||||
|
assert.Equals(t, "accID", accountID)
|
||||||
|
return nil, nil
|
||||||
|
},
|
||||||
},
|
},
|
||||||
vr: func(t *testing.T, o *acme.Order) {
|
vr: func(t *testing.T, o *acme.Order) {
|
||||||
testBufferDur := 5 * time.Second
|
testBufferDur := 5 * time.Second
|
||||||
|
@ -1272,6 +1529,7 @@ func TestHandler_NewOrder(t *testing.T) {
|
||||||
ctx: ctx,
|
ctx: ctx,
|
||||||
statusCode: 201,
|
statusCode: 201,
|
||||||
nor: nor,
|
nor: nor,
|
||||||
|
ca: &mockCA{},
|
||||||
db: &acme.MockDB{
|
db: &acme.MockDB{
|
||||||
MockCreateChallenge: func(ctx context.Context, ch *acme.Challenge) error {
|
MockCreateChallenge: func(ctx context.Context, ch *acme.Challenge) error {
|
||||||
switch count {
|
switch count {
|
||||||
|
@ -1318,11 +1576,119 @@ func TestHandler_NewOrder(t *testing.T) {
|
||||||
assert.Equals(t, o.AuthorizationIDs, []string{*az1ID})
|
assert.Equals(t, o.AuthorizationIDs, []string{*az1ID})
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
|
MockGetExternalAccountKeyByAccountID: func(ctx context.Context, provisionerID, accountID string) (*acme.ExternalAccountKey, error) {
|
||||||
|
assert.Equals(t, prov.GetID(), provisionerID)
|
||||||
|
assert.Equals(t, "accID", accountID)
|
||||||
|
return nil, nil
|
||||||
|
},
|
||||||
},
|
},
|
||||||
vr: func(t *testing.T, o *acme.Order) {
|
vr: func(t *testing.T, o *acme.Order) {
|
||||||
testBufferDur := 5 * time.Second
|
testBufferDur := 5 * time.Second
|
||||||
orderExpiry := now.Add(defaultOrderExpiry)
|
orderExpiry := now.Add(defaultOrderExpiry)
|
||||||
|
|
||||||
|
assert.Equals(t, o.ID, "ordID")
|
||||||
|
assert.Equals(t, o.Status, acme.StatusPending)
|
||||||
|
assert.Equals(t, o.Identifiers, nor.Identifiers)
|
||||||
|
assert.Equals(t, o.AuthorizationURLs, []string{fmt.Sprintf("%s/acme/%s/authz/az1ID", baseURL.String(), escProvName)})
|
||||||
|
assert.True(t, o.NotBefore.Add(-testBufferDur).Before(expNbf))
|
||||||
|
assert.True(t, o.NotBefore.Add(testBufferDur).After(expNbf))
|
||||||
|
assert.True(t, o.NotAfter.Add(-testBufferDur).Before(expNaf))
|
||||||
|
assert.True(t, o.NotAfter.Add(testBufferDur).After(expNaf))
|
||||||
|
assert.True(t, o.ExpiresAt.Add(-testBufferDur).Before(orderExpiry))
|
||||||
|
assert.True(t, o.ExpiresAt.Add(testBufferDur).After(orderExpiry))
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ok/default-naf-nbf-with-policy": func(t *testing.T) test {
|
||||||
|
options := &provisioner.Options{
|
||||||
|
X509: &provisioner.X509Options{
|
||||||
|
AllowedNames: &policy.X509NameOptions{
|
||||||
|
DNSDomains: []string{"*.internal"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
provWithPolicy := newACMEProvWithOptions(t, options)
|
||||||
|
provWithPolicy.RequireEAB = true
|
||||||
|
acc := &acme.Account{ID: "accID"}
|
||||||
|
nor := &NewOrderRequest{
|
||||||
|
Identifiers: []acme.Identifier{
|
||||||
|
{Type: "dns", Value: "zap.internal"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
b, err := json.Marshal(nor)
|
||||||
|
assert.FatalError(t, err)
|
||||||
|
ctx := acme.NewProvisionerContext(context.Background(), provWithPolicy)
|
||||||
|
ctx = context.WithValue(ctx, accContextKey, acc)
|
||||||
|
ctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{value: b})
|
||||||
|
var (
|
||||||
|
ch1, ch2, ch3 **acme.Challenge
|
||||||
|
az1ID *string
|
||||||
|
count = 0
|
||||||
|
)
|
||||||
|
return test{
|
||||||
|
ctx: ctx,
|
||||||
|
statusCode: 201,
|
||||||
|
nor: nor,
|
||||||
|
ca: &mockCA{},
|
||||||
|
db: &acme.MockDB{
|
||||||
|
MockCreateChallenge: func(ctx context.Context, ch *acme.Challenge) error {
|
||||||
|
switch count {
|
||||||
|
case 0:
|
||||||
|
ch.ID = "dns"
|
||||||
|
assert.Equals(t, ch.Type, acme.DNS01)
|
||||||
|
ch1 = &ch
|
||||||
|
case 1:
|
||||||
|
ch.ID = "http"
|
||||||
|
assert.Equals(t, ch.Type, acme.HTTP01)
|
||||||
|
ch2 = &ch
|
||||||
|
case 2:
|
||||||
|
ch.ID = "tls"
|
||||||
|
assert.Equals(t, ch.Type, acme.TLSALPN01)
|
||||||
|
ch3 = &ch
|
||||||
|
default:
|
||||||
|
assert.FatalError(t, errors.New("test logic error"))
|
||||||
|
return errors.New("force")
|
||||||
|
}
|
||||||
|
count++
|
||||||
|
assert.Equals(t, ch.AccountID, "accID")
|
||||||
|
assert.NotEquals(t, ch.Token, "")
|
||||||
|
assert.Equals(t, ch.Status, acme.StatusPending)
|
||||||
|
assert.Equals(t, ch.Value, "zap.internal")
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
MockCreateAuthorization: func(ctx context.Context, az *acme.Authorization) error {
|
||||||
|
az.ID = "az1ID"
|
||||||
|
az1ID = &az.ID
|
||||||
|
assert.Equals(t, az.AccountID, "accID")
|
||||||
|
assert.NotEquals(t, az.Token, "")
|
||||||
|
assert.Equals(t, az.Status, acme.StatusPending)
|
||||||
|
assert.Equals(t, az.Identifier, nor.Identifiers[0])
|
||||||
|
assert.Equals(t, az.Challenges, []*acme.Challenge{*ch1, *ch2, *ch3})
|
||||||
|
assert.Equals(t, az.Wildcard, false)
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
MockCreateOrder: func(ctx context.Context, o *acme.Order) error {
|
||||||
|
o.ID = "ordID"
|
||||||
|
assert.Equals(t, o.AccountID, "accID")
|
||||||
|
assert.Equals(t, o.ProvisionerID, prov.GetID())
|
||||||
|
assert.Equals(t, o.Status, acme.StatusPending)
|
||||||
|
assert.Equals(t, o.Identifiers, nor.Identifiers)
|
||||||
|
assert.Equals(t, o.AuthorizationIDs, []string{*az1ID})
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
MockGetExternalAccountKeyByAccountID: func(ctx context.Context, provisionerID, accountID string) (*acme.ExternalAccountKey, error) {
|
||||||
|
assert.Equals(t, prov.GetID(), provisionerID)
|
||||||
|
assert.Equals(t, "accID", accountID)
|
||||||
|
return nil, nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
vr: func(t *testing.T, o *acme.Order) {
|
||||||
|
now := clock.Now()
|
||||||
|
testBufferDur := 5 * time.Second
|
||||||
|
orderExpiry := now.Add(defaultOrderExpiry)
|
||||||
|
expNbf := now.Add(-defaultOrderBackdate)
|
||||||
|
expNaf := now.Add(prov.DefaultTLSCertDuration())
|
||||||
|
|
||||||
assert.Equals(t, o.ID, "ordID")
|
assert.Equals(t, o.ID, "ordID")
|
||||||
assert.Equals(t, o.Status, acme.StatusPending)
|
assert.Equals(t, o.Status, acme.StatusPending)
|
||||||
assert.Equals(t, o.Identifiers, nor.Identifiers)
|
assert.Equals(t, o.Identifiers, nor.Identifiers)
|
||||||
|
@ -1340,6 +1706,7 @@ func TestHandler_NewOrder(t *testing.T) {
|
||||||
for name, run := range tests {
|
for name, run := range tests {
|
||||||
tc := run(t)
|
tc := run(t)
|
||||||
t.Run(name, func(t *testing.T) {
|
t.Run(name, func(t *testing.T) {
|
||||||
|
mockMustAuthority(t, tc.ca)
|
||||||
ctx := newBaseContext(tc.ctx, tc.db, acme.NewLinker("test.ca.smallstep.com", "acme"))
|
ctx := newBaseContext(tc.ctx, tc.db, acme.NewLinker("test.ca.smallstep.com", "acme"))
|
||||||
req := httptest.NewRequest("GET", u, nil)
|
req := httptest.NewRequest("GET", u, nil)
|
||||||
req = req.WithContext(ctx)
|
req = req.WithContext(ctx)
|
||||||
|
@ -1493,7 +1860,7 @@ func TestHandler_FinalizeOrder(t *testing.T) {
|
||||||
db: &acme.MockDB{},
|
db: &acme.MockDB{},
|
||||||
ctx: ctx,
|
ctx: ctx,
|
||||||
statusCode: 500,
|
statusCode: 500,
|
||||||
err: acme.NewErrorISE("paylod does not exist"),
|
err: acme.NewErrorISE("payload does not exist"),
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"fail/unmarshal-payload-error": func(t *testing.T) test {
|
"fail/unmarshal-payload-error": func(t *testing.T) test {
|
||||||
|
|
|
@ -24,14 +24,16 @@ import (
|
||||||
"github.com/go-chi/chi"
|
"github.com/go-chi/chi"
|
||||||
"github.com/google/go-cmp/cmp"
|
"github.com/google/go-cmp/cmp"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
|
"golang.org/x/crypto/ocsp"
|
||||||
|
|
||||||
|
"go.step.sm/crypto/jose"
|
||||||
|
"go.step.sm/crypto/keyutil"
|
||||||
|
"go.step.sm/crypto/x509util"
|
||||||
|
|
||||||
"github.com/smallstep/assert"
|
"github.com/smallstep/assert"
|
||||||
"github.com/smallstep/certificates/acme"
|
"github.com/smallstep/certificates/acme"
|
||||||
"github.com/smallstep/certificates/authority"
|
"github.com/smallstep/certificates/authority"
|
||||||
"github.com/smallstep/certificates/authority/provisioner"
|
"github.com/smallstep/certificates/authority/provisioner"
|
||||||
"go.step.sm/crypto/jose"
|
|
||||||
"go.step.sm/crypto/keyutil"
|
|
||||||
"go.step.sm/crypto/x509util"
|
|
||||||
"golang.org/x/crypto/ocsp"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// v is a utility function to return the pointer to an integer
|
// v is a utility function to return the pointer to an integer
|
||||||
|
@ -274,14 +276,22 @@ func jwsFinal(sha crypto.Hash, sig []byte, phead, payload string) ([]byte, error
|
||||||
}
|
}
|
||||||
|
|
||||||
type mockCA struct {
|
type mockCA struct {
|
||||||
MockIsRevoked func(sn string) (bool, error)
|
MockIsRevoked func(sn string) (bool, error)
|
||||||
MockRevoke func(ctx context.Context, opts *authority.RevokeOptions) error
|
MockRevoke func(ctx context.Context, opts *authority.RevokeOptions) error
|
||||||
|
MockAreSANsallowed func(ctx context.Context, sans []string) error
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *mockCA) Sign(cr *x509.CertificateRequest, opts provisioner.SignOptions, signOpts ...provisioner.SignOption) ([]*x509.Certificate, error) {
|
func (m *mockCA) Sign(cr *x509.CertificateRequest, opts provisioner.SignOptions, signOpts ...provisioner.SignOption) ([]*x509.Certificate, error) {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *mockCA) AreSANsAllowed(ctx context.Context, sans []string) error {
|
||||||
|
if m.MockAreSANsallowed != nil {
|
||||||
|
return m.MockAreSANsallowed(ctx, sans)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (m *mockCA) IsRevoked(sn string) (bool, error) {
|
func (m *mockCA) IsRevoked(sn string) (bool, error) {
|
||||||
if m.MockIsRevoked != nil {
|
if m.MockIsRevoked != nil {
|
||||||
return m.MockIsRevoked(sn)
|
return m.MockIsRevoked(sn)
|
||||||
|
|
|
@ -22,6 +22,7 @@ var clock Clock
|
||||||
// CertificateAuthority is the interface implemented by a CA authority.
|
// CertificateAuthority is the interface implemented by a CA authority.
|
||||||
type CertificateAuthority interface {
|
type CertificateAuthority interface {
|
||||||
Sign(cr *x509.CertificateRequest, opts provisioner.SignOptions, signOpts ...provisioner.SignOption) ([]*x509.Certificate, error)
|
Sign(cr *x509.CertificateRequest, opts provisioner.SignOptions, signOpts ...provisioner.SignOption) ([]*x509.Certificate, error)
|
||||||
|
AreSANsAllowed(ctx context.Context, sans []string) error
|
||||||
IsRevoked(sn string) (bool, error)
|
IsRevoked(sn string) (bool, error)
|
||||||
Revoke(context.Context, *authority.RevokeOptions) error
|
Revoke(context.Context, *authority.RevokeOptions) error
|
||||||
LoadProvisionerByName(string) (provisioner.Interface, error)
|
LoadProvisionerByName(string) (provisioner.Interface, error)
|
||||||
|
@ -67,6 +68,7 @@ func PrerequisitesCheckerFromContext(ctx context.Context) (PrerequisitesChecker,
|
||||||
// Provisioner is an interface that implements a subset of the provisioner.Interface --
|
// Provisioner is an interface that implements a subset of the provisioner.Interface --
|
||||||
// only those methods required by the ACME api/authority.
|
// only those methods required by the ACME api/authority.
|
||||||
type Provisioner interface {
|
type Provisioner interface {
|
||||||
|
AuthorizeOrderIdentifier(ctx context.Context, identifier provisioner.ACMEIdentifier) error
|
||||||
AuthorizeSign(ctx context.Context, token string) ([]provisioner.SignOption, error)
|
AuthorizeSign(ctx context.Context, token string) ([]provisioner.SignOption, error)
|
||||||
AuthorizeRevoke(ctx context.Context, token string) error
|
AuthorizeRevoke(ctx context.Context, token string) error
|
||||||
GetID() string
|
GetID() string
|
||||||
|
@ -100,14 +102,15 @@ func MustProvisionerFromContext(ctx context.Context) Provisioner {
|
||||||
|
|
||||||
// MockProvisioner for testing
|
// MockProvisioner for testing
|
||||||
type MockProvisioner struct {
|
type MockProvisioner struct {
|
||||||
Mret1 interface{}
|
Mret1 interface{}
|
||||||
Merr error
|
Merr error
|
||||||
MgetID func() string
|
MgetID func() string
|
||||||
MgetName func() string
|
MgetName func() string
|
||||||
MauthorizeSign func(ctx context.Context, ott string) ([]provisioner.SignOption, error)
|
MauthorizeOrderIdentifier func(ctx context.Context, identifier provisioner.ACMEIdentifier) error
|
||||||
MauthorizeRevoke func(ctx context.Context, token string) error
|
MauthorizeSign func(ctx context.Context, ott string) ([]provisioner.SignOption, error)
|
||||||
MdefaultTLSCertDuration func() time.Duration
|
MauthorizeRevoke func(ctx context.Context, token string) error
|
||||||
MgetOptions func() *provisioner.Options
|
MdefaultTLSCertDuration func() time.Duration
|
||||||
|
MgetOptions func() *provisioner.Options
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetName mock
|
// GetName mock
|
||||||
|
@ -118,6 +121,14 @@ func (m *MockProvisioner) GetName() string {
|
||||||
return m.Mret1.(string)
|
return m.Mret1.(string)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AuthorizeOrderIdentifiers mock
|
||||||
|
func (m *MockProvisioner) AuthorizeOrderIdentifier(ctx context.Context, identifier provisioner.ACMEIdentifier) error {
|
||||||
|
if m.MauthorizeOrderIdentifier != nil {
|
||||||
|
return m.MauthorizeOrderIdentifier(ctx, identifier)
|
||||||
|
}
|
||||||
|
return m.Merr
|
||||||
|
}
|
||||||
|
|
||||||
// AuthorizeSign mock
|
// AuthorizeSign mock
|
||||||
func (m *MockProvisioner) AuthorizeSign(ctx context.Context, ott string) ([]provisioner.SignOption, error) {
|
func (m *MockProvisioner) AuthorizeSign(ctx context.Context, ott string) ([]provisioner.SignOption, error) {
|
||||||
if m.MauthorizeSign != nil {
|
if m.MauthorizeSign != nil {
|
||||||
|
|
12
acme/db.go
12
acme/db.go
|
@ -23,6 +23,7 @@ type DB interface {
|
||||||
GetExternalAccountKey(ctx context.Context, provisionerID, keyID string) (*ExternalAccountKey, error)
|
GetExternalAccountKey(ctx context.Context, provisionerID, keyID string) (*ExternalAccountKey, error)
|
||||||
GetExternalAccountKeys(ctx context.Context, provisionerID, cursor string, limit int) ([]*ExternalAccountKey, string, error)
|
GetExternalAccountKeys(ctx context.Context, provisionerID, cursor string, limit int) ([]*ExternalAccountKey, string, error)
|
||||||
GetExternalAccountKeyByReference(ctx context.Context, provisionerID, reference string) (*ExternalAccountKey, error)
|
GetExternalAccountKeyByReference(ctx context.Context, provisionerID, reference string) (*ExternalAccountKey, error)
|
||||||
|
GetExternalAccountKeyByAccountID(ctx context.Context, provisionerID, accountID string) (*ExternalAccountKey, error)
|
||||||
DeleteExternalAccountKey(ctx context.Context, provisionerID, keyID string) error
|
DeleteExternalAccountKey(ctx context.Context, provisionerID, keyID string) error
|
||||||
UpdateExternalAccountKey(ctx context.Context, provisionerID string, eak *ExternalAccountKey) error
|
UpdateExternalAccountKey(ctx context.Context, provisionerID string, eak *ExternalAccountKey) error
|
||||||
|
|
||||||
|
@ -83,6 +84,7 @@ type MockDB struct {
|
||||||
MockGetExternalAccountKey func(ctx context.Context, provisionerID, keyID 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)
|
MockGetExternalAccountKeys func(ctx context.Context, provisionerID, cursor string, limit int) ([]*ExternalAccountKey, string, error)
|
||||||
MockGetExternalAccountKeyByReference func(ctx context.Context, provisionerID, reference string) (*ExternalAccountKey, error)
|
MockGetExternalAccountKeyByReference func(ctx context.Context, provisionerID, reference string) (*ExternalAccountKey, error)
|
||||||
|
MockGetExternalAccountKeyByAccountID func(ctx context.Context, provisionerID, accountID string) (*ExternalAccountKey, error)
|
||||||
MockDeleteExternalAccountKey func(ctx context.Context, provisionerID, keyID string) error
|
MockDeleteExternalAccountKey func(ctx context.Context, provisionerID, keyID string) error
|
||||||
MockUpdateExternalAccountKey func(ctx context.Context, provisionerID string, eak *ExternalAccountKey) error
|
MockUpdateExternalAccountKey func(ctx context.Context, provisionerID string, eak *ExternalAccountKey) error
|
||||||
|
|
||||||
|
@ -191,6 +193,16 @@ func (m *MockDB) GetExternalAccountKeyByReference(ctx context.Context, provision
|
||||||
return m.MockRet1.(*ExternalAccountKey), m.MockError
|
return m.MockRet1.(*ExternalAccountKey), m.MockError
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetExternalAccountKeyByAccountID mock
|
||||||
|
func (m *MockDB) GetExternalAccountKeyByAccountID(ctx context.Context, provisionerID, accountID string) (*ExternalAccountKey, error) {
|
||||||
|
if m.MockGetExternalAccountKeyByAccountID != nil {
|
||||||
|
return m.MockGetExternalAccountKeyByAccountID(ctx, provisionerID, accountID)
|
||||||
|
} else if m.MockError != nil {
|
||||||
|
return nil, m.MockError
|
||||||
|
}
|
||||||
|
return m.MockRet1.(*ExternalAccountKey), m.MockError
|
||||||
|
}
|
||||||
|
|
||||||
// DeleteExternalAccountKey mock
|
// DeleteExternalAccountKey mock
|
||||||
func (m *MockDB) DeleteExternalAccountKey(ctx context.Context, provisionerID, keyID string) error {
|
func (m *MockDB) DeleteExternalAccountKey(ctx context.Context, provisionerID, keyID string) error {
|
||||||
if m.MockDeleteExternalAccountKey != nil {
|
if m.MockDeleteExternalAccountKey != nil {
|
||||||
|
|
|
@ -8,6 +8,7 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
|
|
||||||
"github.com/smallstep/certificates/acme"
|
"github.com/smallstep/certificates/acme"
|
||||||
nosqlDB "github.com/smallstep/nosql"
|
nosqlDB "github.com/smallstep/nosql"
|
||||||
)
|
)
|
||||||
|
@ -23,7 +24,7 @@ type dbExternalAccountKey struct {
|
||||||
ProvisionerID string `json:"provisionerID"`
|
ProvisionerID string `json:"provisionerID"`
|
||||||
Reference string `json:"reference"`
|
Reference string `json:"reference"`
|
||||||
AccountID string `json:"accountID,omitempty"`
|
AccountID string `json:"accountID,omitempty"`
|
||||||
KeyBytes []byte `json:"key"`
|
HmacKey []byte `json:"key"`
|
||||||
CreatedAt time.Time `json:"createdAt"`
|
CreatedAt time.Time `json:"createdAt"`
|
||||||
BoundAt time.Time `json:"boundAt"`
|
BoundAt time.Time `json:"boundAt"`
|
||||||
}
|
}
|
||||||
|
@ -72,7 +73,7 @@ func (db *DB) CreateExternalAccountKey(ctx context.Context, provisionerID, refer
|
||||||
ID: keyID,
|
ID: keyID,
|
||||||
ProvisionerID: provisionerID,
|
ProvisionerID: provisionerID,
|
||||||
Reference: reference,
|
Reference: reference,
|
||||||
KeyBytes: random,
|
HmacKey: random,
|
||||||
CreatedAt: clock.Now(),
|
CreatedAt: clock.Now(),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -99,7 +100,7 @@ func (db *DB) CreateExternalAccountKey(ctx context.Context, provisionerID, refer
|
||||||
ProvisionerID: dbeak.ProvisionerID,
|
ProvisionerID: dbeak.ProvisionerID,
|
||||||
Reference: dbeak.Reference,
|
Reference: dbeak.Reference,
|
||||||
AccountID: dbeak.AccountID,
|
AccountID: dbeak.AccountID,
|
||||||
KeyBytes: dbeak.KeyBytes,
|
HmacKey: dbeak.HmacKey,
|
||||||
CreatedAt: dbeak.CreatedAt,
|
CreatedAt: dbeak.CreatedAt,
|
||||||
BoundAt: dbeak.BoundAt,
|
BoundAt: dbeak.BoundAt,
|
||||||
}, nil
|
}, nil
|
||||||
|
@ -124,7 +125,7 @@ func (db *DB) GetExternalAccountKey(ctx context.Context, provisionerID, keyID st
|
||||||
ProvisionerID: dbeak.ProvisionerID,
|
ProvisionerID: dbeak.ProvisionerID,
|
||||||
Reference: dbeak.Reference,
|
Reference: dbeak.Reference,
|
||||||
AccountID: dbeak.AccountID,
|
AccountID: dbeak.AccountID,
|
||||||
KeyBytes: dbeak.KeyBytes,
|
HmacKey: dbeak.HmacKey,
|
||||||
CreatedAt: dbeak.CreatedAt,
|
CreatedAt: dbeak.CreatedAt,
|
||||||
BoundAt: dbeak.BoundAt,
|
BoundAt: dbeak.BoundAt,
|
||||||
}, nil
|
}, nil
|
||||||
|
@ -191,7 +192,7 @@ func (db *DB) GetExternalAccountKeys(ctx context.Context, provisionerID, cursor
|
||||||
}
|
}
|
||||||
keys = append(keys, &acme.ExternalAccountKey{
|
keys = append(keys, &acme.ExternalAccountKey{
|
||||||
ID: eak.ID,
|
ID: eak.ID,
|
||||||
KeyBytes: eak.KeyBytes,
|
HmacKey: eak.HmacKey,
|
||||||
ProvisionerID: eak.ProvisionerID,
|
ProvisionerID: eak.ProvisionerID,
|
||||||
Reference: eak.Reference,
|
Reference: eak.Reference,
|
||||||
AccountID: eak.AccountID,
|
AccountID: eak.AccountID,
|
||||||
|
@ -226,6 +227,10 @@ func (db *DB) GetExternalAccountKeyByReference(ctx context.Context, provisionerI
|
||||||
return db.GetExternalAccountKey(ctx, provisionerID, dbExternalAccountKeyReference.ExternalAccountKeyID)
|
return db.GetExternalAccountKey(ctx, provisionerID, dbExternalAccountKeyReference.ExternalAccountKeyID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (db *DB) GetExternalAccountKeyByAccountID(ctx context.Context, provisionerID, accountID string) (*acme.ExternalAccountKey, error) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (db *DB) UpdateExternalAccountKey(ctx context.Context, provisionerID string, eak *acme.ExternalAccountKey) error {
|
func (db *DB) UpdateExternalAccountKey(ctx context.Context, provisionerID string, eak *acme.ExternalAccountKey) error {
|
||||||
externalAccountKeyMutex.Lock()
|
externalAccountKeyMutex.Lock()
|
||||||
defer externalAccountKeyMutex.Unlock()
|
defer externalAccountKeyMutex.Unlock()
|
||||||
|
@ -252,7 +257,7 @@ func (db *DB) UpdateExternalAccountKey(ctx context.Context, provisionerID string
|
||||||
ProvisionerID: eak.ProvisionerID,
|
ProvisionerID: eak.ProvisionerID,
|
||||||
Reference: eak.Reference,
|
Reference: eak.Reference,
|
||||||
AccountID: eak.AccountID,
|
AccountID: eak.AccountID,
|
||||||
KeyBytes: eak.KeyBytes,
|
HmacKey: eak.HmacKey,
|
||||||
CreatedAt: eak.CreatedAt,
|
CreatedAt: eak.CreatedAt,
|
||||||
BoundAt: eak.BoundAt,
|
BoundAt: eak.BoundAt,
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,6 +8,7 @@ import (
|
||||||
|
|
||||||
"github.com/google/go-cmp/cmp"
|
"github.com/google/go-cmp/cmp"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
|
|
||||||
"github.com/smallstep/assert"
|
"github.com/smallstep/assert"
|
||||||
"github.com/smallstep/certificates/acme"
|
"github.com/smallstep/certificates/acme"
|
||||||
certdb "github.com/smallstep/certificates/db"
|
certdb "github.com/smallstep/certificates/db"
|
||||||
|
@ -32,7 +33,7 @@ func TestDB_getDBExternalAccountKey(t *testing.T) {
|
||||||
ProvisionerID: provID,
|
ProvisionerID: provID,
|
||||||
Reference: "ref",
|
Reference: "ref",
|
||||||
AccountID: "",
|
AccountID: "",
|
||||||
KeyBytes: []byte{1, 3, 3, 7},
|
HmacKey: []byte{1, 3, 3, 7},
|
||||||
CreatedAt: now,
|
CreatedAt: now,
|
||||||
}
|
}
|
||||||
b, err := json.Marshal(dbeak)
|
b, err := json.Marshal(dbeak)
|
||||||
|
@ -108,7 +109,7 @@ func TestDB_getDBExternalAccountKey(t *testing.T) {
|
||||||
}
|
}
|
||||||
} else if assert.Nil(t, tc.err) {
|
} else if assert.Nil(t, tc.err) {
|
||||||
assert.Equals(t, dbeak.ID, tc.dbeak.ID)
|
assert.Equals(t, dbeak.ID, tc.dbeak.ID)
|
||||||
assert.Equals(t, dbeak.KeyBytes, tc.dbeak.KeyBytes)
|
assert.Equals(t, dbeak.HmacKey, tc.dbeak.HmacKey)
|
||||||
assert.Equals(t, dbeak.ProvisionerID, tc.dbeak.ProvisionerID)
|
assert.Equals(t, dbeak.ProvisionerID, tc.dbeak.ProvisionerID)
|
||||||
assert.Equals(t, dbeak.Reference, tc.dbeak.Reference)
|
assert.Equals(t, dbeak.Reference, tc.dbeak.Reference)
|
||||||
assert.Equals(t, dbeak.CreatedAt, tc.dbeak.CreatedAt)
|
assert.Equals(t, dbeak.CreatedAt, tc.dbeak.CreatedAt)
|
||||||
|
@ -136,7 +137,7 @@ func TestDB_GetExternalAccountKey(t *testing.T) {
|
||||||
ProvisionerID: provID,
|
ProvisionerID: provID,
|
||||||
Reference: "ref",
|
Reference: "ref",
|
||||||
AccountID: "",
|
AccountID: "",
|
||||||
KeyBytes: []byte{1, 3, 3, 7},
|
HmacKey: []byte{1, 3, 3, 7},
|
||||||
CreatedAt: now,
|
CreatedAt: now,
|
||||||
}
|
}
|
||||||
b, err := json.Marshal(dbeak)
|
b, err := json.Marshal(dbeak)
|
||||||
|
@ -154,7 +155,7 @@ func TestDB_GetExternalAccountKey(t *testing.T) {
|
||||||
ProvisionerID: provID,
|
ProvisionerID: provID,
|
||||||
Reference: "ref",
|
Reference: "ref",
|
||||||
AccountID: "",
|
AccountID: "",
|
||||||
KeyBytes: []byte{1, 3, 3, 7},
|
HmacKey: []byte{1, 3, 3, 7},
|
||||||
CreatedAt: now,
|
CreatedAt: now,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -179,7 +180,7 @@ func TestDB_GetExternalAccountKey(t *testing.T) {
|
||||||
ProvisionerID: "aDifferentProvID",
|
ProvisionerID: "aDifferentProvID",
|
||||||
Reference: "ref",
|
Reference: "ref",
|
||||||
AccountID: "",
|
AccountID: "",
|
||||||
KeyBytes: []byte{1, 3, 3, 7},
|
HmacKey: []byte{1, 3, 3, 7},
|
||||||
CreatedAt: now,
|
CreatedAt: now,
|
||||||
}
|
}
|
||||||
b, err := json.Marshal(dbeak)
|
b, err := json.Marshal(dbeak)
|
||||||
|
@ -197,7 +198,7 @@ func TestDB_GetExternalAccountKey(t *testing.T) {
|
||||||
ProvisionerID: provID,
|
ProvisionerID: provID,
|
||||||
Reference: "ref",
|
Reference: "ref",
|
||||||
AccountID: "",
|
AccountID: "",
|
||||||
KeyBytes: []byte{1, 3, 3, 7},
|
HmacKey: []byte{1, 3, 3, 7},
|
||||||
CreatedAt: now,
|
CreatedAt: now,
|
||||||
},
|
},
|
||||||
acmeErr: acme.NewError(acme.ErrorUnauthorizedType, "provisioner does not match provisioner for which the EAB key was created"),
|
acmeErr: acme.NewError(acme.ErrorUnauthorizedType, "provisioner does not match provisioner for which the EAB key was created"),
|
||||||
|
@ -225,7 +226,7 @@ func TestDB_GetExternalAccountKey(t *testing.T) {
|
||||||
}
|
}
|
||||||
} else if assert.Nil(t, tc.err) {
|
} else if assert.Nil(t, tc.err) {
|
||||||
assert.Equals(t, eak.ID, tc.eak.ID)
|
assert.Equals(t, eak.ID, tc.eak.ID)
|
||||||
assert.Equals(t, eak.KeyBytes, tc.eak.KeyBytes)
|
assert.Equals(t, eak.HmacKey, tc.eak.HmacKey)
|
||||||
assert.Equals(t, eak.ProvisionerID, tc.eak.ProvisionerID)
|
assert.Equals(t, eak.ProvisionerID, tc.eak.ProvisionerID)
|
||||||
assert.Equals(t, eak.Reference, tc.eak.Reference)
|
assert.Equals(t, eak.Reference, tc.eak.Reference)
|
||||||
assert.Equals(t, eak.CreatedAt, tc.eak.CreatedAt)
|
assert.Equals(t, eak.CreatedAt, tc.eak.CreatedAt)
|
||||||
|
@ -255,7 +256,7 @@ func TestDB_GetExternalAccountKeyByReference(t *testing.T) {
|
||||||
ProvisionerID: provID,
|
ProvisionerID: provID,
|
||||||
Reference: ref,
|
Reference: ref,
|
||||||
AccountID: "",
|
AccountID: "",
|
||||||
KeyBytes: []byte{1, 3, 3, 7},
|
HmacKey: []byte{1, 3, 3, 7},
|
||||||
CreatedAt: now,
|
CreatedAt: now,
|
||||||
}
|
}
|
||||||
dbref := &dbExternalAccountKeyReference{
|
dbref := &dbExternalAccountKeyReference{
|
||||||
|
@ -288,7 +289,7 @@ func TestDB_GetExternalAccountKeyByReference(t *testing.T) {
|
||||||
ProvisionerID: provID,
|
ProvisionerID: provID,
|
||||||
Reference: ref,
|
Reference: ref,
|
||||||
AccountID: "",
|
AccountID: "",
|
||||||
KeyBytes: []byte{1, 3, 3, 7},
|
HmacKey: []byte{1, 3, 3, 7},
|
||||||
CreatedAt: now,
|
CreatedAt: now,
|
||||||
},
|
},
|
||||||
err: nil,
|
err: nil,
|
||||||
|
@ -392,7 +393,7 @@ func TestDB_GetExternalAccountKeyByReference(t *testing.T) {
|
||||||
assert.Equals(t, eak.AccountID, tc.eak.AccountID)
|
assert.Equals(t, eak.AccountID, tc.eak.AccountID)
|
||||||
assert.Equals(t, eak.BoundAt, tc.eak.BoundAt)
|
assert.Equals(t, eak.BoundAt, tc.eak.BoundAt)
|
||||||
assert.Equals(t, eak.CreatedAt, tc.eak.CreatedAt)
|
assert.Equals(t, eak.CreatedAt, tc.eak.CreatedAt)
|
||||||
assert.Equals(t, eak.KeyBytes, tc.eak.KeyBytes)
|
assert.Equals(t, eak.HmacKey, tc.eak.HmacKey)
|
||||||
assert.Equals(t, eak.ProvisionerID, tc.eak.ProvisionerID)
|
assert.Equals(t, eak.ProvisionerID, tc.eak.ProvisionerID)
|
||||||
assert.Equals(t, eak.Reference, tc.eak.Reference)
|
assert.Equals(t, eak.Reference, tc.eak.Reference)
|
||||||
}
|
}
|
||||||
|
@ -420,7 +421,7 @@ func TestDB_GetExternalAccountKeys(t *testing.T) {
|
||||||
ProvisionerID: provID,
|
ProvisionerID: provID,
|
||||||
Reference: ref,
|
Reference: ref,
|
||||||
AccountID: "",
|
AccountID: "",
|
||||||
KeyBytes: []byte{1, 3, 3, 7},
|
HmacKey: []byte{1, 3, 3, 7},
|
||||||
CreatedAt: now,
|
CreatedAt: now,
|
||||||
}
|
}
|
||||||
b1, err := json.Marshal(dbeak1)
|
b1, err := json.Marshal(dbeak1)
|
||||||
|
@ -430,7 +431,7 @@ func TestDB_GetExternalAccountKeys(t *testing.T) {
|
||||||
ProvisionerID: provID,
|
ProvisionerID: provID,
|
||||||
Reference: ref,
|
Reference: ref,
|
||||||
AccountID: "",
|
AccountID: "",
|
||||||
KeyBytes: []byte{1, 3, 3, 7},
|
HmacKey: []byte{1, 3, 3, 7},
|
||||||
CreatedAt: now,
|
CreatedAt: now,
|
||||||
}
|
}
|
||||||
b2, err := json.Marshal(dbeak2)
|
b2, err := json.Marshal(dbeak2)
|
||||||
|
@ -440,7 +441,7 @@ func TestDB_GetExternalAccountKeys(t *testing.T) {
|
||||||
ProvisionerID: "aDifferentProvID",
|
ProvisionerID: "aDifferentProvID",
|
||||||
Reference: ref,
|
Reference: ref,
|
||||||
AccountID: "",
|
AccountID: "",
|
||||||
KeyBytes: []byte{1, 3, 3, 7},
|
HmacKey: []byte{1, 3, 3, 7},
|
||||||
CreatedAt: now,
|
CreatedAt: now,
|
||||||
}
|
}
|
||||||
b3, err := json.Marshal(dbeak3)
|
b3, err := json.Marshal(dbeak3)
|
||||||
|
@ -513,7 +514,7 @@ func TestDB_GetExternalAccountKeys(t *testing.T) {
|
||||||
ProvisionerID: provID,
|
ProvisionerID: provID,
|
||||||
Reference: ref,
|
Reference: ref,
|
||||||
AccountID: "",
|
AccountID: "",
|
||||||
KeyBytes: []byte{1, 3, 3, 7},
|
HmacKey: []byte{1, 3, 3, 7},
|
||||||
CreatedAt: now,
|
CreatedAt: now,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -521,7 +522,7 @@ func TestDB_GetExternalAccountKeys(t *testing.T) {
|
||||||
ProvisionerID: provID,
|
ProvisionerID: provID,
|
||||||
Reference: ref,
|
Reference: ref,
|
||||||
AccountID: "",
|
AccountID: "",
|
||||||
KeyBytes: []byte{1, 3, 3, 7},
|
HmacKey: []byte{1, 3, 3, 7},
|
||||||
CreatedAt: now,
|
CreatedAt: now,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -598,7 +599,7 @@ func TestDB_GetExternalAccountKeys(t *testing.T) {
|
||||||
assert.Equals(t, "", nextCursor)
|
assert.Equals(t, "", nextCursor)
|
||||||
for i, eak := range eaks {
|
for i, eak := range eaks {
|
||||||
assert.Equals(t, eak.ID, tc.eaks[i].ID)
|
assert.Equals(t, eak.ID, tc.eaks[i].ID)
|
||||||
assert.Equals(t, eak.KeyBytes, tc.eaks[i].KeyBytes)
|
assert.Equals(t, eak.HmacKey, tc.eaks[i].HmacKey)
|
||||||
assert.Equals(t, eak.ProvisionerID, tc.eaks[i].ProvisionerID)
|
assert.Equals(t, eak.ProvisionerID, tc.eaks[i].ProvisionerID)
|
||||||
assert.Equals(t, eak.Reference, tc.eaks[i].Reference)
|
assert.Equals(t, eak.Reference, tc.eaks[i].Reference)
|
||||||
assert.Equals(t, eak.CreatedAt, tc.eaks[i].CreatedAt)
|
assert.Equals(t, eak.CreatedAt, tc.eaks[i].CreatedAt)
|
||||||
|
@ -627,7 +628,7 @@ func TestDB_DeleteExternalAccountKey(t *testing.T) {
|
||||||
ProvisionerID: provID,
|
ProvisionerID: provID,
|
||||||
Reference: ref,
|
Reference: ref,
|
||||||
AccountID: "",
|
AccountID: "",
|
||||||
KeyBytes: []byte{1, 3, 3, 7},
|
HmacKey: []byte{1, 3, 3, 7},
|
||||||
CreatedAt: now,
|
CreatedAt: now,
|
||||||
}
|
}
|
||||||
dbref := &dbExternalAccountKeyReference{
|
dbref := &dbExternalAccountKeyReference{
|
||||||
|
@ -707,7 +708,7 @@ func TestDB_DeleteExternalAccountKey(t *testing.T) {
|
||||||
ProvisionerID: "aDifferentProvID",
|
ProvisionerID: "aDifferentProvID",
|
||||||
Reference: ref,
|
Reference: ref,
|
||||||
AccountID: "",
|
AccountID: "",
|
||||||
KeyBytes: []byte{1, 3, 3, 7},
|
HmacKey: []byte{1, 3, 3, 7},
|
||||||
CreatedAt: now,
|
CreatedAt: now,
|
||||||
}
|
}
|
||||||
b, err := json.Marshal(dbeak)
|
b, err := json.Marshal(dbeak)
|
||||||
|
@ -730,7 +731,7 @@ func TestDB_DeleteExternalAccountKey(t *testing.T) {
|
||||||
ProvisionerID: provID,
|
ProvisionerID: provID,
|
||||||
Reference: ref,
|
Reference: ref,
|
||||||
AccountID: "",
|
AccountID: "",
|
||||||
KeyBytes: []byte{1, 3, 3, 7},
|
HmacKey: []byte{1, 3, 3, 7},
|
||||||
CreatedAt: now,
|
CreatedAt: now,
|
||||||
}
|
}
|
||||||
dbref := &dbExternalAccountKeyReference{
|
dbref := &dbExternalAccountKeyReference{
|
||||||
|
@ -780,7 +781,7 @@ func TestDB_DeleteExternalAccountKey(t *testing.T) {
|
||||||
ProvisionerID: provID,
|
ProvisionerID: provID,
|
||||||
Reference: ref,
|
Reference: ref,
|
||||||
AccountID: "",
|
AccountID: "",
|
||||||
KeyBytes: []byte{1, 3, 3, 7},
|
HmacKey: []byte{1, 3, 3, 7},
|
||||||
CreatedAt: now,
|
CreatedAt: now,
|
||||||
}
|
}
|
||||||
dbref := &dbExternalAccountKeyReference{
|
dbref := &dbExternalAccountKeyReference{
|
||||||
|
@ -830,7 +831,7 @@ func TestDB_DeleteExternalAccountKey(t *testing.T) {
|
||||||
ProvisionerID: provID,
|
ProvisionerID: provID,
|
||||||
Reference: ref,
|
Reference: ref,
|
||||||
AccountID: "",
|
AccountID: "",
|
||||||
KeyBytes: []byte{1, 3, 3, 7},
|
HmacKey: []byte{1, 3, 3, 7},
|
||||||
CreatedAt: now,
|
CreatedAt: now,
|
||||||
}
|
}
|
||||||
dbref := &dbExternalAccountKeyReference{
|
dbref := &dbExternalAccountKeyReference{
|
||||||
|
@ -953,7 +954,7 @@ func TestDB_CreateExternalAccountKey(t *testing.T) {
|
||||||
assert.Equals(t, string(key), dbeak.ID)
|
assert.Equals(t, string(key), dbeak.ID)
|
||||||
assert.Equals(t, eak.ProvisionerID, dbeak.ProvisionerID)
|
assert.Equals(t, eak.ProvisionerID, dbeak.ProvisionerID)
|
||||||
assert.Equals(t, eak.Reference, dbeak.Reference)
|
assert.Equals(t, eak.Reference, dbeak.Reference)
|
||||||
assert.Equals(t, 32, len(dbeak.KeyBytes))
|
assert.Equals(t, 32, len(dbeak.HmacKey))
|
||||||
assert.False(t, dbeak.CreatedAt.IsZero())
|
assert.False(t, dbeak.CreatedAt.IsZero())
|
||||||
assert.Equals(t, dbeak.AccountID, eak.AccountID)
|
assert.Equals(t, dbeak.AccountID, eak.AccountID)
|
||||||
assert.True(t, dbeak.BoundAt.IsZero())
|
assert.True(t, dbeak.BoundAt.IsZero())
|
||||||
|
@ -1078,7 +1079,7 @@ func TestDB_UpdateExternalAccountKey(t *testing.T) {
|
||||||
ProvisionerID: provID,
|
ProvisionerID: provID,
|
||||||
Reference: ref,
|
Reference: ref,
|
||||||
AccountID: "",
|
AccountID: "",
|
||||||
KeyBytes: []byte{1, 3, 3, 7},
|
HmacKey: []byte{1, 3, 3, 7},
|
||||||
CreatedAt: now,
|
CreatedAt: now,
|
||||||
}
|
}
|
||||||
b, err := json.Marshal(dbeak)
|
b, err := json.Marshal(dbeak)
|
||||||
|
@ -1096,7 +1097,7 @@ func TestDB_UpdateExternalAccountKey(t *testing.T) {
|
||||||
ProvisionerID: provID,
|
ProvisionerID: provID,
|
||||||
Reference: ref,
|
Reference: ref,
|
||||||
AccountID: "",
|
AccountID: "",
|
||||||
KeyBytes: []byte{1, 3, 3, 7},
|
HmacKey: []byte{1, 3, 3, 7},
|
||||||
CreatedAt: now,
|
CreatedAt: now,
|
||||||
}
|
}
|
||||||
return test{
|
return test{
|
||||||
|
@ -1120,7 +1121,7 @@ func TestDB_UpdateExternalAccountKey(t *testing.T) {
|
||||||
assert.Equals(t, dbNew.AccountID, dbeak.AccountID)
|
assert.Equals(t, dbNew.AccountID, dbeak.AccountID)
|
||||||
assert.Equals(t, dbNew.CreatedAt, dbeak.CreatedAt)
|
assert.Equals(t, dbNew.CreatedAt, dbeak.CreatedAt)
|
||||||
assert.Equals(t, dbNew.BoundAt, dbeak.BoundAt)
|
assert.Equals(t, dbNew.BoundAt, dbeak.BoundAt)
|
||||||
assert.Equals(t, dbNew.KeyBytes, dbeak.KeyBytes)
|
assert.Equals(t, dbNew.HmacKey, dbeak.HmacKey)
|
||||||
return nu, true, nil
|
return nu, true, nil
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -1148,7 +1149,7 @@ func TestDB_UpdateExternalAccountKey(t *testing.T) {
|
||||||
ProvisionerID: "aDifferentProvID",
|
ProvisionerID: "aDifferentProvID",
|
||||||
Reference: ref,
|
Reference: ref,
|
||||||
AccountID: "",
|
AccountID: "",
|
||||||
KeyBytes: []byte{1, 3, 3, 7},
|
HmacKey: []byte{1, 3, 3, 7},
|
||||||
CreatedAt: now,
|
CreatedAt: now,
|
||||||
}
|
}
|
||||||
b, err := json.Marshal(newDBEAK)
|
b, err := json.Marshal(newDBEAK)
|
||||||
|
@ -1174,7 +1175,7 @@ func TestDB_UpdateExternalAccountKey(t *testing.T) {
|
||||||
ProvisionerID: provID,
|
ProvisionerID: provID,
|
||||||
Reference: ref,
|
Reference: ref,
|
||||||
AccountID: "",
|
AccountID: "",
|
||||||
KeyBytes: []byte{1, 3, 3, 7},
|
HmacKey: []byte{1, 3, 3, 7},
|
||||||
CreatedAt: now,
|
CreatedAt: now,
|
||||||
}
|
}
|
||||||
b, err := json.Marshal(newDBEAK)
|
b, err := json.Marshal(newDBEAK)
|
||||||
|
@ -1200,7 +1201,7 @@ func TestDB_UpdateExternalAccountKey(t *testing.T) {
|
||||||
ProvisionerID: provID,
|
ProvisionerID: provID,
|
||||||
Reference: ref,
|
Reference: ref,
|
||||||
AccountID: "",
|
AccountID: "",
|
||||||
KeyBytes: []byte{1, 3, 3, 7},
|
HmacKey: []byte{1, 3, 3, 7},
|
||||||
CreatedAt: now,
|
CreatedAt: now,
|
||||||
}
|
}
|
||||||
b, err := json.Marshal(newDBEAK)
|
b, err := json.Marshal(newDBEAK)
|
||||||
|
@ -1237,7 +1238,7 @@ func TestDB_UpdateExternalAccountKey(t *testing.T) {
|
||||||
assert.Equals(t, dbeak.AccountID, tc.eak.AccountID)
|
assert.Equals(t, dbeak.AccountID, tc.eak.AccountID)
|
||||||
assert.Equals(t, dbeak.CreatedAt, tc.eak.CreatedAt)
|
assert.Equals(t, dbeak.CreatedAt, tc.eak.CreatedAt)
|
||||||
assert.Equals(t, dbeak.BoundAt, tc.eak.BoundAt)
|
assert.Equals(t, dbeak.BoundAt, tc.eak.BoundAt)
|
||||||
assert.Equals(t, dbeak.KeyBytes, tc.eak.KeyBytes)
|
assert.Equals(t, dbeak.HmacKey, tc.eak.HmacKey)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -268,6 +268,7 @@ func TestOrder_UpdateStatus(t *testing.T) {
|
||||||
|
|
||||||
type mockSignAuth struct {
|
type mockSignAuth struct {
|
||||||
sign func(csr *x509.CertificateRequest, signOpts provisioner.SignOptions, extraOpts ...provisioner.SignOption) ([]*x509.Certificate, error)
|
sign func(csr *x509.CertificateRequest, signOpts provisioner.SignOptions, extraOpts ...provisioner.SignOption) ([]*x509.Certificate, error)
|
||||||
|
areSANsAllowed func(ctx context.Context, sans []string) error
|
||||||
loadProvisionerByName func(string) (provisioner.Interface, error)
|
loadProvisionerByName func(string) (provisioner.Interface, error)
|
||||||
ret1, ret2 interface{}
|
ret1, ret2 interface{}
|
||||||
err error
|
err error
|
||||||
|
@ -282,6 +283,13 @@ func (m *mockSignAuth) Sign(csr *x509.CertificateRequest, signOpts provisioner.S
|
||||||
return []*x509.Certificate{m.ret1.(*x509.Certificate), m.ret2.(*x509.Certificate)}, m.err
|
return []*x509.Certificate{m.ret1.(*x509.Certificate), m.ret2.(*x509.Certificate)}, m.err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *mockSignAuth) AreSANsAllowed(ctx context.Context, sans []string) error {
|
||||||
|
if m.areSANsAllowed != nil {
|
||||||
|
return m.areSANsAllowed(ctx, sans)
|
||||||
|
}
|
||||||
|
return m.err
|
||||||
|
}
|
||||||
|
|
||||||
func (m *mockSignAuth) LoadProvisionerByName(name string) (provisioner.Interface, error) {
|
func (m *mockSignAuth) LoadProvisionerByName(name string) (provisioner.Interface, error) {
|
||||||
if m.loadProvisionerByName != nil {
|
if m.loadProvisionerByName != nil {
|
||||||
return m.loadProvisionerByName(name)
|
return m.loadProvisionerByName(name)
|
||||||
|
|
|
@ -3,16 +3,20 @@ package read
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"io"
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"google.golang.org/protobuf/encoding/protojson"
|
"google.golang.org/protobuf/encoding/protojson"
|
||||||
"google.golang.org/protobuf/proto"
|
"google.golang.org/protobuf/proto"
|
||||||
|
|
||||||
|
"github.com/smallstep/certificates/api/render"
|
||||||
"github.com/smallstep/certificates/errs"
|
"github.com/smallstep/certificates/errs"
|
||||||
)
|
)
|
||||||
|
|
||||||
// JSON reads JSON from the request body and stores it in the value
|
// JSON reads JSON from the request body and stores it in the value
|
||||||
// pointed by v.
|
// pointed to by v.
|
||||||
func JSON(r io.Reader, v interface{}) error {
|
func JSON(r io.Reader, v interface{}) error {
|
||||||
if err := json.NewDecoder(r).Decode(v); err != nil {
|
if err := json.NewDecoder(r).Decode(v); err != nil {
|
||||||
return errs.BadRequestErr(err, "error decoding json")
|
return errs.BadRequestErr(err, "error decoding json")
|
||||||
|
@ -21,11 +25,42 @@ func JSON(r io.Reader, v interface{}) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// ProtoJSON reads JSON from the request body and stores it in the value
|
// ProtoJSON reads JSON from the request body and stores it in the value
|
||||||
// pointed by v.
|
// pointed to by m.
|
||||||
func ProtoJSON(r io.Reader, m proto.Message) error {
|
func ProtoJSON(r io.Reader, m proto.Message) error {
|
||||||
data, err := io.ReadAll(r)
|
data, err := io.ReadAll(r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errs.BadRequestErr(err, "error reading request body")
|
return errs.BadRequestErr(err, "error reading request body")
|
||||||
}
|
}
|
||||||
return protojson.Unmarshal(data, m)
|
|
||||||
|
switch err := protojson.Unmarshal(data, m); {
|
||||||
|
case errors.Is(err, proto.Error):
|
||||||
|
return badProtoJSONError(err.Error())
|
||||||
|
default:
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// badProtoJSONError is an error type that is returned by ProtoJSON
|
||||||
|
// when a proto message cannot be unmarshaled. Usually this is caused
|
||||||
|
// by an error in the request body.
|
||||||
|
type badProtoJSONError string
|
||||||
|
|
||||||
|
// Error implements error for badProtoJSONError
|
||||||
|
func (e badProtoJSONError) Error() string {
|
||||||
|
return string(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render implements render.RenderableError for badProtoJSONError
|
||||||
|
func (e badProtoJSONError) Render(w http.ResponseWriter) {
|
||||||
|
v := struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
Detail string `json:"detail"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
}{
|
||||||
|
Type: "badRequest",
|
||||||
|
Detail: "bad request",
|
||||||
|
// trim the proto prefix for the message
|
||||||
|
Message: strings.TrimSpace(strings.TrimPrefix(e.Error(), "proto:")),
|
||||||
|
}
|
||||||
|
render.JSONStatus(w, v, http.StatusBadRequest)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,21 @@
|
||||||
package read
|
package read
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"io"
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
"reflect"
|
"reflect"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
"testing/iotest"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"google.golang.org/protobuf/proto"
|
||||||
|
"google.golang.org/protobuf/reflect/protoreflect"
|
||||||
|
|
||||||
|
"go.step.sm/linkedca"
|
||||||
|
|
||||||
"github.com/smallstep/certificates/errs"
|
"github.com/smallstep/certificates/errs"
|
||||||
)
|
)
|
||||||
|
@ -44,3 +55,110 @@ func TestJSON(t *testing.T) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestProtoJSON(t *testing.T) {
|
||||||
|
|
||||||
|
p := new(linkedca.Policy) // TODO(hs): can we use something different, so we don't need the import?
|
||||||
|
|
||||||
|
type args struct {
|
||||||
|
r io.Reader
|
||||||
|
m proto.Message
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
args args
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "fail/io.ReadAll",
|
||||||
|
args: args{
|
||||||
|
r: iotest.ErrReader(errors.New("read error")),
|
||||||
|
m: p,
|
||||||
|
},
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "fail/proto",
|
||||||
|
args: args{
|
||||||
|
r: strings.NewReader(`{?}`),
|
||||||
|
m: p,
|
||||||
|
},
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ok",
|
||||||
|
args: args{
|
||||||
|
r: strings.NewReader(`{"x509":{}}`),
|
||||||
|
m: p,
|
||||||
|
},
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
err := ProtoJSON(tt.args.r, tt.args.m)
|
||||||
|
if (err != nil) != tt.wantErr {
|
||||||
|
t.Errorf("ProtoJSON() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
if tt.wantErr {
|
||||||
|
switch err.(type) {
|
||||||
|
case badProtoJSONError:
|
||||||
|
assert.Contains(t, err.Error(), "syntax error")
|
||||||
|
case *errs.Error:
|
||||||
|
var ee *errs.Error
|
||||||
|
if errors.As(err, &ee) {
|
||||||
|
assert.Equal(t, http.StatusBadRequest, ee.Status)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.Equal(t, protoreflect.FullName("linkedca.Policy"), proto.MessageName(tt.args.m))
|
||||||
|
assert.True(t, proto.Equal(&linkedca.Policy{X509: &linkedca.X509Policy{}}, tt.args.m))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_badProtoJSONError_Render(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
e badProtoJSONError
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "bad proto normal space",
|
||||||
|
e: badProtoJSONError("proto: syntax error (line 1:2): invalid value ?"),
|
||||||
|
expected: "syntax error (line 1:2): invalid value ?",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "bad proto non breaking space",
|
||||||
|
e: badProtoJSONError("proto: syntax error (line 1:2): invalid value ?"),
|
||||||
|
expected: "syntax error (line 1:2): invalid value ?",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
tt.e.Render(w)
|
||||||
|
res := w.Result()
|
||||||
|
defer res.Body.Close()
|
||||||
|
|
||||||
|
data, err := io.ReadAll(res.Body)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
v := struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
Detail string `json:"detail"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
}{}
|
||||||
|
|
||||||
|
assert.NoError(t, json.Unmarshal(data, &v))
|
||||||
|
assert.Equal(t, "badRequest", v.Type)
|
||||||
|
assert.Equal(t, "bad request", v.Detail)
|
||||||
|
assert.Equal(t, "syntax error (line 1:2): invalid value ?", v.Message)
|
||||||
|
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,22 +1,15 @@
|
||||||
package api
|
package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/go-chi/chi"
|
|
||||||
|
|
||||||
"go.step.sm/linkedca"
|
"go.step.sm/linkedca"
|
||||||
|
"google.golang.org/protobuf/types/known/timestamppb"
|
||||||
|
|
||||||
|
"github.com/smallstep/certificates/acme"
|
||||||
"github.com/smallstep/certificates/api/render"
|
"github.com/smallstep/certificates/api/render"
|
||||||
"github.com/smallstep/certificates/authority/admin"
|
"github.com/smallstep/certificates/authority/admin"
|
||||||
"github.com/smallstep/certificates/authority/provisioner"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
// provisionerContextKey provisioner key
|
|
||||||
provisionerContextKey = ContextKey("provisioner")
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// CreateExternalAccountKeyRequest is the type for POST /admin/acme/eab requests
|
// CreateExternalAccountKeyRequest is the type for POST /admin/acme/eab requests
|
||||||
|
@ -40,57 +33,26 @@ type GetExternalAccountKeysResponse struct {
|
||||||
|
|
||||||
// requireEABEnabled is a middleware that ensures ACME EAB is enabled
|
// requireEABEnabled is a middleware that ensures ACME EAB is enabled
|
||||||
// before serving requests that act on ACME EAB credentials.
|
// before serving requests that act on ACME EAB credentials.
|
||||||
func requireEABEnabled(next nextHTTP) nextHTTP {
|
func requireEABEnabled(next http.HandlerFunc) http.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
ctx := r.Context()
|
ctx := r.Context()
|
||||||
provName := chi.URLParam(r, "provisionerName")
|
prov := linkedca.MustProvisionerFromContext(ctx)
|
||||||
eabEnabled, prov, err := provisionerHasEABEnabled(ctx, provName)
|
|
||||||
if err != nil {
|
acmeProvisioner := prov.GetDetails().GetACME()
|
||||||
render.Error(w, err)
|
if acmeProvisioner == nil {
|
||||||
|
render.Error(w, admin.NewErrorISE("error getting ACME details for provisioner '%s'", prov.GetName()))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if !eabEnabled {
|
|
||||||
render.Error(w, admin.NewError(admin.ErrorBadRequestType, "ACME EAB not enabled for provisioner %s", prov.GetName()))
|
if !acmeProvisioner.RequireEab {
|
||||||
|
render.Error(w, admin.NewError(admin.ErrorBadRequestType, "ACME EAB not enabled for provisioner '%s'", prov.GetName()))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
ctx = context.WithValue(ctx, provisionerContextKey, prov)
|
|
||||||
next(w, r.WithContext(ctx))
|
next(w, r)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// provisionerHasEABEnabled determines if the "requireEAB" setting for an ACME
|
|
||||||
// provisioner is set to true and thus has EAB enabled.
|
|
||||||
func provisionerHasEABEnabled(ctx context.Context, provisionerName string) (bool, *linkedca.Provisioner, error) {
|
|
||||||
var (
|
|
||||||
p provisioner.Interface
|
|
||||||
err error
|
|
||||||
)
|
|
||||||
|
|
||||||
auth := mustAuthority(ctx)
|
|
||||||
db := admin.MustFromContext(ctx)
|
|
||||||
|
|
||||||
if p, err = auth.LoadProvisionerByName(provisionerName); err != nil {
|
|
||||||
return false, nil, admin.WrapErrorISE(err, "error loading provisioner %s", provisionerName)
|
|
||||||
}
|
|
||||||
|
|
||||||
prov, err := 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
|
|
||||||
}
|
|
||||||
|
|
||||||
type acmeAdminResponderInterface interface {
|
type acmeAdminResponderInterface interface {
|
||||||
GetExternalAccountKeys(w http.ResponseWriter, r *http.Request)
|
GetExternalAccountKeys(w http.ResponseWriter, r *http.Request)
|
||||||
CreateExternalAccountKey(w http.ResponseWriter, r *http.Request)
|
CreateExternalAccountKey(w http.ResponseWriter, r *http.Request)
|
||||||
|
@ -119,3 +81,72 @@ func (h *ACMEAdminResponder) CreateExternalAccountKey(w http.ResponseWriter, r *
|
||||||
func (h *ACMEAdminResponder) DeleteExternalAccountKey(w http.ResponseWriter, r *http.Request) {
|
func (h *ACMEAdminResponder) DeleteExternalAccountKey(w http.ResponseWriter, r *http.Request) {
|
||||||
render.Error(w, admin.NewError(admin.ErrorNotImplementedType, "this functionality is currently only available in Certificate Manager: https://u.step.sm/cm"))
|
render.Error(w, admin.NewError(admin.ErrorNotImplementedType, "this functionality is currently only available in Certificate Manager: https://u.step.sm/cm"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func eakToLinked(k *acme.ExternalAccountKey) *linkedca.EABKey {
|
||||||
|
|
||||||
|
if k == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
eak := &linkedca.EABKey{
|
||||||
|
Id: k.ID,
|
||||||
|
HmacKey: k.HmacKey,
|
||||||
|
Provisioner: k.ProvisionerID,
|
||||||
|
Reference: k.Reference,
|
||||||
|
Account: k.AccountID,
|
||||||
|
CreatedAt: timestamppb.New(k.CreatedAt),
|
||||||
|
BoundAt: timestamppb.New(k.BoundAt),
|
||||||
|
}
|
||||||
|
|
||||||
|
if k.Policy != nil {
|
||||||
|
eak.Policy = &linkedca.Policy{
|
||||||
|
X509: &linkedca.X509Policy{
|
||||||
|
Allow: &linkedca.X509Names{},
|
||||||
|
Deny: &linkedca.X509Names{},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
eak.Policy.X509.Allow.Dns = k.Policy.X509.Allowed.DNSNames
|
||||||
|
eak.Policy.X509.Allow.Ips = k.Policy.X509.Allowed.IPRanges
|
||||||
|
eak.Policy.X509.Deny.Dns = k.Policy.X509.Denied.DNSNames
|
||||||
|
eak.Policy.X509.Deny.Ips = k.Policy.X509.Denied.IPRanges
|
||||||
|
eak.Policy.X509.AllowWildcardNames = k.Policy.X509.AllowWildcardNames
|
||||||
|
}
|
||||||
|
|
||||||
|
return eak
|
||||||
|
}
|
||||||
|
|
||||||
|
func linkedEAKToCertificates(k *linkedca.EABKey) *acme.ExternalAccountKey {
|
||||||
|
if k == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
eak := &acme.ExternalAccountKey{
|
||||||
|
ID: k.Id,
|
||||||
|
ProvisionerID: k.Provisioner,
|
||||||
|
Reference: k.Reference,
|
||||||
|
AccountID: k.Account,
|
||||||
|
HmacKey: k.HmacKey,
|
||||||
|
CreatedAt: k.CreatedAt.AsTime(),
|
||||||
|
BoundAt: k.BoundAt.AsTime(),
|
||||||
|
}
|
||||||
|
|
||||||
|
if policy := k.GetPolicy(); policy != nil {
|
||||||
|
eak.Policy = &acme.Policy{}
|
||||||
|
if x509 := policy.GetX509(); x509 != nil {
|
||||||
|
eak.Policy.X509 = acme.X509Policy{}
|
||||||
|
if allow := x509.GetAllow(); allow != nil {
|
||||||
|
eak.Policy.X509.Allowed = acme.PolicyNames{}
|
||||||
|
eak.Policy.X509.Allowed.DNSNames = allow.Dns
|
||||||
|
eak.Policy.X509.Allowed.IPRanges = allow.Ips
|
||||||
|
}
|
||||||
|
if deny := x509.GetDeny(); deny != nil {
|
||||||
|
eak.Policy.X509.Denied = acme.PolicyNames{}
|
||||||
|
eak.Policy.X509.Denied.DNSNames = deny.Dns
|
||||||
|
eak.Policy.X509.Denied.IPRanges = deny.Ips
|
||||||
|
}
|
||||||
|
eak.Policy.X509.AllowWildcardNames = x509.AllowWildcardNames
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return eak
|
||||||
|
}
|
||||||
|
|
|
@ -4,20 +4,24 @@ import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
|
"reflect"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/go-chi/chi"
|
"github.com/go-chi/chi"
|
||||||
"github.com/smallstep/assert"
|
|
||||||
"github.com/smallstep/certificates/authority/admin"
|
|
||||||
"github.com/smallstep/certificates/authority/provisioner"
|
|
||||||
"go.step.sm/linkedca"
|
|
||||||
"google.golang.org/protobuf/encoding/protojson"
|
"google.golang.org/protobuf/encoding/protojson"
|
||||||
"google.golang.org/protobuf/proto"
|
"google.golang.org/protobuf/proto"
|
||||||
|
"google.golang.org/protobuf/types/known/timestamppb"
|
||||||
|
|
||||||
|
"go.step.sm/linkedca"
|
||||||
|
|
||||||
|
"github.com/smallstep/assert"
|
||||||
|
"github.com/smallstep/certificates/acme"
|
||||||
|
"github.com/smallstep/certificates/authority/admin"
|
||||||
)
|
)
|
||||||
|
|
||||||
func readProtoJSON(r io.ReadCloser, m proto.Message) error {
|
func readProtoJSON(r io.ReadCloser, m proto.Message) error {
|
||||||
|
@ -43,107 +47,76 @@ func mockMustAuthority(t *testing.T, a adminAuthority) {
|
||||||
func TestHandler_requireEABEnabled(t *testing.T) {
|
func TestHandler_requireEABEnabled(t *testing.T) {
|
||||||
type test struct {
|
type test struct {
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
adminDB admin.DB
|
next http.HandlerFunc
|
||||||
auth adminAuthority
|
|
||||||
next nextHTTP
|
|
||||||
err *admin.Error
|
err *admin.Error
|
||||||
statusCode int
|
statusCode int
|
||||||
}
|
}
|
||||||
var tests = map[string]func(t *testing.T) test{
|
var tests = map[string]func(t *testing.T) test{
|
||||||
"fail/h.provisionerHasEABEnabled": func(t *testing.T) test {
|
"fail/prov.GetDetails": func(t *testing.T) test {
|
||||||
chiCtx := chi.NewRouteContext()
|
prov := &linkedca.Provisioner{
|
||||||
chiCtx.URLParams.Add("provisionerName", "provName")
|
Id: "provID",
|
||||||
ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx)
|
Name: "provName",
|
||||||
auth := &mockAdminAuthority{
|
|
||||||
MockLoadProvisionerByName: func(name string) (provisioner.Interface, error) {
|
|
||||||
assert.Equals(t, "provName", name)
|
|
||||||
return nil, errors.New("force")
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
err := admin.NewErrorISE("error loading provisioner provName: force")
|
ctx := linkedca.NewContextWithProvisioner(context.Background(), prov)
|
||||||
err.Message = "error loading provisioner provName: force"
|
err := admin.NewErrorISE("error getting ACME details for provisioner 'provName'")
|
||||||
|
err.Message = "error getting ACME details for provisioner 'provName'"
|
||||||
|
return test{
|
||||||
|
ctx: ctx,
|
||||||
|
err: err,
|
||||||
|
statusCode: 500,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"fail/prov.GetDetails.GetACME": func(t *testing.T) test {
|
||||||
|
prov := &linkedca.Provisioner{
|
||||||
|
Id: "provID",
|
||||||
|
Name: "provName",
|
||||||
|
Details: &linkedca.ProvisionerDetails{},
|
||||||
|
}
|
||||||
|
ctx := linkedca.NewContextWithProvisioner(context.Background(), prov)
|
||||||
|
err := admin.NewErrorISE("error getting ACME details for provisioner 'provName'")
|
||||||
|
err.Message = "error getting ACME details for provisioner 'provName'"
|
||||||
return test{
|
return test{
|
||||||
ctx: ctx,
|
ctx: ctx,
|
||||||
auth: auth,
|
|
||||||
adminDB: &admin.MockDB{},
|
|
||||||
err: err,
|
err: err,
|
||||||
statusCode: 500,
|
statusCode: 500,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ok/eab-disabled": func(t *testing.T) test {
|
"ok/eab-disabled": func(t *testing.T) test {
|
||||||
chiCtx := chi.NewRouteContext()
|
prov := &linkedca.Provisioner{
|
||||||
chiCtx.URLParams.Add("provisionerName", "provName")
|
Id: "provID",
|
||||||
ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx)
|
Name: "provName",
|
||||||
auth := &mockAdminAuthority{
|
Details: &linkedca.ProvisionerDetails{
|
||||||
MockLoadProvisionerByName: func(name string) (provisioner.Interface, error) {
|
Data: &linkedca.ProvisionerDetails_ACME{
|
||||||
assert.Equals(t, "provName", name)
|
ACME: &linkedca.ACMEProvisioner{
|
||||||
return &provisioner.MockProvisioner{
|
RequireEab: false,
|
||||||
MgetID: func() string {
|
|
||||||
return "provID"
|
|
||||||
},
|
},
|
||||||
}, nil
|
},
|
||||||
},
|
|
||||||
}
|
|
||||||
db := &admin.MockDB{
|
|
||||||
MockGetProvisioner: func(ctx context.Context, id string) (*linkedca.Provisioner, error) {
|
|
||||||
assert.Equals(t, "provID", id)
|
|
||||||
return &linkedca.Provisioner{
|
|
||||||
Id: "provID",
|
|
||||||
Name: "provName",
|
|
||||||
Details: &linkedca.ProvisionerDetails{
|
|
||||||
Data: &linkedca.ProvisionerDetails_ACME{
|
|
||||||
ACME: &linkedca.ACMEProvisioner{
|
|
||||||
RequireEab: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}, nil
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
ctx := linkedca.NewContextWithProvisioner(context.Background(), prov)
|
||||||
err := admin.NewError(admin.ErrorBadRequestType, "ACME EAB not enabled for provisioner provName")
|
err := admin.NewError(admin.ErrorBadRequestType, "ACME EAB not enabled for provisioner provName")
|
||||||
err.Message = "ACME EAB not enabled for provisioner provName"
|
err.Message = "ACME EAB not enabled for provisioner 'provName'"
|
||||||
return test{
|
return test{
|
||||||
ctx: ctx,
|
ctx: ctx,
|
||||||
auth: auth,
|
|
||||||
adminDB: db,
|
|
||||||
err: err,
|
err: err,
|
||||||
statusCode: 400,
|
statusCode: 400,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ok/eab-enabled": func(t *testing.T) test {
|
"ok/eab-enabled": func(t *testing.T) test {
|
||||||
chiCtx := chi.NewRouteContext()
|
prov := &linkedca.Provisioner{
|
||||||
chiCtx.URLParams.Add("provisionerName", "provName")
|
Id: "provID",
|
||||||
ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx)
|
Name: "provName",
|
||||||
auth := &mockAdminAuthority{
|
Details: &linkedca.ProvisionerDetails{
|
||||||
MockLoadProvisionerByName: func(name string) (provisioner.Interface, error) {
|
Data: &linkedca.ProvisionerDetails_ACME{
|
||||||
assert.Equals(t, "provName", name)
|
ACME: &linkedca.ACMEProvisioner{
|
||||||
return &provisioner.MockProvisioner{
|
RequireEab: true,
|
||||||
MgetID: func() string {
|
|
||||||
return "provID"
|
|
||||||
},
|
},
|
||||||
}, nil
|
},
|
||||||
},
|
|
||||||
}
|
|
||||||
db := &admin.MockDB{
|
|
||||||
MockGetProvisioner: func(ctx context.Context, id string) (*linkedca.Provisioner, error) {
|
|
||||||
assert.Equals(t, "provID", id)
|
|
||||||
return &linkedca.Provisioner{
|
|
||||||
Id: "provID",
|
|
||||||
Name: "provName",
|
|
||||||
Details: &linkedca.ProvisionerDetails{
|
|
||||||
Data: &linkedca.ProvisionerDetails_ACME{
|
|
||||||
ACME: &linkedca.ACMEProvisioner{
|
|
||||||
RequireEab: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}, nil
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
ctx := linkedca.NewContextWithProvisioner(context.Background(), prov)
|
||||||
return test{
|
return test{
|
||||||
ctx: ctx,
|
ctx: ctx,
|
||||||
auth: auth,
|
|
||||||
adminDB: db,
|
|
||||||
next: func(w http.ResponseWriter, r *http.Request) {
|
next: func(w http.ResponseWriter, r *http.Request) {
|
||||||
w.Write(nil) // mock response with status 200
|
w.Write(nil) // mock response with status 200
|
||||||
},
|
},
|
||||||
|
@ -155,10 +128,7 @@ func TestHandler_requireEABEnabled(t *testing.T) {
|
||||||
for name, prep := range tests {
|
for name, prep := range tests {
|
||||||
tc := prep(t)
|
tc := prep(t)
|
||||||
t.Run(name, func(t *testing.T) {
|
t.Run(name, func(t *testing.T) {
|
||||||
mockMustAuthority(t, tc.auth)
|
req := httptest.NewRequest("GET", "/foo", nil).WithContext(tc.ctx)
|
||||||
ctx := admin.NewContext(tc.ctx, tc.adminDB)
|
|
||||||
req := httptest.NewRequest("GET", "/foo", nil) // chi routing is prepared in test setup
|
|
||||||
req = req.WithContext(ctx)
|
|
||||||
w := httptest.NewRecorder()
|
w := httptest.NewRecorder()
|
||||||
requireEABEnabled(tc.next)(w, req)
|
requireEABEnabled(tc.next)(w, req)
|
||||||
res := w.Result()
|
res := w.Result()
|
||||||
|
@ -184,214 +154,6 @@ func TestHandler_requireEABEnabled(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestHandler_provisionerHasEABEnabled(t *testing.T) {
|
|
||||||
type test struct {
|
|
||||||
adminDB admin.DB
|
|
||||||
auth adminAuthority
|
|
||||||
provisionerName string
|
|
||||||
want bool
|
|
||||||
err *admin.Error
|
|
||||||
}
|
|
||||||
var tests = map[string]func(t *testing.T) test{
|
|
||||||
"fail/auth.LoadProvisionerByName": func(t *testing.T) test {
|
|
||||||
auth := &mockAdminAuthority{
|
|
||||||
MockLoadProvisionerByName: func(name string) (provisioner.Interface, error) {
|
|
||||||
assert.Equals(t, "provName", name)
|
|
||||||
return nil, errors.New("force")
|
|
||||||
},
|
|
||||||
}
|
|
||||||
return test{
|
|
||||||
auth: auth,
|
|
||||||
adminDB: &admin.MockDB{},
|
|
||||||
provisionerName: "provName",
|
|
||||||
want: false,
|
|
||||||
err: admin.WrapErrorISE(errors.New("force"), "error loading provisioner provName"),
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"fail/db.GetProvisioner": func(t *testing.T) test {
|
|
||||||
auth := &mockAdminAuthority{
|
|
||||||
MockLoadProvisionerByName: func(name string) (provisioner.Interface, error) {
|
|
||||||
assert.Equals(t, "provName", name)
|
|
||||||
return &provisioner.MockProvisioner{
|
|
||||||
MgetID: func() string {
|
|
||||||
return "provID"
|
|
||||||
},
|
|
||||||
}, nil
|
|
||||||
},
|
|
||||||
}
|
|
||||||
db := &admin.MockDB{
|
|
||||||
MockGetProvisioner: func(ctx context.Context, id string) (*linkedca.Provisioner, error) {
|
|
||||||
assert.Equals(t, "provID", id)
|
|
||||||
return nil, errors.New("force")
|
|
||||||
},
|
|
||||||
}
|
|
||||||
return test{
|
|
||||||
auth: auth,
|
|
||||||
adminDB: db,
|
|
||||||
provisionerName: "provName",
|
|
||||||
want: false,
|
|
||||||
err: admin.WrapErrorISE(errors.New("force"), "error loading provisioner provName"),
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"fail/prov.GetDetails": func(t *testing.T) test {
|
|
||||||
auth := &mockAdminAuthority{
|
|
||||||
MockLoadProvisionerByName: func(name string) (provisioner.Interface, error) {
|
|
||||||
assert.Equals(t, "provName", name)
|
|
||||||
return &provisioner.MockProvisioner{
|
|
||||||
MgetID: func() string {
|
|
||||||
return "provID"
|
|
||||||
},
|
|
||||||
}, nil
|
|
||||||
},
|
|
||||||
}
|
|
||||||
db := &admin.MockDB{
|
|
||||||
MockGetProvisioner: func(ctx context.Context, id string) (*linkedca.Provisioner, error) {
|
|
||||||
assert.Equals(t, "provID", id)
|
|
||||||
return &linkedca.Provisioner{
|
|
||||||
Id: "provID",
|
|
||||||
Name: "provName",
|
|
||||||
Details: nil,
|
|
||||||
}, nil
|
|
||||||
},
|
|
||||||
}
|
|
||||||
return test{
|
|
||||||
auth: auth,
|
|
||||||
adminDB: db,
|
|
||||||
provisionerName: "provName",
|
|
||||||
want: false,
|
|
||||||
err: admin.WrapErrorISE(errors.New("force"), "error loading provisioner provName"),
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"fail/details.GetACME": func(t *testing.T) test {
|
|
||||||
auth := &mockAdminAuthority{
|
|
||||||
MockLoadProvisionerByName: func(name string) (provisioner.Interface, error) {
|
|
||||||
assert.Equals(t, "provName", name)
|
|
||||||
return &provisioner.MockProvisioner{
|
|
||||||
MgetID: func() string {
|
|
||||||
return "provID"
|
|
||||||
},
|
|
||||||
}, nil
|
|
||||||
},
|
|
||||||
}
|
|
||||||
db := &admin.MockDB{
|
|
||||||
MockGetProvisioner: func(ctx context.Context, id string) (*linkedca.Provisioner, error) {
|
|
||||||
assert.Equals(t, "provID", id)
|
|
||||||
return &linkedca.Provisioner{
|
|
||||||
Id: "provID",
|
|
||||||
Name: "provName",
|
|
||||||
Details: &linkedca.ProvisionerDetails{
|
|
||||||
Data: &linkedca.ProvisionerDetails_ACME{
|
|
||||||
ACME: nil,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}, nil
|
|
||||||
},
|
|
||||||
}
|
|
||||||
return test{
|
|
||||||
auth: auth,
|
|
||||||
adminDB: db,
|
|
||||||
provisionerName: "provName",
|
|
||||||
want: false,
|
|
||||||
err: admin.WrapErrorISE(errors.New("force"), "error loading provisioner provName"),
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"ok/eab-disabled": func(t *testing.T) test {
|
|
||||||
auth := &mockAdminAuthority{
|
|
||||||
MockLoadProvisionerByName: func(name string) (provisioner.Interface, error) {
|
|
||||||
assert.Equals(t, "eab-disabled", name)
|
|
||||||
return &provisioner.MockProvisioner{
|
|
||||||
MgetID: func() string {
|
|
||||||
return "provID"
|
|
||||||
},
|
|
||||||
}, nil
|
|
||||||
},
|
|
||||||
}
|
|
||||||
db := &admin.MockDB{
|
|
||||||
MockGetProvisioner: func(ctx context.Context, id string) (*linkedca.Provisioner, error) {
|
|
||||||
assert.Equals(t, "provID", id)
|
|
||||||
return &linkedca.Provisioner{
|
|
||||||
Id: "provID",
|
|
||||||
Name: "eab-disabled",
|
|
||||||
Details: &linkedca.ProvisionerDetails{
|
|
||||||
Data: &linkedca.ProvisionerDetails_ACME{
|
|
||||||
ACME: &linkedca.ACMEProvisioner{
|
|
||||||
RequireEab: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}, nil
|
|
||||||
},
|
|
||||||
}
|
|
||||||
return test{
|
|
||||||
adminDB: db,
|
|
||||||
auth: auth,
|
|
||||||
provisionerName: "eab-disabled",
|
|
||||||
want: false,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"ok/eab-enabled": func(t *testing.T) test {
|
|
||||||
auth := &mockAdminAuthority{
|
|
||||||
MockLoadProvisionerByName: func(name string) (provisioner.Interface, error) {
|
|
||||||
assert.Equals(t, "eab-enabled", name)
|
|
||||||
return &provisioner.MockProvisioner{
|
|
||||||
MgetID: func() string {
|
|
||||||
return "provID"
|
|
||||||
},
|
|
||||||
}, nil
|
|
||||||
},
|
|
||||||
}
|
|
||||||
db := &admin.MockDB{
|
|
||||||
MockGetProvisioner: func(ctx context.Context, id string) (*linkedca.Provisioner, error) {
|
|
||||||
assert.Equals(t, "provID", id)
|
|
||||||
return &linkedca.Provisioner{
|
|
||||||
Id: "provID",
|
|
||||||
Name: "eab-enabled",
|
|
||||||
Details: &linkedca.ProvisionerDetails{
|
|
||||||
Data: &linkedca.ProvisionerDetails_ACME{
|
|
||||||
ACME: &linkedca.ACMEProvisioner{
|
|
||||||
RequireEab: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}, nil
|
|
||||||
},
|
|
||||||
}
|
|
||||||
return test{
|
|
||||||
adminDB: db,
|
|
||||||
auth: auth,
|
|
||||||
provisionerName: "eab-enabled",
|
|
||||||
want: true,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
for name, prep := range tests {
|
|
||||||
tc := prep(t)
|
|
||||||
t.Run(name, func(t *testing.T) {
|
|
||||||
mockMustAuthority(t, tc.auth)
|
|
||||||
ctx := admin.NewContext(context.Background(), tc.adminDB)
|
|
||||||
got, prov, err := provisionerHasEABEnabled(ctx, tc.provisionerName)
|
|
||||||
if (err != nil) != (tc.err != nil) {
|
|
||||||
t.Errorf("Handler.provisionerHasEABEnabled() error = %v, want err %v", err, tc.err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if tc.err != nil {
|
|
||||||
assert.Type(t, &linkedca.Provisioner{}, prov)
|
|
||||||
assert.Type(t, &admin.Error{}, err)
|
|
||||||
adminError, _ := err.(*admin.Error)
|
|
||||||
assert.Equals(t, tc.err.Type, adminError.Type)
|
|
||||||
assert.Equals(t, tc.err.Status, adminError.Status)
|
|
||||||
assert.Equals(t, tc.err.StatusCode(), adminError.StatusCode())
|
|
||||||
assert.Equals(t, tc.err.Message, adminError.Message)
|
|
||||||
assert.Equals(t, tc.err.Detail, adminError.Detail)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if got != tc.want {
|
|
||||||
t.Errorf("Handler.provisionerHasEABEnabled() = %v, want %v", got, tc.want)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCreateExternalAccountKeyRequest_Validate(t *testing.T) {
|
func TestCreateExternalAccountKeyRequest_Validate(t *testing.T) {
|
||||||
type fields struct {
|
type fields struct {
|
||||||
Reference string
|
Reference string
|
||||||
|
@ -591,3 +353,206 @@ func TestHandler_GetExternalAccountKeys(t *testing.T) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func Test_eakToLinked(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
k *acme.ExternalAccountKey
|
||||||
|
want *linkedca.EABKey
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "no-key",
|
||||||
|
k: nil,
|
||||||
|
want: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "no-policy",
|
||||||
|
k: &acme.ExternalAccountKey{
|
||||||
|
ID: "keyID",
|
||||||
|
ProvisionerID: "provID",
|
||||||
|
Reference: "ref",
|
||||||
|
AccountID: "accID",
|
||||||
|
HmacKey: []byte{1, 3, 3, 7},
|
||||||
|
CreatedAt: time.Date(2022, 04, 12, 9, 30, 30, 0, time.UTC).Add(-1 * time.Hour),
|
||||||
|
BoundAt: time.Date(2022, 04, 12, 9, 30, 30, 0, time.UTC),
|
||||||
|
Policy: nil,
|
||||||
|
},
|
||||||
|
want: &linkedca.EABKey{
|
||||||
|
Id: "keyID",
|
||||||
|
Provisioner: "provID",
|
||||||
|
HmacKey: []byte{1, 3, 3, 7},
|
||||||
|
Reference: "ref",
|
||||||
|
Account: "accID",
|
||||||
|
CreatedAt: timestamppb.New(time.Date(2022, 04, 12, 9, 30, 30, 0, time.UTC).Add(-1 * time.Hour)),
|
||||||
|
BoundAt: timestamppb.New(time.Date(2022, 04, 12, 9, 30, 30, 0, time.UTC)),
|
||||||
|
Policy: nil,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "with-policy",
|
||||||
|
k: &acme.ExternalAccountKey{
|
||||||
|
ID: "keyID",
|
||||||
|
ProvisionerID: "provID",
|
||||||
|
Reference: "ref",
|
||||||
|
AccountID: "accID",
|
||||||
|
HmacKey: []byte{1, 3, 3, 7},
|
||||||
|
CreatedAt: time.Date(2022, 04, 12, 9, 30, 30, 0, time.UTC).Add(-1 * time.Hour),
|
||||||
|
BoundAt: time.Date(2022, 04, 12, 9, 30, 30, 0, time.UTC),
|
||||||
|
Policy: &acme.Policy{
|
||||||
|
X509: acme.X509Policy{
|
||||||
|
Allowed: acme.PolicyNames{
|
||||||
|
DNSNames: []string{"*.local"},
|
||||||
|
IPRanges: []string{"10.0.0.0/24"},
|
||||||
|
},
|
||||||
|
Denied: acme.PolicyNames{
|
||||||
|
DNSNames: []string{"badhost.local"},
|
||||||
|
IPRanges: []string{"10.0.0.30"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: &linkedca.EABKey{
|
||||||
|
Id: "keyID",
|
||||||
|
Provisioner: "provID",
|
||||||
|
HmacKey: []byte{1, 3, 3, 7},
|
||||||
|
Reference: "ref",
|
||||||
|
Account: "accID",
|
||||||
|
CreatedAt: timestamppb.New(time.Date(2022, 04, 12, 9, 30, 30, 0, time.UTC).Add(-1 * time.Hour)),
|
||||||
|
BoundAt: timestamppb.New(time.Date(2022, 04, 12, 9, 30, 30, 0, time.UTC)),
|
||||||
|
Policy: &linkedca.Policy{
|
||||||
|
X509: &linkedca.X509Policy{
|
||||||
|
Allow: &linkedca.X509Names{
|
||||||
|
Dns: []string{"*.local"},
|
||||||
|
Ips: []string{"10.0.0.0/24"},
|
||||||
|
},
|
||||||
|
Deny: &linkedca.X509Names{
|
||||||
|
Dns: []string{"badhost.local"},
|
||||||
|
Ips: []string{"10.0.0.30"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
if got := eakToLinked(tt.k); !reflect.DeepEqual(got, tt.want) {
|
||||||
|
t.Errorf("eakToLinked() = %v, want %v", got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_linkedEAKToCertificates(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
k *linkedca.EABKey
|
||||||
|
want *acme.ExternalAccountKey
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "no-key",
|
||||||
|
k: nil,
|
||||||
|
want: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "no-policy",
|
||||||
|
k: &linkedca.EABKey{
|
||||||
|
Id: "keyID",
|
||||||
|
Provisioner: "provID",
|
||||||
|
HmacKey: []byte{1, 3, 3, 7},
|
||||||
|
Reference: "ref",
|
||||||
|
Account: "accID",
|
||||||
|
CreatedAt: timestamppb.New(time.Date(2022, 04, 12, 9, 30, 30, 0, time.UTC).Add(-1 * time.Hour)),
|
||||||
|
BoundAt: timestamppb.New(time.Date(2022, 04, 12, 9, 30, 30, 0, time.UTC)),
|
||||||
|
Policy: nil,
|
||||||
|
},
|
||||||
|
want: &acme.ExternalAccountKey{
|
||||||
|
ID: "keyID",
|
||||||
|
ProvisionerID: "provID",
|
||||||
|
Reference: "ref",
|
||||||
|
AccountID: "accID",
|
||||||
|
HmacKey: []byte{1, 3, 3, 7},
|
||||||
|
CreatedAt: time.Date(2022, 04, 12, 9, 30, 30, 0, time.UTC).Add(-1 * time.Hour),
|
||||||
|
BoundAt: time.Date(2022, 04, 12, 9, 30, 30, 0, time.UTC),
|
||||||
|
Policy: nil,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "no-x509-policy",
|
||||||
|
k: &linkedca.EABKey{
|
||||||
|
Id: "keyID",
|
||||||
|
Provisioner: "provID",
|
||||||
|
HmacKey: []byte{1, 3, 3, 7},
|
||||||
|
Reference: "ref",
|
||||||
|
Account: "accID",
|
||||||
|
CreatedAt: timestamppb.New(time.Date(2022, 04, 12, 9, 30, 30, 0, time.UTC).Add(-1 * time.Hour)),
|
||||||
|
BoundAt: timestamppb.New(time.Date(2022, 04, 12, 9, 30, 30, 0, time.UTC)),
|
||||||
|
Policy: &linkedca.Policy{},
|
||||||
|
},
|
||||||
|
want: &acme.ExternalAccountKey{
|
||||||
|
ID: "keyID",
|
||||||
|
ProvisionerID: "provID",
|
||||||
|
Reference: "ref",
|
||||||
|
AccountID: "accID",
|
||||||
|
HmacKey: []byte{1, 3, 3, 7},
|
||||||
|
CreatedAt: time.Date(2022, 04, 12, 9, 30, 30, 0, time.UTC).Add(-1 * time.Hour),
|
||||||
|
BoundAt: time.Date(2022, 04, 12, 9, 30, 30, 0, time.UTC),
|
||||||
|
Policy: &acme.Policy{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "with-x509-policy",
|
||||||
|
k: &linkedca.EABKey{
|
||||||
|
Id: "keyID",
|
||||||
|
Provisioner: "provID",
|
||||||
|
HmacKey: []byte{1, 3, 3, 7},
|
||||||
|
Reference: "ref",
|
||||||
|
Account: "accID",
|
||||||
|
CreatedAt: timestamppb.New(time.Date(2022, 04, 12, 9, 30, 30, 0, time.UTC).Add(-1 * time.Hour)),
|
||||||
|
BoundAt: timestamppb.New(time.Date(2022, 04, 12, 9, 30, 30, 0, time.UTC)),
|
||||||
|
Policy: &linkedca.Policy{
|
||||||
|
X509: &linkedca.X509Policy{
|
||||||
|
Allow: &linkedca.X509Names{
|
||||||
|
Dns: []string{"*.local"},
|
||||||
|
Ips: []string{"10.0.0.0/24"},
|
||||||
|
},
|
||||||
|
Deny: &linkedca.X509Names{
|
||||||
|
Dns: []string{"badhost.local"},
|
||||||
|
Ips: []string{"10.0.0.30"},
|
||||||
|
},
|
||||||
|
AllowWildcardNames: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: &acme.ExternalAccountKey{
|
||||||
|
ID: "keyID",
|
||||||
|
ProvisionerID: "provID",
|
||||||
|
Reference: "ref",
|
||||||
|
AccountID: "accID",
|
||||||
|
HmacKey: []byte{1, 3, 3, 7},
|
||||||
|
CreatedAt: time.Date(2022, 04, 12, 9, 30, 30, 0, time.UTC).Add(-1 * time.Hour),
|
||||||
|
BoundAt: time.Date(2022, 04, 12, 9, 30, 30, 0, time.UTC),
|
||||||
|
Policy: &acme.Policy{
|
||||||
|
X509: acme.X509Policy{
|
||||||
|
Allowed: acme.PolicyNames{
|
||||||
|
DNSNames: []string{"*.local"},
|
||||||
|
IPRanges: []string{"10.0.0.0/24"},
|
||||||
|
},
|
||||||
|
Denied: acme.PolicyNames{
|
||||||
|
DNSNames: []string{"badhost.local"},
|
||||||
|
IPRanges: []string{"10.0.0.30"},
|
||||||
|
},
|
||||||
|
AllowWildcardNames: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
if got := linkedEAKToCertificates(tt.k); !reflect.DeepEqual(got, tt.want) {
|
||||||
|
t.Errorf("linkedEAKToCertificates() = %v, want %v", got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -29,6 +29,10 @@ type adminAuthority interface {
|
||||||
LoadProvisionerByID(id string) (provisioner.Interface, error)
|
LoadProvisionerByID(id string) (provisioner.Interface, error)
|
||||||
UpdateProvisioner(ctx context.Context, nu *linkedca.Provisioner) error
|
UpdateProvisioner(ctx context.Context, nu *linkedca.Provisioner) error
|
||||||
RemoveProvisioner(ctx context.Context, id string) error
|
RemoveProvisioner(ctx context.Context, id string) error
|
||||||
|
GetAuthorityPolicy(ctx context.Context) (*linkedca.Policy, error)
|
||||||
|
CreateAuthorityPolicy(ctx context.Context, admin *linkedca.Admin, policy *linkedca.Policy) (*linkedca.Policy, error)
|
||||||
|
UpdateAuthorityPolicy(ctx context.Context, admin *linkedca.Admin, policy *linkedca.Policy) (*linkedca.Policy, error)
|
||||||
|
RemoveAuthorityPolicy(ctx context.Context) error
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateAdminRequest represents the body for a CreateAdmin request.
|
// CreateAdminRequest represents the body for a CreateAdmin request.
|
||||||
|
|
|
@ -14,11 +14,13 @@ import (
|
||||||
"github.com/go-chi/chi"
|
"github.com/go-chi/chi"
|
||||||
"github.com/google/go-cmp/cmp"
|
"github.com/google/go-cmp/cmp"
|
||||||
"github.com/google/go-cmp/cmp/cmpopts"
|
"github.com/google/go-cmp/cmp/cmpopts"
|
||||||
|
"google.golang.org/protobuf/types/known/timestamppb"
|
||||||
|
|
||||||
|
"go.step.sm/linkedca"
|
||||||
|
|
||||||
"github.com/smallstep/assert"
|
"github.com/smallstep/assert"
|
||||||
"github.com/smallstep/certificates/authority/admin"
|
"github.com/smallstep/certificates/authority/admin"
|
||||||
"github.com/smallstep/certificates/authority/provisioner"
|
"github.com/smallstep/certificates/authority/provisioner"
|
||||||
"go.step.sm/linkedca"
|
|
||||||
"google.golang.org/protobuf/types/known/timestamppb"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type mockAdminAuthority struct {
|
type mockAdminAuthority struct {
|
||||||
|
@ -37,6 +39,11 @@ type mockAdminAuthority struct {
|
||||||
MockLoadProvisionerByID func(id string) (provisioner.Interface, error)
|
MockLoadProvisionerByID func(id string) (provisioner.Interface, error)
|
||||||
MockUpdateProvisioner func(ctx context.Context, nu *linkedca.Provisioner) error
|
MockUpdateProvisioner func(ctx context.Context, nu *linkedca.Provisioner) error
|
||||||
MockRemoveProvisioner func(ctx context.Context, id string) error
|
MockRemoveProvisioner func(ctx context.Context, id string) error
|
||||||
|
|
||||||
|
MockGetAuthorityPolicy func(ctx context.Context) (*linkedca.Policy, error)
|
||||||
|
MockCreateAuthorityPolicy func(ctx context.Context, adm *linkedca.Admin, policy *linkedca.Policy) (*linkedca.Policy, error)
|
||||||
|
MockUpdateAuthorityPolicy func(ctx context.Context, adm *linkedca.Admin, policy *linkedca.Policy) (*linkedca.Policy, error)
|
||||||
|
MockRemoveAuthorityPolicy func(ctx context.Context) error
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *mockAdminAuthority) IsAdminAPIEnabled() bool {
|
func (m *mockAdminAuthority) IsAdminAPIEnabled() bool {
|
||||||
|
@ -130,6 +137,34 @@ func (m *mockAdminAuthority) RemoveProvisioner(ctx context.Context, id string) e
|
||||||
return m.MockErr
|
return m.MockErr
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *mockAdminAuthority) GetAuthorityPolicy(ctx context.Context) (*linkedca.Policy, error) {
|
||||||
|
if m.MockGetAuthorityPolicy != nil {
|
||||||
|
return m.MockGetAuthorityPolicy(ctx)
|
||||||
|
}
|
||||||
|
return m.MockRet1.(*linkedca.Policy), m.MockErr
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockAdminAuthority) CreateAuthorityPolicy(ctx context.Context, adm *linkedca.Admin, policy *linkedca.Policy) (*linkedca.Policy, error) {
|
||||||
|
if m.MockCreateAuthorityPolicy != nil {
|
||||||
|
return m.MockCreateAuthorityPolicy(ctx, adm, policy)
|
||||||
|
}
|
||||||
|
return m.MockRet1.(*linkedca.Policy), m.MockErr
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockAdminAuthority) UpdateAuthorityPolicy(ctx context.Context, adm *linkedca.Admin, policy *linkedca.Policy) (*linkedca.Policy, error) {
|
||||||
|
if m.MockUpdateAuthorityPolicy != nil {
|
||||||
|
return m.MockUpdateAuthorityPolicy(ctx, adm, policy)
|
||||||
|
}
|
||||||
|
return m.MockRet1.(*linkedca.Policy), m.MockErr
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockAdminAuthority) RemoveAuthorityPolicy(ctx context.Context) error {
|
||||||
|
if m.MockRemoveAuthorityPolicy != nil {
|
||||||
|
return m.MockRemoveAuthorityPolicy(ctx)
|
||||||
|
}
|
||||||
|
return m.MockErr
|
||||||
|
}
|
||||||
|
|
||||||
func TestCreateAdminRequest_Validate(t *testing.T) {
|
func TestCreateAdminRequest_Validate(t *testing.T) {
|
||||||
type fields struct {
|
type fields struct {
|
||||||
Subject string
|
Subject string
|
||||||
|
|
|
@ -2,6 +2,7 @@ package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
"github.com/smallstep/certificates/acme"
|
"github.com/smallstep/certificates/acme"
|
||||||
"github.com/smallstep/certificates/api"
|
"github.com/smallstep/certificates/api"
|
||||||
|
@ -11,22 +12,22 @@ import (
|
||||||
|
|
||||||
// Handler is the Admin API request handler.
|
// Handler is the Admin API request handler.
|
||||||
type Handler struct {
|
type Handler struct {
|
||||||
acmeResponder acmeAdminResponderInterface
|
acmeResponder acmeAdminResponderInterface
|
||||||
|
policyResponder policyAdminResponderInterface
|
||||||
}
|
}
|
||||||
|
|
||||||
// Route traffic and implement the Router interface.
|
// Route traffic and implement the Router interface.
|
||||||
//
|
//
|
||||||
// Deprecated: use Route(r api.Router, acmeResponder acmeAdminResponderInterface)
|
// Deprecated: use Route(r api.Router, acmeResponder acmeAdminResponderInterface)
|
||||||
func (h *Handler) Route(r api.Router) {
|
func (h *Handler) Route(r api.Router) {
|
||||||
Route(r, h.acmeResponder)
|
Route(r, h.acmeResponder, h.policyResponder)
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewHandler returns a new Authority Config Handler.
|
// NewHandler returns a new Authority Config Handler.
|
||||||
//
|
func NewHandler(auth adminAuthority, adminDB admin.DB, acmeDB acme.DB, acmeResponder acmeAdminResponderInterface, policyResponder policyAdminResponderInterface) api.RouterHandler {
|
||||||
// Deprecated: use Route(r api.Router, acmeResponder acmeAdminResponderInterface)
|
|
||||||
func NewHandler(auth adminAuthority, adminDB admin.DB, acmeDB acme.DB, acmeResponder acmeAdminResponderInterface) api.RouterHandler {
|
|
||||||
return &Handler{
|
return &Handler{
|
||||||
acmeResponder: acmeResponder,
|
acmeResponder: acmeResponder,
|
||||||
|
policyResponder: policyResponder,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -35,11 +36,35 @@ var mustAuthority = func(ctx context.Context) adminAuthority {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Route traffic and implement the Router interface.
|
// Route traffic and implement the Router interface.
|
||||||
func Route(r api.Router, acmeResponder acmeAdminResponderInterface) {
|
func Route(r api.Router, acmeResponder acmeAdminResponderInterface, policyResponder policyAdminResponderInterface) {
|
||||||
authnz := func(next nextHTTP) nextHTTP {
|
authnz := func(next http.HandlerFunc) http.HandlerFunc {
|
||||||
return extractAuthorizeTokenAdmin(requireAPIEnabled(next))
|
return extractAuthorizeTokenAdmin(requireAPIEnabled(next))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enabledInStandalone := func(next http.HandlerFunc) http.HandlerFunc {
|
||||||
|
return checkAction(next, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
disabledInStandalone := func(next http.HandlerFunc) http.HandlerFunc {
|
||||||
|
return checkAction(next, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
acmeEABMiddleware := func(next http.HandlerFunc) http.HandlerFunc {
|
||||||
|
return authnz(loadProvisionerByName(requireEABEnabled(next)))
|
||||||
|
}
|
||||||
|
|
||||||
|
authorityPolicyMiddleware := func(next http.HandlerFunc) http.HandlerFunc {
|
||||||
|
return authnz(enabledInStandalone(next))
|
||||||
|
}
|
||||||
|
|
||||||
|
provisionerPolicyMiddleware := func(next http.HandlerFunc) http.HandlerFunc {
|
||||||
|
return authnz(disabledInStandalone(loadProvisionerByName(next)))
|
||||||
|
}
|
||||||
|
|
||||||
|
acmePolicyMiddleware := func(next http.HandlerFunc) http.HandlerFunc {
|
||||||
|
return authnz(disabledInStandalone(loadProvisionerByName(requireEABEnabled(loadExternalAccountKey(next)))))
|
||||||
|
}
|
||||||
|
|
||||||
// Provisioners
|
// Provisioners
|
||||||
r.MethodFunc("GET", "/provisioners/{name}", authnz(GetProvisioner))
|
r.MethodFunc("GET", "/provisioners/{name}", authnz(GetProvisioner))
|
||||||
r.MethodFunc("GET", "/provisioners", authnz(GetProvisioners))
|
r.MethodFunc("GET", "/provisioners", authnz(GetProvisioners))
|
||||||
|
@ -55,8 +80,31 @@ func Route(r api.Router, acmeResponder acmeAdminResponderInterface) {
|
||||||
r.MethodFunc("DELETE", "/admins/{id}", authnz(DeleteAdmin))
|
r.MethodFunc("DELETE", "/admins/{id}", authnz(DeleteAdmin))
|
||||||
|
|
||||||
// ACME External Account Binding Keys
|
// ACME External Account Binding Keys
|
||||||
r.MethodFunc("GET", "/acme/eab/{provisionerName}/{reference}", authnz(requireEABEnabled(acmeResponder.GetExternalAccountKeys)))
|
r.MethodFunc("GET", "/acme/eab/{provisionerName}/{reference}", acmeEABMiddleware(acmeResponder.GetExternalAccountKeys))
|
||||||
r.MethodFunc("GET", "/acme/eab/{provisionerName}", authnz(requireEABEnabled(acmeResponder.GetExternalAccountKeys)))
|
r.MethodFunc("GET", "/acme/eab/{provisionerName}", acmeEABMiddleware(acmeResponder.GetExternalAccountKeys))
|
||||||
r.MethodFunc("POST", "/acme/eab/{provisionerName}", authnz(requireEABEnabled(acmeResponder.CreateExternalAccountKey)))
|
r.MethodFunc("POST", "/acme/eab/{provisionerName}", acmeEABMiddleware(acmeResponder.CreateExternalAccountKey))
|
||||||
r.MethodFunc("DELETE", "/acme/eab/{provisionerName}/{id}", authnz(requireEABEnabled(acmeResponder.DeleteExternalAccountKey)))
|
r.MethodFunc("DELETE", "/acme/eab/{provisionerName}/{id}", acmeEABMiddleware(acmeResponder.DeleteExternalAccountKey))
|
||||||
|
|
||||||
|
// Policy - Authority
|
||||||
|
r.MethodFunc("GET", "/policy", authorityPolicyMiddleware(policyResponder.GetAuthorityPolicy))
|
||||||
|
r.MethodFunc("POST", "/policy", authorityPolicyMiddleware(policyResponder.CreateAuthorityPolicy))
|
||||||
|
r.MethodFunc("PUT", "/policy", authorityPolicyMiddleware(policyResponder.UpdateAuthorityPolicy))
|
||||||
|
r.MethodFunc("DELETE", "/policy", authorityPolicyMiddleware(policyResponder.DeleteAuthorityPolicy))
|
||||||
|
|
||||||
|
// Policy - Provisioner
|
||||||
|
r.MethodFunc("GET", "/provisioners/{provisionerName}/policy", provisionerPolicyMiddleware(policyResponder.GetProvisionerPolicy))
|
||||||
|
r.MethodFunc("POST", "/provisioners/{provisionerName}/policy", provisionerPolicyMiddleware(policyResponder.CreateProvisionerPolicy))
|
||||||
|
r.MethodFunc("PUT", "/provisioners/{provisionerName}/policy", provisionerPolicyMiddleware(policyResponder.UpdateProvisionerPolicy))
|
||||||
|
r.MethodFunc("DELETE", "/provisioners/{provisionerName}/policy", provisionerPolicyMiddleware(policyResponder.DeleteProvisionerPolicy))
|
||||||
|
|
||||||
|
// Policy - ACME Account
|
||||||
|
r.MethodFunc("GET", "/acme/policy/{provisionerName}/reference/{reference}", acmePolicyMiddleware(policyResponder.GetACMEAccountPolicy))
|
||||||
|
r.MethodFunc("GET", "/acme/policy/{provisionerName}/key/{keyID}", acmePolicyMiddleware(policyResponder.GetACMEAccountPolicy))
|
||||||
|
r.MethodFunc("POST", "/acme/policy/{provisionerName}/reference/{reference}", acmePolicyMiddleware(policyResponder.CreateACMEAccountPolicy))
|
||||||
|
r.MethodFunc("POST", "/acme/policy/{provisionerName}/key/{keyID}", acmePolicyMiddleware(policyResponder.CreateACMEAccountPolicy))
|
||||||
|
r.MethodFunc("PUT", "/acme/policy/{provisionerName}/reference/{reference}", acmePolicyMiddleware(policyResponder.UpdateACMEAccountPolicy))
|
||||||
|
r.MethodFunc("PUT", "/acme/policy/{provisionerName}/key/{keyID}", acmePolicyMiddleware(policyResponder.UpdateACMEAccountPolicy))
|
||||||
|
r.MethodFunc("DELETE", "/acme/policy/{provisionerName}/reference/{reference}", acmePolicyMiddleware(policyResponder.DeleteACMEAccountPolicy))
|
||||||
|
r.MethodFunc("DELETE", "/acme/policy/{provisionerName}/key/{keyID}", acmePolicyMiddleware(policyResponder.DeleteACMEAccountPolicy))
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,18 +1,23 @@
|
||||||
package api
|
package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"errors"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/go-chi/chi"
|
||||||
|
|
||||||
|
"go.step.sm/linkedca"
|
||||||
|
|
||||||
|
"github.com/smallstep/certificates/acme"
|
||||||
"github.com/smallstep/certificates/api/render"
|
"github.com/smallstep/certificates/api/render"
|
||||||
"github.com/smallstep/certificates/authority/admin"
|
"github.com/smallstep/certificates/authority/admin"
|
||||||
|
"github.com/smallstep/certificates/authority/admin/db/nosql"
|
||||||
|
"github.com/smallstep/certificates/authority/provisioner"
|
||||||
)
|
)
|
||||||
|
|
||||||
type nextHTTP = func(http.ResponseWriter, *http.Request)
|
|
||||||
|
|
||||||
// requireAPIEnabled is a middleware that ensures the Administration API
|
// requireAPIEnabled is a middleware that ensures the Administration API
|
||||||
// is enabled before servicing requests.
|
// is enabled before servicing requests.
|
||||||
func requireAPIEnabled(next nextHTTP) nextHTTP {
|
func requireAPIEnabled(next http.HandlerFunc) http.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
if !mustAuthority(r.Context()).IsAdminAPIEnabled() {
|
if !mustAuthority(r.Context()).IsAdminAPIEnabled() {
|
||||||
render.Error(w, admin.NewError(admin.ErrorNotImplementedType, "administration API not enabled"))
|
render.Error(w, admin.NewError(admin.ErrorNotImplementedType, "administration API not enabled"))
|
||||||
|
@ -23,8 +28,9 @@ func requireAPIEnabled(next nextHTTP) nextHTTP {
|
||||||
}
|
}
|
||||||
|
|
||||||
// extractAuthorizeTokenAdmin is a middleware that extracts and caches the bearer token.
|
// extractAuthorizeTokenAdmin is a middleware that extracts and caches the bearer token.
|
||||||
func extractAuthorizeTokenAdmin(next nextHTTP) nextHTTP {
|
func extractAuthorizeTokenAdmin(next http.HandlerFunc) http.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
tok := r.Header.Get("Authorization")
|
tok := r.Header.Get("Authorization")
|
||||||
if tok == "" {
|
if tok == "" {
|
||||||
render.Error(w, admin.NewError(admin.ErrorUnauthorizedType,
|
render.Error(w, admin.NewError(admin.ErrorUnauthorizedType,
|
||||||
|
@ -39,16 +45,104 @@ func extractAuthorizeTokenAdmin(next nextHTTP) nextHTTP {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx = context.WithValue(ctx, adminContextKey, adm)
|
ctx = linkedca.NewContextWithAdmin(ctx, adm)
|
||||||
next(w, r.WithContext(ctx))
|
next(w, r.WithContext(ctx))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ContextKey is the key type for storing and searching for ACME request
|
// loadProvisionerByName is a middleware that searches for a provisioner
|
||||||
// essentials in the context of a request.
|
// by name and stores it in the context.
|
||||||
type ContextKey string
|
func loadProvisionerByName(next http.HandlerFunc) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var (
|
||||||
|
p provisioner.Interface
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
|
||||||
const (
|
ctx := r.Context()
|
||||||
// adminContextKey account key
|
auth := mustAuthority(ctx)
|
||||||
adminContextKey = ContextKey("admin")
|
adminDB := admin.MustFromContext(ctx)
|
||||||
)
|
name := chi.URLParam(r, "provisionerName")
|
||||||
|
|
||||||
|
// TODO(hs): distinguish 404 vs. 500
|
||||||
|
if p, err = auth.LoadProvisionerByName(name); err != nil {
|
||||||
|
render.Error(w, admin.WrapErrorISE(err, "error loading provisioner %s", name))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
prov, err := adminDB.GetProvisioner(ctx, p.GetID())
|
||||||
|
if err != nil {
|
||||||
|
render.Error(w, admin.WrapErrorISE(err, "error retrieving provisioner %s", name))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx = linkedca.NewContextWithProvisioner(ctx, prov)
|
||||||
|
next(w, r.WithContext(ctx))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkAction checks if an action is supported in standalone or not
|
||||||
|
func checkAction(next http.HandlerFunc, supportedInStandalone bool) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// actions allowed in standalone mode are always supported
|
||||||
|
if supportedInStandalone {
|
||||||
|
next(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// when an action is not supported in standalone mode and when
|
||||||
|
// using a nosql.DB backend, actions are not supported
|
||||||
|
if _, ok := admin.MustFromContext(r.Context()).(*nosql.DB); ok {
|
||||||
|
render.Error(w, admin.NewError(admin.ErrorNotImplementedType,
|
||||||
|
"operation not supported in standalone mode"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// continue to next http handler
|
||||||
|
next(w, r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// loadExternalAccountKey is a middleware that searches for an ACME
|
||||||
|
// External Account Key by reference or keyID and stores it in the context.
|
||||||
|
func loadExternalAccountKey(next http.HandlerFunc) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ctx := r.Context()
|
||||||
|
prov := linkedca.MustProvisionerFromContext(ctx)
|
||||||
|
acmeDB := acme.MustDatabaseFromContext(ctx)
|
||||||
|
|
||||||
|
reference := chi.URLParam(r, "reference")
|
||||||
|
keyID := chi.URLParam(r, "keyID")
|
||||||
|
|
||||||
|
var (
|
||||||
|
eak *acme.ExternalAccountKey
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
|
||||||
|
if keyID != "" {
|
||||||
|
eak, err = acmeDB.GetExternalAccountKey(ctx, prov.GetId(), keyID)
|
||||||
|
} else {
|
||||||
|
eak, err = acmeDB.GetExternalAccountKeyByReference(ctx, prov.GetId(), reference)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, acme.ErrNotFound) {
|
||||||
|
render.Error(w, admin.NewError(admin.ErrorNotFoundType, "ACME External Account Key not found"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
render.Error(w, admin.WrapErrorISE(err, "error retrieving ACME External Account Key"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if eak == nil {
|
||||||
|
render.Error(w, admin.NewError(admin.ErrorNotFoundType, "ACME External Account Key not found"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
linkedEAK := eakToLinked(eak)
|
||||||
|
|
||||||
|
ctx = linkedca.NewContextWithExternalAccountKey(ctx, linkedEAK)
|
||||||
|
|
||||||
|
next(w, r.WithContext(ctx))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -4,25 +4,32 @@ import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/go-chi/chi"
|
||||||
"github.com/google/go-cmp/cmp"
|
"github.com/google/go-cmp/cmp"
|
||||||
"github.com/google/go-cmp/cmp/cmpopts"
|
"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"
|
"google.golang.org/protobuf/types/known/timestamppb"
|
||||||
|
|
||||||
|
"go.step.sm/linkedca"
|
||||||
|
|
||||||
|
"github.com/smallstep/assert"
|
||||||
|
"github.com/smallstep/certificates/acme"
|
||||||
|
"github.com/smallstep/certificates/authority/admin"
|
||||||
|
"github.com/smallstep/certificates/authority/admin/db/nosql"
|
||||||
|
"github.com/smallstep/certificates/authority/provisioner"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestHandler_requireAPIEnabled(t *testing.T) {
|
func TestHandler_requireAPIEnabled(t *testing.T) {
|
||||||
type test struct {
|
type test struct {
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
auth adminAuthority
|
auth adminAuthority
|
||||||
next nextHTTP
|
next http.HandlerFunc
|
||||||
err *admin.Error
|
err *admin.Error
|
||||||
statusCode int
|
statusCode int
|
||||||
}
|
}
|
||||||
|
@ -100,7 +107,7 @@ func TestHandler_extractAuthorizeTokenAdmin(t *testing.T) {
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
auth adminAuthority
|
auth adminAuthority
|
||||||
req *http.Request
|
req *http.Request
|
||||||
next nextHTTP
|
next http.HandlerFunc
|
||||||
err *admin.Error
|
err *admin.Error
|
||||||
statusCode int
|
statusCode int
|
||||||
}
|
}
|
||||||
|
@ -150,7 +157,7 @@ func TestHandler_extractAuthorizeTokenAdmin(t *testing.T) {
|
||||||
req.Header["Authorization"] = []string{"token"}
|
req.Header["Authorization"] = []string{"token"}
|
||||||
createdAt := time.Now()
|
createdAt := time.Now()
|
||||||
var deletedAt time.Time
|
var deletedAt time.Time
|
||||||
admin := &linkedca.Admin{
|
adm := &linkedca.Admin{
|
||||||
Id: "adminID",
|
Id: "adminID",
|
||||||
AuthorityId: "authorityID",
|
AuthorityId: "authorityID",
|
||||||
Subject: "admin",
|
Subject: "admin",
|
||||||
|
@ -162,20 +169,15 @@ func TestHandler_extractAuthorizeTokenAdmin(t *testing.T) {
|
||||||
auth := &mockAdminAuthority{
|
auth := &mockAdminAuthority{
|
||||||
MockAuthorizeAdminToken: func(r *http.Request, token string) (*linkedca.Admin, error) {
|
MockAuthorizeAdminToken: func(r *http.Request, token string) (*linkedca.Admin, error) {
|
||||||
assert.Equals(t, "token", token)
|
assert.Equals(t, "token", token)
|
||||||
return admin, nil
|
return adm, nil
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
next := func(w http.ResponseWriter, r *http.Request) {
|
next := func(w http.ResponseWriter, r *http.Request) {
|
||||||
ctx := r.Context()
|
ctx := r.Context()
|
||||||
a := ctx.Value(adminContextKey) // verifying that the context now has a linkedca.Admin
|
adm := linkedca.MustAdminFromContext(ctx) // 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{})}
|
opts := []cmp.Option{cmpopts.IgnoreUnexported(linkedca.Admin{}, timestamppb.Timestamp{})}
|
||||||
if !cmp.Equal(admin, adm, opts...) {
|
if !cmp.Equal(adm, adm, opts...) {
|
||||||
t.Errorf("linkedca.Admin diff =\n%s", cmp.Diff(admin, adm, opts...))
|
t.Errorf("linkedca.Admin diff =\n%s", cmp.Diff(adm, adm, opts...))
|
||||||
}
|
}
|
||||||
w.Write(nil) // mock response with status 200
|
w.Write(nil) // mock response with status 200
|
||||||
}
|
}
|
||||||
|
@ -218,3 +220,452 @@ func TestHandler_extractAuthorizeTokenAdmin(t *testing.T) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestHandler_loadProvisionerByName(t *testing.T) {
|
||||||
|
type test struct {
|
||||||
|
adminDB admin.DB
|
||||||
|
auth adminAuthority
|
||||||
|
ctx context.Context
|
||||||
|
next http.HandlerFunc
|
||||||
|
err *admin.Error
|
||||||
|
statusCode int
|
||||||
|
}
|
||||||
|
var tests = map[string]func(t *testing.T) test{
|
||||||
|
"fail/auth.LoadProvisionerByName": func(t *testing.T) test {
|
||||||
|
chiCtx := chi.NewRouteContext()
|
||||||
|
chiCtx.URLParams.Add("provisionerName", "provName")
|
||||||
|
ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx)
|
||||||
|
auth := &mockAdminAuthority{
|
||||||
|
MockLoadProvisionerByName: func(name string) (provisioner.Interface, error) {
|
||||||
|
assert.Equals(t, "provName", name)
|
||||||
|
return nil, errors.New("force")
|
||||||
|
},
|
||||||
|
}
|
||||||
|
err := admin.WrapErrorISE(errors.New("force"), "error loading provisioner provName")
|
||||||
|
err.Message = "error loading provisioner provName: force"
|
||||||
|
return test{
|
||||||
|
ctx: ctx,
|
||||||
|
auth: auth,
|
||||||
|
adminDB: &admin.MockDB{},
|
||||||
|
statusCode: 500,
|
||||||
|
err: err,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"fail/db.GetProvisioner": func(t *testing.T) test {
|
||||||
|
chiCtx := chi.NewRouteContext()
|
||||||
|
chiCtx.URLParams.Add("provisionerName", "provName")
|
||||||
|
ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx)
|
||||||
|
auth := &mockAdminAuthority{
|
||||||
|
MockLoadProvisionerByName: func(name string) (provisioner.Interface, error) {
|
||||||
|
assert.Equals(t, "provName", name)
|
||||||
|
return &provisioner.MockProvisioner{
|
||||||
|
MgetID: func() string {
|
||||||
|
return "provID"
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
db := &admin.MockDB{
|
||||||
|
MockGetProvisioner: func(ctx context.Context, id string) (*linkedca.Provisioner, error) {
|
||||||
|
assert.Equals(t, "provID", id)
|
||||||
|
return nil, errors.New("force")
|
||||||
|
},
|
||||||
|
}
|
||||||
|
err := admin.WrapErrorISE(errors.New("force"), "error retrieving provisioner provName")
|
||||||
|
err.Message = "error retrieving provisioner provName: force"
|
||||||
|
return test{
|
||||||
|
ctx: ctx,
|
||||||
|
auth: auth,
|
||||||
|
adminDB: db,
|
||||||
|
statusCode: 500,
|
||||||
|
err: err,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ok": func(t *testing.T) test {
|
||||||
|
chiCtx := chi.NewRouteContext()
|
||||||
|
chiCtx.URLParams.Add("provisionerName", "provName")
|
||||||
|
ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx)
|
||||||
|
auth := &mockAdminAuthority{
|
||||||
|
MockLoadProvisionerByName: func(name string) (provisioner.Interface, error) {
|
||||||
|
assert.Equals(t, "provName", name)
|
||||||
|
return &provisioner.MockProvisioner{
|
||||||
|
MgetID: func() string {
|
||||||
|
return "provID"
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
db := &admin.MockDB{
|
||||||
|
MockGetProvisioner: func(ctx context.Context, id string) (*linkedca.Provisioner, error) {
|
||||||
|
assert.Equals(t, "provID", id)
|
||||||
|
return &linkedca.Provisioner{
|
||||||
|
Id: "provID",
|
||||||
|
Name: "provName",
|
||||||
|
}, nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return test{
|
||||||
|
ctx: ctx,
|
||||||
|
auth: auth,
|
||||||
|
adminDB: db,
|
||||||
|
statusCode: 200,
|
||||||
|
next: func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
prov := linkedca.MustProvisionerFromContext(r.Context())
|
||||||
|
assert.NotNil(t, prov)
|
||||||
|
assert.Equals(t, "provID", prov.GetId())
|
||||||
|
assert.Equals(t, "provName", prov.GetName())
|
||||||
|
w.Write(nil) // mock response with status 200
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for name, prep := range tests {
|
||||||
|
tc := prep(t)
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
mockMustAuthority(t, tc.auth)
|
||||||
|
ctx := admin.NewContext(tc.ctx, tc.adminDB)
|
||||||
|
req := httptest.NewRequest("GET", "/foo", nil) // chi routing is prepared in test setup
|
||||||
|
req = req.WithContext(ctx)
|
||||||
|
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
loadProvisionerByName(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
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHandler_checkAction(t *testing.T) {
|
||||||
|
type test struct {
|
||||||
|
adminDB admin.DB
|
||||||
|
next http.HandlerFunc
|
||||||
|
supportedInStandalone bool
|
||||||
|
err *admin.Error
|
||||||
|
statusCode int
|
||||||
|
}
|
||||||
|
var tests = map[string]func(t *testing.T) test{
|
||||||
|
"standalone-nosql-supported": func(t *testing.T) test {
|
||||||
|
return test{
|
||||||
|
supportedInStandalone: true,
|
||||||
|
adminDB: &nosql.DB{},
|
||||||
|
next: func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Write(nil) // mock response with status 200
|
||||||
|
},
|
||||||
|
statusCode: 200,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"standalone-nosql-not-supported": func(t *testing.T) test {
|
||||||
|
err := admin.NewError(admin.ErrorNotImplementedType, "operation not supported in standalone mode")
|
||||||
|
err.Message = "operation not supported in standalone mode"
|
||||||
|
return test{
|
||||||
|
supportedInStandalone: false,
|
||||||
|
adminDB: &nosql.DB{},
|
||||||
|
statusCode: 501,
|
||||||
|
err: err,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"standalone-no-nosql-not-supported": func(t *testing.T) test {
|
||||||
|
err := admin.NewError(admin.ErrorNotImplementedType, "operation not supported")
|
||||||
|
err.Message = "operation not supported"
|
||||||
|
return test{
|
||||||
|
supportedInStandalone: false,
|
||||||
|
adminDB: &admin.MockDB{},
|
||||||
|
next: func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Write(nil) // mock response with status 200
|
||||||
|
},
|
||||||
|
statusCode: 200,
|
||||||
|
err: err,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for name, prep := range tests {
|
||||||
|
tc := prep(t)
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
ctx := admin.NewContext(context.Background(), tc.adminDB)
|
||||||
|
req := httptest.NewRequest("GET", "/foo", nil).WithContext(ctx)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
checkAction(tc.next, tc.supportedInStandalone)(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
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHandler_loadExternalAccountKey(t *testing.T) {
|
||||||
|
type test struct {
|
||||||
|
ctx context.Context
|
||||||
|
acmeDB acme.DB
|
||||||
|
next http.HandlerFunc
|
||||||
|
err *admin.Error
|
||||||
|
statusCode int
|
||||||
|
}
|
||||||
|
var tests = map[string]func(t *testing.T) test{
|
||||||
|
"fail/keyID-not-found-error": func(t *testing.T) test {
|
||||||
|
prov := &linkedca.Provisioner{
|
||||||
|
Id: "provID",
|
||||||
|
}
|
||||||
|
chiCtx := chi.NewRouteContext()
|
||||||
|
chiCtx.URLParams.Add("keyID", "key")
|
||||||
|
ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx)
|
||||||
|
ctx = linkedca.NewContextWithProvisioner(ctx, prov)
|
||||||
|
err := admin.NewError(admin.ErrorNotFoundType, "ACME External Account Key not found")
|
||||||
|
err.Message = "ACME External Account Key not found"
|
||||||
|
return test{
|
||||||
|
ctx: ctx,
|
||||||
|
acmeDB: &acme.MockDB{
|
||||||
|
MockGetExternalAccountKey: func(ctx context.Context, provisionerID, keyID string) (*acme.ExternalAccountKey, error) {
|
||||||
|
assert.Equals(t, "provID", provisionerID)
|
||||||
|
assert.Equals(t, "key", keyID)
|
||||||
|
return nil, acme.ErrNotFound
|
||||||
|
},
|
||||||
|
},
|
||||||
|
err: err,
|
||||||
|
statusCode: 404,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"fail/keyID-error": func(t *testing.T) test {
|
||||||
|
prov := &linkedca.Provisioner{
|
||||||
|
Id: "provID",
|
||||||
|
}
|
||||||
|
chiCtx := chi.NewRouteContext()
|
||||||
|
chiCtx.URLParams.Add("keyID", "key")
|
||||||
|
ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx)
|
||||||
|
ctx = linkedca.NewContextWithProvisioner(ctx, prov)
|
||||||
|
err := admin.WrapErrorISE(errors.New("force"), "error retrieving ACME External Account Key")
|
||||||
|
err.Message = "error retrieving ACME External Account Key: force"
|
||||||
|
return test{
|
||||||
|
ctx: ctx,
|
||||||
|
acmeDB: &acme.MockDB{
|
||||||
|
MockGetExternalAccountKey: func(ctx context.Context, provisionerID, keyID string) (*acme.ExternalAccountKey, error) {
|
||||||
|
assert.Equals(t, "provID", provisionerID)
|
||||||
|
assert.Equals(t, "key", keyID)
|
||||||
|
return nil, errors.New("force")
|
||||||
|
},
|
||||||
|
},
|
||||||
|
err: err,
|
||||||
|
statusCode: 500,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"fail/reference-not-found-error": func(t *testing.T) test {
|
||||||
|
prov := &linkedca.Provisioner{
|
||||||
|
Id: "provID",
|
||||||
|
}
|
||||||
|
chiCtx := chi.NewRouteContext()
|
||||||
|
chiCtx.URLParams.Add("reference", "ref")
|
||||||
|
ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx)
|
||||||
|
ctx = linkedca.NewContextWithProvisioner(ctx, prov)
|
||||||
|
err := admin.NewError(admin.ErrorNotFoundType, "ACME External Account Key not found")
|
||||||
|
err.Message = "ACME External Account Key not found"
|
||||||
|
return test{
|
||||||
|
ctx: ctx,
|
||||||
|
acmeDB: &acme.MockDB{
|
||||||
|
MockGetExternalAccountKeyByReference: func(ctx context.Context, provisionerID, reference string) (*acme.ExternalAccountKey, error) {
|
||||||
|
assert.Equals(t, "provID", provisionerID)
|
||||||
|
assert.Equals(t, "ref", reference)
|
||||||
|
return nil, acme.ErrNotFound
|
||||||
|
},
|
||||||
|
},
|
||||||
|
err: err,
|
||||||
|
statusCode: 404,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"fail/reference-error": func(t *testing.T) test {
|
||||||
|
prov := &linkedca.Provisioner{
|
||||||
|
Id: "provID",
|
||||||
|
}
|
||||||
|
chiCtx := chi.NewRouteContext()
|
||||||
|
chiCtx.URLParams.Add("reference", "ref")
|
||||||
|
ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx)
|
||||||
|
ctx = linkedca.NewContextWithProvisioner(ctx, prov)
|
||||||
|
err := admin.WrapErrorISE(errors.New("force"), "error retrieving ACME External Account Key")
|
||||||
|
err.Message = "error retrieving ACME External Account Key: force"
|
||||||
|
return test{
|
||||||
|
ctx: ctx,
|
||||||
|
acmeDB: &acme.MockDB{
|
||||||
|
MockGetExternalAccountKeyByReference: func(ctx context.Context, provisionerID, reference string) (*acme.ExternalAccountKey, error) {
|
||||||
|
assert.Equals(t, "provID", provisionerID)
|
||||||
|
assert.Equals(t, "ref", reference)
|
||||||
|
return nil, errors.New("force")
|
||||||
|
},
|
||||||
|
},
|
||||||
|
err: err,
|
||||||
|
statusCode: 500,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"fail/no-key": func(t *testing.T) test {
|
||||||
|
prov := &linkedca.Provisioner{
|
||||||
|
Id: "provID",
|
||||||
|
}
|
||||||
|
chiCtx := chi.NewRouteContext()
|
||||||
|
chiCtx.URLParams.Add("reference", "ref")
|
||||||
|
ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx)
|
||||||
|
ctx = linkedca.NewContextWithProvisioner(ctx, prov)
|
||||||
|
err := admin.NewError(admin.ErrorNotFoundType, "ACME External Account Key not found")
|
||||||
|
err.Message = "ACME External Account Key not found"
|
||||||
|
return test{
|
||||||
|
ctx: ctx,
|
||||||
|
acmeDB: &acme.MockDB{
|
||||||
|
MockGetExternalAccountKeyByReference: func(ctx context.Context, provisionerID, reference string) (*acme.ExternalAccountKey, error) {
|
||||||
|
assert.Equals(t, "provID", provisionerID)
|
||||||
|
assert.Equals(t, "ref", reference)
|
||||||
|
return nil, nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
err: err,
|
||||||
|
statusCode: 404,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ok/keyID": func(t *testing.T) test {
|
||||||
|
prov := &linkedca.Provisioner{
|
||||||
|
Id: "provID",
|
||||||
|
}
|
||||||
|
chiCtx := chi.NewRouteContext()
|
||||||
|
chiCtx.URLParams.Add("keyID", "eakID")
|
||||||
|
ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx)
|
||||||
|
ctx = linkedca.NewContextWithProvisioner(ctx, prov)
|
||||||
|
err := admin.NewError(admin.ErrorNotFoundType, "ACME External Account Key not found")
|
||||||
|
err.Message = "ACME External Account Key not found"
|
||||||
|
createdAt := time.Now().Add(-1 * time.Hour)
|
||||||
|
var boundAt time.Time
|
||||||
|
eak := &acme.ExternalAccountKey{
|
||||||
|
ID: "eakID",
|
||||||
|
ProvisionerID: "provID",
|
||||||
|
CreatedAt: createdAt,
|
||||||
|
BoundAt: boundAt,
|
||||||
|
}
|
||||||
|
return test{
|
||||||
|
ctx: ctx,
|
||||||
|
acmeDB: &acme.MockDB{
|
||||||
|
MockGetExternalAccountKey: func(ctx context.Context, provisionerID, keyID string) (*acme.ExternalAccountKey, error) {
|
||||||
|
assert.Equals(t, "provID", provisionerID)
|
||||||
|
assert.Equals(t, "eakID", keyID)
|
||||||
|
return eak, nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
next: func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
contextEAK := linkedca.MustExternalAccountKeyFromContext(r.Context())
|
||||||
|
assert.NotNil(t, eak)
|
||||||
|
exp := &linkedca.EABKey{
|
||||||
|
Id: "eakID",
|
||||||
|
Provisioner: "provID",
|
||||||
|
CreatedAt: timestamppb.New(createdAt),
|
||||||
|
BoundAt: timestamppb.New(boundAt),
|
||||||
|
}
|
||||||
|
assert.Equals(t, exp, contextEAK)
|
||||||
|
w.Write(nil) // mock response with status 200
|
||||||
|
},
|
||||||
|
err: nil,
|
||||||
|
statusCode: 200,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ok/reference": func(t *testing.T) test {
|
||||||
|
prov := &linkedca.Provisioner{
|
||||||
|
Id: "provID",
|
||||||
|
}
|
||||||
|
chiCtx := chi.NewRouteContext()
|
||||||
|
chiCtx.URLParams.Add("reference", "ref")
|
||||||
|
ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx)
|
||||||
|
ctx = linkedca.NewContextWithProvisioner(ctx, prov)
|
||||||
|
err := admin.NewError(admin.ErrorNotFoundType, "ACME External Account Key not found")
|
||||||
|
err.Message = "ACME External Account Key not found"
|
||||||
|
createdAt := time.Now().Add(-1 * time.Hour)
|
||||||
|
var boundAt time.Time
|
||||||
|
eak := &acme.ExternalAccountKey{
|
||||||
|
ID: "eakID",
|
||||||
|
ProvisionerID: "provID",
|
||||||
|
Reference: "ref",
|
||||||
|
CreatedAt: createdAt,
|
||||||
|
BoundAt: boundAt,
|
||||||
|
}
|
||||||
|
return test{
|
||||||
|
ctx: ctx,
|
||||||
|
acmeDB: &acme.MockDB{
|
||||||
|
MockGetExternalAccountKeyByReference: func(ctx context.Context, provisionerID, reference string) (*acme.ExternalAccountKey, error) {
|
||||||
|
assert.Equals(t, "provID", provisionerID)
|
||||||
|
assert.Equals(t, "ref", reference)
|
||||||
|
return eak, nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
next: func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
contextEAK := linkedca.MustExternalAccountKeyFromContext(r.Context())
|
||||||
|
assert.NotNil(t, eak)
|
||||||
|
exp := &linkedca.EABKey{
|
||||||
|
Id: "eakID",
|
||||||
|
Provisioner: "provID",
|
||||||
|
Reference: "ref",
|
||||||
|
CreatedAt: timestamppb.New(createdAt),
|
||||||
|
BoundAt: timestamppb.New(boundAt),
|
||||||
|
}
|
||||||
|
assert.Equals(t, exp, contextEAK)
|
||||||
|
w.Write(nil) // mock response with status 200
|
||||||
|
},
|
||||||
|
err: nil,
|
||||||
|
statusCode: 200,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, prep := range tests {
|
||||||
|
tc := prep(t)
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
ctx := acme.NewDatabaseContext(tc.ctx, tc.acmeDB)
|
||||||
|
req := httptest.NewRequest("GET", "/foo", nil)
|
||||||
|
req = req.WithContext(ctx)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
loadExternalAccountKey(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
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
517
authority/admin/api/policy.go
Normal file
517
authority/admin/api/policy.go
Normal file
|
@ -0,0 +1,517 @@
|
||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"go.step.sm/linkedca"
|
||||||
|
|
||||||
|
"github.com/smallstep/certificates/acme"
|
||||||
|
"github.com/smallstep/certificates/api/read"
|
||||||
|
"github.com/smallstep/certificates/api/render"
|
||||||
|
"github.com/smallstep/certificates/authority"
|
||||||
|
"github.com/smallstep/certificates/authority/admin"
|
||||||
|
"github.com/smallstep/certificates/authority/policy"
|
||||||
|
)
|
||||||
|
|
||||||
|
type policyAdminResponderInterface interface {
|
||||||
|
GetAuthorityPolicy(w http.ResponseWriter, r *http.Request)
|
||||||
|
CreateAuthorityPolicy(w http.ResponseWriter, r *http.Request)
|
||||||
|
UpdateAuthorityPolicy(w http.ResponseWriter, r *http.Request)
|
||||||
|
DeleteAuthorityPolicy(w http.ResponseWriter, r *http.Request)
|
||||||
|
GetProvisionerPolicy(w http.ResponseWriter, r *http.Request)
|
||||||
|
CreateProvisionerPolicy(w http.ResponseWriter, r *http.Request)
|
||||||
|
UpdateProvisionerPolicy(w http.ResponseWriter, r *http.Request)
|
||||||
|
DeleteProvisionerPolicy(w http.ResponseWriter, r *http.Request)
|
||||||
|
GetACMEAccountPolicy(w http.ResponseWriter, r *http.Request)
|
||||||
|
CreateACMEAccountPolicy(w http.ResponseWriter, r *http.Request)
|
||||||
|
UpdateACMEAccountPolicy(w http.ResponseWriter, r *http.Request)
|
||||||
|
DeleteACMEAccountPolicy(w http.ResponseWriter, r *http.Request)
|
||||||
|
}
|
||||||
|
|
||||||
|
// PolicyAdminResponder is responsible for writing ACME admin responses
|
||||||
|
type PolicyAdminResponder struct {
|
||||||
|
auth adminAuthority
|
||||||
|
adminDB admin.DB
|
||||||
|
acmeDB acme.DB
|
||||||
|
isLinkedCA bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewACMEAdminResponder returns a new ACMEAdminResponder
|
||||||
|
func NewPolicyAdminResponder(auth adminAuthority, adminDB admin.DB, acmeDB acme.DB) *PolicyAdminResponder {
|
||||||
|
|
||||||
|
var isLinkedCA bool
|
||||||
|
if a, ok := adminDB.(interface{ IsLinkedCA() bool }); ok {
|
||||||
|
isLinkedCA = a.IsLinkedCA()
|
||||||
|
}
|
||||||
|
|
||||||
|
return &PolicyAdminResponder{
|
||||||
|
auth: auth,
|
||||||
|
adminDB: adminDB,
|
||||||
|
acmeDB: acmeDB,
|
||||||
|
isLinkedCA: isLinkedCA,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAuthorityPolicy handles the GET /admin/authority/policy request
|
||||||
|
func (par *PolicyAdminResponder) GetAuthorityPolicy(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
|
if err := par.blockLinkedCA(); err != nil {
|
||||||
|
render.Error(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
authorityPolicy, err := par.auth.GetAuthorityPolicy(r.Context())
|
||||||
|
if ae, ok := err.(*admin.Error); ok && !ae.IsType(admin.ErrorNotFoundType) {
|
||||||
|
render.Error(w, admin.WrapErrorISE(ae, "error retrieving authority policy"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if authorityPolicy == nil {
|
||||||
|
render.Error(w, admin.NewError(admin.ErrorNotFoundType, "authority policy does not exist"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
render.ProtoJSONStatus(w, authorityPolicy, http.StatusOK)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateAuthorityPolicy handles the POST /admin/authority/policy request
|
||||||
|
func (par *PolicyAdminResponder) CreateAuthorityPolicy(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
|
if err := par.blockLinkedCA(); err != nil {
|
||||||
|
render.Error(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := r.Context()
|
||||||
|
authorityPolicy, err := par.auth.GetAuthorityPolicy(ctx)
|
||||||
|
|
||||||
|
if ae, ok := err.(*admin.Error); ok && !ae.IsType(admin.ErrorNotFoundType) {
|
||||||
|
render.Error(w, admin.WrapErrorISE(err, "error retrieving authority policy"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if authorityPolicy != nil {
|
||||||
|
adminErr := admin.NewError(admin.ErrorConflictType, "authority already has a policy")
|
||||||
|
render.Error(w, adminErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var newPolicy = new(linkedca.Policy)
|
||||||
|
if err := read.ProtoJSON(r.Body, newPolicy); err != nil {
|
||||||
|
render.Error(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
newPolicy.Deduplicate()
|
||||||
|
|
||||||
|
if err := validatePolicy(newPolicy); err != nil {
|
||||||
|
render.Error(w, admin.WrapError(admin.ErrorBadRequestType, err, "error validating authority policy"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
adm := linkedca.MustAdminFromContext(ctx)
|
||||||
|
|
||||||
|
var createdPolicy *linkedca.Policy
|
||||||
|
if createdPolicy, err = par.auth.CreateAuthorityPolicy(ctx, adm, newPolicy); err != nil {
|
||||||
|
if isBadRequest(err) {
|
||||||
|
render.Error(w, admin.WrapError(admin.ErrorBadRequestType, err, "error storing authority policy"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
render.Error(w, admin.WrapErrorISE(err, "error storing authority policy"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
render.ProtoJSONStatus(w, createdPolicy, http.StatusCreated)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateAuthorityPolicy handles the PUT /admin/authority/policy request
|
||||||
|
func (par *PolicyAdminResponder) UpdateAuthorityPolicy(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
|
if err := par.blockLinkedCA(); err != nil {
|
||||||
|
render.Error(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := r.Context()
|
||||||
|
authorityPolicy, err := par.auth.GetAuthorityPolicy(ctx)
|
||||||
|
|
||||||
|
if ae, ok := err.(*admin.Error); ok && !ae.IsType(admin.ErrorNotFoundType) {
|
||||||
|
render.Error(w, admin.WrapErrorISE(err, "error retrieving authority policy"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if authorityPolicy == nil {
|
||||||
|
render.Error(w, admin.NewError(admin.ErrorNotFoundType, "authority policy does not exist"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var newPolicy = new(linkedca.Policy)
|
||||||
|
if err := read.ProtoJSON(r.Body, newPolicy); err != nil {
|
||||||
|
render.Error(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
newPolicy.Deduplicate()
|
||||||
|
|
||||||
|
if err := validatePolicy(newPolicy); err != nil {
|
||||||
|
render.Error(w, admin.WrapError(admin.ErrorBadRequestType, err, "error validating authority policy"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
adm := linkedca.MustAdminFromContext(ctx)
|
||||||
|
|
||||||
|
var updatedPolicy *linkedca.Policy
|
||||||
|
if updatedPolicy, err = par.auth.UpdateAuthorityPolicy(ctx, adm, newPolicy); err != nil {
|
||||||
|
if isBadRequest(err) {
|
||||||
|
render.Error(w, admin.WrapError(admin.ErrorBadRequestType, err, "error updating authority policy"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
render.Error(w, admin.WrapErrorISE(err, "error updating authority policy"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
render.ProtoJSONStatus(w, updatedPolicy, http.StatusOK)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteAuthorityPolicy handles the DELETE /admin/authority/policy request
|
||||||
|
func (par *PolicyAdminResponder) DeleteAuthorityPolicy(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
|
if err := par.blockLinkedCA(); err != nil {
|
||||||
|
render.Error(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := r.Context()
|
||||||
|
authorityPolicy, err := par.auth.GetAuthorityPolicy(ctx)
|
||||||
|
|
||||||
|
if ae, ok := err.(*admin.Error); ok && !ae.IsType(admin.ErrorNotFoundType) {
|
||||||
|
render.Error(w, admin.WrapErrorISE(ae, "error retrieving authority policy"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if authorityPolicy == nil {
|
||||||
|
render.Error(w, admin.NewError(admin.ErrorNotFoundType, "authority policy does not exist"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := par.auth.RemoveAuthorityPolicy(ctx); err != nil {
|
||||||
|
render.Error(w, admin.WrapErrorISE(err, "error deleting authority policy"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
render.JSONStatus(w, DeleteResponse{Status: "ok"}, http.StatusOK)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetProvisionerPolicy handles the GET /admin/provisioners/{name}/policy request
|
||||||
|
func (par *PolicyAdminResponder) GetProvisionerPolicy(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
|
if err := par.blockLinkedCA(); err != nil {
|
||||||
|
render.Error(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
prov := linkedca.MustProvisionerFromContext(r.Context())
|
||||||
|
|
||||||
|
provisionerPolicy := prov.GetPolicy()
|
||||||
|
if provisionerPolicy == nil {
|
||||||
|
render.Error(w, admin.NewError(admin.ErrorNotFoundType, "provisioner policy does not exist"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
render.ProtoJSONStatus(w, provisionerPolicy, http.StatusOK)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateProvisionerPolicy handles the POST /admin/provisioners/{name}/policy request
|
||||||
|
func (par *PolicyAdminResponder) CreateProvisionerPolicy(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
|
if err := par.blockLinkedCA(); err != nil {
|
||||||
|
render.Error(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := r.Context()
|
||||||
|
prov := linkedca.MustProvisionerFromContext(ctx)
|
||||||
|
|
||||||
|
provisionerPolicy := prov.GetPolicy()
|
||||||
|
if provisionerPolicy != nil {
|
||||||
|
adminErr := admin.NewError(admin.ErrorConflictType, "provisioner %s already has a policy", prov.Name)
|
||||||
|
render.Error(w, adminErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var newPolicy = new(linkedca.Policy)
|
||||||
|
if err := read.ProtoJSON(r.Body, newPolicy); err != nil {
|
||||||
|
render.Error(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
newPolicy.Deduplicate()
|
||||||
|
|
||||||
|
if err := validatePolicy(newPolicy); err != nil {
|
||||||
|
render.Error(w, admin.WrapError(admin.ErrorBadRequestType, err, "error validating provisioner policy"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
prov.Policy = newPolicy
|
||||||
|
|
||||||
|
if err := par.auth.UpdateProvisioner(ctx, prov); err != nil {
|
||||||
|
if isBadRequest(err) {
|
||||||
|
render.Error(w, admin.WrapError(admin.ErrorBadRequestType, err, "error creating provisioner policy"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
render.Error(w, admin.WrapErrorISE(err, "error creating provisioner policy"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
render.ProtoJSONStatus(w, newPolicy, http.StatusCreated)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateProvisionerPolicy handles the PUT /admin/provisioners/{name}/policy request
|
||||||
|
func (par *PolicyAdminResponder) UpdateProvisionerPolicy(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
|
if err := par.blockLinkedCA(); err != nil {
|
||||||
|
render.Error(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := r.Context()
|
||||||
|
prov := linkedca.MustProvisionerFromContext(ctx)
|
||||||
|
|
||||||
|
provisionerPolicy := prov.GetPolicy()
|
||||||
|
if provisionerPolicy == nil {
|
||||||
|
render.Error(w, admin.NewError(admin.ErrorNotFoundType, "provisioner policy does not exist"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var newPolicy = new(linkedca.Policy)
|
||||||
|
if err := read.ProtoJSON(r.Body, newPolicy); err != nil {
|
||||||
|
render.Error(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
newPolicy.Deduplicate()
|
||||||
|
|
||||||
|
if err := validatePolicy(newPolicy); err != nil {
|
||||||
|
render.Error(w, admin.WrapError(admin.ErrorBadRequestType, err, "error validating provisioner policy"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
prov.Policy = newPolicy
|
||||||
|
if err := par.auth.UpdateProvisioner(ctx, prov); err != nil {
|
||||||
|
if isBadRequest(err) {
|
||||||
|
render.Error(w, admin.WrapError(admin.ErrorBadRequestType, err, "error updating provisioner policy"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
render.Error(w, admin.WrapErrorISE(err, "error updating provisioner policy"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
render.ProtoJSONStatus(w, newPolicy, http.StatusOK)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteProvisionerPolicy handles the DELETE /admin/provisioners/{name}/policy request
|
||||||
|
func (par *PolicyAdminResponder) DeleteProvisionerPolicy(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
|
if err := par.blockLinkedCA(); err != nil {
|
||||||
|
render.Error(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := r.Context()
|
||||||
|
prov := linkedca.MustProvisionerFromContext(ctx)
|
||||||
|
|
||||||
|
if prov.Policy == nil {
|
||||||
|
render.Error(w, admin.NewError(admin.ErrorNotFoundType, "provisioner policy does not exist"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// remove the policy
|
||||||
|
prov.Policy = nil
|
||||||
|
|
||||||
|
if err := par.auth.UpdateProvisioner(ctx, prov); err != nil {
|
||||||
|
render.Error(w, admin.WrapErrorISE(err, "error deleting provisioner policy"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
render.JSONStatus(w, DeleteResponse{Status: "ok"}, http.StatusOK)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (par *PolicyAdminResponder) GetACMEAccountPolicy(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
|
if err := par.blockLinkedCA(); err != nil {
|
||||||
|
render.Error(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := r.Context()
|
||||||
|
eak := linkedca.MustExternalAccountKeyFromContext(ctx)
|
||||||
|
|
||||||
|
eakPolicy := eak.GetPolicy()
|
||||||
|
if eakPolicy == nil {
|
||||||
|
render.Error(w, admin.NewError(admin.ErrorNotFoundType, "ACME EAK policy does not exist"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
render.ProtoJSONStatus(w, eakPolicy, http.StatusOK)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (par *PolicyAdminResponder) CreateACMEAccountPolicy(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
|
if err := par.blockLinkedCA(); err != nil {
|
||||||
|
render.Error(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := r.Context()
|
||||||
|
prov := linkedca.MustProvisionerFromContext(ctx)
|
||||||
|
eak := linkedca.MustExternalAccountKeyFromContext(ctx)
|
||||||
|
|
||||||
|
eakPolicy := eak.GetPolicy()
|
||||||
|
if eakPolicy != nil {
|
||||||
|
adminErr := admin.NewError(admin.ErrorConflictType, "ACME EAK %s already has a policy", eak.Id)
|
||||||
|
render.Error(w, adminErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var newPolicy = new(linkedca.Policy)
|
||||||
|
if err := read.ProtoJSON(r.Body, newPolicy); err != nil {
|
||||||
|
render.Error(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
newPolicy.Deduplicate()
|
||||||
|
|
||||||
|
if err := validatePolicy(newPolicy); err != nil {
|
||||||
|
render.Error(w, admin.WrapError(admin.ErrorBadRequestType, err, "error validating ACME EAK policy"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
eak.Policy = newPolicy
|
||||||
|
|
||||||
|
acmeEAK := linkedEAKToCertificates(eak)
|
||||||
|
if err := par.acmeDB.UpdateExternalAccountKey(ctx, prov.GetId(), acmeEAK); err != nil {
|
||||||
|
render.Error(w, admin.WrapErrorISE(err, "error creating ACME EAK policy"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
render.ProtoJSONStatus(w, newPolicy, http.StatusCreated)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (par *PolicyAdminResponder) UpdateACMEAccountPolicy(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
|
if err := par.blockLinkedCA(); err != nil {
|
||||||
|
render.Error(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := r.Context()
|
||||||
|
prov := linkedca.MustProvisionerFromContext(ctx)
|
||||||
|
eak := linkedca.MustExternalAccountKeyFromContext(ctx)
|
||||||
|
|
||||||
|
eakPolicy := eak.GetPolicy()
|
||||||
|
if eakPolicy == nil {
|
||||||
|
render.Error(w, admin.NewError(admin.ErrorNotFoundType, "ACME EAK policy does not exist"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var newPolicy = new(linkedca.Policy)
|
||||||
|
if err := read.ProtoJSON(r.Body, newPolicy); err != nil {
|
||||||
|
render.Error(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
newPolicy.Deduplicate()
|
||||||
|
|
||||||
|
if err := validatePolicy(newPolicy); err != nil {
|
||||||
|
render.Error(w, admin.WrapError(admin.ErrorBadRequestType, err, "error validating ACME EAK policy"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
eak.Policy = newPolicy
|
||||||
|
acmeEAK := linkedEAKToCertificates(eak)
|
||||||
|
if err := par.acmeDB.UpdateExternalAccountKey(ctx, prov.GetId(), acmeEAK); err != nil {
|
||||||
|
render.Error(w, admin.WrapErrorISE(err, "error updating ACME EAK policy"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
render.ProtoJSONStatus(w, newPolicy, http.StatusOK)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (par *PolicyAdminResponder) DeleteACMEAccountPolicy(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
|
if err := par.blockLinkedCA(); err != nil {
|
||||||
|
render.Error(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := r.Context()
|
||||||
|
prov := linkedca.MustProvisionerFromContext(ctx)
|
||||||
|
eak := linkedca.MustExternalAccountKeyFromContext(ctx)
|
||||||
|
|
||||||
|
eakPolicy := eak.GetPolicy()
|
||||||
|
if eakPolicy == nil {
|
||||||
|
render.Error(w, admin.NewError(admin.ErrorNotFoundType, "ACME EAK policy does not exist"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// remove the policy
|
||||||
|
eak.Policy = nil
|
||||||
|
|
||||||
|
acmeEAK := linkedEAKToCertificates(eak)
|
||||||
|
if err := par.acmeDB.UpdateExternalAccountKey(ctx, prov.GetId(), acmeEAK); err != nil {
|
||||||
|
render.Error(w, admin.WrapErrorISE(err, "error deleting ACME EAK policy"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
render.JSONStatus(w, DeleteResponse{Status: "ok"}, http.StatusOK)
|
||||||
|
}
|
||||||
|
|
||||||
|
// blockLinkedCA blocks all API operations on linked deployments
|
||||||
|
func (par *PolicyAdminResponder) blockLinkedCA() error {
|
||||||
|
// temporary blocking linked deployments
|
||||||
|
if par.isLinkedCA {
|
||||||
|
return admin.NewError(admin.ErrorNotImplementedType, "policy operations not yet supported in linked deployments")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// isBadRequest checks if an error should result in a bad request error
|
||||||
|
// returned to the client.
|
||||||
|
func isBadRequest(err error) bool {
|
||||||
|
var pe *authority.PolicyError
|
||||||
|
isPolicyError := errors.As(err, &pe)
|
||||||
|
return isPolicyError && (pe.Typ == authority.AdminLockOut || pe.Typ == authority.EvaluationFailure || pe.Typ == authority.ConfigurationFailure)
|
||||||
|
}
|
||||||
|
|
||||||
|
func validatePolicy(p *linkedca.Policy) error {
|
||||||
|
|
||||||
|
// convert the policy; return early if nil
|
||||||
|
options := policy.LinkedToCertificates(p)
|
||||||
|
if options == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
|
||||||
|
// Initialize a temporary x509 allow/deny policy engine
|
||||||
|
if _, err = policy.NewX509PolicyEngine(options.GetX509Options()); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize a temporary SSH allow/deny policy engine for host certificates
|
||||||
|
if _, err = policy.NewSSHHostPolicyEngine(options.GetSSHOptions()); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize a temporary SSH allow/deny policy engine for user certificates
|
||||||
|
if _, err = policy.NewSSHUserPolicyEngine(options.GetSSHOptions()); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
2711
authority/admin/api/policy_test.go
Normal file
2711
authority/admin/api/policy_test.go
Normal file
File diff suppressed because it is too large
Load diff
|
@ -8,18 +8,21 @@ import (
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/go-chi/chi"
|
"github.com/go-chi/chi"
|
||||||
"github.com/google/go-cmp/cmp"
|
"github.com/google/go-cmp/cmp"
|
||||||
"github.com/google/go-cmp/cmp/cmpopts"
|
"github.com/google/go-cmp/cmp/cmpopts"
|
||||||
|
"google.golang.org/protobuf/encoding/protojson"
|
||||||
|
"google.golang.org/protobuf/types/known/timestamppb"
|
||||||
|
|
||||||
|
"go.step.sm/linkedca"
|
||||||
|
|
||||||
"github.com/smallstep/assert"
|
"github.com/smallstep/assert"
|
||||||
"github.com/smallstep/certificates/authority/admin"
|
"github.com/smallstep/certificates/authority/admin"
|
||||||
"github.com/smallstep/certificates/authority/provisioner"
|
"github.com/smallstep/certificates/authority/provisioner"
|
||||||
"go.step.sm/linkedca"
|
|
||||||
"google.golang.org/protobuf/encoding/protojson"
|
|
||||||
"google.golang.org/protobuf/types/known/timestamppb"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestHandler_GetProvisioner(t *testing.T) {
|
func TestHandler_GetProvisioner(t *testing.T) {
|
||||||
|
@ -333,12 +336,12 @@ func TestHandler_CreateProvisioner(t *testing.T) {
|
||||||
return test{
|
return test{
|
||||||
ctx: context.Background(),
|
ctx: context.Background(),
|
||||||
body: body,
|
body: body,
|
||||||
statusCode: 500,
|
statusCode: 400,
|
||||||
err: &admin.Error{ // TODO(hs): this probably needs a better error
|
err: &admin.Error{
|
||||||
Type: "",
|
Type: "badRequest",
|
||||||
Status: 500,
|
Status: 400,
|
||||||
Detail: "",
|
Detail: "bad request",
|
||||||
Message: "",
|
Message: "proto: syntax error (line 1:2): invalid value !",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -419,9 +422,15 @@ func TestHandler_CreateProvisioner(t *testing.T) {
|
||||||
assert.FatalError(t, json.Unmarshal(bytes.TrimSpace(body), &adminErr))
|
assert.FatalError(t, json.Unmarshal(bytes.TrimSpace(body), &adminErr))
|
||||||
|
|
||||||
assert.Equals(t, tc.err.Type, adminErr.Type)
|
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, tc.err.Detail, adminErr.Detail)
|
||||||
assert.Equals(t, []string{"application/json"}, res.Header["Content-Type"])
|
assert.Equals(t, []string{"application/json"}, res.Header["Content-Type"])
|
||||||
|
|
||||||
|
if strings.HasPrefix(tc.err.Message, "proto:") {
|
||||||
|
assert.True(t, strings.Contains(adminErr.Message, "syntax error"))
|
||||||
|
} else {
|
||||||
|
assert.Equals(t, tc.err.Message, adminErr.Message)
|
||||||
|
}
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -611,12 +620,12 @@ func TestHandler_UpdateProvisioner(t *testing.T) {
|
||||||
ctx: context.Background(),
|
ctx: context.Background(),
|
||||||
body: body,
|
body: body,
|
||||||
adminDB: &admin.MockDB{},
|
adminDB: &admin.MockDB{},
|
||||||
statusCode: 500,
|
statusCode: 400,
|
||||||
err: &admin.Error{ // TODO(hs): this probably needs a better error
|
err: &admin.Error{
|
||||||
Type: "",
|
Type: "badRequest",
|
||||||
Status: 500,
|
Status: 400,
|
||||||
Detail: "",
|
Detail: "bad request",
|
||||||
Message: "",
|
Message: "proto: syntax error (line 1:2): invalid value !",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -1068,9 +1077,15 @@ func TestHandler_UpdateProvisioner(t *testing.T) {
|
||||||
assert.FatalError(t, json.Unmarshal(bytes.TrimSpace(body), &adminErr))
|
assert.FatalError(t, json.Unmarshal(bytes.TrimSpace(body), &adminErr))
|
||||||
|
|
||||||
assert.Equals(t, tc.err.Type, adminErr.Type)
|
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, tc.err.Detail, adminErr.Detail)
|
||||||
assert.Equals(t, []string{"application/json"}, res.Header["Content-Type"])
|
assert.Equals(t, []string{"application/json"}, res.Header["Content-Type"])
|
||||||
|
|
||||||
|
if strings.HasPrefix(tc.err.Message, "proto:") {
|
||||||
|
assert.True(t, strings.Contains(adminErr.Message, "syntax error"))
|
||||||
|
} else {
|
||||||
|
assert.Equals(t, tc.err.Message, adminErr.Message)
|
||||||
|
}
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -69,6 +69,11 @@ type DB interface {
|
||||||
GetAdmins(ctx context.Context) ([]*linkedca.Admin, error)
|
GetAdmins(ctx context.Context) ([]*linkedca.Admin, error)
|
||||||
UpdateAdmin(ctx context.Context, admin *linkedca.Admin) error
|
UpdateAdmin(ctx context.Context, admin *linkedca.Admin) error
|
||||||
DeleteAdmin(ctx context.Context, id string) error
|
DeleteAdmin(ctx context.Context, id string) error
|
||||||
|
|
||||||
|
CreateAuthorityPolicy(ctx context.Context, policy *linkedca.Policy) error
|
||||||
|
GetAuthorityPolicy(ctx context.Context) (*linkedca.Policy, error)
|
||||||
|
UpdateAuthorityPolicy(ctx context.Context, policy *linkedca.Policy) error
|
||||||
|
DeleteAuthorityPolicy(ctx context.Context) error
|
||||||
}
|
}
|
||||||
|
|
||||||
type dbKey struct{}
|
type dbKey struct{}
|
||||||
|
@ -109,6 +114,11 @@ type MockDB struct {
|
||||||
MockUpdateAdmin func(ctx context.Context, adm *linkedca.Admin) error
|
MockUpdateAdmin func(ctx context.Context, adm *linkedca.Admin) error
|
||||||
MockDeleteAdmin func(ctx context.Context, id string) error
|
MockDeleteAdmin func(ctx context.Context, id string) error
|
||||||
|
|
||||||
|
MockCreateAuthorityPolicy func(ctx context.Context, policy *linkedca.Policy) error
|
||||||
|
MockGetAuthorityPolicy func(ctx context.Context) (*linkedca.Policy, error)
|
||||||
|
MockUpdateAuthorityPolicy func(ctx context.Context, policy *linkedca.Policy) error
|
||||||
|
MockDeleteAuthorityPolicy func(ctx context.Context) error
|
||||||
|
|
||||||
MockError error
|
MockError error
|
||||||
MockRet1 interface{}
|
MockRet1 interface{}
|
||||||
}
|
}
|
||||||
|
@ -202,3 +212,35 @@ func (m *MockDB) DeleteAdmin(ctx context.Context, id string) error {
|
||||||
}
|
}
|
||||||
return m.MockError
|
return m.MockError
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CreateAuthorityPolicy mock
|
||||||
|
func (m *MockDB) CreateAuthorityPolicy(ctx context.Context, policy *linkedca.Policy) error {
|
||||||
|
if m.MockCreateAuthorityPolicy != nil {
|
||||||
|
return m.MockCreateAuthorityPolicy(ctx, policy)
|
||||||
|
}
|
||||||
|
return m.MockError
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAuthorityPolicy mock
|
||||||
|
func (m *MockDB) GetAuthorityPolicy(ctx context.Context) (*linkedca.Policy, error) {
|
||||||
|
if m.MockGetAuthorityPolicy != nil {
|
||||||
|
return m.MockGetAuthorityPolicy(ctx)
|
||||||
|
}
|
||||||
|
return m.MockRet1.(*linkedca.Policy), m.MockError
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateAuthorityPolicy mock
|
||||||
|
func (m *MockDB) UpdateAuthorityPolicy(ctx context.Context, policy *linkedca.Policy) error {
|
||||||
|
if m.MockUpdateAuthorityPolicy != nil {
|
||||||
|
return m.MockUpdateAuthorityPolicy(ctx, policy)
|
||||||
|
}
|
||||||
|
return m.MockError
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteAuthorityPolicy mock
|
||||||
|
func (m *MockDB) DeleteAuthorityPolicy(ctx context.Context) error {
|
||||||
|
if m.MockDeleteAuthorityPolicy != nil {
|
||||||
|
return m.MockDeleteAuthorityPolicy(ctx)
|
||||||
|
}
|
||||||
|
return m.MockError
|
||||||
|
}
|
||||||
|
|
|
@ -11,8 +11,9 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
adminsTable = []byte("admins")
|
adminsTable = []byte("admins")
|
||||||
provisionersTable = []byte("provisioners")
|
provisionersTable = []byte("provisioners")
|
||||||
|
authorityPoliciesTable = []byte("authority_policies")
|
||||||
)
|
)
|
||||||
|
|
||||||
// DB is a struct that implements the AdminDB interface.
|
// DB is a struct that implements the AdminDB interface.
|
||||||
|
@ -23,7 +24,7 @@ type DB struct {
|
||||||
|
|
||||||
// New configures and returns a new Authority DB backend implemented using a nosql DB.
|
// New configures and returns a new Authority DB backend implemented using a nosql DB.
|
||||||
func New(db nosqlDB.DB, authorityID string) (*DB, error) {
|
func New(db nosqlDB.DB, authorityID string) (*DB, error) {
|
||||||
tables := [][]byte{adminsTable, provisionersTable}
|
tables := [][]byte{adminsTable, provisionersTable, authorityPoliciesTable}
|
||||||
for _, b := range tables {
|
for _, b := range tables {
|
||||||
if err := db.CreateTable(b); err != nil {
|
if err := db.CreateTable(b); err != nil {
|
||||||
return nil, errors.Wrapf(err, "error creating table %s",
|
return nil, errors.Wrapf(err, "error creating table %s",
|
||||||
|
|
339
authority/admin/db/nosql/policy.go
Normal file
339
authority/admin/db/nosql/policy.go
Normal file
|
@ -0,0 +1,339 @@
|
||||||
|
package nosql
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"go.step.sm/linkedca"
|
||||||
|
|
||||||
|
"github.com/smallstep/certificates/authority/admin"
|
||||||
|
"github.com/smallstep/nosql"
|
||||||
|
)
|
||||||
|
|
||||||
|
type dbX509Policy struct {
|
||||||
|
Allow *dbX509Names `json:"allow,omitempty"`
|
||||||
|
Deny *dbX509Names `json:"deny,omitempty"`
|
||||||
|
AllowWildcardNames bool `json:"allow_wildcard_names,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type dbX509Names struct {
|
||||||
|
CommonNames []string `json:"cn,omitempty"`
|
||||||
|
DNSDomains []string `json:"dns,omitempty"`
|
||||||
|
IPRanges []string `json:"ip,omitempty"`
|
||||||
|
EmailAddresses []string `json:"email,omitempty"`
|
||||||
|
URIDomains []string `json:"uri,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type dbSSHPolicy struct {
|
||||||
|
// User contains SSH user certificate options.
|
||||||
|
User *dbSSHUserPolicy `json:"user,omitempty"`
|
||||||
|
// Host contains SSH host certificate options.
|
||||||
|
Host *dbSSHHostPolicy `json:"host,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type dbSSHHostPolicy struct {
|
||||||
|
Allow *dbSSHHostNames `json:"allow,omitempty"`
|
||||||
|
Deny *dbSSHHostNames `json:"deny,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type dbSSHHostNames struct {
|
||||||
|
DNSDomains []string `json:"dns,omitempty"`
|
||||||
|
IPRanges []string `json:"ip,omitempty"`
|
||||||
|
Principals []string `json:"principal,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type dbSSHUserPolicy struct {
|
||||||
|
Allow *dbSSHUserNames `json:"allow,omitempty"`
|
||||||
|
Deny *dbSSHUserNames `json:"deny,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type dbSSHUserNames struct {
|
||||||
|
EmailAddresses []string `json:"email,omitempty"`
|
||||||
|
Principals []string `json:"principal,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type dbPolicy struct {
|
||||||
|
X509 *dbX509Policy `json:"x509,omitempty"`
|
||||||
|
SSH *dbSSHPolicy `json:"ssh,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type dbAuthorityPolicy struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
AuthorityID string `json:"authorityID"`
|
||||||
|
Policy *dbPolicy `json:"policy,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dbap *dbAuthorityPolicy) convert() *linkedca.Policy {
|
||||||
|
if dbap == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return dbToLinked(dbap.Policy)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *DB) getDBAuthorityPolicyBytes(ctx context.Context, authorityID string) ([]byte, error) {
|
||||||
|
data, err := db.db.Get(authorityPoliciesTable, []byte(authorityID))
|
||||||
|
if nosql.IsErrNotFound(err) {
|
||||||
|
return nil, admin.NewError(admin.ErrorNotFoundType, "authority policy not found")
|
||||||
|
} else if err != nil {
|
||||||
|
return nil, fmt.Errorf("error loading authority policy: %w", err)
|
||||||
|
}
|
||||||
|
return data, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *DB) unmarshalDBAuthorityPolicy(data []byte) (*dbAuthorityPolicy, error) {
|
||||||
|
if len(data) == 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
var dba = new(dbAuthorityPolicy)
|
||||||
|
if err := json.Unmarshal(data, dba); err != nil {
|
||||||
|
return nil, fmt.Errorf("error unmarshaling policy bytes into dbAuthorityPolicy: %w", err)
|
||||||
|
}
|
||||||
|
return dba, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *DB) getDBAuthorityPolicy(ctx context.Context, authorityID string) (*dbAuthorityPolicy, error) {
|
||||||
|
data, err := db.getDBAuthorityPolicyBytes(ctx, authorityID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
dbap, err := db.unmarshalDBAuthorityPolicy(data)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if dbap == nil {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
if dbap.AuthorityID != authorityID {
|
||||||
|
return nil, admin.NewError(admin.ErrorAuthorityMismatchType,
|
||||||
|
"authority policy is not owned by authority %s", authorityID)
|
||||||
|
}
|
||||||
|
return dbap, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *DB) CreateAuthorityPolicy(ctx context.Context, policy *linkedca.Policy) error {
|
||||||
|
|
||||||
|
dbap := &dbAuthorityPolicy{
|
||||||
|
ID: db.authorityID,
|
||||||
|
AuthorityID: db.authorityID,
|
||||||
|
Policy: linkedToDB(policy),
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := db.save(ctx, dbap.ID, dbap, nil, "authority_policy", authorityPoliciesTable); err != nil {
|
||||||
|
return admin.WrapErrorISE(err, "error creating authority policy")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *DB) GetAuthorityPolicy(ctx context.Context) (*linkedca.Policy, error) {
|
||||||
|
dbap, err := db.getDBAuthorityPolicy(ctx, db.authorityID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return dbap.convert(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *DB) UpdateAuthorityPolicy(ctx context.Context, policy *linkedca.Policy) error {
|
||||||
|
old, err := db.getDBAuthorityPolicy(ctx, db.authorityID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
dbap := &dbAuthorityPolicy{
|
||||||
|
ID: db.authorityID,
|
||||||
|
AuthorityID: db.authorityID,
|
||||||
|
Policy: linkedToDB(policy),
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := db.save(ctx, dbap.ID, dbap, old, "authority_policy", authorityPoliciesTable); err != nil {
|
||||||
|
return admin.WrapErrorISE(err, "error updating authority policy")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *DB) DeleteAuthorityPolicy(ctx context.Context) error {
|
||||||
|
old, err := db.getDBAuthorityPolicy(ctx, db.authorityID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := db.save(ctx, old.ID, nil, old, "authority_policy", authorityPoliciesTable); err != nil {
|
||||||
|
return admin.WrapErrorISE(err, "error deleting authority policy")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func dbToLinked(p *dbPolicy) *linkedca.Policy {
|
||||||
|
if p == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
r := &linkedca.Policy{}
|
||||||
|
if x509 := p.X509; x509 != nil {
|
||||||
|
r.X509 = &linkedca.X509Policy{}
|
||||||
|
if allow := x509.Allow; allow != nil {
|
||||||
|
r.X509.Allow = &linkedca.X509Names{}
|
||||||
|
r.X509.Allow.Dns = allow.DNSDomains
|
||||||
|
r.X509.Allow.Emails = allow.EmailAddresses
|
||||||
|
r.X509.Allow.Ips = allow.IPRanges
|
||||||
|
r.X509.Allow.Uris = allow.URIDomains
|
||||||
|
r.X509.Allow.CommonNames = allow.CommonNames
|
||||||
|
}
|
||||||
|
if deny := x509.Deny; deny != nil {
|
||||||
|
r.X509.Deny = &linkedca.X509Names{}
|
||||||
|
r.X509.Deny.Dns = deny.DNSDomains
|
||||||
|
r.X509.Deny.Emails = deny.EmailAddresses
|
||||||
|
r.X509.Deny.Ips = deny.IPRanges
|
||||||
|
r.X509.Deny.Uris = deny.URIDomains
|
||||||
|
r.X509.Deny.CommonNames = deny.CommonNames
|
||||||
|
}
|
||||||
|
r.X509.AllowWildcardNames = x509.AllowWildcardNames
|
||||||
|
}
|
||||||
|
if ssh := p.SSH; ssh != nil {
|
||||||
|
r.Ssh = &linkedca.SSHPolicy{}
|
||||||
|
if host := ssh.Host; host != nil {
|
||||||
|
r.Ssh.Host = &linkedca.SSHHostPolicy{}
|
||||||
|
if allow := host.Allow; allow != nil {
|
||||||
|
r.Ssh.Host.Allow = &linkedca.SSHHostNames{}
|
||||||
|
r.Ssh.Host.Allow.Dns = allow.DNSDomains
|
||||||
|
r.Ssh.Host.Allow.Ips = allow.IPRanges
|
||||||
|
r.Ssh.Host.Allow.Principals = allow.Principals
|
||||||
|
}
|
||||||
|
if deny := host.Deny; deny != nil {
|
||||||
|
r.Ssh.Host.Deny = &linkedca.SSHHostNames{}
|
||||||
|
r.Ssh.Host.Deny.Dns = deny.DNSDomains
|
||||||
|
r.Ssh.Host.Deny.Ips = deny.IPRanges
|
||||||
|
r.Ssh.Host.Deny.Principals = deny.Principals
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if user := ssh.User; user != nil {
|
||||||
|
r.Ssh.User = &linkedca.SSHUserPolicy{}
|
||||||
|
if allow := user.Allow; allow != nil {
|
||||||
|
r.Ssh.User.Allow = &linkedca.SSHUserNames{}
|
||||||
|
r.Ssh.User.Allow.Emails = allow.EmailAddresses
|
||||||
|
r.Ssh.User.Allow.Principals = allow.Principals
|
||||||
|
}
|
||||||
|
if deny := user.Deny; deny != nil {
|
||||||
|
r.Ssh.User.Deny = &linkedca.SSHUserNames{}
|
||||||
|
r.Ssh.User.Deny.Emails = deny.EmailAddresses
|
||||||
|
r.Ssh.User.Deny.Principals = deny.Principals
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
func linkedToDB(p *linkedca.Policy) *dbPolicy {
|
||||||
|
|
||||||
|
if p == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// return early if x509 nor SSH is set
|
||||||
|
if p.GetX509() == nil && p.GetSsh() == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
r := &dbPolicy{}
|
||||||
|
// fill x509 policy configuration
|
||||||
|
if x509 := p.GetX509(); x509 != nil {
|
||||||
|
r.X509 = &dbX509Policy{}
|
||||||
|
if allow := x509.GetAllow(); allow != nil {
|
||||||
|
r.X509.Allow = &dbX509Names{}
|
||||||
|
if allow.Dns != nil {
|
||||||
|
r.X509.Allow.DNSDomains = allow.Dns
|
||||||
|
}
|
||||||
|
if allow.Ips != nil {
|
||||||
|
r.X509.Allow.IPRanges = allow.Ips
|
||||||
|
}
|
||||||
|
if allow.Emails != nil {
|
||||||
|
r.X509.Allow.EmailAddresses = allow.Emails
|
||||||
|
}
|
||||||
|
if allow.Uris != nil {
|
||||||
|
r.X509.Allow.URIDomains = allow.Uris
|
||||||
|
}
|
||||||
|
if allow.CommonNames != nil {
|
||||||
|
r.X509.Allow.CommonNames = allow.CommonNames
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if deny := x509.GetDeny(); deny != nil {
|
||||||
|
r.X509.Deny = &dbX509Names{}
|
||||||
|
if deny.Dns != nil {
|
||||||
|
r.X509.Deny.DNSDomains = deny.Dns
|
||||||
|
}
|
||||||
|
if deny.Ips != nil {
|
||||||
|
r.X509.Deny.IPRanges = deny.Ips
|
||||||
|
}
|
||||||
|
if deny.Emails != nil {
|
||||||
|
r.X509.Deny.EmailAddresses = deny.Emails
|
||||||
|
}
|
||||||
|
if deny.Uris != nil {
|
||||||
|
r.X509.Deny.URIDomains = deny.Uris
|
||||||
|
}
|
||||||
|
if deny.CommonNames != nil {
|
||||||
|
r.X509.Deny.CommonNames = deny.CommonNames
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
r.X509.AllowWildcardNames = x509.GetAllowWildcardNames()
|
||||||
|
}
|
||||||
|
|
||||||
|
// fill ssh policy configuration
|
||||||
|
if ssh := p.GetSsh(); ssh != nil {
|
||||||
|
r.SSH = &dbSSHPolicy{}
|
||||||
|
if host := ssh.GetHost(); host != nil {
|
||||||
|
r.SSH.Host = &dbSSHHostPolicy{}
|
||||||
|
if allow := host.GetAllow(); allow != nil {
|
||||||
|
r.SSH.Host.Allow = &dbSSHHostNames{}
|
||||||
|
if allow.Dns != nil {
|
||||||
|
r.SSH.Host.Allow.DNSDomains = allow.Dns
|
||||||
|
}
|
||||||
|
if allow.Ips != nil {
|
||||||
|
r.SSH.Host.Allow.IPRanges = allow.Ips
|
||||||
|
}
|
||||||
|
if allow.Principals != nil {
|
||||||
|
r.SSH.Host.Allow.Principals = allow.Principals
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if deny := host.GetDeny(); deny != nil {
|
||||||
|
r.SSH.Host.Deny = &dbSSHHostNames{}
|
||||||
|
if deny.Dns != nil {
|
||||||
|
r.SSH.Host.Deny.DNSDomains = deny.Dns
|
||||||
|
}
|
||||||
|
if deny.Ips != nil {
|
||||||
|
r.SSH.Host.Deny.IPRanges = deny.Ips
|
||||||
|
}
|
||||||
|
if deny.Principals != nil {
|
||||||
|
r.SSH.Host.Deny.Principals = deny.Principals
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if user := ssh.GetUser(); user != nil {
|
||||||
|
r.SSH.User = &dbSSHUserPolicy{}
|
||||||
|
if allow := user.GetAllow(); allow != nil {
|
||||||
|
r.SSH.User.Allow = &dbSSHUserNames{}
|
||||||
|
if allow.Emails != nil {
|
||||||
|
r.SSH.User.Allow.EmailAddresses = allow.Emails
|
||||||
|
}
|
||||||
|
if allow.Principals != nil {
|
||||||
|
r.SSH.User.Allow.Principals = allow.Principals
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if deny := user.GetDeny(); deny != nil {
|
||||||
|
r.SSH.User.Deny = &dbSSHUserNames{}
|
||||||
|
if deny.Emails != nil {
|
||||||
|
r.SSH.User.Deny.EmailAddresses = deny.Emails
|
||||||
|
}
|
||||||
|
if deny.Principals != nil {
|
||||||
|
r.SSH.User.Deny.Principals = deny.Principals
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return r
|
||||||
|
}
|
1206
authority/admin/db/nosql/policy_test.go
Normal file
1206
authority/admin/db/nosql/policy_test.go
Normal file
File diff suppressed because it is too large
Load diff
|
@ -24,10 +24,12 @@ const (
|
||||||
ErrorBadRequestType
|
ErrorBadRequestType
|
||||||
// ErrorNotImplementedType not implemented.
|
// ErrorNotImplementedType not implemented.
|
||||||
ErrorNotImplementedType
|
ErrorNotImplementedType
|
||||||
// ErrorUnauthorizedType internal server error.
|
// ErrorUnauthorizedType unauthorized.
|
||||||
ErrorUnauthorizedType
|
ErrorUnauthorizedType
|
||||||
// ErrorServerInternalType internal server error.
|
// ErrorServerInternalType internal server error.
|
||||||
ErrorServerInternalType
|
ErrorServerInternalType
|
||||||
|
// ErrorConflictType conflict.
|
||||||
|
ErrorConflictType
|
||||||
)
|
)
|
||||||
|
|
||||||
// String returns the string representation of the admin problem type,
|
// String returns the string representation of the admin problem type,
|
||||||
|
@ -48,6 +50,8 @@ func (ap ProblemType) String() string {
|
||||||
return "unauthorized"
|
return "unauthorized"
|
||||||
case ErrorServerInternalType:
|
case ErrorServerInternalType:
|
||||||
return "internalServerError"
|
return "internalServerError"
|
||||||
|
case ErrorConflictType:
|
||||||
|
return "conflict"
|
||||||
default:
|
default:
|
||||||
return fmt.Sprintf("unsupported error type '%d'", int(ap))
|
return fmt.Sprintf("unsupported error type '%d'", int(ap))
|
||||||
}
|
}
|
||||||
|
@ -64,7 +68,7 @@ var (
|
||||||
errorServerInternalMetadata = errorMetadata{
|
errorServerInternalMetadata = errorMetadata{
|
||||||
typ: ErrorServerInternalType.String(),
|
typ: ErrorServerInternalType.String(),
|
||||||
details: "the server experienced an internal error",
|
details: "the server experienced an internal error",
|
||||||
status: 500,
|
status: http.StatusInternalServerError,
|
||||||
}
|
}
|
||||||
errorMap = map[ProblemType]errorMetadata{
|
errorMap = map[ProblemType]errorMetadata{
|
||||||
ErrorNotFoundType: {
|
ErrorNotFoundType: {
|
||||||
|
@ -98,6 +102,11 @@ var (
|
||||||
status: http.StatusUnauthorized,
|
status: http.StatusUnauthorized,
|
||||||
},
|
},
|
||||||
ErrorServerInternalType: errorServerInternalMetadata,
|
ErrorServerInternalType: errorServerInternalMetadata,
|
||||||
|
ErrorConflictType: {
|
||||||
|
typ: ErrorConflictType.String(),
|
||||||
|
details: "conflict",
|
||||||
|
status: http.StatusConflict,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -59,12 +59,12 @@ func newSubProv(subject, prov string) subProv {
|
||||||
return subProv{subject, prov}
|
return subProv{subject, prov}
|
||||||
}
|
}
|
||||||
|
|
||||||
// LoadBySubProv a admin by the subject and provisioner name.
|
// LoadBySubProv loads an admin by subject and provisioner name.
|
||||||
func (c *Collection) LoadBySubProv(sub, provName string) (*linkedca.Admin, bool) {
|
func (c *Collection) LoadBySubProv(sub, provName string) (*linkedca.Admin, bool) {
|
||||||
return loadAdmin(c.bySubProv, newSubProv(sub, provName))
|
return loadAdmin(c.bySubProv, newSubProv(sub, provName))
|
||||||
}
|
}
|
||||||
|
|
||||||
// LoadByProvisioner a admin by the subject and provisioner name.
|
// LoadByProvisioner loads admins by provisioner name.
|
||||||
func (c *Collection) LoadByProvisioner(provName string) ([]*linkedca.Admin, bool) {
|
func (c *Collection) LoadByProvisioner(provName string) ([]*linkedca.Admin, bool) {
|
||||||
val, ok := c.byProv.Load(provName)
|
val, ok := c.byProv.Load(provName)
|
||||||
if !ok {
|
if !ok {
|
||||||
|
@ -78,7 +78,7 @@ func (c *Collection) LoadByProvisioner(provName string) ([]*linkedca.Admin, bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// Store adds an admin to the collection and enforces the uniqueness of
|
// Store adds an admin to the collection and enforces the uniqueness of
|
||||||
// admin IDs and amdin subject <-> provisioner name combos.
|
// admin IDs and admin subject <-> provisioner name combos.
|
||||||
func (c *Collection) Store(adm *linkedca.Admin, prov provisioner.Interface) error {
|
func (c *Collection) Store(adm *linkedca.Admin, prov provisioner.Interface) error {
|
||||||
// Input validation.
|
// Input validation.
|
||||||
if adm.ProvisionerId != prov.GetID() {
|
if adm.ProvisionerId != prov.GetID() {
|
||||||
|
|
|
@ -12,10 +12,16 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
|
"golang.org/x/crypto/ssh"
|
||||||
|
|
||||||
|
"go.step.sm/crypto/pemutil"
|
||||||
|
"go.step.sm/linkedca"
|
||||||
|
|
||||||
"github.com/smallstep/certificates/authority/admin"
|
"github.com/smallstep/certificates/authority/admin"
|
||||||
adminDBNosql "github.com/smallstep/certificates/authority/admin/db/nosql"
|
adminDBNosql "github.com/smallstep/certificates/authority/admin/db/nosql"
|
||||||
"github.com/smallstep/certificates/authority/administrator"
|
"github.com/smallstep/certificates/authority/administrator"
|
||||||
"github.com/smallstep/certificates/authority/config"
|
"github.com/smallstep/certificates/authority/config"
|
||||||
|
"github.com/smallstep/certificates/authority/policy"
|
||||||
"github.com/smallstep/certificates/authority/provisioner"
|
"github.com/smallstep/certificates/authority/provisioner"
|
||||||
"github.com/smallstep/certificates/cas"
|
"github.com/smallstep/certificates/cas"
|
||||||
casapi "github.com/smallstep/certificates/cas/apiv1"
|
casapi "github.com/smallstep/certificates/cas/apiv1"
|
||||||
|
@ -26,9 +32,6 @@ import (
|
||||||
"github.com/smallstep/certificates/scep"
|
"github.com/smallstep/certificates/scep"
|
||||||
"github.com/smallstep/certificates/templates"
|
"github.com/smallstep/certificates/templates"
|
||||||
"github.com/smallstep/nosql"
|
"github.com/smallstep/nosql"
|
||||||
"go.step.sm/crypto/pemutil"
|
|
||||||
"go.step.sm/linkedca"
|
|
||||||
"golang.org/x/crypto/ssh"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Authority implements the Certificate Authority internal interface.
|
// Authority implements the Certificate Authority internal interface.
|
||||||
|
@ -77,6 +80,9 @@ type Authority struct {
|
||||||
authorizeRenewFunc provisioner.AuthorizeRenewFunc
|
authorizeRenewFunc provisioner.AuthorizeRenewFunc
|
||||||
authorizeSSHRenewFunc provisioner.AuthorizeSSHRenewFunc
|
authorizeSSHRenewFunc provisioner.AuthorizeSSHRenewFunc
|
||||||
|
|
||||||
|
// Policy engines
|
||||||
|
policyEngine *policy.Engine
|
||||||
|
|
||||||
adminMutex sync.RWMutex
|
adminMutex sync.RWMutex
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -232,6 +238,7 @@ func (a *Authority) reloadAdminResources(ctx context.Context) error {
|
||||||
a.provisioners = provClxn
|
a.provisioners = provClxn
|
||||||
a.config.AuthorityConfig.Admins = adminList
|
a.config.AuthorityConfig.Admins = adminList
|
||||||
a.admins = adminClxn
|
a.admins = adminClxn
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -578,6 +585,11 @@ func (a *Authority) init() error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Load x509 and SSH Policy Engines
|
||||||
|
if err := a.reloadPolicyEngines(context.Background()); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
// Configure templates, currently only ssh templates are supported.
|
// Configure templates, currently only ssh templates are supported.
|
||||||
if a.sshCAHostCertSignKey != nil || a.sshCAUserCertSignKey != nil {
|
if a.sshCAHostCertSignKey != nil || a.sshCAUserCertSignKey != nil {
|
||||||
a.templates = a.config.Templates
|
a.templates = a.config.Templates
|
||||||
|
|
|
@ -491,7 +491,7 @@ func TestAuthority_authorizeSign(t *testing.T) {
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if assert.Nil(t, tc.err) {
|
if assert.Nil(t, tc.err) {
|
||||||
assert.Len(t, 8, got)
|
assert.Equals(t, 9, len(got)) // number of provisioner.SignOptions returned
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -1034,7 +1034,7 @@ func TestAuthority_authorizeSSHSign(t *testing.T) {
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if assert.Nil(t, tc.err) {
|
if assert.Nil(t, tc.err) {
|
||||||
assert.Len(t, 7, got)
|
assert.Len(t, 8, got) // number of provisioner.SignOptions returned
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
@ -8,12 +8,15 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
|
|
||||||
|
"go.step.sm/linkedca"
|
||||||
|
|
||||||
|
"github.com/smallstep/certificates/authority/policy"
|
||||||
"github.com/smallstep/certificates/authority/provisioner"
|
"github.com/smallstep/certificates/authority/provisioner"
|
||||||
cas "github.com/smallstep/certificates/cas/apiv1"
|
cas "github.com/smallstep/certificates/cas/apiv1"
|
||||||
"github.com/smallstep/certificates/db"
|
"github.com/smallstep/certificates/db"
|
||||||
kms "github.com/smallstep/certificates/kms/apiv1"
|
kms "github.com/smallstep/certificates/kms/apiv1"
|
||||||
"github.com/smallstep/certificates/templates"
|
"github.com/smallstep/certificates/templates"
|
||||||
"go.step.sm/linkedca"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@ -95,6 +98,7 @@ type AuthConfig struct {
|
||||||
Admins []*linkedca.Admin `json:"-"`
|
Admins []*linkedca.Admin `json:"-"`
|
||||||
Template *ASN1DN `json:"template,omitempty"`
|
Template *ASN1DN `json:"template,omitempty"`
|
||||||
Claims *provisioner.Claims `json:"claims,omitempty"`
|
Claims *provisioner.Claims `json:"claims,omitempty"`
|
||||||
|
Policy *policy.Options `json:"policy,omitempty"`
|
||||||
DisableIssuedAtCheck bool `json:"disableIssuedAtCheck,omitempty"`
|
DisableIssuedAtCheck bool `json:"disableIssuedAtCheck,omitempty"`
|
||||||
Backdate *provisioner.Duration `json:"backdate,omitempty"`
|
Backdate *provisioner.Duration `json:"backdate,omitempty"`
|
||||||
EnableAdmin bool `json:"enableAdmin,omitempty"`
|
EnableAdmin bool `json:"enableAdmin,omitempty"`
|
||||||
|
|
|
@ -15,16 +15,19 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/smallstep/certificates/authority/provisioner"
|
"golang.org/x/crypto/ssh"
|
||||||
"github.com/smallstep/certificates/db"
|
"google.golang.org/grpc"
|
||||||
|
"google.golang.org/grpc/credentials"
|
||||||
|
|
||||||
"go.step.sm/crypto/jose"
|
"go.step.sm/crypto/jose"
|
||||||
"go.step.sm/crypto/keyutil"
|
"go.step.sm/crypto/keyutil"
|
||||||
"go.step.sm/crypto/tlsutil"
|
"go.step.sm/crypto/tlsutil"
|
||||||
"go.step.sm/crypto/x509util"
|
"go.step.sm/crypto/x509util"
|
||||||
"go.step.sm/linkedca"
|
"go.step.sm/linkedca"
|
||||||
"golang.org/x/crypto/ssh"
|
|
||||||
"google.golang.org/grpc"
|
"github.com/smallstep/certificates/authority/admin"
|
||||||
"google.golang.org/grpc/credentials"
|
"github.com/smallstep/certificates/authority/provisioner"
|
||||||
|
"github.com/smallstep/certificates/db"
|
||||||
)
|
)
|
||||||
|
|
||||||
const uuidPattern = "^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}$"
|
const uuidPattern = "^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}$"
|
||||||
|
@ -35,6 +38,9 @@ type linkedCaClient struct {
|
||||||
authorityID string
|
authorityID string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// interface guard
|
||||||
|
var _ admin.DB = (*linkedCaClient)(nil)
|
||||||
|
|
||||||
type linkedCAClaims struct {
|
type linkedCAClaims struct {
|
||||||
jose.Claims
|
jose.Claims
|
||||||
SANs []string `json:"sans"`
|
SANs []string `json:"sans"`
|
||||||
|
@ -116,6 +122,13 @@ func newLinkedCAClient(token string) (*linkedCaClient, error) {
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IsLinkedCA is a sentinel function that can be used to
|
||||||
|
// check if a linkedCaClient is the underlying type of an
|
||||||
|
// admin.DB interface.
|
||||||
|
func (c *linkedCaClient) IsLinkedCA() bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
func (c *linkedCaClient) Run() {
|
func (c *linkedCaClient) Run() {
|
||||||
c.renewer.Run()
|
c.renewer.Run()
|
||||||
}
|
}
|
||||||
|
@ -340,6 +353,22 @@ func (c *linkedCaClient) IsSSHRevoked(serial string) (bool, error) {
|
||||||
return resp.Status != linkedca.RevocationStatus_ACTIVE, nil
|
return resp.Status != linkedca.RevocationStatus_ACTIVE, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *linkedCaClient) CreateAuthorityPolicy(ctx context.Context, policy *linkedca.Policy) error {
|
||||||
|
return errors.New("not implemented yet")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *linkedCaClient) GetAuthorityPolicy(ctx context.Context) (*linkedca.Policy, error) {
|
||||||
|
return nil, errors.New("not implemented yet")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *linkedCaClient) UpdateAuthorityPolicy(ctx context.Context, policy *linkedca.Policy) error {
|
||||||
|
return errors.New("not implemented yet")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *linkedCaClient) DeleteAuthorityPolicy(ctx context.Context) error {
|
||||||
|
return errors.New("not implemented yet")
|
||||||
|
}
|
||||||
|
|
||||||
func createProvisionerIdentity(prov provisioner.Interface) *linkedca.ProvisionerIdentity {
|
func createProvisionerIdentity(prov provisioner.Interface) *linkedca.ProvisionerIdentity {
|
||||||
if prov == nil {
|
if prov == nil {
|
||||||
return nil
|
return nil
|
||||||
|
|
258
authority/policy.go
Normal file
258
authority/policy.go
Normal file
|
@ -0,0 +1,258 @@
|
||||||
|
package authority
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"go.step.sm/linkedca"
|
||||||
|
|
||||||
|
"github.com/smallstep/certificates/authority/admin"
|
||||||
|
authPolicy "github.com/smallstep/certificates/authority/policy"
|
||||||
|
policy "github.com/smallstep/certificates/policy"
|
||||||
|
)
|
||||||
|
|
||||||
|
type policyErrorType int
|
||||||
|
|
||||||
|
const (
|
||||||
|
AdminLockOut policyErrorType = iota + 1
|
||||||
|
StoreFailure
|
||||||
|
ReloadFailure
|
||||||
|
ConfigurationFailure
|
||||||
|
EvaluationFailure
|
||||||
|
InternalFailure
|
||||||
|
)
|
||||||
|
|
||||||
|
type PolicyError struct {
|
||||||
|
Typ policyErrorType
|
||||||
|
Err error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *PolicyError) Error() string {
|
||||||
|
return p.Err.Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Authority) GetAuthorityPolicy(ctx context.Context) (*linkedca.Policy, error) {
|
||||||
|
a.adminMutex.Lock()
|
||||||
|
defer a.adminMutex.Unlock()
|
||||||
|
|
||||||
|
p, err := a.adminDB.GetAuthorityPolicy(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, &PolicyError{
|
||||||
|
Typ: InternalFailure,
|
||||||
|
Err: err,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return p, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Authority) CreateAuthorityPolicy(ctx context.Context, adm *linkedca.Admin, p *linkedca.Policy) (*linkedca.Policy, error) {
|
||||||
|
a.adminMutex.Lock()
|
||||||
|
defer a.adminMutex.Unlock()
|
||||||
|
|
||||||
|
if err := a.checkAuthorityPolicy(ctx, adm, p); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := a.adminDB.CreateAuthorityPolicy(ctx, p); err != nil {
|
||||||
|
return nil, &PolicyError{
|
||||||
|
Typ: StoreFailure,
|
||||||
|
Err: err,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := a.reloadPolicyEngines(ctx); err != nil {
|
||||||
|
return nil, &PolicyError{
|
||||||
|
Typ: ReloadFailure,
|
||||||
|
Err: fmt.Errorf("error reloading policy engines when creating authority policy: %w", err),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return p, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Authority) UpdateAuthorityPolicy(ctx context.Context, adm *linkedca.Admin, p *linkedca.Policy) (*linkedca.Policy, error) {
|
||||||
|
a.adminMutex.Lock()
|
||||||
|
defer a.adminMutex.Unlock()
|
||||||
|
|
||||||
|
if err := a.checkAuthorityPolicy(ctx, adm, p); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := a.adminDB.UpdateAuthorityPolicy(ctx, p); err != nil {
|
||||||
|
return nil, &PolicyError{
|
||||||
|
Typ: StoreFailure,
|
||||||
|
Err: err,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := a.reloadPolicyEngines(ctx); err != nil {
|
||||||
|
return nil, &PolicyError{
|
||||||
|
Typ: ReloadFailure,
|
||||||
|
Err: fmt.Errorf("error reloading policy engines when updating authority policy: %w", err),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return p, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Authority) RemoveAuthorityPolicy(ctx context.Context) error {
|
||||||
|
a.adminMutex.Lock()
|
||||||
|
defer a.adminMutex.Unlock()
|
||||||
|
|
||||||
|
if err := a.adminDB.DeleteAuthorityPolicy(ctx); err != nil {
|
||||||
|
return &PolicyError{
|
||||||
|
Typ: StoreFailure,
|
||||||
|
Err: err,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := a.reloadPolicyEngines(ctx); err != nil {
|
||||||
|
return &PolicyError{
|
||||||
|
Typ: ReloadFailure,
|
||||||
|
Err: fmt.Errorf("error reloading policy engines when deleting authority policy: %w", err),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Authority) checkAuthorityPolicy(ctx context.Context, currentAdmin *linkedca.Admin, p *linkedca.Policy) error {
|
||||||
|
|
||||||
|
// no policy and thus nothing to evaluate; return early
|
||||||
|
if p == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// get all current admins from the database
|
||||||
|
allAdmins, err := a.adminDB.GetAdmins(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return &PolicyError{
|
||||||
|
Typ: InternalFailure,
|
||||||
|
Err: fmt.Errorf("error retrieving admins: %w", err),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return a.checkPolicy(ctx, currentAdmin, allAdmins, p)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Authority) checkProvisionerPolicy(ctx context.Context, currentAdmin *linkedca.Admin, provName string, p *linkedca.Policy) error {
|
||||||
|
|
||||||
|
// no policy and thus nothing to evaluate; return early
|
||||||
|
if p == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// get all admins for the provisioner; ignoring case in which they're not found
|
||||||
|
allProvisionerAdmins, _ := a.admins.LoadByProvisioner(provName)
|
||||||
|
|
||||||
|
return a.checkPolicy(ctx, currentAdmin, allProvisionerAdmins, p)
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkPolicy checks if a new or updated policy configuration results in the user
|
||||||
|
// locking themselves or other admins out of the CA.
|
||||||
|
func (a *Authority) checkPolicy(ctx context.Context, currentAdmin *linkedca.Admin, otherAdmins []*linkedca.Admin, p *linkedca.Policy) error {
|
||||||
|
|
||||||
|
// convert the policy; return early if nil
|
||||||
|
policyOptions := authPolicy.LinkedToCertificates(p)
|
||||||
|
if policyOptions == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
engine, err := authPolicy.NewX509PolicyEngine(policyOptions.GetX509Options())
|
||||||
|
if err != nil {
|
||||||
|
return &PolicyError{
|
||||||
|
Typ: ConfigurationFailure,
|
||||||
|
Err: err,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// when an empty X.509 policy is provided, the resulting engine is nil
|
||||||
|
// and there's no policy to evaluate.
|
||||||
|
if engine == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO(hs): Provide option to force the policy, even when the admin subject would be locked out?
|
||||||
|
|
||||||
|
// check if the admin user that instructed the authority policy to be
|
||||||
|
// created or updated, would still be allowed when the provided policy
|
||||||
|
// would be applied.
|
||||||
|
sans := []string{currentAdmin.GetSubject()}
|
||||||
|
if err := isAllowed(engine, sans); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// loop through admins to verify that none of them would be
|
||||||
|
// locked out when the new policy were to be applied. Returns
|
||||||
|
// an error with a message that includes the admin subject that
|
||||||
|
// would be locked out.
|
||||||
|
for _, adm := range otherAdmins {
|
||||||
|
sans = []string{adm.GetSubject()}
|
||||||
|
if err := isAllowed(engine, sans); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO(hs): mask the error message for non-super admins?
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// reloadPolicyEngines reloads x509 and SSH policy engines using
|
||||||
|
// configuration stored in the DB or from the configuration file.
|
||||||
|
func (a *Authority) reloadPolicyEngines(ctx context.Context) error {
|
||||||
|
var (
|
||||||
|
err error
|
||||||
|
policyOptions *authPolicy.Options
|
||||||
|
)
|
||||||
|
|
||||||
|
if a.config.AuthorityConfig.EnableAdmin {
|
||||||
|
|
||||||
|
// temporarily disable policy loading when LinkedCA is in use
|
||||||
|
if _, ok := a.adminDB.(*linkedCaClient); ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
linkedPolicy, err := a.adminDB.GetAuthorityPolicy(ctx)
|
||||||
|
if err != nil {
|
||||||
|
var ae *admin.Error
|
||||||
|
if isAdminError := errors.As(err, &ae); (isAdminError && ae.Type != admin.ErrorNotFoundType.String()) || !isAdminError {
|
||||||
|
return fmt.Errorf("error getting policy to (re)load policy engines: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
policyOptions = authPolicy.LinkedToCertificates(linkedPolicy)
|
||||||
|
} else {
|
||||||
|
policyOptions = a.config.AuthorityConfig.Policy
|
||||||
|
}
|
||||||
|
|
||||||
|
engine, err := authPolicy.New(policyOptions)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// only update the policy engine when no error was returned
|
||||||
|
a.policyEngine = engine
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func isAllowed(engine authPolicy.X509Policy, sans []string) error {
|
||||||
|
if err := engine.AreSANsAllowed(sans); err != nil {
|
||||||
|
var policyErr *policy.NamePolicyError
|
||||||
|
isNamePolicyError := errors.As(err, &policyErr)
|
||||||
|
if isNamePolicyError && policyErr.Reason == policy.NotAllowed {
|
||||||
|
return &PolicyError{
|
||||||
|
Typ: AdminLockOut,
|
||||||
|
Err: fmt.Errorf("the provided policy would lock out %s from the CA. Please update your policy to include %s as an allowed name", sans, sans),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return &PolicyError{
|
||||||
|
Typ: EvaluationFailure,
|
||||||
|
Err: err,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
114
authority/policy/engine.go
Normal file
114
authority/policy/engine.go
Normal file
|
@ -0,0 +1,114 @@
|
||||||
|
package policy
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/x509"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"golang.org/x/crypto/ssh"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Engine is a container for multiple policies.
|
||||||
|
type Engine struct {
|
||||||
|
x509Policy X509Policy
|
||||||
|
sshUserPolicy UserPolicy
|
||||||
|
sshHostPolicy HostPolicy
|
||||||
|
}
|
||||||
|
|
||||||
|
// New returns a new Engine using Options.
|
||||||
|
func New(options *Options) (*Engine, error) {
|
||||||
|
|
||||||
|
// if no options provided, return early
|
||||||
|
if options == nil {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
x509Policy X509Policy
|
||||||
|
sshHostPolicy HostPolicy
|
||||||
|
sshUserPolicy UserPolicy
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
|
||||||
|
// initialize the x509 allow/deny policy engine
|
||||||
|
if x509Policy, err = NewX509PolicyEngine(options.GetX509Options()); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// initialize the SSH allow/deny policy engine for host certificates
|
||||||
|
if sshHostPolicy, err = NewSSHHostPolicyEngine(options.GetSSHOptions()); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// initialize the SSH allow/deny policy engine for user certificates
|
||||||
|
if sshUserPolicy, err = NewSSHUserPolicyEngine(options.GetSSHOptions()); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Engine{
|
||||||
|
x509Policy: x509Policy,
|
||||||
|
sshHostPolicy: sshHostPolicy,
|
||||||
|
sshUserPolicy: sshUserPolicy,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsX509CertificateAllowed evaluates an X.509 certificate against
|
||||||
|
// the X.509 policy (if available) and returns an error if one of the
|
||||||
|
// names in the certificate is not allowed.
|
||||||
|
func (e *Engine) IsX509CertificateAllowed(cert *x509.Certificate) error {
|
||||||
|
|
||||||
|
// return early if there's no policy to evaluate
|
||||||
|
if e == nil || e.x509Policy == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// return result of X.509 policy evaluation
|
||||||
|
return e.x509Policy.IsX509CertificateAllowed(cert)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AreSANsAllowed evaluates the slice of SANs against the X.509 policy
|
||||||
|
// (if available) and returns an error if one of the SANs is not allowed.
|
||||||
|
func (e *Engine) AreSANsAllowed(sans []string) error {
|
||||||
|
|
||||||
|
// return early if there's no policy to evaluate
|
||||||
|
if e == nil || e.x509Policy == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// return result of X.509 policy evaluation
|
||||||
|
return e.x509Policy.AreSANsAllowed(sans)
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsSSHCertificateAllowed evaluates an SSH certificate against the
|
||||||
|
// user or host policy (if configured) and returns an error if one of the
|
||||||
|
// principals in the certificate is not allowed.
|
||||||
|
func (e *Engine) IsSSHCertificateAllowed(cert *ssh.Certificate) error {
|
||||||
|
|
||||||
|
// return early if there's no policy to evaluate
|
||||||
|
if e == nil || (e.sshHostPolicy == nil && e.sshUserPolicy == nil) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
switch cert.CertType {
|
||||||
|
case ssh.HostCert:
|
||||||
|
// when no host policy engine is configured, but a user policy engine is
|
||||||
|
// configured, the host certificate is denied.
|
||||||
|
if e.sshHostPolicy == nil && e.sshUserPolicy != nil {
|
||||||
|
return errors.New("authority not allowed to sign ssh host certificates")
|
||||||
|
}
|
||||||
|
|
||||||
|
// return result of SSH host policy evaluation
|
||||||
|
return e.sshHostPolicy.IsSSHCertificateAllowed(cert)
|
||||||
|
case ssh.UserCert:
|
||||||
|
// when no user policy engine is configured, but a host policy engine is
|
||||||
|
// configured, the user certificate is denied.
|
||||||
|
if e.sshUserPolicy == nil && e.sshHostPolicy != nil {
|
||||||
|
return errors.New("authority not allowed to sign ssh user certificates")
|
||||||
|
}
|
||||||
|
|
||||||
|
// return result of SSH user policy evaluation
|
||||||
|
return e.sshUserPolicy.IsSSHCertificateAllowed(cert)
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("unexpected ssh certificate type %q", cert.CertType)
|
||||||
|
}
|
||||||
|
}
|
194
authority/policy/options.go
Normal file
194
authority/policy/options.go
Normal file
|
@ -0,0 +1,194 @@
|
||||||
|
package policy
|
||||||
|
|
||||||
|
// Options is a container for authority level x509 and SSH
|
||||||
|
// policy configuration.
|
||||||
|
type Options struct {
|
||||||
|
X509 *X509PolicyOptions `json:"x509,omitempty"`
|
||||||
|
SSH *SSHPolicyOptions `json:"ssh,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetX509Options returns the x509 authority level policy
|
||||||
|
// configuration
|
||||||
|
func (o *Options) GetX509Options() *X509PolicyOptions {
|
||||||
|
if o == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return o.X509
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSSHOptions returns the SSH authority level policy
|
||||||
|
// configuration
|
||||||
|
func (o *Options) GetSSHOptions() *SSHPolicyOptions {
|
||||||
|
if o == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return o.SSH
|
||||||
|
}
|
||||||
|
|
||||||
|
// X509PolicyOptionsInterface is an interface for providers
|
||||||
|
// of x509 allowed and denied names.
|
||||||
|
type X509PolicyOptionsInterface interface {
|
||||||
|
GetAllowedNameOptions() *X509NameOptions
|
||||||
|
GetDeniedNameOptions() *X509NameOptions
|
||||||
|
AreWildcardNamesAllowed() bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// X509PolicyOptions is a container for x509 allowed and denied
|
||||||
|
// names.
|
||||||
|
type X509PolicyOptions struct {
|
||||||
|
// AllowedNames contains the x509 allowed names
|
||||||
|
AllowedNames *X509NameOptions `json:"allow,omitempty"`
|
||||||
|
|
||||||
|
// DeniedNames contains the x509 denied names
|
||||||
|
DeniedNames *X509NameOptions `json:"deny,omitempty"`
|
||||||
|
|
||||||
|
// AllowWildcardNames indicates if literal wildcard names
|
||||||
|
// like *.example.com are allowed. Defaults to false.
|
||||||
|
AllowWildcardNames bool `json:"allowWildcardNames,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// X509NameOptions models the X509 name policy configuration.
|
||||||
|
type X509NameOptions struct {
|
||||||
|
CommonNames []string `json:"cn,omitempty"`
|
||||||
|
DNSDomains []string `json:"dns,omitempty"`
|
||||||
|
IPRanges []string `json:"ip,omitempty"`
|
||||||
|
EmailAddresses []string `json:"email,omitempty"`
|
||||||
|
URIDomains []string `json:"uri,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// HasNames checks if the AllowedNameOptions has one or more
|
||||||
|
// names configured.
|
||||||
|
func (o *X509NameOptions) HasNames() bool {
|
||||||
|
return len(o.CommonNames) > 0 ||
|
||||||
|
len(o.DNSDomains) > 0 ||
|
||||||
|
len(o.IPRanges) > 0 ||
|
||||||
|
len(o.EmailAddresses) > 0 ||
|
||||||
|
len(o.URIDomains) > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAllowedNameOptions returns x509 allowed name policy configuration
|
||||||
|
func (o *X509PolicyOptions) GetAllowedNameOptions() *X509NameOptions {
|
||||||
|
if o == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return o.AllowedNames
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetDeniedNameOptions returns the x509 denied name policy configuration
|
||||||
|
func (o *X509PolicyOptions) GetDeniedNameOptions() *X509NameOptions {
|
||||||
|
if o == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return o.DeniedNames
|
||||||
|
}
|
||||||
|
|
||||||
|
// AreWildcardNamesAllowed returns whether the authority allows
|
||||||
|
// literal wildcard names to be signed.
|
||||||
|
func (o *X509PolicyOptions) AreWildcardNamesAllowed() bool {
|
||||||
|
if o == nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return o.AllowWildcardNames
|
||||||
|
}
|
||||||
|
|
||||||
|
// SSHPolicyOptionsInterface is an interface for providers of
|
||||||
|
// SSH user and host name policy configuration.
|
||||||
|
type SSHPolicyOptionsInterface interface {
|
||||||
|
GetAllowedUserNameOptions() *SSHNameOptions
|
||||||
|
GetDeniedUserNameOptions() *SSHNameOptions
|
||||||
|
GetAllowedHostNameOptions() *SSHNameOptions
|
||||||
|
GetDeniedHostNameOptions() *SSHNameOptions
|
||||||
|
}
|
||||||
|
|
||||||
|
// SSHPolicyOptions is a container for SSH user and host policy
|
||||||
|
// configuration
|
||||||
|
type SSHPolicyOptions struct {
|
||||||
|
// User contains SSH user certificate options.
|
||||||
|
User *SSHUserCertificateOptions `json:"user,omitempty"`
|
||||||
|
// Host contains SSH host certificate options.
|
||||||
|
Host *SSHHostCertificateOptions `json:"host,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAllowedUserNameOptions returns the SSH allowed user name policy
|
||||||
|
// configuration.
|
||||||
|
func (o *SSHPolicyOptions) GetAllowedUserNameOptions() *SSHNameOptions {
|
||||||
|
if o == nil || o.User == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return o.User.AllowedNames
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetDeniedUserNameOptions returns the SSH denied user name policy
|
||||||
|
// configuration.
|
||||||
|
func (o *SSHPolicyOptions) GetDeniedUserNameOptions() *SSHNameOptions {
|
||||||
|
if o == nil || o.User == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return o.User.DeniedNames
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAllowedHostNameOptions returns the SSH allowed host name policy
|
||||||
|
// configuration.
|
||||||
|
func (o *SSHPolicyOptions) GetAllowedHostNameOptions() *SSHNameOptions {
|
||||||
|
if o == nil || o.Host == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return o.Host.AllowedNames
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetDeniedHostNameOptions returns the SSH denied host name policy
|
||||||
|
// configuration.
|
||||||
|
func (o *SSHPolicyOptions) GetDeniedHostNameOptions() *SSHNameOptions {
|
||||||
|
if o == nil || o.Host == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return o.Host.DeniedNames
|
||||||
|
}
|
||||||
|
|
||||||
|
// SSHUserCertificateOptions is a collection of SSH user certificate options.
|
||||||
|
type SSHUserCertificateOptions struct {
|
||||||
|
// AllowedNames contains the names the provisioner is authorized to sign
|
||||||
|
AllowedNames *SSHNameOptions `json:"allow,omitempty"`
|
||||||
|
// DeniedNames contains the names the provisioner is not authorized to sign
|
||||||
|
DeniedNames *SSHNameOptions `json:"deny,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// SSHHostCertificateOptions is a collection of SSH host certificate options.
|
||||||
|
// It's an alias of SSHUserCertificateOptions, as the options are the same
|
||||||
|
// for both types of certificates.
|
||||||
|
type SSHHostCertificateOptions SSHUserCertificateOptions
|
||||||
|
|
||||||
|
// SSHNameOptions models the SSH name policy configuration.
|
||||||
|
type SSHNameOptions struct {
|
||||||
|
DNSDomains []string `json:"dns,omitempty"`
|
||||||
|
IPRanges []string `json:"ip,omitempty"`
|
||||||
|
EmailAddresses []string `json:"email,omitempty"`
|
||||||
|
Principals []string `json:"principal,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAllowedNameOptions returns the AllowedSSHNameOptions, which models the
|
||||||
|
// names that a provisioner is authorized to sign SSH certificates for.
|
||||||
|
func (o *SSHUserCertificateOptions) GetAllowedNameOptions() *SSHNameOptions {
|
||||||
|
if o == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return o.AllowedNames
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetDeniedNameOptions returns the DeniedSSHNameOptions, which models the
|
||||||
|
// names that a provisioner is NOT authorized to sign SSH certificates for.
|
||||||
|
func (o *SSHUserCertificateOptions) GetDeniedNameOptions() *SSHNameOptions {
|
||||||
|
if o == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return o.DeniedNames
|
||||||
|
}
|
||||||
|
|
||||||
|
// HasNames checks if the SSHNameOptions has one or more
|
||||||
|
// names configured.
|
||||||
|
func (o *SSHNameOptions) HasNames() bool {
|
||||||
|
return len(o.DNSDomains) > 0 ||
|
||||||
|
len(o.IPRanges) > 0 ||
|
||||||
|
len(o.EmailAddresses) > 0 ||
|
||||||
|
len(o.Principals) > 0
|
||||||
|
}
|
45
authority/policy/options_test.go
Normal file
45
authority/policy/options_test.go
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
package policy
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestX509PolicyOptions_IsWildcardLiteralAllowed(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
options *X509PolicyOptions
|
||||||
|
want bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "nil-options",
|
||||||
|
options: nil,
|
||||||
|
want: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "not-set",
|
||||||
|
options: &X509PolicyOptions{},
|
||||||
|
want: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "set-true",
|
||||||
|
options: &X509PolicyOptions{
|
||||||
|
AllowWildcardNames: true,
|
||||||
|
},
|
||||||
|
want: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "set-false",
|
||||||
|
options: &X509PolicyOptions{
|
||||||
|
AllowWildcardNames: false,
|
||||||
|
},
|
||||||
|
want: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
if got := tt.options.AreWildcardNamesAllowed(); got != tt.want {
|
||||||
|
t.Errorf("X509PolicyOptions.IsWildcardLiteralAllowed() = %v, want %v", got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
256
authority/policy/policy.go
Normal file
256
authority/policy/policy.go
Normal file
|
@ -0,0 +1,256 @@
|
||||||
|
package policy
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"go.step.sm/linkedca"
|
||||||
|
|
||||||
|
"github.com/smallstep/certificates/policy"
|
||||||
|
)
|
||||||
|
|
||||||
|
// X509Policy is an alias for policy.X509NamePolicyEngine
|
||||||
|
type X509Policy policy.X509NamePolicyEngine
|
||||||
|
|
||||||
|
// UserPolicy is an alias for policy.SSHNamePolicyEngine
|
||||||
|
type UserPolicy policy.SSHNamePolicyEngine
|
||||||
|
|
||||||
|
// HostPolicy is an alias for policy.SSHNamePolicyEngine
|
||||||
|
type HostPolicy policy.SSHNamePolicyEngine
|
||||||
|
|
||||||
|
// NewX509PolicyEngine creates a new x509 name policy engine
|
||||||
|
func NewX509PolicyEngine(policyOptions X509PolicyOptionsInterface) (X509Policy, error) {
|
||||||
|
|
||||||
|
// return early if no policy engine options to configure
|
||||||
|
if policyOptions == nil {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
options := []policy.NamePolicyOption{}
|
||||||
|
|
||||||
|
allowed := policyOptions.GetAllowedNameOptions()
|
||||||
|
if allowed != nil && allowed.HasNames() {
|
||||||
|
options = append(options,
|
||||||
|
policy.WithPermittedCommonNames(allowed.CommonNames...),
|
||||||
|
policy.WithPermittedDNSDomains(allowed.DNSDomains...),
|
||||||
|
policy.WithPermittedIPsOrCIDRs(allowed.IPRanges...),
|
||||||
|
policy.WithPermittedEmailAddresses(allowed.EmailAddresses...),
|
||||||
|
policy.WithPermittedURIDomains(allowed.URIDomains...),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
denied := policyOptions.GetDeniedNameOptions()
|
||||||
|
if denied != nil && denied.HasNames() {
|
||||||
|
options = append(options,
|
||||||
|
policy.WithExcludedCommonNames(denied.CommonNames...),
|
||||||
|
policy.WithExcludedDNSDomains(denied.DNSDomains...),
|
||||||
|
policy.WithExcludedIPsOrCIDRs(denied.IPRanges...),
|
||||||
|
policy.WithExcludedEmailAddresses(denied.EmailAddresses...),
|
||||||
|
policy.WithExcludedURIDomains(denied.URIDomains...),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ensure no policy engine is returned when no name options were provided
|
||||||
|
if len(options) == 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if configuration specifies that wildcard names are allowed
|
||||||
|
if policyOptions.AreWildcardNamesAllowed() {
|
||||||
|
options = append(options, policy.WithAllowLiteralWildcardNames())
|
||||||
|
}
|
||||||
|
|
||||||
|
// enable subject common name verification by default
|
||||||
|
options = append(options, policy.WithSubjectCommonNameVerification())
|
||||||
|
|
||||||
|
return policy.New(options...)
|
||||||
|
}
|
||||||
|
|
||||||
|
type sshPolicyEngineType string
|
||||||
|
|
||||||
|
const (
|
||||||
|
UserPolicyEngineType sshPolicyEngineType = "user"
|
||||||
|
HostPolicyEngineType sshPolicyEngineType = "host"
|
||||||
|
)
|
||||||
|
|
||||||
|
// newSSHUserPolicyEngine creates a new SSH user certificate policy engine
|
||||||
|
func NewSSHUserPolicyEngine(policyOptions SSHPolicyOptionsInterface) (UserPolicy, error) {
|
||||||
|
policyEngine, err := newSSHPolicyEngine(policyOptions, UserPolicyEngineType)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return policyEngine, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// newSSHHostPolicyEngine create a new SSH host certificate policy engine
|
||||||
|
func NewSSHHostPolicyEngine(policyOptions SSHPolicyOptionsInterface) (HostPolicy, error) {
|
||||||
|
policyEngine, err := newSSHPolicyEngine(policyOptions, HostPolicyEngineType)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return policyEngine, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// newSSHPolicyEngine creates a new SSH name policy engine
|
||||||
|
func newSSHPolicyEngine(policyOptions SSHPolicyOptionsInterface, typ sshPolicyEngineType) (policy.SSHNamePolicyEngine, error) {
|
||||||
|
|
||||||
|
// return early if no policy engine options to configure
|
||||||
|
if policyOptions == nil {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
allowed *SSHNameOptions
|
||||||
|
denied *SSHNameOptions
|
||||||
|
)
|
||||||
|
|
||||||
|
switch typ {
|
||||||
|
case UserPolicyEngineType:
|
||||||
|
allowed = policyOptions.GetAllowedUserNameOptions()
|
||||||
|
denied = policyOptions.GetDeniedUserNameOptions()
|
||||||
|
case HostPolicyEngineType:
|
||||||
|
allowed = policyOptions.GetAllowedHostNameOptions()
|
||||||
|
denied = policyOptions.GetDeniedHostNameOptions()
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("unknown SSH policy engine type %s provided", typ)
|
||||||
|
}
|
||||||
|
|
||||||
|
options := []policy.NamePolicyOption{}
|
||||||
|
|
||||||
|
if allowed != nil && allowed.HasNames() {
|
||||||
|
options = append(options,
|
||||||
|
policy.WithPermittedDNSDomains(allowed.DNSDomains...),
|
||||||
|
policy.WithPermittedIPsOrCIDRs(allowed.IPRanges...),
|
||||||
|
policy.WithPermittedEmailAddresses(allowed.EmailAddresses...),
|
||||||
|
policy.WithPermittedPrincipals(allowed.Principals...),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if denied != nil && denied.HasNames() {
|
||||||
|
options = append(options,
|
||||||
|
policy.WithExcludedDNSDomains(denied.DNSDomains...),
|
||||||
|
policy.WithExcludedIPsOrCIDRs(denied.IPRanges...),
|
||||||
|
policy.WithExcludedEmailAddresses(denied.EmailAddresses...),
|
||||||
|
policy.WithExcludedPrincipals(denied.Principals...),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ensure no policy engine is returned when no name options were provided
|
||||||
|
if len(options) == 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return policy.New(options...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func LinkedToCertificates(p *linkedca.Policy) *Options {
|
||||||
|
|
||||||
|
// return early
|
||||||
|
if p == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// return early if x509 nor SSH is set
|
||||||
|
if p.GetX509() == nil && p.GetSsh() == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
opts := &Options{}
|
||||||
|
|
||||||
|
// fill x509 policy configuration
|
||||||
|
if x509 := p.GetX509(); x509 != nil {
|
||||||
|
opts.X509 = &X509PolicyOptions{}
|
||||||
|
if allow := x509.GetAllow(); allow != nil {
|
||||||
|
opts.X509.AllowedNames = &X509NameOptions{}
|
||||||
|
if allow.Dns != nil {
|
||||||
|
opts.X509.AllowedNames.DNSDomains = allow.Dns
|
||||||
|
}
|
||||||
|
if allow.Ips != nil {
|
||||||
|
opts.X509.AllowedNames.IPRanges = allow.Ips
|
||||||
|
}
|
||||||
|
if allow.Emails != nil {
|
||||||
|
opts.X509.AllowedNames.EmailAddresses = allow.Emails
|
||||||
|
}
|
||||||
|
if allow.Uris != nil {
|
||||||
|
opts.X509.AllowedNames.URIDomains = allow.Uris
|
||||||
|
}
|
||||||
|
if allow.CommonNames != nil {
|
||||||
|
opts.X509.AllowedNames.CommonNames = allow.CommonNames
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if deny := x509.GetDeny(); deny != nil {
|
||||||
|
opts.X509.DeniedNames = &X509NameOptions{}
|
||||||
|
if deny.Dns != nil {
|
||||||
|
opts.X509.DeniedNames.DNSDomains = deny.Dns
|
||||||
|
}
|
||||||
|
if deny.Ips != nil {
|
||||||
|
opts.X509.DeniedNames.IPRanges = deny.Ips
|
||||||
|
}
|
||||||
|
if deny.Emails != nil {
|
||||||
|
opts.X509.DeniedNames.EmailAddresses = deny.Emails
|
||||||
|
}
|
||||||
|
if deny.Uris != nil {
|
||||||
|
opts.X509.DeniedNames.URIDomains = deny.Uris
|
||||||
|
}
|
||||||
|
if deny.CommonNames != nil {
|
||||||
|
opts.X509.DeniedNames.CommonNames = deny.CommonNames
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
opts.X509.AllowWildcardNames = x509.GetAllowWildcardNames()
|
||||||
|
}
|
||||||
|
|
||||||
|
// fill ssh policy configuration
|
||||||
|
if ssh := p.GetSsh(); ssh != nil {
|
||||||
|
opts.SSH = &SSHPolicyOptions{}
|
||||||
|
if host := ssh.GetHost(); host != nil {
|
||||||
|
opts.SSH.Host = &SSHHostCertificateOptions{}
|
||||||
|
if allow := host.GetAllow(); allow != nil {
|
||||||
|
opts.SSH.Host.AllowedNames = &SSHNameOptions{}
|
||||||
|
if allow.Dns != nil {
|
||||||
|
opts.SSH.Host.AllowedNames.DNSDomains = allow.Dns
|
||||||
|
}
|
||||||
|
if allow.Ips != nil {
|
||||||
|
opts.SSH.Host.AllowedNames.IPRanges = allow.Ips
|
||||||
|
}
|
||||||
|
if allow.Principals != nil {
|
||||||
|
opts.SSH.Host.AllowedNames.Principals = allow.Principals
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if deny := host.GetDeny(); deny != nil {
|
||||||
|
opts.SSH.Host.DeniedNames = &SSHNameOptions{}
|
||||||
|
if deny.Dns != nil {
|
||||||
|
opts.SSH.Host.DeniedNames.DNSDomains = deny.Dns
|
||||||
|
}
|
||||||
|
if deny.Ips != nil {
|
||||||
|
opts.SSH.Host.DeniedNames.IPRanges = deny.Ips
|
||||||
|
}
|
||||||
|
if deny.Principals != nil {
|
||||||
|
opts.SSH.Host.DeniedNames.Principals = deny.Principals
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if user := ssh.GetUser(); user != nil {
|
||||||
|
opts.SSH.User = &SSHUserCertificateOptions{}
|
||||||
|
if allow := user.GetAllow(); allow != nil {
|
||||||
|
opts.SSH.User.AllowedNames = &SSHNameOptions{}
|
||||||
|
if allow.Emails != nil {
|
||||||
|
opts.SSH.User.AllowedNames.EmailAddresses = allow.Emails
|
||||||
|
}
|
||||||
|
if allow.Principals != nil {
|
||||||
|
opts.SSH.User.AllowedNames.Principals = allow.Principals
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if deny := user.GetDeny(); deny != nil {
|
||||||
|
opts.SSH.User.DeniedNames = &SSHNameOptions{}
|
||||||
|
if deny.Emails != nil {
|
||||||
|
opts.SSH.User.DeniedNames.EmailAddresses = deny.Emails
|
||||||
|
}
|
||||||
|
if deny.Principals != nil {
|
||||||
|
opts.SSH.User.DeniedNames.Principals = deny.Principals
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return opts
|
||||||
|
}
|
155
authority/policy/policy_test.go
Normal file
155
authority/policy/policy_test.go
Normal file
|
@ -0,0 +1,155 @@
|
||||||
|
package policy
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/google/go-cmp/cmp"
|
||||||
|
|
||||||
|
"go.step.sm/linkedca"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestPolicyToCertificates(t *testing.T) {
|
||||||
|
type args struct {
|
||||||
|
policy *linkedca.Policy
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
args args
|
||||||
|
want *Options
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "nil",
|
||||||
|
args: args{
|
||||||
|
policy: nil,
|
||||||
|
},
|
||||||
|
want: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "no-policy",
|
||||||
|
args: args{
|
||||||
|
&linkedca.Policy{},
|
||||||
|
},
|
||||||
|
want: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "partial-policy",
|
||||||
|
args: args{
|
||||||
|
&linkedca.Policy{
|
||||||
|
X509: &linkedca.X509Policy{
|
||||||
|
Allow: &linkedca.X509Names{
|
||||||
|
Dns: []string{"*.local"},
|
||||||
|
},
|
||||||
|
AllowWildcardNames: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: &Options{
|
||||||
|
X509: &X509PolicyOptions{
|
||||||
|
AllowedNames: &X509NameOptions{
|
||||||
|
DNSDomains: []string{"*.local"},
|
||||||
|
},
|
||||||
|
AllowWildcardNames: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "full-policy",
|
||||||
|
args: args{
|
||||||
|
&linkedca.Policy{
|
||||||
|
X509: &linkedca.X509Policy{
|
||||||
|
Allow: &linkedca.X509Names{
|
||||||
|
Dns: []string{"step"},
|
||||||
|
Ips: []string{"127.0.0.1/24"},
|
||||||
|
Emails: []string{"*.example.com"},
|
||||||
|
Uris: []string{"https://*.local"},
|
||||||
|
CommonNames: []string{"some name"},
|
||||||
|
},
|
||||||
|
Deny: &linkedca.X509Names{
|
||||||
|
Dns: []string{"bad"},
|
||||||
|
Ips: []string{"127.0.0.30"},
|
||||||
|
Emails: []string{"badhost.example.com"},
|
||||||
|
Uris: []string{"https://badhost.local"},
|
||||||
|
CommonNames: []string{"another name"},
|
||||||
|
},
|
||||||
|
AllowWildcardNames: true,
|
||||||
|
},
|
||||||
|
Ssh: &linkedca.SSHPolicy{
|
||||||
|
Host: &linkedca.SSHHostPolicy{
|
||||||
|
Allow: &linkedca.SSHHostNames{
|
||||||
|
Dns: []string{"*.localhost"},
|
||||||
|
Ips: []string{"127.0.0.1/24"},
|
||||||
|
Principals: []string{"user"},
|
||||||
|
},
|
||||||
|
Deny: &linkedca.SSHHostNames{
|
||||||
|
Dns: []string{"badhost.localhost"},
|
||||||
|
Ips: []string{"127.0.0.40"},
|
||||||
|
Principals: []string{"root"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
User: &linkedca.SSHUserPolicy{
|
||||||
|
Allow: &linkedca.SSHUserNames{
|
||||||
|
Emails: []string{"@work"},
|
||||||
|
Principals: []string{"user"},
|
||||||
|
},
|
||||||
|
Deny: &linkedca.SSHUserNames{
|
||||||
|
Emails: []string{"root@work"},
|
||||||
|
Principals: []string{"root"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: &Options{
|
||||||
|
X509: &X509PolicyOptions{
|
||||||
|
AllowedNames: &X509NameOptions{
|
||||||
|
DNSDomains: []string{"step"},
|
||||||
|
IPRanges: []string{"127.0.0.1/24"},
|
||||||
|
EmailAddresses: []string{"*.example.com"},
|
||||||
|
URIDomains: []string{"https://*.local"},
|
||||||
|
CommonNames: []string{"some name"},
|
||||||
|
},
|
||||||
|
DeniedNames: &X509NameOptions{
|
||||||
|
DNSDomains: []string{"bad"},
|
||||||
|
IPRanges: []string{"127.0.0.30"},
|
||||||
|
EmailAddresses: []string{"badhost.example.com"},
|
||||||
|
URIDomains: []string{"https://badhost.local"},
|
||||||
|
CommonNames: []string{"another name"},
|
||||||
|
},
|
||||||
|
AllowWildcardNames: true,
|
||||||
|
},
|
||||||
|
SSH: &SSHPolicyOptions{
|
||||||
|
Host: &SSHHostCertificateOptions{
|
||||||
|
AllowedNames: &SSHNameOptions{
|
||||||
|
DNSDomains: []string{"*.localhost"},
|
||||||
|
IPRanges: []string{"127.0.0.1/24"},
|
||||||
|
Principals: []string{"user"},
|
||||||
|
},
|
||||||
|
DeniedNames: &SSHNameOptions{
|
||||||
|
DNSDomains: []string{"badhost.localhost"},
|
||||||
|
IPRanges: []string{"127.0.0.40"},
|
||||||
|
Principals: []string{"root"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
User: &SSHUserCertificateOptions{
|
||||||
|
AllowedNames: &SSHNameOptions{
|
||||||
|
EmailAddresses: []string{"@work"},
|
||||||
|
Principals: []string{"user"},
|
||||||
|
},
|
||||||
|
DeniedNames: &SSHNameOptions{
|
||||||
|
EmailAddresses: []string{"root@work"},
|
||||||
|
Principals: []string{"root"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
got := LinkedToCertificates(tt.args.policy)
|
||||||
|
if !cmp.Equal(tt.want, got) {
|
||||||
|
t.Errorf("policyToCertificates() diff=\n%s", cmp.Diff(tt.want, got))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
1549
authority/policy_test.go
Normal file
1549
authority/policy_test.go
Normal file
File diff suppressed because it is too large
Load diff
|
@ -3,6 +3,8 @@ package provisioner
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
|
@ -23,7 +25,8 @@ type ACME struct {
|
||||||
RequireEAB bool `json:"requireEAB,omitempty"`
|
RequireEAB bool `json:"requireEAB,omitempty"`
|
||||||
Claims *Claims `json:"claims,omitempty"`
|
Claims *Claims `json:"claims,omitempty"`
|
||||||
Options *Options `json:"options,omitempty"`
|
Options *Options `json:"options,omitempty"`
|
||||||
ctl *Controller
|
|
||||||
|
ctl *Controller
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetID returns the provisioner unique identifier.
|
// GetID returns the provisioner unique identifier.
|
||||||
|
@ -71,7 +74,7 @@ func (p *ACME) DefaultTLSCertDuration() time.Duration {
|
||||||
return p.ctl.Claimer.DefaultTLSCertDuration()
|
return p.ctl.Claimer.DefaultTLSCertDuration()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Init initializes and validates the fields of a JWK type.
|
// Init initializes and validates the fields of an ACME type.
|
||||||
func (p *ACME) Init(config Config) (err error) {
|
func (p *ACME) Init(config Config) (err error) {
|
||||||
switch {
|
switch {
|
||||||
case p.Type == "":
|
case p.Type == "":
|
||||||
|
@ -80,15 +83,56 @@ func (p *ACME) Init(config Config) (err error) {
|
||||||
return errors.New("provisioner name cannot be empty")
|
return errors.New("provisioner name cannot be empty")
|
||||||
}
|
}
|
||||||
|
|
||||||
p.ctl, err = NewController(p, p.Claims, config)
|
p.ctl, err = NewController(p, p.Claims, config, p.Options)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ACMEIdentifierType encodes ACME Identifier types
|
||||||
|
type ACMEIdentifierType string
|
||||||
|
|
||||||
|
const (
|
||||||
|
// IP is the ACME ip identifier type
|
||||||
|
IP ACMEIdentifierType = "ip"
|
||||||
|
// DNS is the ACME dns identifier type
|
||||||
|
DNS ACMEIdentifierType = "dns"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ACMEIdentifier encodes ACME Order Identifiers
|
||||||
|
type ACMEIdentifier struct {
|
||||||
|
Type ACMEIdentifierType
|
||||||
|
Value string
|
||||||
|
}
|
||||||
|
|
||||||
|
// AuthorizeOrderIdentifier verifies the provisioner is allowed to issue a
|
||||||
|
// certificate for an ACME Order Identifier.
|
||||||
|
func (p *ACME) AuthorizeOrderIdentifier(ctx context.Context, identifier ACMEIdentifier) error {
|
||||||
|
|
||||||
|
x509Policy := p.ctl.getPolicy().getX509()
|
||||||
|
|
||||||
|
// identifier is allowed if no policy is configured
|
||||||
|
if x509Policy == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// assuming only valid identifiers (IP or DNS) are provided
|
||||||
|
var err error
|
||||||
|
switch identifier.Type {
|
||||||
|
case IP:
|
||||||
|
err = x509Policy.IsIPAllowed(net.ParseIP(identifier.Value))
|
||||||
|
case DNS:
|
||||||
|
err = x509Policy.IsDNSAllowed(identifier.Value)
|
||||||
|
default:
|
||||||
|
err = fmt.Errorf("invalid ACME identifier type '%s' provided", identifier.Type)
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
// AuthorizeSign does not do any validation, because all validation is handled
|
// AuthorizeSign does not do any validation, because all validation is handled
|
||||||
// in the ACME protocol. This method returns a list of modifiers / constraints
|
// in the ACME protocol. This method returns a list of modifiers / constraints
|
||||||
// on the resulting certificate.
|
// on the resulting certificate.
|
||||||
func (p *ACME) AuthorizeSign(ctx context.Context, token string) ([]SignOption, error) {
|
func (p *ACME) AuthorizeSign(ctx context.Context, token string) ([]SignOption, error) {
|
||||||
return []SignOption{
|
opts := []SignOption{
|
||||||
p,
|
p,
|
||||||
// modifiers / withOptions
|
// modifiers / withOptions
|
||||||
newProvisionerExtensionOption(TypeACME, p.Name, ""),
|
newProvisionerExtensionOption(TypeACME, p.Name, ""),
|
||||||
|
@ -97,7 +141,10 @@ func (p *ACME) AuthorizeSign(ctx context.Context, token string) ([]SignOption, e
|
||||||
// validators
|
// validators
|
||||||
defaultPublicKeyValidator{},
|
defaultPublicKeyValidator{},
|
||||||
newValidityValidator(p.ctl.Claimer.MinTLSCertDuration(), p.ctl.Claimer.MaxTLSCertDuration()),
|
newValidityValidator(p.ctl.Claimer.MinTLSCertDuration(), p.ctl.Claimer.MaxTLSCertDuration()),
|
||||||
}, nil
|
newX509NamePolicyValidator(p.ctl.getPolicy().getX509()),
|
||||||
|
}
|
||||||
|
|
||||||
|
return opts, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// AuthorizeRevoke is called just before the certificate is to be revoked by
|
// AuthorizeRevoke is called just before the certificate is to be revoked by
|
||||||
|
|
|
@ -176,7 +176,7 @@ func TestACME_AuthorizeSign(t *testing.T) {
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if assert.Nil(t, tc.err) && assert.NotNil(t, opts) {
|
if assert.Nil(t, tc.err) && assert.NotNil(t, opts) {
|
||||||
assert.Len(t, 6, opts)
|
assert.Equals(t, 7, len(opts)) // number of SignOptions returned
|
||||||
for _, o := range opts {
|
for _, o := range opts {
|
||||||
switch v := o.(type) {
|
switch v := o.(type) {
|
||||||
case *ACME:
|
case *ACME:
|
||||||
|
@ -193,6 +193,8 @@ func TestACME_AuthorizeSign(t *testing.T) {
|
||||||
case *validityValidator:
|
case *validityValidator:
|
||||||
assert.Equals(t, v.min, tc.p.ctl.Claimer.MinTLSCertDuration())
|
assert.Equals(t, v.min, tc.p.ctl.Claimer.MinTLSCertDuration())
|
||||||
assert.Equals(t, v.max, tc.p.ctl.Claimer.MaxTLSCertDuration())
|
assert.Equals(t, v.max, tc.p.ctl.Claimer.MaxTLSCertDuration())
|
||||||
|
case *x509NamePolicyValidator:
|
||||||
|
assert.Equals(t, nil, v.policyEngine)
|
||||||
default:
|
default:
|
||||||
assert.FatalError(t, fmt.Errorf("unexpected sign option of type %T", v))
|
assert.FatalError(t, fmt.Errorf("unexpected sign option of type %T", v))
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,10 +17,12 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/smallstep/certificates/errs"
|
|
||||||
"go.step.sm/crypto/jose"
|
"go.step.sm/crypto/jose"
|
||||||
"go.step.sm/crypto/sshutil"
|
"go.step.sm/crypto/sshutil"
|
||||||
"go.step.sm/crypto/x509util"
|
"go.step.sm/crypto/x509util"
|
||||||
|
|
||||||
|
"github.com/smallstep/certificates/errs"
|
||||||
)
|
)
|
||||||
|
|
||||||
// awsIssuer is the string used as issuer in the generated tokens.
|
// awsIssuer is the string used as issuer in the generated tokens.
|
||||||
|
@ -421,7 +423,7 @@ func (p *AWS) Init(config Config) (err error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
config.Audiences = config.Audiences.WithFragment(p.GetIDForToken())
|
config.Audiences = config.Audiences.WithFragment(p.GetIDForToken())
|
||||||
p.ctl, err = NewController(p, p.Claims, config)
|
p.ctl, err = NewController(p, p.Claims, config, p.Options)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -476,6 +478,7 @@ func (p *AWS) AuthorizeSign(ctx context.Context, token string) ([]SignOption, er
|
||||||
defaultPublicKeyValidator{},
|
defaultPublicKeyValidator{},
|
||||||
commonNameValidator(payload.Claims.Subject),
|
commonNameValidator(payload.Claims.Subject),
|
||||||
newValidityValidator(p.ctl.Claimer.MinTLSCertDuration(), p.ctl.Claimer.MaxTLSCertDuration()),
|
newValidityValidator(p.ctl.Claimer.MinTLSCertDuration(), p.ctl.Claimer.MaxTLSCertDuration()),
|
||||||
|
newX509NamePolicyValidator(p.ctl.getPolicy().getX509()),
|
||||||
), nil
|
), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -543,7 +546,7 @@ func (p *AWS) readURL(url string) ([]byte, error) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return nil, fmt.Errorf("Request for metadata returned non-successful status code %d",
|
return nil, fmt.Errorf("request for metadata returned non-successful status code %d",
|
||||||
resp.StatusCode)
|
resp.StatusCode)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -576,7 +579,7 @@ func (p *AWS) readURLv2(url string) (*http.Response, error) {
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
if resp.StatusCode >= 400 {
|
if resp.StatusCode >= 400 {
|
||||||
return nil, fmt.Errorf("Request for API token returned non-successful status code %d", resp.StatusCode)
|
return nil, fmt.Errorf("request for API token returned non-successful status code %d", resp.StatusCode)
|
||||||
}
|
}
|
||||||
token, err := io.ReadAll(resp.Body)
|
token, err := io.ReadAll(resp.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -754,5 +757,7 @@ func (p *AWS) AuthorizeSSHSign(ctx context.Context, token string) ([]SignOption,
|
||||||
&sshCertValidityValidator{p.ctl.Claimer},
|
&sshCertValidityValidator{p.ctl.Claimer},
|
||||||
// Require all the fields in the SSH certificate
|
// Require all the fields in the SSH certificate
|
||||||
&sshCertDefaultValidator{},
|
&sshCertDefaultValidator{},
|
||||||
|
// Ensure that all principal names are allowed
|
||||||
|
newSSHNamePolicyValidator(p.ctl.getPolicy().getSSHHost(), nil),
|
||||||
), nil
|
), nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -642,11 +642,11 @@ func TestAWS_AuthorizeSign(t *testing.T) {
|
||||||
code int
|
code int
|
||||||
wantErr bool
|
wantErr bool
|
||||||
}{
|
}{
|
||||||
{"ok", p1, args{t1, "foo.local"}, 7, http.StatusOK, false},
|
{"ok", p1, args{t1, "foo.local"}, 8, http.StatusOK, false},
|
||||||
{"ok", p2, args{t2, "instance-id"}, 11, http.StatusOK, false},
|
{"ok", p2, args{t2, "instance-id"}, 12, http.StatusOK, false},
|
||||||
{"ok", p2, args{t2Hostname, "ip-127-0-0-1.us-west-1.compute.internal"}, 11, http.StatusOK, false},
|
{"ok", p2, args{t2Hostname, "ip-127-0-0-1.us-west-1.compute.internal"}, 12, http.StatusOK, false},
|
||||||
{"ok", p2, args{t2PrivateIP, "127.0.0.1"}, 11, http.StatusOK, false},
|
{"ok", p2, args{t2PrivateIP, "127.0.0.1"}, 12, http.StatusOK, false},
|
||||||
{"ok", p1, args{t4, "instance-id"}, 7, http.StatusOK, false},
|
{"ok", p1, args{t4, "instance-id"}, 8, http.StatusOK, false},
|
||||||
{"fail account", p3, args{token: t3}, 0, http.StatusUnauthorized, true},
|
{"fail account", p3, args{token: t3}, 0, http.StatusUnauthorized, true},
|
||||||
{"fail token", p1, args{token: "token"}, 0, http.StatusUnauthorized, true},
|
{"fail token", p1, args{token: "token"}, 0, http.StatusUnauthorized, true},
|
||||||
{"fail subject", p1, args{token: failSubject}, 0, http.StatusUnauthorized, true},
|
{"fail subject", p1, args{token: failSubject}, 0, http.StatusUnauthorized, true},
|
||||||
|
@ -673,7 +673,7 @@ func TestAWS_AuthorizeSign(t *testing.T) {
|
||||||
assert.Fatal(t, ok, "error does not implement StatusCodedError interface")
|
assert.Fatal(t, ok, "error does not implement StatusCodedError interface")
|
||||||
assert.Equals(t, sc.StatusCode(), tt.code)
|
assert.Equals(t, sc.StatusCode(), tt.code)
|
||||||
default:
|
default:
|
||||||
assert.Len(t, tt.wantLen, got)
|
assert.Equals(t, tt.wantLen, len(got))
|
||||||
for _, o := range got {
|
for _, o := range got {
|
||||||
switch v := o.(type) {
|
switch v := o.(type) {
|
||||||
case *AWS:
|
case *AWS:
|
||||||
|
@ -699,6 +699,8 @@ func TestAWS_AuthorizeSign(t *testing.T) {
|
||||||
assert.Equals(t, v, nil)
|
assert.Equals(t, v, nil)
|
||||||
case dnsNamesValidator:
|
case dnsNamesValidator:
|
||||||
assert.Equals(t, []string(v), []string{"ip-127-0-0-1.us-west-1.compute.internal"})
|
assert.Equals(t, []string(v), []string{"ip-127-0-0-1.us-west-1.compute.internal"})
|
||||||
|
case *x509NamePolicyValidator:
|
||||||
|
assert.Equals(t, nil, v.policyEngine)
|
||||||
default:
|
default:
|
||||||
assert.FatalError(t, fmt.Errorf("unexpected sign option of type %T", v))
|
assert.FatalError(t, fmt.Errorf("unexpected sign option of type %T", v))
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,10 +13,12 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/smallstep/certificates/errs"
|
|
||||||
"go.step.sm/crypto/jose"
|
"go.step.sm/crypto/jose"
|
||||||
"go.step.sm/crypto/sshutil"
|
"go.step.sm/crypto/sshutil"
|
||||||
"go.step.sm/crypto/x509util"
|
"go.step.sm/crypto/x509util"
|
||||||
|
|
||||||
|
"github.com/smallstep/certificates/errs"
|
||||||
)
|
)
|
||||||
|
|
||||||
// azureOIDCBaseURL is the base discovery url for Microsoft Azure tokens.
|
// azureOIDCBaseURL is the base discovery url for Microsoft Azure tokens.
|
||||||
|
@ -219,7 +221,7 @@ func (p *Azure) Init(config Config) (err error) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
p.ctl, err = NewController(p, p.Claims, config)
|
p.ctl, err = NewController(p, p.Claims, config, p.Options)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -360,6 +362,7 @@ func (p *Azure) AuthorizeSign(ctx context.Context, token string) ([]SignOption,
|
||||||
// validators
|
// validators
|
||||||
defaultPublicKeyValidator{},
|
defaultPublicKeyValidator{},
|
||||||
newValidityValidator(p.ctl.Claimer.MinTLSCertDuration(), p.ctl.Claimer.MaxTLSCertDuration()),
|
newValidityValidator(p.ctl.Claimer.MinTLSCertDuration(), p.ctl.Claimer.MaxTLSCertDuration()),
|
||||||
|
newX509NamePolicyValidator(p.ctl.getPolicy().getX509()),
|
||||||
), nil
|
), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -425,6 +428,8 @@ func (p *Azure) AuthorizeSSHSign(ctx context.Context, token string) ([]SignOptio
|
||||||
&sshCertValidityValidator{p.ctl.Claimer},
|
&sshCertValidityValidator{p.ctl.Claimer},
|
||||||
// Require all the fields in the SSH certificate
|
// Require all the fields in the SSH certificate
|
||||||
&sshCertDefaultValidator{},
|
&sshCertDefaultValidator{},
|
||||||
|
// Ensure that all principal names are allowed
|
||||||
|
newSSHNamePolicyValidator(p.ctl.getPolicy().getSSHHost(), nil),
|
||||||
), nil
|
), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -474,11 +474,11 @@ func TestAzure_AuthorizeSign(t *testing.T) {
|
||||||
code int
|
code int
|
||||||
wantErr bool
|
wantErr bool
|
||||||
}{
|
}{
|
||||||
{"ok", p1, args{t1}, 6, http.StatusOK, false},
|
{"ok", p1, args{t1}, 7, http.StatusOK, false},
|
||||||
{"ok", p2, args{t2}, 11, http.StatusOK, false},
|
{"ok", p2, args{t2}, 12, http.StatusOK, false},
|
||||||
{"ok", p1, args{t11}, 6, http.StatusOK, false},
|
{"ok", p1, args{t11}, 7, http.StatusOK, false},
|
||||||
{"ok", p5, args{t5}, 6, http.StatusOK, false},
|
{"ok", p5, args{t5}, 7, http.StatusOK, false},
|
||||||
{"ok", p7, args{t7}, 6, http.StatusOK, false},
|
{"ok", p7, args{t7}, 7, http.StatusOK, false},
|
||||||
{"fail tenant", p3, args{t3}, 0, http.StatusUnauthorized, true},
|
{"fail tenant", p3, args{t3}, 0, http.StatusUnauthorized, true},
|
||||||
{"fail resource group", p4, args{t4}, 0, http.StatusUnauthorized, true},
|
{"fail resource group", p4, args{t4}, 0, http.StatusUnauthorized, true},
|
||||||
{"fail subscription", p6, args{t6}, 0, http.StatusUnauthorized, true},
|
{"fail subscription", p6, args{t6}, 0, http.StatusUnauthorized, true},
|
||||||
|
@ -502,7 +502,7 @@ func TestAzure_AuthorizeSign(t *testing.T) {
|
||||||
assert.Fatal(t, ok, "error does not implement StatusCodedError interface")
|
assert.Fatal(t, ok, "error does not implement StatusCodedError interface")
|
||||||
assert.Equals(t, sc.StatusCode(), tt.code)
|
assert.Equals(t, sc.StatusCode(), tt.code)
|
||||||
default:
|
default:
|
||||||
assert.Len(t, tt.wantLen, got)
|
assert.Equals(t, tt.wantLen, len(got))
|
||||||
for _, o := range got {
|
for _, o := range got {
|
||||||
switch v := o.(type) {
|
switch v := o.(type) {
|
||||||
case *Azure:
|
case *Azure:
|
||||||
|
@ -528,6 +528,8 @@ func TestAzure_AuthorizeSign(t *testing.T) {
|
||||||
assert.Equals(t, v, nil)
|
assert.Equals(t, v, nil)
|
||||||
case dnsNamesValidator:
|
case dnsNamesValidator:
|
||||||
assert.Equals(t, []string(v), []string{"virtualMachine"})
|
assert.Equals(t, []string(v), []string{"virtualMachine"})
|
||||||
|
case *x509NamePolicyValidator:
|
||||||
|
assert.Equals(t, nil, v.policyEngine)
|
||||||
default:
|
default:
|
||||||
assert.FatalError(t, fmt.Errorf("unexpected sign option of type %T", v))
|
assert.FatalError(t, fmt.Errorf("unexpected sign option of type %T", v))
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,14 +21,19 @@ type Controller struct {
|
||||||
IdentityFunc GetIdentityFunc
|
IdentityFunc GetIdentityFunc
|
||||||
AuthorizeRenewFunc AuthorizeRenewFunc
|
AuthorizeRenewFunc AuthorizeRenewFunc
|
||||||
AuthorizeSSHRenewFunc AuthorizeSSHRenewFunc
|
AuthorizeSSHRenewFunc AuthorizeSSHRenewFunc
|
||||||
|
policy *policyEngine
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewController initializes a new provisioner controller.
|
// NewController initializes a new provisioner controller.
|
||||||
func NewController(p Interface, claims *Claims, config Config) (*Controller, error) {
|
func NewController(p Interface, claims *Claims, config Config, options *Options) (*Controller, error) {
|
||||||
claimer, err := NewClaimer(claims, config.Claims)
|
claimer, err := NewClaimer(claims, config.Claims)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
policy, err := newPolicyEngine(options)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
return &Controller{
|
return &Controller{
|
||||||
Interface: p,
|
Interface: p,
|
||||||
Audiences: &config.Audiences,
|
Audiences: &config.Audiences,
|
||||||
|
@ -36,6 +41,7 @@ func NewController(p Interface, claims *Claims, config Config) (*Controller, err
|
||||||
IdentityFunc: config.GetIdentityFunc,
|
IdentityFunc: config.GetIdentityFunc,
|
||||||
AuthorizeRenewFunc: config.AuthorizeRenewFunc,
|
AuthorizeRenewFunc: config.AuthorizeRenewFunc,
|
||||||
AuthorizeSSHRenewFunc: config.AuthorizeSSHRenewFunc,
|
AuthorizeSSHRenewFunc: config.AuthorizeSSHRenewFunc,
|
||||||
|
policy: policy,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -192,3 +198,10 @@ func SanitizeSSHUserPrincipal(email string) string {
|
||||||
}
|
}
|
||||||
}, strings.ToLower(email))
|
}, strings.ToLower(email))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *Controller) getPolicy() *policyEngine {
|
||||||
|
if c == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return c.policy
|
||||||
|
}
|
||||||
|
|
|
@ -9,6 +9,8 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"golang.org/x/crypto/ssh"
|
"golang.org/x/crypto/ssh"
|
||||||
|
|
||||||
|
"github.com/smallstep/certificates/authority/policy"
|
||||||
)
|
)
|
||||||
|
|
||||||
var trueValue = true
|
var trueValue = true
|
||||||
|
@ -30,11 +32,40 @@ func mustDuration(t *testing.T, s string) *Duration {
|
||||||
return d
|
return d
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func mustNewPolicyEngine(t *testing.T, options *Options) *policyEngine {
|
||||||
|
t.Helper()
|
||||||
|
c, err := newPolicyEngine(options)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
func TestNewController(t *testing.T) {
|
func TestNewController(t *testing.T) {
|
||||||
|
options := &Options{
|
||||||
|
X509: &X509Options{
|
||||||
|
AllowedNames: &policy.X509NameOptions{
|
||||||
|
DNSDomains: []string{"*.local"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
SSH: &SSHOptions{
|
||||||
|
Host: &policy.SSHHostCertificateOptions{
|
||||||
|
AllowedNames: &policy.SSHNameOptions{
|
||||||
|
DNSDomains: []string{"*.local"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
User: &policy.SSHUserCertificateOptions{
|
||||||
|
AllowedNames: &policy.SSHNameOptions{
|
||||||
|
EmailAddresses: []string{"@example.com"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
type args struct {
|
type args struct {
|
||||||
p Interface
|
p Interface
|
||||||
claims *Claims
|
claims *Claims
|
||||||
config Config
|
config Config
|
||||||
|
options *Options
|
||||||
}
|
}
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
|
@ -45,7 +76,7 @@ func TestNewController(t *testing.T) {
|
||||||
{"ok", args{&JWK{}, nil, Config{
|
{"ok", args{&JWK{}, nil, Config{
|
||||||
Claims: globalProvisionerClaims,
|
Claims: globalProvisionerClaims,
|
||||||
Audiences: testAudiences,
|
Audiences: testAudiences,
|
||||||
}}, &Controller{
|
}, nil}, &Controller{
|
||||||
Interface: &JWK{},
|
Interface: &JWK{},
|
||||||
Audiences: &testAudiences,
|
Audiences: &testAudiences,
|
||||||
Claimer: mustClaimer(t, nil, globalProvisionerClaims),
|
Claimer: mustClaimer(t, nil, globalProvisionerClaims),
|
||||||
|
@ -55,24 +86,49 @@ func TestNewController(t *testing.T) {
|
||||||
}, Config{
|
}, Config{
|
||||||
Claims: globalProvisionerClaims,
|
Claims: globalProvisionerClaims,
|
||||||
Audiences: testAudiences,
|
Audiences: testAudiences,
|
||||||
}}, &Controller{
|
}, nil}, &Controller{
|
||||||
Interface: &JWK{},
|
Interface: &JWK{},
|
||||||
Audiences: &testAudiences,
|
Audiences: &testAudiences,
|
||||||
Claimer: mustClaimer(t, &Claims{
|
Claimer: mustClaimer(t, &Claims{
|
||||||
DisableRenewal: &defaultDisableRenewal,
|
DisableRenewal: &defaultDisableRenewal,
|
||||||
}, globalProvisionerClaims),
|
}, globalProvisionerClaims),
|
||||||
}, false},
|
}, false},
|
||||||
|
{"ok with claims and options", args{&JWK{}, &Claims{
|
||||||
|
DisableRenewal: &defaultDisableRenewal,
|
||||||
|
}, Config{
|
||||||
|
Claims: globalProvisionerClaims,
|
||||||
|
Audiences: testAudiences,
|
||||||
|
}, options}, &Controller{
|
||||||
|
Interface: &JWK{},
|
||||||
|
Audiences: &testAudiences,
|
||||||
|
Claimer: mustClaimer(t, &Claims{
|
||||||
|
DisableRenewal: &defaultDisableRenewal,
|
||||||
|
}, globalProvisionerClaims),
|
||||||
|
policy: mustNewPolicyEngine(t, options),
|
||||||
|
}, false},
|
||||||
{"fail claimer", args{&JWK{}, &Claims{
|
{"fail claimer", args{&JWK{}, &Claims{
|
||||||
MinTLSDur: mustDuration(t, "24h"),
|
MinTLSDur: mustDuration(t, "24h"),
|
||||||
MaxTLSDur: mustDuration(t, "2h"),
|
MaxTLSDur: mustDuration(t, "2h"),
|
||||||
}, Config{
|
}, Config{
|
||||||
Claims: globalProvisionerClaims,
|
Claims: globalProvisionerClaims,
|
||||||
Audiences: testAudiences,
|
Audiences: testAudiences,
|
||||||
|
}, nil}, nil, true},
|
||||||
|
{"fail options", args{&JWK{}, &Claims{
|
||||||
|
DisableRenewal: &defaultDisableRenewal,
|
||||||
|
}, Config{
|
||||||
|
Claims: globalProvisionerClaims,
|
||||||
|
Audiences: testAudiences,
|
||||||
|
}, &Options{
|
||||||
|
X509: &X509Options{
|
||||||
|
AllowedNames: &policy.X509NameOptions{
|
||||||
|
DNSDomains: []string{"**.local"},
|
||||||
|
},
|
||||||
|
},
|
||||||
}}, nil, true},
|
}}, nil, true},
|
||||||
}
|
}
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
got, err := NewController(tt.args.p, tt.args.claims, tt.args.config)
|
got, err := NewController(tt.args.p, tt.args.claims, tt.args.config, tt.args.options)
|
||||||
if (err != nil) != tt.wantErr {
|
if (err != nil) != tt.wantErr {
|
||||||
t.Errorf("NewController() error = %v, wantErr %v", err, tt.wantErr)
|
t.Errorf("NewController() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
return
|
return
|
||||||
|
|
|
@ -14,10 +14,12 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/smallstep/certificates/errs"
|
|
||||||
"go.step.sm/crypto/jose"
|
"go.step.sm/crypto/jose"
|
||||||
"go.step.sm/crypto/sshutil"
|
"go.step.sm/crypto/sshutil"
|
||||||
"go.step.sm/crypto/x509util"
|
"go.step.sm/crypto/x509util"
|
||||||
|
|
||||||
|
"github.com/smallstep/certificates/errs"
|
||||||
)
|
)
|
||||||
|
|
||||||
// gcpCertsURL is the url that serves Google OAuth2 public keys.
|
// gcpCertsURL is the url that serves Google OAuth2 public keys.
|
||||||
|
@ -212,7 +214,7 @@ func (p *GCP) Init(config Config) (err error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
config.Audiences = config.Audiences.WithFragment(p.GetIDForToken())
|
config.Audiences = config.Audiences.WithFragment(p.GetIDForToken())
|
||||||
p.ctl, err = NewController(p, p.Claims, config)
|
p.ctl, err = NewController(p, p.Claims, config, p.Options)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -270,6 +272,7 @@ func (p *GCP) AuthorizeSign(ctx context.Context, token string) ([]SignOption, er
|
||||||
// validators
|
// validators
|
||||||
defaultPublicKeyValidator{},
|
defaultPublicKeyValidator{},
|
||||||
newValidityValidator(p.ctl.Claimer.MinTLSCertDuration(), p.ctl.Claimer.MaxTLSCertDuration()),
|
newValidityValidator(p.ctl.Claimer.MinTLSCertDuration(), p.ctl.Claimer.MaxTLSCertDuration()),
|
||||||
|
newX509NamePolicyValidator(p.ctl.getPolicy().getX509()),
|
||||||
), nil
|
), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -432,5 +435,7 @@ func (p *GCP) AuthorizeSSHSign(ctx context.Context, token string) ([]SignOption,
|
||||||
&sshCertValidityValidator{p.ctl.Claimer},
|
&sshCertValidityValidator{p.ctl.Claimer},
|
||||||
// Require all the fields in the SSH certificate
|
// Require all the fields in the SSH certificate
|
||||||
&sshCertDefaultValidator{},
|
&sshCertDefaultValidator{},
|
||||||
|
// Ensure that all principal names are allowed
|
||||||
|
newSSHNamePolicyValidator(p.ctl.getPolicy().getSSHHost(), nil),
|
||||||
), nil
|
), nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -516,9 +516,9 @@ func TestGCP_AuthorizeSign(t *testing.T) {
|
||||||
code int
|
code int
|
||||||
wantErr bool
|
wantErr bool
|
||||||
}{
|
}{
|
||||||
{"ok", p1, args{t1}, 6, http.StatusOK, false},
|
{"ok", p1, args{t1}, 7, http.StatusOK, false},
|
||||||
{"ok", p2, args{t2}, 11, http.StatusOK, false},
|
{"ok", p2, args{t2}, 12, http.StatusOK, false},
|
||||||
{"ok", p3, args{t3}, 6, http.StatusOK, false},
|
{"ok", p3, args{t3}, 7, http.StatusOK, false},
|
||||||
{"fail token", p1, args{"token"}, 0, http.StatusUnauthorized, true},
|
{"fail token", p1, args{"token"}, 0, http.StatusUnauthorized, true},
|
||||||
{"fail key", p1, args{failKey}, 0, http.StatusUnauthorized, true},
|
{"fail key", p1, args{failKey}, 0, http.StatusUnauthorized, true},
|
||||||
{"fail iss", p1, args{failIss}, 0, http.StatusUnauthorized, true},
|
{"fail iss", p1, args{failIss}, 0, http.StatusUnauthorized, true},
|
||||||
|
@ -545,7 +545,7 @@ func TestGCP_AuthorizeSign(t *testing.T) {
|
||||||
assert.Fatal(t, ok, "error does not implement StatusCodedError interface")
|
assert.Fatal(t, ok, "error does not implement StatusCodedError interface")
|
||||||
assert.Equals(t, sc.StatusCode(), tt.code)
|
assert.Equals(t, sc.StatusCode(), tt.code)
|
||||||
default:
|
default:
|
||||||
assert.Len(t, tt.wantLen, got)
|
assert.Equals(t, tt.wantLen, len(got))
|
||||||
for _, o := range got {
|
for _, o := range got {
|
||||||
switch v := o.(type) {
|
switch v := o.(type) {
|
||||||
case *GCP:
|
case *GCP:
|
||||||
|
@ -571,6 +571,8 @@ func TestGCP_AuthorizeSign(t *testing.T) {
|
||||||
assert.Equals(t, v, nil)
|
assert.Equals(t, v, nil)
|
||||||
case dnsNamesValidator:
|
case dnsNamesValidator:
|
||||||
assert.Equals(t, []string(v), []string{"instance-name.c.project-id.internal", "instance-name.zone.c.project-id.internal"})
|
assert.Equals(t, []string(v), []string{"instance-name.c.project-id.internal", "instance-name.zone.c.project-id.internal"})
|
||||||
|
case *x509NamePolicyValidator:
|
||||||
|
assert.Equals(t, nil, v.policyEngine)
|
||||||
default:
|
default:
|
||||||
assert.FatalError(t, fmt.Errorf("unexpected sign option of type %T", v))
|
assert.FatalError(t, fmt.Errorf("unexpected sign option of type %T", v))
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,10 +7,12 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/smallstep/certificates/errs"
|
|
||||||
"go.step.sm/crypto/jose"
|
"go.step.sm/crypto/jose"
|
||||||
"go.step.sm/crypto/sshutil"
|
"go.step.sm/crypto/sshutil"
|
||||||
"go.step.sm/crypto/x509util"
|
"go.step.sm/crypto/x509util"
|
||||||
|
|
||||||
|
"github.com/smallstep/certificates/errs"
|
||||||
)
|
)
|
||||||
|
|
||||||
// jwtPayload extends jwt.Claims with step attributes.
|
// jwtPayload extends jwt.Claims with step attributes.
|
||||||
|
@ -97,7 +99,7 @@ func (p *JWK) Init(config Config) (err error) {
|
||||||
return errors.New("provisioner key cannot be empty")
|
return errors.New("provisioner key cannot be empty")
|
||||||
}
|
}
|
||||||
|
|
||||||
p.ctl, err = NewController(p, p.Claims, config)
|
p.ctl, err = NewController(p, p.Claims, config, p.Options)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -141,6 +143,7 @@ func (p *JWK) authorizeToken(token string, audiences []string) (*jwtPayload, err
|
||||||
// revoke the certificate with serial number in the `sub` property.
|
// revoke the certificate with serial number in the `sub` property.
|
||||||
func (p *JWK) AuthorizeRevoke(ctx context.Context, token string) error {
|
func (p *JWK) AuthorizeRevoke(ctx context.Context, token string) error {
|
||||||
_, err := p.authorizeToken(token, p.ctl.Audiences.Revoke)
|
_, err := p.authorizeToken(token, p.ctl.Audiences.Revoke)
|
||||||
|
// TODO(hs): authorize the SANs using x509 name policy allow/deny rules (also for other provisioners with AuthorizeRevoke)
|
||||||
return errs.Wrap(http.StatusInternalServerError, err, "jwk.AuthorizeRevoke")
|
return errs.Wrap(http.StatusInternalServerError, err, "jwk.AuthorizeRevoke")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -180,6 +183,7 @@ func (p *JWK) AuthorizeSign(ctx context.Context, token string) ([]SignOption, er
|
||||||
defaultPublicKeyValidator{},
|
defaultPublicKeyValidator{},
|
||||||
defaultSANsValidator(claims.SANs),
|
defaultSANsValidator(claims.SANs),
|
||||||
newValidityValidator(p.ctl.Claimer.MinTLSCertDuration(), p.ctl.Claimer.MaxTLSCertDuration()),
|
newValidityValidator(p.ctl.Claimer.MinTLSCertDuration(), p.ctl.Claimer.MaxTLSCertDuration()),
|
||||||
|
newX509NamePolicyValidator(p.ctl.getPolicy().getX509()),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -188,6 +192,7 @@ func (p *JWK) AuthorizeSign(ctx context.Context, token string) ([]SignOption, er
|
||||||
// revocation status. Just confirms that the provisioner that created the
|
// revocation status. Just confirms that the provisioner that created the
|
||||||
// certificate was configured to allow renewals.
|
// certificate was configured to allow renewals.
|
||||||
func (p *JWK) AuthorizeRenew(ctx context.Context, cert *x509.Certificate) error {
|
func (p *JWK) AuthorizeRenew(ctx context.Context, cert *x509.Certificate) error {
|
||||||
|
// TODO(hs): authorize the SANs using x509 name policy allow/deny rules (also for other provisioners with AuthorizeRewew and AuthorizeSSHRenew)
|
||||||
return p.ctl.AuthorizeRenew(ctx, cert)
|
return p.ctl.AuthorizeRenew(ctx, cert)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -260,11 +265,14 @@ func (p *JWK) AuthorizeSSHSign(ctx context.Context, token string) ([]SignOption,
|
||||||
&sshCertValidityValidator{p.ctl.Claimer},
|
&sshCertValidityValidator{p.ctl.Claimer},
|
||||||
// Require and validate all the default fields in the SSH certificate.
|
// Require and validate all the default fields in the SSH certificate.
|
||||||
&sshCertDefaultValidator{},
|
&sshCertDefaultValidator{},
|
||||||
|
// Ensure that all principal names are allowed
|
||||||
|
newSSHNamePolicyValidator(p.ctl.getPolicy().getSSHHost(), p.ctl.getPolicy().getSSHUser()),
|
||||||
), nil
|
), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// AuthorizeSSHRevoke returns nil if the token is valid, false otherwise.
|
// AuthorizeSSHRevoke returns nil if the token is valid, false otherwise.
|
||||||
func (p *JWK) AuthorizeSSHRevoke(ctx context.Context, token string) error {
|
func (p *JWK) AuthorizeSSHRevoke(ctx context.Context, token string) error {
|
||||||
_, err := p.authorizeToken(token, p.ctl.Audiences.SSHRevoke)
|
_, err := p.authorizeToken(token, p.ctl.Audiences.SSHRevoke)
|
||||||
|
// TODO(hs): authorize the principals using SSH name policy allow/deny rules (also for other provisioners with AuthorizeSSHRevoke)
|
||||||
return errs.Wrap(http.StatusInternalServerError, err, "jwk.AuthorizeSSHRevoke")
|
return errs.Wrap(http.StatusInternalServerError, err, "jwk.AuthorizeSSHRevoke")
|
||||||
}
|
}
|
||||||
|
|
|
@ -297,7 +297,7 @@ func TestJWK_AuthorizeSign(t *testing.T) {
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if assert.NotNil(t, got) {
|
if assert.NotNil(t, got) {
|
||||||
assert.Len(t, 8, got)
|
assert.Equals(t, 9, len(got))
|
||||||
for _, o := range got {
|
for _, o := range got {
|
||||||
switch v := o.(type) {
|
switch v := o.(type) {
|
||||||
case *JWK:
|
case *JWK:
|
||||||
|
@ -317,6 +317,8 @@ func TestJWK_AuthorizeSign(t *testing.T) {
|
||||||
assert.Equals(t, v.max, tt.prov.ctl.Claimer.MaxTLSCertDuration())
|
assert.Equals(t, v.max, tt.prov.ctl.Claimer.MaxTLSCertDuration())
|
||||||
case defaultSANsValidator:
|
case defaultSANsValidator:
|
||||||
assert.Equals(t, []string(v), tt.sans)
|
assert.Equals(t, []string(v), tt.sans)
|
||||||
|
case *x509NamePolicyValidator:
|
||||||
|
assert.Equals(t, nil, v.policyEngine)
|
||||||
default:
|
default:
|
||||||
assert.FatalError(t, fmt.Errorf("unexpected sign option of type %T", v))
|
assert.FatalError(t, fmt.Errorf("unexpected sign option of type %T", v))
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,11 +10,13 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/smallstep/certificates/errs"
|
|
||||||
"go.step.sm/crypto/jose"
|
"go.step.sm/crypto/jose"
|
||||||
"go.step.sm/crypto/pemutil"
|
"go.step.sm/crypto/pemutil"
|
||||||
"go.step.sm/crypto/sshutil"
|
"go.step.sm/crypto/sshutil"
|
||||||
"go.step.sm/crypto/x509util"
|
"go.step.sm/crypto/x509util"
|
||||||
|
|
||||||
|
"github.com/smallstep/certificates/errs"
|
||||||
)
|
)
|
||||||
|
|
||||||
// NOTE: There can be at most one kubernetes service account provisioner configured
|
// NOTE: There can be at most one kubernetes service account provisioner configured
|
||||||
|
@ -91,6 +93,7 @@ func (p *K8sSA) GetEncryptedKey() (string, string, bool) {
|
||||||
|
|
||||||
// Init initializes and validates the fields of a K8sSA type.
|
// Init initializes and validates the fields of a K8sSA type.
|
||||||
func (p *K8sSA) Init(config Config) (err error) {
|
func (p *K8sSA) Init(config Config) (err error) {
|
||||||
|
|
||||||
switch {
|
switch {
|
||||||
case p.Type == "":
|
case p.Type == "":
|
||||||
return errors.New("provisioner type cannot be empty")
|
return errors.New("provisioner type cannot be empty")
|
||||||
|
@ -137,7 +140,7 @@ func (p *K8sSA) Init(config Config) (err error) {
|
||||||
p.kauthn = k8s.AuthenticationV1()
|
p.kauthn = k8s.AuthenticationV1()
|
||||||
*/
|
*/
|
||||||
|
|
||||||
p.ctl, err = NewController(p, p.Claims, config)
|
p.ctl, err = NewController(p, p.Claims, config, p.Options)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -239,6 +242,7 @@ func (p *K8sSA) AuthorizeSign(ctx context.Context, token string) ([]SignOption,
|
||||||
// validators
|
// validators
|
||||||
defaultPublicKeyValidator{},
|
defaultPublicKeyValidator{},
|
||||||
newValidityValidator(p.ctl.Claimer.MinTLSCertDuration(), p.ctl.Claimer.MaxTLSCertDuration()),
|
newValidityValidator(p.ctl.Claimer.MinTLSCertDuration(), p.ctl.Claimer.MaxTLSCertDuration()),
|
||||||
|
newX509NamePolicyValidator(p.ctl.getPolicy().getX509()),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -281,6 +285,8 @@ func (p *K8sSA) AuthorizeSSHSign(ctx context.Context, token string) ([]SignOptio
|
||||||
&sshCertValidityValidator{p.ctl.Claimer},
|
&sshCertValidityValidator{p.ctl.Claimer},
|
||||||
// Require and validate all the default fields in the SSH certificate.
|
// Require and validate all the default fields in the SSH certificate.
|
||||||
&sshCertDefaultValidator{},
|
&sshCertDefaultValidator{},
|
||||||
|
// Ensure that all principal names are allowed
|
||||||
|
newSSHNamePolicyValidator(p.ctl.getPolicy().getSSHHost(), p.ctl.getPolicy().getSSHUser()),
|
||||||
), nil
|
), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -280,7 +280,6 @@ func TestK8sSA_AuthorizeSign(t *testing.T) {
|
||||||
} else {
|
} else {
|
||||||
if assert.Nil(t, tc.err) {
|
if assert.Nil(t, tc.err) {
|
||||||
if assert.NotNil(t, opts) {
|
if assert.NotNil(t, opts) {
|
||||||
tot := 0
|
|
||||||
for _, o := range opts {
|
for _, o := range opts {
|
||||||
switch v := o.(type) {
|
switch v := o.(type) {
|
||||||
case *K8sSA:
|
case *K8sSA:
|
||||||
|
@ -296,12 +295,13 @@ func TestK8sSA_AuthorizeSign(t *testing.T) {
|
||||||
case *validityValidator:
|
case *validityValidator:
|
||||||
assert.Equals(t, v.min, tc.p.ctl.Claimer.MinTLSCertDuration())
|
assert.Equals(t, v.min, tc.p.ctl.Claimer.MinTLSCertDuration())
|
||||||
assert.Equals(t, v.max, tc.p.ctl.Claimer.MaxTLSCertDuration())
|
assert.Equals(t, v.max, tc.p.ctl.Claimer.MaxTLSCertDuration())
|
||||||
|
case *x509NamePolicyValidator:
|
||||||
|
assert.Equals(t, nil, v.policyEngine)
|
||||||
default:
|
default:
|
||||||
assert.FatalError(t, fmt.Errorf("unexpected sign option of type %T", v))
|
assert.FatalError(t, fmt.Errorf("unexpected sign option of type %T", v))
|
||||||
}
|
}
|
||||||
tot++
|
|
||||||
}
|
}
|
||||||
assert.Equals(t, tot, 6)
|
assert.Equals(t, 7, len(opts))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -368,7 +368,7 @@ func TestK8sSA_AuthorizeSSHSign(t *testing.T) {
|
||||||
} else {
|
} else {
|
||||||
if assert.Nil(t, tc.err) {
|
if assert.Nil(t, tc.err) {
|
||||||
if assert.NotNil(t, opts) {
|
if assert.NotNil(t, opts) {
|
||||||
tot := 0
|
assert.Len(t, 7, opts)
|
||||||
for _, o := range opts {
|
for _, o := range opts {
|
||||||
switch v := o.(type) {
|
switch v := o.(type) {
|
||||||
case sshCertificateOptionsFunc:
|
case sshCertificateOptionsFunc:
|
||||||
|
@ -380,12 +380,13 @@ func TestK8sSA_AuthorizeSSHSign(t *testing.T) {
|
||||||
case *sshCertDefaultValidator:
|
case *sshCertDefaultValidator:
|
||||||
case *sshDefaultDuration:
|
case *sshDefaultDuration:
|
||||||
assert.Equals(t, v.Claimer, tc.p.ctl.Claimer)
|
assert.Equals(t, v.Claimer, tc.p.ctl.Claimer)
|
||||||
|
case *sshNamePolicyValidator:
|
||||||
|
assert.Equals(t, nil, v.userPolicyEngine)
|
||||||
|
assert.Equals(t, nil, v.hostPolicyEngine)
|
||||||
default:
|
default:
|
||||||
assert.FatalError(t, fmt.Errorf("unexpected sign option of type %T", v))
|
assert.FatalError(t, fmt.Errorf("unexpected sign option of type %T", v))
|
||||||
}
|
}
|
||||||
tot++
|
|
||||||
}
|
}
|
||||||
assert.Equals(t, tot, 6)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,12 +10,14 @@ import (
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
nebula "github.com/slackhq/nebula/cert"
|
nebula "github.com/slackhq/nebula/cert"
|
||||||
"github.com/smallstep/certificates/errs"
|
|
||||||
"go.step.sm/crypto/jose"
|
"go.step.sm/crypto/jose"
|
||||||
"go.step.sm/crypto/sshutil"
|
"go.step.sm/crypto/sshutil"
|
||||||
"go.step.sm/crypto/x25519"
|
"go.step.sm/crypto/x25519"
|
||||||
"go.step.sm/crypto/x509util"
|
"go.step.sm/crypto/x509util"
|
||||||
"golang.org/x/crypto/ssh"
|
"golang.org/x/crypto/ssh"
|
||||||
|
|
||||||
|
"github.com/smallstep/certificates/errs"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@ -61,7 +63,7 @@ func (p *Nebula) Init(config Config) (err error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
config.Audiences = config.Audiences.WithFragment(p.GetIDForToken())
|
config.Audiences = config.Audiences.WithFragment(p.GetIDForToken())
|
||||||
p.ctl, err = NewController(p, p.Claims, config)
|
p.ctl, err = NewController(p, p.Claims, config, p.Options)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -161,6 +163,7 @@ func (p *Nebula) AuthorizeSign(ctx context.Context, token string) ([]SignOption,
|
||||||
},
|
},
|
||||||
defaultPublicKeyValidator{},
|
defaultPublicKeyValidator{},
|
||||||
newValidityValidator(p.ctl.Claimer.MinTLSCertDuration(), p.ctl.Claimer.MaxTLSCertDuration()),
|
newValidityValidator(p.ctl.Claimer.MinTLSCertDuration(), p.ctl.Claimer.MaxTLSCertDuration()),
|
||||||
|
newX509NamePolicyValidator(p.ctl.getPolicy().getX509()),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -256,6 +259,8 @@ func (p *Nebula) AuthorizeSSHSign(ctx context.Context, token string) ([]SignOpti
|
||||||
&sshCertValidityValidator{p.ctl.Claimer},
|
&sshCertValidityValidator{p.ctl.Claimer},
|
||||||
// Require all the fields in the SSH certificate
|
// Require all the fields in the SSH certificate
|
||||||
&sshCertDefaultValidator{},
|
&sshCertDefaultValidator{},
|
||||||
|
// Ensure that all principal names are allowed
|
||||||
|
newSSHNamePolicyValidator(p.ctl.getPolicy().getSSHHost(), nil),
|
||||||
), nil
|
), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -12,10 +12,12 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/smallstep/certificates/errs"
|
|
||||||
"go.step.sm/crypto/jose"
|
"go.step.sm/crypto/jose"
|
||||||
"go.step.sm/crypto/sshutil"
|
"go.step.sm/crypto/sshutil"
|
||||||
"go.step.sm/crypto/x509util"
|
"go.step.sm/crypto/x509util"
|
||||||
|
|
||||||
|
"github.com/smallstep/certificates/errs"
|
||||||
)
|
)
|
||||||
|
|
||||||
// openIDConfiguration contains the necessary properties in the
|
// openIDConfiguration contains the necessary properties in the
|
||||||
|
@ -195,7 +197,7 @@ func (o *OIDC) Init(config Config) (err error) {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
o.ctl, err = NewController(o, o.Claims, config)
|
o.ctl, err = NewController(o, o.Claims, config, o.Options)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -353,6 +355,7 @@ func (o *OIDC) AuthorizeSign(ctx context.Context, token string) ([]SignOption, e
|
||||||
// validators
|
// validators
|
||||||
defaultPublicKeyValidator{},
|
defaultPublicKeyValidator{},
|
||||||
newValidityValidator(o.ctl.Claimer.MinTLSCertDuration(), o.ctl.Claimer.MaxTLSCertDuration()),
|
newValidityValidator(o.ctl.Claimer.MinTLSCertDuration(), o.ctl.Claimer.MaxTLSCertDuration()),
|
||||||
|
newX509NamePolicyValidator(o.ctl.getPolicy().getX509()),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -439,6 +442,8 @@ func (o *OIDC) AuthorizeSSHSign(ctx context.Context, token string) ([]SignOption
|
||||||
&sshCertValidityValidator{o.ctl.Claimer},
|
&sshCertValidityValidator{o.ctl.Claimer},
|
||||||
// Require all the fields in the SSH certificate
|
// Require all the fields in the SSH certificate
|
||||||
&sshCertDefaultValidator{},
|
&sshCertDefaultValidator{},
|
||||||
|
// Ensure that all principal names are allowed
|
||||||
|
newSSHNamePolicyValidator(o.ctl.getPolicy().getSSHHost(), o.ctl.getPolicy().getSSHUser()),
|
||||||
), nil
|
), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -323,7 +323,7 @@ func TestOIDC_AuthorizeSign(t *testing.T) {
|
||||||
assert.Equals(t, sc.StatusCode(), tt.code)
|
assert.Equals(t, sc.StatusCode(), tt.code)
|
||||||
assert.Nil(t, got)
|
assert.Nil(t, got)
|
||||||
} else if assert.NotNil(t, got) {
|
} else if assert.NotNil(t, got) {
|
||||||
assert.Len(t, 6, got)
|
assert.Equals(t, 7, len(got))
|
||||||
for _, o := range got {
|
for _, o := range got {
|
||||||
switch v := o.(type) {
|
switch v := o.(type) {
|
||||||
case *OIDC:
|
case *OIDC:
|
||||||
|
@ -341,6 +341,8 @@ func TestOIDC_AuthorizeSign(t *testing.T) {
|
||||||
assert.Equals(t, v.max, tt.prov.ctl.Claimer.MaxTLSCertDuration())
|
assert.Equals(t, v.max, tt.prov.ctl.Claimer.MaxTLSCertDuration())
|
||||||
case emailOnlyIdentity:
|
case emailOnlyIdentity:
|
||||||
assert.Equals(t, string(v), "name@smallstep.com")
|
assert.Equals(t, string(v), "name@smallstep.com")
|
||||||
|
case *x509NamePolicyValidator:
|
||||||
|
assert.Equals(t, nil, v.policyEngine)
|
||||||
default:
|
default:
|
||||||
assert.FatalError(t, fmt.Errorf("unexpected sign option of type %T", v))
|
assert.FatalError(t, fmt.Errorf("unexpected sign option of type %T", v))
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,8 +5,11 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
|
|
||||||
"go.step.sm/crypto/jose"
|
"go.step.sm/crypto/jose"
|
||||||
"go.step.sm/crypto/x509util"
|
"go.step.sm/crypto/x509util"
|
||||||
|
|
||||||
|
"github.com/smallstep/certificates/authority/policy"
|
||||||
)
|
)
|
||||||
|
|
||||||
// CertificateOptions is an interface that returns a list of options passed when
|
// CertificateOptions is an interface that returns a list of options passed when
|
||||||
|
@ -56,6 +59,16 @@ type X509Options struct {
|
||||||
// TemplateData is a JSON object with variables that can be used in custom
|
// TemplateData is a JSON object with variables that can be used in custom
|
||||||
// templates.
|
// templates.
|
||||||
TemplateData json.RawMessage `json:"templateData,omitempty"`
|
TemplateData json.RawMessage `json:"templateData,omitempty"`
|
||||||
|
|
||||||
|
// AllowedNames contains the SANs the provisioner is authorized to sign
|
||||||
|
AllowedNames *policy.X509NameOptions `json:"-"`
|
||||||
|
|
||||||
|
// DeniedNames contains the SANs the provisioner is not authorized to sign
|
||||||
|
DeniedNames *policy.X509NameOptions `json:"-"`
|
||||||
|
|
||||||
|
// AllowWildcardNames indicates if literal wildcard names
|
||||||
|
// like *.example.com are allowed. Defaults to false.
|
||||||
|
AllowWildcardNames bool `json:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// HasTemplate returns true if a template is defined in the provisioner options.
|
// HasTemplate returns true if a template is defined in the provisioner options.
|
||||||
|
@ -63,6 +76,31 @@ func (o *X509Options) HasTemplate() bool {
|
||||||
return o != nil && (o.Template != "" || o.TemplateFile != "")
|
return o != nil && (o.Template != "" || o.TemplateFile != "")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetAllowedNameOptions returns the AllowedNames, which models the
|
||||||
|
// SANs that a provisioner is authorized to sign x509 certificates for.
|
||||||
|
func (o *X509Options) GetAllowedNameOptions() *policy.X509NameOptions {
|
||||||
|
if o == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return o.AllowedNames
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetDeniedNameOptions returns the DeniedNames, which models the
|
||||||
|
// SANs that a provisioner is NOT authorized to sign x509 certificates for.
|
||||||
|
func (o *X509Options) GetDeniedNameOptions() *policy.X509NameOptions {
|
||||||
|
if o == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return o.DeniedNames
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *X509Options) AreWildcardNamesAllowed() bool {
|
||||||
|
if o == nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return o.AllowWildcardNames
|
||||||
|
}
|
||||||
|
|
||||||
// TemplateOptions generates a CertificateOptions with the template and data
|
// TemplateOptions generates a CertificateOptions with the template and data
|
||||||
// defined in the ProvisionerOptions, the provisioner generated data, and the
|
// defined in the ProvisionerOptions, the provisioner generated data, and the
|
||||||
// user data provided in the request. If no template has been provided,
|
// user data provided in the request. If no template has been provided,
|
||||||
|
|
|
@ -287,3 +287,38 @@ func Test_unsafeParseSigned(t *testing.T) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestX509Options_IsWildcardLiteralAllowed(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
options *X509Options
|
||||||
|
want bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "nil-options",
|
||||||
|
options: nil,
|
||||||
|
want: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "set-true",
|
||||||
|
options: &X509Options{
|
||||||
|
AllowWildcardNames: true,
|
||||||
|
},
|
||||||
|
want: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "set-false",
|
||||||
|
options: &X509Options{
|
||||||
|
AllowWildcardNames: false,
|
||||||
|
},
|
||||||
|
want: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
if got := tt.options.AreWildcardNamesAllowed(); got != tt.want {
|
||||||
|
t.Errorf("X509PolicyOptions.IsWildcardLiteralAllowed() = %v, want %v", got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
65
authority/provisioner/policy.go
Normal file
65
authority/provisioner/policy.go
Normal file
|
@ -0,0 +1,65 @@
|
||||||
|
package provisioner
|
||||||
|
|
||||||
|
import "github.com/smallstep/certificates/authority/policy"
|
||||||
|
|
||||||
|
type policyEngine struct {
|
||||||
|
x509Policy policy.X509Policy
|
||||||
|
sshHostPolicy policy.HostPolicy
|
||||||
|
sshUserPolicy policy.UserPolicy
|
||||||
|
}
|
||||||
|
|
||||||
|
func newPolicyEngine(options *Options) (*policyEngine, error) {
|
||||||
|
|
||||||
|
if options == nil {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
x509Policy policy.X509Policy
|
||||||
|
sshHostPolicy policy.HostPolicy
|
||||||
|
sshUserPolicy policy.UserPolicy
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
|
||||||
|
// Initialize the x509 allow/deny policy engine
|
||||||
|
if x509Policy, err = policy.NewX509PolicyEngine(options.GetX509Options()); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize the SSH allow/deny policy engine for host certificates
|
||||||
|
if sshHostPolicy, err = policy.NewSSHHostPolicyEngine(options.GetSSHOptions()); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize the SSH allow/deny policy engine for user certificates
|
||||||
|
if sshUserPolicy, err = policy.NewSSHUserPolicyEngine(options.GetSSHOptions()); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &policyEngine{
|
||||||
|
x509Policy: x509Policy,
|
||||||
|
sshHostPolicy: sshHostPolicy,
|
||||||
|
sshUserPolicy: sshUserPolicy,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *policyEngine) getX509() policy.X509Policy {
|
||||||
|
if p == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return p.x509Policy
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *policyEngine) getSSHHost() policy.HostPolicy {
|
||||||
|
if p == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return p.sshHostPolicy
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *policyEngine) getSSHUser() policy.UserPolicy {
|
||||||
|
if p == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return p.sshUserPolicy
|
||||||
|
}
|
|
@ -28,13 +28,12 @@ type SCEP struct {
|
||||||
// Numerical identifier for the ContentEncryptionAlgorithm as defined in github.com/mozilla-services/pkcs7
|
// 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
|
// at https://github.com/mozilla-services/pkcs7/blob/33d05740a3526e382af6395d3513e73d4e66d1cb/encrypt.go#L63
|
||||||
// Defaults to 0, being DES-CBC
|
// Defaults to 0, being DES-CBC
|
||||||
EncryptionAlgorithmIdentifier int `json:"encryptionAlgorithmIdentifier,omitempty"`
|
EncryptionAlgorithmIdentifier int `json:"encryptionAlgorithmIdentifier,omitempty"`
|
||||||
|
Options *Options `json:"options,omitempty"`
|
||||||
Options *Options `json:"options,omitempty"`
|
Claims *Claims `json:"claims,omitempty"`
|
||||||
Claims *Claims `json:"claims,omitempty"`
|
ctl *Controller
|
||||||
secretChallengePassword string
|
secretChallengePassword string
|
||||||
encryptionAlgorithm int
|
encryptionAlgorithm int
|
||||||
ctl *Controller
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetID returns the provisioner unique identifier.
|
// GetID returns the provisioner unique identifier.
|
||||||
|
@ -84,7 +83,6 @@ func (s *SCEP) DefaultTLSCertDuration() time.Duration {
|
||||||
|
|
||||||
// Init initializes and validates the fields of a SCEP type.
|
// Init initializes and validates the fields of a SCEP type.
|
||||||
func (s *SCEP) Init(config Config) (err error) {
|
func (s *SCEP) Init(config Config) (err error) {
|
||||||
|
|
||||||
switch {
|
switch {
|
||||||
case s.Type == "":
|
case s.Type == "":
|
||||||
return errors.New("provisioner type cannot be empty")
|
return errors.New("provisioner type cannot be empty")
|
||||||
|
@ -112,7 +110,7 @@ func (s *SCEP) Init(config Config) (err error) {
|
||||||
|
|
||||||
// TODO: add other, SCEP specific, options?
|
// TODO: add other, SCEP specific, options?
|
||||||
|
|
||||||
s.ctl, err = NewController(s, s.Claims, config)
|
s.ctl, err = NewController(s, s.Claims, config, s.Options)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -129,6 +127,7 @@ func (s *SCEP) AuthorizeSign(ctx context.Context, token string) ([]SignOption, e
|
||||||
// validators
|
// validators
|
||||||
newPublicKeyMinimumLengthValidator(s.MinimumPublicKeyLength),
|
newPublicKeyMinimumLengthValidator(s.MinimumPublicKeyLength),
|
||||||
newValidityValidator(s.ctl.Claimer.MinTLSCertDuration(), s.ctl.Claimer.MaxTLSCertDuration()),
|
newValidityValidator(s.ctl.Claimer.MinTLSCertDuration(), s.ctl.Claimer.MaxTLSCertDuration()),
|
||||||
|
newX509NamePolicyValidator(s.ctl.getPolicy().getX509()),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -13,9 +13,11 @@ import (
|
||||||
"reflect"
|
"reflect"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/smallstep/certificates/errs"
|
|
||||||
"go.step.sm/crypto/keyutil"
|
"go.step.sm/crypto/keyutil"
|
||||||
"go.step.sm/crypto/x509util"
|
"go.step.sm/crypto/x509util"
|
||||||
|
|
||||||
|
"github.com/smallstep/certificates/authority/policy"
|
||||||
|
"github.com/smallstep/certificates/errs"
|
||||||
)
|
)
|
||||||
|
|
||||||
// DefaultCertValidity is the default validity for a certificate if none is specified.
|
// DefaultCertValidity is the default validity for a certificate if none is specified.
|
||||||
|
@ -402,6 +404,32 @@ func (v *validityValidator) Valid(cert *x509.Certificate, o SignOptions) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// x509NamePolicyValidator validates that the certificate (to be signed)
|
||||||
|
// contains only allowed SANs.
|
||||||
|
type x509NamePolicyValidator struct {
|
||||||
|
policyEngine policy.X509Policy
|
||||||
|
}
|
||||||
|
|
||||||
|
// newX509NamePolicyValidator return a new SANs allow/deny validator.
|
||||||
|
func newX509NamePolicyValidator(engine policy.X509Policy) *x509NamePolicyValidator {
|
||||||
|
return &x509NamePolicyValidator{
|
||||||
|
policyEngine: engine,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Valid validates that the certificate (to be signed) contains only allowed SANs.
|
||||||
|
func (v *x509NamePolicyValidator) Valid(cert *x509.Certificate, _ SignOptions) error {
|
||||||
|
if v.policyEngine == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return v.policyEngine.IsX509CertificateAllowed(cert)
|
||||||
|
}
|
||||||
|
|
||||||
|
// var (
|
||||||
|
// stepOIDRoot = asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 37476, 9000, 64}
|
||||||
|
// stepOIDProvisioner = append(asn1.ObjectIdentifier(nil), append(stepOIDRoot, 1)...)
|
||||||
|
// )
|
||||||
|
|
||||||
// type stepProvisionerASN1 struct {
|
// type stepProvisionerASN1 struct {
|
||||||
// Type int
|
// Type int
|
||||||
// Name []byte
|
// Name []byte
|
||||||
|
|
|
@ -4,11 +4,13 @@ import (
|
||||||
"crypto/rsa"
|
"crypto/rsa"
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
"math/big"
|
"math/big"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
|
"github.com/smallstep/certificates/authority/policy"
|
||||||
"github.com/smallstep/certificates/errs"
|
"github.com/smallstep/certificates/errs"
|
||||||
"go.step.sm/crypto/keyutil"
|
"go.step.sm/crypto/keyutil"
|
||||||
"golang.org/x/crypto/ssh"
|
"golang.org/x/crypto/ssh"
|
||||||
|
@ -444,6 +446,53 @@ func (v sshDefaultPublicKeyValidator) Valid(cert *ssh.Certificate, o SignSSHOpti
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// sshNamePolicyValidator validates that the certificate (to be signed)
|
||||||
|
// contains only allowed principals.
|
||||||
|
type sshNamePolicyValidator struct {
|
||||||
|
hostPolicyEngine policy.HostPolicy
|
||||||
|
userPolicyEngine policy.UserPolicy
|
||||||
|
}
|
||||||
|
|
||||||
|
// newSSHNamePolicyValidator return a new SSH allow/deny validator.
|
||||||
|
func newSSHNamePolicyValidator(host policy.HostPolicy, user policy.UserPolicy) *sshNamePolicyValidator {
|
||||||
|
return &sshNamePolicyValidator{
|
||||||
|
hostPolicyEngine: host,
|
||||||
|
userPolicyEngine: user,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Valid validates that the certificate (to be signed) contains only allowed principals.
|
||||||
|
func (v *sshNamePolicyValidator) Valid(cert *ssh.Certificate, _ SignSSHOptions) error {
|
||||||
|
if v.hostPolicyEngine == nil && v.userPolicyEngine == nil {
|
||||||
|
// no policy configured at all; allow anything
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check the policy type to execute based on type of the certificate.
|
||||||
|
// We don't allow user certs if only a host policy engine is configured and
|
||||||
|
// the same for host certs: if only a user policy engine is configured, host
|
||||||
|
// certs are denied. When both policy engines are configured, the type of
|
||||||
|
// cert determines which policy engine is used.
|
||||||
|
switch cert.CertType {
|
||||||
|
case ssh.HostCert:
|
||||||
|
// when no host policy engine is configured, but a user policy engine is
|
||||||
|
// configured, the host certificate is denied.
|
||||||
|
if v.hostPolicyEngine == nil && v.userPolicyEngine != nil {
|
||||||
|
return errors.New("SSH host certificate not authorized")
|
||||||
|
}
|
||||||
|
return v.hostPolicyEngine.IsSSHCertificateAllowed(cert)
|
||||||
|
case ssh.UserCert:
|
||||||
|
// when no user policy engine is configured, but a host policy engine is
|
||||||
|
// configured, the user certificate is denied.
|
||||||
|
if v.userPolicyEngine == nil && v.hostPolicyEngine != nil {
|
||||||
|
return errors.New("SSH user certificate not authorized")
|
||||||
|
}
|
||||||
|
return v.userPolicyEngine.IsSSHCertificateAllowed(cert)
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("unexpected SSH certificate type %d", cert.CertType) // satisfy return; shouldn't happen
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// sshCertTypeUInt32
|
// sshCertTypeUInt32
|
||||||
func sshCertTypeUInt32(ct string) uint32 {
|
func sshCertTypeUInt32(ct string) uint32 {
|
||||||
switch ct {
|
switch ct {
|
||||||
|
|
|
@ -6,6 +6,8 @@ import (
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"go.step.sm/crypto/sshutil"
|
"go.step.sm/crypto/sshutil"
|
||||||
|
|
||||||
|
"github.com/smallstep/certificates/authority/policy"
|
||||||
)
|
)
|
||||||
|
|
||||||
// SSHCertificateOptions is an interface that returns a list of options passed when
|
// SSHCertificateOptions is an interface that returns a list of options passed when
|
||||||
|
@ -33,6 +35,60 @@ type SSHOptions struct {
|
||||||
// TemplateData is a JSON object with variables that can be used in custom
|
// TemplateData is a JSON object with variables that can be used in custom
|
||||||
// templates.
|
// templates.
|
||||||
TemplateData json.RawMessage `json:"templateData,omitempty"`
|
TemplateData json.RawMessage `json:"templateData,omitempty"`
|
||||||
|
|
||||||
|
// User contains SSH user certificate options.
|
||||||
|
User *policy.SSHUserCertificateOptions `json:"-"`
|
||||||
|
|
||||||
|
// Host contains SSH host certificate options.
|
||||||
|
Host *policy.SSHHostCertificateOptions `json:"-"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAllowedUserNameOptions returns the SSHNameOptions that are
|
||||||
|
// allowed when SSH User certificates are requested.
|
||||||
|
func (o *SSHOptions) GetAllowedUserNameOptions() *policy.SSHNameOptions {
|
||||||
|
if o == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if o.User == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return o.User.AllowedNames
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetDeniedUserNameOptions returns the SSHNameOptions that are
|
||||||
|
// denied when SSH user certificates are requested.
|
||||||
|
func (o *SSHOptions) GetDeniedUserNameOptions() *policy.SSHNameOptions {
|
||||||
|
if o == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if o.User == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return o.User.DeniedNames
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAllowedHostNameOptions returns the SSHNameOptions that are
|
||||||
|
// allowed when SSH host certificates are requested.
|
||||||
|
func (o *SSHOptions) GetAllowedHostNameOptions() *policy.SSHNameOptions {
|
||||||
|
if o == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if o.Host == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return o.Host.AllowedNames
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetDeniedHostNameOptions returns the SSHNameOptions that are
|
||||||
|
// denied when SSH host certificates are requested.
|
||||||
|
func (o *SSHOptions) GetDeniedHostNameOptions() *policy.SSHNameOptions {
|
||||||
|
if o == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if o.Host == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return o.Host.DeniedNames
|
||||||
}
|
}
|
||||||
|
|
||||||
// HasTemplate returns true if a template is defined in the provisioner options.
|
// HasTemplate returns true if a template is defined in the provisioner options.
|
||||||
|
|
|
@ -8,9 +8,11 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/smallstep/certificates/errs"
|
|
||||||
"go.step.sm/crypto/jose"
|
|
||||||
"golang.org/x/crypto/ssh"
|
"golang.org/x/crypto/ssh"
|
||||||
|
|
||||||
|
"go.step.sm/crypto/jose"
|
||||||
|
|
||||||
|
"github.com/smallstep/certificates/errs"
|
||||||
)
|
)
|
||||||
|
|
||||||
// sshPOPPayload extends jwt.Claims with step attributes.
|
// sshPOPPayload extends jwt.Claims with step attributes.
|
||||||
|
@ -95,7 +97,7 @@ func (p *SSHPOP) Init(config Config) (err error) {
|
||||||
p.sshPubKeys = config.SSHKeys
|
p.sshPubKeys = config.SSHKeys
|
||||||
|
|
||||||
config.Audiences = config.Audiences.WithFragment(p.GetIDForToken())
|
config.Audiences = config.Audiences.WithFragment(p.GetIDForToken())
|
||||||
p.ctl, err = NewController(p, p.Claims, config)
|
p.ctl, err = NewController(p, p.Claims, config, nil)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -184,7 +184,7 @@ func generateJWK() (*JWK, error) {
|
||||||
}
|
}
|
||||||
p.ctl, err = NewController(p, p.Claims, Config{
|
p.ctl, err = NewController(p, p.Claims, Config{
|
||||||
Audiences: testAudiences,
|
Audiences: testAudiences,
|
||||||
})
|
}, nil)
|
||||||
return p, err
|
return p, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -219,7 +219,7 @@ func generateK8sSA(inputPubKey interface{}) (*K8sSA, error) {
|
||||||
}
|
}
|
||||||
p.ctl, err = NewController(p, p.Claims, Config{
|
p.ctl, err = NewController(p, p.Claims, Config{
|
||||||
Audiences: testAudiences,
|
Audiences: testAudiences,
|
||||||
})
|
}, nil)
|
||||||
return p, err
|
return p, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -256,7 +256,7 @@ func generateSSHPOP() (*SSHPOP, error) {
|
||||||
}
|
}
|
||||||
p.ctl, err = NewController(p, p.Claims, Config{
|
p.ctl, err = NewController(p, p.Claims, Config{
|
||||||
Audiences: testAudiences,
|
Audiences: testAudiences,
|
||||||
})
|
}, nil)
|
||||||
return p, err
|
return p, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -305,7 +305,7 @@ M46l92gdOozT
|
||||||
}
|
}
|
||||||
p.ctl, err = NewController(p, p.Claims, Config{
|
p.ctl, err = NewController(p, p.Claims, Config{
|
||||||
Audiences: testAudiences,
|
Audiences: testAudiences,
|
||||||
})
|
}, nil)
|
||||||
return p, err
|
return p, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -343,7 +343,7 @@ func generateOIDC() (*OIDC, error) {
|
||||||
}
|
}
|
||||||
p.ctl, err = NewController(p, p.Claims, Config{
|
p.ctl, err = NewController(p, p.Claims, Config{
|
||||||
Audiences: testAudiences,
|
Audiences: testAudiences,
|
||||||
})
|
}, nil)
|
||||||
return p, err
|
return p, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -373,7 +373,7 @@ func generateGCP() (*GCP, error) {
|
||||||
}
|
}
|
||||||
p.ctl, err = NewController(p, p.Claims, Config{
|
p.ctl, err = NewController(p, p.Claims, Config{
|
||||||
Audiences: testAudiences.WithFragment("gcp/" + name),
|
Audiences: testAudiences.WithFragment("gcp/" + name),
|
||||||
})
|
}, nil)
|
||||||
return p, err
|
return p, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -411,7 +411,7 @@ func generateAWS() (*AWS, error) {
|
||||||
}
|
}
|
||||||
p.ctl, err = NewController(p, p.Claims, Config{
|
p.ctl, err = NewController(p, p.Claims, Config{
|
||||||
Audiences: testAudiences.WithFragment("aws/" + name),
|
Audiences: testAudiences.WithFragment("aws/" + name),
|
||||||
})
|
}, nil)
|
||||||
return p, err
|
return p, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -518,7 +518,7 @@ func generateAWSV1Only() (*AWS, error) {
|
||||||
}
|
}
|
||||||
p.ctl, err = NewController(p, p.Claims, Config{
|
p.ctl, err = NewController(p, p.Claims, Config{
|
||||||
Audiences: testAudiences.WithFragment("aws/" + name),
|
Audiences: testAudiences.WithFragment("aws/" + name),
|
||||||
})
|
}, nil)
|
||||||
return p, err
|
return p, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -608,7 +608,7 @@ func generateAzure() (*Azure, error) {
|
||||||
}
|
}
|
||||||
p.ctl, err = NewController(p, p.Claims, Config{
|
p.ctl, err = NewController(p, p.Claims, Config{
|
||||||
Audiences: testAudiences,
|
Audiences: testAudiences,
|
||||||
})
|
}, nil)
|
||||||
return p, err
|
return p, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -8,10 +8,12 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/smallstep/certificates/errs"
|
|
||||||
"go.step.sm/crypto/jose"
|
"go.step.sm/crypto/jose"
|
||||||
"go.step.sm/crypto/sshutil"
|
"go.step.sm/crypto/sshutil"
|
||||||
"go.step.sm/crypto/x509util"
|
"go.step.sm/crypto/x509util"
|
||||||
|
|
||||||
|
"github.com/smallstep/certificates/errs"
|
||||||
)
|
)
|
||||||
|
|
||||||
// x5cPayload extends jwt.Claims with step attributes.
|
// x5cPayload extends jwt.Claims with step attributes.
|
||||||
|
@ -121,7 +123,7 @@ func (p *X5C) Init(config Config) (err error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
config.Audiences = config.Audiences.WithFragment(p.GetIDForToken())
|
config.Audiences = config.Audiences.WithFragment(p.GetIDForToken())
|
||||||
p.ctl, err = NewController(p, p.Claims, config)
|
p.ctl, err = NewController(p, p.Claims, config, p.Options)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -233,6 +235,7 @@ func (p *X5C) AuthorizeSign(ctx context.Context, token string) ([]SignOption, er
|
||||||
defaultSANsValidator(claims.SANs),
|
defaultSANsValidator(claims.SANs),
|
||||||
defaultPublicKeyValidator{},
|
defaultPublicKeyValidator{},
|
||||||
newValidityValidator(p.ctl.Claimer.MinTLSCertDuration(), p.ctl.Claimer.MaxTLSCertDuration()),
|
newValidityValidator(p.ctl.Claimer.MinTLSCertDuration(), p.ctl.Claimer.MaxTLSCertDuration()),
|
||||||
|
newX509NamePolicyValidator(p.ctl.getPolicy().getX509()),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -317,5 +320,7 @@ func (p *X5C) AuthorizeSSHSign(ctx context.Context, token string) ([]SignOption,
|
||||||
&sshCertValidityValidator{p.ctl.Claimer},
|
&sshCertValidityValidator{p.ctl.Claimer},
|
||||||
// Require all the fields in the SSH certificate
|
// Require all the fields in the SSH certificate
|
||||||
&sshCertDefaultValidator{},
|
&sshCertDefaultValidator{},
|
||||||
|
// Ensure that all principal names are allowed
|
||||||
|
newSSHNamePolicyValidator(p.ctl.getPolicy().getSSHHost(), p.ctl.getPolicy().getSSHUser()),
|
||||||
), nil
|
), nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -468,7 +468,7 @@ func TestX5C_AuthorizeSign(t *testing.T) {
|
||||||
} else {
|
} else {
|
||||||
if assert.Nil(t, tc.err) {
|
if assert.Nil(t, tc.err) {
|
||||||
if assert.NotNil(t, opts) {
|
if assert.NotNil(t, opts) {
|
||||||
assert.Equals(t, len(opts), 8)
|
assert.Equals(t, 9, len(opts))
|
||||||
for _, o := range opts {
|
for _, o := range opts {
|
||||||
switch v := o.(type) {
|
switch v := o.(type) {
|
||||||
case *X5C:
|
case *X5C:
|
||||||
|
@ -480,7 +480,6 @@ func TestX5C_AuthorizeSign(t *testing.T) {
|
||||||
assert.Len(t, 0, v.KeyValuePairs)
|
assert.Len(t, 0, v.KeyValuePairs)
|
||||||
case profileLimitDuration:
|
case profileLimitDuration:
|
||||||
assert.Equals(t, v.def, tc.p.ctl.Claimer.DefaultTLSCertDuration())
|
assert.Equals(t, v.def, tc.p.ctl.Claimer.DefaultTLSCertDuration())
|
||||||
|
|
||||||
claims, err := tc.p.authorizeToken(tc.token, tc.p.ctl.Audiences.Sign)
|
claims, err := tc.p.authorizeToken(tc.token, tc.p.ctl.Audiences.Sign)
|
||||||
assert.FatalError(t, err)
|
assert.FatalError(t, err)
|
||||||
assert.Equals(t, v.notAfter, claims.chains[0][0].NotAfter)
|
assert.Equals(t, v.notAfter, claims.chains[0][0].NotAfter)
|
||||||
|
@ -492,6 +491,8 @@ func TestX5C_AuthorizeSign(t *testing.T) {
|
||||||
case *validityValidator:
|
case *validityValidator:
|
||||||
assert.Equals(t, v.min, tc.p.ctl.Claimer.MinTLSCertDuration())
|
assert.Equals(t, v.min, tc.p.ctl.Claimer.MinTLSCertDuration())
|
||||||
assert.Equals(t, v.max, tc.p.ctl.Claimer.MaxTLSCertDuration())
|
assert.Equals(t, v.max, tc.p.ctl.Claimer.MaxTLSCertDuration())
|
||||||
|
case *x509NamePolicyValidator:
|
||||||
|
assert.Equals(t, nil, v.policyEngine)
|
||||||
default:
|
default:
|
||||||
assert.FatalError(t, fmt.Errorf("unexpected sign option of type %T", v))
|
assert.FatalError(t, fmt.Errorf("unexpected sign option of type %T", v))
|
||||||
}
|
}
|
||||||
|
@ -788,6 +789,9 @@ func TestX5C_AuthorizeSSHSign(t *testing.T) {
|
||||||
assert.Equals(t, v.NotAfter, x5cCerts[0].NotAfter)
|
assert.Equals(t, v.NotAfter, x5cCerts[0].NotAfter)
|
||||||
case *sshCertValidityValidator:
|
case *sshCertValidityValidator:
|
||||||
assert.Equals(t, v.Claimer, tc.p.ctl.Claimer)
|
assert.Equals(t, v.Claimer, tc.p.ctl.Claimer)
|
||||||
|
case *sshNamePolicyValidator:
|
||||||
|
assert.Equals(t, nil, v.userPolicyEngine)
|
||||||
|
assert.Equals(t, nil, v.hostPolicyEngine)
|
||||||
case *sshDefaultPublicKeyValidator, *sshCertDefaultValidator, sshCertificateOptionsFunc:
|
case *sshDefaultPublicKeyValidator, *sshCertDefaultValidator, sshCertificateOptionsFunc:
|
||||||
default:
|
default:
|
||||||
assert.FatalError(t, fmt.Errorf("unexpected sign option of type %T", v))
|
assert.FatalError(t, fmt.Errorf("unexpected sign option of type %T", v))
|
||||||
|
@ -795,9 +799,9 @@ func TestX5C_AuthorizeSSHSign(t *testing.T) {
|
||||||
tot++
|
tot++
|
||||||
}
|
}
|
||||||
if len(tc.claims.Step.SSH.CertType) > 0 {
|
if len(tc.claims.Step.SSH.CertType) > 0 {
|
||||||
assert.Equals(t, tot, 9)
|
assert.Equals(t, tot, 10)
|
||||||
} else {
|
} else {
|
||||||
assert.Equals(t, tot, 7)
|
assert.Equals(t, tot, 8)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,16 +10,19 @@ import (
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/smallstep/certificates/authority/admin"
|
"gopkg.in/square/go-jose.v2/jwt"
|
||||||
"github.com/smallstep/certificates/authority/config"
|
|
||||||
"github.com/smallstep/certificates/authority/provisioner"
|
|
||||||
"github.com/smallstep/certificates/db"
|
|
||||||
"github.com/smallstep/certificates/errs"
|
|
||||||
"go.step.sm/cli-utils/step"
|
"go.step.sm/cli-utils/step"
|
||||||
"go.step.sm/cli-utils/ui"
|
"go.step.sm/cli-utils/ui"
|
||||||
"go.step.sm/crypto/jose"
|
"go.step.sm/crypto/jose"
|
||||||
"go.step.sm/linkedca"
|
"go.step.sm/linkedca"
|
||||||
"gopkg.in/square/go-jose.v2/jwt"
|
|
||||||
|
"github.com/smallstep/certificates/authority/admin"
|
||||||
|
"github.com/smallstep/certificates/authority/config"
|
||||||
|
"github.com/smallstep/certificates/authority/policy"
|
||||||
|
"github.com/smallstep/certificates/authority/provisioner"
|
||||||
|
"github.com/smallstep/certificates/db"
|
||||||
|
"github.com/smallstep/certificates/errs"
|
||||||
)
|
)
|
||||||
|
|
||||||
// GetEncryptedKey returns the JWE key corresponding to the given kid argument.
|
// GetEncryptedKey returns the JWE key corresponding to the given kid argument.
|
||||||
|
@ -170,6 +173,12 @@ func (a *Authority) StoreProvisioner(ctx context.Context, prov *linkedca.Provisi
|
||||||
return admin.WrapErrorISE(err, "error generating provisioner config")
|
return admin.WrapErrorISE(err, "error generating provisioner config")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
adm := linkedca.MustAdminFromContext(ctx)
|
||||||
|
|
||||||
|
if err := a.checkProvisionerPolicy(ctx, adm, prov.Name, prov.Policy); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
if err := certProv.Init(provisionerConfig); err != nil {
|
if err := certProv.Init(provisionerConfig); err != nil {
|
||||||
return admin.WrapError(admin.ErrorBadRequestType, err, "error validating configuration for provisioner %s", prov.Name)
|
return admin.WrapError(admin.ErrorBadRequestType, err, "error validating configuration for provisioner %s", prov.Name)
|
||||||
}
|
}
|
||||||
|
@ -215,6 +224,12 @@ func (a *Authority) UpdateProvisioner(ctx context.Context, nu *linkedca.Provisio
|
||||||
return admin.WrapErrorISE(err, "error generating provisioner config")
|
return admin.WrapErrorISE(err, "error generating provisioner config")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
adm := linkedca.MustAdminFromContext(ctx)
|
||||||
|
|
||||||
|
if err := a.checkProvisionerPolicy(ctx, adm, nu.Name, nu.Policy); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
if err := certProv.Init(provisionerConfig); err != nil {
|
if err := certProv.Init(provisionerConfig); err != nil {
|
||||||
return admin.WrapErrorISE(err, "error initializing provisioner %s", nu.Name)
|
return admin.WrapErrorISE(err, "error initializing provisioner %s", nu.Name)
|
||||||
}
|
}
|
||||||
|
@ -427,6 +442,60 @@ func optionsToCertificates(p *linkedca.Provisioner) *provisioner.Options {
|
||||||
ops.SSH.Template = string(p.SshTemplate.Template)
|
ops.SSH.Template = string(p.SshTemplate.Template)
|
||||||
ops.SSH.TemplateData = p.SshTemplate.Data
|
ops.SSH.TemplateData = p.SshTemplate.Data
|
||||||
}
|
}
|
||||||
|
if pol := p.GetPolicy(); pol != nil {
|
||||||
|
if x := pol.GetX509(); x != nil {
|
||||||
|
if allow := x.GetAllow(); allow != nil {
|
||||||
|
ops.X509.AllowedNames = &policy.X509NameOptions{
|
||||||
|
DNSDomains: allow.Dns,
|
||||||
|
IPRanges: allow.Ips,
|
||||||
|
EmailAddresses: allow.Emails,
|
||||||
|
URIDomains: allow.Uris,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if deny := x.GetDeny(); deny != nil {
|
||||||
|
ops.X509.DeniedNames = &policy.X509NameOptions{
|
||||||
|
DNSDomains: deny.Dns,
|
||||||
|
IPRanges: deny.Ips,
|
||||||
|
EmailAddresses: deny.Emails,
|
||||||
|
URIDomains: deny.Uris,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ssh := pol.GetSsh(); ssh != nil {
|
||||||
|
if host := ssh.GetHost(); host != nil {
|
||||||
|
ops.SSH.Host = &policy.SSHHostCertificateOptions{}
|
||||||
|
if allow := host.GetAllow(); allow != nil {
|
||||||
|
ops.SSH.Host.AllowedNames = &policy.SSHNameOptions{
|
||||||
|
DNSDomains: allow.Dns,
|
||||||
|
IPRanges: allow.Ips,
|
||||||
|
Principals: allow.Principals,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if deny := host.GetDeny(); deny != nil {
|
||||||
|
ops.SSH.Host.DeniedNames = &policy.SSHNameOptions{
|
||||||
|
DNSDomains: deny.Dns,
|
||||||
|
IPRanges: deny.Ips,
|
||||||
|
Principals: deny.Principals,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if user := ssh.GetUser(); user != nil {
|
||||||
|
ops.SSH.User = &policy.SSHUserCertificateOptions{}
|
||||||
|
if allow := user.GetAllow(); allow != nil {
|
||||||
|
ops.SSH.User.AllowedNames = &policy.SSHNameOptions{
|
||||||
|
EmailAddresses: allow.Emails,
|
||||||
|
Principals: allow.Principals,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if deny := user.GetDeny(); deny != nil {
|
||||||
|
ops.SSH.User.DeniedNames = &policy.SSHNameOptions{
|
||||||
|
EmailAddresses: deny.Emails,
|
||||||
|
Principals: deny.Principals,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
return ops
|
return ops
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -5,18 +5,23 @@ import (
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"golang.org/x/crypto/ssh"
|
||||||
|
|
||||||
|
"go.step.sm/crypto/randutil"
|
||||||
|
"go.step.sm/crypto/sshutil"
|
||||||
|
|
||||||
"github.com/smallstep/certificates/authority/config"
|
"github.com/smallstep/certificates/authority/config"
|
||||||
"github.com/smallstep/certificates/authority/provisioner"
|
"github.com/smallstep/certificates/authority/provisioner"
|
||||||
"github.com/smallstep/certificates/db"
|
"github.com/smallstep/certificates/db"
|
||||||
"github.com/smallstep/certificates/errs"
|
"github.com/smallstep/certificates/errs"
|
||||||
|
policy "github.com/smallstep/certificates/policy"
|
||||||
"github.com/smallstep/certificates/templates"
|
"github.com/smallstep/certificates/templates"
|
||||||
"go.step.sm/crypto/randutil"
|
|
||||||
"go.step.sm/crypto/sshutil"
|
|
||||||
"golang.org/x/crypto/ssh"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@ -241,6 +246,23 @@ func (a *Authority) SignSSH(ctx context.Context, key ssh.PublicKey, opts provisi
|
||||||
return nil, errs.InternalServer("authority.SignSSH: unexpected ssh certificate type: %d", certTpl.CertType)
|
return nil, errs.InternalServer("authority.SignSSH: unexpected ssh certificate type: %d", certTpl.CertType)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if authority is allowed to sign the certificate
|
||||||
|
if err := a.isAllowedToSignSSHCertificate(certTpl); err != nil {
|
||||||
|
var pe *policy.NamePolicyError
|
||||||
|
if errors.As(err, &pe) && pe.Reason == policy.NotAllowed {
|
||||||
|
return nil, &errs.Error{
|
||||||
|
// NOTE: custom forbidden error, so that denied name is sent to client
|
||||||
|
// as well as shown in the logs.
|
||||||
|
Status: http.StatusForbidden,
|
||||||
|
Err: fmt.Errorf("authority not allowed to sign: %w", err),
|
||||||
|
Msg: fmt.Sprintf("The request was forbidden by the certificate authority: %s", err.Error()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, errs.InternalServerErr(err,
|
||||||
|
errs.WithMessage("authority.SignSSH: error creating ssh certificate"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// Sign certificate.
|
// Sign certificate.
|
||||||
cert, err := sshutil.CreateCertificate(certTpl, signer)
|
cert, err := sshutil.CreateCertificate(certTpl, signer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -261,6 +283,11 @@ func (a *Authority) SignSSH(ctx context.Context, key ssh.PublicKey, opts provisi
|
||||||
return cert, nil
|
return cert, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// isAllowedToSignSSHCertificate checks if the Authority is allowed to sign the SSH certificate.
|
||||||
|
func (a *Authority) isAllowedToSignSSHCertificate(cert *ssh.Certificate) error {
|
||||||
|
return a.policyEngine.IsSSHCertificateAllowed(cert)
|
||||||
|
}
|
||||||
|
|
||||||
// RenewSSH creates a signed SSH certificate using the old SSH certificate as a template.
|
// RenewSSH creates a signed SSH certificate using the old SSH certificate as a template.
|
||||||
func (a *Authority) RenewSSH(ctx context.Context, oldCert *ssh.Certificate) (*ssh.Certificate, error) {
|
func (a *Authority) RenewSSH(ctx context.Context, oldCert *ssh.Certificate) (*ssh.Certificate, error) {
|
||||||
if oldCert.ValidAfter == 0 || oldCert.ValidBefore == 0 {
|
if oldCert.ValidAfter == 0 || oldCert.ValidBefore == 0 {
|
||||||
|
|
|
@ -20,6 +20,7 @@ import (
|
||||||
|
|
||||||
"github.com/smallstep/assert"
|
"github.com/smallstep/assert"
|
||||||
"github.com/smallstep/certificates/api/render"
|
"github.com/smallstep/certificates/api/render"
|
||||||
|
"github.com/smallstep/certificates/authority/policy"
|
||||||
"github.com/smallstep/certificates/authority/provisioner"
|
"github.com/smallstep/certificates/authority/provisioner"
|
||||||
"github.com/smallstep/certificates/db"
|
"github.com/smallstep/certificates/db"
|
||||||
"github.com/smallstep/certificates/templates"
|
"github.com/smallstep/certificates/templates"
|
||||||
|
@ -159,6 +160,14 @@ func TestAuthority_SignSSH(t *testing.T) {
|
||||||
assert.FatalError(t, err)
|
assert.FatalError(t, err)
|
||||||
hostTemplateWithHosts, err := provisioner.TemplateSSHOptions(nil, sshutil.CreateTemplateData(sshutil.HostCert, "key-id", []string{"foo.test.com", "bar.test.com"}))
|
hostTemplateWithHosts, err := provisioner.TemplateSSHOptions(nil, sshutil.CreateTemplateData(sshutil.HostCert, "key-id", []string{"foo.test.com", "bar.test.com"}))
|
||||||
assert.FatalError(t, err)
|
assert.FatalError(t, err)
|
||||||
|
userTemplateWithRoot, err := provisioner.TemplateSSHOptions(nil, sshutil.CreateTemplateData(sshutil.UserCert, "key-id", []string{"root"}))
|
||||||
|
assert.FatalError(t, err)
|
||||||
|
hostTemplateWithExampleDotCom, err := provisioner.TemplateSSHOptions(nil, sshutil.CreateTemplateData(sshutil.HostCert, "key-id", []string{"example.com"}))
|
||||||
|
assert.FatalError(t, err)
|
||||||
|
badUserTemplate, err := provisioner.TemplateSSHOptions(nil, sshutil.CreateTemplateData(sshutil.UserCert, "key-id", []string{"127.0.0.1"}))
|
||||||
|
assert.FatalError(t, err)
|
||||||
|
badHostTemplate, err := provisioner.TemplateSSHOptions(nil, sshutil.CreateTemplateData(sshutil.HostCert, "key-id", []string{"host...local"}))
|
||||||
|
assert.FatalError(t, err)
|
||||||
userCustomTemplate, err := provisioner.TemplateSSHOptions(&provisioner.Options{
|
userCustomTemplate, err := provisioner.TemplateSSHOptions(&provisioner.Options{
|
||||||
SSH: &provisioner.SSHOptions{Template: `{
|
SSH: &provisioner.SSHOptions{Template: `{
|
||||||
"type": "{{ .Type }}",
|
"type": "{{ .Type }}",
|
||||||
|
@ -182,11 +191,36 @@ func TestAuthority_SignSSH(t *testing.T) {
|
||||||
}, sshutil.CreateTemplateData(sshutil.UserCert, "key-id", []string{"user"}))
|
}, sshutil.CreateTemplateData(sshutil.UserCert, "key-id", []string{"user"}))
|
||||||
assert.FatalError(t, err)
|
assert.FatalError(t, err)
|
||||||
|
|
||||||
|
userPolicyOptions := &policy.Options{
|
||||||
|
SSH: &policy.SSHPolicyOptions{
|
||||||
|
User: &policy.SSHUserCertificateOptions{
|
||||||
|
AllowedNames: &policy.SSHNameOptions{
|
||||||
|
Principals: []string{"user"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
userPolicy, err := policy.New(userPolicyOptions)
|
||||||
|
assert.FatalError(t, err)
|
||||||
|
|
||||||
|
hostPolicyOptions := &policy.Options{
|
||||||
|
SSH: &policy.SSHPolicyOptions{
|
||||||
|
Host: &policy.SSHHostCertificateOptions{
|
||||||
|
AllowedNames: &policy.SSHNameOptions{
|
||||||
|
DNSDomains: []string{"*.test.com"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
hostPolicy, err := policy.New(hostPolicyOptions)
|
||||||
|
assert.FatalError(t, err)
|
||||||
|
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
|
|
||||||
type fields struct {
|
type fields struct {
|
||||||
sshCAUserCertSignKey ssh.Signer
|
sshCAUserCertSignKey ssh.Signer
|
||||||
sshCAHostCertSignKey ssh.Signer
|
sshCAHostCertSignKey ssh.Signer
|
||||||
|
policyEngine *policy.Engine
|
||||||
}
|
}
|
||||||
type args struct {
|
type args struct {
|
||||||
key ssh.PublicKey
|
key ssh.PublicKey
|
||||||
|
@ -206,39 +240,48 @@ func TestAuthority_SignSSH(t *testing.T) {
|
||||||
want want
|
want want
|
||||||
wantErr bool
|
wantErr bool
|
||||||
}{
|
}{
|
||||||
{"ok-user", fields{signer, signer}, args{pub, provisioner.SignSSHOptions{}, []provisioner.SignOption{userTemplate, userOptions}}, want{CertType: ssh.UserCert}, false},
|
{"ok-user", fields{signer, signer, nil}, args{pub, provisioner.SignSSHOptions{}, []provisioner.SignOption{userTemplate, userOptions}}, want{CertType: ssh.UserCert}, false},
|
||||||
{"ok-host", fields{signer, signer}, args{pub, provisioner.SignSSHOptions{}, []provisioner.SignOption{hostTemplate, hostOptions}}, want{CertType: ssh.HostCert}, false},
|
{"ok-host", fields{signer, signer, nil}, args{pub, provisioner.SignSSHOptions{}, []provisioner.SignOption{hostTemplate, hostOptions}}, want{CertType: ssh.HostCert}, false},
|
||||||
{"ok-user-only", fields{signer, nil}, args{pub, provisioner.SignSSHOptions{}, []provisioner.SignOption{userTemplate, userOptions}}, want{CertType: ssh.UserCert}, false},
|
{"ok-user-only", fields{signer, nil, nil}, args{pub, provisioner.SignSSHOptions{}, []provisioner.SignOption{userTemplate, userOptions}}, want{CertType: ssh.UserCert}, false},
|
||||||
{"ok-host-only", fields{nil, signer}, args{pub, provisioner.SignSSHOptions{}, []provisioner.SignOption{hostTemplate, hostOptions}}, want{CertType: ssh.HostCert}, false},
|
{"ok-host-only", fields{nil, signer, nil}, args{pub, provisioner.SignSSHOptions{}, []provisioner.SignOption{hostTemplate, hostOptions}}, want{CertType: ssh.HostCert}, false},
|
||||||
{"ok-opts-type-user", fields{signer, signer}, args{pub, provisioner.SignSSHOptions{CertType: "user"}, []provisioner.SignOption{userTemplate}}, want{CertType: ssh.UserCert}, false},
|
{"ok-opts-type-user", fields{signer, signer, nil}, args{pub, provisioner.SignSSHOptions{CertType: "user"}, []provisioner.SignOption{userTemplate}}, want{CertType: ssh.UserCert}, false},
|
||||||
{"ok-opts-type-host", fields{signer, signer}, args{pub, provisioner.SignSSHOptions{CertType: "host"}, []provisioner.SignOption{hostTemplate}}, want{CertType: ssh.HostCert}, false},
|
{"ok-opts-type-host", fields{signer, signer, nil}, args{pub, provisioner.SignSSHOptions{CertType: "host"}, []provisioner.SignOption{hostTemplate}}, want{CertType: ssh.HostCert}, false},
|
||||||
{"ok-opts-principals", fields{signer, signer}, args{pub, provisioner.SignSSHOptions{CertType: "user", Principals: []string{"user"}}, []provisioner.SignOption{userTemplateWithUser}}, want{CertType: ssh.UserCert, Principals: []string{"user"}}, false},
|
{"ok-opts-principals", fields{signer, signer, nil}, args{pub, provisioner.SignSSHOptions{CertType: "user", Principals: []string{"user"}}, []provisioner.SignOption{userTemplateWithUser}}, want{CertType: ssh.UserCert, Principals: []string{"user"}}, false},
|
||||||
{"ok-opts-principals", fields{signer, signer}, args{pub, provisioner.SignSSHOptions{CertType: "host", Principals: []string{"foo.test.com", "bar.test.com"}}, []provisioner.SignOption{hostTemplateWithHosts}}, want{CertType: ssh.HostCert, Principals: []string{"foo.test.com", "bar.test.com"}}, false},
|
{"ok-opts-principals", fields{signer, signer, nil}, args{pub, provisioner.SignSSHOptions{CertType: "host", Principals: []string{"foo.test.com", "bar.test.com"}}, []provisioner.SignOption{hostTemplateWithHosts}}, want{CertType: ssh.HostCert, Principals: []string{"foo.test.com", "bar.test.com"}}, false},
|
||||||
{"ok-opts-valid-after", fields{signer, signer}, args{pub, provisioner.SignSSHOptions{CertType: "user", ValidAfter: provisioner.NewTimeDuration(now)}, []provisioner.SignOption{userTemplate}}, want{CertType: ssh.UserCert, ValidAfter: uint64(now.Unix())}, false},
|
{"ok-opts-valid-after", fields{signer, signer, nil}, args{pub, provisioner.SignSSHOptions{CertType: "user", ValidAfter: provisioner.NewTimeDuration(now)}, []provisioner.SignOption{userTemplate}}, want{CertType: ssh.UserCert, ValidAfter: uint64(now.Unix())}, false},
|
||||||
{"ok-opts-valid-before", fields{signer, signer}, args{pub, provisioner.SignSSHOptions{CertType: "host", ValidBefore: provisioner.NewTimeDuration(now)}, []provisioner.SignOption{hostTemplate}}, want{CertType: ssh.HostCert, ValidBefore: uint64(now.Unix())}, false},
|
{"ok-opts-valid-before", fields{signer, signer, nil}, args{pub, provisioner.SignSSHOptions{CertType: "host", ValidBefore: provisioner.NewTimeDuration(now)}, []provisioner.SignOption{hostTemplate}}, want{CertType: ssh.HostCert, ValidBefore: uint64(now.Unix())}, false},
|
||||||
{"ok-cert-validator", fields{signer, signer}, args{pub, provisioner.SignSSHOptions{}, []provisioner.SignOption{userTemplate, userOptions, sshTestCertValidator("")}}, want{CertType: ssh.UserCert}, false},
|
{"ok-cert-validator", fields{signer, signer, nil}, args{pub, provisioner.SignSSHOptions{}, []provisioner.SignOption{userTemplate, userOptions, sshTestCertValidator("")}}, want{CertType: ssh.UserCert}, false},
|
||||||
{"ok-cert-modifier", fields{signer, signer}, args{pub, provisioner.SignSSHOptions{}, []provisioner.SignOption{userTemplate, userOptions, sshTestCertModifier("")}}, want{CertType: ssh.UserCert}, false},
|
{"ok-cert-modifier", fields{signer, signer, nil}, args{pub, provisioner.SignSSHOptions{}, []provisioner.SignOption{userTemplate, userOptions, sshTestCertModifier("")}}, want{CertType: ssh.UserCert}, false},
|
||||||
{"ok-opts-validator", fields{signer, signer}, args{pub, provisioner.SignSSHOptions{}, []provisioner.SignOption{userTemplate, userOptions, sshTestOptionsValidator("")}}, want{CertType: ssh.UserCert}, false},
|
{"ok-opts-validator", fields{signer, signer, nil}, args{pub, provisioner.SignSSHOptions{}, []provisioner.SignOption{userTemplate, userOptions, sshTestOptionsValidator("")}}, want{CertType: ssh.UserCert}, false},
|
||||||
{"ok-opts-modifier", fields{signer, signer}, args{pub, provisioner.SignSSHOptions{}, []provisioner.SignOption{userTemplate, userOptions, sshTestOptionsModifier("")}}, want{CertType: ssh.UserCert}, false},
|
{"ok-opts-modifier", fields{signer, signer, nil}, args{pub, provisioner.SignSSHOptions{}, []provisioner.SignOption{userTemplate, userOptions, sshTestOptionsModifier("")}}, want{CertType: ssh.UserCert}, false},
|
||||||
{"ok-custom-template", fields{signer, signer}, args{pub, provisioner.SignSSHOptions{}, []provisioner.SignOption{userCustomTemplate, userOptions}}, want{CertType: ssh.UserCert, Principals: []string{"user", "admin"}}, false},
|
{"ok-custom-template", fields{signer, signer, nil}, args{pub, provisioner.SignSSHOptions{}, []provisioner.SignOption{userCustomTemplate, userOptions}}, want{CertType: ssh.UserCert, Principals: []string{"user", "admin"}}, false},
|
||||||
{"fail-opts-type", fields{signer, signer}, args{pub, provisioner.SignSSHOptions{CertType: "foo"}, []provisioner.SignOption{userTemplate}}, want{}, true},
|
{"ok-user-policy", fields{signer, signer, userPolicy}, args{pub, provisioner.SignSSHOptions{CertType: "user", Principals: []string{"user"}}, []provisioner.SignOption{userTemplateWithUser}}, want{CertType: ssh.UserCert, Principals: []string{"user"}}, false},
|
||||||
{"fail-cert-validator", fields{signer, signer}, args{pub, provisioner.SignSSHOptions{}, []provisioner.SignOption{userTemplate, userOptions, sshTestCertValidator("an error")}}, want{}, true},
|
{"ok-host-policy", fields{signer, signer, hostPolicy}, args{pub, provisioner.SignSSHOptions{CertType: "host", Principals: []string{"foo.test.com", "bar.test.com"}}, []provisioner.SignOption{hostTemplateWithHosts}}, want{CertType: ssh.HostCert, Principals: []string{"foo.test.com", "bar.test.com"}}, false},
|
||||||
{"fail-cert-modifier", fields{signer, signer}, args{pub, provisioner.SignSSHOptions{}, []provisioner.SignOption{userTemplate, userOptions, sshTestCertModifier("an error")}}, want{}, true},
|
{"fail-opts-type", fields{signer, signer, nil}, args{pub, provisioner.SignSSHOptions{CertType: "foo"}, []provisioner.SignOption{userTemplate}}, want{}, true},
|
||||||
{"fail-opts-validator", fields{signer, signer}, args{pub, provisioner.SignSSHOptions{}, []provisioner.SignOption{userTemplate, userOptions, sshTestOptionsValidator("an error")}}, want{}, true},
|
{"fail-cert-validator", fields{signer, signer, nil}, args{pub, provisioner.SignSSHOptions{}, []provisioner.SignOption{userTemplate, userOptions, sshTestCertValidator("an error")}}, want{}, true},
|
||||||
{"fail-opts-modifier", fields{signer, signer}, args{pub, provisioner.SignSSHOptions{}, []provisioner.SignOption{userTemplate, userOptions, sshTestOptionsModifier("an error")}}, want{}, true},
|
{"fail-cert-modifier", fields{signer, signer, nil}, args{pub, provisioner.SignSSHOptions{}, []provisioner.SignOption{userTemplate, userOptions, sshTestCertModifier("an error")}}, want{}, true},
|
||||||
{"fail-bad-sign-options", fields{signer, signer}, args{pub, provisioner.SignSSHOptions{}, []provisioner.SignOption{userTemplate, userOptions, "wrong type"}}, want{}, true},
|
{"fail-opts-validator", fields{signer, signer, nil}, args{pub, provisioner.SignSSHOptions{}, []provisioner.SignOption{userTemplate, userOptions, sshTestOptionsValidator("an error")}}, want{}, true},
|
||||||
{"fail-no-user-key", fields{nil, signer}, args{pub, provisioner.SignSSHOptions{CertType: "user"}, []provisioner.SignOption{userTemplate}}, want{}, true},
|
{"fail-opts-modifier", fields{signer, signer, nil}, args{pub, provisioner.SignSSHOptions{}, []provisioner.SignOption{userTemplate, userOptions, sshTestOptionsModifier("an error")}}, want{}, true},
|
||||||
{"fail-no-host-key", fields{signer, nil}, args{pub, provisioner.SignSSHOptions{CertType: "host"}, []provisioner.SignOption{hostTemplate}}, want{}, true},
|
{"fail-bad-sign-options", fields{signer, signer, nil}, args{pub, provisioner.SignSSHOptions{}, []provisioner.SignOption{userTemplate, userOptions, "wrong type"}}, want{}, true},
|
||||||
{"fail-bad-type", fields{signer, nil}, args{pub, provisioner.SignSSHOptions{}, []provisioner.SignOption{userTemplate, sshTestModifier{CertType: 100}}}, want{}, true},
|
{"fail-no-user-key", fields{nil, signer, nil}, args{pub, provisioner.SignSSHOptions{CertType: "user"}, []provisioner.SignOption{userTemplate}}, want{}, true},
|
||||||
{"fail-custom-template", fields{signer, signer}, args{pub, provisioner.SignSSHOptions{}, []provisioner.SignOption{userFailTemplate, userOptions}}, want{}, true},
|
{"fail-no-host-key", fields{signer, nil, nil}, args{pub, provisioner.SignSSHOptions{CertType: "host"}, []provisioner.SignOption{hostTemplate}}, want{}, true},
|
||||||
{"fail-custom-template-syntax-error-file", fields{signer, signer}, args{pub, provisioner.SignSSHOptions{}, []provisioner.SignOption{userJSONSyntaxErrorTemplateFile, userOptions}}, want{}, true},
|
{"fail-bad-type", fields{signer, nil, nil}, args{pub, provisioner.SignSSHOptions{}, []provisioner.SignOption{userTemplate, sshTestModifier{CertType: 100}}}, want{}, true},
|
||||||
{"fail-custom-template-syntax-value-file", fields{signer, signer}, args{pub, provisioner.SignSSHOptions{}, []provisioner.SignOption{userJSONValueErrorTemplateFile, userOptions}}, want{}, true},
|
{"fail-custom-template", fields{signer, signer, nil}, args{pub, provisioner.SignSSHOptions{}, []provisioner.SignOption{userFailTemplate, userOptions}}, want{}, true},
|
||||||
|
{"fail-custom-template-syntax-error-file", fields{signer, signer, nil}, args{pub, provisioner.SignSSHOptions{}, []provisioner.SignOption{userJSONSyntaxErrorTemplateFile, userOptions}}, want{}, true},
|
||||||
|
{"fail-custom-template-syntax-value-file", fields{signer, signer, nil}, args{pub, provisioner.SignSSHOptions{}, []provisioner.SignOption{userJSONValueErrorTemplateFile, userOptions}}, want{}, true},
|
||||||
|
{"fail-user-policy", fields{signer, signer, userPolicy}, args{pub, provisioner.SignSSHOptions{CertType: "user", Principals: []string{"root"}}, []provisioner.SignOption{userTemplateWithRoot}}, want{}, true},
|
||||||
|
{"fail-user-policy-with-host-cert", fields{signer, signer, userPolicy}, args{pub, provisioner.SignSSHOptions{CertType: "host", Principals: []string{"foo.test.com"}}, []provisioner.SignOption{hostTemplateWithExampleDotCom}}, want{}, true},
|
||||||
|
{"fail-user-policy-with-bad-user", fields{signer, signer, userPolicy}, args{pub, provisioner.SignSSHOptions{CertType: "user", Principals: []string{"user"}}, []provisioner.SignOption{badUserTemplate}}, want{}, true},
|
||||||
|
{"fail-host-policy", fields{signer, signer, hostPolicy}, args{pub, provisioner.SignSSHOptions{CertType: "host", Principals: []string{"example.com"}}, []provisioner.SignOption{hostTemplateWithExampleDotCom}}, want{}, true},
|
||||||
|
{"fail-host-policy-with-user-cert", fields{signer, signer, hostPolicy}, args{pub, provisioner.SignSSHOptions{CertType: "user", Principals: []string{"user"}}, []provisioner.SignOption{userTemplateWithUser}}, want{}, true},
|
||||||
|
{"fail-host-policy-with-bad-host", fields{signer, signer, hostPolicy}, args{pub, provisioner.SignSSHOptions{CertType: "host", Principals: []string{"example.com"}}, []provisioner.SignOption{badHostTemplate}}, want{}, true},
|
||||||
}
|
}
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
a := testAuthority(t)
|
a := testAuthority(t)
|
||||||
a.sshCAUserCertSignKey = tt.fields.sshCAUserCertSignKey
|
a.sshCAUserCertSignKey = tt.fields.sshCAUserCertSignKey
|
||||||
a.sshCAHostCertSignKey = tt.fields.sshCAHostCertSignKey
|
a.sshCAHostCertSignKey = tt.fields.sshCAHostCertSignKey
|
||||||
|
a.policyEngine = tt.fields.policyEngine
|
||||||
|
|
||||||
got, err := a.SignSSH(context.Background(), tt.args.key, tt.args.opts, tt.args.signOpts...)
|
got, err := a.SignSSH(context.Background(), tt.args.key, tt.args.opts, tt.args.signOpts...)
|
||||||
if (err != nil) != tt.wantErr {
|
if (err != nil) != tt.wantErr {
|
||||||
|
|
|
@ -16,16 +16,19 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
|
"golang.org/x/crypto/ssh"
|
||||||
|
|
||||||
|
"go.step.sm/crypto/jose"
|
||||||
|
"go.step.sm/crypto/keyutil"
|
||||||
|
"go.step.sm/crypto/pemutil"
|
||||||
|
"go.step.sm/crypto/x509util"
|
||||||
|
|
||||||
"github.com/smallstep/certificates/authority/config"
|
"github.com/smallstep/certificates/authority/config"
|
||||||
"github.com/smallstep/certificates/authority/provisioner"
|
"github.com/smallstep/certificates/authority/provisioner"
|
||||||
casapi "github.com/smallstep/certificates/cas/apiv1"
|
casapi "github.com/smallstep/certificates/cas/apiv1"
|
||||||
"github.com/smallstep/certificates/db"
|
"github.com/smallstep/certificates/db"
|
||||||
"github.com/smallstep/certificates/errs"
|
"github.com/smallstep/certificates/errs"
|
||||||
"go.step.sm/crypto/jose"
|
"github.com/smallstep/certificates/policy"
|
||||||
"go.step.sm/crypto/keyutil"
|
|
||||||
"go.step.sm/crypto/pemutil"
|
|
||||||
"go.step.sm/crypto/x509util"
|
|
||||||
"golang.org/x/crypto/ssh"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// GetTLSOptions returns the tls options configured.
|
// GetTLSOptions returns the tls options configured.
|
||||||
|
@ -196,6 +199,25 @@ func (a *Authority) Sign(csr *x509.CertificateRequest, signOpts provisioner.Sign
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if authority is allowed to sign the certificate
|
||||||
|
if err := a.isAllowedToSignX509Certificate(leaf); err != nil {
|
||||||
|
var pe *policy.NamePolicyError
|
||||||
|
if errors.As(err, &pe) && pe.Reason == policy.NotAllowed {
|
||||||
|
return nil, errs.ApplyOptions(&errs.Error{
|
||||||
|
// NOTE: custom forbidden error, so that denied name is sent to client
|
||||||
|
// as well as shown in the logs.
|
||||||
|
Status: http.StatusForbidden,
|
||||||
|
Err: fmt.Errorf("authority not allowed to sign: %w", err),
|
||||||
|
Msg: fmt.Sprintf("The request was forbidden by the certificate authority: %s", err.Error()),
|
||||||
|
}, opts...)
|
||||||
|
}
|
||||||
|
return nil, errs.InternalServerErr(err,
|
||||||
|
errs.WithKeyVal("csr", csr),
|
||||||
|
errs.WithKeyVal("signOptions", signOpts),
|
||||||
|
errs.WithMessage("error creating certificate"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// Sign certificate
|
// Sign certificate
|
||||||
lifetime := leaf.NotAfter.Sub(leaf.NotBefore.Add(signOpts.Backdate))
|
lifetime := leaf.NotAfter.Sub(leaf.NotBefore.Add(signOpts.Backdate))
|
||||||
resp, err := a.x509CAService.CreateCertificate(&casapi.CreateCertificateRequest{
|
resp, err := a.x509CAService.CreateCertificate(&casapi.CreateCertificateRequest{
|
||||||
|
@ -219,6 +241,18 @@ func (a *Authority) Sign(csr *x509.CertificateRequest, signOpts provisioner.Sign
|
||||||
return fullchain, nil
|
return fullchain, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// isAllowedToSignX509Certificate checks if the Authority is allowed
|
||||||
|
// to sign the X.509 certificate.
|
||||||
|
func (a *Authority) isAllowedToSignX509Certificate(cert *x509.Certificate) error {
|
||||||
|
return a.policyEngine.IsX509CertificateAllowed(cert)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AreSANsAllowed evaluates the provided sans against the
|
||||||
|
// authority X.509 policy.
|
||||||
|
func (a *Authority) AreSANsAllowed(ctx context.Context, sans []string) error {
|
||||||
|
return a.policyEngine.AreSANsAllowed(sans)
|
||||||
|
}
|
||||||
|
|
||||||
// Renew creates a new Certificate identical to the old certificate, except
|
// Renew creates a new Certificate identical to the old certificate, except
|
||||||
// with a validity window that begins 'now'.
|
// with a validity window that begins 'now'.
|
||||||
func (a *Authority) Renew(oldCert *x509.Certificate) ([]*x509.Certificate, error) {
|
func (a *Authority) Renew(oldCert *x509.Certificate) ([]*x509.Certificate, error) {
|
||||||
|
|
|
@ -27,6 +27,7 @@ import (
|
||||||
|
|
||||||
"github.com/smallstep/assert"
|
"github.com/smallstep/assert"
|
||||||
"github.com/smallstep/certificates/api/render"
|
"github.com/smallstep/certificates/api/render"
|
||||||
|
"github.com/smallstep/certificates/authority/policy"
|
||||||
"github.com/smallstep/certificates/authority/provisioner"
|
"github.com/smallstep/certificates/authority/provisioner"
|
||||||
"github.com/smallstep/certificates/cas/softcas"
|
"github.com/smallstep/certificates/cas/softcas"
|
||||||
"github.com/smallstep/certificates/db"
|
"github.com/smallstep/certificates/db"
|
||||||
|
@ -511,6 +512,39 @@ ZYtQ9Ot36qc=
|
||||||
code: http.StatusForbidden,
|
code: http.StatusForbidden,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"fail with policy": func(t *testing.T) *signTest {
|
||||||
|
csr := getCSR(t, priv)
|
||||||
|
aa := testAuthority(t)
|
||||||
|
aa.config.AuthorityConfig.Template = a.config.AuthorityConfig.Template
|
||||||
|
aa.db = &db.MockAuthDB{
|
||||||
|
MStoreCertificate: func(crt *x509.Certificate) error {
|
||||||
|
fmt.Println(crt.Subject)
|
||||||
|
assert.Equals(t, crt.Subject.CommonName, "smallstep test")
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
options := &policy.Options{
|
||||||
|
X509: &policy.X509PolicyOptions{
|
||||||
|
DeniedNames: &policy.X509NameOptions{
|
||||||
|
DNSDomains: []string{"test.smallstep.com"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
engine, err := policy.New(options)
|
||||||
|
assert.FatalError(t, err)
|
||||||
|
aa.policyEngine = engine
|
||||||
|
return &signTest{
|
||||||
|
auth: aa,
|
||||||
|
csr: csr,
|
||||||
|
extraOpts: extraOpts,
|
||||||
|
signOpts: signOpts,
|
||||||
|
notBefore: signOpts.NotBefore.Time().Truncate(time.Second),
|
||||||
|
notAfter: signOpts.NotAfter.Time().Truncate(time.Second),
|
||||||
|
extensionsCount: 6,
|
||||||
|
err: errors.New("authority not allowed to sign"),
|
||||||
|
code: http.StatusForbidden,
|
||||||
|
}
|
||||||
|
},
|
||||||
"ok": func(t *testing.T) *signTest {
|
"ok": func(t *testing.T) *signTest {
|
||||||
csr := getCSR(t, priv)
|
csr := getCSR(t, priv)
|
||||||
_a := testAuthority(t)
|
_a := testAuthority(t)
|
||||||
|
@ -653,6 +687,38 @@ ZYtQ9Ot36qc=
|
||||||
extensionsCount: 7,
|
extensionsCount: 7,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"ok with policy": func(t *testing.T) *signTest {
|
||||||
|
csr := getCSR(t, priv)
|
||||||
|
aa := testAuthority(t)
|
||||||
|
aa.config.AuthorityConfig.Template = a.config.AuthorityConfig.Template
|
||||||
|
aa.db = &db.MockAuthDB{
|
||||||
|
MStoreCertificate: func(crt *x509.Certificate) error {
|
||||||
|
fmt.Println(crt.Subject)
|
||||||
|
assert.Equals(t, crt.Subject.CommonName, "smallstep test")
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
options := &policy.Options{
|
||||||
|
X509: &policy.X509PolicyOptions{
|
||||||
|
AllowedNames: &policy.X509NameOptions{
|
||||||
|
CommonNames: []string{"smallstep test"},
|
||||||
|
DNSDomains: []string{"*.smallstep.com"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
engine, err := policy.New(options)
|
||||||
|
assert.FatalError(t, err)
|
||||||
|
aa.policyEngine = engine
|
||||||
|
return &signTest{
|
||||||
|
auth: aa,
|
||||||
|
csr: csr,
|
||||||
|
extraOpts: extraOpts,
|
||||||
|
signOpts: signOpts,
|
||||||
|
notBefore: signOpts.NotBefore.Time().Truncate(time.Second),
|
||||||
|
notAfter: signOpts.NotAfter.Time().Truncate(time.Second),
|
||||||
|
extensionsCount: 6,
|
||||||
|
}
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for name, genTestCase := range tests {
|
for name, genTestCase := range tests {
|
||||||
|
|
|
@ -4,6 +4,7 @@ import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
@ -12,15 +13,17 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
adminAPI "github.com/smallstep/certificates/authority/admin/api"
|
"google.golang.org/protobuf/encoding/protojson"
|
||||||
"github.com/smallstep/certificates/authority/provisioner"
|
|
||||||
"github.com/smallstep/certificates/errs"
|
|
||||||
"go.step.sm/cli-utils/token"
|
"go.step.sm/cli-utils/token"
|
||||||
"go.step.sm/cli-utils/token/provision"
|
"go.step.sm/cli-utils/token/provision"
|
||||||
"go.step.sm/crypto/jose"
|
"go.step.sm/crypto/jose"
|
||||||
"go.step.sm/crypto/randutil"
|
"go.step.sm/crypto/randutil"
|
||||||
"go.step.sm/linkedca"
|
"go.step.sm/linkedca"
|
||||||
"google.golang.org/protobuf/encoding/protojson"
|
|
||||||
|
adminAPI "github.com/smallstep/certificates/authority/admin/api"
|
||||||
|
"github.com/smallstep/certificates/authority/provisioner"
|
||||||
|
"github.com/smallstep/certificates/errs"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@ -687,6 +690,418 @@ retry:
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *AdminClient) GetAuthorityPolicy() (*linkedca.Policy, error) {
|
||||||
|
var retried bool
|
||||||
|
u := c.endpoint.ResolveReference(&url.URL{Path: path.Join(adminURLPrefix, "policy")})
|
||||||
|
tok, err := c.generateAdminToken(u)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error generating admin token: %w", err)
|
||||||
|
}
|
||||||
|
req, err := http.NewRequest(http.MethodGet, u.String(), http.NoBody)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("creating GET %s request failed: %w", u, err)
|
||||||
|
}
|
||||||
|
req.Header.Add("Authorization", tok)
|
||||||
|
retry:
|
||||||
|
resp, err := c.client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("client GET %s failed: %w", u, err)
|
||||||
|
}
|
||||||
|
if resp.StatusCode >= 400 {
|
||||||
|
if !retried && c.retryOnError(resp) {
|
||||||
|
retried = true
|
||||||
|
goto retry
|
||||||
|
}
|
||||||
|
return nil, readAdminError(resp.Body)
|
||||||
|
}
|
||||||
|
var policy = new(linkedca.Policy)
|
||||||
|
if err := readProtoJSON(resp.Body, policy); err != nil {
|
||||||
|
return nil, fmt.Errorf("error reading %s: %w", u, err)
|
||||||
|
}
|
||||||
|
return policy, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *AdminClient) CreateAuthorityPolicy(p *linkedca.Policy) (*linkedca.Policy, error) {
|
||||||
|
var retried bool
|
||||||
|
body, err := protojson.Marshal(p)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error marshaling request: %w", err)
|
||||||
|
}
|
||||||
|
u := c.endpoint.ResolveReference(&url.URL{Path: path.Join(adminURLPrefix, "policy")})
|
||||||
|
tok, err := c.generateAdminToken(u)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error generating admin token: %w", err)
|
||||||
|
}
|
||||||
|
req, err := http.NewRequest(http.MethodPost, u.String(), bytes.NewReader(body))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("creating POST %s request failed: %w", u, err)
|
||||||
|
}
|
||||||
|
req.Header.Add("Authorization", tok)
|
||||||
|
retry:
|
||||||
|
resp, err := c.client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("client POST %s failed: %w", u, err)
|
||||||
|
}
|
||||||
|
if resp.StatusCode >= 400 {
|
||||||
|
if !retried && c.retryOnError(resp) {
|
||||||
|
retried = true
|
||||||
|
goto retry
|
||||||
|
}
|
||||||
|
return nil, readAdminError(resp.Body)
|
||||||
|
}
|
||||||
|
var policy = new(linkedca.Policy)
|
||||||
|
if err := readProtoJSON(resp.Body, policy); err != nil {
|
||||||
|
return nil, fmt.Errorf("error reading %s: %w", u, err)
|
||||||
|
}
|
||||||
|
return policy, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *AdminClient) UpdateAuthorityPolicy(p *linkedca.Policy) (*linkedca.Policy, error) {
|
||||||
|
var retried bool
|
||||||
|
body, err := protojson.Marshal(p)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error marshaling request: %w", err)
|
||||||
|
}
|
||||||
|
u := c.endpoint.ResolveReference(&url.URL{Path: path.Join(adminURLPrefix, "policy")})
|
||||||
|
tok, err := c.generateAdminToken(u)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error generating admin token: %w", err)
|
||||||
|
}
|
||||||
|
req, err := http.NewRequest(http.MethodPut, u.String(), bytes.NewReader(body))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("creating PUT %s request failed: %w", u, err)
|
||||||
|
}
|
||||||
|
req.Header.Add("Authorization", tok)
|
||||||
|
retry:
|
||||||
|
resp, err := c.client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("client PUT %s failed: %w", u, err)
|
||||||
|
}
|
||||||
|
if resp.StatusCode >= 400 {
|
||||||
|
if !retried && c.retryOnError(resp) {
|
||||||
|
retried = true
|
||||||
|
goto retry
|
||||||
|
}
|
||||||
|
return nil, readAdminError(resp.Body)
|
||||||
|
}
|
||||||
|
var policy = new(linkedca.Policy)
|
||||||
|
if err := readProtoJSON(resp.Body, policy); err != nil {
|
||||||
|
return nil, fmt.Errorf("error reading %s: %w", u, err)
|
||||||
|
}
|
||||||
|
return policy, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *AdminClient) RemoveAuthorityPolicy() error {
|
||||||
|
var retried bool
|
||||||
|
u := c.endpoint.ResolveReference(&url.URL{Path: path.Join(adminURLPrefix, "policy")})
|
||||||
|
tok, err := c.generateAdminToken(u)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error generating admin token: %w", err)
|
||||||
|
}
|
||||||
|
req, err := http.NewRequest(http.MethodDelete, u.String(), http.NoBody)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("creating DELETE %s request failed: %w", u, err)
|
||||||
|
}
|
||||||
|
req.Header.Add("Authorization", tok)
|
||||||
|
retry:
|
||||||
|
resp, err := c.client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("client DELETE %s failed: %w", u, err)
|
||||||
|
}
|
||||||
|
if resp.StatusCode >= 400 {
|
||||||
|
if !retried && c.retryOnError(resp) {
|
||||||
|
retried = true
|
||||||
|
goto retry
|
||||||
|
}
|
||||||
|
return readAdminError(resp.Body)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *AdminClient) GetProvisionerPolicy(provisionerName string) (*linkedca.Policy, error) {
|
||||||
|
var retried bool
|
||||||
|
u := c.endpoint.ResolveReference(&url.URL{Path: path.Join(adminURLPrefix, "provisioners", provisionerName, "policy")})
|
||||||
|
tok, err := c.generateAdminToken(u)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error generating admin token: %w", err)
|
||||||
|
}
|
||||||
|
req, err := http.NewRequest(http.MethodGet, u.String(), http.NoBody)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("creating GET %s request failed: %w", u, err)
|
||||||
|
}
|
||||||
|
req.Header.Add("Authorization", tok)
|
||||||
|
retry:
|
||||||
|
resp, err := c.client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("client GET %s failed: %w", u, err)
|
||||||
|
}
|
||||||
|
if resp.StatusCode >= 400 {
|
||||||
|
if !retried && c.retryOnError(resp) {
|
||||||
|
retried = true
|
||||||
|
goto retry
|
||||||
|
}
|
||||||
|
return nil, readAdminError(resp.Body)
|
||||||
|
}
|
||||||
|
var policy = new(linkedca.Policy)
|
||||||
|
if err := readProtoJSON(resp.Body, policy); err != nil {
|
||||||
|
return nil, fmt.Errorf("error reading %s: %w", u, err)
|
||||||
|
}
|
||||||
|
return policy, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *AdminClient) CreateProvisionerPolicy(provisionerName string, p *linkedca.Policy) (*linkedca.Policy, error) {
|
||||||
|
var retried bool
|
||||||
|
body, err := protojson.Marshal(p)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error marshaling request: %w", err)
|
||||||
|
}
|
||||||
|
u := c.endpoint.ResolveReference(&url.URL{Path: path.Join(adminURLPrefix, "provisioners", provisionerName, "policy")})
|
||||||
|
tok, err := c.generateAdminToken(u)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error generating admin token: %w", err)
|
||||||
|
}
|
||||||
|
req, err := http.NewRequest(http.MethodPost, u.String(), bytes.NewReader(body))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("creating POST %s request failed: %w", u, err)
|
||||||
|
}
|
||||||
|
req.Header.Add("Authorization", tok)
|
||||||
|
retry:
|
||||||
|
resp, err := c.client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("client POST %s failed: %w", u, err)
|
||||||
|
}
|
||||||
|
if resp.StatusCode >= 400 {
|
||||||
|
if !retried && c.retryOnError(resp) {
|
||||||
|
retried = true
|
||||||
|
goto retry
|
||||||
|
}
|
||||||
|
return nil, readAdminError(resp.Body)
|
||||||
|
}
|
||||||
|
var policy = new(linkedca.Policy)
|
||||||
|
if err := readProtoJSON(resp.Body, policy); err != nil {
|
||||||
|
return nil, fmt.Errorf("error reading %s: %w", u, err)
|
||||||
|
}
|
||||||
|
return policy, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *AdminClient) UpdateProvisionerPolicy(provisionerName string, p *linkedca.Policy) (*linkedca.Policy, error) {
|
||||||
|
var retried bool
|
||||||
|
body, err := protojson.Marshal(p)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error marshaling request: %w", err)
|
||||||
|
}
|
||||||
|
u := c.endpoint.ResolveReference(&url.URL{Path: path.Join(adminURLPrefix, "provisioners", provisionerName, "policy")})
|
||||||
|
tok, err := c.generateAdminToken(u)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error generating admin token: %w", err)
|
||||||
|
}
|
||||||
|
req, err := http.NewRequest(http.MethodPut, u.String(), bytes.NewReader(body))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("creating PUT %s request failed: %w", u, err)
|
||||||
|
}
|
||||||
|
req.Header.Add("Authorization", tok)
|
||||||
|
retry:
|
||||||
|
resp, err := c.client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("client PUT %s failed: %w", u, err)
|
||||||
|
}
|
||||||
|
if resp.StatusCode >= 400 {
|
||||||
|
if !retried && c.retryOnError(resp) {
|
||||||
|
retried = true
|
||||||
|
goto retry
|
||||||
|
}
|
||||||
|
return nil, readAdminError(resp.Body)
|
||||||
|
}
|
||||||
|
var policy = new(linkedca.Policy)
|
||||||
|
if err := readProtoJSON(resp.Body, policy); err != nil {
|
||||||
|
return nil, fmt.Errorf("error reading %s: %w", u, err)
|
||||||
|
}
|
||||||
|
return policy, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *AdminClient) RemoveProvisionerPolicy(provisionerName string) error {
|
||||||
|
var retried bool
|
||||||
|
u := c.endpoint.ResolveReference(&url.URL{Path: path.Join(adminURLPrefix, "provisioners", provisionerName, "policy")})
|
||||||
|
tok, err := c.generateAdminToken(u)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error generating admin token: %w", err)
|
||||||
|
}
|
||||||
|
req, err := http.NewRequest(http.MethodDelete, u.String(), http.NoBody)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("creating DELETE %s request failed: %w", u, err)
|
||||||
|
}
|
||||||
|
req.Header.Add("Authorization", tok)
|
||||||
|
retry:
|
||||||
|
resp, err := c.client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("client DELETE %s failed: %w", u, err)
|
||||||
|
}
|
||||||
|
if resp.StatusCode >= 400 {
|
||||||
|
if !retried && c.retryOnError(resp) {
|
||||||
|
retried = true
|
||||||
|
goto retry
|
||||||
|
}
|
||||||
|
return readAdminError(resp.Body)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *AdminClient) GetACMEPolicy(provisionerName, reference, keyID string) (*linkedca.Policy, error) {
|
||||||
|
var retried bool
|
||||||
|
var urlPath string
|
||||||
|
switch {
|
||||||
|
case keyID != "":
|
||||||
|
urlPath = path.Join(adminURLPrefix, "acme", "policy", provisionerName, "key", keyID)
|
||||||
|
default:
|
||||||
|
urlPath = path.Join(adminURLPrefix, "acme", "policy", provisionerName, "reference", reference)
|
||||||
|
}
|
||||||
|
u := c.endpoint.ResolveReference(&url.URL{Path: urlPath})
|
||||||
|
tok, err := c.generateAdminToken(u)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error generating admin token: %w", err)
|
||||||
|
}
|
||||||
|
req, err := http.NewRequest(http.MethodGet, u.String(), http.NoBody)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("creating GET %s request failed: %w", u, err)
|
||||||
|
}
|
||||||
|
req.Header.Add("Authorization", tok)
|
||||||
|
retry:
|
||||||
|
resp, err := c.client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("client GET %s failed: %w", u, err)
|
||||||
|
}
|
||||||
|
if resp.StatusCode >= 400 {
|
||||||
|
if !retried && c.retryOnError(resp) {
|
||||||
|
retried = true
|
||||||
|
goto retry
|
||||||
|
}
|
||||||
|
return nil, readAdminError(resp.Body)
|
||||||
|
}
|
||||||
|
var policy = new(linkedca.Policy)
|
||||||
|
if err := readProtoJSON(resp.Body, policy); err != nil {
|
||||||
|
return nil, fmt.Errorf("error reading %s: %w", u, err)
|
||||||
|
}
|
||||||
|
return policy, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *AdminClient) CreateACMEPolicy(provisionerName, reference, keyID string, p *linkedca.Policy) (*linkedca.Policy, error) {
|
||||||
|
var retried bool
|
||||||
|
body, err := protojson.Marshal(p)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error marshaling request: %w", err)
|
||||||
|
}
|
||||||
|
var urlPath string
|
||||||
|
switch {
|
||||||
|
case keyID != "":
|
||||||
|
urlPath = path.Join(adminURLPrefix, "acme", "policy", provisionerName, "key", keyID)
|
||||||
|
default:
|
||||||
|
urlPath = path.Join(adminURLPrefix, "acme", "policy", provisionerName, "reference", reference)
|
||||||
|
}
|
||||||
|
u := c.endpoint.ResolveReference(&url.URL{Path: urlPath})
|
||||||
|
tok, err := c.generateAdminToken(u)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error generating admin token: %w", err)
|
||||||
|
}
|
||||||
|
req, err := http.NewRequest(http.MethodPost, u.String(), bytes.NewReader(body))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("creating POST %s request failed: %w", u, err)
|
||||||
|
}
|
||||||
|
req.Header.Add("Authorization", tok)
|
||||||
|
retry:
|
||||||
|
resp, err := c.client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("client POST %s failed: %w", u, err)
|
||||||
|
}
|
||||||
|
if resp.StatusCode >= 400 {
|
||||||
|
if !retried && c.retryOnError(resp) {
|
||||||
|
retried = true
|
||||||
|
goto retry
|
||||||
|
}
|
||||||
|
return nil, readAdminError(resp.Body)
|
||||||
|
}
|
||||||
|
var policy = new(linkedca.Policy)
|
||||||
|
if err := readProtoJSON(resp.Body, policy); err != nil {
|
||||||
|
return nil, fmt.Errorf("error reading %s: %w", u, err)
|
||||||
|
}
|
||||||
|
return policy, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *AdminClient) UpdateACMEPolicy(provisionerName, reference, keyID string, p *linkedca.Policy) (*linkedca.Policy, error) {
|
||||||
|
var retried bool
|
||||||
|
body, err := protojson.Marshal(p)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error marshaling request: %w", err)
|
||||||
|
}
|
||||||
|
var urlPath string
|
||||||
|
switch {
|
||||||
|
case keyID != "":
|
||||||
|
urlPath = path.Join(adminURLPrefix, "acme", "policy", provisionerName, "key", keyID)
|
||||||
|
default:
|
||||||
|
urlPath = path.Join(adminURLPrefix, "acme", "policy", provisionerName, "reference", reference)
|
||||||
|
}
|
||||||
|
u := c.endpoint.ResolveReference(&url.URL{Path: urlPath})
|
||||||
|
tok, err := c.generateAdminToken(u)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error generating admin token: %w", err)
|
||||||
|
}
|
||||||
|
req, err := http.NewRequest(http.MethodPut, u.String(), bytes.NewReader(body))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("creating PUT %s request failed: %w", u, err)
|
||||||
|
}
|
||||||
|
req.Header.Add("Authorization", tok)
|
||||||
|
retry:
|
||||||
|
resp, err := c.client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("client PUT %s failed: %w", u, err)
|
||||||
|
}
|
||||||
|
if resp.StatusCode >= 400 {
|
||||||
|
if !retried && c.retryOnError(resp) {
|
||||||
|
retried = true
|
||||||
|
goto retry
|
||||||
|
}
|
||||||
|
return nil, readAdminError(resp.Body)
|
||||||
|
}
|
||||||
|
var policy = new(linkedca.Policy)
|
||||||
|
if err := readProtoJSON(resp.Body, policy); err != nil {
|
||||||
|
return nil, fmt.Errorf("error reading %s: %w", u, err)
|
||||||
|
}
|
||||||
|
return policy, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *AdminClient) RemoveACMEPolicy(provisionerName, reference, keyID string) error {
|
||||||
|
var retried bool
|
||||||
|
var urlPath string
|
||||||
|
switch {
|
||||||
|
case keyID != "":
|
||||||
|
urlPath = path.Join(adminURLPrefix, "acme", "policy", provisionerName, "key", keyID)
|
||||||
|
default:
|
||||||
|
urlPath = path.Join(adminURLPrefix, "acme", "policy", provisionerName, "reference", reference)
|
||||||
|
}
|
||||||
|
u := c.endpoint.ResolveReference(&url.URL{Path: urlPath})
|
||||||
|
tok, err := c.generateAdminToken(u)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error generating admin token: %w", err)
|
||||||
|
}
|
||||||
|
req, err := http.NewRequest(http.MethodDelete, u.String(), http.NoBody)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("creating DELETE %s request failed: %w", u, err)
|
||||||
|
}
|
||||||
|
req.Header.Add("Authorization", tok)
|
||||||
|
retry:
|
||||||
|
resp, err := c.client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("client DELETE %s failed: %w", u, err)
|
||||||
|
}
|
||||||
|
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 {
|
func readAdminError(r io.ReadCloser) error {
|
||||||
// TODO: not all errors can be read (i.e. 404); seems to be a bigger issue
|
// TODO: not all errors can be read (i.e. 404); seems to be a bigger issue
|
||||||
defer r.Close()
|
defer r.Close()
|
||||||
|
|
3
ca/ca.go
3
ca/ca.go
|
@ -213,8 +213,9 @@ func (ca *CA) Init(cfg *config.Config) (*CA, error) {
|
||||||
adminDB := auth.GetAdminDatabase()
|
adminDB := auth.GetAdminDatabase()
|
||||||
if adminDB != nil {
|
if adminDB != nil {
|
||||||
acmeAdminResponder := adminAPI.NewACMEAdminResponder()
|
acmeAdminResponder := adminAPI.NewACMEAdminResponder()
|
||||||
|
policyAdminResponder := adminAPI.NewPolicyAdminResponder(auth, adminDB, acmeDB)
|
||||||
mux.Route("/admin", func(r chi.Router) {
|
mux.Route("/admin", func(r chi.Router) {
|
||||||
adminAPI.Route(r, acmeAdminResponder)
|
adminAPI.Route(r, acmeAdminResponder, policyAdminResponder)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,15 +3,15 @@ FROM golang:alpine AS builder
|
||||||
WORKDIR /src
|
WORKDIR /src
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
RUN apk add --no-cache \
|
RUN apk add --no-cache curl git make
|
||||||
curl \
|
RUN make V=1 bin/step-ca bin/step-awskms-init bin/step-cloudkms-init
|
||||||
git \
|
|
||||||
make && \
|
|
||||||
make V=1 bin/step-ca
|
|
||||||
|
|
||||||
FROM smallstep/step-cli:latest
|
FROM smallstep/step-cli:latest
|
||||||
|
|
||||||
COPY --from=builder /src/bin/step-ca /usr/local/bin/step-ca
|
COPY --from=builder /src/bin/step-ca /usr/local/bin/step-ca
|
||||||
|
COPY --from=builder /src/bin/step-awskms-init /usr/local/bin/step-awskms-init
|
||||||
|
COPY --from=builder /src/bin/step-cloudkms-init /usr/local/bin/step-cloudkms-init
|
||||||
|
|
||||||
USER root
|
USER root
|
||||||
RUN apk add --no-cache libcap && setcap CAP_NET_BIND_SERVICE=+eip /usr/local/bin/step-ca
|
RUN apk add --no-cache libcap && setcap CAP_NET_BIND_SERVICE=+eip /usr/local/bin/step-ca
|
||||||
|
|
34
docker/Dockerfile.step-ca.hsm
Normal file
34
docker/Dockerfile.step-ca.hsm
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
FROM golang:alpine AS builder
|
||||||
|
|
||||||
|
WORKDIR /src
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
RUN apk add --no-cache curl git make
|
||||||
|
RUN apk add --no-cache gcc musl-dev pkgconf pcsc-lite-dev
|
||||||
|
RUN make V=1 GOFLAGS="" build
|
||||||
|
|
||||||
|
|
||||||
|
FROM smallstep/step-cli:latest
|
||||||
|
|
||||||
|
COPY --from=builder /src/bin/step-ca /usr/local/bin/step-ca
|
||||||
|
COPY --from=builder /src/bin/step-awskms-init /usr/local/bin/step-awskms-init
|
||||||
|
COPY --from=builder /src/bin/step-cloudkms-init /usr/local/bin/step-cloudkms-init
|
||||||
|
COPY --from=builder /src/bin/step-pkcs11-init /usr/local/bin/step-pkcs11-init
|
||||||
|
COPY --from=builder /src/bin/step-yubikey-init /usr/local/bin/step-yubikey-init
|
||||||
|
|
||||||
|
USER root
|
||||||
|
RUN apk add --no-cache libcap && setcap CAP_NET_BIND_SERVICE=+eip /usr/local/bin/step-ca
|
||||||
|
RUN apk add --no-cache pcsc-lite pcsc-lite-libs
|
||||||
|
USER step
|
||||||
|
|
||||||
|
ENV CONFIGPATH="/home/step/config/ca.json"
|
||||||
|
ENV PWDPATH="/home/step/secrets/password"
|
||||||
|
|
||||||
|
VOLUME ["/home/step"]
|
||||||
|
STOPSIGNAL SIGTERM
|
||||||
|
HEALTHCHECK CMD step ca health 2>/dev/null | grep "^ok" >/dev/null
|
||||||
|
|
||||||
|
COPY docker/entrypoint.sh /entrypoint.sh
|
||||||
|
|
||||||
|
ENTRYPOINT ["/bin/bash", "/entrypoint.sh"]
|
||||||
|
CMD exec /usr/local/bin/step-ca --password-file $PWDPATH $CONFIGPATH
|
|
@ -53,6 +53,10 @@ function step_ca_init () {
|
||||||
mv $STEPPATH/password $PWDPATH
|
mv $STEPPATH/password $PWDPATH
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if [ -f /usr/sbin/pcscd ]; then
|
||||||
|
/usr/sbin/pcscd
|
||||||
|
fi
|
||||||
|
|
||||||
if [ ! -f "${STEPPATH}/config/ca.json" ]; then
|
if [ ! -f "${STEPPATH}/config/ca.json" ]; then
|
||||||
init_if_possible
|
init_if_possible
|
||||||
fi
|
fi
|
||||||
|
|
22
go.mod
22
go.mod
|
@ -14,20 +14,26 @@ require (
|
||||||
github.com/Azure/go-autorest/autorest/validation v0.3.1 // indirect
|
github.com/Azure/go-autorest/autorest/validation v0.3.1 // indirect
|
||||||
github.com/Masterminds/sprig/v3 v3.2.2
|
github.com/Masterminds/sprig/v3 v3.2.2
|
||||||
github.com/ThalesIgnite/crypto11 v1.2.4
|
github.com/ThalesIgnite/crypto11 v1.2.4
|
||||||
github.com/aws/aws-sdk-go v1.30.29
|
github.com/aws/aws-sdk-go v1.37.0
|
||||||
github.com/dgraph-io/ristretto v0.0.4-0.20200906165740-41ebdbffecfd // indirect
|
github.com/dgraph-io/ristretto v0.0.4-0.20200906165740-41ebdbffecfd // indirect
|
||||||
|
github.com/fatih/color v1.9.0 // indirect
|
||||||
|
github.com/form3tech-oss/jwt-go v3.2.3+incompatible // indirect
|
||||||
github.com/go-chi/chi v4.0.2+incompatible
|
github.com/go-chi/chi v4.0.2+incompatible
|
||||||
github.com/go-kit/kit v0.10.0 // indirect
|
github.com/go-kit/kit v0.10.0 // indirect
|
||||||
github.com/go-piv/piv-go v1.7.0
|
github.com/go-piv/piv-go v1.7.0
|
||||||
|
github.com/go-sql-driver/mysql v1.6.0 // indirect
|
||||||
|
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
||||||
github.com/golang/mock v1.6.0
|
github.com/golang/mock v1.6.0
|
||||||
github.com/google/go-cmp v0.5.7
|
github.com/google/go-cmp v0.5.7
|
||||||
github.com/google/uuid v1.3.0
|
github.com/google/uuid v1.3.0
|
||||||
github.com/googleapis/gax-go/v2 v2.1.1
|
github.com/googleapis/gax-go/v2 v2.1.1
|
||||||
github.com/hashicorp/vault/api v1.3.1
|
github.com/hashicorp/vault/api v1.3.1
|
||||||
github.com/hashicorp/vault/api/auth/approle v0.1.1
|
github.com/hashicorp/vault/api/auth/approle v0.1.1
|
||||||
|
github.com/jhump/protoreflect v1.9.0 // indirect
|
||||||
github.com/mattn/go-colorable v0.1.8 // indirect
|
github.com/mattn/go-colorable v0.1.8 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.13 // indirect
|
github.com/mattn/go-isatty v0.0.13 // indirect
|
||||||
github.com/micromdm/scep/v2 v2.1.0
|
github.com/micromdm/scep/v2 v2.1.0
|
||||||
|
github.com/miekg/pkcs11 v1.0.3 // indirect
|
||||||
github.com/newrelic/go-agent v2.15.0+incompatible
|
github.com/newrelic/go-agent v2.15.0+incompatible
|
||||||
github.com/pkg/errors v0.9.1
|
github.com/pkg/errors v0.9.1
|
||||||
github.com/rs/xid v1.2.1
|
github.com/rs/xid v1.2.1
|
||||||
|
@ -37,17 +43,21 @@ require (
|
||||||
github.com/smallstep/nosql v0.4.0
|
github.com/smallstep/nosql v0.4.0
|
||||||
github.com/stretchr/testify v1.7.1
|
github.com/stretchr/testify v1.7.1
|
||||||
github.com/urfave/cli v1.22.4
|
github.com/urfave/cli v1.22.4
|
||||||
|
go.etcd.io/bbolt v1.3.6 // indirect
|
||||||
go.mozilla.org/pkcs7 v0.0.0-20210826202110-33d05740a352
|
go.mozilla.org/pkcs7 v0.0.0-20210826202110-33d05740a352
|
||||||
go.step.sm/cli-utils v0.7.0
|
go.step.sm/cli-utils v0.7.0
|
||||||
go.step.sm/crypto v0.16.1
|
go.step.sm/crypto v0.16.1
|
||||||
go.step.sm/linkedca v0.15.0
|
go.step.sm/linkedca v0.16.1
|
||||||
golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3
|
golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3
|
||||||
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd
|
golang.org/x/net v0.0.0-20220403103023-749bd193bc2b
|
||||||
|
golang.org/x/sys v0.0.0-20220405052023-b1e9470b6e64 // indirect
|
||||||
|
golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba // indirect
|
||||||
google.golang.org/api v0.70.0
|
google.golang.org/api v0.70.0
|
||||||
google.golang.org/genproto v0.0.0-20220222213610-43724f9ea8cf
|
google.golang.org/genproto v0.0.0-20220401170504-314d38edb7de
|
||||||
google.golang.org/grpc v1.44.0
|
google.golang.org/grpc v1.45.0
|
||||||
google.golang.org/protobuf v1.27.1
|
google.golang.org/protobuf v1.28.0
|
||||||
gopkg.in/square/go-jose.v2 v2.6.0
|
gopkg.in/square/go-jose.v2 v2.6.0
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
|
||||||
)
|
)
|
||||||
|
|
||||||
// replace github.com/smallstep/nosql => ../nosql
|
// replace github.com/smallstep/nosql => ../nosql
|
||||||
|
|
65
go.sum
65
go.sum
|
@ -143,8 +143,8 @@ github.com/aryann/difflib v0.0.0-20170710044230-e206f873d14a/go.mod h1:DAHtR1m6l
|
||||||
github.com/aryann/difflib v0.0.0-20170710044230-e206f873d14a/go.mod h1:DAHtR1m6lCRdSC2Tm3DSWRPvIPr6xNKyeHdqDQSQT+A=
|
github.com/aryann/difflib v0.0.0-20170710044230-e206f873d14a/go.mod h1:DAHtR1m6lCRdSC2Tm3DSWRPvIPr6xNKyeHdqDQSQT+A=
|
||||||
github.com/aws/aws-lambda-go v1.13.3/go.mod h1:4UKl9IzQMoD+QF79YdCuzCwp8VbmG4VAQwij/eHl5CU=
|
github.com/aws/aws-lambda-go v1.13.3/go.mod h1:4UKl9IzQMoD+QF79YdCuzCwp8VbmG4VAQwij/eHl5CU=
|
||||||
github.com/aws/aws-sdk-go v1.27.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
|
github.com/aws/aws-sdk-go v1.27.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
|
||||||
github.com/aws/aws-sdk-go v1.30.29 h1:NXNqBS9hjOCpDL8SyCyl38gZX3LLLunKOJc5E7vJ8P0=
|
github.com/aws/aws-sdk-go v1.37.0 h1:GzFnhOIsrGyQ69s7VgqtrG2BG8v7X7vwB3Xpbd/DBBk=
|
||||||
github.com/aws/aws-sdk-go v1.30.29/go.mod h1:5zCpMtNQVjRREroY7sYe8lOMRSxkhG6MZveU8YkpAk0=
|
github.com/aws/aws-sdk-go v1.37.0/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro=
|
||||||
github.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZwxzkQq9wy+g=
|
github.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZwxzkQq9wy+g=
|
||||||
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
||||||
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
|
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
|
||||||
|
@ -235,13 +235,15 @@ github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.m
|
||||||
github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0=
|
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/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||||
github.com/evanphx/json-patch/v5 v5.5.0/go.mod h1:G79N1coSVB93tBe7j6PhzjmR3/2VvlbKOFpnXhI9Bw4=
|
github.com/evanphx/json-patch/v5 v5.5.0/go.mod h1:G79N1coSVB93tBe7j6PhzjmR3/2VvlbKOFpnXhI9Bw4=
|
||||||
github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys=
|
|
||||||
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
|
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
|
||||||
|
github.com/fatih/color v1.9.0 h1:8xPHl4/q1VyqGIPif1F+1V3Y3lSmrq01EabUW3CoW5s=
|
||||||
|
github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=
|
||||||
github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo=
|
github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo=
|
||||||
github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
|
github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
|
||||||
github.com/flynn/noise v1.0.0/go.mod h1:xbMo+0i6+IGbYdJhF31t2eR1BIU0CYc12+BNAKwUTag=
|
github.com/flynn/noise v1.0.0/go.mod h1:xbMo+0i6+IGbYdJhF31t2eR1BIU0CYc12+BNAKwUTag=
|
||||||
github.com/form3tech-oss/jwt-go v3.2.2+incompatible h1:TcekIExNqud5crz4xD2pavyTgWiPvpYe4Xau31I0PRk=
|
|
||||||
github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k=
|
github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k=
|
||||||
|
github.com/form3tech-oss/jwt-go v3.2.3+incompatible h1:7ZaBxOI7TMoYBfyA3cQHErNNyAWIKUMIwqxEtgHOs5c=
|
||||||
|
github.com/form3tech-oss/jwt-go v3.2.3+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k=
|
||||||
github.com/franela/goblin v0.0.0-20200105215937-c9ffbefa60db/go.mod h1:7dvUGVsVBjqR7JHJk0brhHOZYGmfBYOrK0ZhYMEtBr4=
|
github.com/franela/goblin v0.0.0-20200105215937-c9ffbefa60db/go.mod h1:7dvUGVsVBjqR7JHJk0brhHOZYGmfBYOrK0ZhYMEtBr4=
|
||||||
github.com/franela/goreq v0.0.0-20171204163338-bcd34c9993f8/go.mod h1:ZhphrRTfi2rbfLwlschooIH4+wKKDR4Pdxhh+TRoA20=
|
github.com/franela/goreq v0.0.0-20171204163338-bcd34c9993f8/go.mod h1:ZhphrRTfi2rbfLwlschooIH4+wKKDR4Pdxhh+TRoA20=
|
||||||
github.com/frankban/quicktest v1.10.0/go.mod h1:ui7WezCLWMWxVWr1GETZY3smRy0G4KWq9vcPtJmFl7Y=
|
github.com/frankban/quicktest v1.10.0/go.mod h1:ui7WezCLWMWxVWr1GETZY3smRy0G4KWq9vcPtJmFl7Y=
|
||||||
|
@ -269,8 +271,9 @@ github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG
|
||||||
github.com/go-piv/piv-go v1.7.0 h1:rfjdFdASfGV5KLJhSjgpGJ5lzVZVtRWn8ovy/H9HQ/U=
|
github.com/go-piv/piv-go v1.7.0 h1:rfjdFdASfGV5KLJhSjgpGJ5lzVZVtRWn8ovy/H9HQ/U=
|
||||||
github.com/go-piv/piv-go v1.7.0/go.mod h1:ON2WvQncm7dIkCQ7kYJs+nc3V4jHGfrrJnSF8HKy7Gk=
|
github.com/go-piv/piv-go v1.7.0/go.mod h1:ON2WvQncm7dIkCQ7kYJs+nc3V4jHGfrrJnSF8HKy7Gk=
|
||||||
github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
|
github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
|
||||||
github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs=
|
|
||||||
github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
|
github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
|
||||||
|
github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE=
|
||||||
|
github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
|
||||||
github.com/go-stack/stack v1.6.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
github.com/go-stack/stack v1.6.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||||
github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk=
|
github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk=
|
||||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||||
|
@ -287,8 +290,9 @@ github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfU
|
||||||
github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||||
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||||
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||||
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e h1:1r7pUrabqp18hOBcwBwiTsbnFeTZHV9eER/QT5JVZxY=
|
|
||||||
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||||
|
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
|
||||||
|
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||||
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||||
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||||
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
|
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
|
||||||
|
@ -369,6 +373,7 @@ github.com/googleapis/gax-go/v2 v2.1.0/go.mod h1:Q3nei7sK6ybPYH7twZdmQpAd1MKb7pf
|
||||||
github.com/googleapis/gax-go/v2 v2.1.1 h1:dp3bWCh+PPO1zjRRiCSczJav13sBvG4UhNyVTa1KqdU=
|
github.com/googleapis/gax-go/v2 v2.1.1 h1:dp3bWCh+PPO1zjRRiCSczJav13sBvG4UhNyVTa1KqdU=
|
||||||
github.com/googleapis/gax-go/v2 v2.1.1/go.mod h1:hddJymUZASv3XPyGkUpKj8pPO47Rmb0eJc8R6ouapiM=
|
github.com/googleapis/gax-go/v2 v2.1.1/go.mod h1:hddJymUZASv3XPyGkUpKj8pPO47Rmb0eJc8R6ouapiM=
|
||||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||||
|
github.com/gordonklaus/ineffassign v0.0.0-20200309095847-7953dde2c7bf/go.mod h1:cuNKsD1zp2v6XfE/orVX2QE1LC+i254ceGcVeDT3pTU=
|
||||||
github.com/gorilla/context v0.0.0-20160226214623-1ea25387ff6f/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
|
github.com/gorilla/context v0.0.0-20160226214623-1ea25387ff6f/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
|
||||||
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
|
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
|
||||||
github.com/gorilla/mux v1.4.0/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
|
github.com/gorilla/mux v1.4.0/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
|
||||||
|
@ -511,11 +516,14 @@ github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0f
|
||||||
github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
|
github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
|
||||||
github.com/jackc/puddle v1.2.0/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
|
github.com/jackc/puddle v1.2.0/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
|
||||||
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
|
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
|
||||||
github.com/jhump/protoreflect v1.6.0 h1:h5jfMVslIg6l29nsMs0D8Wj17RDVdNYti0vDN/PZZoE=
|
|
||||||
github.com/jhump/protoreflect v1.6.0/go.mod h1:eaTn3RZAmMBcV0fifFvlm6VHNz3wSkYyXYWUh7ymB74=
|
github.com/jhump/protoreflect v1.6.0/go.mod h1:eaTn3RZAmMBcV0fifFvlm6VHNz3wSkYyXYWUh7ymB74=
|
||||||
|
github.com/jhump/protoreflect v1.9.0 h1:npqHz788dryJiR/l6K/RUQAyh2SwV91+d1dnh4RjO9w=
|
||||||
|
github.com/jhump/protoreflect v1.9.0/go.mod h1:7GcYQDdMU/O/BBrl/cX6PNHpXh6cenjd8pneu5yW7Tg=
|
||||||
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
|
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
|
||||||
github.com/jmespath/go-jmespath v0.3.0 h1:OS12ieG61fsCg5+qLJ+SsW9NicxNkg3b25OyT2yCeUc=
|
github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
|
||||||
github.com/jmespath/go-jmespath v0.3.0/go.mod h1:9QtRXoHjLGCJ5IBSaohpXITPlowMeeYCZ7fLUTSywik=
|
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
|
||||||
|
github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8=
|
||||||
|
github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
|
||||||
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
|
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
|
||||||
github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
|
github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
|
||||||
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
||||||
|
@ -585,8 +593,9 @@ github.com/micromdm/scep/v2 v2.1.0 h1:2fS9Rla7qRR266hvUoEauBJ7J6FhgssEiq2OkSKXma
|
||||||
github.com/micromdm/scep/v2 v2.1.0/go.mod h1:BkF7TkPPhmgJAMtHfP+sFTKXmgzNJgLQlvvGoOExBcc=
|
github.com/micromdm/scep/v2 v2.1.0/go.mod h1:BkF7TkPPhmgJAMtHfP+sFTKXmgzNJgLQlvvGoOExBcc=
|
||||||
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
|
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
|
||||||
github.com/miekg/dns v1.1.43/go.mod h1:+evo5L0630/F6ca/Z9+GAqzhjGyn8/c+TBaOyfEl0V4=
|
github.com/miekg/dns v1.1.43/go.mod h1:+evo5L0630/F6ca/Z9+GAqzhjGyn8/c+TBaOyfEl0V4=
|
||||||
github.com/miekg/pkcs11 v1.0.3-0.20190429190417-a667d056470f h1:eVB9ELsoq5ouItQBr5Tj334bhPJG/MX+m7rTchmzVUQ=
|
|
||||||
github.com/miekg/pkcs11 v1.0.3-0.20190429190417-a667d056470f/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs=
|
github.com/miekg/pkcs11 v1.0.3-0.20190429190417-a667d056470f/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs=
|
||||||
|
github.com/miekg/pkcs11 v1.0.3 h1:iMwmD7I5225wv84WxIG/bmxz9AXjWvTWIbM/TYHvWtw=
|
||||||
|
github.com/miekg/pkcs11 v1.0.3/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs=
|
||||||
github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
|
github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
|
||||||
github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw=
|
github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw=
|
||||||
github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=
|
github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=
|
||||||
|
@ -624,6 +633,7 @@ github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OS
|
||||||
github.com/nbrownus/go-metrics-prometheus v0.0.0-20210712211119-974a6260965f/go.mod h1:nwPd6pDNId/Xi16qtKrFHrauSwMNuvk+zcjk89wrnlA=
|
github.com/nbrownus/go-metrics-prometheus v0.0.0-20210712211119-974a6260965f/go.mod h1:nwPd6pDNId/Xi16qtKrFHrauSwMNuvk+zcjk89wrnlA=
|
||||||
github.com/newrelic/go-agent v2.15.0+incompatible h1:IB0Fy+dClpBq9aEoIrLyQXzU34JyI1xVTanPLB/+jvU=
|
github.com/newrelic/go-agent v2.15.0+incompatible h1:IB0Fy+dClpBq9aEoIrLyQXzU34JyI1xVTanPLB/+jvU=
|
||||||
github.com/newrelic/go-agent v2.15.0+incompatible/go.mod h1:a8Fv1b/fYhFSReoTU6HDkTYIMZeSVNffmoS726Y0LzQ=
|
github.com/newrelic/go-agent v2.15.0+incompatible/go.mod h1:a8Fv1b/fYhFSReoTU6HDkTYIMZeSVNffmoS726Y0LzQ=
|
||||||
|
github.com/nishanths/predeclared v0.0.0-20200524104333-86fad755b4d3/go.mod h1:nt3d53pc1VYcphSCIaYAJtnPYnr3Zyn8fMq2wvPGPso=
|
||||||
github.com/oklog/oklog v0.3.2/go.mod h1:FCV+B7mhrz4o+ueLpx+KqkyXRGMWOYEvfiXtdGtbWGs=
|
github.com/oklog/oklog v0.3.2/go.mod h1:FCV+B7mhrz4o+ueLpx+KqkyXRGMWOYEvfiXtdGtbWGs=
|
||||||
github.com/oklog/run v1.0.0 h1:Ru7dDtJNOyC66gQ5dQmaCa0qIsAUFY3sFpK1Xk8igrw=
|
github.com/oklog/run v1.0.0 h1:Ru7dDtJNOyC66gQ5dQmaCa0qIsAUFY3sFpK1Xk8igrw=
|
||||||
github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA=
|
github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA=
|
||||||
|
@ -782,8 +792,9 @@ github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1
|
||||||
github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||||
github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q=
|
github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q=
|
||||||
go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
|
go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
|
||||||
go.etcd.io/bbolt v1.3.5 h1:XAzx9gjCb0Rxj7EoqcClPD1d5ZBxZJk0jbuoPHenBt0=
|
|
||||||
go.etcd.io/bbolt v1.3.5/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ=
|
go.etcd.io/bbolt v1.3.5/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ=
|
||||||
|
go.etcd.io/bbolt v1.3.6 h1:/ecaJf0sk1l4l6V4awd65v2C3ILy7MSj+s/x1ADCIMU=
|
||||||
|
go.etcd.io/bbolt v1.3.6/go.mod h1:qXsaaIqmgQH0T+OPdb99Bf+PKfBBQVAdyD6TY9G8XM4=
|
||||||
go.etcd.io/etcd v0.0.0-20191023171146-3cf2f69b5738/go.mod h1:dnLIgRNXwCJa5e+c6mIZCrds/GIG4ncV9HhK5PX7jPg=
|
go.etcd.io/etcd v0.0.0-20191023171146-3cf2f69b5738/go.mod h1:dnLIgRNXwCJa5e+c6mIZCrds/GIG4ncV9HhK5PX7jPg=
|
||||||
go.mozilla.org/pkcs7 v0.0.0-20210730143726-725912489c62/go.mod h1:SNgMg+EgDFwmvSmLRTNKC5fegJjB7v23qTQ0XLGUNHk=
|
go.mozilla.org/pkcs7 v0.0.0-20210730143726-725912489c62/go.mod h1:SNgMg+EgDFwmvSmLRTNKC5fegJjB7v23qTQ0XLGUNHk=
|
||||||
go.mozilla.org/pkcs7 v0.0.0-20210826202110-33d05740a352 h1:CCriYyAfq1Br1aIYettdHZTy8mBTIPo7We18TuO/bak=
|
go.mozilla.org/pkcs7 v0.0.0-20210826202110-33d05740a352 h1:CCriYyAfq1Br1aIYettdHZTy8mBTIPo7We18TuO/bak=
|
||||||
|
@ -804,8 +815,10 @@ go.step.sm/cli-utils v0.7.0/go.mod h1:Ur6bqA/yl636kCUJbp30J7Unv5JJ226eW2KqXPDwF/
|
||||||
go.step.sm/crypto v0.9.0/go.mod h1:+CYG05Mek1YDqi5WK0ERc6cOpKly2i/a5aZmU1sfGj0=
|
go.step.sm/crypto v0.9.0/go.mod h1:+CYG05Mek1YDqi5WK0ERc6cOpKly2i/a5aZmU1sfGj0=
|
||||||
go.step.sm/crypto v0.16.1 h1:4mnZk21cSxyMGxsEpJwZKKvJvDu1PN09UVrWWFNUBdk=
|
go.step.sm/crypto v0.16.1 h1:4mnZk21cSxyMGxsEpJwZKKvJvDu1PN09UVrWWFNUBdk=
|
||||||
go.step.sm/crypto v0.16.1/go.mod h1:3G0yQr5lQqfEG0CMYz8apC/qMtjLRQlzflL2AxkcN+g=
|
go.step.sm/crypto v0.16.1/go.mod h1:3G0yQr5lQqfEG0CMYz8apC/qMtjLRQlzflL2AxkcN+g=
|
||||||
go.step.sm/linkedca v0.15.0 h1:lEkGRDY+u7FudGKt8yEo7nBy5OzceO9s3rl+/sZVL5M=
|
go.step.sm/linkedca v0.16.0 h1:9xdE150lRTEoBq1gJl+prePpSmNqXRXsez3qzRs3Lic=
|
||||||
go.step.sm/linkedca v0.15.0/go.mod h1:W59ucS4vFpuR0g4PtkGbbtXAwxbDEnNCg+ovkej1ANM=
|
go.step.sm/linkedca v0.16.0/go.mod h1:W59ucS4vFpuR0g4PtkGbbtXAwxbDEnNCg+ovkej1ANM=
|
||||||
|
go.step.sm/linkedca v0.16.1 h1:CdbMV5SjnlRsgeYTXaaZmQCkYIgJq8BOzpewri57M2k=
|
||||||
|
go.step.sm/linkedca v0.16.1/go.mod h1:W59ucS4vFpuR0g4PtkGbbtXAwxbDEnNCg+ovkej1ANM=
|
||||||
go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
|
go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
|
||||||
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
|
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
|
||||||
go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
|
go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
|
||||||
|
@ -927,8 +940,9 @@ golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qx
|
||||||
golang.org/x/net v0.0.0-20211020060615-d418f374d309/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-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||||
golang.org/x/net v0.0.0-20211216030914-fe4d6282115f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
golang.org/x/net v0.0.0-20211216030914-fe4d6282115f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||||
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd h1:O7DYs+zxREGLKzKoMQrtrEacpb0ZVXA5rIwylE2Xchk=
|
|
||||||
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
||||||
|
golang.org/x/net v0.0.0-20220403103023-749bd193bc2b h1:vI32FkLJNAWtGD4BwkThwEy6XS7ZLLMHkSkYfF8M0W0=
|
||||||
|
golang.org/x/net v0.0.0-20220403103023-749bd193bc2b/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
||||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
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-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||||
|
@ -1007,6 +1021,7 @@ golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||||
golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200923182605-d9f96fdee20d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20201015000850-e3ed0017c211/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20201015000850-e3ed0017c211/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20201018230417-eeed37f84f13/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20201018230417-eeed37f84f13/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
@ -1041,8 +1056,9 @@ golang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||||
golang.org/x/sys v0.0.0-20211210111614-af8b64212486/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20211210111614-af8b64212486/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220209214540-3681064d5158 h1:rm+CHSpPEEW2IsXUib1ThaHIjuBVZjxNgSKmBLFfD4c=
|
|
||||||
golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220405052023-b1e9470b6e64 h1:D1v9ucDTYBtbz5vNuBbAhIMAGhQhJ6Ym5ah3maMVNX4=
|
||||||
|
golang.org/x/sys v0.0.0-20220405052023-b1e9470b6e64/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
|
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
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 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY=
|
||||||
|
@ -1062,8 +1078,9 @@ golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxb
|
||||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||||
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||||
golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1 h1:NusfzzA6yGQ+ua51ck7E3omNUX/JuqbFSaRGqU8CcLI=
|
|
||||||
golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||||
|
golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba h1:O8mE0/t419eoIwhTFpKVkHiTs/Igowgfkj25AcZrtiE=
|
||||||
|
golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||||
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
@ -1108,8 +1125,10 @@ golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWc
|
||||||
golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||||
golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||||
golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||||
|
golang.org/x/tools v0.0.0-20200522201501-cb1345f3a375/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||||
golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||||
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||||
|
golang.org/x/tools v0.0.0-20200717024301-6ddee64345a6/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
||||||
golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
||||||
golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
||||||
golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
||||||
|
@ -1244,8 +1263,9 @@ google.golang.org/genproto v0.0.0-20211221195035-429b39de9b1c/go.mod h1:5CzLGKJ6
|
||||||
google.golang.org/genproto v0.0.0-20220126215142-9970aeb2e350/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
|
google.golang.org/genproto v0.0.0-20220126215142-9970aeb2e350/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
|
||||||
google.golang.org/genproto v0.0.0-20220207164111-0872dc986b00/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
|
google.golang.org/genproto v0.0.0-20220207164111-0872dc986b00/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
|
||||||
google.golang.org/genproto v0.0.0-20220218161850-94dd64e39d7c/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI=
|
google.golang.org/genproto v0.0.0-20220218161850-94dd64e39d7c/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI=
|
||||||
google.golang.org/genproto v0.0.0-20220222213610-43724f9ea8cf h1:SVYXkUz2yZS9FWb2Gm8ivSlbNQzL2Z/NpPKE3RG2jWk=
|
|
||||||
google.golang.org/genproto v0.0.0-20220222213610-43724f9ea8cf/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI=
|
google.golang.org/genproto v0.0.0-20220222213610-43724f9ea8cf/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI=
|
||||||
|
google.golang.org/genproto v0.0.0-20220401170504-314d38edb7de h1:9Ti5SG2U4cAcluryUo/sFay3TQKoxiFMfaT0pbizU7k=
|
||||||
|
google.golang.org/genproto v0.0.0-20220401170504-314d38edb7de/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo=
|
||||||
google.golang.org/grpc v1.8.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
|
google.golang.org/grpc v1.8.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
|
||||||
google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs=
|
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.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||||
|
@ -1279,8 +1299,9 @@ google.golang.org/grpc v1.39.1/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnD
|
||||||
google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34=
|
google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34=
|
||||||
google.golang.org/grpc v1.40.1/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34=
|
google.golang.org/grpc v1.40.1/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34=
|
||||||
google.golang.org/grpc v1.41.0/go.mod h1:U3l9uK9J0sini8mHphKoXyaqDA/8VyGnDee1zzIUK6k=
|
google.golang.org/grpc v1.41.0/go.mod h1:U3l9uK9J0sini8mHphKoXyaqDA/8VyGnDee1zzIUK6k=
|
||||||
google.golang.org/grpc v1.44.0 h1:weqSxi/TMs1SqFRMHCtBgXRs8k3X39QIDEZ0pRcttUg=
|
|
||||||
google.golang.org/grpc v1.44.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU=
|
google.golang.org/grpc v1.44.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU=
|
||||||
|
google.golang.org/grpc v1.45.0 h1:NEpgUqV3Z+ZjkqMsxMg11IaDrXY4RY6CQukSGK0uI1M=
|
||||||
|
google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ=
|
||||||
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw=
|
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-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||||
|
@ -1292,10 +1313,12 @@ google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2
|
||||||
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||||
google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
|
google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
|
||||||
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
|
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
|
||||||
|
google.golang.org/protobuf v1.25.1-0.20200805231151-a709e31e5d12/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
|
||||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||||
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||||
google.golang.org/protobuf v1.27.1 h1:SnqbnDw1V7RiZcXPx5MEeqPv2s79L9i7BJUlG/+RurQ=
|
|
||||||
google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||||
|
google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw=
|
||||||
|
google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
|
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
@ -1320,11 +1343,13 @@ gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
|
|
||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||||
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||||
|
|
301
policy/engine.go
Executable file
301
policy/engine.go
Executable file
|
@ -0,0 +1,301 @@
|
||||||
|
package policy
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/x509"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"net/url"
|
||||||
|
|
||||||
|
"golang.org/x/crypto/ssh"
|
||||||
|
|
||||||
|
"go.step.sm/crypto/x509util"
|
||||||
|
)
|
||||||
|
|
||||||
|
type NamePolicyReason int
|
||||||
|
|
||||||
|
const (
|
||||||
|
// NotAllowed results when an instance of NamePolicyEngine
|
||||||
|
// determines that there's a constraint which doesn't permit
|
||||||
|
// a DNS or another type of SAN to be signed (or otherwise used).
|
||||||
|
NotAllowed NamePolicyReason = iota + 1
|
||||||
|
// CannotParseDomain is returned when an error occurs
|
||||||
|
// when parsing the domain part of SAN or subject.
|
||||||
|
CannotParseDomain
|
||||||
|
// CannotParseRFC822Name is returned when an error
|
||||||
|
// occurs when parsing an email address.
|
||||||
|
CannotParseRFC822Name
|
||||||
|
// CannotMatch is the type of error returned when
|
||||||
|
// an error happens when matching SAN types.
|
||||||
|
CannotMatchNameToConstraint
|
||||||
|
)
|
||||||
|
|
||||||
|
type NameType string
|
||||||
|
|
||||||
|
const (
|
||||||
|
CNNameType NameType = "cn"
|
||||||
|
DNSNameType NameType = "dns"
|
||||||
|
IPNameType NameType = "ip"
|
||||||
|
EmailNameType NameType = "email"
|
||||||
|
URINameType NameType = "uri"
|
||||||
|
PrincipalNameType NameType = "principal"
|
||||||
|
)
|
||||||
|
|
||||||
|
type NamePolicyError struct {
|
||||||
|
Reason NamePolicyReason
|
||||||
|
NameType NameType
|
||||||
|
Name string
|
||||||
|
detail string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *NamePolicyError) Error() string {
|
||||||
|
switch e.Reason {
|
||||||
|
case NotAllowed:
|
||||||
|
return fmt.Sprintf("%s name %q not allowed", e.NameType, e.Name)
|
||||||
|
case CannotParseDomain:
|
||||||
|
return fmt.Sprintf("cannot parse %s domain %q", e.NameType, e.Name)
|
||||||
|
case CannotParseRFC822Name:
|
||||||
|
return fmt.Sprintf("cannot parse %s rfc822Name %q", e.NameType, e.Name)
|
||||||
|
case CannotMatchNameToConstraint:
|
||||||
|
return fmt.Sprintf("error matching %s name %q to constraint", e.NameType, e.Name)
|
||||||
|
default:
|
||||||
|
return fmt.Sprintf("unknown error reason (%d): %s", e.Reason, e.detail)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *NamePolicyError) Detail() string {
|
||||||
|
return e.detail
|
||||||
|
}
|
||||||
|
|
||||||
|
// NamePolicyEngine can be used to check that a CSR or Certificate meets all allowed and
|
||||||
|
// denied names before a CA creates and/or signs the Certificate.
|
||||||
|
// TODO(hs): the X509 RFC also defines name checks on directory name; support that?
|
||||||
|
// TODO(hs): implement Stringer interface: describe the contents of the NamePolicyEngine?
|
||||||
|
// TODO(hs): implement matching URI schemes, paths, etc; not just the domain part of URI domains
|
||||||
|
|
||||||
|
type NamePolicyEngine struct {
|
||||||
|
|
||||||
|
// verifySubjectCommonName is set when Subject Common Name must be verified
|
||||||
|
verifySubjectCommonName bool
|
||||||
|
// allowLiteralWildcardNames allows literal wildcard DNS domains
|
||||||
|
allowLiteralWildcardNames bool
|
||||||
|
|
||||||
|
// permitted and exluded constraints similar to x509 Name Constraints
|
||||||
|
permittedCommonNames []string
|
||||||
|
excludedCommonNames []string
|
||||||
|
permittedDNSDomains []string
|
||||||
|
excludedDNSDomains []string
|
||||||
|
permittedIPRanges []*net.IPNet
|
||||||
|
excludedIPRanges []*net.IPNet
|
||||||
|
permittedEmailAddresses []string
|
||||||
|
excludedEmailAddresses []string
|
||||||
|
permittedURIDomains []string
|
||||||
|
excludedURIDomains []string
|
||||||
|
permittedPrincipals []string
|
||||||
|
excludedPrincipals []string
|
||||||
|
|
||||||
|
// some internal counts for housekeeping
|
||||||
|
numberOfCommonNameConstraints int
|
||||||
|
numberOfDNSDomainConstraints int
|
||||||
|
numberOfIPRangeConstraints int
|
||||||
|
numberOfEmailAddressConstraints int
|
||||||
|
numberOfURIDomainConstraints int
|
||||||
|
numberOfPrincipalConstraints int
|
||||||
|
totalNumberOfPermittedConstraints int
|
||||||
|
totalNumberOfExcludedConstraints int
|
||||||
|
totalNumberOfConstraints int
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewNamePolicyEngine creates a new NamePolicyEngine with NamePolicyOptions
|
||||||
|
func New(opts ...NamePolicyOption) (*NamePolicyEngine, error) {
|
||||||
|
|
||||||
|
e := &NamePolicyEngine{}
|
||||||
|
for _, option := range opts {
|
||||||
|
if err := option(e); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
e.permittedCommonNames = removeDuplicates(e.permittedCommonNames)
|
||||||
|
e.permittedDNSDomains = removeDuplicates(e.permittedDNSDomains)
|
||||||
|
e.permittedIPRanges = removeDuplicateIPNets(e.permittedIPRanges)
|
||||||
|
e.permittedEmailAddresses = removeDuplicates(e.permittedEmailAddresses)
|
||||||
|
e.permittedURIDomains = removeDuplicates(e.permittedURIDomains)
|
||||||
|
e.permittedPrincipals = removeDuplicates(e.permittedPrincipals)
|
||||||
|
|
||||||
|
e.excludedCommonNames = removeDuplicates(e.excludedCommonNames)
|
||||||
|
e.excludedDNSDomains = removeDuplicates(e.excludedDNSDomains)
|
||||||
|
e.excludedIPRanges = removeDuplicateIPNets(e.excludedIPRanges)
|
||||||
|
e.excludedEmailAddresses = removeDuplicates(e.excludedEmailAddresses)
|
||||||
|
e.excludedURIDomains = removeDuplicates(e.excludedURIDomains)
|
||||||
|
e.excludedPrincipals = removeDuplicates(e.excludedPrincipals)
|
||||||
|
|
||||||
|
e.numberOfCommonNameConstraints = len(e.permittedCommonNames) + len(e.excludedCommonNames)
|
||||||
|
e.numberOfDNSDomainConstraints = len(e.permittedDNSDomains) + len(e.excludedDNSDomains)
|
||||||
|
e.numberOfIPRangeConstraints = len(e.permittedIPRanges) + len(e.excludedIPRanges)
|
||||||
|
e.numberOfEmailAddressConstraints = len(e.permittedEmailAddresses) + len(e.excludedEmailAddresses)
|
||||||
|
e.numberOfURIDomainConstraints = len(e.permittedURIDomains) + len(e.excludedURIDomains)
|
||||||
|
e.numberOfPrincipalConstraints = len(e.permittedPrincipals) + len(e.excludedPrincipals)
|
||||||
|
|
||||||
|
e.totalNumberOfPermittedConstraints = len(e.permittedCommonNames) + len(e.permittedDNSDomains) +
|
||||||
|
len(e.permittedIPRanges) + len(e.permittedEmailAddresses) + len(e.permittedURIDomains) +
|
||||||
|
len(e.permittedPrincipals)
|
||||||
|
|
||||||
|
e.totalNumberOfExcludedConstraints = len(e.excludedCommonNames) + len(e.excludedDNSDomains) +
|
||||||
|
len(e.excludedIPRanges) + len(e.excludedEmailAddresses) + len(e.excludedURIDomains) +
|
||||||
|
len(e.excludedPrincipals)
|
||||||
|
|
||||||
|
e.totalNumberOfConstraints = e.totalNumberOfPermittedConstraints + e.totalNumberOfExcludedConstraints
|
||||||
|
|
||||||
|
return e, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// removeDuplicates returns a new slice of strings with
|
||||||
|
// duplicate values removed. It retains the order of elements
|
||||||
|
// in the source slice.
|
||||||
|
func removeDuplicates(items []string) (ret []string) {
|
||||||
|
|
||||||
|
// no need to remove dupes; return original
|
||||||
|
if len(items) <= 1 {
|
||||||
|
return items
|
||||||
|
}
|
||||||
|
|
||||||
|
keys := make(map[string]struct{}, len(items))
|
||||||
|
|
||||||
|
ret = make([]string, 0, len(items))
|
||||||
|
for _, item := range items {
|
||||||
|
if _, ok := keys[item]; ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
keys[item] = struct{}{}
|
||||||
|
ret = append(ret, item)
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// removeDuplicateIPNets returns a new slice of net.IPNets with
|
||||||
|
// duplicate values removed. It retains the order of elements in
|
||||||
|
// the source slice. An IPNet is considered duplicate if its CIDR
|
||||||
|
// notation exists multiple times in the slice.
|
||||||
|
func removeDuplicateIPNets(items []*net.IPNet) (ret []*net.IPNet) {
|
||||||
|
|
||||||
|
// no need to remove dupes; return original
|
||||||
|
if len(items) <= 1 {
|
||||||
|
return items
|
||||||
|
}
|
||||||
|
|
||||||
|
keys := make(map[string]struct{}, len(items))
|
||||||
|
|
||||||
|
ret = make([]*net.IPNet, 0, len(items))
|
||||||
|
for _, item := range items {
|
||||||
|
key := item.String() // use CIDR notation as key
|
||||||
|
if _, ok := keys[key]; ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
keys[key] = struct{}{}
|
||||||
|
ret = append(ret, item)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO(hs): implement filter of fully overlapping ranges,
|
||||||
|
// so that the smaller ones are automatically removed?
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsX509CertificateAllowed verifies that all SANs in a Certificate are allowed.
|
||||||
|
func (e *NamePolicyEngine) IsX509CertificateAllowed(cert *x509.Certificate) error {
|
||||||
|
if err := e.validateNames(cert.DNSNames, cert.IPAddresses, cert.EmailAddresses, cert.URIs, []string{}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if e.verifySubjectCommonName {
|
||||||
|
return e.validateCommonName(cert.Subject.CommonName)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsX509CertificateRequestAllowed verifies that all names in the CSR are allowed.
|
||||||
|
func (e *NamePolicyEngine) IsX509CertificateRequestAllowed(csr *x509.CertificateRequest) error {
|
||||||
|
if err := e.validateNames(csr.DNSNames, csr.IPAddresses, csr.EmailAddresses, csr.URIs, []string{}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if e.verifySubjectCommonName {
|
||||||
|
return e.validateCommonName(csr.Subject.CommonName)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// AreSANSAllowed verifies that all names in the slice of SANs are allowed.
|
||||||
|
// The SANs are first split into DNS names, IPs, email addresses and URIs.
|
||||||
|
func (e *NamePolicyEngine) AreSANsAllowed(sans []string) error {
|
||||||
|
dnsNames, ips, emails, uris := x509util.SplitSANs(sans)
|
||||||
|
if err := e.validateNames(dnsNames, ips, emails, uris, []string{}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsDNSAllowed verifies a single DNS domain is allowed.
|
||||||
|
func (e *NamePolicyEngine) IsDNSAllowed(dns string) error {
|
||||||
|
if err := e.validateNames([]string{dns}, []net.IP{}, []string{}, []*url.URL{}, []string{}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsIPAllowed verifies a single IP domain is allowed.
|
||||||
|
func (e *NamePolicyEngine) IsIPAllowed(ip net.IP) error {
|
||||||
|
if err := e.validateNames([]string{}, []net.IP{ip}, []string{}, []*url.URL{}, []string{}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsSSHCertificateAllowed verifies that all principals in an SSH certificate are allowed.
|
||||||
|
func (e *NamePolicyEngine) IsSSHCertificateAllowed(cert *ssh.Certificate) error {
|
||||||
|
dnsNames, ips, emails, principals, err := splitSSHPrincipals(cert)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := e.validateNames(dnsNames, ips, emails, []*url.URL{}, principals); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// splitPrincipals splits SSH certificate principals into DNS names, emails and usernames.
|
||||||
|
func splitSSHPrincipals(cert *ssh.Certificate) (dnsNames []string, ips []net.IP, emails, principals []string, err error) {
|
||||||
|
dnsNames = []string{}
|
||||||
|
ips = []net.IP{}
|
||||||
|
emails = []string{}
|
||||||
|
principals = []string{}
|
||||||
|
var uris []*url.URL
|
||||||
|
switch cert.CertType {
|
||||||
|
case ssh.HostCert:
|
||||||
|
dnsNames, ips, emails, uris = x509util.SplitSANs(cert.ValidPrincipals)
|
||||||
|
if len(uris) > 0 {
|
||||||
|
err = fmt.Errorf("URL principals %v not expected in SSH host certificate ", uris)
|
||||||
|
}
|
||||||
|
case ssh.UserCert:
|
||||||
|
// re-using SplitSANs results in anything that can't be parsed as an IP, URI or email
|
||||||
|
// to be considered a username principal. This allows usernames like h.slatman to be present
|
||||||
|
// in the SSH certificate. We're exluding URIs, because they can be confusing
|
||||||
|
// when used in a SSH user certificate.
|
||||||
|
principals, ips, emails, uris = x509util.SplitSANs(cert.ValidPrincipals)
|
||||||
|
if len(ips) > 0 {
|
||||||
|
err = fmt.Errorf("IP principals %v not expected in SSH user certificate ", ips)
|
||||||
|
}
|
||||||
|
if len(uris) > 0 {
|
||||||
|
err = fmt.Errorf("URL principals %v not expected in SSH user certificate ", uris)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
err = fmt.Errorf("unexpected SSH certificate type %d", cert.CertType)
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
3401
policy/engine_test.go
Executable file
3401
policy/engine_test.go
Executable file
File diff suppressed because it is too large
Load diff
386
policy/options.go
Executable file
386
policy/options.go
Executable file
|
@ -0,0 +1,386 @@
|
||||||
|
package policy
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"golang.org/x/net/idna"
|
||||||
|
)
|
||||||
|
|
||||||
|
type NamePolicyOption func(e *NamePolicyEngine) error
|
||||||
|
|
||||||
|
// TODO: wrap (more) errors; and prove a set of known (exported) errors
|
||||||
|
|
||||||
|
func WithSubjectCommonNameVerification() NamePolicyOption {
|
||||||
|
return func(e *NamePolicyEngine) error {
|
||||||
|
e.verifySubjectCommonName = true
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithAllowLiteralWildcardNames() NamePolicyOption {
|
||||||
|
return func(e *NamePolicyEngine) error {
|
||||||
|
e.allowLiteralWildcardNames = true
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithPermittedCommonNames(commonNames ...string) NamePolicyOption {
|
||||||
|
return func(g *NamePolicyEngine) error {
|
||||||
|
normalizedCommonNames := make([]string, len(commonNames))
|
||||||
|
for i, commonName := range commonNames {
|
||||||
|
normalizedCommonName, err := normalizeAndValidateCommonName(commonName)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot parse permitted common name constraint %q: %w", commonName, err)
|
||||||
|
}
|
||||||
|
normalizedCommonNames[i] = normalizedCommonName
|
||||||
|
}
|
||||||
|
g.permittedCommonNames = normalizedCommonNames
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithExcludedCommonNames(commonNames ...string) NamePolicyOption {
|
||||||
|
return func(g *NamePolicyEngine) error {
|
||||||
|
normalizedCommonNames := make([]string, len(commonNames))
|
||||||
|
for i, commonName := range commonNames {
|
||||||
|
normalizedCommonName, err := normalizeAndValidateCommonName(commonName)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot parse excluded common name constraint %q: %w", commonName, err)
|
||||||
|
}
|
||||||
|
normalizedCommonNames[i] = normalizedCommonName
|
||||||
|
}
|
||||||
|
g.excludedCommonNames = normalizedCommonNames
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithPermittedDNSDomains(domains ...string) NamePolicyOption {
|
||||||
|
return func(e *NamePolicyEngine) error {
|
||||||
|
normalizedDomains := make([]string, len(domains))
|
||||||
|
for i, domain := range domains {
|
||||||
|
normalizedDomain, err := normalizeAndValidateDNSDomainConstraint(domain)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot parse permitted domain constraint %q: %w", domain, err)
|
||||||
|
}
|
||||||
|
normalizedDomains[i] = normalizedDomain
|
||||||
|
}
|
||||||
|
e.permittedDNSDomains = normalizedDomains
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithExcludedDNSDomains(domains ...string) NamePolicyOption {
|
||||||
|
return func(e *NamePolicyEngine) error {
|
||||||
|
normalizedDomains := make([]string, len(domains))
|
||||||
|
for i, domain := range domains {
|
||||||
|
normalizedDomain, err := normalizeAndValidateDNSDomainConstraint(domain)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot parse excluded domain constraint %q: %w", domain, err)
|
||||||
|
}
|
||||||
|
normalizedDomains[i] = normalizedDomain
|
||||||
|
}
|
||||||
|
e.excludedDNSDomains = normalizedDomains
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithPermittedIPRanges(ipRanges ...*net.IPNet) NamePolicyOption {
|
||||||
|
return func(e *NamePolicyEngine) error {
|
||||||
|
e.permittedIPRanges = ipRanges
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithPermittedCIDRs(cidrs ...string) NamePolicyOption {
|
||||||
|
return func(e *NamePolicyEngine) error {
|
||||||
|
networks := make([]*net.IPNet, len(cidrs))
|
||||||
|
for i, cidr := range cidrs {
|
||||||
|
_, nw, err := net.ParseCIDR(cidr)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot parse permitted CIDR constraint %q", cidr)
|
||||||
|
}
|
||||||
|
networks[i] = nw
|
||||||
|
}
|
||||||
|
e.permittedIPRanges = networks
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithExcludedCIDRs(cidrs ...string) NamePolicyOption {
|
||||||
|
return func(e *NamePolicyEngine) error {
|
||||||
|
networks := make([]*net.IPNet, len(cidrs))
|
||||||
|
for i, cidr := range cidrs {
|
||||||
|
_, nw, err := net.ParseCIDR(cidr)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot parse excluded CIDR constraint %q", cidr)
|
||||||
|
}
|
||||||
|
networks[i] = nw
|
||||||
|
}
|
||||||
|
e.excludedIPRanges = networks
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithPermittedIPsOrCIDRs(ipsOrCIDRs ...string) NamePolicyOption {
|
||||||
|
return func(e *NamePolicyEngine) error {
|
||||||
|
networks := make([]*net.IPNet, len(ipsOrCIDRs))
|
||||||
|
for i, ipOrCIDR := range ipsOrCIDRs {
|
||||||
|
_, nw, err := net.ParseCIDR(ipOrCIDR)
|
||||||
|
if err == nil {
|
||||||
|
networks[i] = nw
|
||||||
|
} else if ip := net.ParseIP(ipOrCIDR); ip != nil {
|
||||||
|
networks[i] = networkFor(ip)
|
||||||
|
} else {
|
||||||
|
return fmt.Errorf("cannot parse permitted constraint %q as IP nor CIDR", ipOrCIDR)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
e.permittedIPRanges = networks
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithExcludedIPsOrCIDRs(ipsOrCIDRs ...string) NamePolicyOption {
|
||||||
|
return func(e *NamePolicyEngine) error {
|
||||||
|
networks := make([]*net.IPNet, len(ipsOrCIDRs))
|
||||||
|
for i, ipOrCIDR := range ipsOrCIDRs {
|
||||||
|
_, nw, err := net.ParseCIDR(ipOrCIDR)
|
||||||
|
if err == nil {
|
||||||
|
networks[i] = nw
|
||||||
|
} else if ip := net.ParseIP(ipOrCIDR); ip != nil {
|
||||||
|
networks[i] = networkFor(ip)
|
||||||
|
} else {
|
||||||
|
return fmt.Errorf("cannot parse excluded constraint %q as IP nor CIDR", ipOrCIDR)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
e.excludedIPRanges = networks
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithExcludedIPRanges(ipRanges ...*net.IPNet) NamePolicyOption {
|
||||||
|
return func(e *NamePolicyEngine) error {
|
||||||
|
e.excludedIPRanges = ipRanges
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithPermittedEmailAddresses(emailAddresses ...string) NamePolicyOption {
|
||||||
|
return func(e *NamePolicyEngine) error {
|
||||||
|
normalizedEmailAddresses := make([]string, len(emailAddresses))
|
||||||
|
for i, email := range emailAddresses {
|
||||||
|
normalizedEmailAddress, err := normalizeAndValidateEmailConstraint(email)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot parse permitted email constraint %q: %w", email, err)
|
||||||
|
}
|
||||||
|
normalizedEmailAddresses[i] = normalizedEmailAddress
|
||||||
|
}
|
||||||
|
e.permittedEmailAddresses = normalizedEmailAddresses
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithExcludedEmailAddresses(emailAddresses ...string) NamePolicyOption {
|
||||||
|
return func(e *NamePolicyEngine) error {
|
||||||
|
normalizedEmailAddresses := make([]string, len(emailAddresses))
|
||||||
|
for i, email := range emailAddresses {
|
||||||
|
normalizedEmailAddress, err := normalizeAndValidateEmailConstraint(email)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot parse excluded email constraint %q: %w", email, err)
|
||||||
|
}
|
||||||
|
normalizedEmailAddresses[i] = normalizedEmailAddress
|
||||||
|
}
|
||||||
|
e.excludedEmailAddresses = normalizedEmailAddresses
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithPermittedURIDomains(uriDomains ...string) NamePolicyOption {
|
||||||
|
return func(e *NamePolicyEngine) error {
|
||||||
|
normalizedURIDomains := make([]string, len(uriDomains))
|
||||||
|
for i, domain := range uriDomains {
|
||||||
|
normalizedURIDomain, err := normalizeAndValidateURIDomainConstraint(domain)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot parse permitted URI domain constraint %q: %w", domain, err)
|
||||||
|
}
|
||||||
|
normalizedURIDomains[i] = normalizedURIDomain
|
||||||
|
}
|
||||||
|
e.permittedURIDomains = normalizedURIDomains
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithExcludedURIDomains(domains ...string) NamePolicyOption {
|
||||||
|
return func(e *NamePolicyEngine) error {
|
||||||
|
normalizedURIDomains := make([]string, len(domains))
|
||||||
|
for i, domain := range domains {
|
||||||
|
normalizedURIDomain, err := normalizeAndValidateURIDomainConstraint(domain)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot parse excluded URI domain constraint %q: %w", domain, err)
|
||||||
|
}
|
||||||
|
normalizedURIDomains[i] = normalizedURIDomain
|
||||||
|
}
|
||||||
|
e.excludedURIDomains = normalizedURIDomains
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithPermittedPrincipals(principals ...string) NamePolicyOption {
|
||||||
|
return func(g *NamePolicyEngine) error {
|
||||||
|
g.permittedPrincipals = principals
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithExcludedPrincipals(principals ...string) NamePolicyOption {
|
||||||
|
return func(g *NamePolicyEngine) error {
|
||||||
|
g.excludedPrincipals = principals
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func networkFor(ip net.IP) *net.IPNet {
|
||||||
|
var mask net.IPMask
|
||||||
|
if !isIPv4(ip) {
|
||||||
|
mask = net.CIDRMask(128, 128)
|
||||||
|
} else {
|
||||||
|
mask = net.CIDRMask(32, 32)
|
||||||
|
}
|
||||||
|
nw := &net.IPNet{
|
||||||
|
IP: ip,
|
||||||
|
Mask: mask,
|
||||||
|
}
|
||||||
|
return nw
|
||||||
|
}
|
||||||
|
|
||||||
|
func isIPv4(ip net.IP) bool {
|
||||||
|
return ip.To4() != nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizeAndValidateCommonName(constraint string) (string, error) {
|
||||||
|
normalizedConstraint := strings.ToLower(strings.TrimSpace(constraint))
|
||||||
|
if normalizedConstraint == "" {
|
||||||
|
return "", fmt.Errorf("contraint %q can not be empty or white space string", constraint)
|
||||||
|
}
|
||||||
|
if normalizedConstraint == "*" {
|
||||||
|
return "", fmt.Errorf("wildcard constraint %q is not supported", constraint)
|
||||||
|
}
|
||||||
|
return normalizedConstraint, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizeAndValidateDNSDomainConstraint(constraint string) (string, error) {
|
||||||
|
normalizedConstraint := strings.ToLower(strings.TrimSpace(constraint))
|
||||||
|
if normalizedConstraint == "" {
|
||||||
|
return "", fmt.Errorf("contraint %q can not be empty or white space string", constraint)
|
||||||
|
}
|
||||||
|
if strings.Contains(normalizedConstraint, "..") {
|
||||||
|
return "", fmt.Errorf("domain constraint %q cannot have empty labels", constraint)
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(normalizedConstraint, ".") {
|
||||||
|
return "", fmt.Errorf("domain constraint %q with wildcard should start with *", constraint)
|
||||||
|
}
|
||||||
|
if strings.LastIndex(normalizedConstraint, "*") > 0 {
|
||||||
|
return "", fmt.Errorf("domain constraint %q can only have wildcard as starting character", constraint)
|
||||||
|
}
|
||||||
|
if len(normalizedConstraint) >= 2 && normalizedConstraint[0] == '*' && normalizedConstraint[1] != '.' {
|
||||||
|
return "", fmt.Errorf("wildcard character in domain constraint %q can only be used to match (full) labels", constraint)
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(normalizedConstraint, "*.") {
|
||||||
|
normalizedConstraint = normalizedConstraint[1:] // cut off wildcard character; keep the period
|
||||||
|
}
|
||||||
|
normalizedConstraint, err := idna.Lookup.ToASCII(normalizedConstraint)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("domain constraint %q can not be converted to ASCII: %w", constraint, err)
|
||||||
|
}
|
||||||
|
if _, ok := domainToReverseLabels(normalizedConstraint); !ok {
|
||||||
|
return "", fmt.Errorf("cannot parse domain constraint %q", constraint)
|
||||||
|
}
|
||||||
|
return normalizedConstraint, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizeAndValidateEmailConstraint(constraint string) (string, error) {
|
||||||
|
normalizedConstraint := strings.ToLower(strings.TrimSpace(constraint))
|
||||||
|
if normalizedConstraint == "" {
|
||||||
|
return "", fmt.Errorf("email contraint %q can not be empty or white space string", constraint)
|
||||||
|
}
|
||||||
|
if strings.Contains(normalizedConstraint, "*") {
|
||||||
|
return "", fmt.Errorf("email constraint %q cannot contain asterisk wildcard", constraint)
|
||||||
|
}
|
||||||
|
if strings.Count(normalizedConstraint, "@") > 1 {
|
||||||
|
return "", fmt.Errorf("email constraint %q contains too many @ characters", constraint)
|
||||||
|
}
|
||||||
|
if normalizedConstraint[0] == '@' {
|
||||||
|
normalizedConstraint = normalizedConstraint[1:] // remove the leading @ as wildcard for emails
|
||||||
|
}
|
||||||
|
if normalizedConstraint[0] == '.' {
|
||||||
|
return "", fmt.Errorf("email constraint %q cannot start with period", constraint)
|
||||||
|
}
|
||||||
|
if strings.Contains(normalizedConstraint, "@") {
|
||||||
|
mailbox, ok := parseRFC2821Mailbox(normalizedConstraint)
|
||||||
|
if !ok {
|
||||||
|
return "", fmt.Errorf("cannot parse email constraint %q as RFC 2821 mailbox", constraint)
|
||||||
|
}
|
||||||
|
// According to RFC 5280, section 7.5, emails are considered to match if the local part is
|
||||||
|
// an exact match and the host (domain) part matches the ASCII representation (case-insensitive):
|
||||||
|
// https://datatracker.ietf.org/doc/html/rfc5280#section-7.5
|
||||||
|
domainASCII, err := idna.Lookup.ToASCII(mailbox.domain)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("email constraint %q domain part %q cannot be converted to ASCII: %w", constraint, mailbox.domain, err)
|
||||||
|
}
|
||||||
|
normalizedConstraint = mailbox.local + "@" + domainASCII
|
||||||
|
} else {
|
||||||
|
var err error
|
||||||
|
normalizedConstraint, err = idna.Lookup.ToASCII(normalizedConstraint)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("email constraint %q cannot be converted to ASCII: %w", constraint, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if _, ok := domainToReverseLabels(normalizedConstraint); !ok {
|
||||||
|
return "", fmt.Errorf("cannot parse email domain constraint %q", constraint)
|
||||||
|
}
|
||||||
|
return normalizedConstraint, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizeAndValidateURIDomainConstraint(constraint string) (string, error) {
|
||||||
|
normalizedConstraint := strings.ToLower(strings.TrimSpace(constraint))
|
||||||
|
if normalizedConstraint == "" {
|
||||||
|
return "", fmt.Errorf("URI domain contraint %q cannot be empty or white space string", constraint)
|
||||||
|
}
|
||||||
|
if strings.Contains(normalizedConstraint, "://") {
|
||||||
|
return "", fmt.Errorf("URI domain constraint %q contains scheme (not supported yet)", constraint)
|
||||||
|
}
|
||||||
|
if strings.Contains(normalizedConstraint, "..") {
|
||||||
|
return "", fmt.Errorf("URI domain constraint %q cannot have empty labels", constraint)
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(normalizedConstraint, ".") {
|
||||||
|
return "", fmt.Errorf("URI domain constraint %q with wildcard should start with *", constraint)
|
||||||
|
}
|
||||||
|
if strings.LastIndex(normalizedConstraint, "*") > 0 {
|
||||||
|
return "", fmt.Errorf("URI domain constraint %q can only have wildcard as starting character", constraint)
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(normalizedConstraint, "*.") {
|
||||||
|
normalizedConstraint = normalizedConstraint[1:] // cut off wildcard character; keep the period
|
||||||
|
}
|
||||||
|
// we're being strict with square brackets in domains; we don't allow them, no matter what
|
||||||
|
if strings.Contains(normalizedConstraint, "[") || strings.Contains(normalizedConstraint, "]") {
|
||||||
|
return "", fmt.Errorf("URI domain constraint %q contains invalid square brackets", constraint)
|
||||||
|
}
|
||||||
|
if _, _, err := net.SplitHostPort(normalizedConstraint); err == nil {
|
||||||
|
// a successful split (likely) with host and port; we don't currently allow ports in the config
|
||||||
|
return "", fmt.Errorf("URI domain constraint %q cannot contain port", constraint)
|
||||||
|
}
|
||||||
|
// check if the host part of the URI domain constraint is an IP
|
||||||
|
if net.ParseIP(normalizedConstraint) != nil {
|
||||||
|
return "", fmt.Errorf("URI domain constraint %q cannot be an IP", constraint)
|
||||||
|
}
|
||||||
|
normalizedConstraint, err := idna.Lookup.ToASCII(normalizedConstraint)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("URI domain constraint %q cannot be converted to ASCII: %w", constraint, err)
|
||||||
|
}
|
||||||
|
_, ok := domainToReverseLabels(normalizedConstraint)
|
||||||
|
if !ok {
|
||||||
|
return "", fmt.Errorf("cannot parse URI domain constraint %q", constraint)
|
||||||
|
}
|
||||||
|
return normalizedConstraint, nil
|
||||||
|
}
|
125
policy/options_117_test.go
Normal file
125
policy/options_117_test.go
Normal file
|
@ -0,0 +1,125 @@
|
||||||
|
//go:build !go1.18
|
||||||
|
// +build !go1.18
|
||||||
|
|
||||||
|
package policy
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func Test_normalizeAndValidateURIDomainConstraint(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
constraint string
|
||||||
|
want string
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "fail/empty-constraint",
|
||||||
|
constraint: "",
|
||||||
|
want: "",
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "fail/scheme-https",
|
||||||
|
constraint: `https://*.local`,
|
||||||
|
want: "",
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "fail/too-many-asterisks",
|
||||||
|
constraint: "**.local",
|
||||||
|
want: "",
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "fail/empty-label",
|
||||||
|
constraint: "..local",
|
||||||
|
want: "",
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "fail/empty-reverse",
|
||||||
|
constraint: ".",
|
||||||
|
want: "",
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "fail/no-asterisk",
|
||||||
|
constraint: ".example.com",
|
||||||
|
want: "",
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "fail/domain-with-port",
|
||||||
|
constraint: "host.local:8443",
|
||||||
|
want: "",
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "fail/ipv4",
|
||||||
|
constraint: "127.0.0.1",
|
||||||
|
want: "",
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "fail/ipv6-brackets",
|
||||||
|
constraint: "[::1]",
|
||||||
|
want: "",
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "fail/ipv6-no-brackets",
|
||||||
|
constraint: "::1",
|
||||||
|
want: "",
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "fail/ipv6-no-brackets",
|
||||||
|
constraint: "[::1",
|
||||||
|
want: "",
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "fail/idna-internationalized-domain-name-lookup",
|
||||||
|
constraint: `\00local`,
|
||||||
|
want: "",
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ok/wildcard",
|
||||||
|
constraint: "*.local",
|
||||||
|
want: ".local",
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ok/specific-domain",
|
||||||
|
constraint: "example.local",
|
||||||
|
want: "example.local",
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ok/idna-internationalized-domain-name-lookup",
|
||||||
|
constraint: `*.bücher.example.com`,
|
||||||
|
want: ".xn--bcher-kva.example.com",
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// IDNA2003 vs. 2008 deviation: https://unicode.org/reports/tr46/#Deviations results
|
||||||
|
// in a difference between Go 1.18 and lower versions. Go 1.18 expects ".xn--fa-hia.de"; not .fass.de.
|
||||||
|
name: "ok/idna-internationalized-domain-name-lookup-deviation",
|
||||||
|
constraint: `*.faß.de`,
|
||||||
|
want: ".fass.de",
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
got, err := normalizeAndValidateURIDomainConstraint(tt.constraint)
|
||||||
|
if (err != nil) != tt.wantErr {
|
||||||
|
t.Errorf("normalizeAndValidateURIDomainConstraint() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
|
}
|
||||||
|
if got != tt.want {
|
||||||
|
t.Errorf("normalizeAndValidateURIDomainConstraint() = %v, want %v", got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
125
policy/options_118_test.go
Normal file
125
policy/options_118_test.go
Normal file
|
@ -0,0 +1,125 @@
|
||||||
|
//go:build go1.18
|
||||||
|
// +build go1.18
|
||||||
|
|
||||||
|
package policy
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func Test_normalizeAndValidateURIDomainConstraint(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
constraint string
|
||||||
|
want string
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "fail/empty-constraint",
|
||||||
|
constraint: "",
|
||||||
|
want: "",
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "fail/scheme-https",
|
||||||
|
constraint: `https://*.local`,
|
||||||
|
want: "",
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "fail/too-many-asterisks",
|
||||||
|
constraint: "**.local",
|
||||||
|
want: "",
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "fail/empty-label",
|
||||||
|
constraint: "..local",
|
||||||
|
want: "",
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "fail/empty-reverse",
|
||||||
|
constraint: ".",
|
||||||
|
want: "",
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "fail/domain-with-port",
|
||||||
|
constraint: "host.local:8443",
|
||||||
|
want: "",
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "fail/no-asterisk",
|
||||||
|
constraint: ".example.com",
|
||||||
|
want: "",
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "fail/ipv4",
|
||||||
|
constraint: "127.0.0.1",
|
||||||
|
want: "",
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "fail/ipv6-brackets",
|
||||||
|
constraint: "[::1]",
|
||||||
|
want: "",
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "fail/ipv6-no-brackets",
|
||||||
|
constraint: "::1",
|
||||||
|
want: "",
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "fail/ipv6-no-brackets",
|
||||||
|
constraint: "[::1",
|
||||||
|
want: "",
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "fail/idna-internationalized-domain-name-lookup",
|
||||||
|
constraint: `\00local`,
|
||||||
|
want: "",
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ok/wildcard",
|
||||||
|
constraint: "*.local",
|
||||||
|
want: ".local",
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ok/specific-domain",
|
||||||
|
constraint: "example.local",
|
||||||
|
want: "example.local",
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ok/idna-internationalized-domain-name-lookup",
|
||||||
|
constraint: `*.bücher.example.com`,
|
||||||
|
want: ".xn--bcher-kva.example.com",
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// IDNA2003 vs. 2008 deviation: https://unicode.org/reports/tr46/#Deviations results
|
||||||
|
// in a difference between Go 1.18 and lower versions. Go 1.18 expects ".xn--fa-hia.de"; not .fass.de.
|
||||||
|
name: "ok/idna-internationalized-domain-name-lookup-deviation",
|
||||||
|
constraint: `*.faß.de`,
|
||||||
|
want: ".xn--fa-hia.de",
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
got, err := normalizeAndValidateURIDomainConstraint(tt.constraint)
|
||||||
|
if (err != nil) != tt.wantErr {
|
||||||
|
t.Errorf("normalizeAndValidateURIDomainConstraint() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
|
}
|
||||||
|
if got != tt.want {
|
||||||
|
t.Errorf("normalizeAndValidateURIDomainConstraint() = %v, want %v", got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
660
policy/options_test.go
Normal file
660
policy/options_test.go
Normal file
|
@ -0,0 +1,660 @@
|
||||||
|
package policy
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/google/go-cmp/cmp"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Test_normalizeAndValidateCommonName(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
constraint string
|
||||||
|
want string
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "fail/empty-constraint",
|
||||||
|
constraint: "",
|
||||||
|
want: "",
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "fail/wildcard",
|
||||||
|
constraint: "*",
|
||||||
|
want: "",
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ok",
|
||||||
|
constraint: "step",
|
||||||
|
want: "step",
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
got, err := normalizeAndValidateCommonName(tt.constraint)
|
||||||
|
if (err != nil) != tt.wantErr {
|
||||||
|
t.Errorf("normalizeAndValidateCommonName() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if got != tt.want {
|
||||||
|
t.Errorf("normalizeAndValidateCommonName() = %v, want %v", got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_normalizeAndValidateDNSDomainConstraint(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
constraint string
|
||||||
|
want string
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "fail/empty-constraint",
|
||||||
|
constraint: "",
|
||||||
|
want: "",
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "fail/wildcard-partial-label",
|
||||||
|
constraint: "*xxxx.local",
|
||||||
|
want: "",
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "fail/wildcard-in-the-middle",
|
||||||
|
constraint: "x.*.local",
|
||||||
|
want: "",
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "fail/empty-label",
|
||||||
|
constraint: "..local",
|
||||||
|
want: "",
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "fail/empty-reverse",
|
||||||
|
constraint: ".",
|
||||||
|
want: "",
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "fail/no-asterisk",
|
||||||
|
constraint: ".example.com",
|
||||||
|
want: "",
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "fail/idna-internationalized-domain-name-lookup",
|
||||||
|
constraint: `\00.local`, // invalid IDNA ASCII character
|
||||||
|
want: "",
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ok/wildcard",
|
||||||
|
constraint: "*.local",
|
||||||
|
want: ".local",
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ok/specific-domain",
|
||||||
|
constraint: "example.local",
|
||||||
|
want: "example.local",
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ok/idna-internationalized-domain-name-punycode",
|
||||||
|
constraint: "*.xn--fsq.jp", // Example value from https://www.w3.org/International/articles/idn-and-iri/
|
||||||
|
want: ".xn--fsq.jp",
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ok/idna-internationalized-domain-name-lookup-transformed",
|
||||||
|
constraint: "*.例.jp", // Example value from https://www.w3.org/International/articles/idn-and-iri/
|
||||||
|
want: ".xn--fsq.jp",
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
got, err := normalizeAndValidateDNSDomainConstraint(tt.constraint)
|
||||||
|
if (err != nil) != tt.wantErr {
|
||||||
|
t.Errorf("normalizeAndValidateDNSDomainConstraint() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
|
}
|
||||||
|
if got != tt.want {
|
||||||
|
t.Errorf("normalizeAndValidateDNSDomainConstraint() = %v, want %v", got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_normalizeAndValidateEmailConstraint(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
constraint string
|
||||||
|
want string
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "fail/empty-constraint",
|
||||||
|
constraint: "",
|
||||||
|
want: "",
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "fail/asterisk",
|
||||||
|
constraint: "*.local",
|
||||||
|
want: "",
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "fail/period",
|
||||||
|
constraint: ".local",
|
||||||
|
want: "",
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "fail/@period",
|
||||||
|
constraint: "@.local",
|
||||||
|
want: "",
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "fail/too-many-@s",
|
||||||
|
constraint: "@local@example.com",
|
||||||
|
want: "",
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "fail/parse-mailbox",
|
||||||
|
constraint: "mail@example.com" + string(byte(0)),
|
||||||
|
want: "",
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "fail/idna-internationalized-domain",
|
||||||
|
constraint: "mail@xn--bla.local",
|
||||||
|
want: "",
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "fail/idna-internationalized-domain-name-lookup",
|
||||||
|
constraint: `\00local`,
|
||||||
|
want: "",
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "fail/parse-domain",
|
||||||
|
constraint: "x..example.com",
|
||||||
|
want: "",
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ok/wildcard",
|
||||||
|
constraint: "@local",
|
||||||
|
want: "local",
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ok/specific-mail",
|
||||||
|
constraint: "mail@local",
|
||||||
|
want: "mail@local",
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
// TODO(hs): fix the below; doesn't get past parseRFC2821Mailbox; I think it should be allowed.
|
||||||
|
// {
|
||||||
|
// name: "ok/idna-internationalized-local",
|
||||||
|
// constraint: `bücher@local`,
|
||||||
|
// want: "bücher@local",
|
||||||
|
// wantErr: false,
|
||||||
|
// },
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
got, err := normalizeAndValidateEmailConstraint(tt.constraint)
|
||||||
|
if (err != nil) != tt.wantErr {
|
||||||
|
t.Errorf("normalizeAndValidateEmailConstraint() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
|
}
|
||||||
|
if got != tt.want {
|
||||||
|
t.Errorf("normalizeAndValidateEmailConstraint() = %v, want %v", got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNew(t *testing.T) {
|
||||||
|
type test struct {
|
||||||
|
options []NamePolicyOption
|
||||||
|
want *NamePolicyEngine
|
||||||
|
wantErr bool
|
||||||
|
}
|
||||||
|
var tests = map[string]func(t *testing.T) test{
|
||||||
|
"fail/with-permitted-common-name": func(t *testing.T) test {
|
||||||
|
return test{
|
||||||
|
options: []NamePolicyOption{
|
||||||
|
WithPermittedCommonNames("*"),
|
||||||
|
},
|
||||||
|
want: nil,
|
||||||
|
wantErr: true,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"fail/with-excluded-common-name": func(t *testing.T) test {
|
||||||
|
return test{
|
||||||
|
options: []NamePolicyOption{
|
||||||
|
WithExcludedCommonNames(""),
|
||||||
|
},
|
||||||
|
want: nil,
|
||||||
|
wantErr: true,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"fail/with-permitted-dns-domains": func(t *testing.T) test {
|
||||||
|
return test{
|
||||||
|
options: []NamePolicyOption{
|
||||||
|
WithPermittedDNSDomains("**.local"),
|
||||||
|
},
|
||||||
|
want: nil,
|
||||||
|
wantErr: true,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"fail/with-excluded-dns-domains": func(t *testing.T) test {
|
||||||
|
return test{
|
||||||
|
options: []NamePolicyOption{
|
||||||
|
WithExcludedDNSDomains("**.local"),
|
||||||
|
},
|
||||||
|
want: nil,
|
||||||
|
wantErr: true,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"fail/with-permitted-cidrs": func(t *testing.T) test {
|
||||||
|
return test{
|
||||||
|
options: []NamePolicyOption{
|
||||||
|
WithPermittedCIDRs("127.0.0.1//24"),
|
||||||
|
},
|
||||||
|
want: nil,
|
||||||
|
wantErr: true,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"fail/with-excluded-cidrs": func(t *testing.T) test {
|
||||||
|
return test{
|
||||||
|
options: []NamePolicyOption{
|
||||||
|
WithExcludedCIDRs("127.0.0.1//24"),
|
||||||
|
},
|
||||||
|
want: nil,
|
||||||
|
wantErr: true,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"fail/with-permitted-ipsOrCIDRs-cidr": func(t *testing.T) test {
|
||||||
|
return test{
|
||||||
|
options: []NamePolicyOption{
|
||||||
|
WithPermittedIPsOrCIDRs("127.0.0.1//24"),
|
||||||
|
},
|
||||||
|
want: nil,
|
||||||
|
wantErr: true,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"fail/with-permitted-ipsOrCIDRs-ip": func(t *testing.T) test {
|
||||||
|
return test{
|
||||||
|
options: []NamePolicyOption{
|
||||||
|
WithPermittedIPsOrCIDRs("127.0.0:1"),
|
||||||
|
},
|
||||||
|
want: nil,
|
||||||
|
wantErr: true,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"fail/with-excluded-ipsOrCIDRs-cidr": func(t *testing.T) test {
|
||||||
|
return test{
|
||||||
|
options: []NamePolicyOption{
|
||||||
|
WithExcludedIPsOrCIDRs("127.0.0.1//24"),
|
||||||
|
},
|
||||||
|
want: nil,
|
||||||
|
wantErr: true,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"fail/with-excluded-ipsOrCIDRs-ip": func(t *testing.T) test {
|
||||||
|
return test{
|
||||||
|
options: []NamePolicyOption{
|
||||||
|
WithExcludedIPsOrCIDRs("127.0.0:1"),
|
||||||
|
},
|
||||||
|
want: nil,
|
||||||
|
wantErr: true,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"fail/with-permitted-emails": func(t *testing.T) test {
|
||||||
|
return test{
|
||||||
|
options: []NamePolicyOption{
|
||||||
|
WithPermittedEmailAddresses("*.local"),
|
||||||
|
},
|
||||||
|
want: nil,
|
||||||
|
wantErr: true,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"fail/with-excluded-emails": func(t *testing.T) test {
|
||||||
|
return test{
|
||||||
|
options: []NamePolicyOption{
|
||||||
|
WithExcludedEmailAddresses("*.local"),
|
||||||
|
},
|
||||||
|
want: nil,
|
||||||
|
wantErr: true,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"fail/with-permitted-uris": func(t *testing.T) test {
|
||||||
|
return test{
|
||||||
|
options: []NamePolicyOption{
|
||||||
|
WithPermittedURIDomains("**.local"),
|
||||||
|
},
|
||||||
|
want: nil,
|
||||||
|
wantErr: true,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"fail/with-excluded-uris": func(t *testing.T) test {
|
||||||
|
return test{
|
||||||
|
options: []NamePolicyOption{
|
||||||
|
WithExcludedURIDomains("**.local"),
|
||||||
|
},
|
||||||
|
want: nil,
|
||||||
|
wantErr: true,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ok/default": func(t *testing.T) test {
|
||||||
|
return test{
|
||||||
|
options: []NamePolicyOption{},
|
||||||
|
want: &NamePolicyEngine{},
|
||||||
|
wantErr: false,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ok/subject-verification": func(t *testing.T) test {
|
||||||
|
options := []NamePolicyOption{
|
||||||
|
WithSubjectCommonNameVerification(),
|
||||||
|
}
|
||||||
|
return test{
|
||||||
|
options: options,
|
||||||
|
want: &NamePolicyEngine{
|
||||||
|
verifySubjectCommonName: true,
|
||||||
|
},
|
||||||
|
wantErr: false,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ok/literal-wildcards": func(t *testing.T) test {
|
||||||
|
options := []NamePolicyOption{
|
||||||
|
WithAllowLiteralWildcardNames(),
|
||||||
|
}
|
||||||
|
return test{
|
||||||
|
options: options,
|
||||||
|
want: &NamePolicyEngine{
|
||||||
|
allowLiteralWildcardNames: true,
|
||||||
|
},
|
||||||
|
wantErr: false,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ok/with-permitted-dns-wildcard-domains": func(t *testing.T) test {
|
||||||
|
options := []NamePolicyOption{
|
||||||
|
WithPermittedDNSDomains("*.local", "*.example.com"),
|
||||||
|
}
|
||||||
|
return test{
|
||||||
|
options: options,
|
||||||
|
want: &NamePolicyEngine{
|
||||||
|
permittedDNSDomains: []string{".local", ".example.com"},
|
||||||
|
numberOfDNSDomainConstraints: 2,
|
||||||
|
totalNumberOfPermittedConstraints: 2,
|
||||||
|
totalNumberOfConstraints: 2,
|
||||||
|
},
|
||||||
|
wantErr: false,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ok/with-excluded-dns-domains": func(t *testing.T) test {
|
||||||
|
options := []NamePolicyOption{
|
||||||
|
WithExcludedDNSDomains("*.local", "*.example.com"),
|
||||||
|
}
|
||||||
|
return test{
|
||||||
|
options: options,
|
||||||
|
want: &NamePolicyEngine{
|
||||||
|
excludedDNSDomains: []string{".local", ".example.com"},
|
||||||
|
numberOfDNSDomainConstraints: 2,
|
||||||
|
totalNumberOfExcludedConstraints: 2,
|
||||||
|
totalNumberOfConstraints: 2,
|
||||||
|
},
|
||||||
|
wantErr: false,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ok/with-permitted-ip-ranges": func(t *testing.T) test {
|
||||||
|
_, nw1, err := net.ParseCIDR("127.0.0.1/24")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
_, nw2, err := net.ParseCIDR("192.168.0.1/24")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
options := []NamePolicyOption{
|
||||||
|
WithPermittedIPRanges(nw1, nw2),
|
||||||
|
}
|
||||||
|
return test{
|
||||||
|
options: options,
|
||||||
|
want: &NamePolicyEngine{
|
||||||
|
permittedIPRanges: []*net.IPNet{
|
||||||
|
nw1, nw2,
|
||||||
|
},
|
||||||
|
numberOfIPRangeConstraints: 2,
|
||||||
|
totalNumberOfPermittedConstraints: 2,
|
||||||
|
totalNumberOfConstraints: 2,
|
||||||
|
},
|
||||||
|
wantErr: false,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ok/with-excluded-ip-ranges": func(t *testing.T) test {
|
||||||
|
_, nw1, err := net.ParseCIDR("127.0.0.1/24")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
_, nw2, err := net.ParseCIDR("192.168.0.1/24")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
options := []NamePolicyOption{
|
||||||
|
WithExcludedIPRanges(nw1, nw2),
|
||||||
|
}
|
||||||
|
return test{
|
||||||
|
options: options,
|
||||||
|
want: &NamePolicyEngine{
|
||||||
|
excludedIPRanges: []*net.IPNet{
|
||||||
|
nw1, nw2,
|
||||||
|
},
|
||||||
|
numberOfIPRangeConstraints: 2,
|
||||||
|
totalNumberOfExcludedConstraints: 2,
|
||||||
|
totalNumberOfConstraints: 2,
|
||||||
|
},
|
||||||
|
wantErr: false,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ok/with-permitted-cidrs": func(t *testing.T) test {
|
||||||
|
_, nw1, err := net.ParseCIDR("127.0.0.1/24")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
_, nw2, err := net.ParseCIDR("192.168.0.1/24")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
options := []NamePolicyOption{
|
||||||
|
WithPermittedCIDRs("127.0.0.1/24", "192.168.0.1/24"),
|
||||||
|
}
|
||||||
|
return test{
|
||||||
|
options: options,
|
||||||
|
want: &NamePolicyEngine{
|
||||||
|
permittedIPRanges: []*net.IPNet{
|
||||||
|
nw1, nw2,
|
||||||
|
},
|
||||||
|
numberOfIPRangeConstraints: 2,
|
||||||
|
totalNumberOfPermittedConstraints: 2,
|
||||||
|
totalNumberOfConstraints: 2,
|
||||||
|
},
|
||||||
|
wantErr: false,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ok/with-excluded-cidrs": func(t *testing.T) test {
|
||||||
|
_, nw1, err := net.ParseCIDR("127.0.0.1/24")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
_, nw2, err := net.ParseCIDR("192.168.0.1/24")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
options := []NamePolicyOption{
|
||||||
|
WithExcludedCIDRs("127.0.0.1/24", "192.168.0.1/24"),
|
||||||
|
}
|
||||||
|
return test{
|
||||||
|
options: options,
|
||||||
|
want: &NamePolicyEngine{
|
||||||
|
excludedIPRanges: []*net.IPNet{
|
||||||
|
nw1, nw2,
|
||||||
|
},
|
||||||
|
numberOfIPRangeConstraints: 2,
|
||||||
|
totalNumberOfExcludedConstraints: 2,
|
||||||
|
totalNumberOfConstraints: 2,
|
||||||
|
},
|
||||||
|
wantErr: false,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ok/with-permitted-ipsOrCIDRs-cidr": func(t *testing.T) test {
|
||||||
|
_, nw1, err := net.ParseCIDR("127.0.0.1/24")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
_, nw2, err := net.ParseCIDR("192.168.0.31/32")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
_, nw3, err := net.ParseCIDR("2001:0db8:85a3:0000:0000:8a2e:0370:7334/128")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
options := []NamePolicyOption{
|
||||||
|
WithPermittedIPsOrCIDRs("127.0.0.1/24", "192.168.0.31", "2001:0db8:85a3:0000:0000:8a2e:0370:7334"),
|
||||||
|
}
|
||||||
|
return test{
|
||||||
|
options: options,
|
||||||
|
want: &NamePolicyEngine{
|
||||||
|
permittedIPRanges: []*net.IPNet{
|
||||||
|
nw1, nw2, nw3,
|
||||||
|
},
|
||||||
|
numberOfIPRangeConstraints: 3,
|
||||||
|
totalNumberOfPermittedConstraints: 3,
|
||||||
|
totalNumberOfConstraints: 3,
|
||||||
|
},
|
||||||
|
wantErr: false,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ok/with-excluded-ipsOrCIDRs-cidr": func(t *testing.T) test {
|
||||||
|
_, nw1, err := net.ParseCIDR("127.0.0.1/24")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
_, nw2, err := net.ParseCIDR("192.168.0.31/32")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
_, nw3, err := net.ParseCIDR("2001:0db8:85a3:0000:0000:8a2e:0370:7334/128")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
options := []NamePolicyOption{
|
||||||
|
WithExcludedIPsOrCIDRs("127.0.0.1/24", "192.168.0.31", "2001:0db8:85a3:0000:0000:8a2e:0370:7334"),
|
||||||
|
}
|
||||||
|
return test{
|
||||||
|
options: options,
|
||||||
|
want: &NamePolicyEngine{
|
||||||
|
excludedIPRanges: []*net.IPNet{
|
||||||
|
nw1, nw2, nw3,
|
||||||
|
},
|
||||||
|
numberOfIPRangeConstraints: 3,
|
||||||
|
totalNumberOfExcludedConstraints: 3,
|
||||||
|
totalNumberOfConstraints: 3,
|
||||||
|
},
|
||||||
|
wantErr: false,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ok/with-permitted-emails": func(t *testing.T) test {
|
||||||
|
options := []NamePolicyOption{
|
||||||
|
WithPermittedEmailAddresses("mail@local", "@example.com"),
|
||||||
|
}
|
||||||
|
return test{
|
||||||
|
options: options,
|
||||||
|
want: &NamePolicyEngine{
|
||||||
|
permittedEmailAddresses: []string{"mail@local", "example.com"},
|
||||||
|
numberOfEmailAddressConstraints: 2,
|
||||||
|
totalNumberOfPermittedConstraints: 2,
|
||||||
|
totalNumberOfConstraints: 2,
|
||||||
|
},
|
||||||
|
wantErr: false,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ok/with-excluded-emails": func(t *testing.T) test {
|
||||||
|
options := []NamePolicyOption{
|
||||||
|
WithExcludedEmailAddresses("mail@local", "@example.com"),
|
||||||
|
}
|
||||||
|
return test{
|
||||||
|
options: options,
|
||||||
|
want: &NamePolicyEngine{
|
||||||
|
excludedEmailAddresses: []string{"mail@local", "example.com"},
|
||||||
|
numberOfEmailAddressConstraints: 2,
|
||||||
|
totalNumberOfExcludedConstraints: 2,
|
||||||
|
totalNumberOfConstraints: 2,
|
||||||
|
},
|
||||||
|
wantErr: false,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ok/with-permitted-uris": func(t *testing.T) test {
|
||||||
|
options := []NamePolicyOption{
|
||||||
|
WithPermittedURIDomains("host.local", "*.example.com"),
|
||||||
|
}
|
||||||
|
return test{
|
||||||
|
options: options,
|
||||||
|
want: &NamePolicyEngine{
|
||||||
|
permittedURIDomains: []string{"host.local", ".example.com"},
|
||||||
|
numberOfURIDomainConstraints: 2,
|
||||||
|
totalNumberOfPermittedConstraints: 2,
|
||||||
|
totalNumberOfConstraints: 2,
|
||||||
|
},
|
||||||
|
wantErr: false,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ok/with-excluded-uris": func(t *testing.T) test {
|
||||||
|
options := []NamePolicyOption{
|
||||||
|
WithExcludedURIDomains("host.local", "*.example.com"),
|
||||||
|
}
|
||||||
|
return test{
|
||||||
|
options: options,
|
||||||
|
want: &NamePolicyEngine{
|
||||||
|
excludedURIDomains: []string{"host.local", ".example.com"},
|
||||||
|
numberOfURIDomainConstraints: 2,
|
||||||
|
totalNumberOfExcludedConstraints: 2,
|
||||||
|
totalNumberOfConstraints: 2,
|
||||||
|
},
|
||||||
|
wantErr: false,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ok/with-permitted-principals": func(t *testing.T) test {
|
||||||
|
options := []NamePolicyOption{
|
||||||
|
WithPermittedPrincipals("root", "ops"),
|
||||||
|
}
|
||||||
|
return test{
|
||||||
|
options: options,
|
||||||
|
want: &NamePolicyEngine{
|
||||||
|
permittedPrincipals: []string{"root", "ops"},
|
||||||
|
numberOfPrincipalConstraints: 2,
|
||||||
|
totalNumberOfPermittedConstraints: 2,
|
||||||
|
totalNumberOfConstraints: 2,
|
||||||
|
},
|
||||||
|
wantErr: false,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ok/with-excluded-principals": func(t *testing.T) test {
|
||||||
|
options := []NamePolicyOption{
|
||||||
|
WithExcludedPrincipals("root", "ops"),
|
||||||
|
}
|
||||||
|
return test{
|
||||||
|
options: options,
|
||||||
|
want: &NamePolicyEngine{
|
||||||
|
excludedPrincipals: []string{"root", "ops"},
|
||||||
|
numberOfPrincipalConstraints: 2,
|
||||||
|
totalNumberOfExcludedConstraints: 2,
|
||||||
|
totalNumberOfConstraints: 2,
|
||||||
|
},
|
||||||
|
wantErr: false,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for name, prep := range tests {
|
||||||
|
tc := prep(t)
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
got, err := New(tc.options...)
|
||||||
|
if (err != nil) != tc.wantErr {
|
||||||
|
t.Errorf("New() error = %v, wantErr %v", err, tc.wantErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !cmp.Equal(tc.want, got, cmp.AllowUnexported(NamePolicyEngine{})) {
|
||||||
|
t.Errorf("New() diff =\n %s", cmp.Diff(tc.want, got, cmp.AllowUnexported(NamePolicyEngine{})))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
9
policy/ssh.go
Normal file
9
policy/ssh.go
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
package policy
|
||||||
|
|
||||||
|
import (
|
||||||
|
"golang.org/x/crypto/ssh"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SSHNamePolicyEngine interface {
|
||||||
|
IsSSHCertificateAllowed(cert *ssh.Certificate) error
|
||||||
|
}
|
647
policy/validate.go
Normal file
647
policy/validate.go
Normal file
|
@ -0,0 +1,647 @@
|
||||||
|
// Copyright 2011 The Go Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
//
|
||||||
|
// The code in this file is an adapted version of the code in
|
||||||
|
// https://cs.opensource.google/go/go/+/refs/tags/go1.17.5:src/crypto/x509/verify.go
|
||||||
|
package policy
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"net/url"
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"golang.org/x/net/idna"
|
||||||
|
|
||||||
|
"go.step.sm/crypto/x509util"
|
||||||
|
)
|
||||||
|
|
||||||
|
// validateNames verifies that all names are allowed.
|
||||||
|
func (e *NamePolicyEngine) validateNames(dnsNames []string, ips []net.IP, emailAddresses []string, uris []*url.URL, principals []string) error {
|
||||||
|
|
||||||
|
// nothing to compare against; return early
|
||||||
|
if e.totalNumberOfConstraints == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: set limit on total of all names validated? In x509 there's a limit on the number of comparisons
|
||||||
|
// that protects the CA from a DoS (i.e. many heavy comparisons). The x509 implementation takes
|
||||||
|
// this number as a total of all checks and keeps a (pointer to a) counter of the number of checks
|
||||||
|
// executed so far.
|
||||||
|
|
||||||
|
// TODO: gather all errors, or return early? Currently we return early on the first wrong name; check might fail for multiple names.
|
||||||
|
// Perhaps make that an option?
|
||||||
|
for _, dns := range dnsNames {
|
||||||
|
// if there are DNS names to check, no DNS constraints set, but there are other permitted constraints,
|
||||||
|
// then return error, because DNS should be explicitly configured to be allowed in that case. In case there are
|
||||||
|
// (other) excluded constraints, we'll allow a DNS (implicit allow; currently).
|
||||||
|
if e.numberOfDNSDomainConstraints == 0 && e.totalNumberOfPermittedConstraints > 0 {
|
||||||
|
return &NamePolicyError{
|
||||||
|
Reason: NotAllowed,
|
||||||
|
NameType: DNSNameType,
|
||||||
|
Name: dns,
|
||||||
|
detail: fmt.Sprintf("dns %q is not explicitly permitted by any constraint", dns),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
didCutWildcard := false
|
||||||
|
parsedDNS := dns
|
||||||
|
if strings.HasPrefix(parsedDNS, "*.") {
|
||||||
|
parsedDNS = parsedDNS[1:]
|
||||||
|
didCutWildcard = true
|
||||||
|
}
|
||||||
|
// TODO(hs): fix this above; we need separate rule for Subject Common Name?
|
||||||
|
parsedDNS, err := idna.Lookup.ToASCII(parsedDNS)
|
||||||
|
if err != nil {
|
||||||
|
return &NamePolicyError{
|
||||||
|
Reason: CannotParseDomain,
|
||||||
|
NameType: DNSNameType,
|
||||||
|
Name: dns,
|
||||||
|
detail: fmt.Sprintf("dns %q cannot be converted to ASCII", dns),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if didCutWildcard {
|
||||||
|
parsedDNS = "*" + parsedDNS
|
||||||
|
}
|
||||||
|
if _, ok := domainToReverseLabels(parsedDNS); !ok { // TODO(hs): this also fails with spaces
|
||||||
|
return &NamePolicyError{
|
||||||
|
Reason: CannotParseDomain,
|
||||||
|
NameType: DNSNameType,
|
||||||
|
Name: dns,
|
||||||
|
detail: fmt.Sprintf("cannot parse dns %q", dns),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := checkNameConstraints(DNSNameType, dns, parsedDNS,
|
||||||
|
func(parsedName, constraint interface{}) (bool, error) {
|
||||||
|
return e.matchDomainConstraint(parsedName.(string), constraint.(string))
|
||||||
|
}, e.permittedDNSDomains, e.excludedDNSDomains); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, ip := range ips {
|
||||||
|
if e.numberOfIPRangeConstraints == 0 && e.totalNumberOfPermittedConstraints > 0 {
|
||||||
|
return &NamePolicyError{
|
||||||
|
Reason: NotAllowed,
|
||||||
|
NameType: IPNameType,
|
||||||
|
Name: ip.String(),
|
||||||
|
detail: fmt.Sprintf("ip %q is not explicitly permitted by any constraint", ip.String()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := checkNameConstraints(IPNameType, ip.String(), ip,
|
||||||
|
func(parsedName, constraint interface{}) (bool, error) {
|
||||||
|
return matchIPConstraint(parsedName.(net.IP), constraint.(*net.IPNet))
|
||||||
|
}, e.permittedIPRanges, e.excludedIPRanges); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, email := range emailAddresses {
|
||||||
|
if e.numberOfEmailAddressConstraints == 0 && e.totalNumberOfPermittedConstraints > 0 {
|
||||||
|
return &NamePolicyError{
|
||||||
|
Reason: NotAllowed,
|
||||||
|
NameType: EmailNameType,
|
||||||
|
Name: email,
|
||||||
|
detail: fmt.Sprintf("email %q is not explicitly permitted by any constraint", email),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mailbox, ok := parseRFC2821Mailbox(email)
|
||||||
|
if !ok {
|
||||||
|
return &NamePolicyError{
|
||||||
|
Reason: CannotParseRFC822Name,
|
||||||
|
NameType: EmailNameType,
|
||||||
|
Name: email,
|
||||||
|
detail: fmt.Sprintf("invalid rfc822Name %q", mailbox),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// According to RFC 5280, section 7.5, emails are considered to match if the local part is
|
||||||
|
// an exact match and the host (domain) part matches the ASCII representation (case-insensitive):
|
||||||
|
// https://datatracker.ietf.org/doc/html/rfc5280#section-7.5
|
||||||
|
domainASCII, err := idna.ToASCII(mailbox.domain)
|
||||||
|
if err != nil {
|
||||||
|
return &NamePolicyError{
|
||||||
|
Reason: CannotParseDomain,
|
||||||
|
NameType: EmailNameType,
|
||||||
|
Name: email,
|
||||||
|
detail: fmt.Errorf("cannot parse email domain %q: %w", email, err).Error(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mailbox.domain = domainASCII
|
||||||
|
if err := checkNameConstraints(EmailNameType, email, mailbox,
|
||||||
|
func(parsedName, constraint interface{}) (bool, error) {
|
||||||
|
return e.matchEmailConstraint(parsedName.(rfc2821Mailbox), constraint.(string))
|
||||||
|
}, e.permittedEmailAddresses, e.excludedEmailAddresses); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO(hs): fix internationalization for URIs (IRIs)
|
||||||
|
|
||||||
|
for _, uri := range uris {
|
||||||
|
if e.numberOfURIDomainConstraints == 0 && e.totalNumberOfPermittedConstraints > 0 {
|
||||||
|
return &NamePolicyError{
|
||||||
|
Reason: NotAllowed,
|
||||||
|
NameType: URINameType,
|
||||||
|
Name: uri.String(),
|
||||||
|
detail: fmt.Sprintf("uri %q is not explicitly permitted by any constraint", uri.String()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// TODO(hs): ideally we'd like the uri.String() to be the original contents; now
|
||||||
|
// it's transformed into ASCII. Prevent that here?
|
||||||
|
if err := checkNameConstraints(URINameType, uri.String(), uri,
|
||||||
|
func(parsedName, constraint interface{}) (bool, error) {
|
||||||
|
return e.matchURIConstraint(parsedName.(*url.URL), constraint.(string))
|
||||||
|
}, e.permittedURIDomains, e.excludedURIDomains); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, principal := range principals {
|
||||||
|
if e.numberOfPrincipalConstraints == 0 && e.totalNumberOfPermittedConstraints > 0 {
|
||||||
|
return &NamePolicyError{
|
||||||
|
Reason: NotAllowed,
|
||||||
|
NameType: PrincipalNameType,
|
||||||
|
Name: principal,
|
||||||
|
detail: fmt.Sprintf("username principal %q is not explicitly permitted by any constraint", principal),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// TODO: some validation? I.e. allowed characters?
|
||||||
|
if err := checkNameConstraints(PrincipalNameType, principal, principal,
|
||||||
|
func(parsedName, constraint interface{}) (bool, error) {
|
||||||
|
return matchPrincipalConstraint(parsedName.(string), constraint.(string))
|
||||||
|
}, e.permittedPrincipals, e.excludedPrincipals); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// if all checks out, all SANs are allowed
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// validateCommonName verifies that the Subject Common Name is allowed
|
||||||
|
func (e *NamePolicyEngine) validateCommonName(commonName string) error {
|
||||||
|
|
||||||
|
// nothing to compare against; return early
|
||||||
|
if e.totalNumberOfConstraints == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// empty common names are not validated
|
||||||
|
if commonName == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if e.numberOfCommonNameConstraints > 0 {
|
||||||
|
// Check the Common Name using its dedicated matcher if constraints have been
|
||||||
|
// configured. If no error is returned from matching, the Common Name was
|
||||||
|
// explicitly allowed and nil is returned immediately.
|
||||||
|
if err := checkNameConstraints(CNNameType, commonName, commonName,
|
||||||
|
func(parsedName, constraint interface{}) (bool, error) {
|
||||||
|
return matchCommonNameConstraint(parsedName.(string), constraint.(string))
|
||||||
|
}, e.permittedCommonNames, e.excludedCommonNames); err == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// When an error was returned or when no constraints were configured for Common Names,
|
||||||
|
// the Common Name should be validated against the other types of constraints too,
|
||||||
|
// according to what type it is.
|
||||||
|
dnsNames, ips, emails, uris := x509util.SplitSANs([]string{commonName})
|
||||||
|
|
||||||
|
err := e.validateNames(dnsNames, ips, emails, uris, []string{})
|
||||||
|
|
||||||
|
if pe, ok := err.(*NamePolicyError); ok {
|
||||||
|
// override the name type with CN
|
||||||
|
pe.NameType = CNNameType
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkNameConstraints checks that a name, of type nameType is permitted.
|
||||||
|
// The argument parsedName contains the parsed form of name, suitable for passing
|
||||||
|
// to the match function.
|
||||||
|
func checkNameConstraints(
|
||||||
|
nameType NameType,
|
||||||
|
name string,
|
||||||
|
parsedName interface{},
|
||||||
|
match func(parsedName, constraint interface{}) (match bool, err error),
|
||||||
|
permitted, excluded interface{}) error {
|
||||||
|
|
||||||
|
excludedValue := reflect.ValueOf(excluded)
|
||||||
|
|
||||||
|
for i := 0; i < excludedValue.Len(); i++ {
|
||||||
|
constraint := excludedValue.Index(i).Interface()
|
||||||
|
match, err := match(parsedName, constraint)
|
||||||
|
if err != nil {
|
||||||
|
return &NamePolicyError{
|
||||||
|
Reason: CannotMatchNameToConstraint,
|
||||||
|
NameType: nameType,
|
||||||
|
Name: name,
|
||||||
|
detail: err.Error(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if match {
|
||||||
|
return &NamePolicyError{
|
||||||
|
Reason: NotAllowed,
|
||||||
|
NameType: nameType,
|
||||||
|
Name: name,
|
||||||
|
detail: fmt.Sprintf("%s %q is excluded by constraint %q", nameType, name, constraint),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
permittedValue := reflect.ValueOf(permitted)
|
||||||
|
|
||||||
|
ok := true
|
||||||
|
for i := 0; i < permittedValue.Len(); i++ {
|
||||||
|
constraint := permittedValue.Index(i).Interface()
|
||||||
|
var err error
|
||||||
|
if ok, err = match(parsedName, constraint); err != nil {
|
||||||
|
return &NamePolicyError{
|
||||||
|
Reason: CannotMatchNameToConstraint,
|
||||||
|
NameType: nameType,
|
||||||
|
Name: name,
|
||||||
|
detail: err.Error(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ok {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !ok {
|
||||||
|
return &NamePolicyError{
|
||||||
|
Reason: NotAllowed,
|
||||||
|
NameType: nameType,
|
||||||
|
Name: name,
|
||||||
|
detail: fmt.Sprintf("%s %q is not permitted by any constraint", nameType, name),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// domainToReverseLabels converts a textual domain name like foo.example.com to
|
||||||
|
// the list of labels in reverse order, e.g. ["com", "example", "foo"].
|
||||||
|
func domainToReverseLabels(domain string) (reverseLabels []string, ok bool) {
|
||||||
|
for len(domain) > 0 {
|
||||||
|
if i := strings.LastIndexByte(domain, '.'); i == -1 {
|
||||||
|
reverseLabels = append(reverseLabels, domain)
|
||||||
|
domain = ""
|
||||||
|
} else {
|
||||||
|
reverseLabels = append(reverseLabels, domain[i+1:])
|
||||||
|
domain = domain[:i]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(reverseLabels) > 0 && reverseLabels[0] == "" {
|
||||||
|
// An empty label at the end indicates an absolute value.
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, label := range reverseLabels {
|
||||||
|
if label == "" {
|
||||||
|
// Empty labels are otherwise invalid.
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, c := range label {
|
||||||
|
if c < 33 || c > 126 {
|
||||||
|
// Invalid character.
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return reverseLabels, true
|
||||||
|
}
|
||||||
|
|
||||||
|
// rfc2821Mailbox represents a “mailbox” (which is an email address to most
|
||||||
|
// people) by breaking it into the “local” (i.e. before the '@') and “domain”
|
||||||
|
// parts.
|
||||||
|
type rfc2821Mailbox struct {
|
||||||
|
local, domain string
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseRFC2821Mailbox parses an email address into local and domain parts,
|
||||||
|
// based on the ABNF for a “Mailbox” from RFC 2821. According to RFC 5280,
|
||||||
|
// Section 4.2.1.6 that's correct for an rfc822Name from a certificate: “The
|
||||||
|
// format of an rfc822Name is a "Mailbox" as defined in RFC 2821, Section 4.1.2”.
|
||||||
|
func parseRFC2821Mailbox(in string) (mailbox rfc2821Mailbox, ok bool) {
|
||||||
|
if in == "" {
|
||||||
|
return mailbox, false
|
||||||
|
}
|
||||||
|
|
||||||
|
localPartBytes := make([]byte, 0, len(in)/2)
|
||||||
|
|
||||||
|
if in[0] == '"' {
|
||||||
|
// Quoted-string = DQUOTE *qcontent DQUOTE
|
||||||
|
// non-whitespace-control = %d1-8 / %d11 / %d12 / %d14-31 / %d127
|
||||||
|
// qcontent = qtext / quoted-pair
|
||||||
|
// qtext = non-whitespace-control /
|
||||||
|
// %d33 / %d35-91 / %d93-126
|
||||||
|
// quoted-pair = ("\" text) / obs-qp
|
||||||
|
// text = %d1-9 / %d11 / %d12 / %d14-127 / obs-text
|
||||||
|
//
|
||||||
|
// (Names beginning with “obs-” are the obsolete syntax from RFC 2822,
|
||||||
|
// Section 4. Since it has been 16 years, we no longer accept that.)
|
||||||
|
in = in[1:]
|
||||||
|
QuotedString:
|
||||||
|
for {
|
||||||
|
if in == "" {
|
||||||
|
return mailbox, false
|
||||||
|
}
|
||||||
|
c := in[0]
|
||||||
|
in = in[1:]
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case c == '"':
|
||||||
|
break QuotedString
|
||||||
|
|
||||||
|
case c == '\\':
|
||||||
|
// quoted-pair
|
||||||
|
if in == "" {
|
||||||
|
return mailbox, false
|
||||||
|
}
|
||||||
|
if in[0] == 11 ||
|
||||||
|
in[0] == 12 ||
|
||||||
|
(1 <= in[0] && in[0] <= 9) ||
|
||||||
|
(14 <= in[0] && in[0] <= 127) {
|
||||||
|
localPartBytes = append(localPartBytes, in[0])
|
||||||
|
in = in[1:]
|
||||||
|
} else {
|
||||||
|
return mailbox, false
|
||||||
|
}
|
||||||
|
|
||||||
|
case c == 11 ||
|
||||||
|
c == 12 ||
|
||||||
|
// Space (char 32) is not allowed based on the
|
||||||
|
// BNF, but RFC 3696 gives an example that
|
||||||
|
// assumes that it is. Several “verified”
|
||||||
|
// errata continue to argue about this point.
|
||||||
|
// We choose to accept it.
|
||||||
|
c == 32 ||
|
||||||
|
c == 33 ||
|
||||||
|
c == 127 ||
|
||||||
|
(1 <= c && c <= 8) ||
|
||||||
|
(14 <= c && c <= 31) ||
|
||||||
|
(35 <= c && c <= 91) ||
|
||||||
|
(93 <= c && c <= 126):
|
||||||
|
// qtext
|
||||||
|
localPartBytes = append(localPartBytes, c)
|
||||||
|
|
||||||
|
default:
|
||||||
|
return mailbox, false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Atom ("." Atom)*
|
||||||
|
NextChar:
|
||||||
|
for len(in) > 0 {
|
||||||
|
// atext from RFC 2822, Section 3.2.4
|
||||||
|
c := in[0]
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case c == '\\':
|
||||||
|
// Examples given in RFC 3696 suggest that
|
||||||
|
// escaped characters can appear outside of a
|
||||||
|
// quoted string. Several “verified” errata
|
||||||
|
// continue to argue the point. We choose to
|
||||||
|
// accept it.
|
||||||
|
in = in[1:]
|
||||||
|
if in == "" {
|
||||||
|
return mailbox, false
|
||||||
|
}
|
||||||
|
fallthrough
|
||||||
|
|
||||||
|
case ('0' <= c && c <= '9') ||
|
||||||
|
('a' <= c && c <= 'z') ||
|
||||||
|
('A' <= c && c <= 'Z') ||
|
||||||
|
c == '!' || c == '#' || c == '$' || c == '%' ||
|
||||||
|
c == '&' || c == '\'' || c == '*' || c == '+' ||
|
||||||
|
c == '-' || c == '/' || c == '=' || c == '?' ||
|
||||||
|
c == '^' || c == '_' || c == '`' || c == '{' ||
|
||||||
|
c == '|' || c == '}' || c == '~' || c == '.':
|
||||||
|
localPartBytes = append(localPartBytes, in[0])
|
||||||
|
in = in[1:]
|
||||||
|
|
||||||
|
default:
|
||||||
|
break NextChar
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(localPartBytes) == 0 {
|
||||||
|
return mailbox, false
|
||||||
|
}
|
||||||
|
|
||||||
|
// From RFC 3696, Section 3:
|
||||||
|
// “period (".") may also appear, but may not be used to start
|
||||||
|
// or end the local part, nor may two or more consecutive
|
||||||
|
// periods appear.”
|
||||||
|
twoDots := []byte{'.', '.'}
|
||||||
|
if localPartBytes[0] == '.' ||
|
||||||
|
localPartBytes[len(localPartBytes)-1] == '.' ||
|
||||||
|
bytes.Contains(localPartBytes, twoDots) {
|
||||||
|
return mailbox, false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if in == "" || in[0] != '@' {
|
||||||
|
return mailbox, false
|
||||||
|
}
|
||||||
|
in = in[1:]
|
||||||
|
|
||||||
|
// The RFC species a format for domains, but that's known to be
|
||||||
|
// violated in practice so we accept that anything after an '@' is the
|
||||||
|
// domain part.
|
||||||
|
if _, ok := domainToReverseLabels(in); !ok {
|
||||||
|
return mailbox, false
|
||||||
|
}
|
||||||
|
|
||||||
|
mailbox.local = string(localPartBytes)
|
||||||
|
mailbox.domain = in
|
||||||
|
return mailbox, true
|
||||||
|
}
|
||||||
|
|
||||||
|
// matchDomainConstraint matches a domain against the given constraint
|
||||||
|
func (e *NamePolicyEngine) matchDomainConstraint(domain, constraint string) (bool, error) {
|
||||||
|
// The meaning of zero length constraints is not specified, but this
|
||||||
|
// code follows NSS and accepts them as matching everything.
|
||||||
|
if constraint == "" {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// A single whitespace seems to be considered a valid domain, but we don't allow it.
|
||||||
|
if domain == " " {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Block domains that start with just a period
|
||||||
|
if domain[0] == '.' {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Block wildcard domains that don't start with exactly "*." (i.e. double wildcards and such)
|
||||||
|
if domain[0] == '*' && domain[1] != '.' {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the domain starts with a wildcard and return early if not allowed
|
||||||
|
if strings.HasPrefix(domain, "*.") && !e.allowLiteralWildcardNames {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only allow asterisk at the start of the domain; we don't allow them as part of a domain label or as a (sub)domain label (currently)
|
||||||
|
if strings.LastIndex(domain, "*") > 0 {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Don't allow constraints with empty labels in any position
|
||||||
|
if strings.Contains(constraint, "..") {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
domainLabels, ok := domainToReverseLabels(domain)
|
||||||
|
if !ok {
|
||||||
|
return false, fmt.Errorf("cannot parse domain %q", domain)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RFC 5280 says that a leading period in a domain name means that at
|
||||||
|
// least one label must be prepended, but only for URI and email
|
||||||
|
// constraints, not DNS constraints. The code also supports that
|
||||||
|
// behavior for DNS constraints. In our adaptation of the original
|
||||||
|
// Go stdlib x509 Name Constraint implementation we look for exactly
|
||||||
|
// one subdomain, currently.
|
||||||
|
|
||||||
|
mustHaveSubdomains := false
|
||||||
|
if constraint[0] == '.' {
|
||||||
|
mustHaveSubdomains = true
|
||||||
|
constraint = constraint[1:]
|
||||||
|
}
|
||||||
|
|
||||||
|
constraintLabels, ok := domainToReverseLabels(constraint)
|
||||||
|
if !ok {
|
||||||
|
return false, fmt.Errorf("cannot parse domain constraint %q", constraint)
|
||||||
|
}
|
||||||
|
|
||||||
|
expectedNumberOfLabels := len(constraintLabels)
|
||||||
|
if mustHaveSubdomains {
|
||||||
|
// we expect exactly one more label if it starts with the "canonical" x509 "wildcard": "."
|
||||||
|
// in the future we could extend this to support multiple additional labels and/or more
|
||||||
|
// complex matching.
|
||||||
|
expectedNumberOfLabels++
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(domainLabels) != expectedNumberOfLabels {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, constraintLabel := range constraintLabels {
|
||||||
|
if !strings.EqualFold(constraintLabel, domainLabels[i]) {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SOURCE: https://cs.opensource.google/go/go/+/refs/tags/go1.17.5:src/crypto/x509/verify.go
|
||||||
|
func matchIPConstraint(ip net.IP, constraint *net.IPNet) (bool, error) {
|
||||||
|
|
||||||
|
// TODO(hs): this is code from Go library, but I got some unexpected result:
|
||||||
|
// with permitted net 127.0.0.0/24, 127.0.0.1 is NOT allowed. When parsing 127.0.0.1 as net.IP
|
||||||
|
// which is in the IPAddresses slice, the underlying length is 16. The contraint.IP has a length
|
||||||
|
// of 4 instead. I currently don't believe that this is a bug in Go now, but why is it like that?
|
||||||
|
// Is there a difference because we're not operating on a sans []string slice? Or is the Go
|
||||||
|
// implementation stricter regarding IPv4 vs. IPv6? I've been bitten by some unfortunate differences
|
||||||
|
// between the two before (i.e. IPv4 in IPv6; IP SANS in ACME)
|
||||||
|
// if len(ip) != len(constraint.IP) {
|
||||||
|
// return false, nil
|
||||||
|
// }
|
||||||
|
|
||||||
|
// for i := range ip {
|
||||||
|
// if mask := constraint.Mask[i]; ip[i]&mask != constraint.IP[i]&mask {
|
||||||
|
// return false, nil
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
contained := constraint.Contains(ip) // TODO(hs): validate that this is the correct behavior; also check IPv4-in-IPv6 (again)
|
||||||
|
|
||||||
|
return contained, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SOURCE: https://cs.opensource.google/go/go/+/refs/tags/go1.17.5:src/crypto/x509/verify.go
|
||||||
|
func (e *NamePolicyEngine) matchEmailConstraint(mailbox rfc2821Mailbox, constraint string) (bool, error) {
|
||||||
|
if strings.Contains(constraint, "@") {
|
||||||
|
constraintMailbox, ok := parseRFC2821Mailbox(constraint)
|
||||||
|
if !ok {
|
||||||
|
return false, fmt.Errorf("cannot parse constraint %q", constraint)
|
||||||
|
}
|
||||||
|
return mailbox.local == constraintMailbox.local && strings.EqualFold(mailbox.domain, constraintMailbox.domain), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise the constraint is like a DNS constraint of the domain part
|
||||||
|
// of the mailbox.
|
||||||
|
return e.matchDomainConstraint(mailbox.domain, constraint)
|
||||||
|
}
|
||||||
|
|
||||||
|
// matchURIConstraint matches an URL against a constraint
|
||||||
|
func (e *NamePolicyEngine) matchURIConstraint(uri *url.URL, constraint string) (bool, error) {
|
||||||
|
// From RFC 5280, Section 4.2.1.10:
|
||||||
|
// “a uniformResourceIdentifier that does not include an authority
|
||||||
|
// component with a host name specified as a fully qualified domain
|
||||||
|
// name (e.g., if the URI either does not include an authority
|
||||||
|
// component or includes an authority component in which the host name
|
||||||
|
// is specified as an IP address), then the application MUST reject the
|
||||||
|
// certificate.”
|
||||||
|
|
||||||
|
host := uri.Host
|
||||||
|
if host == "" {
|
||||||
|
return false, fmt.Errorf("URI with empty host (%q) cannot be matched against constraints", uri.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Block hosts with the wildcard character; no exceptions, also not when wildcards allowed.
|
||||||
|
if strings.Contains(host, "*") {
|
||||||
|
return false, fmt.Errorf("URI host %q cannot contain asterisk", uri.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.Contains(host, ":") && !strings.HasSuffix(host, "]") {
|
||||||
|
var err error
|
||||||
|
host, _, err = net.SplitHostPort(host)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.HasPrefix(host, "[") && strings.HasSuffix(host, "]") ||
|
||||||
|
net.ParseIP(host) != nil {
|
||||||
|
return false, fmt.Errorf("URI with IP %q cannot be matched against constraints", uri.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO(hs): add checks for scheme, path, etc.; either here, or in a different constraint matcher (to keep this one simple)
|
||||||
|
|
||||||
|
return e.matchDomainConstraint(host, constraint)
|
||||||
|
}
|
||||||
|
|
||||||
|
// matchPrincipalConstraint performs a string literal equality check against a constraint.
|
||||||
|
func matchPrincipalConstraint(principal, constraint string) (bool, error) {
|
||||||
|
// allow any plain principal when wildcard constraint is used
|
||||||
|
if constraint == "*" {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
return strings.EqualFold(principal, constraint), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// matchCommonNameConstraint performs a string literal equality check against constraint.
|
||||||
|
func matchCommonNameConstraint(commonName, constraint string) (bool, error) {
|
||||||
|
// wildcard constraint is (currently) not supported for common names
|
||||||
|
if constraint == "*" {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
return strings.EqualFold(commonName, constraint), nil
|
||||||
|
}
|
14
policy/x509.go
Normal file
14
policy/x509.go
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
package policy
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/x509"
|
||||||
|
"net"
|
||||||
|
)
|
||||||
|
|
||||||
|
type X509NamePolicyEngine interface {
|
||||||
|
IsX509CertificateAllowed(cert *x509.Certificate) error
|
||||||
|
IsX509CertificateRequestAllowed(csr *x509.CertificateRequest) error
|
||||||
|
AreSANsAllowed(sans []string) error
|
||||||
|
IsDNSAllowed(dns string) error
|
||||||
|
IsIPAllowed(ip net.IP) error
|
||||||
|
}
|
Loading…
Reference in a new issue