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