110 lines
3.8 KiB
Python
110 lines
3.8 KiB
Python
"""
|
|
Applet validation profiles for the j3r180 batch provisioning tool.
|
|
|
|
Each profile defines how to validate a specific applet after installation.
|
|
Profiles are matched to CAP files automatically via their applet AID.
|
|
|
|
To add a new profile:
|
|
1. Subclass AppletProfile
|
|
2. Implement name and validate()
|
|
3. Register it in APPLET_PROFILES keyed by the applet AID (hex string)
|
|
"""
|
|
|
|
from abc import ABC, abstractmethod
|
|
|
|
from smartcard.Exceptions import CardConnectionException, NoCardException
|
|
from smartcard.util import toHexString
|
|
|
|
|
|
class AppletProfile(ABC):
|
|
"""Base class for applet-specific validation profiles."""
|
|
|
|
@property
|
|
@abstractmethod
|
|
def name(self) -> str:
|
|
"""Human-readable name for logging/display."""
|
|
...
|
|
|
|
@abstractmethod
|
|
def validate(self, connection) -> dict[str, str]:
|
|
"""
|
|
Validate the applet on a live pyscard connection.
|
|
|
|
Must return a dict of named results:
|
|
{"installed": "yes", "public_key": "04AB..."} -- success
|
|
{"installed": "no", "error": "SELECT failed"} -- failure
|
|
|
|
Must NOT raise exceptions.
|
|
"""
|
|
...
|
|
|
|
|
|
class DefaultProfile(AppletProfile):
|
|
"""Fallback profile for CAPs without a registered profile. SELECT-only check."""
|
|
|
|
def __init__(self, aid: list[int]):
|
|
self._aid = aid
|
|
self._name = f"Applet {bytes(aid).hex().upper()}"
|
|
|
|
@property
|
|
def name(self) -> str:
|
|
return self._name
|
|
|
|
def validate(self, connection) -> dict[str, str]:
|
|
select_apdu = [0x00, 0xA4, 0x04, 0x00, len(self._aid)] + self._aid
|
|
try:
|
|
response, sw1, sw2 = connection.transmit(select_apdu)
|
|
except (CardConnectionException, NoCardException):
|
|
return {"installed": "no", "error": "Card communication error"}
|
|
if (sw1, sw2) == (0x90, 0x00):
|
|
return {"installed": "yes"}
|
|
return {"installed": "no", "error": f"SELECT failed (SW={sw1:02X}{sw2:02X})"}
|
|
|
|
|
|
class TeslaVCSEC(AppletProfile):
|
|
"""Profile for the Tesla VCSEC identity applet (TeslaIdent.cap)."""
|
|
|
|
PACKAGE_AID = [0xF4, 0x65, 0x73, 0x6C, 0x61, 0x4C, 0x6F, 0x67, 0x69, 0x63]
|
|
GET_PUBLIC_KEY_APDU = [0x80, 0x04, 0x00, 0x00, 0x00]
|
|
|
|
@property
|
|
def name(self) -> str:
|
|
return "Tesla VCSEC"
|
|
|
|
def validate(self, connection) -> dict[str, str]:
|
|
select_apdu = [0x00, 0xA4, 0x04, 0x00, len(self.PACKAGE_AID)] + self.PACKAGE_AID
|
|
try:
|
|
response, sw1, sw2 = connection.transmit(select_apdu)
|
|
except (CardConnectionException, NoCardException):
|
|
return {"installed": "no", "error": "Card communication error during SELECT"}
|
|
|
|
if (sw1, sw2) != (0x90, 0x00):
|
|
return {"installed": "no", "error": f"SELECT failed (SW={sw1:02X}{sw2:02X})"}
|
|
|
|
try:
|
|
response, sw1, sw2 = connection.transmit(self.GET_PUBLIC_KEY_APDU)
|
|
except (CardConnectionException, NoCardException):
|
|
return {"installed": "yes", "error": "Card communication error reading public key"}
|
|
|
|
if (sw1, sw2) != (0x90, 0x00):
|
|
return {"installed": "yes", "error": f"GET PUBLIC KEY failed (SW={sw1:02X}{sw2:02X})"}
|
|
|
|
pubkey_hex = toHexString(response).replace(" ", "")
|
|
return {"installed": "yes", "public_key": pubkey_hex}
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Registry: applet AID (hex string) → profile instance
|
|
# Keys are the APPLET-level AID (as returned by extract_applet_aids()),
|
|
# not the package AID.
|
|
# ---------------------------------------------------------------------------
|
|
APPLET_PROFILES: dict[str, AppletProfile] = {
|
|
"F465736C614C6F67696330303201": TeslaVCSEC(),
|
|
}
|
|
|
|
|
|
def get_profile(aid_bytes: list[int]) -> AppletProfile | None:
|
|
"""Look up a registered profile for the given applet AID."""
|
|
aid_hex = bytes(aid_bytes).hex().upper()
|
|
return APPLET_PROFILES.get(aid_hex)
|