Attack Scenario: Protocol Namespace Downgrade (ISO 15118-2 → DIN SPEC 70121)
Agenda
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 5: Certificate Validation
Part 6: Protocol Namespace Downgrade ← you 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:
- SDP TLS downgrade. The SECC advertises Security: NO_TLS in its SDP response, and the EVCC accepts it without complaint.
- 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.