diff --git a/testing/requirements.txt b/testing/requirements.txt index e0e1a6ff..3db8695e 100644 --- a/testing/requirements.txt +++ b/testing/requirements.txt @@ -23,3 +23,5 @@ git+https://github.com/coinkite/bsms-bitcoin-secure-multisig-setup.git@master#eg # BBQr library git+https://github.com/coinkite/BBQr.git@master#egg=bbqr&subdirectory=python +# for backend testing +requests==2.32.3 diff --git a/testing/test_ccc.py b/testing/test_ccc.py new file mode 100644 index 00000000..678ef30f --- /dev/null +++ b/testing/test_ccc.py @@ -0,0 +1,121 @@ +# (c) Copyright 2024 by Coinkite Inc. This file is covered by license found in COPYING-CC. +# +# tests related to CCC feature +# +# +import pytest, requests, re, time, random +from binascii import a2b_hex, b2a_hex +from base64 import urlsafe_b64encode +from urllib.parse import urlparse, parse_qs +from onetimepass import get_totp +from helpers import prandom + +# TODO: we will rotate the server key before release. +SERVER_PUBKEY = '036d0f95c3aaf5cd3e8be561b07814fbb1c9ee2171ed301828151975411472a2fd' + +def make_session_key(his_pubkey=None): + # - second call: given the pubkey of far side, calculate the shared pt on curve + # - creates session key based on that + from ecdsa.curves import SECP256k1 + from ecdsa import VerifyingKey, SigningKey + from ecdsa.util import number_to_string + from hashlib import sha256 + + my_key = SigningKey.generate(curve=SECP256k1, hashfunc=sha256) + + his_pubkey = VerifyingKey.from_string(bytes.fromhex(SERVER_PUBKEY), + curve=SECP256k1, hashfunc=sha256) + + # do the D-H thing + pt = my_key.privkey.secret_multiplier * his_pubkey.pubkey.point + + # final key is sha256 of that point, serialized (64 bytes). + order = SECP256k1.order + kk = number_to_string(pt.x(), order) + number_to_string(pt.y(), order) + + return sha256(kk).digest(), my_key.get_verifying_key().to_string('compressed') + + +@pytest.fixture +def make_2fa_url(): + def doit(shared_secret=b'A'*16, nonce='12345678', + wallet='Example wallet name', is_q=0, prod=True, encrypted=False): + + base = 'http://127.0.0.1:5070/2fa?' if not prod else 'https://coldcard.com/2fa?' + + assert is_q in {0, 1} + assert len(shared_secret) == 16 # base32 + assert isinstance(nonce, str) # hex digits or 8 dec digits in Mk4 mode + + from urllib.parse import quote + + qs = f'ss={shared_secret}&q={is_q}&g={nonce}&nm={quote(wallet)}' + + print(f'2fa URL: {qs}') + + if not encrypted: + return base + qs + + # pick eph key + ses_key, pubkey = make_session_key() + + import pyaes + enc = pyaes.AESModeOfOperationCTR(ses_key, pyaes.Counter(0)).encrypt + + qs = urlsafe_b64encode(pubkey + enc(qs.encode('ascii'))) + + return base + qs.decode('ascii') + + return doit + +@pytest.fixture +def roundtrip_2fa(): + def doit(url, shared_secret, local=False): + if local: + url = url.replace('https://coldcard.com/', 'http://127.0.0.1:5070/') + + if int(time.time() % 30) > 29: + # avoid end of time period + time.sleep(3) + + answer = '%06d' % get_totp(shared_secret) + assert len(answer) == 6 + + resp = requests.post(url, data=dict(answer=answer)) + + # server HTML will have this line in response for our use + # + + if '