diff --git a/builder/oracle/oci/config.go b/builder/oracle/oci/config.go index 52229cc48..c046cecc7 100644 --- a/builder/oracle/oci/config.go +++ b/builder/oracle/oci/config.go @@ -19,6 +19,7 @@ import ( "github.com/hashicorp/packer/packer" "github.com/hashicorp/packer/template/interpolate" ocicommon "github.com/oracle/oci-go-sdk/common" + ociauth "github.com/oracle/oci-go-sdk/common/auth" ) type Config struct { @@ -27,6 +28,18 @@ type Config struct { configProvider ocicommon.ConfigurationProvider + // Instance Principals (OPTIONAL) + // If set to true the following can't have non empty values + // - AccessCfgFile + // - AccessCfgFileAccount + // - UserID + // - TenancyID + // - Region + // - Fingerprint + // - KeyFile + // - PassPhrase + InstancePrincipals bool `mapstructure:"use_instance_principals"` + AccessCfgFile string `mapstructure:"access_cfg_file"` AccessCfgFileAccount string `mapstructure:"access_cfg_file_account"` @@ -86,84 +99,134 @@ func (c *Config) Prepare(raws ...interface{}) error { return fmt.Errorf("Failed to mapstructure Config: %+v", err) } - // Determine where the SDK config is located - if c.AccessCfgFile == "" { - c.AccessCfgFile, err = getDefaultOCISettingsPath() - if err != nil { - log.Println("Default OCI settings file not found") - } - } - - if c.AccessCfgFileAccount == "" { - c.AccessCfgFileAccount = "DEFAULT" - } - - var keyContent []byte - if c.KeyFile != "" { - path, err := packer.ExpandUser(c.KeyFile) - if err != nil { - return err - } - - // Read API signing key - keyContent, err = ioutil.ReadFile(path) - if err != nil { - return err - } - } - - fileProvider, _ := ocicommon.ConfigurationProviderFromFileWithProfile(c.AccessCfgFile, c.AccessCfgFileAccount, c.PassPhrase) - if c.Region == "" { - var region string - if fileProvider != nil { - region, _ = fileProvider.Region() - } - if region == "" { - c.Region = "us-phoenix-1" - } - } - - providers := []ocicommon.ConfigurationProvider{ - NewRawConfigurationProvider(c.TenancyID, c.UserID, c.Region, c.Fingerprint, string(keyContent), &c.PassPhrase), - } - - if fileProvider != nil { - providers = append(providers, fileProvider) - } - - // Load API access configuration from SDK - configProvider, err := ocicommon.ComposingConfigurationProvider(providers) - if err != nil { - return err - } - var errs *packer.MultiError if es := c.Comm.Prepare(&c.ctx); len(es) > 0 { errs = packer.MultiErrorAppend(errs, es...) } - if userOCID, _ := configProvider.UserOCID(); userOCID == "" { - errs = packer.MultiErrorAppend( - errs, errors.New("'user_ocid' must be specified")) - } + var tenancyOCID string - tenancyOCID, _ := configProvider.TenancyOCID() - if tenancyOCID == "" { - errs = packer.MultiErrorAppend( - errs, errors.New("'tenancy_ocid' must be specified")) - } + if c.InstancePrincipals { + // We could go through all keys in one go and report that the below set + // of keys cannot coexist with use_instance_principals but decided to + // split them and report them seperately so that the user sees the specific + // key involved. + var message string = " cannot be present when use_instance_principals is set to true." + if c.AccessCfgFile != "" { + errs = packer.MultiErrorAppend(errs, fmt.Errorf("access_cfg_file"+message)) + } + if c.AccessCfgFileAccount != "" { + errs = packer.MultiErrorAppend(errs, fmt.Errorf("access_cfg_file_account"+message)) + } + if c.UserID != "" { + errs = packer.MultiErrorAppend(errs, fmt.Errorf("user_ocid"+message)) + } + if c.TenancyID != "" { + errs = packer.MultiErrorAppend(errs, fmt.Errorf("tenancy_ocid"+message)) + } + if c.Region != "" { + errs = packer.MultiErrorAppend(errs, fmt.Errorf("region"+message)) + } + if c.Fingerprint != "" { + errs = packer.MultiErrorAppend(errs, fmt.Errorf("fingerprint"+message)) + } + if c.KeyFile != "" { + errs = packer.MultiErrorAppend(errs, fmt.Errorf("key_file"+message)) + } + if c.PassPhrase != "" { + errs = packer.MultiErrorAppend(errs, fmt.Errorf("pass_phrase"+message)) + } + // This check is used to facilitate testing. During testing a Mock struct + // is assigned to c.configProvider otherwise testing fails because Instance + // Principals cannot be obtained. + if c.configProvider == nil { + // Even though the previous configuraion checks might fail we don't want + // to skip this step. It seems that the logic behind the checks in this + // file is to check everything even getting the configProvider. + c.configProvider, err = ociauth.InstancePrincipalConfigurationProvider() + if err != nil { + return err + } + } + tenancyOCID, err = c.configProvider.TenancyOCID() + if err != nil { + return err + } + } else { + // Determine where the SDK config is located + if c.AccessCfgFile == "" { + c.AccessCfgFile, err = getDefaultOCISettingsPath() + if err != nil { + log.Println("Default OCI settings file not found") + } + } - if fingerprint, _ := configProvider.KeyFingerprint(); fingerprint == "" { - errs = packer.MultiErrorAppend( - errs, errors.New("'fingerprint' must be specified")) - } + if c.AccessCfgFileAccount == "" { + c.AccessCfgFileAccount = "DEFAULT" + } - if _, err := configProvider.PrivateRSAKey(); err != nil { - errs = packer.MultiErrorAppend( - errs, errors.New("'key_file' must be specified")) - } + var keyContent []byte + if c.KeyFile != "" { + path, err := packer.ExpandUser(c.KeyFile) + if err != nil { + return err + } - c.configProvider = configProvider + // Read API signing key + keyContent, err = ioutil.ReadFile(path) + if err != nil { + return err + } + } + + fileProvider, _ := ocicommon.ConfigurationProviderFromFileWithProfile(c.AccessCfgFile, c.AccessCfgFileAccount, c.PassPhrase) + if c.Region == "" { + var region string + if fileProvider != nil { + region, _ = fileProvider.Region() + } + if region == "" { + c.Region = "us-phoenix-1" + } + } + + providers := []ocicommon.ConfigurationProvider{ + NewRawConfigurationProvider(c.TenancyID, c.UserID, c.Region, c.Fingerprint, string(keyContent), &c.PassPhrase), + } + + if fileProvider != nil { + providers = append(providers, fileProvider) + } + + // Load API access configuration from SDK + configProvider, err := ocicommon.ComposingConfigurationProvider(providers) + if err != nil { + return err + } + + if userOCID, _ := configProvider.UserOCID(); userOCID == "" { + errs = packer.MultiErrorAppend( + errs, errors.New("'user_ocid' must be specified")) + } + + tenancyOCID, _ = configProvider.TenancyOCID() + if tenancyOCID == "" { + errs = packer.MultiErrorAppend( + errs, errors.New("'tenancy_ocid' must be specified")) + } + + if fingerprint, _ := configProvider.KeyFingerprint(); fingerprint == "" { + errs = packer.MultiErrorAppend( + errs, errors.New("'fingerprint' must be specified")) + } + + if _, err := configProvider.PrivateRSAKey(); err != nil { + errs = packer.MultiErrorAppend( + errs, errors.New("'key_file' must be specified")) + } + + c.configProvider = configProvider + } if c.AvailabilityDomain == "" { errs = packer.MultiErrorAppend( diff --git a/builder/oracle/oci/config.hcl2spec.go b/builder/oracle/oci/config.hcl2spec.go index 107c1ae43..c839a107d 100644 --- a/builder/oracle/oci/config.hcl2spec.go +++ b/builder/oracle/oci/config.hcl2spec.go @@ -56,6 +56,7 @@ type FlatConfig struct { WinRMUseSSL *bool `mapstructure:"winrm_use_ssl" cty:"winrm_use_ssl"` WinRMInsecure *bool `mapstructure:"winrm_insecure" cty:"winrm_insecure"` WinRMUseNTLM *bool `mapstructure:"winrm_use_ntlm" cty:"winrm_use_ntlm"` + InstancePrincipals *bool `mapstructure:"use_instance_principals" cty:"use_instance_principals"` AccessCfgFile *string `mapstructure:"access_cfg_file" cty:"access_cfg_file"` AccessCfgFileAccount *string `mapstructure:"access_cfg_file_account" cty:"access_cfg_file_account"` UserID *string `mapstructure:"user_ocid" cty:"user_ocid"` @@ -138,6 +139,7 @@ func (*FlatConfig) HCL2Spec() map[string]hcldec.Spec { "winrm_use_ssl": &hcldec.AttrSpec{Name: "winrm_use_ssl", Type: cty.Bool, Required: false}, "winrm_insecure": &hcldec.AttrSpec{Name: "winrm_insecure", Type: cty.Bool, Required: false}, "winrm_use_ntlm": &hcldec.AttrSpec{Name: "winrm_use_ntlm", Type: cty.Bool, Required: false}, + "use_instance_principals": &hcldec.AttrSpec{Name: "use_instance_principals", Type: cty.Bool, Required: false}, "access_cfg_file": &hcldec.AttrSpec{Name: "access_cfg_file", Type: cty.String, Required: false}, "access_cfg_file_account": &hcldec.AttrSpec{Name: "access_cfg_file_account", Type: cty.String, Required: false}, "user_ocid": &hcldec.AttrSpec{Name: "user_ocid", Type: cty.String, Required: false}, diff --git a/builder/oracle/oci/config_test.go b/builder/oracle/oci/config_test.go index 117f1190d..22ac18172 100644 --- a/builder/oracle/oci/config_test.go +++ b/builder/oracle/oci/config_test.go @@ -15,6 +15,7 @@ import ( func testConfig(accessConfFile *os.File) map[string]interface{} { return map[string]interface{}{ + "availability_domain": "aaaa:PHX-AD-3", "access_cfg_file": accessConfFile.Name(), @@ -252,6 +253,36 @@ func TestConfig(t *testing.T) { t.Errorf("Expected ConfigProvider.KeyFingerprint: %s, got %s", expected, fingerprint) } }) + + // Test the correct errors are produced when certain template keys + // are present alongside use_instance_principals key. + invalidKeys := []string{ + "access_cfg_file", + "access_cfg_file_account", + "user_ocid", + "tenancy_ocid", + "region", + "fingerprint", + "key_file", + "pass_phrase", + } + for _, k := range invalidKeys { + t.Run(k+"_mixed_with_use_instance_principals", func(t *testing.T) { + raw := testConfig(cfgFile) + raw["use_instance_principals"] = "true" + raw[k] = "some_random_value" + + var c Config + + c.configProvider = instancePrincipalConfigurationProviderMock{} + + errs := c.Prepare(raw) + + if !strings.Contains(errs.Error(), k) { + t.Errorf("Expected '%s' to contain '%s'", errs.Error(), k) + } + }) + } } // BaseTestConfig creates the base (DEFAULT) config including a temporary key diff --git a/builder/oracle/oci/instance_principal_configuration_provider_mock.go b/builder/oracle/oci/instance_principal_configuration_provider_mock.go new file mode 100644 index 000000000..27969d1aa --- /dev/null +++ b/builder/oracle/oci/instance_principal_configuration_provider_mock.go @@ -0,0 +1,34 @@ +package oci + +import ( + "crypto/rand" + "crypto/rsa" +) + +// Mock struct to be used during testing to obtain Instance Principals. +type instancePrincipalConfigurationProviderMock struct { +} + +func (p instancePrincipalConfigurationProviderMock) PrivateRSAKey() (*rsa.PrivateKey, error) { + return rsa.GenerateKey(rand.Reader, 1024) +} + +func (p instancePrincipalConfigurationProviderMock) KeyID() (string, error) { + return "some_random_key_id", nil +} + +func (p instancePrincipalConfigurationProviderMock) TenancyOCID() (string, error) { + return "some_random_tenancy", nil +} + +func (p instancePrincipalConfigurationProviderMock) UserOCID() (string, error) { + return "", nil +} + +func (p instancePrincipalConfigurationProviderMock) KeyFingerprint() (string, error) { + return "", nil +} + +func (p instancePrincipalConfigurationProviderMock) Region() (string, error) { + return "some_random_region", nil +} diff --git a/vendor/github.com/oracle/oci-go-sdk/common/auth/certificate_retriever.go b/vendor/github.com/oracle/oci-go-sdk/common/auth/certificate_retriever.go new file mode 100644 index 000000000..dcd452da1 --- /dev/null +++ b/vendor/github.com/oracle/oci-go-sdk/common/auth/certificate_retriever.go @@ -0,0 +1,158 @@ +// Copyright (c) 2016, 2018, Oracle and/or its affiliates. All rights reserved. + +package auth + +import ( + "bytes" + "crypto/rsa" + "crypto/x509" + "encoding/pem" + "fmt" + "github.com/oracle/oci-go-sdk/common" + "sync" +) + +// x509CertificateRetriever provides an X509 certificate with the RSA private key +type x509CertificateRetriever interface { + Refresh() error + CertificatePemRaw() []byte + Certificate() *x509.Certificate + PrivateKeyPemRaw() []byte + PrivateKey() *rsa.PrivateKey +} + +// urlBasedX509CertificateRetriever retrieves PEM-encoded X509 certificates from the given URLs. +type urlBasedX509CertificateRetriever struct { + certURL string + privateKeyURL string + passphrase string + certificatePemRaw []byte + certificate *x509.Certificate + privateKeyPemRaw []byte + privateKey *rsa.PrivateKey + mux sync.Mutex +} + +func newURLBasedX509CertificateRetriever(certURL, privateKeyURL, passphrase string) x509CertificateRetriever { + return &urlBasedX509CertificateRetriever{ + certURL: certURL, + privateKeyURL: privateKeyURL, + passphrase: passphrase, + mux: sync.Mutex{}, + } +} + +// Refresh() is failure atomic, i.e., CertificatePemRaw(), Certificate(), PrivateKeyPemRaw(), and PrivateKey() would +// return their previous values if Refresh() fails. +func (r *urlBasedX509CertificateRetriever) Refresh() error { + common.Debugln("Refreshing certificate") + + r.mux.Lock() + defer r.mux.Unlock() + + var err error + + var certificatePemRaw []byte + var certificate *x509.Certificate + if certificatePemRaw, certificate, err = r.renewCertificate(r.certURL); err != nil { + return fmt.Errorf("failed to renew certificate: %s", err.Error()) + } + + var privateKeyPemRaw []byte + var privateKey *rsa.PrivateKey + if r.privateKeyURL != "" { + if privateKeyPemRaw, privateKey, err = r.renewPrivateKey(r.privateKeyURL, r.passphrase); err != nil { + return fmt.Errorf("failed to renew private key: %s", err.Error()) + } + } + + r.certificatePemRaw = certificatePemRaw + r.certificate = certificate + r.privateKeyPemRaw = privateKeyPemRaw + r.privateKey = privateKey + return nil +} + +func (r *urlBasedX509CertificateRetriever) renewCertificate(url string) (certificatePemRaw []byte, certificate *x509.Certificate, err error) { + var body bytes.Buffer + if body, err = httpGet(url); err != nil { + return nil, nil, fmt.Errorf("failed to get certificate from %s: %s", url, err.Error()) + } + + certificatePemRaw = body.Bytes() + var block *pem.Block + block, _ = pem.Decode(certificatePemRaw) + if block == nil { + return nil, nil, fmt.Errorf("failed to parse the new certificate, not valid pem data") + } + + if certificate, err = x509.ParseCertificate(block.Bytes); err != nil { + return nil, nil, fmt.Errorf("failed to parse the new certificate: %s", err.Error()) + } + + return certificatePemRaw, certificate, nil +} + +func (r *urlBasedX509CertificateRetriever) renewPrivateKey(url, passphrase string) (privateKeyPemRaw []byte, privateKey *rsa.PrivateKey, err error) { + var body bytes.Buffer + if body, err = httpGet(url); err != nil { + return nil, nil, fmt.Errorf("failed to get private key from %s: %s", url, err.Error()) + } + + privateKeyPemRaw = body.Bytes() + if privateKey, err = common.PrivateKeyFromBytes(privateKeyPemRaw, &passphrase); err != nil { + return nil, nil, fmt.Errorf("failed to parse the new private key: %s", err.Error()) + } + + return privateKeyPemRaw, privateKey, nil +} + +func (r *urlBasedX509CertificateRetriever) CertificatePemRaw() []byte { + r.mux.Lock() + defer r.mux.Unlock() + + if r.certificatePemRaw == nil { + return nil + } + + c := make([]byte, len(r.certificatePemRaw)) + copy(c, r.certificatePemRaw) + return c +} + +func (r *urlBasedX509CertificateRetriever) Certificate() *x509.Certificate { + r.mux.Lock() + defer r.mux.Unlock() + + if r.certificate == nil { + return nil + } + + c := *r.certificate + return &c +} + +func (r *urlBasedX509CertificateRetriever) PrivateKeyPemRaw() []byte { + r.mux.Lock() + defer r.mux.Unlock() + + if r.privateKeyPemRaw == nil { + return nil + } + + c := make([]byte, len(r.privateKeyPemRaw)) + copy(c, r.privateKeyPemRaw) + return c +} + +func (r *urlBasedX509CertificateRetriever) PrivateKey() *rsa.PrivateKey { + r.mux.Lock() + defer r.mux.Unlock() + + if r.privateKey == nil { + return nil + } + + c := *r.privateKey + return &c +} diff --git a/vendor/github.com/oracle/oci-go-sdk/common/auth/configuration.go b/vendor/github.com/oracle/oci-go-sdk/common/auth/configuration.go new file mode 100644 index 000000000..32be79574 --- /dev/null +++ b/vendor/github.com/oracle/oci-go-sdk/common/auth/configuration.go @@ -0,0 +1,61 @@ +// Copyright (c) 2016, 2018, Oracle and/or its affiliates. All rights reserved. + +package auth + +import ( + "crypto/rsa" + "fmt" + "github.com/oracle/oci-go-sdk/common" +) + +type instancePrincipalConfigurationProvider struct { + keyProvider *instancePrincipalKeyProvider + region *common.Region +} + +//InstancePrincipalConfigurationProvider returns a configuration for instance principals +func InstancePrincipalConfigurationProvider() (common.ConfigurationProvider, error) { + var err error + var keyProvider *instancePrincipalKeyProvider + if keyProvider, err = newInstancePrincipalKeyProvider(); err != nil { + return nil, fmt.Errorf("failed to create a new key provider for instance principal: %s", err.Error()) + } + return instancePrincipalConfigurationProvider{keyProvider: keyProvider, region: nil}, nil +} + +//InstancePrincipalConfigurationProviderForRegion returns a configuration for instance principals with a given region +func InstancePrincipalConfigurationProviderForRegion(region common.Region) (common.ConfigurationProvider, error) { + var err error + var keyProvider *instancePrincipalKeyProvider + if keyProvider, err = newInstancePrincipalKeyProvider(); err != nil { + return nil, fmt.Errorf("failed to create a new key provider for instance principal: %s", err.Error()) + } + return instancePrincipalConfigurationProvider{keyProvider: keyProvider, region: ®ion}, nil +} + +func (p instancePrincipalConfigurationProvider) PrivateRSAKey() (*rsa.PrivateKey, error) { + return p.keyProvider.PrivateRSAKey() +} + +func (p instancePrincipalConfigurationProvider) KeyID() (string, error) { + return p.keyProvider.KeyID() +} + +func (p instancePrincipalConfigurationProvider) TenancyOCID() (string, error) { + return p.keyProvider.TenancyOCID() +} + +func (p instancePrincipalConfigurationProvider) UserOCID() (string, error) { + return "", nil +} + +func (p instancePrincipalConfigurationProvider) KeyFingerprint() (string, error) { + return "", nil +} + +func (p instancePrincipalConfigurationProvider) Region() (string, error) { + if p.region == nil { + return string(p.keyProvider.RegionForFederationClient()), nil + } + return string(*p.region), nil +} diff --git a/vendor/github.com/oracle/oci-go-sdk/common/auth/federation_client.go b/vendor/github.com/oracle/oci-go-sdk/common/auth/federation_client.go new file mode 100644 index 000000000..1aa258e83 --- /dev/null +++ b/vendor/github.com/oracle/oci-go-sdk/common/auth/federation_client.go @@ -0,0 +1,292 @@ +// Copyright (c) 2016, 2018, Oracle and/or its affiliates. All rights reserved. + +// Package auth provides supporting functions and structs for authentication +package auth + +import ( + "context" + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "encoding/pem" + "fmt" + "github.com/oracle/oci-go-sdk/common" + "net/http" + "strings" + "sync" +) + +// federationClient is a client to retrieve the security token for an instance principal necessary to sign a request. +// It also provides the private key whose corresponding public key is used to retrieve the security token. +type federationClient interface { + PrivateKey() (*rsa.PrivateKey, error) + SecurityToken() (string, error) +} + +// x509FederationClient retrieves a security token from Auth service. +type x509FederationClient struct { + tenancyID string + sessionKeySupplier sessionKeySupplier + leafCertificateRetriever x509CertificateRetriever + intermediateCertificateRetrievers []x509CertificateRetriever + securityToken securityToken + authClient *common.BaseClient + mux sync.Mutex +} + +func newX509FederationClient(region common.Region, tenancyID string, leafCertificateRetriever x509CertificateRetriever, intermediateCertificateRetrievers []x509CertificateRetriever) federationClient { + client := &x509FederationClient{ + tenancyID: tenancyID, + leafCertificateRetriever: leafCertificateRetriever, + intermediateCertificateRetrievers: intermediateCertificateRetrievers, + } + client.sessionKeySupplier = newSessionKeySupplier() + client.authClient = newAuthClient(region, client) + return client +} + +var ( + genericHeaders = []string{"date", "(request-target)"} // "host" is not needed for the federation endpoint. Don't ask me why. + bodyHeaders = []string{"content-length", "content-type", "x-content-sha256"} +) + +func newAuthClient(region common.Region, provider common.KeyProvider) *common.BaseClient { + signer := common.RequestSigner(provider, genericHeaders, bodyHeaders) + client := common.DefaultBaseClientWithSigner(signer) + if region == common.RegionSEA { + client.Host = "https://auth.r1.oracleiaas.com" + } else { + client.Host = fmt.Sprintf(common.DefaultHostURLTemplate, "auth", string(region)) + } + client.BasePath = "v1/x509" + return &client +} + +// For authClient to sign requests to X509 Federation Endpoint +func (c *x509FederationClient) KeyID() (string, error) { + tenancy := c.tenancyID + fingerprint := fingerprint(c.leafCertificateRetriever.Certificate()) + return fmt.Sprintf("%s/fed-x509/%s", tenancy, fingerprint), nil +} + +// For authClient to sign requests to X509 Federation Endpoint +func (c *x509FederationClient) PrivateRSAKey() (*rsa.PrivateKey, error) { + return c.leafCertificateRetriever.PrivateKey(), nil +} + +func (c *x509FederationClient) PrivateKey() (*rsa.PrivateKey, error) { + c.mux.Lock() + defer c.mux.Unlock() + + if err := c.renewSecurityTokenIfNotValid(); err != nil { + return nil, err + } + return c.sessionKeySupplier.PrivateKey(), nil +} + +func (c *x509FederationClient) SecurityToken() (token string, err error) { + c.mux.Lock() + defer c.mux.Unlock() + + if err = c.renewSecurityTokenIfNotValid(); err != nil { + return "", err + } + return c.securityToken.String(), nil +} + +func (c *x509FederationClient) renewSecurityTokenIfNotValid() (err error) { + if c.securityToken == nil || !c.securityToken.Valid() { + if err = c.renewSecurityToken(); err != nil { + return fmt.Errorf("failed to renew security token: %s", err.Error()) + } + } + return nil +} + +func (c *x509FederationClient) renewSecurityToken() (err error) { + if err = c.sessionKeySupplier.Refresh(); err != nil { + return fmt.Errorf("failed to refresh session key: %s", err.Error()) + } + + if err = c.leafCertificateRetriever.Refresh(); err != nil { + return fmt.Errorf("failed to refresh leaf certificate: %s", err.Error()) + } + + updatedTenancyID := extractTenancyIDFromCertificate(c.leafCertificateRetriever.Certificate()) + if c.tenancyID != updatedTenancyID { + err = fmt.Errorf("unexpected update of tenancy OCID in the leaf certificate. Previous tenancy: %s, Updated: %s", c.tenancyID, updatedTenancyID) + return + } + + for _, retriever := range c.intermediateCertificateRetrievers { + if err = retriever.Refresh(); err != nil { + return fmt.Errorf("failed to refresh intermediate certificate: %s", err.Error()) + } + } + + if c.securityToken, err = c.getSecurityToken(); err != nil { + return fmt.Errorf("failed to get security token: %s", err.Error()) + } + + return nil +} + +func (c *x509FederationClient) getSecurityToken() (securityToken, error) { + request := c.makeX509FederationRequest() + + var err error + var httpRequest http.Request + if httpRequest, err = common.MakeDefaultHTTPRequestWithTaggedStruct(http.MethodPost, "", request); err != nil { + return nil, fmt.Errorf("failed to make http request: %s", err.Error()) + } + + var httpResponse *http.Response + defer common.CloseBodyIfValid(httpResponse) + if httpResponse, err = c.authClient.Call(context.Background(), &httpRequest); err != nil { + return nil, fmt.Errorf("failed to call: %s", err.Error()) + } + + response := x509FederationResponse{} + if err = common.UnmarshalResponse(httpResponse, &response); err != nil { + return nil, fmt.Errorf("failed to unmarshal the response: %s", err.Error()) + } + + return newInstancePrincipalToken(response.Token.Token) +} + +type x509FederationRequest struct { + X509FederationDetails `contributesTo:"body"` +} + +// X509FederationDetails x509 federation details +type X509FederationDetails struct { + Certificate string `mandatory:"true" json:"certificate,omitempty"` + PublicKey string `mandatory:"true" json:"publicKey,omitempty"` + IntermediateCertificates []string `mandatory:"false" json:"intermediateCertificates,omitempty"` +} + +type x509FederationResponse struct { + Token `presentIn:"body"` +} + +// Token token +type Token struct { + Token string `mandatory:"true" json:"token,omitempty"` +} + +func (c *x509FederationClient) makeX509FederationRequest() *x509FederationRequest { + certificate := c.sanitizeCertificateString(string(c.leafCertificateRetriever.CertificatePemRaw())) + publicKey := c.sanitizeCertificateString(string(c.sessionKeySupplier.PublicKeyPemRaw())) + var intermediateCertificates []string + for _, retriever := range c.intermediateCertificateRetrievers { + intermediateCertificates = append(intermediateCertificates, c.sanitizeCertificateString(string(retriever.CertificatePemRaw()))) + } + + details := X509FederationDetails{ + Certificate: certificate, + PublicKey: publicKey, + IntermediateCertificates: intermediateCertificates, + } + return &x509FederationRequest{details} +} + +func (c *x509FederationClient) sanitizeCertificateString(certString string) string { + certString = strings.Replace(certString, "-----BEGIN CERTIFICATE-----", "", -1) + certString = strings.Replace(certString, "-----END CERTIFICATE-----", "", -1) + certString = strings.Replace(certString, "-----BEGIN PUBLIC KEY-----", "", -1) + certString = strings.Replace(certString, "-----END PUBLIC KEY-----", "", -1) + certString = strings.Replace(certString, "\n", "", -1) + return certString +} + +// sessionKeySupplier provides an RSA keypair which can be re-generated by calling Refresh(). +type sessionKeySupplier interface { + Refresh() error + PrivateKey() *rsa.PrivateKey + PublicKeyPemRaw() []byte +} + +// inMemorySessionKeySupplier implements sessionKeySupplier to vend an RSA keypair. +// Refresh() generates a new RSA keypair with a random source, and keeps it in memory. +// +// inMemorySessionKeySupplier is not thread-safe. +type inMemorySessionKeySupplier struct { + keySize int + privateKey *rsa.PrivateKey + publicKeyPemRaw []byte +} + +// newSessionKeySupplier creates and returns a sessionKeySupplier instance which generates key pairs of size 2048. +func newSessionKeySupplier() sessionKeySupplier { + return &inMemorySessionKeySupplier{keySize: 2048} +} + +// Refresh() is failure atomic, i.e., PrivateKey() and PublicKeyPemRaw() would return their previous values +// if Refresh() fails. +func (s *inMemorySessionKeySupplier) Refresh() (err error) { + common.Debugln("Refreshing session key") + + var privateKey *rsa.PrivateKey + privateKey, err = rsa.GenerateKey(rand.Reader, s.keySize) + if err != nil { + return fmt.Errorf("failed to generate a new keypair: %s", err) + } + + var publicKeyAsnBytes []byte + if publicKeyAsnBytes, err = x509.MarshalPKIXPublicKey(privateKey.Public()); err != nil { + return fmt.Errorf("failed to marshal the public part of the new keypair: %s", err.Error()) + } + publicKeyPemRaw := pem.EncodeToMemory(&pem.Block{ + Type: "PUBLIC KEY", + Bytes: publicKeyAsnBytes, + }) + + s.privateKey = privateKey + s.publicKeyPemRaw = publicKeyPemRaw + return nil +} + +func (s *inMemorySessionKeySupplier) PrivateKey() *rsa.PrivateKey { + if s.privateKey == nil { + return nil + } + + c := *s.privateKey + return &c +} + +func (s *inMemorySessionKeySupplier) PublicKeyPemRaw() []byte { + if s.publicKeyPemRaw == nil { + return nil + } + + c := make([]byte, len(s.publicKeyPemRaw)) + copy(c, s.publicKeyPemRaw) + return c +} + +type securityToken interface { + fmt.Stringer + Valid() bool +} + +type instancePrincipalToken struct { + tokenString string + jwtToken *jwtToken +} + +func newInstancePrincipalToken(tokenString string) (newToken securityToken, err error) { + var jwtToken *jwtToken + if jwtToken, err = parseJwt(tokenString); err != nil { + return nil, fmt.Errorf("failed to parse the token string \"%s\": %s", tokenString, err.Error()) + } + return &instancePrincipalToken{tokenString, jwtToken}, nil +} + +func (t *instancePrincipalToken) String() string { + return t.tokenString +} + +func (t *instancePrincipalToken) Valid() bool { + return !t.jwtToken.expired() +} diff --git a/vendor/github.com/oracle/oci-go-sdk/common/auth/instance_principal_key_provider.go b/vendor/github.com/oracle/oci-go-sdk/common/auth/instance_principal_key_provider.go new file mode 100644 index 000000000..b0f329e32 --- /dev/null +++ b/vendor/github.com/oracle/oci-go-sdk/common/auth/instance_principal_key_provider.go @@ -0,0 +1,100 @@ +// Copyright (c) 2016, 2018, Oracle and/or its affiliates. All rights reserved. + +package auth + +import ( + "bytes" + "crypto/rsa" + "fmt" + "github.com/oracle/oci-go-sdk/common" +) + +const ( + regionURL = `http://169.254.169.254/opc/v1/instance/region` + leafCertificateURL = `http://169.254.169.254/opc/v1/identity/cert.pem` + leafCertificateKeyURL = `http://169.254.169.254/opc/v1/identity/key.pem` + leafCertificateKeyPassphrase = `` // No passphrase for the private key for Compute instances + intermediateCertificateURL = `http://169.254.169.254/opc/v1/identity/intermediate.pem` + intermediateCertificateKeyURL = `` + intermediateCertificateKeyPassphrase = `` // No passphrase for the private key for Compute instances +) + +// instancePrincipalKeyProvider implements KeyProvider to provide a key ID and its corresponding private key +// for an instance principal by getting a security token via x509FederationClient. +// +// The region name of the endpoint for x509FederationClient is obtained from the metadata service on the compute +// instance. +type instancePrincipalKeyProvider struct { + regionForFederationClient common.Region + federationClient federationClient + tenancyID string +} + +// newInstancePrincipalKeyProvider creates and returns an instancePrincipalKeyProvider instance based on +// x509FederationClient. +// +// NOTE: There is a race condition between PrivateRSAKey() and KeyID(). These two pieces are tightly coupled; KeyID +// includes a security token obtained from Auth service by giving a public key which is paired with PrivateRSAKey. +// The x509FederationClient caches the security token in memory until it is expired. Thus, even if a client obtains a +// KeyID that is not expired at the moment, the PrivateRSAKey that the client acquires at a next moment could be +// invalid because the KeyID could be already expired. +func newInstancePrincipalKeyProvider() (provider *instancePrincipalKeyProvider, err error) { + var region common.Region + if region, err = getRegionForFederationClient(regionURL); err != nil { + err = fmt.Errorf("failed to get the region name from %s: %s", regionURL, err.Error()) + common.Logln(err) + return nil, err + } + + leafCertificateRetriever := newURLBasedX509CertificateRetriever( + leafCertificateURL, leafCertificateKeyURL, leafCertificateKeyPassphrase) + intermediateCertificateRetrievers := []x509CertificateRetriever{ + newURLBasedX509CertificateRetriever( + intermediateCertificateURL, intermediateCertificateKeyURL, intermediateCertificateKeyPassphrase), + } + + if err = leafCertificateRetriever.Refresh(); err != nil { + err = fmt.Errorf("failed to refresh the leaf certificate: %s", err.Error()) + return nil, err + } + tenancyID := extractTenancyIDFromCertificate(leafCertificateRetriever.Certificate()) + + federationClient := newX509FederationClient( + region, tenancyID, leafCertificateRetriever, intermediateCertificateRetrievers) + + provider = &instancePrincipalKeyProvider{regionForFederationClient: region, federationClient: federationClient, tenancyID: tenancyID} + return +} + +func getRegionForFederationClient(url string) (r common.Region, err error) { + var body bytes.Buffer + if body, err = httpGet(url); err != nil { + return + } + return common.StringToRegion(body.String()), nil +} + +func (p *instancePrincipalKeyProvider) RegionForFederationClient() common.Region { + return p.regionForFederationClient +} + +func (p *instancePrincipalKeyProvider) PrivateRSAKey() (privateKey *rsa.PrivateKey, err error) { + if privateKey, err = p.federationClient.PrivateKey(); err != nil { + err = fmt.Errorf("failed to get private key: %s", err.Error()) + return nil, err + } + return privateKey, nil +} + +func (p *instancePrincipalKeyProvider) KeyID() (string, error) { + var securityToken string + var err error + if securityToken, err = p.federationClient.SecurityToken(); err != nil { + return "", fmt.Errorf("failed to get security token: %s", err.Error()) + } + return fmt.Sprintf("ST$%s", securityToken), nil +} + +func (p *instancePrincipalKeyProvider) TenancyOCID() (string, error) { + return p.tenancyID, nil +} diff --git a/vendor/github.com/oracle/oci-go-sdk/common/auth/jwt.go b/vendor/github.com/oracle/oci-go-sdk/common/auth/jwt.go new file mode 100644 index 000000000..b199a4d68 --- /dev/null +++ b/vendor/github.com/oracle/oci-go-sdk/common/auth/jwt.go @@ -0,0 +1,61 @@ +// Copyright (c) 2016, 2018, Oracle and/or its affiliates. All rights reserved. + +package auth + +import ( + "bytes" + "encoding/base64" + "encoding/json" + "fmt" + "strings" + "time" +) + +type jwtToken struct { + raw string + header map[string]interface{} + payload map[string]interface{} +} + +func (t *jwtToken) expired() bool { + exp := int64(t.payload["exp"].(float64)) + return exp <= time.Now().Unix() +} + +func parseJwt(tokenString string) (*jwtToken, error) { + parts := strings.Split(tokenString, ".") + if len(parts) != 3 { + return nil, fmt.Errorf("the given token string contains an invalid number of parts") + } + + token := &jwtToken{raw: tokenString} + var err error + + // Parse Header part + var headerBytes []byte + if headerBytes, err = decodePart(parts[0]); err != nil { + return nil, fmt.Errorf("failed to decode the header bytes: %s", err.Error()) + } + if err = json.Unmarshal(headerBytes, &token.header); err != nil { + return nil, err + } + + // Parse Payload part + var payloadBytes []byte + if payloadBytes, err = decodePart(parts[1]); err != nil { + return nil, fmt.Errorf("failed to decode the payload bytes: %s", err.Error()) + } + decoder := json.NewDecoder(bytes.NewBuffer(payloadBytes)) + if err = decoder.Decode(&token.payload); err != nil { + return nil, fmt.Errorf("failed to decode the payload json: %s", err.Error()) + } + + return token, nil +} + +func decodePart(partString string) ([]byte, error) { + if l := len(partString) % 4; 0 < l { + partString += strings.Repeat("=", 4-l) + } + return base64.URLEncoding.DecodeString(partString) +} diff --git a/vendor/github.com/oracle/oci-go-sdk/common/auth/utils.go b/vendor/github.com/oracle/oci-go-sdk/common/auth/utils.go new file mode 100644 index 000000000..f84490c7d --- /dev/null +++ b/vendor/github.com/oracle/oci-go-sdk/common/auth/utils.go @@ -0,0 +1,64 @@ +// Copyright (c) 2016, 2018, Oracle and/or its affiliates. All rights reserved. + +package auth + +import ( + "bytes" + "crypto/sha1" + "crypto/x509" + "fmt" + "github.com/oracle/oci-go-sdk/common" + "net/http" + "net/http/httputil" + "strings" +) + +// httpGet makes a simple HTTP GET request to the given URL, expecting only "200 OK" status code. +// This is basically for the Instance Metadata Service. +func httpGet(url string) (body bytes.Buffer, err error) { + var response *http.Response + if response, err = http.Get(url); err != nil { + return + } + + common.IfDebug(func() { + if dump, e := httputil.DumpResponse(response, true); e == nil { + common.Logf("Dump Response %v", string(dump)) + } else { + common.Debugln(e) + } + }) + + defer response.Body.Close() + if _, err = body.ReadFrom(response.Body); err != nil { + return + } + + if response.StatusCode != http.StatusOK { + err = fmt.Errorf("HTTP Get failed: URL: %s, Status: %s, Message: %s", + url, response.Status, body.String()) + return + } + + return +} + +func extractTenancyIDFromCertificate(cert *x509.Certificate) string { + for _, nameAttr := range cert.Subject.Names { + value := nameAttr.Value.(string) + if strings.HasPrefix(value, "opc-tenant:") { + return value[len("opc-tenant:"):] + } + } + return "" +} + +func fingerprint(certificate *x509.Certificate) string { + fingerprint := sha1.Sum(certificate.Raw) + return colonSeparatedString(fingerprint) +} + +func colonSeparatedString(fingerprint [sha1.Size]byte) string { + spaceSeparated := fmt.Sprintf("% x", fingerprint) + return strings.Replace(spaceSeparated, " ", ":", -1) +} diff --git a/vendor/modules.txt b/vendor/modules.txt index 42b1a7e22..dbd0648ac 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -484,6 +484,7 @@ github.com/nu7hatch/gouuid github.com/olekukonko/tablewriter # github.com/oracle/oci-go-sdk v1.8.0 github.com/oracle/oci-go-sdk/common +github.com/oracle/oci-go-sdk/common/auth github.com/oracle/oci-go-sdk/core # github.com/outscale/osc-go v0.0.1 github.com/outscale/osc-go/oapi