github.com/Venafi/vcert/v5@v5.10.2/pkg/venafi/firefly/firefly.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  	"bytes"
    21  	"crypto/tls"
    22  	"encoding/json"
    23  	"fmt"
    24  	"io"
    25  	"log"
    26  	"net"
    27  	"net/http"
    28  	"net/url"
    29  	"regexp"
    30  	"strings"
    31  	"time"
    32  
    33  	"github.com/go-http-utils/headers"
    34  
    35  	"github.com/Venafi/vcert/v5/pkg/certificate"
    36  	"github.com/Venafi/vcert/v5/pkg/endpoint"
    37  	"github.com/Venafi/vcert/v5/pkg/verror"
    38  )
    39  
    40  type urlResource string
    41  
    42  const (
    43  	urlResourceCertificateRequest    urlResource = "v1/certificaterequest"
    44  	urlResourceCertificateRequestCSR urlResource = "v1/certificatesigningrequest"
    45  
    46  	scopesSeparator = " "
    47  )
    48  
    49  var (
    50  	rsaSizes = map[int]bool{certificate.DefaultRSAlength: true, 3072: true, 4096: true}
    51  )
    52  
    53  type certificateRequest struct {
    54  	CSR             string            `json:"request,omitempty"`
    55  	Subject         Subject           `json:"subject,omitempty"`
    56  	AlternativeName *AlternativeNames `json:"altNames,omitempty"`
    57  	ValidityPeriod  *string           `json:"validityPeriod,omitempty"`
    58  	PolicyName      string            `json:"policyName,omitempty"`
    59  	KeyAlgorithm    string            `json:"keyType,omitempty"`
    60  }
    61  
    62  type Subject struct {
    63  	CommonName   string   `json:"commonName,omitempty"`
    64  	Organization string   `json:"organization,omitempty"`
    65  	OrgUnits     []string `json:"orgUnits,omitempty"`
    66  	Locality     string   `json:"locality,omitempty"`
    67  	State        string   `json:"state,omitempty"`
    68  	Country      string   `json:"country,omitempty"`
    69  }
    70  
    71  type AlternativeNames struct {
    72  	DnsNames       []string `json:"dnsNames,omitempty"`
    73  	IpAddresses    []string `json:"ipAddresses,omitempty"`
    74  	EmailAddresses []string `json:"emailAddresses,omitempty"`
    75  	Uris           []string `json:"uris,omitempty"`
    76  }
    77  
    78  type certificateRequestResponse struct {
    79  	CertificateChain string `json:"certificateChain,omitempty"`
    80  	PrivateKey       string `json:"privateKey"`
    81  }
    82  
    83  // GenerateRequest should generate a CertificateRequest based on the zone configuration when the csrOrigin was
    84  // set to LocalGeneratedCSR but given that is not supported by Firefly yet, then it's only validating if the CSR
    85  // was provided when the csrOrigin was set to UserProvidedCSR
    86  func (c *Connector) GenerateRequest(_ *endpoint.ZoneConfiguration, req *certificate.Request) (err error) {
    87  	switch req.CsrOrigin {
    88  	case certificate.LocalGeneratedCSR:
    89  		return fmt.Errorf("local generated CSR it's not supported by Firefly yet")
    90  	case certificate.UserProvidedCSR:
    91  		if len(req.GetCSR()) == 0 {
    92  			return fmt.Errorf("%w: CSR was supposed to be provided by user, but it's empty", verror.UserDataError)
    93  		}
    94  		return nil
    95  
    96  	case certificate.ServiceGeneratedCSR:
    97  		return nil
    98  	default:
    99  		return fmt.Errorf("%w: unrecognised req.CsrOrigin %v", verror.UserDataError, req.CsrOrigin)
   100  	}
   101  }
   102  
   103  func (c *Connector) request(method string, resource urlResource, data interface{}) (statusCode int, statusText string, body []byte, err error) {
   104  
   105  	resourceUrl := string(resource)
   106  
   107  	//validating if the resource is already a full url
   108  	reg := regexp.MustCompile("^http(|s)://")
   109  	//if the resourceUrl is not a full Url then prefixing it with the baseUrl
   110  	if reg.FindStringIndex(strings.ToLower(string(resource))) == nil {
   111  		resourceUrl = c.baseURL + resourceUrl
   112  	}
   113  
   114  	var payload io.Reader
   115  	var b []byte
   116  	var values url.Values
   117  
   118  	contentType := "application/json"
   119  
   120  	if method == "POST" || method == "PUT" {
   121  		//determining if the data is type of url.Values
   122  		v, ok := data.(url.Values)
   123  		//if the data is type of url.Values then commonly they are passed to the request as form
   124  		if ok {
   125  			payload = strings.NewReader(v.Encode())
   126  			values = v
   127  			contentType = "application/x-www-form-urlencoded"
   128  		} else {
   129  			b, _ = json.Marshal(data)
   130  			payload = bytes.NewReader(b)
   131  		}
   132  	}
   133  
   134  	r, _ := http.NewRequest(method, resourceUrl, payload)
   135  	r.Close = true
   136  	r.Header.Set(headers.UserAgent, c.userAgent)
   137  	if c.accessToken != "" {
   138  		r.Header.Add("Authorization", fmt.Sprintf("Bearer %s", c.accessToken))
   139  	}
   140  	r.Header.Add("content-type", contentType)
   141  	r.Header.Add("cache-control", "no-cache")
   142  
   143  	res, err := c.getHTTPClient().Do(r)
   144  	if err != nil {
   145  		return
   146  	}
   147  	if res != nil {
   148  		statusCode = res.StatusCode
   149  		statusText = res.Status
   150  	}
   151  
   152  	defer res.Body.Close()
   153  	body, err = io.ReadAll(res.Body)
   154  	// Do not enable trace in production
   155  	trace := false // IMPORTANT: sensitive information can be diclosured
   156  	// I hope you know what are you doing
   157  	if trace {
   158  		log.Println("#################")
   159  		log.Printf("Headers are:\n%s", r.Header)
   160  		if method == "POST" || method == "PUT" {
   161  			if len(values) > 0 {
   162  				log.Printf("Values sent for %s\n%s\n", resourceUrl, values.Encode())
   163  			} else {
   164  				log.Printf("JSON sent for %s\n%s\n", resourceUrl, string(b))
   165  			}
   166  		} else {
   167  			log.Printf("%s request sent to %s\n", method, resourceUrl)
   168  		}
   169  		log.Printf("Response:\n%s\n", string(body))
   170  	} else if c.verbose {
   171  		log.Printf("Got %s status for %s %s\n", statusText, method, resourceUrl)
   172  	}
   173  	return
   174  }
   175  
   176  func (c *Connector) getHTTPClient() *http.Client {
   177  	if c.client != nil {
   178  		return c.client
   179  	}
   180  	var netTransport = &http.Transport{
   181  		Proxy: http.ProxyFromEnvironment,
   182  		DialContext: (&net.Dialer{
   183  			Timeout:   30 * time.Second,
   184  			KeepAlive: 30 * time.Second,
   185  			DualStack: true,
   186  		}).DialContext,
   187  		MaxIdleConns:          100,
   188  		IdleConnTimeout:       90 * time.Second,
   189  		TLSHandshakeTimeout:   10 * time.Second,
   190  		ExpectContinueTimeout: 1 * time.Second,
   191  	}
   192  	tlsConfig := http.DefaultTransport.(*http.Transport).TLSClientConfig
   193  	/* #nosec */
   194  	if c.trust != nil {
   195  		if tlsConfig == nil {
   196  			tlsConfig = &tls.Config{
   197  				MinVersion: tls.VersionTLS12,
   198  			}
   199  		} else {
   200  			tlsConfig = tlsConfig.Clone()
   201  		}
   202  		tlsConfig.RootCAs = c.trust
   203  	}
   204  
   205  	netTransport.TLSClientConfig = tlsConfig
   206  	c.client = &http.Client{
   207  		Timeout:   time.Second * 30,
   208  		Transport: netTransport,
   209  	}
   210  	return c.client
   211  }
   212  
   213  func parseCertificateRequestResult(httpStatusCode int, httpStatus string, body []byte) (*certificateRequestResponse, error) {
   214  	switch httpStatusCode {
   215  	case http.StatusOK:
   216  		return parseCertificateRequestData(body)
   217  	default:
   218  		respError, err := NewResponseError(body)
   219  		if err != nil {
   220  			return nil, err
   221  		}
   222  
   223  		return nil, fmt.Errorf("unexpected status code on Venafi Firefly. Status: %s: %w", httpStatus, respError)
   224  	}
   225  }
   226  
   227  func parseCertificateRequestData(b []byte) (*certificateRequestResponse, error) {
   228  	var data certificateRequestResponse
   229  	err := json.Unmarshal(b, &data)
   230  	if err != nil {
   231  		return nil, fmt.Errorf("%w: %v", verror.ServerError, err)
   232  	}
   233  
   234  	return &data, nil
   235  }
   236  
   237  func (c *Connector) getURL(resource urlResource) string {
   238  	return fmt.Sprintf("%s%s", c.baseURL, resource)
   239  }