├── .gitignore
├── COPYING-CC
├── LICENSE
├── Makefile
├── README.md
├── chain.py
├── chrono.py
├── conn.py
├── data
├── .gitignore
└── README.md
├── docs
├── CNAME
├── README.md
├── examples.md
├── hacking.md
├── img
│ ├── animated-captcha.gif
│ ├── bk-setup-tab.png
│ ├── bunker-local-conf.png
│ ├── cc-setup-tab.png
│ ├── hsm-local-code.png
│ ├── simple-captcha.png
│ ├── snap-login.png
│ ├── snap-msg-sign.png
│ ├── snap-other-policy.png
│ ├── snap-paths.png
│ ├── snap-psbt-after.png
│ ├── snap-psbt-start.png
│ ├── snap-psbt-uploaded.png
│ ├── snap-psbt-visualized.png
│ ├── snap-rules.png
│ ├── snap-users-empty.png
│ └── snap-users.png
├── index.md
├── install.md
├── msg-signing.md
├── policy.md
├── psbt.md
├── screen-shot.jpg
└── setup.md
├── example-settings.yaml
├── main.py
├── make_captcha.py
├── objstruct.py
├── persist.py
├── policy.py
├── requirements.txt
├── setup.py
├── static
├── ext
│ ├── README.md
│ ├── base64js.min.js
│ ├── jquery-3.6.4.min.js
│ ├── lato-font.css
│ ├── lato
│ │ ├── README.md
│ │ ├── S6u8w4BMUTPHjxsAXC-v.ttf
│ │ ├── S6u9w4BMUTPHh6UVSwiPHA.ttf
│ │ ├── S6u_w4BMUTPHjxsI5wq_Gwfo.ttf
│ │ └── S6uyw4BMUTPHjx4wWw.ttf
│ ├── livestamp.min.js
│ ├── lodash.js
│ ├── lodash.min.js
│ ├── moment.min.js
│ ├── semantic-fonts
│ │ ├── brand-icons.eot
│ │ ├── brand-icons.svg
│ │ ├── brand-icons.ttf
│ │ ├── brand-icons.woff
│ │ ├── brand-icons.woff2
│ │ ├── icons.eot
│ │ ├── icons.otf
│ │ ├── icons.svg
│ │ ├── icons.ttf
│ │ ├── icons.woff
│ │ ├── icons.woff2
│ │ ├── outline-icons.eot
│ │ ├── outline-icons.svg
│ │ ├── outline-icons.ttf
│ │ ├── outline-icons.woff
│ │ └── outline-icons.woff2
│ ├── semantic-ubuntu.min.css
│ ├── semantic.min.css
│ ├── semantic.min.js
│ ├── ubuntu-font.css
│ ├── ubuntu
│ │ ├── 4iCp6KVjbNBYlgoKejZPslyPN4Q.ttf
│ │ ├── 4iCs6KVjbNBYlgoKfw7z.ttf
│ │ ├── 4iCu6KVjbNBYlgoKej70l0w.ttf
│ │ └── 4iCv6KVjbNBYlgoCxCvjsGyI.ttf
│ ├── vue.js
│ └── vue.min.js
├── favicon.png
├── fonts
│ ├── proximanova-semibold.ttf
│ └── ransom-note.ttf
├── html
│ ├── login.html
│ └── logout.html
├── html5shiv.js
├── navicon.png
├── project.js
└── style.css
├── status.py
├── templates
├── base.html
├── basic.html
├── bunker
│ └── index.html
├── help.html
├── login.html
├── macros.html
├── navpage.html
├── setup
│ ├── hsm-rule-component.html
│ ├── hsm-rule.html
│ ├── index.html
│ ├── misc.html
│ ├── onion.html
│ ├── paths.html
│ ├── rules.html
│ └── users.html
├── tools
│ └── index.html
└── txn
│ └── index.html
├── torsion.py
├── utils.py
├── version.py
└── webapp.py
/.gitignore:
--------------------------------------------------------------------------------
1 | ENV
2 | .DS_Store
3 |
4 | *.log
5 | *.swp
6 |
7 | __pycache__/
8 | *.pyc
9 |
10 | .tags
11 | pp
12 | history.yaml
13 |
14 | bunker.egg-info/
15 |
--------------------------------------------------------------------------------
/COPYING-CC:
--------------------------------------------------------------------------------
1 | (c) Copyright 2020 by Coinkite Inc.
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining
4 | a copy of this software and associated documentation files (the
5 | "Software"), to deal in the Software without restriction, including
6 | without limitation the rights to use, copy, modify, merge, publish,
7 | distribute, sublicense, and/or sell copies of the Software, and to
8 | permit persons to whom the Software is furnished to do so, subject
9 | to the following conditions:
10 |
11 | The above copyright notice and this permission notice shall be
12 | included in all copies or substantial portions of the Software.
13 |
14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
17 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR
18 | ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
19 | CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21 |
22 |
23 | "Commons Clause" License Condition v1.0
24 |
25 | The Software is provided to you by the Licensor under the License,
26 | as defined below, subject to the following condition.
27 |
28 | Without limiting other conditions in the License, the grant of
29 | rights under the License will not include, and the License does not
30 | grant to you, the right to Sell the Software.
31 |
32 | For purposes of the foregoing, "Sell" means practicing any or all
33 | of the rights granted to you under the License to provide to third
34 | parties, for a fee or other consideration (including without
35 | limitation fees for hosting or consulting/ support services related
36 | to the Software), a product or service whose value derives, entirely
37 | or substantially, from the functionality of the Software. Any license
38 | notice or attribution required by the License must also include
39 | this Commons Clause License Condition notice.
40 |
41 | Software: All Coldcard associated files.
42 | License: MIT
43 | Licensor: Coinkite Inc.
44 |
45 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | COPYING-CC
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | all:
2 | @echo "No default"
3 |
4 |
5 | tags:
6 | ctags -f .tags *.py */*.py
7 |
8 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # CKBunker
2 |
3 | 
4 |
5 | - [CKBunker preview screencast (youtube)](https://www.youtube.com/watch?v=0bHhZbYOiSM)
6 | - [Usage examples](https://github.com/Coldcard/ckbunker/blob/master/docs/examples.md) for HSM/CKBunker.
7 | - [CKBunker Documentation Website](https://ckbunker.com)
8 | - [Github for CKBunker](https://github.com/Coldcard/ckbunker)
9 | - [HSM Feature (on Coldcard) Docs](https://coldcardwallet.com/docs/ckbunker-hsm)
10 |
11 | ## Full Documentation
12 |
13 | 1. [Installation](https://github.com/Coldcard/ckbunker/blob/master/docs/install.md)
14 | 2. [Setup Bunker](https://github.com/Coldcard/ckbunker/blob/master/docs/setup.md)
15 | 2. [HSM Policy](https://github.com/Coldcard/ckbunker/blob/master/docs/policy.md)
16 | 2. [PSBT Signing](https://github.com/Coldcard/ckbunker/blob/master/docs/psbt.md)
17 | 2. [Message Signing](https://github.com/Coldcard/ckbunker/blob/master/docs/msg-signing.md)
18 | 2. [Contributing Code](https://github.com/Coldcard/ckbunker/blob/master/docs/hacking.md)
19 |
20 | ## What is the Coinkite Bunker?
21 |
22 | It's a python program that you run on a computer attached to a
23 | Coldcard. It will setup and operate the Coldcard in "HSM Mode" where
24 | it signs without a human pressing the OK key. To keep your
25 | funds safe, the Coldcard implements a complex set of spending rules
26 | which cannot be changed once HSM mode is started.
27 |
28 | Using the `tord` (Tor deamon) you already have, the CK Bunker can
29 | make itself available as a hidden service for remote access over
30 | Tor. A pretty website for setup and operation allows access to all
31 | HSM-related Coldcard features, including:
32 |
33 | - transaction signing, by uploading a PSBT; can broadcast signed txn using Blockstream.info (onion)
34 | - define policy rules, spending limits, velocity controls, logging policy
35 | - user setup (TOTP QR scan to enroll on Coldcard, or random passwords (Coldcard) or known password
36 |
37 | The bunker encrypts its own settings and stores the private key for
38 | that inside Coldcard's storage locker (which is kept inside the
39 | secure element of the Coldcard). The private key for the onion
40 | service, for example, is protected by that key.
41 |
42 | ## What is Coldcard?
43 |
44 | Coldcard is a Cheap, Ultra-secure & Opensource Hardware Wallet for Bitcoin.
45 | Get yours at [ColdcardWallet.com](http://coldcardwallet.com)
46 |
47 | Learn more about the [Coldcard HSM-related features](https://coldcardwallet.com/docs/ckbunker-hsm).
48 |
49 | [Follow @COLDCARDwallet on Twitter](https://twitter.com/coldcardwallet) to keep up
50 | with the latest updates and security alerts.
51 |
52 | ## FAQ
53 |
54 | ### Will HSM mode be supported on Mk1 or Mk2?
55 |
56 | Sorry no. CK Bunker only works on Mk3 because we need the extra RAM
57 | and the newer features of the 608 secure element.
58 |
59 | ### What is HSM?
60 |
61 | "Hardware Security Module"
62 |
63 | Learn more about the [Coldcard in HSM Mode](https://coldcardwallet.com/docs/ckbunker-hsm)
64 |
65 | ## Quotes
66 |
67 | > "Basically the cost of a Bitcoin HSM with custom policies is now the cost of a coldcard and you don't need a thirty party to maintain it." - Francis P.
68 |
--------------------------------------------------------------------------------
/chain.py:
--------------------------------------------------------------------------------
1 | # Copyright 2020 by Coinkite Inc. This file is covered by license found in COPYING-CC.
2 | #
3 | # chain.py --- API to blockstream stuff
4 | #
5 | import sys, os, asyncio, logging, requests
6 | from objstruct import ObjectStruct
7 | from persist import settings
8 | from status import STATUS
9 | from binascii import b2a_hex, a2b_hex
10 | from utils import json_loads
11 |
12 | logging.getLogger(__name__).addHandler(logging.NullHandler())
13 |
14 | def broadcast_txn(txn):
15 | # take bytes and get them shared over P2P to the world
16 | # - raise w/ text about what happened if it fails
17 | # - limited docs:
18 | ses = requests.session()
19 | ses.proxies = dict(http=settings.TOR_SOCKS)
20 | ses.headers.clear() # hide user-agent
21 |
22 | url = settings.EXPLORA
23 | url += '/api/tx' if not STATUS.is_testnet else '/testnet/api/tx'
24 |
25 | assert '.onion/' in url, 'dude, your privacy'
26 | logging.warning(f"Sending txn via: {url}")
27 | resp = ses.post(url, data=b2a_hex(txn).decode('ascii'))
28 |
29 | msg = resp.text
30 |
31 | if not resp.ok:
32 | # content is like:
33 | # sendrawtransaction RPC error: {"code":-22,"message":"TX decode failed"}
34 | # which is a text thing, including some JSON from bitcoind?
35 |
36 | if '"message":' in msg:
37 | try:
38 | prefix, rest = msg.split(': ', 1)
39 | j = json_loads(rest)
40 | if prefix == 'sendrawtransaction RPC error':
41 | msg = j.message
42 | else:
43 | msg = prefix + ': ' + j.message
44 | except:
45 | pass
46 |
47 | msg = f"Transaction broadcast FAILED: {msg}"
48 | logging.error(msg)
49 | return msg
50 |
51 | # untested
52 | msg = f"Transaction broadcast success: {msg}"
53 | logging.info(msg)
54 |
55 | return msg
56 |
57 | def link_to_txn(txn_hash):
58 | path = '/tx/' if not STATUS.is_testnet else '/testnet/tx/'
59 | assert len(txn_hash) == 64
60 | return settings.EXPLORA + path + txn_hash
61 |
62 |
63 | if __name__ == '__main__':
64 | # test code
65 | r = broadcast_txn(b'sdhffkhjkdfshdfshjdfshdfkshdfs')
66 |
67 | # EOF
68 |
--------------------------------------------------------------------------------
/chrono.py:
--------------------------------------------------------------------------------
1 | #
2 | # Time/data related functions. Trying hard not to rewrite existing things.
3 | #
4 | # Everything should be stored/calculated in UTC.
5 | #
6 | import time, datetime, calendar, pendulum
7 | from email.utils import formatdate as rfc_format
8 |
9 | def NOW():
10 | # Use this for everything.
11 | return pendulum.now()
12 |
13 | def TIME_AGO(**kws):
14 | # Return a time in past, like TIME_AGO(hours=3)
15 | return pendulum.now() - datetime.timedelta(**kws)
16 |
17 | def TIME_FUTURE(**kws):
18 | # Return a time in future, like TIME_FUTURE(hours=3)
19 | return pendulum.now() + datetime.timedelta(**kws)
20 |
21 | def as_time_t(dt):
22 | " convert datetime into unix timestamp (all UTC)"
23 | if hasattr(dt, 'timestamp'):
24 | # expected case for Pendulum values
25 | return dt.timestamp()
26 | else:
27 | return calendar.timegm(dt.utctimetuple())
28 |
29 | def from_time_t(time_t):
30 | " convert unix timestamp into datetime (all UTC)"
31 | return pendulum.from_timestamp(time_t)
32 |
33 | def from_iso(s):
34 | # Convert from ISO8601, and capture TZ
35 | return pendulum.parse(s)
36 |
37 | # EOF
38 |
--------------------------------------------------------------------------------
/conn.py:
--------------------------------------------------------------------------------
1 | # Copyright 2020 by Coinkite Inc. This file is covered by license found in COPYING-CC.
2 | #
3 | # Connection to Coldcard (and/or simulator).
4 | #
5 | import asyncio, logging, os
6 | from utils import Singleton, xfp2str, json_loads, json_dumps
7 | from status import STATUS
8 | from persist import settings, BP
9 | from binascii import a2b_hex
10 | import policy
11 | from objstruct import ObjectStruct
12 | from hmac import HMAC
13 | from hashlib import sha256
14 | from concurrent.futures import ThreadPoolExecutor
15 |
16 | from ckcc.protocol import CCProtocolPacker, CCFramingError
17 | from ckcc.protocol import CCProtoError, CCUserRefused
18 | from ckcc.constants import USB_NCRY_V2
19 | from ckcc.client import ColdcardDevice
20 | from ckcc.constants import (USER_AUTH_TOTP, USER_AUTH_HMAC, USER_AUTH_SHOW_QR, MAX_USERNAME_LEN)
21 | from ckcc.utils import calc_local_pincode
22 |
23 | logging.getLogger(__name__).addHandler(logging.NullHandler())
24 |
25 | executor = ThreadPoolExecutor(max_workers=5)
26 |
27 | # if you see this, it means the USB plug is fell out!
28 | class MissingColdcard(RuntimeError):
29 | pass
30 |
31 | #logging.info("fd = %d" % open('/dev/null').fileno())
32 |
33 | class Connection(metaclass=Singleton):
34 |
35 | def __init__(self, serial):
36 | self.serial = serial
37 | self.dev = None
38 | self.dev_key = None
39 | self.lock = asyncio.Lock()
40 | self.sign_lock = asyncio.Lock()
41 | self._conn_broken(setup_time=True)
42 |
43 | async def run(self):
44 | # connect to, and maintain a connection to a single Coldcard
45 |
46 | logging.info("Connecting to Coldcard.")
47 |
48 | while 1:
49 | try:
50 | if not self.serial and os.path.exists(settings.SIMULATOR_SOCK):
51 | # if simulator is running, just use it.
52 | sn = settings.SIMULATOR_SOCK
53 | else:
54 | sn = self.serial
55 |
56 | ncry_ver = settings.USB_NCRY_VERSION
57 | d = ColdcardDevice(sn=sn, ncry_ver=ncry_ver)
58 | logging.info(f"Found Coldcard {d.serial}. USB encryption version: {ncry_ver}")
59 |
60 | await asyncio.get_running_loop().run_in_executor(executor, d.check_mitm)
61 |
62 | async with self.lock:
63 | self.dev = d
64 | except:
65 | logging.error("Cannot connect to Coldcard (will retry)", exc_info=0)
66 | await asyncio.sleep(settings.RECONNECT_DELAY)
67 | continue
68 |
69 | # stay connected, and check we are working periodically
70 | logging.info(f"Connected to Coldcard {self.dev.serial}.")
71 |
72 | STATUS.connected = True
73 |
74 | # read static info about coldcard
75 | STATUS.xfp = xfp2str(self.dev.master_fingerprint)
76 | STATUS.serial_number = self.dev.serial
77 | STATUS.is_testnet = (self.dev.master_xpub[0] == 't')
78 | STATUS.hsm = {}
79 | STATUS.reset_pending_auth()
80 | STATUS.notify_watchers()
81 | await self.hsm_status()
82 |
83 | while 1:
84 | await asyncio.sleep(settings.PING_RATE)
85 | try:
86 | # use long timeout here, even tho simple command, because the CC may
87 | # we working on something else right now (thinking).
88 | h = await self.send_recv(CCProtocolPacker.hsm_status(), timeout=20000)
89 | logging.info("ping ok")
90 | await self.hsm_status(h)
91 | except MissingColdcard:
92 | self._conn_broken()
93 | break
94 | except:
95 | logging.error("Ping failed", exc_info=1)
96 |
97 | def _conn_broken(self, setup_time=False):
98 | # our connection is lost, so clear/reset system state
99 | if self.dev:
100 | self.dev.close()
101 | self.dev = None
102 |
103 | STATUS.connected = False
104 | STATUS.xfp = None
105 | STATUS.serial_number = None
106 | STATUS.is_testnet = False
107 | STATUS.hsm = {}
108 | STATUS.reset_pending_auth()
109 |
110 | if not setup_time:
111 | BP.reset()
112 |
113 | STATUS.notify_watchers()
114 |
115 | async def activated_hsm(self):
116 | # just connected to a Coldcard w/ HSM active already
117 | # - ready storage locker, decrypt and use those settings
118 | logging.info("Coldcard now in HSM mode. Fetching storage locker.")
119 |
120 | try:
121 | sl = await self.get_storage_locker()
122 | except CCProtoError as exc:
123 | if 'consumed' in str(exc):
124 | import os, sys
125 | msg = "Coldcard refused access to storage locker. Reboot it and enter HSM again"
126 | logging.error(msg)
127 | print(msg, file=sys.stderr)
128 | sys.exit(1)
129 | else:
130 | raise
131 |
132 | try:
133 | import policy
134 | xk = policy.decode_sl(sl)
135 | except:
136 | logging.error("Unable to parse contents of storage locker: %r" % sl)
137 | return
138 |
139 | if BP.open(xk):
140 | # unable to read our settings specific to this CC? Go to defaults
141 | # or continue?
142 | logging.error("Unable to read bunker settings for this Coldcard; forging on")
143 | else:
144 | STATUS.sl_loaded = True
145 |
146 | if BP.get('tor_enabled', False) and not (STATUS.force_local_mode or STATUS.setup_mode):
147 | # get onto Tor as a HS
148 | from torsion import TOR
149 | STATUS.tor_enabled = True
150 | logging.info(f"Starting hidden service: %s" % BP['onion_addr'])
151 | asyncio.create_task(TOR.start_tunnel())
152 |
153 | h = STATUS.hsm
154 | if ('summary' in h) and h.summary and not BP.get('priv_over_ux') and not BP.get('summary'):
155 | logging.info("Captured CC's summary of the policy")
156 | BP['summary'] = h.summary
157 | BP.save()
158 |
159 | STATUS.reset_pending_auth()
160 | STATUS.notify_watchers()
161 |
162 | async def send_recv(self, msg, **kws):
163 | # a more-async version of ColdcardDevice.send_recv?
164 |
165 | if not self.dev or not STATUS.connected:
166 | raise MissingColdcard
167 |
168 | try:
169 | def doit():
170 | return self.dev.send_recv(msg, **kws)
171 |
172 | # we do need this lock
173 | async with self.lock:
174 | return await asyncio.get_running_loop().run_in_executor(executor, doit)
175 |
176 | except CCFramingError:
177 | self._conn_broken()
178 | raise MissingColdcard
179 | except (CCProtoError, CCUserRefused):
180 | raise
181 | except BaseException as exc:
182 | logging.error(f"Error from Coldcard: {exc} (for msg: {msg!r}")
183 | self._conn_broken()
184 | raise MissingColdcard
185 |
186 | async def hsm_status(self, h=None):
187 | # refresh HSM status
188 | b4 = STATUS.hsm.get('active', False)
189 |
190 | try:
191 | b4_nlc = STATUS.hsm.get('next_local_code')
192 | h = h or (await self.send_recv(CCProtocolPacker.hsm_status()))
193 | STATUS.hsm = h = json_loads(h)
194 | STATUS.notify_watchers()
195 | except MissingColdcard:
196 | h = {}
197 |
198 | if h.get('next_local_code') and STATUS.psbt_hash:
199 | if b4_nlc != h.next_local_code:
200 | STATUS.local_code = calc_local_pincode(a2b_hex(STATUS.psbt_hash), h.next_local_code)
201 | else:
202 | # won't be required
203 | STATUS.local_code = None
204 |
205 | # has it just transitioned into HSM mode?
206 | if STATUS.connected and STATUS.hsm.active and not b4:
207 | await self.activated_hsm()
208 |
209 | return STATUS.hsm
210 |
211 | async def hsm_start(self, new_policy=None):
212 | args = []
213 | if new_policy is not None:
214 | # must upload it first
215 | data = json_dumps(new_policy).encode('utf8')
216 | args = self.dev.upload_file(data)
217 |
218 | # save a trimmed copy of some details, if they want that
219 | bk = policy.desensitize(new_policy)
220 | BP['summary'] = None
221 | if not bk.get('priv_over_ux'):
222 | BP['priv_over_ux'] = False
223 | BP['policy'] = bk # full copy
224 | BP['xfp'] = xfp2str(self.dev.master_fingerprint)
225 | BP['serial'] = self.dev.serial
226 | else:
227 | BP['priv_over_ux'] = True
228 | BP['policy'] = None
229 | BP['xfp'] = None
230 | BP['serial'] = None
231 |
232 | BP.save()
233 |
234 | try:
235 | await self.send_recv(CCProtocolPacker.hsm_start(*args))
236 | except CCProtoError as exc:
237 | msg = str(exc)
238 | logging.error("Coldcard didn't like policy: %s" % msg)
239 | raise RuntimeError(str(msg))
240 |
241 | async def delete_user(self, username):
242 | await self.send_recv(CCProtocolPacker.delete_user(username))
243 |
244 | async def create_user(self, username, authmode, new_pw=None):
245 | # typically we'll let Coldcard pick password
246 | if authmode == USER_AUTH_HMAC and new_pw:
247 | secret = self.dev.hash_password(new_pw.encode('utf8'))
248 | else:
249 | secret = b''
250 |
251 | await self.send_recv(CCProtocolPacker.create_user(username, authmode, secret))
252 |
253 | async def user_auth(self, username, token, totp, psbt_hash):
254 | if len(token) == 6 and token.isdigit():
255 | # assume TOTP if token (password) is 6-numeric digits
256 | totp_time = totp or int(time.time() // 30)
257 | token = token.encode('ascii')
258 | else:
259 | # assume it's a raw password. need to hash it up
260 | # TODO: move this hashing into browser
261 | secret = self.dev.hash_password(token.encode('utf8'))
262 | token = HMAC(secret, msg=psbt_hash, digestmod=sha256).digest()
263 | totp_time = 0
264 |
265 | await self.send_recv(CCProtocolPacker.user_auth(username.encode('ascii'), token, totp_time))
266 |
267 | async def get_storage_locker(self):
268 | return await self.send_recv(CCProtocolPacker.get_storage_locker())
269 |
270 | async def sign_psbt(self, data, finalize=False, flags=0x0):
271 | # upload it first
272 |
273 | async with self.sign_lock:
274 | sz, chk = self.dev.upload_file(data)
275 | assert chk == a2b_hex(STATUS.psbt_hash)
276 |
277 | await self.send_recv(CCProtocolPacker.sign_transaction(sz, chk, finalize, flags))
278 |
279 | # wait for it to finish
280 | return await self.wait_and_download(CCProtocolPacker.get_signed_txn())
281 |
282 | async def wait_and_download(self, req, fn=1):
283 | # Wait for user action (sic) on the device... by polling w/ indicated request
284 | # - also download resulting file
285 |
286 | while 1:
287 | await asyncio.sleep(0.250)
288 | done = await self.send_recv(req, timeout=None)
289 | if done == None:
290 | continue
291 | break
292 |
293 | if len(done) != 2:
294 | logging.error('Coldcard failed: %r' % done)
295 | raise RuntimeError(done)
296 |
297 | result_len, result_sha = done
298 |
299 | # download the result.
300 | result = self.dev.download_file(result_len, result_sha, file_number=fn)
301 |
302 | return result
303 |
304 | async def sign_text_msg(self, msg, subpath, addr_fmt):
305 | # send text and path to sign with; no policy check
306 |
307 | msg = msg.encode('ascii')
308 |
309 | async with self.sign_lock:
310 | try:
311 | await self.send_recv(CCProtocolPacker.sign_message(msg, subpath, addr_fmt))
312 |
313 | while 1:
314 | await asyncio.sleep(0.250)
315 | done = await self.send_recv(CCProtocolPacker.get_signed_msg(), timeout=None)
316 | if done == None:
317 | continue
318 | break
319 |
320 | except CCUserRefused:
321 | raise RuntimeError("Coldcard refused request based on policy.")
322 |
323 | if len(done) != 2:
324 | logging.error('Coldcard failed: %r' % done)
325 | raise RuntimeError(done)
326 |
327 | addr, sig = done
328 |
329 | return sig, addr
330 |
331 |
332 |
333 | # EOF
334 |
--------------------------------------------------------------------------------
/data/.gitignore:
--------------------------------------------------------------------------------
1 | *.dat
2 |
--------------------------------------------------------------------------------
/data/README.md:
--------------------------------------------------------------------------------
1 |
2 | # Data Files
3 |
4 | - this directory will hold data files for the Bunker
5 | - they are encrypted with a private key held in the "storage locker" of a Coldcard
6 | - contents include Tor hidden service private key and settings for Bunker
7 | - filename based on key
8 | - you may see unused junk accumulate in this directory; those are random keys that
9 | never got saved as a policy file for any Coldcard
10 |
--------------------------------------------------------------------------------
/docs/CNAME:
--------------------------------------------------------------------------------
1 | ckbunker.com
--------------------------------------------------------------------------------
/docs/README.md:
--------------------------------------------------------------------------------
1 |
2 | # CKBunker Docs
3 |
4 | This directory holds the documentation for CKBunker.
5 |
6 | Start reading with [index.md](index.md).
7 |
8 | We welcome contributions to these docs and code!
9 |
10 |
--------------------------------------------------------------------------------
/docs/examples.md:
--------------------------------------------------------------------------------
1 | ## Example CKBunker Use Cases
2 |
3 | ### Sign small transactions with just a password
4 |
5 | You can set a password and have your Coldcard sign anything below a custom amount provided you enter that password.
6 | Or instead require you to type a pin into the Coldcard for verification. [@6102](https://twitter.com/6102bitcoin/status/1228425672827293696)
7 |
8 | ### 2/2 multisig with geographic separation and specific spending rules
9 |
10 | You can use another Coldcard and program it to make sure no transactions get through that don't adhere to a set of rules you set in advance. This uses 2/2 multisig (meaning your regular wallet is the first and the Coldcard is the second, both needed to send any transaction).
11 |
12 | Your regular wallet will sign any transaction you send, but the Coldcard is like a security guard that won't also sign the transaciton unless it follows the rules you set. Rules can include where the bitcoins go (so if an attacker tries to steal your BTC using your main wallet, the Coldcard won't sign).
13 |
14 | Rules could also include a time of day or a max amount per day (or any other period of time). For a long-term hodl you might not want to let anyone be able to send the whole amount at once.
15 |
16 | What's cool is that this Coldcard could be located anywhere in the world and still sign your transaction.
17 |
18 | Because we give the Coldcard a set of rules we can let it execute automatically and sign any transaction that your main wallet presents, as long as the transaction follows whatever rules you set. An attacker would have to nab both your main wallet and the Coldcard (and possibly even reprogram the CC).
19 |
20 | While a geographically distributed 2/2 multi-sig is already possible today, it would require someone on the other end to click the buttons and co-sign. As this is pre-programmed it can run automatically and sign any TX within the rules.
21 |
22 | [Ben Prentice](https://twitter.com/mrcoolbp/status/1228868296486924289)
23 |
24 | ### Geographic separation
25 |
26 | Advanced: Your Coldcard could be in another country; you can lock Coldcard (boot-to-HSM™ feature). Remote hands can do power cycles if needed & keep Bunker running. Video-conference with them to send 6-digit code to complete a PSBT authentication (entered on Coldcard keypad). [@DocHex](https://twitter.com/DocHex/status/1228392653592649728)
27 |
28 |
29 | ### Freeze your warm wallet
30 |
31 | The HSM policy on your Coldcard could enable spending to just one single cold-storage address (via a whitelist). When your warm wallet is in danger, pull this cord to collect all UTXOs and send them to safety, signed, unattended, by the COLDCARDwallet
32 | [@DocHex](https://twitter.com/DocHex/status/1228394738841157632)
33 |
34 | ### Meet-me-in-the-Bunker™
35 |
36 | Time-based 2FA code from the phones of 3 of these 5 executives needed to authorize spending; Each exec connects to Bunker at the same time, checks proposed transaction and adds their OTP code. Only the Coldcard and exec's phone knows the shared 2FA secret. [@DocHex](https://twitter.com/DocHex/status/1228395590662397953)
37 | ·
38 |
39 | ### Text message signing
40 |
41 | You can disable PSBT signing completely and allow automatic signatures on text messages. This turn the Coldcard wallet
42 | into an HSM for Bitcoin-based auth/attestations. Can be limited to specific BIP32 subpath derivations. Same with address generation/derived XPUBS. [@DocHex](https://twitter.com/DocHex/status/1228396805102194688)
43 |
44 |
45 | ### Storage Locker™
46 |
47 | There is a locker of about 400 bytes of secret storage in the Mk3 secure element. CK Bunker uses this to hold the secret that encrypts bunker's settings (when at rest), such as the private key for the address of the Tor hidden service. So a corrupt LEA can't impersonate your bunker after capture. [@DocHex](https://twitter.com/DocHex/status/1228398842313310208)
48 |
49 | ### Multsig
50 |
51 | Heard you like co-signing! All the Bunker/HSM features work with multisig (P2SH / P2WSH) so maybe you're automatically co-signing some complex multisig from a CasaHODL quorum. [@DocHex](https://twitter.com/DocHex/status/1228403787955687427)
52 |
53 |
54 |
55 |
--------------------------------------------------------------------------------
/docs/hacking.md:
--------------------------------------------------------------------------------
1 |
2 | # Hacking CKBunker
3 |
4 | So you want to improve CKBunker? Sure. Here are some starting points.
5 |
6 | ## Structure
7 |
8 | It's a python program, based on `aiohttp` for async http operation. The web UI is
9 | provided using _Semantic UI_ and _Vue_ for model/view management. HTML pages
10 | are constructed using Jinja templates. Data between
11 | the browser and backend is communicated mainly via a websocket that
12 | stays open the entire time a page is shown in the browser.
13 |
14 | ## Important Dependancies
15 |
16 | See `requirements.txt` for complete list, but in summary, here are the major Python packages
17 | we are using.
18 |
19 | - `stem`
20 | - `aiohttp`
21 | - `aiohttp-jinja2`
22 | - `ckcc-protocol`
23 | - `pynacl`
24 | - `click`
25 | - `pendulum`
26 | - `requests[socks]`
27 |
28 | ## Major Files
29 |
30 | webapp.py - Web backend
31 | chain.py - API access for sending transactions
32 | chrono.py - Time related stuff
33 | conn.py - Connection to a Coldcard, somewhat async wrapping for ckcc-protocol
34 | main.py - Startup code
35 | persist.py - Data persistance and default settings
36 | policy.py - Manage HSM policy details.
37 | status.py - Live state information about the system and attached Coldcard
38 | torsion.py - Manage Tor hidden service connection (via stem)
39 | utils.py - My favourite type of code.
40 | make_captcha.py - Construct the capatcha.
41 | setup.py - Pip/Pypi glue
42 |
43 | templates/ - Jinja HTML templates, with JS and Vue code mixed in
44 | static/ - static CSS, JS and font resources (web)
45 | data/ - encrypted Bunker settings saved at run time.
46 | docs/ - these docs
47 |
48 | # Project Ideas
49 |
50 | Looking for something to do?
51 | Here are some loose ends or ideas we haven't been able start:
52 |
53 | - Integrate [PSBT faker](https://github.com/Coldcard/psbt_faker) for testing policy.
54 |
55 | - Recovery Tool: This will provide a means for you to construct a
56 | PSBT which moves all the funds the system can find on the blockchain
57 | to a new address. Use an onion-enabled block explorer to find UTXO
58 | or maybe some other backend.
59 |
60 | - Address Generator: Use this tool to make deposit addresses for
61 | your Coldcard's wallets.
62 |
63 |
64 | # Code Submission Guidelines
65 |
66 | PR's are welcome but...
67 |
68 | - Please think of other users: don't remove existing use cases.
69 | - Don't add weird dependancies if easy to avoid.
70 | - Try to match existing coding style.
71 | - Large diffs are hard to accept with security-sensitive projects like this.
72 | - Feel free to start your own fork and own it... we love that too!
73 |
74 |
75 |
76 |
--------------------------------------------------------------------------------
/docs/img/animated-captcha.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Coldcard/ckbunker/852675568a7c8a5ba27b80f648376ac51d612049/docs/img/animated-captcha.gif
--------------------------------------------------------------------------------
/docs/img/bk-setup-tab.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Coldcard/ckbunker/852675568a7c8a5ba27b80f648376ac51d612049/docs/img/bk-setup-tab.png
--------------------------------------------------------------------------------
/docs/img/bunker-local-conf.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Coldcard/ckbunker/852675568a7c8a5ba27b80f648376ac51d612049/docs/img/bunker-local-conf.png
--------------------------------------------------------------------------------
/docs/img/cc-setup-tab.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Coldcard/ckbunker/852675568a7c8a5ba27b80f648376ac51d612049/docs/img/cc-setup-tab.png
--------------------------------------------------------------------------------
/docs/img/hsm-local-code.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Coldcard/ckbunker/852675568a7c8a5ba27b80f648376ac51d612049/docs/img/hsm-local-code.png
--------------------------------------------------------------------------------
/docs/img/simple-captcha.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Coldcard/ckbunker/852675568a7c8a5ba27b80f648376ac51d612049/docs/img/simple-captcha.png
--------------------------------------------------------------------------------
/docs/img/snap-login.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Coldcard/ckbunker/852675568a7c8a5ba27b80f648376ac51d612049/docs/img/snap-login.png
--------------------------------------------------------------------------------
/docs/img/snap-msg-sign.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Coldcard/ckbunker/852675568a7c8a5ba27b80f648376ac51d612049/docs/img/snap-msg-sign.png
--------------------------------------------------------------------------------
/docs/img/snap-other-policy.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Coldcard/ckbunker/852675568a7c8a5ba27b80f648376ac51d612049/docs/img/snap-other-policy.png
--------------------------------------------------------------------------------
/docs/img/snap-paths.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Coldcard/ckbunker/852675568a7c8a5ba27b80f648376ac51d612049/docs/img/snap-paths.png
--------------------------------------------------------------------------------
/docs/img/snap-psbt-after.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Coldcard/ckbunker/852675568a7c8a5ba27b80f648376ac51d612049/docs/img/snap-psbt-after.png
--------------------------------------------------------------------------------
/docs/img/snap-psbt-start.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Coldcard/ckbunker/852675568a7c8a5ba27b80f648376ac51d612049/docs/img/snap-psbt-start.png
--------------------------------------------------------------------------------
/docs/img/snap-psbt-uploaded.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Coldcard/ckbunker/852675568a7c8a5ba27b80f648376ac51d612049/docs/img/snap-psbt-uploaded.png
--------------------------------------------------------------------------------
/docs/img/snap-psbt-visualized.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Coldcard/ckbunker/852675568a7c8a5ba27b80f648376ac51d612049/docs/img/snap-psbt-visualized.png
--------------------------------------------------------------------------------
/docs/img/snap-rules.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Coldcard/ckbunker/852675568a7c8a5ba27b80f648376ac51d612049/docs/img/snap-rules.png
--------------------------------------------------------------------------------
/docs/img/snap-users-empty.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Coldcard/ckbunker/852675568a7c8a5ba27b80f648376ac51d612049/docs/img/snap-users-empty.png
--------------------------------------------------------------------------------
/docs/img/snap-users.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Coldcard/ckbunker/852675568a7c8a5ba27b80f648376ac51d612049/docs/img/snap-users.png
--------------------------------------------------------------------------------
/docs/index.md:
--------------------------------------------------------------------------------
1 | # CKBunker
2 |
3 | 
4 |
5 | - [CKBunker preview screencast (youtube)](https://www.youtube.com/watch?v=0bHhZbYOiSM)
6 | - [Video: How To Use CKBUNKER Part 1: Install + Setup](https://www.youtube.com/watch?v=UVcnVb41NWQ)
7 | - [Video: How To Use CKBUNKER Part 2: Multi-Sig Policy](https://www.youtube.com/watch?v=_Jc7sLTT6ls)
8 | - [Usage examples](examples.md) for HSM/CKBunker.
9 | - [Documentation Website](https://ckbunker.com)
10 | - [Github for CKBunker](https://github.com/Coldcard/ckbunker)
11 | - [HSM Feature (on Coldcard) Docs](https://coldcardwallet.com/docs/ckbunker-hsm)
12 |
13 | ## Full Documentation
14 |
15 | 1. [Installation](install.md)
16 | 2. [Setup Bunker](setup.md)
17 | 2. [HSM Policy](policy.md)
18 | 2. [PSBT Signing](psbt.md)
19 | 2. [Message Signing](msg-signing.md)
20 | 2. [Contributing Code](hacking.md)
21 | 2. [Usage Examples](examples.md)
22 |
23 | ## What is the Coinkite Bunker?
24 |
25 | It's a python program that you run on a computer attached to a
26 | Coldcard. It will setup and operate the Coldcard in "HSM Mode" where
27 | it signs without a human pressing the OK key. To keep your
28 | funds safe, the Coldcard implements a complex set of spending rules
29 | which cannot be changed once HSM mode is started.
30 |
31 | Using the `tord` (Tor deamon) you already have, the CK Bunker can
32 | make itself available as a hidden service for remote access over
33 | Tor. A pretty website for setup and operation allows access to all
34 | HSM-related Coldcard features, including:
35 |
36 | - transaction signing, by uploading a PSBT; can broadcast signed txn using Blockstream.info (onion)
37 | - define policy rules, spending limits, velocity controls, logging policy
38 | - user setup (TOTP QR scan to enroll on Coldcard, or random passwords (Coldcard) or known password
39 |
40 | The bunker encrypts its own settings and stores the private key for
41 | that inside Coldcard's storage locker (which is kept inside the
42 | secure element of the Coldcard). The private key for the onion
43 | service, for example, is protected by that key.
44 |
45 | ## What is Coldcard?
46 |
47 | Coldcard is a Cheap, Ultra-secure & Opensource Hardware Wallet for Bitcoin.
48 | Get yours at [ColdcardWallet.com](http://coldcardwallet.com)
49 |
50 | Learn more about the [Coldcard HSM-related features](https://coldcardwallet.com/docs/ckbunker-hsm).
51 |
52 | [Follow @COLDCARDwallet on Twitter](https://twitter.com/coldcardwallet) to keep up
53 | with the latest updates and security alerts.
54 |
55 | ## FAQ
56 |
57 | ### Will HSM mode be supported on Mk1 or Mk2?
58 |
59 | Sorry no. CK Bunker only works on Mk3 because we need the extra RAM
60 | and the newer features of the 608 secure element.
61 |
62 | ### What is HSM?
63 |
64 | "Hardware Security Module"
65 |
66 | Learn more about the [Coldcard in HSM Mode](https://coldcardwallet.com/docs/ckbunker-hsm)
67 |
68 | ## Quotes
69 |
70 | > "Basically the cost of a Bitcoin HSM with custom policies is now the cost of a coldcard and you don't need a thirty party to maintain it." - Francis P.
71 |
72 |
73 |
--------------------------------------------------------------------------------
/docs/install.md:
--------------------------------------------------------------------------------
1 | # Installation
2 |
3 | ## Check-out and Setup
4 |
5 | Do a checkout, recursively to get all the submodules:
6 |
7 | git clone --recursive https://github.com/Coldcard/ckbunker.git
8 |
9 | Then:
10 |
11 | - `virtualenv -p python3 ENV` (Python 3.7 or higher is required)
12 | - `source ENV/bin/activate` (or `source ENV/bin/activate.csh` based on shell preference)
13 | - `pip install -r requirements.txt`
14 | - `pip install --editable .`
15 |
16 | ## Operational Requirements
17 |
18 | You will need:
19 |
20 | - this code (see above)
21 | - a Mk3 or Mk4 Coldcard connected via USB, running
22 | [firmware version 3.1.0 or later](https://coldcardwallet.com/docs/upgrade)
23 | - `tord` (Tor program)
24 | - an Internet connection
25 | - a Tor-capable browser, like "Tor Browser" or Tails.
26 | - (optional) a microSD card, for logging of transactions on Coldcard
27 | - (optional, recommended) a mobile phone with TOTP 2FA app, like Google Authenticator or FreeOTP
28 |
29 | ## Usage
30 |
31 | The executable is called `ckbunker`:
32 |
33 | ```sh
34 | $ ckbunker --help
35 | Usage: ckbunker [OPTIONS] COMMAND [ARGS]...
36 |
37 | Options:
38 | -s, --serial HEX Operate on specific unit (default: first found)
39 | --help Show this message and exit.
40 |
41 | Commands:
42 | list List all attached Coldcard devices
43 | example Show an example config file, using the default values
44 | run Start the CKBunker for normal operation
45 | setup Configure your transaction signing policy, install it and then...
46 | ```
47 |
48 | There are two modes for the Bunker: "setup" and "run mode". In setup
49 | mode, Tor connections are disabled, as is the login screen. There is no
50 | security and it's meant for initial setup of the Coldcard and Bunker.
51 |
52 | You would typically use the setup mode for picking the onion address, the
53 | master login password and all the details of the HSM policy.
54 |
55 | ```sh
56 | $ ckbunker setup
57 | ```
58 |
59 | Open this URL in your local web browser (must be same machine):
60 |
61 |
62 | Once the Coldcard is running in HSM mode, with your policy installed,
63 | it makes sense to operate in normal "run" mode. This enables a simple
64 | login screen to keep out visitors:
65 |
66 | ```sh
67 | $ ckbunker run
68 | ```
69 |
70 | You may also run with remote connections (and login) disabled. This would be useful
71 | if you have some existing web proxy already in place.
72 |
73 | ```sh
74 | $ ckbunker --local run
75 | ```
76 |
77 | ## Tor Use
78 |
79 | To access over Tor as a hidden service, you must have `tord` running
80 | on the same machine. For desktop systems, keeping TorBrowser open
81 | is enough to acheive this. On servers, start tord with default options,
82 | and ckbunker will use the control port (localhost port 9051 or 9151).
83 |
84 | If you use the bunker to broadcast the final (signed) transaction,
85 | the socks proxy of tord (port 9050) will also be used.
86 |
87 |
88 |
89 | ## Other Command Line Options
90 |
91 | ```sh
92 | % ckbunker run --help
93 | Usage: ckbunker run [OPTIONS]
94 |
95 | Start the CKBunker for normal operation
96 |
97 | Options:
98 | -l, --local Don't enable Tor (onion) access: just be on localhost
99 | -f, --psbt filename.psbt Preload first PSBT to be signed
100 | -c, --config-file FILENAME
101 | --help Show this message and exit.
102 |
103 | ```
104 |
105 | You can specify a PSBT file for immediate use. That file will be "uploaded"
106 | and be ready to sign, but the system operates normally from there. You can
107 | upload further PSBT files and so on.
108 |
109 | ```sh
110 | % ckbunker setup --help
111 | Usage: ckbunker setup [OPTIONS]
112 |
113 | Configure your transaction signing policy, install it and then operate.
114 |
115 | Options:
116 | -l, --local Don't enable Tor (onion) access: just be on localhost
117 | -c, --config-file FILENAME
118 | --help Show this message and exit.
119 | ```
120 |
121 | Both forms take an optional config file. It's simple YAML and allows
122 | you to change the web server port number and similar values.
123 | The values that can be configured are defined in `persist.py` in
124 | the `Settings` class. See also `example-settings.yaml`.
125 |
126 |
127 |
128 | # Next Steps
129 |
130 | [Bunker setup](setup.md)
131 |
--------------------------------------------------------------------------------
/docs/msg-signing.md:
--------------------------------------------------------------------------------
1 |
2 | # Text Message Signing
3 |
4 | Visit the tab "Tools". You can type in a short text message (single line is best) and
5 | select a derivation path to be used for signing. Segwit or classic bitcoin address
6 | is an option as well.
7 |
8 | Base64 signature in standard form is provided. The address is underneath.
9 |
10 | 
11 |
12 | Keep in mind when you share a signature, you are also sharing the public key for
13 | that address. This is a little more information than just a payment address.
14 |
15 | # Next Steps
16 |
17 | [Contributing code](hacking.md)
18 |
--------------------------------------------------------------------------------
/docs/policy.md:
--------------------------------------------------------------------------------
1 | # HSM Policy Config
2 |
3 | 
4 |
5 |
6 | # Spending Rules
7 |
8 | Multiple spending rules can be defined. The system scans the rules
9 | starting from the first one, and will test each rule. The first
10 | rule that is satisfied is applied and following rules are not considered.
11 |
12 | We recommend putting the most narrow rules first. Catch-all rules, which
13 | might move more money should be later in the list.
14 |
15 | By combining multiple rules, with diffent restrictions, it's possible
16 | to create a secure and yet flexible policy.
17 |
18 |
19 | 
20 |
21 |
22 | ## Velocity Time Period
23 |
24 | To implement spending limits based on time, the Coldcard requires you to define
25 | a period. This period, expressed in minutes, applies to all rules. Any rule with
26 | a defined "Per-Period Amount" value, will be affected.
27 |
28 | The period starts when it is first used. There is no absolute concept of
29 | time on the Coldcard because it doesn't have a real time clock. There is only one
30 | period, so it will begin as soon as any rule using a velocity limit
31 | is applied successfully.
32 |
33 | At the end of the time period, the totals are reset to zero.
34 |
35 | ## Individual Rules
36 |
37 | Each rule consists of these values, which are all considered at the same time.
38 |
39 | - _Max Amount_: max BTC per transaction, that this rule can apply to (independent of period)
40 | - _Per-Period Amount_: total BTC that can move thu this rule in the period
41 | - _Destination Whitelist_: a list of specific addresses which are allowed as destinations
42 | - _Multisig Wallet Name_: either name of a multisig wallet, or
43 | `1` indicating rule only applies to non-multisig wallets.
44 | - _Authorizing Users_: a list of users that are able to approve the transaction (N)
45 | _ _Minimum Users Needed_: number of users (M) needed to approve (from list of users
46 | for this rule, not the system)
47 | - _Local Confirmation Code needed_: a local user must (also) approve (via 6-digits entered on keypad)
48 |
49 | When an element of the rule is has no value, then the restriction
50 | does not apply. For example, if _Destination Whitelist_ is empty,
51 | then the Coldcard will not consider the destination address when
52 | considering the rule.
53 |
54 | If no rules are defined, then no PSBT will be signed. This can be
55 | useful for text message signing applications. On the other hand,
56 | a single empty rule, allows any transaction to be signed, so be careful!
57 |
58 | ### Max Transaction Amount
59 |
60 | Max amount per transaction is seems less useful because a number of transactions
61 | could be put together to "work around" this rule. However, if there is natural
62 | rate-limiting in your system, for example, by requiring a
63 | local operator to enter a code eeach time, then this is still helpful.
64 |
65 | ### Authorizing Users
66 |
67 | You can list username in the `users` field. If defined the `min_users` controls
68 | how many of those are required. By default (if `min_users` isn't defined), all users
69 | listed must confirm the operation. You can achieve 2-of-5 and similar setups
70 | using `min_users`. All users listed must already be defined on the Coldcard
71 | before the policy is activated.
72 |
73 | ### Limit to Named Wallet
74 |
75 | The `wallet` field can be omitted, or set to the name of a multisig wallet. If set
76 | to the string `1`, it indicates this rule only applies to the non-multisig wallet.
77 |
78 | ### Destination Whitelist
79 |
80 | You may specify a list of addresses in the whitelist field. The Coldcard will
81 | only apply the rule if all destination addresses of the PSBT transaction are
82 | included in the whitelist. This is a powerful feature when your target wallets
83 | that you control, such as emergency cold wallets.
84 |
85 |
86 |
87 | # User Management
88 |
89 | 
90 |
91 |
92 | To support use of the Coldcard in HSM mode, the Coldcard can hold
93 | usernames and their shared secrets for authentication purposes. At
94 | present, this is only useful for use in HSM mode. The user's login
95 | data (secrets) are stored exclusively on the Coldcard, and are never
96 | stored in the CKBunker.
97 |
98 | Two methods are offered: shared password (ie. classic "something
99 | you know") or TOTP (time-based one-time pass) 2FA authentication,
100 | compatible with [RFC6238](https://tools.ietf.org/html/rfc6238). Most
101 | people will already have an app on their mobile phone to hold
102 | the shared secrets and simplify the number-calculating process.
103 |
104 | Creating new users can **only** be done over USB protocol with the
105 | help of CKBunker or `ckcc` programs. However, once the user is
106 | established, you may view it and remove them from the menu system
107 | on the Coldcard, in the Advanced menu, under "User Management".
108 |
109 | The best practice is for the Coldcard to generate the password or
110 | TOTP secret and display it on-screen in a QR code. If you are using
111 | a TOTP app, such as Google Authenticator or FreeOTP, then you can
112 | scan the screen of the Coldcard to install the code. Unfortunately,
113 | due to limited screen space, there isn't room for the meta data
114 | such as username or specific Coldcard number: your app will only
115 | show "CC".
116 |
117 | It is possible to send a user-provided password over USB, in which
118 | case, the QR code is not shown. This requires trust of the attached
119 | computer during this operation, and so we do not recommend it.
120 |
121 | Click the (X) beside a username to remove it. This will invalid your
122 | HSM policy if that user is involved with a rule. You'll need to change
123 | the rule, or create a new user with the old name.
124 |
125 |
126 | # Derivation Paths
127 |
128 | This section controls message signing and derivation paths allowed for
129 | sharing addresses and derived xpubs.
130 |
131 | 
132 |
133 | ## Message Signing
134 |
135 | To enable text message signing, list one or more BIP32 derivation paths in this section.
136 | You can use the special value `any` to allow all signing. You may also use a star
137 | in the last position of a path, like these examples:
138 |
139 | - `m/84'/0'/0/*`
140 | - `m/84'/0'/0'/*'`
141 | - `m/9984/*`
142 |
143 | The star allows any number in the final position (only). It does not allow deeper paths.
144 |
145 | ## Sharing Xpubs
146 |
147 | The Coldcard can calculate XPUB values for derived paths, if desired.
148 | You can limit this feature by giving a list of permitted paths, or
149 | the keyword `any` to allow any subpath. The master xpub (`m`)
150 | is always available over USB protocol and cannot be disabled.
151 |
152 | ## Share Derived Addresses
153 |
154 | Similarly, the Coldcard can calculate wallet addresses, if this
155 | setting contains a list of whitelisted derivation paths. Star
156 | patterns, and the keyword `any` can be used, as well as the keyword
157 | `p2sh` which allows addresses in multisig wallets to be shared.
158 |
159 | In the case of multisig wallets, we do not check the script provided,
160 | beyond the normal checks for inclusion into a known multisig wallet.
161 |
162 | # Other Policy
163 |
164 | This section covers global policy choices. Most settings are simple booleans.
165 |
166 | 
167 |
168 |
169 | ## Logging to MicroSD Card
170 |
171 | Two setting affect logging: _Must log_ and _Never log_. By default,
172 | the Coldcard will log if a card is inserted. It does not fail if
173 | the card is missing. If that is an issue for you, then set _Must log_
174 | and transactions will be refused if the card isn't installed and
175 | working. _Never log_ is useful when you don't want to keep records
176 | at the Coldcard's location.
177 |
178 | ## Warnings Okay?
179 |
180 | This boolean allows the Coldcard to sign PSBT files that have
181 | warnings. Typically warning are generated by overly-large fees or
182 | weird path derivations. Since we don't expect warnings, any
183 | transactions with a warning is normally refused.
184 |
185 | ## Privacy over UX
186 |
187 | During development of the Bunker, we found there were numerous
188 | status and informational values being shared over USB that, to some
189 | degree, assist potential attackers. However, those values are needed
190 | to provide a usable interface and a nice user experience (UX).
191 |
192 | If you set _Privacy over UX_, the following values will not be
193 | shared over [USB in the HSM status response](protocol):
194 |
195 | - text summary of the spending policy
196 | - count of approvals / refusals
197 | - the number of time the storage locker has been read
198 | - the period length
199 | - when the period will end
200 | - how much each rule has spent in current period
201 | - system uptime
202 | - list of usernames
203 | - number of users which have provided auth credentials for current PSBT
204 |
205 | The CKBunker can operate in either mode, but you will find it harder
206 | to use, as it's not possible to know where you stand in terms of
207 | velocity spending and user authorization.
208 |
209 | ## Boot to HSM
210 |
211 | This feature forces the Coldcard to start in HSM mode immediately
212 | after boot up (after entry of the master PIN).
213 |
214 | You specify a 6-digit numeric code and if that code is provided in
215 | the first 60 seconds after startup, the Coldcard will leave HSM
216 | mode. Alternatively, you may
217 | set _Do not accept any code_, and the Coldcard can never leave HSM mode.
218 |
219 | !!! warning "Bricking Hazard"
220 |
221 | No changes to firmware, HSM policy, Coldcard settings will be possible—ever again.
222 |
223 | Not even the master PIN holder can change HSM policy nor escape HSM
224 | mode! Firmware upgrades are not possible.
225 |
226 | Boot-to-HSM is mainly useful mainly if the local operator does not
227 | have the authority to spend the funds, but does know the PIN code
228 | so they can assist with powerfail restart. In most applications, we
229 | expect someone with PIN knowledge and also the spending authority
230 | to power-up and enable HSM mode.
231 |
232 | ## Notes
233 |
234 | This is simply free-form text shown on the Coldcard when approving HSM Policy.
235 | Up to 80 characters allowed. You could put the master password and/or
236 | onion address here for documentation purposes.
237 |
238 | # Save and Start Policy
239 |
240 | When you are happy with your HSM policy, press "Save and Start
241 | Policy". You may want to capture a copy of the settings for later
242 | use. The CKBunker does not save the details (but may same a text
243 | summary) so it cannot re-create the policy rules completely. Coldcard
244 | saves the details but does not offer a means to share them later.
245 |
246 | Once you've hit the button, the attached Coldcard will prompt you for
247 | approval. You will get to see the policy as the Coldcard understands it,
248 | and you should read that careful to confirm it matches you intentions.
249 |
250 | After final approval, the HSM policy file is written into the
251 | Coldcard's flash memory and it enters HSM mode. On subsequent
252 | reboots, you will be prompted to start HSM mode on each time.
253 |
254 | # Next Steps
255 |
256 | [Signing a PSBT file](psbt.md)
257 |
258 |
--------------------------------------------------------------------------------
/docs/psbt.md:
--------------------------------------------------------------------------------
1 |
2 | # Daily Operation: Signing PSBT Files
3 |
4 | ## PSBT Files
5 |
6 | You need to construct the PSBT file on another system.
7 | The CKBunker does not track the blockchain or know your UTXO. We will not make
8 | any assumptions about how you create PSBT files, and there are a growing number of
9 | wallets that can do it: BitcoinCore and Electrum for example.
10 |
11 | For testing purposes, we recommend
12 | [`psbt_faker`](https://github.com/Coldcard/psbt_faker) which will
13 | take your XPUB, and make arbitrary fake transactions immediately
14 | suitable for signing as PSBT files.
15 | This is a good way to test your policy choices with specific values and other what-ifs.
16 |
17 | ## Sign Transaction Tab
18 |
19 | Select "Sign Transaction" from the top bar, and you will see something similar to this:
20 |
21 | 
22 |
23 | The top is status dashboard, showing details of the Coldcard's current state. Hover
24 | over those fields for additional detail, such as a breakdown of spending per rule.
25 |
26 | At the bottom, a text summary, original from the Coldcard, shows the current
27 | policy in effect. For this example, the policy is as follows:
28 |
29 | ```text
30 | Transactions:
31 | - Rule #1: Up to 1 XTN per period will be approved
32 | - Rule #2: Any amount will be approved if local user confirms
33 |
34 | Velocity Period:
35 | 60 minutes
36 | = 1 hrs
37 |
38 | Message signing:
39 | - Allowed if path matches: (any path)
40 |
41 | Other policy:
42 | - MicroSD card will receive log entries.
43 | - Storage Locker will be updated, and can be read 13 times.
44 | ```
45 |
46 | The next step is to upload a PSBT file. Binary, hex or base64 encoded files are
47 | accepted. When you've done that, the screen will change:
48 |
49 | 
50 |
51 | If you want to know what the PSBT transaction will do, based on the Coldcard's
52 | understanding of it, press the "Display" button. The PSBT is sent to the Coldcard,
53 | and the text summary of what it does will be shown on your web browser. This is
54 | the same text that would be shown on-screen if the Coldcard was not in HSM mode.
55 |
56 | 
57 |
58 | Depending on your goals, there are three toggles to consider now:
59 |
60 | - Broadcast transaction immediately: CKBunker will send the finalized transaction to the
61 | Bitcoin P2P network for possible inclusion in the next block. Implies finalize.
62 | - Finalize transaction: In addition to PSBT signing, ask the Coldcard to finalize
63 | the transaction. Resulting file will be hex, ready for broadcast.
64 | - Download signed PSBT or transaction. Send the file to your browser immediately. You may not
65 | need the file if it's broadcast already.
66 |
67 | Adjust those to your needs, and press "Sign Transaction".
68 |
69 | If the transaction is acceptable to the policy, then it will be
70 | signed and broadcast directly, and/or downloaded to your browser.
71 |
72 | The dashboard updates immediately after signing, and in this case, shows the
73 | updated velocity limits.
74 |
75 | 
76 |
77 | ## Local User Confirmation Code
78 |
79 | The only local physical interaction possible with a Coldcard in HSM
80 | mode is to enter a local authorization code. This can be required
81 | by specific HSM policy rules, but is completely optional.
82 |
83 | As the local operator enters the 6-digit numeric code, the digits
84 | are shown in the top right corner of the screen. Press OK to apply
85 | them, or X to clear and start over. Codes are always 6 digits. There
86 | is no indication the code worked or failed, in part because it isn't
87 | tested until the PSBT is given for signing, which could be some
88 | time later.
89 |
90 | 
91 |
92 | The required code is a combination of the specific bytes
93 | of the PSBT file being approved, and also a salt value picked by
94 | the Coldcard.
95 |
96 | When the PSBT file has been uploaded, use the CKBunker updates the local code:
97 |
98 | 
99 |
100 | How you get this code into the Coldcard left to you. Please note
101 | the local operator must enter the code _before_ you press the "Sign Transaction"
102 | button on the CKBunker.
103 |
104 | A different code will be required for each attempted signing (because
105 | Coldcard changes the salt value) and for every PSBT file (because
106 | the hash of the PSBT is a factor in this number).
107 |
108 | ## User Authorization
109 |
110 | Depending on your configuration, you may see a number of username/password
111 | fields, in the top right area.
112 |
113 | 
114 |
115 | Enter the TOTP 2FA code (6 digits) from your app. If you are using
116 | passwords, enter that instead. If you have enabled "Privacy
117 | over UX", you will need to enter the username on the left side as well.
118 | Obviously, both the username and password must be correct.
119 |
120 | When you press "Sign Transaction", all the OTP/password data you
121 | have entered is submitted before the PSBT is considered by the
122 | Coldcard. If do you provide codes/passwords, they must be right, or
123 | the Coldcard will not proceed. When values are omitted (left blank)
124 | then it is not an error, but of course, rules that require the
125 | user's authorization will not be accepted.
126 |
127 | ## "Meet Me In The Bunker" Usage Case
128 |
129 | When multiple people need to authorize a transaction, they can all
130 | meet in the bunker: each user, from anywhere in the world, connects
131 | to the Bunker via Tor. Each will see the same state on the _Sign Transaction_ page.
132 | One can can upload a transaction (PSBT) and
133 | press the visualize button. All the screens update at the
134 | same time. When each person is satisfied, they may enter their OTP code
135 | or password to help approve the transaction. The other users will
136 | see this happening in real time.
137 |
138 | The CKBunker stores the current PSBT under consideration in RAM,
139 | so there is no need for this interaction to happen simultaneously.
140 | Users can come and go as needed to complete this process serially.
141 |
142 | # Next Steps
143 |
144 | [Msg Signing](msg-signing.md)
145 |
--------------------------------------------------------------------------------
/docs/screen-shot.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Coldcard/ckbunker/852675568a7c8a5ba27b80f648376ac51d612049/docs/screen-shot.jpg
--------------------------------------------------------------------------------
/docs/setup.md:
--------------------------------------------------------------------------------
1 | # Setup of Your CKBunker
2 |
3 | Step 1 is to install the bunker code and get it running.
4 |
5 | Once it's running, use any ordinary browser to connect to:
6 |
7 | http://localhost:9823/setup
8 |
9 | At this point, you should be focused on the two tabs "Coldcard Setup"
10 | and "Bunker Setup" tabs.
11 |
12 | ## Coldcard Setup tab
13 |
14 | 
15 |
16 | "Coldcard Setup" is devoted to creating an HSM spending policy. These settings
17 | are held in a JSON file to be uploaded and confirmed on the Coldcard.
18 |
19 | Typically the Bunker will send the file to the Coldcard when you
20 | press "Save & Start Policy" but you have the option of downloading
21 | the file, and/or importing the JSON file before that point. This is useful if you
22 | want to hand-edit the data, or keep a backup/restore a backup. When you
23 | send the file to the Coldcard, your browser will download a copy of the file,
24 | if the checkbox "Download (sanitized) copy" is set. This version of the policy
25 | file will have the following sensitive fields stripped out:
26 |
27 | - `boot_to_hsm` - unlock code for
28 | - `set_sl`, `allow_sl` - details of the storage locker
29 |
30 | Continue reading [here for details about HSM rules and policy.](policy.md)
31 |
32 | ### Using Coldcard Setup _without_ a Coldcard
33 |
34 | By using the "Download Policy" button you can use the pretty web
35 | interface to build your policy file without using any other part
36 | of the CKBunker. In fact a Coldcard does not need to be connected.
37 |
38 | One limitation of this mode is the _Users_ section. It communicates
39 | directly with the Coldcard to read the list of users and add/remove them.
40 | If you are using those features, you may need to edit the JSON. The same
41 | is true for the names of your multisig wallets.
42 |
43 | If you use the Bunker to upload your policy, the `allow_sl` and
44 | `set_sl` fields will be overriden by the Bunker and replaced. You
45 | can use `ckcc hsm-start file.json` to upload the JSON policy file,
46 | and start HSM mode on the command line.
47 |
48 | ## Bunker Setup tab
49 |
50 | 
51 |
52 | There are only a few settings for the Bunker itself:
53 |
54 | - Enable or disable Tor Hidden Service: Once enabled, you may
55 | generate a different onion address by picking "Spin Again".
56 |
57 | - Master Login password: this password is needed to get in to the CKBunker
58 | over the Tor network. There are no usernames. Cannot be shorter than 4 characters.
59 |
60 | - Simple captcha: on the login form, there are two possible styles of captcha.
61 |
62 | - Simple Captcha
63 |
64 | 
65 |
66 | - Animated Captcha
67 |
68 | 
69 |
70 | - "Allow Bunker to be restarted without requiring a restart of the Coldcard":
71 | This setting should be configured before you save and apply your HSM policy
72 | on the "Coldcard Setup" page. It controls the number `allow_sl` inside
73 | the policy, and sets that value to `1` or `13`. If it's `1`, then the
74 | Coldcard will allow only a single read of the Storage Locker, and the
75 | effect of that is the CKBunker can only be reset/started once without
76 | knowing the PIN of the Coldcard.
77 |
78 | # Login Screen
79 |
80 | For your reference, the login screen presented to visitors looks like this
81 | with the animated captcha.
82 |
83 | 
84 |
85 | ### How Bunker Settings are Saved
86 |
87 | In our security model, we assume the CKBunker may be "captured" by
88 | your adversaries. The CKBunker is not the last line of defense---that's the Coldcard
89 | and your HSM policy.
90 |
91 | If the Bunker is captured while it is turned on and running, then
92 | in the worst case, the attackers can read all of main memory, and
93 | will get the Tor Hidden service private key. This will allow them
94 | to impersonate your Bunker in the future. Presumably they can view
95 | your PSBT file and all other web interactions while you work with the Bunker.
96 |
97 | However, they cannot change the HSM policy of the Coldcard. They
98 | do not know the TOTP/2FA secrets, and cannot generate OTP codes
99 | (only the Coldcard knows those) and so they cannot authorize spending
100 | that way and impersonate users. If the attackers are remote, relative
101 | to the location of the Coldcard, the "local confirmation code" feature
102 | will also protect you, since the attackers would need to convince
103 | your remote hands to enter a specific code.
104 |
105 | When the data associated with the Bunker is "at rest", meaning the
106 | system is not running, we have good protections in place. All Bunker
107 | settings are saved to `./data` directory, as encrypted files. The
108 | name of the file is a hash of the private key, and the contents are
109 | a NaCl secret box (Curve25519). We store the private key for that
110 | file on the Coldcard itself, in the storage locker.
111 |
112 | When the Bunker starts, it searchs for a Coldcard on USB (and it also
113 | looks for the simulator). If it finds one already in HSM mode, then
114 | it reads the storage locker and uses the NaCl private key (32 bytes)
115 | to select and open the corresponding Bunker settings file. Therefore,
116 | each Coldcard has it's own settings for the Bunker.
117 |
118 | In setup mode, bunker settings are effectively not saved until
119 | the NaCL secret is saved into the policy of a Coldcard and saved
120 | there.
121 |
122 |
123 | #### Other Notes
124 |
125 | - PSBT files are never saved to disk. They stay in-memory only.
126 | - HSM Policy files are not saved to disk, except as part of the settings.
127 | - If you don't choose _Privacy over UX_ then many key details of your HSM
128 | policy are captured and saved into the encrypted settings. This includes details
129 | like the HSM text summary, user names, and other details that are know only
130 | when the policy is created.
131 | - USB Encryption should be set to version 2 if firmware supports this .
132 | For now default version is still 1. To enable version 2:
133 | `echo "USB_NCRY_VERSION: 2" > /tmp/ckbunker_ncryV2.yaml; ckbunker setup -c /tmp/ckbunker_ncryV2.yaml`. With version 2
134 | enabled, in case of any ckbunker or communication failure, one needs to re-login to Coldcard
135 |
136 | # Next Steps
137 |
138 | [HSM policy setup](policy.md)
139 |
--------------------------------------------------------------------------------
/example-settings.yaml:
--------------------------------------------------------------------------------
1 | # Example configuration file. See persist.py for variable names and default values.
2 | #
3 | # Usage:
4 | #
5 | # ckbunker setup -c example-settings.yaml
6 | # ckbunker run -c example-settings.yaml
7 | #
8 | PORT_NUMBER: 9800
9 |
--------------------------------------------------------------------------------
/main.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | #
3 | # Copyright 2020 by Coinkite Inc. This file is covered by license found in COPYING-CC.
4 | #
5 | # Main entry-point for project.
6 | #
7 | # To use this, install with:
8 | #
9 | # pip install --editable .
10 | #
11 | # That will create the command "ckbunker" in your path. Use it!
12 | #
13 | import os, sys, click, hid, asyncio, logging
14 | from pprint import pformat, pprint
15 |
16 | global force_serial
17 | force_serial = None
18 |
19 | from ckcc.protocol import CCProtocolPacker
20 | from ckcc.protocol import CCProtoError, CCUserRefused, CCBusyError
21 | from ckcc.client import ColdcardDevice, COINKITE_VID, CKCC_PID
22 |
23 |
24 | # Options we want for all commands
25 | @click.group()
26 | @click.option('--serial', '-s', default=None, metavar="HEX",
27 | help="Operate on specific unit (default: first found)")
28 | def main(serial):
29 | global force_serial
30 | force_serial = serial
31 |
32 | @main.command('list')
33 | def _list():
34 | "List all attached Coldcard devices"
35 |
36 | count = 0
37 | for info in hid.enumerate(COINKITE_VID, CKCC_PID):
38 | #click.echo("\nColdcard {serial_number}:\n{nice}".format(
39 | # nice=pformat(info, indent=4)[1:-1], **info))
40 | click.echo(info['serial_number'])
41 | count += 1
42 |
43 | if not count:
44 | click.echo("(none found)")
45 |
46 | @main.command('example')
47 | def example_config():
48 | "Show an example config file, using the default values"
49 |
50 | from persist import Settings
51 |
52 | click.echo(Settings.make_sample())
53 |
54 | @main.command('run')
55 | @click.option('--local', '-l', default=False, is_flag=True,
56 | help="Don't enable Tor (onion) access: just be on localhost")
57 | @click.option('--psbt', '-f', metavar="filename.psbt",
58 | help="Preload first PSBT to be signed", default=None,
59 | type=click.File('rb'))
60 | @click.option('--config-file', '-c', type=click.File('rt'), required=False)
61 | def start_service(local=False, config_file=None, psbt=None):
62 | "Start the CKBunker for normal operation"
63 |
64 | if psbt:
65 | psbt = psbt.read()
66 |
67 | asyncio.run(startup(False, local, config_file, psbt), debug=True)
68 |
69 | @main.command('setup')
70 | @click.option('--local', '-l', default=False, is_flag=True,
71 | help="Don't enable Tor (onion) access: just be on localhost")
72 | @click.option('--config-file', '-c', type=click.File('rt'), required=False)
73 | def setup_hsm(local=False, config_file=None):
74 | "Configure your transaction signing policy, install it and then operate."
75 |
76 | asyncio.run(startup(True, local, config_file, None), debug=True)
77 |
78 | async def startup(setup_mode, force_local_mode, config_file, first_psbt):
79 | # All startup/operation code
80 |
81 | loop = asyncio.get_running_loop()
82 | if loop.get_debug():
83 | # quiet noise about slow stuff
84 | loop.slow_callback_duration = 10
85 |
86 | from utils import setup_logging
87 | setup_logging()
88 |
89 | from persist import Settings
90 | Settings.startup(config_file)
91 |
92 | aws = []
93 |
94 | # copy some args into status area
95 | from status import STATUS
96 | STATUS.force_local_mode = force_local_mode
97 | STATUS.setup_mode = setup_mode
98 |
99 | # preload the contents of a PSBT
100 | if first_psbt:
101 | STATUS.import_psbt(first_psbt)
102 |
103 | from torsion import TOR
104 | aws.append(TOR.startup())
105 |
106 | from conn import Connection
107 | aws.append(Connection(force_serial).run())
108 |
109 | import webapp
110 | aws.append(webapp.startup(setup_mode))
111 |
112 |
113 | await asyncio.gather(*aws)
114 |
115 |
116 | # EOF
117 |
--------------------------------------------------------------------------------
/make_captcha.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | #
3 | # Draw a Captchas ... not meant to be hard, but easier to replace out, and challenging
4 | # to read image itself..
5 | #
6 | import random, os, io
7 | from PIL import Image, ImageDraw, ImageFont
8 |
9 | # Avoid similar-looking letters/numbers.
10 | TOKEN_CHARS = 'abcdefghkmnpqrstuvwxyz23456789'
11 |
12 | class CaptchaMaker:
13 | size = (256, 64) # limited by iphone case. esp. when entering value
14 |
15 | def __init__(self, seed=None):
16 | self.rng = random.Random(seed)
17 |
18 |
19 | def draw(self, token):
20 | # draw some crazy captcha for indicated token and return as a
21 | # tuple: extension, raw_data
22 | raise NotImplementedError
23 |
24 | def get_font(self, size=40, which='nova'):
25 | fn = { 'ransom': 'static/fonts/ransom-note.ttf',
26 | 'nova': 'static/fonts/proximanova-semibold.ttf'
27 | }
28 | return ImageFont.truetype(fn[which], size)
29 |
30 | class RansomCaptcha(CaptchaMaker):
31 | #
32 | # Just the simple ransom note that most people expect today.
33 | #
34 | def draw(self, token, foreground='#000', seed=None):
35 | fn = self.get_font(size=40, which='ransom')
36 | w,h = self.size
37 | _,_,dx,dy = fn.getbbox('W')
38 |
39 | im = Image.new('RGBA', self.size)
40 | dr = ImageDraw.Draw(im)
41 |
42 | # some of the lower-case letters are confusing, so replace them
43 | remap = dict(x = 'X', s='S', a='A', g='G', q='Q')
44 |
45 | x = 10
46 | ix = (w - (x*2)) * (len(token)-2) / dx
47 | for ch in token:
48 | ch = remap.get(ch, ch)
49 | y = self.rng.randint(-5, h-dy+5)
50 | dr.text( (x, y), ch, fill=foreground, font=fn)
51 | x += ix
52 |
53 | data = io.BytesIO()
54 | im.save(data, format='png')
55 |
56 | return 'png', data.getvalue()
57 |
58 | class MegaGifCaptcha(CaptchaMaker):
59 | # These are fun, but too large to be practical? Keep in toolbox for later.
60 |
61 | def draw(self, token, foreground='#fff', background='white'):
62 |
63 | token = ' '.join(token.upper())
64 |
65 | fn = self.get_font(size=30, which='nova')
66 | w,h = self.size
67 | _,_,dx,dy = fn.getbbox('W')
68 |
69 | randint = self.rng.randint
70 | sample = self.rng.sample
71 |
72 | actual = []
73 | pass_thru = set()
74 | frames = []
75 |
76 | ans_y = self.rng.randint(3, h-dy-3)
77 | ans_w = fn.getbbox(token)[2]
78 | count = 15
79 |
80 | for fr_num in range(count):
81 | im = Image.new('P', self.size, background)
82 | dr = ImageDraw.Draw(im)
83 |
84 | # give them noise chars, except they are mostly correct so that
85 | # the order is not so clear at all. don't want to just be able to
86 | # pick the most common chars observed
87 | charset = set(token)
88 | while len(charset) < len(token) + 4:
89 | charset.add(sample(TOKEN_CHARS, 1)[0])
90 |
91 | for k in range(int(w * 1.5/dx)):
92 | #ch = ''.join(self.rng.sample(TOKEN_CHARS, 1)).upper()
93 | ch = ''.join(sample(list(charset), 1)).upper()
94 | x = randint(-dx, w)
95 | y = ans_y + randint(int(-dy*3/4), int(dy*3/4))
96 | dr.text( (x,y), ch, fill=foreground, font=fn)
97 |
98 | frames.append(im)
99 |
100 | x = (w-ans_w)*fr_num / count
101 | dr.text( (x, ans_y), token, fill=foreground, font=fn)
102 |
103 | data = io.BytesIO()
104 |
105 | frames[0].save(data, format='gif', save_all=True, loop=0,
106 | append_images=frames + list(reversed(frames[1:-1])))
107 |
108 | return 'gif', data.getvalue()
109 |
110 | # EOF
111 |
--------------------------------------------------------------------------------
/objstruct.py:
--------------------------------------------------------------------------------
1 | # Copyright 2020 by Coinkite Inc. This file is covered by license found in COPYING-CC.
2 | #
3 | # objstruct.py
4 | #
5 |
6 | class ObjectStruct(dict):
7 | '''An object like both a dict and also an object that you can
8 | easily use attr reference to get members. Construct with a
9 | dict or like a dict.
10 | '''
11 |
12 | def __getattr__(self, name):
13 | if name in self:
14 | return self[name]
15 | else:
16 | # Do not return a default here because it breaks things
17 | raise AttributeError('No such attribute: %s' % name)
18 |
19 | def __setattr__(self, name, value):
20 | self[name] = value
21 |
22 | def __delattr__(self, name):
23 | del self[name]
24 |
25 | def __repr__(self):
26 | ret = '<%s:' % self.__class__.__name__
27 | for k,v in self.items():
28 | ret += ' %s=%r' % (k, v)
29 | return ret + '>'
30 |
31 | @classmethod
32 | def promote(cls, x):
33 | # Often I get a dict() from an API wrapper that's taken some json and
34 | # run it thru json.loads(). It would be better to have that as nested
35 | # ObjectStruct (which is easily done with some arguments to loads, but
36 | # usually they don't provide that feature)... so call this function
37 | #
38 |
39 | if isinstance(x, list):
40 | return [cls.promote(i) for i in x]
41 |
42 | if isinstance(x, dict):
43 | x = cls(x)
44 | for k in x:
45 | x[k] = cls.promote(x[k])
46 |
47 | return x
48 |
49 | class DefaultObjectStruct(ObjectStruct):
50 | ''' Same, but can provide a default value if get_default is overriden'''
51 | def get_default(self, fldname):
52 | # override me
53 | return None
54 |
55 | def __getattr__(self, name):
56 | if name in self:
57 | return self[name]
58 | else:
59 | # sometimes you do want a default.
60 | return self.get_default(name)
61 |
62 | # EOF
63 |
--------------------------------------------------------------------------------
/persist.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | #
3 | # Persistent data for Bunker itself. Trying to minimize this for privacy.
4 | #
5 | import os, yaml, nacl.secret, logging
6 | from utils import Singleton, xfp2str, json_dumps, json_loads, WatchableMixin
7 | from hashlib import sha256
8 | from objstruct import ObjectStruct
9 |
10 | logging.getLogger(__name__).addHandler(logging.NullHandler())
11 |
12 | # globals, used system-wide
13 | settings = None
14 | BP = None
15 |
16 | # System-wide settings for Bunker itself.
17 | #
18 | class Settings(metaclass=Singleton):
19 |
20 | # web server port
21 | PORT_NUMBER = 9823
22 |
23 | # session idle time, before we kick you out and require re-auth (seconds)
24 | MAX_IDLE_TIME = 10*60
25 |
26 | # max time between showing login page, and the would-be user entering something useful (seconds)
27 | MAX_LOGIN_WAIT_TIME = 5*60
28 |
29 | # bogus fixed password to get started
30 | MASTER_PW = 'test1234'
31 |
32 | # default is harder captcha
33 | EASY_CAPTCHA = False
34 |
35 | # default for "allow reboot of bunker"
36 | # - can you restart the bunker w/o restarting the Coldcard HSM?
37 | ALLOW_REBOOTS = True
38 |
39 | # path to data files
40 | DATA_FILES = './data'
41 |
42 | # endpoint to use for sending txn; we assume it's Explora protocol (Blockstream.info)
43 | EXPLORA = 'http://explorerzydxu5ecjrkwceayqybizmpjjznk5izmitf2modhcusuqlid.onion'
44 |
45 | # port number for local instance of tord
46 | # - will try 9051 and 9151
47 | # - but first /var/run/tor/control as unix socket
48 | TORD_PORT = 'default'
49 |
50 | # for broadcasting, socks proxy via Tord
51 | TOR_SOCKS = 'socks5h://127.0.0.1:9150'
52 |
53 | # unix pipe for local Coldcard Simulator
54 | SIMULATOR_SOCK = '/tmp/ckcc-simulator.sock'
55 |
56 | # delay between retries connecting to missing/awol Coldcard
57 | RECONNECT_DELAY = 10 # seconds between retries
58 | PING_RATE = 15 # seconds between pings (CC status checks)
59 | USB_NCRY_VERSION = 0x01 # default ncry version is 1
60 |
61 | # USB encryption versions (default 1)
62 | #
63 | # V2 introduces a new ncry version to close a potential attack vector:
64 | #
65 | # A malicious program may re-initialize the connection encryption by sending the ncry command a second time during USB operation.
66 | # This may prove particularly harmful in HSM mode.
67 | #
68 | # Sending version 0x02 changes the behavior in two ways:
69 | # * All future commands must be encrypted
70 | # * Returns an error if the ncry command is sent again for the duration of the power cycle
71 | #
72 | # If using 0x02 and ckbunker is killed - you also need to re-login to Coldcard
73 |
74 | def read(self, fobj):
75 | t = yaml.safe_load(fobj)
76 | if not t: return
77 |
78 | for k,v in t.items():
79 | if k.upper() != k or k[0]=='_':
80 | logging.error(f"{k}: must be upper case")
81 | continue
82 | if not hasattr(self, k):
83 | logging.error(f"{k}: unknown setting")
84 | continue
85 |
86 | setattr(self, k, v)
87 |
88 | @classmethod
89 | def make_sample(cls):
90 | # produce an example config file
91 | d = {}
92 | x = cls()
93 | for k in dir(x):
94 | if k.upper() != k or k[0]=='_': continue
95 | d[k] = getattr(x, k)
96 |
97 | return yaml.safe_dump(d)
98 |
99 | @classmethod
100 | def startup(cls, config_file=None):
101 | # creates singleton
102 | global settings, BP
103 |
104 | # only safe place to create singletons is here
105 | assert not settings and not BP
106 |
107 | settings = Settings()
108 | if config_file:
109 | settings.read(config_file)
110 |
111 | # load defaults into BP
112 | BP = BunkerPersistance()
113 | BP.reset()
114 |
115 | # Store some state, encrypted.
116 | # - inial values are the settings, but lower case for some reason
117 | # - some are adjustable on "Bunker Setup" page
118 | class BunkerPersistance(WatchableMixin, dict, metaclass=Singleton):
119 | fields = ['tor_enabled', 'onion_pk', 'onion_addr', 'allow_reboots',
120 | 'easy_captcha', 'master_pw']
121 |
122 | def __init__(self):
123 | super(BunkerPersistance, self).__init__()
124 | self.filename = None
125 | self.reset()
126 |
127 | def reset(self):
128 | self.clear()
129 | self.set_secret(os.urandom(32))
130 | self.set_defaults()
131 |
132 | def set_defaults(self):
133 | # defaults here
134 | for fn in self.fields:
135 | if fn not in self:
136 | self[fn] = getattr(settings, fn.upper(), None)
137 |
138 | def set_secret(self, key):
139 | # setup for reading/writing using indicated key
140 | assert len(key) == 32
141 |
142 | self.key = key
143 | self.box = nacl.secret.SecretBox(self.key)
144 |
145 | # calc filename
146 | bn = 'bp-%s.dat' % sha256(sha256(b'salty' + self.key).digest()).hexdigest()[-16:].lower()
147 | self.filename = os.path.join(settings.DATA_FILES, bn)
148 |
149 | def open(self, key):
150 | # Given a private key (via storage locker) open a Nacl secret box
151 | # and use that for the data.
152 | self.set_secret(key)
153 |
154 | try:
155 | with open(self.filename, 'rb') as fp:
156 | d = self.box.decrypt(fp.read())
157 | d = json_loads(d)
158 | except FileNotFoundError:
159 | logging.info("%s: not found (probably fine)" % self.filename)
160 | return True
161 |
162 | self.update(d)
163 |
164 | # copy a setting to status (XXX feels wrong)
165 | from status import STATUS
166 | STATUS.tor_enabled = self.get('tor_enabled', False)
167 |
168 | logging.info(f"Got bunker settings from: {self.filename}")
169 |
170 | def save(self):
171 | fn = self.filename
172 | tmp = fn + '.tmp'
173 | with open(tmp, 'wb') as fp:
174 | d = json_dumps(dict(self)).encode('utf8')
175 | d = self.box.encrypt(d)
176 | fp.write(d)
177 |
178 | os.rename(tmp, fn)
179 | logging.info(f"Saved bunker settings to: {fn}")
180 |
181 | self.notify_watchers()
182 |
183 | def delete_file(self):
184 | # useful when changing keys; old file won't be readable
185 | try:
186 | os.unlink(self.filename)
187 | logging.info(f"Deleted bunker settings in: {self.filename}")
188 | except:
189 | pass
190 |
191 |
192 | # EOF
193 |
--------------------------------------------------------------------------------
/policy.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | #
3 | # policy.py -- code which knows various details about HSM policy as defined by Coldcard.
4 | #
5 | import re, logging
6 | from decimal import Decimal
7 | from objstruct import ObjectStruct
8 | from persist import BP, settings
9 | from base64 import b64encode, b64decode
10 |
11 | logging.getLogger(__name__).addHandler(logging.NullHandler())
12 |
13 | def invalid_pincode(code):
14 | return (not code) or (len(code) != 6) or (not code.isdigit())
15 |
16 | def web_cleanup(p):
17 | # takes policy details from Vue/Semantic/web browser format into proper JSON-able dict
18 | # - final product should serialize into something the Coldcard will accept
19 |
20 | def relist(n):
21 | # split on spaces or commas, assume values don't have either; trim whitespace
22 | if n is None: return n
23 | return [i for i in re.split(r' |,|\n', n) if i]
24 |
25 | for fn in ['msg_paths', 'share_xpubs', 'share_addrs']:
26 | p[fn] = relist(p.get(fn, None))
27 |
28 | p.period = int(p.period) if p.period else None
29 |
30 | for idx, rule in enumerate(p.rules):
31 | for fn in ['whitelist', 'users']:
32 | rule[fn] = relist(rule[fn])
33 |
34 | # change from BTC to satoshis (send as string here)
35 | for fn in ['per_period', 'max_amount']:
36 | v = rule.get(fn, None) or None
37 | if v is not None:
38 | try:
39 | v = Decimal(v)
40 | except:
41 | raise ValueError(f"Rule #{idx+1} field {fn} is invalid: {rule[fn]}")
42 | rule[fn] = int(v * Decimal('1E8'))
43 | else:
44 | # cleans up empty strings
45 | rule[fn] = None
46 |
47 | # text to number
48 | if not rule.users:
49 | rule.pop('min_users')
50 | else:
51 | rule.min_users = len(rule.users) if rule.min_users == 'all' else int(rule.min_users)
52 |
53 | if p.pop('ewaste_enable', False):
54 | p.boot_to_hsm = 'xyzzy' # impossible to enter
55 | assert invalid_pincode(p.boot_to_hsm)
56 | else:
57 | p.boot_to_hsm = p.get('boot_to_hsm') or None
58 | if p.boot_to_hsm:
59 | assert not invalid_pincode(p.boot_to_hsm), \
60 | "Boot to HSM code must be 6 numeric digits."
61 |
62 | return p
63 |
64 | def web_cookup(proposed):
65 | # converse of above: take Coldcard policy file, and rework it so
66 | # Vue can display on webpage
67 |
68 | p = ObjectStruct.promote(proposed)
69 |
70 | def unlist(n):
71 | if not n: return ''
72 | return ','.join(n)
73 |
74 | for fn in ['msg_paths', 'share_xpubs', 'share_addrs']:
75 | p[fn] = unlist(p.get(fn))
76 |
77 | for rule in p.rules:
78 | for fn in ['whitelist', 'users']:
79 | rule[fn] = unlist(rule.get(fn))
80 |
81 | for fn in ['per_period', 'max_amount']:
82 | if rule[fn] is not None:
83 | rule[fn] = str(Decimal(rule[fn]) / Decimal('1E8'))
84 |
85 | if 'min_users' not in rule:
86 | rule.min_users = 'all'
87 | else:
88 | rule.min_users = str(rule.min_users)
89 |
90 | if ('boot_to_hsm' in p) and p.boot_to_hsm and invalid_pincode(p.boot_to_hsm):
91 | p.ewaste_enable = True
92 | else:
93 | p.ewaste_enable = False
94 |
95 | return p
96 |
97 |
98 | def desensitize(policy):
99 | # remove the most sensitive stuff in the policy.
100 | bk = policy.copy()
101 | bk.pop('set_sl', None)
102 | bk.pop('allow_sl', None)
103 | bk.pop('boot_to_hsm', None)
104 |
105 | return bk
106 |
107 | def decode_sl(xk):
108 | # Unpack what we saved into the Storage Locker
109 | # - 32 bytes of nacl secret box for BunkerPersistance, plus "Bunk" prefix => 36 bytes
110 | # - base64 encoded => 48 bytes (and has no padding)
111 | assert len(xk) == 48, repr(xk)
112 | xk = b64decode(xk)
113 | assert xk[0:4] == b'Bunk'
114 | rv = xk[4:]
115 | assert len(rv) == 32
116 |
117 | return rv
118 |
119 | def update_sl(proposed):
120 | # We control the set_sl/allow_sl values solely for bunker purposes (sl=storage locker)
121 |
122 | # try to use any value already provided (but unlikely)
123 | xk = proposed.get('set_sl', None) or None
124 | if xk:
125 | try:
126 | xk = decode_sl(xk)
127 | except:
128 | logging.error("Unable to decode existing storage locker; replacing", exc_info=1)
129 | xk = None
130 |
131 | if not xk:
132 | # capture settings key
133 | xk = BP.key
134 |
135 | assert len(xk) == 32
136 | proposed['set_sl'] = b64encode(b'Bunk' + xk).decode('ascii')
137 |
138 | if xk != BP.key:
139 | # re-use existing key, and switch over to using new/eixsting key
140 | BP.delete_file()
141 | BP.set_secret(xk)
142 | BP.save()
143 | else:
144 | logging.info("Re-using old secret for holding Bunker settings")
145 |
146 | # simple fixed value for how many times we can re-read the storage locker
147 | proposed['allow_sl'] = 13 if BP.get('allow_reboots', True) else 1
148 |
149 |
150 | # EOF
151 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | # See also setup.py install_requires
2 | #
3 | stem==1.8.0
4 | aiohttp>=3.7.4
5 | aiohttp-jinja2>=1.2
6 | ckcc-protocol>=1.0.1
7 | pynacl==1.3.0
8 | aiohttp_session
9 | click
10 | pendulum==2.0.3
11 | pyyaml
12 | Pillow
13 | pytest
14 | requests[socks]
15 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | # Copyright 2020 by Coinkite Inc. This file is covered by license found in COPYING-CC.
2 | #
3 | # based on
4 | #
5 | # To use this, install with:
6 | #
7 | # pip install --editable .
8 |
9 | from setuptools import setup
10 |
11 | with open("README.md", "r") as fh:
12 | long_description = fh.read()
13 |
14 | setup(
15 | name='bunker',
16 | version='0.1',
17 | license='MIT+CC',
18 | python_requires='>=3.7.0',
19 | url='https://github.com/Coldcard/ckbunker',
20 | author='Coinkite Inc.',
21 | author_email='support@coinkite.com',
22 | description="Submit PSBT files automatically to your Coldcard for signing",
23 | long_description=long_description,
24 | long_description_content_type="text/markdown",
25 | install_requires=[ # see requirements.txt tho.
26 | 'Click',
27 | 'stem',
28 | 'aiohttp',
29 | 'aiohttp-jinja2',
30 | 'ckcc-protocol>=1.3.2',
31 | 'pyyaml',
32 | 'pynacl==1.3.0',
33 | 'pendulum==2.0.3',
34 | 'aiohttp_session',
35 | 'requests[socks]',
36 | ],
37 | entry_points='''
38 | [console_scripts]
39 | ckbunker=main:main
40 | ''',
41 | classifiers=[
42 | 'Operating System :: POSIX :: Linux',
43 | 'Operating System :: Microsoft :: Windows',
44 | 'Operating System :: MacOS :: MacOS X',
45 | ],
46 | packages=[],
47 | )
48 |
--------------------------------------------------------------------------------
/static/ext/README.md:
--------------------------------------------------------------------------------
1 |
2 | ## Background
3 |
4 | - Semantic UI parts taken from v2.4.1 at
5 |
6 | - one long path is remapped (rather than edit min.css files)
7 | /static/ext/themes/default/assets/fonts/ => /static/semantic-fonts
8 |
--------------------------------------------------------------------------------
/static/ext/base64js.min.js:
--------------------------------------------------------------------------------
1 | (function(r){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=r()}else if(typeof define==="function"&&define.amd){define([],r)}else{var e;if(typeof window!=="undefined"){e=window}else if(typeof global!=="undefined"){e=global}else if(typeof self!=="undefined"){e=self}else{e=this}e.base64js=r()}})(function(){var r,e,n;return function(){function d(a,f,i){function u(n,r){if(!f[n]){if(!a[n]){var e="function"==typeof require&&require;if(!r&&e)return e(n,!0);if(v)return v(n,!0);var t=new Error("Cannot find module '"+n+"'");throw t.code="MODULE_NOT_FOUND",t}var o=f[n]={exports:{}};a[n][0].call(o.exports,function(r){var e=a[n][1][r];return u(e||r)},o,o.exports,d,a,f,i)}return f[n].exports}for(var v="function"==typeof require&&require,r=0;r0){throw new Error("Invalid string. Length must be a multiple of 4")}var n=r.indexOf("=");if(n===-1)n=e;var t=n===e?0:4-n%4;return[n,t]}function f(r){var e=c(r);var n=e[0];var t=e[1];return(n+t)*3/4-t}function h(r,e,n){return(e+n)*3/4-n}function i(r){var e;var n=c(r);var t=n[0];var o=n[1];var a=new d(h(r,t,o));var f=0;var i=o>0?t-4:t;var u;for(u=0;u>16&255;a[f++]=e>>8&255;a[f++]=e&255}if(o===2){e=v[r.charCodeAt(u)]<<2|v[r.charCodeAt(u+1)]>>4;a[f++]=e&255}if(o===1){e=v[r.charCodeAt(u)]<<10|v[r.charCodeAt(u+1)]<<4|v[r.charCodeAt(u+2)]>>2;a[f++]=e>>8&255;a[f++]=e&255}return a}function s(r){return u[r>>18&63]+u[r>>12&63]+u[r>>6&63]+u[r&63]}function l(r,e,n){var t;var o=[];for(var a=e;ai?i:f+a))}if(t===1){e=r[n-1];o.push(u[e>>2]+u[e<<4&63]+"==")}else if(t===2){e=(r[n-2]<<8)+r[n-1];o.push(u[e>>10]+u[e>>4&63]+u[e<<2&63]+"=")}return o.join("")}},{}]},{},[])("/")});
2 |
--------------------------------------------------------------------------------
/static/ext/lato-font.css:
--------------------------------------------------------------------------------
1 | @font-face {
2 | font-family: 'Lato';
3 | font-style: italic;
4 | font-weight: 400;
5 | src: local('Lato Italic'), local('Lato-Italic'), url(lato/S6u8w4BMUTPHjxsAXC-v.ttf) format('truetype');
6 | }
7 | @font-face {
8 | font-family: 'Lato';
9 | font-style: italic;
10 | font-weight: 700;
11 | src: local('Lato Bold Italic'), local('Lato-BoldItalic'), url(lato/S6u_w4BMUTPHjxsI5wq_Gwfo.ttf) format('truetype');
12 | }
13 | @font-face {
14 | font-family: 'Lato';
15 | font-style: normal;
16 | font-weight: 400;
17 | src: local('Lato Regular'), local('Lato-Regular'), url(lato/S6uyw4BMUTPHjx4wWw.ttf) format('truetype');
18 | }
19 | @font-face {
20 | font-family: 'Lato';
21 | font-style: normal;
22 | font-weight: 700;
23 | src: local('Lato Bold'), local('Lato-Bold'), url(lato/S6u9w4BMUTPHh6UVSwiPHA.ttf) format('truetype');
24 | }
25 |
--------------------------------------------------------------------------------
/static/ext/lato/README.md:
--------------------------------------------------------------------------------
1 |
2 | Based on files referenced from the response to:
3 |
4 |
5 |
6 | As requested by semantic.min.css
7 |
--------------------------------------------------------------------------------
/static/ext/lato/S6u8w4BMUTPHjxsAXC-v.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Coldcard/ckbunker/852675568a7c8a5ba27b80f648376ac51d612049/static/ext/lato/S6u8w4BMUTPHjxsAXC-v.ttf
--------------------------------------------------------------------------------
/static/ext/lato/S6u9w4BMUTPHh6UVSwiPHA.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Coldcard/ckbunker/852675568a7c8a5ba27b80f648376ac51d612049/static/ext/lato/S6u9w4BMUTPHh6UVSwiPHA.ttf
--------------------------------------------------------------------------------
/static/ext/lato/S6u_w4BMUTPHjxsI5wq_Gwfo.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Coldcard/ckbunker/852675568a7c8a5ba27b80f648376ac51d612049/static/ext/lato/S6u_w4BMUTPHjxsI5wq_Gwfo.ttf
--------------------------------------------------------------------------------
/static/ext/lato/S6uyw4BMUTPHjx4wWw.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Coldcard/ckbunker/852675568a7c8a5ba27b80f648376ac51d612049/static/ext/lato/S6uyw4BMUTPHjx4wWw.ttf
--------------------------------------------------------------------------------
/static/ext/livestamp.min.js:
--------------------------------------------------------------------------------
1 | // Livestamp.js / v1.1.2 / (c) 2012 Matt Bradley / MIT License
2 | (function(d,g){var h=1E3,i=!1,e=d([]),j=function(b,a){var c=b.data("livestampdata");"number"==typeof a&&(a*=1E3);b.removeAttr("data-livestamp").removeData("livestamp");a=g(a);g.isMoment(a)&&!isNaN(+a)&&(c=d.extend({},{original:b.contents()},c),c.moment=g(a),b.data("livestampdata",c).empty(),e.push(b[0]))},k=function(){i||(f.update(),setTimeout(k,h))},f={update:function(){d("[data-livestamp]").each(function(){var a=d(this);j(a,a.data("livestamp"))});var b=[];e.each(function(){var a=d(this),c=a.data("livestampdata");
3 | if(void 0===c)b.push(this);else if(g.isMoment(c.moment)){var e=a.html(),c=c.moment.fromNow();if(e!=c){var f=d.Event("change.livestamp");a.trigger(f,[e,c]);f.isDefaultPrevented()||a.html(c)}}});e=e.not(b)},pause:function(){i=!0},resume:function(){i=!1;k()},interval:function(b){if(void 0===b)return h;h=b}},l={add:function(b,a){"number"==typeof a&&(a*=1E3);a=g(a);g.isMoment(a)&&!isNaN(+a)&&(b.each(function(){j(d(this),a)}),f.update());return b},destroy:function(b){e=e.not(b);b.each(function(){var a=
4 | d(this),c=a.data("livestampdata");if(void 0===c)return b;a.html(c.original?c.original:"").removeData("livestampdata")});return b},isLivestamp:function(b){return void 0!==b.data("livestampdata")}};d.livestamp=f;d(function(){f.resume()});d.fn.livestamp=function(b,a){l[b]||(a=b,b="add");return l[b](this,a)}})(jQuery,moment);
5 |
--------------------------------------------------------------------------------
/static/ext/semantic-fonts/brand-icons.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Coldcard/ckbunker/852675568a7c8a5ba27b80f648376ac51d612049/static/ext/semantic-fonts/brand-icons.eot
--------------------------------------------------------------------------------
/static/ext/semantic-fonts/brand-icons.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Coldcard/ckbunker/852675568a7c8a5ba27b80f648376ac51d612049/static/ext/semantic-fonts/brand-icons.ttf
--------------------------------------------------------------------------------
/static/ext/semantic-fonts/brand-icons.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Coldcard/ckbunker/852675568a7c8a5ba27b80f648376ac51d612049/static/ext/semantic-fonts/brand-icons.woff
--------------------------------------------------------------------------------
/static/ext/semantic-fonts/brand-icons.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Coldcard/ckbunker/852675568a7c8a5ba27b80f648376ac51d612049/static/ext/semantic-fonts/brand-icons.woff2
--------------------------------------------------------------------------------
/static/ext/semantic-fonts/icons.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Coldcard/ckbunker/852675568a7c8a5ba27b80f648376ac51d612049/static/ext/semantic-fonts/icons.eot
--------------------------------------------------------------------------------
/static/ext/semantic-fonts/icons.otf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Coldcard/ckbunker/852675568a7c8a5ba27b80f648376ac51d612049/static/ext/semantic-fonts/icons.otf
--------------------------------------------------------------------------------
/static/ext/semantic-fonts/icons.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Coldcard/ckbunker/852675568a7c8a5ba27b80f648376ac51d612049/static/ext/semantic-fonts/icons.ttf
--------------------------------------------------------------------------------
/static/ext/semantic-fonts/icons.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Coldcard/ckbunker/852675568a7c8a5ba27b80f648376ac51d612049/static/ext/semantic-fonts/icons.woff
--------------------------------------------------------------------------------
/static/ext/semantic-fonts/icons.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Coldcard/ckbunker/852675568a7c8a5ba27b80f648376ac51d612049/static/ext/semantic-fonts/icons.woff2
--------------------------------------------------------------------------------
/static/ext/semantic-fonts/outline-icons.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Coldcard/ckbunker/852675568a7c8a5ba27b80f648376ac51d612049/static/ext/semantic-fonts/outline-icons.eot
--------------------------------------------------------------------------------
/static/ext/semantic-fonts/outline-icons.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Coldcard/ckbunker/852675568a7c8a5ba27b80f648376ac51d612049/static/ext/semantic-fonts/outline-icons.ttf
--------------------------------------------------------------------------------
/static/ext/semantic-fonts/outline-icons.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Coldcard/ckbunker/852675568a7c8a5ba27b80f648376ac51d612049/static/ext/semantic-fonts/outline-icons.woff
--------------------------------------------------------------------------------
/static/ext/semantic-fonts/outline-icons.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Coldcard/ckbunker/852675568a7c8a5ba27b80f648376ac51d612049/static/ext/semantic-fonts/outline-icons.woff2
--------------------------------------------------------------------------------
/static/ext/ubuntu-font.css:
--------------------------------------------------------------------------------
1 | /* from https://fonts.googleapis.com/css?family=Ubuntu:400,400i,700,700i&display=swap */
2 | @font-face {
3 | font-family: 'Ubuntu';
4 | font-style: italic;
5 | font-weight: 400;
6 | font-display: swap;
7 | src: local('Ubuntu Italic'), local('Ubuntu-Italic'), url(ubuntu/4iCu6KVjbNBYlgoKej70l0w.ttf) format('truetype');
8 | }
9 | @font-face {
10 | font-family: 'Ubuntu';
11 | font-style: italic;
12 | font-weight: 700;
13 | font-display: swap;
14 | src: local('Ubuntu Bold Italic'), local('Ubuntu-BoldItalic'), url(ubuntu/4iCp6KVjbNBYlgoKejZPslyPN4Q.ttf) format('truetype');
15 | }
16 | @font-face {
17 | font-family: 'Ubuntu';
18 | font-style: normal;
19 | font-weight: 400;
20 | font-display: swap;
21 | src: local('Ubuntu Regular'), local('Ubuntu-Regular'), url(ubuntu/4iCs6KVjbNBYlgoKfw7z.ttf) format('truetype');
22 | }
23 | @font-face {
24 | font-family: 'Ubuntu';
25 | font-style: normal;
26 | font-weight: 700;
27 | font-display: swap;
28 | src: local('Ubuntu Bold'), local('Ubuntu-Bold'), url(ubuntu/4iCv6KVjbNBYlgoCxCvjsGyI.ttf) format('truetype');
29 | }
30 |
--------------------------------------------------------------------------------
/static/ext/ubuntu/4iCp6KVjbNBYlgoKejZPslyPN4Q.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Coldcard/ckbunker/852675568a7c8a5ba27b80f648376ac51d612049/static/ext/ubuntu/4iCp6KVjbNBYlgoKejZPslyPN4Q.ttf
--------------------------------------------------------------------------------
/static/ext/ubuntu/4iCs6KVjbNBYlgoKfw7z.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Coldcard/ckbunker/852675568a7c8a5ba27b80f648376ac51d612049/static/ext/ubuntu/4iCs6KVjbNBYlgoKfw7z.ttf
--------------------------------------------------------------------------------
/static/ext/ubuntu/4iCu6KVjbNBYlgoKej70l0w.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Coldcard/ckbunker/852675568a7c8a5ba27b80f648376ac51d612049/static/ext/ubuntu/4iCu6KVjbNBYlgoKej70l0w.ttf
--------------------------------------------------------------------------------
/static/ext/ubuntu/4iCv6KVjbNBYlgoCxCvjsGyI.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Coldcard/ckbunker/852675568a7c8a5ba27b80f648376ac51d612049/static/ext/ubuntu/4iCv6KVjbNBYlgoCxCvjsGyI.ttf
--------------------------------------------------------------------------------
/static/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Coldcard/ckbunker/852675568a7c8a5ba27b80f648376ac51d612049/static/favicon.png
--------------------------------------------------------------------------------
/static/fonts/proximanova-semibold.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Coldcard/ckbunker/852675568a7c8a5ba27b80f648376ac51d612049/static/fonts/proximanova-semibold.ttf
--------------------------------------------------------------------------------
/static/fonts/ransom-note.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Coldcard/ckbunker/852675568a7c8a5ba27b80f648376ac51d612049/static/fonts/ransom-note.ttf
--------------------------------------------------------------------------------
/static/html/login.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | Login
4 |
58 |
59 |
8 |
9 | {% call message_box('Tor Daemon Missing', '!STATUS.tord_good && !force_local_mode',
10 | icon='warning sign') %}
11 | The Bunker cannot communicate with tord (Tor daemon) which we need. It should
12 | be available on ports 9051 and 9151 of localhost.
13 | {% endcall %}
14 |
15 |
This feature needs a USB connection to the Coldcard. Please
65 | check the USB cable is connected, and verify the Coldcard
66 | is unlocked with the master PIN code.
27 |
28 |
29 | {% block uncontained_body %}
30 | {% block pre_main_body %}{% endblock %}
31 |
32 |
33 | {{ flash_msg_holder() }}
34 | {{ ws_fail_message() }}
35 |
36 | {% block main_body %}{% endblock %}
37 |
38 | {% endblock %}
39 |
40 | {% block footer %}
41 |
56 | {% endblock footer %}
57 |
58 |
59 | {% endblock body %}
60 |
--------------------------------------------------------------------------------
/templates/setup/hsm-rule-component.html:
--------------------------------------------------------------------------------
1 | {# implement a "vue component" for individual HSM rules
2 |
3 | limitation:
4 | - dropdown's do not track changes to the data, but the other direction (user makes choice)
5 | does get into the vue data model
6 | - other controls are bi-directional
7 | #}
8 |
9 |
12 |
13 |
50 |
51 |
--------------------------------------------------------------------------------
/templates/setup/hsm-rule.html:
--------------------------------------------------------------------------------
1 | {% from "macros.html" import amount, textarea, choice, bool_choice %}
2 |
3 | {% macro mu_radio(label, desc) %}
4 | {{ choice('rule.min_users', label, desc, extras=[('', "(n/a)"), (1, 'Any One'), (2, 'Two Users'), (3, 'Three Users'), ('all', 'All Users')], disabled_if="rule.users.length == 0") }}
5 | {% endmacro %}
6 |
7 |
8 |
9 |
10 | {{amount('rule.max_amount', "Max Amount",
11 | "Max amount allowed per single transaction. Leave blank for no limit.")}}
12 | {{amount('rule.per_period', "Per-Period Amount",
13 | "Max amount in one period. Leave blank for no period limit.",
14 | disabled_if="no_period")}}
15 |
16 |
17 |
18 | {{textarea('rule.whitelist', "Destination Whitelist",
19 | "Can only send to these specific payment addresses.",
20 | "(base58 or segwit addresses)")}}
21 | {{ choice('rule.wallet', "Multisig Wallet Name",
22 | "Rule only applies to a specific multisig wallet, or only single-signer (not multisig), or all wallets.",
23 | "wallet_list",
24 | extras = [('', '(any wallet)'), ('1', '(single signer wallet)')],
25 | disabled_if="wallet_list.length==0") }}
26 |
27 |
28 |
29 | {{ choice('rule.users', "Authorizing Users",
30 | "These users must authenticate to allow the transaction, or blank for no user auth.",
31 | "user_list", multi=1, default_text="(no user auth)")}}
32 | {{ mu_radio("Minimum Users Needed", "How many of those users are required?") }}
33 |
34 |
35 | {{ bool_choice('rule.local_conf', "Unique 6-digit code (per transaction) must be entered on Coldcard keypad.") }}
36 |
37 |
37 | Further changes to the policy are not possible, because
38 | the Coldcard is already in HSM mode.
39 |
40 | To exit HSM mode, the Coldcard must be power-cycled.
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 | Existing Policy
50 |
51 |
52 |
53 | Your Coldcard already holds a policy file from a previous setup. Click here
54 | to enable that policy and start the Coldcard in HSM mode. On-device
55 | confirmation will be required.
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 | {% endraw %}
64 |
65 |
66 |
67 | {% include 'setup/rules.html' with context %}
68 | {% include 'setup/users.html' with context %}
69 | {% include 'setup/paths.html' with context %}
70 | {% include 'setup/misc.html' with context %}
71 | {# include 'setup/onion.html' with context #}
72 |
73 |
97 | These policy files (JSON) contain sensitive information, including private key
98 | for onion server and boot-to-HSM unlock code, when enabled.
99 |
7 | {{ bool_choice('POLICY.must_log', "Fail transactions if we cannot log to MicroSD") }}
8 | {{ bool_choice('POLICY.never_log', "Do not log anything, even if MicroSD is inserted") }}
9 |
10 |
11 | {{HR()}}
12 |
13 |
Warnings
14 |
15 | {{ bool_choice('POLICY.warnings_ok', "Permit signing of transactions (PSBT) which have warnings (default: does not).") }}
16 |
17 | {{HR()}}
18 |
19 |
Privacy Vs. Easy of Use
20 | {{ bool_choice('POLICY.priv_over_ux', "If you prefer privacy over convenience, this causes Coldcard to be more secretive and makes the Bunker harder to use, because both will store less data about policy, usernames, and derivation paths.") }}
21 |
22 | {{HR()}}
23 |
Boot To HSM
24 |
25 |
26 | {{ text_field('POLICY.boot_to_hsm',
27 | "Coldcard will reboot directly to HSM mode, but will accept this 6-digit code to escape, if provided immediately.",
28 | placeholder="(optional)", extras='pattern="[0-9]{6}" required minlength=6 maxlength=6 ',
29 | rhs_label="6-digit code", disabled_if="POLICY.ewaste_enable") }}
30 | {{ bool_choice('POLICY.ewaste_enable', "Do not accept any code. Always boot to HSM mode.") }}
31 | {# bool_choice('POLICY.ewaste_enable', "Do not accept any code. Always stay in HSM mode and there is no way out. CAUTION: Even master PIN holder cannot change HSM policy nor escape HSM mode! Firmware upgrades are not possible.") #}
32 |
33 |
34 |
35 |
36 |
37 |
38 | Warning
39 |
40 |
41 | This setting is irreversible.
42 | No changes to firmware, HSM policy, Coldcard settings will be possible—ever again.
43 |
44 | Not even the master PIN holder can change HSM policy nor escape HSM mode! Firmware upgrades are not possible.
45 |
46 |
47 |
48 |
49 |
50 |
51 | {{HR()}}
52 |
Notes
53 |
54 | {{ textarea('POLICY.notes', "Free-form text shown on Coldcard when approving HSM Policy.",
55 | placeholder="(optional)", rows=3) }}
56 |
57 | {% endcall %}
58 |
59 |
--------------------------------------------------------------------------------
/templates/setup/onion.html:
--------------------------------------------------------------------------------
1 | {% from "macros.html" import amount, text_field, bool_choice, wrap_field %}
2 |
3 | {% call accord('Onion Address', 'Enable Tor access to the Bunker') %}
4 |
10 | You may limit text message signing to specific derivation paths. Similarly, the Coldcard will
11 | not share calculated addresses or derived XPUB values, unless whitelisted here.
12 |
13 | Leave blank to block corresponding feature. Multiple values maybe be given,
14 | separated by spaces or commas.
15 |
16 |
17 |
18 | {{ textarea('POLICY.msg_paths', "Will only sign messages using indicated path(s)",
19 | placeholder="(signing not be allowed)") }}
20 |
21 |
22 |
23 |
24 |
25 |
Special Values
26 |
27 |
any
Allow any path.
28 |
m/1/2/3/*
Require that path but the last position is any value.
29 |
m/1/2/3/*'
Last position must be hardened.
30 |
p2sh
Allow P2SH (script) addresses for multisig wallets.
31 |
45 | Time period starts as soon as rule with a velocity limit is applied.
46 | The total amount sent is tracked, for each rule independently, until the period ends.
47 | All rules share the same time period for these velocity calculations.
48 |
49 |
50 | {{ subhead("Rules") }}
51 |
52 |
53 | Multiple spending rules can be defined. The first matching rule is applied
54 | when a PSBT is considered, and its velocity limit is the one affected, so place
55 | more restrictive rules first. All elements of the specific rule must be met
56 | before it is applied.
57 |
58 |
59 |
60 |
61 |
62 |
63 |
NOTE: You have no rules defined. It will not be possible to authorize transactions.
64 |
9 | Add up to 30 user names here. These users can authorize
10 | spending based on the specific rules where they are named. Verification of the user is
11 | done via 2FA or passwords.
12 |
22 | The Coldcard is not in HSM mode.
23 | Typically, you should enable a spending policy before using this page,
24 | but it is possible to upload and sign PSBT files as it is now. You will need to
25 | approve each transaction on the Coldcard's screen.
26 |
One-Time Code or Password
185 | {% call info_hover_long('top center') %}
186 | Depending on your spending policy rules, all or none of these users may be
187 | required to authorize spending. Leave blank those you are not using.
188 | {% endcall %}
189 |
190 | {% raw %}
191 |
192 |
Local Code
193 |
194 | {{STATUS.local_code}}
195 | TBD — need PSBT first
196 | {% endraw %}
197 | {% call info_hover_long('top center') %}
198 | When required, the local Coldcard operator should enter this 6-digit code,
199 | before you press the button on this page.
200 | {% endcall %}
201 | {% raw %}
202 |