Merge pull request #84 from smallstep/iid-common-name

Allow custom common names in cloud identity provisioners
This commit is contained in:
Mariano Cano 2019-07-16 11:15:55 -07:00 committed by GitHub
commit 5356bce4d8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 185 additions and 280 deletions

194
Gopkg.lock generated
View file

@ -66,14 +66,6 @@
pruneopts = "UT"
revision = "6a90982ecee230ff6cba02d5bd386acc030be9d3"
[[projects]]
digest = "1:2cd7915ab26ede7d95b8749e6b1f933f1c6d5398030684e6505940a10f31cfda"
name = "github.com/ghodss/yaml"
packages = ["."]
pruneopts = "UT"
revision = "0ca9ea5df5451ffdf184b4428c902747c2c11cd7"
version = "v1.0.0"
[[projects]]
branch = "master"
digest = "1:81fda4d18a16651bf92245ce5d6178cdd99f918db30ae9794732655f0686e895"
@ -90,17 +82,6 @@
revision = "72cd26f257d44c1114970e19afddcd812016007e"
version = "v1.4.1"
[[projects]]
digest = "1:b402bb9a24d108a9405a6f34675091b036c8b056aac843bf6ef2389a65c5cf48"
name = "github.com/gogo/protobuf"
packages = [
"proto",
"sortkeys",
]
pruneopts = "UT"
revision = "4cbf7e384e768b4e01799441fdf2a706a5635ae7"
version = "v1.2.0"
[[projects]]
branch = "travis-1.9"
digest = "1:e8f5d9c09a7209c740e769713376abda388c41b777ba8e9ed52767e21acf379f"
@ -113,27 +94,13 @@
revision = "883fe33ffc4344bad1ecd881f61afd5ec5d80e0a"
[[projects]]
digest = "1:4c0989ca0bcd10799064318923b9bc2db6b4d6338dd75f3f2d86c3511aaaf5cf"
digest = "1:97df918963298c287643883209a2c3f642e6593379f97ab400c2a2e219ab647d"
name = "github.com/golang/protobuf"
packages = [
"proto",
"ptypes",
"ptypes/any",
"ptypes/duration",
"ptypes/timestamp",
]
packages = ["proto"]
pruneopts = "UT"
revision = "aa810b61a9c79d51363740d207bb46cf8e620ed5"
version = "v1.2.0"
[[projects]]
branch = "master"
digest = "1:3ee90c0d94da31b442dde97c99635aaafec68d0b8a3c12ee2075c6bdabeec6bb"
name = "github.com/google/gofuzz"
packages = ["."]
pruneopts = "UT"
revision = "24818f796faf91cd76ec7bddd72458fbced7a6c1"
[[projects]]
branch = "master"
digest = "1:750e747d0aad97b79f4a4e00034bae415c2ea793fd9e61438d966ee9c79579bf"
@ -150,14 +117,6 @@
pruneopts = "UT"
revision = "1003c8bd00dc2869cb5ca5282e6ce33834fed514"
[[projects]]
digest = "1:3e551bbb3a7c0ab2a2bf4660e7fcad16db089fdcfbb44b0199e62838038623ea"
name = "github.com/json-iterator/go"
packages = ["."]
pruneopts = "UT"
revision = "1624edc4454b8682399def8740d46db5e4362ba4"
version = "v1.1.5"
[[projects]]
branch = "master"
digest = "1:e51f40f0c19b39c1825eadd07d5c0a98a2ad5942b166d9fc4f54750ce9a04810"
@ -235,22 +194,6 @@
pruneopts = "UT"
revision = "2e7d06bc7ada2979f17ccf8ebf486dba23b84fc7"
[[projects]]
digest = "1:33422d238f147d247752996a26574ac48dcf472976eda7f5134015f06bf16563"
name = "github.com/modern-go/concurrent"
packages = ["."]
pruneopts = "UT"
revision = "bacd9c7ef1dd9b15be4a9909b8ac7a4e313eec94"
version = "1.0.3"
[[projects]]
digest = "1:e32bdbdb7c377a07a9a46378290059822efdce5c8d96fe71940d87cb4f918855"
name = "github.com/modern-go/reflect2"
packages = ["."]
pruneopts = "UT"
revision = "4b7aa43c6742a2c18fdef89dd197aaae7dac7ccd"
version = "1.0.1"
[[projects]]
digest = "1:266d082179f3a29a4bdcf1dcc49d4a304f5c7107e65bd22d1fecacf45f1ac348"
name = "github.com/newrelic/go-agent"
@ -493,54 +436,6 @@
revision = "54a98f90d1c46b7731eb8fb305d2a321c30ef610"
version = "v1.5.0"
[[projects]]
branch = "master"
digest = "1:077c1c599507b3b3e9156d17d36e1e61928ee9b53a5b420f10f28ebd4a0b275c"
name = "google.golang.org/genproto"
packages = ["googleapis/rpc/status"]
pruneopts = "UT"
revision = "4b09977fb92221987e99d190c8f88f2c92727a29"
[[projects]]
digest = "1:9ab5a33d8cb5c120602a34d2e985ce17956a4e8c2edce7e6961568f95e40c09a"
name = "google.golang.org/grpc"
packages = [
".",
"balancer",
"balancer/base",
"balancer/roundrobin",
"binarylog/grpc_binarylog_v1",
"codes",
"connectivity",
"credentials",
"credentials/internal",
"encoding",
"encoding/proto",
"grpclog",
"internal",
"internal/backoff",
"internal/binarylog",
"internal/channelz",
"internal/envconfig",
"internal/grpcrand",
"internal/grpcsync",
"internal/syscall",
"internal/transport",
"keepalive",
"metadata",
"naming",
"peer",
"resolver",
"resolver/dns",
"resolver/passthrough",
"stats",
"status",
"tap",
]
pruneopts = "UT"
revision = "a02b0774206b209466313a0b525d2c738fe407eb"
version = "v1.18.0"
[[projects]]
digest = "1:39efb07a0d773dc09785b237ada4e10b5f28646eb6505d97bc18f8d2ff439362"
name = "gopkg.in/alecthomas/kingpin.v3-unstable"
@ -548,14 +443,6 @@
pruneopts = "UT"
revision = "63abe20a23e29e80bbef8089bd3dee3ac25e5306"
[[projects]]
digest = "1:2d1fbdc6777e5408cabeb02bf336305e724b925ff4546ded0fa8715a7267922a"
name = "gopkg.in/inf.v0"
packages = ["."]
pruneopts = "UT"
revision = "d2d2541c53f18d2a059457998ce2876cc8e67cbf"
version = "v0.9.1"
[[projects]]
digest = "1:9593bab40e981b1f90b7e07faeab0d09b75fe338880d08880f986a9d3283c53f"
name = "gopkg.in/square/go-jose.v2"
@ -577,82 +464,14 @@
revision = "5420a8b6744d3b0345ab293f6fcba19c978f1183"
version = "v2.2.1"
[[projects]]
branch = "master"
digest = "1:767b6c0b2c1d9487ee50cb8df1d0fdebf06ac0b19b723f6489d388e7b47c962d"
name = "k8s.io/api"
packages = [
"admission/v1beta1",
"authentication/v1",
"core/v1",
]
pruneopts = "UT"
revision = "de494049e22a9ccf748c5bbda7492f42f344d0cd"
[[projects]]
branch = "master"
digest = "1:5eb353533eaebdfec2392210ab218a389965ba5d4dc02b4aef87b9549e5d0f84"
name = "k8s.io/apimachinery"
packages = [
"pkg/api/resource",
"pkg/apis/meta/v1",
"pkg/apis/meta/v1/unstructured",
"pkg/conversion",
"pkg/conversion/queryparams",
"pkg/fields",
"pkg/labels",
"pkg/runtime",
"pkg/runtime/schema",
"pkg/runtime/serializer",
"pkg/runtime/serializer/json",
"pkg/runtime/serializer/protobuf",
"pkg/runtime/serializer/recognizer",
"pkg/runtime/serializer/versioning",
"pkg/selection",
"pkg/types",
"pkg/util/errors",
"pkg/util/framer",
"pkg/util/intstr",
"pkg/util/json",
"pkg/util/naming",
"pkg/util/net",
"pkg/util/runtime",
"pkg/util/sets",
"pkg/util/validation",
"pkg/util/validation/field",
"pkg/util/yaml",
"pkg/watch",
"third_party/forked/golang/reflect",
]
pruneopts = "UT"
revision = "4b3b852955ebe47857fcf134b531b23dd8f3e793"
[[projects]]
digest = "1:72fd56341405f53c745377e0ebc4abeff87f1a048e0eea6568a20212650f5a82"
name = "k8s.io/klog"
packages = ["."]
pruneopts = "UT"
revision = "71442cd4037d612096940ceb0f3fec3f7fff66e0"
version = "v0.2.0"
[[projects]]
digest = "1:7719608fe0b52a4ece56c2dde37bedd95b938677d1ab0f84b8a7852e4c59f849"
name = "sigs.k8s.io/yaml"
packages = ["."]
pruneopts = "UT"
revision = "fd68e9863619f6ec2fdd8625fe1f02e7c877e480"
version = "v1.1.0"
[solve-meta]
analyzer-name = "dep"
analyzer-version = 1
input-imports = [
"github.com/alecthomas/gometalinter",
"github.com/client9/misspell/cmd/misspell",
"github.com/ghodss/yaml",
"github.com/go-chi/chi",
"github.com/golang/lint/golint",
"github.com/golang/protobuf/proto",
"github.com/gordonklaus/ineffassign",
"github.com/newrelic/go-agent",
"github.com/pkg/errors",
@ -675,18 +494,9 @@
"github.com/tsenart/deadcode",
"github.com/urfave/cli",
"golang.org/x/crypto/ocsp",
"golang.org/x/net/context",
"golang.org/x/net/http2",
"google.golang.org/grpc",
"google.golang.org/grpc/credentials",
"google.golang.org/grpc/peer",
"gopkg.in/square/go-jose.v2",
"gopkg.in/square/go-jose.v2/jwt",
"k8s.io/api/admission/v1beta1",
"k8s.io/api/core/v1",
"k8s.io/apimachinery/pkg/apis/meta/v1",
"k8s.io/apimachinery/pkg/runtime",
"k8s.io/apimachinery/pkg/runtime/serializer",
]
solver-name = "gps-cdcl"
solver-version = 1

View file

@ -171,7 +171,7 @@ func (p *AWS) GetEncryptedKey() (kid string, key string, ok bool) {
// GetIdentityToken retrieves the identity document and it's signature and
// generates a token with them.
func (p *AWS) GetIdentityToken(caURL string) (string, error) {
func (p *AWS) GetIdentityToken(subject, caURL string) (string, error) {
// Initialize the config if this method is used from the cli.
if err := p.assertConfig(); err != nil {
return "", err
@ -221,7 +221,7 @@ func (p *AWS) GetIdentityToken(caURL string) (string, error) {
payload := awsPayload{
Claims: jose.Claims{
Issuer: awsIssuer,
Subject: idoc.InstanceID,
Subject: subject,
Audience: []string{audience},
Expiry: jose.NewNumericDate(now.Add(5 * time.Minute)),
NotBefore: jose.NewNumericDate(now),
@ -273,8 +273,8 @@ func (p *AWS) AuthorizeSign(token string) ([]SignOption, error) {
}
doc := payload.document
// Enforce default DNS and IP if configured.
// By default we'll accept the SANs in the CSR.
// Enforce known CN and default DNS and IP if configured.
// By default we'll accept the CN and SANs in the CSR.
// There's no way to trust them other than TOFU.
var so []SignOption
if p.DisableCustomSANs {
@ -287,9 +287,9 @@ func (p *AWS) AuthorizeSign(token string) ([]SignOption, error) {
}
return append(so,
commonNameValidator(doc.InstanceID),
commonNameValidator(payload.Claims.Subject),
profileDefaultDuration(p.claimer.DefaultTLSCertDuration()),
newProvisionerExtensionOption(TypeAWS, p.Name, doc.AccountID),
newProvisionerExtensionOption(TypeAWS, p.Name, doc.AccountID, "InstanceID", doc.InstanceID),
newValidityValidator(p.claimer.MinTLSCertDuration(), p.claimer.MaxTLSCertDuration()),
), nil
}
@ -388,19 +388,26 @@ func (p *AWS) authorizeToken(token string) (*awsPayload, error) {
// more than a few minutes.
now := time.Now().UTC()
if err = payload.ValidateWithLeeway(jose.Expected{
Issuer: awsIssuer,
Subject: doc.InstanceID,
Time: now,
Issuer: awsIssuer,
Time: now,
}, time.Minute); err != nil {
return nil, errors.Wrapf(err, "invalid token")
}
// validate audiences with the defaults
if !matchesAudience(payload.Audience, p.audiences.Sign) {
fmt.Println(payload.Audience, "vs", p.audiences.Sign)
return nil, errors.New("invalid token: invalid audience claim (aud)")
}
// Validate subject, it has to be known if disableCustomSANs is enabled
if p.DisableCustomSANs {
if payload.Subject != doc.InstanceID &&
payload.Subject != doc.PrivateIP &&
payload.Subject != fmt.Sprintf("ip-%s.%s.compute.internal", strings.Replace(doc.PrivateIP, ".", "-", -1), doc.Region) {
return nil, errors.New("invalid token: invalid subject claim (sub)")
}
}
// validate accounts
if len(p.Accounts) > 0 {
var found bool

View file

@ -48,14 +48,14 @@ func TestAWS_GetTokenID(t *testing.T) {
p2.config = p1.config
p2.DisableTrustOnFirstUse = true
t1, err := p1.GetIdentityToken("https://ca.smallstep.com")
t1, err := p1.GetIdentityToken("foo.local", "https://ca.smallstep.com")
assert.FatalError(t, err)
_, claims, err := parseAWSToken(t1)
assert.FatalError(t, err)
sum := sha256.Sum256([]byte(fmt.Sprintf("%s.%s", p1.GetID(), claims.document.InstanceID)))
w1 := strings.ToLower(hex.EncodeToString(sum[:]))
t2, err := p2.GetIdentityToken("https://ca.smallstep.com")
t2, err := p2.GetIdentityToken("foo.local", "https://ca.smallstep.com")
assert.FatalError(t, err)
sum = sha256.Sum256([]byte(t2))
w2 := strings.ToLower(hex.EncodeToString(sum[:]))
@ -111,12 +111,31 @@ func TestAWS_GetIdentityToken(t *testing.T) {
p4.config.signatureURL = srv.URL + "/bad-signature"
p4.config.identityURL = p1.config.identityURL
p5, err := generateAWS()
assert.FatalError(t, err)
p5.Accounts = p1.Accounts
p5.config.identityURL = "https://1234.1234.1234.1234"
p5.config.signatureURL = p1.config.signatureURL
p6, err := generateAWS()
assert.FatalError(t, err)
p6.Accounts = p1.Accounts
p6.config.identityURL = p1.config.identityURL
p6.config.signatureURL = "https://1234.1234.1234.1234"
p7, err := generateAWS()
assert.FatalError(t, err)
p7.Accounts = p1.Accounts
p7.config.identityURL = srv.URL + "/bad-json"
p7.config.signatureURL = p1.config.signatureURL
caURL := "https://ca.smallstep.com"
u, err := url.Parse(caURL)
assert.FatalError(t, err)
type args struct {
caURL string
subject string
caURL string
}
tests := []struct {
name string
@ -124,15 +143,18 @@ func TestAWS_GetIdentityToken(t *testing.T) {
args args
wantErr bool
}{
{"ok", p1, args{caURL}, false},
{"fail ca url", p1, args{"://ca.smallstep.com"}, true},
{"fail identityURL", p2, args{caURL}, true},
{"fail signatureURL", p3, args{caURL}, true},
{"fail signature", p4, args{caURL}, true},
{"ok", p1, args{"foo.local", caURL}, false},
{"fail ca url", p1, args{"foo.local", "://ca.smallstep.com"}, true},
{"fail identityURL", p2, args{"foo.local", caURL}, true},
{"fail signatureURL", p3, args{"foo.local", caURL}, true},
{"fail signature", p4, args{"foo.local", caURL}, true},
{"fail read identityURL", p5, args{"foo.local", caURL}, true},
{"fail read signatureURL", p6, args{"foo.local", caURL}, true},
{"fail unmarshal identityURL", p7, args{"foo.local", caURL}, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := tt.aws.GetIdentityToken(tt.args.caURL)
got, err := tt.aws.GetIdentityToken(tt.args.subject, tt.args.caURL)
if (err != nil) != tt.wantErr {
t.Errorf("AWS.GetIdentityToken() error = %v, wantErr %v", err, tt.wantErr)
return
@ -141,7 +163,7 @@ func TestAWS_GetIdentityToken(t *testing.T) {
_, c, err := parseAWSToken(got)
if assert.NoError(t, err) {
assert.Equals(t, awsIssuer, c.Issuer)
assert.Equals(t, c.document.InstanceID, c.Subject)
assert.Equals(t, tt.args.subject, c.Subject)
assert.Equals(t, jose.Audience{u.ResolveReference(&url.URL{Path: "/1.0/sign", Fragment: tt.aws.GetID()}).String()}, c.Audience)
assert.Equals(t, tt.aws.Accounts[0], c.document.AccountID)
err = tt.aws.config.certificate.CheckSignature(
@ -221,11 +243,18 @@ func TestAWS_AuthorizeSign(t *testing.T) {
assert.FatalError(t, err)
p3.config = p1.config
t1, err := p1.GetIdentityToken("https://ca.smallstep.com")
t1, err := p1.GetIdentityToken("foo.local", "https://ca.smallstep.com")
assert.FatalError(t, err)
t2, err := p2.GetIdentityToken("https://ca.smallstep.com")
t2, err := p2.GetIdentityToken("instance-id", "https://ca.smallstep.com")
assert.FatalError(t, err)
t3, err := p3.GetIdentityToken("https://ca.smallstep.com")
assert.FatalError(t, err)
t3, err := p3.GetIdentityToken("foo.local", "https://ca.smallstep.com")
assert.FatalError(t, err)
// Alternative common names with DisableCustomSANs = true
t2PrivateIP, err := p2.GetIdentityToken("127.0.0.1", "https://ca.smallstep.com")
assert.FatalError(t, err)
t2Hostname, err := p2.GetIdentityToken("ip-127-0-0-1.us-west-1.compute.internal", "https://ca.smallstep.com")
assert.FatalError(t, err)
block, _ := pem.Decode([]byte(awsTestKey))
@ -243,7 +272,7 @@ func TestAWS_AuthorizeSign(t *testing.T) {
"127.0.0.1", "us-west-1", time.Now(), key)
assert.FatalError(t, err)
failSubject, err := generateAWSToken(
"bad-subject", awsIssuer, p1.GetID(), p1.Accounts[0], "instance-id",
"bad-subject", awsIssuer, p2.GetID(), p2.Accounts[0], "instance-id",
"127.0.0.1", "us-west-1", time.Now(), key)
assert.FatalError(t, err)
failIssuer, err := generateAWSToken(
@ -299,6 +328,8 @@ func TestAWS_AuthorizeSign(t *testing.T) {
}{
{"ok", p1, args{t1}, 4, false},
{"ok", p2, args{t2}, 6, false},
{"ok", p2, args{t2Hostname}, 6, false},
{"ok", p2, args{t2PrivateIP}, 6, false},
{"ok", p1, args{t4}, 4, false},
{"fail account", p3, args{t3}, 0, true},
{"fail token", p1, args{"token"}, 0, true},
@ -364,7 +395,7 @@ func TestAWS_AuthorizeRevoke(t *testing.T) {
assert.FatalError(t, err)
defer srv.Close()
t1, err := p1.GetIdentityToken("https://ca.smallstep.com")
t1, err := p1.GetIdentityToken("foo.local", "https://ca.smallstep.com")
assert.FatalError(t, err)
type args struct {

View file

@ -141,7 +141,7 @@ func (p *Azure) GetEncryptedKey() (kid string, key string, ok bool) {
// GetIdentityToken retrieves from the metadata service the identity token and
// returns it.
func (p *Azure) GetIdentityToken() (string, error) {
func (p *Azure) GetIdentityToken(subject, caURL string) (string, error) {
// Initialize the config if this method is used from the cli.
p.assertConfig()
@ -264,17 +264,17 @@ func (p *Azure) AuthorizeSign(token string) ([]SignOption, error) {
}
}
// Enforce default DNS if configured.
// By default we'll accept the SANs in the CSR.
// Enforce known common name and default DNS if configured.
// By default we'll accept the CN and SANs in the CSR.
// There's no way to trust them other than TOFU.
var so []SignOption
if p.DisableCustomSANs {
// name will work only inside the virtual network
so = append(so, commonNameValidator(name))
so = append(so, dnsNamesValidator([]string{name}))
}
return append(so,
commonNameValidator(name),
profileDefaultDuration(p.claimer.DefaultTLSCertDuration()),
newProvisionerExtensionOption(TypeAzure, p.Name, p.TenantID),
newValidityValidator(p.claimer.MinTLSCertDuration(), p.claimer.MaxTLSCertDuration()),

View file

@ -46,9 +46,9 @@ func TestAzure_GetTokenID(t *testing.T) {
p2.keyStore = p1.keyStore
p2.DisableTrustOnFirstUse = true
t1, err := p1.GetIdentityToken()
t1, err := p1.GetIdentityToken("subject", "caURL")
assert.FatalError(t, err)
t2, err := p2.GetIdentityToken()
t2, err := p2.GetIdentityToken("subject", "caURL")
assert.FatalError(t, err)
sum := sha256.Sum256([]byte("/subscriptions/subscriptionID/resourceGroups/resourceGroup/providers/Microsoft.Compute/virtualMachines/virtualMachine"))
@ -105,23 +105,28 @@ func TestAzure_GetIdentityToken(t *testing.T) {
}))
defer srv.Close()
type args struct {
subject string
caURL string
}
tests := []struct {
name string
azure *Azure
args args
identityTokenURL string
want string
wantErr bool
}{
{"ok", p1, srv.URL, t1, false},
{"fail request", p1, srv.URL + "/bad-request", "", true},
{"fail unmarshal", p1, srv.URL + "/bad-json", "", true},
{"fail url", p1, "://ca.smallstep.com", "", true},
{"fail connect", p1, "foobarzar", "", true},
{"ok", p1, args{"subject", "caURL"}, srv.URL, t1, false},
{"fail request", p1, args{"subject", "caURL"}, srv.URL + "/bad-request", "", true},
{"fail unmarshal", p1, args{"subject", "caURL"}, srv.URL + "/bad-json", "", true},
{"fail url", p1, args{"subject", "caURL"}, "://ca.smallstep.com", "", true},
{"fail connect", p1, args{"subject", "caURL"}, "foobarzar", "", true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tt.azure.config.identityTokenURL = tt.identityTokenURL
got, err := tt.azure.GetIdentityToken()
got, err := tt.azure.GetIdentityToken(tt.args.subject, tt.args.caURL)
if (err != nil) != tt.wantErr {
t.Errorf("Azure.GetIdentityToken() error = %v, wantErr %v", err, tt.wantErr)
return
@ -231,13 +236,13 @@ func TestAzure_AuthorizeSign(t *testing.T) {
badKey, err := generateJSONWebKey()
assert.FatalError(t, err)
t1, err := p1.GetIdentityToken()
t1, err := p1.GetIdentityToken("subject", "caURL")
assert.FatalError(t, err)
t2, err := p2.GetIdentityToken()
t2, err := p2.GetIdentityToken("subject", "caURL")
assert.FatalError(t, err)
t3, err := p3.GetIdentityToken()
t3, err := p3.GetIdentityToken("subject", "caURL")
assert.FatalError(t, err)
t4, err := p4.GetIdentityToken()
t4, err := p4.GetIdentityToken("subject", "caURL")
assert.FatalError(t, err)
t11, err := generateAzureToken("subject", p1.oidcConfig.Issuer, azureDefaultAudience,
@ -276,9 +281,9 @@ func TestAzure_AuthorizeSign(t *testing.T) {
wantLen int
wantErr bool
}{
{"ok", p1, args{t1}, 4, false},
{"ok", p1, args{t1}, 3, false},
{"ok", p2, args{t2}, 5, false},
{"ok", p1, args{t11}, 4, false},
{"ok", p1, args{t11}, 3, false},
{"fail tenant", p3, args{t3}, 0, true},
{"fail resource group", p4, args{t4}, 0, true},
{"fail token", p1, args{"token"}, 0, true},
@ -338,7 +343,7 @@ func TestAzure_AuthorizeRevoke(t *testing.T) {
assert.FatalError(t, err)
defer srv.Close()
token, err := az.GetIdentityToken()
token, err := az.GetIdentityToken("subject", "caURL")
assert.FatalError(t, err)
type args struct {

View file

@ -150,7 +150,7 @@ func (p *GCP) GetIdentityURL(audience string) string {
}
// GetIdentityToken does an HTTP request to the identity url.
func (p *GCP) GetIdentityToken(caURL string) (string, error) {
func (p *GCP) GetIdentityToken(subject, caURL string) (string, error) {
audience, err := generateSignAudience(caURL, p.GetID())
if err != nil {
return "", err
@ -212,21 +212,24 @@ func (p *GCP) AuthorizeSign(token string) ([]SignOption, error) {
}
ce := claims.Google.ComputeEngine
// Enforce default DNS if configured.
// By default we we'll accept the SANs in the CSR.
// Enforce known common name and default DNS if configured.
// By default we we'll accept the CN and SANs in the CSR.
// There's no way to trust them other than TOFU.
var so []SignOption
if p.DisableCustomSANs {
dnsName1 := fmt.Sprintf("%s.c.%s.internal", ce.InstanceName, ce.ProjectID)
dnsName2 := fmt.Sprintf("%s.%s.c.%s.internal", ce.InstanceName, ce.Zone, ce.ProjectID)
so = append(so, commonNameSliceValidator([]string{
ce.InstanceName, ce.InstanceID, dnsName1, dnsName2,
}))
so = append(so, dnsNamesValidator([]string{
fmt.Sprintf("%s.c.%s.internal", ce.InstanceName, ce.ProjectID),
fmt.Sprintf("%s.%s.c.%s.internal", ce.InstanceName, ce.Zone, ce.ProjectID),
dnsName1, dnsName2,
}))
}
return append(so,
commonNameValidator(ce.InstanceName),
profileDefaultDuration(p.claimer.DefaultTLSCertDuration()),
newProvisionerExtensionOption(TypeGCP, p.Name, claims.Subject),
newProvisionerExtensionOption(TypeGCP, p.Name, claims.Subject, "InstanceID", ce.InstanceID, "InstanceName", ce.InstanceName),
newValidityValidator(p.claimer.MinTLSCertDuration(), p.claimer.MaxTLSCertDuration()),
), nil
}

View file

@ -117,7 +117,8 @@ func TestGCP_GetIdentityToken(t *testing.T) {
defer srv.Close()
type args struct {
caURL string
subject string
caURL string
}
tests := []struct {
name string
@ -127,16 +128,16 @@ func TestGCP_GetIdentityToken(t *testing.T) {
want string
wantErr bool
}{
{"ok", p1, args{"https://ca"}, srv.URL, t1, false},
{"fail ca url", p1, args{"://ca"}, srv.URL, "", true},
{"fail request", p1, args{"https://ca"}, srv.URL + "/bad-request", "", true},
{"fail url", p1, args{"https://ca"}, "://ca.smallstep.com", "", true},
{"fail connect", p1, args{"https://ca"}, "foobarzar", "", true},
{"ok", p1, args{"subject", "https://ca"}, srv.URL, t1, false},
{"fail ca url", p1, args{"subject", "://ca"}, srv.URL, "", true},
{"fail request", p1, args{"subject", "https://ca"}, srv.URL + "/bad-request", "", true},
{"fail url", p1, args{"subject", "https://ca"}, "://ca.smallstep.com", "", true},
{"fail connect", p1, args{"subject", "https://ca"}, "foobarzar", "", true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tt.gcp.config.IdentityURL = tt.identityURL
got, err := tt.gcp.GetIdentityToken(tt.args.caURL)
got, err := tt.gcp.GetIdentityToken(tt.args.subject, tt.args.caURL)
t.Log(err)
if (err != nil) != tt.wantErr {
t.Errorf("GCP.GetIdentityToken() error = %v, wantErr %v", err, tt.wantErr)
@ -310,9 +311,9 @@ func TestGCP_AuthorizeSign(t *testing.T) {
wantLen int
wantErr bool
}{
{"ok", p1, args{t1}, 4, false},
{"ok", p1, args{t1}, 3, false},
{"ok", p2, args{t2}, 5, false},
{"ok", p3, args{t3}, 4, false},
{"ok", p3, args{t3}, 3, false},
{"fail token", p1, args{"token"}, 0, true},
{"fail key", p1, args{failKey}, 0, true},
{"fail iss", p1, args{failIss}, 0, true},

View file

@ -55,7 +55,12 @@ func (v profileWithOption) Option(Options) x509util.WithOption {
type profileDefaultDuration time.Duration
func (v profileDefaultDuration) Option(so Options) x509util.WithOption {
return x509util.WithNotBeforeAfterDuration(so.NotBefore.Time(), so.NotAfter.Time(), time.Duration(v))
notBefore := so.NotBefore.Time()
if notBefore.IsZero() {
notBefore = time.Now()
}
notAfter := so.NotAfter.RelativeTime(notBefore)
return x509util.WithNotBeforeAfterDuration(notBefore, notAfter, time.Duration(v))
}
// emailOnlyIdentity is a CertificateRequestValidator that checks that the only
@ -97,6 +102,21 @@ func (v commonNameValidator) Valid(req *x509.CertificateRequest) error {
return nil
}
// commonNameSliceValidator validates thats the common name of a certificate request is present in the slice.
type commonNameSliceValidator []string
func (v commonNameSliceValidator) Valid(req *x509.CertificateRequest) error {
if req.Subject.CommonName == "" {
return errors.New("certificate request cannot contain an empty common name")
}
for _, cn := range v {
if req.Subject.CommonName == cn {
return nil
}
}
return errors.Errorf("certificate request does not contain the valid common name, got %s, want %s", req.Subject.CommonName, v)
}
// dnsNamesValidator validates the DNS names SAN of a certificate request.
type dnsNamesValidator []string
@ -180,29 +200,32 @@ var (
)
type stepProvisionerASN1 struct {
Type int
Name []byte
CredentialID []byte
Type int
Name []byte
CredentialID []byte
KeyValuePairs []string `asn1:"optional,omitempty"`
}
type provisionerExtensionOption struct {
Type int
Name string
CredentialID string
Type int
Name string
CredentialID string
KeyValuePairs []string
}
func newProvisionerExtensionOption(typ Type, name, credentialID string) *provisionerExtensionOption {
func newProvisionerExtensionOption(typ Type, name, credentialID string, keyValuePairs ...string) *provisionerExtensionOption {
return &provisionerExtensionOption{
Type: int(typ),
Name: name,
CredentialID: credentialID,
Type: int(typ),
Name: name,
CredentialID: credentialID,
KeyValuePairs: keyValuePairs,
}
}
func (o *provisionerExtensionOption) Option(Options) x509util.WithOption {
return func(p x509util.Profile) error {
crt := p.Subject()
ext, err := createProvisionerExtension(o.Type, o.Name, o.CredentialID)
ext, err := createProvisionerExtension(o.Type, o.Name, o.CredentialID, o.KeyValuePairs...)
if err != nil {
return err
}
@ -211,11 +234,12 @@ func (o *provisionerExtensionOption) Option(Options) x509util.WithOption {
}
}
func createProvisionerExtension(typ int, name, credentialID string) (pkix.Extension, error) {
func createProvisionerExtension(typ int, name, credentialID string, keyValuePairs ...string) (pkix.Extension, error) {
b, err := asn1.Marshal(stepProvisionerASN1{
Type: typ,
Name: []byte(name),
CredentialID: []byte(credentialID),
Type: typ,
Name: []byte(name),
CredentialID: []byte(credentialID),
KeyValuePairs: keyValuePairs,
})
if err != nil {
return pkix.Extension{}, errors.Wrapf(err, "error marshaling provisioner extension")

View file

@ -64,6 +64,30 @@ func Test_commonNameValidator_Valid(t *testing.T) {
}
}
func Test_commonNameSliceValidator_Valid(t *testing.T) {
type args struct {
req *x509.CertificateRequest
}
tests := []struct {
name string
v commonNameSliceValidator
args args
wantErr bool
}{
{"ok", []string{"foo.bar.zar"}, args{&x509.CertificateRequest{Subject: pkix.Name{CommonName: "foo.bar.zar"}}}, false},
{"ok", []string{"example.com", "foo.bar.zar"}, args{&x509.CertificateRequest{Subject: pkix.Name{CommonName: "foo.bar.zar"}}}, false},
{"empty", []string{""}, args{&x509.CertificateRequest{Subject: pkix.Name{CommonName: ""}}}, true},
{"wrong", []string{"foo.bar.zar"}, args{&x509.CertificateRequest{Subject: pkix.Name{CommonName: "example.com"}}}, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if err := tt.v.Valid(tt.args.req); (err != nil) != tt.wantErr {
t.Errorf("commonNameSliceValidator.Valid() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}
func Test_dnsNamesValidator_Valid(t *testing.T) {
type args struct {
req *x509.CertificateRequest

View file

@ -104,6 +104,12 @@ func (t *TimeDuration) UnmarshalJSON(data []byte) error {
// Time calculates the embedded time.Time, sets it if necessary, and returns it.
func (t *TimeDuration) Time() time.Time {
return t.RelativeTime(now())
}
// RelativeTime returns the embedded time.Time or the base time plus the
// duration if this is not zero.
func (t *TimeDuration) RelativeTime(base time.Time) time.Time {
switch {
case t == nil:
return time.Time{}
@ -111,8 +117,8 @@ func (t *TimeDuration) Time() time.Time {
if t.d == 0 {
return time.Time{}
}
t.t = now().Add(t.d)
return t.t
t.t = base.Add(t.d)
return t.t.UTC()
default:
return t.t.UTC()
}

View file

@ -283,20 +283,12 @@ func generateAWSWithServer() (*AWS, *httptest.Server, error) {
if err != nil {
return nil, nil, errors.Wrap(err, "error parsing AWS private key")
}
instanceID, err := randutil.Alphanumeric(10)
if err != nil {
return nil, nil, err
}
imageID, err := randutil.Alphanumeric(10)
if err != nil {
return nil, nil, err
}
doc, err := json.MarshalIndent(awsInstanceIdentityDocument{
AccountID: aws.Accounts[0],
Architecture: "x86_64",
AvailabilityZone: "us-west-2b",
ImageID: imageID,
InstanceID: instanceID,
ImageID: "image-id",
InstanceID: "instance-id",
InstanceType: "t2.micro",
PendingTime: time.Now(),
PrivateIP: "127.0.0.1",
@ -322,6 +314,8 @@ func generateAWSWithServer() (*AWS, *httptest.Server, error) {
w.Write([]byte("{}"))
case "/bad-signature":
w.Write([]byte("YmFkLXNpZ25hdHVyZQo="))
case "/bad-json":
w.Write([]byte("{"))
default:
http.NotFound(w, r)
}