mirror of
https://gitea.osmocom.org/sim-card/pysim.git
synced 2026-09-13 01:20:57 +03:00
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:
@@ -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]:
|
||||
|
||||
@@ -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')
|
||||
|
||||
Reference in New Issue
Block a user