diff options
| author | J08nY | 2023-02-17 13:40:33 +0100 |
|---|---|---|
| committer | J08nY | 2023-02-17 13:40:33 +0100 |
| commit | 9efa088d462899c94afd06fbad25003c403a6cee (patch) | |
| tree | bd279114ad01e4549fc9841f190090b63e03c3ce | |
| parent | fb6f0a428dbbd1e53fce582b5e6c7af3b7316485 (diff) | |
| download | pyecsca-9efa088d462899c94afd06fbad25003c403a6cee.tar.gz pyecsca-9efa088d462899c94afd06fbad25003c403a6cee.tar.zst pyecsca-9efa088d462899c94afd06fbad25003c403a6cee.zip | |
| -rw-r--r-- | pyecsca/sca/target/ISO7816.py | 49 | ||||
| -rw-r--r-- | pyecsca/sca/target/PCSC.py | 16 | ||||
| -rw-r--r-- | pyecsca/sca/target/ectester.py | 171 | ||||
| -rw-r--r-- | pyecsca/sca/target/leia.py | 41 | ||||
| -rw-r--r-- | setup.py | 1 |
5 files changed, 177 insertions, 101 deletions
diff --git a/pyecsca/sca/target/ISO7816.py b/pyecsca/sca/target/ISO7816.py index 4441f94..d0e2118 100644 --- a/pyecsca/sca/target/ISO7816.py +++ b/pyecsca/sca/target/ISO7816.py @@ -1,6 +1,7 @@ """Provides classes for working with ISO7816-4 APDUs and an abstract base class for an ISO7816-4 based target.""" from abc import abstractmethod, ABC from dataclasses import dataclass +from enum import IntEnum from typing import Optional from public import public @@ -9,6 +10,18 @@ from .base import Target @public +class CardConnectionException(Exception): + pass + + +@public +class CardProtocol(IntEnum): + """Card protocol to use/negotiate.""" + T0 = 0 + T1 = 1 + + +@public @dataclass class CommandAPDU: # pragma: no cover """Command APDU that can be sent to an ISO7816-4 target.""" @@ -45,35 +58,35 @@ class CommandAPDU: # pragma: no cover if len(self.data) <= 255: # Case 3s return ( - bytes([self.cls, self.ins, self.p1, self.p2, len(self.data)]) - + self.data + bytes([self.cls, self.ins, self.p1, self.p2, len(self.data)]) + + self.data ) else: # Case 3e return ( - bytes([self.cls, self.ins, self.p1, self.p2, 0]) - + len(self.data).to_bytes(2, "big") - + self.data + bytes([self.cls, self.ins, self.p1, self.p2, 0]) + + len(self.data).to_bytes(2, "big") + + self.data ) else: if len(self.data) <= 255 and self.ne <= 256: # Case 4s return ( - bytes([self.cls, self.ins, self.p1, self.p2, len(self.data)]) - + self.data - + bytes([self.ne if self.ne != 256 else 0]) + bytes([self.cls, self.ins, self.p1, self.p2, len(self.data)]) + + self.data + + bytes([self.ne if self.ne != 256 else 0]) ) else: # Case 4e return ( - bytes([self.cls, self.ins, self.p1, self.p2, 0]) - + len(self.data).to_bytes(2, "big") - + self.data - + ( - self.ne.to_bytes(2, "big") - if self.ne != 65536 - else bytes([0, 0]) - ) + bytes([self.cls, self.ins, self.p1, self.p2, 0]) + + len(self.data).to_bytes(2, "big") + + self.data + + ( + self.ne.to_bytes(2, "big") + if self.ne != 65536 + else bytes([0, 0]) + ) ) @@ -90,6 +103,10 @@ class ResponseAPDU: class ISO7816Target(Target, ABC): """ISO7816-4 target.""" + @abstractmethod + def connect(self, protocol: Optional[CardProtocol] = None): + raise NotImplementedError + @property @abstractmethod def atr(self) -> bytes: diff --git a/pyecsca/sca/target/PCSC.py b/pyecsca/sca/target/PCSC.py index f36843f..ace59cc 100644 --- a/pyecsca/sca/target/PCSC.py +++ b/pyecsca/sca/target/PCSC.py @@ -1,5 +1,5 @@ """Provides a smartcard target communicating via PC/SC (Personal Computer/Smart Card).""" -from typing import Union +from typing import Union, Optional from public import public from smartcard.CardConnection import CardConnection @@ -7,7 +7,7 @@ from smartcard.System import readers from smartcard.pcsc.PCSCCardConnection import PCSCCardConnection from smartcard.pcsc.PCSCReader import PCSCReader -from .ISO7816 import ISO7816Target, CommandAPDU, ResponseAPDU, ISO7816 +from .ISO7816 import ISO7816Target, CommandAPDU, ResponseAPDU, ISO7816, CardProtocol, CardConnectionException @public @@ -27,8 +27,16 @@ class PCSCTarget(ISO7816Target): # pragma: no cover self.reader = reader self.connection: PCSCCardConnection = self.reader.createConnection() - def connect(self): - self.connection.connect(CardConnection.T0_protocol | CardConnection.T1_protocol) + def connect(self, protocol: Optional[CardProtocol] = None): + proto = CardConnection.T0_protocol | CardConnection.T1_protocol + if protocol == CardProtocol.T0: + proto = CardConnection.T0_protocol + elif protocol == CardProtocol.T1: + proto = CardConnection.T1_protocol + try: + self.connection.connect(proto) + except: # noqa + raise CardConnectionException() @property def atr(self) -> bytes: diff --git a/pyecsca/sca/target/ectester.py b/pyecsca/sca/target/ectester.py index a4a13c2..74c4a95 100644 --- a/pyecsca/sca/target/ectester.py +++ b/pyecsca/sca/target/ectester.py @@ -8,10 +8,9 @@ from operator import or_ from typing import Optional, Mapping, List, Union from public import public -from smartcard.CardConnection import CardConnection -from smartcard.Exceptions import CardConnectionException -from .ISO7816 import CommandAPDU, ResponseAPDU, ISO7816 +from .ISO7816 import CommandAPDU, ResponseAPDU, ISO7816, ISO7816Target, CardProtocol, CardConnectionException +from .leia import LEIATarget from .PCSC import PCSCTarget from ...ec.model import ShortWeierstrassModel from ...ec.params import DomainParameters @@ -247,7 +246,7 @@ class Response(ABC): # pragma: no cover offset = 0 for i in range(num_sw): if len(resp.data) >= offset + 2: - self.sws[i] = int.from_bytes(resp.data[offset : offset + 2], "big") + self.sws[i] = int.from_bytes(resp.data[offset: offset + 2], "big") offset += 2 if self.sws[i] != ISO7816.SW_NO_ERROR: self.success = False @@ -264,13 +263,13 @@ class Response(ABC): # pragma: no cover self.success = False self.error = True break - param_len = int.from_bytes(resp.data[offset : offset + 2], "big") + param_len = int.from_bytes(resp.data[offset: offset + 2], "big") offset += 2 if len(resp.data) < offset + param_len: self.success = False self.error = True break - self.params[i] = resp.data[offset : offset + param_len] + self.params[i] = resp.data[offset: offset + param_len] offset += param_len def __repr__(self): @@ -342,11 +341,11 @@ class ExportResponse(Response): # pragma: no cover parameters: ParameterEnum def __init__( - self, - resp: ResponseAPDU, - keypair: KeypairEnum, - key: KeyEnum, - params: ParameterEnum, + self, + resp: ResponseAPDU, + keypair: KeypairEnum, + key: KeyEnum, + params: ParameterEnum, ): self.keypair = keypair self.key = key @@ -472,31 +471,31 @@ class InfoResponse(Response): # pragma: no cover super().__init__(resp, 1, 0) offset = 2 - version_len = int.from_bytes(resp.data[offset : offset + 2], "big") + version_len = int.from_bytes(resp.data[offset: offset + 2], "big") offset += 2 - self.version = resp.data[offset : offset + version_len].decode() + self.version = resp.data[offset: offset + version_len].decode() offset += version_len self.base = AppletBaseEnum( - int.from_bytes(resp.data[offset : offset + 2], "big") + int.from_bytes(resp.data[offset: offset + 2], "big") ) offset += 2 - system_version = int.from_bytes(resp.data[offset : offset + 2], "big") + system_version = int.from_bytes(resp.data[offset: offset + 2], "big") system_major = system_version >> 8 system_minor = system_version & 0xFF minor_size = 1 if system_minor == 0 else ceil(log(system_minor, 10)) self.system_version = system_major + system_minor / (minor_size * 10) offset += 2 self.object_deletion_supported = ( - int.from_bytes(resp.data[offset : offset + 2], "big") == 1 + int.from_bytes(resp.data[offset: offset + 2], "big") == 1 ) offset += 2 - self.buf_len = int.from_bytes(resp.data[offset : offset + 2], "big") + self.buf_len = int.from_bytes(resp.data[offset: offset + 2], "big") offset += 2 - self.ram1_len = int.from_bytes(resp.data[offset : offset + 2], "big") + self.ram1_len = int.from_bytes(resp.data[offset: offset + 2], "big") offset += 2 - self.ram2_len = int.from_bytes(resp.data[offset : offset + 2], "big") + self.ram2_len = int.from_bytes(resp.data[offset: offset + 2], "big") offset += 2 - self.apdu_len = int.from_bytes(resp.data[offset : offset + 2], "big") + self.apdu_len = int.from_bytes(resp.data[offset: offset + 2], "big") offset += 2 def __repr__(self): @@ -508,7 +507,7 @@ class InfoResponse(Response): # pragma: no cover @public -class ECTesterTarget(PCSCTarget): # pragma: no cover +class ECTesterTarget(ISO7816Target, ABC): # pragma: no cover """Smartcard target which communicates with the `ECTester <https://github.com/crocs-muni/ECTester>`_ sapplet on smartcards of the JavaCard platform using PCSC.""" CLA_ECTESTER = 0xB0 @@ -519,15 +518,15 @@ class ECTesterTarget(PCSCTarget): # pragma: no cover chunking: bool - def connect(self): + def connect(self, protocol: Optional[CardProtocol] = None): self.chunking = False try: - self.connection.connect(CardConnection.T1_protocol) + super().connect(CardProtocol.T1) except CardConnectionException: - self.connection.connect(CardConnection.T0_protocol) + super().connect(CardProtocol.T0) self.chunking = True - def send_apdu(self, apdu: CommandAPDU) -> ResponseAPDU: + def send(self, apdu: CommandAPDU) -> ResponseAPDU: if self.chunking: data = bytes(apdu) num_chunks = (len(data) + 254) // 255 @@ -536,23 +535,23 @@ class ECTesterTarget(PCSCTarget): # pragma: no cover chunk_length = 255 if chunk_start + chunk_length > len(data): chunk_length = len(data) - chunk_start - chunk = data[chunk_start : chunk_start + chunk_length] + chunk = data[chunk_start: chunk_start + chunk_length] chunk_apdu = CommandAPDU( self.CLA_ECTESTER, InstructionEnum.INS_BUFFER, 0, 0, chunk ) - resp = super().send_apdu(chunk_apdu) + resp = self.send_apdu(chunk_apdu) if resp.sw != 0x9000: raise ChunkingException() apdu = CommandAPDU(self.CLA_ECTESTER, InstructionEnum.INS_PERFORM, 0, 0) - resp = super().send_apdu(apdu) + resp = self.send_apdu(apdu) if resp.sw & 0xFF00 == ISO7816.SW_BYTES_REMAINING_00: - resp = super().send_apdu( + resp = self.send_apdu( CommandAPDU(0x00, 0xC0, 0x00, 0x00, None, resp.sw & 0xFF) ) return resp def select_applet( - self, latest_version: bytes = AID_CURRENT_VERSION, count_back: int = 10 + self, latest_version: bytes = AID_CURRENT_VERSION, count_back: int = 10 ) -> bool: """ Select the *ECTester* applet, with a specified version or older. @@ -590,7 +589,7 @@ class ECTesterTarget(PCSCTarget): # pragma: no cover @staticmethod def encode_parameters( - params: ParameterEnum, obj: Union[DomainParameters, Point, int] + params: ParameterEnum, obj: Union[DomainParameters, Point, int] ) -> Mapping[ParameterEnum, bytes]: """Encode values from `obj` into the byte parameters that the **ECTester** applet expects.""" @@ -603,7 +602,7 @@ class ECTesterTarget(PCSCTarget): # pragma: no cover result = {} if isinstance(obj, DomainParameters) and isinstance( - obj.curve.model, ShortWeierstrassModel + obj.curve.model, ShortWeierstrassModel ): for param in params & ParameterEnum.DOMAIN_FP: if param == ParameterEnum.G: @@ -623,7 +622,7 @@ class ECTesterTarget(PCSCTarget): # pragma: no cover result[param] = convert_point(obj) elif isinstance(obj, int): for param in params & ( - (ParameterEnum.DOMAIN_FP ^ ParameterEnum.G) | ParameterEnum.S + (ParameterEnum.DOMAIN_FP ^ ParameterEnum.G) | ParameterEnum.S ): result[param] = convert_int(obj) else: @@ -637,7 +636,7 @@ class ECTesterTarget(PCSCTarget): # pragma: no cover :param ka_type: Which KeyAgreement type to allocate. :return: The response. """ - resp = self.send_apdu( + resp = self.send( CommandAPDU( self.CLA_ECTESTER, InstructionEnum.INS_ALLOCATE_KA, @@ -655,7 +654,7 @@ class ECTesterTarget(PCSCTarget): # pragma: no cover :param sig_type: Which Signature type to allocate. :return: The response. """ - resp = self.send_apdu( + resp = self.send( CommandAPDU( self.CLA_ECTESTER, InstructionEnum.INS_ALLOCATE_SIG, @@ -667,11 +666,11 @@ class ECTesterTarget(PCSCTarget): # pragma: no cover return AllocateSigResponse(resp) def allocate( - self, - keypair: KeypairEnum, - builder: KeyBuildEnum, - key_length: int, - key_class: KeyClassEnum, + self, + keypair: KeypairEnum, + builder: KeyBuildEnum, + key_length: int, + key_class: KeyClassEnum, ) -> AllocateResponse: """ Send the Allocate KeyPair command. @@ -682,7 +681,7 @@ class ECTesterTarget(PCSCTarget): # pragma: no cover :param key_class: Type of the allocated keypair. :return: The response. """ - resp = self.send_apdu( + resp = self.send( CommandAPDU( self.CLA_ECTESTER, InstructionEnum.INS_ALLOCATE, @@ -700,17 +699,17 @@ class ECTesterTarget(PCSCTarget): # pragma: no cover :param keypair: Which keypair to clear. :return: The response. """ - resp = self.send_apdu( + resp = self.send( CommandAPDU(self.CLA_ECTESTER, InstructionEnum.INS_CLEAR, keypair, 0, None) ) return ClearResponse(resp, keypair) def set( - self, - keypair: KeypairEnum, - curve: CurveEnum, - params: ParameterEnum, - values: Optional[Mapping[ParameterEnum, bytes]] = None, + self, + keypair: KeypairEnum, + curve: CurveEnum, + params: ParameterEnum, + values: Optional[Mapping[ParameterEnum, bytes]] = None, ) -> SetResponse: """ Send the Set command. @@ -732,7 +731,7 @@ class ECTesterTarget(PCSCTarget): # pragma: no cover if e == ParameterEnum.S: break e <<= 1 - resp = self.send_apdu( + resp = self.send( CommandAPDU( self.CLA_ECTESTER, InstructionEnum.INS_SET, keypair, curve, payload ) @@ -740,7 +739,7 @@ class ECTesterTarget(PCSCTarget): # pragma: no cover elif values is not None: raise ValueError("Values should be specified only if curve is external.") else: - resp = self.send_apdu( + resp = self.send( CommandAPDU( self.CLA_ECTESTER, InstructionEnum.INS_SET, @@ -752,11 +751,11 @@ class ECTesterTarget(PCSCTarget): # pragma: no cover return SetResponse(resp, keypair) def transform( - self, - keypair: KeypairEnum, - key: KeyEnum, - params: ParameterEnum, - transformation: TransformationEnum, + self, + keypair: KeypairEnum, + key: KeyEnum, + params: ParameterEnum, + transformation: TransformationEnum, ) -> TransformResponse: """ Send the Transform command. @@ -767,7 +766,7 @@ class ECTesterTarget(PCSCTarget): # pragma: no cover :param transformation: What transformation to apply. :return: The response. """ - resp = self.send_apdu( + resp = self.send( CommandAPDU( self.CLA_ECTESTER, InstructionEnum.INS_TRANSFORM, @@ -785,7 +784,7 @@ class ECTesterTarget(PCSCTarget): # pragma: no cover :param keypair: Which keypair to generate. :return: The response. """ - resp = self.send_apdu( + resp = self.send( CommandAPDU( self.CLA_ECTESTER, InstructionEnum.INS_GENERATE, keypair, 0, None ) @@ -793,7 +792,7 @@ class ECTesterTarget(PCSCTarget): # pragma: no cover return GenerateResponse(resp, keypair) def export( - self, keypair: KeypairEnum, key: KeyEnum, params: ParameterEnum + self, keypair: KeypairEnum, key: KeyEnum, params: ParameterEnum ) -> ExportResponse: """ Send the Export command. @@ -803,7 +802,7 @@ class ECTesterTarget(PCSCTarget): # pragma: no cover :param params: Which parameters to export. :return: The response, containing the exported parameters. """ - resp = self.send_apdu( + resp = self.send( CommandAPDU( self.CLA_ECTESTER, InstructionEnum.INS_EXPORT, @@ -815,12 +814,12 @@ class ECTesterTarget(PCSCTarget): # pragma: no cover return ExportResponse(resp, keypair, key, params) def ecdh( - self, - pubkey: KeypairEnum, - privkey: KeypairEnum, - export: bool, - transformation: TransformationEnum, - ka_type: KeyAgreementEnum, + self, + pubkey: KeypairEnum, + privkey: KeypairEnum, + export: bool, + transformation: TransformationEnum, + ka_type: KeyAgreementEnum, ) -> ECDHResponse: """ Send the ECDH command. @@ -832,7 +831,7 @@ class ECTesterTarget(PCSCTarget): # pragma: no cover :param ka_type: The key-agreement type to use. :return: The response. """ - resp = self.send_apdu( + resp = self.send( CommandAPDU( self.CLA_ECTESTER, InstructionEnum.INS_ECDH, @@ -846,12 +845,12 @@ class ECTesterTarget(PCSCTarget): # pragma: no cover return ECDHResponse(resp, export) def ecdh_direct( - self, - privkey: KeypairEnum, - export: bool, - transformation: TransformationEnum, - ka_type: KeyAgreementEnum, - pubkey: bytes, + self, + privkey: KeypairEnum, + export: bool, + transformation: TransformationEnum, + ka_type: KeyAgreementEnum, + pubkey: bytes, ) -> ECDHResponse: """ Send the ECDH direct command. @@ -863,7 +862,7 @@ class ECTesterTarget(PCSCTarget): # pragma: no cover :param pubkey: The raw bytes that will be used as a pubkey in the key-agreement. :return: The response. """ - resp = self.send_apdu( + resp = self.send( CommandAPDU( self.CLA_ECTESTER, InstructionEnum.INS_ECDH_DIRECT, @@ -878,7 +877,7 @@ class ECTesterTarget(PCSCTarget): # pragma: no cover return ECDHResponse(resp, export) def ecdsa( - self, keypair: KeypairEnum, export: bool, sig_type: SignatureEnum, data: bytes + self, keypair: KeypairEnum, export: bool, sig_type: SignatureEnum, data: bytes ) -> ECDSAResponse: """ Send the ECDSA command. @@ -889,7 +888,7 @@ class ECTesterTarget(PCSCTarget): # pragma: no cover :param data: The data to sign and verify. :return: The response. """ - resp = self.send_apdu( + resp = self.send( CommandAPDU( self.CLA_ECTESTER, InstructionEnum.INS_ECDSA, @@ -901,7 +900,7 @@ class ECTesterTarget(PCSCTarget): # pragma: no cover return ECDSAResponse(resp, export) def ecdsa_sign( - self, keypair: KeypairEnum, export: bool, sig_type: SignatureEnum, data: bytes + self, keypair: KeypairEnum, export: bool, sig_type: SignatureEnum, data: bytes ) -> ECDSAResponse: """ Send the ECDSA sign command. @@ -912,7 +911,7 @@ class ECTesterTarget(PCSCTarget): # pragma: no cover :param data: The data to sign. :return: The response. """ - resp = self.send_apdu( + resp = self.send( CommandAPDU( self.CLA_ECTESTER, InstructionEnum.INS_ECDSA_SIGN, @@ -924,7 +923,7 @@ class ECTesterTarget(PCSCTarget): # pragma: no cover return ECDSAResponse(resp, export) def ecdsa_verify( - self, keypair: KeypairEnum, sig_type: SignatureEnum, sig: bytes, data: bytes + self, keypair: KeypairEnum, sig_type: SignatureEnum, sig: bytes, data: bytes ) -> ECDSAResponse: """ Send the ECDSA verify command. @@ -935,7 +934,7 @@ class ECTesterTarget(PCSCTarget): # pragma: no cover :param data: The data. :return: The response. """ - resp = self.send_apdu( + resp = self.send( CommandAPDU( self.CLA_ECTESTER, InstructionEnum.INS_ECDSA_VERIFY, @@ -952,7 +951,7 @@ class ECTesterTarget(PCSCTarget): # pragma: no cover :return: The response. """ - resp = self.send_apdu( + resp = self.send( CommandAPDU(self.CLA_ECTESTER, InstructionEnum.INS_CLEANUP, 0, 0, None) ) return CleanupResponse(resp) @@ -963,7 +962,7 @@ class ECTesterTarget(PCSCTarget): # pragma: no cover :return: The response. """ - resp = self.send_apdu( + resp = self.send( CommandAPDU(self.CLA_ECTESTER, InstructionEnum.INS_GET_INFO, 0, 0, None) ) return InfoResponse(resp) @@ -974,7 +973,7 @@ class ECTesterTarget(PCSCTarget): # pragma: no cover :return: The response. """ - resp = self.send_apdu( + resp = self.send( CommandAPDU( self.CLA_ECTESTER, InstructionEnum.INS_SET_DRY_RUN_MODE, @@ -984,3 +983,13 @@ class ECTesterTarget(PCSCTarget): # pragma: no cover ) ) return RunModeResponse(resp) + + +@public +class ECTesterTargetPCSC(ECTesterTarget, PCSCTarget): + pass + + +@public +class ECTesterTargetLEIA(ECTesterTarget, LEIATarget): + pass diff --git a/pyecsca/sca/target/leia.py b/pyecsca/sca/target/leia.py new file mode 100644 index 0000000..00f784c --- /dev/null +++ b/pyecsca/sca/target/leia.py @@ -0,0 +1,41 @@ +"""Provides a smartcard target communicating via the LEIA board in solo mode.""" +from typing import Optional + +from smartleia import LEIA, create_APDU_from_bytes, T + +from .ISO7816 import ISO7816Target, CommandAPDU, ResponseAPDU, ISO7816, CardProtocol, CardConnectionException + + +class LEIATarget(ISO7816Target): + """Smartcard target communicating via LEIA in solo mode.""" + + def __init__(self, leia: LEIA): + self.leia = leia + + @property + def atr(self) -> bytes: + return self.leia.get_ATR().normalized() + + def select(self, aid: bytes) -> bool: + apdu = CommandAPDU(0x00, 0xA4, 0x04, 0x00, aid) + resp = self.send_apdu(apdu) + return resp.sw == ISO7816.SW_NO_ERROR + + def send_apdu(self, apdu: CommandAPDU) -> ResponseAPDU: + leia_apdu = create_APDU_from_bytes(bytes(apdu)) + resp = self.leia.send_APDU(leia_apdu) + return ResponseAPDU(resp.data, resp.sw1 << 8 | resp.sw2) + + def connect(self, protocol: Optional[CardProtocol] = None): + proto = T.AUTO + if protocol == CardProtocol.T0: + proto = T.T0 + elif protocol == CardProtocol.T1: + proto = T.T1 + try: + self.leia.configure_smartcard(protocol_to_use=proto) + except: # noqa + raise CardConnectionException() + + def disconnect(self): + pass @@ -48,6 +48,7 @@ setup( "picoscope_alt": ["picoscope"], "chipwhisperer": ["chipwhisperer"], "smartcard": ["pyscard"], + "leia": ["smartleia"], "gmp": ["gmpy2"], "dev": ["mypy", "flake8", "interrogate", "pyinstrument", "black", "types-setuptools"], "test": ["nose2", "parameterized", "coverage"], |
