1import io 2import re 3 4from binascii import b2a_base64, a2b_base64 5 6from pycoin.intbytes import byte2int, int2byte 7from pycoin.satoshi.satoshi_string import stream_satoshi_string 8 9from ..encoding.bytes32 import to_bytes_32, from_bytes_32 10from ..encoding.exceptions import EncodingError 11from ..encoding.hash import double_sha256 12from ..encoding.sec import public_pair_to_hash160_sec 13 14 15class MessageSigner(object): 16 17 # According to brainwallet, this is "inputs.io" format, but it seems practical 18 # and is deployed in the wild. Core bitcoin doesn't offer a message wrapper like this. 19 signature_template = ('-----BEGIN {net_name} SIGNED MESSAGE-----\n{msg}\n-----BEGIN ' 20 'SIGNATURE-----\n{addr}\n{sig}\n-----END {net_name} SIGNED MESSAGE-----') 21 22 def __init__(self, network, generator): 23 self._network = network 24 self._network_name = network.network_name 25 self._generator = generator 26 27 @classmethod 28 def parse_sections(class_, msg_in): 29 # Convert to Unix line feeds from DOS style, iff we find them, but 30 # restore to same at the end. The RFC implies we should be using 31 # DOS \r\n in the message, but that does not always happen in today's 32 # world of MacOS and Linux devs. A mix of types will not work here. 33 dos_nl = ('\r\n' in msg_in) 34 if dos_nl: 35 msg_in = msg_in.replace('\r\n', '\n') 36 37 try: 38 # trim any junk in front 39 _, body = msg_in.split('SIGNED MESSAGE-----\n', 1) 40 except ValueError: 41 raise EncodingError("expecting text SIGNED MESSSAGE somewhere") 42 43 # - sometimes middle sep is BEGIN BITCOIN SIGNATURE, other times just BEGIN SIGNATURE 44 # - choose the last instance, in case someone signs a signed message 45 parts = re.split('\n-----BEGIN [A-Z ]*SIGNATURE-----\n', body) 46 if len(parts) < 2: 47 raise EncodingError("expected BEGIN SIGNATURE line", body) 48 msg, hdr = ''.join(parts[:-1]), parts[-1] 49 50 if dos_nl: 51 msg = msg.replace('\n', '\r\n') 52 53 return msg, hdr 54 55 @classmethod 56 def parse_signed_message(class_, msg_in): 57 """ 58 Take an "armoured" message and split into the message body, signing address 59 and the base64 signature. Should work on all altcoin networks, and should 60 accept both Inputs.IO and Multibit formats but not Armory. 61 62 Looks like RFC2550 <https://www.ietf.org/rfc/rfc2440.txt> was an "inspiration" 63 for this, so in case of confusion it's a reference, but I've never found 64 a real spec for this. Should be a BIP really. 65 """ 66 67 msg, hdr = class_.parse_sections(msg_in) 68 69 # after message, expect something like an email/http headers, so split into lines 70 hdr = list(filter(None, [i.strip() for i in hdr.split('\n')])) 71 72 if '-----END' not in hdr[-1]: 73 raise EncodingError("expecting END on last line") 74 75 sig = hdr[-2] 76 addr = None 77 for line in hdr: 78 line = line.strip() 79 if not line: 80 continue 81 82 if line.startswith('-----END'): 83 break 84 85 if ':' in line: 86 label, value = [i.strip() for i in line.split(':', 1)] 87 88 if label.lower() == 'address': 89 addr = line.split(':')[1].strip() 90 break 91 92 continue 93 94 addr = line 95 break 96 97 if not addr or addr == sig: 98 raise EncodingError("Could not find address") 99 100 return msg, addr, sig 101 102 def signature_for_message_hash(self, secret_exponent, msg_hash, is_compressed): 103 """ 104 Return a signature, encoded in Base64, of msg_hash. 105 """ 106 r, s, recid = self._generator.sign_with_recid(secret_exponent, msg_hash) 107 108 # See http://bitcoin.stackexchange.com/questions/14263 and key.cpp 109 # for discussion of the proprietary format used for the signature 110 111 first = 27 + recid + (4 if is_compressed else 0) 112 sig = b2a_base64(int2byte(first) + to_bytes_32(r) + to_bytes_32(s)).strip() 113 sig = sig.decode("utf8") 114 return sig 115 116 def sign_message(self, key, message, verbose=False): 117 """ 118 Return a signature, encoded in Base64, which can be verified by anyone using the 119 public key. 120 """ 121 secret_exponent = key.secret_exponent() 122 if not secret_exponent: 123 raise ValueError("Private key is required to sign a message") 124 125 addr = key.address() 126 127 msg_hash = self.hash_for_signing(message) 128 is_compressed = key.is_compressed() 129 130 sig = self.signature_for_message_hash(secret_exponent, msg_hash, is_compressed) 131 132 if not verbose or message is None: 133 return sig 134 135 return self.signature_template.format( 136 msg=message, sig=sig, addr=addr, 137 net_name=self._network_name.upper()) 138 139 def pair_for_message_hash(self, signature, msg_hash): 140 """ 141 Take a signature, encoded in Base64, and return the pair it was signed by. 142 May raise EncodingError (from _decode_signature) 143 """ 144 145 # Decode base64 and a bitmask in first byte. 146 is_compressed, recid, r, s = self._decode_signature(signature) 147 148 # Calculate the specific public key used to sign this message. 149 y_parity = recid & 1 150 q = self._generator.possible_public_pairs_for_signature(msg_hash, (r, s), y_parity=y_parity)[0] 151 if recid > 1: 152 order = self._generator.order() 153 q = self._generator.Point(q[0] + order, q[1]) 154 return q, is_compressed 155 156 def pair_matches_key(self, pair, key, is_compressed): 157 # Check signing public pair is the one expected for the signature. It must be an 158 # exact match for this key's public pair... or else we are looking at a validly 159 # signed message, but signed by some other key. 160 # 161 if hasattr(key, "public_pair"): 162 # expect an exact match for public pair. 163 return key.public_pair() == pair 164 else: 165 # Key() constructed from a hash of pubkey doesn't know the exact public pair, so 166 # must compare hashed addresses instead. 167 key_hash160 = key.hash160() 168 pair_hash160 = public_pair_to_hash160_sec(pair, compressed=is_compressed) 169 return key_hash160 == pair_hash160 170 171 def verify_message(self, key_or_address, signature, message=None, msg_hash=None): 172 """ 173 Take a signature, encoded in Base64, and verify it against a 174 key object (which implies the public key), 175 or a specific base58-encoded pubkey hash. 176 """ 177 if isinstance(key_or_address, str): 178 # they gave us a private key or a public key already loaded. 179 key = self._network.parse.address(key_or_address) 180 else: 181 key = key_or_address 182 183 try: 184 msg_hash = self.hash_for_signing(message) if message is not None else msg_hash 185 pair, is_compressed = self.pair_for_message_hash(signature, msg_hash) 186 except EncodingError: 187 return False 188 return self.pair_matches_key(pair, key, is_compressed) 189 190 def msg_magic_for_netcode(self): 191 """ 192 We need the constant "strMessageMagic" in C++ source code, from file "main.cpp" 193 194 It is not shown as part of the signed message, but it is prefixed to the message 195 as part of calculating the hash of the message (for signature). It's also what 196 prevents a message signature from ever being a valid signature for a transaction. 197 198 Each altcoin finds and changes this string... But just simple substitution. 199 """ 200 return '%s Signed Message:\n' % self._network_name 201 202 def _decode_signature(self, signature): 203 """ 204 Decode the internal fields of the base64-encoded signature. 205 """ 206 207 sig = a2b_base64(signature) 208 if len(sig) != 65: 209 raise EncodingError("Wrong length, expected 65") 210 211 # split into the parts. 212 first = byte2int(sig) 213 r = from_bytes_32(sig[1:33]) 214 s = from_bytes_32(sig[33:33+32]) 215 216 # first byte encodes a bits we need to know about the point used in signature 217 if not (27 <= first < 35): 218 raise EncodingError("First byte out of range") 219 220 # NOTE: The first byte encodes the "recovery id", or "recid" which is a 3-bit values 221 # which selects compressed/not-compressed and one of 4 possible public pairs. 222 # 223 first -= 27 224 is_compressed = bool(first & 0x4) 225 226 return is_compressed, (first & 0x3), r, s 227 228 def hash_for_signing(self, msg): 229 """ 230 Return a hash of msg, according to odd bitcoin method: double SHA256 over a bitcoin 231 encoded stream of two strings: a fixed magic prefix and the actual message. 232 """ 233 magic = self.msg_magic_for_netcode() 234 235 fd = io.BytesIO() 236 stream_satoshi_string(fd, magic.encode('utf8')) 237 stream_satoshi_string(fd, msg.encode('utf8')) 238 239 # return as a number, since it's an input to signing algos like that anyway 240 return from_bytes_32(double_sha256(fd.getvalue())) 241