diff options
| -rw-r--r-- | docs/conf.py | 4 | ||||
| -rw-r--r-- | pyecsca/ec/curve.py | 49 | ||||
| -rw-r--r-- | pyecsca/ec/mod.py | 78 | ||||
| -rw-r--r-- | pyecsca/sca/target/ISO7816.py | 2 | ||||
| -rw-r--r-- | pyecsca/sca/target/__init__.py | 2 | ||||
| -rw-r--r-- | pyecsca/sca/target/ectester.py | 272 | ||||
| -rw-r--r-- | test/ec/test_curve.py | 13 | ||||
| -rw-r--r-- | test/ec/test_mod.py | 18 | ||||
| -rw-r--r-- | test/sca/test_target.py | 288 |
9 files changed, 687 insertions, 39 deletions
diff --git a/docs/conf.py b/docs/conf.py index f84364b..7f4489b 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -207,4 +207,6 @@ autodoc_default_options = { "undoc-members": True, "inherited-members": True, "show-inheritance": True -}
\ No newline at end of file +} + +nbsphinx_allow_errors = True diff --git a/pyecsca/ec/curve.py b/pyecsca/ec/curve.py index e8ae66c..a05ff3f 100644 --- a/pyecsca/ec/curve.py +++ b/pyecsca/ec/curve.py @@ -6,8 +6,8 @@ from public import public from .coordinates import CoordinateModel, AffineCoordinateModel from .mod import Mod -from .model import CurveModel -from .point import Point +from .model import CurveModel, ShortWeierstrassModel +from .point import Point, InfinityPoint @public @@ -93,17 +93,62 @@ class EllipticCurve(object): @property def neutral_is_affine(self): + """Whether the neurtal point is an affine point.""" return bool(self.model.base_neutral) def is_neutral(self, point: Point) -> bool: + """Check whether the point is the neutral point.""" return self.neutral == point def is_on_curve(self, point: Point) -> bool: + """Check whether the point is on the curve.""" if point.coordinate_model.curve_model != self.model: return False + if self.is_neutral(point): + return True loc = {**self.parameters, **point.to_affine().coords} return eval(compile(self.model.equation, "", mode="eval"), loc) + def to_affine(self) -> "EllipticCurve": + """Convert this curve into the affine coordinate model, if possible.""" + coord_model = AffineCoordinateModel(self.model) + return EllipticCurve(self.model, coord_model, self.prime, self.neutral.to_affine(), self.parameters) + + def decode_point(self, encoded: bytes) -> Point: + """Decode a point encoded as a sequence of bytes (ANSI X9.62).""" + if encoded[0] == 0x00 and len(encoded) == 1: + return InfinityPoint(self.coordinate_model) + coord_len = (self.prime.bit_length() + 7) // 8 + if encoded[0] in (0x04, 0x06): + data = encoded[1:] + if len(data) != coord_len * len(self.coordinate_model.variables): + raise ValueError("Encoded point has bad length") + coords = {} + for var in sorted(self.coordinate_model.variables): + coords[var] = Mod(int.from_bytes(data[:coord_len], "big"), self.prime) + data = data[coord_len:] + return Point(self.coordinate_model, **coords) + elif encoded[0] in (0x02, 0x03): + if isinstance(self.coordinate_model, AffineCoordinateModel) and isinstance(self.model, ShortWeierstrassModel): + data = encoded[1:] + if len(data) != coord_len: + raise ValueError("Encoded point has bad length") + x = Mod(int.from_bytes(data, "big"), self.prime) + rhs = x**3 + self.parameters["a"] * x + self.parameters["b"] + if not rhs.is_residue(): + raise ValueError("Point not on curve") + sqrt = rhs.sqrt() + yp = encoded[0] & 0x01 + if int(sqrt) & 0x01 == yp: + y = sqrt + else: + y = -sqrt + return Point(self.coordinate_model, x=x, y=y) + else: + raise NotImplementedError + else: + raise ValueError(f"Wrong encoding type: {hex(encoded[0])}, should be one of 0x04, 0x06, 0x02, 0x03 or 0x00") + def __eq__(self, other): if not isinstance(other, EllipticCurve): return False diff --git a/pyecsca/ec/mod.py b/pyecsca/ec/mod.py index ec5dfe6..421f521 100644 --- a/pyecsca/ec/mod.py +++ b/pyecsca/ec/mod.py @@ -1,5 +1,6 @@ +import random import secrets -from functools import wraps +from functools import wraps, lru_cache from public import public @@ -39,6 +40,34 @@ def extgcd(a, b): return x2, y2, a +@public +@lru_cache +def miller_rabin(n: int, rounds: int = 50) -> bool: + """Miller-Rabin probabilistic primality test.""" + if n == 2 or n == 3: + return True + + if n % 2 == 0: + return False + + r, s = 0, n - 1 + while s % 2 == 0: + r += 1 + s //= 2 + for _ in range(rounds): + a = random.randrange(2, n - 1) + x = pow(a, s, n) + if x == 1 or x == n - 1: + continue + for _ in range(r - 1): + x = pow(x, 2, n) + if x == n - 1: + break + else: + return False + return True + + def check(func): @wraps(func) def method(self, other): @@ -99,6 +128,53 @@ class Mod(object): def __invert__(self): return self.inverse() + def is_residue(self): + """Whether this element is a quadratic residue (only implemented for prime modulus).""" + if not miller_rabin(self.n): + raise NotImplementedError + if self.x == 0: + return True + if self.n == 2: + return self.x in (0, 1) + legendre = self ** ((self.n - 1) // 2) + return legendre == 1 + + def sqrt(self): + """ + The modular square root of this element (only implemented for prime modulus). + + Uses the `Tonelli-Shanks <https://en.wikipedia.org/wiki/Tonelli–Shanks_algorithm>`_ algorithm. + """ + if not miller_rabin(self.n): + raise NotImplementedError + q = self.n - 1 + s = 0 + while q % 2 == 0: + q //= 2 + s += 1 + + z = 2 + while Mod(z, self.n).is_residue(): + z += 1 + + m = s + c = Mod(z, self.n) ** q + t = self ** q + r_exp = (q + 1) // 2 + r = self ** r_exp + + while t != 1: + i = 1 + while not (t ** (2**i)) == 1: + i += 1 + two_exp = m - (i + 1) + b = c ** int(Mod(2, self.n)**two_exp) + m = int(Mod(i, self.n)) + c = b ** 2 + t *= c + r *= b + return r + @check def __mul__(self, other): return Mod((self.x * other.x) % self.n, self.n) diff --git a/pyecsca/sca/target/ISO7816.py b/pyecsca/sca/target/ISO7816.py index 8d5385a..3cf712f 100644 --- a/pyecsca/sca/target/ISO7816.py +++ b/pyecsca/sca/target/ISO7816.py @@ -48,7 +48,7 @@ class CommandAPDU(object): # pragma: no cover @dataclass class ResponseAPDU(object): """A response APDU that can be received from an ISO7816-4 target.""" - data: Optional[bytes] + data: bytes sw: int diff --git a/pyecsca/sca/target/__init__.py b/pyecsca/sca/target/__init__.py index a30ea25..f1555dd 100644 --- a/pyecsca/sca/target/__init__.py +++ b/pyecsca/sca/target/__init__.py @@ -18,7 +18,7 @@ except ImportError: # pragma: no cover pass try: - import pyscard + import smartcard has_pyscard = True except ImportError: # pragma: no cover diff --git a/pyecsca/sca/target/ectester.py b/pyecsca/sca/target/ectester.py index 31ff2a2..e69695a 100644 --- a/pyecsca/sca/target/ectester.py +++ b/pyecsca/sca/target/ectester.py @@ -1,16 +1,20 @@ from abc import ABC +from binascii import hexlify from enum import IntEnum, IntFlag from functools import reduce from math import ceil, log from operator import or_ -from typing import Optional, Mapping, List +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 .PCSC import PCSCTarget -from .. import CommandAPDU, ResponseAPDU, ISO7816 +from ...ec.model import ShortWeierstrassModel +from ...ec.params import DomainParameters +from ...ec.point import Point class ShiftableFlag(IntFlag): @@ -28,6 +32,16 @@ class ShiftableFlag(IntFlag): return e raise ValueError + def __iter__(self): + val = int(self) + for e in self.__class__: + i = int(e) + if i & val == i: + while i % 2 == 0 and i != 0: + i //= 2 + if i == 1: + yield e + @public class KeypairEnum(ShiftableFlag): @@ -226,50 +240,69 @@ class Response(ABC): self.params[i] = resp.data[offset:offset + param_len] offset += param_len + def __repr__(self): + return f"{self.__class__.__name__}(sws=[{', '.join(list(map(hex, self.sws)))}], sw={hex(self.resp.sw)}, success={self.success}, error={self.error})" + +@public class AllocateKaResponse(Response): + """A response to the KeyAgreement allocation command.""" def __init__(self, resp: ResponseAPDU): super().__init__(resp, 1, 0) +@public class AllocateSigResponse(Response): + """A response to the Signature allocation command.""" def __init__(self, resp: ResponseAPDU): super().__init__(resp, 1, 0) +@public class AllocateResponse(Response): + """A response to the KeyPair allocation command.""" def __init__(self, resp: ResponseAPDU, keypair: KeypairEnum): super().__init__(resp, 2 if keypair == KeypairEnum.KEYPAIR_BOTH else 1, 0) +@public class ClearResponse(Response): + """A response to the Clear key command.""" def __init__(self, resp: ResponseAPDU, keypair: KeypairEnum): super().__init__(resp, 2 if keypair == KeypairEnum.KEYPAIR_BOTH else 1, 0) +@public class SetResponse(Response): + """A response to the Set command.""" def __init__(self, resp: ResponseAPDU, keypair: KeypairEnum): super().__init__(resp, 2 if keypair == KeypairEnum.KEYPAIR_BOTH else 1, 0) +@public class TransformResponse(Response): + """A response to the Transform command.""" def __init__(self, resp: ResponseAPDU, keypair: KeypairEnum): super().__init__(resp, 2 if keypair == KeypairEnum.KEYPAIR_BOTH else 1, 0) +@public class GenerateResponse(Response): + """A response to the Generate command.""" def __init__(self, resp: ResponseAPDU, keypair: KeypairEnum): super().__init__(resp, 2 if keypair == KeypairEnum.KEYPAIR_BOTH else 1, 0) +@public class ExportResponse(Response): + """A response to the Export command, contains the exported parameters/values.""" keypair: KeypairEnum key: KeyEnum parameters: ParameterEnum @@ -288,7 +321,7 @@ class ExportResponse(Response): param_count += 1 if param == ParameterEnum.K: break - param << 1 + param <<= 1 other = 0 other += 1 if key & KeyEnum.PUBLIC and params & ParameterEnum.W else 0 other += 1 if key & KeyEnum.PRIVATE and params & ParameterEnum.S else 0 @@ -325,45 +358,63 @@ class ExportResponse(Response): return self.params[index] return None + def __repr__(self): + return f"{self.__class__.__name__}(sws=[{', '.join(list(map(hex, self.sws)))}], sw={hex(self.resp.sw)}, success={self.success}, error={self.error}, " \ + f"keypair={self.keypair.name}, key={self.key.name}, params={self.parameters.name})" + +@public class ECDHResponse(Response): + """A response to the ECDH and ECDH_direct KeyAgreement commands.""" def __init__(self, resp: ResponseAPDU, export: bool): super().__init__(resp, 1, 1 if export else 0) @property def secret(self): - if len(self.params) == 0: + if len(self.params) != 0: return self.params[0] return None + def __repr__(self): + return f"{self.__class__.__name__}(sws=[{', '.join(list(map(hex, self.sws)))}], sw={hex(self.resp.sw)}, success={self.success}, error={self.error}, secret={hexlify(self.secret).decode() if self.secret else ''})" + +@public class ECDSAResponse(Response): + """A response to the ECDSA and ECDSA sign and ECDSA verify commands.""" def __init__(self, resp: ResponseAPDU, export: bool): super().__init__(resp, 1, 1 if export else 0) @property def signature(self): - if len(self.params) == 0: + if len(self.params) != 0: return self.params[0] return None + def __repr__(self): + return f"{self.__class__.__name__}(sws=[{', '.join(list(map(hex, self.sws)))}], sw={hex(self.resp.sw)}, success={self.success}, error={self.error}, sig={hexlify(self.signature).decode() if self.signature else ''})" + +@public class CleanupResponse(Response): + """A response to the Cleanup command.""" def __init__(self, resp: ResponseAPDU): super().__init__(resp, 1, 0) +@public class RunModeResponse(Response): + """A response to the Set run mode command.""" def __init__(self, resp: ResponseAPDU): super().__init__(resp, 1, 0) class InfoResponse(Response): - sw: int + """A response to the Info command, contains all information about the applet version/environment.""" version: str base: AppletBaseEnum system_version: float @@ -376,9 +427,7 @@ class InfoResponse(Response): def __init__(self, resp: ResponseAPDU): super().__init__(resp, 1, 0) - offset = 0 - self.sw = int.from_bytes(resp.data[offset:offset + 2], "big") - offset += 2 + offset = 2 version_len = int.from_bytes(resp.data[offset:offset + 2], "big") offset += 2 self.version = resp.data[offset:offset + version_len].decode() @@ -402,9 +451,18 @@ class InfoResponse(Response): self.apdu_len = int.from_bytes(resp.data[offset:offset + 2], "big") offset += 2 + def __repr__(self): + return f"{self.__class__.__name__}(sws=[{', '.join(list(map(hex, self.sws)))}], sw={hex(self.resp.sw)}, " \ + f"success={self.success}, error={self.error}, version={self.version}, base={self.base.name}, system_version={self.system_version}, " \ + f"object_deletion_supported={self.object_deletion_supported}, buf_len={self.buf_len}, ram1_len={self.ram1_len}, ram2_len={self.ram2_len}, apdu_len={self.apdu_len})" + @public class ECTesterTarget(PCSCTarget): + """ + A smartcard target which communicates with the `ECTester <https://github.com/crocs-muni/ECTester>`_ + applet on smartcards of the JavaCard platform using PCSC. + """ CLA_ECTESTER = 0xb0 AID_PREFIX = bytes([0x45, 0x43, 0x54, 0x65, 0x73, 0x74, 0x65, 0x72]) AID_CURRENT_VERSION = bytes([0x30, 0x33, 0x33]) # Version v0.3.3 @@ -442,6 +500,7 @@ class ECTesterTarget(PCSCTarget): return resp def select_applet(self, latest_version: bytes = AID_CURRENT_VERSION): + """Select the *ECTester* applet, with a specified version or older.""" version_bytes = bytearray(latest_version) for i in range(10): aid_222 = self.AID_PREFIX + version_bytes + self.AID_SUFFIX_222 @@ -469,31 +528,104 @@ class ECTesterTarget(PCSCTarget): return False return True - def allocate_ka(self, ka_type: KeyAgreementEnum): + @staticmethod + def encode_parameters(params: ParameterEnum, obj: Union[DomainParameters, Point, int]) -> \ + Mapping[ParameterEnum, bytes]: + """Encode values from `obj` into the byte parameters that the **ECTester** applet expects.""" + + def convert_int(obj: int) -> bytes: + ilen = (obj.bit_length() + 7) // 8 + return obj.to_bytes(ilen, "big") + + def convert_point(obj: Point) -> bytes: + return bytes(obj) + + result = {} + if isinstance(obj, DomainParameters) and isinstance(obj.curve.model, ShortWeierstrassModel): + for param in params & ParameterEnum.DOMAIN_FP: + if param == ParameterEnum.G: + result[param] = convert_point(obj.generator.to_affine()) + elif param == ParameterEnum.FP: + result[param] = convert_int(obj.curve.prime) + elif param == ParameterEnum.A: + result[param] = convert_int(obj.curve.parameters["a"].x) + elif param == ParameterEnum.B: + result[param] = convert_int(obj.curve.parameters["b"].x) + elif param == ParameterEnum.R: + result[param] = convert_int(obj.order) + elif param == ParameterEnum.K: + result[param] = convert_int(obj.cofactor) + elif isinstance(obj, Point): + for param in params & (ParameterEnum.G | ParameterEnum.W): + result[param] = convert_point(obj) + elif isinstance(obj, int): + for param in params & ((ParameterEnum.DOMAIN_FP ^ ParameterEnum.G) | ParameterEnum.S): + result[param] = convert_int(obj) + else: + raise TypeError + return result + + def allocate_ka(self, ka_type: KeyAgreementEnum) -> AllocateKaResponse: + """ + Send the Allocate KeyAgreement command. + + :param ka_type: Which KeyAgreement type to allocate. + :return: The response. + """ resp = self.send_apdu( CommandAPDU(self.CLA_ECTESTER, InstructionEnum.INS_ALLOCATE_KA, 0, 0, bytes([ka_type]))) return AllocateKaResponse(resp) - def allocate_sig(self, sig_type: SignatureEnum): + def allocate_sig(self, sig_type: SignatureEnum) -> AllocateSigResponse: + """ + Send the Allocate Signature command. + + :param sig_type: Which Signature type to allocate. + :return: The response. + """ resp = self.send_apdu(CommandAPDU(self.CLA_ECTESTER, InstructionEnum.INS_ALLOCATE_SIG, 0, 0, bytes([sig_type]))) return AllocateSigResponse(resp) def allocate(self, keypair: KeypairEnum, builder: KeyBuildEnum, key_length: int, - key_class: KeyClassEnum): + key_class: KeyClassEnum) -> AllocateResponse: + """ + Send the Allocate KeyPair command. + + :param keypair: Which keypair to allocate. + :param builder: Which builder to use to allocate the keypair. + :param key_length: Bit-size of the allocated keypair. + :param key_class: Type of the allocated keypair. + :return: The response. + """ resp = self.send_apdu( CommandAPDU(self.CLA_ECTESTER, InstructionEnum.INS_ALLOCATE, keypair, builder, key_length.to_bytes(2, "big") + bytes([key_class]))) return AllocateResponse(resp, keypair) - def clear(self, keypair: KeypairEnum): + def clear(self, keypair: KeypairEnum) -> ClearResponse: + """ + Send the Clear key command. + + :param keypair: Which keypair to clear. + :return: The response. + """ resp = self.send_apdu( 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): + values: Optional[Mapping[ParameterEnum, bytes]] = None) -> SetResponse: + """ + Send the Set command. + + :param keypair: Which keypair to set values on. + :param curve: Which pre-set curve to use to set values, or default or external. + :param params: Which parameters to set on the keypair. + :param values: External values to set on the keypair. + :return: The response. + """ if curve == CurveEnum.external and values is not None: if params != reduce(or_, values.keys()): raise ValueError("Params and values need to have the same keys.") @@ -517,25 +649,58 @@ class ECTesterTarget(PCSCTarget): return SetResponse(resp, keypair) def transform(self, keypair: KeypairEnum, key: KeyEnum, params: ParameterEnum, - transformation: TransformationEnum): + transformation: TransformationEnum) -> TransformResponse: + """ + Send the Transform command. + + :param keypair: Which keypair to transform. + :param key: Which key to apply the transform to. + :param params: Which parameters to transform. + :param transformation: What transformation to apply. + :return: The response. + """ resp = self.send_apdu( CommandAPDU(self.CLA_ECTESTER, InstructionEnum.INS_TRANSFORM, keypair, key, params.to_bytes(2, "big") + transformation.to_bytes(2, "big"))) return TransformResponse(resp, keypair) - def generate(self, keypair: KeypairEnum): + def generate(self, keypair: KeypairEnum) -> GenerateResponse: + """ + Send the Generate command. + + :param keypair: Which keypair to generate. + :return: The response. + """ resp = self.send_apdu( CommandAPDU(self.CLA_ECTESTER, InstructionEnum.INS_GENERATE, keypair, 0, None)) return GenerateResponse(resp, keypair) - def export(self, keypair: KeypairEnum, key: KeyEnum, params: ParameterEnum): + def export(self, keypair: KeypairEnum, key: KeyEnum, params: ParameterEnum) -> ExportResponse: + """ + Send the Export command. + + :param keypair: Which keypair to export from. + :param key: Which key to export from. + :param params: Which parameters to export. + :return: The response, containing the exported parameters. + """ resp = self.send_apdu( CommandAPDU(self.CLA_ECTESTER, InstructionEnum.INS_EXPORT, keypair, key, params.to_bytes(2, "big"))) return ExportResponse(resp, keypair, key, params) def ecdh(self, pubkey: KeypairEnum, privkey: KeypairEnum, export: bool, - transformation: TransformationEnum, ka_type: KeyAgreementEnum): + transformation: TransformationEnum, ka_type: KeyAgreementEnum) -> ECDHResponse: + """ + Send the ECDH command. + + :param pubkey: Which keypair to use the pubkey from, in the key-agreement. + :param privkey: Which keypair to use the privkey from, in the key-agreement. + :param export: Whether to export the shared secret. + :param transformation: The transformation to apply to the pubkey before key-agreement. + :param ka_type: The key-agreement type to use. + :return: The response. + """ resp = self.send_apdu( CommandAPDU(self.CLA_ECTESTER, InstructionEnum.INS_ECDH, pubkey, privkey, bytes([ExportEnum.from_bool(export)]) + transformation.to_bytes( @@ -543,7 +708,17 @@ class ECTesterTarget(PCSCTarget): return ECDHResponse(resp, export) def ecdh_direct(self, privkey: KeypairEnum, export: bool, transformation: TransformationEnum, - ka_type: KeyAgreementEnum, pubkey: bytes): + ka_type: KeyAgreementEnum, pubkey: bytes) -> ECDHResponse: + """ + Send the ECDH direct command. + + :param privkey: Which keypair to use the privkey from, in the key-agreement. + :param export: Whether to export the shared secret. + :param transformation: The transformation to apply to the pubkey before key-agreement. + :param ka_type: The key-agreement type to use. + :param pubkey: The raw bytes that will be used as a pubkey in the key-agreement. + :return: The response. + """ resp = self.send_apdu( CommandAPDU(self.CLA_ECTESTER, InstructionEnum.INS_ECDH_DIRECT, privkey, ExportEnum.from_bool(export), @@ -551,16 +726,33 @@ class ECTesterTarget(PCSCTarget): pubkey).to_bytes(2, "big") + pubkey)) return ECDHResponse(resp, export) - def ecdsa(self, keypair: KeypairEnum, export: bool, sig_type: SignatureEnum, data: bytes): + def ecdsa(self, keypair: KeypairEnum, export: bool, sig_type: SignatureEnum, + data: bytes) -> ECDSAResponse: + """ + Send the ECDSA command. + + :param keypair: The keypair to use. + :param export: Whether to export the signature. + :param sig_type: The Signature type to use. + :param data: The data to sign and verify. + :return: The response. + """ resp = self.send_apdu(CommandAPDU(self.CLA_ECTESTER, InstructionEnum.INS_ECDSA, keypair, ExportEnum.from_bool(export), bytes([sig_type]) + len(data).to_bytes(2, "big") + data)) return ECDSAResponse(resp, export) def ecdsa_sign(self, keypair: KeypairEnum, export: bool, sig_type: SignatureEnum, - data: Optional[bytes] = None): - if not data: - data = bytes() + data: bytes) -> ECDSAResponse: + """ + Send the ECDSA sign command. + + :param keypair: The keypair to use to sign. + :param export: Whether to export the signature. + :param sig_type: The Signature type to use. + :param data: The data to sign. + :return: The response. + """ resp = self.send_apdu( CommandAPDU(self.CLA_ECTESTER, InstructionEnum.INS_ECDSA_SIGN, keypair, ExportEnum.from_bool(export), @@ -568,26 +760,48 @@ class ECTesterTarget(PCSCTarget): return ECDSAResponse(resp, export) def ecdsa_verify(self, keypair: KeypairEnum, sig_type: SignatureEnum, sig: bytes, - data: Optional[bytes] = None): - if not data: - data = bytes() + data: bytes) -> ECDSAResponse: + """ + Send the ECDSA verify command. + + :param keypair: The keypair to use to verify. + :param sig_type: The Signature type to use. + :param sig: The signature to verify. + :param data: The data. + :return: The response. + """ resp = self.send_apdu(CommandAPDU(self.CLA_ECTESTER, InstructionEnum.INS_ECDSA_VERIFY, keypair, sig_type, len(data).to_bytes(2, "big") + data + len(sig).to_bytes(2, "big") + sig)) return ECDSAResponse(resp, False) - def cleanup(self): + def cleanup(self) -> CleanupResponse: + """ + Send the Cleanup command. + + :return: The response. + """ resp = self.send_apdu( CommandAPDU(self.CLA_ECTESTER, InstructionEnum.INS_CLEANUP, 0, 0, None)) return CleanupResponse(resp) - def info(self): + def info(self) -> InfoResponse: + """ + Send the Info command. + + :return: The response. + """ resp = self.send_apdu( CommandAPDU(self.CLA_ECTESTER, InstructionEnum.INS_GET_INFO, 0, 0, None)) return InfoResponse(resp) - def run_mode(self, run_mode: RunModeEnum): + def run_mode(self, run_mode: RunModeEnum) -> RunModeResponse: + """ + Send the Run mode command. + + :return: The response. + """ resp = self.send_apdu( CommandAPDU(self.CLA_ECTESTER, InstructionEnum.INS_SET_DRY_RUN_MODE, run_mode, 0, None)) diff --git a/test/ec/test_curve.py b/test/ec/test_curve.py index 91d9f16..1b22279 100644 --- a/test/ec/test_curve.py +++ b/test/ec/test_curve.py @@ -1,5 +1,7 @@ +from binascii import unhexlify from unittest import TestCase +from pyecsca.ec.coordinates import AffineCoordinateModel from pyecsca.ec.curve import EllipticCurve from pyecsca.ec.params import get_params from pyecsca.ec.mod import Mod @@ -76,3 +78,14 @@ class CurveTests(TestCase): self.assertEqual(self.secp128r1.curve, self.secp128r1.curve) self.assertNotEqual(self.secp128r1.curve, self.curve25519.curve) self.assertNotEqual(self.secp128r1.curve, None) + + def test_decode(self): + affine_curve = self.secp128r1.curve.to_affine() + affine_point = self.secp128r1.generator.to_affine() + decoded = affine_curve.decode_point(bytes(affine_point)) + self.assertEqual(decoded, affine_point) + + affine_compressed_bytes = unhexlify("03161ff7528b899b2d0c28607ca52c5b86") + decoded_compressed = affine_curve.decode_point(affine_compressed_bytes) + self.assertEqual(decoded_compressed, affine_point) + diff --git a/test/ec/test_mod.py b/test/ec/test_mod.py index 59b716e..59c8e24 100644 --- a/test/ec/test_mod.py +++ b/test/ec/test_mod.py @@ -1,6 +1,6 @@ from unittest import TestCase -from pyecsca.ec.mod import Mod, gcd, extgcd, Undefined +from pyecsca.ec.mod import Mod, gcd, extgcd, Undefined, miller_rabin class ModTests(TestCase): @@ -10,6 +10,22 @@ class ModTests(TestCase): self.assertEqual(extgcd(15, 0), (1, 0, 15)) self.assertEqual(extgcd(15, 20), (-1, 1, 5)) + def test_miller_rabin(self): + self.assertTrue(miller_rabin(2)) + self.assertTrue(miller_rabin(3)) + self.assertTrue(miller_rabin(5)) + self.assertFalse(miller_rabin(8)) + self.assertTrue(miller_rabin(0xe807561107ccf8fa82af74fd492543a918ca2e9c13750233a9)) + self.assertFalse(miller_rabin(0x6f6889deb08da211927370810f026eb4c17b17755f72ea005)) + + def test_is_residue(self): + self.assertTrue(Mod(4, 11).is_residue()) + self.assertFalse(Mod(11, 31).is_residue()) + + def test_sqrt(self): + p = 0xffffffff00000001000000000000000000000000ffffffffffffffffffffffff + self.assertIn(Mod(0xffffffff00000001000000000000000000000000fffffffffffffffffffffffc, p).sqrt(), (0x9add512515b70d9ec471151c1dec46625cd18b37bde7ca7fb2c8b31d7033599d, 0x6522aed9ea48f2623b8eeae3e213b99da32e74c9421835804d374ce28fcca662)) + def test_wrong_mod(self): a = Mod(5, 7) b = Mod(4, 11) diff --git a/test/sca/test_target.py b/test/sca/test_target.py index 25d506a..5519502 100644 --- a/test/sca/test_target.py +++ b/test/sca/test_target.py @@ -1,12 +1,30 @@ -from unittest import TestCase +from copy import copy from os.path import realpath, dirname, join +from typing import Optional +from unittest import TestCase, SkipTest +from pyecsca.ec.key_agreement import ECDH_SHA1 +from pyecsca.ec.key_generation import KeyGeneration +from pyecsca.ec.mod import Mod +from pyecsca.ec.mult import LTRMultiplier +from pyecsca.ec.params import DomainParameters, get_params +from pyecsca.ec.point import Point +from pyecsca.ec.signature import SignatureResult, ECDSA_SHA1 +from pyecsca.sca.target import BinaryTarget, SimpleSerialTarget, SimpleSerialMessage, has_pyscard +from pyecsca.sca.target.ectester import (KeyAgreementEnum, SignatureEnum, KeypairEnum, KeyBuildEnum, + KeyClassEnum, CurveEnum, ParameterEnum, RunModeEnum, + KeyEnum, TransformationEnum) + +if has_pyscard: + from pyecsca.sca.target.ectester import ECTesterTarget +else: + ECTesterTarget = None -from pyecsca.sca.target import BinaryTarget, SimpleSerialTarget, SimpleSerialMessage class TestTarget(SimpleSerialTarget, BinaryTarget): pass + class BinaryTargetTests(TestCase): def test_basic_target(self): @@ -17,4 +35,268 @@ class BinaryTargetTests(TestCase): self.assertIn("r", resp) self.assertIn("z", resp) self.assertEqual(resp["r"].data, "01020304") - target.disconnect()
\ No newline at end of file + target.disconnect() + + +class ECTesterTargetTests(TestCase): + reader: Optional[str] = None + target: Optional[ECTesterTarget] = None + secp256r1: DomainParameters + secp256r1_projective: DomainParameters + + @classmethod + def setUpClass(cls): + if not has_pyscard: + return + from smartcard.System import readers + rs = readers() + if not rs: + return + cls.reader = rs[0] + cls.secp256r1 = get_params("secg", "secp256r1", "affine") + cls.secp256r1_projective = get_params("secg", "secp256r1", "projective") + + def setUp(self): + if not ECTesterTargetTests.reader: + raise SkipTest("No smartcard readers.") + self.target = ECTesterTarget(ECTesterTargetTests.reader) + self.target.connect() + if not self.target.select_applet(): + self.target.disconnect() + raise SkipTest("No applet in reader: {}".format(ECTesterTargetTests.reader)) + + def tearDown(self): + self.target.cleanup() + self.target.disconnect() + + def test_allocate(self): + ka_resp = self.target.allocate_ka(KeyAgreementEnum.ALG_EC_SVDP_DH) + self.assertTrue(ka_resp.success) + sig_resp = self.target.allocate_sig(SignatureEnum.ALG_ECDSA_SHA) + self.assertTrue(sig_resp.success) + key_resp = self.target.allocate(KeypairEnum.KEYPAIR_LOCAL, KeyBuildEnum.BUILD_KEYPAIR, 256, + KeyClassEnum.ALG_EC_FP) + self.assertTrue(key_resp.success) + + def test_set(self): + self.target.allocate(KeypairEnum.KEYPAIR_LOCAL, KeyBuildEnum.BUILD_KEYPAIR, 256, + KeyClassEnum.ALG_EC_FP) + set_resp = self.target.set(KeypairEnum.KEYPAIR_LOCAL, CurveEnum.secp256r1, + ParameterEnum.DOMAIN_FP) + self.assertTrue(set_resp.success) + + def test_set_explicit(self): + self.target.allocate(KeypairEnum.KEYPAIR_LOCAL, KeyBuildEnum.BUILD_KEYPAIR, 256, + KeyClassEnum.ALG_EC_FP) + values = ECTesterTarget.encode_parameters(ParameterEnum.DOMAIN_FP, self.secp256r1) + set_resp = self.target.set(KeypairEnum.KEYPAIR_LOCAL, CurveEnum.external, + ParameterEnum.DOMAIN_FP, values) + self.assertTrue(set_resp.success) + + def test_generate(self): + self.target.allocate(KeypairEnum.KEYPAIR_LOCAL, KeyBuildEnum.BUILD_KEYPAIR, 256, + KeyClassEnum.ALG_EC_FP) + self.target.set(KeypairEnum.KEYPAIR_LOCAL, CurveEnum.secp256r1, ParameterEnum.DOMAIN_FP) + generate_resp = self.target.generate(KeypairEnum.KEYPAIR_LOCAL) + self.assertTrue(generate_resp.success) + + def test_clear(self): + self.target.allocate(KeypairEnum.KEYPAIR_LOCAL, KeyBuildEnum.BUILD_KEYPAIR, 256, + KeyClassEnum.ALG_EC_FP) + clear_resp = self.target.clear(KeypairEnum.KEYPAIR_LOCAL) + self.assertTrue(clear_resp.success) + + def test_cleanup(self): + cleanup_resp = self.target.cleanup() + self.assertTrue(cleanup_resp.success) + + def test_info(self): + info_resp = self.target.info() + self.assertTrue(info_resp.success) + + def test_dry_run(self): + dry_run_resp = self.target.run_mode(RunModeEnum.MODE_DRY_RUN) + self.assertTrue(dry_run_resp.success) + allocate_resp = self.target.allocate(KeypairEnum.KEYPAIR_LOCAL, KeyBuildEnum.BUILD_KEYPAIR, + 256, + KeyClassEnum.ALG_EC_FP) + self.assertTrue(allocate_resp.success) + dry_run_resp = self.target.run_mode(RunModeEnum.MODE_NORMAL) + self.assertTrue(dry_run_resp.success) + + def test_export(self): + self.target.allocate(KeypairEnum.KEYPAIR_LOCAL, KeyBuildEnum.BUILD_KEYPAIR, 256, + KeyClassEnum.ALG_EC_FP) + self.target.set(KeypairEnum.KEYPAIR_LOCAL, CurveEnum.secp256r1, ParameterEnum.DOMAIN_FP) + self.target.generate(KeypairEnum.KEYPAIR_LOCAL) + export_public_resp = self.target.export(KeypairEnum.KEYPAIR_LOCAL, KeyEnum.PUBLIC, + ParameterEnum.W) + self.assertTrue(export_public_resp.success) + pubkey_bytes = export_public_resp.get_param(KeypairEnum.KEYPAIR_LOCAL, ParameterEnum.W) + pubkey = self.secp256r1.curve.decode_point(pubkey_bytes) + export_privkey_resp = self.target.export(KeypairEnum.KEYPAIR_LOCAL, KeyEnum.PRIVATE, + ParameterEnum.S) + self.assertTrue(export_privkey_resp.success) + privkey = int.from_bytes( + export_privkey_resp.get_param(KeypairEnum.KEYPAIR_LOCAL, ParameterEnum.S), "big") + self.assertEqual(pubkey, + self.secp256r1.curve.affine_multiply(self.secp256r1.generator, privkey)) + + def test_export_curve(self): + self.target.allocate(KeypairEnum.KEYPAIR_LOCAL, KeyBuildEnum.BUILD_KEYPAIR, 256, + KeyClassEnum.ALG_EC_FP) + self.target.set(KeypairEnum.KEYPAIR_LOCAL, CurveEnum.secp256r1, ParameterEnum.DOMAIN_FP) + export_resp = self.target.export(KeypairEnum.KEYPAIR_LOCAL, KeyEnum.PUBLIC, + ParameterEnum.DOMAIN_FP) + self.assertTrue(export_resp.success) + + def test_transform(self): + self.target.allocate(KeypairEnum.KEYPAIR_LOCAL, KeyBuildEnum.BUILD_KEYPAIR, 256, + KeyClassEnum.ALG_EC_FP) + self.target.set(KeypairEnum.KEYPAIR_LOCAL, CurveEnum.secp256r1, ParameterEnum.DOMAIN_FP) + self.target.generate(KeypairEnum.KEYPAIR_LOCAL) + export_privkey_resp1 = self.target.export(KeypairEnum.KEYPAIR_LOCAL, KeyEnum.PRIVATE, + ParameterEnum.S) + privkey = int.from_bytes( + export_privkey_resp1.get_param(KeypairEnum.KEYPAIR_LOCAL, ParameterEnum.S), "big") + transform_resp = self.target.transform(KeypairEnum.KEYPAIR_LOCAL, KeyEnum.PRIVATE, + ParameterEnum.S, TransformationEnum.INCREMENT) + self.assertTrue(transform_resp.success) + export_privkey_resp2 = self.target.export(KeypairEnum.KEYPAIR_LOCAL, KeyEnum.PRIVATE, + ParameterEnum.S) + privkey_new = int.from_bytes( + export_privkey_resp2.get_param(KeypairEnum.KEYPAIR_LOCAL, ParameterEnum.S), "big") + self.assertEqual(privkey + 1, privkey_new) + + def test_ecdh(self): + self.target.allocate_ka(KeyAgreementEnum.ALG_EC_SVDP_DH) + self.target.allocate(KeypairEnum.KEYPAIR_BOTH, KeyBuildEnum.BUILD_KEYPAIR, 256, + KeyClassEnum.ALG_EC_FP) + self.target.set(KeypairEnum.KEYPAIR_BOTH, CurveEnum.secp256r1, ParameterEnum.DOMAIN_FP) + self.target.generate(KeypairEnum.KEYPAIR_BOTH) + ecdh_resp = self.target.ecdh(KeypairEnum.KEYPAIR_LOCAL, KeypairEnum.KEYPAIR_REMOTE, True, + TransformationEnum.NONE, KeyAgreementEnum.ALG_EC_SVDP_DH) + self.assertTrue(ecdh_resp.success) + export_public_resp = self.target.export(KeypairEnum.KEYPAIR_LOCAL, KeyEnum.PUBLIC, + ParameterEnum.W) + pubkey_bytes = export_public_resp.get_param(KeypairEnum.KEYPAIR_LOCAL, ParameterEnum.W) + pubkey = self.secp256r1.curve.decode_point(pubkey_bytes) + export_privkey_resp = self.target.export(KeypairEnum.KEYPAIR_REMOTE, KeyEnum.PRIVATE, + ParameterEnum.S) + privkey = Mod(int.from_bytes( + export_privkey_resp.get_param(KeypairEnum.KEYPAIR_REMOTE, ParameterEnum.S), "big"), + self.secp256r1.curve.prime) + pubkey_projective = Point.from_affine(self.secp256r1_projective.curve.coordinate_model, + pubkey) + mult = LTRMultiplier( + self.secp256r1_projective.curve.coordinate_model.formulas["add-2016-rcb"], + self.secp256r1_projective.curve.coordinate_model.formulas["dbl-2016-rcb"]) + ecdh = ECDH_SHA1(mult, self.secp256r1_projective, pubkey_projective, privkey) + expected = ecdh.perform() + self.assertEqual(ecdh_resp.secret, expected) + + def test_ecdh_raw(self): + self.target.allocate_ka(KeyAgreementEnum.ALG_EC_SVDP_DH) + self.target.allocate(KeypairEnum.KEYPAIR_LOCAL, KeyBuildEnum.BUILD_KEYPAIR, 256, + KeyClassEnum.ALG_EC_FP) + self.target.set(KeypairEnum.KEYPAIR_LOCAL, CurveEnum.secp256r1, ParameterEnum.DOMAIN_FP) + self.target.generate(KeypairEnum.KEYPAIR_LOCAL) + mult = LTRMultiplier( + self.secp256r1_projective.curve.coordinate_model.formulas["add-2016-rcb"], + self.secp256r1_projective.curve.coordinate_model.formulas["dbl-2016-rcb"]) + keygen = KeyGeneration(copy(mult), self.secp256r1_projective) + priv, pubkey_projective = keygen.generate() + + ecdh_resp = self.target.ecdh_direct(KeypairEnum.KEYPAIR_LOCAL, True, + TransformationEnum.NONE, + KeyAgreementEnum.ALG_EC_SVDP_DH, + bytes(pubkey_projective.to_affine())) + self.assertTrue(ecdh_resp.success) + export_privkey_resp = self.target.export(KeypairEnum.KEYPAIR_LOCAL, KeyEnum.PRIVATE, + ParameterEnum.S) + privkey = Mod(int.from_bytes( + export_privkey_resp.get_param(KeypairEnum.KEYPAIR_LOCAL, ParameterEnum.S), "big"), + self.secp256r1.curve.prime) + + ecdh = ECDH_SHA1(copy(mult), self.secp256r1_projective, pubkey_projective, privkey) + expected = ecdh.perform() + self.assertEqual(ecdh_resp.secret, expected) + + def test_ecdsa(self): + self.target.allocate_sig(SignatureEnum.ALG_ECDSA_SHA) + self.target.allocate(KeypairEnum.KEYPAIR_LOCAL, KeyBuildEnum.BUILD_KEYPAIR, 256, + KeyClassEnum.ALG_EC_FP) + self.target.set(KeypairEnum.KEYPAIR_LOCAL, CurveEnum.secp256r1, ParameterEnum.DOMAIN_FP) + self.target.generate(KeypairEnum.KEYPAIR_LOCAL) + data = "Some text over here.".encode() + ecdsa_resp = self.target.ecdsa(KeypairEnum.KEYPAIR_LOCAL, True, SignatureEnum.ALG_ECDSA_SHA, + data) + self.assertTrue(ecdsa_resp.success) + export_public_resp = self.target.export(KeypairEnum.KEYPAIR_LOCAL, KeyEnum.PUBLIC, + ParameterEnum.W) + pubkey_bytes = export_public_resp.get_param(KeypairEnum.KEYPAIR_LOCAL, ParameterEnum.W) + pubkey = self.secp256r1.curve.decode_point(pubkey_bytes) + pubkey_projective = Point.from_affine(self.secp256r1_projective.curve.coordinate_model, + pubkey) + + sig = SignatureResult.from_DER(ecdsa_resp.signature) + mult = LTRMultiplier( + self.secp256r1_projective.curve.coordinate_model.formulas["add-2016-rcb"], + self.secp256r1_projective.curve.coordinate_model.formulas["dbl-2016-rcb"]) + ecdsa = ECDSA_SHA1(copy(mult), self.secp256r1_projective, + self.secp256r1_projective.curve.coordinate_model.formulas[ + "add-2016-rcb"], + pubkey_projective) + self.assertTrue(ecdsa.verify_data(sig, data)) + + def test_ecdsa_sign(self): + self.target.allocate_sig(SignatureEnum.ALG_ECDSA_SHA) + self.target.allocate(KeypairEnum.KEYPAIR_LOCAL, KeyBuildEnum.BUILD_KEYPAIR, 256, + KeyClassEnum.ALG_EC_FP) + self.target.set(KeypairEnum.KEYPAIR_LOCAL, CurveEnum.secp256r1, ParameterEnum.DOMAIN_FP) + self.target.generate(KeypairEnum.KEYPAIR_LOCAL) + data = "Some text over here.".encode() + ecdsa_resp = self.target.ecdsa_sign(KeypairEnum.KEYPAIR_LOCAL, True, + SignatureEnum.ALG_ECDSA_SHA, data) + self.assertTrue(ecdsa_resp.success) + export_public_resp = self.target.export(KeypairEnum.KEYPAIR_LOCAL, KeyEnum.PUBLIC, + ParameterEnum.W) + pubkey_bytes = export_public_resp.get_param(KeypairEnum.KEYPAIR_LOCAL, ParameterEnum.W) + pubkey = self.secp256r1.curve.decode_point(pubkey_bytes) + pubkey_projective = Point.from_affine(self.secp256r1_projective.curve.coordinate_model, + pubkey) + + sig = SignatureResult.from_DER(ecdsa_resp.signature) + mult = LTRMultiplier( + self.secp256r1_projective.curve.coordinate_model.formulas["add-2016-rcb"], + self.secp256r1_projective.curve.coordinate_model.formulas["dbl-2016-rcb"]) + ecdsa = ECDSA_SHA1(copy(mult), self.secp256r1_projective, + self.secp256r1_projective.curve.coordinate_model.formulas[ + "add-2016-rcb"], + pubkey_projective) + self.assertTrue(ecdsa.verify_data(sig, data)) + + def test_ecdsa_verify(self): + self.target.allocate_sig(SignatureEnum.ALG_ECDSA_SHA) + self.target.allocate(KeypairEnum.KEYPAIR_LOCAL, KeyBuildEnum.BUILD_KEYPAIR, 256, + KeyClassEnum.ALG_EC_FP) + self.target.set(KeypairEnum.KEYPAIR_LOCAL, CurveEnum.secp256r1, ParameterEnum.DOMAIN_FP) + mult = LTRMultiplier( + self.secp256r1_projective.curve.coordinate_model.formulas["add-2016-rcb"], + self.secp256r1_projective.curve.coordinate_model.formulas["dbl-2016-rcb"]) + keygen = KeyGeneration(copy(mult), self.secp256r1_projective) + priv, pubkey_projective = keygen.generate() + self.target.set(KeypairEnum.KEYPAIR_LOCAL, CurveEnum.external, ParameterEnum.W, + ECTesterTarget.encode_parameters(ParameterEnum.W, + pubkey_projective.to_affine())) + ecdsa = ECDSA_SHA1(copy(mult), self.secp256r1_projective, + self.secp256r1_projective.curve.coordinate_model.formulas[ + "add-2016-rcb"], + pubkey_projective, + priv) + data = "Some text over here.".encode() + sig = ecdsa.sign_data(data) + + ecdsa_resp = self.target.ecdsa_verify(KeypairEnum.KEYPAIR_LOCAL, + SignatureEnum.ALG_ECDSA_SHA, sig.to_DER(), data) + self.assertTrue(ecdsa_resp.success) |
