github.com/phrase/openapi@v0.0.0-20240514140800-49e8a106740e/openapi-generator/templates/python/python-experimental/signing.mustache (about)

     1  # coding: utf-8
     2  {{>partial_header}}
     3  from __future__ import absolute_import
     4  
     5  from base64 import b64encode
     6  from Crypto.IO import PEM, PKCS8
     7  from Crypto.Hash import SHA256, SHA512
     8  from Crypto.PublicKey import RSA, ECC
     9  from Crypto.Signature import PKCS1_v1_5, pss, DSS
    10  from email.utils import formatdate
    11  import json
    12  import os
    13  import re
    14  from six.moves.urllib.parse import urlencode, urlparse
    15  from time import time
    16  
    17  # The constants below define a subset of HTTP headers that can be included in the
    18  # HTTP signature scheme. Additional headers may be included in the signature.
    19  
    20  # The '(request-target)' header is a calculated field that includes the HTTP verb,
    21  # the URL path and the URL query.
    22  HEADER_REQUEST_TARGET = '(request-target)'
    23  # The time when the HTTP signature was generated.
    24  HEADER_CREATED = '(created)'
    25  # The time when the HTTP signature expires. The API server should reject HTTP requests
    26  # that have expired.
    27  HEADER_EXPIRES = '(expires)'
    28  # The 'Host' header.
    29  HEADER_HOST = 'Host'
    30  # The 'Date' header.
    31  HEADER_DATE = 'Date'
    32  # When the 'Digest' header is included in the HTTP signature, the client automatically
    33  # computes the digest of the HTTP request body, per RFC 3230.
    34  HEADER_DIGEST = 'Digest'
    35  # The 'Authorization' header is automatically generated by the client. It includes
    36  # the list of signed headers and a base64-encoded signature.
    37  HEADER_AUTHORIZATION = 'Authorization'
    38  
    39  # The constants below define the cryptographic schemes for the HTTP signature scheme.
    40  SCHEME_HS2019 = 'hs2019'
    41  SCHEME_RSA_SHA256 = 'rsa-sha256'
    42  SCHEME_RSA_SHA512 = 'rsa-sha512'
    43  
    44  # The constants below define the signature algorithms that can be used for the HTTP
    45  # signature scheme.
    46  ALGORITHM_RSASSA_PSS = 'RSASSA-PSS'
    47  ALGORITHM_RSASSA_PKCS1v15 = 'RSASSA-PKCS1-v1_5'
    48  
    49  ALGORITHM_ECDSA_MODE_FIPS_186_3 = 'fips-186-3'
    50  ALGORITHM_ECDSA_MODE_DETERMINISTIC_RFC6979 = 'deterministic-rfc6979'
    51  ALGORITHM_ECDSA_KEY_SIGNING_ALGORITHMS = {
    52      ALGORITHM_ECDSA_MODE_FIPS_186_3,
    53      ALGORITHM_ECDSA_MODE_DETERMINISTIC_RFC6979
    54  }
    55  
    56  # The cryptographic hash algorithm for the message signature.
    57  HASH_SHA256 = 'sha256'
    58  HASH_SHA512 = 'sha512'
    59  
    60  
    61  class HttpSigningConfiguration(object):
    62      """The configuration parameters for the HTTP signature security scheme.
    63      The HTTP signature security scheme is used to sign HTTP requests with a private key
    64      which is in possession of the API client.
    65      An 'Authorization' header is calculated by creating a hash of select headers,
    66      and optionally the body of the HTTP request, then signing the hash value using
    67      a private key. The 'Authorization' header is added to outbound HTTP requests.
    68  
    69      NOTE: This class is auto generated by OpenAPI Generator
    70  
    71      Ref: https://openapi-generator.tech
    72      Do not edit the class manually.
    73  
    74      :param key_id: A string value specifying the identifier of the cryptographic key,
    75          when signing HTTP requests.
    76      :param signing_scheme: A string value specifying the signature scheme, when
    77          signing HTTP requests.
    78          Supported value are hs2019, rsa-sha256, rsa-sha512.
    79          Avoid using rsa-sha256, rsa-sha512 as they are deprecated. These values are
    80          available for server-side applications that only support the older
    81          HTTP signature algorithms.
    82      :param private_key_path: A string value specifying the path of the file containing
    83          a private key. The private key is used to sign HTTP requests.
    84      :param private_key_passphrase: A string value specifying the passphrase to decrypt
    85          the private key.
    86      :param signed_headers: A list of strings. Each value is the name of a HTTP header
    87          that must be included in the HTTP signature calculation.
    88          The two special signature headers '(request-target)' and '(created)' SHOULD be
    89          included in SignedHeaders.
    90          The '(created)' header expresses when the signature was created.
    91          The '(request-target)' header is a concatenation of the lowercased :method, an
    92          ASCII space, and the :path pseudo-headers.
    93          When signed_headers is not specified, the client defaults to a single value,
    94          '(created)', in the list of HTTP headers.
    95          When SignedHeaders contains the 'Digest' value, the client performs the
    96          following operations:
    97          1. Calculate a digest of request body, as specified in RFC3230, section 4.3.2.
    98          2. Set the 'Digest' header in the request body.
    99          3. Include the 'Digest' header and value in the HTTP signature.
   100      :param signing_algorithm: A string value specifying the signature algorithm, when
   101          signing HTTP requests.
   102          Supported values are:
   103          1. For RSA keys: RSASSA-PSS, RSASSA-PKCS1-v1_5.
   104          2. For ECDSA keys: fips-186-3, deterministic-rfc6979.
   105          If None, the signing algorithm is inferred from the private key.
   106          The default signing algorithm for RSA keys is RSASSA-PSS.
   107          The default signing algorithm for ECDSA keys is fips-186-3.
   108      :param hash_algorithm: The hash algorithm for the signature. Supported values are
   109          sha256 and sha512.
   110          If the signing_scheme is rsa-sha256, the hash algorithm must be set
   111          to None or sha256.
   112          If the signing_scheme is rsa-sha512, the hash algorithm must be set
   113          to None or sha512.
   114      :param signature_max_validity: The signature max validity, expressed as
   115          a datetime.timedelta value. It must be a positive value.
   116      """
   117      def __init__(self, key_id, signing_scheme, private_key_path,
   118                   private_key_passphrase=None,
   119                   signed_headers=None,
   120                   signing_algorithm=None,
   121                   hash_algorithm=None,
   122                   signature_max_validity=None):
   123          self.key_id = key_id
   124          if signing_scheme not in {SCHEME_HS2019, SCHEME_RSA_SHA256, SCHEME_RSA_SHA512}:
   125              raise Exception("Unsupported security scheme: {0}".format(signing_scheme))
   126          self.signing_scheme = signing_scheme
   127          if not os.path.exists(private_key_path):
   128              raise Exception("Private key file does not exist")
   129          self.private_key_path = private_key_path
   130          self.private_key_passphrase = private_key_passphrase
   131          self.signing_algorithm = signing_algorithm
   132          self.hash_algorithm = hash_algorithm
   133          if signing_scheme == SCHEME_RSA_SHA256:
   134              if self.hash_algorithm is None:
   135                  self.hash_algorithm = HASH_SHA256
   136              elif self.hash_algorithm != HASH_SHA256:
   137                  raise Exception("Hash algorithm must be sha256 when security scheme is %s" %
   138                                  SCHEME_RSA_SHA256)
   139          elif signing_scheme == SCHEME_RSA_SHA512:
   140              if self.hash_algorithm is None:
   141                  self.hash_algorithm = HASH_SHA512
   142              elif self.hash_algorithm != HASH_SHA512:
   143                  raise Exception("Hash algorithm must be sha512 when security scheme is %s" %
   144                                  SCHEME_RSA_SHA512)
   145          elif signing_scheme == SCHEME_HS2019:
   146              if self.hash_algorithm is None:
   147                  self.hash_algorithm = HASH_SHA256
   148              elif self.hash_algorithm not in {HASH_SHA256, HASH_SHA512}:
   149                  raise Exception("Invalid hash algorithm")
   150          if signature_max_validity is not None and signature_max_validity.total_seconds() < 0:
   151              raise Exception("The signature max validity must be a positive value")
   152          self.signature_max_validity = signature_max_validity
   153          # If the user has not provided any signed_headers, the default must be set to '(created)',
   154          # as specified in the 'HTTP signature' standard.
   155          if signed_headers is None or len(signed_headers) == 0:
   156              signed_headers = [HEADER_CREATED]
   157          if self.signature_max_validity is None and HEADER_EXPIRES in signed_headers:
   158              raise Exception(
   159                  "Signature max validity must be set when "
   160                  "'(expires)' signature parameter is specified")
   161          if len(signed_headers) != len(set(signed_headers)):
   162              raise Exception("Cannot have duplicates in the signed_headers parameter")
   163          if HEADER_AUTHORIZATION in signed_headers:
   164              raise Exception("'Authorization' header cannot be included in signed headers")
   165          self.signed_headers = signed_headers
   166          self.private_key = None
   167          """The private key used to sign HTTP requests.
   168             Initialized when the PEM-encoded private key is loaded from a file.
   169          """
   170          self.host = None
   171          """The host name, optionally followed by a colon and TCP port number.
   172          """
   173          self._load_private_key()
   174  
   175      def get_http_signature_headers(self, resource_path, method, headers, body, query_params):
   176          """Create a cryptographic message signature for the HTTP request and add the signed headers.
   177  
   178          :param resource_path : A string representation of the HTTP request resource path.
   179          :param method: A string representation of the HTTP request method, e.g. GET, POST.
   180          :param headers: A dict containing the HTTP request headers.
   181          :param body: The object representing the HTTP request body.
   182          :param query_params: A string representing the HTTP request query parameters.
   183          :return: A dict of HTTP headers that must be added to the outbound HTTP request.
   184          """
   185          if method is None:
   186              raise Exception("HTTP method must be set")
   187          if resource_path is None:
   188              raise Exception("Resource path must be set")
   189  
   190          signed_headers_list, request_headers_dict = self._get_signed_header_info(
   191              resource_path, method, headers, body, query_params)
   192  
   193          header_items = [
   194              "{0}: {1}".format(key.lower(), value) for key, value in signed_headers_list]
   195          string_to_sign = "\n".join(header_items)
   196  
   197          digest, digest_prefix = self._get_message_digest(string_to_sign.encode())
   198          b64_signed_msg = self._sign_digest(digest)
   199  
   200          request_headers_dict[HEADER_AUTHORIZATION] = self._get_authorization_header(
   201              signed_headers_list, b64_signed_msg)
   202  
   203          return request_headers_dict
   204  
   205      def get_public_key(self):
   206          """Returns the public key object associated with the private key.
   207          """
   208          pubkey = None
   209          if isinstance(self.private_key, RSA.RsaKey):
   210              pubkey = self.private_key.publickey()
   211          elif isinstance(self.private_key, ECC.EccKey):
   212              pubkey = self.private_key.public_key()
   213          return pubkey
   214  
   215      def _load_private_key(self):
   216          """Load the private key used to sign HTTP requests.
   217              The private key is used to sign HTTP requests as defined in
   218              https://datatracker.ietf.org/doc/draft-cavage-http-signatures/.
   219          """
   220          if self.private_key is not None:
   221              return
   222          with open(self.private_key_path, 'r') as f:
   223              pem_data = f.read()
   224              # Verify PEM Pre-Encapsulation Boundary
   225              r = re.compile(r"\s*-----BEGIN (.*)-----\s+")
   226              m = r.match(pem_data)
   227              if not m:
   228                  raise ValueError("Not a valid PEM pre boundary")
   229              pem_header = m.group(1)
   230              if pem_header == 'RSA PRIVATE KEY':
   231                  self.private_key = RSA.importKey(pem_data, self.private_key_passphrase)
   232              elif pem_header == 'EC PRIVATE KEY':
   233                  self.private_key = ECC.import_key(pem_data, self.private_key_passphrase)
   234              elif pem_header in {'PRIVATE KEY', 'ENCRYPTED PRIVATE KEY'}:
   235                  # Key is in PKCS8 format, which is capable of holding many different
   236                  # types of private keys, not just EC keys.
   237                  (key_binary, pem_header, is_encrypted) = \
   238                      PEM.decode(pem_data, self.private_key_passphrase)
   239                  (oid, privkey, params) = \
   240                      PKCS8.unwrap(key_binary, passphrase=self.private_key_passphrase)
   241                  if oid == '1.2.840.10045.2.1':
   242                      self.private_key = ECC.import_key(pem_data, self.private_key_passphrase)
   243                  else:
   244                      raise Exception("Unsupported key: {0}. OID: {1}".format(pem_header, oid))
   245              else:
   246                  raise Exception("Unsupported key: {0}".format(pem_header))
   247              # Validate the specified signature algorithm is compatible with the private key.
   248              if self.signing_algorithm is not None:
   249                  supported_algs = None
   250                  if isinstance(self.private_key, RSA.RsaKey):
   251                      supported_algs = {ALGORITHM_RSASSA_PSS, ALGORITHM_RSASSA_PKCS1v15}
   252                  elif isinstance(self.private_key, ECC.EccKey):
   253                      supported_algs = ALGORITHM_ECDSA_KEY_SIGNING_ALGORITHMS
   254                  if supported_algs is not None and self.signing_algorithm not in supported_algs:
   255                      raise Exception(
   256                          "Signing algorithm {0} is not compatible with private key".format(
   257                              self.signing_algorithm))
   258  
   259      def _get_signed_header_info(self, resource_path, method, headers, body, query_params):
   260          """Build the HTTP headers (name, value) that need to be included in
   261          the HTTP signature scheme.
   262  
   263          :param resource_path : A string representation of the HTTP request resource path.
   264          :param method: A string representation of the HTTP request method, e.g. GET, POST.
   265          :param headers: A dict containing the HTTP request headers.
   266          :param body: The object (e.g. a dict) representing the HTTP request body.
   267          :param query_params: A string representing the HTTP request query parameters.
   268          :return: A tuple containing two dict objects:
   269              The first dict contains the HTTP headers that are used to calculate
   270              the HTTP signature.
   271              The second dict contains the HTTP headers that must be added to
   272              the outbound HTTP request.
   273          """
   274  
   275          if body is None:
   276              body = ''
   277          else:
   278              body = json.dumps(body)
   279  
   280          # Build the '(request-target)' HTTP signature parameter.
   281          target_host = urlparse(self.host).netloc
   282          target_path = urlparse(self.host).path
   283          request_target = method.lower() + " " + target_path + resource_path
   284          if query_params:
   285              request_target += "?" + urlencode(query_params)
   286  
   287          # Get UNIX time, e.g. seconds since epoch, not including leap seconds.
   288          now = time()
   289          # Format date per RFC 7231 section-7.1.1.2. An example is:
   290          # Date: Wed, 21 Oct 2015 07:28:00 GMT
   291          cdate = formatdate(timeval=now, localtime=False, usegmt=True)
   292          # The '(created)' value MUST be a Unix timestamp integer value.
   293          # Subsecond precision is not supported.
   294          created = int(now)
   295          if self.signature_max_validity is not None:
   296              expires = now + self.signature_max_validity.total_seconds()
   297  
   298          signed_headers_list = []
   299          request_headers_dict = {}
   300          for hdr_key in self.signed_headers:
   301              hdr_key = hdr_key.lower()
   302              if hdr_key == HEADER_REQUEST_TARGET:
   303                  value = request_target
   304              elif hdr_key == HEADER_CREATED:
   305                  value = '{0}'.format(created)
   306              elif hdr_key == HEADER_EXPIRES:
   307                  value = '{0}'.format(expires)
   308              elif hdr_key == HEADER_DATE.lower():
   309                  value = cdate
   310                  request_headers_dict[HEADER_DATE] = '{0}'.format(cdate)
   311              elif hdr_key == HEADER_DIGEST.lower():
   312                  request_body = body.encode()
   313                  body_digest, digest_prefix = self._get_message_digest(request_body)
   314                  b64_body_digest = b64encode(body_digest.digest())
   315                  value = digest_prefix + b64_body_digest.decode('ascii')
   316                  request_headers_dict[HEADER_DIGEST] = '{0}{1}'.format(
   317                      digest_prefix, b64_body_digest.decode('ascii'))
   318              elif hdr_key == HEADER_HOST.lower():
   319                  value = target_host
   320                  request_headers_dict[HEADER_HOST] = '{0}'.format(target_host)
   321              else:
   322                  value = next((v for k, v in headers.items() if k.lower() == hdr_key), None)
   323                  if value is None:
   324                      raise Exception(
   325                          "Cannot sign HTTP request. "
   326                          "Request does not contain the '{0}' header".format(hdr_key))
   327              signed_headers_list.append((hdr_key, value))
   328  
   329          return signed_headers_list, request_headers_dict
   330  
   331      def _get_message_digest(self, data):
   332          """Calculates and returns a cryptographic digest of a specified HTTP request.
   333  
   334          :param data: The string representation of the date to be hashed with a cryptographic hash.
   335          :return: A tuple of (digest, prefix).
   336              The digest is a hashing object that contains the cryptographic digest of
   337              the HTTP request.
   338              The prefix is a string that identifies the cryptographc hash. It is used
   339              to generate the 'Digest' header as specified in RFC 3230.
   340          """
   341          if self.hash_algorithm == HASH_SHA512:
   342              digest = SHA512.new()
   343              prefix = 'SHA-512='
   344          elif self.hash_algorithm == HASH_SHA256:
   345              digest = SHA256.new()
   346              prefix = 'SHA-256='
   347          else:
   348              raise Exception("Unsupported hash algorithm: {0}".format(self.hash_algorithm))
   349          digest.update(data)
   350          return digest, prefix
   351  
   352      def _sign_digest(self, digest):
   353          """Signs a message digest with a private key specified in the signing_info.
   354  
   355          :param digest: A hashing object that contains the cryptographic digest of the HTTP request.
   356          :return: A base-64 string representing the cryptographic signature of the input digest.
   357          """
   358          sig_alg = self.signing_algorithm
   359          if isinstance(self.private_key, RSA.RsaKey):
   360              if sig_alg is None or sig_alg == ALGORITHM_RSASSA_PSS:
   361                  # RSASSA-PSS in Section 8.1 of RFC8017.
   362                  signature = pss.new(self.private_key).sign(digest)
   363              elif sig_alg == ALGORITHM_RSASSA_PKCS1v15:
   364                  # RSASSA-PKCS1-v1_5 in Section 8.2 of RFC8017.
   365                  signature = PKCS1_v1_5.new(self.private_key).sign(digest)
   366              else:
   367                  raise Exception("Unsupported signature algorithm: {0}".format(sig_alg))
   368          elif isinstance(self.private_key, ECC.EccKey):
   369              if sig_alg is None:
   370                  sig_alg = ALGORITHM_ECDSA_MODE_FIPS_186_3
   371              if sig_alg in ALGORITHM_ECDSA_KEY_SIGNING_ALGORITHMS:
   372                  # draft-ietf-httpbis-message-signatures-00 does not specify the ECDSA encoding.
   373                  # Issue: https://github.com/w3c-ccg/http-signatures/issues/107
   374                  signature = DSS.new(key=self.private_key, mode=sig_alg,
   375                                      encoding='der').sign(digest)
   376              else:
   377                  raise Exception("Unsupported signature algorithm: {0}".format(sig_alg))
   378          else:
   379              raise Exception("Unsupported private key: {0}".format(type(self.private_key)))
   380          return b64encode(signature)
   381  
   382      def _get_authorization_header(self, signed_headers, signed_msg):
   383          """Calculates and returns the value of the 'Authorization' header when signing HTTP requests.
   384  
   385          :param signed_headers : A list of tuples. Each value is the name of a HTTP header that
   386              must be included in the HTTP signature calculation.
   387          :param signed_msg: A base-64 encoded string representation of the signature.
   388          :return: The string value of the 'Authorization' header, representing the signature
   389              of the HTTP request.
   390          """
   391          created_ts = None
   392          expires_ts = None
   393          for k, v in signed_headers:
   394              if k == HEADER_CREATED:
   395                  created_ts = v
   396              elif k == HEADER_EXPIRES:
   397                  expires_ts = v
   398          lower_keys = [k.lower() for k, v in signed_headers]
   399          headers_value = " ".join(lower_keys)
   400  
   401          auth_str = "Signature keyId=\"{0}\",algorithm=\"{1}\",".format(
   402                                                  self.key_id, self.signing_scheme)
   403          if created_ts is not None:
   404              auth_str = auth_str + "created={0},".format(created_ts)
   405          if expires_ts is not None:
   406              auth_str = auth_str + "expires={0},".format(expires_ts)
   407          auth_str = auth_str + "headers=\"{0}\",signature=\"{1}\"".format(
   408                                                  headers_value, signed_msg.decode('ascii'))
   409  
   410          return auth_str