xous-core/tools/pddbdbg.py

206 lines
8.8 KiB
Python
Raw Normal View History

2021-11-14 11:02:34 +00:00
#! /usr/bin/env python3
import argparse
2021-12-27 05:07:01 +00:00
import logging
2021-11-14 11:02:34 +00:00
from pddbcommon import *
2021-11-14 11:02:34 +00:00
def main():
global DO_CI_TESTS
2021-11-14 11:02:34 +00:00
parser = argparse.ArgumentParser(description="Debug PDDB Images")
parser.add_argument(
2021-11-22 06:00:43 +00:00
"--name", required=False, help="pddb disk image root name", type=str, nargs='?', metavar=('name'), const='./pddb'
2021-11-14 11:02:34 +00:00
)
2021-12-27 05:07:01 +00:00
parser.add_argument(
"--loglevel", required=False, help="set logging level (INFO/DEBUG/WARNING/ERROR)", type=str, default="INFO",
2021-12-27 05:07:01 +00:00
)
parser.add_argument(
"--dump", required=False, help="Only dump the image, skip the automated CI checks", action="store_true"
)
parser.add_argument(
"--renode", required=False, help="Override flex-size settings and read a Renode bin file", action="store_true"
)
parser.add_argument(
"--smalldb", required=False, help="Override default size of PDDB and set it to 4MiB (for images built to that size only)", action="store_true"
)
parser.add_argument(
"--basis", type=str, help="Extra Bases to unlock, as `name:pass`. Each additional basis requires another --basis separator. Note that : is not legal to use in a Basis name.", action="append", nargs="+"
)
parser.add_argument(
"-p", "--pin", help="Unlock PIN", type=str, default='a'
)
parser.add_argument(
"--ci", help="Run in CI test mode. Requires all keys to have a specific checksum structure for automated checking.", action="store_true"
)
2021-11-14 11:02:34 +00:00
args = parser.parse_args()
2021-12-27 05:07:01 +00:00
numeric_level = getattr(logging, args.loglevel.upper(), None)
if args.dump:
numeric_level = getattr(logging, 'DEBUG', None)
else:
numeric_level = getattr(logging, args.loglevel.upper(), None)
2021-12-27 05:07:01 +00:00
if not isinstance(numeric_level, int):
raise ValueError('Invalid log level: %s' % args.loglevel)
logging.basicConfig(level=numeric_level)
set_ci_tests_flag(args.ci)
if args.renode is True:
keyfile = "./emulation/renode-keybox.bin"
imagefile = "./tools/pddb-images/renode.bin"
basis_credentials = []
if args.basis:
for pair in args.basis:
credpair = pair[0].split(':', 1)
if len(credpair) != 2:
logging.error("Basis credential pair with name {} has a formatting problem, aborting!".format(credpair[0]))
exit(1)
basis_credentials += [credpair]
with open(keyfile, 'rb') as key_f:
keyrom = key_f.read()
with open(imagefile, 'rb') as img_f:
raw_img = img_f.read()
if args.smalldb:
raw_img = raw_img[0x01D80000:0x1D8_0000 + 1024 * 1024 * 4]
else:
raw_img = raw_img[0x01D80000:0x07F80000]
keys = extract_keys(keyrom, raw_img, args.pin, basis_credentials)
else:
if args.name == None:
keyfile = './tools/pddb-images/pddb.key'
imagefile = './tools/pddb-images/pddb.bin'
else:
keyfile = './tools/pddb-images/{}.key'.format(args.name)
imagefile = './tools/pddb-images/{}.bin'.format(args.name)
if args.dump:
DO_CI_TESTS = False
keys = {}
with open(keyfile, 'rb') as key_f:
raw_key = key_f.read()
num_keys = int.from_bytes(raw_key[:4], 'little')
for i in range(num_keys):
name_all = raw_key[4 + i*128 : 4 + i*128 + 64]
name_bytes = bytearray(b'')
for b in name_all:
if b != 0:
name_bytes.append(b)
else:
break
name = name_bytes.decode('utf8', errors='ignore')
key_data = raw_key[4 + i*128 + 64 : 4 + i*128 + 96]
key_pt = raw_key[4 + i*128 + 96 : 4 + i*128 + 128]
keys[name] = [key_pt, key_data]
2021-11-14 11:02:34 +00:00
2022-04-19 07:57:51 +00:00
logging.info("Found basis keys (pt, data):")
2021-12-27 05:07:01 +00:00
logging.info(str(keys))
2021-11-14 11:02:34 +00:00
# tunable parameters for a filesystem
2021-11-15 11:05:03 +00:00
global MBBB_PAGES
FSCB_PAGES = 16
FSCB_LEN_PAGES = 2
2021-11-14 11:02:34 +00:00
KEY_PAGES = 1
2021-11-15 11:05:03 +00:00
global PAGE_SIZE
2021-11-17 18:01:30 +00:00
global VPAGE_SIZE
2022-11-03 10:33:22 +00:00
MAX_DICTS = 16383
2021-11-14 11:02:34 +00:00
with open(imagefile, 'rb') as img_f:
raw_img = img_f.read()
if args.renode:
if args.smalldb:
raw_img = raw_img[0x01D80000:0x1D8_0000 + 1024 * 1024 * 4]
else:
raw_img = raw_img[0x01D80000:0x07F80000]
2021-11-14 11:02:34 +00:00
pddb_len = len(raw_img)
pddb_size_pages = pddb_len // PAGE_SIZE
2022-01-06 15:15:24 +00:00
logging.info("Disk size: 0x{:x}".format(pddb_len))
2021-11-14 11:02:34 +00:00
2021-11-15 11:05:03 +00:00
mbbb_offset = pddb_size_pages * Pte.PTE_LEN + PAGE_SIZE * KEY_PAGES
2022-01-06 15:15:24 +00:00
if mbbb_offset & (PAGE_SIZE - 1) != 0:
mbbb_offset = (mbbb_offset + PAGE_SIZE) & 0xFFFF_F000 # round up to nearest page
logging.info("MBBB: 0x{:x}".format(mbbb_offset))
2021-11-14 11:02:34 +00:00
img_index = 0
2021-11-15 11:05:03 +00:00
tables = decode_pagetable(raw_img, pddb_size_pages, keys, raw_img[mbbb_offset:mbbb_offset + MBBB_PAGES * PAGE_SIZE])
2021-11-14 11:02:34 +00:00
img_index += pddb_size_pages * Pte.PTE_LEN
2022-01-06 15:15:24 +00:00
if img_index & (PAGE_SIZE - 1) != 0:
img_index = (img_index + PAGE_SIZE) & 0xFFFF_F000
2021-11-14 11:02:34 +00:00
rawkeys = raw_img[img_index : img_index + PAGE_SIZE * KEY_PAGES]
2022-01-06 15:15:24 +00:00
logging.debug("Keys: 0x{:x}".format(img_index))
2021-11-14 11:02:34 +00:00
img_index += PAGE_SIZE * KEY_PAGES
2022-01-06 15:15:24 +00:00
2021-11-14 11:02:34 +00:00
mbbb = raw_img[img_index : img_index + PAGE_SIZE * MBBB_PAGES]
2022-01-06 15:15:24 +00:00
logging.debug("MBBB check: 0x{:x}".format(img_index))
2021-11-14 11:02:34 +00:00
img_index += PAGE_SIZE * MBBB_PAGES
2022-01-06 15:15:24 +00:00
fscb = decode_fscb(raw_img[img_index: img_index + PAGE_SIZE * FSCB_PAGES], keys, FSCB_LEN_PAGES=FSCB_LEN_PAGES)
2022-01-06 15:15:24 +00:00
logging.debug("FSCB: 0x{:x}".format(img_index))
img_index += PAGE_SIZE * FSCB_PAGES
2022-01-06 15:15:24 +00:00
logging.debug("Data: 0x{:x}".format(img_index))
data = raw_img[img_index:]
for name, key in keys.items():
if name in tables:
2022-04-19 07:57:51 +00:00
logging.info("Basis {}, key_pt: {}, key_data: {}".format(name, key[0].hex(), key[1].hex()))
v2p_table = tables[name][0]
p2v_table = tables[name][1]
# v2p_table[0xfe0fe0] = 0x1200* 0x100
basis_data = bytearray()
2021-11-17 18:01:30 +00:00
pp_start = v2p_table[VPAGE_SIZE]
evict salamanders from AES-GCM-SIV in the PDDB So you'd like to think that an authenticating cipher would like, authenticate stuff on decryption. And AES-GCM-SIV was billed as sort of AES with the pointy edges taken off. Well, there's always one more bug, actually this time a salamander: https://eprint.iacr.org/2020/1456.pdf Apparently one can forge authentication if the attacker can control keys and ciphertext. In the case of the PDDB I think the attack is largely theoretical, because you'd have to forge through both the AES-GCM-SIV and the rather insufficient nonce/murmur hash check on the PTE that locates the page to be decrypted. But it's never good to have cryptographic properties depend upon something that's known to be insufficient, so let's assume the attacker can simply forge keys and stick some data on the device. This wouldn't be a problem for confidentiality but it could lead to the ability to plant evidence, but more importantly could lead to code paths that were thought to be protected against fuzzing being fuzzed as you could conceivably create a Basis root record that "checked out" and then the larger body of the PDDB code would try to chomp on other pages that are forged, leading to a bigger attack surface. Plus who knows what else might exist beyond salamanders... This patch does a "key commitment" on the Basis root page, so that it is now cryptographically hard to create a salamander. Basically instead of using the system root key directly in AES-GCM-SIV, the key goes through a KDF that expands it with yet another nonce plus a bit of header data. The KDF is SHA512/256 and it expands it twice, changing one bit in the header data (as prescribed by the paper cited above), to create a version of the key that is written to disk (the commitment), and a version of the key that is used to decrypt/authenticate the AES-GCM-SIV data. Only if both the commitment and the authentication check out, do we accept a Basis root page as valid. I had some concerns about the slightly different structure of the Basis Root page being detectable, but I have been assured that the key commitment form and the MAC in AES-GCM-SIV satisfy a property known as IND$-CPA ("indistinguishable from uniform randomness by a chosen-plaintext attacker") by Rogaway[1], and, more strongly, "Secure nonce misuse-resistant authenticated encryption (mrAE)" by Gueron and Lindell (see appendix for def)[2]. Still, just out of total paranoia and nothing else, I organize the basis root page such that the MAC is in the identical location as the other non-committed pages...just in case there is some mathematical property of the MAC that could be leveraged to leak info about the existence of the basis root record. The commitment nonce and sha512/256 result I understand better and feel pretty comfortable that they are going to be strongly random and indistinguishable from AES ciphertext. Thanks to Michael Rosenberg for taking the time to explain all this to me and to guide me through the process of making the fixes. It is a major PDDB API-breaking change and I'm glad it's able to be rolled out before most users have received a device. [1] https://web.cs.ucdavis.edu/~rogaway/papers/ad.pdf [2] https://eprint.iacr.org/2015/102
2022-02-27 19:13:26 +00:00
# print("pp_start: {:x}".format(pp_start))
2021-11-17 18:01:30 +00:00
pp_data = data[pp_start:pp_start + PAGE_SIZE]
try:
2022-04-19 07:57:51 +00:00
pt_data = keycommit_decrypt(key[1], basis_aad(name), pp_data)
2021-11-17 18:01:30 +00:00
basis_data.extend(bytearray(pt_data))
2021-12-27 05:07:01 +00:00
logging.debug("decrypted vpage @ {:x} ppage @ {:x}".format(VPAGE_SIZE, v2p_table[VPAGE_SIZE]))
# print([hex(x) for x in basis_data[:256]])
basis = Basis(basis_data)
2021-12-27 05:07:01 +00:00
logging.info(basis.as_str())
2021-11-17 18:01:30 +00:00
basis_dicts = {}
2021-11-22 18:57:13 +00:00
dicts_found = 0
dict_index = 0
while dict_index < MAX_DICTS and dicts_found < basis.num_dicts:
2022-04-19 07:57:51 +00:00
bdict = BasisDicts(dict_index, v2p_table, data, key[1], name)
2021-11-22 18:57:13 +00:00
if bdict.valid:
basis_dicts[bdict.name] = bdict
dicts_found += 1
dict_index += 1
if dicts_found != basis.num_dicts:
2021-12-27 05:07:01 +00:00
logging.error("Expected {} dictionaries, only found {}; searched {}".format(basis.num_dicts, dicts_found, dict_index))
2021-11-22 18:57:13 +00:00
found_all_dicts = False
else:
found_all_dicts = True
2021-11-17 18:01:30 +00:00
2021-12-27 05:07:01 +00:00
logging.debug(" Dictionaries: ")
2021-11-17 18:01:30 +00:00
for bdict in basis_dicts.values():
2021-12-27 05:07:01 +00:00
logging.debug(bdict.as_str())
2021-11-17 18:01:30 +00:00
#d2 = basis_dicts["dict2"]
#logging.info("listing {} keys".format(len(d2.keys)))
#for key in d2.keys:
# logging.info("{}".format(key))
if args.ci == True:
logging.info("CI checks:")
for bdict in basis_dicts.values():
bdict.ci_check()
if found_all_dicts:
logging.info("All dicts were found.") # this message is searched for in CI, don't change it
else:
logging.error("Missing dictionaries, something is wrong.")
2021-11-22 18:57:13 +00:00
else:
for bdict in basis_dicts.values():
logging.info("==================================================================")
logging.info("Dict {}".format(bdict.as_str()))
2021-11-22 10:26:19 +00:00
2021-11-17 18:01:30 +00:00
except ValueError:
2021-12-27 05:07:01 +00:00
logging.error("couldn't decrypt basis root vpage @ {:x} ppage @ {:x}".format(VPAGE_SIZE, v2p_table[VPAGE_SIZE]))
2021-11-17 18:01:30 +00:00
2021-11-14 11:02:34 +00:00
if __name__ == "__main__":
main()
exit(0)