Skip to content

Commit bc4699f

Browse files
committed
docs: elaborate on OCPP security
1 parent 7c17d5c commit bc4699f

File tree

1 file changed

+166
-10
lines changed

1 file changed

+166
-10
lines changed

README.md

Lines changed: 166 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,11 @@ This module is built for Node.js and does not currently work in browsers.
2323
## Features
2424

2525
* **Authentication** - Optional authentication step for initiating session data and filtering incoming clients.
26+
* **[OCPP Security](#ocpp-security)** - Compatible with OCPP security profiles 1, 2 & 3.
27+
* **Serve multiple subprotocols** - Simultaneously serve multiple different subprotocols from the same service endpoint.
2628
* **[Strict Validation](#strict-validation)** - Optionally enforce subprotocol schemas to prevent invalid calls & responses.
2729
* **Automatic reconnects** - Client supports automatic exponential-backoff reconnects.
2830
* **Automatic keep-alive** - Regularly performs pings, and drops dangling TCP connections.
29-
* **Serve multiple subprotocols** - Simultaneously serve multiple different subprotocols from the same service endpoint.
30-
* **[HTTP Basic Auth](#http-basic-auth)** - Easy-to-use HTTP Basic Auth compatible with OCPP security profiles 1 & 2.
3131
* **Graceful shutdowns** - Supports waiting for all in-flight messages to be responded to before closing sockets.
3232
* **Clean closing of websockets** - Supports sending & receiving WebSocket close codes & reasons.
3333
* **Embraces abort signals** - `AbortSignal`s can be passed to most async methods.
@@ -42,7 +42,10 @@ This module is built for Node.js and does not currently work in browsers.
4242
* [Using with Express.js](#using-with-expressjs)
4343
* [API Docs](#api-docs)
4444
* [Strict Validation](#strict-validation)
45-
* [HTTP Basic Auth](#http-basic-auth)
45+
* [OCPP Security](#ocpp-security)
46+
* [OCPP Security Profile 1](#security-profile-1)
47+
* [OCPP Security Profile 2](#security-profile-2)
48+
* [OCPP Security Profile 3](#security-profile-3)
4649
* [RPCClient state lifecycle](#rpcclient-state-lifecycle)
4750
* [License](#license)
4851

@@ -129,7 +132,7 @@ await cli.call('StatusNotification', {
129132

130133
```js
131134
const {RPCServer, RPCClient} = require('ocpp-rpc');
132-
const express = require("express");
135+
const express = require('express');
133136

134137
const app = express();
135138
const httpServer = app.listen(3000, 'localhost');
@@ -257,7 +260,7 @@ The callback function is called with the following three arguments:
257260
* `handshake` {Object} - A handshake object
258261
* `protocols` {Set} - A set of subprotocols purportedly supported by the client.
259262
* `identity` {String} - The identity portion of the connection URL, decoded.
260-
* `password` {String} - If HTTP Basic auth was used in the connection, and the username correctly matches the identity, this field will contain the password (otherwise `undefined`). Read [HTTP Basic Auth](#http-basic-auth) for more details of how this works.
263+
* `password` {String} - If HTTP Basic auth was used in the connection, and the username correctly matches the identity, this field will contain the password (otherwise `undefined`). Read [Security Profile 1](#security-profile-1) for more details of how this works.
261264
* `endpoint` {String} - The endpoint path portion of the connection URL. This is the part of the path before the identity.
262265
* `query` {URLSearchParams} - The query string parsed as [URLSearchParams](https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams).
263266
* `remoteAddress` {String} - The remote IP address of the socket.
@@ -333,7 +336,7 @@ Returns a `Promise` which resolves when the server has completed closing.
333336
- `endpoint` {String} - The RPC server's endpoint (a websocket URL). **Required**.
334337
- `identity` {String} - The RPC client's identity. Will be automatically encoded. **Required**.
335338
- `protocols` {Array<String>} - Array of subprotocols supported by this client. Defaults to `[]`.
336-
- `password` {String} - Optional password to use in [HTTP Basic auth](#http-basic-auth). (The username will always be the identity).
339+
- `password` {String} - Optional password to use in [HTTP Basic auth](#security-profile-1). (The username will always be the identity).
337340
- `headers` {Object} - Additional HTTP headers to send along with the websocket upgrade request. Defaults to `{}`.
338341
- `query` {Object|String} - An optional query string or object to append as the query string of the connection URL. Defaults to `''`.
339342
- `callTimeoutMs` {Number} - Milliseconds to wait before unanswered outbound calls are rejected automatically. Defaults to `60000`.
@@ -704,9 +707,15 @@ client.call('Echo', ['bar']); // throws RPCError
704707

705708
Once created, the `Validator` is immutable and can be reused as many times as is required.
706709

707-
## HTTP Basic Auth
710+
## OCPP Security
711+
712+
It is possible to achieve all levels of OCPP security using this module. Keep in mind though that many aspects of OCPP security (such as key management, certificate generation, etc...) are beyond the scope of this module and it will be up to you to implement them yourself.
713+
714+
### Security Profile 1
715+
716+
This security profile requires HTTP Basic Authentication. Clients are able to provide a HTTP basic auth password via the `password` option of the [`RPCClient` constructor](#new-rpcclientoptions). Servers are able to validate the password within the callback passed to [`auth()`](#serverauthcallback).
708717

709-
### Usage Example
718+
#### Client & Server Example
710719

711720
```js
712721
const cli = new RPCClient({
@@ -727,17 +736,19 @@ await server.listen(80);
727736
await cli.connect();
728737
```
729738

730-
### Identities containing colons
739+
#### A note on identities containing colons
731740

732741
This module supports HTTP Basic auth slightly differently than how it is specified in [RFC7617](https://datatracker.ietf.org/doc/html/rfc7617). In that spec, it is made clear that usernames cannot contain colons (:) as a colon is used to delineate where a username ends and a password begins.
733742

734743
In the context of OCPP, the basic-auth username must always be equal to the client's identity. However, since OCPP does not forbid colons in identities, this can possibly lead to a conflict and unexpected behaviours.
735744

736-
In practice, it's not uncommon to see violations of RFC7617 in the wild. All major browsers allow basic-auth usernames to contain colons, despite the fact that this won't make any sense to the server. RFC7617 acknowledges this fact. The prevalent solution to this problem seems to be to simply ignore it.
745+
In practice, it's not uncommon to see violations of RFC7617 in the wild. All major browsers allow basic-auth usernames to contain colons, despite the fact that this won't make any sense to the server; RFC7617 acknowledges this fact in its text. The established solution to this problem seems to be to simply ignore it.
737746

738747
However, in OCPP, since we have the luxury of knowing that the username must always be equal to the client's identity, it is no longer necessary to rely upon a colon to delineate the username from the password. This module makes use of this guarantee to enable identities and passwords to contain as many or as few colons as you wish.
739748

740749
```js
750+
const { RPCClient, RPCServer } = require('ocpp-rpc');
751+
741752
const cli = new RPCClient({
742753
identity: "this:is:ok",
743754
password: "as:is:this",
@@ -778,6 +789,151 @@ await server.listen(80);
778789
await cli.connect();
779790
```
780791

792+
### Security Profile 2
793+
794+
This security profile requires that the central system offers a TLS-secured endpoint in addition to HTTP Basic Authentication [(as per profile 1)](#security-profile-1).
795+
796+
When implementing TLS, keep in mind that OCPP specifies a minimum TLS version and minimum set of cipher suites for maximal compatibility and security. Node.js natively supports this minimum set of requirements, but there's a couple of things you should keep in mind:
797+
798+
* The minimum TLS version should be explicitly enforced to prevent a client from using a weak TLS version. The OCPP spec currently sets the minimum TLS version at v1.2 (with v1.1 and v1.0 being permitted for OCPP1.6 only under exceptional circumstances).
799+
* The central server role must support both RSA & ECDSA algorithms, so will need a corresponding certificate for each.
800+
801+
#### TLS Client Example
802+
803+
```js
804+
const { RPCClient } = require('ocpp-rpc');
805+
806+
const cli = new RPCClient({
807+
endpoint: 'wss://localhost',
808+
identity: 'EXAMPLE',
809+
password: `monkey1`,
810+
wsOpts: { minVersion: 'TLSv1.2' }
811+
});
812+
813+
await cli.connect();
814+
```
815+
816+
#### TLS Server Example
817+
818+
Implementing TLS on the server can be achieved in a couple of different ways. The most direct way is to [create an HTTPS server](https://nodejs.org/api/https.html#httpscreateserveroptions-requestlistener), giving you full end-to-end control over the TLS connectivity.
819+
820+
```js
821+
const https = require('https');
822+
const { RPCServer } = require('ocpp-rpc');
823+
const { readFile } = require('fs/promises');
824+
825+
const server = new RPCServer();
826+
827+
const httpsServer = https.createServer({
828+
cert: [
829+
await readFile('./server.crt', 'utf8'), // RSA certificate
830+
await readFile('./ec_server.crt', 'utf8'), // ECDSA certificate
831+
],
832+
key: [
833+
await readFile('./server.key', 'utf8'), // RSA key
834+
await readFile('./ec_server.key', 'utf8'), // ECDSA key
835+
],
836+
minVersion: 'TLSv1.2', // require TLS >= v1.2
837+
});
838+
839+
httpsServer.on('upgrade', server.handleUpgrade);
840+
httpsServer.listen(443);
841+
842+
server.auth((accept, reject, handshake) => {
843+
const tlsClient = handshake.request.client;
844+
845+
if (!tlsClient) {
846+
return reject();
847+
}
848+
849+
console.log(`${handshake.identity} connected using TLS:`, {
850+
password: handshake.password, // the HTTP auth password
851+
cert: tlsClient.getCertificate(), // the certificate used by the server
852+
cipher: tlsClient.getCipher(), // the cipher suite
853+
version: tlsClient.getProtocol(), // the TLS version
854+
});
855+
accept();
856+
});
857+
```
858+
859+
Alternatively, your TLS endpoint might be terminated at a different service (e.g. an Ingress controller in a Kubernetes environment or a third-party SaaS reverse-proxy such as Cloudflare). In this case, you may either try to manage your server's TLS through configuration of the aforementioned service, or perhaps by inspecting trusted HTTP headers appended to the request by a proxy.
860+
861+
### Security Profile 3
862+
863+
This security profile requires a TLS-secured central system and client-side certificates; This is also known as "Mutual TLS" (or "mTLS" for short).
864+
865+
The client-side example is fairly straight-forward:
866+
867+
#### TLS Client Example
868+
869+
```js
870+
const { RPCClient } = require('ocpp-rpc');
871+
const { readFile } = require('fs/promises');
872+
873+
// Read PEM-encoded certificate & key
874+
const cert = await readFile('./client.crt', 'utf8');
875+
const key = await readFile('./client.key', 'utf8');
876+
877+
const cli = new RPCClient({
878+
endpoint: 'wss://localhost',
879+
identity: 'EXAMPLE',
880+
wsOpts: { cert, key, minVersion: 'TLSv1.2' }
881+
});
882+
883+
await cli.connect();
884+
```
885+
886+
#### TLS Server Example
887+
888+
This example is very similar to the example for [security profile 2](#security-profile-2), except for these changes:
889+
890+
* The HTTPS server needs the option `requestCert: true` to allow the client to send its certificate.
891+
* The client's certificate can be inspected during the auth() callback via `handshake.request.client.getPeerCertificate()`.
892+
* A HTTP auth password is no longer required.
893+
894+
**Note:** If the client does not present a certificate (or the presented certificate is invalid), [`getPeerCertificate()`](https://nodejs.org/api/tls.html#tlssocketgetpeercertificatedetailed) will return an empty object instead.
895+
896+
```js
897+
const https = require('https');
898+
const { RPCServer } = require('ocpp-rpc');
899+
const { readFile } = require('fs/promises');
900+
901+
const server = new RPCServer();
902+
903+
const httpsServer = https.createServer({
904+
cert: [
905+
await readFile('./server.crt', 'utf8'), // RSA certificate
906+
await readFile('./ec_server.crt', 'utf8'), // ECDSA certificate
907+
],
908+
key: [
909+
await readFile('./server.key', 'utf8'), // RSA key
910+
await readFile('./ec_server.key', 'utf8'), // ECDSA key
911+
],
912+
minVersion: 'TLSv1.2', // require TLS >= v1.2
913+
requestCert: true, // ask client for a certificate
914+
});
915+
916+
httpsServer.on('upgrade', server.handleUpgrade);
917+
httpsServer.listen(443);
918+
919+
server.auth((accept, reject, handshake) => {
920+
const tlsClient = handshake.request.client;
921+
922+
if (!tlsClient) {
923+
return reject();
924+
}
925+
926+
console.log(`${handshake.identity} connected using TLS:`, {
927+
clientCert: tlsClient.getPeerCertificate(), // the certificate used by the client
928+
serverCert: tlsClient.getCertificate(), // the certificate used by the server
929+
cipher: tlsClient.getCipher(), // the cipher suite
930+
version: tlsClient.getProtocol(), // the TLS version
931+
});
932+
933+
accept();
934+
});
935+
```
936+
781937
## RPCClient state lifecycle
782938

783939
![RPCClient state lifecycle](./docs/statelifecycle.png)

0 commit comments

Comments
 (0)