diff --git a/authority/provisioner/aws.go b/authority/provisioner/aws.go index 1bf0f478..0692a638 100644 --- a/authority/provisioner/aws.go +++ b/authority/provisioner/aws.go @@ -118,10 +118,10 @@ type awsInstanceIdentityDocument struct { type AWS struct { Type string `json:"type"` Name string `json:"name"` - Claims *Claims `json:"claims,omitempty"` Accounts []string `json:"accounts"` DisableCustomSANs bool `json:"disableCustomSANs"` DisableTrustOnFirstUse bool `json:"disableTrustOnFirstUse"` + Claims *Claims `json:"claims,omitempty"` claimer *Claimer config *awsConfig } @@ -192,7 +192,7 @@ func (p *AWS) GetIdentityToken() (string, error) { // Create unique ID for Trust On First Use (TOFU). Only the first instance // per provisioner is allowed as we don't have a way to trust the given // sans. - unique := fmt.Sprintf("%s:%s", p.GetID(), idoc.InstanceID) + unique := fmt.Sprintf("%s.%s", p.GetID(), idoc.InstanceID) sum := sha256.Sum256([]byte(unique)) // Create a JWT from the identity document @@ -256,7 +256,7 @@ func (p *AWS) AuthorizeSign(token string) ([]SignOption, error) { // Enforce default DNS and IP if configured. // By default we we'll accept the SANs in the CSR. - // There's no way to trust them. + // There's no way to trust them other than TOFU. var so []SignOption if p.DisableCustomSANs { so = append(so, dnsNamesValidator([]string{ diff --git a/authority/provisioner/aws_test.go b/authority/provisioner/aws_test.go new file mode 100644 index 00000000..1b998b7b --- /dev/null +++ b/authority/provisioner/aws_test.go @@ -0,0 +1,181 @@ +// +build ignore + +package provisioner + +import ( + "crypto/x509" + "encoding/base64" + "encoding/pem" + "net/http" + "net/http/httptest" + "testing" + + "github.com/fullsailor/pkcs7" + "github.com/smallstep/assert" +) + +var rsaCert = `-----BEGIN CERTIFICATE----- +MIIDIjCCAougAwIBAgIJAKnL4UEDMN/FMA0GCSqGSIb3DQEBBQUAMGoxCzAJBgNV +BAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdTZWF0dGxlMRgw +FgYDVQQKEw9BbWF6b24uY29tIEluYy4xGjAYBgNVBAMTEWVjMi5hbWF6b25hd3Mu +Y29tMB4XDTE0MDYwNTE0MjgwMloXDTI0MDYwNTE0MjgwMlowajELMAkGA1UEBhMC +VVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1NlYXR0bGUxGDAWBgNV +BAoTD0FtYXpvbi5jb20gSW5jLjEaMBgGA1UEAxMRZWMyLmFtYXpvbmF3cy5jb20w +gZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAIe9GN//SRK2knbjySG0ho3yqQM3 +e2TDhWO8D2e8+XZqck754gFSo99AbT2RmXClambI7xsYHZFapbELC4H91ycihvrD +jbST1ZjkLQgga0NE1q43eS68ZeTDccScXQSNivSlzJZS8HJZjgqzBlXjZftjtdJL +XeE4hwvo0sD4f3j9AgMBAAGjgc8wgcwwHQYDVR0OBBYEFCXWzAgVyrbwnFncFFIs +77VBdlE4MIGcBgNVHSMEgZQwgZGAFCXWzAgVyrbwnFncFFIs77VBdlE4oW6kbDBq +MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHU2Vh +dHRsZTEYMBYGA1UEChMPQW1hem9uLmNvbSBJbmMuMRowGAYDVQQDExFlYzIuYW1h +em9uYXdzLmNvbYIJAKnL4UEDMN/FMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEF +BQADgYEAFYcz1OgEhQBXIwIdsgCOS8vEtiJYF+j9uO6jz7VOmJqO+pRlAbRlvY8T +C1haGgSI/A1uZUKs/Zfnph0oEI0/hu1IIJ/SKBDtN5lvmZ/IzbOPIJWirlsllQIQ +7zvWbGd9c9+Rm3p04oTvhup99la7kZqevJK0QRdD/6NpCKsqP/0= +-----END CERTIFICATE-----` + +var rsaSig = `eYko51V+DBTE/pLMwqH9tekcIGdIL6jGkgmh0faKQbHUrWVfaw2ffx032iqbEkvbqIMx0I4ewl+Cq5IejPQ5ax4+Nb9gSoMHS8VCjAUkpj9dUXPG2DEvTHukpvUTy8fGn1a/3LS5GdEPnDVkMj2QDHDBGskH4eA46x9c069xeyE=` + +var dsaCert = `-----BEGIN CERTIFICATE----- +MIIC7TCCAq0CCQCWukjZ5V4aZzAJBgcqhkjOOAQDMFwxCzAJBgNVBAYTAlVTMRkw +FwYDVQQIExBXYXNoaW5ndG9uIFN0YXRlMRAwDgYDVQQHEwdTZWF0dGxlMSAwHgYD +VQQKExdBbWF6b24gV2ViIFNlcnZpY2VzIExMQzAeFw0xMjAxMDUxMjU2MTJaFw0z +ODAxMDUxMjU2MTJaMFwxCzAJBgNVBAYTAlVTMRkwFwYDVQQIExBXYXNoaW5ndG9u +IFN0YXRlMRAwDgYDVQQHEwdTZWF0dGxlMSAwHgYDVQQKExdBbWF6b24gV2ViIFNl +cnZpY2VzIExMQzCCAbcwggEsBgcqhkjOOAQBMIIBHwKBgQCjkvcS2bb1VQ4yt/5e +ih5OO6kK/n1Lzllr7D8ZwtQP8fOEpp5E2ng+D6Ud1Z1gYipr58Kj3nssSNpI6bX3 +VyIQzK7wLclnd/YozqNNmgIyZecN7EglK9ITHJLP+x8FtUpt3QbyYXJdmVMegN6P +hviYt5JH/nYl4hh3Pa1HJdskgQIVALVJ3ER11+Ko4tP6nwvHwh6+ERYRAoGBAI1j +k+tkqMVHuAFcvAGKocTgsjJem6/5qomzJuKDmbJNu9Qxw3rAotXau8Qe+MBcJl/U +hhy1KHVpCGl9fueQ2s6IL0CaO/buycU1CiYQk40KNHCcHfNiZbdlx1E9rpUp7bnF +lRa2v1ntMX3caRVDdbtPEWmdxSCYsYFDk4mZrOLBA4GEAAKBgEbmeve5f8LIE/Gf +MNmP9CM5eovQOGx5ho8WqD+aTebs+k2tn92BBPqeZqpWRa5P/+jrdKml1qx4llHW +MXrs3IgIb6+hUIB+S8dz8/mmO0bpr76RoZVCXYab2CZedFut7qc3WUH9+EUAH5mw +vSeDCOUMYQR7R9LINYwouHIziqQYMAkGByqGSM44BAMDLwAwLAIUWXBlk40xTwSw +7HX32MxXYruse9ACFBNGmdX2ZBrVNGrN9N2f6ROk0k9K +-----END CERTIFICATE-----` + +var dsaSig = `MIAGCSqGSIb3DQEHAqCAMIACAQExCzAJBgUrDgMCGgUAMIAGCSqGSIb3DQEHAaCAJIAEggHTewog +ICJwcml2YXRlSXAiIDogIjE3Mi4zMS4yMy40NyIsCiAgImRldnBheVByb2R1Y3RDb2RlcyIgOiBu +dWxsLAogICJtYXJrZXRwbGFjZVByb2R1Y3RDb2RlcyIgOiBudWxsLAogICJ2ZXJzaW9uIiA6ICIy +MDE3LTA5LTMwIiwKICAiaW5zdGFuY2VJZCIgOiAiaS0wMmUzYmVjMWY2MDBmNWUzMyIsCiAgImJp +bGxpbmdQcm9kdWN0cyIgOiBudWxsLAogICJpbnN0YW5jZVR5cGUiIDogInQyLm1pY3JvIiwKICAi +YXZhaWxhYmlsaXR5Wm9uZSIgOiAidXMtd2VzdC0xYiIsCiAgImtlcm5lbElkIiA6IG51bGwsCiAg +InJhbWRpc2tJZCIgOiBudWxsLAogICJhY2NvdW50SWQiIDogIjgwNzQ5MjQ3MzI2MyIsCiAgImFy +Y2hpdGVjdHVyZSIgOiAieDg2XzY0IiwKICAiaW1hZ2VJZCIgOiAiYW1pLTFjMWQyMTdjIiwKICAi +cGVuZGluZ1RpbWUiIDogIjIwMTctMTEtMjFUMDA6MjU6MjNaIiwKICAicmVnaW9uIiA6ICJ1cy13 +ZXN0LTEiCn0AAAAAAAAxggEYMIIBFAIBATBpMFwxCzAJBgNVBAYTAlVTMRkwFwYDVQQIExBXYXNo +aW5ndG9uIFN0YXRlMRAwDgYDVQQHEwdTZWF0dGxlMSAwHgYDVQQKExdBbWF6b24gV2ViIFNlcnZp +Y2VzIExMQwIJAJa6SNnlXhpnMAkGBSsOAwIaBQCgXTAYBgkqhkiG9w0BCQMxCwYJKoZIhvcNAQcB +MBwGCSqGSIb3DQEJBTEPFw0xODA3MzAyMzMxMDRaMCMGCSqGSIb3DQEJBDEWBBQUze548OLd+uOT +aOSTDLlV9mevbTAJBgcqhkjOOAQDBC8wLQIUDGeP44Ge1atMQghe+ENV4IDM0zQCFQCBTOEvfKu+ +uscwutj+7RCNgSVaWgAAAAAAAA==` + +var doc = `{ + "privateIp" : "172.31.23.47", + "devpayProductCodes" : null, + "marketplaceProductCodes" : null, + "version" : "2017-09-30", + "instanceId" : "i-02e3bec1f600f5e33", + "billingProducts" : null, + "instanceType" : "t2.micro", + "availabilityZone" : "us-west-1b", + "kernelId" : null, + "ramdiskId" : null, + "accountId" : "807492473263", + "architecture" : "x86_64", + "imageId" : "ami-1c1d217c", + "pendingTime" : "2017-11-21T00:25:23Z", + "region" : "us-west-1" +}` + +func TestAWSRSA(t *testing.T) { + block, _ := pem.Decode([]byte(rsaCert)) + + cert, err := x509.ParseCertificate(block.Bytes) + assert.FatalError(t, err) + + signature, err := base64.StdEncoding.DecodeString(rsaSig) + assert.FatalError(t, err) + + err = cert.CheckSignature(x509.SHA256WithRSA, []byte(doc), signature) + assert.FatalError(t, err) +} + +func TestAWSDSA(t *testing.T) { + block, _ := pem.Decode([]byte(dsaCert)) + + cert, err := x509.ParseCertificate(block.Bytes) + assert.FatalError(t, err) + + signature, err := base64.StdEncoding.DecodeString(dsaSig) + assert.FatalError(t, err) + + p7, err := pkcs7.Parse(signature) + assert.FatalError(t, err) + + p7.Certificates = append(p7.Certificates, cert) + + assert.FatalError(t, p7.Verify()) +} + +func TestAWS_GetIdentityToken(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case "/document": + w.Write([]byte(doc)) + case "/signature": + w.Write([]byte(rsaSig)) + default: + http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) + } + })) + defer srv.Close() + + config, err := newAWSConfig() + assert.FatalError(t, err) + config.identityURL = srv.URL + "/document" + config.signatureURL = srv.URL + "/signature" + + type fields struct { + Type string + Name string + Claims *Claims + claimer *Claimer + config *awsConfig + } + tests := []struct { + name string + fields fields + want string + wantErr bool + }{ + {"ok", fields{"AWS", "name", nil, nil, config}, "", false}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + p := &AWS{ + Type: tt.fields.Type, + Name: tt.fields.Name, + Claims: tt.fields.Claims, + claimer: tt.fields.claimer, + config: tt.fields.config, + } + got, err := p.GetIdentityToken() + if (err != nil) != tt.wantErr { + t.Errorf("AWS.GetIdentityToken() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got != tt.want { + t.Errorf("AWS.GetIdentityToken() = %v, want %v", got, tt.want) + } + t.Error(got) + // parts := strings.Split(got, ".") + // signed, err := base64.RawURLEncoding.DecodeString(parts[0]) + // assert.FatalError(t, err) + // signature, err := base64.RawURLEncoding.DecodeString(parts[1]) + // assert.FatalError(t, err) + // assert.FatalError(t, err, config.certificate.CheckSignature(config.signatureAlgorithm, signed, signature)) + }) + } +} diff --git a/authority/provisioner/gcp.go b/authority/provisioner/gcp.go index 21752748..1d67c567 100644 --- a/authority/provisioner/gcp.go +++ b/authority/provisioner/gcp.go @@ -59,14 +59,23 @@ func newGCPConfig() *gcpConfig { // GCP is the provisioner that supports identity tokens created by the Google // Cloud Platform metadata API. +// +// If DisableCustomSANs is true, only the internal DNS and IP will be added as a +// SAN. By default it will accept any SAN in the CSR. +// +// If DisableTrustOnFirstUse is true, multiple sign request for this provisioner +// with the same instance will be accepted. By default only the first request +// will be accepted. type GCP struct { - Type string `json:"type"` - Name string `json:"name"` - ServiceAccounts []string `json:"serviceAccounts"` - Claims *Claims `json:"claims,omitempty"` - claimer *Claimer - config *gcpConfig - keyStore *keyStore + Type string `json:"type"` + Name string `json:"name"` + ServiceAccounts []string `json:"serviceAccounts"` + DisableCustomSANs bool `json:"disableCustomSANs"` + DisableTrustOnFirstUse bool `json:"disableTrustOnFirstUse"` + Claims *Claims `json:"claims,omitempty"` + claimer *Claimer + config *gcpConfig + keyStore *keyStore } // GetID returns the provisioner unique identifier. The name should uniquely @@ -75,24 +84,31 @@ func (p *GCP) GetID() string { return "gcp:" + p.Name } -// GetTokenID returns the identifier of the token. For GCP this is the sha256 of -// "provisioner_id.instance_id.iat.exp". +// GetTokenID returns the identifier of the token. The default value for GCP the +// SHA256 of "provisioner_id.instance_id", but if DisableTrustOnFirstUse is set +// to true, then it will be the SHA256 of the token. func (p *GCP) GetTokenID(token string) (string, error) { jwt, err := jose.ParseSigned(token) if err != nil { return "", errors.Wrap(err, "error parsing token") } + // If TOFU is disabled create an ID for the token, so it cannot be reused. + if p.DisableTrustOnFirstUse { + sum := sha256.Sum256([]byte(token)) + return strings.ToLower(hex.EncodeToString(sum[:])), nil + } + // Get claims w/out verification. var claims gcpPayload if err = jwt.UnsafeClaimsWithoutVerification(&claims); err != nil { return "", errors.Wrap(err, "error verifying claims") } - // This string should be mostly unique - unique := fmt.Sprintf("%s.%s.%d.%d", - p.GetID(), claims.Google.ComputeEngine.InstanceID, - *claims.IssuedAt, *claims.Expiry, - ) + + // Create unique ID for Trust On First Use (TOFU). Only the first instance + // per provisioner is allowed as we don't have a way to trust the given + // sans. + unique := fmt.Sprintf("%s.%s", p.GetID(), claims.Google.ComputeEngine.InstanceID) sum := sha256.Sum256([]byte(unique)) return strings.ToLower(hex.EncodeToString(sum[:])), nil } @@ -176,20 +192,25 @@ func (p *GCP) AuthorizeSign(token string) ([]SignOption, error) { if err != nil { return nil, err } - ce := claims.Google.ComputeEngine - dnsNames := []string{ - fmt.Sprintf("%s.c.%s.internal", ce.InstanceName, ce.ProjectID), - fmt.Sprintf("%s.%s.c.%s.internal", ce.InstanceName, ce.Zone, ce.ProjectID), + + // Enforce default DNS if configured. + // By default we we'll accept the SANs in the CSR. + // There's no way to trust them other than TOFU. + var so []SignOption + if p.DisableCustomSANs { + 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), + })) } - return []SignOption{ + return append(so, commonNameValidator(ce.InstanceName), - dnsNamesValidator(dnsNames), profileDefaultDuration(p.claimer.DefaultTLSCertDuration()), newProvisionerExtensionOption(TypeGCP, p.Name, claims.Subject), newValidityValidator(p.claimer.MinTLSCertDuration(), p.claimer.MaxTLSCertDuration()), - }, nil + ), nil } // AuthorizeRenewal returns an error if the renewal is disabled. diff --git a/authority/provisioner/gcp_test.go b/authority/provisioner/gcp_test.go index b4fed04a..98b45907 100644 --- a/authority/provisioner/gcp_test.go +++ b/authority/provisioner/gcp_test.go @@ -44,16 +44,26 @@ func TestGCP_GetTokenID(t *testing.T) { assert.FatalError(t, err) p1.Name = "name" + p2, err := generateGCP() + assert.FatalError(t, err) + p2.DisableTrustOnFirstUse = true + now := time.Now() t1, err := generateGCPToken(p1.ServiceAccounts[0], "https://accounts.google.com", "gcp:name", "instance-id", "instance-name", "project-id", "zone", now, &p1.keyStore.keySet.Keys[0]) assert.FatalError(t, err) + t2, err := generateGCPToken(p2.ServiceAccounts[0], + "https://accounts.google.com", p2.GetID(), + "instance-id", "instance-name", "project-id", "zone", + now, &p2.keyStore.keySet.Keys[0]) + assert.FatalError(t, err) - unique := fmt.Sprintf("gcp:name.instance-id.%d.%d", now.Unix(), now.Add(5*time.Minute).Unix()) - sum := sha256.Sum256([]byte(unique)) - want := strings.ToLower(hex.EncodeToString(sum[:])) + sum := sha256.Sum256([]byte("gcp:name.instance-id")) + want1 := strings.ToLower(hex.EncodeToString(sum[:])) + sum = sha256.Sum256([]byte(t2)) + want2 := strings.ToLower(hex.EncodeToString(sum[:])) type args struct { token string @@ -65,7 +75,8 @@ func TestGCP_GetTokenID(t *testing.T) { want string wantErr bool }{ - {"ok", p1, args{t1}, want, false}, + {"ok", p1, args{t1}, want1, false}, + {"ok", p2, args{t2}, want2, false}, {"fail token", p1, args{"token"}, "", true}, {"fail claims", p1, args{"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.ey.fooo"}, "", true}, } @@ -188,6 +199,10 @@ func TestGCP_AuthorizeSign(t *testing.T) { p1, err := generateGCP() assert.FatalError(t, err) + p2, err := generateGCP() + assert.FatalError(t, err) + p2.DisableCustomSANs = true + aKey, err := generateJSONWebKey() assert.FatalError(t, err) @@ -196,6 +211,12 @@ func TestGCP_AuthorizeSign(t *testing.T) { "instance-id", "instance-name", "project-id", "zone", time.Now(), &p1.keyStore.keySet.Keys[0]) assert.FatalError(t, err) + t2, err := generateGCPToken(p2.ServiceAccounts[0], + "https://accounts.google.com", p2.GetID(), + "instance-id", "instance-name", "project-id", "zone", + time.Now(), &p2.keyStore.keySet.Keys[0]) + assert.FatalError(t, err) + failKey, err := generateGCPToken(p1.ServiceAccounts[0], "https://accounts.google.com", p1.GetID(), "instance-id", "instance-name", "project-id", "zone", @@ -253,20 +274,22 @@ func TestGCP_AuthorizeSign(t *testing.T) { name string gcp *GCP args args + wantLen int wantErr bool }{ - {"ok", p1, args{t1}, false}, - {"fail token", p1, args{"token"}, true}, - {"fail key", p1, args{failKey}, true}, - {"fail iss", p1, args{failIss}, true}, - {"fail aud", p1, args{failAud}, true}, - {"fail exp", p1, args{failExp}, true}, - {"fail nbf", p1, args{failNbf}, true}, - {"fail service account", p1, args{failServiceAccount}, true}, - {"fail instance id", p1, args{failInstanceID}, true}, - {"fail instance name", p1, args{failInstanceName}, true}, - {"fail project id", p1, args{failProjectID}, true}, - {"fail zone", p1, args{failZone}, true}, + {"ok", p1, args{t1}, 4, false}, + {"ok", p2, args{t2}, 5, false}, + {"fail token", p1, args{"token"}, 0, true}, + {"fail key", p1, args{failKey}, 0, true}, + {"fail iss", p1, args{failIss}, 0, true}, + {"fail aud", p1, args{failAud}, 0, true}, + {"fail exp", p1, args{failExp}, 0, true}, + {"fail nbf", p1, args{failNbf}, 0, true}, + {"fail service account", p1, args{failServiceAccount}, 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}, + {"fail zone", p1, args{failZone}, 0, true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -275,11 +298,7 @@ func TestGCP_AuthorizeSign(t *testing.T) { t.Errorf("GCP.AuthorizeSign() error = %v, wantErr %v", err, tt.wantErr) return } - if err != nil { - assert.Nil(t, got) - } else { - assert.Len(t, 5, got) - } + assert.Len(t, tt.wantLen, got) }) } }