forked from TrueCloudLab/certificates
Add kubernetes service account provisioner
This commit is contained in:
parent
64c72cf938
commit
8f07ff6a39
10 changed files with 654 additions and 3 deletions
|
@ -37,8 +37,9 @@ func (p provisionerSlice) Swap(i, j int) { p[i], p[j] = p[j], p[i] }
|
|||
// provisioner.
|
||||
type loadByTokenPayload struct {
|
||||
jose.Claims
|
||||
AuthorizedParty string `json:"azp"` // OIDC client id
|
||||
TenantID string `json:"tid"` // Microsoft Azure tenant id
|
||||
AuthorizedParty string `json:"azp"` // OIDC client id
|
||||
TenantID string `json:"tid"` // Microsoft Azure tenant id
|
||||
ServiceAccountName string `json:"kubernetes.io/serviceaccount/service-account.name"` // Kubernetes Service Acct Name
|
||||
}
|
||||
|
||||
// Collection is a memory map of provisioners.
|
||||
|
@ -91,10 +92,21 @@ func (c *Collection) LoadByToken(token *jose.JSONWebToken, claims *jose.Claims)
|
|||
if err := token.UnsafeClaimsWithoutVerification(&payload); err != nil {
|
||||
return nil, false
|
||||
}
|
||||
// Audience is required
|
||||
|
||||
// Kubernetes Service Account tokens.
|
||||
if len(payload.ServiceAccountName) > 0 {
|
||||
if p, ok := c.Load(K8sSAID); ok {
|
||||
return p, ok
|
||||
}
|
||||
// Kubernetes service account provisioner not found
|
||||
return nil, false
|
||||
}
|
||||
|
||||
// Audience is required for non k8sSA tokens.
|
||||
if len(payload.Audience) == 0 {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
// Try with azp (OIDC)
|
||||
if len(payload.AuthorizedParty) > 0 {
|
||||
if p, ok := c.Load(payload.AuthorizedParty); ok {
|
||||
|
@ -131,6 +143,8 @@ func (c *Collection) LoadByCertificate(cert *x509.Certificate) (Interface, bool)
|
|||
return c.Load("acme/" + string(provisioner.Name))
|
||||
case TypeX5C:
|
||||
return c.Load("x5c/" + string(provisioner.Name))
|
||||
case TypeK8sSA:
|
||||
return c.Load(K8sSAID)
|
||||
default:
|
||||
return c.Load(string(provisioner.CredentialID))
|
||||
}
|
||||
|
|
|
@ -59,13 +59,21 @@ func TestCollection_LoadByToken(t *testing.T) {
|
|||
assert.FatalError(t, err)
|
||||
p3, err := generateOIDC()
|
||||
assert.FatalError(t, err)
|
||||
p4, err := generateK8sSA(nil)
|
||||
assert.FatalError(t, err)
|
||||
|
||||
byID := new(sync.Map)
|
||||
byID.Store(p1.GetID(), p1)
|
||||
byID.Store(p2.GetID(), p2)
|
||||
byID.Store(p3.GetID(), p3)
|
||||
byID.Store(p4.GetID(), p4)
|
||||
byID.Store("string", "a-string")
|
||||
|
||||
byID2 := new(sync.Map)
|
||||
byID2.Store(p1.GetID(), p1)
|
||||
byID2.Store(p2.GetID(), p2)
|
||||
byID2.Store(p3.GetID(), p3)
|
||||
|
||||
jwk, err := decryptJSONWebKey(p1.EncryptedKey)
|
||||
assert.FatalError(t, err)
|
||||
token, err := generateSimpleToken(p1.Name, testAudiences.Sign[0], jwk)
|
||||
|
@ -90,6 +98,13 @@ func TestCollection_LoadByToken(t *testing.T) {
|
|||
t4, c4, err := parseToken(token)
|
||||
assert.FatalError(t, err)
|
||||
|
||||
jwk, err = jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0)
|
||||
assert.FatalError(t, err)
|
||||
token, err = generateK8sSAToken(jwk, nil)
|
||||
assert.FatalError(t, err)
|
||||
t5, c5, err := parseToken(token)
|
||||
assert.FatalError(t, err)
|
||||
|
||||
type fields struct {
|
||||
byID *sync.Map
|
||||
audiences Audiences
|
||||
|
@ -108,8 +123,10 @@ func TestCollection_LoadByToken(t *testing.T) {
|
|||
{"ok1", fields{byID, testAudiences}, args{t1, c1}, p1, true},
|
||||
{"ok2", fields{byID, testAudiences}, args{t2, c2}, p2, true},
|
||||
{"ok3", fields{byID, testAudiences}, args{t3, c3}, p3, true},
|
||||
{"ok4", fields{byID, testAudiences}, args{t5, c5}, p4, true},
|
||||
{"bad", fields{byID, testAudiences}, args{t4, c4}, nil, false},
|
||||
{"fail", fields{byID, Audiences{Sign: []string{"https://foo"}}}, args{t1, c1}, nil, false},
|
||||
{"fail-no-k8sSa-provisioner", fields{byID2, testAudiences}, args{t5, c5}, nil, false},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
|
|
259
authority/provisioner/k8sSA.go
Normal file
259
authority/provisioner/k8sSA.go
Normal file
|
@ -0,0 +1,259 @@
|
|||
package provisioner
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/ecdsa"
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/smallstep/cli/crypto/pemutil"
|
||||
"github.com/smallstep/cli/jose"
|
||||
"golang.org/x/crypto/ed25519"
|
||||
)
|
||||
|
||||
// NOTE: There can be at most one kubernetes service account provisioner configured
|
||||
// per instance of step-ca. This is due to a lack of distinguishing information
|
||||
// contained in kubernetes service account tokens.
|
||||
|
||||
const (
|
||||
// K8sSAName is the default name used for kubernetes service account provisioners.
|
||||
K8sSAName = "k8sSA-default"
|
||||
// K8sSAID is the default ID for kubernetes service account provisioners.
|
||||
K8sSAID = "k8ssa/" + K8sSAName
|
||||
k8sSAIssuer = "kubernetes/serviceaccount"
|
||||
)
|
||||
|
||||
// This number must <= 1. We'll verify this in Init() below.
|
||||
var numK8sSAProvisioners = 0
|
||||
|
||||
// jwtPayload extends jwt.Claims with step attributes.
|
||||
type k8sSAPayload struct {
|
||||
jose.Claims
|
||||
Namespace string `json:"kubernetes.io/serviceaccount/namespace,omitempty"`
|
||||
SecretName string `json:"kubernetes.io/serviceaccount/secret.name,omitempty"`
|
||||
ServiceAccountName string `json:"kubernetes.io/serviceaccount/service-account.name,omitempty"`
|
||||
ServiceAccountUID string `json:"kubernetes.io/serviceaccount/service-account.uid,omitempty"`
|
||||
}
|
||||
|
||||
// K8sSA represents a Kubernetes ServiceAccount provisioner; an
|
||||
// entity trusted to make signature requests.
|
||||
type K8sSA struct {
|
||||
Type string `json:"type"`
|
||||
Name string `json:"name"`
|
||||
Claims *Claims `json:"claims,omitempty"`
|
||||
PubKeys []byte `json:"publicKeys,omitempty"`
|
||||
claimer *Claimer
|
||||
audiences Audiences
|
||||
//kauthn kauthn.AuthenticationV1Interface
|
||||
pubKeys []interface{}
|
||||
}
|
||||
|
||||
// GetID returns the provisioner unique identifier. The name and credential id
|
||||
// should uniquely identify any K8sSA provisioner.
|
||||
func (p *K8sSA) GetID() string {
|
||||
return K8sSAID
|
||||
}
|
||||
|
||||
// GetTokenID returns an unimplemented error and does not use the input ott.
|
||||
func (p *K8sSA) GetTokenID(ott string) (string, error) {
|
||||
return "", errors.New("not implemented")
|
||||
}
|
||||
|
||||
// GetName returns the name of the provisioner.
|
||||
func (p *K8sSA) GetName() string {
|
||||
return p.Name
|
||||
}
|
||||
|
||||
// GetType returns the type of provisioner.
|
||||
func (p *K8sSA) GetType() Type {
|
||||
return TypeK8sSA
|
||||
}
|
||||
|
||||
// GetEncryptedKey returns false, because the kubernetes provisioner does not
|
||||
// have access to the private key.
|
||||
func (p *K8sSA) GetEncryptedKey() (string, string, bool) {
|
||||
return "", "", false
|
||||
}
|
||||
|
||||
// Init initializes and validates the fields of a K8sSA type.
|
||||
func (p *K8sSA) Init(config Config) (err error) {
|
||||
switch {
|
||||
case p.Type == "":
|
||||
return errors.New("provisioner type cannot be empty")
|
||||
case p.Name == "":
|
||||
return errors.New("provisioner name cannot be empty")
|
||||
case numK8sSAProvisioners >= 1:
|
||||
return errors.New("cannot have more than one kubernetes service account provisioner")
|
||||
}
|
||||
|
||||
if p.PubKeys != nil {
|
||||
var (
|
||||
block *pem.Block
|
||||
rest = p.PubKeys
|
||||
)
|
||||
for rest != nil {
|
||||
block, rest = pem.Decode(rest)
|
||||
if block == nil {
|
||||
break
|
||||
}
|
||||
key, err := pemutil.ParseKey(pem.EncodeToMemory(block))
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "error parsing public key in provisioner %s", p.GetID())
|
||||
}
|
||||
switch q := key.(type) {
|
||||
case *rsa.PublicKey, *ecdsa.PublicKey, ed25519.PublicKey:
|
||||
default:
|
||||
return errors.Errorf("Unexpected public key type %T in provisioner %s", q, p.GetID())
|
||||
}
|
||||
p.pubKeys = append(p.pubKeys, key)
|
||||
}
|
||||
} else {
|
||||
// TODO: Use the TokenReview API if no pub keys provided. This will need to
|
||||
// be configured with additional attributes in the K8sSA struct for
|
||||
// connecting to the kubernetes API server.
|
||||
return errors.New("K8s Service Account provisioner cannot be initialized without pub keys")
|
||||
}
|
||||
/*
|
||||
// NOTE: Not sure if we should be doing this initialization here ...
|
||||
// If you have a k8sSA provisioner defined in your config, but you're not
|
||||
// in a kubernetes pod then your CA will fail to startup. Maybe we just postpone
|
||||
// creating the authn until token validation time?
|
||||
if err := checkAccess(k8s.AuthorizationV1()); err != nil {
|
||||
return errors.Wrapf(err, "error verifying access to kubernetes authz service for provisioner %s", p.GetID())
|
||||
}
|
||||
|
||||
p.kauthn = k8s.AuthenticationV1()
|
||||
*/
|
||||
|
||||
// Update claims with global ones
|
||||
if p.claimer, err = NewClaimer(p.Claims, config.Claims); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
p.audiences = config.Audiences
|
||||
numK8sSAProvisioners++
|
||||
return err
|
||||
}
|
||||
|
||||
// authorizeToken performs common jwt authorization actions and returns the
|
||||
// claims for case specific downstream parsing.
|
||||
// e.g. a Sign request will auth/validate different fields than a Revoke request.
|
||||
func (p *K8sSA) authorizeToken(token string, audiences []string) (*k8sSAPayload, error) {
|
||||
jwt, err := jose.ParseSigned(token)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "error parsing token")
|
||||
}
|
||||
|
||||
var (
|
||||
valid bool
|
||||
claims k8sSAPayload
|
||||
)
|
||||
if p.pubKeys == nil {
|
||||
return nil, errors.New("TokenReview API integration not implemented")
|
||||
/* NOTE: We plan to support the TokenReview API in a future release.
|
||||
Below is some code that should be useful when we prioritize
|
||||
this integration.
|
||||
|
||||
tr := kauthnApi.TokenReview{Spec: kauthnApi.TokenReviewSpec{Token: string(token)}}
|
||||
rvw, err := p.kauthn.TokenReviews().Create(&tr)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "error using kubernetes TokenReview API")
|
||||
}
|
||||
if rvw.Status.Error != "" {
|
||||
return nil, errors.Errorf("error from kubernetes TokenReviewAPI: %s", rvw.Status.Error)
|
||||
}
|
||||
if !rvw.Status.Authenticated {
|
||||
return nil, errors.New("error from kubernetes TokenReviewAPI: token could not be authenticated")
|
||||
}
|
||||
if err = jwt.UnsafeClaimsWithoutVerification(&claims); err != nil {
|
||||
return nil, errors.Wrap(err, "error parsing claims")
|
||||
}
|
||||
*/
|
||||
}
|
||||
for _, pk := range p.pubKeys {
|
||||
if err = jwt.Claims(pk, &claims); err == nil {
|
||||
valid = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !valid {
|
||||
return nil, errors.New("error validating token and extracting claims")
|
||||
}
|
||||
|
||||
// According to "rfc7519 JSON Web Token" acceptable skew should be no
|
||||
// more than a few minutes.
|
||||
if err = claims.Validate(jose.Expected{
|
||||
Issuer: k8sSAIssuer,
|
||||
}); err != nil {
|
||||
return nil, errors.Wrapf(err, "invalid token claims")
|
||||
}
|
||||
|
||||
if claims.Subject == "" {
|
||||
return nil, errors.New("token subject cannot be empty")
|
||||
}
|
||||
|
||||
return &claims, nil
|
||||
}
|
||||
|
||||
// AuthorizeRevoke returns an error if the provisioner does not have rights to
|
||||
// revoke the certificate with serial number in the `sub` property.
|
||||
func (p *K8sSA) AuthorizeRevoke(token string) error {
|
||||
_, err := p.authorizeToken(token, p.audiences.Revoke)
|
||||
return err
|
||||
}
|
||||
|
||||
// AuthorizeSign validates the given token.
|
||||
func (p *K8sSA) AuthorizeSign(ctx context.Context, token string) ([]SignOption, error) {
|
||||
_, err := p.authorizeToken(token, p.audiences.Sign)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Check for SSH sign-ing request.
|
||||
if MethodFromContext(ctx) == SignSSHMethod {
|
||||
return nil, errors.New("ssh certificates not enabled for k8s ServiceAccount provisioners")
|
||||
}
|
||||
|
||||
return []SignOption{
|
||||
// modifiers / withOptions
|
||||
newProvisionerExtensionOption(TypeK8sSA, p.Name, ""),
|
||||
profileDefaultDuration(p.claimer.DefaultTLSCertDuration()),
|
||||
// validators
|
||||
defaultPublicKeyValidator{},
|
||||
newValidityValidator(p.claimer.MinTLSCertDuration(), p.claimer.MaxTLSCertDuration()),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// AuthorizeRenewal returns an error if the renewal is disabled.
|
||||
func (p *K8sSA) AuthorizeRenewal(cert *x509.Certificate) error {
|
||||
if p.claimer.IsDisableRenewal() {
|
||||
return errors.Errorf("renew is disabled for provisioner %s", p.GetID())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
/*
|
||||
func checkAccess(authz kauthz.AuthorizationV1Interface) error {
|
||||
r := &kauthzApi.SelfSubjectAccessReview{
|
||||
Spec: kauthzApi.SelfSubjectAccessReviewSpec{
|
||||
ResourceAttributes: &kauthzApi.ResourceAttributes{
|
||||
Group: "authentication.k8s.io",
|
||||
Version: "v1",
|
||||
Resource: "tokenreviews",
|
||||
Verb: "create",
|
||||
},
|
||||
},
|
||||
}
|
||||
rvw, err := authz.SelfSubjectAccessReviews().Create(r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !rvw.Status.Allowed {
|
||||
return fmt.Errorf("Unable to create kubernetes token reviews: %s", rvw.Status.Reason)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
*/
|
264
authority/provisioner/k8sSA_test.go
Normal file
264
authority/provisioner/k8sSA_test.go
Normal file
|
@ -0,0 +1,264 @@
|
|||
package provisioner
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/x509"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/smallstep/assert"
|
||||
"github.com/smallstep/cli/jose"
|
||||
)
|
||||
|
||||
func TestK8sSA_Getters(t *testing.T) {
|
||||
p, err := generateK8sSA(nil)
|
||||
assert.FatalError(t, err)
|
||||
id := "k8ssa/" + p.Name
|
||||
if got := p.GetID(); got != id {
|
||||
t.Errorf("K8sSA.GetID() = %v, want %v", got, id)
|
||||
}
|
||||
if got := p.GetName(); got != p.Name {
|
||||
t.Errorf("K8sSA.GetName() = %v, want %v", got, p.Name)
|
||||
}
|
||||
if got := p.GetType(); got != TypeK8sSA {
|
||||
t.Errorf("K8sSA.GetType() = %v, want %v", got, TypeK8sSA)
|
||||
}
|
||||
kid, key, ok := p.GetEncryptedKey()
|
||||
if kid != "" || key != "" || ok == true {
|
||||
t.Errorf("K8sSA.GetEncryptedKey() = (%v, %v, %v), want (%v, %v, %v)",
|
||||
kid, key, ok, "", "", false)
|
||||
}
|
||||
}
|
||||
|
||||
func TestK8sSA_authorizeToken(t *testing.T) {
|
||||
type test struct {
|
||||
p *K8sSA
|
||||
token string
|
||||
err error
|
||||
}
|
||||
tests := map[string]func(*testing.T) test{
|
||||
"fail/bad-token": func(t *testing.T) test {
|
||||
p, err := generateK8sSA(nil)
|
||||
assert.FatalError(t, err)
|
||||
return test{
|
||||
p: p,
|
||||
token: "foo",
|
||||
err: errors.New("error parsing token"),
|
||||
}
|
||||
},
|
||||
"fail/error-validating-token": func(t *testing.T) test {
|
||||
jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0)
|
||||
assert.FatalError(t, err)
|
||||
p, err := generateK8sSA(nil)
|
||||
assert.FatalError(t, err)
|
||||
tok, err := generateToken("", p.Name, testAudiences.Sign[0], "",
|
||||
[]string{"test.smallstep.com"}, time.Now(), jwk)
|
||||
assert.FatalError(t, err)
|
||||
return test{
|
||||
p: p,
|
||||
token: tok,
|
||||
err: errors.New("error validating token and extracting claims"),
|
||||
}
|
||||
},
|
||||
"fail/invalid-issuer": func(t *testing.T) test {
|
||||
jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0)
|
||||
assert.FatalError(t, err)
|
||||
p, err := generateK8sSA(jwk.Public().Key)
|
||||
assert.FatalError(t, err)
|
||||
claims := getK8sSAPayload()
|
||||
claims.Claims.Issuer = "invalid"
|
||||
tok, err := generateK8sSAToken(jwk, claims)
|
||||
assert.FatalError(t, err)
|
||||
return test{
|
||||
p: p,
|
||||
token: tok,
|
||||
err: errors.New("invalid token claims: square/go-jose/jwt: validation failed, invalid issuer claim (iss)"),
|
||||
}
|
||||
},
|
||||
"ok": func(t *testing.T) test {
|
||||
jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0)
|
||||
assert.FatalError(t, err)
|
||||
p, err := generateK8sSA(jwk.Public().Key)
|
||||
assert.FatalError(t, err)
|
||||
tok, err := generateK8sSAToken(jwk, nil)
|
||||
assert.FatalError(t, err)
|
||||
return test{
|
||||
p: p,
|
||||
token: tok,
|
||||
}
|
||||
},
|
||||
}
|
||||
for name, tt := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
tc := tt(t)
|
||||
if claims, err := tc.p.authorizeToken(tc.token, testAudiences.Sign); err != nil {
|
||||
if assert.NotNil(t, tc.err) {
|
||||
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
||||
}
|
||||
} else {
|
||||
if assert.Nil(t, tc.err) {
|
||||
assert.NotNil(t, claims)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestK8sSA_AuthorizeSign(t *testing.T) {
|
||||
type test struct {
|
||||
p *K8sSA
|
||||
token string
|
||||
ctx context.Context
|
||||
err error
|
||||
}
|
||||
tests := map[string]func(*testing.T) test{
|
||||
"fail/invalid-token": func(t *testing.T) test {
|
||||
p, err := generateK8sSA(nil)
|
||||
assert.FatalError(t, err)
|
||||
return test{
|
||||
p: p,
|
||||
token: "foo",
|
||||
err: errors.New("error parsing token"),
|
||||
}
|
||||
},
|
||||
"fail/ssh-unimplemented": func(t *testing.T) test {
|
||||
jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0)
|
||||
assert.FatalError(t, err)
|
||||
p, err := generateK8sSA(jwk.Public().Key)
|
||||
assert.FatalError(t, err)
|
||||
tok, err := generateK8sSAToken(jwk, nil)
|
||||
assert.FatalError(t, err)
|
||||
return test{
|
||||
p: p,
|
||||
ctx: NewContextWithMethod(context.Background(), SignSSHMethod),
|
||||
token: tok,
|
||||
err: errors.Errorf("ssh certificates not enabled for k8s ServiceAccount provisioners"),
|
||||
}
|
||||
},
|
||||
"ok": func(t *testing.T) test {
|
||||
jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0)
|
||||
assert.FatalError(t, err)
|
||||
p, err := generateK8sSA(jwk.Public().Key)
|
||||
assert.FatalError(t, err)
|
||||
tok, err := generateK8sSAToken(jwk, nil)
|
||||
assert.FatalError(t, err)
|
||||
return test{
|
||||
p: p,
|
||||
ctx: NewContextWithMethod(context.Background(), SignMethod),
|
||||
token: tok,
|
||||
}
|
||||
},
|
||||
}
|
||||
for name, tt := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
tc := tt(t)
|
||||
if opts, err := tc.p.AuthorizeSign(tc.ctx, tc.token); err != nil {
|
||||
if assert.NotNil(t, tc.err) {
|
||||
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
||||
}
|
||||
} else {
|
||||
if assert.Nil(t, tc.err) {
|
||||
if assert.NotNil(t, opts) {
|
||||
tot := 0
|
||||
for _, o := range opts {
|
||||
switch v := o.(type) {
|
||||
case *provisionerExtensionOption:
|
||||
assert.Equals(t, v.Type, int(TypeK8sSA))
|
||||
assert.Equals(t, v.Name, tc.p.GetName())
|
||||
assert.Equals(t, v.CredentialID, "")
|
||||
assert.Len(t, 0, v.KeyValuePairs)
|
||||
case profileDefaultDuration:
|
||||
assert.Equals(t, time.Duration(v), tc.p.claimer.DefaultTLSCertDuration())
|
||||
case defaultPublicKeyValidator:
|
||||
case *validityValidator:
|
||||
assert.Equals(t, v.min, tc.p.claimer.MinTLSCertDuration())
|
||||
assert.Equals(t, v.max, tc.p.claimer.MaxTLSCertDuration())
|
||||
default:
|
||||
assert.FatalError(t, errors.Errorf("unexpected sign option of type %T", v))
|
||||
}
|
||||
tot++
|
||||
}
|
||||
assert.Equals(t, tot, 4)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestK8sSA_AuthorizeRevoke(t *testing.T) {
|
||||
type test struct {
|
||||
p *K8sSA
|
||||
token string
|
||||
err error
|
||||
}
|
||||
tests := map[string]func(*testing.T) test{
|
||||
"fail/invalid-token": func(t *testing.T) test {
|
||||
p, err := generateK8sSA(nil)
|
||||
assert.FatalError(t, err)
|
||||
return test{
|
||||
p: p,
|
||||
token: "foo",
|
||||
err: errors.New("error parsing token"),
|
||||
}
|
||||
},
|
||||
"ok": func(t *testing.T) test {
|
||||
jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0)
|
||||
assert.FatalError(t, err)
|
||||
p, err := generateK8sSA(jwk.Public().Key)
|
||||
assert.FatalError(t, err)
|
||||
tok, err := generateK8sSAToken(jwk, nil)
|
||||
assert.FatalError(t, err)
|
||||
return test{
|
||||
p: p,
|
||||
token: tok,
|
||||
}
|
||||
},
|
||||
}
|
||||
for name, tt := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
tc := tt(t)
|
||||
if err := tc.p.AuthorizeRevoke(tc.token); err != nil {
|
||||
if assert.NotNil(t, tc.err) {
|
||||
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
||||
}
|
||||
} else {
|
||||
assert.Nil(t, tc.err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestK8sSA_AuthorizeRenewal(t *testing.T) {
|
||||
p1, err := generateK8sSA(nil)
|
||||
assert.FatalError(t, err)
|
||||
p2, err := generateK8sSA(nil)
|
||||
assert.FatalError(t, err)
|
||||
|
||||
// disable renewal
|
||||
disable := true
|
||||
p2.Claims = &Claims{DisableRenewal: &disable}
|
||||
p2.claimer, err = NewClaimer(p2.Claims, globalProvisionerClaims)
|
||||
assert.FatalError(t, err)
|
||||
|
||||
type args struct {
|
||||
cert *x509.Certificate
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
prov *K8sSA
|
||||
args args
|
||||
wantErr bool
|
||||
}{
|
||||
{"ok", p1, args{nil}, false},
|
||||
{"fail", p2, args{nil}, true},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if err := tt.prov.AuthorizeRenewal(tt.args.cert); (err != nil) != tt.wantErr {
|
||||
t.Errorf("X5C.AuthorizeRenewal() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -88,6 +88,8 @@ const (
|
|||
TypeACME Type = 6
|
||||
// TypeX5C is used to indicate the X5C provisioners.
|
||||
TypeX5C Type = 7
|
||||
// TypeK8sSA is used to indicate the X5C provisioners.
|
||||
TypeK8sSA Type = 8
|
||||
|
||||
// RevokeAudienceKey is the key for the 'revoke' audiences in the audiences map.
|
||||
RevokeAudienceKey = "revoke"
|
||||
|
@ -112,6 +114,8 @@ func (t Type) String() string {
|
|||
return "ACME"
|
||||
case TypeX5C:
|
||||
return "X5C"
|
||||
case TypeK8sSA:
|
||||
return "K8sSA"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
|
@ -163,6 +167,8 @@ func (l *List) UnmarshalJSON(data []byte) error {
|
|||
p = &ACME{}
|
||||
case "x5c":
|
||||
p = &X5C{}
|
||||
case "k8ssa":
|
||||
p = &K8sSA{}
|
||||
default:
|
||||
// Skip unsupported provisioners. A client using this method may be
|
||||
// compiled with a version of smallstep/certificates that does not
|
||||
|
|
5
authority/provisioner/testdata/bar.priv
vendored
Normal file
5
authority/provisioner/testdata/bar.priv
vendored
Normal file
|
@ -0,0 +1,5 @@
|
|||
-----BEGIN EC PRIVATE KEY-----
|
||||
MHcCAQEEIM8wGIzCKjAOGdBFmYHtS791Ly2I9FtmknEsR2sa63s7oAoGCCqGSM49
|
||||
AwEHoUQDQgAEGQIXbsr73X28pzwC1wa+ccY2H3s8PplbkapCrwxyYVvM78y/GmeS
|
||||
A7fv2MKQ8iKpCw461MlOQGX+VlWT+ChRFw==
|
||||
-----END EC PRIVATE KEY-----
|
4
authority/provisioner/testdata/bar.pub
vendored
Normal file
4
authority/provisioner/testdata/bar.pub
vendored
Normal file
|
@ -0,0 +1,4 @@
|
|||
-----BEGIN PUBLIC KEY-----
|
||||
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEGQIXbsr73X28pzwC1wa+ccY2H3s8
|
||||
PplbkapCrwxyYVvM78y/GmeSA7fv2MKQ8iKpCw461MlOQGX+VlWT+ChRFw==
|
||||
-----END PUBLIC KEY-----
|
5
authority/provisioner/testdata/foo.priv
vendored
Normal file
5
authority/provisioner/testdata/foo.priv
vendored
Normal file
|
@ -0,0 +1,5 @@
|
|||
-----BEGIN EC PRIVATE KEY-----
|
||||
MHcCAQEEIB8ovUg0Atvz+b+XiF8QV722OivOm1geGtI3sP0F48N1oAoGCCqGSM49
|
||||
AwEHoUQDQgAEzriaeV2e1aEz33x62kyqVC6ootU7rl41L8cyeOJ4SjTu4FV+o5i4
|
||||
NsS6DCE07JJSHlqc9PsrzjSs4LZD4gWVLQ==
|
||||
-----END EC PRIVATE KEY-----
|
4
authority/provisioner/testdata/foo.pub
vendored
Normal file
4
authority/provisioner/testdata/foo.pub
vendored
Normal file
|
@ -0,0 +1,4 @@
|
|||
-----BEGIN PUBLIC KEY-----
|
||||
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEzriaeV2e1aEz33x62kyqVC6ootU7
|
||||
rl41L8cyeOJ4SjTu4FV+o5i4NsS6DCE07JJSHlqc9PsrzjSs4LZD4gWVLQ==
|
||||
-----END PUBLIC KEY-----
|
|
@ -10,11 +10,13 @@ import (
|
|||
"encoding/json"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/smallstep/cli/crypto/pemutil"
|
||||
"github.com/smallstep/cli/crypto/randutil"
|
||||
"github.com/smallstep/cli/jose"
|
||||
)
|
||||
|
@ -197,6 +199,43 @@ func generateJWK() (*JWK, error) {
|
|||
}, nil
|
||||
}
|
||||
|
||||
func generateK8sSA(inputPubKey interface{}) (*K8sSA, error) {
|
||||
fooPubB, err := ioutil.ReadFile("./testdata/foo.pub")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
fooPub, err := pemutil.ParseKey(fooPubB)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
barPubB, err := ioutil.ReadFile("./testdata/bar.pub")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
barPub, err := pemutil.ParseKey(barPubB)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
claimer, err := NewClaimer(nil, globalProvisionerClaims)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
pubKeys := []interface{}{fooPub, barPub}
|
||||
if inputPubKey != nil {
|
||||
pubKeys = append(pubKeys, inputPubKey)
|
||||
}
|
||||
|
||||
return &K8sSA{
|
||||
Name: K8sSAName,
|
||||
Type: "K8sSA",
|
||||
Claims: &globalProvisionerClaims,
|
||||
audiences: testAudiences,
|
||||
claimer: claimer,
|
||||
pubKeys: pubKeys,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func generateX5C(root []byte) (*X5C, error) {
|
||||
if root == nil {
|
||||
root = []byte(`-----BEGIN CERTIFICATE-----
|
||||
|
@ -587,6 +626,40 @@ func generateToken(sub, iss, aud string, email string, sans []string, iat time.T
|
|||
return jose.Signed(sig).Claims(claims).CompactSerialize()
|
||||
}
|
||||
|
||||
func getK8sSAPayload() *k8sSAPayload {
|
||||
return &k8sSAPayload{
|
||||
Claims: jose.Claims{
|
||||
Issuer: k8sSAIssuer,
|
||||
Subject: "foo",
|
||||
},
|
||||
Namespace: "ns-foo",
|
||||
SecretName: "sn-foo",
|
||||
ServiceAccountName: "san-foo",
|
||||
ServiceAccountUID: "sauid-foo",
|
||||
}
|
||||
}
|
||||
|
||||
func generateK8sSAToken(jwk *jose.JSONWebKey, claims *k8sSAPayload, tokOpts ...tokOption) (string, error) {
|
||||
so := new(jose.SignerOptions)
|
||||
so.WithHeader("kid", jwk.KeyID)
|
||||
|
||||
for _, o := range tokOpts {
|
||||
if err := o(so); err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
|
||||
sig, err := jose.NewSigner(jose.SigningKey{Algorithm: jose.ES256, Key: jwk.Key}, so)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if claims == nil {
|
||||
claims = getK8sSAPayload()
|
||||
}
|
||||
return jose.Signed(sig).Claims(*claims).CompactSerialize()
|
||||
}
|
||||
|
||||
func generateSimpleSSHUserToken(iss, aud string, jwk *jose.JSONWebKey) (string, error) {
|
||||
return generateSSHToken("subject@localhost", iss, aud, time.Now(), &SSHOptions{
|
||||
CertType: "user",
|
||||
|
|
Loading…
Reference in a new issue