Files
vsmartcard/virtualsmartcard/src/vpicc/virtualsmartcard/SEutils.py
2022-09-17 21:32:56 +02:00

731 lines
28 KiB
Python

#
# Copyright (C) 2011 Dominik Oepen
#
# This file is part of virtualsmartcard.
#
# virtualsmartcard 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 3 of the License, or (at your option) any
# later version.
#
# virtualsmartcard 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
# virtualsmartcard. If not, see <http://www.gnu.org/licenses/>.
from time import time
from random import seed, randint
from virtualsmartcard.TLVutils import pack, unpack, bertlv_pack
from virtualsmartcard.ConstantDefinitions import CRT_TEMPLATE, ALGO_MAPPING
from virtualsmartcard.ConstantDefinitions import SM_Class, MAX_EXTENDED_LE
from virtualsmartcard.utils import inttostring, stringtoint, C_APDU
from virtualsmartcard.SWutils import SwError, SW
import virtualsmartcard.CryptoUtils as vsCrypto
class ControlReferenceTemplate:
"""
Control Reference Templates are used to configure the Security
Environments. They specify which algorithms to use in which mode of
operation and with which keys. There are six different types of Control
Reference Template:
HT, AT, KT, CCT, DST, CT-sym, CT-asym.
"""
def __init__(self, tag, config=""):
"""
Generates a new CRT
:param tag: Indicates the type of the CRT (HT, AT, KT, CCT, DST,
CT-sym, CT-asym)
:param config: A string containing TLV encoded Security Environment
parameters
"""
if tag not in (CRT_TEMPLATE["AT"], CRT_TEMPLATE["HT"],
CRT_TEMPLATE["KAT"], CRT_TEMPLATE["CCT"],
CRT_TEMPLATE["DST"], CRT_TEMPLATE["CT"]):
raise ValueError("Unknown control reference tag.")
else:
self.type = tag
self.iv = None
self.keyref_secret_key = None
self.keyref_public_key = None
self.keyref_private_key = None
self.key = None
self.fileref = None
self.DFref = None
self.keylength = None
self.algorithm = None
self.blocklength = None
self.usage_qualifier = None
if config != "":
self.parse_SE_config(config)
self.__config_string = config
def parse_SE_config(self, config):
"""
Parse a control reference template as given e.g. in an MSE APDU.
:param config: a TLV string containing the configuration for the CRT.
"""
error = False
structure = unpack(config)
for tlv in structure:
tag, length, value = tlv
if tag == 0x80:
self.__set_algo(value)
elif tag in (0x81, 0x82, 0x83, 0x84):
self.__set_key(tag, value)
elif tag in range(0x85, 0x93):
self.__set_iv(tag, length, value)
elif tag == 0x95:
self.usage_qualifier = value
else:
error = True
if error:
raise SwError(SW["ERR_REFNOTUSABLE"])
else:
return SW["NORMAL"], ""
def __set_algo(self, data):
"""
Set the algorithm to be used by this CRT. The algorithms are specified
in a global dictionary. New cards may add or modify this table in order
to support new or different algorithms.
:param data: reference to an algorithm
"""
if data not in ALGO_MAPPING:
raise SwError(SW["ERR_REFNOTUSABLE"])
else:
self.algorithm = ALGO_MAPPING[data]
self.__replace_tag(0x80, data)
def __set_key(self, tag, value):
if tag == 0x81:
self.fileref = value
elif tag == 0x82:
self.DFref = value
elif tag == 0x83:
self.keyref_secret_key = value
self.keyref_public_key = value
elif tag == 0x84:
self.keyref_private_key = value
def __set_iv(self, tag, length, value):
iv = None
if tag == 0x85:
iv = 0x00
elif tag == 0x86:
pass # What is initial chaining block?
elif tag == 0x87 or tag == 0x93:
if length != 0:
iv = value
else:
iv = self.iv + 1
elif tag == 0x91:
if length != 0:
iv = value
else:
seed(time())
iv = hex(randint(0, 255))
elif tag == 0x92:
if length != 0:
iv = value
else:
iv = int(time())
self.iv = iv
def __replace_tag(self, tag, data):
"""
Adjust the config string using a given tag, value combination. If the
config string already contains a tag, value pair for the given tag,
replace it. Otherwise append tag, length and value to the config
string.
"""
position = 0
while position < len(self.__config_string) and \
self.__config_string[position] != tag:
length = stringtoint(self.__config_string[position+1])
position += length + 3
if position < len(self.__config_string): # Replace Tag
length = stringtoint(self.__config_string[position+1])
self.__config_string = self.__config_string[:position] +\
inttostring(tag) + inttostring(len(data)) + data +\
self.__config_string[position+2+length:]
else: # Add new tag
self.__config_string += inttostring(tag) + inttostring(len(data)) + data
def to_string(self):
"""
Return the content of the CRT, encoded as TLV data in a string
"""
return self.__config_string
def __str__(self):
return self.__config_string
class Security_Environment(object):
def __init__(self, MF, SAM):
self.sam = SAM
self.mf = MF
self.SEID = None
# Control Reference Tables
self.at = ControlReferenceTemplate(CRT_TEMPLATE["AT"])
self.kat = ControlReferenceTemplate(CRT_TEMPLATE["KAT"])
self.ht = ControlReferenceTemplate(CRT_TEMPLATE["HT"])
self.cct = ControlReferenceTemplate(CRT_TEMPLATE["CCT"])
self.dst = ControlReferenceTemplate(CRT_TEMPLATE["DST"])
self.ct = ControlReferenceTemplate(CRT_TEMPLATE["CT"])
self.capdu_sm = False
self.rapdu_sm = False
self.internal_auth = False
self.external_auth = False
def manage_security_environment(self, p1, p2, data):
"""
This method is used to store, restore or erase Security Environments
or to manipulate the various parameters of the current SE.
P1 specifies the operation to perform, p2 is either the SEID for the
referred SE or the tag of a control reference template
:param p1:
Bitmask according to this table
== == == == == == == == =======================================
b8 b7 b6 b5 b4 b3 b2 b1 Meaning
== == == == == == == == =======================================
- - - 1 - - - - Secure messaging in command data field
- - 1 - - - - - Secure messaging in response data field
- 1 - - - - - - Computation, decipherment, internal
authentication and key agreement
1 - - - - - - - Verification, encipherment, external
authentication and key agreement
- - - - 0 0 0 1 SET
1 1 1 1 0 0 1 0 STORE
1 1 1 1 0 0 1 1 RESTORE
1 1 1 1 0 1 0 0 ERASE
== == == == == == == == =======================================
"""
cmd = p1 & 0x0F
se = p1 >> 4
if(cmd == 0x01):
# Secure messaging in command data field
if se & 0x01:
self.capdu_sm = True
# Secure messaging in response data field
if se & 0x02:
self.rapdu_sm = True
# Computation, decryption, internal authentication and key
# agreement
if se & 0x04:
self.internal_auth = True
# Verification, encryption, external authentication and key
# agreement
if se & 0x08:
self.external_auth = True
return self._set_SE(p2, data)
elif cmd == 0x02:
return self.sam.store_SE(p2)
elif cmd == 0x03:
return self.sam.restore_SE(p2)
elif cmd == 0x04:
return self.sam.erase_SE(p2)
else:
raise SwError(SW["ERR_INCORRECTP1P2"])
def _set_SE(self, p2, data):
"""
Manipulate the current Security Environment. P2 is the tag of a
control reference template, data contains control reference objects
"""
if p2 == 0xA4:
return self.at.parse_SE_config(data)
elif p2 == 0xA6:
return self.kat.parse_SE_config(data)
elif p2 == 0xAA:
return self.ht.parse_SE_config(data)
elif p2 == 0xB4:
return self.cct.parse_SE_config(data)
elif p2 == 0xB6:
return self.dst.parse_SE_config(data)
elif p2 == 0xB8:
return self.ct.parse_SE_config(data)
raise SwError(SW["ERR_INCORRECTP1P2"])
def parse_SM_CAPDU(self, CAPDU, authenticate_header):
"""
This methods parses a data field including Secure Messaging objects.
SM_header indicates whether or not the header of the message shall be
authenticated. It returns an unprotected command APDU
:param CAPDU: The protected CAPDU to be parsed
:param authenticate_header: Whether or not the header should be
included in authentication mechanisms
:returns: Unprotected command APDU
"""
structure = unpack(CAPDU.data)
return_data = ["", ]
cla = None
ins = None
p1 = None
p2 = None
le = None
if authenticate_header:
to_authenticate = inttostring(CAPDU.cla) +\
inttostring(CAPDU.ins) + inttostring(CAPDU.p1) +\
inttostring(CAPDU.p2)
to_authenticate = vsCrypto.append_padding(self.cct.blocklength,
to_authenticate)
else:
to_authenticate = b""
for tlv in structure:
tag, length, value = tlv
if tag % 2 == 1: # Include object in checksum calculation
to_authenticate += bertlv_pack([[tag, length, value]])
# SM data objects for encapsulating plain values
if tag in (SM_Class["PLAIN_VALUE_NO_TLV"],
SM_Class["PLAIN_VALUE_NO_TLV_ODD"]):
return_data.append(value) # FIXME: Need TLV coding?
# Encapsulated SM objects. Parse them
# FIXME: Need to pack value into a dummy CAPDU
elif tag in (SM_Class["PLAIN_VALUE_TLV_INCULDING_SM"],
SM_Class["PLAIN_VALUE_TLV_INCULDING_SM_ODD"]):
return_data.append(self.parse_SM_CAPDU(value,
authenticate_header))
# Encapsulated plaintext BER-TLV objects
elif tag in (SM_Class["PLAIN_VALUE_TLV_NO_SM"],
SM_Class["PLAIN_VALUE_TLV_NO_SM_ODD"]):
return_data.append(value)
elif tag in (SM_Class["Ne"], SM_Class["Ne_ODD"]):
le = value
elif tag == SM_Class["PLAIN_COMMAND_HEADER"]:
if len(value) != 8:
raise SwError(SW["ERR_SECMESSOBJECTSINCORRECT"])
else:
cla = value[:2]
ins = value[2:4]
p1 = value[4:6]
p2 = value[6:8]
# SM data objects for confidentiality
if tag in (SM_Class["CRYPTOGRAM_PLAIN_TLV_INCLUDING_SM"],
SM_Class["CRYPTOGRAM_PLAIN_TLV_INCLUDING_SM_ODD"]):
# The cryptogram includes SM objects.
# We decrypt them and parse the objects.
plain = self.decipher(tag, 0x80, value)
# TODO: Need Le = length
return_data.append(self.parse_SM_CAPDU(plain,
authenticate_header))
elif tag in (SM_Class["CRYPTOGRAM_PLAIN_TLV_NO_SM"],
SM_Class["CRYPTOGRAM_PLAIN_TLV_NO_SM_ODD"]):
# The cryptogram includes BER-TLV encoded plaintext.
# We decrypt them and return the objects.
plain = self.decipher(tag, 0x80, value)
return_data.append(plain)
elif tag in (SM_Class["CRYPTOGRAM_PADDING_INDICATOR"],
SM_Class["CRYPTOGRAM_PADDING_INDICATOR_ODD"]):
# The first byte of the data field indicates the padding to use
"""
Value Meaning
'00' No further indication
'01' Padding as specified in 6.2.3.1
'02' No padding
'1X' One to four secret keys for enciphering information,
not keys ('X' is a bitmap with any value from '0' to
'F')
'11' indicates the first key (e.g., an "even" control word
in a pay TV system)
'12' indicates the second key (e.g., an "odd" control word
in a pay TV system)
'13' indicates the first key followed by the second key
(e.g., a pair of control words in a pay TV system)
'2X' Secret key for enciphering keys, not information
('X' is a reference with any value from '0' to 'F')
(e.g., in a pay TV system, either an operational key
for enciphering control words, or a management key for
enciphering operational keys)
'3X' Private key of an asymmetric key pair ('X' is a
reference with any value from '0' to 'F')
'4X' Password ('X' is a reference with any value from '0' to
'F')
'80' to '8E' Proprietary
"""
padding_indicator = stringtoint(value[0])
plain = self.decipher(tag, 0x80, value[1:])
plain = vsCrypto.strip_padding(self.ct.blocklength, plain,
padding_indicator)
return_data.append(plain)
# SM data objects for authentication
if tag == SM_Class["CHECKSUM"]:
auth = vsCrypto.append_padding(self.cct.blocklength,
to_authenticate)
checksum = self.compute_cryptographic_checksum(0x8E, 0x80,
auth)
if checksum != value:
raise SwError(SW["ERR_SECMESSOBJECTSINCORRECT"])
elif tag == SM_Class["DIGITAL_SIGNATURE"]:
auth = to_authenticate # FIXME: Need padding?
signature = self.compute_digital_signature(0x9E, 0x9A, auth)
if signature != value:
raise SwError(SW["ERR_SECMESSOBJECTSINCORRECT"])
elif tag in (SM_Class["HASH_CODE"], SM_Class["HASH_CODE_ODD"]):
hash = self.hash(p1, p2, to_authenticate)
if hash != value:
raise SwError(SW["ERR_SECMESSOBJECTSINCORRECT"])
# Form unprotected CAPDU
if cla is None:
cla = CAPDU.cla
if ins is None:
ins = CAPDU.ins
if p1 is None:
p1 = CAPDU.p1
if p2 is None:
p2 = CAPDU.p2
# FIXME:
# if expected != b"":
# raise SwError(SW["ERR_SECMESSOBJECTSMISSING"])
if isinstance(le, bytes):
# FIXME: C_APDU only handles le with strings of length 1.
# Better patch utils.py to support extended length apdus
le_int = stringtoint(le)
if le_int == 0 and len(le) > 1:
le_int = MAX_EXTENDED_LE
le = le_int
c = C_APDU(cla=cla, ins=ins, p1=p1, p2=p2, le=le,
data=b"".join(return_data))
return c
def protect_response(self, sw, result):
"""
This method protects a response APDU using secure messaging mechanisms
:returns: the protected data and the SW bytes
"""
return_data = b""
# if sw == SW["NORMAL"]:
# sw = inttostring(sw)
# length = len(sw)
# tag = SM_Class["PLAIN_PROCESSING_STATUS"]
# tlv_sw = pack([(tag,length,sw)])
# return_data += tlv_sw
if result != b"": # Encrypt the data included in the RAPDU
encrypted = self.encipher(0x82, 0x80, result)
encrypted = b"\x01" + encrypted
encrypted_tlv = pack([(
SM_Class["CRYPTOGRAM_PADDING_INDICATOR_ODD"],
len(encrypted),
encrypted)])
return_data += encrypted_tlv
if sw == SW["NORMAL"]:
if self.cct.algorithm is None:
raise SwError(SW["CONDITIONSNOTSATISFIED"])
elif self.cct.algorithm == "CC":
tag = SM_Class["CHECKSUM"]
padded = vsCrypto.append_padding(self.cct.blocklength,
return_data)
auth = self.compute_cryptographic_checksum(0x8E, 0x80, padded)
length = len(auth)
return_data += pack([(tag, length, auth)])
elif self.cct.algorithm == "SIGNATURE":
tag = SM_Class["DIGITAL_SIGNATURE"]
hash = self.hash(0x90, 0x80, return_data)
auth = self.compute_digital_signature(0x9E, 0x9A, hash)
length = len(auth)
return_data += pack([(tag, length, auth)])
return sw, return_data
# The following commands implement ISO 7816-8 {{{
def perform_security_operation(self, p1, p2, data):
"""
In the end this command is nothing but a big switch for all the other
commands in ISO 7816-8. It will invoke the appropriate command and
return its result
"""
allowed_P1P2 = ((0x90, 0x80), (0x90, 0xA0), (0x9E, 0x9A), (0x9E, 0xAC),
(0x9E, 0xBC), (0x00, 0xA2), (0x00, 0xA8), (0x00, 0x92),
(0x00, 0xAE), (0x00, 0xBE), (0x82, 0x80), (0x84, 0x80),
(0x86, 0x80), (0x80, 0x82), (0x80, 0x84), (0x80, 0x86))
if (p1, p2) not in allowed_P1P2:
raise SwError(SW["INCORRECTP1P2"])
if((p2 in (0x80, 0xA0)) and (p1 == 0x90)):
response_data = self.hash(p1, p2, data)
elif(p2 in (0x9A, 0xAC, 0xBC) and p1 == 0x9E):
response_data = self.compute_digital_signature(p1, p2, data)
elif(p2 == 0xA2 and p1 == 0x00):
response_data = self.verify_cryptographic_checksum(p1, p2, data)
elif(p2 == 0xA8 and p1 == 0x00):
response_data = self.verify_digital_signature(p1, p2, data)
elif(p2 in (0x92, 0xAE, 0xBE) and p1 == 0x00):
response_data = self.verify_certificate(p1, p2, data)
elif (p2 == 0x80 and p1 in (0x82, 0x84, 0x86)):
response_data = self.encipher(p1, p2, data)
elif (p2 in (0x82, 0x84, 0x86) and p1 == 0x80):
response_data = self.decipher(p1, p2, data)
if p1 == 0x00:
assert response_data == b""
return SW["NORMAL"], response_data
def compute_cryptographic_checksum(self, p1, p2, data):
"""
Compute a cryptographic checksum (e.g. MAC) for the given data.
Algorithm and key are specified in the current SE
"""
if p1 != 0x8E or p2 != 0x80:
raise SwError(SW["ERR_INCORRECTP1P2"])
if self.cct.key is None:
raise SwError(SW["ERR_CONDITIONNOTSATISFIED"])
checksum = vsCrypto.crypto_checksum(self.cct.algorithm, self.cct.key,
data, self.cct.iv)
return checksum
def compute_digital_signature(self, p1, p2, data):
"""
Compute a digital signature for the given data.
Algorithm and key are specified in the current SE
:param p1: Must be 0x9E = Secure Messaging class for digital signatures
:param p2: Must be one of 0x9A, 0xAC, 0xBC. Indicates what kind of data
is included in the data field.
"""
if p1 != 0x9E or p2 not in (0x9A, 0xAC, 0xBC):
raise SwError(SW["ERR_INCORRECTP1P2"])
if self.dst.key is None:
raise SwError(SW["ERR_CONDITIONNOTSATISFIED"])
to_sign = b""
if p2 == 0x9A: # Data to be signed
to_sign = data
elif p2 == 0xAC: # Data objects, sign values
to_sign = b""
structure = unpack(data)
for tag, length, value in structure:
to_sign += value
elif p2 == 0xBC: # Data objects to be signed
pass
signature = self.dst.key.sign(to_sign, "")
return signature
def hash(self, p1, p2, data):
"""
Hash the given data using the algorithm specified by the current
Security environment.
:return: raw data (no TLV coding).
"""
if p1 != 0x90 or p2 not in (0x80, 0xA0):
raise SwError(SW["ERR_INCORRECTP1P2"])
algo = self.ht.algorithm
if algo is None:
raise SwError(SW["ERR_CONDITIONNOTSATISFIED"])
try:
hash = vsCrypto.hash(algo, data)
except ValueError:
raise SwError(SW["ERR_EXECUTION"])
return hash
def verify_cryptographic_checksum(self, p1, p2, data):
"""
Verify the cryptographic checksum contained in the data field.
Data field must contain a cryptographic checksum (tag 0x8E) and a plain
value (tag 0x80)
"""
plain = b""
cct = b""
algo = self.cct.algorithm
key = self.cct.key
iv = self.cct.iv
if algo is None or key is None:
raise SwError(SW["ERR_CONDITIONNOTSATISFIED"])
structure = unpack(data)
for tag, length, value in structure:
if tag == 0x80:
plain = value
elif tag == 0x8E:
cct = value
if plain == b"" or cct == b"":
raise SwError(SW["ERR_SECMESSOBJECTSMISSING"])
else:
my_cct = vsCrypto.crypto_checksum(algo, key, plain, iv)
if my_cct == cct:
return ""
else:
raise SwError["ERR_SECMESSOBJECTSINCORRECT"]
def verify_digital_signature(self, p1, p2, data):
"""
Verify the digital signature contained in the data field. Data must
contain a data to sign (tag 0x9A, 0xAC or 0xBC) and a digital signature
(0x9E)
"""
key = self.dst.key
to_sign = b""
signature = b""
if key is None:
raise SwError(SW["ERR_CONDITIONNOTSATISFIED"])
structure = unpack(data)
for tag, length, value in structure:
if tag == 0x9E:
signature = value
elif tag == 0x9A: # FIXME: Correct treatment of all possible tags
to_sign = value
elif tag == 0xAC:
pass
elif tag == 0xBC:
pass
if to_sign == b"" or signature == b"":
raise SwError(SW["ERR_SECMESSOBJECTSMISSING"])
my_signature = key.sign(value)
if my_signature == signature:
return ""
else:
raise SwError(["ERR_SECMESSOBJECTSINCORRECT"])
def verify_certificate(self, p1, p2, data):
"""
Verify a certificate send by the terminal using the internal trust
anchors.
This method is currently not implemented.
"""
if p1 != 0x00 or p2 not in (0x92, 0xAE, 0xBE):
raise SwError(SW["ERR_INCORRECTP1P2"])
else:
raise SwError(SW["ERR_NOTSUPPORTED"])
def encipher(self, p1, p2, data):
"""
Encipher data using key, algorithm, IV and Padding specified
by the current Security environment.
:returns: raw data (no TLV coding).
"""
algo = self.ct.algorithm
key = self.ct.key
if key is None or algo is None:
raise SwError(SW["ERR_CONDITIONNOTSATISFIED"])
else:
blocklen = vsCrypto.get_cipher_blocklen(algo)
padded = vsCrypto.append_padding(blocklen, data)
crypted = vsCrypto.encrypt(algo, key, padded, self.ct.iv)
return crypted
def decipher(self, p1, p2, data):
"""
Decipher data using key, algorithm, IV and Padding specified
by the current Security environment.
:returns: raw data (no TLV coding). Padding is not removed!!!
"""
algo = self.ct.algorithm
key = self.ct.key
if key is None or algo is None:
raise SwError(SW["ERR_CONDITIONNOTSATISFIED"])
else:
plain = vsCrypto.decrypt(algo, key, data, self.ct.iv)
return plain
def generate_public_key_pair(self, p1, p2, data):
"""
The GENERATE PUBLIC-KEY PAIR command either initiates the generation
and storing of a key pair, i.e., a public key and a private key, in the
card, or accesses a key pair previously generated in the card.
:param p1: should be 0x00 (generate new key)
:param p2: '00' (no information provided) or reference of the key to
be generated
:param data: One or more CRTs associated to the key generation if
P1-P2 different from '0000'
"""
cipher = self.ct.algorithm
c_class = locals().get(cipher, None)
if c_class is None:
raise SwError(SW["ERR_CONDITIONNOTSATISFIED"])
if p1 & 0x01 == 0x00: # Generate key
PublicKey = c_class.generate(self.dst.keylength)
self.dst.key = PublicKey
else:
pass # Read key
# Encode keys
if cipher == "RSA":
# Public key
n = inttostring(PublicKey.n)
e = inttostring(PublicKey.e)
pk = ((0x81, len(n), n), (0x82, len(e), e))
result = bertlv_pack(pk)
# Private key
d = PublicKey.d
elif cipher == "DSA":
# DSAParams
p = inttostring(PublicKey.p)
q = inttostring(PublicKey.q)
g = inttostring(PublicKey.g)
# Public key
y = inttostring(PublicKey.y)
pk = ((0x81, len(p), p), (0x82, len(q), q), (0x83, len(g), g),
(0x84, len(y), y))
# Private key
x = inttostring(PublicKey.x)
# Add more algorithms here
# elif cipher = "ECDSA":
else:
raise SwError(SW["ERR_CONDITIONNOTSATISFIED"])
result = bertlv_pack([[0x7F49, len(pk), pk]])
# TODO: Internally store key pair
if p1 & 0x02 == 0x02:
# We do not support extended header lists yet
raise SwError["ERR_NOTSUPPORTED"]
else:
return SW["NORMAL"], result