forked from TrueCloudLab/certificates
change provisioners api
* /provisioners -> /provisioners/jwk-set-by-issuer * /provisioners now returns a list of Provisioners
This commit is contained in:
parent
7b6a3ea427
commit
0b5f6487e1
13 changed files with 354 additions and 144 deletions
34
api/api.go
34
api/api.go
|
@ -11,6 +11,7 @@ import (
|
||||||
|
|
||||||
"github.com/go-chi/chi"
|
"github.com/go-chi/chi"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
|
"github.com/smallstep/ca-component/provisioner"
|
||||||
"github.com/smallstep/cli/crypto/tlsutil"
|
"github.com/smallstep/cli/crypto/tlsutil"
|
||||||
"github.com/smallstep/cli/crypto/x509util"
|
"github.com/smallstep/cli/crypto/x509util"
|
||||||
"github.com/smallstep/cli/jose"
|
"github.com/smallstep/cli/jose"
|
||||||
|
@ -46,7 +47,7 @@ type Authority interface {
|
||||||
Root(shasum string) (*x509.Certificate, error)
|
Root(shasum string) (*x509.Certificate, error)
|
||||||
Sign(cr *x509.CertificateRequest, opts SignOptions, claims ...Claim) (*x509.Certificate, *x509.Certificate, error)
|
Sign(cr *x509.CertificateRequest, opts SignOptions, claims ...Claim) (*x509.Certificate, *x509.Certificate, error)
|
||||||
Renew(cert *x509.Certificate) (*x509.Certificate, *x509.Certificate, error)
|
Renew(cert *x509.Certificate) (*x509.Certificate, *x509.Certificate, error)
|
||||||
GetProvisioners() (map[string]*jose.JSONWebKeySet, error)
|
GetProvisioners() ([]*provisioner.Provisioner, error)
|
||||||
GetEncryptedKey(kid string) (string, error)
|
GetEncryptedKey(kid string) (string, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -172,10 +173,16 @@ type SignRequest struct {
|
||||||
NotBefore time.Time `json:"notBefore"`
|
NotBefore time.Time `json:"notBefore"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ProvisionersResponse is the response object that returns the map of
|
// ProvisionersResponse is the response object that returns the list of
|
||||||
// provisioners.
|
// provisioners.
|
||||||
type ProvisionersResponse struct {
|
type ProvisionersResponse struct {
|
||||||
Provisioners map[string]*jose.JSONWebKeySet `json:"provisioners"`
|
Provisioners []*provisioner.Provisioner `json:"provisioners"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// JWKSetByIssuerResponse is the response object that returns the map of
|
||||||
|
// provisioners.
|
||||||
|
type JWKSetByIssuerResponse struct {
|
||||||
|
Map map[string]*jose.JSONWebKeySet `json:"map"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ProvisionerKeyResponse is the response object that returns the encryptoed key
|
// ProvisionerKeyResponse is the response object that returns the encryptoed key
|
||||||
|
@ -250,6 +257,7 @@ func (h *caHandler) Route(r Router) {
|
||||||
r.MethodFunc("POST", "/renew", h.Renew)
|
r.MethodFunc("POST", "/renew", h.Renew)
|
||||||
r.MethodFunc("GET", "/provisioners", h.Provisioners)
|
r.MethodFunc("GET", "/provisioners", h.Provisioners)
|
||||||
r.MethodFunc("GET", "/provisioners/{kid}/encrypted-key", h.ProvisionerKey)
|
r.MethodFunc("GET", "/provisioners/{kid}/encrypted-key", h.ProvisionerKey)
|
||||||
|
r.MethodFunc("GET", "/provisioners/jwk-set-by-issuer", h.JWKSetByIssuer)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Health is an HTTP handler that returns the status of the server.
|
// Health is an HTTP handler that returns the status of the server.
|
||||||
|
@ -353,3 +361,23 @@ func (h *caHandler) ProvisionerKey(w http.ResponseWriter, r *http.Request) {
|
||||||
}
|
}
|
||||||
JSON(w, &ProvisionerKeyResponse{key})
|
JSON(w, &ProvisionerKeyResponse{key})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h *caHandler) JWKSetByIssuer(w http.ResponseWriter, r *http.Request) {
|
||||||
|
m := map[string]*jose.JSONWebKeySet{}
|
||||||
|
ps, err := h.Authority.GetProvisioners()
|
||||||
|
if err != nil {
|
||||||
|
WriteError(w, InternalServerError(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for _, p := range ps {
|
||||||
|
ks, found := m[p.Issuer]
|
||||||
|
if found {
|
||||||
|
ks.Keys = append(ks.Keys, *p.Key)
|
||||||
|
} else {
|
||||||
|
ks = new(jose.JSONWebKeySet)
|
||||||
|
ks.Keys = []jose.JSONWebKey{*p.Key}
|
||||||
|
m[p.Issuer] = ks
|
||||||
|
}
|
||||||
|
}
|
||||||
|
JSON(w, &JWKSetByIssuerResponse{m})
|
||||||
|
}
|
||||||
|
|
104
api/api_test.go
104
api/api_test.go
|
@ -17,6 +17,7 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/go-chi/chi"
|
"github.com/go-chi/chi"
|
||||||
|
"github.com/smallstep/ca-component/provisioner"
|
||||||
"github.com/smallstep/cli/crypto/tlsutil"
|
"github.com/smallstep/cli/crypto/tlsutil"
|
||||||
"github.com/smallstep/cli/jose"
|
"github.com/smallstep/cli/jose"
|
||||||
)
|
)
|
||||||
|
@ -397,7 +398,7 @@ type mockAuthority struct {
|
||||||
root func(shasum string) (*x509.Certificate, error)
|
root func(shasum string) (*x509.Certificate, error)
|
||||||
sign func(cr *x509.CertificateRequest, opts SignOptions, claims ...Claim) (*x509.Certificate, *x509.Certificate, error)
|
sign func(cr *x509.CertificateRequest, opts SignOptions, claims ...Claim) (*x509.Certificate, *x509.Certificate, error)
|
||||||
renew func(cert *x509.Certificate) (*x509.Certificate, *x509.Certificate, error)
|
renew func(cert *x509.Certificate) (*x509.Certificate, *x509.Certificate, error)
|
||||||
getProvisioners func() (map[string]*jose.JSONWebKeySet, error)
|
getProvisioners func() ([]*provisioner.Provisioner, error)
|
||||||
getEncryptedKey func(kid string) (string, error)
|
getEncryptedKey func(kid string) (string, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -444,11 +445,11 @@ func (m *mockAuthority) Renew(cert *x509.Certificate) (*x509.Certificate, *x509.
|
||||||
return m.ret1.(*x509.Certificate), m.ret2.(*x509.Certificate), m.err
|
return m.ret1.(*x509.Certificate), m.ret2.(*x509.Certificate), m.err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *mockAuthority) GetProvisioners() (map[string]*jose.JSONWebKeySet, error) {
|
func (m *mockAuthority) GetProvisioners() ([]*provisioner.Provisioner, error) {
|
||||||
if m.getProvisioners != nil {
|
if m.getProvisioners != nil {
|
||||||
return m.getProvisioners()
|
return m.getProvisioners()
|
||||||
}
|
}
|
||||||
return m.ret1.(map[string]*jose.JSONWebKeySet), m.err
|
return m.ret1.([]*provisioner.Provisioner), m.err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *mockAuthority) GetEncryptedKey(kid string) (string, error) {
|
func (m *mockAuthority) GetEncryptedKey(kid string) (string, error) {
|
||||||
|
@ -670,6 +671,82 @@ func Test_caHandler_Renew(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func Test_caHandler_JWKSetByIssuer(t *testing.T) {
|
||||||
|
type fields struct {
|
||||||
|
Authority Authority
|
||||||
|
}
|
||||||
|
type args struct {
|
||||||
|
w http.ResponseWriter
|
||||||
|
r *http.Request
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := http.NewRequest("GET", "http://example.com/provisioners/jwk-set-by-issuer", nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var key jose.JSONWebKey
|
||||||
|
if err := json.Unmarshal([]byte(pubKey), &key); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
p := []*provisioner.Provisioner{
|
||||||
|
&provisioner.Provisioner{
|
||||||
|
Issuer: "p1",
|
||||||
|
Key: &key,
|
||||||
|
},
|
||||||
|
&provisioner.Provisioner{
|
||||||
|
Issuer: "p2",
|
||||||
|
Key: &key,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
fields fields
|
||||||
|
args args
|
||||||
|
statusCode int
|
||||||
|
}{
|
||||||
|
{"ok", fields{&mockAuthority{ret1: p}}, args{httptest.NewRecorder(), req}, 200},
|
||||||
|
{"fail", fields{&mockAuthority{ret1: p, err: fmt.Errorf("the error")}}, args{httptest.NewRecorder(), req}, 500},
|
||||||
|
}
|
||||||
|
|
||||||
|
expectedKey, err := json.Marshal(key)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
expected := []byte(`{"map":{"p1":{"keys":[` + string(expectedKey) + `]},"p2":{"keys":[` + string(expectedKey) + `]}}}`)
|
||||||
|
expectedError := []byte(`{"status":500,"message":"Internal Server Error"}`)
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
h := &caHandler{
|
||||||
|
Authority: tt.fields.Authority,
|
||||||
|
}
|
||||||
|
h.JWKSetByIssuer(tt.args.w, tt.args.r)
|
||||||
|
|
||||||
|
rec := tt.args.w.(*httptest.ResponseRecorder)
|
||||||
|
res := rec.Result()
|
||||||
|
if res.StatusCode != tt.statusCode {
|
||||||
|
t.Errorf("caHandler.JWKSetByIssuer StatusCode = %d, wants %d", res.StatusCode, tt.statusCode)
|
||||||
|
}
|
||||||
|
body, err := ioutil.ReadAll(res.Body)
|
||||||
|
res.Body.Close()
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("caHandler.JWKSetByIssuer unexpected error = %v", err)
|
||||||
|
}
|
||||||
|
if tt.statusCode < http.StatusBadRequest {
|
||||||
|
if !bytes.Equal(bytes.TrimSpace(body), expected) {
|
||||||
|
t.Errorf("caHandler.JWKSetByIssuer Body = %s, wants %s", body, expected)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if !bytes.Equal(bytes.TrimSpace(body), expectedError) {
|
||||||
|
t.Errorf("caHandler.JWKSetByIssuer Body = %s, wants %s", body, expectedError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func Test_caHandler_Provisioners(t *testing.T) {
|
func Test_caHandler_Provisioners(t *testing.T) {
|
||||||
type fields struct {
|
type fields struct {
|
||||||
Authority Authority
|
Authority Authority
|
||||||
|
@ -689,14 +766,21 @@ func Test_caHandler_Provisioners(t *testing.T) {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
p := map[string]*jose.JSONWebKeySet{
|
p := []*provisioner.Provisioner{
|
||||||
"p1": &jose.JSONWebKeySet{
|
&provisioner.Provisioner{
|
||||||
Keys: []jose.JSONWebKey{key},
|
Type: "JWK",
|
||||||
|
Issuer: "max",
|
||||||
|
EncryptedKey: "abc",
|
||||||
|
Key: &key,
|
||||||
},
|
},
|
||||||
"p2": &jose.JSONWebKeySet{
|
&provisioner.Provisioner{
|
||||||
Keys: []jose.JSONWebKey{key},
|
Type: "JWK",
|
||||||
|
Issuer: "mariano",
|
||||||
|
EncryptedKey: "def",
|
||||||
|
Key: &key,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
pr := ProvisionersResponse{p}
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
|
@ -708,11 +792,11 @@ func Test_caHandler_Provisioners(t *testing.T) {
|
||||||
{"fail", fields{&mockAuthority{ret1: p, err: fmt.Errorf("the error")}}, args{httptest.NewRecorder(), req}, 500},
|
{"fail", fields{&mockAuthority{ret1: p, err: fmt.Errorf("the error")}}, args{httptest.NewRecorder(), req}, 500},
|
||||||
}
|
}
|
||||||
|
|
||||||
expectedKey, err := json.Marshal(key)
|
expected, err := json.Marshal(pr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
expected := []byte(`{"provisioners":{"p1":{"keys":[` + string(expectedKey) + `]},"p2":{"keys":[` + string(expectedKey) + `]}}}`)
|
|
||||||
expectedError := []byte(`{"status":500,"message":"Internal Server Error"}`)
|
expectedError := []byte(`{"status":500,"message":"Internal Server Error"}`)
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
|
|
@ -7,6 +7,7 @@ import (
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/smallstep/assert"
|
"github.com/smallstep/assert"
|
||||||
|
"github.com/smallstep/ca-component/provisioner"
|
||||||
stepJOSE "github.com/smallstep/cli/jose"
|
stepJOSE "github.com/smallstep/cli/jose"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -15,7 +16,7 @@ func testAuthority(t *testing.T) *Authority {
|
||||||
assert.FatalError(t, err)
|
assert.FatalError(t, err)
|
||||||
clijwk, err := stepJOSE.ParseKey("testdata/secrets/step_cli_key_pub.jwk")
|
clijwk, err := stepJOSE.ParseKey("testdata/secrets/step_cli_key_pub.jwk")
|
||||||
assert.FatalError(t, err)
|
assert.FatalError(t, err)
|
||||||
p := []*Provisioner{
|
p := []*provisioner.Provisioner{
|
||||||
{
|
{
|
||||||
Issuer: "Max",
|
Issuer: "Max",
|
||||||
Type: "JWK",
|
Type: "JWK",
|
||||||
|
|
|
@ -6,6 +6,7 @@ import (
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/smallstep/ca-component/api"
|
"github.com/smallstep/ca-component/api"
|
||||||
|
"github.com/smallstep/ca-component/provisioner"
|
||||||
"gopkg.in/square/go-jose.v2/jwt"
|
"gopkg.in/square/go-jose.v2/jwt"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -63,7 +64,7 @@ func (a *Authority) Authorize(ott string) ([]api.Claim, error) {
|
||||||
return nil, &apiError{errors.Errorf("Provisioner with KeyID %s could not be found", kid),
|
return nil, &apiError{errors.Errorf("Provisioner with KeyID %s could not be found", kid),
|
||||||
http.StatusUnauthorized, errContext}
|
http.StatusUnauthorized, errContext}
|
||||||
}
|
}
|
||||||
p, ok := val.(*Provisioner)
|
p, ok := val.(*provisioner.Provisioner)
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, &apiError{errors.Errorf("stored value is not a *Provisioner"),
|
return nil, &apiError{errors.Errorf("stored value is not a *Provisioner"),
|
||||||
http.StatusInternalServerError, context{}}
|
http.StatusInternalServerError, context{}}
|
||||||
|
|
|
@ -6,9 +6,9 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
|
"github.com/smallstep/ca-component/provisioner"
|
||||||
"github.com/smallstep/cli/crypto/tlsutil"
|
"github.com/smallstep/cli/crypto/tlsutil"
|
||||||
"github.com/smallstep/cli/crypto/x509util"
|
"github.com/smallstep/cli/crypto/x509util"
|
||||||
jose "gopkg.in/square/go-jose.v2"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// DefaultTLSOptions represents the default TLS version as well as the cipher
|
// DefaultTLSOptions represents the default TLS version as well as the cipher
|
||||||
|
@ -51,30 +51,6 @@ func (d *duration) UnmarshalJSON(data []byte) (err error) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Provisioner - authorized entity that can sign tokens necessary for signature requests.
|
|
||||||
type Provisioner struct {
|
|
||||||
Issuer string `json:"issuer,omitempty"`
|
|
||||||
Type string `json:"type,omitempty"`
|
|
||||||
Key *jose.JSONWebKey `json:"key,omitempty"`
|
|
||||||
EncryptedKey string `json:"encryptedKey,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate validates a provisioner.
|
|
||||||
func (p *Provisioner) Validate() error {
|
|
||||||
switch {
|
|
||||||
case p.Issuer == "":
|
|
||||||
return errors.New("provisioner issuer cannot be empty")
|
|
||||||
|
|
||||||
case p.Type == "":
|
|
||||||
return errors.New("provisioner type cannot be empty")
|
|
||||||
|
|
||||||
case p.Key == nil:
|
|
||||||
return errors.New("provisioner key cannot be empty")
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Config represents the CA configuration and it's mapped to a JSON object.
|
// Config represents the CA configuration and it's mapped to a JSON object.
|
||||||
type Config struct {
|
type Config struct {
|
||||||
Root string `json:"root"`
|
Root string `json:"root"`
|
||||||
|
@ -91,7 +67,7 @@ type Config struct {
|
||||||
|
|
||||||
// AuthConfig represents the configuration options for the authority.
|
// AuthConfig represents the configuration options for the authority.
|
||||||
type AuthConfig struct {
|
type AuthConfig struct {
|
||||||
Provisioners []*Provisioner `json:"provisioners,omitempty"`
|
Provisioners []*provisioner.Provisioner `json:"provisioners,omitempty"`
|
||||||
Template *x509util.ASN1DN `json:"template,omitempty"`
|
Template *x509util.ASN1DN `json:"template,omitempty"`
|
||||||
MinCertDuration *duration `json:"minCertDuration,omitempty"`
|
MinCertDuration *duration `json:"minCertDuration,omitempty"`
|
||||||
MaxCertDuration *duration `json:"maxCertDuration,omitempty"`
|
MaxCertDuration *duration `json:"maxCertDuration,omitempty"`
|
||||||
|
|
|
@ -5,65 +5,20 @@ import (
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/smallstep/assert"
|
"github.com/smallstep/assert"
|
||||||
|
"github.com/smallstep/ca-component/provisioner"
|
||||||
"github.com/smallstep/cli/crypto/tlsutil"
|
"github.com/smallstep/cli/crypto/tlsutil"
|
||||||
"github.com/smallstep/cli/crypto/x509util"
|
"github.com/smallstep/cli/crypto/x509util"
|
||||||
stepJOSE "github.com/smallstep/cli/jose"
|
stepJOSE "github.com/smallstep/cli/jose"
|
||||||
jose "gopkg.in/square/go-jose.v2"
|
jose "gopkg.in/square/go-jose.v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestProvisionerValidate(t *testing.T) {
|
|
||||||
type ProvisionerValidateTest struct {
|
|
||||||
p *Provisioner
|
|
||||||
err error
|
|
||||||
}
|
|
||||||
tests := map[string]func(*testing.T) ProvisionerValidateTest{
|
|
||||||
"fail-empty-issuer": func(t *testing.T) ProvisionerValidateTest {
|
|
||||||
return ProvisionerValidateTest{
|
|
||||||
p: &Provisioner{},
|
|
||||||
err: errors.New("provisioner issuer cannot be empty"),
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"fail-empty-type": func(t *testing.T) ProvisionerValidateTest {
|
|
||||||
return ProvisionerValidateTest{
|
|
||||||
p: &Provisioner{Issuer: "foo"},
|
|
||||||
err: errors.New("provisioner type cannot be empty"),
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"fail-empty-key": func(t *testing.T) ProvisionerValidateTest {
|
|
||||||
return ProvisionerValidateTest{
|
|
||||||
p: &Provisioner{Issuer: "foo", Type: "bar"},
|
|
||||||
err: errors.New("provisioner key cannot be empty"),
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"ok": func(t *testing.T) ProvisionerValidateTest {
|
|
||||||
return ProvisionerValidateTest{
|
|
||||||
p: &Provisioner{Issuer: "foo", Type: "bar", Key: &jose.JSONWebKey{}},
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for name, get := range tests {
|
|
||||||
t.Run(name, func(t *testing.T) {
|
|
||||||
tc := get(t)
|
|
||||||
err := tc.p.Validate()
|
|
||||||
if err != nil {
|
|
||||||
if assert.NotNil(t, tc.err) {
|
|
||||||
assert.Equals(t, tc.err.Error(), err.Error())
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
assert.Nil(t, tc.err)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestConfigValidate(t *testing.T) {
|
func TestConfigValidate(t *testing.T) {
|
||||||
maxjwk, err := stepJOSE.ParseKey("testdata/secrets/max_pub.jwk")
|
maxjwk, err := stepJOSE.ParseKey("testdata/secrets/max_pub.jwk")
|
||||||
assert.FatalError(t, err)
|
assert.FatalError(t, err)
|
||||||
clijwk, err := stepJOSE.ParseKey("testdata/secrets/step_cli_key_pub.jwk")
|
clijwk, err := stepJOSE.ParseKey("testdata/secrets/step_cli_key_pub.jwk")
|
||||||
assert.FatalError(t, err)
|
assert.FatalError(t, err)
|
||||||
ac := &AuthConfig{
|
ac := &AuthConfig{
|
||||||
Provisioners: []*Provisioner{
|
Provisioners: []*provisioner.Provisioner{
|
||||||
{
|
{
|
||||||
Issuer: "Max",
|
Issuer: "Max",
|
||||||
Type: "JWK",
|
Type: "JWK",
|
||||||
|
@ -261,7 +216,7 @@ func TestAuthConfigValidate(t *testing.T) {
|
||||||
assert.FatalError(t, err)
|
assert.FatalError(t, err)
|
||||||
clijwk, err := stepJOSE.ParseKey("testdata/secrets/step_cli_key_pub.jwk")
|
clijwk, err := stepJOSE.ParseKey("testdata/secrets/step_cli_key_pub.jwk")
|
||||||
assert.FatalError(t, err)
|
assert.FatalError(t, err)
|
||||||
p := []*Provisioner{
|
p := []*provisioner.Provisioner{
|
||||||
{
|
{
|
||||||
Issuer: "Max",
|
Issuer: "Max",
|
||||||
Type: "JWK",
|
Type: "JWK",
|
||||||
|
@ -295,9 +250,9 @@ func TestAuthConfigValidate(t *testing.T) {
|
||||||
"fail-invalid-provisioners": func(t *testing.T) AuthConfigValidateTest {
|
"fail-invalid-provisioners": func(t *testing.T) AuthConfigValidateTest {
|
||||||
return AuthConfigValidateTest{
|
return AuthConfigValidateTest{
|
||||||
ac: &AuthConfig{
|
ac: &AuthConfig{
|
||||||
Provisioners: []*Provisioner{
|
Provisioners: []*provisioner.Provisioner{
|
||||||
&Provisioner{Issuer: "foo", Type: "bar", Key: &jose.JSONWebKey{}},
|
{Issuer: "foo", Type: "bar", Key: &jose.JSONWebKey{}},
|
||||||
&Provisioner{Issuer: "foo", Key: &jose.JSONWebKey{}},
|
{Issuer: "foo", Key: &jose.JSONWebKey{}},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
err: errors.New("provisioner type cannot be empty"),
|
err: errors.New("provisioner type cannot be empty"),
|
||||||
|
|
|
@ -1,11 +1,10 @@
|
||||||
package authority
|
package authority
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"log"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/smallstep/cli/jose"
|
"github.com/smallstep/ca-component/provisioner"
|
||||||
)
|
)
|
||||||
|
|
||||||
// GetEncryptedKey returns the JWE key corresponding to the given kid argument.
|
// GetEncryptedKey returns the JWE key corresponding to the given kid argument.
|
||||||
|
@ -26,24 +25,6 @@ func (a *Authority) GetEncryptedKey(kid string) (string, error) {
|
||||||
|
|
||||||
// GetProvisioners returns a map listing each provisioner and the JWK Key Set
|
// GetProvisioners returns a map listing each provisioner and the JWK Key Set
|
||||||
// with their public keys.
|
// with their public keys.
|
||||||
func (a *Authority) GetProvisioners() (map[string]*jose.JSONWebKeySet, error) {
|
func (a *Authority) GetProvisioners() ([]*provisioner.Provisioner, error) {
|
||||||
pks := map[string]*jose.JSONWebKeySet{}
|
return a.config.AuthorityConfig.Provisioners, nil
|
||||||
a.provisionerIDIndex.Range(func(key, val interface{}) bool {
|
|
||||||
p, ok := val.(*Provisioner)
|
|
||||||
if !ok {
|
|
||||||
log.Printf("authority.GetProvisioners: expected type *Provisioner, but got %T\n", val)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
ks, found := pks[p.Issuer]
|
|
||||||
if found {
|
|
||||||
ks.Keys = append(ks.Keys, *p.Key)
|
|
||||||
} else {
|
|
||||||
ks = new(jose.JSONWebKeySet)
|
|
||||||
ks.Keys = []jose.JSONWebKey{*p.Key}
|
|
||||||
pks[p.Issuer] = ks
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
})
|
|
||||||
return pks, nil
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,7 +6,7 @@ import (
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/smallstep/assert"
|
"github.com/smallstep/assert"
|
||||||
"github.com/smallstep/cli/jose"
|
"github.com/smallstep/ca-component/provisioner"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestGetEncryptedKey(t *testing.T) {
|
func TestGetEncryptedKey(t *testing.T) {
|
||||||
|
@ -73,7 +73,7 @@ func TestGetEncryptedKey(t *testing.T) {
|
||||||
if assert.Nil(t, tc.err) {
|
if assert.Nil(t, tc.err) {
|
||||||
val, ok := tc.a.provisionerIDIndex.Load(tc.kid)
|
val, ok := tc.a.provisionerIDIndex.Load(tc.kid)
|
||||||
assert.Fatal(t, ok)
|
assert.Fatal(t, ok)
|
||||||
p, ok := val.(*Provisioner)
|
p, ok := val.(*provisioner.Provisioner)
|
||||||
assert.Fatal(t, ok)
|
assert.Fatal(t, ok)
|
||||||
assert.Equals(t, p.EncryptedKey, ek)
|
assert.Equals(t, p.EncryptedKey, ek)
|
||||||
}
|
}
|
||||||
|
@ -115,19 +115,7 @@ func TestGetProvisioners(t *testing.T) {
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if assert.Nil(t, tc.err) {
|
if assert.Nil(t, tc.err) {
|
||||||
var cps = tc.a.config.AuthorityConfig.Provisioners
|
assert.Equals(t, ps, tc.a.config.AuthorityConfig.Provisioners)
|
||||||
|
|
||||||
maxks, found := ps["max"]
|
|
||||||
assert.Fatal(t, found)
|
|
||||||
assert.Equals(t, maxks.Keys, []jose.JSONWebKey{*cps[0].Key, *cps[1].Key})
|
|
||||||
|
|
||||||
marianoks, found := ps["mariano"]
|
|
||||||
assert.Fatal(t, found)
|
|
||||||
assert.Equals(t, marianoks.Keys, []jose.JSONWebKey{*cps[3].Key, *cps[4].Key})
|
|
||||||
|
|
||||||
stepcliks, found := ps["step-cli"]
|
|
||||||
assert.Fatal(t, found)
|
|
||||||
assert.Equals(t, stepcliks.Keys, []jose.JSONWebKey{*cps[2].Key})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
@ -298,16 +298,65 @@ func TestCAProvisioners(t *testing.T) {
|
||||||
|
|
||||||
tc.ca.srv.Handler.ServeHTTP(rr, rq)
|
tc.ca.srv.Handler.ServeHTTP(rr, rq)
|
||||||
|
|
||||||
|
if assert.Equals(t, rr.Code, tc.status) {
|
||||||
|
body := &ClosingBuffer{rr.Body}
|
||||||
|
if rr.Code < http.StatusBadRequest {
|
||||||
|
var resp api.ProvisionersResponse
|
||||||
|
|
||||||
|
assert.FatalError(t, readJSON(body, &resp))
|
||||||
|
assert.Equals(t, config.AuthorityConfig.Provisioners, resp.Provisioners)
|
||||||
|
} else {
|
||||||
|
err := readError(body)
|
||||||
|
if len(tc.errMsg) == 0 {
|
||||||
|
assert.FatalError(t, errors.New("must validate response error"))
|
||||||
|
}
|
||||||
|
assert.HasPrefix(t, err.Error(), tc.errMsg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCAJWKSetByIssuer(t *testing.T) {
|
||||||
|
config, err := authority.LoadConfiguration("testdata/ca.json")
|
||||||
|
assert.FatalError(t, err)
|
||||||
|
ca, err := New(config)
|
||||||
|
assert.FatalError(t, err)
|
||||||
|
|
||||||
|
type ekt struct {
|
||||||
|
ca *CA
|
||||||
|
status int
|
||||||
|
errMsg string
|
||||||
|
}
|
||||||
|
tests := map[string]func(t *testing.T) *ekt{
|
||||||
|
"ok": func(t *testing.T) *ekt {
|
||||||
|
return &ekt{
|
||||||
|
ca: ca,
|
||||||
|
status: http.StatusOK,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, genTestCase := range tests {
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
tc := genTestCase(t)
|
||||||
|
|
||||||
|
rq, err := http.NewRequest("GET", fmt.Sprintf("/provisioners/jwk-set-by-issuer"), strings.NewReader(""))
|
||||||
|
assert.FatalError(t, err)
|
||||||
|
rr := httptest.NewRecorder()
|
||||||
|
|
||||||
|
tc.ca.srv.Handler.ServeHTTP(rr, rq)
|
||||||
|
|
||||||
if assert.Equals(t, rr.Code, tc.status) {
|
if assert.Equals(t, rr.Code, tc.status) {
|
||||||
body := &ClosingBuffer{rr.Body}
|
body := &ClosingBuffer{rr.Body}
|
||||||
if rr.Code < http.StatusBadRequest {
|
if rr.Code < http.StatusBadRequest {
|
||||||
var (
|
var (
|
||||||
resp api.ProvisionersResponse
|
resp api.JWKSetByIssuerResponse
|
||||||
psList = config.AuthorityConfig.Provisioners
|
psList = config.AuthorityConfig.Provisioners
|
||||||
)
|
)
|
||||||
|
|
||||||
assert.FatalError(t, readJSON(body, &resp))
|
assert.FatalError(t, readJSON(body, &resp))
|
||||||
psMap := resp.Provisioners
|
psMap := resp.Map
|
||||||
|
|
||||||
maxks, found := psMap["max"]
|
maxks, found := psMap["max"]
|
||||||
assert.Fatal(t, found)
|
assert.Fatal(t, found)
|
||||||
|
|
|
@ -13,7 +13,7 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/smallstep/ca-component/api"
|
"github.com/smallstep/ca-component/api"
|
||||||
"github.com/smallstep/cli/jose"
|
"github.com/smallstep/ca-component/provisioner"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@ -388,6 +388,66 @@ func TestClient_Renew(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestClient_Provisioners(t *testing.T) {
|
||||||
|
ok := &api.ProvisionersResponse{
|
||||||
|
Provisioners: []*provisioner.Provisioner{},
|
||||||
|
}
|
||||||
|
internalServerError := api.InternalServerError(fmt.Errorf("Internal Server Error"))
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
response interface{}
|
||||||
|
responseCode int
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{"ok", ok, 200, false},
|
||||||
|
{"fail", internalServerError, 500, true},
|
||||||
|
}
|
||||||
|
|
||||||
|
srv := httptest.NewServer(nil)
|
||||||
|
defer srv.Close()
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
c, err := NewClient(srv.URL, WithTransport(http.DefaultTransport))
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("NewClient() error = %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
srv.Config.Handler = http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||||||
|
expected := "/provisioners"
|
||||||
|
if req.RequestURI != expected {
|
||||||
|
t.Errorf("RequestURI = %s, want %s", req.RequestURI, expected)
|
||||||
|
}
|
||||||
|
w.WriteHeader(tt.responseCode)
|
||||||
|
api.JSON(w, tt.response)
|
||||||
|
})
|
||||||
|
|
||||||
|
got, err := c.Provisioners()
|
||||||
|
if (err != nil) != tt.wantErr {
|
||||||
|
t.Errorf("Client.Provisioners() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case err != nil:
|
||||||
|
if got != nil {
|
||||||
|
t.Errorf("Client.Provisioners() = %v, want nil", got)
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(err, tt.response) {
|
||||||
|
t.Errorf("Client.Provisioners() error = %v, want %v", err, tt.response)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
if !reflect.DeepEqual(got, tt.response) {
|
||||||
|
t.Errorf("Client.Provisioners() = %v, want %v", got, tt.response)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
func TestClient_Provisioners(t *testing.T) {
|
func TestClient_Provisioners(t *testing.T) {
|
||||||
ok := &api.ProvisionersResponse{
|
ok := &api.ProvisionersResponse{
|
||||||
Provisioners: map[string]*jose.JSONWebKeySet{},
|
Provisioners: map[string]*jose.JSONWebKeySet{},
|
||||||
|
@ -446,6 +506,7 @@ func TestClient_Provisioners(t *testing.T) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
func TestClient_ProvisionerKey(t *testing.T) {
|
func TestClient_ProvisionerKey(t *testing.T) {
|
||||||
ok := &api.ProvisionerKeyResponse{
|
ok := &api.ProvisionerKeyResponse{
|
||||||
|
|
31
provisioner/provisioner.go
Normal file
31
provisioner/provisioner.go
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
package provisioner
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
jose "gopkg.in/square/go-jose.v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Provisioner - authorized entity that can sign tokens necessary for signature requests.
|
||||||
|
type Provisioner struct {
|
||||||
|
Issuer string `json:"issuer,omitempty"`
|
||||||
|
Type string `json:"type,omitempty"`
|
||||||
|
Key *jose.JSONWebKey `json:"key,omitempty"`
|
||||||
|
EncryptedKey string `json:"encryptedKey,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate validates a provisioner.
|
||||||
|
func (p *Provisioner) Validate() error {
|
||||||
|
switch {
|
||||||
|
case p.Issuer == "":
|
||||||
|
return errors.New("provisioner issuer cannot be empty")
|
||||||
|
|
||||||
|
case p.Type == "":
|
||||||
|
return errors.New("provisioner type cannot be empty")
|
||||||
|
|
||||||
|
case p.Key == nil:
|
||||||
|
return errors.New("provisioner key cannot be empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
55
provisioner/provisioner_test.go
Normal file
55
provisioner/provisioner_test.go
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
package provisioner
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/smallstep/assert"
|
||||||
|
jose "gopkg.in/square/go-jose.v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestProvisionerValidate(t *testing.T) {
|
||||||
|
type ProvisionerValidateTest struct {
|
||||||
|
p *Provisioner
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
tests := map[string]func(*testing.T) ProvisionerValidateTest{
|
||||||
|
"fail-empty-issuer": func(t *testing.T) ProvisionerValidateTest {
|
||||||
|
return ProvisionerValidateTest{
|
||||||
|
p: &Provisioner{},
|
||||||
|
err: errors.New("provisioner issuer cannot be empty"),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"fail-empty-type": func(t *testing.T) ProvisionerValidateTest {
|
||||||
|
return ProvisionerValidateTest{
|
||||||
|
p: &Provisioner{Issuer: "foo"},
|
||||||
|
err: errors.New("provisioner type cannot be empty"),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"fail-empty-key": func(t *testing.T) ProvisionerValidateTest {
|
||||||
|
return ProvisionerValidateTest{
|
||||||
|
p: &Provisioner{Issuer: "foo", Type: "bar"},
|
||||||
|
err: errors.New("provisioner key cannot be empty"),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ok": func(t *testing.T) ProvisionerValidateTest {
|
||||||
|
return ProvisionerValidateTest{
|
||||||
|
p: &Provisioner{Issuer: "foo", Type: "bar", Key: &jose.JSONWebKey{}},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, get := range tests {
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
tc := get(t)
|
||||||
|
err := tc.p.Validate()
|
||||||
|
if err != nil {
|
||||||
|
if assert.NotNil(t, tc.err) {
|
||||||
|
assert.Equals(t, tc.err.Error(), err.Error())
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
assert.Nil(t, tc.err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue