Hoernchen has uploaded this change for review.
GP: mixed PSK TLS PUT KEY (Amendment B Table 3-13)
AES PSK + DES DEK for scp81, tested with sysmoEUICC1 C2T
Change-Id: I480a9d049a052aa5ae54fe6e2771dba44e89434d
---
M pySim/global_platform/__init__.py
M tests/unittests/test_globalplatform.py
2 files changed, 189 insertions(+), 15 deletions(-)
git pull ssh://gerrit.osmocom.org:29418/pysim refs/changes/36/43136/1
diff --git a/pySim/global_platform/__init__.py b/pySim/global_platform/__init__.py
index 6ff4b28..31f3907 100644
--- a/pySim/global_platform/__init__.py
+++ b/pySim/global_platform/__init__.py
@@ -18,6 +18,7 @@
"""
import io
+import hashlib
from copy import deepcopy
from typing import Optional, List, Dict, Tuple
from construct import Optional as COptional
@@ -602,8 +603,8 @@
See GlobalPlatform CardSpecification v2.3 Section 11.8 for details.
The KCV (Key Check Values) can either be explicitly specified using `--key-check`, or will
- otherwise be automatically generated for DES and AES keys. You can suppress the latter using
- `--suppress-key-check`.
+ otherwise be automatically generated for DES, AES and TLS-PSK keys. You can suppress the
+ latter using `--suppress-key-check`.
Example (SCP80 KIC/KID/KIK):
put_key --key-version-nr 1 --key-id 0x01 --key-type aes --key-data 000102030405060708090a0b0c0d0e0f
@@ -620,19 +621,12 @@
kdb = []
for i in range(0, len(opts.key_type)):
if opts.key_check and len(opts.key_check) > i:
- kcv = opts.key_check[i]
+ kcv = h2b(opts.key_check[i])
elif opts.suppress_key_check:
- kcv = ''
+ kcv = b''
else:
- kcv_bin = compute_kcv(opts.key_type[i], h2b(opts.key_data[i])) or b''
- kcv = b2h(kcv_bin)
- if self._cmd.lchan.scc.scp:
- # encrypted key data with DEK of current SCP
- kcb = b2h(self._cmd.lchan.scc.scp.encrypt_key(h2b(opts.key_data[i])))
- else:
- # (for example) during personalization, DEK might not be required)
- kcb = opts.key_data[i]
- kdb.append({'key_type': opts.key_type[i], 'kcb': kcb, 'kcv': kcv})
+ kcv = compute_kcv(opts.key_type[i], h2b(opts.key_data[i])) or b''
+ kdb.append({'key_type': opts.key_type[i], 'clear_key': h2b(opts.key_data[i]), 'kcv': kcv})
p2 = opts.key_id
if len(opts.key_type) > 1:
p2 |= 0x80
@@ -643,10 +637,58 @@
'kcb'/Prefixed(Int8ub, GreedyBytes),
'kcv'/Prefixed(Int8ub, GreedyBytes)))
- def put_key(self, old_kvn:int, kvn: int, kid: int, key_dict: dict) -> bytes:
+ @classmethod
+ def encode_key_data_basic(cls, key_type: str, kcb: bytes, kcv: bytes) -> bytes:
+ """Generic Basic key data field, GP CardSpec v2.3 Table 11-68):
+ type || <1-byte length> KCB || <1-byte length> KCV"""
+ return build_construct(cls.KeyDataBasic, [{'key_type': key_type, 'kcb': b2h(kcb), 'kcv': b2h(kcv)}])
+
+ @staticmethod
+ def encode_key_data_psk(clear_key: bytes, ciphered_key: bytes, kcv: bytes) -> bytes:
+ """Single PSK TLS '85' key data field per GP Amendment B 1.2, 3.9.1 / Table 3-13:
+ 85 | L1 | L2 | <ciphered PSK key> | <KCV length> | <KCV>
+ - L2 is the BER-encoded length of the *clear* PSK key in bytes
+ - L1 is the BER-encoded length of the (L2 || ciphered PSK key) block
+ - KCV, if present, is the 3 most significant bytes of SHA-1(clear key).
+ - 'ciphered_key' is DEK(block-padded clear key), no additional length prefix."""
+ block = bertlv_encode_len(len(clear_key)) + ciphered_key
+ return (b'\x85' + bertlv_encode_len(len(block)) + block +
+ bytes([len(kcv)]) + kcv)
+
+ @classmethod
+ def build_put_key_data(cls, kvn: int, keys: List[dict], scp) -> bytes:
+ """Assemble the PUT KEY data field, mixed PSK + DES DEK is supported:
+ - new KVN followed by one key data field per key.
+ - tls_psk keys per GP Amendment B
+ - other key types generic Basic format
+ Param 'keys' is a dict:
+ - 'key_type' (str)
+ - 'clear_key' (bytes)
+ - 'kcv' (bytes / empty).
+ 'scp' may be None (e.g. during personalization, when the DEK may not be required)."""
+ key_data = kvn.to_bytes(1, 'big')
+ for k in keys:
+ clear = k['clear_key']
+ if scp:
+ # encipher the clear key with the DEK of the current SCP
+ ciphered = scp.encrypt_key(clear)
+ else:
+ ciphered = clear
+ if k['key_type'] == 'tls_psk':
+ # Table 3-13 contains the clear key length L2, but scp.encrypt_key() already
+ # prepends that during right padding a non block aligned key (GP CardSpec Table 11-70)
+ # strip it so the 'Ciphered PSK key' field is only the raw ciphertext.
+ if scp and len(clear) % scp.sk.blocksize:
+ ciphered = ciphered[len(bertlv_encode_len(len(clear))):]
+ key_data += cls.encode_key_data_psk(clear, ciphered, k['kcv'])
+ else:
+ key_data += cls.encode_key_data_basic(k['key_type'], ciphered, k['kcv'])
+ return key_data
+
+ def put_key(self, old_kvn:int, kvn: int, kid: int, keys: List[dict]) -> bytes:
"""Perform the GlobalPlatform PUT KEY command in order to store a new key on the card.
See GlobalPlatform CardSpecification v2.3 Section 11.8 for details."""
- key_data = kvn.to_bytes(1, 'big') + build_construct(ADF_SD.AddlShellCommands.KeyDataBasic, key_dict)
+ key_data = self.build_put_key_data(kvn, keys, self._cmd.lchan.scc.scp)
hdr = "80D8%02x%02x%02x" % (old_kvn, kid, len(key_data))
data, _sw = self._cmd.lchan.scc.send_apdu_checksw(hdr + b2h(key_data) + "00")
return data
@@ -1065,10 +1107,16 @@
cipher = AES.new(key, AES.MODE_ECB)
return cipher.encrypt(plaintext)
+def compute_kcv_psk(key:bytes) -> bytes:
+ # GP Amendment B v1.2, 3.9.1 / Table 3-13
+ # KCV of a PSK TLS key is the 3 highest-order bytes of the SHA-1 digest of the clear key value.
+ return hashlib.sha1(key).digest()
+
# dict is keyed by the string name of the KeyType enum above in this file
KCV_CALCULATOR = {
'aes': compute_kcv_aes,
'des': compute_kcv_des,
+ 'tls_psk': compute_kcv_psk,
}
def compute_kcv(key_type: str, key: bytes) -> Optional[bytes]:
diff --git a/tests/unittests/test_globalplatform.py b/tests/unittests/test_globalplatform.py
index 8698470..d016d7d 100644
--- a/tests/unittests/test_globalplatform.py
+++ b/tests/unittests/test_globalplatform.py
@@ -17,7 +17,9 @@
import unittest
import logging
+import hashlib
from osmocom.utils import b2h, h2b
+from osmocom.tlv import bertlv_encode_len
from pySim.global_platform import *
from pySim.global_platform.scp import *
@@ -290,6 +292,130 @@
self.assertEqual(compute_kcv('aes', KEYSET_AES128.dek), h2b('840DE5'))
+class PutKey_PSK_Test(unittest.TestCase):
+ """Tests for the PUT KEY command data field encoding, in particular the PSK TLS ('85') key data
+ field defined by GlobalPlatform Amendment B (Remote Application Management over HTTP) Table 3-13."""
+
+ # the PUT KEY encoder we exercise
+ C = ADF_SD.AddlShellCommands
+
+ # SCP80 TLS-PSK example key from the do_put_key docstring (16 bytes)
+ PSK_CLEAR = h2b('303132333435363738393a3b3c3d3e3f')
+ # its DEK ciphertext + Table 3-13 KCV with SCP02 session set up below
+ PSK_CIPHERED = h2b('15abf1fe16ccc5aa13743394442942cd')
+ PSK_KCV = h2b('06125d') # = SHA-1(PSK_CLEAR)[:3]
+
+ def setUp(self):
+ # SCP02 with same vectors as SCP02_Test so encrypt_key() so the whole PUT KEY data field are reproducible.
+ self.scp02 = SCP02(card_keys=ck_3des_70)
+ self.scp02.gen_init_update_apdu(host_challenge=h2b('40A62C37FA6304F8'))
+ self.scp02.parse_init_update_resp(h2b('00000000000000000000700200016B4524ABEE7CF32EA3838BC148F3'))
+ self.scp02.gen_ext_auth_apdu()
+
+ def test_psk_kcv_is_sha1(self):
+ # GP Amendment B Table 3-13: KCV = 3 most significant bytes of SHA-1(clear key)
+ self.assertEqual(compute_kcv('tls_psk', self.PSK_CLEAR), hashlib.sha1(self.PSK_CLEAR).digest()[:3])
+ self.assertEqual(compute_kcv('tls_psk', self.PSK_CLEAR), self.PSK_KCV)
+
+ def test_encode_psk_framing_golden(self):
+ # assert the exact Table 3-13 layout
+ # 85 | L1 | L2 | <ciphered> | 03 | <SHA-1(clear)[:3]>
+ clear = self.PSK_CLEAR
+ ciphered = h2b('aabbccddeeff00112233445566778899') # arbitrary 16-byte ciphertext
+ kcv = hashlib.sha1(clear).digest()[:3]
+ field = self.C.encode_key_data_psk(clear, ciphered, kcv)
+ # 85 L1 L2 <---------- ciphered -----------> 03 <-kcv->
+ self.assertEqual(b2h(field),'85' '11' '10' 'aabbccddeeff00112233445566778899' '03' + b2h(kcv))
+ self.assertEqual(b2h(field),'851110aabbccddeeff0011223344556677889903' + '06125d')
+
+ def test_psk_golden_over_scp02(self):
+ # Full PUT KEY data field (KVN 0x40 + single PSK key) enciphered with the SCP02 DEK.
+ keys = [{'key_type': 'tls_psk', 'clear_key': self.PSK_CLEAR,
+ 'kcv': compute_kcv('tls_psk', self.PSK_CLEAR)}]
+ data = self.C.build_put_key_data(0x40, keys, self.scp02)
+ self.assertEqual(b2h(data),
+ '40' '85' '11' '10' + b2h(self.PSK_CIPHERED) + '03' + b2h(self.PSK_KCV))
+
+ def test_wrong_basic_format_differs(self):
+ # regression test, the generic "Basic format" does NOT match Table 3-13 for a PSK key
+ # rejected by card with with 6a88
+ wrong_basic = self.C.encode_key_data_basic('tls_psk', self.PSK_CIPHERED, b'')
+ right_psk = self.C.encode_key_data_psk(self.PSK_CLEAR, self.PSK_CIPHERED, self.PSK_KCV)
+ self.assertEqual(b2h(wrong_basic), '8510' + b2h(self.PSK_CIPHERED) + '00')
+ self.assertEqual(b2h(right_psk), '8511' '10' + b2h(self.PSK_CIPHERED) + '03' + b2h(self.PSK_KCV))
+ self.assertNotEqual(wrong_basic, right_psk)
+
+ def test_basic_format_unchanged(self):
+ # as before
+ for kt, clear in [('des', h2b('404142434445464748494a4b4c4d4e4f')),
+ ('aes', h2b('000102030405060708090a0b0c0d0e0f'))]:
+ ciph = self.scp02.encrypt_key(clear)
+ kcv = compute_kcv(kt, clear)
+ via_construct = build_construct(self.C.KeyDataBasic, [{'key_type': kt, 'kcb': b2h(ciph), 'kcv': b2h(kcv)}])
+ via_helper = self.C.encode_key_data_basic(kt, ciph, kcv)
+ self.assertEqual(via_helper, via_construct)
+
+ def test_psk_padding_no_double_length(self):
+ # A PSK key whose length is not a multiple of the DEK block size (DES: 8) is right-padded before
+ # ciphering. Table 3-13 already contains the clear key length (L2), so encrypt_key() length
+ # prefix must be stripped:
+ # - ciphered field == padded ciphertext (no doubling),
+ # - clear key == first L2 bytes.
+ clear = bytes(range(20)) # 20 bytes, 20 % 8 = 4 -> pad to 24
+ field = self.C.build_put_key_data(0x40, [{'key_type': 'tls_psk', 'clear_key': clear,
+ 'kcv': compute_kcv('tls_psk', clear)}], self.scp02)[1:]
+ self.assertEqual(field[0], 0x85)
+ l1 = field[1]
+ l2 = field[2]
+ self.assertEqual(l2, 20) # single-byte BER length of clear key
+ ciphered = field[3:3 + (l1 - 1)] # block = L2(1 byte) || ciphered
+ self.assertEqual(len(ciphered), 24) # padded to the 8-byte DES block size
+ self.assertEqual(l1, 1 + 24) # no duplicated length prefix
+ self.assertEqual(self.scp02.dek_decrypt(ciphered)[:20], clear)
+
+ def test_kcv_suppressed(self):
+ # --suppress-key-check -> KCV length 00 and no KCV bytes
+ field = self.C.build_put_key_data(0x40, [{'key_type': 'tls_psk', 'clear_key': self.PSK_CLEAR,
+ 'kcv': b''}], self.scp02)[1:]
+ self.assertEqual(b2h(field), '8511' '10' + b2h(self.PSK_CIPHERED) + '00')
+
+ def test_multikey_psk_plus_des_dek(self):
+ # load a PSK TLS key (KID 1, Amendment B format) together with its DES DEK
+ # (KID 2, Basic format) in one PUT KEY.
+ # Verify the concatenated data field parses back into the two components with proper type formats.
+ dek = h2b('404142434445464748494a4b4c4d4e4f')
+ keys = [{'key_type': 'tls_psk', 'clear_key': self.PSK_CLEAR, 'kcv': compute_kcv('tls_psk', self.PSK_CLEAR)},
+ {'key_type': 'des', 'clear_key': dek, 'kcv': compute_kcv('des', dek)}]
+ data = self.C.build_put_key_data(0x40, keys, self.scp02)
+
+ b = data
+ self.assertEqual(b[0], 0x40) # KVN
+ b = b[1:]
+ # component 1: PSK TLS (Table 3-13)
+ self.assertEqual(b[0], 0x85)
+ self.assertEqual(b[1], 0x11) # L1 = 17
+ self.assertEqual(b[2], 0x10) # L2 = 16 (clear key length)
+ self.assertEqual(b[3:3 + 16], self.PSK_CIPHERED)
+ self.assertEqual(b[3 + 16], 0x03) # KCV length
+ self.assertEqual(b[3 + 16 + 1:3 + 16 + 1 + 3], self.PSK_KCV)
+ b = b[3 + 16 + 1 + 3:]
+ # component 2: DES DEK (Basic format)
+ self.assertEqual(b[0], 0x80) # key type des
+ kcb_len = b[1]
+ self.assertEqual(kcb_len, 16)
+ self.assertEqual(b[2:2 + kcb_len], self.scp02.encrypt_key(dek))
+ b = b[2 + kcb_len:]
+ self.assertEqual(b[0], 0x03) # KCV length
+ self.assertEqual(b[1:1 + 3], compute_kcv('des', dek))
+ self.assertEqual(b[1 + 3:], b'') # no trailing bytes
+
+ def test_no_scp_leaves_key_clear(self):
+ # During personalization (no SCP) the key is not enciphered, framing still follows Table 3-13.
+ field = self.C.build_put_key_data(0x40, [{'key_type': 'tls_psk', 'clear_key': self.PSK_CLEAR,
+ 'kcv': self.PSK_KCV}], None)[1:]
+ self.assertEqual(b2h(field), '8511' '10' + b2h(self.PSK_CLEAR) + '03' + b2h(self.PSK_KCV))
+
+
class Install_param_Test(unittest.TestCase):
def test_gen_install_parameters(self):
load_parameters = gen_install_parameters(256, 256, '010001001505000000000000000000000000')
To view, visit change 43136. To unsubscribe, or for help writing mail filters, visit settings.