github.com/juju/juju@v0.0.0-20240430160146-1752b71fcf00/provider/lxd/credentials.go (about)

     1  // Copyright 2016 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package lxd
     5  
     6  import (
     7  	"crypto/x509"
     8  	"encoding/base64"
     9  	"fmt"
    10  	"io"
    11  	"net/url"
    12  	"os"
    13  	"path/filepath"
    14  	"runtime"
    15  
    16  	"github.com/canonical/lxd/shared/api"
    17  	"github.com/juju/errors"
    18  	"github.com/juju/utils/v3"
    19  
    20  	"github.com/juju/juju/cloud"
    21  	"github.com/juju/juju/container/lxd"
    22  	"github.com/juju/juju/environs"
    23  	environscloudspec "github.com/juju/juju/environs/cloudspec"
    24  	"github.com/juju/juju/juju/osenv"
    25  	"github.com/juju/juju/pki"
    26  	pkiassertion "github.com/juju/juju/pki/assertion"
    27  	"github.com/juju/juju/provider/lxd/lxdnames"
    28  )
    29  
    30  const (
    31  	credAttrServerCert    = "server-cert"
    32  	credAttrClientCert    = "client-cert"
    33  	credAttrClientKey     = "client-key"
    34  	credAttrTrustPassword = "trust-password"
    35  )
    36  
    37  // CertificateReadWriter groups methods that is required to read and write
    38  // certificates at a given path.
    39  type CertificateReadWriter interface {
    40  	// Read takes a path and returns both a cert and key PEM.
    41  	// Returns an error if there was an issue reading the certs.
    42  	Read(path string) (certPEM, keyPEM []byte, err error)
    43  
    44  	// Write takes a path and cert, key PEM and stores them.
    45  	// Returns an error if there was an issue writing the certs.
    46  	Write(path string, certPEM, keyPEM []byte) error
    47  }
    48  
    49  // CertificateGenerator groups methods for generating a new certificate
    50  type CertificateGenerator interface {
    51  	// Generate creates client or server certificate and key pair,
    52  	// returning them as byte arrays in memory.
    53  	Generate(client bool, addHosts bool) (certPEM, keyPEM []byte, err error)
    54  }
    55  
    56  // environProviderCredentials implements environs.ProviderCredentials.
    57  type environProviderCredentials struct {
    58  	certReadWriter  CertificateReadWriter
    59  	certGenerator   CertificateGenerator
    60  	serverFactory   ServerFactory
    61  	lxcConfigReader LXCConfigReader
    62  }
    63  
    64  // CredentialSchemas is part of the environs.ProviderCredentials interface.
    65  func (environProviderCredentials) CredentialSchemas() map[cloud.AuthType]cloud.CredentialSchema {
    66  	return map[cloud.AuthType]cloud.CredentialSchema{
    67  		cloud.CertificateAuthType: {
    68  			{
    69  				Name: credAttrServerCert,
    70  				CredentialAttr: cloud.CredentialAttr{
    71  					Description:    "the path to the PEM-encoded LXD server certificate file",
    72  					ExpandFilePath: true,
    73  					Hidden:         true,
    74  				},
    75  			}, {
    76  				Name: credAttrClientCert,
    77  				CredentialAttr: cloud.CredentialAttr{
    78  					Description:    "the path to the PEM-encoded LXD client certificate file",
    79  					ExpandFilePath: true,
    80  					Hidden:         true,
    81  				},
    82  			}, {
    83  				Name: credAttrClientKey,
    84  				CredentialAttr: cloud.CredentialAttr{
    85  					Description:    "the path to the PEM-encoded LXD client key file",
    86  					ExpandFilePath: true,
    87  					Hidden:         true,
    88  				},
    89  			},
    90  		},
    91  		cloud.InteractiveAuthType: {
    92  			{
    93  				Name: credAttrTrustPassword,
    94  				CredentialAttr: cloud.CredentialAttr{
    95  					Description: "the LXD server trust password",
    96  					Hidden:      true,
    97  				},
    98  			},
    99  		},
   100  	}
   101  }
   102  
   103  // RegisterCredentials is part of the environs.ProviderCredentialsRegister interface.
   104  func (p environProviderCredentials) RegisterCredentials(cld cloud.Cloud) (map[string]*cloud.CloudCredential, error) {
   105  	// only register credentials if the operator is attempting to access "lxd"
   106  	// or "localhost"
   107  	cloudName := cld.Name
   108  	if !lxdnames.IsDefaultCloud(cloudName) {
   109  		return make(map[string]*cloud.CloudCredential), nil
   110  	}
   111  
   112  	nopLogf := func(msg string, args ...interface{}) {}
   113  	certPEM, keyPEM, err := p.readOrGenerateCert(nopLogf)
   114  	if err != nil {
   115  		return nil, errors.Trace(err)
   116  	}
   117  
   118  	localCertCredential, err := p.detectLocalCredentials(certPEM, keyPEM)
   119  	if err != nil {
   120  		return nil, errors.Trace(err)
   121  	}
   122  
   123  	return map[string]*cloud.CloudCredential{
   124  		cloudName: {
   125  			DefaultCredential: cloudName,
   126  			AuthCredentials: map[string]cloud.Credential{
   127  				cloudName: *localCertCredential,
   128  			},
   129  		},
   130  	}, nil
   131  }
   132  
   133  // DetectCredentials is part of the environs.ProviderCredentials interface.
   134  func (p environProviderCredentials) DetectCredentials(cloudName string) (*cloud.CloudCredential, error) {
   135  	nopLogf := func(msg string, args ...interface{}) {}
   136  	certPEM, keyPEM, err := p.readOrGenerateCert(nopLogf)
   137  	if err != nil {
   138  		return nil, errors.Trace(err)
   139  	}
   140  
   141  	remoteCertCredentials, err := p.detectRemoteCredentials()
   142  	if err != nil {
   143  		logger.Debugf("unable to detect remote LXC credentials: %s", err)
   144  	}
   145  
   146  	// If the cloud is built-in, we can start a local server to
   147  	// finalise the credential over the LXD Unix docket.
   148  	var localCertCredentials *cloud.Credential
   149  	if cloudName == "" || lxdnames.IsDefaultCloud(cloudName) {
   150  		if localCertCredentials, err = p.detectLocalCredentials(certPEM, keyPEM); err != nil {
   151  			logger.Debugf("unable to detect local LXC credentials: %s", err)
   152  		}
   153  	}
   154  
   155  	authCredentials := make(map[string]cloud.Credential)
   156  	for k, v := range remoteCertCredentials {
   157  		if cloudName != "" && k != cloudName {
   158  			continue
   159  		}
   160  		authCredentials[k] = v
   161  	}
   162  	if localCertCredentials != nil {
   163  		if cloudName == "" || lxdnames.IsDefaultCloud(cloudName) {
   164  			authCredentials[lxdnames.DefaultCloud] = *localCertCredentials
   165  		}
   166  	}
   167  	return &cloud.CloudCredential{
   168  		AuthCredentials: authCredentials,
   169  	}, nil
   170  }
   171  
   172  // detectLocalCredentials will use the local server to read and finalize the
   173  // cloud credentials.
   174  func (p environProviderCredentials) detectLocalCredentials(certPEM, keyPEM []byte) (*cloud.Credential, error) {
   175  	svr, err := p.serverFactory.LocalServer()
   176  	if err != nil {
   177  		return nil, errors.NewNotFound(err, "failed to connect to local LXD")
   178  	}
   179  
   180  	label := fmt.Sprintf("LXD credential %q", lxdnames.DefaultCloud)
   181  	certCredential, err := p.finalizeLocalCredential(
   182  		io.Discard, svr, string(certPEM), string(keyPEM), label,
   183  	)
   184  	return certCredential, errors.Trace(err)
   185  }
   186  
   187  // detectRemoteCredentials will attempt to gather all the potential existing
   188  // remote lxc configurations found in `$HOME/.config/lxc/.config` file.
   189  // Any setups found in the configuration will then be returned as a credential
   190  // that can be automatically loaded into juju.
   191  func (p environProviderCredentials) detectRemoteCredentials() (map[string]cloud.Credential, error) {
   192  	credentials := make(map[string]cloud.Credential)
   193  	for _, configDir := range configDirs() {
   194  		configPath := filepath.Join(configDir, "config.yml")
   195  		config, err := p.lxcConfigReader.ReadConfig(configPath)
   196  		if err != nil {
   197  			return nil, errors.Trace(err)
   198  		}
   199  
   200  		if len(config.Remotes) == 0 {
   201  			continue
   202  		}
   203  
   204  		certPEM, keyPEM, err := p.certReadWriter.Read(configDir)
   205  		if err != nil && !os.IsNotExist(errors.Cause(err)) {
   206  			return nil, errors.Trace(err)
   207  		} else if os.IsNotExist(err) {
   208  			continue
   209  		}
   210  
   211  		for name, remote := range config.Remotes {
   212  			if remote.Protocol != lxdnames.ProviderType {
   213  				continue
   214  			}
   215  			certPath := filepath.Join(configDir, "servercerts", fmt.Sprintf("%s.crt", name))
   216  			authConfig := map[string]string{
   217  				credAttrClientCert: string(certPEM),
   218  				credAttrClientKey:  string(keyPEM),
   219  			}
   220  			serverCert, err := p.lxcConfigReader.ReadCert(certPath)
   221  			if err != nil {
   222  				if !os.IsNotExist(errors.Cause(err)) {
   223  					logger.Errorf("unable to read certificate from %s with error %s", certPath, err)
   224  					continue
   225  				}
   226  			} else {
   227  				authConfig[credAttrServerCert] = string(serverCert)
   228  			}
   229  			credential := cloud.NewCredential(cloud.CertificateAuthType, authConfig)
   230  			credential.Label = fmt.Sprintf("LXD credential %q", name)
   231  			credentials[name] = credential
   232  		}
   233  	}
   234  	return credentials, nil
   235  }
   236  
   237  func (p environProviderCredentials) readOrGenerateCert(logf func(string, ...interface{})) (certPEM, keyPEM []byte, _ error) {
   238  	for _, dir := range configDirs() {
   239  		certPEM, keyPEM, err := p.certReadWriter.Read(dir)
   240  		if err == nil {
   241  			logf("Loaded client cert/key from %q", dir)
   242  			return certPEM, keyPEM, nil
   243  		} else if !os.IsNotExist(errors.Cause(err)) {
   244  			return nil, nil, errors.Trace(err)
   245  		}
   246  	}
   247  
   248  	// No certs were found, so generate one and cache it in the
   249  	// Juju XDG_DATA dir. We cache the certificate so that we
   250  	// avoid uploading a new certificate each time we bootstrap.
   251  	jujuLXDDir := osenv.JujuXDGDataHomePath("lxd")
   252  	certPEM, keyPEM, err := p.certGenerator.Generate(true, true)
   253  	if err != nil {
   254  		return nil, nil, errors.Trace(err)
   255  	}
   256  	if err := p.certReadWriter.Write(jujuLXDDir, certPEM, keyPEM); err != nil {
   257  		return nil, nil, errors.Trace(err)
   258  	}
   259  	logf("Generating client cert/key in %q", jujuLXDDir)
   260  	return certPEM, keyPEM, nil
   261  }
   262  
   263  // ShouldFinalizeCredential is part of the environs.RequestFinalizeCredential
   264  // interface.
   265  // This is an optional interface to check if the server certificate has not
   266  // been filled in.
   267  func (p environProviderCredentials) ShouldFinalizeCredential(cred cloud.Credential) bool {
   268  	// We should always finalize the credentials  so we can perform some sanity
   269  	// checking on them.
   270  	return true
   271  }
   272  
   273  // FinalizeCredential is part of the environs.ProviderCredentials interface.
   274  func (p environProviderCredentials) FinalizeCredential(
   275  	ctx environs.FinalizeCredentialContext,
   276  	args environs.FinalizeCredentialParams,
   277  ) (*cloud.Credential, error) {
   278  	switch authType := args.Credential.AuthType(); authType {
   279  	case cloud.InteractiveAuthType:
   280  		credAttrs := args.Credential.Attributes()
   281  		// We don't care if the password is empty, just that it exists. Empty
   282  		// passwords can be valid ones...
   283  		if _, ok := credAttrs[credAttrTrustPassword]; ok {
   284  			// check to see if the client cert, keys exist, if they do not,
   285  			// generate them for the user.
   286  			if _, ok := getClientCertificates(args.Credential); !ok {
   287  				stderr := ctx.GetStderr()
   288  				nopLogf := func(s string, args ...interface{}) {
   289  					fmt.Fprintf(stderr, s+"\n", args...)
   290  				}
   291  				clientCert, clientKey, err := p.readOrGenerateCert(nopLogf)
   292  				if err != nil {
   293  					return nil, err
   294  				}
   295  
   296  				credAttrs[credAttrClientCert] = string(clientCert)
   297  				credAttrs[credAttrClientKey] = string(clientKey)
   298  
   299  				credential := cloud.NewCredential(cloud.CertificateAuthType, credAttrs)
   300  				credential.Label = args.Credential.Label
   301  
   302  				args.Credential = credential
   303  			}
   304  		}
   305  		fallthrough
   306  	case cloud.CertificateAuthType:
   307  		return p.finalizeCredential(ctx, args)
   308  	default:
   309  		return &args.Credential, nil
   310  	}
   311  }
   312  
   313  // validateServerCertificate validates the server certificate we have
   314  // been supplied with to make sure that it meets our expectations. Checks
   315  // involve making sure there is a certificate contained in the pem data and that
   316  // certificate can be used for server authentication.
   317  //
   318  // NOTE (tlm): This was added to help users who have potentially made a mistake
   319  // in their credentials.yaml file when calling juju add-credential. The case
   320  // that prompted this function was mixing the client certificate with the server
   321  // certificate.
   322  func validateServerCertificate(serverPemCertificate string) error {
   323  	certs, _, err := pki.UnmarshalPemData([]byte(serverPemCertificate))
   324  	if err != nil {
   325  		return fmt.Errorf("parsing LXD server certificate as PEM: %w", err)
   326  	}
   327  
   328  	if len(certs) == 0 {
   329  		return errors.NotFoundf("LXD server certificate")
   330  	}
   331  
   332  	// We only care about the first certificate here as any more cert's in the
   333  	// file are considered to form the rest of the supporting chain.
   334  	if !pkiassertion.HasExtKeyUsage(certs[0], x509.ExtKeyUsageServerAuth) {
   335  		return errors.New("certificate from LXD server certificate file is not signed for server authentication")
   336  	}
   337  
   338  	return nil
   339  }
   340  
   341  func (p environProviderCredentials) finalizeCredential(
   342  	ctx environs.FinalizeCredentialContext,
   343  	args environs.FinalizeCredentialParams,
   344  ) (*cloud.Credential, error) {
   345  	// Credential detection yields a partial certificate containing just
   346  	// the client certificate and key. We check if we have a partial
   347  	// credential, and fill in the server certificate if we can.
   348  	stderr := ctx.GetStderr()
   349  	credAttrs := args.Credential.Attributes()
   350  
   351  	certPEM := credAttrs[credAttrClientCert]
   352  	keyPEM := credAttrs[credAttrClientKey]
   353  	if certPEM == "" {
   354  		return nil, errors.NotValidf("missing or empty %q attribute", credAttrClientCert)
   355  	}
   356  	if keyPEM == "" {
   357  		return nil, errors.NotValidf("missing or empty %q attribute", credAttrClientKey)
   358  	}
   359  
   360  	// If the cloud is built-in, the endpoint will be empty
   361  	// and we can start a local server to finalise the credential
   362  	// over the LXD Unix docket.
   363  	if args.CloudEndpoint == "" {
   364  		svr, err := p.serverFactory.LocalServer()
   365  		if err != nil {
   366  			return nil, errors.Trace(err)
   367  		}
   368  		cred, err := p.finalizeLocalCredential(
   369  			stderr, svr, certPEM, keyPEM,
   370  			args.Credential.Label,
   371  		)
   372  		return cred, errors.Trace(err)
   373  	}
   374  
   375  	if v, ok := credAttrs[credAttrServerCert]; ok && v != "" {
   376  		return &args.Credential, validateServerCertificate(v)
   377  	}
   378  
   379  	// We're not local, so set up the remote server and automate the remote
   380  	// certificate credentials.
   381  	return p.finalizeRemoteCredential(
   382  		stderr,
   383  		args.CloudEndpoint,
   384  		args.Credential,
   385  	)
   386  }
   387  
   388  func (p environProviderCredentials) finalizeRemoteCredential(
   389  	output io.Writer,
   390  	endpoint string,
   391  	credentials cloud.Credential,
   392  ) (*cloud.Credential, error) {
   393  	clientCert, ok := getClientCertificates(credentials)
   394  	if !ok {
   395  		return nil, errors.NotFoundf("client credentials")
   396  	}
   397  	if err := clientCert.Validate(); err != nil {
   398  		return nil, errors.Annotate(err, "client credentials")
   399  	}
   400  
   401  	credAttrs := credentials.Attributes()
   402  	trustPassword, ok := credAttrs[credAttrTrustPassword]
   403  	if !ok {
   404  		return nil, errors.NotValidf("missing %q attribute", credAttrTrustPassword)
   405  	}
   406  
   407  	insecureCreds := cloud.NewCredential(cloud.CertificateAuthType, credAttrs)
   408  	server, err := p.serverFactory.InsecureRemoteServer(CloudSpec{
   409  		CloudSpec: environscloudspec.CloudSpec{
   410  			Endpoint:   endpoint,
   411  			Credential: &insecureCreds,
   412  		},
   413  	})
   414  	if err != nil {
   415  		return nil, errors.Trace(err)
   416  	}
   417  
   418  	clientX509Cert, err := clientCert.X509()
   419  	if err != nil {
   420  		return nil, errors.Annotate(err, "client credentials")
   421  	}
   422  
   423  	// check to see if the cert already exists
   424  	fingerprint, err := clientCert.Fingerprint()
   425  	if err != nil {
   426  		return nil, errors.Trace(err)
   427  	}
   428  
   429  	cert, _, err := server.GetCertificate(fingerprint)
   430  	if err != nil || cert == nil {
   431  		if err := server.CreateCertificate(api.CertificatesPost{
   432  			CertificatePut: api.CertificatePut{
   433  				Name:        credentials.Label,
   434  				Type:        "client",
   435  				Certificate: base64.StdEncoding.EncodeToString(clientX509Cert.Raw),
   436  			},
   437  			Password: trustPassword,
   438  		}); err != nil {
   439  			return nil, errors.Trace(err)
   440  		}
   441  		_, _ = fmt.Fprintln(output, "Uploaded certificate to LXD server.")
   442  	} else {
   443  		_, _ = fmt.Fprintln(output, "Reusing certificate from LXD server.")
   444  	}
   445  
   446  	lxdServer, _, err := server.GetServer()
   447  	if err != nil {
   448  		return nil, errors.Trace(err)
   449  	}
   450  	lxdServerCert := lxdServer.Environment.Certificate
   451  
   452  	// request to make sure that we can actually query correctly in a secure
   453  	// manner.
   454  	attributes := make(map[string]string)
   455  	for k, v := range credAttrs {
   456  		if k == credAttrTrustPassword {
   457  			continue
   458  		}
   459  		attributes[k] = v
   460  	}
   461  	attributes[credAttrServerCert] = lxdServerCert
   462  
   463  	secureCreds := cloud.NewCredential(cloud.CertificateAuthType, attributes)
   464  	server, err = p.serverFactory.RemoteServer(CloudSpec{
   465  		CloudSpec: environscloudspec.CloudSpec{
   466  			Endpoint:   endpoint,
   467  			Credential: &secureCreds,
   468  		},
   469  	})
   470  	if err != nil {
   471  		return nil, errors.Trace(err)
   472  	}
   473  
   474  	// Store the server's certificate in the credential.
   475  	out := cloud.NewCredential(cloud.CertificateAuthType, map[string]string{
   476  		credAttrClientCert: string(clientCert.CertPEM),
   477  		credAttrClientKey:  string(clientCert.KeyPEM),
   478  		credAttrServerCert: server.ServerCertificate(),
   479  	})
   480  	out.Label = credentials.Label
   481  	return &out, nil
   482  }
   483  
   484  func (p environProviderCredentials) finalizeLocalCredential(
   485  	output io.Writer,
   486  	svr Server,
   487  	certPEM, keyPEM, label string,
   488  ) (*cloud.Credential, error) {
   489  
   490  	// Upload the certificate to the server if necessary.
   491  	clientCert := &lxd.Certificate{
   492  		Name:    "juju",
   493  		CertPEM: []byte(certPEM),
   494  		KeyPEM:  []byte(keyPEM),
   495  	}
   496  	fingerprint, err := clientCert.Fingerprint()
   497  	if err != nil {
   498  		return nil, errors.Trace(err)
   499  	}
   500  	if _, _, err := svr.GetCertificate(fingerprint); lxd.IsLXDNotFound(err) {
   501  		if addCertErr := svr.CreateClientCertificate(clientCert); addCertErr != nil {
   502  			// There is no specific error code returned when
   503  			// attempting to add a certificate that already
   504  			// exists in the database. We can just check
   505  			// again to see if another process added the
   506  			// certificate concurrently with us checking the
   507  			// first time.
   508  			if _, _, err := svr.GetCertificate(fingerprint); lxd.IsLXDNotFound(err) {
   509  				// The cert still isn't there, so report the AddCert error.
   510  				return nil, errors.Annotatef(
   511  					addCertErr, "adding certificate %q", clientCert.Name,
   512  				)
   513  			} else if err != nil {
   514  				return nil, errors.Annotate(err, "querying certificates")
   515  			}
   516  			// The certificate is there now, which implies
   517  			// there was a concurrent AddCert by another
   518  			// process. Carry on.
   519  		}
   520  		fmt.Fprintln(output, "Uploaded certificate to LXD server.")
   521  
   522  	} else if err != nil {
   523  		return nil, errors.Annotate(err, "querying certificates")
   524  	}
   525  
   526  	// Store the server's certificate in the credential.
   527  	out := cloud.NewCredential(cloud.CertificateAuthType, map[string]string{
   528  		credAttrClientCert: certPEM,
   529  		credAttrClientKey:  keyPEM,
   530  		credAttrServerCert: svr.ServerCertificate(),
   531  	})
   532  	out.Label = label
   533  	return &out, nil
   534  }
   535  
   536  // certificateReadWriter is the default implementation for reading and writing
   537  // certificates to disk.
   538  type certificateReadWriter struct{}
   539  
   540  func (certificateReadWriter) Read(path string) ([]byte, []byte, error) {
   541  	clientCertPath := filepath.Join(path, "client.crt")
   542  	clientKeyPath := filepath.Join(path, "client.key")
   543  	certPEM, err := os.ReadFile(clientCertPath)
   544  	if err != nil {
   545  		return nil, nil, errors.Trace(err)
   546  	}
   547  	keyPEM, err := os.ReadFile(clientKeyPath)
   548  	if err != nil {
   549  		return nil, nil, errors.Trace(err)
   550  	}
   551  	return certPEM, keyPEM, nil
   552  }
   553  
   554  func (certificateReadWriter) Write(path string, certPEM, keyPEM []byte) error {
   555  	clientCertPath := filepath.Join(path, "client.crt")
   556  	clientKeyPath := filepath.Join(path, "client.key")
   557  	if err := os.MkdirAll(path, 0700); err != nil {
   558  		return errors.Trace(err)
   559  	}
   560  	if err := os.WriteFile(clientCertPath, certPEM, 0600); err != nil {
   561  		return errors.Trace(err)
   562  	}
   563  	if err := os.WriteFile(clientKeyPath, keyPEM, 0600); err != nil {
   564  		return errors.Trace(err)
   565  	}
   566  	return nil
   567  }
   568  
   569  // certificateGenerator is the default implementation for generating a
   570  // certificate if it's not found on disk.
   571  type certificateGenerator struct{}
   572  
   573  func (certificateGenerator) Generate(client bool, addHosts bool) (certPEM, keyPEM []byte, err error) {
   574  	return lxd.GenerateMemCert(client, addHosts)
   575  }
   576  
   577  func endpointURL(endpoint string) (*url.URL, error) {
   578  	remoteURL, err := url.Parse(endpoint)
   579  	if err != nil || remoteURL.Scheme == "" {
   580  		remoteURL = &url.URL{
   581  			Scheme: "https",
   582  			Host:   endpoint,
   583  		}
   584  	} else {
   585  		// If the user specifies an endpoint, it must be either
   586  		// host:port, or https://host:port. We do not support
   587  		// unix:// endpoints at present.
   588  		if remoteURL.Scheme != "https" {
   589  			return nil, errors.Errorf(
   590  				"invalid URL %q: only HTTPS is supported",
   591  				endpoint,
   592  			)
   593  		}
   594  	}
   595  	return remoteURL, nil
   596  }
   597  
   598  func getCertificates(credentials cloud.Credential) (client *lxd.Certificate, server string, ok bool) {
   599  	clientCert, ok := getClientCertificates(credentials)
   600  	if !ok {
   601  		return nil, "", false
   602  	}
   603  	credAttrs := credentials.Attributes()
   604  	serverCertPEM, ok := credAttrs[credAttrServerCert]
   605  	if !ok {
   606  		return nil, "", false
   607  	}
   608  	return clientCert, serverCertPEM, true
   609  }
   610  
   611  func getClientCertificates(credentials cloud.Credential) (client *lxd.Certificate, ok bool) {
   612  	credAttrs := credentials.Attributes()
   613  	clientCertPEM, ok := credAttrs[credAttrClientCert]
   614  	if !ok {
   615  		return nil, false
   616  	}
   617  	clientKeyPEM, ok := credAttrs[credAttrClientKey]
   618  	if !ok {
   619  		return nil, false
   620  	}
   621  	clientCert := &lxd.Certificate{
   622  		Name:    "juju",
   623  		CertPEM: []byte(clientCertPEM),
   624  		KeyPEM:  []byte(clientKeyPEM),
   625  	}
   626  	return clientCert, true
   627  }
   628  
   629  func configDirs() []string {
   630  	dirs := []string{
   631  		osenv.JujuXDGDataHomePath("lxd"),
   632  	}
   633  	if lxdConf := os.Getenv("LXD_CONF"); lxdConf != "" {
   634  		dirs = append(dirs, lxdConf)
   635  	}
   636  	dirs = append(dirs, filepath.Join(utils.Home(), ".config", "lxc"))
   637  	if runtime.GOOS == "linux" {
   638  		dirs = append(dirs, filepath.Join(utils.Home(), "snap", "lxd", "current", ".config", "lxc"))
   639  		dirs = append(dirs, filepath.Join(utils.Home(), "snap", "lxd", "common", "config"))
   640  	}
   641  	return dirs
   642  }