github.com/Venafi/vcert/v5@v5.10.2/pkg/venafi/firefly/connector.go (about)

     1  /*
     2   * Copyright 2023 Venafi, Inc.
     3   *
     4   * Licensed under the Apache License, Version 2.0 (the "License");
     5   * you may not use this file except in compliance with the License.
     6   * You may obtain a copy of the License at
     7   *
     8   *  http://www.apache.org/licenses/LICENSE-2.0
     9   *
    10   * Unless required by applicable law or agreed to in writing, software
    11   * distributed under the License is distributed on an "AS IS" BASIS,
    12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13   * See the License for the specific language governing permissions and
    14   * limitations under the License.
    15   */
    16  
    17  package firefly
    18  
    19  import (
    20  	"context"
    21  	"crypto/x509"
    22  	"errors"
    23  	"fmt"
    24  	"net/http"
    25  	"net/url"
    26  	"strings"
    27  	"time"
    28  
    29  	"github.com/sosodev/duration"
    30  	"go.uber.org/zap"
    31  	"golang.org/x/oauth2"
    32  	"golang.org/x/oauth2/clientcredentials"
    33  
    34  	"github.com/Venafi/vcert/v5/pkg/certificate"
    35  	"github.com/Venafi/vcert/v5/pkg/domain"
    36  	"github.com/Venafi/vcert/v5/pkg/endpoint"
    37  	"github.com/Venafi/vcert/v5/pkg/policy"
    38  	"github.com/Venafi/vcert/v5/pkg/util"
    39  	"github.com/Venafi/vcert/v5/pkg/venafi"
    40  	"github.com/Venafi/vcert/v5/pkg/verror"
    41  )
    42  
    43  var (
    44  	fieldPlatform = zap.String("platform", venafi.Firefly.String())
    45  )
    46  
    47  // Connector contains the base data needed to communicate with a Firefly Server
    48  type Connector struct {
    49  	baseURL     string
    50  	accessToken string
    51  	verbose     bool
    52  	trust       *x509.CertPool
    53  	client      *http.Client
    54  	zone        string // holds the policyName
    55  	userAgent   string
    56  }
    57  
    58  // NewConnector creates a new Firefly Connector object used to communicate with Firefly
    59  func NewConnector(url string, zone string, verbose bool, trust *x509.CertPool) (*Connector, error) {
    60  	if url != "" {
    61  		var err error
    62  		url, err = normalizeURL(url)
    63  		if err != nil {
    64  			return nil, fmt.Errorf("%w: failed to normalize URL: %v", verror.UserDataError, err)
    65  		}
    66  	}
    67  	return &Connector{baseURL: url, zone: zone, verbose: verbose, trust: trust, userAgent: util.DefaultUserAgent}, nil
    68  }
    69  
    70  // normalizeURL normalizes the base URL used to communicate with Firefly
    71  func normalizeURL(url string) (normalizedURL string, err error) {
    72  	normalizedURL = util.NormalizeUrl(url)
    73  	return normalizedURL, err
    74  }
    75  
    76  func (c *Connector) SetZone(zone string) {
    77  	//for now the zone refers to the policyName
    78  	c.zone = zone
    79  }
    80  
    81  func (c *Connector) SetUserAgent(userAgent string) {
    82  	c.userAgent = userAgent
    83  }
    84  
    85  func (c *Connector) GetType() endpoint.ConnectorType {
    86  	return endpoint.ConnectorTypeFirefly
    87  }
    88  
    89  func (c *Connector) Authenticate(auth *endpoint.Authentication) error {
    90  	if auth == nil {
    91  		msg := "failed to authenticate: no credentials provided"
    92  		zap.L().Error(msg, fieldPlatform)
    93  		return errors.New(msg)
    94  	}
    95  
    96  	if auth.AccessToken == "" {
    97  		zap.L().Info("no access token provided. Authorization needed", fieldPlatform)
    98  		var token *oauth2.Token
    99  		token, err := c.Authorize(auth)
   100  		if err != nil {
   101  			return err
   102  		}
   103  		auth.AccessToken = token.AccessToken
   104  	}
   105  
   106  	zap.L().Info("successfully authenticated", fieldPlatform)
   107  	//setting the accessToken to the connector
   108  	c.accessToken = auth.AccessToken
   109  	return nil
   110  }
   111  
   112  // Authorize Get an OAuth access token
   113  func (c *Connector) Authorize(auth *endpoint.Authentication) (token *oauth2.Token, err error) {
   114  	defer func() {
   115  		if err != nil {
   116  			err = fmt.Errorf("%w: %s", verror.AuthError, err)
   117  		}
   118  	}()
   119  
   120  	zap.L().Info("authorizing to OAuth2 server", fieldPlatform)
   121  
   122  	if auth == nil {
   123  		msg := "failed to authenticate: missing credentials"
   124  		zap.L().Error(msg, fieldPlatform)
   125  		return nil, errors.New(msg)
   126  	}
   127  
   128  	successMsg := "successfully authorized to OAuth2 server"
   129  	failureMsg := "authorization flow failed"
   130  
   131  	// if it's a client credentials flow grant
   132  	if auth.ClientSecret != "" && auth.IdentityProvider.DeviceURL == "" {
   133  		zap.L().Info("authorizing using credentials flow", fieldPlatform)
   134  
   135  		config := clientcredentials.Config{
   136  			ClientID:     auth.ClientId,
   137  			ClientSecret: auth.ClientSecret,
   138  			TokenURL:     auth.IdentityProvider.TokenURL,
   139  			Scopes:       strings.Split(auth.Scope, scopesSeparator),
   140  		}
   141  		//if the audience was provided, then it's required to set it to the config.
   142  		if auth.IdentityProvider.Audience != "" {
   143  			config.EndpointParams = url.Values{
   144  				"audience": []string{auth.IdentityProvider.Audience},
   145  			}
   146  		}
   147  
   148  		token, err = config.Token(context.Background())
   149  		if err != nil {
   150  			zap.L().Error(failureMsg, fieldPlatform, zap.Error(err))
   151  			return token, err
   152  		}
   153  
   154  		zap.L().Info(successMsg, fieldPlatform)
   155  		return
   156  	}
   157  
   158  	// if it's a password flow grant
   159  	if auth.User != "" && auth.Password != "" {
   160  		zap.L().Info("authorizing using password flow", fieldPlatform)
   161  
   162  		config := oauth2.Config{
   163  			ClientID:     auth.ClientId,
   164  			ClientSecret: auth.ClientSecret,
   165  			Scopes:       strings.Split(auth.Scope, scopesSeparator),
   166  			//RedirectURL:  "http://localhost:9094/oauth2",
   167  			// This points to our Authorization Server
   168  			// if our Client ID and Client Secret are valid
   169  			// it will attempt to authorize our user
   170  			Endpoint: oauth2.Endpoint{
   171  				//AuthURL:  "http://localhost:9096/authorize",
   172  				TokenURL: auth.IdentityProvider.TokenURL,
   173  			},
   174  		}
   175  
   176  		token, err = config.PasswordCredentialsToken(context.Background(), auth.User, auth.Password)
   177  		if err != nil {
   178  			zap.L().Error(failureMsg, fieldPlatform, zap.Error(err))
   179  			return token, err
   180  		}
   181  
   182  		zap.L().Info(successMsg, fieldPlatform)
   183  		return
   184  	}
   185  
   186  	// if it's a device flow grant
   187  	if auth.IdentityProvider.DeviceURL != "" {
   188  		zap.L().Info("authorizing using device flow", fieldPlatform)
   189  
   190  		token, err = c.getDeviceAccessToken(auth)
   191  		if err != nil {
   192  			zap.L().Error(failureMsg, fieldPlatform, zap.Error(err))
   193  			return token, err
   194  		}
   195  
   196  		zap.L().Info(successMsg, fieldPlatform)
   197  		return
   198  	}
   199  
   200  	errMsg := "authorization failed: cannot determine the authorization flow required for the credentials provided"
   201  	zap.L().Error(errMsg, fieldPlatform)
   202  	return token, errors.New(errMsg)
   203  }
   204  
   205  // SynchronousRequestCertificate It's not supported yet in VaaS
   206  func (c *Connector) SynchronousRequestCertificate(req *certificate.Request) (certificates *certificate.PEMCollection, err error) {
   207  
   208  	zap.L().Info("requesting certificate", zap.String("cn", req.Subject.CommonName), fieldPlatform)
   209  	//creating the request object
   210  	certReq, err := c.getCertificateRequest(req)
   211  	if err != nil {
   212  		zap.L().Error("HTTP request failed", fieldPlatform, zap.Error(err))
   213  		return nil, err
   214  	}
   215  
   216  	zap.L().Info("sending HTTP request", fieldPlatform)
   217  	statusCode, status, body, err := c.request("POST", c.getCertificateRequestUrl(req), certReq)
   218  	if err != nil {
   219  		zap.L().Error("HTTP request failed", fieldPlatform, zap.Error(err))
   220  		return nil, err
   221  	}
   222  
   223  	//parsing the result
   224  	cr, err := parseCertificateRequestResult(statusCode, status, body)
   225  	if err != nil {
   226  		zap.L().Error("failed to request a certificate", fieldPlatform, zap.Error(err))
   227  		return nil, err
   228  	}
   229  
   230  	//converting to PEMCollection
   231  	certificates, err = certificate.PEMCollectionFromBytes([]byte(cr.CertificateChain), req.ChainOption)
   232  	if err != nil {
   233  		zap.L().Error("failed to create pem collection", fieldPlatform, zap.Error(err))
   234  		return nil, err
   235  	}
   236  
   237  	certificates.PrivateKey = cr.PrivateKey
   238  	zap.L().Info("successfully requested certificate", fieldPlatform)
   239  	return certificates, nil
   240  }
   241  
   242  func (c *Connector) getCertificateRequest(req *certificate.Request) (*certificateRequest, error) {
   243  	zap.L().Info("building certificate request", fieldPlatform)
   244  	fireflyCertRequest := &certificateRequest{}
   245  
   246  	if req.CsrOrigin == certificate.UserProvidedCSR {
   247  		fireflyCertRequest.CSR = string(req.GetCSR())
   248  	} else { // it's considered as a ServiceGeneratedCSR
   249  		//getting the subject
   250  		subject := Subject{
   251  			CommonName: req.Subject.CommonName,
   252  		}
   253  
   254  		if len(req.Subject.Organization) > 0 {
   255  			subject.Organization = req.Subject.Organization[0]
   256  		}
   257  
   258  		if len(req.Subject.OrganizationalUnit) > 0 {
   259  			subject.OrgUnits = req.Subject.OrganizationalUnit
   260  		}
   261  
   262  		if len(req.Subject.Locality) > 0 {
   263  			subject.Locality = req.Subject.Locality[0]
   264  		}
   265  
   266  		if len(req.Subject.Province) > 0 {
   267  			subject.State = req.Subject.Province[0]
   268  		}
   269  
   270  		if len(req.Subject.Country) > 0 {
   271  			subject.Country = req.Subject.Country[0]
   272  		}
   273  
   274  		fireflyCertRequest.Subject = subject
   275  
   276  		//getting the altnames
   277  		if len(req.DNSNames) > 0 || len(req.IPAddresses) > 0 || len(req.EmailAddresses) > 0 || len(req.URIs) > 0 {
   278  			altNames := &AlternativeNames{}
   279  			if len(req.DNSNames) > 0 {
   280  				altNames.DnsNames = req.DNSNames
   281  			}
   282  
   283  			if len(req.IPAddresses) > 0 {
   284  				sIPAddresses := make([]string, 0)
   285  				for _, address := range req.IPAddresses {
   286  					sIPAddresses = append(sIPAddresses, address.String())
   287  				}
   288  
   289  				altNames.IpAddresses = sIPAddresses
   290  			}
   291  
   292  			if len(req.EmailAddresses) > 0 {
   293  				altNames.EmailAddresses = req.EmailAddresses
   294  			}
   295  
   296  			if len(req.URIs) > 0 {
   297  				sUris := make([]string, 0)
   298  				for _, uri := range req.URIs {
   299  					sUris = append(sUris, uri.String())
   300  				}
   301  				altNames.Uris = sUris
   302  			}
   303  
   304  			fireflyCertRequest.AlternativeName = altNames
   305  		}
   306  	}
   307  
   308  	if req.ValidityPeriod != "" {
   309  		fireflyCertRequest.ValidityPeriod = &req.ValidityPeriod
   310  	} else {
   311  		if req.ValidityDuration != nil { //if the validityDuration was set then it will convert to ISO 8601
   312  			validityPeriod := duration.Format(*req.ValidityDuration)
   313  			fireflyCertRequest.ValidityPeriod = &validityPeriod
   314  		}
   315  	}
   316  
   317  	fireflyCertRequest.PolicyName = c.zone
   318  
   319  	//getting the keyAlgorithm
   320  	keyAlgorithm := ""
   321  	switch req.KeyType {
   322  	case certificate.KeyTypeRSA:
   323  		keySize, err := GetRSASize(req.KeyLength)
   324  		if err != nil {
   325  			return nil, err
   326  		}
   327  		keyAlgorithm = fmt.Sprintf("RSA_%d", keySize)
   328  	case certificate.KeyTypeECDSA, certificate.KeyTypeED25519:
   329  		keyCurve := req.KeyCurve
   330  		if keyCurve == certificate.EllipticCurveNotSet {
   331  			keyCurve = certificate.EllipticCurveDefault
   332  		}
   333  		keyAlgorithm = fmt.Sprintf("EC_%s", keyCurve.String())
   334  	}
   335  	fireflyCertRequest.KeyAlgorithm = keyAlgorithm
   336  
   337  	zap.L().Info("successfully built certificate request", fieldPlatform)
   338  	return fireflyCertRequest, nil
   339  }
   340  
   341  func (c *Connector) getCertificateRequestUrl(req *certificate.Request) urlResource {
   342  	if req.CsrOrigin == certificate.UserProvidedCSR {
   343  		return urlResourceCertificateRequestCSR
   344  	}
   345  
   346  	return urlResourceCertificateRequest
   347  }
   348  
   349  // SupportSynchronousRequestCertificate returns if the connector support synchronous calls to request a certificate.
   350  func (c *Connector) SupportSynchronousRequestCertificate() bool {
   351  	return true
   352  }
   353  
   354  type ErrCertNotFound struct {
   355  	error
   356  }
   357  
   358  func (e *ErrCertNotFound) Error() string {
   359  	return e.error.Error()
   360  }
   361  
   362  func (e *ErrCertNotFound) Unwrap() error {
   363  	return e.error
   364  }
   365  
   366  func (c *Connector) Ping() (err error) {
   367  	panic("operation is not supported yet")
   368  }
   369  
   370  func (c *Connector) RetrieveSystemVersion() (string, error) {
   371  	panic("operation is not supported yet")
   372  }
   373  
   374  // RequestCertificate submits the CSR to the Venafi Firefly API for processing
   375  func (c *Connector) RequestCertificate(_ *certificate.Request) (requestID string, err error) {
   376  	panic("operation is not supported yet")
   377  }
   378  
   379  func (c *Connector) IsCSRServiceGenerated(_ *certificate.Request) (bool, error) {
   380  	panic("operation is not supported yet")
   381  }
   382  
   383  func (c *Connector) RetrieveSshConfig(_ *certificate.SshCaTemplateRequest) (*certificate.SshConfig, error) {
   384  	panic("operation is not supported yet")
   385  }
   386  
   387  func (c *Connector) RetrieveAvailableSSHTemplates() (response []certificate.SshAvaliableTemplate, err error) {
   388  	panic("operation is not supported yet")
   389  }
   390  
   391  func (c *Connector) ResetCertificate(_ *certificate.Request, _ bool) (err error) {
   392  	panic("operation is not supported yet")
   393  }
   394  
   395  func (c *Connector) GetPolicy(_ string) (*policy.PolicySpecification, error) {
   396  	panic("operation is not supported yet")
   397  }
   398  
   399  func (c *Connector) SetPolicy(_ string, _ *policy.PolicySpecification) (string, error) {
   400  	panic("operation is not supported yet")
   401  }
   402  
   403  func (c *Connector) RetrieveCertificate(_ *certificate.Request) (certificates *certificate.PEMCollection, err error) {
   404  	panic("operation is not supported yet")
   405  }
   406  
   407  func (c *Connector) RenewCertificate(_ *certificate.RenewalRequest) (requestID string, err error) {
   408  	panic("operation is not supported yet")
   409  }
   410  
   411  func (c *Connector) RevokeCertificate(_ *certificate.RevocationRequest) (err error) {
   412  	panic("operation is not supported yet")
   413  }
   414  
   415  func (c *Connector) ReadPolicyConfiguration() (policy *endpoint.Policy, err error) {
   416  	panic("operation is not supported yet")
   417  }
   418  
   419  func (c *Connector) ReadZoneConfiguration() (config *endpoint.ZoneConfiguration, err error) {
   420  	return nil, nil
   421  }
   422  
   423  func (c *Connector) ImportCertificate(_ *certificate.ImportRequest) (*certificate.ImportResponse, error) {
   424  	panic("operation is not supported yet")
   425  }
   426  
   427  func (c *Connector) SearchCertificates(_ *certificate.SearchRequest) (*certificate.CertSearchResponse, error) {
   428  	panic("operation is not supported yet")
   429  }
   430  
   431  func (c *Connector) SearchCertificate(_ string, _ string, _ *certificate.Sans, _ time.Duration) (certificateInfo *certificate.CertificateInfo, err error) {
   432  	panic("operation is not supported yet")
   433  }
   434  
   435  func (c *Connector) SetHTTPClient(client *http.Client) {
   436  	c.client = client
   437  }
   438  
   439  func (c *Connector) WriteLog(_ *endpoint.LogRequest) error {
   440  	panic("operation is not supported yet")
   441  }
   442  
   443  func (c *Connector) ListCertificates(_ endpoint.Filter) ([]certificate.CertificateInfo, error) {
   444  	panic("operation is not supported yet")
   445  }
   446  
   447  func (c *Connector) GetZonesByParent(_ string) ([]string, error) {
   448  	panic("operation is not supported yet")
   449  }
   450  
   451  func (c *Connector) RequestSSHCertificate(_ *certificate.SshCertRequest) (response *certificate.SshCertificateObject, err error) {
   452  	panic("operation is not supported yet")
   453  }
   454  
   455  func (c *Connector) RetrieveSSHCertificate(_ *certificate.SshCertRequest) (response *certificate.SshCertificateObject, err error) {
   456  	panic("operation is not supported yet")
   457  }
   458  
   459  func (c *Connector) ProvisionCertificate(_ *domain.ProvisioningRequest, _ *domain.ProvisioningOptions) (*domain.ProvisioningMetadata, error) {
   460  	panic("operation is not supported yet")
   461  }
   462  
   463  func (c *Connector) RetrieveCertificateMetaData(_ string) (*certificate.CertificateMetaData, error) {
   464  	panic("operation is not supported yet")
   465  }
   466  
   467  func (c *Connector) RetireCertificate(_ *certificate.RetireRequest) error {
   468  	panic("operation is not supported yet")
   469  }