spending policy implemented
This commit is contained in:
parent
28ba1adce3
commit
f12457cbb5
@ -1,12 +1,43 @@
|
||||
|
||||
# Spending Policy
|
||||
|
||||
A special mode where your coldcard will stop you from signing transactions if
|
||||
they exceed a spending policy you define beforehand.
|
||||
|
||||
# Tips and Tricks
|
||||
|
||||
## Money Manager Mode
|
||||
|
||||
## Tips and Tricks
|
||||
You could setup a Coldcard for another person, perhaps a family member,
|
||||
and enable web 2FA authentication. There does not need to be any
|
||||
other spending policy limits (velocity could be unlimited).
|
||||
|
||||
Then enrol your own phone with the required 2FA values, and
|
||||
keep both that and the spending policy bypass PIN confidential.
|
||||
|
||||
The holder the the Coldcard will need a 2FA code from your phone
|
||||
when they want to spend. They can call you for the 6-digit code
|
||||
from the 2FA app on your phone. This is not hard to provide over a
|
||||
voice call.
|
||||
|
||||
Because a spending policy is in effect, they will not be able to
|
||||
see the seed words, other private key material, so regardless of
|
||||
any spoofing or phishing, they cannot move funds without your help.
|
||||
|
||||
You should record the bypass PIN in a safe way, so it can be revelaed
|
||||
should you die. You do not need to share the risks associated with
|
||||
holding a copy of their seed words.
|
||||
|
||||
## Lock Out Changes to Policy
|
||||
|
||||
You may go into the Trick Pin menu, find the Bypass PIN there. You
|
||||
could delete or "hide" it. Hiding it is pointless since you cannot
|
||||
get to the trick PIN while the policy is in effect. Deleting the
|
||||
PIN however, is useful because it assures changes to spending policy
|
||||
are impossible. To recover the COLDCARD when this move is later
|
||||
regretted, under Advanced, there is "Destroy Seed" option which
|
||||
will clear the seed words and all settings, including the spending policy.
|
||||
|
||||
## Passphrase Considerations
|
||||
|
||||
If you are using a BIP-39 passphrase for everything, you should
|
||||
probably do a "Lock Down Seed" (Advanced/Tools > Danger Zone > Seed
|
||||
@ -17,6 +48,10 @@ reversed, so other funds you may have on the same seed words are
|
||||
protected. Once you are operating in XPRV mode, you can define a
|
||||
spending policy and know that it is restricted to only that wallet.
|
||||
|
||||
You can also block access to other related keys, which removes the
|
||||
"Passphrase" entry option from the main menu, but that protection
|
||||
doesn seem as strong.
|
||||
|
||||
## Trick PIN Thoughts
|
||||
|
||||
When doing your game theory w.r.t to bypass mode and this feature,
|
||||
@ -27,8 +62,9 @@ have all your UTXO locations and total wallet balance (because they
|
||||
can export xpubs to any wallet and load balance from there).
|
||||
|
||||
Therefore, a trick pin that leads to a duress wallet after giving up
|
||||
the bypass unlock PIN does not fool them. Best would be to provide
|
||||
a false bypass PIN that is in fact a wipe PIN.
|
||||
the bypass unlock PIN, will not fool them. Best would be to provide
|
||||
a false bypass PIN that is in fact a brick/wipe PIN.
|
||||
|
||||
|
||||
### Unlock Policy & Wipe
|
||||
|
||||
@ -41,10 +77,10 @@ to blank wallet now (no seed loaded).
|
||||
### Delta Mode and Spending Policy
|
||||
|
||||
If, from the start, you gave your "delta mode PIN" to the attackers,
|
||||
then when they bypass the policy (after also getting the bypass PIN from you),
|
||||
they will still be in Delta Mode.
|
||||
then when they bypass the policy (after also getting the bypass PIN
|
||||
from you), they will still be in Delta Mode.
|
||||
|
||||
They could attempt unlimited spending, but transactions signed will
|
||||
not be valid. If they try to view the seed words or generally export
|
||||
private key material, they may hit many of the "wipe seed if delta mode"
|
||||
cases.
|
||||
private key material, they will hit many of the "wipe seed if delta
|
||||
mode" cases.
|
||||
|
||||
@ -19,7 +19,7 @@ class Graphics:
|
||||
|
||||
scroll = (3, 61, 1, 0, b'@\x00\x00@\x00\x00@\x00\x00@\x00\x00@\x00\x00@\x00\x00@\x00\x00@\x00\x00@\x00\x00@\x00\x00@\x00\x00@\x00\x00@\x00\x00@\x00\x00@\x00\x00@\x00\x00@\x00\x00@\x00\x00@\x00\x00@@\xe0@')
|
||||
|
||||
selected = (15, 12, 2, 0, b'\x00\x00\x00\x00\x00\x06\x00\x0c\x00\x18\x0000`\x18\xc0\r\x80\x07\x00\x02\x00\x00\x00')
|
||||
selected = (9, 12, 2, 0, b'\x00\x00\x00\x00\x00\x80\x01\x80\x01\x00\x03\x00\x82\x00\xc6\x00d\x00<\x00\x18\x00\x00\x00')
|
||||
|
||||
sm_box = (11, 17, 2, 0, b'\xe4\xe0\x80 \x80 \x80 \x00\x00\x00\x00\x80 \x00\x00\x00\x00\x00\x00\x80 \x00\x00\x00\x00\x80 \x80 \x80 \xe4\xe0')
|
||||
|
||||
|
||||
@ -12,6 +12,12 @@ This lists the new changes that have not yet been published in a normal release.
|
||||
- Bugfix: Disallow negative input/output amounts in PSBT.
|
||||
- Bugfix: Fix filesystem initialization after Wife LFS or Destroy Seed
|
||||
|
||||
# Spending Policy Feature
|
||||
- "Enable HSM" and "User Management" have moved into Advanced > Spending Policy
|
||||
- old "CCC" feature has been renamed and moved into that menu
|
||||
- new feature: Spending policies for "Single Signer" added:
|
||||
- power new stuff
|
||||
|
||||
# Mk4 Specific Changes
|
||||
|
||||
## 5.4.? - 2025-08-xx
|
||||
|
||||
@ -327,7 +327,7 @@ class ApproveTransaction(UserAuthorizedAction):
|
||||
async def interact(self):
|
||||
# Prompt user w/ details and get approval
|
||||
from glob import dis, hsm_active
|
||||
from ccc import CCCFeature
|
||||
from ccc import CCCFeature, SSSPFeature
|
||||
|
||||
# step 1: parse PSBT from PSRAM into in-memory objects.
|
||||
|
||||
@ -387,7 +387,13 @@ class ApproveTransaction(UserAuthorizedAction):
|
||||
|
||||
# early test for spending policy; not an error if violates policy
|
||||
# - might add warnings
|
||||
could_ccc_sign, needs_2fa = CCCFeature.could_sign(self.psbt)
|
||||
could_ccc_sign, ccc_needs_2fa = CCCFeature.could_cosign(self.psbt)
|
||||
|
||||
# test for allowing any signature when in single-signer mode
|
||||
# - but CCC will override it.
|
||||
should_block, ss_needs_2fa = SSSPFeature.can_allow(self.psbt)
|
||||
if should_block and not could_ccc_sign:
|
||||
return await self.failure('Spending Policy violation.')
|
||||
|
||||
# step 2: figure out what we are approving, so we can get sign-off
|
||||
# - outputs, amounts
|
||||
@ -500,7 +506,7 @@ class ApproveTransaction(UserAuthorizedAction):
|
||||
self.done()
|
||||
return
|
||||
|
||||
if needs_2fa and could_ccc_sign:
|
||||
if ccc_needs_2fa and could_ccc_sign:
|
||||
# They still need to pass web2fa challenge (but it meets other specs ok)
|
||||
try:
|
||||
await CCCFeature.web2fa_challenge()
|
||||
@ -510,6 +516,13 @@ class ApproveTransaction(UserAuthorizedAction):
|
||||
if ch2 != 'y':
|
||||
return await self.failure("2FA Failed")
|
||||
|
||||
elif ss_needs_2fa:
|
||||
# Need 2FA for single-sig case .. refuse to sign if it fails.
|
||||
try:
|
||||
await SSSPFeature.web2fa_challenge()
|
||||
except:
|
||||
return await self.failure("2FA Failed")
|
||||
|
||||
# do the actual signing.
|
||||
try:
|
||||
dis.fullscreen('Wait...')
|
||||
@ -517,9 +530,13 @@ class ApproveTransaction(UserAuthorizedAction):
|
||||
self.psbt.sign_it()
|
||||
|
||||
if could_ccc_sign:
|
||||
dis.fullscreen('CCC Sign...')
|
||||
# this is where the CCC co-signing happens.
|
||||
dis.fullscreen('Co-Signing...')
|
||||
gc.collect()
|
||||
CCCFeature.sign_psbt(self.psbt)
|
||||
else:
|
||||
# maybe capture new min-height for velocity limit
|
||||
SSSPFeature.update_last_signed(self.psbt)
|
||||
|
||||
except FraudulentChangeOutput as exc:
|
||||
return await self.failure(exc.args[0], title='Change Fraud')
|
||||
@ -530,8 +547,9 @@ class ApproveTransaction(UserAuthorizedAction):
|
||||
return await self.failure("Signing failed late", exc)
|
||||
|
||||
try:
|
||||
await done_signing(self.psbt, self, self.input_method, self.filename, self.output_encoder,
|
||||
slot_b=True if ch == "b" else False, finalize=self.do_finalize)
|
||||
await done_signing(self.psbt, self, self.input_method,
|
||||
self.filename, self.output_encoder,
|
||||
slot_b=(ch == "b"), finalize=self.do_finalize)
|
||||
self.done()
|
||||
except AbortInteraction:
|
||||
# user might have sent new sign cmd, while we still at export prompt
|
||||
|
||||
440
shared/ccc.py
440
shared/ccc.py
@ -2,10 +2,10 @@
|
||||
#
|
||||
# ccc.py - ColdCard Co-sign feature. Be a leg in a 2-of-3 that is signed based on a policy.
|
||||
#
|
||||
# Rebranding/single-signer addtions:
|
||||
# Rebranding/single-signer additions:
|
||||
#
|
||||
# - "CCC" will now be branded as "Spending Policy (Co-Sign)" was "ColdCard Cosigning"
|
||||
# - single singer policies will be called "Spending Policy"
|
||||
# - "CCC" (was "ColdCard Cosigning") will now be branded as "Spending Policy: Multisig"
|
||||
# - single singer policies will be called "Spending Policy: Single Sig"
|
||||
# - internally: CCC is the multisig stuff, vs SSSP: Single Signer Spending Policy
|
||||
# - "hobbled" refers to less-than full control over Coldcard, even though you have main PIN
|
||||
#
|
||||
@ -18,18 +18,185 @@ from menu import MenuSystem, MenuItem, start_chooser
|
||||
from seed import seed_words_to_encoded_secret
|
||||
from stash import SecretStash
|
||||
from charcodes import KEY_QR, KEY_CANCEL, KEY_NFC
|
||||
from exceptions import CCCPolicyViolationError
|
||||
from exceptions import SpendPolicyViolation
|
||||
|
||||
|
||||
# limit to number of addresses in list
|
||||
MAX_WHITELIST = const(25)
|
||||
|
||||
class CCCFeature:
|
||||
class SpendingPolicy(dict):
|
||||
# Details of what is allowed or not. Same for single vs. multisig signing.
|
||||
# - a dict() but with write-thru to setting value
|
||||
|
||||
# we don't show the user the reason for policy fail (by design, so attacker
|
||||
# We don't show the user the reason for policy fail (by design, so attacker
|
||||
# cannot maximize their take against the policy), but during setup/experiments
|
||||
# we offer to show the reason in the menu
|
||||
last_fail_reason = ""
|
||||
# we offer to show the reason in the menu. Includes both SS and MS cases.
|
||||
last_fail_reason = ''
|
||||
|
||||
def __init__(self, nvkey, pol_dict):
|
||||
# deserialize and construct
|
||||
#assert nvkey in { 'ccc', 'sssp' }
|
||||
self.nvkey = nvkey
|
||||
super().__init__(pol_dict)
|
||||
|
||||
def _update_policy(self):
|
||||
# serialize the spending policy, save it
|
||||
v = dict(settings.get(self.nvkey, {}))
|
||||
v['pol'] = self.copy()
|
||||
settings.set(self.nvkey, v)
|
||||
|
||||
def update_policy_key(self, **kws):
|
||||
# update a few elements of the spending policy
|
||||
# - all settings "saved" as they are changed.
|
||||
# - return updated policy
|
||||
self.update(kws)
|
||||
self._update_policy()
|
||||
|
||||
def meets_policy(self, psbt):
|
||||
# Does policy allow signing this? Else raise why. Return T if web2fa required.
|
||||
pol = self
|
||||
|
||||
# not safe to sign any txn w/ warnings: might be complaining about
|
||||
# massive miner fees, or weird OP_RETURN stuff
|
||||
if psbt.warnings:
|
||||
raise SpendPolicyViolation("has warnings")
|
||||
|
||||
# Magnitude: size limits for output side (non change)
|
||||
magnitude = pol.get("mag", None)
|
||||
if magnitude is not None:
|
||||
if magnitude < 1000:
|
||||
# it is a BTC, convert to sats
|
||||
magnitude = magnitude * 100000000
|
||||
|
||||
outgoing = psbt.total_value_out - psbt.total_change_value
|
||||
if outgoing > magnitude:
|
||||
raise SpendPolicyViolation("magnitude")
|
||||
|
||||
# Velocity: if zero => no velocity checks
|
||||
velocity = pol.get("vel", None)
|
||||
if velocity:
|
||||
if not psbt.lock_time:
|
||||
raise SpendPolicyViolation("no nLockTime")
|
||||
|
||||
if psbt.lock_time >= NLOCK_IS_TIME:
|
||||
# this is unix timestamp - not allowed - fail
|
||||
raise SpendPolicyViolation("nLockTime not height")
|
||||
|
||||
block_h = pol.get("block_h", chains.current_chain().ccc_min_block)
|
||||
if psbt.lock_time <= block_h:
|
||||
raise SpendPolicyViolation("rewound (%d)" % psbt.lock_time)
|
||||
|
||||
# we won't sign txn unless old height + velocity >= new height
|
||||
if psbt.lock_time < (block_h + velocity):
|
||||
raise SpendPolicyViolation("velocity (%d)" % psbt.lock_time)
|
||||
|
||||
# Whitelist of outputs addresses
|
||||
wl = pol.get("addrs", None)
|
||||
if wl:
|
||||
c = chains.current_chain()
|
||||
wl = set(wl)
|
||||
for idx, txo in psbt.output_iter():
|
||||
out = psbt.outputs[idx]
|
||||
if not out.is_change: # ignore change
|
||||
addr = c.render_address(txo.scriptPubKey)
|
||||
if addr not in wl:
|
||||
raise SpendPolicyViolation("whitelist: " + addr)
|
||||
|
||||
# Web 2FA
|
||||
# - slow, requires UX, and they might not acheive it...
|
||||
# - wait until about to do signature
|
||||
if pol.get('web2fa', False):
|
||||
psbt.warnings.append(('CCC', 'Web 2FA required.'))
|
||||
return True
|
||||
|
||||
async def web2fa_challenge(self, msg):
|
||||
# they are trying to sign something, so make them get out their phone
|
||||
# - at this point they have already ok'ed the details of the txn
|
||||
# - and we have approved other elements of the spending policy.
|
||||
# - could show MS wallet name, or txn details but will not because that is
|
||||
# an info leak to Coinkite... and we just don't want to know.
|
||||
assert self.get('web2fa')
|
||||
|
||||
ok = await web2fa.perform_web2fa(msg, self.get('web2fa'))
|
||||
if not ok:
|
||||
SpendingPolicy.last_fail_reason = '2FA Fail'
|
||||
raise SpendPolicyViolation
|
||||
|
||||
def update_last_signed(self, psbt):
|
||||
# Call after successfully signing a PSBT ... notes the height involved.
|
||||
# - might add other things besides height here someday
|
||||
old_h = self.get('block_h', 1)
|
||||
|
||||
if old_h < psbt.lock_time < NLOCK_IS_TIME:
|
||||
# always update last block height, even if velocity isn't enabled yet
|
||||
# - attacker might have changed to testnet, but there is no
|
||||
# reason to ever lower block height. strictly ascending
|
||||
self.update_policy_key(block_h=psbt.lock_time)
|
||||
settings.save()
|
||||
|
||||
class SSSPFeature:
|
||||
# Using setting value "sssp"
|
||||
|
||||
@classmethod
|
||||
def is_enabled(cls):
|
||||
return sssp_spending_policy('en')
|
||||
|
||||
@classmethod
|
||||
def update_last_signed(cls, psbt):
|
||||
# new PSBT has been completely signed successfully.
|
||||
if not cls.is_enabled():
|
||||
return
|
||||
pol = cls.get_policy()
|
||||
pol.update_last_signed(psbt)
|
||||
|
||||
@classmethod
|
||||
def default_policy(cls):
|
||||
# a very basic and permissive policy, but non-zero too.
|
||||
# - 1BTC per day
|
||||
chain = chains.current_chain()
|
||||
return SpendingPolicy('sssp', dict(mag=1, vel=144,
|
||||
block_h=chain.ccc_min_block, web2fa='', addrs=[]))
|
||||
|
||||
@classmethod
|
||||
def get_policy(cls):
|
||||
# de-serialize just the spending policy
|
||||
return SpendingPolicy('sssp', settings.get('sssp', dict(pol={})).get('pol'))
|
||||
|
||||
@classmethod
|
||||
def can_allow(cls, psbt):
|
||||
# We are looking at a PSBT: should we let user sign it, or block?
|
||||
# - return (block_signing, needs_2fa_step)
|
||||
if not cls.is_enabled:
|
||||
exists = bool(settings.get('sssp', False))
|
||||
if exists:
|
||||
# this will not block CCC co-signing, because that test is already
|
||||
# done before this call.
|
||||
psbt.warnings.append(('SP', "Spending Policy defined but disabled."))
|
||||
return False, False
|
||||
|
||||
try:
|
||||
# check policy
|
||||
pol = cls.get_policy()
|
||||
needs_2fa = pol.meets_policy(psbt)
|
||||
except SpendPolicyViolation as e:
|
||||
SpendingPolicy.last_fail_reason = str(e)
|
||||
# caller will show msg
|
||||
return True, False
|
||||
|
||||
return False, needs_2fa
|
||||
|
||||
@classmethod
|
||||
async def web2fa_challenge(cls):
|
||||
# they are trying to sign something, so make them get out their phone
|
||||
# - at this point they have already ok'ed the details of the txn
|
||||
# - and we have approved other elements of the spending policy.
|
||||
# - could show MS wallet name, or txn details but will not because that is
|
||||
# an info leak to Coinkite... and we just don't want to know.
|
||||
await cls.get_policy().perform_web2fa('Approve Transaction')
|
||||
|
||||
|
||||
class CCCFeature:
|
||||
# Using setting value "ccc"
|
||||
|
||||
@classmethod
|
||||
def is_enabled(cls):
|
||||
@ -92,29 +259,13 @@ class CCCFeature:
|
||||
# a very basic and permissive policy, but non-zero too.
|
||||
# - 1BTC per day
|
||||
chain = chains.current_chain()
|
||||
return dict(mag=1, vel=144, block_h=chain.ccc_min_block, web2fa='', addrs=[])
|
||||
return SpendingPolicy('ccc', dict(mag=1, vel=144,
|
||||
block_h=chain.ccc_min_block, web2fa='', addrs=[]))
|
||||
|
||||
@classmethod
|
||||
def get_policy(cls):
|
||||
# de-serialize just the spending policy
|
||||
return dict(settings.get('ccc', dict(pol={})).get('pol'))
|
||||
|
||||
@classmethod
|
||||
def update_policy(cls, pol):
|
||||
# serialize the spending policy, save it
|
||||
v = dict(settings.get('ccc', {}))
|
||||
v['pol'] = dict(pol)
|
||||
settings.set('ccc', v)
|
||||
return v['pol']
|
||||
|
||||
@classmethod
|
||||
def update_policy_key(cls, **kws):
|
||||
# update a few elements of the spending policy
|
||||
# - all settings "saved" as they are changed.
|
||||
# - return updated policy
|
||||
p = cls.get_policy()
|
||||
p.update(kws)
|
||||
return cls.update_policy(p)
|
||||
return SpendingPolicy('ccc', settings.get('ccc', dict(pol={})).get('pol'))
|
||||
|
||||
@classmethod
|
||||
def remove_ccc(cls):
|
||||
@ -124,66 +275,7 @@ class CCCFeature:
|
||||
settings.save()
|
||||
|
||||
@classmethod
|
||||
def meets_policy(cls, psbt):
|
||||
# Does policy allow signing this? Else raise why
|
||||
pol = cls.get_policy()
|
||||
|
||||
# not safe to sign any txn w/ warnings: might be complaining about
|
||||
# massive miner fees, or weird OP_RETURN stuff
|
||||
if psbt.warnings:
|
||||
raise CCCPolicyViolationError("has warnings")
|
||||
|
||||
# Magnitude: size limits for output side (non change)
|
||||
magnitude = pol.get("mag", None)
|
||||
if magnitude is not None:
|
||||
if magnitude < 1000:
|
||||
# it is a BTC, convert to sats
|
||||
magnitude = magnitude * 100000000
|
||||
|
||||
outgoing = psbt.total_value_out - psbt.total_change_value
|
||||
if outgoing > magnitude:
|
||||
raise CCCPolicyViolationError("magnitude")
|
||||
|
||||
# Velocity: if zero => no velocity checks
|
||||
velocity = pol.get("vel", None)
|
||||
if velocity:
|
||||
if not psbt.lock_time:
|
||||
raise CCCPolicyViolationError("no nLockTime")
|
||||
|
||||
if psbt.lock_time >= NLOCK_IS_TIME:
|
||||
# this is unix timestamp - not allowed - fail
|
||||
raise CCCPolicyViolationError("nLockTime not height")
|
||||
|
||||
block_h = pol.get("block_h", chains.current_chain().ccc_min_block)
|
||||
if psbt.lock_time <= block_h:
|
||||
raise CCCPolicyViolationError("rewound (%d)" % psbt.lock_time)
|
||||
|
||||
# we won't sign txn unless old height + velocity >= new height
|
||||
if psbt.lock_time < (block_h + velocity):
|
||||
raise CCCPolicyViolationError("velocity (%d)" % psbt.lock_time)
|
||||
|
||||
# Whitelist of outputs addresses
|
||||
wl = pol.get("addrs", None)
|
||||
if wl:
|
||||
c = chains.current_chain()
|
||||
wl = set(wl)
|
||||
for idx, txo in psbt.output_iter():
|
||||
out = psbt.outputs[idx]
|
||||
if not out.is_change: # ignore change
|
||||
addr = c.render_address(txo.scriptPubKey)
|
||||
if addr not in wl:
|
||||
raise CCCPolicyViolationError("whitelist")
|
||||
|
||||
# Web 2FA
|
||||
# - slow, requires UX, and they might not acheive it...
|
||||
# - wait until about to do signature
|
||||
if pol.get('web2fa', False):
|
||||
psbt.warnings.append(('CCC', 'Web 2FA required.'))
|
||||
return True
|
||||
|
||||
|
||||
@classmethod
|
||||
def could_sign(cls, psbt):
|
||||
def could_cosign(cls, psbt):
|
||||
# We are looking at a PSBT: can we sign it, and would we?
|
||||
# - if we **could** but will not, due to policy, add warning msg
|
||||
# - return (we could sign, needs 2fa step)
|
||||
@ -192,7 +284,7 @@ class CCCFeature:
|
||||
|
||||
ms = psbt.active_multisig
|
||||
if not ms:
|
||||
# single-sig CCC not supported
|
||||
# not multisig, so ignore/permit
|
||||
return False, False
|
||||
|
||||
# TODO: if key B has already signed the PSBT, and so we don't need key C,
|
||||
@ -205,41 +297,29 @@ class CCCFeature:
|
||||
|
||||
try:
|
||||
# check policy
|
||||
needs_2fa = cls.meets_policy(psbt)
|
||||
except CCCPolicyViolationError as e:
|
||||
cls.last_fail_reason = str(e)
|
||||
pol = cls.get_policy()
|
||||
needs_2fa = pol.meets_policy(psbt)
|
||||
except SpendPolicyViolation as e:
|
||||
SpendingPolicy.last_fail_reason = str(e)
|
||||
psbt.warnings.append(('CCC', "Violates spending policy. Won't sign."))
|
||||
return False, False
|
||||
|
||||
return True, needs_2fa
|
||||
|
||||
@classmethod
|
||||
async def web2fa_challenge(cls):
|
||||
# they are trying to sign something, so make them get out their phone
|
||||
# - at this point they have already ok'ed the details of the txn
|
||||
# - and we have approved other elements of the spending policy.
|
||||
# - could show MS wallet name, or txn details but will not because that is
|
||||
# an info leak to Coinkite... and we just don't want to know.
|
||||
pol = cls.get_policy()
|
||||
|
||||
ok = await web2fa.perform_web2fa('Approve CCC Transaction', pol.get('web2fa'))
|
||||
if not ok:
|
||||
cls.last_fail_reason = '2FA Fail'
|
||||
raise CCCPolicyViolationError
|
||||
|
||||
@classmethod
|
||||
def sign_psbt(cls, psbt):
|
||||
# do the math
|
||||
psbt.sign_it(cls.get_encoded_secret(), cls.get_xfp())
|
||||
cls.last_fail_reason = ""
|
||||
SpendingPolicy.last_fail_reason = ""
|
||||
|
||||
old_h = cls.get_policy().get('block_h', 1)
|
||||
if old_h < psbt.lock_time < NLOCK_IS_TIME:
|
||||
# always update last block height, even if velocity isn't enabled yet
|
||||
# - attacker might have changed to testnet, but there is no
|
||||
# reason to ever lower block height. strictly ascending
|
||||
cls.update_policy_key(block_h=psbt.lock_time)
|
||||
settings.save()
|
||||
pol = cls.get_policy()
|
||||
pol.update_last_signed(psbt)
|
||||
|
||||
@classmethod
|
||||
async def web2fa_challenge(cls):
|
||||
# do UX for web2fa; user is given option to proceed even if it fails
|
||||
# (without the co-signing)
|
||||
await cls.get_policy().web2fa_challenge('Approve Transaction: Co-Sign')
|
||||
|
||||
|
||||
def render_mag_value(mag):
|
||||
@ -264,9 +344,10 @@ class CCCConfigMenu(MenuSystem):
|
||||
|
||||
my_xfp = CCCFeature.get_xfp()
|
||||
items = [
|
||||
# xxxxxxxxxxxxxxxx
|
||||
MenuItem('CCC [%s]' % xfp2str(my_xfp), f=self.show_ident),
|
||||
MenuItem('Spending Policy', menu=CCCPolicyMenu.be_a_submenu),
|
||||
MenuItem(('[%s] Co-Signing' if version.has_qwerty else '[%s]')
|
||||
% xfp2str(my_xfp), f=self.show_ident),
|
||||
MenuItem('Spending Policy',
|
||||
menu=lambda *a: SpendingPolicyMenu.be_a_submenu(SSSPFeature.get_policy())),
|
||||
MenuItem('Export CCC XPUBs', f=self.export_xpub_c),
|
||||
MenuItem('Multisig Wallets'),
|
||||
]
|
||||
@ -281,7 +362,7 @@ class CCCConfigMenu(MenuSystem):
|
||||
|
||||
items.append(MenuItem('↳ Build 2-of-N', f=self.build_2ofN, arg=count))
|
||||
|
||||
if CCCFeature.last_fail_reason:
|
||||
if SpendingPolicy.last_fail_reason:
|
||||
# xxxxxxxxxxxxxxxx
|
||||
items.insert(1, MenuItem('Last Violation', f=self.debug_last_fail))
|
||||
|
||||
@ -299,11 +380,11 @@ class CCCConfigMenu(MenuSystem):
|
||||
msg += "CCC height:\n\n%s\n\n" % bh
|
||||
|
||||
msg += 'The most recent policy check failed because of:\n\n%s\n\nPress (4) to clear.' \
|
||||
% CCCFeature.last_fail_reason
|
||||
% SpendingPolicy.last_fail_reason
|
||||
ch = await ux_show_story(msg, escape='4')
|
||||
|
||||
if ch == '4':
|
||||
CCCFeature.last_fail_reason = ''
|
||||
SpendingPolicy.last_fail_reason = ''
|
||||
self.update_contents()
|
||||
|
||||
async def remove_ccc(self, *a):
|
||||
@ -386,23 +467,11 @@ be ready to show it as a QR, before proceeding.'''
|
||||
|
||||
goto_top_menu()
|
||||
|
||||
class PolCheckedMenuItem(MenuItem):
|
||||
# Show a checkmark if **policy** setting is defined and not the default
|
||||
# - only works inside CCCPolicyMenu
|
||||
def __init__(self, label, polkey, **kws):
|
||||
super().__init__(label, **kws)
|
||||
self.polkey = polkey
|
||||
|
||||
def is_chosen(self):
|
||||
# should we show a check in parent menu? check the policy
|
||||
m = the_ux.top_of_stack()
|
||||
#assert isinstance(m, CCCPolicyMenu)
|
||||
return bool(m.policy.get(self.polkey, False))
|
||||
|
||||
|
||||
class CCCAddrWhitelist(MenuSystem):
|
||||
class SPAddrWhitelist(MenuSystem):
|
||||
# simulator arg: --seq tcENTERENTERsENTERwENTER
|
||||
def __init__(self):
|
||||
def __init__(self, pol):
|
||||
self.policy = pol
|
||||
items = self.construct()
|
||||
super().__init__(items)
|
||||
|
||||
@ -411,12 +480,12 @@ class CCCAddrWhitelist(MenuSystem):
|
||||
self.replace_items(tmp)
|
||||
|
||||
@classmethod
|
||||
async def be_a_submenu(cls, *a):
|
||||
return cls()
|
||||
async def be_a_submenu(cls, pol, *a):
|
||||
return cls(pol)
|
||||
|
||||
def construct(self):
|
||||
# list of addresses
|
||||
addrs = CCCFeature.get_policy().get('addrs', [])
|
||||
addrs = self.policy.get('addrs', [])
|
||||
maxxed = (len(addrs) >= MAX_WHITELIST)
|
||||
|
||||
items = []
|
||||
@ -451,15 +520,14 @@ class CCCAddrWhitelist(MenuSystem):
|
||||
|
||||
def delete_addr(self, addr):
|
||||
# no confirm, stakes are low
|
||||
addrs = CCCFeature.get_policy().get('addrs', [])
|
||||
addrs = self.policy.get('addrs', [])
|
||||
addrs.remove(addr)
|
||||
CCCFeature.update_policy_key(addrs=addrs)
|
||||
self.policy.update_policy_key(addrs=addrs)
|
||||
self.update_contents()
|
||||
|
||||
async def clear_all(self, *a):
|
||||
if await ux_confirm("Irreversibly remove all addresses from the whitelist?",
|
||||
confirm_key='4'):
|
||||
CCCFeature.update_policy_key(addrs=[])
|
||||
if await ux_confirm("Remove all addresses from the whitelist?", confirm_key='4'):
|
||||
self.policy.update_policy_key(addrs=[])
|
||||
self.update_contents()
|
||||
|
||||
async def import_file(self, *a):
|
||||
@ -544,7 +612,7 @@ class CCCAddrWhitelist(MenuSystem):
|
||||
|
||||
async def add_addresses(self, more_addrs):
|
||||
# add new entries, if unique; preserve ordering
|
||||
addrs = CCCFeature.get_policy().get('addrs', [])
|
||||
addrs = self.policy.get('addrs', [])
|
||||
new = []
|
||||
for a in more_addrs:
|
||||
if a not in addrs:
|
||||
@ -559,23 +627,39 @@ class CCCAddrWhitelist(MenuSystem):
|
||||
if len(addrs) > MAX_WHITELIST:
|
||||
return await self.maxed_out()
|
||||
|
||||
CCCFeature.update_policy_key(addrs=addrs)
|
||||
self.policy.update_policy_key(addrs=addrs)
|
||||
self.update_contents()
|
||||
|
||||
if len(new) > 1:
|
||||
await ux_show_story("Added %d new addresses to whitelist:\n\n%s" %
|
||||
(len(new), '\n\n'.join(show_single_address(a) for a in new)))
|
||||
else:
|
||||
await ux_show_story("Added new address to whitelist:\n\n%s" % show_single_address(new[0]))
|
||||
await ux_show_story("Added new address to whitelist:\n\n%s" %
|
||||
show_single_address(new[0]))
|
||||
|
||||
class CCCPolicyMenu(MenuSystem):
|
||||
class SPCheckedMenuItem(MenuItem):
|
||||
# Show a checkmark if **policy** setting is defined and not the default
|
||||
# - only works inside SpendingPolicyMenu
|
||||
def __init__(self, label, polkey, **kws):
|
||||
super().__init__(label, **kws)
|
||||
self.polkey = polkey
|
||||
|
||||
def is_chosen(self):
|
||||
# should we show a check in parent menu? check the policy
|
||||
m = the_ux.top_of_stack()
|
||||
#assert isinstance(m, SpendingPolicyMenu)
|
||||
return bool(m.policy.get(self.polkey, False))
|
||||
|
||||
class SpendingPolicyMenu(MenuSystem):
|
||||
# Build menu stack that allows edit of all features of the spending
|
||||
# policy. Key C is set already at this point.
|
||||
# policy.
|
||||
# - supports both CCC and SSSP modes w/ same policies
|
||||
# - Key C is set already at this point.
|
||||
# - and delete/cancel CCC (clears setting?)
|
||||
# - be a sticky menu that's hard to exit (ie. SAVE choice and no cancel out)
|
||||
|
||||
def __init__(self):
|
||||
self.policy = CCCFeature.get_policy()
|
||||
def __init__(self, pol):
|
||||
self.policy = pol
|
||||
items = self.construct()
|
||||
super().__init__(items)
|
||||
|
||||
@ -584,17 +668,18 @@ class CCCPolicyMenu(MenuSystem):
|
||||
self.replace_items(tmp)
|
||||
|
||||
@classmethod
|
||||
async def be_a_submenu(cls, *a):
|
||||
return cls()
|
||||
async def be_a_submenu(cls, pol, *a):
|
||||
return cls(pol)
|
||||
|
||||
def construct(self):
|
||||
items = [
|
||||
# xxxxxxxxxxxxxxxx
|
||||
PolCheckedMenuItem('Max Magnitude', 'mag', f=self.set_magnitude),
|
||||
PolCheckedMenuItem('Limit Velocity', 'vel', f=self.set_velocity),
|
||||
PolCheckedMenuItem('Whitelist' + (' Addresses' if version.has_qr else ''),
|
||||
'addrs', menu=CCCAddrWhitelist.be_a_submenu),
|
||||
PolCheckedMenuItem('Web 2FA', 'web2fa', f=self.toggle_2fa),
|
||||
SPCheckedMenuItem('Max Magnitude', 'mag', f=self.set_magnitude),
|
||||
SPCheckedMenuItem('Limit Velocity', 'vel', f=self.set_velocity),
|
||||
SPCheckedMenuItem('Whitelist' + (' Addresses' if version.has_qr else ''),
|
||||
'addrs',
|
||||
menu=lambda *a: SPAddrWhitelist.be_a_submenu(self.policy)),
|
||||
SPCheckedMenuItem('Web 2FA', 'web2fa', f=self.toggle_2fa),
|
||||
]
|
||||
|
||||
if self.policy.get('web2fa'):
|
||||
@ -608,15 +693,15 @@ class CCCPolicyMenu(MenuSystem):
|
||||
async def test_2fa(self, *a):
|
||||
ss = self.policy.get('web2fa')
|
||||
assert ss
|
||||
ok = await web2fa.perform_web2fa('CCC Test', ss)
|
||||
ok = await web2fa.perform_web2fa('Testing Only', ss)
|
||||
|
||||
await ux_show_story('Correct code was given.' if ok else 'Failed or aborted.')
|
||||
|
||||
async def enroll_more_2fa(self, *a):
|
||||
# let more phones in on the party
|
||||
# let more phones in on the party, but they get same shared secret
|
||||
ss = self.policy.get('web2fa')
|
||||
assert ss
|
||||
await web2fa.web2fa_enroll('CCC', ss)
|
||||
await web2fa.web2fa_enroll(ss)
|
||||
|
||||
async def set_magnitude(self, *a):
|
||||
# Looks decent on both Q and Mk4...
|
||||
@ -640,7 +725,7 @@ class CCCPolicyMenu(MenuSystem):
|
||||
else:
|
||||
msg += " maximum per-transaction: \n\n %s" % render_mag_value(val)
|
||||
|
||||
self.policy = CCCFeature.update_policy_key(**args)
|
||||
self.policy.update_policy_key(**args)
|
||||
|
||||
await ux_show_story(msg, title="TX Magnitude")
|
||||
|
||||
@ -650,7 +735,7 @@ class CCCPolicyMenu(MenuSystem):
|
||||
if not mag:
|
||||
msg = 'Velocity limit requires a per-transaction magnitude to be set.'\
|
||||
' This has been set to 1BTC as a starting value.'
|
||||
self.policy = CCCFeature.update_policy_key(mag=1)
|
||||
self.policy.update_policy_key(mag=1)
|
||||
|
||||
await ux_show_story(msg)
|
||||
|
||||
@ -685,7 +770,7 @@ class CCCPolicyMenu(MenuSystem):
|
||||
which = 0
|
||||
|
||||
def set(idx, text):
|
||||
self.policy = CCCFeature.update_policy_key(vel=va[idx])
|
||||
self.policy.update_policy_key(vel=va[idx])
|
||||
|
||||
return which, ch, set
|
||||
|
||||
@ -696,7 +781,7 @@ class CCCPolicyMenu(MenuSystem):
|
||||
if not await ux_confirm("Disable web 2FA check? Effect is immediate."):
|
||||
return
|
||||
|
||||
self.policy = CCCFeature.update_policy_key(web2fa='')
|
||||
self.policy.update_policy_key(web2fa='')
|
||||
self.update_contents()
|
||||
|
||||
await ux_show_story("Web 2FA has been disabled. If you re-enable it, a new "
|
||||
@ -716,12 +801,12 @@ phone with Internet access and 2FA app holding correct shared-secret.''',
|
||||
return
|
||||
|
||||
# challenge them, and don't set unless it works
|
||||
ss = await web2fa.web2fa_enroll('CCC')
|
||||
ss = await web2fa.web2fa_enroll()
|
||||
if not ss:
|
||||
return
|
||||
|
||||
# update state
|
||||
self.policy = CCCFeature.update_policy_key(web2fa=ss)
|
||||
self.policy.update_policy_key(web2fa=ss)
|
||||
self.update_contents()
|
||||
|
||||
async def gen_or_import():
|
||||
@ -921,7 +1006,8 @@ def sssp_spending_policy(key, default=False, change=None):
|
||||
|
||||
return default
|
||||
|
||||
async def toggle_sssp_feature(*a):
|
||||
async def sssp_feature_menu(*a):
|
||||
# Show the top menu for SSSP feature, or enable access first time.
|
||||
from pincodes import pa
|
||||
from actions import goto_top_menu
|
||||
|
||||
@ -933,6 +1019,7 @@ async def toggle_sssp_feature(*a):
|
||||
# normal entry into menu system, after the first time
|
||||
assert not pa.hobbled_mode
|
||||
else:
|
||||
# tell them a story, and maybe enable feature
|
||||
en = await sssp_enable()
|
||||
if not en: return
|
||||
|
||||
@ -1088,19 +1175,18 @@ class SSSPConfigMenu(MenuSystem):
|
||||
def construct(self):
|
||||
from multisig import MultisigWallet, make_ms_wallet_menu
|
||||
|
||||
my_xfp = CCCFeature.get_xfp()
|
||||
items = [
|
||||
# xxxxxxxxxxxxxxxx
|
||||
MenuItem('Spending Policy'), # just a title?
|
||||
MenuItem('Set Policy...'), #, menu=CCCPolicyMenu.be_a_submenu),
|
||||
MenuItem('Edit Policy...',
|
||||
menu=lambda *a: SpendingPolicyMenu.be_a_submenu(SSSPFeature.get_policy())),
|
||||
SSSPCheckedMenuItem('Word Check', 'notes', 'Allow (read-only) access to secure notes and passwords? Otherwise, they are inaccessible.'),
|
||||
SSSPCheckedMenuItem('Allow Notes', 'words', 'To change Spending Policy, addition to special PIN, you must provide the first and last seed words.'),
|
||||
SSSPCheckedMenuItem('Related Keys', 'okeys', 'Allow access to BIP-39 passphrase wallets based on master seed, or Seed Vault (if any). Spending Policy applies too all.'),
|
||||
SSSPCheckedMenuItem('Allow Notes', 'words', 'To change Spending Policy, in addition to special PIN, you must provide the first and last seed words.'),
|
||||
SSSPCheckedMenuItem('Related Keys', 'okeys', 'Allow access to BIP-39 passphrase wallets based on master seed, or Seed Vault (if any). Same spending Policy applies to all.'),
|
||||
#MenuItem('Test Word Challenge', f=sssp_word_challenge), # XXX test only?
|
||||
]
|
||||
|
||||
if CCCFeature.last_fail_reason:
|
||||
# xxxxxxxxxxxxxxxx
|
||||
if SpendingPolicy.last_fail_reason:
|
||||
# xxxxxxxxxxxxxxxx
|
||||
items.insert(1, MenuItem('Last Violation', f=self.debug_last_fail))
|
||||
|
||||
items.append(MenuItem('Remove Policy', f=self.remove_sssp))
|
||||
@ -1149,18 +1235,18 @@ class SSSPConfigMenu(MenuSystem):
|
||||
|
||||
async def debug_last_fail(self, *a):
|
||||
# debug for customers: why did we reject that last txn?
|
||||
pol = CCCFeature.get_policy()
|
||||
pol = SSSPFeature.get_policy()
|
||||
bh = pol.get('block_h', None)
|
||||
msg = ''
|
||||
if bh:
|
||||
msg += "CCC height:\n\n%s\n\n" % bh
|
||||
msg += "Last height:\n\n%s\n\n" % bh
|
||||
|
||||
msg += 'The most recent policy check failed because of:\n\n%s\n\nPress (4) to clear.' \
|
||||
% CCCFeature.last_fail_reason
|
||||
% SpendingPolicy.last_fail_reason
|
||||
ch = await ux_show_story(msg, escape='4')
|
||||
|
||||
if ch == '4':
|
||||
CCCFeature.last_fail_reason = ''
|
||||
SpendingPolicy.last_fail_reason = ''
|
||||
self.update_contents()
|
||||
|
||||
async def remove_sssp(self, *a):
|
||||
|
||||
@ -266,17 +266,18 @@ class Display:
|
||||
if is_sel:
|
||||
self.dis.fill_rect(0, y, Display.WIDTH, h-1, 1)
|
||||
self.icon(2, y, 'wedge', invert=1)
|
||||
self.text(x, y, msg, invert=1)
|
||||
nx = self.text(x, y, msg, invert=1)
|
||||
else:
|
||||
self.text(x, y, msg)
|
||||
nx = self.text(x, y, msg)
|
||||
|
||||
# LATER: removed because caused confusion w/ underscore
|
||||
#if msg[0] == ' ' and space_indicators:
|
||||
# see also graphics/mono/space.txt
|
||||
#self.icon(x-2, y+9, 'space', invert=is_sel)
|
||||
|
||||
if is_checked:
|
||||
self.icon(108, y, 'selected', invert=is_sel)
|
||||
if is_checked and nx <= 113:
|
||||
# omit checkmark if it doesn't fit
|
||||
self.icon(113, y, 'selected', invert=is_sel)
|
||||
|
||||
def menu_show(self, *a):
|
||||
self.show()
|
||||
|
||||
@ -52,7 +52,7 @@ class UnknownAddressExplained(ValueError):
|
||||
pass
|
||||
|
||||
# We're not going to co-sign using CCC feature
|
||||
class CCCPolicyViolationError(RuntimeError):
|
||||
class SpendPolicyViolation(RuntimeError):
|
||||
pass
|
||||
|
||||
# EOF
|
||||
|
||||
@ -19,7 +19,7 @@ from countdowns import countdown_chooser
|
||||
from paper import make_paper_wallet
|
||||
from trick_pins import TrickPinMenu
|
||||
from tapsigner import import_tapsigner_backup_file
|
||||
from ccc import toggle_ccc_feature, sssp_spending_policy, toggle_sssp_feature
|
||||
from ccc import toggle_ccc_feature, sssp_spending_policy, sssp_feature_menu
|
||||
|
||||
# useful shortcut keys
|
||||
from charcodes import KEY_QR, KEY_NFC
|
||||
@ -367,6 +367,19 @@ NFCToolsMenu = [
|
||||
MenuItem('Push Transaction', f=nfc_pushtx_file, predicate=lambda: settings.get("ptxurl", False)),
|
||||
]
|
||||
|
||||
|
||||
SpendingPolicySubMenu = [
|
||||
NonDefaultMenuItem('Single-Signer', 'sssp', f=sssp_feature_menu, predicate=has_real_secret),
|
||||
NonDefaultMenuItem('Co-Sign Multi.' if not version.has_qwerty else 'Co-Sign Multisig (CCC)',
|
||||
'ccc', f=toggle_ccc_feature, predicate=is_not_tmp),
|
||||
ToggleMenuItem('HSM Mode', 'hsmcmd', ['Default Off', 'Enable'],
|
||||
story=("Enable HSM? Enables all user management commands, and other HSM-only USB commands. "
|
||||
"By default these commands are disabled."),
|
||||
predicate=hsm_available),
|
||||
MenuItem('User Management', menu=make_users_menu,
|
||||
predicate=lambda: hsm_available() and settings.get('hsmcmd', False)),
|
||||
]
|
||||
|
||||
AdvancedNormalMenu = [
|
||||
# xxxxxxxxxxxxxxxx
|
||||
MenuItem("Backup", menu=BackupStuffMenu),
|
||||
@ -380,14 +393,8 @@ AdvancedNormalMenu = [
|
||||
MenuItem("View Identity", f=view_ident),
|
||||
MenuItem("Temporary Seed", menu=make_ephemeral_seed_menu),
|
||||
MenuItem("Key Teleport (start)", f=kt_start_rx, predicate=version.has_qr),
|
||||
MenuItem("Spending Policy", menu=SpendingPolicySubMenu, shortcut='s'),
|
||||
MenuItem('Paper Wallets', f=make_paper_wallet),
|
||||
ToggleMenuItem('Enable HSM', 'hsmcmd', ['Default Off', 'Enable'],
|
||||
story=("Enable HSM? Enables all user management commands, and other HSM-only USB commands. "
|
||||
"By default these commands are disabled."),
|
||||
predicate=hsm_available),
|
||||
NonDefaultMenuItem('Spending Policy', 'sssp', f=toggle_sssp_feature, predicate=has_real_secret, shortcut='p'),
|
||||
NonDefaultMenuItem('Spending Policy: Co-Signing', 'ccc', f=toggle_ccc_feature, predicate=is_not_tmp), # XXX mk4 width
|
||||
MenuItem('User Management', menu=make_users_menu, predicate=hsm_available),
|
||||
MenuItem('NFC Tools', predicate=nfc_enabled, menu=NFCToolsMenu, shortcut=KEY_NFC),
|
||||
MenuItem("Danger Zone", menu=DangerZoneMenu, shortcut='z'),
|
||||
]
|
||||
@ -534,6 +541,6 @@ HobbledTopMenu = [
|
||||
predicate=lambda: settings.master_get('seedvault') and sssp_spending_policy('okeys')),
|
||||
MenuItem('Advanced/Tools', menu=HobbledAdvancedMenu, shortcut='t'),
|
||||
MenuItem('Secure Logout', f=logout_now, predicate=not version.has_battery),
|
||||
MenuItem('EXIT TEST DRIVE', f=toggle_sssp_feature, predicate=is_hobble_testdrive),
|
||||
MenuItem('EXIT TEST DRIVE', f=sssp_feature_menu, predicate=is_hobble_testdrive),
|
||||
ShortcutItem(KEY_NFC, predicate=nfc_enabled, menu=HobbledNFCToolsMenu),
|
||||
]
|
||||
|
||||
@ -58,8 +58,8 @@ class ImportantTask:
|
||||
else:
|
||||
# uncaught exception in an unnamed (and unimportant) task
|
||||
print("UNNAMED: " + context["message"])
|
||||
# sys.print_exception(context["exception"])
|
||||
print("... future: %r" % context.get("future", '?'))
|
||||
sys.print_exception(context["exception"]) # VERY USEFUL on sim
|
||||
#print("... future: %r" % context.get("future", '?'))
|
||||
|
||||
def start_task(self, name, awaitable):
|
||||
# start a critical task and watch for it to never die
|
||||
|
||||
@ -306,10 +306,6 @@ class MenuSystem:
|
||||
if fcn and fcn():
|
||||
checked = True
|
||||
|
||||
if not has_qwerty and checked and (len(msg) > 14):
|
||||
# on mk4 every label longer than 14 will overlap with checkmark
|
||||
checked = False
|
||||
|
||||
if self.multi_selected is not None and (real_idx in self.multi_selected):
|
||||
# ignore length constraint above, we need to visually show that
|
||||
# smthg is selected - in any case
|
||||
|
||||
@ -95,7 +95,7 @@ async def perform_web2fa(label, shared_secret):
|
||||
return False
|
||||
|
||||
|
||||
async def web2fa_enroll(label, ss=None):
|
||||
async def web2fa_enroll(ss=None):
|
||||
#
|
||||
# Enroll: Pick a secret and test they have loaded it into their phone.
|
||||
#
|
||||
@ -115,8 +115,8 @@ async def web2fa_enroll(label, ss=None):
|
||||
# - can't fit any metadata, like username or our serial # in there
|
||||
# - better on Q1 where no limitations for this size of QR
|
||||
|
||||
qr = 'otpauth://totp/{nm}?secret={ss}'.format(ss=ss,
|
||||
nm=url_quote(label if has_qr else label[0:4]))
|
||||
nm = 'COLDCARD' if has_qr else 'CC' # must be url-safe
|
||||
qr = 'otpauth://totp/{nm}?secret={ss}'.format(ss=ss, nm=nm)
|
||||
|
||||
while 1:
|
||||
# show QR for enroll
|
||||
@ -124,13 +124,12 @@ async def web2fa_enroll(label, ss=None):
|
||||
force_msg=True)
|
||||
|
||||
# important: force them to prove they store it correctly
|
||||
ok = await perform_web2fa('Enroll: ' + label, ss)
|
||||
ok = await perform_web2fa('Enroll: COLDCARD', ss)
|
||||
if ok: break
|
||||
|
||||
ch = await ux_show_story("That isn't correct. Please re-import and/or "
|
||||
"try again or %s to give up." % X)
|
||||
if ch == 'x':
|
||||
# mk4 only?
|
||||
return None
|
||||
|
||||
return ss
|
||||
|
||||
@ -2,6 +2,9 @@
|
||||
#
|
||||
# Verify hobble works: a restricted access mode, without export/view of seed and more.
|
||||
#
|
||||
# - spending policy menu and txn checks should not be in this file, instead expand
|
||||
# test_ccc.py or create test_sssp.py
|
||||
#
|
||||
import pytest, time, re, pdb
|
||||
from helpers import prandom, xfp2str, str2xfp, str_to_path
|
||||
from bbqr import join_qrs
|
||||
@ -13,6 +16,8 @@ from test_backup import make_big_notes
|
||||
|
||||
'''TODO
|
||||
|
||||
When hobbled...
|
||||
|
||||
- check adv menu is minimal
|
||||
- load a secure note/pw; check readonly once hobbled
|
||||
- cannot export
|
||||
@ -22,9 +27,10 @@ from test_backup import make_big_notes
|
||||
- check readonly features on notes when note pre-defined before entering hobbled mode
|
||||
- notes hidden if the exist but access disabled in policy
|
||||
|
||||
- check KT only offered if MS wallet setup
|
||||
- scan a KT and have it rejected if not PSBT type: so R and E types
|
||||
- MS psbt KT should still work in hobbled mode: test_teleport.py::test_teleport_ms_sign
|
||||
- key teleport
|
||||
- check KT only offered if MS wallet setup
|
||||
- scan a KT and have it rejected if not PSBT type: so R and E types
|
||||
- MS psbt KT should still work in hobbled mode: test_teleport.py::test_teleport_ms_sign
|
||||
|
||||
- verify no settings menu
|
||||
- temp seeds are read only: no create, no rename, etc.
|
||||
@ -39,9 +45,9 @@ from test_backup import make_big_notes
|
||||
- q1 vs mk4 style
|
||||
- wrong values given, etc
|
||||
|
||||
- update menu tree w/ hobble mode view
|
||||
|
||||
- verify whitelist of QR types is correct when in hobbled mode
|
||||
- no private key material, no teleport starting, unless "okeys" is set
|
||||
|
||||
- update menu tree w/ hobble mode view
|
||||
|
||||
'''
|
||||
|
||||
Loading…
Reference in New Issue
Block a user