forked from TrueCloudLab/certificates
7e95fc0e45
Services might have proxies behind them so we cannot rely on them. Fixes #17
312 lines
9.4 KiB
Go
312 lines
9.4 KiB
Go
package authority
|
|
|
|
import (
|
|
"net/http"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/pkg/errors"
|
|
"github.com/smallstep/assert"
|
|
"github.com/smallstep/cli/crypto/keys"
|
|
stepJOSE "github.com/smallstep/cli/jose"
|
|
jose "gopkg.in/square/go-jose.v2"
|
|
"gopkg.in/square/go-jose.v2/jwt"
|
|
)
|
|
|
|
func TestMatchesAudience(t *testing.T) {
|
|
type matchesTest struct {
|
|
a, b []string
|
|
exp bool
|
|
}
|
|
tests := map[string]matchesTest{
|
|
"false arg1 empty": {
|
|
a: []string{},
|
|
b: []string{"https://127.0.0.1:0/sign", "https://test.ca.smallstep.com/sign"},
|
|
exp: false,
|
|
},
|
|
"false arg2 empty": {
|
|
a: []string{"https://127.0.0.1:0/sign", "https://test.ca.smallstep.com/sign"},
|
|
b: []string{},
|
|
exp: false,
|
|
},
|
|
"false arg1,arg2 empty": {
|
|
a: []string{"https://127.0.0.1:0/sign", "https://test.ca.smallstep.com/sign"},
|
|
b: []string{"step-gateway", "step-cli"},
|
|
exp: false,
|
|
},
|
|
"false": {
|
|
a: []string{"step-gateway", "step-cli"},
|
|
b: []string{"https://127.0.0.1:0/sign", "https://test.ca.smallstep.com/sign"},
|
|
exp: false,
|
|
},
|
|
"true": {
|
|
a: []string{"step-gateway", "https://test.ca.smallstep.com/sign"},
|
|
b: []string{"https://127.0.0.1:0/sign", "https://test.ca.smallstep.com/sign"},
|
|
exp: true,
|
|
},
|
|
"true,portsA": {
|
|
a: []string{"step-gateway", "https://test.ca.smallstep.com:9000/sign"},
|
|
b: []string{"https://127.0.0.1:0/sign", "https://test.ca.smallstep.com/sign"},
|
|
exp: true,
|
|
},
|
|
"true,portsB": {
|
|
a: []string{"step-gateway", "https://test.ca.smallstep.com/sign"},
|
|
b: []string{"https://127.0.0.1:0/sign", "https://test.ca.smallstep.com:9000/sign"},
|
|
exp: true,
|
|
},
|
|
"true,portsAB": {
|
|
a: []string{"step-gateway", "https://test.ca.smallstep.com:9000/sign"},
|
|
b: []string{"https://127.0.0.1:0/sign", "https://test.ca.smallstep.com:8000/sign"},
|
|
exp: true,
|
|
},
|
|
}
|
|
for name, tc := range tests {
|
|
t.Run(name, func(t *testing.T) {
|
|
assert.Equals(t, tc.exp, matchesAudience(tc.a, tc.b))
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestStripPort(t *testing.T) {
|
|
type args struct {
|
|
rawurl string
|
|
}
|
|
tests := []struct {
|
|
name string
|
|
args args
|
|
want string
|
|
}{
|
|
{"with port", args{"https://ca.smallstep.com:9000/sign"}, "https://ca.smallstep.com/sign"},
|
|
{"with no port", args{"https://ca.smallstep.com/sign/"}, "https://ca.smallstep.com/sign/"},
|
|
{"bad url", args{"https://a bad url:9000"}, "https://a bad url:9000"},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
if got := stripPort(tt.args.rawurl); got != tt.want {
|
|
t.Errorf("stripPort() = %v, want %v", got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestAuthorize(t *testing.T) {
|
|
a := testAuthority(t)
|
|
jwk, err := stepJOSE.ParseKey("testdata/secrets/step_cli_key_priv.jwk",
|
|
stepJOSE.WithPassword([]byte("pass")))
|
|
assert.FatalError(t, err)
|
|
|
|
sig, err := jose.NewSigner(jose.SigningKey{Algorithm: jose.ES256, Key: jwk.Key},
|
|
(&jose.SignerOptions{}).WithType("JWT").WithHeader("kid", jwk.KeyID))
|
|
assert.FatalError(t, err)
|
|
|
|
now := time.Now()
|
|
|
|
validIssuer := "step-cli"
|
|
validAudience := []string{"https://test.ca.smallstep.com/sign"}
|
|
|
|
type authorizeTest struct {
|
|
auth *Authority
|
|
ott string
|
|
err *apiError
|
|
res []interface{}
|
|
}
|
|
tests := map[string]func(t *testing.T) *authorizeTest{
|
|
"fail invalid ott": func(t *testing.T) *authorizeTest {
|
|
return &authorizeTest{
|
|
auth: a,
|
|
ott: "foo",
|
|
err: &apiError{errors.New("authorize: error parsing token"),
|
|
http.StatusUnauthorized, context{"ott": "foo"}},
|
|
}
|
|
},
|
|
"fail empty key id": func(t *testing.T) *authorizeTest {
|
|
_sig, err := jose.NewSigner(jose.SigningKey{Algorithm: jose.ES256, Key: jwk.Key},
|
|
(&jose.SignerOptions{}).WithType("JWT"))
|
|
assert.FatalError(t, err)
|
|
cl := jwt.Claims{
|
|
Subject: "test.smallstep.com",
|
|
Issuer: validIssuer,
|
|
NotBefore: jwt.NewNumericDate(now),
|
|
Expiry: jwt.NewNumericDate(now.Add(time.Minute)),
|
|
Audience: validAudience,
|
|
ID: "43",
|
|
}
|
|
raw, err := jwt.Signed(_sig).Claims(cl).CompactSerialize()
|
|
assert.FatalError(t, err)
|
|
return &authorizeTest{
|
|
auth: a,
|
|
ott: raw,
|
|
err: &apiError{errors.New("authorize: token KeyID cannot be empty"),
|
|
http.StatusUnauthorized, context{"ott": raw}},
|
|
}
|
|
},
|
|
"fail provisioner not found": func(t *testing.T) *authorizeTest {
|
|
_sig, err := jose.NewSigner(jose.SigningKey{Algorithm: jose.ES256, Key: jwk.Key},
|
|
(&jose.SignerOptions{}).WithType("JWT").WithHeader("kid", "foo"))
|
|
assert.FatalError(t, err)
|
|
|
|
cl := jwt.Claims{
|
|
Subject: "test.smallstep.com",
|
|
Issuer: validIssuer,
|
|
NotBefore: jwt.NewNumericDate(now),
|
|
Expiry: jwt.NewNumericDate(now.Add(time.Minute)),
|
|
Audience: validAudience,
|
|
ID: "43",
|
|
}
|
|
raw, err := jwt.Signed(_sig).Claims(cl).CompactSerialize()
|
|
assert.FatalError(t, err)
|
|
return &authorizeTest{
|
|
auth: a,
|
|
ott: raw,
|
|
err: &apiError{errors.New("authorize: provisioner with id step-cli:foo not found"),
|
|
http.StatusUnauthorized, context{"ott": raw}},
|
|
}
|
|
},
|
|
"fail invalid provisioner": func(t *testing.T) *authorizeTest {
|
|
_a := testAuthority(t)
|
|
|
|
_sig, err := jose.NewSigner(jose.SigningKey{Algorithm: jose.ES256, Key: jwk.Key},
|
|
(&jose.SignerOptions{}).WithType("JWT").WithHeader("kid", "foo"))
|
|
assert.FatalError(t, err)
|
|
|
|
_a.provisionerIDIndex.Store(validIssuer+":foo", "42")
|
|
|
|
cl := jwt.Claims{
|
|
Subject: "test.smallstep.com",
|
|
Issuer: validIssuer,
|
|
NotBefore: jwt.NewNumericDate(now),
|
|
Expiry: jwt.NewNumericDate(now.Add(time.Minute)),
|
|
Audience: validAudience,
|
|
ID: "43",
|
|
}
|
|
raw, err := jwt.Signed(_sig).Claims(cl).CompactSerialize()
|
|
assert.FatalError(t, err)
|
|
return &authorizeTest{
|
|
auth: _a,
|
|
ott: raw,
|
|
err: &apiError{errors.New("authorize: invalid provisioner type"),
|
|
http.StatusInternalServerError, context{"ott": raw}},
|
|
}
|
|
},
|
|
"fail invalid issuer": func(t *testing.T) *authorizeTest {
|
|
cl := jwt.Claims{
|
|
Subject: "subject",
|
|
Issuer: "invalid-issuer",
|
|
NotBefore: jwt.NewNumericDate(now),
|
|
Expiry: jwt.NewNumericDate(now.Add(time.Minute)),
|
|
Audience: validAudience,
|
|
}
|
|
raw, err := jwt.Signed(sig).Claims(cl).CompactSerialize()
|
|
assert.FatalError(t, err)
|
|
return &authorizeTest{
|
|
auth: a,
|
|
ott: raw,
|
|
err: &apiError{errors.New("authorize: provisioner with id invalid-issuer:4UELJx8e0aS9m0CH3fZ0EB7D5aUPICb759zALHFejvc not found"),
|
|
http.StatusUnauthorized, context{"ott": raw}},
|
|
}
|
|
},
|
|
"fail empty subject": func(t *testing.T) *authorizeTest {
|
|
cl := jwt.Claims{
|
|
Subject: "",
|
|
Issuer: validIssuer,
|
|
NotBefore: jwt.NewNumericDate(now),
|
|
Expiry: jwt.NewNumericDate(now.Add(time.Minute)),
|
|
Audience: validAudience,
|
|
}
|
|
raw, err := jwt.Signed(sig).Claims(cl).CompactSerialize()
|
|
assert.FatalError(t, err)
|
|
return &authorizeTest{
|
|
auth: a,
|
|
ott: raw,
|
|
err: &apiError{errors.New("authorize: token subject cannot be empty"),
|
|
http.StatusUnauthorized, context{"ott": raw}},
|
|
}
|
|
},
|
|
"fail verify-sig-failure": func(t *testing.T) *authorizeTest {
|
|
_, priv2, err := keys.GenerateDefaultKeyPair()
|
|
assert.FatalError(t, err)
|
|
invalidKeySig, err := jose.NewSigner(jose.SigningKey{
|
|
Algorithm: jose.ES256,
|
|
Key: priv2,
|
|
}, (&jose.SignerOptions{}).WithType("JWT").WithHeader("kid", jwk.KeyID))
|
|
assert.FatalError(t, err)
|
|
cl := jwt.Claims{
|
|
Subject: "test.smallstep.com",
|
|
Issuer: validIssuer,
|
|
NotBefore: jwt.NewNumericDate(now),
|
|
Expiry: jwt.NewNumericDate(now.Add(time.Minute)),
|
|
Audience: validAudience,
|
|
}
|
|
raw, err := jwt.Signed(invalidKeySig).Claims(cl).CompactSerialize()
|
|
assert.FatalError(t, err)
|
|
return &authorizeTest{
|
|
auth: a,
|
|
ott: raw,
|
|
err: &apiError{errors.New("square/go-jose: error in cryptographic primitive"),
|
|
http.StatusUnauthorized, context{"ott": raw}},
|
|
}
|
|
},
|
|
"fail token-already-used": func(t *testing.T) *authorizeTest {
|
|
cl := jwt.Claims{
|
|
Subject: "test.smallstep.com",
|
|
Issuer: validIssuer,
|
|
NotBefore: jwt.NewNumericDate(now),
|
|
Expiry: jwt.NewNumericDate(now.Add(time.Minute)),
|
|
Audience: validAudience,
|
|
ID: "42",
|
|
}
|
|
raw, err := jwt.Signed(sig).Claims(cl).CompactSerialize()
|
|
assert.FatalError(t, err)
|
|
_, err = a.Authorize(raw)
|
|
assert.FatalError(t, err)
|
|
return &authorizeTest{
|
|
auth: a,
|
|
ott: raw,
|
|
err: &apiError{errors.New("token already used"),
|
|
http.StatusUnauthorized, context{"ott": raw}},
|
|
}
|
|
},
|
|
"ok": func(t *testing.T) *authorizeTest {
|
|
cl := jwt.Claims{
|
|
Subject: "test.smallstep.com",
|
|
Issuer: validIssuer,
|
|
NotBefore: jwt.NewNumericDate(now),
|
|
Expiry: jwt.NewNumericDate(now.Add(time.Minute)),
|
|
Audience: validAudience,
|
|
ID: "43",
|
|
}
|
|
raw, err := jwt.Signed(sig).Claims(cl).CompactSerialize()
|
|
assert.FatalError(t, err)
|
|
return &authorizeTest{
|
|
auth: a,
|
|
ott: raw,
|
|
res: []interface{}{"1", "2", "3", "4"},
|
|
}
|
|
},
|
|
}
|
|
|
|
for name, genTestCase := range tests {
|
|
t.Run(name, func(t *testing.T) {
|
|
tc := genTestCase(t)
|
|
assert.FatalError(t, err)
|
|
|
|
crtOpts, err := tc.auth.Authorize(tc.ott)
|
|
if err != nil {
|
|
if assert.NotNil(t, tc.err) {
|
|
switch v := err.(type) {
|
|
case *apiError:
|
|
assert.HasPrefix(t, v.err.Error(), tc.err.Error())
|
|
assert.Equals(t, v.code, tc.err.code)
|
|
assert.Equals(t, v.context, tc.err.context)
|
|
default:
|
|
t.Errorf("unexpected error type: %T", v)
|
|
}
|
|
}
|
|
} else {
|
|
if assert.Nil(t, tc.err) {
|
|
assert.Equals(t, len(crtOpts), len(tc.res))
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|