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

AES PSK + DES DEK for scp81, tested with sysmoEUICC1 C2T

Change-Id: I480a9d049a052aa5ae54fe6e2771dba44e89434d
This commit is contained in:
Eric Wild
2026-09-07 19:51:23 +02:00
parent c582b5fee3
commit 078ac2bf19
2 changed files with 237 additions and 19 deletions
+85 -19
View File
@@ -18,10 +18,12 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
import io
import hashlib
from copy import deepcopy
from typing import Optional, List, Dict, Tuple
from construct import Optional as COptional
from construct import Struct, GreedyRange, FlagsEnum, Int16ub, Int24ub, Padding, Bit, Const
from construct import Construct, stream_read, stream_write
from Cryptodome.Random import get_random_bytes
from Cryptodome.Cipher import DES, DES3, AES
from osmocom.utils import *
@@ -148,6 +150,24 @@ sw_table = {
},
}
class PutKeyLength(Construct):
"""A length field of a PUT KEY data field, GP CardSpec v2.3.1 11.8.2.3.1
- all lengths ASN.1 BER-TLV (ITU-T X.690 Section 8.1.3)
- except that the length 128 may also be coded on one byte as '80' for backwards compatibility
80 does not introduce the indefinite form here which is unused in GP as far as i know.
That legacy form is accepted when parsing, but never generated, which agrees with the spec"""
def _parse(self, stream, context, path):
first = stream_read(stream, 1, path)[0]
if first <= 0x80:
return first
return int.from_bytes(stream_read(stream, first & 0x7f, path), 'big')
def _build(self, obj, stream, context, path):
data = bertlv_encode_len(obj)
stream_write(stream, data, len(data), path)
return obj
# GlobalPlatform 2.1.1 Section 9.1.6
KeyType = Enum(Byte, des=0x80,
tls_psk=0x85, # v2.3.1 Section 11.1.8
@@ -602,8 +622,8 @@ class ADF_SD(CardADF):
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,33 +640,73 @@ class ADF_SD(CardADF):
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
self.put_key(opts.old_key_version_nr, opts.key_version_nr, p2, kdb)
# Table 11-68: Key Data Field - Format 1 (Basic Format)
KeyDataBasic = GreedyRange(Struct('key_type'/KeyType,
'kcb'/Prefixed(Int8ub, GreedyBytes),
'kcv'/Prefixed(Int8ub, GreedyBytes)))
# Table 11-68: Key Data Field - Format 1 (Basic Format). The key component block length is
# BER-TLV coded (Section 11.8.2.3.1), the key check value length is always '00' - '7F'.
KeyDataBasic = Struct('key_type'/KeyType,
'kcb'/Prefixed(PutKeyLength(), 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):
tag || L1 || <maybe L2> KCB || <1-byte length> KCV"""
return cls.KeyDataBasic.build({'key_type': key_type, 'kcb': kcb, 'kcv': kcv})
@classmethod
def encode_key_data_psk(cls, 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>
- framing is like Basic Format, but the kcb is always GP CardSpec Table 11-70
so always with the length of the clear text key value, even without padding!
- 'ciphered_key' is DEK(block-padded clear key), no additional length prefix."""
kcb = bertlv_encode_len(len(clear_key)) + ciphered_key
return cls.encode_key_data_basic('tls_psk', kcb, 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 k['key_type'] == 'tls_psk':
# len always part of the data see CardSpec Table 11-70 vs Table 11-71
if scp:
ciphered = scp.dek_encrypt(scp.pad_to_blocksize(clear))
else:
ciphered = clear
key_data += cls.encode_key_data_psk(clear, ciphered, k['kcv'])
else:
if scp:
ciphered = scp.encrypt_key(clear)
else:
# (for example) during personalization, DEK might not be required
ciphered = clear
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 +1125,16 @@ def compute_kcv_aes(key:bytes) -> bytes:
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]:
+152
View File
@@ -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 *
@@ -325,6 +327,156 @@ class SCP03_KCV_Test(unittest.TestCase):
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 the same vectors as SCP02_Test, so that the whole PUT KEY data field is 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_key_component_block_length_is_bertlv(self):
# GP CardSpec v2.3.1 Section 11.8.2.3.1: all lengths ofPUT KEY are always BER TLV coded
for kcb_len, exp_len_field in [(127, '7f'), (128, '8180'), (129, '8181'), (256, '820100')]:
with self.subTest(kcb_len=kcb_len):
kcb = bytes(kcb_len)
field = self.C.encode_key_data_basic('rsa_modulus_n', kcb, b'')
self.assertEqual(b2h(field), 'a2' + exp_len_field + b2h(kcb) + '00')
# 85 field of Amendment B Table 3-13 uses the same coding
# single byte inner length (clear key < 128) == block kcb_len bytes long
psk = self.C.encode_key_data_psk(bytes(120), bytes(kcb_len - 1), b'')
self.assertEqual(b2h(psk)[:2 + len(exp_len_field)], '85' + exp_len_field)
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 states the clear key length (L2) in the '85' DO itself, so the ciphered
# key field is the bare cryptogram:
# - ciphered field == padded ciphertext (no duplicated length prefix),
# - clear key == first L2 bytes.
for keylen in (18, 20):
with self.subTest(keylen=keylen):
clear = bytes(range(keylen))
padded_len = keylen + (-keylen % 8)
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, keylen) # single-byte BER length of clear key
ciphered = field[3:3 + (l1 - 1)] # value = L2 (1 byte) || ciphered key
self.assertEqual(len(ciphered), padded_len) # padded to the 8-byte DES block size
self.assertEqual(l1, 1 + padded_len) # no duplicated length prefix
self.assertEqual(self.scp02.dek_decrypt(ciphered)[:keylen], clear)
def test_psk_clear_key_is_not_padded_in_place(self):
# padding the bytearray in place would make L2 the padded length,
# then stored as key material and rejected thanks to the KCV
clear = h2b('000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d') # 30, not %8
kcv = compute_kcv('tls_psk', clear)
field = self.C.build_put_key_data(0x40, [{'key_type': 'tls_psk', 'clear_key': clear,
'kcv': kcv}], self.scp02)[1:]
self.assertEqual(len(clear), 30)
self.assertEqual(field[2], 30) # L2 == clear key length, not 32
self.assertEqual(self.scp02.dek_decrypt(field[3:3 + field[1] - 1])[:30], 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')