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 }