vcert 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/lib/cloud/cloud.rb +281 -0
- data/lib/fake/fake.rb +130 -0
- data/lib/objects/objects.rb +359 -0
- data/lib/tpp/tpp.rb +338 -0
- data/lib/vcert.rb +91 -0
- metadata +49 -0
    
        checksums.yaml
    ADDED
    
    | @@ -0,0 +1,7 @@ | |
| 1 | 
            +
            ---
         | 
| 2 | 
            +
            SHA256:
         | 
| 3 | 
            +
              metadata.gz: aca37924c54553b285f4a71cb5dd665a67899ee035024acb0fdae853d0fda053
         | 
| 4 | 
            +
              data.tar.gz: 16683bfaaf3d43b2b6b2f899fee72a13860121cf8be85975b4808fd687b0b165
         | 
| 5 | 
            +
            SHA512:
         | 
| 6 | 
            +
              metadata.gz: 32b44cb26e66b63fc39dd4066470e79363b3de5af723d6ad5e9a63eaf6447568041bcacbd71ebfebc5beff36174204c201d0346e47cfc25994da33f37b63cf11
         | 
| 7 | 
            +
              data.tar.gz: 29c0bdf783fd33860c32685d9f7465b1a788acb3a60371b9e998f809c46182da36e2315f7567a07b5a073221678f411796e53db71e7d00534f57ff11d98747ce
         | 
    
        data/lib/cloud/cloud.rb
    ADDED
    
    | @@ -0,0 +1,281 @@ | |
| 1 | 
            +
            require 'json'
         | 
| 2 | 
            +
            require 'utils/utils'
         | 
| 3 | 
            +
             | 
| 4 | 
            +
            class Vcert::CloudConnection
         | 
| 5 | 
            +
              def initialize(url, token)
         | 
| 6 | 
            +
                @url = url
         | 
| 7 | 
            +
                @token = token
         | 
| 8 | 
            +
              end
         | 
| 9 | 
            +
             | 
| 10 | 
            +
             | 
| 11 | 
            +
              def request(zone_tag, request)
         | 
| 12 | 
            +
                zone_id = get_zoneId_by_tag(zone_tag)
         | 
| 13 | 
            +
                _, data = post(URL_CERTIFICATE_REQUESTS, {:zoneId => zone_id, :certificateSigningRequest => request.csr})
         | 
| 14 | 
            +
                LOG.debug("Raw response to certificate request:")
         | 
| 15 | 
            +
                LOG.debug(JSON.pretty_generate(data))
         | 
| 16 | 
            +
                request.id = data['certificateRequests'][0]["id"]
         | 
| 17 | 
            +
                request
         | 
| 18 | 
            +
              end
         | 
| 19 | 
            +
             | 
| 20 | 
            +
              def retrieve(request)
         | 
| 21 | 
            +
                LOG.info(("Getting certificate status for ID %s" % request.id))
         | 
| 22 | 
            +
                status, data = get(URL_CERTIFICATE_STATUS % request.id)
         | 
| 23 | 
            +
                if [200, 409].include? status
         | 
| 24 | 
            +
                  case data['status']
         | 
| 25 | 
            +
                  when CERT_STATUS_PENDING, CERT_STATUS_REQUESTED
         | 
| 26 | 
            +
                    LOG.info(("Certificate status is: %s" % data['status']))
         | 
| 27 | 
            +
                    return nil
         | 
| 28 | 
            +
                  when CERT_STATUS_FAILED
         | 
| 29 | 
            +
                    raise Vcert::ServerUnexpectedBehaviorError, "Certificate issue status is FAILED"
         | 
| 30 | 
            +
                  when CERT_STATUS_ISSUED
         | 
| 31 | 
            +
                    status, full_chain = get(URL_CERTIFICATE_RETRIEVE % request.id + "?chainOrder=#{CHAIN_OPTION_ROOT_LAST}&format=PEM")
         | 
| 32 | 
            +
                    if status == 200
         | 
| 33 | 
            +
                      cert = parse_full_chain full_chain
         | 
| 34 | 
            +
                      if cert.private_key == nil
         | 
| 35 | 
            +
                        cert.private_key = request.private_key
         | 
| 36 | 
            +
                      end
         | 
| 37 | 
            +
                      return cert
         | 
| 38 | 
            +
                    else
         | 
| 39 | 
            +
                      LOG.error("Can't issue certificate: #{full_chain}")
         | 
| 40 | 
            +
                      raise Vcert::ServerUnexpectedBehaviorError, "Status #{status}"
         | 
| 41 | 
            +
                    end
         | 
| 42 | 
            +
                  else
         | 
| 43 | 
            +
                    raise Vcert::ServerUnexpectedBehaviorError, "Unknown certificate status #{data['status']}"
         | 
| 44 | 
            +
                  end
         | 
| 45 | 
            +
                end
         | 
| 46 | 
            +
              end
         | 
| 47 | 
            +
             | 
| 48 | 
            +
              def renew(request, generate_new_key: true)
         | 
| 49 | 
            +
                puts("Trying to renew certificate")
         | 
| 50 | 
            +
                if request.id == nil && request.thumbprint == nil
         | 
| 51 | 
            +
                  raise Vcert::ClientBadDataError, "Either request ID or certificate thumbprint is required to renew the certificate"
         | 
| 52 | 
            +
                end
         | 
| 53 | 
            +
                if request.thumbprint != nil
         | 
| 54 | 
            +
                  manage_id = search_by_thumbprint(request.thumbprint)
         | 
| 55 | 
            +
                end
         | 
| 56 | 
            +
                if request.id != nil
         | 
| 57 | 
            +
                  prev_request = get_cert_status(request)
         | 
| 58 | 
            +
                  manage_id = prev_request[:manage_id]
         | 
| 59 | 
            +
                  zone = prev_request[:zoneId]
         | 
| 60 | 
            +
                end
         | 
| 61 | 
            +
                if manage_id == nil
         | 
| 62 | 
            +
                  raise Vcert::VcertError, "Can't find the existing certificate"
         | 
| 63 | 
            +
                end
         | 
| 64 | 
            +
             | 
| 65 | 
            +
                status, data = get(URL_MANAGED_CERTIFICATE_BY_ID % manage_id)
         | 
| 66 | 
            +
                if status == 200
         | 
| 67 | 
            +
                  request.id = data['latestCertificateRequestId']
         | 
| 68 | 
            +
                else
         | 
| 69 | 
            +
                  raise Vcert::ServerUnexpectedBehaviorError, "Status #{status}"
         | 
| 70 | 
            +
                end
         | 
| 71 | 
            +
             | 
| 72 | 
            +
                if zone == nil
         | 
| 73 | 
            +
                  prev_request = get_cert_status(request)
         | 
| 74 | 
            +
                  zone = prev_request[:zoneId]
         | 
| 75 | 
            +
                end
         | 
| 76 | 
            +
             | 
| 77 | 
            +
                d = {existingManagedCertificateId: manage_id, zoneId: zone}
         | 
| 78 | 
            +
                if request.csr?
         | 
| 79 | 
            +
                  d.merge!(certificateSigningRequest: request.csr)
         | 
| 80 | 
            +
                  d.merge!(reuseCSR: false)
         | 
| 81 | 
            +
                elsif generate_new_key
         | 
| 82 | 
            +
                  parsed_csr = parse_csr_fields(prev_request[:csr])
         | 
| 83 | 
            +
                  renew_request = Vcert::Request.new(
         | 
| 84 | 
            +
                      common_name: parsed_csr[:CN],
         | 
| 85 | 
            +
                      san_dns: parsed_csr[:DNS],
         | 
| 86 | 
            +
                      country: parsed_csr[:C],
         | 
| 87 | 
            +
                      province: parsed_csr[:ST],
         | 
| 88 | 
            +
                      locality: parsed_csr[:L],
         | 
| 89 | 
            +
                      organization: parsed_csr[:O],
         | 
| 90 | 
            +
                      organizational_unit: parsed_csr[:OU])
         | 
| 91 | 
            +
                  d.merge!(certificateSigningRequest: renew_request.csr)
         | 
| 92 | 
            +
                else
         | 
| 93 | 
            +
                  d.merge!(reuseCSR: true)
         | 
| 94 | 
            +
                end
         | 
| 95 | 
            +
             | 
| 96 | 
            +
                status, data = post(URL_CERTIFICATE_REQUESTS, data = d)
         | 
| 97 | 
            +
                if status == 201
         | 
| 98 | 
            +
                  if generate_new_key
         | 
| 99 | 
            +
                    return data['certificateRequests'][0]['id'], renew_request.private_key
         | 
| 100 | 
            +
                  else
         | 
| 101 | 
            +
                    return data['certificateRequests'][0]['id'], nil
         | 
| 102 | 
            +
                  end
         | 
| 103 | 
            +
             | 
| 104 | 
            +
                else
         | 
| 105 | 
            +
                  raise Vcert::ServerUnexpectedBehaviorError, "Status: #{status} Message: #{data}"
         | 
| 106 | 
            +
                end
         | 
| 107 | 
            +
             | 
| 108 | 
            +
              end
         | 
| 109 | 
            +
             | 
| 110 | 
            +
              def zone_configuration(tag)
         | 
| 111 | 
            +
                if tag.to_s.strip.empty?
         | 
| 112 | 
            +
                  raise Vcert::ClientBadDataError, "Zone should not be empty"
         | 
| 113 | 
            +
                end
         | 
| 114 | 
            +
                LOG.info("Getting configuration for zone #{tag}")
         | 
| 115 | 
            +
                _, data = get(URL_ZONE_BY_TAG % tag)
         | 
| 116 | 
            +
                template_id = data['certificateIssuingTemplateId']
         | 
| 117 | 
            +
                _, data = get(URL_TEMPLATE_BY_ID % template_id)
         | 
| 118 | 
            +
                kt = Vcert::KeyType.new data['keyTypes'][0]["keyType"], data['keyTypes'][0]["keyLengths"][0].to_i
         | 
| 119 | 
            +
                z = Vcert::ZoneConfiguration.new(
         | 
| 120 | 
            +
                    country: Vcert::CertField.new(""),
         | 
| 121 | 
            +
                    province: Vcert::CertField.new(""),
         | 
| 122 | 
            +
                    locality: Vcert::CertField.new(""),
         | 
| 123 | 
            +
                    organization: Vcert::CertField.new(""),
         | 
| 124 | 
            +
                    organizational_unit: Vcert::CertField.new(""),
         | 
| 125 | 
            +
                    key_type: Vcert::CertField.new(kt, locked: true),
         | 
| 126 | 
            +
                )
         | 
| 127 | 
            +
                return z
         | 
| 128 | 
            +
              end
         | 
| 129 | 
            +
             | 
| 130 | 
            +
              def policy(zone_id)
         | 
| 131 | 
            +
                unless zone_id
         | 
| 132 | 
            +
                  raise Vcert::ClientBadDataError, "Zone should be not nil"
         | 
| 133 | 
            +
                end
         | 
| 134 | 
            +
                status, data = get(URL_PROJECT_ZONE_DETAILS % zone_id)
         | 
| 135 | 
            +
                if status != 200
         | 
| 136 | 
            +
                  raise Vcert::ServerUnexpectedBehaviorError, "Invalid status getting issuing template: %s for zone %s" % status, zone_id
         | 
| 137 | 
            +
                end
         | 
| 138 | 
            +
                template_id = data['certificateIssuingTemplateId']
         | 
| 139 | 
            +
                status, data = get(URL_TEMPLATE_BY_ID % template_id)
         | 
| 140 | 
            +
                if status != 200
         | 
| 141 | 
            +
                  raise Vcert::ServerUnexpectedBehaviorError, "Invalid status getting policy: %s for issuing template %s" % status, template_id
         | 
| 142 | 
            +
                end
         | 
| 143 | 
            +
                parse_policy_responce_to_object(data)
         | 
| 144 | 
            +
              end
         | 
| 145 | 
            +
             | 
| 146 | 
            +
              private
         | 
| 147 | 
            +
             | 
| 148 | 
            +
              TOKEN_HEADER_NAME = "tppl-api-key"
         | 
| 149 | 
            +
              CHAIN_OPTION_ROOT_FIRST = "ROOT_FIRST"
         | 
| 150 | 
            +
              CHAIN_OPTION_ROOT_LAST = "EE_FIRST"
         | 
| 151 | 
            +
              CERT_STATUS_REQUESTED = 'REQUESTED'
         | 
| 152 | 
            +
              CERT_STATUS_PENDING = 'PENDING'
         | 
| 153 | 
            +
              CERT_STATUS_FAILED = 'FAILED'
         | 
| 154 | 
            +
              CERT_STATUS_ISSUED = 'ISSUED'
         | 
| 155 | 
            +
              URL_ZONE_BY_TAG = "zones/tag/%s"
         | 
| 156 | 
            +
              URL_PROJECT_ZONE_DETAILS = "projectzones/%s"
         | 
| 157 | 
            +
              URL_TEMPLATE_BY_ID = "certificateissuingtemplates/%s"
         | 
| 158 | 
            +
              URL_CERTIFICATE_REQUESTS = "certificaterequests"
         | 
| 159 | 
            +
              URL_CERTIFICATE_STATUS = URL_CERTIFICATE_REQUESTS + "/%s"
         | 
| 160 | 
            +
              URL_CERTIFICATE_RETRIEVE = URL_CERTIFICATE_REQUESTS + "/%s/certificate"
         | 
| 161 | 
            +
              URL_CERTIFICATE_SEARCH = "certificatesearch"
         | 
| 162 | 
            +
              URL_MANAGED_CERTIFICATES = "managedcertificates"
         | 
| 163 | 
            +
              URL_MANAGED_CERTIFICATE_BY_ID = URL_MANAGED_CERTIFICATES + "/%s"
         | 
| 164 | 
            +
             | 
| 165 | 
            +
              def get_zoneId_by_tag(tag)
         | 
| 166 | 
            +
                _, data = get(URL_ZONE_BY_TAG % tag)
         | 
| 167 | 
            +
                data['id']
         | 
| 168 | 
            +
              end
         | 
| 169 | 
            +
             | 
| 170 | 
            +
              def get(url)
         | 
| 171 | 
            +
                uri = URI.parse(@url)
         | 
| 172 | 
            +
                request = Net::HTTP.new(uri.host, uri.port)
         | 
| 173 | 
            +
                request.use_ssl = true
         | 
| 174 | 
            +
                url = uri.path + "/" + url
         | 
| 175 | 
            +
             | 
| 176 | 
            +
             | 
| 177 | 
            +
                response = request.get(url, {TOKEN_HEADER_NAME => @token})
         | 
| 178 | 
            +
                case response.code.to_i
         | 
| 179 | 
            +
                when 200, 201, 202, 409
         | 
| 180 | 
            +
                  LOG.info(("HTTP status OK"))
         | 
| 181 | 
            +
                when 403
         | 
| 182 | 
            +
                  raise Vcert::AuthenticationError
         | 
| 183 | 
            +
                else
         | 
| 184 | 
            +
                  raise Vcert::ServerUnexpectedBehaviorError, "Unexpected code #{response.code} for URL #{url}. Message: #{response.body}"
         | 
| 185 | 
            +
                end
         | 
| 186 | 
            +
                case response.header['content-type']
         | 
| 187 | 
            +
                when "application/json"
         | 
| 188 | 
            +
                  begin
         | 
| 189 | 
            +
                    data = JSON.parse(response.body)
         | 
| 190 | 
            +
                  rescue JSON::ParserError
         | 
| 191 | 
            +
                    raise Vcert::ServerUnexpectedBehaviorError, "Invalid JSON"
         | 
| 192 | 
            +
                  end
         | 
| 193 | 
            +
                when "text/plain"
         | 
| 194 | 
            +
                  data = response.body
         | 
| 195 | 
            +
                else
         | 
| 196 | 
            +
                  raise Vcert::ServerUnexpectedBehaviorError, "Unexpected content-type #{response.header['content-type']}"
         | 
| 197 | 
            +
                end
         | 
| 198 | 
            +
                # rescue *ALL_NET_HTTP_ERRORS
         | 
| 199 | 
            +
                return response.code.to_i, data
         | 
| 200 | 
            +
                # end
         | 
| 201 | 
            +
              end
         | 
| 202 | 
            +
             | 
| 203 | 
            +
              def post(url, data)
         | 
| 204 | 
            +
                uri = URI.parse(@url)
         | 
| 205 | 
            +
                request = Net::HTTP.new(uri.host, uri.port)
         | 
| 206 | 
            +
                request.use_ssl = true
         | 
| 207 | 
            +
                url = uri.path + "/" + url
         | 
| 208 | 
            +
                encoded_data = JSON.generate(data)
         | 
| 209 | 
            +
                response = request.post(url, encoded_data, {TOKEN_HEADER_NAME => @token, "Content-Type" => "application/json", "Accept" => "application/json"})
         | 
| 210 | 
            +
                case response.code.to_i
         | 
| 211 | 
            +
                when 200, 201, 202, 409
         | 
| 212 | 
            +
                  LOG.info(("HTTP status OK"))
         | 
| 213 | 
            +
                when 403
         | 
| 214 | 
            +
                  raise Vcert::AuthenticationError
         | 
| 215 | 
            +
                else
         | 
| 216 | 
            +
                  raise Vcert::ServerUnexpectedBehaviorError, "Unexpected code #{response.code} for URL #{url}. Message: #{response.body}"
         | 
| 217 | 
            +
                end
         | 
| 218 | 
            +
                data = JSON.parse(response.body)
         | 
| 219 | 
            +
                return response.code.to_i, data
         | 
| 220 | 
            +
              end
         | 
| 221 | 
            +
             | 
| 222 | 
            +
              def parse_full_chain(full_chain)
         | 
| 223 | 
            +
                pems = parse_pem_list(full_chain)
         | 
| 224 | 
            +
                Vcert::Certificate.new(
         | 
| 225 | 
            +
                    cert: pems[0],
         | 
| 226 | 
            +
                    chain: pems[1..-1]
         | 
| 227 | 
            +
                )
         | 
| 228 | 
            +
              end
         | 
| 229 | 
            +
             | 
| 230 | 
            +
              def parse_policy_responce_to_object(d)
         | 
| 231 | 
            +
                key_types = []
         | 
| 232 | 
            +
                d['keyTypes'].each { |kt| key_types.push(['keyType']) }
         | 
| 233 | 
            +
                Vcert::Policy.new(policy_id: d['id'],
         | 
| 234 | 
            +
                                           name: d['name'],
         | 
| 235 | 
            +
                                           system_generated: d['systemGenerated'],
         | 
| 236 | 
            +
                                           creation_date: d['creationDate'],
         | 
| 237 | 
            +
                                           subject_cn_regexes: d['subjectCNRegexes'],
         | 
| 238 | 
            +
                                           subject_o_regexes: d['subjectORegexes'],
         | 
| 239 | 
            +
                                           subject_ou_regexes: d['subjectOURegexes'],
         | 
| 240 | 
            +
                                           subject_st_regexes: d['subjectSTRegexes'],
         | 
| 241 | 
            +
                                           subject_l_regexes: d['subjectLRegexes'],
         | 
| 242 | 
            +
                                           subject_c_regexes: d['subjectCValues'],
         | 
| 243 | 
            +
                                           san_regexes: d['sanRegexes'],
         | 
| 244 | 
            +
                                           key_types: key_types)
         | 
| 245 | 
            +
              end
         | 
| 246 | 
            +
             | 
| 247 | 
            +
              def search_by_thumbprint(thumbprint)
         | 
| 248 | 
            +
                # thumbprint = re.sub(r'[^\dabcdefABCDEF]', "", thumbprint)
         | 
| 249 | 
            +
                thumbprint = thumbprint.upcase
         | 
| 250 | 
            +
                status, data = post(URL_CERTIFICATE_SEARCH, data = {"expression": {operands: [
         | 
| 251 | 
            +
                    {field: "fingerprint", operator: "MATCH", value: thumbprint}]}})
         | 
| 252 | 
            +
                # TODO: check that data have valid certificate in it
         | 
| 253 | 
            +
                if status != 200
         | 
| 254 | 
            +
                  raise Vcert::ServerUnexpectedBehaviorError, "Status: #{status}. Message: #{data.body.to_s}"
         | 
| 255 | 
            +
                end
         | 
| 256 | 
            +
                # TODO: check data
         | 
| 257 | 
            +
                manageId = data['certificates'][0]['managedCertificateId']
         | 
| 258 | 
            +
                LOG.info("Found existing certificate with ID #{manageId}")
         | 
| 259 | 
            +
                return manageId
         | 
| 260 | 
            +
              end
         | 
| 261 | 
            +
             | 
| 262 | 
            +
              def get_cert_status(request)
         | 
| 263 | 
            +
                status, d = get(URL_CERTIFICATE_STATUS % request.id)
         | 
| 264 | 
            +
                if status == 200
         | 
| 265 | 
            +
                  request_status = Hash.new
         | 
| 266 | 
            +
                  request_status[:status] = d['status']
         | 
| 267 | 
            +
                  request_status[:subject] = d['subjectDN'] or d['subjectCN'][0]
         | 
| 268 | 
            +
                  request_status[:subject_alt_names] = d['subjectAlternativeNamesByType']
         | 
| 269 | 
            +
                  request_status[:zoneId] = d['zoneId']
         | 
| 270 | 
            +
                  request_status[:manage_id] = d['managedCertificateId']
         | 
| 271 | 
            +
                  request_status[:csr] = d['certificateSigningRequest']
         | 
| 272 | 
            +
                  request_status[:key_lenght] = d['keyLength']
         | 
| 273 | 
            +
                  request_status[:key_type] = d['keyType']
         | 
| 274 | 
            +
                  return request_status
         | 
| 275 | 
            +
                else
         | 
| 276 | 
            +
                  raise Vcert::ServerUnexpectedBehaviorError, "status: #{status}"
         | 
| 277 | 
            +
                end
         | 
| 278 | 
            +
              end
         | 
| 279 | 
            +
             | 
| 280 | 
            +
            end
         | 
| 281 | 
            +
             | 
    
        data/lib/fake/fake.rb
    ADDED
    
    | @@ -0,0 +1,130 @@ | |
| 1 | 
            +
            require 'openssl'
         | 
| 2 | 
            +
            require 'base64'
         | 
| 3 | 
            +
             | 
| 4 | 
            +
             | 
| 5 | 
            +
             | 
| 6 | 
            +
            class Vcert::FakeConnection
         | 
| 7 | 
            +
              def initialize()
         | 
| 8 | 
            +
                @cert_cache = {}
         | 
| 9 | 
            +
              end
         | 
| 10 | 
            +
             | 
| 11 | 
            +
              def request(zone_tag, request)
         | 
| 12 | 
            +
                request.id = Base64.encode64(request.csr)
         | 
| 13 | 
            +
              end
         | 
| 14 | 
            +
             | 
| 15 | 
            +
              def retrieve(request)
         | 
| 16 | 
            +
                csrpem = Base64.decode64(request.id)
         | 
| 17 | 
            +
                csr =  OpenSSL::X509::Request.new(csrpem)
         | 
| 18 | 
            +
                root_ca = OpenSSL::X509::Certificate.new ROOT_CA
         | 
| 19 | 
            +
                root_key = OpenSSL::PKey::RSA.new ROOT_KEY
         | 
| 20 | 
            +
                cert = OpenSSL::X509::Certificate.new
         | 
| 21 | 
            +
                cert.version = 2
         | 
| 22 | 
            +
                cert.serial = (Time.new.to_f() * 100).to_i
         | 
| 23 | 
            +
                cert.subject = csr.subject
         | 
| 24 | 
            +
                cert.issuer = root_ca.subject
         | 
| 25 | 
            +
                cert.not_before = Time.now
         | 
| 26 | 
            +
                cert.public_key = csr.public_key
         | 
| 27 | 
            +
                cert.not_after = cert.not_before + 1 * 365 * 24 * 60 * 60
         | 
| 28 | 
            +
                # todo: add extensions
         | 
| 29 | 
            +
                cert.sign(root_key, OpenSSL::Digest::SHA256.new)
         | 
| 30 | 
            +
                c = Vcert::Certificate.new cert:cert.to_pem, chain: [ROOT_CA], private_key: request.private_key
         | 
| 31 | 
            +
                thumbprint = OpenSSL::Digest::SHA1.new(cert.to_der).to_s
         | 
| 32 | 
            +
                @cert_cache[thumbprint] = cert
         | 
| 33 | 
            +
                c
         | 
| 34 | 
            +
              end
         | 
| 35 | 
            +
             | 
| 36 | 
            +
              def policy(zone_tag)
         | 
| 37 | 
            +
                    key_types = [1024, 2048, 4096, 8192].map {|s| Vcert::KeyType.new("rsa", s) } + Vcert::SUPPORTED_CURVES.map {|c| Vcert::KeyType.new("ecdsa", c) }
         | 
| 38 | 
            +
                    Vcert::Policy.new(policy_id: zone_tag, name: zone_tag, system_generated: false, creation_date: nil,
         | 
| 39 | 
            +
                                      subject_cn_regexes: [".*"], subject_o_regexes: [".*"],
         | 
| 40 | 
            +
                                      subject_ou_regexes: [".*"], subject_st_regexes: [".*"],
         | 
| 41 | 
            +
                                      subject_l_regexes: [".*"], subject_c_regexes: [".*"], san_regexes: [".*"],
         | 
| 42 | 
            +
                                      key_types: key_types)
         | 
| 43 | 
            +
              end
         | 
| 44 | 
            +
             | 
| 45 | 
            +
              def zone_configuration(zone_tag)
         | 
| 46 | 
            +
                Vcert::ZoneConfiguration.new(
         | 
| 47 | 
            +
                    country: Vcert::CertField.new("US"),
         | 
| 48 | 
            +
                    province: Vcert::CertField.new("Utah"),
         | 
| 49 | 
            +
                    locality: Vcert::CertField.new("Salt Lake City"),
         | 
| 50 | 
            +
                    organization: Vcert::CertField.new("Venafi"),
         | 
| 51 | 
            +
                    organizational_unit: Vcert::CertField.new("DevOps"),
         | 
| 52 | 
            +
                    key_type: Vcert::CertField.new(Vcert::KeyType.new("rsa", 2048), locked: true),
         | 
| 53 | 
            +
                    )
         | 
| 54 | 
            +
              end
         | 
| 55 | 
            +
             | 
| 56 | 
            +
              def renew(request, generate_new_key: true)
         | 
| 57 | 
            +
                if request.thumbprint
         | 
| 58 | 
            +
                  if generate_new_key
         | 
| 59 | 
            +
                    new_key = OpenSSL::PKey::RSA.new 2048
         | 
| 60 | 
            +
                    csr = OpenSSL::X509::Request.new
         | 
| 61 | 
            +
                    csr.subject = @cert_cache[request.thumbprint].subject
         | 
| 62 | 
            +
                    csr.public_key =  new_key.public_key
         | 
| 63 | 
            +
                    csr.sign new_key, OpenSSL::Digest::SHA256.new
         | 
| 64 | 
            +
                    return Base64.encode64(csr.to_pem), new_key.to_pem
         | 
| 65 | 
            +
                  else
         | 
| 66 | 
            +
                    raise Vcert::VcertError, "can not be implemented"
         | 
| 67 | 
            +
                  end
         | 
| 68 | 
            +
                end
         | 
| 69 | 
            +
                unless generate_new_key
         | 
| 70 | 
            +
                  return request.id, request.private_key
         | 
| 71 | 
            +
                end
         | 
| 72 | 
            +
                new_key = OpenSSL::PKey::RSA.new 2048
         | 
| 73 | 
            +
                csr = OpenSSL::X509::Request.new Base64.decode64(request.id)
         | 
| 74 | 
            +
                csr.public_key = new_key.public_key
         | 
| 75 | 
            +
                csr.sign new_key, OpenSSL::Digest::SHA256.new
         | 
| 76 | 
            +
                return Base64.encode64(csr.to_pem), new_key.to_pem
         | 
| 77 | 
            +
              end
         | 
| 78 | 
            +
              private
         | 
| 79 | 
            +
              ROOT_CA = "-----BEGIN CERTIFICATE-----
         | 
| 80 | 
            +
            MIIDYDCCAkigAwIBAgIBATANBgkqhkiG9w0BAQsFADBBMRMwEQYKCZImiZPyLGQB
         | 
| 81 | 
            +
            GRYDb3JnMRYwFAYKCZImiZPyLGQBGRYGVmVuYWZpMRIwEAYDVQQDDAlWZW5hZmkg
         | 
| 82 | 
            +
            Q0EwHhcNMTkxMDIzMTIzNzExWhcNMjkxMDIwMTIzNzExWjBBMRMwEQYKCZImiZPy
         | 
| 83 | 
            +
            LGQBGRYDb3JnMRYwFAYKCZImiZPyLGQBGRYGVmVuYWZpMRIwEAYDVQQDDAlWZW5h
         | 
| 84 | 
            +
            ZmkgQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC4NfBNSVzOqj8A
         | 
| 85 | 
            +
            E6+NSuEufK5EhgE/o8KPCTtM7qvMCa0X3P+T0I5IUDMXz5Yi/TXjhTANolYEz2RS
         | 
| 86 | 
            +
            9u5Pdv5dvBCe1hwMhXdLlcxEhLtJrjnQvBUTqzuFUBausvRZvE3GwozoZncakEEP
         | 
| 87 | 
            +
            OqTvGEpqjbnF1uiIJf944kjIq9oWnPudatOOlCFtpA1TG1mLJg8jcCrbeiXvRo9d
         | 
| 88 | 
            +
            /dyg7B7URgKdxMukdjCkUMqUwArlu7mnv1kN6UdzhfFRCH0MBH4pisVze9XP/QrV
         | 
| 89 | 
            +
            MJ+gMlultrpDFuMpiruJyPeapDnGloxtWKQ/aQHlnwwaX8fcaA3ADKUAFr66nFT2
         | 
| 90 | 
            +
            14fgmByFAgMBAAGjYzBhMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEG
         | 
| 91 | 
            +
            MB0GA1UdDgQWBBSFrxYL7Fd190DFUZFJ4ahzptV7tTAfBgNVHSMEGDAWgBSFrxYL
         | 
| 92 | 
            +
            7Fd190DFUZFJ4ahzptV7tTANBgkqhkiG9w0BAQsFAAOCAQEAZsRtfC+4j6RWbWDZ
         | 
| 93 | 
            +
            3eRabfY4Nl7z3q3hL2cYo98ZQVb5wssYwKPpX8/DFMnmgiObe0Na5zqaB9PxDBpZ
         | 
| 94 | 
            +
            4wkRFpRfQ2oS13dzPMDdW0/IHhWfyWZiUqdpIacWIHvyuotZ/k3IZLT7zc9Lbs2p
         | 
| 95 | 
            +
            FPmW5/Oe7lNRu3xgqaMuhRid8i426c+fR5YPf32umZtwRnB5hFFE9IlFBPpRl5Z7
         | 
| 96 | 
            +
            GDJKJBgZi9+sk13a3CM8Zn0A9fiCaRASDPKRVhWPjDJwzLy44WF+1GiMZRCR79MX
         | 
| 97 | 
            +
            8B/rNxkrKpWJmjkQj3jqmQOOnp7+QwdZ5OIV7NNlc/Kx2QDV9QV+hnRPetXmsVfy
         | 
| 98 | 
            +
            y5KjSQ==
         | 
| 99 | 
            +
            -----END CERTIFICATE-----
         | 
| 100 | 
            +
            "
         | 
| 101 | 
            +
              ROOT_KEY = "-----BEGIN RSA PRIVATE KEY-----
         | 
| 102 | 
            +
            MIIEpAIBAAKCAQEAuDXwTUlczqo/ABOvjUrhLnyuRIYBP6PCjwk7TO6rzAmtF9z/
         | 
| 103 | 
            +
            k9COSFAzF8+WIv0144UwDaJWBM9kUvbuT3b+XbwQntYcDIV3S5XMRIS7Sa450LwV
         | 
| 104 | 
            +
            E6s7hVAWrrL0WbxNxsKM6GZ3GpBBDzqk7xhKao25xdboiCX/eOJIyKvaFpz7nWrT
         | 
| 105 | 
            +
            jpQhbaQNUxtZiyYPI3Aq23ol70aPXf3coOwe1EYCncTLpHYwpFDKlMAK5bu5p79Z
         | 
| 106 | 
            +
            DelHc4XxUQh9DAR+KYrFc3vVz/0K1TCfoDJbpba6QxbjKYq7icj3mqQ5xpaMbVik
         | 
| 107 | 
            +
            P2kB5Z8MGl/H3GgNwAylABa+upxU9teH4JgchQIDAQABAoIBAEa/YIUuUdiFhiCv
         | 
| 108 | 
            +
            btLjGUzTUdK7bKtWZ5irwPyxBYYdiT8K/5VzmdGoC5dvgIf7m8DAHE6ANG0wgaVj
         | 
| 109 | 
            +
            dO9MEjFJ01BNhwRAFisPYx5Fo/COW2IRej7NmtR+h9ecnz//lBdsDNYM1F19XZ9N
         | 
| 110 | 
            +
            tJ6nQ51cxSZ4fWIcxdtVfQKlDeN0y7ZanHsltv4cpCCuVaVk8uzI6O5E8dNbDmpR
         | 
| 111 | 
            +
            Wotefps+9HHREa6uL39SbzU+S5SkdcVofs6/g/eL6RsP4D6VcF+qdBEQ38ffDaSZ
         | 
| 112 | 
            +
            Q1hOwfTFf6Ahv4HhpCVC6vlIpXJi/RyUu6yqbmInvcXfGHvYoMYhud/lasYZDAm2
         | 
| 113 | 
            +
            RdGB0gECgYEA7OHnaAqOaaYnE2rQkOAn3ZzA7VRhJgMvPgKUeQoUHDhXJOD/6wWD
         | 
| 114 | 
            +
            1/wYd4BKiQyODi5cAlOkLvdrcRlnrGOKRiLgemyNrG2GzJTzgrkJc1IOpRnl2QKw
         | 
| 115 | 
            +
            w1k0Xrv2qDpoebKMqhxjgEnYp+ddVB7kG561vl7JfhjQidrVqx6y/0UCgYEAxxPQ
         | 
| 116 | 
            +
            M4myF4JKHpxU4+21JKxB3bTY7CWmKM1ZBon/ZFVKd8bsq7wt0tWP83oxWq5b11o+
         | 
| 117 | 
            +
            AnWx4CsQQyl7EWanrDoPag2SjfI/q+AySq0VUNjcAsvPLfT2Q7WQQxEMQGoabZ7j
         | 
| 118 | 
            +
            u8uxkNvZmDy5XGDjcZVdANq2kynC++v1AtwO3EECgYEAnmeeaCuO+kU6ojhuikLr
         | 
| 119 | 
            +
            Rb3aIZqocFP21n/BK4O62PgwBiBT4qTIerlA30CyFx2HLSKBMqkeBK49cd8sPdI+
         | 
| 120 | 
            +
            mBIgjJ1ky+ZeGxaMFGGKWUyJMIy18D1lWOyhIayOEAcm8CKe/+6F9zbqo7UK6wLR
         | 
| 121 | 
            +
            RUsHe+tE0IblhRoKgijASAUCgYEAra7KkXxLhRklw0kPAwBLbqBeoqf6LSS3r5dg
         | 
| 122 | 
            +
            WUUiLQ4Adzl1GGuH6w5plbmAv6Wo+NyBhzHZq0LG4GGbPlY6aRcKhbMrrm2wQSrL
         | 
| 123 | 
            +
            lb0mALACWuonaefyxqXsI6cG8lffkM3zz87prwEv+RLZgRACvwDZ8Dng2cmwlIuK
         | 
| 124 | 
            +
            6iDFUkECgYBL4U3E+trPuVEQm3Nj9nyIFV1efKDZ+uehPSvglYdO+ca7UgwA0btZ
         | 
| 125 | 
            +
            iAAu3L3yP3TSJ6SbLV3hX1VoyyNQpUr+ODZo7VWf+MdZDh9XiiuLNqr5tx7yGhOb
         | 
| 126 | 
            +
            1JXZ1QPAZeRvALui6fdj5yCHjTvayEL2nzPAFbgFrgVYRoF8L9O0gg==
         | 
| 127 | 
            +
            -----END RSA PRIVATE KEY-----
         | 
| 128 | 
            +
            "
         | 
| 129 | 
            +
             | 
| 130 | 
            +
            end
         | 
| @@ -0,0 +1,359 @@ | |
| 1 | 
            +
            require 'openssl'
         | 
| 2 | 
            +
            require "logger"
         | 
| 3 | 
            +
            LOG = Logger.new(STDOUT)
         | 
| 4 | 
            +
             | 
| 5 | 
            +
            OpenSSL::PKey::EC.send(:alias_method, :private?, :private_key?)
         | 
| 6 | 
            +
             | 
| 7 | 
            +
            module Vcert
         | 
| 8 | 
            +
              SUPPORTED_CURVES = ["secp224r1", "prime256v1", "secp521r1"]
         | 
| 9 | 
            +
              class Request
         | 
| 10 | 
            +
                attr_accessor :id, :thumbprint
         | 
| 11 | 
            +
                attr_reader :common_name, :country, :province, :locality, :organization, :organizational_unit, :san_dns,:key_type
         | 
| 12 | 
            +
             | 
| 13 | 
            +
                def initialize(common_name: nil, private_key: nil, key_type: nil,
         | 
| 14 | 
            +
                               organization: nil, organizational_unit: nil, country: nil, province: nil, locality: nil, san_dns: nil,
         | 
| 15 | 
            +
                               friendly_name: nil, csr: nil)
         | 
| 16 | 
            +
                  @common_name = common_name
         | 
| 17 | 
            +
                  @private_key = private_key
         | 
| 18 | 
            +
                  #todo: parse private key and set public
         | 
| 19 | 
            +
                  if key_type != nil && !key_type.instance_of?(KeyType)
         | 
| 20 | 
            +
                    raise Vcert::ClientBadDataError, "key_type bad type. should be Vcert::KeyType. for example KeyType('rsa', 2048)"
         | 
| 21 | 
            +
                  end
         | 
| 22 | 
            +
                  @key_type = key_type
         | 
| 23 | 
            +
                  @organization = organization
         | 
| 24 | 
            +
                  @organizational_unit = organizational_unit
         | 
| 25 | 
            +
                  @country = country
         | 
| 26 | 
            +
                  @province = province
         | 
| 27 | 
            +
                  @locality = locality
         | 
| 28 | 
            +
                  @san_dns = san_dns
         | 
| 29 | 
            +
                  @friendly_name = friendly_name
         | 
| 30 | 
            +
                  @id = nil
         | 
| 31 | 
            +
                  @csr = csr
         | 
| 32 | 
            +
                end
         | 
| 33 | 
            +
             | 
| 34 | 
            +
                def generate_csr
         | 
| 35 | 
            +
                  if @private_key == nil
         | 
| 36 | 
            +
                    generate_private_key
         | 
| 37 | 
            +
                  end
         | 
| 38 | 
            +
                  subject_attrs = [
         | 
| 39 | 
            +
                      ['CN', @common_name]
         | 
| 40 | 
            +
                  ]
         | 
| 41 | 
            +
                  if @organization != nil
         | 
| 42 | 
            +
                    subject_attrs.push(['O', @organization])
         | 
| 43 | 
            +
                  end
         | 
| 44 | 
            +
                  if @organizational_unit != nil
         | 
| 45 | 
            +
                    if @organizational_unit.kind_of?(Array)
         | 
| 46 | 
            +
                      @organizational_unit.each { |ou| subject_attrs.push(['OU', ou]) }
         | 
| 47 | 
            +
                    else
         | 
| 48 | 
            +
                      subject_attrs.push(['OU', @organizational_unit])
         | 
| 49 | 
            +
                    end
         | 
| 50 | 
            +
                  end
         | 
| 51 | 
            +
                  if @country != nil
         | 
| 52 | 
            +
                    subject_attrs.push(['C', @country])
         | 
| 53 | 
            +
                  end
         | 
| 54 | 
            +
                  if @province != nil
         | 
| 55 | 
            +
                    subject_attrs.push(['ST', @province])
         | 
| 56 | 
            +
                  end
         | 
| 57 | 
            +
                  if @locality != nil
         | 
| 58 | 
            +
                    subject_attrs.push(['L', @locality])
         | 
| 59 | 
            +
                  end
         | 
| 60 | 
            +
             | 
| 61 | 
            +
                  LOG.info("Making request from subject array #{subject_attrs.inspect}")
         | 
| 62 | 
            +
                  subject = OpenSSL::X509::Name.new subject_attrs
         | 
| 63 | 
            +
                  csr = OpenSSL::X509::Request.new
         | 
| 64 | 
            +
                  csr.version = 0
         | 
| 65 | 
            +
                  csr.subject = subject
         | 
| 66 | 
            +
                  csr.public_key = @public_key
         | 
| 67 | 
            +
                  if @san_dns != nil
         | 
| 68 | 
            +
                    unless @san_dns.kind_of?(Array)
         | 
| 69 | 
            +
                      @san_dns = [@san_dns]
         | 
| 70 | 
            +
                    end
         | 
| 71 | 
            +
                    #TODO: add check that san_dns is an array
         | 
| 72 | 
            +
                    san_list = @san_dns.map { |domain| "DNS:#{domain}" }
         | 
| 73 | 
            +
                    extensions = [
         | 
| 74 | 
            +
                        OpenSSL::X509::ExtensionFactory.new.create_extension('subjectAltName', san_list.join(','))
         | 
| 75 | 
            +
                    ]
         | 
| 76 | 
            +
                    attribute_values = OpenSSL::ASN1::Set [OpenSSL::ASN1::Sequence(extensions)]
         | 
| 77 | 
            +
                    [
         | 
| 78 | 
            +
                        OpenSSL::X509::Attribute.new('extReq', attribute_values),
         | 
| 79 | 
            +
                        OpenSSL::X509::Attribute.new('msExtReq', attribute_values)
         | 
| 80 | 
            +
                    ].each do |attribute|
         | 
| 81 | 
            +
                      csr.add_attribute attribute
         | 
| 82 | 
            +
                    end
         | 
| 83 | 
            +
                  end
         | 
| 84 | 
            +
                  csr.sign @private_key, OpenSSL::Digest::SHA256.new # todo: changable sign alg
         | 
| 85 | 
            +
                  @csr = csr.to_pem
         | 
| 86 | 
            +
                end
         | 
| 87 | 
            +
             | 
| 88 | 
            +
                def csr
         | 
| 89 | 
            +
                  # TODO: find a way to pass CSR generation if renew is requested
         | 
| 90 | 
            +
                  if @csr == nil
         | 
| 91 | 
            +
                    generate_csr
         | 
| 92 | 
            +
                  end
         | 
| 93 | 
            +
                  @csr
         | 
| 94 | 
            +
                end
         | 
| 95 | 
            +
             | 
| 96 | 
            +
                def csr?
         | 
| 97 | 
            +
                  @csr != nil
         | 
| 98 | 
            +
                end
         | 
| 99 | 
            +
             | 
| 100 | 
            +
                def private_key
         | 
| 101 | 
            +
                  if @private_key == nil
         | 
| 102 | 
            +
                    generate_private_key
         | 
| 103 | 
            +
                  end
         | 
| 104 | 
            +
                  @private_key.to_pem
         | 
| 105 | 
            +
                end
         | 
| 106 | 
            +
             | 
| 107 | 
            +
                def friendly_name
         | 
| 108 | 
            +
                  if @friendly_name != nil
         | 
| 109 | 
            +
                    return @friendly_name
         | 
| 110 | 
            +
                  end
         | 
| 111 | 
            +
                  @common_name
         | 
| 112 | 
            +
                end
         | 
| 113 | 
            +
             | 
| 114 | 
            +
                # @param [ZoneConfiguration] zone_config
         | 
| 115 | 
            +
                def update_from_zone_config(zone_config)
         | 
| 116 | 
            +
                  if zone_config.country.locked || (!@country && !!zone_config.country.value)
         | 
| 117 | 
            +
                    @country = zone_config.country.value
         | 
| 118 | 
            +
                  end
         | 
| 119 | 
            +
                  if zone_config.locality.locked || (!@locality && !!zone_config.locality.value)
         | 
| 120 | 
            +
                    @locality = zone_config.locality.value
         | 
| 121 | 
            +
                  end
         | 
| 122 | 
            +
                  if zone_config.province.locked || (!@province && !!zone_config.province.value)
         | 
| 123 | 
            +
                    @province = zone_config.province.value
         | 
| 124 | 
            +
                  end
         | 
| 125 | 
            +
                  if zone_config.organization.locked || (!@organization && !!zone_config.organization.value)
         | 
| 126 | 
            +
                    @organization = zone_config.organization.value
         | 
| 127 | 
            +
                  end
         | 
| 128 | 
            +
                  if zone_config.organizational_unit.locked || (!@organizational_unit && !!zone_config.organizational_unit.value)
         | 
| 129 | 
            +
                    @organizational_unit = zone_config.organizational_unit.value
         | 
| 130 | 
            +
                  end
         | 
| 131 | 
            +
                  if zone_config.key_type.locked || (@key_type == nil && zone_config.key_type.value != nil)
         | 
| 132 | 
            +
                    @key_type = zone_config.key_type.value
         | 
| 133 | 
            +
                  end
         | 
| 134 | 
            +
                end
         | 
| 135 | 
            +
             | 
| 136 | 
            +
                private
         | 
| 137 | 
            +
             | 
| 138 | 
            +
             | 
| 139 | 
            +
                def generate_private_key
         | 
| 140 | 
            +
                  if @key_type == nil
         | 
| 141 | 
            +
                    @key_type = DEFAULT_KEY_TYPE
         | 
| 142 | 
            +
                  end
         | 
| 143 | 
            +
                  if @key_type.type == "rsa"
         | 
| 144 | 
            +
                    @private_key = OpenSSL::PKey::RSA.new @key_type.option
         | 
| 145 | 
            +
                    @public_key = @private_key.public_key
         | 
| 146 | 
            +
                  elsif @key_type.type == "ecdsa"
         | 
| 147 | 
            +
                    @private_key, @public_key = OpenSSL::PKey::EC.new(@key_type.option), OpenSSL::PKey::EC.new(@key_type.option)
         | 
| 148 | 
            +
                    @private_key.generate_key
         | 
| 149 | 
            +
                    @public_key.public_key = @private_key.public_key
         | 
| 150 | 
            +
                  end
         | 
| 151 | 
            +
                end
         | 
| 152 | 
            +
              end
         | 
| 153 | 
            +
             | 
| 154 | 
            +
              class Certificate
         | 
| 155 | 
            +
                attr_accessor :private_key
         | 
| 156 | 
            +
                attr_reader :cert, :chain
         | 
| 157 | 
            +
             | 
| 158 | 
            +
                def initialize(cert: nil, chain: nil, private_key: nil)
         | 
| 159 | 
            +
                  @cert = cert
         | 
| 160 | 
            +
                  @chain = chain
         | 
| 161 | 
            +
                  @private_key = private_key
         | 
| 162 | 
            +
                end
         | 
| 163 | 
            +
              end
         | 
| 164 | 
            +
             | 
| 165 | 
            +
              class Policy
         | 
| 166 | 
            +
                attr_reader :policy_id, :name, :system_generated, :creation_date
         | 
| 167 | 
            +
             | 
| 168 | 
            +
                def initialize(policy_id:, name:, system_generated:, creation_date:, subject_cn_regexes:, subject_o_regexes:,
         | 
| 169 | 
            +
                               subject_ou_regexes:, subject_st_regexes:, subject_l_regexes:, subject_c_regexes:, san_regexes:,
         | 
| 170 | 
            +
                               key_types:)
         | 
| 171 | 
            +
             | 
| 172 | 
            +
                  @policy_id = policy_id
         | 
| 173 | 
            +
                  @name = name
         | 
| 174 | 
            +
                  @system_generated = system_generated
         | 
| 175 | 
            +
                  @creation_date = creation_date
         | 
| 176 | 
            +
                  @subject_cn_regexes = subject_cn_regexes
         | 
| 177 | 
            +
                  @subject_c_regexes = subject_c_regexes
         | 
| 178 | 
            +
                  @subject_st_regexes = subject_st_regexes
         | 
| 179 | 
            +
                  @subject_l_regexes = subject_l_regexes
         | 
| 180 | 
            +
                  @subject_o_regexes = subject_o_regexes
         | 
| 181 | 
            +
                  @subject_ou_regexes = subject_ou_regexes
         | 
| 182 | 
            +
                  @san_regexes = san_regexes
         | 
| 183 | 
            +
                  @key_types = key_types
         | 
| 184 | 
            +
                end
         | 
| 185 | 
            +
             | 
| 186 | 
            +
                # @param [Request] request
         | 
| 187 | 
            +
                def simple_check_request(request)
         | 
| 188 | 
            +
                  if request.csr?
         | 
| 189 | 
            +
                    csr = parse_csr_fields(request.csr)
         | 
| 190 | 
            +
                    unless component_is_valid?(csr[:CN], @subject_cn_regexes)
         | 
| 191 | 
            +
                      raise ValidationError, "Common name #{csr[:CN]} doesnt match #{@subject_cn_regexes}"
         | 
| 192 | 
            +
                    end
         | 
| 193 | 
            +
                    unless component_is_valid?(request.san_dns, @san_regexes, optional: true)
         | 
| 194 | 
            +
                      raise ValidationError, "SANs #{csr[:DNS]} doesnt match #{ @san_regexes }"
         | 
| 195 | 
            +
                    end
         | 
| 196 | 
            +
                  else
         | 
| 197 | 
            +
                    unless component_is_valid?(request.common_name, @subject_cn_regexes)
         | 
| 198 | 
            +
                      raise ValidationError, "Common name #{request.common_name} doesnt match #{@subject_cn_regexes}"
         | 
| 199 | 
            +
                    end
         | 
| 200 | 
            +
                    unless component_is_valid?(request.san_dns, @san_regexes, optional: true)
         | 
| 201 | 
            +
                      raise ValidationError, "SANs #{request.san_dns} doesnt match #{ @san_regexes }"
         | 
| 202 | 
            +
                    end
         | 
| 203 | 
            +
                  end
         | 
| 204 | 
            +
                end
         | 
| 205 | 
            +
             | 
| 206 | 
            +
                # @param [Request] request
         | 
| 207 | 
            +
                def check_request(request)
         | 
| 208 | 
            +
                  simple_check_request(request)
         | 
| 209 | 
            +
                  if request.csr?
         | 
| 210 | 
            +
                    csr = parse_csr_fields(request.csr)
         | 
| 211 | 
            +
                    unless component_is_valid?(csr[:C], @subject_c_regexes)
         | 
| 212 | 
            +
                      raise ValidationError, "Country #{csr[:C]} doesnt match #{@subject_c_regexes}"
         | 
| 213 | 
            +
                    end
         | 
| 214 | 
            +
                    unless component_is_valid?(csr[:ST], @subject_st_regexes)
         | 
| 215 | 
            +
                      raise ValidationError, "Province #{csr[:ST]} doesnt match #{@subject_st_regexes}"
         | 
| 216 | 
            +
                    end
         | 
| 217 | 
            +
                    unless component_is_valid?(csr[:L], @subject_l_regexes)
         | 
| 218 | 
            +
                      raise ValidationError, "Locality #{csr[:L]} doesnt match #{@subject_l_regexes}"
         | 
| 219 | 
            +
                    end
         | 
| 220 | 
            +
                    unless component_is_valid?(csr[:O], @subject_o_regexes)
         | 
| 221 | 
            +
                      raise ValidationError, "Organization #{csr[:O]} doesnt match #{@subject_o_regexes}"
         | 
| 222 | 
            +
                    end
         | 
| 223 | 
            +
                    unless component_is_valid?(csr[:OU], @subject_ou_regexes)
         | 
| 224 | 
            +
                      raise ValidationError, "Organizational unit #{csr[:OU]} doesnt match #{@subject_ou_regexes}"
         | 
| 225 | 
            +
                    end
         | 
| 226 | 
            +
                    #todo: add uri, upn, ip, email
         | 
| 227 | 
            +
                    unless is_key_type_is_valid?(csr[:key_type], @key_types)
         | 
| 228 | 
            +
                      raise ValidationError, "Key Type #{csr[:key_type]} doesnt match allowed #{@key_types}"
         | 
| 229 | 
            +
                    end
         | 
| 230 | 
            +
                  else
         | 
| 231 | 
            +
                    # subject
         | 
| 232 | 
            +
                    unless component_is_valid?(request.country, @subject_c_regexes)
         | 
| 233 | 
            +
                      raise ValidationError, "Country #{request.country} doesnt match #{@subject_c_regexes}"
         | 
| 234 | 
            +
                    end
         | 
| 235 | 
            +
                    unless component_is_valid?(request.province, @subject_st_regexes)
         | 
| 236 | 
            +
                      raise ValidationError, "Province #{request.province} doesnt match #{@subject_st_regexes}"
         | 
| 237 | 
            +
                    end
         | 
| 238 | 
            +
                    unless component_is_valid?(request.locality, @subject_l_regexes)
         | 
| 239 | 
            +
                      raise ValidationError, "Locality #{request.locality} doesnt match #{@subject_l_regexes}"
         | 
| 240 | 
            +
                    end
         | 
| 241 | 
            +
                    unless component_is_valid?(request.organization, @subject_o_regexes)
         | 
| 242 | 
            +
                      raise ValidationError, "Organization #{request.organization} doesnt match #{@subject_o_regexes}"
         | 
| 243 | 
            +
                    end
         | 
| 244 | 
            +
                    unless component_is_valid?(request.organizational_unit, @subject_ou_regexes)
         | 
| 245 | 
            +
                      raise ValidationError, "Organizational unit #{request.organizational_unit} doesnt match #{@subject_ou_regexes}"
         | 
| 246 | 
            +
                    end
         | 
| 247 | 
            +
                    #todo: add uri, upn, ip, email
         | 
| 248 | 
            +
                    unless is_key_type_is_valid?(request.key_type, @key_types)
         | 
| 249 | 
            +
                      raise ValidationError, "Key Type #{request.key_type} doesnt match allowed #{@key_types}"
         | 
| 250 | 
            +
                    end
         | 
| 251 | 
            +
                  end
         | 
| 252 | 
            +
             | 
| 253 | 
            +
             | 
| 254 | 
            +
                  # todo: (!important!) parse csr if it alredy generated (!important!)
         | 
| 255 | 
            +
                end
         | 
| 256 | 
            +
             | 
| 257 | 
            +
                private
         | 
| 258 | 
            +
             | 
| 259 | 
            +
                def is_key_type_is_valid?(key_type, allowed_key_types)
         | 
| 260 | 
            +
                  if key_type == nil
         | 
| 261 | 
            +
                    key_type = DEFAULT_KEY_TYPE
         | 
| 262 | 
            +
                  end
         | 
| 263 | 
            +
                  for i in 0 ... allowed_key_types.length
         | 
| 264 | 
            +
                    if allowed_key_types[i] == key_type
         | 
| 265 | 
            +
                      return true
         | 
| 266 | 
            +
                    end
         | 
| 267 | 
            +
                  end
         | 
| 268 | 
            +
                  false
         | 
| 269 | 
            +
                end
         | 
| 270 | 
            +
             | 
| 271 | 
            +
                def component_is_valid?(component, regexps, optional:false)
         | 
| 272 | 
            +
                  if component == nil
         | 
| 273 | 
            +
                    component = []
         | 
| 274 | 
            +
                  end
         | 
| 275 | 
            +
                  unless component.instance_of? Array
         | 
| 276 | 
            +
                    component = [component]
         | 
| 277 | 
            +
                  end
         | 
| 278 | 
            +
                  if component.length == 0 && optional
         | 
| 279 | 
            +
                    return true
         | 
| 280 | 
            +
                  end
         | 
| 281 | 
            +
                  if component.length == 0
         | 
| 282 | 
            +
                    component = [""]
         | 
| 283 | 
            +
                  end
         | 
| 284 | 
            +
                  for i in 0 ... component.length
         | 
| 285 | 
            +
                    unless match_regexps?(component[i], regexps)
         | 
| 286 | 
            +
                      return false
         | 
| 287 | 
            +
                    end
         | 
| 288 | 
            +
                  end
         | 
| 289 | 
            +
                  true
         | 
| 290 | 
            +
                end
         | 
| 291 | 
            +
             | 
| 292 | 
            +
                def match_regexps?(s, regexps)
         | 
| 293 | 
            +
                  for i in 0 ... regexps.length
         | 
| 294 | 
            +
                    if Regexp.new(regexps[i]).match(s)
         | 
| 295 | 
            +
                      return true
         | 
| 296 | 
            +
                    end
         | 
| 297 | 
            +
                  end
         | 
| 298 | 
            +
                  false
         | 
| 299 | 
            +
                end
         | 
| 300 | 
            +
              end
         | 
| 301 | 
            +
             | 
| 302 | 
            +
              class ZoneConfiguration
         | 
| 303 | 
            +
                attr_reader :country, :province, :locality, :organization, :organizational_unit, :key_type
         | 
| 304 | 
            +
             | 
| 305 | 
            +
                # @param [CertField] country
         | 
| 306 | 
            +
                # @param [CertField] province
         | 
| 307 | 
            +
                # @param [CertField] locality
         | 
| 308 | 
            +
                # @param [CertField] organization
         | 
| 309 | 
            +
                # @param [CertField] organizational_unit
         | 
| 310 | 
            +
                # @param [CertField] key_type
         | 
| 311 | 
            +
                def initialize(country:, province:, locality:, organization:, organizational_unit:, key_type:)
         | 
| 312 | 
            +
                  @country = country
         | 
| 313 | 
            +
                  @province = province
         | 
| 314 | 
            +
                  @locality = locality
         | 
| 315 | 
            +
                  @organization = organization
         | 
| 316 | 
            +
                  @organizational_unit = organizational_unit
         | 
| 317 | 
            +
                  @key_type = key_type
         | 
| 318 | 
            +
                end
         | 
| 319 | 
            +
              end
         | 
| 320 | 
            +
             | 
| 321 | 
            +
              class CertField
         | 
| 322 | 
            +
                attr_reader :value, :locked
         | 
| 323 | 
            +
             | 
| 324 | 
            +
                def initialize(value, locked: false)
         | 
| 325 | 
            +
                  @value = value
         | 
| 326 | 
            +
                  @locked = locked
         | 
| 327 | 
            +
                end
         | 
| 328 | 
            +
              end
         | 
| 329 | 
            +
             | 
| 330 | 
            +
              class KeyType
         | 
| 331 | 
            +
                attr_reader :type, :option
         | 
| 332 | 
            +
             | 
| 333 | 
            +
                def initialize(type, option)
         | 
| 334 | 
            +
                  @type = {"rsa" => "rsa", "ec" => "ecdsa", "ecdsa" => "ecdsa"}[type.downcase]
         | 
| 335 | 
            +
                  if @type == nil
         | 
| 336 | 
            +
                    raise Vcert::VcertError, "bad key type"
         | 
| 337 | 
            +
                  end
         | 
| 338 | 
            +
                  if @type == "rsa"
         | 
| 339 | 
            +
                    unless  [512, 1024, 2048, 3072, 4096, 8192].include?(option)
         | 
| 340 | 
            +
                      raise  Vcert::VcertError,"bad option for rsa key: #{option}. should be one from list 512, 1024, 2048, 3072, 4096, 8192"
         | 
| 341 | 
            +
                    end
         | 
| 342 | 
            +
                  else
         | 
| 343 | 
            +
                    unless SUPPORTED_CURVES.include?(option)
         | 
| 344 | 
            +
                      raise Vcert::VcertError, "bad option for ec key: #{option}. should be one from list #{ SUPPORTED_CURVES}"
         | 
| 345 | 
            +
                    end
         | 
| 346 | 
            +
                  end
         | 
| 347 | 
            +
                  @option = option
         | 
| 348 | 
            +
                end
         | 
| 349 | 
            +
                def ==(other)
         | 
| 350 | 
            +
                  unless other.instance_of? KeyType
         | 
| 351 | 
            +
                    return false
         | 
| 352 | 
            +
                  end
         | 
| 353 | 
            +
                  self.type == other.type && self.option == other.option
         | 
| 354 | 
            +
                end
         | 
| 355 | 
            +
              end
         | 
| 356 | 
            +
             | 
| 357 | 
            +
              DEFAULT_KEY_TYPE = KeyType.new("rsa", 2048)
         | 
| 358 | 
            +
            end
         | 
| 359 | 
            +
             | 
    
        data/lib/tpp/tpp.rb
    ADDED
    
    | @@ -0,0 +1,338 @@ | |
| 1 | 
            +
            require 'json'
         | 
| 2 | 
            +
            require 'date'
         | 
| 3 | 
            +
            require 'base64'
         | 
| 4 | 
            +
            require 'utils/utils'
         | 
| 5 | 
            +
             | 
| 6 | 
            +
            class Vcert::TPPConnection
         | 
| 7 | 
            +
              def initialize(url, user, password, trust_bundle: nil)
         | 
| 8 | 
            +
                @url = normalize_url url
         | 
| 9 | 
            +
                @user = user
         | 
| 10 | 
            +
                @password = password
         | 
| 11 | 
            +
                @token = nil
         | 
| 12 | 
            +
                @trust_bundle = trust_bundle
         | 
| 13 | 
            +
              end
         | 
| 14 | 
            +
             | 
| 15 | 
            +
              def request(zone_tag, request)
         | 
| 16 | 
            +
                data = {:PolicyDN => policy_dn(zone_tag),
         | 
| 17 | 
            +
                        :PKCS10 => request.csr,
         | 
| 18 | 
            +
                        :ObjectName => request.friendly_name,
         | 
| 19 | 
            +
                        :DisableAutomaticRenewal => "true"}
         | 
| 20 | 
            +
                code, response = post URL_CERTIFICATE_REQUESTS, data
         | 
| 21 | 
            +
                if code != 200
         | 
| 22 | 
            +
                  raise Vcert::ServerUnexpectedBehaviorError, "Status  #{code}"
         | 
| 23 | 
            +
                end
         | 
| 24 | 
            +
                request.id = response['CertificateDN']
         | 
| 25 | 
            +
              end
         | 
| 26 | 
            +
             | 
| 27 | 
            +
              def retrieve(request)
         | 
| 28 | 
            +
                retrieve_request = {CertificateDN: request.id, Format: "base64", IncludeChain: 'true', RootFirstOrder: "false"}
         | 
| 29 | 
            +
                code, response = post URL_CERTIFICATE_RETRIEVE, retrieve_request
         | 
| 30 | 
            +
                if code != 200
         | 
| 31 | 
            +
                  return nil
         | 
| 32 | 
            +
                end
         | 
| 33 | 
            +
                full_chain = Base64.decode64(response['CertificateData'])
         | 
| 34 | 
            +
                cert = parse_full_chain full_chain
         | 
| 35 | 
            +
                if cert.private_key == nil
         | 
| 36 | 
            +
                  cert.private_key = request.private_key
         | 
| 37 | 
            +
                end
         | 
| 38 | 
            +
                cert
         | 
| 39 | 
            +
              end
         | 
| 40 | 
            +
             | 
| 41 | 
            +
              def policy(zone_tag)
         | 
| 42 | 
            +
                code, response = post URL_ZONE_CONFIG, {:PolicyDN => policy_dn(zone_tag)}
         | 
| 43 | 
            +
                if code != 200
         | 
| 44 | 
            +
                  raise Vcert::ServerUnexpectedBehaviorError, "Status  #{code}"
         | 
| 45 | 
            +
                end
         | 
| 46 | 
            +
                parse_policy_response response, zone_tag
         | 
| 47 | 
            +
              end
         | 
| 48 | 
            +
             | 
| 49 | 
            +
              def zone_configuration(zone_tag)
         | 
| 50 | 
            +
                code, response = post URL_ZONE_CONFIG, {:PolicyDN => policy_dn(zone_tag)}
         | 
| 51 | 
            +
                if code != 200
         | 
| 52 | 
            +
                  raise Vcert::ServerUnexpectedBehaviorError, "Status  #{code}"
         | 
| 53 | 
            +
                end
         | 
| 54 | 
            +
                parse_zone_configuration response
         | 
| 55 | 
            +
              end
         | 
| 56 | 
            +
             | 
| 57 | 
            +
              def renew(request, generate_new_key: true)
         | 
| 58 | 
            +
                if request.id == nil && request.thumbprint == nil
         | 
| 59 | 
            +
                  raise("Either request ID or certificate thumbprint is required to renew the certificate")
         | 
| 60 | 
            +
                end
         | 
| 61 | 
            +
             | 
| 62 | 
            +
                if request.thumbprint != nil
         | 
| 63 | 
            +
                  request.id = search_by_thumbprint(request.thumbprint)
         | 
| 64 | 
            +
                end
         | 
| 65 | 
            +
                renew_req_data = {"CertificateDN": request.id}
         | 
| 66 | 
            +
                if generate_new_key
         | 
| 67 | 
            +
                  _, r = post(URL_SECRET_STORE_SEARCH, d = {"Namespace": "config", "Owner": request.id, "VaultType": 512})
         | 
| 68 | 
            +
                  vaultId = r["VaultIDs"][0]
         | 
| 69 | 
            +
                  _, r = post(URL_SECRET_STORE_RETRIEVE, d = {"VaultID": vaultId})
         | 
| 70 | 
            +
                  csr_base64_data = r['Base64Data']
         | 
| 71 | 
            +
                  csr_pem = "-----BEGIN CERTIFICATE REQUEST-----\n#{csr_base64_data}\n-----END CERTIFICATE REQUEST-----\n"
         | 
| 72 | 
            +
                  parsed_csr = parse_csr_fields(csr_pem)
         | 
| 73 | 
            +
                  renew_request = Vcert::Request.new(
         | 
| 74 | 
            +
                      common_name: parsed_csr.fetch(:CN, nil),
         | 
| 75 | 
            +
                      san_dns: parsed_csr.fetch(:DNS, nil),
         | 
| 76 | 
            +
                      country: parsed_csr.fetch(:C, nil),
         | 
| 77 | 
            +
                      province: parsed_csr.fetch(:ST, nil),
         | 
| 78 | 
            +
                      locality: parsed_csr.fetch(:L, nil),
         | 
| 79 | 
            +
                      organization: parsed_csr.fetch(:O, nil),
         | 
| 80 | 
            +
                      organizational_unit: parsed_csr.fetch(:OU, nil))
         | 
| 81 | 
            +
                  renew_req_data.merge!(PKCS10: renew_request.csr)
         | 
| 82 | 
            +
                end
         | 
| 83 | 
            +
                LOG.info("Trying to renew certificate %s" % request.id)
         | 
| 84 | 
            +
                _, d = post(URL_CERTIFICATE_RENEW, renew_req_data)
         | 
| 85 | 
            +
                if d.key?('Success')
         | 
| 86 | 
            +
                  if generate_new_key
         | 
| 87 | 
            +
                    return request.id, renew_request.private_key
         | 
| 88 | 
            +
                  else
         | 
| 89 | 
            +
                    return request.id, nil
         | 
| 90 | 
            +
                  end
         | 
| 91 | 
            +
                else
         | 
| 92 | 
            +
                  raise "Certificate renew error"
         | 
| 93 | 
            +
                end
         | 
| 94 | 
            +
             | 
| 95 | 
            +
              end
         | 
| 96 | 
            +
             | 
| 97 | 
            +
              private
         | 
| 98 | 
            +
             | 
| 99 | 
            +
              URL_AUTHORIZE = "authorize/"
         | 
| 100 | 
            +
              URL_CERTIFICATE_REQUESTS = "certificates/request"
         | 
| 101 | 
            +
              URL_ZONE_CONFIG = "certificates/checkpolicy"
         | 
| 102 | 
            +
              URL_CERTIFICATE_RETRIEVE = "certificates/retrieve"
         | 
| 103 | 
            +
              URL_CERTIFICATE_SEARCH = "certificates/"
         | 
| 104 | 
            +
              URL_CERTIFICATE_RENEW = "certificates/renew"
         | 
| 105 | 
            +
              URL_SECRET_STORE_SEARCH = "SecretStore/LookupByOwner"
         | 
| 106 | 
            +
              URL_SECRET_STORE_RETRIEVE = "SecretStore/Retrieve"
         | 
| 107 | 
            +
             | 
| 108 | 
            +
              TOKEN_HEADER_NAME = "x-venafi-api-key"
         | 
| 109 | 
            +
              ALL_ALLOWED_REGEX = ".*"
         | 
| 110 | 
            +
             | 
| 111 | 
            +
              def auth
         | 
| 112 | 
            +
                uri = URI.parse(@url)
         | 
| 113 | 
            +
                request = Net::HTTP.new(uri.host, uri.port)
         | 
| 114 | 
            +
                request.use_ssl = true
         | 
| 115 | 
            +
                if @trust_bundle != nil
         | 
| 116 | 
            +
                  request.ca_file = @trust_bundle
         | 
| 117 | 
            +
                end
         | 
| 118 | 
            +
                url = uri.path + URL_AUTHORIZE
         | 
| 119 | 
            +
                data = {:Username => @user, :Password => @password}
         | 
| 120 | 
            +
                encoded_data = JSON.generate(data)
         | 
| 121 | 
            +
                response = request.post(url, encoded_data, {"Content-Type" => "application/json"})
         | 
| 122 | 
            +
                if response.code.to_i != 200
         | 
| 123 | 
            +
                  raise Vcert::AuthenticationError
         | 
| 124 | 
            +
                end
         | 
| 125 | 
            +
                data = JSON.parse(response.body)
         | 
| 126 | 
            +
                token = data['APIKey']
         | 
| 127 | 
            +
                valid_until = DateTime.strptime(data['ValidUntil'].gsub(/\D/, ''), '%Q')
         | 
| 128 | 
            +
                @token = token, valid_until
         | 
| 129 | 
            +
              end
         | 
| 130 | 
            +
             | 
| 131 | 
            +
              def post(url, data)
         | 
| 132 | 
            +
                if @token == nil || @token[1] < DateTime.now
         | 
| 133 | 
            +
                  auth()
         | 
| 134 | 
            +
                end
         | 
| 135 | 
            +
                uri = URI.parse(@url)
         | 
| 136 | 
            +
                request = Net::HTTP.new(uri.host, uri.port)
         | 
| 137 | 
            +
                request.use_ssl = true
         | 
| 138 | 
            +
                if @trust_bundle != nil
         | 
| 139 | 
            +
                  request.ca_file = @trust_bundle
         | 
| 140 | 
            +
                end
         | 
| 141 | 
            +
                url = uri.path + url
         | 
| 142 | 
            +
                encoded_data = JSON.generate(data)
         | 
| 143 | 
            +
                response = request.post(url, encoded_data, {TOKEN_HEADER_NAME => @token[0], "Content-Type" => "application/json"})
         | 
| 144 | 
            +
                data = JSON.parse(response.body)
         | 
| 145 | 
            +
                return response.code.to_i, data
         | 
| 146 | 
            +
              end
         | 
| 147 | 
            +
             | 
| 148 | 
            +
              def get(url)
         | 
| 149 | 
            +
                if @token == nil || @token[1] < DateTime.now
         | 
| 150 | 
            +
                  auth()
         | 
| 151 | 
            +
                end
         | 
| 152 | 
            +
                uri = URI.parse(@url)
         | 
| 153 | 
            +
                request = Net::HTTP.new(uri.host, uri.port)
         | 
| 154 | 
            +
                request.use_ssl = true
         | 
| 155 | 
            +
                if @trust_bundle != nil
         | 
| 156 | 
            +
                  request.ca_file = @trust_bundle
         | 
| 157 | 
            +
                end
         | 
| 158 | 
            +
                url = uri.path + url
         | 
| 159 | 
            +
                response = request.get(url, {TOKEN_HEADER_NAME => @token[0]})
         | 
| 160 | 
            +
                # TODO: check valid json
         | 
| 161 | 
            +
                data = JSON.parse(response.body)
         | 
| 162 | 
            +
                return response.code.to_i, data
         | 
| 163 | 
            +
              end
         | 
| 164 | 
            +
             | 
| 165 | 
            +
              def policy_dn(zone)
         | 
| 166 | 
            +
                if zone == nil || zone == ''
         | 
| 167 | 
            +
                  raise Vcert::ClientBadDataError, "Zone should not be empty"
         | 
| 168 | 
            +
                end
         | 
| 169 | 
            +
                if zone =~ /^\\\\VED\\\\Poplicy/
         | 
| 170 | 
            +
                  return zone
         | 
| 171 | 
            +
                end
         | 
| 172 | 
            +
                if zone =~ /^\\\\/
         | 
| 173 | 
            +
                  return '\\VED\\Policy' + zone
         | 
| 174 | 
            +
                else
         | 
| 175 | 
            +
                  return '\\VED\\Policy\\' + zone
         | 
| 176 | 
            +
                end
         | 
| 177 | 
            +
              end
         | 
| 178 | 
            +
             | 
| 179 | 
            +
              def normalize_url(url)
         | 
| 180 | 
            +
                if url.index('http://') == 0
         | 
| 181 | 
            +
                  url = "https://" + url[7..-1]
         | 
| 182 | 
            +
                elsif url.index('https://') != 0
         | 
| 183 | 
            +
                  url = 'https://' + url
         | 
| 184 | 
            +
                end
         | 
| 185 | 
            +
                unless url.end_with?('/')
         | 
| 186 | 
            +
                  url = url + '/'
         | 
| 187 | 
            +
                end
         | 
| 188 | 
            +
                unless url.end_with?('/vedsdk/')
         | 
| 189 | 
            +
                  url = url + 'vedsdk/'
         | 
| 190 | 
            +
                end
         | 
| 191 | 
            +
                unless url =~ /^https:\/\/[a-z\d]+[-a-z\d.]+[a-z\d][:\d]*\/vedsdk\/$/
         | 
| 192 | 
            +
                  raise Vcert::ClientBadDataError, "Invalid URL for TPP"
         | 
| 193 | 
            +
                end
         | 
| 194 | 
            +
                url
         | 
| 195 | 
            +
              end
         | 
| 196 | 
            +
             | 
| 197 | 
            +
              def parse_full_chain(full_chain)
         | 
| 198 | 
            +
                pems = parse_pem_list(full_chain)
         | 
| 199 | 
            +
                Vcert::Certificate.new cert: pems[0], chain: pems[1..-1], private_key: nil
         | 
| 200 | 
            +
              end
         | 
| 201 | 
            +
             | 
| 202 | 
            +
              def search_by_thumbprint(thumbprint)
         | 
| 203 | 
            +
                # thumbprint = re.sub(r'[^\dabcdefABCDEF]', "", thumbprint)
         | 
| 204 | 
            +
                thumbprint = thumbprint.upcase
         | 
| 205 | 
            +
                status, data = get(URL_CERTIFICATE_SEARCH+"?Thumbprint=#{thumbprint}")
         | 
| 206 | 
            +
                # TODO: check that data have valid certificate in it
         | 
| 207 | 
            +
                if status != 200
         | 
| 208 | 
            +
                  raise Vcert::ServerUnexpectedBehaviorError, "Status: #{status}. Message:\n #{data.body.to_s}"
         | 
| 209 | 
            +
                end
         | 
| 210 | 
            +
                # TODO: check valid data
         | 
| 211 | 
            +
                return data['Certificates'][0]['DN']
         | 
| 212 | 
            +
              end
         | 
| 213 | 
            +
             | 
| 214 | 
            +
              def parse_zone_configuration(data)
         | 
| 215 | 
            +
                s = data["Policy"]["Subject"]
         | 
| 216 | 
            +
                country = Vcert::CertField.new s["Country"]["Value"], locked: s["Country"]["Locked"]
         | 
| 217 | 
            +
                state = Vcert::CertField.new s["State"]["Value"], locked: s["State"]["Locked"]
         | 
| 218 | 
            +
                city = Vcert::CertField.new s["City"]["Value"], locked: s["City"]["Locked"]
         | 
| 219 | 
            +
                organization = Vcert::CertField.new s["Organization"]["Value"], locked: s["Organization"]["Locked"]
         | 
| 220 | 
            +
                organizational_unit = Vcert::CertField.new s["OrganizationalUnit"]["Values"], locked: s["OrganizationalUnit"]["Locked"]
         | 
| 221 | 
            +
                key_type = Vcert::KeyType.new data["Policy"]["KeyPair"]["KeyAlgorithm"]["Value"], data["Policy"]["KeyPair"]["KeySize"]["Value"]
         | 
| 222 | 
            +
                Vcert::ZoneConfiguration.new country: country, province: state, locality: city, organization: organization,
         | 
| 223 | 
            +
                                             organizational_unit: organizational_unit, key_type: Vcert::CertField.new(key_type)
         | 
| 224 | 
            +
              end
         | 
| 225 | 
            +
             | 
| 226 | 
            +
              def parse_policy_response(response, zone_tag)
         | 
| 227 | 
            +
                def addStartEnd(s)
         | 
| 228 | 
            +
                  unless s.index("^") == 0
         | 
| 229 | 
            +
                    s = "^" + s
         | 
| 230 | 
            +
                  end
         | 
| 231 | 
            +
                  unless s.end_with?("$")
         | 
| 232 | 
            +
                    s = s + "$"
         | 
| 233 | 
            +
                  end
         | 
| 234 | 
            +
                  s
         | 
| 235 | 
            +
                end
         | 
| 236 | 
            +
             | 
| 237 | 
            +
                def escape(value)
         | 
| 238 | 
            +
                  if value.kind_of? Array
         | 
| 239 | 
            +
                    return value.map { |v| addStartEnd(Regexp.escape(v)) }
         | 
| 240 | 
            +
                  else
         | 
| 241 | 
            +
                    return addStartEnd(Regexp.escape(value))
         | 
| 242 | 
            +
                  end
         | 
| 243 | 
            +
                end
         | 
| 244 | 
            +
             | 
| 245 | 
            +
                policy = response["Policy"]
         | 
| 246 | 
            +
                s = policy["Subject"]
         | 
| 247 | 
            +
                if policy["WhitelistedDomains"].empty?
         | 
| 248 | 
            +
                  subjectCNRegex = [ALL_ALLOWED_REGEX]
         | 
| 249 | 
            +
                else
         | 
| 250 | 
            +
                  if policy["WildcardsAllowed"]
         | 
| 251 | 
            +
                    subjectCNRegex = policy["WhitelistedDomains"].map { |d| addStartEnd('[\w\-*]+' + Regexp.escape("." + d)) }
         | 
| 252 | 
            +
                  else
         | 
| 253 | 
            +
                    subjectCNRegex = policy["WhitelistedDomains"].map { |d| addStartEnd('[\w\-]+' + Regexp.escape("." + d)) }
         | 
| 254 | 
            +
                  end
         | 
| 255 | 
            +
                end
         | 
| 256 | 
            +
                if s["OrganizationalUnit"]["Locked"]
         | 
| 257 | 
            +
                  subjectOURegexes = escape(s["OrganizationalUnit"]["Values"])
         | 
| 258 | 
            +
                else
         | 
| 259 | 
            +
                  subjectOURegexes = [ALL_ALLOWED_REGEX]
         | 
| 260 | 
            +
                end
         | 
| 261 | 
            +
                if s["Organization"]["Locked"]
         | 
| 262 | 
            +
                  subjectORegexes = [escape(s["Organization"]["Value"])]
         | 
| 263 | 
            +
                else
         | 
| 264 | 
            +
                  subjectORegexes = [ALL_ALLOWED_REGEX]
         | 
| 265 | 
            +
                end
         | 
| 266 | 
            +
                if s["City"]["Locked"]
         | 
| 267 | 
            +
                  subjectLRegexes = [escape(s["City"]["Value"])]
         | 
| 268 | 
            +
                else
         | 
| 269 | 
            +
                  subjectLRegexes = [ALL_ALLOWED_REGEX]
         | 
| 270 | 
            +
                end
         | 
| 271 | 
            +
                if s["State"]["Locked"]
         | 
| 272 | 
            +
                  subjectSTRegexes = [escape(s["State"]["Value"])]
         | 
| 273 | 
            +
                else
         | 
| 274 | 
            +
                  subjectSTRegexes = [ALL_ALLOWED_REGEX]
         | 
| 275 | 
            +
                end
         | 
| 276 | 
            +
                if s["Country"]["Locked"]
         | 
| 277 | 
            +
                  subjectCRegexes = [escape(s["Country"]["Value"])]
         | 
| 278 | 
            +
                else
         | 
| 279 | 
            +
                  subjectCRegexes = [ALL_ALLOWED_REGEX]
         | 
| 280 | 
            +
                end
         | 
| 281 | 
            +
                if policy["SubjAltNameDnsAllowed"]
         | 
| 282 | 
            +
                  if policy["WhitelistedDomains"].length == 0
         | 
| 283 | 
            +
                    dnsSanRegExs = [ALL_ALLOWED_REGEX]
         | 
| 284 | 
            +
                  else
         | 
| 285 | 
            +
                    dnsSanRegExs = policy["WhitelistedDomains"].map { |d| addStartEnd('[\w-]+' + Regexp.escape("." + d)) }
         | 
| 286 | 
            +
                  end
         | 
| 287 | 
            +
                else
         | 
| 288 | 
            +
                  dnsSanRegExs = []
         | 
| 289 | 
            +
                end
         | 
| 290 | 
            +
                if policy["SubjAltNameIpAllowed"]
         | 
| 291 | 
            +
                  ipSanRegExs = [ALL_ALLOWED_REGEX] # todo: support
         | 
| 292 | 
            +
                else
         | 
| 293 | 
            +
                  ipSanRegExs = []
         | 
| 294 | 
            +
                end
         | 
| 295 | 
            +
                if policy["SubjAltNameEmailAllowed"]
         | 
| 296 | 
            +
                  emailSanRegExs = [ALL_ALLOWED_REGEX] # todo: support
         | 
| 297 | 
            +
                else
         | 
| 298 | 
            +
                  emailSanRegExs = []
         | 
| 299 | 
            +
                end
         | 
| 300 | 
            +
                if policy["SubjAltNameUriAllowed"]
         | 
| 301 | 
            +
                  uriSanRegExs = [ALL_ALLOWED_REGEX] # todo: support
         | 
| 302 | 
            +
                else
         | 
| 303 | 
            +
                  uriSanRegExs = []
         | 
| 304 | 
            +
                end
         | 
| 305 | 
            +
             | 
| 306 | 
            +
                if policy["SubjAltNameUpnAllowed"]
         | 
| 307 | 
            +
                  upnSanRegExs = [ALL_ALLOWED_REGEX] # todo: support
         | 
| 308 | 
            +
                else
         | 
| 309 | 
            +
                  upnSanRegExs = []
         | 
| 310 | 
            +
                end
         | 
| 311 | 
            +
                unless policy["KeyPair"]["KeyAlgorithm"]["Locked"]
         | 
| 312 | 
            +
                  key_types = [1024, 2048, 4096, 8192].map { |s| Vcert::KeyType.new("rsa", s) } + Vcert::SUPPORTED_CURVES.map { |c| Vcert::KeyType.new("ecdsa", c) }
         | 
| 313 | 
            +
                else
         | 
| 314 | 
            +
                  if policy["KeyPair"]["KeyAlgorithm"]["Value"] == "RSA"
         | 
| 315 | 
            +
                    if policy["KeyPair"]["KeySize"]["Locked"]
         | 
| 316 | 
            +
                      key_types = [Vcert::KeyType.new("rsa", policy["KeyPair"]["KeySize"]["Value"])]
         | 
| 317 | 
            +
                    else
         | 
| 318 | 
            +
                      key_types = [1024, 2048, 4096, 8192].map { |s| Vcert::KeyType.new("rsa", s) }
         | 
| 319 | 
            +
                    end
         | 
| 320 | 
            +
                  elsif policy["KeyPair"]["KeyAlgorithm"]["Value"] == "EC"
         | 
| 321 | 
            +
                    if policy["KeyPair"]["EllipticCurve"]["Locked"]
         | 
| 322 | 
            +
                      curve = {"p224" => "secp224r1", "p256" => "prime256v1", "p521" => "secp521r1"}[policy["KeyPair"]["EllipticCurve"]["Value"].downcase]
         | 
| 323 | 
            +
                      key_types = [Vcert::KeyType.new("ecdsa", curve)]
         | 
| 324 | 
            +
                    else
         | 
| 325 | 
            +
                      key_types = Vcert::SUPPORTED_CURVES.map { |c| Vcert::KeyType.new("ecdsa", c) }
         | 
| 326 | 
            +
                    end
         | 
| 327 | 
            +
                  end
         | 
| 328 | 
            +
                end
         | 
| 329 | 
            +
             | 
| 330 | 
            +
                Vcert::Policy.new(policy_id: policy_dn(zone_tag), name: zone_tag, system_generated: false, creation_date: nil,
         | 
| 331 | 
            +
                                  subject_cn_regexes: subjectCNRegex, subject_o_regexes: subjectORegexes,
         | 
| 332 | 
            +
                                  subject_ou_regexes: subjectOURegexes, subject_st_regexes: subjectSTRegexes,
         | 
| 333 | 
            +
                                  subject_l_regexes: subjectLRegexes, subject_c_regexes: subjectCRegexes, san_regexes: dnsSanRegExs,
         | 
| 334 | 
            +
                                  key_types: key_types)
         | 
| 335 | 
            +
              end
         | 
| 336 | 
            +
            end
         | 
| 337 | 
            +
             | 
| 338 | 
            +
             | 
    
        data/lib/vcert.rb
    ADDED
    
    | @@ -0,0 +1,91 @@ | |
| 1 | 
            +
            require 'net/https'
         | 
| 2 | 
            +
            require 'time'
         | 
| 3 | 
            +
             | 
| 4 | 
            +
            TIMEOUT = 420
         | 
| 5 | 
            +
             | 
| 6 | 
            +
            module Vcert
         | 
| 7 | 
            +
              class VcertError < StandardError ; end
         | 
| 8 | 
            +
              class AuthenticationError < VcertError; end
         | 
| 9 | 
            +
              class ServerUnexpectedBehaviorError < VcertError; end
         | 
| 10 | 
            +
              class ClientBadDataError < VcertError; end
         | 
| 11 | 
            +
              class ValidationError < VcertError; end
         | 
| 12 | 
            +
             | 
| 13 | 
            +
              class Connection
         | 
| 14 | 
            +
                def initialize(url: nil, user: nil, password: nil, cloud_token: nil, trust_bundle:nil, fake: false)
         | 
| 15 | 
            +
                  if fake
         | 
| 16 | 
            +
                    @conn = FakeConnection.new
         | 
| 17 | 
            +
                  elsif cloud_token != nil
         | 
| 18 | 
            +
                    @conn = CloudConnection.new url, cloud_token
         | 
| 19 | 
            +
                  elsif user != nil && password != nil && url != nil then
         | 
| 20 | 
            +
                    @conn = TPPConnection.new url, user, password, trust_bundle:trust_bundle
         | 
| 21 | 
            +
                  else
         | 
| 22 | 
            +
                    raise ClientBadDataError, "Invalid credentials list"
         | 
| 23 | 
            +
                  end
         | 
| 24 | 
            +
                end
         | 
| 25 | 
            +
             | 
| 26 | 
            +
             | 
| 27 | 
            +
                # @param [String] zone
         | 
| 28 | 
            +
                # @param [Request] request
         | 
| 29 | 
            +
                def request(zone, request)
         | 
| 30 | 
            +
                  @conn.request(zone, request)
         | 
| 31 | 
            +
                end
         | 
| 32 | 
            +
             | 
| 33 | 
            +
                # @param [Request] request
         | 
| 34 | 
            +
                # @return [Certificate]
         | 
| 35 | 
            +
                def retrieve(request)
         | 
| 36 | 
            +
                  @conn.retrieve(request)
         | 
| 37 | 
            +
                end
         | 
| 38 | 
            +
             | 
| 39 | 
            +
                def revoke(*args)
         | 
| 40 | 
            +
                  @conn.revoke(*args)
         | 
| 41 | 
            +
                end
         | 
| 42 | 
            +
             | 
| 43 | 
            +
                # @param [String] zone
         | 
| 44 | 
            +
                # @return [ZoneConfiguration]
         | 
| 45 | 
            +
                def zone_configuration(zone)
         | 
| 46 | 
            +
                  @conn.zone_configuration(zone)
         | 
| 47 | 
            +
                end
         | 
| 48 | 
            +
             | 
| 49 | 
            +
                # @param [String] zone
         | 
| 50 | 
            +
                # @return [Policy]
         | 
| 51 | 
            +
                def policy(zone)
         | 
| 52 | 
            +
                  @conn.policy(zone)
         | 
| 53 | 
            +
                end
         | 
| 54 | 
            +
             | 
| 55 | 
            +
                def renew(*args)
         | 
| 56 | 
            +
                  @conn.renew(*args)
         | 
| 57 | 
            +
                end
         | 
| 58 | 
            +
             | 
| 59 | 
            +
                # @param [Request] req
         | 
| 60 | 
            +
                # @param [String] zone
         | 
| 61 | 
            +
                # @param [Integer] timeout
         | 
| 62 | 
            +
                # @return [Certificate]
         | 
| 63 | 
            +
                def request_and_retrieve(req, zone, timeout: TIMEOUT)
         | 
| 64 | 
            +
                  request zone, req
         | 
| 65 | 
            +
                  cert = retrieve_loop(req, timeout: timeout)
         | 
| 66 | 
            +
                  return cert
         | 
| 67 | 
            +
                end
         | 
| 68 | 
            +
             | 
| 69 | 
            +
                def retrieve_loop(req, timeout: TIMEOUT)
         | 
| 70 | 
            +
                  t = Time.new() + timeout
         | 
| 71 | 
            +
                  loop do
         | 
| 72 | 
            +
                    if Time.new() > t
         | 
| 73 | 
            +
                      LOG.info("Waiting certificate #{req.id}")
         | 
| 74 | 
            +
                      break
         | 
| 75 | 
            +
                    end
         | 
| 76 | 
            +
                    certificate = @conn.retrieve(req)
         | 
| 77 | 
            +
                    if certificate != nil
         | 
| 78 | 
            +
                      return certificate
         | 
| 79 | 
            +
                    end
         | 
| 80 | 
            +
                    sleep 10
         | 
| 81 | 
            +
                  end
         | 
| 82 | 
            +
                  return nil
         | 
| 83 | 
            +
                end
         | 
| 84 | 
            +
             | 
| 85 | 
            +
              end
         | 
| 86 | 
            +
            end
         | 
| 87 | 
            +
             | 
| 88 | 
            +
            require 'fake/fake'
         | 
| 89 | 
            +
            require 'cloud/cloud'
         | 
| 90 | 
            +
            require 'tpp/tpp'
         | 
| 91 | 
            +
            require 'objects/objects'
         | 
    
        metadata
    ADDED
    
    | @@ -0,0 +1,49 @@ | |
| 1 | 
            +
            --- !ruby/object:Gem::Specification
         | 
| 2 | 
            +
            name: vcert
         | 
| 3 | 
            +
            version: !ruby/object:Gem::Version
         | 
| 4 | 
            +
              version: 0.1.0
         | 
| 5 | 
            +
            platform: ruby
         | 
| 6 | 
            +
            authors:
         | 
| 7 | 
            +
            - Denis Subbotin
         | 
| 8 | 
            +
            - Alexander Rykalin
         | 
| 9 | 
            +
            autorequire: 
         | 
| 10 | 
            +
            bindir: bin
         | 
| 11 | 
            +
            cert_chain: []
         | 
| 12 | 
            +
            date: 2019-09-18 00:00:00.000000000 Z
         | 
| 13 | 
            +
            dependencies: []
         | 
| 14 | 
            +
            description: Ruby client for Venafi Cloud and Trust Protection Platform
         | 
| 15 | 
            +
            email: opensource@venafi.com
         | 
| 16 | 
            +
            executables: []
         | 
| 17 | 
            +
            extensions: []
         | 
| 18 | 
            +
            extra_rdoc_files: []
         | 
| 19 | 
            +
            files:
         | 
| 20 | 
            +
            - lib/cloud/cloud.rb
         | 
| 21 | 
            +
            - lib/fake/fake.rb
         | 
| 22 | 
            +
            - lib/objects/objects.rb
         | 
| 23 | 
            +
            - lib/tpp/tpp.rb
         | 
| 24 | 
            +
            - lib/vcert.rb
         | 
| 25 | 
            +
            homepage: https://rubygems.org/gems/vcert
         | 
| 26 | 
            +
            licenses:
         | 
| 27 | 
            +
            - Apache-2.0
         | 
| 28 | 
            +
            metadata: {}
         | 
| 29 | 
            +
            post_install_message: 
         | 
| 30 | 
            +
            rdoc_options: []
         | 
| 31 | 
            +
            require_paths:
         | 
| 32 | 
            +
            - lib
         | 
| 33 | 
            +
            required_ruby_version: !ruby/object:Gem::Requirement
         | 
| 34 | 
            +
              requirements:
         | 
| 35 | 
            +
              - - ">="
         | 
| 36 | 
            +
                - !ruby/object:Gem::Version
         | 
| 37 | 
            +
                  version: '0'
         | 
| 38 | 
            +
            required_rubygems_version: !ruby/object:Gem::Requirement
         | 
| 39 | 
            +
              requirements:
         | 
| 40 | 
            +
              - - ">="
         | 
| 41 | 
            +
                - !ruby/object:Gem::Version
         | 
| 42 | 
            +
                  version: '0'
         | 
| 43 | 
            +
            requirements: []
         | 
| 44 | 
            +
            rubyforge_project: 
         | 
| 45 | 
            +
            rubygems_version: 2.7.6
         | 
| 46 | 
            +
            signing_key: 
         | 
| 47 | 
            +
            specification_version: 4
         | 
| 48 | 
            +
            summary: Library for Venafi products
         | 
| 49 | 
            +
            test_files: []
         |