def sign_message(self, sequence, message, password): sig = None try: message = message.encode('utf8') inputPath = self.get_derivation() + "/%d/%d" % sequence msg_hash = Hash(msg_magic(message)) inputHash = to_hexstr(msg_hash) hasharray = [] hasharray.append({'hash': inputHash, 'keypath': inputPath}) hasharray = json.dumps(hasharray) msg = b'{"sign":{"meta":"sign message", "data":%s}}' % hasharray.encode('utf8') dbb_client = self.plugin.get_client(self) if not dbb_client.is_paired(): raise Exception(_("Could not sign message.")) reply = dbb_client.hid_send_encrypt(msg) self.handler.show_message(_("Signing message ...") + "\n\n" + _("To continue, touch the Digital Bitbox's blinking light for 3 seconds.") + "\n\n" + _("To cancel, briefly touch the blinking light or wait for the timeout.")) reply = dbb_client.hid_send_encrypt(msg) # Send twice, first returns an echo for smart verification (not implemented) self.handler.finished() if 'error' in reply: raise Exception(reply['error']['message']) if 'sign' not in reply: raise Exception(_("Could not sign message.")) if 'recid' in reply['sign'][0]: # firmware > v2.1.1 sig = bytes([27 + int(reply['sign'][0]['recid'], 16) + 4]) + binascii.unhexlify(reply['sign'][0]['sig']) pk, compressed = pubkey_from_signature(sig, msg_hash) pk = point_to_ser(pk.pubkey.point, compressed) addr = public_key_to_p2pkh(pk) if verify_message(addr, sig, message) is False: raise Exception(_("Could not sign message")) elif 'pubkey' in reply['sign'][0]: # firmware <= v2.1.1 for i in range(4): sig = bytes([27 + i + 4]) + binascii.unhexlify(reply['sign'][0]['sig']) try: addr = public_key_to_p2pkh(binascii.unhexlify(reply['sign'][0]['pubkey'])) if verify_message(addr, sig, message): break except Exception: continue else: raise Exception(_("Could not sign message")) except BaseException as e: self.give_error(e) return sig
def verify_signature(self, signature, message, verification_key): "This method verifies signature of message" try: pk, compressed = pubkey_from_signature(signature, Hash(msg_magic(message))) pubkey = point_to_ser(pk.pubkey.point, compressed).hex() return pubkey == verification_key except Exception as e: self.print_error("verify_signature:", repr(e)) return False
def verify_tx_signature(signature, transaction, verification_key, utxo): '''Verify the signature for a specific utxo ("prevout_hash:n") given a transaction and verification key. Ensures that the signature is valid AND canonically encoded, so it will be accepted by network. ''' tx_num = None for n, x in enumerate(transaction.inputs()): if (verification_key in x['pubkeys'] and utxo == "{}:{}".format(x['tx_hash'], x['tx_pos'])): tx_num = n break else: # verification_key / utxo combo not found in tx inputs, bail return False # calculate sighash digest (implicitly this is for sighash 0x41) pre_hash = Hash(bfh(transaction.serialize_preimage(tx_num))) order = generator_secp256k1.order() try: sigbytes = bfh(signature.decode()) except ValueError: # not properly hex encoded or UnicodeDecodeError (garbage data) return False if not sigbytes or sigbytes[-1] != 0x41: return False DERsig = sigbytes[: -1] # lop off the sighash byte for the DER check below try: # ensure DER encoding is canonical, and extract r,s if OK r, s = CoinUtils.IsValidDERSignatureEncoding_With_Extract(DERsig) except AssertionError: return False if (s << 1) > order: # high S values are rejected by ERG network return False try: pubkey_point = ser_to_point(bfh(verification_key)) except: # ser_to_point will fail if pubkey is off-curve, infinity, or garbage. return False vk = MyVerifyingKey.from_public_point(pubkey_point, curve=SECP256k1) try: return vk.verify_digest(DERsig, pre_hash, sigdecode=ecdsa.util.sigdecode_der) except: # verify_digest returns True on success, otherwise raises return False
def get_transaction_signature(transaction, inputs, secret_keys): "get transaction signature" signatures = {} for txin in transaction.inputs(): pubkey = txin['pubkeys'][0] if pubkey in inputs: tx_num = transaction.inputs().index(txin) pre_hash = Hash(bfh(transaction.serialize_preimage(tx_num))) private_key = MySigningKey.from_secret_exponent( secret_keys[pubkey].secret, curve=SECP256k1) public_key = private_key.get_verifying_key() sig = private_key.sign_digest_deterministic( pre_hash, hashfunc=hashlib.sha256, sigencode=ecdsa.util.sigencode_der) assert public_key.verify_digest( sig, pre_hash, sigdecode=ecdsa.util.sigdecode_der) signatures[txin['tx_hash'] + ":" + str(txin['tx_pos'])] = (bh2u(sig) + '41').encode('utf-8') return signatures
def sign_transaction(self, tx, password, *, use_cache=False): if tx.is_complete(): return try: p2pkhTransaction = True derivations = self.get_tx_derivations(tx) inputhasharray = [] hasharray = [] pubkeyarray = [] # Build hasharray from inputs for i, txin in enumerate(tx.inputs()): if txin['type'] == 'coinbase': self.give_error("Coinbase not supported") # should never happen if txin['type'] != 'p2pkh': p2pkhTransaction = False for x_pubkey in txin['x_pubkeys']: if x_pubkey in derivations: index = derivations.get(x_pubkey) inputPath = "%s/%d/%d" % (self.get_derivation(), index[0], index[1]) inputHash = Hash(binascii.unhexlify(tx.serialize_preimage(i))) hasharray_i = {'hash': to_hexstr(inputHash), 'keypath': inputPath} hasharray.append(hasharray_i) inputhasharray.append(inputHash) break else: self.give_error("No matching x_key for sign_transaction") # should never happen # Build pubkeyarray from outputs for _type, address, amount in tx.outputs(): info = tx.output_info.get(address) if info is not None: index, xpubs, m, script_type = info changePath = self.get_derivation() + "/%d/%d" % index changePubkey = self.derive_pubkey(index[0], index[1]) pubkeyarray_i = {'pubkey': changePubkey, 'keypath': changePath} pubkeyarray.append(pubkeyarray_i) # Special serialization of the unsigned transaction for # the mobile verification app. # At the moment, verification only works for p2pkh transactions. if p2pkhTransaction: class CustomTXSerialization(Transaction): @classmethod def input_script(self, txin, estimate_size=False, sign_schnorr=False): if txin['type'] == 'p2pkh': return Transaction.get_preimage_script(txin) if txin['type'] == 'p2sh': # Multisig verification has partial support, but is disabled. This is the # expected serialization though, so we leave it here until we activate it. return '00' + push_script(Transaction.get_preimage_script(txin)) raise Exception("unsupported type %s" % txin['type']) tx_dbb_serialized = CustomTXSerialization(tx.serialize()).serialize() else: # We only need this for the signing echo / verification. tx_dbb_serialized = None # Build sign command dbb_signatures = [] steps = math.ceil(1.0 * len(hasharray) / self.maxInputs) for step in range(int(steps)): hashes = hasharray[step * self.maxInputs : (step + 1) * self.maxInputs] msg = { "sign": { "data": hashes, "checkpub": pubkeyarray, }, } if tx_dbb_serialized is not None: msg["sign"]["meta"] = to_hexstr(Hash(tx_dbb_serialized)) msg = json.dumps(msg).encode('ascii') dbb_client = self.plugin.get_client(self) if not dbb_client.is_paired(): raise Exception("Could not sign transaction.") reply = dbb_client.hid_send_encrypt(msg) if 'error' in reply: raise Exception(reply['error']['message']) if 'echo' not in reply: raise Exception("Could not sign transaction.") if self.plugin.is_mobile_paired() and tx_dbb_serialized is not None: reply['tx'] = tx_dbb_serialized self.plugin.comserver_post_notification(reply) if steps > 1: self.handler.show_message(_("Signing large transaction. Please be patient ...") + "\n\n" + _("To continue, touch the Digital Bitbox's blinking light for 3 seconds.") + " " + _("(Touch {} of {})").format((step + 1), steps) + "\n\n" + _("To cancel, briefly touch the blinking light or wait for the timeout.") + "\n\n") else: self.handler.show_message(_("Signing transaction...") + "\n\n" + _("To continue, touch the Digital Bitbox's blinking light for 3 seconds.") + "\n\n" + _("To cancel, briefly touch the blinking light or wait for the timeout.")) # Send twice, first returns an echo for smart verification reply = dbb_client.hid_send_encrypt(msg) self.handler.finished() if 'error' in reply: if reply["error"].get('code') in (600, 601): # aborted via LED short touch or timeout raise UserCancelled() raise Exception(reply['error']['message']) if 'sign' not in reply: raise Exception("Could not sign transaction.") dbb_signatures.extend(reply['sign']) # Fill signatures if len(dbb_signatures) != len(tx.inputs()): raise Exception("Incorrect number of transactions signed.") # Should never occur for i, txin in enumerate(tx.inputs()): num = txin['num_sig'] for pubkey in txin['pubkeys']: signatures = list(filter(None, txin['signatures'])) if len(signatures) == num: break # txin is complete ii = txin['pubkeys'].index(pubkey) signed = dbb_signatures[i] if 'recid' in signed: # firmware > v2.1.1 recid = int(signed['recid'], 16) s = binascii.unhexlify(signed['sig']) h = inputhasharray[i] pk = MyVerifyingKey.from_signature(s, recid, h, curve = SECP256k1) pk = to_hexstr(point_to_ser(pk.pubkey.point, True)) elif 'pubkey' in signed: # firmware <= v2.1.1 pk = signed['pubkey'] if pk != pubkey: continue sig_r = int(signed['sig'][:64], 16) sig_s = int(signed['sig'][64:], 16) sig = sigencode_der(sig_r, sig_s, generator_secp256k1.order()) txin['signatures'][ii] = to_hexstr(sig) + '41' tx._inputs[i] = txin except UserCancelled: raise except BaseException as e: self.give_error(e, True) else: print_error("Transaction is_complete", tx.is_complete()) tx.raw = tx.serialize()
def derive_keys(x): h = Hash(x) h = hashlib.sha512(h).digest() return (h[:32],h[32:])
def hash(text): ''' Returns sha256(sha256(text)) as bytes. text may be bytes or str. ''' return Hash(text) # bitcoin.Hash is sha256(sha256(x))
def sign_transaction(self, tx, password, *, use_cache=False): self.print_error('sign_transaction(): tx: '+ str(tx)) #debugSatochip client = self.get_client() # outputs txOutputs= ''.join(tx.serialize_output(o) for o in tx.outputs()) hashOutputs = bh2u(Hash(bfh(txOutputs))) txOutputs = var_int(len(tx.outputs()))+txOutputs self.print_error('sign_transaction(): hashOutputs= ', hashOutputs) #debugSatochip self.print_error('sign_transaction(): outputs= ', txOutputs) #debugSatochip # Fetch inputs of the transaction to sign derivations = self.get_tx_derivations(tx) for i,txin in enumerate(tx.inputs()): self.print_error('sign_transaction(): input =', i) #debugSatochip self.print_error('sign_transaction(): input[type]:', txin['type']) #debugSatochip if txin['type'] == 'coinbase': self.give_error("Coinbase not supported") # should never happen if txin['type'] in ['p2sh']: p2shTransaction = True pubkeys, x_pubkeys = tx.get_sorted_pubkeys(txin) for j, x_pubkey in enumerate(x_pubkeys): self.print_error('sign_transaction(): forforloop: j=', j) #debugSatochip if tx.is_txin_complete(txin): break if x_pubkey in derivations: signingPos = j s = derivations.get(x_pubkey) address_path = "%s/%d/%d" % (self.get_derivation()[2:], s[0], s[1]) # get corresponing extended key (depth, bytepath)= bip32path2bytes(address_path) (key, chaincode)=client.cc.card_bip32_get_extendedkey(bytepath) # parse tx pre_tx_hex= tx.serialize_preimage(i) pre_tx= bytes.fromhex(pre_tx_hex)# hex representation => converted to bytes pre_hash = Hash(bfh(pre_tx_hex)) pre_hash_hex= pre_hash.hex() self.print_error('sign_transaction(): pre_tx_hex=', pre_tx_hex) #debugSatochip self.print_error('sign_transaction(): pre_hash=', pre_hash_hex) #debugSatochip #(response, sw1, sw2) = client.cc.card_parse_transaction(pre_tx, True) # use 'True' since ERG use BIP143 as in Segwit... #print_error('[satochip] sign_transaction(): response= '+str(response)) #debugSatochip #(tx_hash, needs_2fa) = client.parser.parse_parse_transaction(response) (response, sw1, sw2, tx_hash, needs_2fa) = client.cc.card_parse_transaction(pre_tx, True) # use 'True' since ERG use BIP143 as in Segwit... tx_hash_hex= bytearray(tx_hash).hex() if pre_hash_hex!= tx_hash_hex: raise RuntimeError(f"[Satochip_KeyStore] Tx preimage mismatch: {pre_hash_hex} vs {tx_hash_hex}") # sign tx keynbr= 0xFF #for extended key if needs_2fa: # format & encrypt msg import json coin_type= 145 #see https://github.com/satoshilabs/slips/blob/master/slip-0044.md test_net= networks.net.TESTNET msg= {'action':"sign_tx", 'tx':pre_tx_hex, 'ct':coin_type, 'sw':True, 'tn':test_net, 'txo':txOutputs, 'ty':txin['type']} msg= json.dumps(msg) (id_2FA, msg_out)= client.cc.card_crypt_transaction_2FA(msg, True) d={} d['msg_encrypt']= msg_out d['id_2FA']= id_2FA # self.print_error("encrypted message: "+msg_out) self.print_error("id_2FA:", id_2FA) #do challenge-response with 2FA device... client.handler.show_message('2FA request sent! Approve or reject request on your second device.') Satochip2FA.do_challenge_response(d) # decrypt and parse reply to extract challenge response try: reply_encrypt= None # init it in case of exc below reply_encrypt= d['reply_encrypt'] except Exception as e: # Note: give_error here will raise again.. :/ self.give_error("No response received from 2FA.\nPlease ensure that the Satochip-2FA plugin is enabled in Tools>Optional Features", True) break if reply_encrypt is None: #todo: abort tx break reply_decrypt= client.cc.card_crypt_transaction_2FA(reply_encrypt, False) self.print_error("challenge:response=", reply_decrypt) reply_decrypt= reply_decrypt.split(":") rep_pre_hash_hex= reply_decrypt[0][0:64] if rep_pre_hash_hex!= pre_hash_hex: #todo: abort tx or retry? self.print_error("Abort transaction: tx mismatch:",rep_pre_hash_hex,"!=",pre_hash_hex) break chalresponse=reply_decrypt[1] if chalresponse=="00"*20: #todo: abort tx? self.print_error("Abort transaction: rejected by 2FA!") break chalresponse= list(bytes.fromhex(chalresponse)) else: chalresponse= None (tx_sig, sw1, sw2) = client.cc.card_sign_transaction(keynbr, tx_hash, chalresponse) #self.print_error('sign_transaction(): sig=', bytes(tx_sig).hex()) #debugSatochip #todo: check sw1sw2 for error (0x9c0b if wrong challenge-response) # enforce low-S signature (BIP 62) tx_sig = bytearray(tx_sig) r,s= get_r_and_s_from_der_sig(tx_sig) if s > CURVE_ORDER//2: s = CURVE_ORDER - s tx_sig=der_sig_from_r_and_s(r, s) #update tx with signature tx_sig = tx_sig.hex()+'41' #tx.add_signature_to_txin(i,j,tx_sig) txin['signatures'][j] = tx_sig break else: self.give_error("No matching x_key for sign_transaction") # should never happen self.print_error("is_complete", tx.is_complete()) tx.raw = tx.serialize() return