Skip to content

Commit be6cffd

Browse files
authored
Merge pull request #1854 from juherr/feature/ocpp16-security
Implement OCPP 1.6 Security
2 parents bca07e6 + b3982a7 commit be6cffd

File tree

134 files changed

+5373
-752
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

134 files changed

+5373
-752
lines changed

README.md

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -27,14 +27,26 @@ Electric charge points using the following OCPP versions are supported:
2727
* OCPP1.5S
2828
* OCPP1.5J
2929
* OCPP1.6S
30-
* OCPP1.6J
31-
32-
⚠️ Currently, Steve doesn't support [the OCPP-1.6 security whitepaper](https://openchargealliance.org/wp-content/uploads/2023/11/OCPP-1.6-security-whitepaper-edition-3-2.zip) yet (see [#100](https://github.com/steve-community/steve/issues/100)) and anyone can send events to a public steve instance once the chargebox id is known.
33-
Please, don't expose a Steve instance without knowing that risk.
30+
* OCPP1.6J (incl. _Security Extensions_)
3431

3532
For Charging Station compatibility please check:
3633
https://github.com/steve-community/steve/wiki/Charging-Station-Compatibility
3734

35+
---
36+
37+
#### OCPP 1.6J Security Extensions
38+
39+
SteVe has a complete implementation of [OCPP 1.6 Security Whitepaper Edition 3](https://openchargealliance.org/wp-content/uploads/2023/11/OCPP-1.6-security-whitepaper-edition-3-2.zip), providing:
40+
41+
* **Security Profiles 0-3**: Unsecured, Basic Auth, Basic Auth with server TLS, and Mutual TLS (mTLS)
42+
* **Certificate Management**: Certificate signing, installation, and deletion
43+
* **Security Events**: Real-time security event logging and monitoring
44+
* **Signed Firmware Updates**: Cryptographically signed firmware updates with certificate validation
45+
* **Diagnostic Logs**: Secure log retrieval with configurable time ranges
46+
47+
See [dedicated Wiki page](https://github.com/steve-community/steve/wiki/OCPP-1.6J-Security-Configuration) for detailed configuration guide.
48+
49+
3850
### System Requirements
3951

4052
SteVe requires
@@ -77,7 +89,7 @@ SteVe is designed to run standalone, a java servlet container / web server (e.g.
7789
- You _must_ change [the host](src/main/resources/application-prod.properties) to the correct IP address of your server
7890
- You _must_ change [web interface credentials](src/main/resources/application-prod.properties)
7991
- You _can_ access the application via HTTPS, by [enabling it and setting the keystore properties](src/main/resources/application-prod.properties)
80-
92+
8193
For advanced configuration please see the [Configuration wiki](https://github.com/steve-community/steve/wiki/Configuration)
8294
8395
4. Build SteVe:
@@ -145,9 +157,8 @@ After SteVe has successfully started, you can access the web interface using the
145157
- SOAP: `http://<your-server-ip>:<port>/steve/services/CentralSystemService`
146158
- WebSocket/JSON: `ws://<your-server-ip>:<port>/steve/websocket/CentralSystemService`
147159
148-
149160
As soon as a heartbeat is received, you should see the status of the charge point in the SteVe Dashboard.
150-
161+
151162
*Have fun!*
152163
153164
Screenshots

pom.xml

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -443,7 +443,7 @@
443443
<dependency>
444444
<groupId>com.github.steve-community</groupId>
445445
<artifactId>ocpp-jaxb</artifactId>
446-
<version>0.0.9</version>
446+
<version>0.0.12</version>
447447
</dependency>
448448
<dependency>
449449
<groupId>org.jetbrains</groupId>
@@ -569,5 +569,17 @@
569569
<artifactId>encoder-jakarta-jsp</artifactId>
570570
<version>1.3.1</version>
571571
</dependency>
572+
573+
<!-- Bouncy Castle for certificate signing (OCPP 1.6 Security) -->
574+
<dependency>
575+
<groupId>org.bouncycastle</groupId>
576+
<artifactId>bcprov-jdk18on</artifactId>
577+
<version>1.79</version>
578+
</dependency>
579+
<dependency>
580+
<groupId>org.bouncycastle</groupId>
581+
<artifactId>bcpkix-jdk18on</artifactId>
582+
<version>1.79</version>
583+
</dependency>
572584
</dependencies>
573585
</project>

src/main/java/de/rwth/idsg/steve/config/SteveProperties.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,5 +66,17 @@ public static class Ocpp {
6666
WsSessionSelectStrategyEnum wsSessionSelectStrategy;
6767
boolean autoRegisterUnknownStations;
6868
String chargeBoxIdValidationRegex;
69+
Security security = new Security();
70+
71+
@Data
72+
public static class Security {
73+
private int profile;
74+
private int certificateValidityYears;
75+
private String clientCertHeaderFromProxy;
76+
77+
public boolean requiresTls() {
78+
return profile >= 2;
79+
}
80+
}
6981
}
7082
}

src/main/java/de/rwth/idsg/steve/config/WebSocketConfiguration.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import de.rwth.idsg.steve.ocpp.ws.ocpp12.Ocpp12WebSocketEndpoint;
2424
import de.rwth.idsg.steve.ocpp.ws.ocpp15.Ocpp15WebSocketEndpoint;
2525
import de.rwth.idsg.steve.ocpp.ws.ocpp16.Ocpp16WebSocketEndpoint;
26+
import de.rwth.idsg.steve.service.CertificateValidator;
2627
import de.rwth.idsg.steve.service.ChargePointService;
2728
import de.rwth.idsg.steve.web.validation.ChargeBoxIdValidator;
2829
import lombok.RequiredArgsConstructor;
@@ -53,6 +54,7 @@ public class WebSocketConfiguration implements WebSocketConfigurer {
5354
private final Ocpp12WebSocketEndpoint ocpp12WebSocketEndpoint;
5455
private final Ocpp15WebSocketEndpoint ocpp15WebSocketEndpoint;
5556
private final Ocpp16WebSocketEndpoint ocpp16WebSocketEndpoint;
57+
private final CertificateValidator certificateValidator;
5658

5759
public static final String PATH_INFIX = "/websocket/CentralSystemService/";
5860
public static final Duration PING_INTERVAL = Duration.ofMinutes(15);
@@ -65,7 +67,8 @@ public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
6567
chargeBoxIdValidator,
6668
handshakeHandler(),
6769
Lists.newArrayList(ocpp16WebSocketEndpoint, ocpp15WebSocketEndpoint, ocpp12WebSocketEndpoint),
68-
chargePointService
70+
chargePointService,
71+
certificateValidator
6972
);
7073

7174
registry.addHandler(handshakeHandler.getDummyWebSocketHandler(), PATH_INFIX + "*")

src/main/java/de/rwth/idsg/steve/ocpp/ChargePointServiceInvokerImpl.java

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,21 +20,28 @@
2020

2121
import de.rwth.idsg.steve.ocpp.soap.ChargePointServiceSoapInvoker;
2222
import de.rwth.idsg.steve.ocpp.task.CancelReservationTask;
23+
import de.rwth.idsg.steve.ocpp.task.CertificateSignedTask;
2324
import de.rwth.idsg.steve.ocpp.task.ChangeAvailabilityTask;
2425
import de.rwth.idsg.steve.ocpp.task.ChangeConfigurationTask;
2526
import de.rwth.idsg.steve.ocpp.task.ClearCacheTask;
2627
import de.rwth.idsg.steve.ocpp.task.ClearChargingProfileTask;
2728
import de.rwth.idsg.steve.ocpp.task.DataTransferTask;
29+
import de.rwth.idsg.steve.ocpp.task.DeleteCertificateTask;
30+
import de.rwth.idsg.steve.ocpp.task.ExtendedTriggerMessageTask;
2831
import de.rwth.idsg.steve.ocpp.task.GetCompositeScheduleTask;
2932
import de.rwth.idsg.steve.ocpp.task.GetConfigurationTask;
3033
import de.rwth.idsg.steve.ocpp.task.GetDiagnosticsTask;
34+
import de.rwth.idsg.steve.ocpp.task.GetInstalledCertificateIdsTask;
3135
import de.rwth.idsg.steve.ocpp.task.GetLocalListVersionTask;
36+
import de.rwth.idsg.steve.ocpp.task.GetLogTask;
37+
import de.rwth.idsg.steve.ocpp.task.InstallCertificateTask;
3238
import de.rwth.idsg.steve.ocpp.task.RemoteStartTransactionTask;
3339
import de.rwth.idsg.steve.ocpp.task.RemoteStopTransactionTask;
3440
import de.rwth.idsg.steve.ocpp.task.ReserveNowTask;
3541
import de.rwth.idsg.steve.ocpp.task.ResetTask;
3642
import de.rwth.idsg.steve.ocpp.task.SendLocalListTask;
3743
import de.rwth.idsg.steve.ocpp.task.SetChargingProfileTask;
44+
import de.rwth.idsg.steve.ocpp.task.SignedUpdateFirmwareTask;
3845
import de.rwth.idsg.steve.ocpp.task.TriggerMessageTask;
3946
import de.rwth.idsg.steve.ocpp.task.UnlockConnectorTask;
4047
import de.rwth.idsg.steve.ocpp.task.UpdateFirmwareTask;
@@ -235,4 +242,36 @@ public void triggerMessage(ChargePointSelect cp, TriggerMessageTask task) {
235242
chargePointServiceJsonInvoker.runPipeline(cp, task);
236243
}
237244
}
245+
246+
// -------------------------------------------------------------------------
247+
// "Improved security for OCPP 1.6-J" additions. Only for JSON
248+
// -------------------------------------------------------------------------
249+
250+
public void extendedTriggerMessage(ChargePointSelect cp, ExtendedTriggerMessageTask task) {
251+
chargePointServiceJsonInvoker.runPipeline(cp, task);
252+
}
253+
254+
public void getLog(ChargePointSelect cp, GetLogTask task) {
255+
chargePointServiceJsonInvoker.runPipeline(cp, task);
256+
}
257+
258+
public void signedUpdateFirmware(ChargePointSelect cp, SignedUpdateFirmwareTask task) {
259+
chargePointServiceJsonInvoker.runPipeline(cp, task);
260+
}
261+
262+
public void installCertificate(ChargePointSelect cp, InstallCertificateTask task) {
263+
chargePointServiceJsonInvoker.runPipeline(cp, task);
264+
}
265+
266+
public void deleteCertificate(ChargePointSelect cp, DeleteCertificateTask task) {
267+
chargePointServiceJsonInvoker.runPipeline(cp, task);
268+
}
269+
270+
public void certificateSigned(ChargePointSelect cp, CertificateSignedTask task) {
271+
chargePointServiceJsonInvoker.runPipeline(cp, task);
272+
}
273+
274+
public void getInstalledCertificateIds(ChargePointSelect cp, GetInstalledCertificateIdsTask task) {
275+
chargePointServiceJsonInvoker.runPipeline(cp, task);
276+
}
238277
}

src/main/java/de/rwth/idsg/steve/ocpp/CommunicationTask.java

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232

3333
import jakarta.xml.ws.AsyncHandler;
3434
import java.util.ArrayList;
35+
import java.util.Collections;
3536
import java.util.HashMap;
3637
import java.util.List;
3738
import java.util.Map;
@@ -51,6 +52,7 @@ public abstract class CommunicationTask<S extends ChargePointSelection, RESPONSE
5152
private final String operationName;
5253
private final TaskOrigin origin;
5354
private final String caller;
55+
private final Map<String, String> customDetails; // task-specific details
5456
protected final S params;
5557

5658
private final Map<String, OcppVersion> versionMap;
@@ -70,19 +72,24 @@ public abstract class CommunicationTask<S extends ChargePointSelection, RESPONSE
7072
private final ArrayList<OcppCallback<RESPONSE>> callbackList = new ArrayList<>(2);
7173

7274
public CommunicationTask(S params) {
73-
this(params, TaskOrigin.INTERNAL, "SteVe");
75+
this(params, Collections.emptyMap());
76+
}
77+
78+
public CommunicationTask(S params, Map<String, String> customDetails) {
79+
this(params, TaskOrigin.INTERNAL, "SteVe", customDetails);
7480
}
7581

7682
/**
7783
* Do not expose the constructor, make it package-private
7884
*/
79-
CommunicationTask(S params, TaskOrigin origin, String caller) {
85+
CommunicationTask(S params, TaskOrigin origin, String caller, Map<String, String> customDetails) {
8086
List<ChargePointSelect> cpsList = params.getChargePointSelectList();
8187

8288
this.resultSize = cpsList.size();
8389
this.origin = origin;
8490
this.caller = caller;
8591
this.params = params;
92+
this.customDetails = customDetails;
8693

8794
resultMap = new HashMap<>(resultSize);
8895
versionMap = new HashMap<>(resultSize);

src/main/java/de/rwth/idsg/steve/ocpp/Ocpp15AndAboveTask.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import de.rwth.idsg.steve.web.dto.ocpp.ChargePointSelection;
2424

2525
import jakarta.xml.ws.AsyncHandler;
26+
import java.util.Map;
2627

2728
/**
2829
* @author Sevket Goekay <sevketgokay@gmail.com>
@@ -34,6 +35,10 @@ public Ocpp15AndAboveTask(S params) {
3435
super(params);
3536
}
3637

38+
public Ocpp15AndAboveTask(S params, Map<String, String> customDetails) {
39+
super(params, customDetails);
40+
}
41+
3742
@Deprecated
3843
@Override
3944
public <T extends RequestType> T getOcpp12Request() {

src/main/java/de/rwth/idsg/steve/ocpp/Ocpp16AndAboveTask.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import de.rwth.idsg.steve.web.dto.ocpp.ChargePointSelection;
2424

2525
import jakarta.xml.ws.AsyncHandler;
26+
import java.util.Map;
2627

2728
/**
2829
* @author Sevket Goekay <sevketgokay@gmail.com>
@@ -34,6 +35,10 @@ public Ocpp16AndAboveTask(S params) {
3435
super(params);
3536
}
3637

38+
public Ocpp16AndAboveTask(S params, Map<String, String> customDetails) {
39+
super(params, customDetails);
40+
}
41+
3742
@Deprecated
3843
@Override
3944
public <T extends RequestType> T getOcpp15Request() {
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
/*
2+
* SteVe - SteckdosenVerwaltung - https://github.com/steve-community/steve
3+
* Copyright (C) 2013-2025 SteVe Community Team
4+
* All Rights Reserved.
5+
*
6+
* This program is free software: you can redistribute it and/or modify
7+
* it under the terms of the GNU General Public License as published by
8+
* the Free Software Foundation, either version 3 of the License, or
9+
* (at your option) any later version.
10+
*
11+
* This program is distributed in the hope that it will be useful,
12+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14+
* GNU General Public License for more details.
15+
*
16+
* You should have received a copy of the GNU General Public License
17+
* along with this program. If not, see <https://www.gnu.org/licenses/>.
18+
*/
19+
package de.rwth.idsg.steve.ocpp;
20+
21+
import lombok.Getter;
22+
import lombok.RequiredArgsConstructor;
23+
24+
/**
25+
* @author Sevket Goekay <sevketgokay@gmail.com>
26+
* @since 10.11.2025
27+
*/
28+
@RequiredArgsConstructor
29+
@Getter
30+
public enum OcppSecurityProfile {
31+
Profile_0(0, "0: No HTTP basic authentication, no TLS"),
32+
Profile_1(1, "1: HTTP basic authentication, no TLS"),
33+
Profile_2(2, "2: HTTP basic authentication, TLS with server-side certificate"),
34+
Profile_3(3, "3: TLS with client-side and server-side certificates (mutual TLS or mTLS)");
35+
36+
private final int value;
37+
private final String description;
38+
39+
public static OcppSecurityProfile fromValue(Integer value) {
40+
if (value == null) {
41+
return null;
42+
}
43+
for (OcppSecurityProfile c: OcppSecurityProfile.values()) {
44+
if (c.getValue() == value) {
45+
return c;
46+
}
47+
}
48+
throw new IllegalArgumentException(String.valueOf(value));
49+
}
50+
51+
public boolean requiresBasicAuth() {
52+
return switch (this) {
53+
case Profile_1, Profile_2 -> true;
54+
default -> false;
55+
};
56+
}
57+
}

src/main/java/de/rwth/idsg/steve/ocpp/soap/CentralSystemService16_SoapServer.java

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,14 @@
2323
import de.rwth.idsg.steve.service.CentralSystemService16_Service;
2424
import lombok.RequiredArgsConstructor;
2525
import lombok.extern.slf4j.Slf4j;
26+
import ocpp._2022._02.security.LogStatusNotification;
27+
import ocpp._2022._02.security.LogStatusNotificationResponse;
28+
import ocpp._2022._02.security.SecurityEventNotification;
29+
import ocpp._2022._02.security.SecurityEventNotificationResponse;
30+
import ocpp._2022._02.security.SignCertificate;
31+
import ocpp._2022._02.security.SignCertificateResponse;
32+
import ocpp._2022._02.security.SignedFirmwareStatusNotification;
33+
import ocpp._2022._02.security.SignedFirmwareStatusNotificationResponse;
2634
import ocpp.cs._2015._10.AuthorizeRequest;
2735
import ocpp.cs._2015._10.AuthorizeResponse;
2836
import ocpp.cs._2015._10.BootNotificationRequest;
@@ -134,6 +142,25 @@ public DataTransferResponse dataTransfer(DataTransferRequest parameters, String
134142
return service.dataTransfer(parameters, chargeBoxIdentity);
135143
}
136144

145+
public SignCertificateResponse signCertificate(SignCertificate parameters, String chargeBoxIdentity) {
146+
return service.signCertificate(parameters, chargeBoxIdentity);
147+
}
148+
149+
public SecurityEventNotificationResponse securityEventNotification(SecurityEventNotification parameters,
150+
String chargeBoxIdentity) {
151+
return service.securityEventNotification(parameters, chargeBoxIdentity);
152+
}
153+
154+
public SignedFirmwareStatusNotificationResponse signedFirmwareStatusNotification(SignedFirmwareStatusNotification parameters,
155+
String chargeBoxIdentity) {
156+
return service.signedFirmwareStatusNotification(parameters, chargeBoxIdentity);
157+
}
158+
159+
public LogStatusNotificationResponse logStatusNotification(LogStatusNotification parameters,
160+
String chargeBoxIdentity) {
161+
return service.logStatusNotification(parameters, chargeBoxIdentity);
162+
}
163+
137164
// -------------------------------------------------------------------------
138165
// No-op
139166
// -------------------------------------------------------------------------

0 commit comments

Comments
 (0)