Hoernchen has uploaded this change for review. ( 
https://gerrit.osmocom.org/c/pysim/+/43136?usp=email )


Change subject: GP: mixed PSK TLS PUT KEY (Amendment B Table 3-13)
......................................................................

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 https://gerrit.osmocom.org/c/pysim/+/43136?usp=email
To unsubscribe, or for help writing mail filters, visit 
https://gerrit.osmocom.org/settings?usp=email

Gerrit-MessageType: newchange
Gerrit-Project: pysim
Gerrit-Branch: master
Gerrit-Change-Id: I480a9d049a052aa5ae54fe6e2771dba44e89434d
Gerrit-Change-Number: 43136
Gerrit-PatchSet: 1
Gerrit-Owner: Hoernchen <[email protected]>

Reply via email to