first pass NFC pushtx

This commit is contained in:
Peter D. Gray 2024-06-03 14:07:40 -04:00
parent 9fe6ab13ef
commit 24bfca106a
No known key found for this signature in database
GPG Key ID: A2DCD558C2BE5D7C
8 changed files with 142 additions and 12 deletions

76
docs/nfc-pushtx.md Normal file
View File

@ -0,0 +1,76 @@
# NFC Push Tx
This feature allows single-tap broadcast of the freshly-signed transaction.
Once enabled with a URL, the COLDCARD will show the NFC animation
after signing the transaction. When the user taps their phone, the
phone will see an NFC tag with URL inside. That URL contains the
signed transaction ready to go, and once opening in the mobile
browser, that URL will load. The landing page will connect to a
Bitcoin node (or similar) and send the transaction on the public
Bitcoin network.
This feature is available on Q and Mk4 and requires NFC to be enabled.
See `Advanced/Tools > NFC Push Tx`
## Protocol Spec
The COLDCARD needs a URL prefix. To that it appends some values:
- `t=...`
- this is the transaction, in binary encoded with
[base64url](https://datatracker.ietf.org/doc/html/rfc4648#section-5)
- `&c=...`
- the rightmost 8 bytes of SHA256 over the transaction. Also `base64url` encoded.
- `&n=XTN`
- if, and only if, the COLDCARD is set for Testnet, this value is appended to
indicate that the transaction is for Testnet3 and not MainNet.
- when RegTest is enabled, the value will be `XRT`
We provide a few default URL values to our customers, including one backend we
will operate. The URL can also be directly entered by the customer. On the Q,
it can be scanned from a QR code.
For COLDCARD backend, the url used is:
https://coldcard.com/pushtx#
The complete URL with a typical transaction might look like this (but longer):
https://coldcard.com/pushtx#t=AgAAAAMNCxXtp2GVYVhkRXHLMmdZFs4p3kbFK ⋯ ABf&c=uiSVRda-1tw
We are using hash symbol here so that our server logs do not get
contaminated with the arguments. The landing page uses javascript
to read the hash part of the URL and decodes from there. If you
prefer, your URL can end with `?` and then the arguments will be
sent by the phone's browser to your server. Your processing can be
entirely done in the backend in this case.
## Expectations for the Backend
Your code should decode the transaction and check the SHA-256 hash
matches. If it does not match, or if `c` value is missing, assume
the URL has been truncated and report that to the user.
Once decoded, your code should immediately broadcast the transaction.
A confirmation step is not required in our opinion. Once it is
submitted to Bitcoin Core (or other API), any status response should
be decoded and shown to the user so they know it is on it's way.
If it was not accepted, please report the error to the user as
clearly as possible.
Next, it would make sense to either link to the TXID on a block
explorer to provide further proof that it has been sent and that
it is now waiting in the mempool.
### Notes
- Complete URL might be as large as 8,000 bytes. Some web servers will not support beyond
4k bytes and the NFC implementation of the phone may also have limits.
- The service URL provided must end in `?` or `#` or `&`.
- `base64url` values from COLDCARD will not have padding (`=` bytes) at end.
- Honest backends will not log the IP address of incoming transactions, but there is
no way to enforce that, and CloudFlare sees all.

View File

@ -4,6 +4,8 @@ This lists the new changes that have not yet been published in a normal release.
# Shared Improvements - Both Mk4 and Q
- New Feature: PushTX: when enabled with a service provider's URL, you can tap the COLDCARD
after signing with your phone and the transaction will be broadcast directly.
- Enhancement: Stricter p2sh-p2wpkh validation
- Enhancement: mention the need to remove old duress wallets before locking down temporary seed
- Bugfix: Fix PSBTv2 `PSBT_GLOBAL_TX_MODIFIABLE` parsing

View File

@ -2155,8 +2155,8 @@ async def _scan_any_qr(expect_secret=False, tmp=False):
PUSHTX_SUPPLIERS = [
# (label, URL)
('coldcard.com', 'https://coldcard.com/pushtx?' ),
('mempool.space', 'https://mempool.space/tx/push?tx=' ),
('coldcard.com', 'https://coldcard.com/pushtx#' ),
('mempool.space', 'https://mempool.space/tx/push?' ),
]
async def pushtx_setup_menu(*a):
@ -2203,8 +2203,14 @@ async def pushtx_setup_menu(*a):
while 1:
nv = await ux_input_text(val, confirm_exit=True, scan_ok=True, prompt="Enter URL")
# cleanup? URL validation?
if nv and ('http://' not in nv and 'https://' not in nv) or len(nv) < 12:
await ux_show_story("Must start with http:// or https://. Try again")
if nv:
if ('http://' not in nv and 'https://' not in nv):
prob = "Must start with http:// or https://."
elif len(nv) < 12:
prob = "Too short."
if nv[-1] not in '#?&':
prob = "Final char must be # or ? or &."
await ux_show_story(prob + " Try again.")
val = nv
continue
break

View File

@ -806,9 +806,20 @@ class ApproveTransaction(UserAuthorizedAction):
except BaseException as exc:
return await self.failure("PSBT output failed", exc)
from glob import NFC
from glob import NFC, settings
if self.do_finalize and txid and not hsm_active:
# if NFC PushTx is enabled, do that w/o questions.
url = settings.get('ptxurl', False)
if NFC and url:
try:
await NFC.share_push_tx(url, txid, TXN_OUTPUT_OFFSET, self.result[0], self.result[1])
return
except:
# continue normally if it fails, perhaps too big?
pass
kq, kn = "(1)", "(3)"
if version.has_qwerty:
kq, kn = KEY_QR, KEY_NFC

View File

@ -235,7 +235,7 @@ class Display:
ch = (h+CELL_H) // CELL_H
#print('pixel %dx%d @ (%d,%d) => %dx%d @ (%d,%d)' % (w, h, px,py, cw, ch, cx,cy))
for y in range(cy, cy+ch+1):
for y in range(cy, cy+ch):
for x in range(cx, cx+cw+1):
try:
self.last_buf[y][x] = self.next_buf[y][x] = 0xfffe

View File

@ -239,6 +239,39 @@ class NFCHandler:
return await self.share_start(n)
async def share_push_tx(self, url, txid, file_offset, txn_len, txn_sha):
# Given a signed TXN, we convert to URL which a web backend can broadcast directly
# - using base64url encoding
# - just appends to provided URL
# - keeps showing it until they press CANCEL
#
if (txn_len * 1.4) >= MAX_NFC_SIZE:
raise ValueError('too big')
from glob import PSRAM
from utils import b2a_base64url
from chains import current_chain
is_https = url.startswith('https://')
if is_https:
url = url[8:]
url += 't=' + b2a_base64url(PSRAM.read_at(file_offset, txn_len)) \
+ '&c=' + b2a_base64url(txn_sha[-8:])
ch = current_chain()
if ch.ctype != 'BTC':
url += '&n=' + ch.ctype # XTN or XRT
n = ndef.ndefMaker()
n.add_url(url, https=is_https)
while 1:
done = await self.share_start(n, prompt="Tap to broadcast, CANCEL when done",
line2="Signed TXID: %s%s" % (txid[0:8], txid[-8:]))
if done: break
async def share_psbt(self, file_offset, psbt_len, psbt_sha, label=None):
# we just signed something, share it over NFC
if psbt_len >= MAX_NFC_SIZE:
@ -297,7 +330,7 @@ class NFCHandler:
self.write_dyn(GPO_CTRL_Dyn, 0x01) # GPO_EN
self.read_dyn(IT_STS_Dyn) # clear interrupt
async def ux_animation(self, write_mode, allow_enter=True):
async def ux_animation(self, write_mode, allow_enter=True, prompt=None, line2=None):
# Run the pretty animation, and detect both when we are written, and/or key to exit/abort.
# - similar when "read" and then removed from field
# - return T if aborted by user
@ -309,7 +342,9 @@ class NFCHandler:
if dis.has_lcd:
dis.real_clear() # bugfix
dis.text(None, -2, 'Tap phone to screen, or CANCEL.', dark=True)
dis.text(None, -2, prompt or 'Tap phone to screen, or CANCEL.', dark=True)
if line2:
dis.text(None, -3, line2)
else:
from graphics_mk4 import Graphics
frames = [getattr(Graphics, 'mk4_nfc_%d'%i) for i in range(1, 5)]

View File

@ -180,7 +180,7 @@ class Base64Writer:
def b2a_base64url(s):
# see <https://datatracker.ietf.org/doc/html/rfc4648#section-5>
return b2a_base64(s).rstrip(b'=\n').replace(b'+', b'-').replace(b'/', b'_')
return b2a_base64(s).rstrip(b'=\n').replace(b'+', b'-').replace(b'/', b'_').decode()
def swab32(n):
# endian swap: 32 bits

View File

@ -2,12 +2,12 @@
//
// AUTO-generated.
//
// built: 2024-05-08
// version: 1.2.1Q
// built: 2024-06-03
// version: 1.2.2Q
//
#include <stdint.h>
// this overrides ports/stm32/fatfs_port.c
uint32_t get_fattime(void) {
return 0x58a80840UL;
return 0x58c30840UL;
}