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