Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
53 commits
Select commit Hold shift + click to select a range
81ba853
feat: implement OCPP 1.6 Security
florinmandache Sep 27, 2025
b753424
Merge branch steve-community/steve 'master' into feature/ocpp16-security
goekay Oct 31, 2025
b021b4f
fix: build error because of db migration with same version exists
goekay Oct 31, 2025
ca48906
use data models from ocpp-jaxb dependency
goekay Nov 2, 2025
ad62b97
fix builds
goekay Nov 2, 2025
eb13078
refactor
goekay Nov 2, 2025
3878731
refactor: move password validation to ChargePointService
goekay Nov 3, 2025
59c1c8d
refactor DB changes
goekay Nov 5, 2025
a47ed12
make project compile
goekay Nov 5, 2025
9ad638a
implement LogStatusNotification
goekay Nov 5, 2025
cb36fe7
implement SignedFirmwareStatusNotification
goekay Nov 5, 2025
d84e47c
implement ExtendedTriggerMessage
goekay Nov 5, 2025
0cfb615
implement GetLog
goekay Nov 5, 2025
b305cb5
implement SignedUpdateFirmware
goekay Nov 5, 2025
ca26cc4
refactor ocpp 1.6 left menu
goekay Nov 5, 2025
7dd51f5
revisit and refine the impl for security events page
goekay Nov 6, 2025
a6440e1
rename "security" dir to "security-man" to represent security management
goekay Nov 6, 2025
47d455d
work on SignCertificate
goekay Nov 6, 2025
2f214d3
add support for new Configuration Keys
goekay Nov 6, 2025
1331528
implement InstallCertificate
goekay Nov 6, 2025
5c29fae
revisit and refine the impl for "firmware updates" page(s)
goekay Nov 6, 2025
76a5698
generalize "firmware update" into "status event" pages
goekay Nov 6, 2025
d75ca6b
nits
goekay Nov 6, 2025
b3fa5f8
refactor/cleanup: remove unnecessary SecurityRepository methods
goekay Nov 6, 2025
f6da094
implement CertificateSigned and revise CertificateSigningServiceLocal
goekay Nov 7, 2025
c23d84c
refactor and nits
goekay Nov 7, 2025
9c92479
merge SecurityProfileConfiguration into org.springframework.boot.web.…
goekay Nov 7, 2025
14c0cf9
refactor and nits
goekay Nov 7, 2025
256445e
implement GetInstalledCertificateIds
goekay Nov 7, 2025
96f4d14
implement DeleteCertificate
goekay Nov 7, 2025
abb7ee9
implement and refactor certificates pages
goekay Nov 7, 2025
45aab67
implement page for "signed certificates"
goekay Nov 7, 2025
30270ba
refactor: split SecurityRepository into CertificateRepository and Eve…
goekay Nov 7, 2025
bb6c4a2
build fix, refactor and cleanup
goekay Nov 7, 2025
2f5722c
add CRUD functionality for new station fields: security profile and a…
goekay Nov 10, 2025
f766840
refactor
goekay Nov 10, 2025
d3cfba4
add temporal filtering to certificate and event pages
goekay Nov 10, 2025
0261550
refactor: move columns for consistency
goekay Nov 10, 2025
9f3a7fe
nits [skip ci]
goekay Nov 10, 2025
c8032fd
add authPassword validation for profiles 1 and 2 in ChargePointForm
goekay Nov 10, 2025
976d208
implement basic auth validation during websocket handshake
goekay Nov 11, 2025
2a5f8b0
nits
goekay Nov 11, 2025
a4e730f
add info boxes
goekay Nov 11, 2025
2f2f5a9
nits
goekay Nov 11, 2025
d8e0147
nits for mTLS
goekay Nov 12, 2025
2bdb892
refactor: we can access Ssl in ServerProperties (which is a bean)
goekay Nov 12, 2025
7ea8a11
work on markdown docs
goekay Nov 12, 2025
31cb162
refactor: PR feedback
goekay Nov 12, 2025
d8c089e
push isJson() check down to ChargePointServiceJsonInvoker
goekay Nov 12, 2025
5940aea
nits
goekay Nov 13, 2025
2919fda
get CpoName and update in DB eagerly after each ws connection
goekay Nov 15, 2025
4c64664
implement ocpp-related field checks as client cert validation
goekay Nov 17, 2025
3716f68
make job_id nullable (i.e. requestId optional in notification messages)
goekay Nov 17, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 18 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,14 +27,26 @@ Electric charge points using the following OCPP versions are supported:
* OCPP1.5S
* OCPP1.5J
* OCPP1.6S
* OCPP1.6J

⚠️ 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.
Please, don't expose a Steve instance without knowing that risk.
* OCPP1.6J (incl. _Security Extensions_)

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

---

#### OCPP 1.6J Security Extensions

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:

* **Security Profiles 0-3**: Unsecured, Basic Auth, Basic Auth with server TLS, and Mutual TLS (mTLS)
* **Certificate Management**: Certificate signing, installation, and deletion
* **Security Events**: Real-time security event logging and monitoring
* **Signed Firmware Updates**: Cryptographically signed firmware updates with certificate validation
* **Diagnostic Logs**: Secure log retrieval with configurable time ranges

See [dedicated Wiki page](https://github.com/steve-community/steve/wiki/OCPP-1.6J-Security-Configuration) for detailed configuration guide.


### System Requirements

SteVe requires
Expand Down Expand Up @@ -77,7 +89,7 @@ SteVe is designed to run standalone, a java servlet container / web server (e.g.
- You _must_ change [the host](src/main/resources/application-prod.properties) to the correct IP address of your server
- You _must_ change [web interface credentials](src/main/resources/application-prod.properties)
- You _can_ access the application via HTTPS, by [enabling it and setting the keystore properties](src/main/resources/application-prod.properties)

For advanced configuration please see the [Configuration wiki](https://github.com/steve-community/steve/wiki/Configuration)

4. Build SteVe:
Expand Down Expand Up @@ -145,9 +157,8 @@ After SteVe has successfully started, you can access the web interface using the
- SOAP: `http://<your-server-ip>:<port>/steve/services/CentralSystemService`
- WebSocket/JSON: `ws://<your-server-ip>:<port>/steve/websocket/CentralSystemService`


As soon as a heartbeat is received, you should see the status of the charge point in the SteVe Dashboard.

*Have fun!*

Screenshots
Expand Down
14 changes: 13 additions & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -443,7 +443,7 @@
<dependency>
<groupId>com.github.steve-community</groupId>
<artifactId>ocpp-jaxb</artifactId>
<version>0.0.9</version>
<version>0.0.11</version>
</dependency>
<dependency>
<groupId>org.jetbrains</groupId>
Expand Down Expand Up @@ -569,5 +569,17 @@
<artifactId>encoder-jakarta-jsp</artifactId>
<version>1.3.1</version>
</dependency>

<!-- Bouncy Castle for certificate signing (OCPP 1.6 Security) -->
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk18on</artifactId>
<version>1.79</version>
</dependency>
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcpkix-jdk18on</artifactId>
<version>1.79</version>
</dependency>
</dependencies>
</project>
12 changes: 12 additions & 0 deletions src/main/java/de/rwth/idsg/steve/config/SteveProperties.java
Original file line number Diff line number Diff line change
Expand Up @@ -66,5 +66,17 @@ public static class Ocpp {
WsSessionSelectStrategyEnum wsSessionSelectStrategy;
boolean autoRegisterUnknownStations;
String chargeBoxIdValidationRegex;
Security security = new Security();

@Data
public static class Security {
private int profile;
private int certificateValidityYears;
private String clientCertHeaderFromProxy;

public boolean requiresTls() {
return profile >= 2;
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
import de.rwth.idsg.steve.ocpp.ws.ocpp12.Ocpp12WebSocketEndpoint;
import de.rwth.idsg.steve.ocpp.ws.ocpp15.Ocpp15WebSocketEndpoint;
import de.rwth.idsg.steve.ocpp.ws.ocpp16.Ocpp16WebSocketEndpoint;
import de.rwth.idsg.steve.service.CertificateValidator;
import de.rwth.idsg.steve.service.ChargePointService;
import de.rwth.idsg.steve.web.validation.ChargeBoxIdValidator;
import lombok.RequiredArgsConstructor;
Expand Down Expand Up @@ -53,6 +54,7 @@ public class WebSocketConfiguration implements WebSocketConfigurer {
private final Ocpp12WebSocketEndpoint ocpp12WebSocketEndpoint;
private final Ocpp15WebSocketEndpoint ocpp15WebSocketEndpoint;
private final Ocpp16WebSocketEndpoint ocpp16WebSocketEndpoint;
private final CertificateValidator certificateValidator;

public static final String PATH_INFIX = "/websocket/CentralSystemService/";
public static final Duration PING_INTERVAL = Duration.ofMinutes(15);
Expand All @@ -65,7 +67,8 @@ public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
chargeBoxIdValidator,
handshakeHandler(),
Lists.newArrayList(ocpp16WebSocketEndpoint, ocpp15WebSocketEndpoint, ocpp12WebSocketEndpoint),
chargePointService
chargePointService,
certificateValidator
);

registry.addHandler(handshakeHandler.getDummyWebSocketHandler(), PATH_INFIX + "*")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,21 +20,28 @@

import de.rwth.idsg.steve.ocpp.soap.ChargePointServiceSoapInvoker;
import de.rwth.idsg.steve.ocpp.task.CancelReservationTask;
import de.rwth.idsg.steve.ocpp.task.CertificateSignedTask;
import de.rwth.idsg.steve.ocpp.task.ChangeAvailabilityTask;
import de.rwth.idsg.steve.ocpp.task.ChangeConfigurationTask;
import de.rwth.idsg.steve.ocpp.task.ClearCacheTask;
import de.rwth.idsg.steve.ocpp.task.ClearChargingProfileTask;
import de.rwth.idsg.steve.ocpp.task.DataTransferTask;
import de.rwth.idsg.steve.ocpp.task.DeleteCertificateTask;
import de.rwth.idsg.steve.ocpp.task.ExtendedTriggerMessageTask;
import de.rwth.idsg.steve.ocpp.task.GetCompositeScheduleTask;
import de.rwth.idsg.steve.ocpp.task.GetConfigurationTask;
import de.rwth.idsg.steve.ocpp.task.GetDiagnosticsTask;
import de.rwth.idsg.steve.ocpp.task.GetInstalledCertificateIdsTask;
import de.rwth.idsg.steve.ocpp.task.GetLocalListVersionTask;
import de.rwth.idsg.steve.ocpp.task.GetLogTask;
import de.rwth.idsg.steve.ocpp.task.InstallCertificateTask;
import de.rwth.idsg.steve.ocpp.task.RemoteStartTransactionTask;
import de.rwth.idsg.steve.ocpp.task.RemoteStopTransactionTask;
import de.rwth.idsg.steve.ocpp.task.ReserveNowTask;
import de.rwth.idsg.steve.ocpp.task.ResetTask;
import de.rwth.idsg.steve.ocpp.task.SendLocalListTask;
import de.rwth.idsg.steve.ocpp.task.SetChargingProfileTask;
import de.rwth.idsg.steve.ocpp.task.SignedUpdateFirmwareTask;
import de.rwth.idsg.steve.ocpp.task.TriggerMessageTask;
import de.rwth.idsg.steve.ocpp.task.UnlockConnectorTask;
import de.rwth.idsg.steve.ocpp.task.UpdateFirmwareTask;
Expand Down Expand Up @@ -235,4 +242,36 @@ public void triggerMessage(ChargePointSelect cp, TriggerMessageTask task) {
chargePointServiceJsonInvoker.runPipeline(cp, task);
}
}

// -------------------------------------------------------------------------
// "Improved security for OCPP 1.6-J" additions. Only for JSON
// -------------------------------------------------------------------------

public void extendedTriggerMessage(ChargePointSelect cp, ExtendedTriggerMessageTask task) {
chargePointServiceJsonInvoker.runPipeline(cp, task);
}

public void getLog(ChargePointSelect cp, GetLogTask task) {
chargePointServiceJsonInvoker.runPipeline(cp, task);
}

public void signedUpdateFirmware(ChargePointSelect cp, SignedUpdateFirmwareTask task) {
chargePointServiceJsonInvoker.runPipeline(cp, task);
}

public void installCertificate(ChargePointSelect cp, InstallCertificateTask task) {
chargePointServiceJsonInvoker.runPipeline(cp, task);
}

public void deleteCertificate(ChargePointSelect cp, DeleteCertificateTask task) {
chargePointServiceJsonInvoker.runPipeline(cp, task);
}

public void certificateSigned(ChargePointSelect cp, CertificateSignedTask task) {
chargePointServiceJsonInvoker.runPipeline(cp, task);
}

public void getInstalledCertificateIds(ChargePointSelect cp, GetInstalledCertificateIdsTask task) {
chargePointServiceJsonInvoker.runPipeline(cp, task);
}
}
57 changes: 57 additions & 0 deletions src/main/java/de/rwth/idsg/steve/ocpp/OcppSecurityProfile.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/*
* SteVe - SteckdosenVerwaltung - https://github.com/steve-community/steve
* Copyright (C) 2013-2025 SteVe Community Team
* All Rights Reserved.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.rwth.idsg.steve.ocpp;

import lombok.Getter;
import lombok.RequiredArgsConstructor;

/**
* @author Sevket Goekay <[email protected]>
* @since 10.11.2025
*/
@RequiredArgsConstructor
@Getter
public enum OcppSecurityProfile {
Profile_0(0, "0: No HTTP basic authentication, no TLS"),
Profile_1(1, "1: HTTP basic authentication, no TLS"),
Profile_2(2, "2: HTTP basic authentication, TLS with server-side certificate"),
Profile_3(3, "3: TLS with client-side and server-side certificates (mutual TLS or mTLS)");

private final int value;
private final String description;

public static OcppSecurityProfile fromValue(Integer value) {
if (value == null) {
return null;
}
for (OcppSecurityProfile c: OcppSecurityProfile.values()) {
if (c.getValue() == value) {
return c;
}
}
throw new IllegalArgumentException(String.valueOf(value));
}

public boolean requiresBasicAuth() {
return switch (this) {
case Profile_1, Profile_2 -> true;
default -> false;
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,14 @@
import de.rwth.idsg.steve.service.CentralSystemService16_Service;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import ocpp._2022._02.security.LogStatusNotification;
import ocpp._2022._02.security.LogStatusNotificationResponse;
import ocpp._2022._02.security.SecurityEventNotification;
import ocpp._2022._02.security.SecurityEventNotificationResponse;
import ocpp._2022._02.security.SignCertificate;
import ocpp._2022._02.security.SignCertificateResponse;
import ocpp._2022._02.security.SignedFirmwareStatusNotification;
import ocpp._2022._02.security.SignedFirmwareStatusNotificationResponse;
import ocpp.cs._2015._10.AuthorizeRequest;
import ocpp.cs._2015._10.AuthorizeResponse;
import ocpp.cs._2015._10.BootNotificationRequest;
Expand Down Expand Up @@ -134,6 +142,25 @@
return service.dataTransfer(parameters, chargeBoxIdentity);
}

public SignCertificateResponse signCertificate(SignCertificate parameters, String chargeBoxIdentity) {
return service.signCertificate(parameters, chargeBoxIdentity);
}

public SecurityEventNotificationResponse securityEventNotification(SecurityEventNotification parameters,
String chargeBoxIdentity) {
return service.securityEventNotification(parameters, chargeBoxIdentity);
}

public SignedFirmwareStatusNotificationResponse signedFirmwareStatusNotification(SignedFirmwareStatusNotification parameters,

Check failure on line 154 in src/main/java/de/rwth/idsg/steve/ocpp/soap/CentralSystemService16_SoapServer.java

View workflow job for this annotation

GitHub Actions / checkstyle

[checkstyle] reported by reviewdog 🐶 Line is longer than 120 characters (found 129). Raw Output: /github/workspace/./src/main/java/de/rwth/idsg/steve/ocpp/soap/CentralSystemService16_SoapServer.java:154:0: error: Line is longer than 120 characters (found 129). (com.puppycrawl.tools.checkstyle.checks.sizes.LineLengthCheck)
String chargeBoxIdentity) {
return service.signedFirmwareStatusNotification(parameters, chargeBoxIdentity);
}

public LogStatusNotificationResponse logStatusNotification(LogStatusNotification parameters,
String chargeBoxIdentity) {
return service.logStatusNotification(parameters, chargeBoxIdentity);
}

// -------------------------------------------------------------------------
// No-op
// -------------------------------------------------------------------------
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
/*
* SteVe - SteckdosenVerwaltung - https://github.com/steve-community/steve
* Copyright (C) 2013-2025 SteVe Community Team
* All Rights Reserved.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.rwth.idsg.steve.ocpp.task;

import de.rwth.idsg.steve.ocpp.Ocpp16AndAboveTask;
import de.rwth.idsg.steve.ocpp.OcppCallback;
import de.rwth.idsg.steve.repository.CertificateRepository;
import de.rwth.idsg.steve.web.dto.ocpp.CertificateSignedParams;
import lombok.extern.slf4j.Slf4j;
import ocpp._2022._02.security.CertificateSigned;
import ocpp._2022._02.security.CertificateSignedResponse;
import ocpp._2022._02.security.CertificateSignedResponse.CertificateSignedStatusEnumType;

import jakarta.xml.ws.AsyncHandler;

@Slf4j
public class CertificateSignedTask extends Ocpp16AndAboveTask<CertificateSignedParams, String> {

private final CertificateRepository certificateRepository;

public CertificateSignedTask(CertificateSignedParams params,
CertificateRepository certificateRepository) {
super(params);
this.certificateRepository = certificateRepository;
}

@Override
public OcppCallback<String> defaultCallback() {
return new StringOcppCallback();
}

@Override
public CertificateSigned getOcpp16Request() {
var request = new CertificateSigned();
request.setCertificateChain(params.getCertificateChain());
return request;
}

@Override
public AsyncHandler<CertificateSignedResponse> getOcpp16Handler(String chargeBoxId) {
return res -> {
try {
var status = res.get().getStatus();
success(chargeBoxId, status.value());

switch (status) {
case ACCEPTED -> log.info("Request was {} by charge point '{}'", status, chargeBoxId);
case REJECTED -> log.warn("Request was {} by charge point '{}'", status, chargeBoxId);
default -> log.warn("Unexpected status {} by charge point '{}'", status, chargeBoxId);
}

boolean accepted = (status == CertificateSignedStatusEnumType.ACCEPTED);
certificateRepository.insertCertificateSignResponse(chargeBoxId, params.getCertificateId(), accepted);
} catch (Exception e) {
failed(chargeBoxId, e);
}
};
}
}
Loading
Loading