diff --git a/azurerm/azurerm_sweeper_test.go b/azurerm/azurerm_sweeper_test.go index 264e8a1cb653..042e2098a900 100644 --- a/azurerm/azurerm_sweeper_test.go +++ b/azurerm/azurerm_sweeper_test.go @@ -30,16 +30,24 @@ func buildConfigForSweepers() (*ArmClient, error) { return nil, fmt.Errorf("ARM_SUBSCRIPTION_ID, ARM_CLIENT_ID, ARM_CLIENT_SECRET and ARM_TENANT_ID must be set for acceptance tests") } - config := &authentication.Config{ - SubscriptionID: subscriptionID, - ClientID: clientID, - ClientSecret: clientSecret, - TenantID: tenantID, - Environment: environment, - SkipProviderRegistration: false, + builder := &authentication.Builder{ + SubscriptionID: subscriptionID, + ClientID: clientID, + ClientSecret: clientSecret, + TenantID: tenantID, + Environment: environment, + + // Feature Toggles + SupportsClientSecretAuth: true, + } + + config, err := builder.Build() + if err != nil { + return nil, fmt.Errorf("Error building ARM Client: %+v", err) } - return getArmClient(config) + skipProviderRegistration := false + return getArmClient(config, skipProviderRegistration) } func shouldSweepAcceptanceTestResource(name string, resourceLocation string, region string) bool { diff --git a/azurerm/config.go b/azurerm/config.go index 67439f464b6d..2cad7df8309e 100644 --- a/azurerm/config.go +++ b/azurerm/config.go @@ -354,7 +354,7 @@ func setUserAgent(client *autorest.Client) { // getArmClient is a helper method which returns a fully instantiated // *ArmClient based on the Config's current settings. -func getArmClient(c *authentication.Config) (*ArmClient, error) { +func getArmClient(c *authentication.Config, skipProviderRegistration bool) (*ArmClient, error) { env, err := authentication.DetermineEnvironment(c.Environment) if err != nil { return nil, err @@ -366,8 +366,8 @@ func getArmClient(c *authentication.Config) (*ArmClient, error) { tenantId: c.TenantID, subscriptionId: c.SubscriptionID, environment: *env, - usingServicePrincipal: c.ClientSecret != "", - skipProviderRegistration: c.SkipProviderRegistration, + usingServicePrincipal: c.AuthenticatedAsAServicePrincipal, + skipProviderRegistration: skipProviderRegistration, } oauthConfig, err := adal.NewOAuthConfig(env.ActiveDirectoryEndpoint, c.TenantID) @@ -382,14 +382,14 @@ func getArmClient(c *authentication.Config) (*ArmClient, error) { // Resource Manager endpoints endpoint := env.ResourceManagerEndpoint - auth, err := authentication.GetAuthorizationToken(c, oauthConfig, endpoint) + auth, err := c.GetAuthorizationToken(oauthConfig, endpoint) if err != nil { return nil, err } // Graph Endpoints graphEndpoint := env.GraphEndpoint - graphAuth, err := authentication.GetAuthorizationToken(c, oauthConfig, graphEndpoint) + graphAuth, err := c.GetAuthorizationToken(oauthConfig, graphEndpoint) if err != nil { return nil, err } @@ -397,7 +397,7 @@ func getArmClient(c *authentication.Config) (*ArmClient, error) { // Key Vault Endpoints sender := azure.BuildSender() keyVaultAuth := autorest.NewBearerAuthorizerCallback(sender, func(tenantID, resource string) (*autorest.BearerAuthorizer, error) { - keyVaultSpt, err := authentication.GetAuthorizationToken(c, oauthConfig, resource) + keyVaultSpt, err := c.GetAuthorizationToken(oauthConfig, resource) if err != nil { return nil, err } diff --git a/azurerm/helpers/authentication/auth_method.go b/azurerm/helpers/authentication/auth_method.go new file mode 100644 index 000000000000..d599b2eeec27 --- /dev/null +++ b/azurerm/helpers/authentication/auth_method.go @@ -0,0 +1,20 @@ +package authentication + +import ( + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/adal" +) + +type authMethod interface { + build(b Builder) (authMethod, error) + + isApplicable(b Builder) bool + + getAuthorizationToken(oauthConfig *adal.OAuthConfig, endpoint string) (*autorest.BearerAuthorizer, error) + + name() string + + populateConfig(c *Config) error + + validate() error +} diff --git a/azurerm/helpers/authentication/auth_method_azure_cli_parsing.go b/azurerm/helpers/authentication/auth_method_azure_cli_parsing.go new file mode 100644 index 000000000000..623e68d9ef54 --- /dev/null +++ b/azurerm/helpers/authentication/auth_method_azure_cli_parsing.go @@ -0,0 +1,116 @@ +package authentication + +import ( + "fmt" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/adal" + "github.com/Azure/go-autorest/autorest/azure/cli" + "github.com/hashicorp/go-multierror" +) + +type azureCliParsingAuth struct { + profile *azureCLIProfile +} + +func (a azureCliParsingAuth) build(b Builder) (authMethod, error) { + auth := azureCliParsingAuth{ + profile: &azureCLIProfile{ + clientId: b.ClientID, + environment: b.Environment, + subscriptionId: b.SubscriptionID, + tenantId: b.TenantID, + }, + } + profilePath, err := cli.ProfilePath() + if err != nil { + return nil, fmt.Errorf("Error loading the Profile Path from the Azure CLI: %+v", err) + } + + profile, err := cli.LoadProfile(profilePath) + if err != nil { + return nil, fmt.Errorf("Azure CLI Authorization Profile was not found. Please ensure the Azure CLI is installed and then log-in with `az login`.") + } + + auth.profile.profile = profile + + err = auth.profile.populateFields() + if err != nil { + return nil, err + } + + err = auth.profile.populateClientIdAndAccessToken() + if err != nil { + return nil, fmt.Errorf("Error populating Access Tokens from the Azure CLI: %+v", err) + } + + return auth, nil +} + +func (a azureCliParsingAuth) isApplicable(b Builder) bool { + return b.SupportsAzureCliCloudShellParsing +} + +func (a azureCliParsingAuth) getAuthorizationToken(oauthConfig *adal.OAuthConfig, endpoint string) (*autorest.BearerAuthorizer, error) { + if a.profile.usingCloudShell { + // load the refreshed tokens from the CloudShell Azure CLI credentials + err := a.profile.populateClientIdAndAccessToken() + if err != nil { + return nil, fmt.Errorf("Error loading the refreshed CloudShell tokens: %+v", err) + } + } + + spt, err := adal.NewServicePrincipalTokenFromManualToken(*oauthConfig, a.profile.clientId, endpoint, *a.profile.accessToken) + if err != nil { + return nil, err + } + + err = spt.Refresh() + + if err != nil { + return nil, fmt.Errorf("Error refreshing Service Principal Token: %+v", err) + } + + auth := autorest.NewBearerAuthorizer(spt) + return auth, nil +} + +func (a azureCliParsingAuth) name() string { + return "Parsing credentials from the Azure CLI" +} + +func (a azureCliParsingAuth) populateConfig(c *Config) error { + c.ClientID = a.profile.clientId + c.Environment = a.profile.environment + c.SubscriptionID = a.profile.subscriptionId + c.TenantID = a.profile.tenantId + return nil +} + +func (a azureCliParsingAuth) validate() error { + var err *multierror.Error + + errorMessageFmt := "A %s was not found in your Azure CLI Credentials.\n\nPlease login to the Azure CLI again via `az login`" + + if a.profile == nil { + return fmt.Errorf("Azure CLI Profile is nil - this is an internal error and should be reported.") + } + + if a.profile.accessToken == nil { + err = multierror.Append(err, fmt.Errorf(errorMessageFmt, "Access Token")) + } + + if a.profile.clientId == "" { + err = multierror.Append(err, fmt.Errorf(errorMessageFmt, "Client ID")) + } + + if a.profile.subscriptionId == "" { + err = multierror.Append(err, fmt.Errorf(errorMessageFmt, "Subscription ID")) + } + + if a.profile.tenantId == "" { + err = multierror.Append(err, fmt.Errorf(errorMessageFmt, "Tenant ID")) + } + + return err.ErrorOrNil() +} diff --git a/azurerm/helpers/authentication/auth_method_azure_cli_parsing_test.go b/azurerm/helpers/authentication/auth_method_azure_cli_parsing_test.go new file mode 100644 index 000000000000..c54fa0f7f8e2 --- /dev/null +++ b/azurerm/helpers/authentication/auth_method_azure_cli_parsing_test.go @@ -0,0 +1,157 @@ +package authentication + +import ( + "testing" + + "github.com/Azure/go-autorest/autorest/adal" +) + +func TestAzureCLIParsingAuth_isApplicable(t *testing.T) { + cases := []struct { + Description string + Builder Builder + Valid bool + }{ + { + Description: "Empty Configuration", + Builder: Builder{}, + Valid: false, + }, + { + Description: "Feature Toggled off", + Builder: Builder{ + SupportsAzureCliCloudShellParsing: false, + }, + Valid: false, + }, + { + Description: "Feature Toggled on", + Builder: Builder{ + SupportsAzureCliCloudShellParsing: true, + }, + Valid: true, + }, + } + + for _, v := range cases { + applicable := azureCliParsingAuth{}.isApplicable(v.Builder) + if v.Valid != applicable { + t.Fatalf("Expected %q to be %t but got %t", v.Description, v.Valid, applicable) + } + } +} + +func TestAzureCLIParsingAuth_populateConfig(t *testing.T) { + config := &Config{} + auth := azureCliParsingAuth{ + profile: &azureCLIProfile{ + clientId: "some-subscription-id", + environment: "dimension-c137", + subscriptionId: "some-subscription-id", + tenantId: "some-tenant-id", + }, + } + + err := auth.populateConfig(config) + if err != nil { + t.Fatalf("Error populating config: %s", err) + } + + if auth.profile.clientId != config.ClientID { + t.Fatalf("Expected Client ID to be %q but got %q", auth.profile.tenantId, config.TenantID) + } + + if auth.profile.environment != config.Environment { + t.Fatalf("Expected Environment to be %q but got %q", auth.profile.tenantId, config.TenantID) + } + + if auth.profile.subscriptionId != config.SubscriptionID { + t.Fatalf("Expected Subscription ID to be %q but got %q", auth.profile.tenantId, config.TenantID) + } + + if auth.profile.tenantId != config.TenantID { + t.Fatalf("Expected Tenant ID to be %q but got %q", auth.profile.tenantId, config.TenantID) + } +} + +func TestAzureCLIParsingAuth_validate(t *testing.T) { + cases := []struct { + Description string + Config azureCliParsingAuth + ExpectError bool + }{ + { + Description: "Empty Configuration", + Config: azureCliParsingAuth{}, + ExpectError: true, + }, + { + Description: "Missing Access Token", + Config: azureCliParsingAuth{ + profile: &azureCLIProfile{ + clientId: "62e73395-5017-43b6-8ebf-d6c30a514cf1", + subscriptionId: "8e8b5e02-5c13-4822-b7dc-4232afb7e8c2", + tenantId: "9834f8d0-24b3-41b7-8b8d-c611c461a129", + }, + }, + ExpectError: true, + }, + { + Description: "Missing Client ID", + Config: azureCliParsingAuth{ + profile: &azureCLIProfile{ + accessToken: &adal.Token{}, + subscriptionId: "8e8b5e02-5c13-4822-b7dc-4232afb7e8c2", + tenantId: "9834f8d0-24b3-41b7-8b8d-c611c461a129", + }, + }, + ExpectError: true, + }, + { + Description: "Missing Subscription ID", + Config: azureCliParsingAuth{ + profile: &azureCLIProfile{ + accessToken: &adal.Token{}, + clientId: "62e73395-5017-43b6-8ebf-d6c30a514cf1", + tenantId: "9834f8d0-24b3-41b7-8b8d-c611c461a129", + }, + }, + ExpectError: true, + }, + { + Description: "Missing Tenant ID", + Config: azureCliParsingAuth{ + profile: &azureCLIProfile{ + accessToken: &adal.Token{}, + clientId: "62e73395-5017-43b6-8ebf-d6c30a514cf1", + subscriptionId: "8e8b5e02-5c13-4822-b7dc-4232afb7e8c2", + }, + }, + ExpectError: true, + }, + { + Description: "Valid Configuration", + Config: azureCliParsingAuth{ + profile: &azureCLIProfile{ + accessToken: &adal.Token{}, + clientId: "62e73395-5017-43b6-8ebf-d6c30a514cf1", + subscriptionId: "8e8b5e02-5c13-4822-b7dc-4232afb7e8c2", + tenantId: "9834f8d0-24b3-41b7-8b8d-c611c461a129", + }, + }, + ExpectError: false, + }, + } + + for _, v := range cases { + err := v.Config.validate() + + if v.ExpectError && err == nil { + t.Fatalf("Expected an error for %q: didn't get one", v.Description) + } + + if !v.ExpectError && err != nil { + t.Fatalf("Expected there to be no error for %q - but got: %v", v.Description, err) + } + } +} diff --git a/azurerm/helpers/authentication/auth_method_client_cert.go b/azurerm/helpers/authentication/auth_method_client_cert.go new file mode 100644 index 000000000000..00a0d2794410 --- /dev/null +++ b/azurerm/helpers/authentication/auth_method_client_cert.go @@ -0,0 +1,123 @@ +package authentication + +import ( + "crypto/rsa" + "crypto/x509" + "fmt" + "io/ioutil" + "os" + "strings" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/adal" + "github.com/hashicorp/go-multierror" + "golang.org/x/crypto/pkcs12" +) + +type servicePrincipalClientCertificateAuth struct { + clientId string + clientCertPath string + clientCertPassword string + subscriptionId string + tenantId string +} + +func (a servicePrincipalClientCertificateAuth) build(b Builder) (authMethod, error) { + method := servicePrincipalClientCertificateAuth{ + clientId: b.ClientID, + clientCertPath: b.ClientCertPath, + clientCertPassword: b.ClientCertPassword, + subscriptionId: b.SubscriptionID, + tenantId: b.TenantID, + } + return method, nil +} + +func (a servicePrincipalClientCertificateAuth) isApplicable(b Builder) bool { + return b.SupportsClientCertAuth && b.ClientCertPath != "" +} + +func (a servicePrincipalClientCertificateAuth) name() string { + return "Service Principal / Client Certificate" +} + +func (a servicePrincipalClientCertificateAuth) getAuthorizationToken(oauthConfig *adal.OAuthConfig, endpoint string) (*autorest.BearerAuthorizer, error) { + certificateData, err := ioutil.ReadFile(a.clientCertPath) + if err != nil { + return nil, fmt.Errorf("Error reading Client Certificate %q: %v", a.clientCertPath, err) + } + + // Get the certificate and private key from pfx file + certificate, rsaPrivateKey, err := decodePkcs12(certificateData, a.clientCertPassword) + if err != nil { + return nil, fmt.Errorf("Error decoding pkcs12 certificate: %v", err) + } + + spt, err := adal.NewServicePrincipalTokenFromCertificate(*oauthConfig, a.clientId, certificate, rsaPrivateKey, endpoint) + if err != nil { + return nil, err + } + + err = spt.Refresh() + if err != nil { + return nil, err + } + + auth := autorest.NewBearerAuthorizer(spt) + return auth, nil +} + +func (a servicePrincipalClientCertificateAuth) populateConfig(c *Config) error { + c.AuthenticatedAsAServicePrincipal = true + return nil +} + +func (a servicePrincipalClientCertificateAuth) validate() error { + var err *multierror.Error + + fmtErrorMessage := "A %s must be configured when authenticating as a Service Principal using a Client Certificate." + + if a.subscriptionId == "" { + err = multierror.Append(err, fmt.Errorf(fmtErrorMessage, "Subscription ID")) + } + + if a.clientId == "" { + err = multierror.Append(err, fmt.Errorf(fmtErrorMessage, "Client ID")) + } + + if a.clientCertPath == "" { + err = multierror.Append(err, fmt.Errorf(fmtErrorMessage, "Client Certificate Path")) + } else { + if strings.HasSuffix(strings.ToLower(a.clientCertPath), ".pfx") { + // ensure it exists on disk + _, fileErr := os.Stat(a.clientCertPath) + if os.IsNotExist(fileErr) { + err = multierror.Append(err, fmt.Errorf("Error locating Client Certificate specified at %q: %s", a.clientCertPath, fileErr)) + } + + // we're intentionally /not/ checking it's an actual PFX file at this point, as that happens in the getAuthorizationToken + } else { + err = multierror.Append(err, fmt.Errorf("The Client Certificate Path is not a *.pfx file: %q", a.clientCertPath)) + } + } + + if a.tenantId == "" { + err = multierror.Append(err, fmt.Errorf(fmtErrorMessage, "Tenant ID")) + } + + return err.ErrorOrNil() +} + +func decodePkcs12(pkcs []byte, password string) (*x509.Certificate, *rsa.PrivateKey, error) { + privateKey, certificate, err := pkcs12.Decode(pkcs, password) + if err != nil { + return nil, nil, err + } + + rsaPrivateKey, isRsaKey := privateKey.(*rsa.PrivateKey) + if !isRsaKey { + return nil, nil, fmt.Errorf("PKCS#12 certificate must contain an RSA private key") + } + + return certificate, rsaPrivateKey, nil +} diff --git a/azurerm/helpers/authentication/auth_method_client_cert_test.go b/azurerm/helpers/authentication/auth_method_client_cert_test.go new file mode 100644 index 000000000000..6a0a81beed2f --- /dev/null +++ b/azurerm/helpers/authentication/auth_method_client_cert_test.go @@ -0,0 +1,217 @@ +package authentication + +import ( + "io/ioutil" + "os" + "testing" +) + +func TestServicePrincipalClientCertAuth_builder(t *testing.T) { + builder := Builder{ + ClientID: "some-client-id", + ClientCertPath: "some-client-cert-path", + ClientCertPassword: "some-password", + Environment: "some-environment", + SubscriptionID: "some-subscription-id", + TenantID: "some-tenant-id", + } + config, err := servicePrincipalClientCertificateAuth{}.build(builder) + if err != nil { + t.Fatalf("Error building client cert auth: %s", err) + } + + servicePrincipal := config.(servicePrincipalClientCertificateAuth) + + if builder.ClientID != servicePrincipal.clientId { + t.Fatalf("Expected Client ID to be %q but got %q", builder.ClientID, servicePrincipal.clientId) + } + + if builder.ClientCertPath != servicePrincipal.clientCertPath { + t.Fatalf("Expected Client Certificate Path to be %q but got %q", builder.ClientCertPath, servicePrincipal.clientCertPath) + } + + if builder.ClientCertPassword != servicePrincipal.clientCertPassword { + t.Fatalf("Expected Client Certificate Password to be %q but got %q", builder.ClientCertPassword, servicePrincipal.clientCertPassword) + } + + if builder.SubscriptionID != servicePrincipal.subscriptionId { + t.Fatalf("Expected Subscription ID to be %q but got %q", builder.SubscriptionID, servicePrincipal.subscriptionId) + } + + if builder.TenantID != servicePrincipal.tenantId { + t.Fatalf("Expected Tenant ID to be %q but got %q", builder.TenantID, servicePrincipal.tenantId) + } +} + +func TestServicePrincipalClientCertAuth_isApplicable(t *testing.T) { + cases := []struct { + Description string + Builder Builder + Valid bool + }{ + { + Description: "Empty Configuration", + Builder: Builder{}, + Valid: false, + }, + { + Description: "Feature Toggled off", + Builder: Builder{ + SupportsClientCertAuth: false, + }, + Valid: false, + }, + { + Description: "Feature Toggled on but no cert specified", + Builder: Builder{ + SupportsClientCertAuth: true, + }, + Valid: false, + }, + { + Description: "Cert specified but feature toggled off", + Builder: Builder{ + ClientCertPath: "./path/to/file", + }, + Valid: false, + }, + { + Description: "Valid configuration", + Builder: Builder{ + SupportsClientCertAuth: true, + ClientCertPath: "./path/to/file", + }, + Valid: true, + }, + } + + for _, v := range cases { + applicable := servicePrincipalClientCertificateAuth{}.isApplicable(v.Builder) + if v.Valid != applicable { + t.Fatalf("Expected %q to be %t but got %t", v.Description, v.Valid, applicable) + } + } +} + +func TestServicePrincipalClientCertAuth_populateConfig(t *testing.T) { + config := &Config{} + err := servicePrincipalClientCertificateAuth{}.populateConfig(config) + if err != nil { + t.Fatalf("Error populating config: %s", err) + } + + if config.AuthenticatedAsAServicePrincipal != true { + t.Fatalf("Expected `AuthenticatedAsAServicePrincipal` to be true but it wasn't") + } +} + +func TestServicePrincipalClientCertAuth_validate(t *testing.T) { + data := []byte("client-cert-auth") + filePath := "./example.pfx" + err := ioutil.WriteFile(filePath, data, 0600) + if err != nil { + t.Fatal(err) + } + defer os.Remove(filePath) + + cases := []struct { + Description string + Config servicePrincipalClientCertificateAuth + ExpectError bool + }{ + { + Description: "Empty Configuration", + Config: servicePrincipalClientCertificateAuth{}, + ExpectError: true, + }, + { + Description: "Missing Client ID", + Config: servicePrincipalClientCertificateAuth{ + subscriptionId: "8e8b5e02-5c13-4822-b7dc-4232afb7e8c2", + clientCertPath: filePath, + tenantId: "9834f8d0-24b3-41b7-8b8d-c611c461a129", + }, + ExpectError: true, + }, + { + Description: "Missing Subscription ID", + Config: servicePrincipalClientCertificateAuth{ + clientId: "62e73395-5017-43b6-8ebf-d6c30a514cf1", + clientCertPath: filePath, + tenantId: "9834f8d0-24b3-41b7-8b8d-c611c461a129", + }, + ExpectError: true, + }, + { + Description: "Missing Client Certificate Path", + Config: servicePrincipalClientCertificateAuth{ + clientId: "62e73395-5017-43b6-8ebf-d6c30a514cf1", + subscriptionId: "8e8b5e02-5c13-4822-b7dc-4232afb7e8c2", + tenantId: "9834f8d0-24b3-41b7-8b8d-c611c461a129", + }, + ExpectError: true, + }, + { + Description: "Missing Tenant ID", + Config: servicePrincipalClientCertificateAuth{ + clientId: "62e73395-5017-43b6-8ebf-d6c30a514cf1", + subscriptionId: "8e8b5e02-5c13-4822-b7dc-4232afb7e8c2", + clientCertPath: filePath, + }, + ExpectError: true, + }, + { + Description: "File isn't a pfx", + Config: servicePrincipalClientCertificateAuth{ + clientId: "62e73395-5017-43b6-8ebf-d6c30a514cf1", + subscriptionId: "8e8b5e02-5c13-4822-b7dc-4232afb7e8c2", + clientCertPath: "not-valid.txt", + tenantId: "9834f8d0-24b3-41b7-8b8d-c611c461a129", + }, + ExpectError: true, + }, + { + Description: "File does not exist", + Config: servicePrincipalClientCertificateAuth{ + clientId: "62e73395-5017-43b6-8ebf-d6c30a514cf1", + subscriptionId: "8e8b5e02-5c13-4822-b7dc-4232afb7e8c2", + clientCertPath: "does-not-exist.pfx", + tenantId: "9834f8d0-24b3-41b7-8b8d-c611c461a129", + }, + ExpectError: true, + }, + { + Description: "Valid Configuration (Basic)", + Config: servicePrincipalClientCertificateAuth{ + clientId: "62e73395-5017-43b6-8ebf-d6c30a514cf1", + subscriptionId: "8e8b5e02-5c13-4822-b7dc-4232afb7e8c2", + clientCertPath: filePath, + tenantId: "9834f8d0-24b3-41b7-8b8d-c611c461a129", + }, + ExpectError: false, + }, + { + Description: "Valid Configuration (Complete)", + Config: servicePrincipalClientCertificateAuth{ + clientId: "62e73395-5017-43b6-8ebf-d6c30a514cf1", + subscriptionId: "8e8b5e02-5c13-4822-b7dc-4232afb7e8c2", + clientCertPath: filePath, + clientCertPassword: "Password1234!", + tenantId: "9834f8d0-24b3-41b7-8b8d-c611c461a129", + }, + ExpectError: false, + }, + } + + for _, v := range cases { + err := v.Config.validate() + + if v.ExpectError && err == nil { + t.Fatalf("Expected an error for %q: didn't get one", v.Description) + } + + if !v.ExpectError && err != nil { + t.Fatalf("Expected there to be no error for %q - but got: %v", v.Description, err) + } + } +} diff --git a/azurerm/helpers/authentication/auth_method_client_secret.go b/azurerm/helpers/authentication/auth_method_client_secret.go new file mode 100644 index 000000000000..4e41d5a93535 --- /dev/null +++ b/azurerm/helpers/authentication/auth_method_client_secret.go @@ -0,0 +1,70 @@ +package authentication + +import ( + "fmt" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/adal" + "github.com/hashicorp/go-multierror" +) + +type servicePrincipalClientSecretAuth struct { + clientId string + clientSecret string + subscriptionId string + tenantId string +} + +func (a servicePrincipalClientSecretAuth) build(b Builder) (authMethod, error) { + method := servicePrincipalClientSecretAuth{ + clientId: b.ClientID, + clientSecret: b.ClientSecret, + subscriptionId: b.SubscriptionID, + tenantId: b.TenantID, + } + return method, nil +} + +func (a servicePrincipalClientSecretAuth) isApplicable(b Builder) bool { + return b.SupportsClientSecretAuth && b.ClientSecret != "" +} + +func (a servicePrincipalClientSecretAuth) name() string { + return "Service Principal / Client Secret" +} + +func (a servicePrincipalClientSecretAuth) getAuthorizationToken(oauthConfig *adal.OAuthConfig, endpoint string) (*autorest.BearerAuthorizer, error) { + spt, err := adal.NewServicePrincipalToken(*oauthConfig, a.clientId, a.clientSecret, endpoint) + if err != nil { + return nil, err + } + + auth := autorest.NewBearerAuthorizer(spt) + return auth, nil +} + +func (a servicePrincipalClientSecretAuth) populateConfig(c *Config) error { + c.AuthenticatedAsAServicePrincipal = true + return nil +} + +func (a servicePrincipalClientSecretAuth) validate() error { + var err *multierror.Error + + fmtErrorMessage := "A %s must be configured when authenticating as a Service Principal using a Client Secret." + + if a.subscriptionId == "" { + err = multierror.Append(err, fmt.Errorf(fmtErrorMessage, "Subscription ID")) + } + if a.clientId == "" { + err = multierror.Append(err, fmt.Errorf(fmtErrorMessage, "Client ID")) + } + if a.clientSecret == "" { + err = multierror.Append(err, fmt.Errorf(fmtErrorMessage, "Client Secret")) + } + if a.tenantId == "" { + err = multierror.Append(err, fmt.Errorf(fmtErrorMessage, "Tenant ID")) + } + + return err.ErrorOrNil() +} diff --git a/azurerm/helpers/authentication/auth_method_client_secret_test.go b/azurerm/helpers/authentication/auth_method_client_secret_test.go new file mode 100644 index 000000000000..e06e0b82c85f --- /dev/null +++ b/azurerm/helpers/authentication/auth_method_client_secret_test.go @@ -0,0 +1,167 @@ +package authentication + +import "testing" + +func TestServicePrincipalClientSecretAuth_builder(t *testing.T) { + builder := Builder{ + ClientID: "some-client-id", + ClientSecret: "some-client-secret", + SubscriptionID: "some-subscription-id", + TenantID: "some-tenant-id", + } + config, err := servicePrincipalClientSecretAuth{}.build(builder) + if err != nil { + t.Fatalf("Error building client secret auth: %s", err) + } + servicePrincipal := config.(servicePrincipalClientSecretAuth) + + if builder.ClientID != servicePrincipal.clientId { + t.Fatalf("Expected Client ID to be %q but got %q", builder.ClientID, servicePrincipal.clientId) + } + + if builder.ClientSecret != servicePrincipal.clientSecret { + t.Fatalf("Expected Client Secret to be %q but got %q", builder.ClientSecret, servicePrincipal.clientSecret) + } + + if builder.SubscriptionID != servicePrincipal.subscriptionId { + t.Fatalf("Expected Subscription ID to be %q but got %q", builder.SubscriptionID, servicePrincipal.subscriptionId) + } + + if builder.TenantID != servicePrincipal.tenantId { + t.Fatalf("Expected Tenant ID to be %q but got %q", builder.TenantID, servicePrincipal.tenantId) + } +} + +func TestServicePrincipalClientSecretAuth_isApplicable(t *testing.T) { + cases := []struct { + Description string + Builder Builder + Valid bool + }{ + { + Description: "Empty Configuration", + Builder: Builder{}, + Valid: false, + }, + { + Description: "Feature Toggled off", + Builder: Builder{ + SupportsClientSecretAuth: false, + }, + Valid: false, + }, + { + Description: "Feature Toggled on but no secret specified", + Builder: Builder{ + SupportsClientSecretAuth: true, + }, + Valid: false, + }, + { + Description: "Secret specified but feature toggled off", + Builder: Builder{ + ClientSecret: "I turned myself into a pickle morty!", + }, + Valid: false, + }, + { + Description: "Valid configuration", + Builder: Builder{ + SupportsClientSecretAuth: true, + ClientSecret: "I turned myself into a pickle morty!", + }, + Valid: true, + }, + } + + for _, v := range cases { + applicable := servicePrincipalClientSecretAuth{}.isApplicable(v.Builder) + if v.Valid != applicable { + t.Fatalf("Expected %q to be %t but got %t", v.Description, v.Valid, applicable) + } + } +} + +func TestServicePrincipalClientSecretAuth_populateConfig(t *testing.T) { + config := &Config{} + err := servicePrincipalClientSecretAuth{}.populateConfig(config) + if err != nil { + t.Fatalf("Error populating config: %s", err) + } + + if config.AuthenticatedAsAServicePrincipal != true { + t.Fatalf("Expected `AuthenticatedAsAServicePrincipal` to be true but it wasn't") + } +} + +func TestServicePrincipalClientSecretAuth_validate(t *testing.T) { + cases := []struct { + Description string + Config servicePrincipalClientSecretAuth + ExpectError bool + }{ + { + Description: "Empty Configuration", + Config: servicePrincipalClientSecretAuth{}, + ExpectError: true, + }, + { + Description: "Missing Client ID", + Config: servicePrincipalClientSecretAuth{ + subscriptionId: "8e8b5e02-5c13-4822-b7dc-4232afb7e8c2", + clientSecret: "Does Hammer Time have Daylight Savings Time?", + tenantId: "9834f8d0-24b3-41b7-8b8d-c611c461a129", + }, + ExpectError: true, + }, + { + Description: "Missing Subscription ID", + Config: servicePrincipalClientSecretAuth{ + clientId: "62e73395-5017-43b6-8ebf-d6c30a514cf1", + clientSecret: "Does Hammer Time have Daylight Savings Time?", + tenantId: "9834f8d0-24b3-41b7-8b8d-c611c461a129", + }, + ExpectError: true, + }, + { + Description: "Missing Client Secret", + Config: servicePrincipalClientSecretAuth{ + clientId: "62e73395-5017-43b6-8ebf-d6c30a514cf1", + subscriptionId: "8e8b5e02-5c13-4822-b7dc-4232afb7e8c2", + tenantId: "9834f8d0-24b3-41b7-8b8d-c611c461a129", + }, + ExpectError: true, + }, + { + Description: "Missing Tenant ID", + Config: servicePrincipalClientSecretAuth{ + clientId: "62e73395-5017-43b6-8ebf-d6c30a514cf1", + subscriptionId: "8e8b5e02-5c13-4822-b7dc-4232afb7e8c2", + clientSecret: "Does Hammer Time have Daylight Savings Time?", + }, + ExpectError: true, + }, + { + Description: "Valid Configuration", + Config: servicePrincipalClientSecretAuth{ + clientId: "62e73395-5017-43b6-8ebf-d6c30a514cf1", + subscriptionId: "8e8b5e02-5c13-4822-b7dc-4232afb7e8c2", + clientSecret: "Does Hammer Time have Daylight Savings Time?", + tenantId: "9834f8d0-24b3-41b7-8b8d-c611c461a129", + }, + ExpectError: false, + }, + } + + for _, v := range cases { + err := v.Config.validate() + + if v.ExpectError && err == nil { + t.Fatalf("Expected an error for %q: didn't get one", v.Description) + } + + if !v.ExpectError && err != nil { + t.Fatalf("Expected there to be no error for %q - but got: %v", v.Description, err) + } + } +} diff --git a/azurerm/helpers/authentication/auth_method_msi.go b/azurerm/helpers/authentication/auth_method_msi.go new file mode 100644 index 000000000000..9b0de8f5d463 --- /dev/null +++ b/azurerm/helpers/authentication/auth_method_msi.go @@ -0,0 +1,64 @@ +package authentication + +import ( + "fmt" + "log" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/adal" + "github.com/hashicorp/go-multierror" +) + +type managedServiceIdentityAuth struct { + endpoint string +} + +func (a managedServiceIdentityAuth) build(b Builder) (authMethod, error) { + endpoint := b.MsiEndpoint + if endpoint == "" { + msiEndpoint, err := adal.GetMSIVMEndpoint() + if err != nil { + return nil, fmt.Errorf("Error determining MSI Endpoint: ensure the VM has MSI enabled, or configure the MSI Endpoint. Error: %s", err) + } + endpoint = msiEndpoint + } + + log.Printf("[DEBUG] Using MSI endpoint %q", endpoint) + + auth := managedServiceIdentityAuth{ + endpoint: endpoint, + } + return auth, nil +} + +func (a managedServiceIdentityAuth) isApplicable(b Builder) bool { + return b.SupportsManagedServiceIdentity +} + +func (a managedServiceIdentityAuth) name() string { + return "Managed Service Identity" +} + +func (a managedServiceIdentityAuth) getAuthorizationToken(oauthConfig *adal.OAuthConfig, endpoint string) (*autorest.BearerAuthorizer, error) { + spt, err := adal.NewServicePrincipalTokenFromMSI(a.endpoint, endpoint) + if err != nil { + return nil, err + } + auth := autorest.NewBearerAuthorizer(spt) + return auth, nil +} + +func (a managedServiceIdentityAuth) populateConfig(c *Config) error { + // nothing to populate back + return nil +} + +func (a managedServiceIdentityAuth) validate() error { + var err *multierror.Error + + if a.endpoint == "" { + err = multierror.Append(err, fmt.Errorf("An MSI Endpoint must be configured")) + } + + return err.ErrorOrNil() +} diff --git a/azurerm/helpers/authentication/auth_method_msi_test.go b/azurerm/helpers/authentication/auth_method_msi_test.go new file mode 100644 index 000000000000..f026d89c5c22 --- /dev/null +++ b/azurerm/helpers/authentication/auth_method_msi_test.go @@ -0,0 +1,97 @@ +package authentication + +import "testing" + +func TestManagedServiceIdentity_builder(t *testing.T) { + builder := Builder{ + MsiEndpoint: "https://hello-world", + } + + method, err := managedServiceIdentityAuth{}.build(builder) + if err != nil { + t.Fatalf("Error building MSI Identity Auth: %+v", err) + } + + authMethod := method.(managedServiceIdentityAuth) + if builder.MsiEndpoint != authMethod.endpoint { + t.Fatalf("Expected MSI Endpoint to be %q but got %q", builder.MsiEndpoint, authMethod.endpoint) + } +} + +func TestManagedServiceIdentity_isApplicable(t *testing.T) { + cases := []struct { + Description string + Builder Builder + Valid bool + }{ + { + Description: "Empty Configuration", + Builder: Builder{}, + Valid: false, + }, + { + Description: "Feature Toggled off", + Builder: Builder{ + SupportsManagedServiceIdentity: false, + }, + Valid: false, + }, + { + Description: "Feature Toggled on", + Builder: Builder{ + SupportsManagedServiceIdentity: true, + }, + Valid: false, + }, + } + + for _, v := range cases { + applicable := servicePrincipalClientSecretAuth{}.isApplicable(v.Builder) + if v.Valid != applicable { + t.Fatalf("Expected %q to be %t but got %t", v.Description, v.Valid, applicable) + } + } +} + +func TestManagedServiceIdentity_populateConfig(t *testing.T) { + config := &Config{} + err := servicePrincipalClientSecretAuth{}.populateConfig(config) + if err != nil { + t.Fatalf("Error populating config: %s", err) + } + + // nothing to check since it's not doing anything +} + +func TestManagedServiceIdentity_validate(t *testing.T) { + cases := []struct { + Description string + Config managedServiceIdentityAuth + ExpectError bool + }{ + { + Description: "Empty Configuration", + Config: managedServiceIdentityAuth{}, + ExpectError: true, + }, + { + Description: "Valid Configuration", + Config: managedServiceIdentityAuth{ + endpoint: "https://some-location", + }, + ExpectError: false, + }, + } + + for _, v := range cases { + err := v.Config.validate() + + if v.ExpectError && err == nil { + t.Fatalf("Expected an error for %q: didn't get one", v.Description) + } + + if !v.ExpectError && err != nil { + t.Fatalf("Expected there to be no error for %q - but got: %v", v.Description, err) + } + } +} diff --git a/azurerm/helpers/authentication/authorization_token.go b/azurerm/helpers/authentication/authorization_token.go deleted file mode 100644 index 77927b8f8259..000000000000 --- a/azurerm/helpers/authentication/authorization_token.go +++ /dev/null @@ -1,54 +0,0 @@ -package authentication - -import ( - "fmt" - - "github.com/Azure/go-autorest/autorest" - "github.com/Azure/go-autorest/autorest/adal" -) - -// GetAuthorizationToken returns an authentication token for the current authentication method -func GetAuthorizationToken(c *Config, oauthConfig *adal.OAuthConfig, endpoint string) (*autorest.BearerAuthorizer, error) { - useServicePrincipal := c.ClientSecret != "" - - if useServicePrincipal { - spt, err := adal.NewServicePrincipalToken(*oauthConfig, c.ClientID, c.ClientSecret, endpoint) - if err != nil { - return nil, err - } - - auth := autorest.NewBearerAuthorizer(spt) - return auth, nil - } - - if c.UseMsi { - spt, err := adal.NewServicePrincipalTokenFromMSI(c.MsiEndpoint, endpoint) - if err != nil { - return nil, err - } - auth := autorest.NewBearerAuthorizer(spt) - return auth, nil - } - - if c.IsCloudShell { - // load the refreshed tokens from the Azure CLI - err := c.LoadTokensFromAzureCLI() - if err != nil { - return nil, fmt.Errorf("Error loading the refreshed CloudShell tokens: %+v", err) - } - } - - spt, err := adal.NewServicePrincipalTokenFromManualToken(*oauthConfig, c.ClientID, endpoint, *c.AccessToken) - if err != nil { - return nil, err - } - - err = spt.Refresh() - - if err != nil { - return nil, fmt.Errorf("Error refreshing Service Principal Token: %+v", err) - } - - auth := autorest.NewBearerAuthorizer(spt) - return auth, nil -} diff --git a/azurerm/helpers/authentication/access_token.go b/azurerm/helpers/authentication/azure_cli_access_token.go similarity index 92% rename from azurerm/helpers/authentication/access_token.go rename to azurerm/helpers/authentication/azure_cli_access_token.go index 6fa06355b8d6..fc35da3594a0 100644 --- a/azurerm/helpers/authentication/access_token.go +++ b/azurerm/helpers/authentication/azure_cli_access_token.go @@ -10,13 +10,13 @@ import ( "github.com/Azure/go-autorest/autorest/azure/cli" ) -type AccessToken struct { +type azureCliAccessToken struct { ClientID string AccessToken *adal.Token IsCloudShell bool } -func findValidAccessTokenForTenant(tokens []cli.Token, tenantId string) (*AccessToken, error) { +func findValidAccessTokenForTenant(tokens []cli.Token, tenantId string) (*azureCliAccessToken, error) { for _, accessToken := range tokens { token, err := accessToken.ToADALToken() if err != nil { @@ -43,7 +43,7 @@ func findValidAccessTokenForTenant(tokens []cli.Token, tenantId string) (*Access continue } - validAccessToken := AccessToken{ + validAccessToken := azureCliAccessToken{ ClientID: accessToken.ClientID, AccessToken: &token, IsCloudShell: accessToken.RefreshToken == "", diff --git a/azurerm/helpers/authentication/access_token_test.go b/azurerm/helpers/authentication/azure_cli_access_token_test.go similarity index 100% rename from azurerm/helpers/authentication/access_token_test.go rename to azurerm/helpers/authentication/azure_cli_access_token_test.go diff --git a/azurerm/helpers/authentication/azure_cli_profile.go b/azurerm/helpers/authentication/azure_cli_profile.go index 8b563d7ab9cd..f33d8a496117 100644 --- a/azurerm/helpers/authentication/azure_cli_profile.go +++ b/azurerm/helpers/authentication/azure_cli_profile.go @@ -1,33 +1,49 @@ package authentication import ( - "strings" - - "fmt" - + "github.com/Azure/go-autorest/autorest/adal" "github.com/Azure/go-autorest/autorest/azure/cli" ) -type AzureCLIProfile struct { - cli.Profile +type azureCLIProfile struct { + profile cli.Profile + + clientId string + environment string + subscriptionId string + tenantId string + accessToken *adal.Token + usingCloudShell bool } -func (a AzureCLIProfile) FindDefaultSubscriptionId() (string, error) { - for _, subscription := range a.Subscriptions { - if subscription.IsDefault { - return subscription.ID, nil +func (a *azureCLIProfile) populateFields() error { + // ensure we know the Subscription ID - since it's needed for everything else + if a.subscriptionId == "" { + err := a.populateSubscriptionID() + if err != nil { + return err } } - return "", fmt.Errorf("No Subscription was Marked as Default in the Azure Profile.") -} - -func (a AzureCLIProfile) FindSubscription(subscriptionId string) (*cli.Subscription, error) { - for _, subscription := range a.Subscriptions { - if strings.EqualFold(subscription.ID, subscriptionId) { - return &subscription, nil + if a.tenantId == "" { + // now we know the subscription ID, find the associated Tenant ID + err := a.populateTenantID() + if err != nil { + return err } } - return nil, fmt.Errorf("Subscription %q was not found in your Azure CLI credentials. Please verify it exists in `az account list`.", subscriptionId) + // now we know the Subscription ID & Tenant ID we can find the associated Client ID/Access Token + err := a.populateClientIdAndAccessToken() + if err != nil { + return err + } + + // always pull the environment from the Azure CLI, since the Access Token's associated with it + err = a.populateEnvironment() + if err != nil { + return err + } + + return nil } diff --git a/azurerm/helpers/authentication/azure_cli_profile_population.go b/azurerm/helpers/authentication/azure_cli_profile_population.go new file mode 100644 index 000000000000..65aa7f6d1e9f --- /dev/null +++ b/azurerm/helpers/authentication/azure_cli_profile_population.go @@ -0,0 +1,83 @@ +package authentication + +import ( + "fmt" + "strings" + + "github.com/Azure/go-autorest/autorest/azure/cli" +) + +func (a *azureCLIProfile) populateSubscriptionID() error { + subscriptionId, err := a.findDefaultSubscriptionId() + if err != nil { + return err + } + + a.subscriptionId = subscriptionId + return nil +} + +func (a *azureCLIProfile) populateTenantID() error { + subscription, err := a.findSubscription(a.subscriptionId) + if err != nil { + return err + } + + a.tenantId = subscription.TenantID + return nil +} + +func (a *azureCLIProfile) populateClientIdAndAccessToken() error { + // we can now pull out the ClientID and the Access Token to use from the Access Token + tokensPath, err := cli.AccessTokensPath() + if err != nil { + return fmt.Errorf("Error loading the Tokens Path from the Azure CLI: %+v", err) + } + + tokens, err := cli.LoadTokens(tokensPath) + if err != nil { + return fmt.Errorf("No Authorization Tokens were found - please ensure the Azure CLI is installed and then log-in with `az login`.") + } + + validToken, err := findValidAccessTokenForTenant(tokens, a.tenantId) + if err != nil { + return fmt.Errorf("No (unexpired) Authorization Tokens were found - please re-authenticate using `az login`.") + } + + token := *validToken + a.accessToken = token.AccessToken + a.clientId = token.ClientID + a.usingCloudShell = token.IsCloudShell + + return nil +} + +func (a *azureCLIProfile) populateEnvironment() error { + subscription, err := a.findSubscription(a.subscriptionId) + if err != nil { + return err + } + + a.environment = normalizeEnvironmentName(subscription.EnvironmentName) + return nil +} + +func (a azureCLIProfile) findDefaultSubscriptionId() (string, error) { + for _, subscription := range a.profile.Subscriptions { + if subscription.IsDefault { + return subscription.ID, nil + } + } + + return "", fmt.Errorf("No Subscription was Marked as Default in the Azure Profile.") +} + +func (a azureCLIProfile) findSubscription(subscriptionId string) (*cli.Subscription, error) { + for _, subscription := range a.profile.Subscriptions { + if strings.EqualFold(subscription.ID, subscriptionId) { + return &subscription, nil + } + } + + return nil, fmt.Errorf("Subscription %q was not found in your Azure CLI credentials. Please verify it exists in `az account list`.", subscriptionId) +} diff --git a/azurerm/helpers/authentication/azure_cli_profile_population_test.go b/azurerm/helpers/authentication/azure_cli_profile_population_test.go new file mode 100644 index 000000000000..8745f5541e72 --- /dev/null +++ b/azurerm/helpers/authentication/azure_cli_profile_population_test.go @@ -0,0 +1,121 @@ +package authentication + +import ( + "testing" + + "github.com/Azure/go-autorest/autorest/azure/cli" +) + +func TestAzureCliProfile_populateSubscriptionIdMissing(t *testing.T) { + cliProfile := azureCLIProfile{ + profile: cli.Profile{ + Subscriptions: []cli.Subscription{}, + }, + } + + err := cliProfile.populateSubscriptionID() + if err == nil { + t.Fatalf("Expected an error to be returned - but didn't get one") + } +} + +func TestAzureCliProfile_populateSubscriptionIdNoDefault(t *testing.T) { + cliProfile := azureCLIProfile{ + profile: cli.Profile{ + Subscriptions: []cli.Subscription{ + { + IsDefault: false, + ID: "abc123", + }, + }, + }, + } + + err := cliProfile.populateSubscriptionID() + if err == nil { + t.Fatalf("Expected an error to be returned - but didn't get one") + } +} + +func TestAzureCliProfile_populateSubscriptionIdValid(t *testing.T) { + subscriptionId := "abc123" + cliProfile := azureCLIProfile{ + profile: cli.Profile{ + Subscriptions: []cli.Subscription{ + { + IsDefault: true, + ID: subscriptionId, + }, + }, + }, + } + + err := cliProfile.populateSubscriptionID() + if err != nil { + t.Fatalf("Expected no error to be returned - but got: %+v", err) + } + + if cliProfile.subscriptionId != subscriptionId { + t.Fatalf("Expected the Subscription ID to be %q but got %q", subscriptionId, cliProfile.subscriptionId) + } +} + +func TestAzureCliProfile_populateTenantIdEmpty(t *testing.T) { + cliProfile := azureCLIProfile{ + profile: cli.Profile{ + Subscriptions: []cli.Subscription{}, + }, + } + + err := cliProfile.populateEnvironment() + if err == nil { + t.Fatalf("Expected an error to be returned - but didn't get one") + } +} + +func TestAzureCliProfile_populateTenantIdMissingSubscription(t *testing.T) { + cliProfile := azureCLIProfile{ + subscriptionId: "bcd234", + profile: cli.Profile{ + Subscriptions: []cli.Subscription{ + { + IsDefault: false, + ID: "abc123", + }, + }, + }, + } + + err := cliProfile.populateTenantID() + if err == nil { + t.Fatalf("Expected an error to be returned - but didn't get one") + } +} + +func TestAzureCliProfile_populateTenantIdValid(t *testing.T) { + cliProfile := azureCLIProfile{ + subscriptionId: "abc123", + profile: cli.Profile{ + Subscriptions: []cli.Subscription{ + { + IsDefault: false, + ID: "abc123", + TenantID: "bcd234", + }, + }, + }, + } + + err := cliProfile.populateTenantID() + if err != nil { + t.Fatalf("Expected no error to be returned - but got: %+v", err) + } + + if cliProfile.subscriptionId != "abc123" { + t.Fatalf("Expected Subscription ID to be 'abc123' - got %q", cliProfile.subscriptionId) + } + + if cliProfile.tenantId != "bcd234" { + t.Fatalf("Expected Tenant ID to be 'bcd234' - got %q", cliProfile.tenantId) + } +} diff --git a/azurerm/helpers/authentication/azure_cli_profile_test.go b/azurerm/helpers/authentication/azure_cli_profile_test.go index 3b051ce366f3..70bcc7a7118d 100644 --- a/azurerm/helpers/authentication/azure_cli_profile_test.go +++ b/azurerm/helpers/authentication/azure_cli_profile_test.go @@ -76,12 +76,12 @@ func TestAzureCLIProfileFindDefaultSubscription(t *testing.T) { } for _, v := range cases { - profile := AzureCLIProfile{ - Profile: cli.Profile{ + profile := azureCLIProfile{ + profile: cli.Profile{ Subscriptions: v.Subscriptions, }, } - actualSubscriptionId, err := profile.FindDefaultSubscriptionId() + actualSubscriptionId, err := profile.findDefaultSubscriptionId() if v.ExpectError && err == nil { t.Fatalf("Expected an error for %q: didn't get one", v.Description) @@ -169,13 +169,13 @@ func TestAzureCLIProfileFindSubscription(t *testing.T) { } for _, v := range cases { - profile := AzureCLIProfile{ - Profile: cli.Profile{ + profile := azureCLIProfile{ + profile: cli.Profile{ Subscriptions: v.Subscriptions, }, } - subscription, err := profile.FindSubscription(v.SubscriptionIdToSearchFor) + subscription, err := profile.findSubscription(v.SubscriptionIdToSearchFor) if v.ExpectError && err == nil { t.Fatalf("Expected an error for %q: didn't get one", v.Description) diff --git a/azurerm/helpers/authentication/builder.go b/azurerm/helpers/authentication/builder.go new file mode 100644 index 000000000000..8abcb38bc042 --- /dev/null +++ b/azurerm/helpers/authentication/builder.go @@ -0,0 +1,76 @@ +package authentication + +import ( + "fmt" + "log" +) + +// Builder supports all of the possible Authentication values and feature toggles +// required to build a working Config for Authentication purposes. +type Builder struct { + // Core + ClientID string + SubscriptionID string + TenantID string + Environment string + + // Azure CLI Parsing / CloudShell Auth + SupportsAzureCliCloudShellParsing bool + + // Managed Service Identity Auth + SupportsManagedServiceIdentity bool + MsiEndpoint string + + // Service Principal (Client Cert) Auth + SupportsClientCertAuth bool + ClientCertPath string + ClientCertPassword string + + // Service Principal (Client Secret) Auth + SupportsClientSecretAuth bool + ClientSecret string +} + +// Build takes the configuration from the Builder and builds up a validated Config +// for authenticating with Azure +func (b Builder) Build() (*Config, error) { + config := Config{ + ClientID: b.ClientID, + SubscriptionID: b.SubscriptionID, + TenantID: b.TenantID, + Environment: b.Environment, + } + + // NOTE: the ordering here is important + // since the Azure CLI Parsing should always be the last thing checked + supportedAuthenticationMethods := []authMethod{ + servicePrincipalClientCertificateAuth{}, + servicePrincipalClientSecretAuth{}, + managedServiceIdentityAuth{}, + azureCliParsingAuth{}, + } + + for _, method := range supportedAuthenticationMethods { + name := method.name() + log.Printf("Testing if %s is applicable for Authentication..", name) + if method.isApplicable(b) { + log.Printf("Using %s for Authentication", name) + auth, err := method.build(b) + if err != nil { + return nil, err + } + + // populate authentication specific fields on the Config + // (e.g. is service principal, fields parsed from the azure cli) + err = auth.populateConfig(&config) + if err != nil { + return nil, err + } + + config.authMethod = auth + return config.validate() + } + } + + return nil, fmt.Errorf("No supported authentication methods were found!") +} diff --git a/azurerm/helpers/authentication/config.go b/azurerm/helpers/authentication/config.go index 70bbb8dec4d8..d9b606baac02 100644 --- a/azurerm/helpers/authentication/config.go +++ b/azurerm/helpers/authentication/config.go @@ -1,153 +1,32 @@ package authentication import ( - "fmt" - - "log" - + "github.com/Azure/go-autorest/autorest" "github.com/Azure/go-autorest/autorest/adal" - "github.com/Azure/go-autorest/autorest/azure/cli" ) -// TODO: separate objects for Input/Output - // Config is the configuration structure used to instantiate a // new Azure management client. type Config struct { - // TODO: feature toggles for which Authentication Providers are supported - - // Core - ClientID string - SubscriptionID string - TenantID string - Environment string - SkipCredentialsValidation bool - SkipProviderRegistration bool - - // Service Principal Auth - ClientSecret string - - // Bearer Auth - AccessToken *adal.Token - IsCloudShell bool - UseMsi bool - MsiEndpoint string -} - -// LoadTokensFromAzureCLI loads the access tokens and subscription/tenant ID's from the -// Azure CLI metadata if it's not provided -// NOTE: this'll become an internal-only method in the near future -func (c *Config) LoadTokensFromAzureCLI() error { - profilePath, err := cli.ProfilePath() - if err != nil { - return fmt.Errorf("Error loading the Profile Path from the Azure CLI: %+v", err) - } - - profile, err := cli.LoadProfile(profilePath) - if err != nil { - return fmt.Errorf("Azure CLI Authorization Profile was not found. Please ensure the Azure CLI is installed and then log-in with `az login`.") - } - - cliProfile := AzureCLIProfile{ - Profile: profile, - } - - // find the Subscription ID if it's not specified - if c.SubscriptionID == "" { - // we want to expose a more friendly error to the user, but this is useful for debug purposes - err := c.populateSubscriptionFromCLIProfile(cliProfile) - if err != nil { - log.Printf("Error Populating the Subscription from the CLI Profile: %s", err) - } - } - - // find the Tenant ID for that subscription if they're not specified - if c.TenantID == "" { - err := c.populateTenantFromCLIProfile(cliProfile) - if err != nil { - // we want to expose a more friendly error to the user, but this is useful for debug purposes - log.Printf("Error Populating the Tenant from the CLI Profile: %s", err) - } - } - - foundToken := false - if c.TenantID != "" { - // pull out the ClientID and the AccessToken from the Azure Access Token - tokensPath, err := cli.AccessTokensPath() - if err != nil { - return fmt.Errorf("Error loading the Tokens Path from the Azure CLI: %+v", err) - } - - tokens, err := cli.LoadTokens(tokensPath) - if err != nil { - return fmt.Errorf("Azure CLI Authorization Tokens were not found. Please ensure the Azure CLI is installed and then log-in with `az login`.") - } - - validToken, _ := findValidAccessTokenForTenant(tokens, c.TenantID) - if validToken != nil { - foundToken, err = c.populateFromAccessToken(validToken) - if err != nil { - return err - } - } - } - - if !foundToken { - return fmt.Errorf("No valid (unexpired) Azure CLI Auth Tokens found. Please run `az login`.") - } - - // always pull the Environment from the CLI - err = c.populateEnvironmentFromCLIProfile(cliProfile) - if err != nil { - // we want to expose a more friendly error to the user, but this is useful for debug purposes - log.Printf("Error Populating the Environment from the CLI Profile: %s", err) - } - - return nil -} - -func (c *Config) populateSubscriptionFromCLIProfile(cliProfile AzureCLIProfile) error { - subscriptionId, err := cliProfile.FindDefaultSubscriptionId() - if err != nil { - return err - } + ClientID string + SubscriptionID string + TenantID string + Environment string + AuthenticatedAsAServicePrincipal bool - c.SubscriptionID = subscriptionId - return nil + authMethod authMethod } -func (c *Config) populateTenantFromCLIProfile(cliProfile AzureCLIProfile) error { - subscription, err := cliProfile.FindSubscription(c.SubscriptionID) - if err != nil { - return err - } - - if c.TenantID == "" { - c.TenantID = subscription.TenantID - } - - return nil +// GetAuthorizationToken returns an authorization token for the authentication method defined in the Config +func (c Config) GetAuthorizationToken(oauthConfig *adal.OAuthConfig, endpoint string) (*autorest.BearerAuthorizer, error) { + return c.authMethod.getAuthorizationToken(oauthConfig, endpoint) } -func (c *Config) populateEnvironmentFromCLIProfile(cliProfile AzureCLIProfile) error { - subscription, err := cliProfile.FindSubscription(c.SubscriptionID) +func (c Config) validate() (*Config, error) { + err := c.authMethod.validate() if err != nil { - return err + return nil, err } - c.Environment = normalizeEnvironmentName(subscription.EnvironmentName) - - return nil -} - -func (c *Config) populateFromAccessToken(token *AccessToken) (bool, error) { - if token == nil { - return false, fmt.Errorf("No valid access token was found to populate from") - } - - c.ClientID = token.ClientID - c.AccessToken = token.AccessToken - c.IsCloudShell = token.IsCloudShell - - return true, nil + return &c, nil } diff --git a/azurerm/helpers/authentication/config_test.go b/azurerm/helpers/authentication/config_test.go deleted file mode 100644 index 0a76bf2d6b6f..000000000000 --- a/azurerm/helpers/authentication/config_test.go +++ /dev/null @@ -1,206 +0,0 @@ -package authentication - -import ( - "testing" - - "github.com/Azure/go-autorest/autorest/adal" - "github.com/Azure/go-autorest/autorest/azure/cli" -) - -func TestAzurePopulateSubscriptionFromCLIProfile_Missing(t *testing.T) { - config := Config{} - profiles := AzureCLIProfile{ - Profile: cli.Profile{ - Subscriptions: []cli.Subscription{}, - }, - } - - err := config.populateSubscriptionFromCLIProfile(profiles) - if err == nil { - t.Fatalf("Expected an error to be returned - but didn't get one") - } -} - -func TestAzurePopulateSubscriptionFromCLIProfile_NoDefault(t *testing.T) { - config := Config{} - profiles := AzureCLIProfile{ - Profile: cli.Profile{ - Subscriptions: []cli.Subscription{ - { - IsDefault: false, - ID: "abc123", - }, - }, - }, - } - - err := config.populateSubscriptionFromCLIProfile(profiles) - if err == nil { - t.Fatalf("Expected an error to be returned - but didn't get one") - } -} - -func TestAzurePopulateSubscriptionFromCLIProfile_Default(t *testing.T) { - subscriptionId := "abc123" - config := Config{} - profiles := AzureCLIProfile{ - Profile: cli.Profile{ - Subscriptions: []cli.Subscription{ - { - IsDefault: true, - ID: subscriptionId, - }, - }, - }, - } - - err := config.populateSubscriptionFromCLIProfile(profiles) - if err != nil { - t.Fatalf("Expected no error to be returned - but got: %+v", err) - } - - if config.SubscriptionID != subscriptionId { - t.Fatalf("Expected the Subscription ID to be %q but got %q", subscriptionId, config.SubscriptionID) - } -} - -func TestAzurePopulateTenantFromCLIProfile_Empty(t *testing.T) { - config := Config{} - profiles := AzureCLIProfile{ - Profile: cli.Profile{ - Subscriptions: []cli.Subscription{}, - }, - } - - err := config.populateEnvironmentFromCLIProfile(profiles) - if err == nil { - t.Fatalf("Expected an error to be returned - but didn't get one") - } -} - -func TestAzurePopulateTenantFromCLIProfile_MissingSubscription(t *testing.T) { - config := Config{ - SubscriptionID: "bcd234", - } - profiles := AzureCLIProfile{ - Profile: cli.Profile{ - Subscriptions: []cli.Subscription{ - { - IsDefault: false, - ID: "abc123", - }, - }, - }, - } - - err := config.populateTenantFromCLIProfile(profiles) - if err == nil { - t.Fatalf("Expected an error to be returned - but didn't get one") - } -} - -func TestAzurePopulateTenantFromCLIProfile_PopulateTenantId(t *testing.T) { - config := Config{ - SubscriptionID: "abc123", - } - profiles := AzureCLIProfile{ - Profile: cli.Profile{ - Subscriptions: []cli.Subscription{ - { - IsDefault: false, - ID: "abc123", - TenantID: "bcd234", - }, - }, - }, - } - - err := config.populateTenantFromCLIProfile(profiles) - if err != nil { - t.Fatalf("Expected no error to be returned - but got: %+v", err) - } - - if config.SubscriptionID != "abc123" { - t.Fatalf("Expected Subscription ID to be 'abc123' - got %q", config.SubscriptionID) - } - - if config.TenantID != "bcd234" { - t.Fatalf("Expected Tenant ID to be 'bcd234' - got %q", config.TenantID) - } -} - -func TestAzurePopulateTenantFromCLIProfile_Complete(t *testing.T) { - config := Config{ - SubscriptionID: "abc123", - TenantID: "bcd234", - } - profiles := AzureCLIProfile{ - Profile: cli.Profile{ - Subscriptions: []cli.Subscription{ - { - IsDefault: false, - ID: "abc123", - }, - }, - }, - } - - err := config.populateTenantFromCLIProfile(profiles) - if err != nil { - t.Fatalf("Expected no error to be returned - but got: %+v", err) - } - - if config.SubscriptionID != "abc123" { - t.Fatalf("Expected Subscription ID to be 'abc123' - got %q", config.SubscriptionID) - } - - if config.TenantID != "bcd234" { - t.Fatalf("Expected Tenant ID to be 'bcd234' - got %q", config.TenantID) - } -} - -func TestAzurePopulateFromAccessToken_Missing(t *testing.T) { - config := Config{} - - successful, err := config.populateFromAccessToken(nil) - if err == nil { - t.Fatalf("Expected an error but didn't get one") - } - - if successful { - t.Fatalf("Expected the population of a null token to be false, got true") - } -} - -func TestAzurePopulateFromAccessToken_Exists(t *testing.T) { - config := Config{} - - token := AccessToken{ - AccessToken: &adal.Token{ - AccessToken: "abc123", - }, - ClientID: "bcd234", - IsCloudShell: true, - } - - successful, err := config.populateFromAccessToken(&token) - if err != nil { - t.Fatalf("Expected no error but got: %+v", err) - } - - if !successful { - t.Fatalf("Expected the population of an existing token to be successful, it wasn't") - } - - if config.IsCloudShell != token.IsCloudShell { - t.Fatalf("Expected `IsCloudShell` to be %t, got %t", token.IsCloudShell, config.IsCloudShell) - } - - if config.ClientID != token.ClientID { - t.Fatalf("Expected `ClientID` to be %q, got %q", token.ClientID, config.ClientID) - } - - if config.AccessToken != token.AccessToken { - t.Fatalf("Expected `AccessToken` to be %+v, got %+v", token.AccessToken, config.AccessToken) - } -} diff --git a/azurerm/helpers/authentication/validation.go b/azurerm/helpers/authentication/validation.go deleted file mode 100644 index bedce68e63e7..000000000000 --- a/azurerm/helpers/authentication/validation.go +++ /dev/null @@ -1,115 +0,0 @@ -package authentication - -import ( - "fmt" - "log" - - "github.com/Azure/go-autorest/autorest/adal" - "github.com/hashicorp/go-multierror" -) - -// Validate ensures that the current set of credentials provided -// are valid for the selected authentication type (e.g. Client Secret, Azure CLI, MSI etc.) -func (c *Config) Validate() error { - if c.UseMsi { - log.Printf("[DEBUG] use_msi specified - using MSI Authentication") - if c.MsiEndpoint == "" { - msiEndpoint, err := adal.GetMSIVMEndpoint() - if err != nil { - return fmt.Errorf("Could not retrieve MSI endpoint from VM settings."+ - "Ensure the VM has MSI enabled, or try setting msi_endpoint. Error: %s", err) - } - c.MsiEndpoint = msiEndpoint - } - log.Printf("[DEBUG] Using MSI endpoint %s", c.MsiEndpoint) - if err := c.validateMsi(); err != nil { - return err - } - - return nil - } - - if c.ClientSecret != "" { - log.Printf("[DEBUG] Client Secret specified - using Service Principal for Authentication") - if err := c.validateServicePrincipal(); err != nil { - return err - } - - return nil - } - - // Azure CLI / CloudShell - log.Printf("[DEBUG] No Client Secret specified - loading credentials from Azure CLI") - if err := c.LoadTokensFromAzureCLI(); err != nil { - return err - } - - if err := c.validateAzureCliBearerAuth(); err != nil { - return fmt.Errorf("Please specify either a Service Principal, or log in with the Azure CLI (using `az login`)") - } - - return nil -} - -func (c *Config) validateAzureCliBearerAuth() error { - var err *multierror.Error - - if c.AccessToken == nil { - err = multierror.Append(err, fmt.Errorf("Access Token was not found in your Azure CLI Credentials.\n\nPlease login to the Azure CLI again via `az login`")) - } - - if c.ClientID == "" { - err = multierror.Append(err, fmt.Errorf("Client ID was not found in your Azure CLI Credentials.\n\nPlease login to the Azure CLI again via `az login`")) - } - - if c.SubscriptionID == "" { - err = multierror.Append(err, fmt.Errorf("Subscription ID was not found in your Azure CLI Credentials.\n\nPlease login to the Azure CLI again via `az login`")) - } - - if c.TenantID == "" { - err = multierror.Append(err, fmt.Errorf("Tenant ID was not found in your Azure CLI Credentials.\n\nPlease login to the Azure CLI again via `az login`")) - } - - return err.ErrorOrNil() -} - -func (c *Config) validateServicePrincipal() error { - var err *multierror.Error - - if c.SubscriptionID == "" { - err = multierror.Append(err, fmt.Errorf("Subscription ID must be configured for the AzureRM provider")) - } - if c.ClientID == "" { - err = multierror.Append(err, fmt.Errorf("Client ID must be configured for the AzureRM provider")) - } - if c.ClientSecret == "" { - err = multierror.Append(err, fmt.Errorf("Client Secret must be configured for the AzureRM provider")) - } - if c.TenantID == "" { - err = multierror.Append(err, fmt.Errorf("Tenant ID must be configured for the AzureRM provider")) - } - if c.Environment == "" { - err = multierror.Append(err, fmt.Errorf("Environment must be configured for the AzureRM provider")) - } - - return err.ErrorOrNil() -} - -func (c *Config) validateMsi() error { - var err *multierror.Error - - if c.SubscriptionID == "" { - err = multierror.Append(err, fmt.Errorf("Subscription ID must be configured for the AzureRM provider")) - } - if c.TenantID == "" { - err = multierror.Append(err, fmt.Errorf("Tenant ID must be configured for the AzureRM provider")) - } - if c.Environment == "" { - err = multierror.Append(err, fmt.Errorf("Environment must be configured for the AzureRM provider")) - } - if c.MsiEndpoint == "" { - err = multierror.Append(err, fmt.Errorf("MSI endpoint must be configured for the AzureRM provider")) - } - - return err.ErrorOrNil() -} diff --git a/azurerm/helpers/authentication/validation_test.go b/azurerm/helpers/authentication/validation_test.go deleted file mode 100644 index 62a7888cb53a..000000000000 --- a/azurerm/helpers/authentication/validation_test.go +++ /dev/null @@ -1,238 +0,0 @@ -package authentication - -import ( - "testing" - - "github.com/Azure/go-autorest/autorest/adal" -) - -func TestAzureValidateBearerAuth(t *testing.T) { - cases := []struct { - Description string - Config Config - ExpectError bool - }{ - { - Description: "Empty Configuration", - Config: Config{}, - ExpectError: true, - }, - { - Description: "Missing Access Token", - Config: Config{ - ClientID: "62e73395-5017-43b6-8ebf-d6c30a514cf1", - SubscriptionID: "8e8b5e02-5c13-4822-b7dc-4232afb7e8c2", - TenantID: "9834f8d0-24b3-41b7-8b8d-c611c461a129", - }, - ExpectError: true, - }, - { - Description: "Missing Client ID", - Config: Config{ - AccessToken: &adal.Token{}, - SubscriptionID: "8e8b5e02-5c13-4822-b7dc-4232afb7e8c2", - TenantID: "9834f8d0-24b3-41b7-8b8d-c611c461a129", - }, - ExpectError: true, - }, - { - Description: "Missing Subscription ID", - Config: Config{ - AccessToken: &adal.Token{}, - ClientID: "62e73395-5017-43b6-8ebf-d6c30a514cf1", - TenantID: "9834f8d0-24b3-41b7-8b8d-c611c461a129", - }, - ExpectError: true, - }, - { - Description: "Missing Tenant ID", - Config: Config{ - AccessToken: &adal.Token{}, - ClientID: "62e73395-5017-43b6-8ebf-d6c30a514cf1", - SubscriptionID: "8e8b5e02-5c13-4822-b7dc-4232afb7e8c2", - }, - ExpectError: true, - }, - { - Description: "Valid Configuration", - Config: Config{ - AccessToken: &adal.Token{}, - ClientID: "62e73395-5017-43b6-8ebf-d6c30a514cf1", - SubscriptionID: "8e8b5e02-5c13-4822-b7dc-4232afb7e8c2", - TenantID: "9834f8d0-24b3-41b7-8b8d-c611c461a129", - }, - ExpectError: false, - }, - } - - for _, v := range cases { - err := v.Config.validateAzureCliBearerAuth() - - if v.ExpectError && err == nil { - t.Fatalf("Expected an error for %q: didn't get one", v.Description) - } - - if !v.ExpectError && err != nil { - t.Fatalf("Expected there to be no error for %q - but got: %v", v.Description, err) - } - } -} - -func TestAzureValidateServicePrincipal(t *testing.T) { - cases := []struct { - Description string - Config Config - ExpectError bool - }{ - { - Description: "Empty Configuration", - Config: Config{}, - ExpectError: true, - }, - { - Description: "Missing Client ID", - Config: Config{ - SubscriptionID: "8e8b5e02-5c13-4822-b7dc-4232afb7e8c2", - ClientSecret: "Does Hammer Time have Daylight Savings Time?", - TenantID: "9834f8d0-24b3-41b7-8b8d-c611c461a129", - Environment: "public", - }, - ExpectError: true, - }, - { - Description: "Missing Subscription ID", - Config: Config{ - ClientID: "62e73395-5017-43b6-8ebf-d6c30a514cf1", - ClientSecret: "Does Hammer Time have Daylight Savings Time?", - TenantID: "9834f8d0-24b3-41b7-8b8d-c611c461a129", - Environment: "public", - }, - ExpectError: true, - }, - { - Description: "Missing Client Secret", - Config: Config{ - ClientID: "62e73395-5017-43b6-8ebf-d6c30a514cf1", - SubscriptionID: "8e8b5e02-5c13-4822-b7dc-4232afb7e8c2", - TenantID: "9834f8d0-24b3-41b7-8b8d-c611c461a129", - Environment: "public", - }, - ExpectError: true, - }, - { - Description: "Missing Tenant ID", - Config: Config{ - ClientID: "62e73395-5017-43b6-8ebf-d6c30a514cf1", - SubscriptionID: "8e8b5e02-5c13-4822-b7dc-4232afb7e8c2", - ClientSecret: "Does Hammer Time have Daylight Savings Time?", - Environment: "public", - }, - ExpectError: true, - }, - { - Description: "Missing Environment", - Config: Config{ - ClientID: "62e73395-5017-43b6-8ebf-d6c30a514cf1", - SubscriptionID: "8e8b5e02-5c13-4822-b7dc-4232afb7e8c2", - ClientSecret: "Does Hammer Time have Daylight Savings Time?", - TenantID: "9834f8d0-24b3-41b7-8b8d-c611c461a129", - }, - ExpectError: true, - }, - { - Description: "Valid Configuration", - Config: Config{ - ClientID: "62e73395-5017-43b6-8ebf-d6c30a514cf1", - SubscriptionID: "8e8b5e02-5c13-4822-b7dc-4232afb7e8c2", - ClientSecret: "Does Hammer Time have Daylight Savings Time?", - TenantID: "9834f8d0-24b3-41b7-8b8d-c611c461a129", - Environment: "public", - }, - ExpectError: false, - }, - } - - for _, v := range cases { - err := v.Config.validateServicePrincipal() - - if v.ExpectError && err == nil { - t.Fatalf("Expected an error for %q: didn't get one", v.Description) - } - - if !v.ExpectError && err != nil { - t.Fatalf("Expected there to be no error for %q - but got: %v", v.Description, err) - } - } -} - -func TestAzureValidateMsi(t *testing.T) { - cases := []struct { - Description string - Config Config - ExpectError bool - }{ - { - Description: "Empty Configuration", - Config: Config{}, - ExpectError: true, - }, - { - Description: "Missing Subscription ID", - Config: Config{ - MsiEndpoint: "http://localhost:50342/oauth2/token", - TenantID: "9834f8d0-24b3-41b7-8b8d-c611c461a129", - Environment: "public", - }, - ExpectError: true, - }, - { - Description: "Missing Tenant ID", - Config: Config{ - MsiEndpoint: "http://localhost:50342/oauth2/token", - SubscriptionID: "8e8b5e02-5c13-4822-b7dc-4232afb7e8c2", - Environment: "public", - }, - ExpectError: true, - }, - { - Description: "Missing Environment", - Config: Config{ - MsiEndpoint: "http://localhost:50342/oauth2/token", - SubscriptionID: "8e8b5e02-5c13-4822-b7dc-4232afb7e8c2", - TenantID: "9834f8d0-24b3-41b7-8b8d-c611c461a129", - }, - ExpectError: true, - }, - { - Description: "Missing MSI Endpoint", - Config: Config{ - SubscriptionID: "8e8b5e02-5c13-4822-b7dc-4232afb7e8c2", - TenantID: "9834f8d0-24b3-41b7-8b8d-c611c461a129", - Environment: "public", - }, - ExpectError: true, - }, - { - Description: "Valid Configuration", - Config: Config{ - MsiEndpoint: "http://localhost:50342/oauth2/token", - SubscriptionID: "8e8b5e02-5c13-4822-b7dc-4232afb7e8c2", - TenantID: "9834f8d0-24b3-41b7-8b8d-c611c461a129", - Environment: "public", - }, - ExpectError: false, - }, - } - - for _, v := range cases { - err := v.Config.validateMsi() - - if v.ExpectError && err == nil { - t.Fatalf("Expected an error for %q: didn't get one", v.Description) - } - - if !v.ExpectError && err != nil { - t.Fatalf("Expected there to be no error for %q - but got: %v", v.Description, err) - } - } -} diff --git a/azurerm/helpers/resourceproviders/registration.go b/azurerm/helpers/resourceproviders/registration.go index 04a81f6c1710..ff9802b5cb64 100644 --- a/azurerm/helpers/resourceproviders/registration.go +++ b/azurerm/helpers/resourceproviders/registration.go @@ -10,6 +10,7 @@ import ( "github.com/Azure/azure-sdk-for-go/services/resources/mgmt/2017-05-10/resources" ) +// DetermineResourceProvidersRequiringRegistration determines which Resource Providers require registration to be able to be used func DetermineResourceProvidersRequiringRegistration(availableResourceProviders []resources.Provider, requiredResourceProviders map[string]struct{}) map[string]struct{} { providers := requiredResourceProviders @@ -28,6 +29,7 @@ func DetermineResourceProvidersRequiringRegistration(availableResourceProviders return providers } +// RegisterForSubscription registers the specified Resource Providers in the current Subscription func RegisterForSubscription(ctx context.Context, client resources.ProvidersClient, providersToRegister map[string]struct{}) error { var err error var wg sync.WaitGroup diff --git a/azurerm/provider.go b/azurerm/provider.go index f73892f8a199..5c4592ad8f32 100644 --- a/azurerm/provider.go +++ b/azurerm/provider.go @@ -311,24 +311,27 @@ func Provider() terraform.ResourceProvider { func providerConfigure(p *schema.Provider) schema.ConfigureFunc { return func(d *schema.ResourceData) (interface{}, error) { - config := &authentication.Config{ - SubscriptionID: d.Get("subscription_id").(string), - ClientID: d.Get("client_id").(string), - ClientSecret: d.Get("client_secret").(string), - TenantID: d.Get("tenant_id").(string), - Environment: d.Get("environment").(string), - UseMsi: d.Get("use_msi").(bool), - MsiEndpoint: d.Get("msi_endpoint").(string), - SkipCredentialsValidation: d.Get("skip_credentials_validation").(bool), - SkipProviderRegistration: d.Get("skip_provider_registration").(bool), + builder := &authentication.Builder{ + SubscriptionID: d.Get("subscription_id").(string), + ClientID: d.Get("client_id").(string), + ClientSecret: d.Get("client_secret").(string), + TenantID: d.Get("tenant_id").(string), + Environment: d.Get("environment").(string), + MsiEndpoint: d.Get("msi_endpoint").(string), + + // Feature Toggles + SupportsClientSecretAuth: true, + SupportsManagedServiceIdentity: d.Get("use_msi").(bool), + SupportsAzureCliCloudShellParsing: true, } - err := config.Validate() + config, err := builder.Build() if err != nil { - return nil, fmt.Errorf("Error validating provider: %s", err) + return nil, fmt.Errorf("Error building AzureRM Client: %s", err) } - client, err := getArmClient(config) + skipProviderRegistration := d.Get("skip_provider_registration").(bool) + client, err := getArmClient(config, skipProviderRegistration) if err != nil { return nil, err } @@ -341,7 +344,8 @@ func providerConfigure(p *schema.Provider) schema.ConfigureFunc { return nil } - if !config.SkipCredentialsValidation { + skipCredentialsValidation := d.Get("skip_credentials_validation").(bool) + if !skipCredentialsValidation { // List all the available providers and their registration state to avoid unnecessary // requests. This also lets us check if the provider credentials are correct. ctx := client.StopContext @@ -352,7 +356,7 @@ func providerConfigure(p *schema.Provider) schema.ConfigureFunc { "error: %s", err) } - if !config.SkipProviderRegistration { + if !skipProviderRegistration { availableResourceProviders := providerList.Values() requiredResourceProviders := requiredResourceProviders() diff --git a/azurerm/provider_test.go b/azurerm/provider_test.go index 4255c4b7baa7..c41a6013963b 100644 --- a/azurerm/provider_test.go +++ b/azurerm/provider_test.go @@ -80,14 +80,21 @@ func testGetAzureConfig(t *testing.T) *authentication.Config { environment := testArmEnvironmentName() - // we deliberately don't use the main config - since we care about - config := authentication.Config{ - SubscriptionID: os.Getenv("ARM_SUBSCRIPTION_ID"), - ClientID: os.Getenv("ARM_CLIENT_ID"), - TenantID: os.Getenv("ARM_TENANT_ID"), - ClientSecret: os.Getenv("ARM_CLIENT_SECRET"), - Environment: environment, - SkipProviderRegistration: false, + builder := authentication.Builder{ + SubscriptionID: os.Getenv("ARM_SUBSCRIPTION_ID"), + ClientID: os.Getenv("ARM_CLIENT_ID"), + TenantID: os.Getenv("ARM_TENANT_ID"), + ClientSecret: os.Getenv("ARM_CLIENT_SECRET"), + Environment: environment, + + // we intentionally only support Client Secret auth for tests (since those variables are used all over) + SupportsClientSecretAuth: true, } - return &config + config, err := builder.Build() + if err != nil { + t.Fatalf("Error building ARM Client: %+v", err) + return nil + } + + return config } diff --git a/azurerm/required_resource_providers_test.go b/azurerm/required_resource_providers_test.go index 7830fc56d97c..95c40857084c 100644 --- a/azurerm/required_resource_providers_test.go +++ b/azurerm/required_resource_providers_test.go @@ -13,7 +13,9 @@ func TestAccAzureRMEnsureRequiredResourceProvidersAreRegistered(t *testing.T) { return } - armClient, err := getArmClient(config) + // this test intentionally checks all the RP's are registered - so this is intentional + skipProviderRegistration := true + armClient, err := getArmClient(config, skipProviderRegistration) if err != nil { t.Fatalf("Error building ARM Client: %+v", err) } diff --git a/azurerm/resource_arm_container_registry_migrate_test.go b/azurerm/resource_arm_container_registry_migrate_test.go index 2df208979fa1..2b24078ef750 100644 --- a/azurerm/resource_arm_container_registry_migrate_test.go +++ b/azurerm/resource_arm_container_registry_migrate_test.go @@ -18,7 +18,9 @@ func TestAccAzureRMContainerRegistryMigrateState(t *testing.T) { t.SkipNow() return } - client, err := getArmClient(config) + + skipProviderRegistration := false + client, err := getArmClient(config, skipProviderRegistration) if err != nil { t.Fatal(fmt.Errorf("Error building ARM Client: %+v", err)) return diff --git a/azurerm/resource_arm_data_lake_store_file_migration_test.go b/azurerm/resource_arm_data_lake_store_file_migration_test.go index 7fcedf0811bc..8dea874e8b7a 100644 --- a/azurerm/resource_arm_data_lake_store_file_migration_test.go +++ b/azurerm/resource_arm_data_lake_store_file_migration_test.go @@ -16,7 +16,8 @@ func TestAccAzureRMDataLakeStoreFileMigrateState(t *testing.T) { return } - client, err := getArmClient(config) + skipProviderRegistration := false + client, err := getArmClient(config, skipProviderRegistration) if err != nil { t.Fatal(fmt.Errorf("Error building ARM Client: %+v", err)) return diff --git a/azurerm/resource_arm_storage_blob_migration_test.go b/azurerm/resource_arm_storage_blob_migration_test.go index 30b10751bd4d..de7974607419 100644 --- a/azurerm/resource_arm_storage_blob_migration_test.go +++ b/azurerm/resource_arm_storage_blob_migration_test.go @@ -16,7 +16,8 @@ func TestAccAzureRMStorageBlobMigrateState(t *testing.T) { return } - client, err := getArmClient(config) + skipProviderRegistration := false + client, err := getArmClient(config, skipProviderRegistration) if err != nil { t.Fatal(fmt.Errorf("Error building ARM Client: %+v", err)) return diff --git a/azurerm/resource_arm_storage_container_migration_test.go b/azurerm/resource_arm_storage_container_migration_test.go index ce1eb4658b23..c18c06201c4f 100644 --- a/azurerm/resource_arm_storage_container_migration_test.go +++ b/azurerm/resource_arm_storage_container_migration_test.go @@ -16,7 +16,8 @@ func TestAccAzureRMStorageContainerMigrateState(t *testing.T) { return } - client, err := getArmClient(config) + skipProviderRegistration := false + client, err := getArmClient(config, skipProviderRegistration) if err != nil { t.Fatal(fmt.Errorf("Error building ARM Client: %+v", err)) return diff --git a/azurerm/resource_arm_storage_queue_migration_test.go b/azurerm/resource_arm_storage_queue_migration_test.go index 5399acef9d5f..8b69c93b4b22 100644 --- a/azurerm/resource_arm_storage_queue_migration_test.go +++ b/azurerm/resource_arm_storage_queue_migration_test.go @@ -16,7 +16,8 @@ func TestAccAzureRMStorageQueueMigrateState(t *testing.T) { return } - client, err := getArmClient(config) + skipProviderRegistration := false + client, err := getArmClient(config, skipProviderRegistration) if err != nil { t.Fatal(fmt.Errorf("Error building ARM Client: %+v", err)) return diff --git a/azurerm/resource_arm_storage_table_migration_test.go b/azurerm/resource_arm_storage_table_migration_test.go index 198468243c39..5eb4725de74c 100644 --- a/azurerm/resource_arm_storage_table_migration_test.go +++ b/azurerm/resource_arm_storage_table_migration_test.go @@ -16,7 +16,8 @@ func TestAccAzureRMStorageTableMigrateState(t *testing.T) { return } - client, err := getArmClient(config) + skipProviderRegistration := false + client, err := getArmClient(config, skipProviderRegistration) if err != nil { t.Fatal(fmt.Errorf("Error building ARM Client: %+v", err)) return diff --git a/vendor/golang.org/x/crypto/pkcs12/bmp-string.go b/vendor/golang.org/x/crypto/pkcs12/bmp-string.go new file mode 100644 index 000000000000..233b8b62cc27 --- /dev/null +++ b/vendor/golang.org/x/crypto/pkcs12/bmp-string.go @@ -0,0 +1,50 @@ +// Copyright 2015 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package pkcs12 + +import ( + "errors" + "unicode/utf16" +) + +// bmpString returns s encoded in UCS-2 with a zero terminator. +func bmpString(s string) ([]byte, error) { + // References: + // https://tools.ietf.org/html/rfc7292#appendix-B.1 + // https://en.wikipedia.org/wiki/Plane_(Unicode)#Basic_Multilingual_Plane + // - non-BMP characters are encoded in UTF 16 by using a surrogate pair of 16-bit codes + // EncodeRune returns 0xfffd if the rune does not need special encoding + // - the above RFC provides the info that BMPStrings are NULL terminated. + + ret := make([]byte, 0, 2*len(s)+2) + + for _, r := range s { + if t, _ := utf16.EncodeRune(r); t != 0xfffd { + return nil, errors.New("pkcs12: string contains characters that cannot be encoded in UCS-2") + } + ret = append(ret, byte(r/256), byte(r%256)) + } + + return append(ret, 0, 0), nil +} + +func decodeBMPString(bmpString []byte) (string, error) { + if len(bmpString)%2 != 0 { + return "", errors.New("pkcs12: odd-length BMP string") + } + + // strip terminator if present + if l := len(bmpString); l >= 2 && bmpString[l-1] == 0 && bmpString[l-2] == 0 { + bmpString = bmpString[:l-2] + } + + s := make([]uint16, 0, len(bmpString)/2) + for len(bmpString) > 0 { + s = append(s, uint16(bmpString[0])<<8+uint16(bmpString[1])) + bmpString = bmpString[2:] + } + + return string(utf16.Decode(s)), nil +} diff --git a/vendor/golang.org/x/crypto/pkcs12/crypto.go b/vendor/golang.org/x/crypto/pkcs12/crypto.go new file mode 100644 index 000000000000..484ca51b7154 --- /dev/null +++ b/vendor/golang.org/x/crypto/pkcs12/crypto.go @@ -0,0 +1,131 @@ +// Copyright 2015 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package pkcs12 + +import ( + "bytes" + "crypto/cipher" + "crypto/des" + "crypto/x509/pkix" + "encoding/asn1" + "errors" + + "golang.org/x/crypto/pkcs12/internal/rc2" +) + +var ( + oidPBEWithSHAAnd3KeyTripleDESCBC = asn1.ObjectIdentifier([]int{1, 2, 840, 113549, 1, 12, 1, 3}) + oidPBEWithSHAAnd40BitRC2CBC = asn1.ObjectIdentifier([]int{1, 2, 840, 113549, 1, 12, 1, 6}) +) + +// pbeCipher is an abstraction of a PKCS#12 cipher. +type pbeCipher interface { + // create returns a cipher.Block given a key. + create(key []byte) (cipher.Block, error) + // deriveKey returns a key derived from the given password and salt. + deriveKey(salt, password []byte, iterations int) []byte + // deriveKey returns an IV derived from the given password and salt. + deriveIV(salt, password []byte, iterations int) []byte +} + +type shaWithTripleDESCBC struct{} + +func (shaWithTripleDESCBC) create(key []byte) (cipher.Block, error) { + return des.NewTripleDESCipher(key) +} + +func (shaWithTripleDESCBC) deriveKey(salt, password []byte, iterations int) []byte { + return pbkdf(sha1Sum, 20, 64, salt, password, iterations, 1, 24) +} + +func (shaWithTripleDESCBC) deriveIV(salt, password []byte, iterations int) []byte { + return pbkdf(sha1Sum, 20, 64, salt, password, iterations, 2, 8) +} + +type shaWith40BitRC2CBC struct{} + +func (shaWith40BitRC2CBC) create(key []byte) (cipher.Block, error) { + return rc2.New(key, len(key)*8) +} + +func (shaWith40BitRC2CBC) deriveKey(salt, password []byte, iterations int) []byte { + return pbkdf(sha1Sum, 20, 64, salt, password, iterations, 1, 5) +} + +func (shaWith40BitRC2CBC) deriveIV(salt, password []byte, iterations int) []byte { + return pbkdf(sha1Sum, 20, 64, salt, password, iterations, 2, 8) +} + +type pbeParams struct { + Salt []byte + Iterations int +} + +func pbDecrypterFor(algorithm pkix.AlgorithmIdentifier, password []byte) (cipher.BlockMode, int, error) { + var cipherType pbeCipher + + switch { + case algorithm.Algorithm.Equal(oidPBEWithSHAAnd3KeyTripleDESCBC): + cipherType = shaWithTripleDESCBC{} + case algorithm.Algorithm.Equal(oidPBEWithSHAAnd40BitRC2CBC): + cipherType = shaWith40BitRC2CBC{} + default: + return nil, 0, NotImplementedError("algorithm " + algorithm.Algorithm.String() + " is not supported") + } + + var params pbeParams + if err := unmarshal(algorithm.Parameters.FullBytes, ¶ms); err != nil { + return nil, 0, err + } + + key := cipherType.deriveKey(params.Salt, password, params.Iterations) + iv := cipherType.deriveIV(params.Salt, password, params.Iterations) + + block, err := cipherType.create(key) + if err != nil { + return nil, 0, err + } + + return cipher.NewCBCDecrypter(block, iv), block.BlockSize(), nil +} + +func pbDecrypt(info decryptable, password []byte) (decrypted []byte, err error) { + cbc, blockSize, err := pbDecrypterFor(info.Algorithm(), password) + if err != nil { + return nil, err + } + + encrypted := info.Data() + if len(encrypted) == 0 { + return nil, errors.New("pkcs12: empty encrypted data") + } + if len(encrypted)%blockSize != 0 { + return nil, errors.New("pkcs12: input is not a multiple of the block size") + } + decrypted = make([]byte, len(encrypted)) + cbc.CryptBlocks(decrypted, encrypted) + + psLen := int(decrypted[len(decrypted)-1]) + if psLen == 0 || psLen > blockSize { + return nil, ErrDecryption + } + + if len(decrypted) < psLen { + return nil, ErrDecryption + } + ps := decrypted[len(decrypted)-psLen:] + decrypted = decrypted[:len(decrypted)-psLen] + if bytes.Compare(ps, bytes.Repeat([]byte{byte(psLen)}, psLen)) != 0 { + return nil, ErrDecryption + } + + return +} + +// decryptable abstracts an object that contains ciphertext. +type decryptable interface { + Algorithm() pkix.AlgorithmIdentifier + Data() []byte +} diff --git a/vendor/golang.org/x/crypto/pkcs12/errors.go b/vendor/golang.org/x/crypto/pkcs12/errors.go new file mode 100644 index 000000000000..7377ce6fb2b8 --- /dev/null +++ b/vendor/golang.org/x/crypto/pkcs12/errors.go @@ -0,0 +1,23 @@ +// Copyright 2015 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package pkcs12 + +import "errors" + +var ( + // ErrDecryption represents a failure to decrypt the input. + ErrDecryption = errors.New("pkcs12: decryption error, incorrect padding") + + // ErrIncorrectPassword is returned when an incorrect password is detected. + // Usually, P12/PFX data is signed to be able to verify the password. + ErrIncorrectPassword = errors.New("pkcs12: decryption password incorrect") +) + +// NotImplementedError indicates that the input is not currently supported. +type NotImplementedError string + +func (e NotImplementedError) Error() string { + return "pkcs12: " + string(e) +} diff --git a/vendor/golang.org/x/crypto/pkcs12/internal/rc2/rc2.go b/vendor/golang.org/x/crypto/pkcs12/internal/rc2/rc2.go new file mode 100644 index 000000000000..7499e3fb69d2 --- /dev/null +++ b/vendor/golang.org/x/crypto/pkcs12/internal/rc2/rc2.go @@ -0,0 +1,271 @@ +// Copyright 2015 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package rc2 implements the RC2 cipher +/* +https://www.ietf.org/rfc/rfc2268.txt +http://people.csail.mit.edu/rivest/pubs/KRRR98.pdf + +This code is licensed under the MIT license. +*/ +package rc2 + +import ( + "crypto/cipher" + "encoding/binary" +) + +// The rc2 block size in bytes +const BlockSize = 8 + +type rc2Cipher struct { + k [64]uint16 +} + +// New returns a new rc2 cipher with the given key and effective key length t1 +func New(key []byte, t1 int) (cipher.Block, error) { + // TODO(dgryski): error checking for key length + return &rc2Cipher{ + k: expandKey(key, t1), + }, nil +} + +func (*rc2Cipher) BlockSize() int { return BlockSize } + +var piTable = [256]byte{ + 0xd9, 0x78, 0xf9, 0xc4, 0x19, 0xdd, 0xb5, 0xed, 0x28, 0xe9, 0xfd, 0x79, 0x4a, 0xa0, 0xd8, 0x9d, + 0xc6, 0x7e, 0x37, 0x83, 0x2b, 0x76, 0x53, 0x8e, 0x62, 0x4c, 0x64, 0x88, 0x44, 0x8b, 0xfb, 0xa2, + 0x17, 0x9a, 0x59, 0xf5, 0x87, 0xb3, 0x4f, 0x13, 0x61, 0x45, 0x6d, 0x8d, 0x09, 0x81, 0x7d, 0x32, + 0xbd, 0x8f, 0x40, 0xeb, 0x86, 0xb7, 0x7b, 0x0b, 0xf0, 0x95, 0x21, 0x22, 0x5c, 0x6b, 0x4e, 0x82, + 0x54, 0xd6, 0x65, 0x93, 0xce, 0x60, 0xb2, 0x1c, 0x73, 0x56, 0xc0, 0x14, 0xa7, 0x8c, 0xf1, 0xdc, + 0x12, 0x75, 0xca, 0x1f, 0x3b, 0xbe, 0xe4, 0xd1, 0x42, 0x3d, 0xd4, 0x30, 0xa3, 0x3c, 0xb6, 0x26, + 0x6f, 0xbf, 0x0e, 0xda, 0x46, 0x69, 0x07, 0x57, 0x27, 0xf2, 0x1d, 0x9b, 0xbc, 0x94, 0x43, 0x03, + 0xf8, 0x11, 0xc7, 0xf6, 0x90, 0xef, 0x3e, 0xe7, 0x06, 0xc3, 0xd5, 0x2f, 0xc8, 0x66, 0x1e, 0xd7, + 0x08, 0xe8, 0xea, 0xde, 0x80, 0x52, 0xee, 0xf7, 0x84, 0xaa, 0x72, 0xac, 0x35, 0x4d, 0x6a, 0x2a, + 0x96, 0x1a, 0xd2, 0x71, 0x5a, 0x15, 0x49, 0x74, 0x4b, 0x9f, 0xd0, 0x5e, 0x04, 0x18, 0xa4, 0xec, + 0xc2, 0xe0, 0x41, 0x6e, 0x0f, 0x51, 0xcb, 0xcc, 0x24, 0x91, 0xaf, 0x50, 0xa1, 0xf4, 0x70, 0x39, + 0x99, 0x7c, 0x3a, 0x85, 0x23, 0xb8, 0xb4, 0x7a, 0xfc, 0x02, 0x36, 0x5b, 0x25, 0x55, 0x97, 0x31, + 0x2d, 0x5d, 0xfa, 0x98, 0xe3, 0x8a, 0x92, 0xae, 0x05, 0xdf, 0x29, 0x10, 0x67, 0x6c, 0xba, 0xc9, + 0xd3, 0x00, 0xe6, 0xcf, 0xe1, 0x9e, 0xa8, 0x2c, 0x63, 0x16, 0x01, 0x3f, 0x58, 0xe2, 0x89, 0xa9, + 0x0d, 0x38, 0x34, 0x1b, 0xab, 0x33, 0xff, 0xb0, 0xbb, 0x48, 0x0c, 0x5f, 0xb9, 0xb1, 0xcd, 0x2e, + 0xc5, 0xf3, 0xdb, 0x47, 0xe5, 0xa5, 0x9c, 0x77, 0x0a, 0xa6, 0x20, 0x68, 0xfe, 0x7f, 0xc1, 0xad, +} + +func expandKey(key []byte, t1 int) [64]uint16 { + + l := make([]byte, 128) + copy(l, key) + + var t = len(key) + var t8 = (t1 + 7) / 8 + var tm = byte(255 % uint(1<<(8+uint(t1)-8*uint(t8)))) + + for i := len(key); i < 128; i++ { + l[i] = piTable[l[i-1]+l[uint8(i-t)]] + } + + l[128-t8] = piTable[l[128-t8]&tm] + + for i := 127 - t8; i >= 0; i-- { + l[i] = piTable[l[i+1]^l[i+t8]] + } + + var k [64]uint16 + + for i := range k { + k[i] = uint16(l[2*i]) + uint16(l[2*i+1])*256 + } + + return k +} + +func rotl16(x uint16, b uint) uint16 { + return (x >> (16 - b)) | (x << b) +} + +func (c *rc2Cipher) Encrypt(dst, src []byte) { + + r0 := binary.LittleEndian.Uint16(src[0:]) + r1 := binary.LittleEndian.Uint16(src[2:]) + r2 := binary.LittleEndian.Uint16(src[4:]) + r3 := binary.LittleEndian.Uint16(src[6:]) + + var j int + + for j <= 16 { + // mix r0 + r0 = r0 + c.k[j] + (r3 & r2) + ((^r3) & r1) + r0 = rotl16(r0, 1) + j++ + + // mix r1 + r1 = r1 + c.k[j] + (r0 & r3) + ((^r0) & r2) + r1 = rotl16(r1, 2) + j++ + + // mix r2 + r2 = r2 + c.k[j] + (r1 & r0) + ((^r1) & r3) + r2 = rotl16(r2, 3) + j++ + + // mix r3 + r3 = r3 + c.k[j] + (r2 & r1) + ((^r2) & r0) + r3 = rotl16(r3, 5) + j++ + + } + + r0 = r0 + c.k[r3&63] + r1 = r1 + c.k[r0&63] + r2 = r2 + c.k[r1&63] + r3 = r3 + c.k[r2&63] + + for j <= 40 { + // mix r0 + r0 = r0 + c.k[j] + (r3 & r2) + ((^r3) & r1) + r0 = rotl16(r0, 1) + j++ + + // mix r1 + r1 = r1 + c.k[j] + (r0 & r3) + ((^r0) & r2) + r1 = rotl16(r1, 2) + j++ + + // mix r2 + r2 = r2 + c.k[j] + (r1 & r0) + ((^r1) & r3) + r2 = rotl16(r2, 3) + j++ + + // mix r3 + r3 = r3 + c.k[j] + (r2 & r1) + ((^r2) & r0) + r3 = rotl16(r3, 5) + j++ + + } + + r0 = r0 + c.k[r3&63] + r1 = r1 + c.k[r0&63] + r2 = r2 + c.k[r1&63] + r3 = r3 + c.k[r2&63] + + for j <= 60 { + // mix r0 + r0 = r0 + c.k[j] + (r3 & r2) + ((^r3) & r1) + r0 = rotl16(r0, 1) + j++ + + // mix r1 + r1 = r1 + c.k[j] + (r0 & r3) + ((^r0) & r2) + r1 = rotl16(r1, 2) + j++ + + // mix r2 + r2 = r2 + c.k[j] + (r1 & r0) + ((^r1) & r3) + r2 = rotl16(r2, 3) + j++ + + // mix r3 + r3 = r3 + c.k[j] + (r2 & r1) + ((^r2) & r0) + r3 = rotl16(r3, 5) + j++ + } + + binary.LittleEndian.PutUint16(dst[0:], r0) + binary.LittleEndian.PutUint16(dst[2:], r1) + binary.LittleEndian.PutUint16(dst[4:], r2) + binary.LittleEndian.PutUint16(dst[6:], r3) +} + +func (c *rc2Cipher) Decrypt(dst, src []byte) { + + r0 := binary.LittleEndian.Uint16(src[0:]) + r1 := binary.LittleEndian.Uint16(src[2:]) + r2 := binary.LittleEndian.Uint16(src[4:]) + r3 := binary.LittleEndian.Uint16(src[6:]) + + j := 63 + + for j >= 44 { + // unmix r3 + r3 = rotl16(r3, 16-5) + r3 = r3 - c.k[j] - (r2 & r1) - ((^r2) & r0) + j-- + + // unmix r2 + r2 = rotl16(r2, 16-3) + r2 = r2 - c.k[j] - (r1 & r0) - ((^r1) & r3) + j-- + + // unmix r1 + r1 = rotl16(r1, 16-2) + r1 = r1 - c.k[j] - (r0 & r3) - ((^r0) & r2) + j-- + + // unmix r0 + r0 = rotl16(r0, 16-1) + r0 = r0 - c.k[j] - (r3 & r2) - ((^r3) & r1) + j-- + } + + r3 = r3 - c.k[r2&63] + r2 = r2 - c.k[r1&63] + r1 = r1 - c.k[r0&63] + r0 = r0 - c.k[r3&63] + + for j >= 20 { + // unmix r3 + r3 = rotl16(r3, 16-5) + r3 = r3 - c.k[j] - (r2 & r1) - ((^r2) & r0) + j-- + + // unmix r2 + r2 = rotl16(r2, 16-3) + r2 = r2 - c.k[j] - (r1 & r0) - ((^r1) & r3) + j-- + + // unmix r1 + r1 = rotl16(r1, 16-2) + r1 = r1 - c.k[j] - (r0 & r3) - ((^r0) & r2) + j-- + + // unmix r0 + r0 = rotl16(r0, 16-1) + r0 = r0 - c.k[j] - (r3 & r2) - ((^r3) & r1) + j-- + + } + + r3 = r3 - c.k[r2&63] + r2 = r2 - c.k[r1&63] + r1 = r1 - c.k[r0&63] + r0 = r0 - c.k[r3&63] + + for j >= 0 { + // unmix r3 + r3 = rotl16(r3, 16-5) + r3 = r3 - c.k[j] - (r2 & r1) - ((^r2) & r0) + j-- + + // unmix r2 + r2 = rotl16(r2, 16-3) + r2 = r2 - c.k[j] - (r1 & r0) - ((^r1) & r3) + j-- + + // unmix r1 + r1 = rotl16(r1, 16-2) + r1 = r1 - c.k[j] - (r0 & r3) - ((^r0) & r2) + j-- + + // unmix r0 + r0 = rotl16(r0, 16-1) + r0 = r0 - c.k[j] - (r3 & r2) - ((^r3) & r1) + j-- + + } + + binary.LittleEndian.PutUint16(dst[0:], r0) + binary.LittleEndian.PutUint16(dst[2:], r1) + binary.LittleEndian.PutUint16(dst[4:], r2) + binary.LittleEndian.PutUint16(dst[6:], r3) +} diff --git a/vendor/golang.org/x/crypto/pkcs12/mac.go b/vendor/golang.org/x/crypto/pkcs12/mac.go new file mode 100644 index 000000000000..5f38aa7de83c --- /dev/null +++ b/vendor/golang.org/x/crypto/pkcs12/mac.go @@ -0,0 +1,45 @@ +// Copyright 2015 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package pkcs12 + +import ( + "crypto/hmac" + "crypto/sha1" + "crypto/x509/pkix" + "encoding/asn1" +) + +type macData struct { + Mac digestInfo + MacSalt []byte + Iterations int `asn1:"optional,default:1"` +} + +// from PKCS#7: +type digestInfo struct { + Algorithm pkix.AlgorithmIdentifier + Digest []byte +} + +var ( + oidSHA1 = asn1.ObjectIdentifier([]int{1, 3, 14, 3, 2, 26}) +) + +func verifyMac(macData *macData, message, password []byte) error { + if !macData.Mac.Algorithm.Algorithm.Equal(oidSHA1) { + return NotImplementedError("unknown digest algorithm: " + macData.Mac.Algorithm.Algorithm.String()) + } + + key := pbkdf(sha1Sum, 20, 64, macData.MacSalt, password, macData.Iterations, 3, 20) + + mac := hmac.New(sha1.New, key) + mac.Write(message) + expectedMAC := mac.Sum(nil) + + if !hmac.Equal(macData.Mac.Digest, expectedMAC) { + return ErrIncorrectPassword + } + return nil +} diff --git a/vendor/golang.org/x/crypto/pkcs12/pbkdf.go b/vendor/golang.org/x/crypto/pkcs12/pbkdf.go new file mode 100644 index 000000000000..5c419d41e32c --- /dev/null +++ b/vendor/golang.org/x/crypto/pkcs12/pbkdf.go @@ -0,0 +1,170 @@ +// Copyright 2015 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package pkcs12 + +import ( + "bytes" + "crypto/sha1" + "math/big" +) + +var ( + one = big.NewInt(1) +) + +// sha1Sum returns the SHA-1 hash of in. +func sha1Sum(in []byte) []byte { + sum := sha1.Sum(in) + return sum[:] +} + +// fillWithRepeats returns v*ceiling(len(pattern) / v) bytes consisting of +// repeats of pattern. +func fillWithRepeats(pattern []byte, v int) []byte { + if len(pattern) == 0 { + return nil + } + outputLen := v * ((len(pattern) + v - 1) / v) + return bytes.Repeat(pattern, (outputLen+len(pattern)-1)/len(pattern))[:outputLen] +} + +func pbkdf(hash func([]byte) []byte, u, v int, salt, password []byte, r int, ID byte, size int) (key []byte) { + // implementation of https://tools.ietf.org/html/rfc7292#appendix-B.2 , RFC text verbatim in comments + + // Let H be a hash function built around a compression function f: + + // Z_2^u x Z_2^v -> Z_2^u + + // (that is, H has a chaining variable and output of length u bits, and + // the message input to the compression function of H is v bits). The + // values for u and v are as follows: + + // HASH FUNCTION VALUE u VALUE v + // MD2, MD5 128 512 + // SHA-1 160 512 + // SHA-224 224 512 + // SHA-256 256 512 + // SHA-384 384 1024 + // SHA-512 512 1024 + // SHA-512/224 224 1024 + // SHA-512/256 256 1024 + + // Furthermore, let r be the iteration count. + + // We assume here that u and v are both multiples of 8, as are the + // lengths of the password and salt strings (which we denote by p and s, + // respectively) and the number n of pseudorandom bits required. In + // addition, u and v are of course non-zero. + + // For information on security considerations for MD5 [19], see [25] and + // [1], and on those for MD2, see [18]. + + // The following procedure can be used to produce pseudorandom bits for + // a particular "purpose" that is identified by a byte called "ID". + // This standard specifies 3 different values for the ID byte: + + // 1. If ID=1, then the pseudorandom bits being produced are to be used + // as key material for performing encryption or decryption. + + // 2. If ID=2, then the pseudorandom bits being produced are to be used + // as an IV (Initial Value) for encryption or decryption. + + // 3. If ID=3, then the pseudorandom bits being produced are to be used + // as an integrity key for MACing. + + // 1. Construct a string, D (the "diversifier"), by concatenating v/8 + // copies of ID. + var D []byte + for i := 0; i < v; i++ { + D = append(D, ID) + } + + // 2. Concatenate copies of the salt together to create a string S of + // length v(ceiling(s/v)) bits (the final copy of the salt may be + // truncated to create S). Note that if the salt is the empty + // string, then so is S. + + S := fillWithRepeats(salt, v) + + // 3. Concatenate copies of the password together to create a string P + // of length v(ceiling(p/v)) bits (the final copy of the password + // may be truncated to create P). Note that if the password is the + // empty string, then so is P. + + P := fillWithRepeats(password, v) + + // 4. Set I=S||P to be the concatenation of S and P. + I := append(S, P...) + + // 5. Set c=ceiling(n/u). + c := (size + u - 1) / u + + // 6. For i=1, 2, ..., c, do the following: + A := make([]byte, c*20) + var IjBuf []byte + for i := 0; i < c; i++ { + // A. Set A2=H^r(D||I). (i.e., the r-th hash of D||1, + // H(H(H(... H(D||I)))) + Ai := hash(append(D, I...)) + for j := 1; j < r; j++ { + Ai = hash(Ai) + } + copy(A[i*20:], Ai[:]) + + if i < c-1 { // skip on last iteration + // B. Concatenate copies of Ai to create a string B of length v + // bits (the final copy of Ai may be truncated to create B). + var B []byte + for len(B) < v { + B = append(B, Ai[:]...) + } + B = B[:v] + + // C. Treating I as a concatenation I_0, I_1, ..., I_(k-1) of v-bit + // blocks, where k=ceiling(s/v)+ceiling(p/v), modify I by + // setting I_j=(I_j+B+1) mod 2^v for each j. + { + Bbi := new(big.Int).SetBytes(B) + Ij := new(big.Int) + + for j := 0; j < len(I)/v; j++ { + Ij.SetBytes(I[j*v : (j+1)*v]) + Ij.Add(Ij, Bbi) + Ij.Add(Ij, one) + Ijb := Ij.Bytes() + // We expect Ijb to be exactly v bytes, + // if it is longer or shorter we must + // adjust it accordingly. + if len(Ijb) > v { + Ijb = Ijb[len(Ijb)-v:] + } + if len(Ijb) < v { + if IjBuf == nil { + IjBuf = make([]byte, v) + } + bytesShort := v - len(Ijb) + for i := 0; i < bytesShort; i++ { + IjBuf[i] = 0 + } + copy(IjBuf[bytesShort:], Ijb) + Ijb = IjBuf + } + copy(I[j*v:(j+1)*v], Ijb) + } + } + } + } + // 7. Concatenate A_1, A_2, ..., A_c together to form a pseudorandom + // bit string, A. + + // 8. Use the first n bits of A as the output of this entire process. + return A[:size] + + // If the above process is being used to generate a DES key, the process + // should be used to create 64 random bits, and the key's parity bits + // should be set after the 64 bits have been produced. Similar concerns + // hold for 2-key and 3-key triple-DES keys, for CDMF keys, and for any + // similar keys with parity bits "built into them". +} diff --git a/vendor/golang.org/x/crypto/pkcs12/pkcs12.go b/vendor/golang.org/x/crypto/pkcs12/pkcs12.go new file mode 100644 index 000000000000..eff9ad3a98f8 --- /dev/null +++ b/vendor/golang.org/x/crypto/pkcs12/pkcs12.go @@ -0,0 +1,346 @@ +// Copyright 2015 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package pkcs12 implements some of PKCS#12. +// +// This implementation is distilled from https://tools.ietf.org/html/rfc7292 +// and referenced documents. It is intended for decoding P12/PFX-stored +// certificates and keys for use with the crypto/tls package. +package pkcs12 + +import ( + "crypto/ecdsa" + "crypto/rsa" + "crypto/x509" + "crypto/x509/pkix" + "encoding/asn1" + "encoding/hex" + "encoding/pem" + "errors" +) + +var ( + oidDataContentType = asn1.ObjectIdentifier([]int{1, 2, 840, 113549, 1, 7, 1}) + oidEncryptedDataContentType = asn1.ObjectIdentifier([]int{1, 2, 840, 113549, 1, 7, 6}) + + oidFriendlyName = asn1.ObjectIdentifier([]int{1, 2, 840, 113549, 1, 9, 20}) + oidLocalKeyID = asn1.ObjectIdentifier([]int{1, 2, 840, 113549, 1, 9, 21}) + oidMicrosoftCSPName = asn1.ObjectIdentifier([]int{1, 3, 6, 1, 4, 1, 311, 17, 1}) +) + +type pfxPdu struct { + Version int + AuthSafe contentInfo + MacData macData `asn1:"optional"` +} + +type contentInfo struct { + ContentType asn1.ObjectIdentifier + Content asn1.RawValue `asn1:"tag:0,explicit,optional"` +} + +type encryptedData struct { + Version int + EncryptedContentInfo encryptedContentInfo +} + +type encryptedContentInfo struct { + ContentType asn1.ObjectIdentifier + ContentEncryptionAlgorithm pkix.AlgorithmIdentifier + EncryptedContent []byte `asn1:"tag:0,optional"` +} + +func (i encryptedContentInfo) Algorithm() pkix.AlgorithmIdentifier { + return i.ContentEncryptionAlgorithm +} + +func (i encryptedContentInfo) Data() []byte { return i.EncryptedContent } + +type safeBag struct { + Id asn1.ObjectIdentifier + Value asn1.RawValue `asn1:"tag:0,explicit"` + Attributes []pkcs12Attribute `asn1:"set,optional"` +} + +type pkcs12Attribute struct { + Id asn1.ObjectIdentifier + Value asn1.RawValue `asn1:"set"` +} + +type encryptedPrivateKeyInfo struct { + AlgorithmIdentifier pkix.AlgorithmIdentifier + EncryptedData []byte +} + +func (i encryptedPrivateKeyInfo) Algorithm() pkix.AlgorithmIdentifier { + return i.AlgorithmIdentifier +} + +func (i encryptedPrivateKeyInfo) Data() []byte { + return i.EncryptedData +} + +// PEM block types +const ( + certificateType = "CERTIFICATE" + privateKeyType = "PRIVATE KEY" +) + +// unmarshal calls asn1.Unmarshal, but also returns an error if there is any +// trailing data after unmarshaling. +func unmarshal(in []byte, out interface{}) error { + trailing, err := asn1.Unmarshal(in, out) + if err != nil { + return err + } + if len(trailing) != 0 { + return errors.New("pkcs12: trailing data found") + } + return nil +} + +// ConvertToPEM converts all "safe bags" contained in pfxData to PEM blocks. +func ToPEM(pfxData []byte, password string) ([]*pem.Block, error) { + encodedPassword, err := bmpString(password) + if err != nil { + return nil, ErrIncorrectPassword + } + + bags, encodedPassword, err := getSafeContents(pfxData, encodedPassword) + + if err != nil { + return nil, err + } + + blocks := make([]*pem.Block, 0, len(bags)) + for _, bag := range bags { + block, err := convertBag(&bag, encodedPassword) + if err != nil { + return nil, err + } + blocks = append(blocks, block) + } + + return blocks, nil +} + +func convertBag(bag *safeBag, password []byte) (*pem.Block, error) { + block := &pem.Block{ + Headers: make(map[string]string), + } + + for _, attribute := range bag.Attributes { + k, v, err := convertAttribute(&attribute) + if err != nil { + return nil, err + } + block.Headers[k] = v + } + + switch { + case bag.Id.Equal(oidCertBag): + block.Type = certificateType + certsData, err := decodeCertBag(bag.Value.Bytes) + if err != nil { + return nil, err + } + block.Bytes = certsData + case bag.Id.Equal(oidPKCS8ShroundedKeyBag): + block.Type = privateKeyType + + key, err := decodePkcs8ShroudedKeyBag(bag.Value.Bytes, password) + if err != nil { + return nil, err + } + + switch key := key.(type) { + case *rsa.PrivateKey: + block.Bytes = x509.MarshalPKCS1PrivateKey(key) + case *ecdsa.PrivateKey: + block.Bytes, err = x509.MarshalECPrivateKey(key) + if err != nil { + return nil, err + } + default: + return nil, errors.New("found unknown private key type in PKCS#8 wrapping") + } + default: + return nil, errors.New("don't know how to convert a safe bag of type " + bag.Id.String()) + } + return block, nil +} + +func convertAttribute(attribute *pkcs12Attribute) (key, value string, err error) { + isString := false + + switch { + case attribute.Id.Equal(oidFriendlyName): + key = "friendlyName" + isString = true + case attribute.Id.Equal(oidLocalKeyID): + key = "localKeyId" + case attribute.Id.Equal(oidMicrosoftCSPName): + // This key is chosen to match OpenSSL. + key = "Microsoft CSP Name" + isString = true + default: + return "", "", errors.New("pkcs12: unknown attribute with OID " + attribute.Id.String()) + } + + if isString { + if err := unmarshal(attribute.Value.Bytes, &attribute.Value); err != nil { + return "", "", err + } + if value, err = decodeBMPString(attribute.Value.Bytes); err != nil { + return "", "", err + } + } else { + var id []byte + if err := unmarshal(attribute.Value.Bytes, &id); err != nil { + return "", "", err + } + value = hex.EncodeToString(id) + } + + return key, value, nil +} + +// Decode extracts a certificate and private key from pfxData. This function +// assumes that there is only one certificate and only one private key in the +// pfxData. +func Decode(pfxData []byte, password string) (privateKey interface{}, certificate *x509.Certificate, err error) { + encodedPassword, err := bmpString(password) + if err != nil { + return nil, nil, err + } + + bags, encodedPassword, err := getSafeContents(pfxData, encodedPassword) + if err != nil { + return nil, nil, err + } + + if len(bags) != 2 { + err = errors.New("pkcs12: expected exactly two safe bags in the PFX PDU") + return + } + + for _, bag := range bags { + switch { + case bag.Id.Equal(oidCertBag): + if certificate != nil { + err = errors.New("pkcs12: expected exactly one certificate bag") + } + + certsData, err := decodeCertBag(bag.Value.Bytes) + if err != nil { + return nil, nil, err + } + certs, err := x509.ParseCertificates(certsData) + if err != nil { + return nil, nil, err + } + if len(certs) != 1 { + err = errors.New("pkcs12: expected exactly one certificate in the certBag") + return nil, nil, err + } + certificate = certs[0] + + case bag.Id.Equal(oidPKCS8ShroundedKeyBag): + if privateKey != nil { + err = errors.New("pkcs12: expected exactly one key bag") + } + + if privateKey, err = decodePkcs8ShroudedKeyBag(bag.Value.Bytes, encodedPassword); err != nil { + return nil, nil, err + } + } + } + + if certificate == nil { + return nil, nil, errors.New("pkcs12: certificate missing") + } + if privateKey == nil { + return nil, nil, errors.New("pkcs12: private key missing") + } + + return +} + +func getSafeContents(p12Data, password []byte) (bags []safeBag, updatedPassword []byte, err error) { + pfx := new(pfxPdu) + if err := unmarshal(p12Data, pfx); err != nil { + return nil, nil, errors.New("pkcs12: error reading P12 data: " + err.Error()) + } + + if pfx.Version != 3 { + return nil, nil, NotImplementedError("can only decode v3 PFX PDU's") + } + + if !pfx.AuthSafe.ContentType.Equal(oidDataContentType) { + return nil, nil, NotImplementedError("only password-protected PFX is implemented") + } + + // unmarshal the explicit bytes in the content for type 'data' + if err := unmarshal(pfx.AuthSafe.Content.Bytes, &pfx.AuthSafe.Content); err != nil { + return nil, nil, err + } + + if len(pfx.MacData.Mac.Algorithm.Algorithm) == 0 { + return nil, nil, errors.New("pkcs12: no MAC in data") + } + + if err := verifyMac(&pfx.MacData, pfx.AuthSafe.Content.Bytes, password); err != nil { + if err == ErrIncorrectPassword && len(password) == 2 && password[0] == 0 && password[1] == 0 { + // some implementations use an empty byte array + // for the empty string password try one more + // time with empty-empty password + password = nil + err = verifyMac(&pfx.MacData, pfx.AuthSafe.Content.Bytes, password) + } + if err != nil { + return nil, nil, err + } + } + + var authenticatedSafe []contentInfo + if err := unmarshal(pfx.AuthSafe.Content.Bytes, &authenticatedSafe); err != nil { + return nil, nil, err + } + + if len(authenticatedSafe) != 2 { + return nil, nil, NotImplementedError("expected exactly two items in the authenticated safe") + } + + for _, ci := range authenticatedSafe { + var data []byte + + switch { + case ci.ContentType.Equal(oidDataContentType): + if err := unmarshal(ci.Content.Bytes, &data); err != nil { + return nil, nil, err + } + case ci.ContentType.Equal(oidEncryptedDataContentType): + var encryptedData encryptedData + if err := unmarshal(ci.Content.Bytes, &encryptedData); err != nil { + return nil, nil, err + } + if encryptedData.Version != 0 { + return nil, nil, NotImplementedError("only version 0 of EncryptedData is supported") + } + if data, err = pbDecrypt(encryptedData.EncryptedContentInfo, password); err != nil { + return nil, nil, err + } + default: + return nil, nil, NotImplementedError("only data and encryptedData content types are supported in authenticated safe") + } + + var safeContents []safeBag + if err := unmarshal(data, &safeContents); err != nil { + return nil, nil, err + } + bags = append(bags, safeContents...) + } + + return bags, password, nil +} diff --git a/vendor/golang.org/x/crypto/pkcs12/safebags.go b/vendor/golang.org/x/crypto/pkcs12/safebags.go new file mode 100644 index 000000000000..def1f7b98d7d --- /dev/null +++ b/vendor/golang.org/x/crypto/pkcs12/safebags.go @@ -0,0 +1,57 @@ +// Copyright 2015 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package pkcs12 + +import ( + "crypto/x509" + "encoding/asn1" + "errors" +) + +var ( + // see https://tools.ietf.org/html/rfc7292#appendix-D + oidCertTypeX509Certificate = asn1.ObjectIdentifier([]int{1, 2, 840, 113549, 1, 9, 22, 1}) + oidPKCS8ShroundedKeyBag = asn1.ObjectIdentifier([]int{1, 2, 840, 113549, 1, 12, 10, 1, 2}) + oidCertBag = asn1.ObjectIdentifier([]int{1, 2, 840, 113549, 1, 12, 10, 1, 3}) +) + +type certBag struct { + Id asn1.ObjectIdentifier + Data []byte `asn1:"tag:0,explicit"` +} + +func decodePkcs8ShroudedKeyBag(asn1Data, password []byte) (privateKey interface{}, err error) { + pkinfo := new(encryptedPrivateKeyInfo) + if err = unmarshal(asn1Data, pkinfo); err != nil { + return nil, errors.New("pkcs12: error decoding PKCS#8 shrouded key bag: " + err.Error()) + } + + pkData, err := pbDecrypt(pkinfo, password) + if err != nil { + return nil, errors.New("pkcs12: error decrypting PKCS#8 shrouded key bag: " + err.Error()) + } + + ret := new(asn1.RawValue) + if err = unmarshal(pkData, ret); err != nil { + return nil, errors.New("pkcs12: error unmarshaling decrypted private key: " + err.Error()) + } + + if privateKey, err = x509.ParsePKCS8PrivateKey(pkData); err != nil { + return nil, errors.New("pkcs12: error parsing PKCS#8 private key: " + err.Error()) + } + + return privateKey, nil +} + +func decodeCertBag(asn1Data []byte) (x509Certificates []byte, err error) { + bag := new(certBag) + if err := unmarshal(asn1Data, bag); err != nil { + return nil, errors.New("pkcs12: error decoding cert bag: " + err.Error()) + } + if !bag.Id.Equal(oidCertTypeX509Certificate) { + return nil, NotImplementedError("only X509 certificates are supported") + } + return bag.Data, nil +} diff --git a/vendor/vendor.json b/vendor/vendor.json index a781e29dc384..b0e5b335d2b5 100644 --- a/vendor/vendor.json +++ b/vendor/vendor.json @@ -1478,6 +1478,18 @@ "revision": "b176d7def5d71bdd214203491f89843ed217f420", "revisionTime": "2017-07-23T04:49:35Z" }, + { + "checksumSHA1": "PJY7uCr3UnX4/Mf/RoWnbieSZ8o=", + "path": "golang.org/x/crypto/pkcs12", + "revision": "4d3f4d9ffa16a13f451c3b2999e9c49e9750bf06", + "revisionTime": "2018-10-23T16:52:47Z" + }, + { + "checksumSHA1": "p0GC51McIdA7JygoP223twJ1s0E=", + "path": "golang.org/x/crypto/pkcs12/internal/rc2", + "revision": "4d3f4d9ffa16a13f451c3b2999e9c49e9750bf06", + "revisionTime": "2018-10-23T16:52:47Z" + }, { "checksumSHA1": "fsrFs762jlaILyqqQImS1GfvIvw=", "path": "golang.org/x/crypto/ssh",