# (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