Files
pysim/pySim/global_platform/__init__.py
T
Eric Wild 26a3fc09dc GP: only send a GET STATUS tag list to cards that support it
get_status() has appended a hardcoded '5c054f9f70c5cc' to the command data
field. Of the data objects in GP CS v2.3.1 Table 11-35 only the AID
search tag 4F is mandatory, the tag list is optional and not supported
in v2.1.1, where Section 9.4.2.3 defines the data field as the search
qualifier. Cards implementing that revision can reject anything else
with 6A80 as per v2.1.1 Table 9-26.

A sysmocom SJA5 does that. Its data field must be one 4F
TLV, the value is free, but nothing may precede or follow it.
So every subset returned nothing at all...

There is no need to guess: v2.1.1/v2.3.1 Section 7.4.1.3 Card Recognition
Data is "shall be present" and contains the GP version on selected SD.
Query it once, and send the tag list to cards that announce v2.2 or later.
SJA5 reports 2.1.1, sysmoEUICC reports 2.2.

Two more problems with the old list:
- A tag list is an inclusion list, old list omits tag 84, so it
suppressed the Executable Module AIDs
- It asks for tag C5 for Executable Load Files, which "may" be answered
with an error status.

Fix this by constricting or omitting the tag list depending on reported
GP version.

Change-Id: I74cd2bd47617d616bede6453397f544cde5abcb7
2026-09-23 18:01:16 +02:00

1278 lines
68 KiB
Python

# coding=utf-8
"""Partial Support for GlobalPLatform Card Spec (currently 2.1.1)
(C) 2022-2024 by Harald Welte <laforge@osmocom.org>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
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 *
from osmocom.tlv import *
from osmocom.construct import *
from pySim.utils import ResTuple
from pySim.card_key_provider import card_key_provider_get_field
from pySim.global_platform.scp import SCP02, SCP03
from pySim.global_platform.install_param import gen_install_parameters
from pySim.filesystem import *
from pySim.profile import CardProfile
from pySim.ota import SimFileAccessAndToolkitAppSpecParams
from pySim.javacard import CapFile
from pySim.log import PySimLogger
log = PySimLogger.get(__name__)
# GPCS Table 11-48 Load Parameter Tags
class NonVolatileCodeMinMemoryReq(BER_TLV_IE, tag=0xC6):
_construct = GreedyInteger()
# GPCS Table 11-48 Load Parameter Tags
class VolatileMinMemoryReq(BER_TLV_IE, tag=0xC7):
_construct = GreedyInteger()
# GPCS Table 11-48 Load Parameter Tags
class NonVolatileDataMinMemoryReq(BER_TLV_IE, tag=0xC8):
_construct = GreedyInteger()
# GPCS Table 11-49: Install Parameter Tags
class GlobalServiceParams(BER_TLV_IE, tag=0xCB):
pass
# GPCS Table 11-49: Install Parameter Tags
class VolatileReservedMemory(BER_TLV_IE, tag=0xD7):
_construct = GreedyInteger()
# GPCS Table 11-49: Install Parameter Tags
class NonVolatileReservedMemory(BER_TLV_IE, tag=0xD8):
_construct = GreedyInteger()
# GPCS Table 11-49: Install Parameter Tags
class Ts102226SpecificParameter(BER_TLV_IE, tag=0xCA):
_construct = SimFileAccessAndToolkitAppSpecParams
# GPCS Table 11-48 Load Parameter Tags
class LoadFileDataBlockFormatId(BER_TLV_IE, tag=0xCD):
pass
# GPCS Table 11-50: Make Selectable Parameter Tags
class ImplicitSelectionParam(BER_TLV_IE, tag=0xCF):
pass
# GPCS Table 11-48 Load Parameter Tags
class LoadFileDtaBlockParameters(BER_TLV_IE, tag=0xDD):
pass
# GPCS Table 11-48 Load Parameter Tags / 11-49: Install Parameter Tags
class SystemSpecificParams(BER_TLV_IE, tag=0xEF,
nested=[NonVolatileCodeMinMemoryReq,
VolatileMinMemoryReq,
NonVolatileDataMinMemoryReq,
GlobalServiceParams,
VolatileReservedMemory,
NonVolatileReservedMemory,
Ts102226SpecificParameter,
LoadFileDataBlockFormatId,
ImplicitSelectionParam,
LoadFileDtaBlockParameters]):
pass
# GPCS Table 11-49: Install Parameter Tags
class ApplicationSpecificParams(BER_TLV_IE, tag=0xC9):
_construct = GreedyBytes
class Ts102226SpecificTemplate(BER_TLV_IE, tag=0xEA):
pass
class CrtForDigitalSignature(BER_TLV_IE, tag=0xB6):
# FIXME: nested
pass
class InstallParameters(TLV_IE_Collection, nested=[ApplicationSpecificParams,
SystemSpecificParams,
Ts102226SpecificTemplate,
CrtForDigitalSignature]):
pass
sw_table = {
'Warnings': {
'6200': 'Logical Channel already closed',
'6283': 'Card Life Cycle State is CARD_LOCKED',
'6310': 'More data available',
},
'Execution errors': {
'6400': 'No specific diagnosis',
'6581': 'Memory failure',
},
'Checking errors': {
'6700': 'Wrong length in Lc',
},
'Functions in CLA not supported': {
'6881': 'Logical channel not supported or active',
'6882': 'Secure messaging not supported',
},
'Command not allowed': {
'6982': 'Security Status not satisfied',
'6985': 'Conditions of use not satisfied',
},
'Wrong parameters': {
'6a80': 'Incorrect values in command data',
'6a81': 'Function not supported e.g. card Life Cycle State is CARD_LOCKED',
'6a82': 'Application not found',
'6a84': 'Not enough memory space',
'6a86': 'Incorrect P1 P2',
'6a88': 'Referenced data not found',
},
'GlobalPlatform': {
'6d00': 'Invalid instruction',
'6e00': 'Invalid class',
},
'Application errors': {
'9484': 'Algorithm not supported',
'9485': 'Invalid key check value',
},
}
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
aes=0x88, # v2.3.1 Section 11.1.8
hmac_sha1=0x90, # v2.3.1 Section 11.1.8
hmac_sha1_160=0x91, # v2.3.1 Section 11.1.8
rsa_public_exponent_e_cleartex=0xA0,
rsa_modulus_n_cleartext=0xA1,
rsa_modulus_n=0xA2,
rsa_private_exponent_d=0xA3,
rsa_chines_remainder_p=0xA4,
rsa_chines_remainder_q=0xA5,
rsa_chines_remainder_pq=0xA6,
rsa_chines_remainder_dpi=0xA7,
rsa_chines_remainder_dqi=0xA8,
ecc_public_key=0xB0, # v2.3.1 Section 11.1.8
ecc_private_key=0xB1, # v2.3.1 Section 11.1.8
ecc_field_parameter_p=0xB2, # v2.3.1 Section 11.1.8
ecc_field_parameter_a=0xB3, # v2.3.1 Section 11.1.8
ecc_field_parameter_b=0xB4, # v2.3.1 Section 11.1.8
ecc_field_parameter_g=0xB5, # v2.3.1 Section 11.1.8
ecc_field_parameter_n=0xB6, # v2.3.1 Section 11.1.8
ecc_field_parameter_k=0xB7, # v2.3.1 Section 11.1.8
ecc_key_parameters_reference=0xF0, # v2.3.1 Section 11.1.8
not_available=0xff)
# GlobalPlatform 2.3 Section 11.10.2.1 Table 11-86
SetStatusScope = Enum(Byte, isd=0x80, app_or_ssd=0x40, isd_and_assoc_apps=0xc0)
# GlobalPlatform 2.3 section 11.1.1
CLifeCycleState = Enum(Byte, loaded=0x01, installed=0x03, selectable=0x07, personalized=0x0f, locked=0x83)
# GlobalPlatform 2.1.1 Section 9.3.3.1
class KeyInformationData(BER_TLV_IE, tag=0xc0):
_test_de_encode = [
( 'c00401708010', {"key_identifier": 1, "key_version_number": 112, "key_types": [ {"length": 16, "type": "des"} ]} ),
( 'c00402708010', {"key_identifier": 2, "key_version_number": 112, "key_types": [ {"length": 16, "type": "des"} ]} ),
( 'c00403708010', {"key_identifier": 3, "key_version_number": 112, "key_types": [ {"length": 16, "type": "des"} ]} ),
( 'c00401018010', {"key_identifier": 1, "key_version_number": 1, "key_types": [ {"length": 16, "type": "des"} ]} ),
( 'c00402018010', {"key_identifier": 2, "key_version_number": 1, "key_types": [ {"length": 16, "type": "des"} ]} ),
( 'c00403018010', {"key_identifier": 3, "key_version_number": 1, "key_types": [ {"length": 16, "type": "des"} ]} ),
( 'c00401028010', {"key_identifier": 1, "key_version_number": 2, "key_types": [ {"length": 16, "type": "des"} ]} ),
( 'c00402028010', {"key_identifier": 2, "key_version_number": 2, "key_types": [ {"length": 16, "type": "des"} ]} ),
( 'c00403038010', {"key_identifier": 3, "key_version_number": 3, "key_types": [ {"length": 16, "type": "des"} ]} ),
( 'c00401038010', {"key_identifier": 1, "key_version_number": 3, "key_types": [ {"length": 16, "type": "des"} ]} ),
( 'c00402038010', {"key_identifier": 2, "key_version_number": 3, "key_types": [ {"length": 16, "type": "des"} ]} ),
( 'c00402038810', {"key_identifier": 2, "key_version_number": 3, "key_types": [ {"length": 16, "type": "aes"} ]} ),
]
KeyTypeLen = Struct('type'/KeyType, 'length'/Int8ub)
_construct = Struct('key_identifier'/Byte, 'key_version_number'/Byte,
'key_types'/GreedyRange(KeyTypeLen))
class KeyInformation(BER_TLV_IE, tag=0xe0, nested=[KeyInformationData]):
pass
# GP v2.3 11.1.9
KeyUsageQualifier = FlagsEnum(StripTrailerAdapter(GreedyBytes, 2),
verification_encryption=0x8000,
computation_decipherment=0x4000,
sm_response=0x2000,
sm_command=0x1000,
confidentiality=0x0800,
crypto_checksum=0x0400,
digital_signature=0x0200,
crypto_authorization=0x0100,
key_agreement=0x0080)
# GP v2.3 11.1.10
KeyAccess = Enum(Byte, sd_and_any_assoc_app=0x00, sd_only=0x01, any_assoc_app_but_not_sd=0x02,
not_available=0xff)
class KeyLoading:
# Global Platform Specification v2.3 Section 11.11.4.2.2.3 DGIs for the CC Private Key
class KeyUsageQualifier(BER_TLV_IE, tag=0x95):
_construct = KeyUsageQualifier
class KeyAccess(BER_TLV_IE, tag=0x96):
_construct = KeyAccess
class KeyType(BER_TLV_IE, tag=0x80):
_construct = KeyType
class KeyLength(BER_TLV_IE, tag=0x81):
_construct = GreedyInteger()
class KeyIdentifier(BER_TLV_IE, tag=0x82):
_construct = Int8ub
class KeyVersionNumber(BER_TLV_IE, tag=0x83):
_construct = Int8ub
class KeyParameterReferenceValue(BER_TLV_IE, tag=0x85):
_construct = Enum(Byte, secp256r1=0x00, secp384r1=0x01, secp521r1=0x02, brainpoolP256r1=0x03,
brainpoolP256t1=0x04, brainpoolP384r1=0x05, brainpoolP384t1=0x06,
brainpoolP512r1=0x07, brainpoolP512t1=0x08)
# pylint: disable=undefined-variable
class ControlReferenceTemplate(BER_TLV_IE, tag=0xb9,
nested=[KeyUsageQualifier,
KeyAccess,
KeyType,
KeyLength,
KeyIdentifier,
KeyVersionNumber,
KeyParameterReferenceValue]):
pass
# Table 11-103
class EccPublicKey(DGI_TLV_IE, tag=0x0036):
_construct = GreedyBytes
# Table 11-105
class EccPrivateKey(DGI_TLV_IE, tag=0x8137):
_construct = GreedyBytes
# Global Platform Specification v2.3 Section 11.11.4 / Table 11-91
class KeyControlReferenceTemplate(DGI_TLV_IE, tag=0x00b9, nested=[ControlReferenceTemplate]):
pass
# GlobalPlatform v2.3.1 Section H.4 / Table H-6
class ScpType(BER_TLV_IE, tag=0x80):
_construct = HexAdapter(Byte)
class ListOfSupportedOptions(BER_TLV_IE, tag=0x81):
_construct = GreedyBytes
class SupportedKeysForScp03(BER_TLV_IE, tag=0x82):
_construct = FlagsEnum(Byte, aes128=0x01, aes192=0x02, aes256=0x04)
class SupportedTlsCipherSuitesForScp81(BER_TLV_IE, tag=0x83):
_construct = GreedyRange(Int16ub)
class ScpInformation(BER_TLV_IE, tag=0xa0, nested=[ScpType, ListOfSupportedOptions, SupportedKeysForScp03,
SupportedTlsCipherSuitesForScp81]):
pass
class PrivilegesAvailableSSD(BER_TLV_IE, tag=0x81):
pass
class PrivilegesAvailableApplication(BER_TLV_IE, tag=0x82):
pass
class SupportedLFDBHAlgorithms(BER_TLV_IE, tag=0x83):
pass
# GlobalPlatform Card Specification v2.3 / Table H-8
class CiphersForLFDBEncryption(BER_TLV_IE, tag=0x84):
_construct = Enum(Byte, tripledes16=0x01, aes128=0x02, aes192=0x04, aes256=0x08,
icv_supported_for_lfdb=0x80)
CipherSuitesForSignatures = FlagsEnum(StripTrailerAdapter(GreedyBytes, 2),
rsa1024_pkcsv15_sha1=0x0100,
rsa_gt1024_pss_sha256=0x0200,
single_des_plus_final_triple_des_mac_16b=0x0400,
cmac_aes128=0x0800, cmac_aes192=0x1000, cmac_aes256=0x2000,
ecdsa_ecc256_sha256=0x4000, ecdsa_ecc384_sha384=0x8000,
ecdsa_ecc512_sha512=0x0001, ecdsa_ecc_521_sha512=0x0002)
class CiphersForTokens(BER_TLV_IE, tag=0x85):
_construct = CipherSuitesForSignatures
class CiphersForReceipts(BER_TLV_IE, tag=0x86):
_construct = CipherSuitesForSignatures
class CiphersForDAPs(BER_TLV_IE, tag=0x87):
_construct = CipherSuitesForSignatures
class KeyParameterReferenceList(BER_TLV_IE, tag=0x88, nested=[KeyLoading.KeyParameterReferenceValue]):
pass
class CardCapabilityInformation(BER_TLV_IE, tag=0x67, nested=[ScpInformation, PrivilegesAvailableSSD,
PrivilegesAvailableApplication,
SupportedLFDBHAlgorithms,
CiphersForLFDBEncryption, CiphersForTokens,
CiphersForReceipts, CiphersForDAPs,
KeyParameterReferenceList]):
pass
class CurrentSecurityLevel(BER_TLV_IE, tag=0xd3):
_construct = Int8ub
# GlobalPlatform v2.3.1 Section 11.3.3.1.3
class ApplicationAID(BER_TLV_IE, tag=0x4f):
_construct = GreedyBytes
class ApplicationTemplate(BER_TLV_IE, tag=0x61, nested=[ApplicationAID]):
pass
class ListOfApplications(BER_TLV_IE, tag=0x2f00, nested=[ApplicationTemplate]):
pass
# GlobalPlatform v2.3.1 Section 11.3.3.1.2 + TS 102 226
class NumberOFInstalledApp(BER_TLV_IE, tag=0x81):
_construct = GreedyInteger()
class FreeNonVolatileMemory(BER_TLV_IE, tag=0x82):
_construct = GreedyInteger()
class FreeVolatileMemory(BER_TLV_IE, tag=0x83):
_construct = GreedyInteger()
class ExtendedCardResourcesInfo(BER_TLV_IE, tag=0xff21, nested=[NumberOFInstalledApp, FreeNonVolatileMemory,
FreeVolatileMemory]):
pass
# GlobalPlatform v2.3.1 Section 7.4.2.4 + GP SPDM
class SecurityDomainManagerURL(BER_TLV_IE, tag=0x5f50):
pass
# card data sample, returned in response to GET DATA (80ca006600):
# 66 31
# 73 2f
# 06 07
# 2a864886fc6b01
# 60 0c
# 06 0a
# 2a864886fc6b02020101
# 63 09
# 06 07
# 2a864886fc6b03
# 64 0b
# 06 09
# 2a864886fc6b040215
# GlobalPlatform 2.1.1 Table F-1
class ObjectIdentifier(BER_TLV_IE, tag=0x06):
_construct = GreedyBytes
class CardManagementTypeAndVersion(BER_TLV_IE, tag=0x60, nested=[ObjectIdentifier]):
pass
class CardIdentificationScheme(BER_TLV_IE, tag=0x63, nested=[ObjectIdentifier]):
pass
class SecureChannelProtocolOfISD(BER_TLV_IE, tag=0x64, nested=[ObjectIdentifier]):
pass
class CardConfigurationDetails(BER_TLV_IE, tag=0x65):
_construct = GreedyBytes
class CardChipDetails(BER_TLV_IE, tag=0x66):
_construct = GreedyBytes
class CardRecognitionData(BER_TLV_IE, tag=0x73, nested=[ObjectIdentifier,
CardManagementTypeAndVersion,
CardIdentificationScheme,
SecureChannelProtocolOfISD,
CardConfigurationDetails,
CardChipDetails]):
pass
class CardData(BER_TLV_IE, tag=0x66, nested=[CardRecognitionData]):
pass
# GlobalPlatform 2.1.1 Table F-2
class SecureChannelProtocolOfSelectedSD(BER_TLV_IE, tag=0x64, nested=[ObjectIdentifier]):
pass
class SecurityDomainMgmtData(BER_TLV_IE, tag=0x73, nested=[CardManagementTypeAndVersion,
CardIdentificationScheme,
SecureChannelProtocolOfSelectedSD,
CardConfigurationDetails,
CardChipDetails]):
pass
# GlobalPlatform 2.1.1 Section 9.1.1
IsdLifeCycleState = Enum(Byte, op_ready=0x01, initialized=0x07, secured=0x0f,
card_locked = 0x7f, terminated=0xff)
# GlobalPlatform 2.1.1 Section 9.9.3.1
class ApplicationID(BER_TLV_IE, tag=0x84):
_construct = GreedyBytes
# GlobalPlatform 2.1.1 Section 9.9.3.1
class SecurityDomainManagementData(BER_TLV_IE, tag=0x73):
_construct = GreedyBytes
# GlobalPlatform 2.1.1 Section 9.9.3.1
class ApplicationProductionLifeCycleData(BER_TLV_IE, tag=0x9f6e):
_construct = GreedyBytes
# GlobalPlatform 2.1.1 Section 9.9.3.1
class MaximumLengthOfDataFieldInCommandMessage(BER_TLV_IE, tag=0x9f65):
_construct = GreedyInteger()
# GlobalPlatform 2.1.1 Section 9.9.3.1
class ProprietaryData(BER_TLV_IE, tag=0xA5, nested=[SecurityDomainManagementData,
ApplicationProductionLifeCycleData,
MaximumLengthOfDataFieldInCommandMessage]):
pass
# explicitly define this list and give it a name so pySim.euicc can reference it
FciTemplateNestedList = [ApplicationID, SecurityDomainManagementData,
ApplicationProductionLifeCycleData,
MaximumLengthOfDataFieldInCommandMessage,
ProprietaryData]
# GlobalPlatform 2.1.1 Section 9.9.3.1
class FciTemplate(BER_TLV_IE, tag=0x6f, nested=FciTemplateNestedList):
pass
class IssuerIdentificationNumber(BER_TLV_IE, tag=0x42):
_construct = GreedyBytes
class CardImageNumber(BER_TLV_IE, tag=0x45):
_construct = GreedyBytes
class SequenceCounterOfDefaultKvn(BER_TLV_IE, tag=0xc1):
_construct = GreedyInteger()
class ConfirmationCounter(BER_TLV_IE, tag=0xc2):
_construct = GreedyInteger()
# Collection of all the data objects we can get from GET DATA
class DataCollection(TLV_IE_Collection, nested=[IssuerIdentificationNumber,
CardImageNumber,
CardData,
KeyInformation,
SequenceCounterOfDefaultKvn,
ConfirmationCounter,
# v2.3.1
CardCapabilityInformation,
CurrentSecurityLevel,
ListOfApplications,
ExtendedCardResourcesInfo,
SecurityDomainManagerURL]):
pass
def decode_select_response(resp_hex: str) -> object:
t = FciTemplate()
t.from_tlv(h2b(resp_hex))
d = t.to_dict()
return flatten_dict_lists(d['fci_template'])
# 11.4.2.1
StatusSubset = Enum(Byte, isd=0x80, applications=0x40, files=0x20, files_and_modules=0x10)
# Section 11.4.3.1 Table 11-36
class LifeCycleState(BER_TLV_IE, tag=0x9f70):
_construct = CLifeCycleState
# Section 11.4.3.1 Table 11-36 + Section 11.1.2
class Privileges(BER_TLV_IE, tag=0xc5):
_construct = FlagsEnum(StripTrailerAdapter(GreedyBytes, 3, steps = [1, 3]),
security_domain=0x800000, dap_verification=0x400000,
delegated_management=0x200000, card_lock=0x100000, card_terminate=0x080000,
card_reset=0x040000, cvm_management=0x020000,
mandated_dap_verification=0x010000,
trusted_path=0x8000, authorized_management=0x4000,
token_management=0x2000, global_delete=0x1000, global_lock=0x0800,
global_registry=0x0400, final_application=0x0200, global_service=0x0100,
receipt_generation=0x80, ciphered_load_file_data_block=0x40,
contactless_activation=0x20, contactless_self_activation=0x10)
# Section 11.4.3.1 Table 11-36 + Section 11.1.7
class ImplicitSelectionParameter(BER_TLV_IE, tag=0xcf):
_construct = BitStruct('contactless_io'/Flag,
'contact_io'/Flag,
'_rfu'/Flag,
'logical_channel_number'/BitsInteger(5))
# Section 11.4.3.1 Table 11-36
class ExecutableLoadFileAID(BER_TLV_IE, tag=0xc4):
_construct = GreedyBytes
# Section 11.4.3.1 Table 11-36
class ExecutableLoadFileVersionNumber(BER_TLV_IE, tag=0xce):
# Note: the Executable Load File Version Number format and contents are beyond the scope of this
# specification. It shall consist of the version information contained in the original Load File: on a
# Java Card based card, this version number represents the major and minor version attributes of the
# original Load File Data Block.
_construct = GreedyBytes
# Section 11.4.3.1 Table 11-36
class ExecutableModuleAID(BER_TLV_IE, tag=0x84):
_construct = GreedyBytes
# Section 11.4.3.1 Table 11-36
class AssociatedSecurityDomainAID(BER_TLV_IE, tag=0xcc):
_construct = GreedyBytes
# Section 11.4.3.1 Table 11-36
class GpRegistryRelatedData(BER_TLV_IE, tag=0xe3, nested=[ApplicationAID, LifeCycleState, Privileges,
ImplicitSelectionParameter, ExecutableLoadFileAID,
ExecutableLoadFileVersionNumber,
ExecutableModuleAID, AssociatedSecurityDomainAID]):
pass
# GP CS v2.3.1 Table 11-36/11-37 possible data objects requested/returned from GET STATUS for each registry entry.
# Applications and Executable Load Files have _different_ sets, so a tag list requesting them has
# to match the subset because 11.4.2.3 warns that asking for a data object an entry does not have
# "may" be answered with an error status.
GetStatusTagListIEs = {
# Table 11-36 GP Application Data
'isd': [ApplicationAID, LifeCycleState, Privileges, ImplicitSelectionParameter,
ExecutableLoadFileAID, AssociatedSecurityDomainAID],
'applications': [ApplicationAID, LifeCycleState, Privileges, ImplicitSelectionParameter,
ExecutableLoadFileAID, AssociatedSecurityDomainAID],
# Table 11-37 GP Executable Load File Data. 84 only for the subset that asks for the modules (Note 2)!
'files': [ApplicationAID, LifeCycleState, ExecutableLoadFileVersionNumber,
AssociatedSecurityDomainAID],
'files_and_modules': [ApplicationAID, LifeCycleState, ExecutableLoadFileVersionNumber,
ExecutableModuleAID, AssociatedSecurityDomainAID],
}
def get_status_tag_list(subset: str) -> bytes:
"""Encode the GET STATUS tag list for the given status subset"""
tags = b''.join([bertlv_encode_tag(ie.tag) for ie in GetStatusTagListIEs[subset]])
return b'\x5c' + bertlv_encode_len(len(tags)) + tags
# GP CS v2.3.1 Appendix H.2 / Table H-1
# oid prefix {iso(1) member-body(2) country-USA(840) globalPlatform(114283)} + card management type 2
# afterwards GP version.
OID_GP_CARD_MGMT_TYPE = h2b('2a864886fc6b02')
def _find_tlv_value(decoded, key: str):
"""depth first search for the nested decoded TLV_IE dict/list"""
if isinstance(decoded, dict):
for k, v in decoded.items():
if k == key:
return v
found = _find_tlv_value(v, key)
if found is not None:
return found
elif isinstance(decoded, list):
for item in decoded:
found = _find_tlv_value(item, key)
if found is not None:
return found
return None
def decode_gp_version(card_data: bytes) -> Optional[Tuple[int, ...]]:
"""GP version from Card Data returned by GET DATA, like (2, 1, 1) or (2, 2).
None if cm type OID is absent/unknown"""
cd = CardData()
cd.from_tlv(card_data)
ctv = _find_tlv_value(cd.to_dict(), 'card_management_type_and_version')
oid = _find_tlv_value(ctv, 'object_identifier') if ctv is not None else None
if oid is None:
return None
oid = h2b(oid) if isinstance(oid, str) else bytes(oid)
if not oid.startswith(OID_GP_CARD_MGMT_TYPE):
return None
return tuple(oid[len(OID_GP_CARD_MGMT_TYPE):])
# Application Dedicated File of a Security Domain
class ADF_SD(CardADF):
StoreData = BitStruct('last_block'/Flag,
'encryption'/Enum(BitsInteger(2), none=0, application_dependent=1, rfu=2, encrypted=3),
'structure'/Enum(BitsInteger(2), none=0, dgi=1, ber_tlv=2, rfu=3),
'_pad'/Padding(2),
'response'/Enum(Bit, not_expected=0, may_be_returned=1))
def __init__(self, aid: str, name: str, desc: str):
super().__init__(aid=aid, fid=None, sfid=None, name=name, desc=desc)
self.shell_commands += [self.AddlShellCommands()]
def decode_select_response(self, data_hex: str) -> object:
return decode_select_response(data_hex)
@with_default_category('Application-Specific Commands')
class AddlShellCommands(CommandSet):
get_data_parser = argparse.ArgumentParser()
get_data_parser.add_argument('data_object_name', type=str,
help='Name of the data object to be retrieved from the card')
@cmd2.with_argparser(get_data_parser)
def do_get_data(self, opts):
"""Perform the GlobalPlatform GET DATA command in order to obtain some card-specific data."""
tlv_cls_name = opts.data_object_name
try:
tlv_cls = DataCollection().members_by_name[tlv_cls_name]
except KeyError:
do_names = [camel_to_snake(str(x.__name__)) for x in DataCollection.possible_nested]
self._cmd.poutput('Unknown data object "%s", available options: %s' % (tlv_cls_name,
do_names))
return
(data, _sw) = self._cmd.lchan.scc.get_data(cla=0x80, tag=tlv_cls.tag)
ie = tlv_cls()
ie.from_tlv(h2b(data))
self._cmd.poutput_json(ie.to_dict())
def complete_get_data(self, text, line, begidx, endidx) -> List[str]:
data_dict = {camel_to_snake(str(x.__name__)): x for x in DataCollection.possible_nested}
index_dict = {1: data_dict}
return self._cmd.index_based_complete(text, line, begidx, endidx, index_dict=index_dict)
store_data_parser = argparse.ArgumentParser()
store_data_parser.add_argument('--data-structure', type=str, choices=['none','dgi','ber_tlv','rfu'], default='none')
store_data_parser.add_argument('--encryption', type=str, choices=['none','application_dependent', 'rfu', 'encrypted'], default='none')
store_data_parser.add_argument('--response', type=str, choices=['not_expected','may_be_returned'], default='not_expected')
store_data_parser.add_argument('DATA', type=is_hexstr)
@cmd2.with_argparser(store_data_parser)
def do_store_data(self, opts):
"""Perform the GlobalPlatform STORE DATA command in order to store some card-specific data.
See GlobalPlatform CardSpecification v2.3 Section 11.11 for details."""
response_permitted = opts.response == 'may_be_returned'
self.store_data(h2b(opts.DATA), opts.data_structure, opts.encryption, response_permitted)
def store_data(self, data: bytes, structure:str = 'none', encryption:str = 'none', response_permitted: bool = False) -> bytes:
"""Perform the GlobalPlatform STORE DATA command in order to store some card-specific data.
See GlobalPlatform CardSpecification v2.3 Section 11.11 for details."""
max_cmd_len = self._cmd.lchan.scc.max_cmd_len
# Table 11-89 of GP Card Specification v2.3
remainder = data
block_nr = 0
response = ''
while len(remainder):
chunk = remainder[:max_cmd_len]
remainder = remainder[max_cmd_len:]
p1b = build_construct(ADF_SD.StoreData,
{'last_block': len(remainder) == 0, 'encryption': encryption,
'structure': structure, 'response': response_permitted})
hdr = "80E2%02x%02x%02x" % (p1b[0], block_nr, len(chunk))
data, _sw = self._cmd.lchan.scc.send_apdu_checksw(hdr + b2h(chunk) + "00")
block_nr += 1
response += data
return h2b(response)
put_key_parser = argparse.ArgumentParser()
put_key_parser.add_argument('--old-key-version-nr', type=auto_uint8, default=0, help='Old Key Version Number')
put_key_parser.add_argument('--key-version-nr', type=auto_uint8, required=True, help='Key Version Number')
put_key_parser.add_argument('--key-id', type=auto_uint7, required=True, help='Key Identifier (base)')
put_key_parser.add_argument('--key-type', choices=list(KeyType.ksymapping.values()), action='append', required=True, help='Key Type')
put_key_parser.add_argument('--key-data', type=is_hexstr, action='append', required=True, help='Key Data Block')
put_key_parser.add_argument('--key-check', type=is_hexstr, action='append', help='Key Check Value')
put_key_parser.add_argument('--suppress-key-check', action='store_true', help='Suppress generation of Key Check Values')
@cmd2.with_argparser(put_key_parser)
def do_put_key(self, opts):
"""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.
The KCV (Key Check Values) can either be explicitly specified using `--key-check`, or will
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
--key-type aes --key-data 101112131415161718191a1b1c1d1e1f
--key-type aes --key-data 202122232425262728292a2b2c2d2e2f
Example (SCP81 TLS-PSK/KEK):
put_key --key-version-nr 0x40 --key-id 0x01 --key-type tls_psk --key-data 303132333435363738393a3b3c3d3e3f
--key-type des --key-data 404142434445464748494a4b4c4d4e4f
"""
if len(opts.key_type) != len(opts.key_data):
raise ValueError('There must be an equal number of key-type and key-data arguments')
kdb = []
for i in range(0, len(opts.key_type)):
if opts.key_check and len(opts.key_check) > i:
kcv = h2b(opts.key_check[i])
elif opts.suppress_key_check:
kcv = b''
else:
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). 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))
@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 = self.build_put_key_data(kvn, keys, self._cmd.lchan.scc.scp)
# Lc of Table 11-64 is a single byte, while LOAD or STORE DATA splits we can't:
# 11.8.2.3.3 splits a key at component boundaries -> not helping here
max_cmd_len = self._cmd.lchan.scc.max_cmd_len
if len(key_data) > max_cmd_len:
raise ValueError('key data field of %u bytes exceeds the maximum command length of %u '
'(limited by the overhead of the current secure channel); use fewer '
'keys per command, a single key component that large needs STORE DATA' %
(len(key_data), max_cmd_len))
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
get_status_parser = argparse.ArgumentParser()
get_status_parser.add_argument('subset', choices=list(StatusSubset.ksymapping.values()),
help='Subset of statuses to be included in the response')
get_status_parser.add_argument('--aid', type=is_hexstr, default='',
help='AID Search Qualifier (search only for given AID)')
@cmd2.with_argparser(get_status_parser)
def do_get_status(self, opts):
"""Perform GlobalPlatform GET STATUS command in order to retrieve status information
on Issuer Security Domain, Executable Load File, Executable Module or Applications."""
grd_list = self.get_status(opts.subset, opts.aid)
for grd in grd_list:
self._cmd.poutput_json(grd.to_dict())
def gp_version(self) -> Optional[Tuple[int, ...]]:
"""GP version the selected SD reports in its Card Recognition
Data, e.g. (2, 1, 1). Card Recognition Data "shall be present" v2.1.1/v2.3.1 section 7.4.1.3,
so this must succeed no matter the GP version. None if card did not answer GET DATA / OID unknown.
Cached, it cannot change during a session."""
if not hasattr(self, '_gp_version'):
self._gp_version = None
try:
data, _sw = self._cmd.lchan.scc.get_data(cla=0x80, tag=CardData.tag)
self._gp_version = decode_gp_version(h2b(data))
except (SwMatchError, ValueError) as e:
log.warning("Could not determine GlobalPlatform version: %s", e)
return self._gp_version
def get_status(self, subset:str, aid_search_qualifier:Hexstr = '') -> List[GpRegistryRelatedData]:
aid = ApplicationAID(decoded=aid_search_qualifier)
# GPC CardSpec v2.3.1 Table 11-35 says only the AID search tag is mandatory, tag list is
# Optional and not present in the older v2.1.1, where section 9.4.2.3 defines the data
# field as the search qualifier.
# Cards like the sja5 implementing that old GP version reject anything else with 6A80
# from v2.1.1 Table 9-26 so only send a tag list to a card that announces v2.2 or later.
#
# Not sending one is not a problem on older cards, the tag list only gives us data beyond
# what 11.4.3.1 gives us anyway, for example the associated SD AID which matters on an eUICC
# where entries belong to different SD.
version = self.gp_version()
log.debug("Card Recognition Data reports GlobalPlatform %s",
'.'.join(str(v) for v in version) if version else 'unknown')
if version is not None and version >= (2, 2):
try:
return self._get_status(subset, aid.to_tlv() + get_status_tag_list(subset))
except SwMatchError as e:
# Retry if v2.2 or later but rejected the tag list anyway.
# 6A80 and 6A88 are the error conditions GET STATUS defines in table 11-39.
# Retrying beats not ending up with a list again...
if e.sw_actual not in ('6a80', '6a88'):
raise
log.warning("Card reports GlobalPlatform %s but answered %s to the GET STATUS tag list; "
"retrying with the default search",
'.'.join(str(v) for v in version), e.sw_actual)
return self._get_status(subset, aid.to_tlv(), empty_on_6a88=True)
def _get_status(self, subset:str, cmd_data:bytes,
empty_on_6a88: bool = False) -> List[GpRegistryRelatedData]:
subset_hex = b2h(build_construct(StatusSubset, subset))
p2 = 0x02 # GPC v2.3.1 11.4.2.2 table 11-34, b2: response data structure per table 11-36
grd_list = []
while True:
hdr = "80F2%s%02x%02x" % (subset_hex, p2, len(cmd_data))
data, sw = self._cmd.lchan.scc.send_apdu(hdr + b2h(cmd_data) + "00")
if sw == '6a88':
# Table 11-39 "Referenced data not found". After collecting all pages this can
# only mean "nothing more matches" -> listing is complete. On the first page
# it is ambiguous, empty result or bad command data field, so leave that to get_status()
# which knows if a tag list was sent.
if grd_list or empty_on_6a88:
return grd_list
raise SwMatchError(sw, ['9000', '6310'])
if sw not in ['9000', '6310']:
# Never return a silently truncated registry
raise SwMatchError(sw, ['9000', '6310'])
remainder = h2b(data)
while len(remainder):
# tlv sequence, each element is one GpRegistryRelatedData()
grd = GpRegistryRelatedData()
_dec, remainder = grd.from_tlv(remainder)
grd_list.append(grd)
if sw == '9000':
return grd_list
# 6310 = more data available, table 11-38: reissue as get next occurrence(s), b1 of
# table 11-34. Keeps b2 unchanged.
p2 |= 0x01
set_status_parser = argparse.ArgumentParser()
set_status_parser.add_argument('scope', choices=list(SetStatusScope.ksymapping.values()),
help='Defines the scope of the requested status change')
set_status_parser.add_argument('status', choices=list(CLifeCycleState.ksymapping.values()),
help='Specify the new intended status')
set_status_parser.add_argument('--aid', type=is_hexstr,
help='AID of the target Application or Security Domain')
@cmd2.with_argparser(set_status_parser)
def do_set_status(self, opts):
"""Perform GlobalPlatform SET STATUS command in order to change the life cycle state of the
Issuer Security Domain, Supplementary Security Domain or Application. This normally requires
prior authentication with a Secure Channel Protocol."""
self.set_status(opts.scope, opts.status, opts.aid)
def set_status(self, scope:str, status:str, aid:Hexstr = ''):
SetStatus = Struct(Const(0x80, Byte), Const(0xF0, Byte),
'scope'/SetStatusScope, 'status'/CLifeCycleState,
'aid'/Prefixed(Int8ub, COptional(GreedyBytes)))
apdu = build_construct(SetStatus, {'scope':scope, 'status':status, 'aid':aid})
_data, _sw = self._cmd.lchan.scc.send_apdu_checksw(b2h(apdu))
inst_perso_parser = argparse.ArgumentParser()
inst_perso_parser.add_argument('application_aid', type=is_hexstr, help='Application AID')
@cmd2.with_argparser(inst_perso_parser)
def do_install_for_personalization(self, opts):
"""Perform GlobalPlatform INSTALL [for personalization] command in order to inform a Security
Domain that the following STORE DATA commands are meant for a specific AID (specified here)."""
# Section 11.5.2.3.6 / Table 11-47
self.install(0x20, 0x00, "0000%02x%s000000" % (len(opts.application_aid)//2, opts.application_aid))
inst_inst_parser = argparse.ArgumentParser()
inst_inst_parser.add_argument('--load-file-aid', type=is_hexstr, default='',
help='Executable Load File AID')
inst_inst_parser.add_argument('--module-aid', type=is_hexstr, default='',
help='Executable Module AID')
inst_inst_parser.add_argument('--application-aid', type=is_hexstr, required=True,
help='Application AID')
inst_inst_parser.add_argument('--install-parameters', type=is_hexstr, default='',
help='Install Parameters (GPC_SPE_034, section 11.5.2.3.7, table 11-49)')
inst_inst_parser.add_argument('--privilege', action='append', dest='privileges', default=[],
choices=list(Privileges._construct.flags.keys()),
help='Privilege granted to newly installed Application')
inst_inst_parser.add_argument('--install-token', type=is_hexstr, default='',
help='Install Token (Section GPCS C.4.2/C.4.7)')
inst_inst_parser.add_argument('--make-selectable', action='store_true',
help='Install and make selectable')
@cmd2.with_argparser(inst_inst_parser)
def do_install_for_install(self, opts):
"""Perform GlobalPlatform INSTALL [for install] command in order to install an application."""
InstallForInstallCD = Struct('load_file_aid'/Prefixed(Int8ub, GreedyBytes),
'module_aid'/Prefixed(Int8ub, GreedyBytes),
'application_aid'/Prefixed(Int8ub, GreedyBytes),
'privileges'/Prefixed(Int8ub, Privileges._construct),
'install_parameters'/Prefixed(Int8ub, GreedyBytes),
'install_token'/Prefixed(Int8ub, GreedyBytes))
p1 = 0x04
if opts.make_selectable:
p1 |= 0x08
decoded = vars(opts)
# convert from list to "true-dict" as required by construct.FlagsEnum
decoded['privileges'] = {x: True for x in decoded['privileges']}
ifi_bytes = build_construct(InstallForInstallCD, decoded)
self.install(p1, 0x00, b2h(ifi_bytes))
inst_load_parser = argparse.ArgumentParser()
inst_load_parser.add_argument('--load-file-aid', type=is_hexstr, required=True,
help='AID of the loaded file')
inst_load_parser.add_argument('--security-domain-aid', type=is_hexstr, default='',
help='AID of the Security Domain into which the file shalle be added')
inst_load_parser.add_argument('--load-file-hash', type=is_hexstr, default='',
help='Load File Data Block Hash (GPC_SPE_034, section C.2)')
inst_load_parser.add_argument('--load-parameters', type=is_hexstr, default='',
help='Load Parameters (GPC_SPE_034, section 11.5.2.3.7, table 11-49)')
inst_load_parser.add_argument('--load-token', type=is_hexstr, default='',
help='Load Token (GPC_SPE_034, section C.4.1)')
@cmd2.with_argparser(inst_load_parser)
def do_install_for_load(self, opts):
"""Perform GlobalPlatform INSTALL [for load] command in order to prepare to load an application."""
if opts.load_token != '' and opts.load_file_hash == '':
raise ValueError('Load File Data Block Hash is mandatory if a Load Token is present')
InstallForLoadCD = Struct('load_file_aid'/Prefixed(Int8ub, GreedyBytes),
'security_domain_aid'/Prefixed(Int8ub, GreedyBytes),
'load_file_hash'/Prefixed(Int8ub, GreedyBytes),
'load_parameters'/Prefixed(Int8ub, GreedyBytes),
'load_token'/Prefixed(Int8ub, GreedyBytes))
ifl_bytes = build_construct(InstallForLoadCD, vars(opts))
self.install(0x02, 0x00, b2h(ifl_bytes))
def install(self, p1:int, p2:int, data:Hexstr) -> ResTuple:
cmd_hex = "80E6%02x%02x%02x%s00" % (p1, p2, len(data)//2, data)
return self._cmd.lchan.scc.send_apdu_checksw(cmd_hex)
del_cc_parser = argparse.ArgumentParser()
del_cc_parser.add_argument('aid', type=is_hexstr,
help='Executable Load File or Application AID')
del_cc_parser.add_argument('--delete-related-objects', action='store_true',
help='Delete not only the object but also its related objects')
@cmd2.with_argparser(del_cc_parser)
def do_delete_card_content(self, opts):
"""Perform a GlobalPlatform DELETE [card content] command in order to delete an Executable Load
File, an Application or an Executable Load File and its related Applications."""
p2 = 0x80 if opts.delete_related_objects else 0x00
aid = ApplicationAID(decoded=opts.aid)
self.delete(0x00, p2, b2h(aid.to_tlv()))
del_key_parser = argparse.ArgumentParser()
del_key_parser.add_argument('--key-id', type=auto_uint7, help='Key Identifier (KID)')
del_key_parser.add_argument('--key-ver', type=auto_uint8, help='Key Version Number (KVN)')
del_key_parser.add_argument('--delete-related-objects', action='store_true',
help='Delete not only the object but also its related objects')
@cmd2.with_argparser(del_key_parser)
def do_delete_key(self, opts):
"""Perform GlobalPlaform DELETE (Key) command.
If both KID and KVN are specified, exactly one key is deleted. If only either of the two is
specified, multiple matching keys may be deleted."""
if opts.key_id is None and opts.key_ver is None:
raise ValueError('At least one of KID or KVN must be specified')
p2 = 0x80 if opts.delete_related_objects else 0x00
cmd = ""
if opts.key_id is not None:
cmd += "d001%02x" % opts.key_id
if opts.key_ver is not None:
cmd += "d201%02x" % opts.key_ver
self.delete(0x00, p2, cmd)
def delete(self, p1:int, p2:int, data:Hexstr) -> ResTuple:
cmd_hex = "80E4%02x%02x%02x%s00" % (p1, p2, len(data)//2, data)
return self._cmd.lchan.scc.send_apdu_checksw(cmd_hex)
load_parser = argparse.ArgumentParser()
load_parser_from_grp = load_parser.add_mutually_exclusive_group(required=True)
load_parser_from_grp.add_argument('--from-hex', type=is_hexstr, help='load from hex string')
load_parser_from_grp.add_argument('--from-file', type=argparse.FileType('rb', 0), help='load from binary file')
load_parser_from_grp.add_argument('--from-cap-file', type=argparse.FileType('rb', 0), help='load from JAVA-card CAP file')
load_parser.add_argument('--chunk-len', type=auto_uint8, default=None,
help='Block size for the LOAD command; default: as large as the current secure channel overhead permits, at most 240')
@cmd2.with_argparser(load_parser)
def do_load(self, opts):
"""Perform a GlobalPlatform LOAD command. (We currently only support loading without DAP and
without ciphering.)"""
if opts.from_hex is not None:
self.load(h2b(opts.from_hex), opts.chunk_len)
elif opts.from_file is not None:
self.load(opts.from_file.read(), opts.chunk_len)
elif opts.from_cap_file is not None:
cap = CapFile(opts.from_cap_file)
self.load(cap.get_loadfile(), opts.chunk_len)
else:
raise ValueError('load source not specified!')
def load(self, contents:bytes, chunk_len:Optional[int] = None):
# scc.max_cmd_len knows the overhead the currently active SCP
# 240 is the old default, keep it for now.
max_chunk_len = self._cmd.lchan.scc.max_cmd_len
if chunk_len is None:
chunk_len = min(240, max_chunk_len)
elif not 1 <= chunk_len <= max_chunk_len:
raise ValueError('chunk_len must be in range 1..%u (limited by the overhead of the current secure channel)' %
max_chunk_len)
# build TLV according to GPC_SPE_034 section 11.6.2.3 / Table 11-58 for unencrypted case
remainder = b'\xC4' + bertlv_encode_len(len(contents)) + contents
# transfer this in various chunks to the card
total_size = len(remainder)
block_nr = 0
while len(remainder):
block = remainder[:chunk_len]
remainder = remainder[chunk_len:]
# build LOAD command APDU according to GPC_SPE_034 section 11.6.2 / Table 11-56
p1 = 0x00 if len(remainder) else 0x80
p2 = block_nr % 256
block_nr += 1
cmd_hex = "80E8%02x%02x%02x%s00" % (p1, p2, len(block), b2h(block))
_rsp_hex, _sw = self._cmd.lchan.scc.send_apdu_checksw(cmd_hex)
self._cmd.poutput("Loaded a total of %u bytes in %u blocks. Don't forget install_for_install (and make selectable) now!" % (total_size, block_nr))
install_cap_parser = argparse.ArgumentParser(usage='%(prog)s FILE [--install-parameters | --install-parameters-*]')
install_cap_parser.add_argument('cap_file', type=str, metavar='FILE',
help='JAVA-CARD CAP file to install')
# Ideally, the parser should enforce that:
# * either the `--install-parameters` is given alone,
# * or distinct `--install-parameters-*` are optionally given instead.
# We tried to achieve this using mutually exclusive groups (add_mutually_exclusive_group).
# However, group nesting was never supported, often failed to work correctly, and was unintentionally
# exposed through inheritance. It has been deprecated since version 3.11, removed in version 3.14.
# Hence, we have to implement the enforcement manually.
install_cap_parser_inst_prm_grp = install_cap_parser.add_argument_group('Install Parameters')
install_cap_parser_inst_prm_grp.add_argument('--install-parameters', type=is_hexstr, default=None,
help='install Parameters (GPC_SPE_034, section 11.5.2.3.7, table 11-49)')
install_cap_parser_inst_prm_grp.add_argument('--install-parameters-volatile-memory-quota',
type=int, default=None,
help='volatile memory quota (GPC_SPE_034, section 11.5.2.3.7, table 11-49)')
install_cap_parser_inst_prm_grp.add_argument('--install-parameters-non-volatile-memory-quota',
type=int, default=None,
help='non volatile memory quota (GPC_SPE_034, section 11.5.2.3.7, table 11-49)')
install_cap_parser_inst_prm_grp.add_argument('--install-parameters-stk',
type=is_hexstr, default=None,
help='Load Parameters (ETSI TS 102 226, section 8.2.1.3.2.1)')
install_cap_parser.add_argument('--chunk-len', type=auto_uint8, default=None,
help='Block size for the LOAD command; default: as large as the current secure channel overhead permits, at most 240')
@cmd2.with_argparser(install_cap_parser)
def do_install_cap(self, opts):
"""Perform a .cap file installation using GlobalPlatform LOAD and INSTALL commands."""
self._cmd.poutput("loading cap file: %s ..." % opts.cap_file)
cap = CapFile(opts.cap_file)
security_domain_aid = self._cmd.lchan.selected_file.aid
load_file = cap.get_loadfile()
load_file_aid = cap.get_loadfile_aid()
module_aid = cap.get_applet_aid()
application_aid = module_aid
if opts.install_parameters is not None:
# `--install-parameters` and `--install-parameters-*` are mutually exclusive
# make sure that none of `--install-parameters-*` is given; abort otherwise
if any(p is not None for p in [opts.install_parameters_non_volatile_memory_quota,
opts.install_parameters_volatile_memory_quota,
opts.install_parameters_stk]):
self.install_cap_parser.error('arguments --install-parameters-* are '
'not allowed with --install-parameters')
install_parameters = opts.install_parameters;
else:
# `--install-parameters-*` are all optional
install_parameters = gen_install_parameters(opts.install_parameters_non_volatile_memory_quota,
opts.install_parameters_volatile_memory_quota,
opts.install_parameters_stk)
self._cmd.poutput("parameters:")
self._cmd.poutput(" security-domain-aid: %s" % security_domain_aid)
self._cmd.poutput(" load-file: %u bytes" % len(load_file))
self._cmd.poutput(" load-file-aid: %s" % load_file_aid)
self._cmd.poutput(" module-aid: %s" % module_aid)
self._cmd.poutput(" application-aid: %s" % application_aid)
self._cmd.poutput(" install-parameters: %s" % install_parameters)
self._cmd.poutput("step #1: install for load...")
self.do_install_for_load("--load-file-aid %s --security-domain-aid %s" % (load_file_aid, security_domain_aid))
self._cmd.poutput("step #2: load...")
self.load(load_file, opts.chunk_len)
self._cmd.poutput("step #3: install_for_install (and make selectable)...")
self.do_install_for_install("--load-file-aid %s --module-aid %s --application-aid %s --install-parameters %s --make-selectable" %
(load_file_aid, module_aid, application_aid, install_parameters))
self._cmd.poutput("done.")
est_scp02_parser = argparse.ArgumentParser()
est_scp02_parser.add_argument('--key-ver', type=auto_uint8, default=0, help='Key Version Number (KVN)')
est_scp02_parser.add_argument('--host-challenge', type=is_hexstr,
help='Hard-code the host challenge; default: random')
est_scp02_parser.add_argument('--security-level', type=auto_uint8, default=0x01,
help='Security Level. Default: 0x01 (C-MAC only)')
est_scp02_p_k = est_scp02_parser.add_argument_group('Manual key specification')
est_scp02_p_k.add_argument('--key-enc', type=is_hexstr, help='Secure Channel Encryption Key')
est_scp02_p_k.add_argument('--key-mac', type=is_hexstr, help='Secure Channel MAC Key')
est_scp02_p_k.add_argument('--key-dek', type=is_hexstr, help='Data Encryption Key')
est_scp02_p_csv = est_scp02_parser.add_argument_group('Obtain keys from CardKeyProvider (e.g. CSV')
est_scp02_p_csv.add_argument('--key-provider-suffix', help='Suffix for key names in CardKeyProvider')
@cmd2.with_argparser(est_scp02_parser)
def do_establish_scp02(self, opts):
"""Establish a secure channel using the GlobalPlatform SCP02 protocol. It can be released
again by using `release_scp`."""
if opts.key_provider_suffix:
suffix = opts.key_provider_suffix
id_field_name = self._cmd.lchan.selected_adf.scp_key_identity
identity = self._cmd.rs.identity.get(id_field_name)
opts.key_enc = card_key_provider_get_field('SCP02_ENC_' + suffix, key=id_field_name, value=identity)
opts.key_mac = card_key_provider_get_field('SCP02_MAC_' + suffix, key=id_field_name, value=identity)
opts.key_dek = card_key_provider_get_field('SCP02_DEK_' + suffix, key=id_field_name, value=identity)
else:
if not opts.key_enc or not opts.key_mac:
self._cmd.poutput("Cannot establish SCP02 without at least ENC and MAC keys given!")
return
if self._cmd.lchan.scc.scp:
self._cmd.poutput("Cannot establish SCP02 as this lchan already has a SCP instance!")
return
host_challenge = h2b(opts.host_challenge) if opts.host_challenge else get_random_bytes(8)
kset = GpCardKeyset(opts.key_ver, h2b(opts.key_enc), h2b(opts.key_mac), h2b(opts.key_dek))
scp02 = SCP02(card_keys=kset)
self._establish_scp(scp02, host_challenge, opts.security_level)
est_scp03_parser = deepcopy(est_scp02_parser)
est_scp03_parser.description = None
est_scp03_parser.add_argument('--s16-mode', action='store_true', help='S16 mode (S8 is default)')
@cmd2.with_argparser(est_scp03_parser)
def do_establish_scp03(self, opts):
"""Establish a secure channel using the GlobalPlatform SCP03 protocol. It can be released
again by using `release_scp`."""
if opts.key_provider_suffix:
suffix = opts.key_provider_suffix
id_field_name = self._cmd.lchan.selected_adf.scp_key_identity
identity = self._cmd.rs.identity.get(id_field_name)
opts.key_enc = card_key_provider_get_field('SCP03_ENC_' + suffix, key=id_field_name, value=identity)
opts.key_mac = card_key_provider_get_field('SCP03_MAC_' + suffix, key=id_field_name, value=identity)
opts.key_dek = card_key_provider_get_field('SCP03_DEK_' + suffix, key=id_field_name, value=identity)
else:
if not opts.key_enc or not opts.key_mac:
self._cmd.poutput("Cannot establish SCP03 without at least ENC and MAC keys given!")
return
if self._cmd.lchan.scc.scp:
self._cmd.poutput("Cannot establish SCP03 as this lchan already has a SCP instance!")
return
s_mode = 16 if opts.s16_mode else 8
host_challenge = h2b(opts.host_challenge) if opts.host_challenge else get_random_bytes(s_mode)
kset = GpCardKeyset(opts.key_ver, h2b(opts.key_enc), h2b(opts.key_mac), h2b(opts.key_dek))
scp03 = SCP03(card_keys=kset, s_mode = s_mode)
self._establish_scp(scp03, host_challenge, opts.security_level)
def _establish_scp(self, scp, host_challenge, security_level):
# perform the common functionality shared by SCP02 and SCP03 establishment
init_update_apdu = scp.gen_init_update_apdu(host_challenge=host_challenge)
init_update_resp, _sw = self._cmd.lchan.scc.send_apdu_checksw(b2h(init_update_apdu))
scp.parse_init_update_resp(h2b(init_update_resp))
ext_auth_apdu = scp.gen_ext_auth_apdu(security_level)
_ext_auth_resp, _sw = self._cmd.lchan.scc.send_apdu_checksw(b2h(ext_auth_apdu))
self._cmd.poutput("Successfully established a %s secure channel" % str(scp))
# store a reference to the SCP instance
self._cmd.lchan.scc.scp = scp
self._cmd.update_prompt()
def do_release_scp(self, _opts):
"""Release a previously establiehed secure channel."""
if not self._cmd.lchan.scc.scp:
self._cmd.poutput("Cannot release SCP as none is established")
return
self._cmd.lchan.scc.scp = None
self._cmd.update_prompt()
# Card Application of a Security Domain
class CardApplicationSD(CardApplication):
__intermediate = True
def __init__(self, aid: str, name: str, desc: str):
super().__init__(name, adf=ADF_SD(aid, name, desc), sw=sw_table)
# the identity (e.g. 'ICCID', 'EID') that should be used as a look-up key to attempt to retrieve
# the key material for the security domain from the CardKeyProvider
self.adf.scp_key_identity = None
# Card Application of Issuer Security Domain
class CardApplicationISD(CardApplicationSD):
# FIXME: ISD AID is not static, but could be different. One can select the empty
# application using '00a4040000' and then parse the response FCI to get the ISD AID
def __init__(self, aid='a000000003000000'):
super().__init__(aid=aid, name='ADF.ISD', desc='Issuer Security Domain')
self.adf.scp_key_identity = 'ICCID'
class GpCardKeyset:
"""A single set of GlobalPlatform card keys and the associated KVN."""
def __init__(self, kvn: int, enc: bytes, mac: bytes, dek: bytes):
# The Key Version Number is an 8 bit integer number, where 0 refers to the first available key,
# see also: GPC_SPE_034, section E.5.1.3
assert 0 <= kvn < 256
assert len(enc) == len(mac) == len(dek)
self.kvn = kvn
self.enc = enc
self.mac = mac
self.dek = dek
@classmethod
def from_single_key(cls, kvn: int, base_key: bytes) -> 'GpCardKeyset':
return cls(kvn, base_key, base_key, base_key)
def __str__(self):
return "%s(KVN=%u, ENC=%s, MAC=%s, DEK=%s)" % (self.__class__.__name__,
self.kvn, b2h(self.enc), b2h(self.mac), b2h(self.dek))
def compute_kcv_des(key:bytes) -> bytes:
# GP Card Spec B.6: For a DES key, the key check value is computed by encrypting 8 bytes, each with
# value '00', with the key to be checked and retaining the 3 highest-order bytes of the encrypted
# result.
plaintext = b'\x00' * 8
cipher = DES3.new(key, DES.MODE_ECB)
return cipher.encrypt(plaintext)
def compute_kcv_aes(key:bytes) -> bytes:
# GP Card Spec B.6: For a AES key, the key check value is computed by encrypting 16 bytes, each with
# value '01', with the key to be checked and retaining the 3 highest-order bytes of the encrypted
# result.
plaintext = b'\x01' * 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]:
"""Compute the KCV (Key Check Value) for given key type and key."""
kcv_calculator = KCV_CALCULATOR.get(key_type)
if not kcv_calculator:
return None
else:
return kcv_calculator(key)[:3]