Skip to content

Conversation

@juherr
Copy link
Contributor

@juherr juherr commented Oct 19, 2025

@florinmandache I built on your work and made the minimal required changes.
Some tests would be very welcome. I also noticed that CSMS security requests aren’t implemented yet.

@goekay It seems to be working (at least it starts correctly), but I haven’t fully tested it yet and it could use some polishing.
Feel free to rework or improve it as you see fit.

Fix #100
Fix #1353

@juherr juherr force-pushed the feature/ocpp16-security branch from e9b8666 to 81ba853 Compare October 19, 2025 11:28
@goekay
Copy link
Member

goekay commented Oct 20, 2025

@juherr i will take a look at this ASAP when i am finished with some other duties.

on a more general note: i had a conversation with @florinmandache after his initial big PR. he said that he has many things going on and is short on time, and he wanted to contribute on good will. since he is a busy person, he was not interested in follow-up, review rounds and eventual clean-ups (i.e. the usual process of PR and reviewing). due to the substantial nature of the contribution, i accepted it as-is and took it upon me to do whatever is necessary to bring these features to main branch.

in this context, i thank you for doing one part of it.

@juherr
Copy link
Contributor Author

juherr commented Oct 20, 2025

@goekay Thanks for the clarification. I think Gemini did an interesting initial job exploring the different topics, but it definitely needs more work based on what I’ve seen so far (especially regarding OCPP 1.6 Security and OICP). Since OCPI and OCPP 2.x are an even tougher challenge, and my time is just as limited, I’ll let you take the first shot on those ones 😉

# Conflicts:
#	src/main/java/de/rwth/idsg/steve/service/CentralSystemService16_Service.java
#	src/test/java/de/rwth/idsg/steve/utils/__DatabasePreparer__.java
@goekay
Copy link
Member

goekay commented Oct 31, 2025

FYI @juherr: steve-community/ocpp-jaxb#18

naming of the json schema files differ a bit from the original (see links from comment). do you know why?

@juherr
Copy link
Contributor Author

juherr commented Oct 31, 2025

Ah, you mean the request suffix in some filenames?
I personally find it cleaner and more robust to change it, but your approach seems to work fine too 👍

* clean up resources folder and pom file
* and do necessary changes for project to compile
@goekay
Copy link
Member

goekay commented Nov 2, 2025

i did some "low-hanging fruit" refactoring to reduce the size of the PR (i.e. moving the data models and their generation to ocpp-jaxb library). will do some more. i think this is very important in order to able to properly review the PR and separate the good parts from inaccurate AI slop. while being on this...

i did a litmus test and tried to understand how "http basic auth" (security profile 1) is supposed to work in the impl. ChargePointRepository has 2 new methods isRegistered(String chargeBoxId) and validatePassword(String chargeBoxId, String password), but no one is using them.

moreover, there is no change in SecurityConfiguration or OcppWebSocketHandshakeHandler (more emphasis on this), such that the handler retrieves the basic auth header from request and validates the password against the DB.

* and also make getRegistrationStatus return the password additionally, in order prevent another DB lookup later
@goekay goekay force-pushed the feature/ocpp16-security branch from 036e217 to 3878731 Compare November 3, 2025 09:30
@goekay
Copy link
Member

goekay commented Nov 4, 2025

WIP - Implementation status of new operations:

Operation Initiator Request Handling Response Handling
CertificateSigned Central System ✅ (no Web UI)
DeleteCertificate Central System ✅ (UI via data table)
ExtendedTriggerMessage Central System
GetInstalledCertificateIds Central System
GetLog Central System
InstallCertificate Central System
LogStatusNotification Charge Point
SecurityEventNotification Charge Point
SignCertificate Charge Point
SignedFirmwareStatusNotification Charge Point
SignedUpdateFirmware Central System

Other ToDos:

  • Web UI: Security Management > Certificates page (edit: split into 2 -- installed and signed certificates)
  • Web UI: Security Management > Configuration page (edit: this page is deleted)
  • Web UI: Security Management > Security events page
  • Web UI: Security Management > Firmware and log upload pages (by unifying them in "status events")
  • Regarding local signing: merge SecurityProfileConfiguration into org.springframework.boot.web.server.Ssl
  • Implement local certificate signing (i.e. revise CertificateSigningServiceLocal)
  • Implement remote certificate signing (i.e. forward to a CA authority, such as Let's Encrypt).
  • Add new Configuration Keys
  • Security Profiles 1 and 2: Incorporate security profile decision/validation into websocket connection handshake
  • Security Profile 3: Handle (new) station certificates, update trust store during runtime etc.

@goekay
Copy link
Member

goekay commented Nov 5, 2025

ignoring and removing the following method completely, since it has no correlation with reality IMO:

    private static String determineSeverity(String eventType) {
        if (eventType == null) {
            return "INFO";
        }

        var upperType = eventType.toUpperCase();

        if (upperType.contains("ATTACK") || upperType.contains("BREACH") || upperType.contains("TAMPER")) {
            return "CRITICAL";
        }

        if (upperType.contains("FAIL") || upperType.contains("ERROR") || upperType.contains("INVALID")
            || upperType.contains("UNAUTHORIZED") || upperType.contains("REJECT")) {
            return "HIGH";
        }

        if (upperType.contains("WARNING") || upperType.contains("EXPIR")) {
            return "MEDIUM";
        }

        return "INFO";
    }

the eventType references type field in SecurityEventNotification message with the following value possibilities:

Screenshot 2025-11-05 at 13 49 44

the table actually continues in the next page with more types, but there is no type with INFO, ATTACK (heh?), BREACH (what?) etc. so this whole thing is made up.

moreover, the PR invents new types (that are not in the list), such as SignCertificateError with a HIGH severity, for ex in

            securityRepository.insertSecurityEvent(
                chargeBoxIdentity,
                "SignCertificateError",
                DateTime.now(),
                "Error signing CSR: " + e.getMessage(),
                "HIGH"
            );

these usages feel also wrong. actually, the motivation is nice: if there is an error during the signing of certificate, create a security event and store it in DB. but, these security events are NOT coming from station, and intermixing these with regular events is problematic IMO.

@juherr do you happen to know whether these are retrofits coming from ocpp 2.x ?

@juherr
Copy link
Contributor Author

juherr commented Nov 5, 2025

@juherr do you happen to know whether these are retrofits coming from ocpp 2.x ?

I haven’t looked deeply into OCPP 2.x or even the current PR once I understood where it originated from.
My assumption is that Gemini applied a pattern from another context by mistake.

According to the whitepaper, only critical events are supposed to be sent by the station — and I believe all of those should indeed be stored.
Happy to help if you can elaborate a bit more on your question.

@goekay
Copy link
Member

goekay commented Nov 5, 2025

so, i can assume that these additions were a mistake and i can remove them

  • additional severity column derived from security event type
  • additional security event inserts (triggered by backend/steve) when local certificate-related operation fails

?

* various simplifications and improvements
* certificate is still as is. this is a future TODO
@goekay goekay force-pushed the feature/ocpp16-security branch from db70668 to 59c1c8d Compare November 5, 2025 15:12
@juherr
Copy link
Contributor Author

juherr commented Nov 5, 2025

In my opinion, security events triggered by the backend are a separate concern, and a warning log should be enough for now.
At the very least, an internal event could be raised so that any fork can decide how it wants to handle it.

@goekay goekay force-pushed the feature/ocpp16-security branch from d54af17 to d84e47c Compare November 5, 2025 17:43
Copy link
Contributor Author

@juherr juherr left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some remarks after a first review of the changes.
I should be able to make them by myself as it comes from my branch, but I would like your review first.

Comment on lines 385 to 395
try {
if (parameters.getRequestId() == null) {
log.warn("No requestId in {}", parameters);
} else {
securityRepository.insertLogUploadStatus(
chargeBoxIdentity,
parameters.getRequestId(),
parameters.getStatus().value(),
DateTime.now()
);
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
try {
if (parameters.getRequestId() == null) {
log.warn("No requestId in {}", parameters);
} else {
securityRepository.insertLogUploadStatus(
chargeBoxIdentity,
parameters.getRequestId(),
parameters.getStatus().value(),
DateTime.now()
);
}
if (parameters.getRequestId() == null) {
throw new InvalidArgumentException("RequestId is null");
}
try {
securityRepository.insertLogUploadStatus(
chargeBoxIdentity,
parameters.getRequestId(),
parameters.getStatus().value(),
DateTime.now()
);
}

Comment on lines 365 to 375
try {
if (parameters.getRequestId() == null) {
log.warn("No requestId in {}", parameters);
} else {
securityRepository.insertFirmwareUpdateStatus(
chargeBoxIdentity,
parameters.getRequestId(),
parameters.getStatus().value(),
DateTime.now()
);
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
try {
if (parameters.getRequestId() == null) {
log.warn("No requestId in {}", parameters);
} else {
securityRepository.insertFirmwareUpdateStatus(
chargeBoxIdentity,
parameters.getRequestId(),
parameters.getStatus().value(),
DateTime.now()
);
}
if (parameters.getRequestId() == null) {
throw new InvalidArgumentException("RequestId is null");
}
try {
securityRepository.insertFirmwareUpdateStatus(
chargeBoxIdentity,
parameters.getRequestId(),
parameters.getStatus().value(),
DateTime.now()
);
}

Comment on lines 85 to 94
String keystorePath = securityConfig.getKeystorePath();
String keystorePassword = securityConfig.getKeystorePassword();
String keystoreType = securityConfig.getKeystoreType();

KeyStore keystore = KeyStore.getInstance(keystoreType);
try (FileInputStream fis = new FileInputStream(keystorePath)) {
keystore.load(fis, keystorePassword.toCharArray());
}

String alias = keystore.aliases().nextElement();
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
String keystorePath = securityConfig.getKeystorePath();
String keystorePassword = securityConfig.getKeystorePassword();
String keystoreType = securityConfig.getKeystoreType();
KeyStore keystore = KeyStore.getInstance(keystoreType);
try (FileInputStream fis = new FileInputStream(keystorePath)) {
keystore.load(fis, keystorePassword.toCharArray());
}
String alias = keystore.aliases().nextElement();
var keystorePath = securityConfig.getKeystorePath();
var keystorePassword = securityConfig.getKeystorePassword();
var keystoreType = securityConfig.getKeystoreType();
var keystore = KeyStore.getInstance(keystoreType);
try (var fis = new FileInputStream(keystorePath)) {
keystore.load(fis, keystorePassword.toCharArray());
}
var alias = keystore.aliases().nextElement();

@goekay
Copy link
Member

goekay commented Nov 5, 2025

@juherr pls do not review yet. i did not go over all changes yet. some of your remarks will be obsolete when i finally arrive at these areas to improve. pls see table in #1854 (comment)

@goekay
Copy link
Member

goekay commented Nov 12, 2025

FYI: the implementation work is finished, with the exception of security profile 3 (mTLS). i need to investigate how to do it.

@juherr
Copy link
Contributor Author

juherr commented Nov 12, 2025

@goekay Congratulations, it looks great! If you're fine with it, I can review the changes now.

Regarding profile 3, please share your thoughts. I’ve had similar reflections on that topic.

@goekay
Copy link
Member

goekay commented Nov 12, 2025

Congratulations, it looks great! If you're fine with it, I can review the changes now.

yes, please do. when reviewing this, could you focus mainly on the logic, correctness, and functional aspects? i’d like to prevent drifting into stylistic and preference discussions ;)

Regarding profile 3, please share your thoughts. I’ve had similar reflections on that topic.

there is many. as starters: it should actually work with current state of this implementation as long as the user (the user of steve) configures and activates SSL and maintains the certs in truststore in a file. then, spring boot will configure SSL for jetty including the SSLContext using the truststore. then, the underlying standard TLS implementation will handle all mTLS stuff.

but there are many issues with this:

  1. managing truststore (and adding/removing root/CA certs) becomes a process outside of steve. it is not integrated. we need to refer users to terminal tools or other software.
  2. it requires restart of steve whenever something changes.
  3. after cert changes in station (after CertificateSigned etc.), steve cannot seamlessly import the root cert etc. into truststore. it will be an additional external step to update truststore.
  4. mTLS should be mandatory only in profile 3. stations using other profiles should be able to connect without station cert.
  5. what if steve is behind a reverse proxy that handles and terminates TLS? such that steve runs only on HTTP?
  6. truststore as a file on filesystem feels clunky in modern cloud environments

i am thinking about a solution approach that involves following ideas:

  • truststore certs to be stored in relational DB
  • the user can see and manage them
  • certificate-related OCPP operations can insert/remove certs in DB
  • SSLContext can be recreated/refreshed and attached to jetty during runtime after truststore changes
  • handling cert validation/cheks in application code conditionally (addresses the points 4 and 5 from above). big issue here: we cannot rely on standard TLS implementation anymore, and need to re-impl some aspects of it which makes me worried due to potential mistakes/errors. i really would like to NOT hand-roll TLS/security stuff.

Copy link
Contributor Author

@juherr juherr left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't review the JSP files.

Aside from a few style inconsistencies (like mixed use of var and occasional silent errors), everything looks good to me.

// -------------------------------------------------------------------------

AdditionalRootCertificateCheck("boolean", R, newHashSet(V_16)),
AuthorizationKey("string", W, newHashSet(V_16)),
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Like WebSocketPingInterval , this one is defined in Specification OCPPJ

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Like WebSocketPingInterval , this one is defined in Specification OCPPJ

i dont know what to do with this comment.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You may move it before the comment because it was not defined first in security extension.
I need to check it again because it could be an issue in my file.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it was not defined first in security extension.

WebSocketPingInterval was there in ocpp 1.6 spec (no extension). the enum was added years ago at the right place.

AuthorizationKey was added with the ocpp 1.6 security extension. it is there in edition 3 (final). this enum was added in this work at the right place.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AuthorizationKey is referenced in
ocpp-j-1.6-specification - section 6.2.2. Charge point authentication
image

Copy link
Member

@goekay goekay Nov 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fair enough. it comes up in text, but not in the list of

  • "9. Standard Configuration Key Names & Values" section of ocpp-1.6 edition 2.pdf, or
  • "7. Configuration" section of ocpp-j-1.6-specification.pdf

which i took as basis for the initial ocpp 1.6 impl.

now, it is part of "7. Configuration Keys" of OCPP 1.6 security whitepaper edition 3.pdf which i took as the basis for the security extension impl.

moreover, since we did not initially support basic auth (and AuthorizationKey) and this is such a bikeshedding topic, i have no problem having it at the current place of the code.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since SteVe is one of the reference OCPP implementations, it's worth adding a comment here with a bit of context.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks for your careful review and the detailed thoughts you’ve shared. i appreciate your attention to detail.

however, this is such a small and not-so-important thing we are discussing, i.e. the place of the enum in the source code. we should not dwell on it. i propose we move on. as i said earlier: i’d like to prevent drifting into stylistic and preference discussions. would you be okay with this? thank you!

lets put our precious time and energy into things that matter. lets talk about the more important (core) stuff where i am not sure how to continue. for example this. WDYT? (we should talk about it in a separate thread)

reason: we don't want to silently ignore (or skip) non-Json chargers.
ChargePointServiceJsonInvoker.runPipeline(..) has the necessary error-handling
in case of exceptions. so, we can do the check there.
reason: preparation for security profile 3 (mTLS) impl. we need to validate CpoName
of the certificate, which means the info has to exist somewhere within steve beforehand.
@juherr
Copy link
Contributor Author

juherr commented Nov 15, 2025

@goekay

lets talk about the more important (core) stuff where i am not sure how to continue. for example #1854 (comment). WDYT? (we should talk about it in a separate thread)

The TLS/mTLS topic is much broader than choosing a coding convention. It is a strategic architectural decision that affects how the entire project is deployed and operated.

Since you asked for my view: TLS/mTLS termination provided by the CSMS itself is fundamentally an infrastructure concern, not an application concern. Because of that, I don’t think steve should try to solve it internally or make it a core responsibility. This is exactly what edge components (reverse proxies, ingress controllers, API gateways, service meshes) are designed for.

Steve’s responsibility is on the business side of the certificate logic (e.g. mapping certificate → chargeBoxId → security profile), not on performing low-level TLS operations.

If, after further analysis, we still conclude that steve must handle TLS/mTLS directly, then the clean solution would require a multi-layer, event-driven architecture with dedicated gateway nodes and a separate backend for the business logic. Christian from Monta has written several useful posts about this approach:
https://www.linkedin.com/posts/cweinberger_engineering-ocpp-evbetter-activity-7198954253028081664-sn0y

But I don’t see how an all-in-one product could reliably manage TLS, mTLS, certificate lifecycle, OCPP logic, scaling, and cloud-native deployment without breaking how Spring Boot is supposed to work or without rebuilding infrastructure features inside the application.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add support for SignedUpdateFirmware OCPP 1.6-J Security

3 participants