From c431538ff23dd5e47ee3c92b39a8f2aabd2f4d08 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Tue, 4 Jun 2019 15:57:15 -0700 Subject: [PATCH] Add support for instance age check in GCP. Fixes smallstep/step#164 --- authority/provisioner/duration.go | 8 ++++++++ authority/provisioner/duration_test.go | 21 +++++++++++++++++++++ authority/provisioner/gcp.go | 13 ++++++++++++- authority/provisioner/gcp_test.go | 25 ++++++++++++++++++------- authority/provisioner/utils_test.go | 2 +- 5 files changed, 60 insertions(+), 9 deletions(-) diff --git a/authority/provisioner/duration.go b/authority/provisioner/duration.go index 38d504a3..68ffbb0a 100644 --- a/authority/provisioner/duration.go +++ b/authority/provisioner/duration.go @@ -43,3 +43,11 @@ func (d *Duration) UnmarshalJSON(data []byte) (err error) { d.Duration = _d return } + +// Value returns 0 if the duration is null, the inner duration otherwise. +func (d *Duration) Value() time.Duration { + if d == nil { + return 0 + } + return d.Duration +} diff --git a/authority/provisioner/duration_test.go b/authority/provisioner/duration_test.go index 4f7304a0..faf5e7f4 100644 --- a/authority/provisioner/duration_test.go +++ b/authority/provisioner/duration_test.go @@ -59,3 +59,24 @@ func TestDuration_MarshalJSON(t *testing.T) { }) } } + +func TestDuration_Value(t *testing.T) { + var dur *Duration + tests := []struct { + name string + duration *Duration + want time.Duration + }{ + {"ok", &Duration{Duration: 1 * time.Minute}, 1 * time.Minute}, + {"ok new", new(Duration), 0}, + {"ok nil", nil, 0}, + {"ok nil var", dur, 0}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := tt.duration.Value(); got != tt.want { + t.Errorf("Duration.Value() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/authority/provisioner/gcp.go b/authority/provisioner/gcp.go index eed4e672..421ec77e 100644 --- a/authority/provisioner/gcp.go +++ b/authority/provisioner/gcp.go @@ -73,6 +73,7 @@ type GCP struct { ProjectIDs []string `json:"projectIDs"` DisableCustomSANs bool `json:"disableCustomSANs"` DisableTrustOnFirstUse bool `json:"disableTrustOnFirstUse"` + InstanceAge Duration `json:"instanceAge,omitempty"` Claims *Claims `json:"claims,omitempty"` claimer *Claimer config *gcpConfig @@ -177,6 +178,8 @@ func (p *GCP) Init(config Config) error { return errors.New("provisioner type cannot be empty") case p.Name == "": return errors.New("provisioner name cannot be empty") + case p.InstanceAge.Value() < 0: + return errors.New("provisioner instanceAge cannot be negative") } // Initialize config p.assertConfig() @@ -271,9 +274,10 @@ func (p *GCP) authorizeToken(token string) (*gcpPayload, error) { // According to "rfc7519 JSON Web Token" acceptable skew should be no // more than a few minutes. + now := time.Now().UTC() if err = claims.ValidateWithLeeway(jose.Expected{ Issuer: "https://accounts.google.com", - Time: time.Now().UTC(), + Time: now, }, time.Minute); err != nil { return nil, errors.Wrapf(err, "invalid token") } @@ -311,6 +315,13 @@ func (p *GCP) authorizeToken(token string) (*gcpPayload, error) { } } + // validate instance age + if d := p.InstanceAge.Value(); d > 0 { + if now.Sub(claims.Google.ComputeEngine.InstanceCreationTimestamp.Time()) > d { + return nil, errors.New("token google.compute_engine.instance_creation_timestamp is too old") + } + } + switch { case claims.Google.ComputeEngine.InstanceID == "": return nil, errors.New("token google.compute_engine.instance_id cannot be empty") diff --git a/authority/provisioner/gcp_test.go b/authority/provisioner/gcp_test.go index 47013bdf..ca3a9507 100644 --- a/authority/provisioner/gcp_test.go +++ b/authority/provisioner/gcp_test.go @@ -159,11 +159,12 @@ func TestGCP_Init(t *testing.T) { badClaims := &Claims{ DefaultTLSDur: &Duration{0}, } - + zero := Duration{Duration: 0} type fields struct { Type string Name string ServiceAccounts []string + InstanceAge Duration Claims *Claims } type args struct { @@ -176,12 +177,14 @@ func TestGCP_Init(t *testing.T) { args args wantErr bool }{ - {"ok", fields{"GCP", "name", nil, nil}, args{config, srv.URL}, false}, - {"ok", fields{"GCP", "name", []string{"service-account"}, nil}, args{config, srv.URL}, false}, - {"bad type", fields{"", "name", nil, nil}, args{config, srv.URL}, true}, - {"bad name", fields{"GCP", "", nil, nil}, args{config, srv.URL}, true}, - {"bad claims", fields{"GCP", "name", nil, badClaims}, args{config, srv.URL}, true}, - {"bad certs", fields{"GCP", "name", nil, nil}, args{config, srv.URL + "/error"}, true}, + {"ok", fields{"GCP", "name", nil, zero, nil}, args{config, srv.URL}, false}, + {"ok", fields{"GCP", "name", []string{"service-account"}, zero, nil}, args{config, srv.URL}, false}, + {"ok", fields{"GCP", "name", []string{"service-account"}, Duration{Duration: 1 * time.Minute}, nil}, args{config, srv.URL}, false}, + {"bad type", fields{"", "name", nil, zero, nil}, args{config, srv.URL}, true}, + {"bad name", fields{"GCP", "", nil, zero, nil}, args{config, srv.URL}, true}, + {"bad duration", fields{"GCP", "name", nil, Duration{Duration: -1 * time.Minute}, nil}, args{config, srv.URL}, true}, + {"bad claims", fields{"GCP", "name", nil, zero, badClaims}, args{config, srv.URL}, true}, + {"bad certs", fields{"GCP", "name", nil, zero, nil}, args{config, srv.URL + "/error"}, true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -189,6 +192,7 @@ func TestGCP_Init(t *testing.T) { Type: tt.fields.Type, Name: tt.fields.Name, ServiceAccounts: tt.fields.ServiceAccounts, + InstanceAge: tt.fields.InstanceAge, Claims: tt.fields.Claims, config: &gcpConfig{ CertsURL: tt.args.certsURL, @@ -214,6 +218,7 @@ func TestGCP_AuthorizeSign(t *testing.T) { assert.FatalError(t, err) p3.ProjectIDs = []string{"other-project-id"} p3.ServiceAccounts = []string{"foo@developer.gserviceaccount.com"} + p3.InstanceAge = Duration{1 * time.Minute} aKey, err := generateJSONWebKey() assert.FatalError(t, err) @@ -269,6 +274,11 @@ func TestGCP_AuthorizeSign(t *testing.T) { "instance-id", "instance-name", "project-id", "zone", time.Now(), &p3.keyStore.keySet.Keys[0]) assert.FatalError(t, err) + failInvalidInstanceAge, err := generateGCPToken(p3.ServiceAccounts[0], + "https://accounts.google.com", p3.GetID(), + "instance-id", "instance-name", "other-project-id", "zone", + time.Now().Add(-1*time.Minute), &p3.keyStore.keySet.Keys[0]) + assert.FatalError(t, err) failInstanceID, err := generateGCPToken(p1.ServiceAccounts[0], "https://accounts.google.com", p1.GetID(), "", "instance-name", "project-id", "zone", @@ -311,6 +321,7 @@ func TestGCP_AuthorizeSign(t *testing.T) { {"fail nbf", p1, args{failNbf}, 0, true}, {"fail service account", p1, args{failServiceAccount}, 0, true}, {"fail invalid project id", p3, args{failInvalidProjectID}, 0, true}, + {"fail invalid instance age", p3, args{failInvalidInstanceAge}, 0, true}, {"fail instance id", p1, args{failInstanceID}, 0, true}, {"fail instance name", p1, args{failInstanceName}, 0, true}, {"fail project id", p1, args{failProjectID}, 0, true}, diff --git a/authority/provisioner/utils_test.go b/authority/provisioner/utils_test.go index d89cbc5d..a2bfeee0 100644 --- a/authority/provisioner/utils_test.go +++ b/authority/provisioner/utils_test.go @@ -513,7 +513,7 @@ func generateGCPToken(sub, iss, aud, instanceID, instanceName, projectID, zone s ComputeEngine: gcpComputeEnginePayload{ InstanceID: instanceID, InstanceName: instanceName, - InstanceCreationTimestamp: jose.NewNumericDate(iat.Add(-24 * time.Hour)), + InstanceCreationTimestamp: jose.NewNumericDate(iat), ProjectID: projectID, ProjectNumber: 1234567890, Zone: zone,