forked from TrueCloudLab/certificates
Use on GCP audiences with the format https://<ca-url>#<provisioner-type>/<provisioner-name>
Fixes smallstep/step#156
This commit is contained in:
parent
a54bf925eb
commit
0a756ce9d0
5 changed files with 104 additions and 23 deletions
|
@ -66,8 +66,21 @@ func (c *Collection) Load(id string) (Interface, bool) {
|
||||||
|
|
||||||
// LoadByToken parses the token claims and loads the provisioner associated.
|
// LoadByToken parses the token claims and loads the provisioner associated.
|
||||||
func (c *Collection) LoadByToken(token *jose.JSONWebToken, claims *jose.Claims) (Interface, bool) {
|
func (c *Collection) LoadByToken(token *jose.JSONWebToken, claims *jose.Claims) (Interface, bool) {
|
||||||
|
var audiences []string
|
||||||
|
// Get all audiences with the given fragment
|
||||||
|
fragment := extractFragment(claims.Audience)
|
||||||
|
if fragment == "" {
|
||||||
|
audiences = c.audiences.All()
|
||||||
|
} else {
|
||||||
|
audiences = c.audiences.WithFragment(fragment).All()
|
||||||
|
}
|
||||||
|
|
||||||
// match with server audiences
|
// match with server audiences
|
||||||
if matchesAudience(claims.Audience, c.audiences.All()) {
|
if matchesAudience(claims.Audience, audiences) {
|
||||||
|
// Use fragment to get audiences (GCP)
|
||||||
|
if fragment != "" {
|
||||||
|
return c.Load(fragment)
|
||||||
|
}
|
||||||
// If matches with stored audiences it will be a JWT token (default), and
|
// If matches with stored audiences it will be a JWT token (default), and
|
||||||
// the id would be <issuer>:<kid>.
|
// the id would be <issuer>:<kid>.
|
||||||
return c.Load(claims.Issuer + ":" + token.Headers[0].KeyID)
|
return c.Load(claims.Issuer + ":" + token.Headers[0].KeyID)
|
||||||
|
@ -234,3 +247,13 @@ func stripPort(rawurl string) string {
|
||||||
u.Host = u.Hostname()
|
u.Host = u.Hostname()
|
||||||
return u.String()
|
return u.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// extractFragment extracts the
|
||||||
|
func extractFragment(audience []string) string {
|
||||||
|
for _, s := range audience {
|
||||||
|
if u, err := url.Parse(s); err == nil && u.Fragment != "" {
|
||||||
|
return u.Fragment
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
|
@ -77,12 +77,13 @@ type GCP struct {
|
||||||
claimer *Claimer
|
claimer *Claimer
|
||||||
config *gcpConfig
|
config *gcpConfig
|
||||||
keyStore *keyStore
|
keyStore *keyStore
|
||||||
|
audiences Audiences
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetID returns the provisioner unique identifier. The name should uniquely
|
// GetID returns the provisioner unique identifier. The name should uniquely
|
||||||
// identify any GCP provisioner.
|
// identify any GCP provisioner.
|
||||||
func (p *GCP) GetID() string {
|
func (p *GCP) GetID() string {
|
||||||
return "gcp:" + p.Name
|
return "gcp/" + p.Name
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetTokenID returns the identifier of the token. The default value for GCP the
|
// GetTokenID returns the identifier of the token. The default value for GCP the
|
||||||
|
@ -130,20 +131,25 @@ func (p *GCP) GetEncryptedKey() (kid string, key string, ok bool) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetIdentityURL returns the url that generates the GCP token.
|
// GetIdentityURL returns the url that generates the GCP token.
|
||||||
func (p *GCP) GetIdentityURL() string {
|
func (p *GCP) GetIdentityURL(audience string) string {
|
||||||
// Initialize config if required
|
// Initialize config if required
|
||||||
p.assertConfig()
|
p.assertConfig()
|
||||||
|
|
||||||
q := url.Values{}
|
q := url.Values{}
|
||||||
q.Add("audience", p.GetID())
|
q.Add("audience", audience)
|
||||||
q.Add("format", "full")
|
q.Add("format", "full")
|
||||||
q.Add("licenses", "FALSE")
|
q.Add("licenses", "FALSE")
|
||||||
return fmt.Sprintf("%s?%s", p.config.IdentityURL, q.Encode())
|
return fmt.Sprintf("%s?%s", p.config.IdentityURL, q.Encode())
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetIdentityToken does an HTTP request to the identity url.
|
// GetIdentityToken does an HTTP request to the identity url.
|
||||||
func (p *GCP) GetIdentityToken() (string, error) {
|
func (p *GCP) GetIdentityToken(caURL string) (string, error) {
|
||||||
req, err := http.NewRequest("GET", p.GetIdentityURL(), http.NoBody)
|
audience, err := generateSignAudience(caURL, p.GetID())
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := http.NewRequest("GET", p.GetIdentityURL(audience), http.NoBody)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", errors.Wrap(err, "error creating identity request")
|
return "", errors.Wrap(err, "error creating identity request")
|
||||||
}
|
}
|
||||||
|
@ -183,6 +189,8 @@ func (p *GCP) Init(config Config) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
p.audiences = config.Audiences.WithFragment(p.GetID())
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -264,13 +272,17 @@ func (p *GCP) authorizeToken(token string) (*gcpPayload, error) {
|
||||||
// According to "rfc7519 JSON Web Token" acceptable skew should be no
|
// According to "rfc7519 JSON Web Token" acceptable skew should be no
|
||||||
// more than a few minutes.
|
// more than a few minutes.
|
||||||
if err = claims.ValidateWithLeeway(jose.Expected{
|
if err = claims.ValidateWithLeeway(jose.Expected{
|
||||||
Issuer: "https://accounts.google.com",
|
Issuer: "https://accounts.google.com",
|
||||||
Audience: []string{p.GetID()},
|
Time: time.Now().UTC(),
|
||||||
Time: time.Now().UTC(),
|
|
||||||
}, time.Minute); err != nil {
|
}, time.Minute); err != nil {
|
||||||
return nil, errors.Wrapf(err, "invalid token")
|
return nil, errors.Wrapf(err, "invalid token")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// validate audiences with the defaults
|
||||||
|
if !matchesAudience(claims.Audience, p.audiences.Sign) {
|
||||||
|
return nil, errors.New("invalid token: invalid audience claim (aud)")
|
||||||
|
}
|
||||||
|
|
||||||
// validate subject (service account)
|
// validate subject (service account)
|
||||||
if len(p.ServiceAccounts) > 0 {
|
if len(p.ServiceAccounts) > 0 {
|
||||||
var found bool
|
var found bool
|
||||||
|
|
|
@ -18,9 +18,9 @@ import (
|
||||||
func TestGCP_Getters(t *testing.T) {
|
func TestGCP_Getters(t *testing.T) {
|
||||||
p, err := generateGCP()
|
p, err := generateGCP()
|
||||||
assert.FatalError(t, err)
|
assert.FatalError(t, err)
|
||||||
aud := "gcp:" + p.Name
|
id := "gcp/" + p.Name
|
||||||
if got := p.GetID(); got != aud {
|
if got := p.GetID(); got != id {
|
||||||
t.Errorf("GCP.GetID() = %v, want %v", got, aud)
|
t.Errorf("GCP.GetID() = %v, want %v", got, id)
|
||||||
}
|
}
|
||||||
if got := p.GetName(); got != p.Name {
|
if got := p.GetName(); got != p.Name {
|
||||||
t.Errorf("GCP.GetName() = %v, want %v", got, p.Name)
|
t.Errorf("GCP.GetName() = %v, want %v", got, p.Name)
|
||||||
|
@ -33,8 +33,10 @@ func TestGCP_Getters(t *testing.T) {
|
||||||
t.Errorf("GCP.GetEncryptedKey() = (%v, %v, %v), want (%v, %v, %v)",
|
t.Errorf("GCP.GetEncryptedKey() = (%v, %v, %v), want (%v, %v, %v)",
|
||||||
kid, key, ok, "", "", false)
|
kid, key, ok, "", "", false)
|
||||||
}
|
}
|
||||||
expected := fmt.Sprintf("http://metadata/computeMetadata/v1/instance/service-accounts/default/identity?audience=%s&format=full&licenses=FALSE", url.QueryEscape(p.GetID()))
|
|
||||||
if got := p.GetIdentityURL(); got != expected {
|
aud := "https://ca.smallstep.com/1.0/sign#" + url.QueryEscape(id)
|
||||||
|
expected := fmt.Sprintf("http://metadata/computeMetadata/v1/instance/service-accounts/default/identity?audience=%s&format=full&licenses=FALSE", url.QueryEscape(aud))
|
||||||
|
if got := p.GetIdentityURL(aud); got != expected {
|
||||||
t.Errorf("GCP.GetIdentityURL() = %v, want %v", got, expected)
|
t.Errorf("GCP.GetIdentityURL() = %v, want %v", got, expected)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -50,7 +52,7 @@ func TestGCP_GetTokenID(t *testing.T) {
|
||||||
|
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
t1, err := generateGCPToken(p1.ServiceAccounts[0],
|
t1, err := generateGCPToken(p1.ServiceAccounts[0],
|
||||||
"https://accounts.google.com", "gcp:name",
|
"https://accounts.google.com", "gcp/name",
|
||||||
"instance-id", "instance-name", "project-id", "zone",
|
"instance-id", "instance-name", "project-id", "zone",
|
||||||
now, &p1.keyStore.keySet.Keys[0])
|
now, &p1.keyStore.keySet.Keys[0])
|
||||||
assert.FatalError(t, err)
|
assert.FatalError(t, err)
|
||||||
|
@ -60,7 +62,7 @@ func TestGCP_GetTokenID(t *testing.T) {
|
||||||
now, &p2.keyStore.keySet.Keys[0])
|
now, &p2.keyStore.keySet.Keys[0])
|
||||||
assert.FatalError(t, err)
|
assert.FatalError(t, err)
|
||||||
|
|
||||||
sum := sha256.Sum256([]byte("gcp:name.instance-id"))
|
sum := sha256.Sum256([]byte("gcp/name.instance-id"))
|
||||||
want1 := strings.ToLower(hex.EncodeToString(sum[:]))
|
want1 := strings.ToLower(hex.EncodeToString(sum[:]))
|
||||||
sum = sha256.Sum256([]byte(t2))
|
sum = sha256.Sum256([]byte(t2))
|
||||||
want2 := strings.ToLower(hex.EncodeToString(sum[:]))
|
want2 := strings.ToLower(hex.EncodeToString(sum[:]))
|
||||||
|
@ -114,22 +116,27 @@ func TestGCP_GetIdentityToken(t *testing.T) {
|
||||||
}))
|
}))
|
||||||
defer srv.Close()
|
defer srv.Close()
|
||||||
|
|
||||||
|
type args struct {
|
||||||
|
caURL string
|
||||||
|
}
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
gcp *GCP
|
gcp *GCP
|
||||||
|
args args
|
||||||
identityURL string
|
identityURL string
|
||||||
want string
|
want string
|
||||||
wantErr bool
|
wantErr bool
|
||||||
}{
|
}{
|
||||||
{"ok", p1, srv.URL, t1, false},
|
{"ok", p1, args{"https://ca"}, srv.URL, t1, false},
|
||||||
{"fail request", p1, srv.URL + "/bad-request", "", true},
|
{"fail ca url", p1, args{"://ca"}, srv.URL, "", true},
|
||||||
{"fail url", p1, "://ca.smallstep.com", "", true},
|
{"fail request", p1, args{"https://ca"}, srv.URL + "/bad-request", "", true},
|
||||||
{"fail connect", p1, "foobarzar", "", true},
|
{"fail url", p1, args{"https://ca"}, "://ca.smallstep.com", "", true},
|
||||||
|
{"fail connect", p1, args{"https://ca"}, "foobarzar", "", true},
|
||||||
}
|
}
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
tt.gcp.config.IdentityURL = tt.identityURL
|
tt.gcp.config.IdentityURL = tt.identityURL
|
||||||
got, err := tt.gcp.GetIdentityToken()
|
got, err := tt.gcp.GetIdentityToken(tt.args.caURL)
|
||||||
t.Log(err)
|
t.Log(err)
|
||||||
if (err != nil) != tt.wantErr {
|
if (err != nil) != tt.wantErr {
|
||||||
t.Errorf("GCP.GetIdentityToken() error = %v, wantErr %v", err, tt.wantErr)
|
t.Errorf("GCP.GetIdentityToken() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
|
|
|
@ -3,6 +3,7 @@ package provisioner
|
||||||
import (
|
import (
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"net/url"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
|
@ -28,10 +29,44 @@ type Audiences struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
// All returns all supported audiences across all request types in one list.
|
// All returns all supported audiences across all request types in one list.
|
||||||
func (a *Audiences) All() []string {
|
func (a Audiences) All() []string {
|
||||||
return append(a.Sign, a.Revoke...)
|
return append(a.Sign, a.Revoke...)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WithFragment returns a copy of audiences where the url audiences contains the
|
||||||
|
// given fragment.
|
||||||
|
func (a Audiences) WithFragment(fragment string) Audiences {
|
||||||
|
ret := Audiences{
|
||||||
|
Sign: make([]string, len(a.Sign)),
|
||||||
|
Revoke: make([]string, len(a.Revoke)),
|
||||||
|
}
|
||||||
|
for i, s := range a.Sign {
|
||||||
|
if u, err := url.Parse(s); err == nil {
|
||||||
|
ret.Sign[i] = u.ResolveReference(&url.URL{Fragment: fragment}).String()
|
||||||
|
} else {
|
||||||
|
ret.Sign[i] = s
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for i, s := range a.Revoke {
|
||||||
|
if u, err := url.Parse(s); err == nil {
|
||||||
|
ret.Revoke[i] = u.ResolveReference(&url.URL{Fragment: fragment}).String()
|
||||||
|
} else {
|
||||||
|
ret.Revoke[i] = s
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
// generateSignAudience generates a sign audience with the format
|
||||||
|
// https://<ca-url>/1.0/sign#provisionerID
|
||||||
|
func generateSignAudience(caURL string, provisionerID string) (string, error) {
|
||||||
|
u, err := url.Parse(caURL)
|
||||||
|
if err != nil {
|
||||||
|
return "", errors.Wrapf(err, "error parsing %s", caURL)
|
||||||
|
}
|
||||||
|
return u.ResolveReference(&url.URL{Path: "/1.0/sign", Fragment: provisionerID}).String(), nil
|
||||||
|
}
|
||||||
|
|
||||||
// Type indicates the provisioner Type.
|
// Type indicates the provisioner Type.
|
||||||
type Type int
|
type Type int
|
||||||
|
|
||||||
|
|
|
@ -229,6 +229,7 @@ func generateGCP() (*GCP, error) {
|
||||||
keySet: jose.JSONWebKeySet{Keys: []jose.JSONWebKey{*jwk}},
|
keySet: jose.JSONWebKeySet{Keys: []jose.JSONWebKey{*jwk}},
|
||||||
expiry: time.Now().Add(24 * time.Hour),
|
expiry: time.Now().Add(24 * time.Hour),
|
||||||
},
|
},
|
||||||
|
audiences: testAudiences.WithFragment("gcp/" + name),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -492,7 +493,10 @@ func generateGCPToken(sub, iss, aud, instanceID, instanceName, projectID, zone s
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
aud, err = generateSignAudience("https://ca.smallstep.com", aud)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
claims := gcpPayload{
|
claims := gcpPayload{
|
||||||
Claims: jose.Claims{
|
Claims: jose.Claims{
|
||||||
Subject: sub,
|
Subject: sub,
|
||||||
|
|
Loading…
Reference in a new issue