""" File: vde-ar-e-2532-100_read.py Author: springcard/johann.d Created: 2025-07-30 Description: Read (and try to verify) an EV-charging card compliant with standard VDE-AR-E 2532-100:2021 It has been tested with Mifare DuoX/EV cards only. This script is a free sample developed by SpringCard for demonstration purposes only. It is provided "as is", without support and without any warranty of any kind. Use it at your own risk. License: MIT License (see LICENSE file for details) Copyright (c) 2025 SpringCard SAS, France Dependencies: - Python 3.13+ - pyscard module (pip install pyscard) - cryptography module (pip install cryptography) Usage: Place a compliant EV-charging card on a SpringCard NFC/RFID HF PC/SC Coupler and run pip duox_vde-ar-e-2532-100_read.py """ import os import base64 from smartcard.scard import * import smartcard.util from cryptography import x509 from cryptography.hazmat.primitives import hashes, serialization from cryptography.hazmat.primitives.asymmetric import rsa, ec from cryptography.hazmat.primitives.serialization import load_pem_public_key from cryptography.hazmat.primitives.asymmetric.utils import encode_dss_signature from cryptography.hazmat.primitives.asymmetric.ec import EllipticCurvePublicNumbers, BrainpoolP256R1 from cryptography.hazmat.backends import default_backend from cryptography.x509 import load_der_x509_certificate from cryptography.exceptions import InvalidSignature """ ISO/IEC 7816-4 APDUs used to communicate with the card """ VDE_SELECT_APPLICATION = [0x00, 0xA4, 0x04, 0x00, 0x10, 0xA0, 0x00, 0x00, 0x08, 0x45, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00] VDE_READ_DATA_00 = [0x80, 0x02, 0x00, 0x00, 0x00] VDE_READ_DATA_01 = [0x80, 0x02, 0x01, 0x00, 0x00] VDE_ECDSA_SIGN_HEADER = [0x80, 0x03, 0x0C, 0x09, 0x20] VDE_ECDSA_SIGN_FOOTER = [0x00] """ PC/SC pseudo-APDU to get the card's protocole level ID """ GET_CARD_UID = [0xFF, 0xCA, 0x00, 0x00, 0x00] """ Quick'n'dirty ASN.1 DER parser """ def parse_tlv(data): i = 0 tlvs = {} raw_fields = {} while i < len(data): tag_start = i tag = data[i] i += 1 if tag in (0x5F, 0x7F): tag = (tag << 8) | data[i] i += 1 length = data[i] i += 1 if length & 0x80: # Long-form length num_len_bytes = length & 0x7F length = int.from_bytes(data[i:i+num_len_bytes], 'big') i += num_len_bytes value = data[i:i+length] i += length tlvs[tag] = value raw_fields[tag] = data[tag_start:i] return tlvs, raw_fields """ Take a BCD date and translate it to the YYYY-MM-DD format """ def bcd_to_date(b): return f"{b[0]>>4}{b[0]&0x0F}{b[1]>>4}{b[1]&0x0F}-{b[2]>>4}{b[2]&0x0F}-{b[3]>>4}{b[3]&0x0F}" """ Process the certificate of the VDE card Return the card UID of stored in the certificate, the public key of the card, and a boolean telling whether the certificate has been validated over a CA or not The certificate of the CA(s) are supposed to be available in the current directory (see Mifare DuoX datasheet to know how to retrieve their CA certificates). """ def process_certificate(certificate: bytes): """ Verify the root TLV """ tlvs, raw = parse_tlv(certificate) root = tlvs.get(0x7F21) if root is None: raise Exception("Not a valid ASN.1 GP certificate") """ Dig under the root TLV """ tlvs, raw = parse_tlv(root) serial_number = tlvs.get(0x93) if serial_number is None: raise Exception("Serial Number entry not found") print(f"Serial Number: " + serial_number.hex().upper()) ca_id = tlvs.get(0x42) if ca_id is None: raise Exception("CA-ID entry not found") print(f"CA-ID: " + ca_id.hex().upper()) card_uid = tlvs.get(0x5F20) # Tag is 5F20 as written in VDE standard, not 5720 as written in Mifare DuoX datasheet if card_uid is None: raise Exception("Card UID entry not found") print("Card UID: " + card_uid.hex().upper()) date_begin = tlvs.get(0x5F25) date_end = tlvs.get(0x5F24) if date_begin is not None and date_end is not None: print("Validity: " + bcd_to_date(date_begin) + " to " + bcd_to_date(date_end)) public_key = tlvs.get(0x7F49) if public_key is None: raise Exception("Public Key entry not found") print("Public Key of the Card: " + public_key.hex().upper()) if len(public_key) != 70: raise Exception("Length of Public Key is invalid") signature = tlvs.get(0x5F37) print("Signature of the Certificate: " + signature.hex().upper()) if len(signature) != 64: raise Exception("Length of Signature is invalid") """ Do we have the certificate of the CA? If so, try to verify the signature """ certificate_valid = False ca_certificate_file = ca_id.hex().upper() + ".crt" if os.path.isfile(ca_certificate_file): with open(ca_certificate_file, "rb") as f: ca_certificate = load_der_x509_certificate(f.read(), default_backend()) ca_public_key = ca_certificate.public_key() print("Verifying the Certificate of the Card against the Public Key of the CA") """ The signed data include the tags 0x93 until and including 0x7F49 """ signed_tags = [0x93, 0x42, 0x5F20, 0x95, 0x5F25, 0x5F24, 0x45, 0x7F49] signed_data = b''.join(raw[tag] for tag in signed_tags if tag in raw) """ Prepare the verification """ r = int.from_bytes(signature[:32], byteorder='big') s = int.from_bytes(signature[32:], byteorder='big') signature_der = encode_dss_signature(r, s) print("Signature (DER): " + signature_der.hex().upper()) """ Verify the signature """ try: ca_public_key.verify( signature_der, signed_data, ec.ECDSA(hashes.SHA256())) print("The Signature of the Card's Certificate by the CA is OK") certificate_valid = True except InvalidSignature: print("ERROR: The Signature of the Card's Certificate by the CA is invalid") else: print("WARNING: The Certificate of the CA is not available, skipping the verification of the Card's Certificate") if serial_number.hex().upper() != card_uid.hex().upper(): print("WARNING: The Serial Number of the Card's Certificate does not match the Subject (Card's UID)") return card_uid, public_key, certificate_valid """ Verify the signature returned by the VDE card in response to our challenge """ def verify_signature_of_challenge(challenge: bytes, signature: bytes, public_key: bytes): print("Verifying the Signature of the Challenge") print("\tRandom Challenge: " + challenge.hex().upper()) print("\tSignature computed by the Card: " + signature.hex().upper()) print("\tPublic Key of the the Card: " + public_key.hex().upper()) """ Dig under the signature TLV """ tlvs, raw = parse_tlv(public_key) public_key_bytes = tlvs.get(0xB0) if public_key_bytes is None: raise Exception("Public Key does not have the B0 TLV"); public_key_curve = tlvs.get(0xF0) if public_key_curve is None: raise Exception("Public Key does not have the F0 TLV"); """ Verify the parameters """ if len(public_key_bytes) != 65: raise Exception("Length of Public Key is invalid") if public_key_bytes[0] != 0x04: raise Exception("Format of Public Key is invalid") if public_key_curve[0] != 0x03: raise Exception("Curve of Public Key is not Brainpool P256r1") if len(signature) != 64: raise Exception("Length of Signature is invalid") """ Prepare the verification """ curve = BrainpoolP256R1() x = int.from_bytes(public_key_bytes[1:33], byteorder='big') y = int.from_bytes(public_key_bytes[33:65], byteorder='big') public_numbers = EllipticCurvePublicNumbers(x, y, curve) public_key = public_numbers.public_key() r = int.from_bytes(signature[:32], byteorder='big') s = int.from_bytes(signature[32:], byteorder='big') signature_der = encode_dss_signature(r, s) """ Verify the signature """ signature_valid = False try: public_key.verify( signature_der, challenge, ec.ECDSA(hashes.SHA256()) ) print("The Signature of the Challenge by Card is OK") signature_valid = True except InvalidSignature: print("ERROR: The Signature of the Challenge by Card is invalid") return signature_valid """ Format the data as specified in the VDE standard """ def format_backend_data(challenge: bytes, certificate: bytes, metadata: bytes, signature: bytes): message = "{\n" message = message + "\t\"randomNumber\": \"" + base64.b64encode(challenge).decode('utf-8') + "\",\n" message = message + "\t\"certificate\": \"" + base64.b64encode(certificate).decode('utf-8') + "\",\n" message = message + "\t\"signature\": \"" + base64.b64encode(signature).decode('utf-8') + "\",\n" if all(b == 0 for b in METADATA): message = message + "\t\"metaData\": \"\"\n" # All bytes are 00 else: message = message + "\t\"metaData\": \"" + base64.b64encode(metadata).decode('utf-8') + "\"\n" message = message + "}" return message """ APDU exchange """ def transmit_apdu(hcard, protocol, command, label=""): print(f"{label} command:\n\t{bytes(command).hex().upper()}") hresult, response = SCardTransmit(hcard, protocol, command) if hresult != SCARD_S_SUCCESS: raise Exception(f"Failed to transmit {label}: " + SCardGetErrorMessage(hresult)) sw = (response[-2] << 8) + response[-1] print(f"{label} response:\n\t{bytes(response).hex().upper()} (SW={sw:04X})") return bytes(response[:-2]), sw """ Main function """ try: hresult, hcontext = SCardEstablishContext(SCARD_SCOPE_USER) if hresult != SCARD_S_SUCCESS: raise Exception("Failed to establish context : " + SCardGetErrorMessage(hresult)) print("Context established!") try: hresult, readers = SCardListReaders(hcontext, []) if hresult != SCARD_S_SUCCESS: raise Exception("Failed to list readers: " + SCardGetErrorMessage(hresult)) if len(readers) < 1: raise Exception("No smart card readers") contactless_reader = "" print("PCSC Readers:") for reader in readers: print("\t" + reader) if "springcard" in reader.lower(): if (" contactless " in reader.lower()) or (" nfc " in reader.lower()): contactless_reader = reader if contactless_reader == "": raise Exception("SpringCard contactless reader not found") print("Using reader: " + contactless_reader) CERTIFICATE = None SIGNATURE = None try: hresult, hcard, dwActiveProtocol = SCardConnect(hcontext, contactless_reader, SCARD_SHARE_SHARED, SCARD_PROTOCOL_T0 | SCARD_PROTOCOL_T1) if hresult != SCARD_S_SUCCESS: raise Exception("Unable to connect: " + SCardGetErrorMessage(hresult)) print(f"Connected with active protocol={dwActiveProtocol}") print() try: CARD_UID, SW = transmit_apdu(hcard, dwActiveProtocol, GET_CARD_UID, "GetCardUid") if SW != 0x9000: raise Exception("GetCardUid failed") response, SW = transmit_apdu(hcard, dwActiveProtocol, VDE_SELECT_APPLICATION, "Select VDE Application") if SW != 0x9000: raise Exception("Card does not seem to be a VDE EV-charging card") print("Card seems to be a VDE EV-charging card") CERTIFICATE, SW = transmit_apdu(hcard, dwActiveProtocol, VDE_READ_DATA_00, "ReadData(0) [Certificate]") if SW != 0x9000: raise Exception("ReadData(0) failed") METADATA, SW = transmit_apdu(hcard, dwActiveProtocol, VDE_READ_DATA_01, "ReadData(1) [MetaData]") if SW != 0x9000: raise Exception("ReadData(1) failed") """ generate a random challenge """ CHALLENGE = list(os.urandom(32)) VDE_ECDSA_SIGN = VDE_ECDSA_SIGN_HEADER + CHALLENGE + VDE_ECDSA_SIGN_FOOTER CHALLENGE = bytes(CHALLENGE) SIGNATURE, SW = transmit_apdu(hcard, dwActiveProtocol, VDE_ECDSA_SIGN, "ECDSASign(Challenge)") if SW != 0x9000: raise Exception("ECDSASign failed") finally: hresult = SCardDisconnect(hcard, SCARD_UNPOWER_CARD) if hresult != SCARD_S_SUCCESS: raise Exception("Failed to disconnect: " + SCardGetErrorMessage(hresult)) print("Disconnected") print() except Exception as e: print("Exception:", e) try: if CERTIFICATE is not None and SIGNATURE is not None: print("Verifying that the EV card is genuine...") try: card_uid, public_key, certificate_valid = process_certificate(CERTIFICATE) except Exception as e: raise Exception(e) print() try: signature_valid = verify_signature_of_challenge(CHALLENGE, SIGNATURE, public_key) except Exception as e: raise Exception(e) print() card_uid_valid = False if card_uid.hex().upper() == CARD_UID.hex().upper(): card_uid_valid = True if card_uid_valid and certificate_valid and signature_valid: print("Good news: the Card is perfectly genuine, with UID=" + CARD_UID.hex().upper()) else: print("Bad news: we have some concerns with this Card") if not certificate_valid: print("\tThe Certificate of the Card has not been validated against a CA Certificate") if not card_uid_valid: print("\tThe Certificate seems to belong to a different Card") if not signature_valid: print("\tThe Card has been unabled to sign our Challenge") print() message = format_backend_data(CHALLENGE, CERTIFICATE, METADATA, SIGNATURE) print() print("Here's the data of the message to send to the backend:") print() print(message) print() except Exception as e: print("Exception:", e) finally: hresult = SCardReleaseContext(hcontext) if hresult != SCARD_S_SUCCESS: raise Exception("Failed to release context: " + SCardGetErrorMessage(hresult)) print("Released context.") except Exception as e: print("Exception:", e) import sys if "win32" == sys.platform: print("Press Enter to continue") sys.stdin.read(1)