diff --git a/ChangeLog b/ChangeLog index 42297cd7..d2fdd224 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,7 +1,7 @@ 2.2.0 (October 2024) ==================== - patches from Ludovic Rousseau - * PCSCCardRequest: + * PCSCCardRequest: - handle KeyboardInterrupt in waitforcard() & waitforcardevent() - use a local PC/SC context to avoid locks * smartcard.util.padd(): do NOT modify the input parameter diff --git a/src/smartcard/ATR.py b/src/smartcard/ATR.py index daffd4fe..29221769 100644 --- a/src/smartcard/ATR.py +++ b/src/smartcard/ATR.py @@ -22,14 +22,22 @@ Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA """ +from __future__ import annotations + +import functools +import operator +import warnings + from smartcard.Exceptions import SmartcardException -from smartcard.util import toHexString class ATR: - """ATR class.""" + """Parse and represent Answer to Reset sequences. - clockrateconversion = [ + Answer to Reset sequences are defined in ISO 7816-3, section 8. + """ + + clockrateconversion: list[int | str] = [ 372, 372, 558, @@ -47,7 +55,7 @@ class ATR: "RFU", "RFU", ] - bitratefactor = [ + bitratefactor: list[int | str] = [ "RFU", 1, 2, @@ -65,21 +73,11 @@ class ATR: "RFU", "RFU", ] - currenttable = [25, 50, 100, "RFU"] + currenttable: list[int | str] = [25, 50, 100, "RFU"] - def __init__(self, atr): - """Construct a new atr from atr.""" - self.atr = atr - self.__initInstance__() + def __init__(self, atr: list[int]) -> None: + """Parse ATR and initialize members: - def __checksyncbyte__(self): - """Check validity of TS.""" - if not 0x3B == self.atr[0] and not 0x03F == self.atr[0]: - raise SmartcardException("invalid TS 0x%-0.2x" % self.atr[0]) - - def __initInstance__(self): - """ - Parse ATR and initialize members: - TS: initial character - T0: format character - TA[n], TB[n], TC[n], TD[n], for n=0,1,...: protocol parameters @@ -96,7 +94,13 @@ def __initInstance__(self): - II: maximum programming current factor - N: extra guard time """ - self.__checksyncbyte__() + + if len(atr) < 2: + raise SmartcardException(f"ATR sequences must be at least 2 bytes long") + if atr[0] not in {0x3B, 0x3F}: + raise SmartcardException(f"invalid TS 0x{atr[0]:02x}") + + self.atr = atr # initial character self.TS = self.atr[0] @@ -108,244 +112,223 @@ def __initInstance__(self): self.K = self.T0 & 0x0F # initialize optional characters lists - self.TA = [] - self.TB = [] - self.TC = [] - self.TD = [] - self.Y = [] - self.hasTA = [] - self.hasTB = [] - self.hasTC = [] - self.hasTD = [] - - TD = self.T0 - hasTD = 1 - n = 0 - offset = 1 - self.interfaceBytesCount = 0 - while hasTD: - self.Y += [TD >> 4 & 0x0F] + self.TA: list[None | int] = [] + self.TB: list[None | int] = [] + self.TC: list[None | int] = [] + self.TD: list[None | int] = [] + self.Y: list[int] = [] - self.hasTD += [(self.Y[n] & 0x08) != 0] - self.hasTC += [(self.Y[n] & 0x04) != 0] - self.hasTB += [(self.Y[n] & 0x02) != 0] - self.hasTA += [(self.Y[n] & 0x01) != 0] + td: None | int = self.T0 + offset = 1 + while td is not None: + self.Y.append(td >> 4 & 0x0F) self.TA += [None] self.TB += [None] self.TC += [None] self.TD += [None] - if self.hasTA[n]: - self.TA[n] = self.atr[offset + self.hasTA[n]] - if self.hasTB[n]: - self.TB[n] = self.atr[offset + self.hasTA[n] + self.hasTB[n]] - if self.hasTC[n]: - self.TC[n] = self.atr[ - offset + self.hasTA[n] + self.hasTB[n] + self.hasTC[n] - ] - if self.hasTD[n]: - self.TD[n] = self.atr[ - offset - + self.hasTA[n] - + self.hasTB[n] - + self.hasTC[n] - + self.hasTD[n] - ] - - self.interfaceBytesCount += ( - self.hasTA[n] + self.hasTB[n] + self.hasTC[n] + self.hasTD[n] - ) - TD = self.TD[n] - hasTD = self.hasTD[n] - offset = ( - offset + self.hasTA[n] + self.hasTB[n] + self.hasTC[n] + self.hasTD[n] - ) - n = n + 1 + if self.Y[-1] & 0x01: # TA + offset += 1 + self.TA[-1] = self.atr[offset] + if self.Y[-1] & 0x02: # TB + offset += 1 + self.TB[-1] = self.atr[offset] + if self.Y[-1] & 0x04: # TC + offset += 1 + self.TC[-1] = self.atr[offset] + if self.Y[-1] & 0x08: # TD + offset += 1 + self.TD[-1] = self.atr[offset] + + td = self.TD[-1] + + self.interfaceBytesCount = offset - 1 # historical bytes self.historicalBytes = self.atr[offset + 1 : offset + 1 + self.K] # checksum + self.TCK: int | None = None + self.checksumOK: bool | None = None self.hasChecksum = len(self.atr) == offset + 1 + self.K + 1 if self.hasChecksum: self.TCK = self.atr[-1] - checksum = 0 - for b in self.atr[1:]: - checksum = checksum ^ b - self.checksumOK = checksum == 0 - else: - self.TCK = None + self.checksumOK = functools.reduce(operator.xor, self.atr[1:]) == 0 # clock-rate conversion factor - if self.hasTA[0]: + self.FI: int | None = None + if self.TA[0] is not None: self.FI = self.TA[0] >> 4 & 0x0F - else: - self.FI = None # bit-rate adjustment factor - if self.hasTA[0]: + self.DI: int | None = None + if self.TA[0] is not None: self.DI = self.TA[0] & 0x0F - else: - self.DI = None # maximum programming current factor - if self.hasTB[0]: + self.II: int | None = None + if self.TB[0] is not None: self.II = self.TB[0] >> 5 & 0x03 - else: - self.II = None # programming voltage factor - if self.hasTB[0]: + self.PI1: int | None = None + if self.TB[0] is not None: self.PI1 = self.TB[0] & 0x1F - else: - self.PI1 = None # extra guard time self.N = self.TC[0] - def getChecksum(self): + @property + def hasTA(self) -> list[bool]: + """Deprecated. Replace usage with `ATR.TA[i] is not None`.""" + + warnings.warn("Replace usage with `ATR.TA[i] is not None`", DeprecationWarning) + return [ta is not None for ta in self.TA] + + @property + def hasTB(self) -> list[bool]: + """Deprecated. Replace usage with `ATR.TB[i] is not None`.""" + + warnings.warn("Replace usage with `ATR.TB[i] is not None`", DeprecationWarning) + return [tb is not None for tb in self.TB] + + @property + def hasTC(self) -> list[bool]: + """Deprecated. Replace usage with `ATR.TC[i] is not None`.""" + + warnings.warn("Replace usage with `ATR.TC[i] is not None`", DeprecationWarning) + return [tc is not None for tc in self.TC] + + @property + def hasTD(self) -> list[bool]: + """Deprecated. Replace usage with `ATR.TD[i] is not None`.""" + + warnings.warn("Replace usage with `ATR.TD[i] is not None`", DeprecationWarning) + return [td is not None for td in self.TD] + + def getChecksum(self) -> int | None: """Return the checksum of the ATR. Checksum is mandatory only for T=1.""" return self.TCK - def getHistoricalBytes(self): + def getHistoricalBytes(self) -> list[int]: """Return historical bytes.""" return self.historicalBytes - def getHistoricalBytesCount(self): + def getHistoricalBytesCount(self) -> int: """Return count of historical bytes.""" return len(self.historicalBytes) - def getInterfaceBytesCount(self): + def getInterfaceBytesCount(self) -> int: """Return count of interface bytes.""" return self.interfaceBytesCount - def getTA1(self): + def getTA1(self) -> int | None: """Return TA1 byte.""" return self.TA[0] - def getTB1(self): + def getTB1(self) -> int | None: """Return TB1 byte.""" return self.TB[0] - def getTC1(self): + def getTC1(self) -> int | None: """Return TC1 byte.""" return self.TC[0] - def getTD1(self): + def getTD1(self) -> int | None: """Return TD1 byte.""" return self.TD[0] - def getBitRateFactor(self): + def getBitRateFactor(self) -> int | str: """Return bit rate factor.""" if self.DI is not None: return ATR.bitratefactor[self.DI] - else: - return 1 + return 1 - def getClockRateConversion(self): + def getClockRateConversion(self) -> int | str: """Return clock rate conversion.""" if self.FI is not None: return ATR.clockrateconversion[self.FI] - else: - return 372 + return 372 - def getProgrammingCurrent(self): + def getProgrammingCurrent(self) -> int | str: """Return maximum programming current.""" if self.II is not None: return ATR.currenttable[self.II] - else: - return 50 + return 50 - def getProgrammingVoltage(self): + def getProgrammingVoltage(self) -> int: """Return programming voltage.""" if self.PI1 is not None: return 5 * (1 + self.PI1) - else: - return 5 + return 5 - def getGuardTime(self): + def getGuardTime(self) -> int | None: """Return extra guard time.""" return self.N - def getSupportedProtocols(self): + def getSupportedProtocols(self) -> dict[str, bool]: """Returns a dictionary of supported protocols.""" - protocols = {} + protocols: dict[str, bool] = {} for td in self.TD: if td is not None: - strprotocol = "T=%d" % (td & 0x0F) - protocols[strprotocol] = True - if not self.hasTD[0]: + protocols[f"T={td & 0x0F}"] = True + if self.TD[0] is None: protocols["T=0"] = True return protocols - def isT0Supported(self): + def isT0Supported(self) -> bool: """Return True if T=0 is supported.""" - protocols = self.getSupportedProtocols() - return "T=0" in protocols + return "T=0" in self.getSupportedProtocols() - def isT1Supported(self): + def isT1Supported(self) -> bool: """Return True if T=1 is supported.""" - protocols = self.getSupportedProtocols() - return "T=1" in protocols + return "T=1" in self.getSupportedProtocols() - def isT15Supported(self): + def isT15Supported(self) -> bool: """Return True if T=15 is supported.""" - protocols = self.getSupportedProtocols() - return "T=15" in protocols - - def dump(self): - """Dump the details of an ATR.""" - - for i in range(0, len(self.TA)): - if self.TA[i] is not None: - print("TA%d: %x" % (i + 1, self.TA[i])) - if self.TB[i] is not None: - print("TB%d: %x" % (i + 1, self.TB[i])) - if self.TC[i] is not None: - print("TC%d: %x" % (i + 1, self.TC[i])) - if self.TD[i] is not None: - print("TD%d: %x" % (i + 1, self.TD[i])) - - print("supported protocols " + ",".join(self.getSupportedProtocols())) - print("T=0 supported: " + str(self.isT0Supported())) - print("T=1 supported: " + str(self.isT1Supported())) - - if self.getChecksum(): - print("checksum: %d" % self.getChecksum()) - - print("\tclock rate conversion factor: " + str(self.getClockRateConversion())) - print("\tbit rate adjustment factor: " + str(self.getBitRateFactor())) + return "T=15" in self.getSupportedProtocols() + + def render(self) -> str: + """Render the ATR to a readable format.""" + + lines: list[str] = [] + enumerated_tx_values = enumerate(zip(self.TA, self.TB, self.TC, self.TD), 1) + for i, (ta, tb, tc, td) in enumerated_tx_values: + if ta is not None: + lines.append(f"TA{i}: {ta:x}") + if tb is not None: + lines.append(f"TB{i}: {tb:x}") + if tc is not None: + lines.append(f"TC{i}: {tc:x}") + if td is not None: + lines.append(f"TD{i}: {td:x}") - print("\tmaximum programming current: " + str(self.getProgrammingCurrent())) - print("\tprogramming voltage: " + str(self.getProgrammingVoltage())) + lines.append(f"supported protocols {','.join(self.getSupportedProtocols())}") + lines.append(f"T=0 supported: {self.isT0Supported()}") + lines.append(f"T=1 supported: {self.isT1Supported()}") - print("\tguard time: " + str(self.getGuardTime())) + if self.getChecksum() is not None: + lines.append(f"checksum: {self.getChecksum()}") - print("nb of interface bytes: %d" % self.getInterfaceBytesCount()) - print("nb of historical bytes: %d" % self.getHistoricalBytesCount()) + lines.append(f"\tclock rate conversion factor: {self.getClockRateConversion()}") + lines.append(f"\tbit rate adjustment factor: {self.getBitRateFactor()}") + lines.append(f"\tmaximum programming current: {self.getProgrammingCurrent()}") + lines.append(f"\tprogramming voltage: {self.getProgrammingVoltage()}") + lines.append(f"\tguard time: {self.getGuardTime()}") + lines.append(f"nb of interface bytes: {self.getInterfaceBytesCount()}") + lines.append(f"nb of historical bytes: {self.getHistoricalBytesCount()}") - def __str__(self): - """Returns a string representation of the ATR as a stream of bytes.""" - return toHexString(self.atr) + return "\n".join(lines) + def dump(self) -> None: + """Deprecated. Replace usage with `print(ATR.render())`""" -if __name__ == "__main__": - """Small sample illustrating the use of ATR.""" + warnings.warn("Replace usage with `print(ATR.render())`", DeprecationWarning) + print(self.render()) - atrs = [ - [0x3F, 0x65, 0x25, 0x00, 0x2C, 0x09, 0x69, 0x90, 0x00], - [0x3F, 0x65, 0x25, 0x08, 0x93, 0x04, 0x6C, 0x90, 0x00], - [0x3B, 0x16, 0x94, 0x7C, 0x03, 0x01, 0x00, 0x00, 0x0D], - [0x3B, 0x65, 0x00, 0x00, 0x9C, 0x11, 0x01, 0x01, 0x03], - [0x3B, 0xE3, 0x00, 0xFF, 0x81, 0x31, 0x52, 0x45, 0xA1, 0xA2, 0xA3, 0x1B], - [0x3B, 0xE5, 0x00, 0x00, 0x81, 0x21, 0x45, 0x9C, 0x10, 0x01, 0x00, 0x80, 0x0D], - ] + def __str__(self) -> str: + """Render the ATR as a space-separated string of uppercase hexadecimal pairs.""" - for atr in atrs: - a = ATR(atr) - print(80 * "-") - print(a) - a.dump() - print(toHexString(a.getHistoricalBytes())) + return bytes(self.atr).hex(" ").upper() diff --git a/test/test_ATR.py b/test/test_ATR.py index 9509af8e..e2030425 100644 --- a/test/test_ATR.py +++ b/test/test_ATR.py @@ -1,3 +1,6 @@ +import re +import textwrap + import pytest from smartcard.ATR import ATR @@ -7,142 +10,173 @@ def test_atr1(capsys): atr = [0x3F, 0x65, 0x25, 0x00, 0x2C, 0x09, 0x69, 0x90, 0x00] - data_out = """TB1: 25 -TC1: 0 -supported protocols T=0 -T=0 supported: True -T=1 supported: False -\tclock rate conversion factor: 372 -\tbit rate adjustment factor: 1 -\tmaximum programming current: 50 -\tprogramming voltage: 30 -\tguard time: 0 -nb of interface bytes: 2 -nb of historical bytes: 5 -""" + data_out = textwrap.dedent( + """\ + TB1: 25 + TC1: 0 + supported protocols T=0 + T=0 supported: True + T=1 supported: False + \tclock rate conversion factor: 372 + \tbit rate adjustment factor: 1 + \tmaximum programming current: 50 + \tprogramming voltage: 30 + \tguard time: 0 + nb of interface bytes: 2 + nb of historical bytes: 5 + """ + ) a = ATR(atr) - a.dump() + with pytest.warns(DeprecationWarning, match=re.escape("print(ATR.render())")): + a.dump() stdout, _ = capsys.readouterr() assert stdout == data_out def test_atr2(capsys): atr = [0x3F, 0x65, 0x25, 0x08, 0x93, 0x04, 0x6C, 0x90, 0x00] - data_out = """TB1: 25 -TC1: 8 -supported protocols T=0 -T=0 supported: True -T=1 supported: False -\tclock rate conversion factor: 372 -\tbit rate adjustment factor: 1 -\tmaximum programming current: 50 -\tprogramming voltage: 30 -\tguard time: 8 -nb of interface bytes: 2 -nb of historical bytes: 5 -""" + data_out = textwrap.dedent( + """\ + TB1: 25 + TC1: 8 + supported protocols T=0 + T=0 supported: True + T=1 supported: False + \tclock rate conversion factor: 372 + \tbit rate adjustment factor: 1 + \tmaximum programming current: 50 + \tprogramming voltage: 30 + \tguard time: 8 + nb of interface bytes: 2 + nb of historical bytes: 5 + """ + ) a = ATR(atr) - a.dump() - + with pytest.warns(DeprecationWarning, match=re.escape("print(ATR.render())")): + a.dump() stdout, _ = capsys.readouterr() assert stdout == data_out def test_atr3(capsys): atr = [0x3B, 0x16, 0x94, 0x7C, 0x03, 0x01, 0x00, 0x00, 0x0D] - data_out = """TA1: 94 -supported protocols T=0 -T=0 supported: True -T=1 supported: False -\tclock rate conversion factor: 512 -\tbit rate adjustment factor: 8 -\tmaximum programming current: 50 -\tprogramming voltage: 5 -\tguard time: None -nb of interface bytes: 1 -nb of historical bytes: 6 -""" + data_out = textwrap.dedent( + """\ + TA1: 94 + supported protocols T=0 + T=0 supported: True + T=1 supported: False + \tclock rate conversion factor: 512 + \tbit rate adjustment factor: 8 + \tmaximum programming current: 50 + \tprogramming voltage: 5 + \tguard time: None + nb of interface bytes: 1 + nb of historical bytes: 6 + """ + ) a = ATR(atr) - a.dump() + with pytest.warns(DeprecationWarning, match=re.escape("print(ATR.render())")): + a.dump() stdout, _ = capsys.readouterr() assert stdout == data_out def test_atr4(capsys): atr = [0x3B, 0x65, 0x00, 0x00, 0x9C, 0x11, 0x01, 0x01, 0x03] - data_out = """TB1: 0 -TC1: 0 -supported protocols T=0 -T=0 supported: True -T=1 supported: False -\tclock rate conversion factor: 372 -\tbit rate adjustment factor: 1 -\tmaximum programming current: 25 -\tprogramming voltage: 5 -\tguard time: 0 -nb of interface bytes: 2 -nb of historical bytes: 5 -""" + data_out = textwrap.dedent( + """\ + TB1: 0 + TC1: 0 + supported protocols T=0 + T=0 supported: True + T=1 supported: False + \tclock rate conversion factor: 372 + \tbit rate adjustment factor: 1 + \tmaximum programming current: 25 + \tprogramming voltage: 5 + \tguard time: 0 + nb of interface bytes: 2 + nb of historical bytes: 5 + """ + ) a = ATR(atr) - a.dump() + with pytest.warns(DeprecationWarning, match=re.escape("print(ATR.render())")): + a.dump() stdout, _ = capsys.readouterr() assert stdout == data_out def test_atr5(capsys): atr = [0x3B, 0xE3, 0x00, 0xFF, 0x81, 0x31, 0x52, 0x45, 0xA1, 0xA2, 0xA3, 0x1B] - data_out = """TB1: 0 -TC1: ff -TD1: 81 -TD2: 31 -TA3: 52 -TB3: 45 -supported protocols T=1 -T=0 supported: False -T=1 supported: True -checksum: 27 -\tclock rate conversion factor: 372 -\tbit rate adjustment factor: 1 -\tmaximum programming current: 25 -\tprogramming voltage: 5 -\tguard time: 255 -nb of interface bytes: 6 -nb of historical bytes: 3 -""" + data_out = textwrap.dedent( + """\ + TB1: 0 + TC1: ff + TD1: 81 + TD2: 31 + TA3: 52 + TB3: 45 + supported protocols T=1 + T=0 supported: False + T=1 supported: True + checksum: 27 + \tclock rate conversion factor: 372 + \tbit rate adjustment factor: 1 + \tmaximum programming current: 25 + \tprogramming voltage: 5 + \tguard time: 255 + nb of interface bytes: 6 + nb of historical bytes: 3 + """ + ) a = ATR(atr) - a.dump() + with pytest.warns(DeprecationWarning, match=re.escape("print(ATR.render())")): + a.dump() stdout, _ = capsys.readouterr() assert stdout == data_out def test_atr6(capsys): atr = [0x3B, 0xE5, 0x00, 0x00, 0x81, 0x21, 0x45, 0x9C, 0x10, 0x01, 0x00, 0x80, 0x0D] - data_out = """TB1: 0 -TC1: 0 -TD1: 81 -TD2: 21 -TB3: 45 -supported protocols T=1 -T=0 supported: False -T=1 supported: True -checksum: 13 -\tclock rate conversion factor: 372 -\tbit rate adjustment factor: 1 -\tmaximum programming current: 25 -\tprogramming voltage: 5 -\tguard time: 0 -nb of interface bytes: 5 -nb of historical bytes: 5 -""" + data_out = textwrap.dedent( + """\ + TB1: 0 + TC1: 0 + TD1: 81 + TD2: 21 + TB3: 45 + supported protocols T=1 + T=0 supported: False + T=1 supported: True + checksum: 13 + \tclock rate conversion factor: 372 + \tbit rate adjustment factor: 1 + \tmaximum programming current: 25 + \tprogramming voltage: 5 + \tguard time: 0 + nb of interface bytes: 5 + nb of historical bytes: 5 + """ + ) a = ATR(atr) - a.dump() + with pytest.warns(DeprecationWarning, match=re.escape("print(ATR.render())")): + a.dump() stdout, _ = capsys.readouterr() assert stdout == data_out -def test_atr_ts(): - atr = [0x42] - with pytest.raises(SmartcardException): +@pytest.mark.parametrize( + "ts", + ( + pytest.param("0x42", id="numeric"), + pytest.param("0xaa", id="lowercase"), + pytest.param("0x00", id="zero padding"), + ), +) +def test_invalid_ts(ts: str): + atr = [int(ts[2:], 16), 0x00] + with pytest.raises(SmartcardException, match=f"invalid TS {ts}"): ATR(atr) @@ -172,3 +206,210 @@ def test_map_lengths(field, expected_length): """ assert len(getattr(ATR, field)) == expected_length + + +@pytest.mark.parametrize( + "atr,", + ( + pytest.param([], id="ATR is too short (0 bytes)"), + pytest.param([0x3B], id="ATR is too short (1 byte, valid TS)"), + ), +) +def test_invalid_atr_lengths(atr: list[int]): + """Verify that short ATRs raise exceptions.""" + + with pytest.raises(SmartcardException, match="at least 2 bytes"): + ATR(atr) + + +@pytest.mark.parametrize("ts", (0x3B, 0x3F)) +def test_2_bytes(ts): + """Verify that a completely empty ATR parses well.""" + + atr = ATR([ts, 0b0000_0000]) + # |||| `-- no historical bytes + # |||`-- no TA + # ||`-- no TB + # |`-- no TC + # `-- no TD + assert atr.getTA1() is None + assert atr.getTB1() is None + assert atr.II is None + assert atr.PI1 is None + assert atr.getTC1() is None + assert atr.getTD1() is None + assert atr.getChecksum() is None + assert atr.getGuardTime() is None + assert atr.getHistoricalBytesCount() == 0 + assert atr.getHistoricalBytes() == [] + assert atr.getInterfaceBytesCount() == 0 + + # Default values + assert atr.getBitRateFactor() == 1 + assert atr.getClockRateConversion() == 372 + assert atr.getProgrammingCurrent() == 50 + assert atr.getProgrammingVoltage() == 5 + + # Protocols + assert len(atr.getSupportedProtocols()) == 1 + assert "T=0" in atr.getSupportedProtocols() + assert atr.isT0Supported() is True + assert atr.isT1Supported() is False + assert atr.isT15Supported() is False + + # Rendering + expected_rendering = textwrap.dedent( + """\ + supported protocols T=0 + T=0 supported: True + T=1 supported: False + \tclock rate conversion factor: 372 + \tbit rate adjustment factor: 1 + \tmaximum programming current: 50 + \tprogramming voltage: 5 + \tguard time: None + nb of interface bytes: 0 + nb of historical bytes: 0 + """.rstrip() + ) + assert atr.render() == expected_rendering + + # Warnings + with pytest.warns(DeprecationWarning, match="ATR.TA"): + assert atr.hasTA == [False] + with pytest.warns(DeprecationWarning, match="ATR.TB"): + assert atr.hasTB == [False] + with pytest.warns(DeprecationWarning, match="ATR.TC"): + assert atr.hasTC == [False] + with pytest.warns(DeprecationWarning, match="ATR.TD"): + assert atr.hasTD == [False] + + +def test_only_ta1(): + """Verify that TA1 can be conveyed standalone.""" + + atr = ATR([0x3B, 0b0001_0000, 0xA7]) + # `-- only enable TA + assert atr.TA == [0xA7] + assert "TA1: a7\n" in atr.render() + with pytest.warns(DeprecationWarning, match="ATR.TA"): + assert atr.hasTA == [True] + # TA1 affects these values + assert atr.getClockRateConversion() == 768 + assert atr.getBitRateFactor() == 64 + # Sanity check + assert atr.TB == atr.TC == atr.TD == [None] + assert atr.N is None + assert atr.getInterfaceBytesCount() == 1 + assert atr.getHistoricalBytesCount() == 0 + assert atr.hasChecksum is False + assert atr.checksumOK is None + assert atr.getChecksum() is None + + +def test_only_tb1(): + """Verify that TB1 can be conveyed standalone. + + TB1 and TB2 are deprecated in ISO 7816-3 2006, so no values are checked here. + """ + + atr = ATR([0x3B, 0b0010_0000, 0b0_10_11111]) + # `-- only enable TB + assert atr.TB == [0b0_10_11111] + assert "TB1: 5f\n" in atr.render() + with pytest.warns(DeprecationWarning, match="ATR.TB"): + assert atr.hasTB == [True] + # TB1 affects these values + assert atr.II == 0b10 + assert atr.PI1 == 0b11111 + assert atr.getProgrammingVoltage() != 5 + assert atr.getProgrammingCurrent() != 50 + # Sanity check + assert atr.TA == atr.TC == atr.TD == [None] + assert atr.N is None + assert atr.getInterfaceBytesCount() == 1 + assert atr.getHistoricalBytesCount() == 0 + assert atr.hasChecksum is False + assert atr.checksumOK is None + assert atr.getChecksum() is None + + +def test_only_tc1(): + """Verify that TC1 can be conveyed standalone.""" + + atr = ATR([0x3B, 0b0100_0000, 0xC1]) + # `-- only enable TC + assert atr.TC == [0xC1] + assert "TC1: c1\n" in atr.render() + with pytest.warns(DeprecationWarning, match="ATR.TC"): + assert atr.hasTC == [True] + # TC1 affects these values + assert atr.N == 0xC1 + # Sanity check + assert atr.TA == atr.TB == atr.TD == [None] + assert atr.getInterfaceBytesCount() == 1 + assert atr.getHistoricalBytesCount() == 0 + assert atr.hasChecksum is False + assert atr.checksumOK is None + assert atr.getChecksum() is None + + +def test_only_td1(): + """Verify that TD1 can be conveyed standalone.""" + + atr = ATR([0x3B, 0b1000_0000, 0x00]) + # `-- only enable TD + assert atr.TD == [0x00, None] + assert atr.isT0Supported() is True + assert atr.isT1Supported() is False + assert atr.isT15Supported() is False + assert "TD1: 0\n" in atr.render() + with pytest.warns(DeprecationWarning, match="ATR.TD"): + assert atr.hasTD == [True, False] + # Sanity check + assert atr.TA == atr.TB == atr.TC == [None, None] + assert atr.N is None + assert atr.getHistoricalBytesCount() == 0 + assert atr.hasChecksum is False + assert atr.checksumOK is None + assert atr.getChecksum() is None + + +def test_historical_bytes(): + """Verify that historical bytes can be conveyed standalone.""" + + atr = ATR([0x3B, 0x0F, *list(range(15))]) + # `-- indicate 15 historical bytes + assert atr.K == 15 + assert atr.getHistoricalBytesCount() == 15 + assert atr.getHistoricalBytes() == list(range(15)) + # Sanity check + assert atr.TA == atr.TB == atr.TC == atr.TD == [None] + assert atr.N is None + assert atr.hasChecksum is False + assert atr.checksumOK is None + assert atr.getChecksum() is None + + +@pytest.mark.parametrize("ts", (0x3B, 0x3F)) +@pytest.mark.parametrize("atr_bytes", ([0x00, 0x00], [0x1, 0xFE, 0xFF])) +def test_valid_checksums(ts, atr_bytes): + """Verify behavior of valid checksums.""" + + atr = ATR([ts] + atr_bytes) + assert atr.hasChecksum is True + assert atr.checksumOK is True + assert atr.getChecksum() == atr_bytes[-1] + assert f"checksum: {atr_bytes[-1]}\n" in atr.render() + + +@pytest.mark.parametrize("ts", (0x3B, 0x3F)) +@pytest.mark.parametrize("atr_bytes", ([0x00, 0x01], [0x01, 0xFE, 0x0])) +def test_invalid_checksums(ts, atr_bytes): + """Verify behavior of invalid checksums.""" + + atr = ATR([ts] + atr_bytes) + assert atr.hasChecksum is True + assert atr.checksumOK is False + assert atr.getChecksum() == atr_bytes[-1] + assert f"checksum: {atr_bytes[-1]:x}\n" in atr.render()