Merge pull request #133 from smallstep/service-account

kubernetes service account provisioner
This commit is contained in:
Max 2019-10-29 17:45:13 -07:00 committed by GitHub
commit ff13b2a699
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 654 additions and 3 deletions

View file

@ -37,8 +37,9 @@ func (p provisionerSlice) Swap(i, j int) { p[i], p[j] = p[j], p[i] }
// provisioner. // provisioner.
type loadByTokenPayload struct { type loadByTokenPayload struct {
jose.Claims jose.Claims
AuthorizedParty string `json:"azp"` // OIDC client id AuthorizedParty string `json:"azp"` // OIDC client id
TenantID string `json:"tid"` // Microsoft Azure tenant 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. // 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 { if err := token.UnsafeClaimsWithoutVerification(&payload); err != nil {
return nil, false 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 { if len(payload.Audience) == 0 {
return nil, false return nil, false
} }
// Try with azp (OIDC) // Try with azp (OIDC)
if len(payload.AuthorizedParty) > 0 { if len(payload.AuthorizedParty) > 0 {
if p, ok := c.Load(payload.AuthorizedParty); ok { 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)) return c.Load("acme/" + string(provisioner.Name))
case TypeX5C: case TypeX5C:
return c.Load("x5c/" + string(provisioner.Name)) return c.Load("x5c/" + string(provisioner.Name))
case TypeK8sSA:
return c.Load(K8sSAID)
default: default:
return c.Load(string(provisioner.CredentialID)) return c.Load(string(provisioner.CredentialID))
} }

View file

@ -59,13 +59,21 @@ func TestCollection_LoadByToken(t *testing.T) {
assert.FatalError(t, err) assert.FatalError(t, err)
p3, err := generateOIDC() p3, err := generateOIDC()
assert.FatalError(t, err) assert.FatalError(t, err)
p4, err := generateK8sSA(nil)
assert.FatalError(t, err)
byID := new(sync.Map) byID := new(sync.Map)
byID.Store(p1.GetID(), p1) byID.Store(p1.GetID(), p1)
byID.Store(p2.GetID(), p2) byID.Store(p2.GetID(), p2)
byID.Store(p3.GetID(), p3) byID.Store(p3.GetID(), p3)
byID.Store(p4.GetID(), p4)
byID.Store("string", "a-string") 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) jwk, err := decryptJSONWebKey(p1.EncryptedKey)
assert.FatalError(t, err) assert.FatalError(t, err)
token, err := generateSimpleToken(p1.Name, testAudiences.Sign[0], jwk) token, err := generateSimpleToken(p1.Name, testAudiences.Sign[0], jwk)
@ -90,6 +98,13 @@ func TestCollection_LoadByToken(t *testing.T) {
t4, c4, err := parseToken(token) t4, c4, err := parseToken(token)
assert.FatalError(t, err) 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 { type fields struct {
byID *sync.Map byID *sync.Map
audiences Audiences audiences Audiences
@ -108,8 +123,10 @@ func TestCollection_LoadByToken(t *testing.T) {
{"ok1", fields{byID, testAudiences}, args{t1, c1}, p1, true}, {"ok1", fields{byID, testAudiences}, args{t1, c1}, p1, true},
{"ok2", fields{byID, testAudiences}, args{t2, c2}, p2, true}, {"ok2", fields{byID, testAudiences}, args{t2, c2}, p2, true},
{"ok3", fields{byID, testAudiences}, args{t3, c3}, p3, 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}, {"bad", fields{byID, testAudiences}, args{t4, c4}, nil, false},
{"fail", fields{byID, Audiences{Sign: []string{"https://foo"}}}, args{t1, c1}, 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 { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {

View 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
}
*/

View 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)
}
})
}
}

View file

@ -88,6 +88,8 @@ const (
TypeACME Type = 6 TypeACME Type = 6
// TypeX5C is used to indicate the X5C provisioners. // TypeX5C is used to indicate the X5C provisioners.
TypeX5C Type = 7 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 is the key for the 'revoke' audiences in the audiences map.
RevokeAudienceKey = "revoke" RevokeAudienceKey = "revoke"
@ -112,6 +114,8 @@ func (t Type) String() string {
return "ACME" return "ACME"
case TypeX5C: case TypeX5C:
return "X5C" return "X5C"
case TypeK8sSA:
return "K8sSA"
default: default:
return "" return ""
} }
@ -163,6 +167,8 @@ func (l *List) UnmarshalJSON(data []byte) error {
p = &ACME{} p = &ACME{}
case "x5c": case "x5c":
p = &X5C{} p = &X5C{}
case "k8ssa":
p = &K8sSA{}
default: default:
// Skip unsupported provisioners. A client using this method may be // Skip unsupported provisioners. A client using this method may be
// compiled with a version of smallstep/certificates that does not // compiled with a version of smallstep/certificates that does not

View file

@ -0,0 +1,5 @@
-----BEGIN EC PRIVATE KEY-----
MHcCAQEEIM8wGIzCKjAOGdBFmYHtS791Ly2I9FtmknEsR2sa63s7oAoGCCqGSM49
AwEHoUQDQgAEGQIXbsr73X28pzwC1wa+ccY2H3s8PplbkapCrwxyYVvM78y/GmeS
A7fv2MKQ8iKpCw461MlOQGX+VlWT+ChRFw==
-----END EC PRIVATE KEY-----

View file

@ -0,0 +1,4 @@
-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEGQIXbsr73X28pzwC1wa+ccY2H3s8
PplbkapCrwxyYVvM78y/GmeSA7fv2MKQ8iKpCw461MlOQGX+VlWT+ChRFw==
-----END PUBLIC KEY-----

View file

@ -0,0 +1,5 @@
-----BEGIN EC PRIVATE KEY-----
MHcCAQEEIB8ovUg0Atvz+b+XiF8QV722OivOm1geGtI3sP0F48N1oAoGCCqGSM49
AwEHoUQDQgAEzriaeV2e1aEz33x62kyqVC6ootU7rl41L8cyeOJ4SjTu4FV+o5i4
NsS6DCE07JJSHlqc9PsrzjSs4LZD4gWVLQ==
-----END EC PRIVATE KEY-----

View file

@ -0,0 +1,4 @@
-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEzriaeV2e1aEz33x62kyqVC6ootU7
rl41L8cyeOJ4SjTu4FV+o5i4NsS6DCE07JJSHlqc9PsrzjSs4LZD4gWVLQ==
-----END PUBLIC KEY-----

View file

@ -10,11 +10,13 @@ import (
"encoding/json" "encoding/json"
"encoding/pem" "encoding/pem"
"fmt" "fmt"
"io/ioutil"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"time" "time"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/smallstep/cli/crypto/pemutil"
"github.com/smallstep/cli/crypto/randutil" "github.com/smallstep/cli/crypto/randutil"
"github.com/smallstep/cli/jose" "github.com/smallstep/cli/jose"
) )
@ -197,6 +199,43 @@ func generateJWK() (*JWK, error) {
}, nil }, 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) { func generateX5C(root []byte) (*X5C, error) {
if root == nil { if root == nil {
root = []byte(`-----BEGIN CERTIFICATE----- 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() 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) { func generateSimpleSSHUserToken(iss, aud string, jwk *jose.JSONWebKey) (string, error) {
return generateSSHToken("subject@localhost", iss, aud, time.Now(), &SSHOptions{ return generateSSHToken("subject@localhost", iss, aud, time.Now(), &SSHOptions{
CertType: "user", CertType: "user",