1# -*- coding: utf-8 -*- 2# 3# Electrum - lightweight Bitcoin client 4# Copyright (C) 2018 The Electrum developers 5# 6# Permission is hereby granted, free of charge, to any person 7# obtaining a copy of this software and associated documentation files 8# (the "Software"), to deal in the Software without restriction, 9# including without limitation the rights to use, copy, modify, merge, 10# publish, distribute, sublicense, and/or sell copies of the Software, 11# and to permit persons to whom the Software is furnished to do so, 12# subject to the following conditions: 13# 14# The above copyright notice and this permission notice shall be 15# included in all copies or substantial portions of the Software. 16# 17# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 18# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 19# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 20# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS 21# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN 22# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 23# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24# SOFTWARE. 25 26import base64 27import hashlib 28import functools 29from typing import Union, Tuple, Optional 30from ctypes import ( 31 byref, c_byte, c_int, c_uint, c_char_p, c_size_t, c_void_p, create_string_buffer, 32 CFUNCTYPE, POINTER, cast 33) 34 35from .util import bfh, bh2u, assert_bytes, to_bytes, InvalidPassword, profiler, randrange 36from .crypto import (sha256d, aes_encrypt_with_iv, aes_decrypt_with_iv, hmac_oneshot) 37from . import constants 38from .logging import get_logger 39from .ecc_fast import _libsecp256k1, SECP256K1_EC_UNCOMPRESSED 40 41_logger = get_logger(__name__) 42 43 44def string_to_number(b: bytes) -> int: 45 return int.from_bytes(b, byteorder='big', signed=False) 46 47 48def sig_string_from_der_sig(der_sig: bytes) -> bytes: 49 r, s = get_r_and_s_from_der_sig(der_sig) 50 return sig_string_from_r_and_s(r, s) 51 52 53def der_sig_from_sig_string(sig_string: bytes) -> bytes: 54 r, s = get_r_and_s_from_sig_string(sig_string) 55 return der_sig_from_r_and_s(r, s) 56 57 58def der_sig_from_r_and_s(r: int, s: int) -> bytes: 59 sig_string = (int.to_bytes(r, length=32, byteorder="big") + 60 int.to_bytes(s, length=32, byteorder="big")) 61 sig = create_string_buffer(64) 62 ret = _libsecp256k1.secp256k1_ecdsa_signature_parse_compact(_libsecp256k1.ctx, sig, sig_string) 63 if not ret: 64 raise Exception("Bad signature") 65 ret = _libsecp256k1.secp256k1_ecdsa_signature_normalize(_libsecp256k1.ctx, sig, sig) 66 der_sig = create_string_buffer(80) # this much space should be enough 67 der_sig_size = c_size_t(len(der_sig)) 68 ret = _libsecp256k1.secp256k1_ecdsa_signature_serialize_der(_libsecp256k1.ctx, der_sig, byref(der_sig_size), sig) 69 if not ret: 70 raise Exception("failed to serialize DER sig") 71 der_sig_size = der_sig_size.value 72 return bytes(der_sig)[:der_sig_size] 73 74 75def get_r_and_s_from_der_sig(der_sig: bytes) -> Tuple[int, int]: 76 assert isinstance(der_sig, bytes) 77 sig = create_string_buffer(64) 78 ret = _libsecp256k1.secp256k1_ecdsa_signature_parse_der(_libsecp256k1.ctx, sig, der_sig, len(der_sig)) 79 if not ret: 80 raise Exception("Bad signature") 81 ret = _libsecp256k1.secp256k1_ecdsa_signature_normalize(_libsecp256k1.ctx, sig, sig) 82 compact_signature = create_string_buffer(64) 83 _libsecp256k1.secp256k1_ecdsa_signature_serialize_compact(_libsecp256k1.ctx, compact_signature, sig) 84 r = int.from_bytes(compact_signature[:32], byteorder="big") 85 s = int.from_bytes(compact_signature[32:], byteorder="big") 86 return r, s 87 88 89def get_r_and_s_from_sig_string(sig_string: bytes) -> Tuple[int, int]: 90 if not (isinstance(sig_string, bytes) and len(sig_string) == 64): 91 raise Exception("sig_string must be bytes, and 64 bytes exactly") 92 sig = create_string_buffer(64) 93 ret = _libsecp256k1.secp256k1_ecdsa_signature_parse_compact(_libsecp256k1.ctx, sig, sig_string) 94 if not ret: 95 raise Exception("Bad signature") 96 ret = _libsecp256k1.secp256k1_ecdsa_signature_normalize(_libsecp256k1.ctx, sig, sig) 97 compact_signature = create_string_buffer(64) 98 _libsecp256k1.secp256k1_ecdsa_signature_serialize_compact(_libsecp256k1.ctx, compact_signature, sig) 99 r = int.from_bytes(compact_signature[:32], byteorder="big") 100 s = int.from_bytes(compact_signature[32:], byteorder="big") 101 return r, s 102 103 104def sig_string_from_r_and_s(r: int, s: int) -> bytes: 105 sig_string = (int.to_bytes(r, length=32, byteorder="big") + 106 int.to_bytes(s, length=32, byteorder="big")) 107 sig = create_string_buffer(64) 108 ret = _libsecp256k1.secp256k1_ecdsa_signature_parse_compact(_libsecp256k1.ctx, sig, sig_string) 109 if not ret: 110 raise Exception("Bad signature") 111 ret = _libsecp256k1.secp256k1_ecdsa_signature_normalize(_libsecp256k1.ctx, sig, sig) 112 compact_signature = create_string_buffer(64) 113 _libsecp256k1.secp256k1_ecdsa_signature_serialize_compact(_libsecp256k1.ctx, compact_signature, sig) 114 return bytes(compact_signature) 115 116 117def _x_and_y_from_pubkey_bytes(pubkey: bytes) -> Tuple[int, int]: 118 assert isinstance(pubkey, bytes), f'pubkey must be bytes, not {type(pubkey)}' 119 pubkey_ptr = create_string_buffer(64) 120 ret = _libsecp256k1.secp256k1_ec_pubkey_parse( 121 _libsecp256k1.ctx, pubkey_ptr, pubkey, len(pubkey)) 122 if not ret: 123 raise InvalidECPointException('public key could not be parsed or is invalid') 124 125 pubkey_serialized = create_string_buffer(65) 126 pubkey_size = c_size_t(65) 127 _libsecp256k1.secp256k1_ec_pubkey_serialize( 128 _libsecp256k1.ctx, pubkey_serialized, byref(pubkey_size), pubkey_ptr, SECP256K1_EC_UNCOMPRESSED) 129 pubkey_serialized = bytes(pubkey_serialized) 130 assert pubkey_serialized[0] == 0x04, pubkey_serialized 131 x = int.from_bytes(pubkey_serialized[1:33], byteorder='big', signed=False) 132 y = int.from_bytes(pubkey_serialized[33:65], byteorder='big', signed=False) 133 return x, y 134 135 136class InvalidECPointException(Exception): 137 """e.g. not on curve, or infinity""" 138 139 140@functools.total_ordering 141class ECPubkey(object): 142 143 def __init__(self, b: Optional[bytes]): 144 if b is not None: 145 assert isinstance(b, (bytes, bytearray)), f'pubkey must be bytes-like, not {type(b)}' 146 if isinstance(b, bytearray): 147 b = bytes(b) 148 self._x, self._y = _x_and_y_from_pubkey_bytes(b) 149 else: 150 self._x, self._y = None, None 151 152 @classmethod 153 def from_sig_string(cls, sig_string: bytes, recid: int, msg_hash: bytes) -> 'ECPubkey': 154 assert_bytes(sig_string) 155 if len(sig_string) != 64: 156 raise Exception(f'wrong encoding used for signature? len={len(sig_string)} (should be 64)') 157 if recid < 0 or recid > 3: 158 raise ValueError('recid is {}, but should be 0 <= recid <= 3'.format(recid)) 159 sig65 = create_string_buffer(65) 160 ret = _libsecp256k1.secp256k1_ecdsa_recoverable_signature_parse_compact( 161 _libsecp256k1.ctx, sig65, sig_string, recid) 162 if not ret: 163 raise Exception('failed to parse signature') 164 pubkey = create_string_buffer(64) 165 ret = _libsecp256k1.secp256k1_ecdsa_recover(_libsecp256k1.ctx, pubkey, sig65, msg_hash) 166 if not ret: 167 raise InvalidECPointException('failed to recover public key') 168 return ECPubkey._from_libsecp256k1_pubkey_ptr(pubkey) 169 170 @classmethod 171 def from_signature65(cls, sig: bytes, msg_hash: bytes) -> Tuple['ECPubkey', bool]: 172 if len(sig) != 65: 173 raise Exception(f'wrong encoding used for signature? len={len(sig)} (should be 65)') 174 nV = sig[0] 175 if nV < 27 or nV >= 35: 176 raise Exception("Bad encoding") 177 if nV >= 31: 178 compressed = True 179 nV -= 4 180 else: 181 compressed = False 182 recid = nV - 27 183 return cls.from_sig_string(sig[1:], recid, msg_hash), compressed 184 185 @classmethod 186 def from_x_and_y(cls, x: int, y: int) -> 'ECPubkey': 187 _bytes = (b'\x04' 188 + int.to_bytes(x, length=32, byteorder='big', signed=False) 189 + int.to_bytes(y, length=32, byteorder='big', signed=False)) 190 return ECPubkey(_bytes) 191 192 def get_public_key_bytes(self, compressed=True): 193 if self.is_at_infinity(): raise Exception('point is at infinity') 194 x = int.to_bytes(self.x(), length=32, byteorder='big', signed=False) 195 y = int.to_bytes(self.y(), length=32, byteorder='big', signed=False) 196 if compressed: 197 header = b'\x03' if self.y() & 1 else b'\x02' 198 return header + x 199 else: 200 header = b'\x04' 201 return header + x + y 202 203 def get_public_key_hex(self, compressed=True): 204 return bh2u(self.get_public_key_bytes(compressed)) 205 206 def point(self) -> Tuple[int, int]: 207 return self.x(), self.y() 208 209 def x(self) -> int: 210 return self._x 211 212 def y(self) -> int: 213 return self._y 214 215 def _to_libsecp256k1_pubkey_ptr(self): 216 pubkey = create_string_buffer(64) 217 public_pair_bytes = self.get_public_key_bytes(compressed=False) 218 ret = _libsecp256k1.secp256k1_ec_pubkey_parse( 219 _libsecp256k1.ctx, pubkey, public_pair_bytes, len(public_pair_bytes)) 220 if not ret: 221 raise Exception('public key could not be parsed or is invalid') 222 return pubkey 223 224 @classmethod 225 def _from_libsecp256k1_pubkey_ptr(cls, pubkey) -> 'ECPubkey': 226 pubkey_serialized = create_string_buffer(65) 227 pubkey_size = c_size_t(65) 228 _libsecp256k1.secp256k1_ec_pubkey_serialize( 229 _libsecp256k1.ctx, pubkey_serialized, byref(pubkey_size), pubkey, SECP256K1_EC_UNCOMPRESSED) 230 return ECPubkey(bytes(pubkey_serialized)) 231 232 def __repr__(self): 233 if self.is_at_infinity(): 234 return f"<ECPubkey infinity>" 235 return f"<ECPubkey {self.get_public_key_hex()}>" 236 237 def __mul__(self, other: int): 238 if not isinstance(other, int): 239 raise TypeError('multiplication not defined for ECPubkey and {}'.format(type(other))) 240 241 other %= CURVE_ORDER 242 if self.is_at_infinity() or other == 0: 243 return POINT_AT_INFINITY 244 pubkey = self._to_libsecp256k1_pubkey_ptr() 245 246 ret = _libsecp256k1.secp256k1_ec_pubkey_tweak_mul(_libsecp256k1.ctx, pubkey, other.to_bytes(32, byteorder="big")) 247 if not ret: 248 return POINT_AT_INFINITY 249 return ECPubkey._from_libsecp256k1_pubkey_ptr(pubkey) 250 251 def __rmul__(self, other: int): 252 return self * other 253 254 def __add__(self, other): 255 if not isinstance(other, ECPubkey): 256 raise TypeError('addition not defined for ECPubkey and {}'.format(type(other))) 257 if self.is_at_infinity(): return other 258 if other.is_at_infinity(): return self 259 260 pubkey1 = self._to_libsecp256k1_pubkey_ptr() 261 pubkey2 = other._to_libsecp256k1_pubkey_ptr() 262 pubkey_sum = create_string_buffer(64) 263 264 pubkey1 = cast(pubkey1, c_char_p) 265 pubkey2 = cast(pubkey2, c_char_p) 266 array_of_pubkey_ptrs = (c_char_p * 2)(pubkey1, pubkey2) 267 ret = _libsecp256k1.secp256k1_ec_pubkey_combine(_libsecp256k1.ctx, pubkey_sum, array_of_pubkey_ptrs, 2) 268 if not ret: 269 return POINT_AT_INFINITY 270 return ECPubkey._from_libsecp256k1_pubkey_ptr(pubkey_sum) 271 272 def __eq__(self, other) -> bool: 273 if not isinstance(other, ECPubkey): 274 return False 275 return self.point() == other.point() 276 277 def __ne__(self, other): 278 return not (self == other) 279 280 def __hash__(self): 281 return hash(self.point()) 282 283 def __lt__(self, other): 284 if not isinstance(other, ECPubkey): 285 raise TypeError('comparison not defined for ECPubkey and {}'.format(type(other))) 286 return (self.x() or 0) < (other.x() or 0) 287 288 def verify_message_for_address(self, sig65: bytes, message: bytes, algo=lambda x: sha256d(msg_magic(x))) -> None: 289 assert_bytes(message) 290 h = algo(message) 291 public_key, compressed = self.from_signature65(sig65, h) 292 # check public key 293 if public_key != self: 294 raise Exception("Bad signature") 295 # check message 296 self.verify_message_hash(sig65[1:], h) 297 298 # TODO return bool instead of raising 299 def verify_message_hash(self, sig_string: bytes, msg_hash: bytes) -> None: 300 assert_bytes(sig_string) 301 if len(sig_string) != 64: 302 raise Exception(f'wrong encoding used for signature? len={len(sig_string)} (should be 64)') 303 if not (isinstance(msg_hash, bytes) and len(msg_hash) == 32): 304 raise Exception("msg_hash must be bytes, and 32 bytes exactly") 305 306 sig = create_string_buffer(64) 307 ret = _libsecp256k1.secp256k1_ecdsa_signature_parse_compact(_libsecp256k1.ctx, sig, sig_string) 308 if not ret: 309 raise Exception("Bad signature") 310 ret = _libsecp256k1.secp256k1_ecdsa_signature_normalize(_libsecp256k1.ctx, sig, sig) 311 312 pubkey = self._to_libsecp256k1_pubkey_ptr() 313 if 1 != _libsecp256k1.secp256k1_ecdsa_verify(_libsecp256k1.ctx, sig, msg_hash, pubkey): 314 raise Exception("Bad signature") 315 316 def encrypt_message(self, message: bytes, magic: bytes = b'BIE1') -> bytes: 317 """ 318 ECIES encryption/decryption methods; AES-128-CBC with PKCS7 is used as the cipher; hmac-sha256 is used as the mac 319 """ 320 assert_bytes(message) 321 322 ephemeral = ECPrivkey.generate_random_key() 323 ecdh_key = (self * ephemeral.secret_scalar).get_public_key_bytes(compressed=True) 324 key = hashlib.sha512(ecdh_key).digest() 325 iv, key_e, key_m = key[0:16], key[16:32], key[32:] 326 ciphertext = aes_encrypt_with_iv(key_e, iv, message) 327 ephemeral_pubkey = ephemeral.get_public_key_bytes(compressed=True) 328 encrypted = magic + ephemeral_pubkey + ciphertext 329 mac = hmac_oneshot(key_m, encrypted, hashlib.sha256) 330 331 return base64.b64encode(encrypted + mac) 332 333 @classmethod 334 def order(cls): 335 return CURVE_ORDER 336 337 def is_at_infinity(self): 338 return self == POINT_AT_INFINITY 339 340 @classmethod 341 def is_pubkey_bytes(cls, b: bytes): 342 try: 343 ECPubkey(b) 344 return True 345 except: 346 return False 347 348 349GENERATOR = ECPubkey(bytes.fromhex('0479be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798' 350 '483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8')) 351CURVE_ORDER = 0xFFFFFFFF_FFFFFFFF_FFFFFFFF_FFFFFFFE_BAAEDCE6_AF48A03B_BFD25E8C_D0364141 352POINT_AT_INFINITY = ECPubkey(None) 353 354 355def msg_magic(message: bytes) -> bytes: 356 from .bitcoin import var_int 357 length = bfh(var_int(len(message))) 358 return b"\x18Bitcoin Signed Message:\n" + length + message 359 360 361def verify_signature(pubkey: bytes, sig: bytes, h: bytes) -> bool: 362 try: 363 ECPubkey(pubkey).verify_message_hash(sig, h) 364 except: 365 return False 366 return True 367 368def verify_message_with_address(address: str, sig65: bytes, message: bytes, *, net=None): 369 from .bitcoin import pubkey_to_address 370 assert_bytes(sig65, message) 371 if net is None: net = constants.net 372 try: 373 h = sha256d(msg_magic(message)) 374 public_key, compressed = ECPubkey.from_signature65(sig65, h) 375 # check public key using the address 376 pubkey_hex = public_key.get_public_key_hex(compressed) 377 for txin_type in ['p2pkh','p2wpkh','p2wpkh-p2sh']: 378 addr = pubkey_to_address(txin_type, pubkey_hex, net=net) 379 if address == addr: 380 break 381 else: 382 raise Exception("Bad signature") 383 # check message 384 public_key.verify_message_hash(sig65[1:], h) 385 return True 386 except Exception as e: 387 _logger.info(f"Verification error: {repr(e)}") 388 return False 389 390 391def is_secret_within_curve_range(secret: Union[int, bytes]) -> bool: 392 if isinstance(secret, bytes): 393 secret = string_to_number(secret) 394 return 0 < secret < CURVE_ORDER 395 396 397class ECPrivkey(ECPubkey): 398 399 def __init__(self, privkey_bytes: bytes): 400 assert_bytes(privkey_bytes) 401 if len(privkey_bytes) != 32: 402 raise Exception('unexpected size for secret. should be 32 bytes, not {}'.format(len(privkey_bytes))) 403 secret = string_to_number(privkey_bytes) 404 if not is_secret_within_curve_range(secret): 405 raise InvalidECPointException('Invalid secret scalar (not within curve order)') 406 self.secret_scalar = secret 407 408 pubkey = GENERATOR * secret 409 super().__init__(pubkey.get_public_key_bytes(compressed=False)) 410 411 @classmethod 412 def from_secret_scalar(cls, secret_scalar: int): 413 secret_bytes = int.to_bytes(secret_scalar, length=32, byteorder='big', signed=False) 414 return ECPrivkey(secret_bytes) 415 416 @classmethod 417 def from_arbitrary_size_secret(cls, privkey_bytes: bytes): 418 """This method is only for legacy reasons. Do not introduce new code that uses it. 419 Unlike the default constructor, this method does not require len(privkey_bytes) == 32, 420 and the secret does not need to be within the curve order either. 421 """ 422 return ECPrivkey(cls.normalize_secret_bytes(privkey_bytes)) 423 424 @classmethod 425 def normalize_secret_bytes(cls, privkey_bytes: bytes) -> bytes: 426 scalar = string_to_number(privkey_bytes) % CURVE_ORDER 427 if scalar == 0: 428 raise Exception('invalid EC private key scalar: zero') 429 privkey_32bytes = int.to_bytes(scalar, length=32, byteorder='big', signed=False) 430 return privkey_32bytes 431 432 def __repr__(self): 433 return f"<ECPrivkey {self.get_public_key_hex()}>" 434 435 @classmethod 436 def generate_random_key(cls): 437 randint = randrange(CURVE_ORDER) 438 ephemeral_exponent = int.to_bytes(randint, length=32, byteorder='big', signed=False) 439 return ECPrivkey(ephemeral_exponent) 440 441 def get_secret_bytes(self) -> bytes: 442 return int.to_bytes(self.secret_scalar, length=32, byteorder='big', signed=False) 443 444 def sign(self, msg_hash: bytes, sigencode=None) -> bytes: 445 if not (isinstance(msg_hash, bytes) and len(msg_hash) == 32): 446 raise Exception("msg_hash to be signed must be bytes, and 32 bytes exactly") 447 if sigencode is None: 448 sigencode = sig_string_from_r_and_s 449 450 privkey_bytes = self.secret_scalar.to_bytes(32, byteorder="big") 451 nonce_function = None 452 sig = create_string_buffer(64) 453 def sign_with_extra_entropy(extra_entropy): 454 ret = _libsecp256k1.secp256k1_ecdsa_sign( 455 _libsecp256k1.ctx, sig, msg_hash, privkey_bytes, 456 nonce_function, extra_entropy) 457 if not ret: 458 raise Exception('the nonce generation function failed, or the private key was invalid') 459 compact_signature = create_string_buffer(64) 460 _libsecp256k1.secp256k1_ecdsa_signature_serialize_compact(_libsecp256k1.ctx, compact_signature, sig) 461 r = int.from_bytes(compact_signature[:32], byteorder="big") 462 s = int.from_bytes(compact_signature[32:], byteorder="big") 463 return r, s 464 465 r, s = sign_with_extra_entropy(extra_entropy=None) 466 counter = 0 467 while r >= 2**255: # grind for low R value https://github.com/bitcoin/bitcoin/pull/13666 468 counter += 1 469 extra_entropy = counter.to_bytes(32, byteorder="little") 470 r, s = sign_with_extra_entropy(extra_entropy=extra_entropy) 471 472 sig_string = sig_string_from_r_and_s(r, s) 473 self.verify_message_hash(sig_string, msg_hash) 474 475 sig = sigencode(r, s) 476 return sig 477 478 def sign_transaction(self, hashed_preimage: bytes) -> bytes: 479 return self.sign(hashed_preimage, sigencode=der_sig_from_r_and_s) 480 481 def sign_message(self, message: bytes, is_compressed: bool, algo=lambda x: sha256d(msg_magic(x))) -> bytes: 482 def bruteforce_recid(sig_string): 483 for recid in range(4): 484 sig65 = construct_sig65(sig_string, recid, is_compressed) 485 try: 486 self.verify_message_for_address(sig65, message, algo) 487 return sig65, recid 488 except Exception as e: 489 continue 490 else: 491 raise Exception("error: cannot sign message. no recid fits..") 492 493 message = to_bytes(message, 'utf8') 494 msg_hash = algo(message) 495 sig_string = self.sign(msg_hash, sigencode=sig_string_from_r_and_s) 496 sig65, recid = bruteforce_recid(sig_string) 497 return sig65 498 499 def decrypt_message(self, encrypted: Union[str, bytes], magic: bytes=b'BIE1') -> bytes: 500 encrypted = base64.b64decode(encrypted) # type: bytes 501 if len(encrypted) < 85: 502 raise Exception('invalid ciphertext: length') 503 magic_found = encrypted[:4] 504 ephemeral_pubkey_bytes = encrypted[4:37] 505 ciphertext = encrypted[37:-32] 506 mac = encrypted[-32:] 507 if magic_found != magic: 508 raise Exception('invalid ciphertext: invalid magic bytes') 509 try: 510 ephemeral_pubkey = ECPubkey(ephemeral_pubkey_bytes) 511 except InvalidECPointException as e: 512 raise Exception('invalid ciphertext: invalid ephemeral pubkey') from e 513 ecdh_key = (ephemeral_pubkey * self.secret_scalar).get_public_key_bytes(compressed=True) 514 key = hashlib.sha512(ecdh_key).digest() 515 iv, key_e, key_m = key[0:16], key[16:32], key[32:] 516 if mac != hmac_oneshot(key_m, encrypted[:-32], hashlib.sha256): 517 raise InvalidPassword() 518 return aes_decrypt_with_iv(key_e, iv, ciphertext) 519 520 521def construct_sig65(sig_string: bytes, recid: int, is_compressed: bool) -> bytes: 522 comp = 4 if is_compressed else 0 523 return bytes([27 + recid + comp]) + sig_string 524