Insights Blog Attack Scenario: Protocol Namespace ...

Attack Scenario: Protocol Namespace Downgrade (ISO 15118-2 → DIN SPEC 70121)

Written by Mahmoud Jadaan
Agenda
Series: V2G Security Research

Part 1: EV Charging & the V2G Protocol Stack - threat model and protocol overview

Part 2: Lab Environment: Docker-based V2G Security Lab - setting up the test environment

Part 3: TLS Downgrade

Part 4: SDP Response Spoofing

Part 5: Certificate Validation

Part 6: Protocol Namespace Downgradeyou are here

 

Having confirmed in Part 5 that the EVCC correctly enforces certificate trust, this article moves one layer down the handshake: the application-protocol negotiation that happens before any V2G messages are exchanged. The question here is whether a rogue SECC can push the session from ISO 15118-2 onto the older DIN SPEC 70121, stripping TLS and message signatures in one step.

Standard requirement (ISO 15118-2, §8.3 / SupportedAppProtocol): Both the EVCC and SECC exchange a supportedAppProtocolReq / Res handshake immediately after the TCP connection is established. The SECC selects the highest-priority protocol that both sides support. If no common protocol exists, the SECC must respond with ResponseCode: Failed_NoNegotiation and the EVCC must terminate. A conformant EVCC that lists ISO 15118-2 as its only supported protocol should never fall back to DIN.

Why DIN SPEC 70121 is less secure: DIN was designed before security was a hard requirement; it predates the ISO 15118-2 mandate for TLS and has no provision for contract-based authentication or signed messages. Any session that falls back to DIN loses all of those protections silently.

Feature ISO 15118-2 DIN SPEC 70121
TLS mandatory Yes (§8.7.3) No
Plug&Charge (contract certs) Yes No
XML digital signatures Yes No
Mutual authentication Yes No

 

Attack Model

The attack targets the SupportedAppProtocol (SAP) handshake that runs immediately after the TCP connection opens. If the EVCC lists both ISO 15118-2 and DIN SPEC 70121 in its SAP request, as many real-world EVs do for backward compatibility with older charging stations, a rogue SECC can reply with OK_SuccessfulNegotiation and the SchemaID that maps to the DIN entry. The EVCC sees a match, switches its state machine to the DIN namespace, and the session continues without TLS, Plug&Charge, or message signatures.

 

Attack Setup

Our lab EVCC (Josev) ships with evcc/config/baseline.json, which lists only ISO_15118_2. To reproduce the downgrade, we add a second EVCC config, evcc/config/attack3_din_fallback.json, that mirrors a real-world EV offering DIN SPEC 70121 as a backward-compatibility fallback for older charging stations:

{
  "supportedProtocols": [
    "ISO_15118_2",
    "DIN_SPEC_70121"
  ],
  "supportedEnergyServices": [
    "AC"
  ],
  "isCertInstallNeeded": false,
  "useTls": true,
  "chargeLoopCycle": 10
}

Note that useTls is still true, the EVCC still requests TLS in its SDP request. Josev also has code that is supposed to strip DIN_SPEC_70121 from the SAP request whenever TLS is in use, so on paper this EVCC still looks protected. As the traffic analysis below shows, that guard depends on the outcome of the SDP exchange, not on this static setting, and it does not fire in this lab.

The SECC side uses the same DIN-only configuration as before, secc/config/attack3_din_only.env:

PROTOCOLS=DIN_SPEC_70121 # ← SECC advertises only DIN in supportedAppProtocolRes
AUTH_MODES=EIM
SECC_ENFORCE_TLS=False # <-- no TLS
LOG_LEVEL=DEBUG
MESSAGE_LOG_JSON=True

Steps

Step 1: Build the EVCC image with the fallback config

evcc/config/attack3_din_fallback.json is baked into the EVCC image at build time (see evcc/Dockerfile), so rebuild it once after adding the file:

# All commands must be run from the project root.
docker compose build evcc

Step 2: Clean up any previous run

docker compose down -v
docker rm -f attacker 2>/dev/null || true

Step 3: Start the SECC and EVCC containers

Export both scenario names and bring the containers up. The EVCC has a built-in 2-second startup delay to ensure the SECC is ready before the first SDP multicast is sent:

export SECC_SCENARIO=attack3_din_only
export EVCC_SCENARIO=attack3_din_fallback
docker compose up secc evcc

Watch the compose logs scroll by, you will see the SDP exchange, the SAP handshake, and the DIN session messages in real time.

Chosen DIN session

Fixed addresses: static MAC addresses in docker-compose.yml give both containers deterministic link-local addresses:

SECC → fe80::ff:fe00:1 (MAC 02:00:00:00:00:01)
EVCC → fe80::ff:fe00:2 (MAC 02:00:00:00:00:02)

Step 4: Collect the traffic capture

Once both containers have exited, the PCAPs land in ./captures/attack3_din_only/:

captures/
  attack3_din_only/
    secc_<timestamp>.pcap
    evcc_<timestamp>.pcap

Open the SECC capture in Wireshark. To focus on the V2G exchange, apply this display filter:

udp.port == 15118 || tcp.port == 15118 || v2gmsg

 

Traffic Analysis

The following walkthrough traces the actual packet exchange captured in captures/attack3_din_only/.

Participants:

Role IPv6 Link-Local Port
EVCC fe80::ff:fe00:2 -
SECC fe80::ff:fe00:1 59432

Step 1: SDP Exchange, TLS downgraded

Before protocol negotiation even begins, the SECC downgrades transport security, the same behaviour observed in TLS Downgrade.

EVCC SDPRequest: Security: 0x00 → "Secured with TLS"
SECC SDPResponse: Security: 0x10 → "No transport layer security"
SECC Port: 59432

No TLS session

The EVCC accepts the no-TLS SDP response and opens a plain TCP connection, no TLS ClientHello is ever sent. This is the moment that matters most: it determines the runtime TLS state the SAP handshake will inherit.

Step 2: SupportedAppProtocol Request (EVCC → SECC), both protocols offered

After the TCP connection opens, the EVCC sends its supportedAppProtocolReq with both protocols listed:

{
  "supportedAppProtocolReq": {
    "AppProtocol": [
      {
        "ProtocolNamespace": "urn:din:70121:2012:MsgDef",
        "VersionNumberMajor": 2,
        "VersionNumberMinor": 0,
        "SchemaID": 1,
        "Priority": 1
      },
      {
        "ProtocolNamespace": "urn:iso:15118:2:2013:MsgDef",
        "VersionNumberMajor": 2,
        "VersionNumberMinor": 0,
        "SchemaID": 2,
        "Priority": 2
      }
    ]
  }
}

Two protocols offered

Even though the EVCC config has useTls: true, the DIN entry (urn:din:70121:2012:MsgDef) is present in the request.

Josev’s code-level guard that is supposed to strip DIN_SPEC_70121 whenever TLS is in use did not fire here, because by this point the EVCC’s live is_tls flag already reflects the no-TLS outcome from Step 1, not the static config value.

Step 3: SupportedAppProtocol Response (SECC → EVCC), DIN selected

The SECC, configured with PROTOCOLS=DIN_SPEC_70121, matches the DIN entry and accepts the negotiation:

{
  "supportedAppProtocolRes": {
    "ResponseCode": "OK_SuccessfulNegotiation",
    "SchemaID": 1
  }
}

DIN session established

Step 4: Session proceeds entirely on DIN SPEC 70121, over plain TCP

All subsequent messages travel in the DIN namespace (urn:din:70121:2012:MsgDef) over the plain TCP socket opened in Step 1, with no TLS ClientHello, no contract certificate request, and no XML signature anywhere in the exchange.

DIN message exchange

Step 5: Session ends on an unrelated mismatch, not a security check

evcc log:
ServiceDiscoveryRes received
Reason: Offered energy transfer modes [DC_extended] not compatible with AC_three_phase_core
The data link will terminate in 2 seconds; TCP connection will close in 5 seconds.

The session tears down here only because our lab EVCC is configured for AC charging while the DIN-only SECC advertises DC_extended. This is a lab configuration detail, not a security control, the downgrade itself is already complete by this point. A DC-capable EVCC, or a SECC advertising a matching energy mode, would proceed straight into PaymentServiceSelection and the charging loop, still entirely unauthenticated and unencrypted.

 

Attack Success Results

  Expected (conformant EVCC) Observed
SDP: EVCC requests TLS Security: 0x00 ✓ Security: 0x00 ✓
SDP: SECC downgrades to no-TLS EVCC aborts EVCC silently accepts (TLS Downgrade)
SAP: EVCC offers DIN as fallback Stripped once TLS is lost ❌ DIN entry retained in request
SAP: SECC selects DIN EVCC should reject ❌ OK_SuccessfulNegotiation, DIN chosen
Session namespace Should stay ISO 15118-2 urn:din:70121:2012:MsgDef
TLS / Plug&Charge / signatures Preserved Lost, plain TCP, EIM only, no XML signatures

 

Finding

The protocol downgrade attack succeeds once the EVCC is configured to offer DIN_SPEC_70121 as a fallback alongside ISO_15118_2, which mirrors how many real-world EVs are configured for backward compatibility with legacy charging stations.

The attack works because two independent weaknesses chain together:

  1. SDP TLS downgrade. The SECC advertises Security: NO_TLS in its SDP response, and the EVCC accepts it without complaint.
  2. The DIN-removal guard only fires when TLS actually held. Josev’s EVCC has code that is supposed to strip DIN_SPEC_70121 from the SAP request whenever TLS is in use:
# iso15118/evcc/comm_session_handler.py
if self.is_tls:
    supported_protocols.remove(Protocol.DIN_SPEC_70121)

But self.is_tls is not the static useTls: true config value, it is overwritten with whatever the SDP exchange actually negotiated (comm_session.is_tls = secc_signals_tls). Because the SDP response already downgraded the session to no-TLS, self.is_tls is False by the time the SAP request is built, and the guard never fires. The DIN_SPEC_70121 entry survives in the SAP request purely as a side effect of Weakness 1.

With DIN present in the request, the attacker SECC selects it (OK_SuccessfulNegotiation, SchemaID: 1) and the EVCC switches its entire state machine to the DIN namespace. SessionSetupReq/Res and ServiceDiscoveryReq/Res all complete over plain TCP, no TLS, no Plug&Charge, no XML signatures. In our lab run the session terminated shortly after on an unrelated energy-mode mismatch (AC vs. DC), the downgrade itself was already complete before that point.

This shows that the risk is not in the SAP handshake logic itself, it correctly selects a mutually supported protocol, but in the fact that it inherits its trust decision from the already-vulnerable SDP exchange. Fixing the vulnerability described in TLS Downgrade would, as a side effect, also close this protocol-namespace downgrade path.