firmware/misc/obsolete-code/ds28c36b.py
Peter D. Gray 9938538f37
edits
2021-09-07 08:33:03 -04:00

446 lines
14 KiB
Python

# (c) Copyright 2021 by Coinkite Inc. This file is covered by license found in COPYING-CC.
#
# ds28c36b.py -- Talk to DS28C36B Secure Element (SE2) on Mk4
#
'''
About this chip.
- 32 pages, 16 general purpose, others are keys: all 32 bytes wide
- up to 3 keys
- SHA-256 (HMAC) auth or ECC (P256) auth via ECDH
- RNG, one useless dec counter
- pages individually lockable (nice)
- slow: 1ms to read page, 15ms to write, 50-150 for ECC ops, 3ms for HMAC
Challenges:
- reading auth'd data via SHA method is insecure against replay by emulated device
- HMAC+SHA auth methods do not allow us to inject nonce/random values, but rely on
device serial number and 8 bytes picked by the device (or the attacker/mitm)
- concerned it can sign/HMAC user-provided value in a way that might be used to
fake other responses, but I'm probably missing something
Plan:
- in factory, random privkey/pubkey generated on-device, locked.
- bootrom stores pubkey
- use ECDH to generate S value when we need to get auth data in/out
'''
from utime import sleep_ms
from utils import B2A
import ngu, ckcc
# i2c address (7-bits)
SE2_ADDR = const(0x1b)
# page numbers (Table 1)
PGN_PUBKEY_A = const(16) # also +1
PGN_PUBKEY_B = const(18) # also +1
PGN_PUBKEY_C = const(20) # also +1
PGN_PRIVKEY_A = const(22)
PGN_PRIVKEY_B = const(23)
PGN_PRIVKEY_C = const(24)
PGN_SECRET_A = const(25)
PGN_SECRET_B = const(26)
PGN_DEC_COUNTER = const(27)
PGN_ROM_OPTIONS = const(28)
PGN_GPIO = const(29)
PGN_PUBKEY_S = const(30) # also 31, volatile
# page protection bitmask (Table 11)
PROT_RP = const(0x01)
PROT_WP = const(0x02)
PROT_EM = const(0x04)
PROT_APH = const(0x08)
PROT_EPH = const(0x10)
PROT_AUTH = const(0x20)
PROT_ECH = const(0x40)
PROT_ECW = const(0x80)
# random example
T_pubkey = b'\xf0\xa5\xba6A\xe1\xd5/\n\xed\xbc8b\xfe\xca\xfe7\xe0\xdd\xbd\xad\x1f\xfb%\xb2cL\xd9>\x13\xfd5 &8\xe0l$/\x14\x94dLT,\x92X.;\x95\xe4\x13\xc3\x03\xc7\n\xc6\x15\xa6\xd2\xfd\x8a\xbe\xba'
T_privkey = b'\x8ev\xa4\xce\xb5B\xe2\x90\x06\x925\xa9\xc3\x90\xe61s^\xa9\x10q\x17\x82\\r$\xc0\xbe\xb7\xfc\xa5>'
class SE2Handler:
def __init__(self):
from machine import I2C
self.i2c = I2C(2, freq=500000)
try:
self.read_ident()
#self.verify_page(1, 0, bytes(32), b'a'*32)
except: pass
def _write(self, cmd, data=None):
# chip will not ack bytes that are past end of command+args, etc.
# data can be one int (a byte, will be prefixed w/1) or a sequence of bytes/ints
if data is not None:
if isinstance(data, int):
b = bytes([cmd, 1, data])
else:
b = bytearray([cmd, len(data)])
b.extend(data)
else:
b = bytes([cmd])
txl = self.i2c.writeto(SE2_ADDR, b)
assert txl == len(b), 'chip nak'
# all commands need at least tRM recovery time
sleep_ms(2) # tRM*2
def _read(self, num):
# responses usually have length in first byte, then status byte, then data
# - chip provides 0xff when reading past end of response (does not nak)
# - poll until it starts responding again, might take >200ms
for retry in range(200):
try:
rx = self.i2c.readfrom(SE2_ADDR, num)
assert len(rx) == num, 'short read'
return rx
except OSError:
# expect OSError: [Errno 19] ENODEV
pass
sleep_ms(2)
raise RuntimeError('se2 timeout')
def _read1(self):
# when expecting a single-byte status byte back
rx = self._read(2)
assert rx[0] == 1
return rx[1]
def _check_result(self, rv):
if rv == 0xAA: return
raise RuntimeError("bad response: 0x%x" % rv)
def write_buffer(self, data):
# write up to 80 bytes into a RAM buffer on device
# - remainder of buffer is set to 0xff, valid length is remembered
assert 1 <= len(data) <= 80
self._write(0x87, data)
def read_buffer(self):
# length implied from previous write buffer (1..80)
self._write(0x5a)
rx = self._read(81)
assert 0 < rx[0] <= 80
assert len(rx) >= rx[0]+1
return rx[1:1+rx[0]]
def read_rng(self, num):
# Read RNG, and allow any MiTM to modulate as needed.
# - do not use for any purpose
assert 1 <= num <= 63
self._write(0xd2, num)
rx = self._read(1+num)
return rx[1:]
def load_thash(self, buf):
# Perform SHA256 and store result into THASH register of chip
assert 1<= len(buf) <= 64 # zero not supported by chip, this code only 64
tmp = bytes([0xc0]) + bytes(buf)
self._write(0x33, tmp)
self._check_result(self._read1())
def page_protection(self, page):
# return active page protection for page
assert 0 <= page < 32
self._write(0xaa, page)
return self._read1()
def set_page_protection(self, page, bitmap):
# set the protection for one page
assert 0 <= page < 32
self._write(0xc3, bytes([page, bitmap]))
self._check_result(self._read1())
def read_page(self, page):
# unauth version, mitm vulnerable, no encryption
assert 0 <= page < 32
self._write(0x69, page)
rx = self._read(34)
if rx[1] == 0x55:
raise RuntimeError('read protected')
self._check_result(rx[1])
return rx[2:]
def write_page(self, page, value):
# unauth version, mitm vulnerable
assert 0 <= page < 32
assert len(value) == 32
self._write(0x96, bytes([page])+bytes(value))
self._check_result(self._read1())
def read_enc_page(self, page_num, secret_num, secret=None):
# Use secret key, and read encrypted contents of page (XOR w/ HMAC output)
# - key for HMAC is pre-shared secret, either key A, B or secret S established by ECDH
# - EPH for keys A/B, ECH forces key S
# - IMPORTANT: not secure against simple replay, so we always verify
assert 0 <= page_num <= 32
if secret_num == 2:
secret = self.shared_secret
else:
assert 0 <= secret_num <= 1
self._write(0x4b, (secret_num << 6) | page_num)
rx = self._read(42)
self._check_result(rx[1])
# do decryption
chal = rx[2:2+8]
enc = rx[2+8:]
assert len(enc) == 32
msg = chal + self.rom_id + bytes([page_num]) + self.manid
assert len(msg) == 19
chk = ngu.hmac.hmac_sha256(secret, msg)
readback = bytes(a^b for a,b in zip(chk, enc))
# Must always verify the response because it can be replayed w/o
# knowing any secrets
# - also catches wrong decryption if key/secret wrong
ok = self.verify_page(page_num, secret_num, readback, secret)
if not ok:
raise RuntimeError("wrong key/MitM")
return readback
def write_enc_page(self, page_num, secret_num, secret, old_data, new_data):
# Authenticated write to a page.
# - only for pages with APH or EPH
# - assume EPH here, with encrypted data tx
assert 0 <= page_num <= 32
assert 0 <= secret_num <= 2
assert len(new_data) == len(old_data) == 32
PGDV = bytes([page_num | 0x80])
# This is used for encryption: hmac w/ nonce we pick
chal = ngu.random.bytes(8)
msg = chal + self.rom_id + PGDV + self.manid
assert len(msg) == 19
otp = ngu.hmac.hmac_sha256(secret, msg)
# Must know old data to authenticate change.
msg2 = self.rom_id + old_data + new_data + PGDV + self.manid
assert len(msg2) == 75
auth_chk = ngu.hmac.hmac_sha256(secret, msg2)
# write that + our nonce into buffer
self.write_buffer(auth_chk + chal)
# encrypt new data
args = bytearray(33)
args[0] = (secret_num << 6) | page_num
for i in range(32):
args[i+1] = otp[i] ^ new_data[i]
self._write(0x99, args)
self._check_result(self._read1())
def read_ident(self):
# identity details needed for auth setup
b = self.read_page(28)
self.rom_id = b[24:24+8]
self.manid = b[22:22+2]
assert self.rom_id[0] == 0x4c # for this device family
def pick_keypair(self, kn, lock=False):
# use device RNG to pick a EC keypair
assert 0 <= kn <= 2 # A,B, or C
wpe = 0x1 if lock else 0x0
self._write(0xcb, (wpe<<6) | kn)
self._check_result(self._read1())
def verify_page(self, page_num, secret_num, expected, secret=None, hmac=True):
# See if chip is holding expected value in a page.
# - if this fails, you have the secret wrong, or the data is wrong
assert 0 <= secret_num <= 2 # Secret A,B, or S (or PrivkeyA/B/C)
assert 0 <= page_num < 32
assert len(expected) == 32
assert not secret or len(secret) == 32
chal = ngu.random.bytes(32)
self.write_buffer(chal)
if hmac:
arg = (secret_num << 5) | page_num
else:
assert 0 <= secret_num <= 1 # privkey A,B only
arg = ((0x3 + secret_num) << 5) | page_num
self._write(0xa5, arg)
if hmac:
rx = self._read(2+32)
else:
rx = self._read(2+64)
self._check_result(rx[1])
msg = self.rom_id + expected + chal + bytes([page_num]) + self.manid
assert len(msg) == 75
if hmac:
# response will be HMAC-SHA256 output
chk = ngu.hmac.hmac_sha256(secret, msg)
return rx[2:] == chk
else:
# response will be signature over SHA256(msg)
# - need p256r1 code to be able to verify here
md = ngu.hash.sha256s(msg)
pn = PGN_PUBKEY_A + (2*secret_num)
pubkey = self.read_page(pn) + self.read_page(pn+1)
# R and S are swapped in the new signature
sig = rx[2+32:2+32+32] + rx[2:2+32]
args = bytearray(pubkey + md + sig)
rv = ckcc.gate(130, args, 0)
return rv == 0
def setup_auth(self, ecdh_kn=0):
# do "Authenticate ECDSA Public Key" proving we know the privkey for
# pubkey held in slot C. Set volatile state: AUTH and maybe W_PUB_KEY and S
# - must enable ECDH because we want to read using this authority
# - lengths/offsets are all messed in spec
# - only supporting READ; we will do our writes before locking page(s)
# this is remembered in SRAM, but needed in general
self.write_page(PGN_PUBKEY_S+0, T_pubkey[:32])
self.write_page(PGN_PUBKEY_S+1, T_pubkey[32:])
chal = ngu.random.bytes(32+32)
self.write_buffer(chal)
cs_offset = 32 # very confusing, might be implied by buffer length?
md = ngu.hash.sha256s(T_pubkey + chal[0:32])
# sign md with our privkey
args = bytearray(T_privkey + md + bytes(64))
rv = ckcc.gate(132, args, 0)
assert rv == 0
sig = bytes(args[-64:])
args = bytearray()
args.append( ((cs_offset-1) << 3) | (ecdh_kn << 2) | 0x2 )
args.extend(sig)
self._write(0xa8, args)
self._check_result(self._read1())
print('auth ok')
# ecdh multi
pubkey_pn = PGN_PUBKEY_A + (ecdh_kn*2)
their_pubkey = self.read_page(pubkey_pn) + self.read_page(pubkey_pn+1)
args = bytearray(their_pubkey + T_privkey + bytes(32))
rv = ckcc.gate(133, args, 0)
assert rv == 0
x = args[-32:]
# shared secret S will be SHA over X of shared ECDH point + chal[32:]
s = ngu.hash.sha256s(x + chal[32:])
self.shared_secret = s
return True
def load_s(self, s):
# take string dumped by ROM
self.shared_secret = bytes(int(s[i:i+2], 16) for i in range(0, 64, 2))
def clear_state(self):
# No command to reset the volatile state on this chip! Could
# be sensitive at times. 608 has a watchdog for this!!
self.write_page(PGN_PUBKEY_S+0, bytes(32))
self.write_page(PGN_PUBKEY_S+1, bytes(32))
chal = ngu.random.bytes(32)
self.write_buffer(chal)
# rotate the secret S ... not ideal but only way I've got to change it
# - also clears ECDH_SECRET_S flag
self._write(0x3c, bytes([ (2<<6), 0 ]))
self._read1()
def selftest_sig(self):
# SELFTEST
# make sig, check on device
md = b'm'*32
args = bytearray(T_privkey + md + bytes(64))
rv = ckcc.gate(132, args, 0)
assert rv == 0
sig = bytes(args[-64:])
# check we like our own work
args = bytearray(T_pubkey + md + sig)
rv = ckcc.gate(130, args, 0)
assert rv == 0
# try against the chip
self.write_page(PGN_PUBKEY_S+0, T_pubkey[:32])
self.write_page(PGN_PUBKEY_S+1, T_pubkey[32:])
self.write_buffer(md)
b = bytearray([0x03])
b.extend(sig)
self._write(0x59, b)
self._check_result(self._read1())
def first_time(self):
# reset and lock the ANON flag == 0, so request/responses require serial number
prot = self.page_protection(PGN_ROM_OPTIONS)
b = bytearray(self.read_page(PGN_ROM_OPTIONS))
if prot != 0:
# after first run, should be protected and in right state.
assert b[1] == 0x0
else:
b[1] = 0x00 # same as default
self.write_page(PGN_ROM_OPTIONS, b)
self.read_ident()
assert self.manid[1] & 0xc0 == 0x80, 'not B rev?'
assert self.rom_id != b'\xff\xff\xff\xff\xff\xff\xff\xff'
if prot != PROT_APH:
# set write lock, except WP isn't possible on this page?! So use APH
self.set_page_protection(PGN_ROM_OPTIONS, PROT_APH)
# pick a keypair for communications (key C, no choice)
#self.pick_keypair(kn=2)
self.write_page(PGN_SECRET_A, b'a'*32)
self.write_page(PGN_SECRET_B, b'b'*32)
if self.page_protection(PGN_PUBKEY_C) == 0:
# write a pubkey for AUTH purposes
self.write_page(PGN_PUBKEY_C, T_pubkey[:32])
self.write_page(PGN_PUBKEY_C+1, T_pubkey[32:])
self.set_page_protection(PGN_PUBKEY_C, PROT_AUTH|PROT_RP|PROT_WP)
# known values in all pages
for i in range(0, 16):
try:
SE2.write_page(i, (b'%x'%i)*32)
except: pass
# EOF