@open-charging-cloud/wwcp-poi 0.1.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,362 +1,522 @@
1
- # WWCP POI for TypeScript
2
-
3
- [![CI](https://github.com/OpenChargingCloud/WWCP_POI.TS/actions/workflows/ci.yml/badge.svg)](https://github.com/OpenChargingCloud/WWCP_POI.TS/actions/workflows/ci.yml)
4
- [![Nightly](https://github.com/OpenChargingCloud/WWCP_POI.TS/actions/workflows/nightly.yml/badge.svg)](https://github.com/OpenChargingCloud/WWCP_POI.TS/actions/workflows/nightly.yml)
5
- [![npm](https://img.shields.io/npm/v/@open-charging-cloud/wwcp-poi)](https://www.npmjs.com/package/@open-charging-cloud/wwcp-poi)
6
-
7
- Verifies what a roaming network of [WWCP/POI](https://github.com/OpenChargingCloud/WWCP_POI) publishes about its
8
- charging stations and their tariffs, from the **trust anchor** of the roaming network alone: the QR code its operator
9
- publishes, naming the identity keys of the operator and its quorum.
10
-
11
- An EV driver's app selects a charging station; this library downloads the proof of the station and the price statements
12
- of its EVSEs, and checks that the station, its EVSEs as they are at runtime, the charging tariffs they take and the
13
- prices of the moment are the ones the roaming network operator attested and the charging station operator signed - before
14
- charging begins. Months later, e.g. disputing an invoice, the evidence the app kept proves which price applied, witnessed
15
- by checkpoints. The same proof gives the public keys of the energy meters, with which
16
- [ChargyCore.TS](https://github.com/OpenChargingCloud/ChargyCore.TS) checks the signed meter values of the session, and the
17
- transparency software and its certificate that say who checks them.
18
-
19
- Everything is calculated as the C# implementation calculates it, byte for byte: the canonical JSON and the deterministic
20
- CBOR of Styx, ETags, the effective tree with templates (section 8 of the specification), commit and checkpoint
21
- identifications of every epoch, COSE keys and their thumbprints, and the signatures ES256/384/512, Ed25519, Ed448 and
22
- ML-DSA-44/65/87.
23
-
24
-
25
- ## Status
26
-
27
- **Milestone 1: the examples of WWCP_POI, checked in TypeScript.** The examples A1 to A14 of
28
- [WWCP_POI/examples](https://github.com/OpenChargingCloud/WWCP_POI/tree/master/examples) at commit `33447ac` are checked
29
- from what the C# examples publish, as their `Validate()` methods check them, and the same for proofs of charging stations
30
- the C# implementation makes; also from a second run of the C# examples, with new keys. See [Tests](#tests).
31
-
32
- What a review of the C# implementation found while porting it is in
33
- [docs/csharp-review.md of WWCPPOIConformanceTests](https://github.com/OpenChargingCloud/WWCPPOIConformanceTests/blob/master/docs/csharp-review.md),
34
- cited here as *csharp-review N*.
35
-
36
- Whether both implementations really calculate, take and refuse the same is the business of
37
- [WWCPPOIConformanceTests](https://github.com/OpenChargingCloud/WWCPPOIConformanceTests): golden test vectors, run against
38
- this library and the C# implementation alike, and where they differ, written down and pinned
39
- ([docs/implementation-deviations.md](https://github.com/OpenChargingCloud/WWCPPOIConformanceTests/blob/master/docs/implementation-deviations.md)).
40
- This repository vendors the vectors and runs them in its own suite (`tests/conformance.test.ts`).
41
-
42
-
43
- ## Installation
44
-
45
- From npm, as [`@open-charging-cloud/wwcp-poi`](https://www.npmjs.com/package/@open-charging-cloud/wwcp-poi):
46
-
47
- ```bash
48
- npm install @open-charging-cloud/wwcp-poi
49
- ```
50
-
51
- ESM, Node 22.18 or newer, browsers and React Native alike: no `node:*` module, no `Buffer`, no WebCrypto. The
52
- cryptography is [`@vanaheimr/cose`](https://github.com/Vanaheimr/COSE.TS) on the `@noble` family, the CBOR codec
53
- [`@vanaheimr/metrological-cbor`](https://github.com/Vanaheimr/MetrologicalCBOR.TS).
54
-
55
-
56
- ## An app: a charging station and its prices
57
-
58
- ```ts
59
- import { POIClient } from '@open-charging-cloud/wwcp-poi';
60
-
61
- // The text of the QR code of the roaming network, as scanned, e.g. from the website of its operator.
62
- const client = new POIClient('WWCP/TA-2:B7LJ683VCBCACVCC$C6REF6B…');
63
-
64
- // GET ~/RNs/QA/EVSEs/DE*GEF*E1*1/proof and …/priceStatements, from the first server of the trust anchor whose answer verifies.
65
- const checked = await client.chargingStation({ evseId: 'DE*GEF*E1*1' });
66
-
67
- checked.station.roamingNetwork.name; // 'Quality Assurance', as the operator signed it, not as the QR code says
68
- checked.station.chargingStation; // the station as it is at runtime, with what its templates give it
69
- checked.station.tariffs.get('DE*GEF*E1*1'); // the charging tariffs the EVSE takes
70
- checked.station.manufacturer?.model; // its model, as its manufacturer states it: revisions, firmware, ratings
71
- checked.station.energyMeters; // with their public keys, for transparency software
72
- checked.station.energyMeterModels; // per meter: its manufacturer and model
73
- checked.station.certificates; // of the station and its meters: each for a model, with its components
74
- checked.station.transparencySoftware; // per meter: the release, its manufacturer, the certificates naming it
75
- checked.prices.get('EVSE:DE*GEF*E1*1')?.statement; // the price statement valid now, signed by DE*GEF
76
- checked.documents; // as they came: keep them as evidence of the session
77
- ```
78
-
79
- What is checked (sections 8.4 to 8.6, 12.4 of the specification):
80
-
81
- - the proof leads to a root, the `effectiveTree` ETag of a version of the roaming network;
82
- - the key statement of the roaming network operator the proof gives is signed by identity keys the trust anchor pins, of
83
- as many holders as its `minConsensus` requires, counted as the trust anchor names them per holder, names their holders
84
- so as well, and is valid now;
85
- - attestations of that root, valid now - signed then or before, until their `validUntil`, at most a day later - by
86
- signing keys of the operator, valid when they attested and now, each certified by a trusted identity key of its
87
- holder, come from as many holders, each counted for the holder of that identity key as the trust anchor knows it;
88
- - each price statement is signed by a signing key of the charging station operator that the proof gives and that may sign
89
- `priceStatements`, names only charging tariffs of its EVSE, and chains with the statements before it; it is current no
90
- longer than its key.
91
-
92
- When a roaming network moves into a new epoch and replaces the keys of its operator (example A11), its operator publishes
93
- a new QR code: a key statement signed only by the new keys is refused under the old trust anchor. A verifier that follows
94
- the history commit by commit follows the keys as well (`OperatorTrust.followedBy`), and finds the new trust anchor naming
95
- the keys it followed.
96
-
97
- Without the client, the same with documents of any source:
98
-
99
- ```ts
100
- import { TrustAnchor, verifyChargingStation, verifyPriceStatements } from '@open-charging-cloud/wwcp-poi';
101
-
102
- const anchor = TrustAnchor.parse(qrCodeText);
103
- const station = verifyChargingStation(anchor, proofJSON, { evseId: 'DE*GEF*E1*1' });
104
- const prices = verifyPriceStatements(station, priceStatementsJSON);
105
- ```
106
-
107
- Who checks the meter (example A13): the certificates the station and its energy meters name, each of a conformity
108
- assessment body for a model of charging stations or of energy meters, in hardware revisions and firmware versions, with
109
- its components, the transparency software and, of a station, the meters it mounts; and the transparency software each
110
- meter, and a station signing measurements itself, names with its legal status, proven by the certificates naming it.
111
- POI proves who states them, not that they are true. `statedBy` tells whether the body states a certificate or the
112
- roaming network operator in its place, as the certificate says, and `forThisModel` whether it is for the model, the
113
- hardware revision and the firmware version of what names it (csharp-review 15, 16, 17).
114
-
115
- ```ts
116
- for (const { namedBy, certificate, body, statedBy, forThisModel } of station.certificates)
117
- console.log(namedBy.entityId, certificate['@id'], certificate['legislation'], body['@id'], statedBy, forThisModel);
118
-
119
- for (const { namedBy, legalStatus, release, manufacturer, certificates } of station.transparencySoftware)
120
- console.log(namedBy.entityId, legalStatus, release['version'], manufacturer['craRole'], certificates.map(({ certificate }) => certificate['@id']));
121
- ```
122
-
123
- Months later, the evidence of the session (example A10): the station proof attested and the price statement valid as
124
- charging began, the commit of the station proof and the statement each proven to be part of checkpoints the trusted
125
- witnesses signed:
126
-
127
- ```ts
128
- import { parseEvidence, parseTrustedWitnesses, verifyEvidence } from '@open-charging-cloud/wwcp-poi';
129
-
130
- // driver/evidence-DE_GEF_E1_1.json, as the app of example A10 keeps it
131
- const evidence = verifyEvidence(parseEvidence(keptJSON), { witnesses: parseTrustedWitnesses(witnessesJSON), requiredWitnesses: 2 });
132
- ```
133
-
134
-
135
- ## The traffic report: the status of an EVSE, live
136
-
137
- The status of an EVSE, its energy status and the price shown at this moment are the runtime status of the head of the
138
- history (example A12), pushed over the WebSocket `~/RNs/{RN_Id}/history/ws` the moment they change. They are unsigned,
139
- as trustworthy as the server telling them, and no evidence: a price that has to be provable is a price statement. A
140
- commit pushed with them is signed like every other, and checked like every other.
141
-
142
- ```ts
143
- import { RuntimeStatus, parseFrame, parseStatusUpdate, subscribeRequest } from '@open-charging-cloud/wwcp-poi';
144
-
145
- const status = RuntimeStatus.fromSnapshot(snapshotJSON); // the runtime status beside the head, not part of its version
146
- socket.send(subscribeRequest('1')); // [2,"1","subscribe",{"topics":["statusUpdates", …, "commits"]}]
147
-
148
- socket.onmessage = event => {
149
- const frame = parseFrame(event.data); // [6, id, action, payload]: a push
150
- if (frame.type === 'push' && frame.action !== 'commit')
151
- status.apply(parseStatusUpdate(frame.payload)); // the latest timestamp wins: a late report is 'Outdated'
152
- };
153
-
154
- status.get('EVSE', 'DE*GEF*E1*1', 'status')?.value; // 'charging'
155
- ```
156
-
157
-
158
- ## A charging station operator, a witness, a server: versions and histories
159
-
160
- ```ts
161
- import { History, TrustAnchor, verifyVersion, verifyCheckpointSignatures } from '@open-charging-cloud/wwcp-poi';
162
-
163
- // Example A1: a version of the roaming network and its commit, from the trust anchor alone.
164
- const version = verifyVersion(TrustAnchor.parse(qrCodeText), networkJSON, commitJSON);
165
- version.signedBy; // [ 'AlphaOrg', 'BetaOrg', 'GammaOrg' ]
166
-
167
- // Examples A4 to A6: join from the snapshot of a checkpoint, anchored in the trust anchor (10.5): its checkpoint signed as
168
- // the roaming network operator by the holders the trust anchor requires; a snapshot of the head instead by the
169
- // attestations it carries. Then take the commits since one by one, each checked as section 5.2 says: its change set
170
- // signed by its owners, fitting the head, leading to the ETags of the commit. Signed commits and checkpoints are taken
171
- // only with a verifier of their signatures.
172
- let trust = version.trust;
173
- const history = History.fromSnapshot(checkpointSnapshotJSON, {
174
- trustAnchor: TrustAnchor.parse(qrCodeText),
175
- verifyCommit: (commit, before, after) => {
176
- trust.verifyOperatorSignatures(commit.signatures, (s, key) => commit.verifySignature(s, key), 'the commit');
177
- trust = trust.followedBy(after); // the keys its operator states anew, signed by those trusted
178
- },
179
- verifyCheckpoint: checkpoint => verifyCheckpointSignatures(checkpoint, trust, { witnesses, requiredWitnesses: 2 }),
180
- });
181
-
182
- for (const commit of commitsSince)
183
- history.append(commit);
184
-
185
- // Example A11: the final checkpoint closes the epoch; the next one starts from its version, its genesis the one every
186
- // server makes, SHA-512 identifications and ML-DSA signatures from then on.
187
- history.takeCheckpoint(finalCheckpointJSON);
188
- const epoch2 = history.startNextEpoch();
189
- epoch2.root.commit.id; // the published genesis of epoch 2, 128 hexadecimal digits
190
- ```
191
-
192
-
193
- ## What is ported, and what is not yet
194
-
195
- | | |
196
- |---|---|
197
- | Canonical JSON, deterministic CBOR (Styx) | complete, including Styx's string escaping and decimals as `System.Decimal` holds them |
198
- | Keys, thumbprints, signatures | EC2, OKP and AKP COSE keys, read in the words of WWCP_POI; all signature algorithms of section 4; the key usages `infrastructure`, `tariffs`, `priceStatements`, `models`, `software`, `certificates`, `payments`, `party` |
199
- | Trust anchors | QR code (`WWCP/TA-2:` and Base45 of CBOR), URL and JSON forms, read and written; key statements followed by succession |
200
- | Roaming networks | ETags `json`, `cbor`, `effectiveTree`; the roaming network operator, its key statement and certificates; the keys of every party and of the roaming network operator as WWCP_POI reads them, none twice, none both a signing and an identity key, no identity key naming usages; the identifications unique within the network (grid connection points, energy meters, parking entities, transparency software releases and certificates, energy meter templates and groups, groups of operators and providers, offers), each once, compared as the C# types compare them; hardware revisions, firmware versions and serial numbers as WWCP_POI reads them, not empty, no whitespace at either end, those of a model each once; certificates as WWCP_POI reads them, for exactly one model, with its versions and releases; the conformity assessment bodies issuing them as WWCP_POI reads them, a notified body number of four digits for one body only, the legislations they are notified for neither empty nor twice, in their ordinal order, a certificate only under one of them; the content as WWCP_POI writes it (SPECIFICATION.md 2): what it takes and writes otherwise is refused, as written again it would be another text than the one hashed (ordered lists out of their order or with untrimmed texts, lists it leaves out given empty or writes also empty left out, properties given as null, embedded objects naming a JSON-LD context, texts in languages not as Styx writes them or empty, timestamps not UTC to the millisecond, entities without their lastChange, parties without their name, identifications given as numbers, properties of keys and of EV Taler merchants and exchanges it does not read, their base URLs and currencies not as it writes them); the roaming network operator, its key statement and certificates, the tree hash algorithms and the identifications of entities in the words of WWCP_POI.; of every entity what its type in WWCP_POI3 neither writes nor reads, and values it writes otherwise (trimmed, with a scheme, in its case, coordinates as doubles), in the words of its written form since 3de830a ("the content leaves it out", "the content holds it as …"). Not yet: properties of another JSON type or missing, the formats of identifications, and the references, which WWCP_POI refuses |
201
- | Effective members (9.2) | EVSEs, charging stations, charging pools, charging tariffs, energy meters. Parking spaces and energy meters of grid connection points taking templates are refused, not calculated |
202
- | Proofs (8.4), attestations (8.5) | complete, with the manufacturers and models of the station and its meters, the transparency software releases and certificates of the meters; subject attestations do not count for a whole proof |
203
- | Change sets (4) | `Add`, `Remove`, `UpdateProperty`, `RemoveProperty` of parties (charging station operators, e-mobility providers, grid and parking operators, charging station and energy meter manufacturers, transparency software manufacturers, conformity assessment bodies), the roaming network and its operator, of charging pools, stations, EVSEs, tariffs, templates and groups of charging station operators, energy meter templates and groups, models of manufacturers, transparency software releases and certificates, with their owners' signatures, quorum and proof of possession, and the properties a change set may change: not the version of a release, also not by removing it and adding it again; the `notifiedBodyNumber` and `notifiedFor` of a conformity assessment body by the roaming network operator only. Element changes, preconditions, offers and the entities of providers, grid and parking operators are refused, not applied. The references the C# implementation checks after every change set (a manufacturer, model, release or certificate a station, meter or certificate names) are not checked again: the `afterETags` of the change set pin the version it leads to. The identifications unique within the network are, as in every version |
204
- | Histories (5, 10, 11) | linear histories from snapshots, anchored in the trust anchor as a peer of WWCP_POI starts (10.5): a snapshot of a checkpoint by the checkpoint, signed as the roaming network operator, a snapshot of the head by its attestations, none without a trust anchor unless taken unanchored explicitly; commits, checkpoints, their lines and signatures, signed ones only with a verifier; proofs of commits, archive segments; epochs: their profiles, the final checkpoint, the genesis of the next epoch, its first checkpoint. Merges are not taken yet; no peer, no server |
205
- | Price statements (12) | statements, chains, checkpoint trees and proofs in every epoch |
206
- | Runtime status (12.5) | status updates, frames of the WebSocket, the runtime status of a snapshot. No WebSocket client: the app brings its own |
207
- | QI-Digital documents | a proof of concept of WWCP_POI, as example A14 uses it: digital calibration certificates, digital certificates of conformity of a model or of the quality system of a manufacturer, digital declarations of conformity, read in the words of WWCP_POI and checked against a roaming network (the keys of their issuers, the models, the charging stations and energy meters as they are at runtime) and against each other (a declaration and the certificates it names, a certificate and the meter it covers). Their signature input is what WWCP_POI writes again of what it read, numbers without trailing zeros. Read and checked, not signed |
208
- | EV Taler | phase 1 of WWCP_POI's concept of paying for charging with GNU Taler: the merchant of a charging station operator (`evTaler`), read and refused as WWCP_POI reads it, changed by a key that may sign `payments`; payment options an open set of names, none twice whatever its case; the payment option EVTaler of a charging pool or template only with a merchant; the EV Taler tariff of an EVSE and the exchanges in its currency, from the proof of its station (`evTalerTariffOf`, `evTalerExchangesAt`, `paymentOptionName`). Nothing here takes or makes a payment |
209
-
210
- Whatever is not ported is refused with a `POIError` saying so, but for the references named above: the library never
211
- takes as checked what it can not check.
212
- And whatever it calculated otherwise than the C# implementation would end in other ETags or identifications, and be
213
- refused as well.
214
-
215
-
216
- ## Local authorization and shared trust contexts
217
-
218
- An app that lets its users import a trust anchor before they trust its keys does not hand that anchor to a verifier: a
219
- verifier given a `TrustAnchor` trusts every identity key it pins. The app evaluates a `TrustContext` instead, from the
220
- identity keys it trusts locally:
221
-
222
- ```ts
223
- import { ChargingStationProof, RoamingNetworkOperator, TrustContext, verifyChargingStation } from '@open-charging-cloud/wwcp-poi';
224
-
225
- // The key statement and the identity keys of the operator as the proof gives them: trusted only once evaluated.
226
- const proof = ChargingStationProof.parse(proofJSON);
227
- const operator = RoamingNetworkOperator.parse(proof.roamingNetwork['operator'], anchor.id);
228
-
229
- if (operator.keyStatement !== undefined) {
230
-
231
- const result = TrustContext.evaluate(anchor, operator.keyStatement, {
232
- trustedIdentityKeyIds: locallyAuthorizedKeyIds,
233
- otherIdentityKeys: operator.identityKeys,
234
- at: Date.now()
235
- });
236
-
237
- // Show signatures, signers, quorum and validity apart. canTrustKeys allows importing
238
- // validly signed data even without a trusted quorum.
239
- if (result.context !== undefined) {
240
- const station = verifyChargingStation(result.context, proof, { evseId: 'DE*GEF*E1*1' });
241
- }
242
-
243
- }
244
- ```
245
-
246
- `TrustContext.evaluate` keeps of the holders of the trust anchor the identity keys the app trusts, and checks every
247
- signature of the key statement and that its key was valid when the statement was signed, that the statement is valid at
248
- `at`, that it names the holders of the trusted keys as the trust anchor does, and that trusted keys of as many holders
249
- as the larger of both quorums signed it. Two trusted keys of one holder still count once. A context exists only when all
250
- of that holds; `TrustContext.create` evaluates the same and throws otherwise.
251
-
252
- The context binds what is verified with it to the roaming network, its operator and the signed content of the key
253
- statement, which a statement embedded without its JSON-LD context gives as well. Another key statement, even one signed
254
- genuinely, needs another evaluation: a context does not follow the keys of the operator to their successors by itself.
255
- Other valid signatures, or the same in another order, do not change the content; every signature given and the trusted
256
- quorum are checked again.
257
-
258
- `OperatorTrust.of` and `fromOperatorJSON`, `verifyVersion`, `verifyChargingStation`, `ChargingStationProof.verify` and
259
- `verifyCheckpoint` take a context in place of a trust anchor. A context is evaluated for one time: `at` is that time or
260
- left out, another time needs another context. Given a trust anchor, they verify as before, trusting every key it pins.
261
- The other APIs taking a trust anchor, among them `verifyEvidence` and `POIClient`, take only one the app has authorized:
262
- they are no way around its local policy.
263
-
264
- The app keeps storing and showing its decisions, choosing keys and excluding them locally. It reads its current
265
- decisions consistently and creates a context from them for every verification: a context is no authority to store, nor
266
- to use again after a key was withdrawn or deleted or the policy changed, and does not see what changes after it was
267
- created. The library needs no IndexedDB, localStorage, network, DOM, Node or anything of an app.
268
-
269
- Removing a pinned key from the local roots removes the trust it started with. A key statement signed by the quorum may
270
- still name it among its identity keys and so delegate to it: excluding a key locally is no global revocation or deny
271
- list. Revocation and verified succession need policies of their own. Unknown imports, invalid evidence and expired data
272
- never give an authorized context.
273
-
274
-
275
- ## Tests
276
-
277
- ```bash
278
- npm run verify
279
- ```
280
-
281
- `verify` runs what CI runs: the type checks (as Node and as a browser sees `src/`), ESLint with the rules of
282
- ChargyCore.TS, and `npm test`, which is
283
-
284
- - `npm run test:node`: the tests below, in Node;
285
- - `npm run build` and `npm run test:bundle`: what `dist/` holds, as an app gets it: the bundle imports only the
286
- dependencies `package.json` names and neither `node:*` modules, `Buffer` nor `process`; an app imports it by the name
287
- of the package in plain Node and gets its declared types with TypeScript (`nodenext` and `bundler`); `npm pack`
288
- publishes nothing but `dist/` and the documents;
289
- - `npm run test:browser`: a trust anchor, a commit, a station proof of ML-DSA attestations, a price and the evidence of
290
- a session, checked in Chromium (Vitest browser mode with Playwright; once: `npx playwright install chromium`).
291
-
292
- GitHub Actions run it on every push and pull request (`.github/workflows/ci.yml`): Ubuntu on Node 22.18, the minimum
293
- `package.json` names, and on Node 26, Windows on Node 24. Every night (`.github/workflows/nightly.yml`) the same suite
294
- runs again, and, informational, with the dependencies the lockfile does not pin, with the newest releases of
295
- `@vanaheimr/cose` and `@vanaheimr/metrological-cbor`, and as `npm audit`. The C# implementation as it is today is checked
296
- every night by [WWCPPOIConformanceTests](https://github.com/OpenChargingCloud/WWCPPOIConformanceTests): its nightly run
297
- holds WWCP_POI master and this library to the same golden vectors, and runs the C# examples of WWCP_POI master anew,
298
- checked with `examples/validate-examples.ts` of this repository, which turns red the day WWCP_POI moves on and this port
299
- needs work.
300
-
301
- The test vectors are in `tests/vectors`, vendored from
302
- [WWCPPOIConformanceTests](https://github.com/OpenChargingCloud/WWCPPOIConformanceTests), where they are generated -
303
- never edit them here, and its `VendoredCopiesTests` fail when these copies drift:
304
-
305
- - `tests/vectors/poi-*.json`, the golden vectors: canonical JSON and CBOR, Base64 and Base45, the examples of section 6
306
- of the specification, station proofs of its example network signed with its keys against a trust anchor, what to
307
- calculate of the documents of the run below, documents to refuse, the QI-Digital documents of example A14 of that run
308
- taken and refused as WWCP_POI checks them, and small histories WWCPPOIConformanceTests signs itself, replayed step by
309
- step (the format: the
310
- [README of the vectors](https://github.com/OpenChargingCloud/WWCPPOIConformanceTests/blob/master/test-vectors/README.md));
311
- - `tests/vectors/csharp-33447ac`, what one run of the C# examples of WWCP_POI at commit `33447ac` published (public data
312
- only; its output is `csharp-output.log` there), and in `proofs/`, what `proofs/MakeProofs.cs.txt` made of the same
313
- history with the C# implementation: proofs of charging stations of epoch 2, the commits of epoch 2 up to the head, and
314
- the ECDSA twin of a price statement.
315
-
316
- - `tests/conformance.test.ts`: the golden vectors, as the C# implementation runs them in WWCPPOIConformanceTests.
317
- - `tests/canonical.test.ts`, `tests/specification.test.ts`: the test vectors of Styx and of section 6 of the
318
- specification.
319
- - `tests/exampleA1.test.ts`, `tests/examples.test.ts`: the examples A1 to A9, epoch 1, the manufacturers and the models
320
- they state among them.
321
- - `tests/evidence.test.ts`: example A10, the evidence a driver's app keeps, and ECDSA twins of price statements.
322
- - `tests/exampleA11.test.ts`: example A11, epoch 2, followed with the old trust anchor and joined with the new one.
323
- - `tests/exampleA12.test.ts`: example A12, the runtime status and the commit pushed over the WebSocket.
324
- - `tests/exampleA13.test.ts`: example A13, transparency software manufacturers, conformity assessment bodies, and who
325
- checks the meter.
326
- - `tests/qidigital.test.ts`: example A14, the QI-Digital documents of a meter as WWCP_POI signed them, and others signed
327
- with the keys of section 6 of the specification, to change them at will.
328
- - `tests/chargingStation.test.ts`, `tests/client.test.ts`: proofs of charging stations of epoch 2, as an app checks them.
329
- - `tests/replay.test.ts`: the whole history of `QA`, both epochs, commit by commit, with every checkpoint.
330
- - `tests/build/bundle.test.js`, `tests/browser/browser.test.ts`: the bundle and the browser, as above.
331
-
332
- To check a run of your own of the C# examples, with its new keys:
333
-
334
- ```bash
335
- npx tsx examples/validate-examples.ts D:\Coding\OpenChargingCloud\WWCP_POI\examples\csharp
336
- ```
337
-
338
-
339
- ## Publishing
340
-
341
- ```
342
- npm version 0.1.1 --no-git-tag-version
343
- git commit -a
344
- git tag -s v0.1.1 -m "v0.1.1"
345
- git push origin master v0.1.1
346
- git push git1 master v0.1.1
347
- git push git2 master v0.1.1
348
-
349
- npm run verify
350
- npm pack --dry-run
351
- npm pack
352
- npm login
353
- npm whoami
354
- npm publish
355
- ```
356
-
357
-
358
- ## License
359
-
360
- [GNU Affero General Public License v3.0](LICENSE), as WWCP_POI.
361
-
362
- Copyright 2014-2026 GraphDefined GmbH
1
+ # WWCP POI for TypeScript
2
+
3
+ [![CI](https://github.com/OpenChargingCloud/WWCP_POI.TS/actions/workflows/ci.yml/badge.svg)](https://github.com/OpenChargingCloud/WWCP_POI.TS/actions/workflows/ci.yml)
4
+ [![Nightly](https://github.com/OpenChargingCloud/WWCP_POI.TS/actions/workflows/nightly.yml/badge.svg)](https://github.com/OpenChargingCloud/WWCP_POI.TS/actions/workflows/nightly.yml)
5
+ [![npm](https://img.shields.io/npm/v/@open-charging-cloud/wwcp-poi)](https://www.npmjs.com/package/@open-charging-cloud/wwcp-poi)
6
+
7
+ Verifies what a roaming network of [WWCP/POI](https://github.com/OpenChargingCloud/WWCP_POI) publishes about its
8
+ charging stations and their tariffs, from the **trust anchor** of the roaming network alone: the QR code its operator
9
+ publishes, naming the identity keys of the operator and its quorum.
10
+
11
+ An EV driver's app selects a charging station; this library downloads the proof of the station and the price statements
12
+ of its EVSEs, and checks that the station, its EVSEs as they are at runtime, the charging tariffs they take and the
13
+ prices of the moment are the ones the roaming network operator attested and the charging station operator signed - before
14
+ charging begins. Months later, e.g. disputing an invoice, the evidence the app kept proves which price applied, witnessed
15
+ by checkpoints. The same proof gives the public keys of the energy meters, with which
16
+ [ChargyCore.TS](https://github.com/OpenChargingCloud/ChargyCore.TS) checks the signed meter values of the session, and the
17
+ transparency software and its certificate that say who checks them.
18
+
19
+ Everything is calculated as the C# implementation calculates it, byte for byte: the canonical JSON and the deterministic
20
+ CBOR of Styx, ETags, the effective tree with templates (section 8 of the specification), commit and checkpoint
21
+ identifications of every epoch, COSE keys and their thumbprints, and the signatures ES256/384/512, Ed25519, Ed448 and
22
+ ML-DSA-44/65/87.
23
+
24
+
25
+ ## Status
26
+
27
+ **0.2.0 is prepared, not yet published to npm.** It includes the shared trust and session verification APIs previously
28
+ supplied as 0.1.2-dev.0 development packages. See [the changelog](CHANGELOG.md#020-prepared-not-yet-published) for the
29
+ changes and migration requirements. The experimental profiles below remain experimental; a package release does not
30
+ establish production witness services or independently verified actual measurement time.
31
+
32
+ **Milestone 1: the examples of WWCP_POI, checked in TypeScript.** The examples A1 to A14 of
33
+ [WWCP_POI/examples](https://github.com/OpenChargingCloud/WWCP_POI/tree/master/examples) at commit `33447ac` are checked
34
+ from what the C# examples publish, as their `Validate()` methods check them, and the same for proofs of charging stations
35
+ the C# implementation makes; also from a second run of the C# examples, with new keys. See [Tests](#tests).
36
+
37
+ What a review of the C# implementation found while porting it is in
38
+ [docs/csharp-review.md of WWCPPOIConformanceTests](https://github.com/OpenChargingCloud/WWCPPOIConformanceTests/blob/master/docs/csharp-review.md),
39
+ cited here as *csharp-review N*.
40
+
41
+ Whether both implementations really calculate, take and refuse the same is the business of
42
+ [WWCPPOIConformanceTests](https://github.com/OpenChargingCloud/WWCPPOIConformanceTests): golden test vectors, run against
43
+ this library and the C# implementation alike, and where they differ, written down and pinned
44
+ ([docs/implementation-deviations.md](https://github.com/OpenChargingCloud/WWCPPOIConformanceTests/blob/master/docs/implementation-deviations.md)).
45
+ This repository vendors the vectors and runs them in its own suite (`tests/conformance.test.ts`).
46
+
47
+
48
+ ## Installation
49
+
50
+ From npm, as [`@open-charging-cloud/wwcp-poi`](https://www.npmjs.com/package/@open-charging-cloud/wwcp-poi):
51
+
52
+ ```bash
53
+ npm install @open-charging-cloud/wwcp-poi
54
+ ```
55
+
56
+ ESM, Node 22.18 or newer, browsers and React Native alike: no `node:*` module, no `Buffer`, no WebCrypto. The
57
+ cryptography is [`@vanaheimr/cose`](https://github.com/Vanaheimr/COSE.TS) on the `@noble` family, the CBOR codec
58
+ [`@vanaheimr/metrological-cbor`](https://github.com/Vanaheimr/MetrologicalCBOR.TS).
59
+
60
+
61
+ ## An app: a charging station and its prices
62
+
63
+ ```ts
64
+ import { POIClient } from '@open-charging-cloud/wwcp-poi';
65
+
66
+ // The text of the QR code of the roaming network, as scanned, e.g. from the website of its operator.
67
+ const client = new POIClient('WWCP/TA-2:B7LJ683VCBCACVCC$C6REF6B…');
68
+
69
+ // GET ~/RNs/QA/EVSEs/DE*GEF*E1*1/proof and …/priceStatements, from the first server of the trust anchor whose answer verifies.
70
+ const checked = await client.chargingStation({ evseId: 'DE*GEF*E1*1' });
71
+
72
+ checked.station.roamingNetwork.name; // 'Quality Assurance', as the operator signed it, not as the QR code says
73
+ checked.station.chargingStation; // the station as it is at runtime, with what its templates give it
74
+ checked.station.tariffs.get('DE*GEF*E1*1'); // the charging tariffs the EVSE takes
75
+ checked.station.manufacturer?.model; // its model, as its manufacturer states it: revisions, firmware, ratings
76
+ checked.station.energyMeters; // with their public keys, for transparency software
77
+ checked.station.energyMeterModels; // per meter: its manufacturer and model
78
+ checked.station.certificates; // of the station and its meters: each for a model, with its components
79
+ checked.station.transparencySoftware; // per meter: the release, its manufacturer, the certificates naming it
80
+ checked.prices.get('EVSE:DE*GEF*E1*1')?.statement; // the price statement valid now, signed by DE*GEF
81
+ checked.documents; // as they came: keep them as evidence of the session
82
+ ```
83
+
84
+ What is checked (sections 8.4 to 8.6, 12.4 of the specification):
85
+
86
+ - the proof leads to a root, the `effectiveTree` ETag of a version of the roaming network;
87
+ - the key statement of the roaming network operator the proof gives is signed by identity keys the trust anchor pins, of
88
+ as many holders as its `minConsensus` requires, counted as the trust anchor names them per holder, names their holders
89
+ so as well, and is valid now;
90
+ - attestations of that root, valid now - signed then or before, until their `validUntil`, at most a day later - by
91
+ signing keys of the operator, valid when they attested and now, each certified by a trusted identity key of its
92
+ holder, come from as many holders, each counted for the holder of that identity key as the trust anchor knows it;
93
+ - each price statement is signed by a signing key of the charging station operator that the proof gives and that may sign
94
+ `priceStatements`, names only charging tariffs of its EVSE, and chains with the statements before it; it is current no
95
+ longer than its key.
96
+
97
+ When a roaming network moves into a new epoch and replaces the keys of its operator (example A11), its operator publishes
98
+ a new QR code: a key statement signed only by the new keys is refused under the old trust anchor. A verifier that follows
99
+ the history commit by commit follows the keys as well (`OperatorTrust.followedBy`), and finds the new trust anchor naming
100
+ the keys it followed.
101
+
102
+ Without the client, the same with documents of any source:
103
+
104
+ ```ts
105
+ import { TrustAnchor, verifyChargingStation, verifyPriceStatements } from '@open-charging-cloud/wwcp-poi';
106
+
107
+ const anchor = TrustAnchor.parse(qrCodeText);
108
+ const station = verifyChargingStation(anchor, proofJSON, { evseId: 'DE*GEF*E1*1' });
109
+ const prices = verifyPriceStatements(station, priceStatementsJSON);
110
+ ```
111
+
112
+ Who checks the meter (example A13): the certificates the station and its energy meters name, each of a conformity
113
+ assessment body for a model of charging stations or of energy meters, in hardware revisions and firmware versions, with
114
+ its components, the transparency software and, of a station, the meters it mounts; and the transparency software each
115
+ meter, and a station signing measurements itself, names with its legal status, proven by the certificates naming it.
116
+ POI proves who states them, not that they are true. `statedBy` tells whether the body states a certificate or the
117
+ roaming network operator in its place, as the certificate says, and `forThisModel` whether it is for the model, the
118
+ hardware revision and the firmware version of what names it (csharp-review 15, 16, 17).
119
+
120
+ ```ts
121
+ for (const { namedBy, certificate, body, statedBy, forThisModel } of station.certificates)
122
+ console.log(namedBy.entityId, certificate['@id'], certificate['legislation'], body['@id'], statedBy, forThisModel);
123
+
124
+ for (const { namedBy, legalStatus, release, manufacturer, certificates } of station.transparencySoftware)
125
+ console.log(namedBy.entityId, legalStatus, release['version'], manufacturer['craRole'], certificates.map(({ certificate }) => certificate['@id']));
126
+ ```
127
+
128
+ Months later, the evidence of the session (example A10): the station proof attested and the price statement valid as
129
+ charging began, the commit of the station proof and the statement each proven to be part of checkpoints the trusted
130
+ witnesses signed:
131
+
132
+ ```ts
133
+ import { parseEvidence, parseTrustedWitnesses, verifyEvidence } from '@open-charging-cloud/wwcp-poi';
134
+
135
+ // driver/evidence-DE_GEF_E1_1.json, as the app of example A10 keeps it
136
+ const evidence = verifyEvidence(parseEvidence(keptJSON), { witnesses: parseTrustedWitnesses(witnessesJSON), requiredWitnesses: 2 });
137
+ ```
138
+
139
+
140
+ ## The traffic report: the status of an EVSE, live
141
+
142
+ The status of an EVSE, its energy status and the price shown at this moment are the runtime status of the head of the
143
+ history (example A12), pushed over the WebSocket `~/RNs/{RN_Id}/history/ws` the moment they change. They are unsigned,
144
+ as trustworthy as the server telling them, and no evidence: a price that has to be provable is a price statement. A
145
+ commit pushed with them is signed like every other, and checked like every other.
146
+
147
+ ```ts
148
+ import { RuntimeStatus, parseFrame, parseStatusUpdate, subscribeRequest } from '@open-charging-cloud/wwcp-poi';
149
+
150
+ const status = RuntimeStatus.fromSnapshot(snapshotJSON); // the runtime status beside the head, not part of its version
151
+ socket.send(subscribeRequest('1')); // [2,"1","subscribe",{"topics":["statusUpdates", …, "commits"]}]
152
+
153
+ socket.onmessage = event => {
154
+ const frame = parseFrame(event.data); // [6, id, action, payload]: a push
155
+ if (frame.type === 'push' && frame.action !== 'commit')
156
+ status.apply(parseStatusUpdate(frame.payload)); // the latest timestamp wins: a late report is 'Outdated'
157
+ };
158
+
159
+ status.get('EVSE', 'DE*GEF*E1*1', 'status')?.value; // 'charging'
160
+ ```
161
+
162
+
163
+ ## A charging station operator, a witness, a server: versions and histories
164
+
165
+ ```ts
166
+ import { History, TrustAnchor, verifyVersion, verifyCheckpointSignatures } from '@open-charging-cloud/wwcp-poi';
167
+
168
+ // Example A1: a version of the roaming network and its commit, from the trust anchor alone.
169
+ const version = verifyVersion(TrustAnchor.parse(qrCodeText), networkJSON, commitJSON);
170
+ version.signedBy; // [ 'AlphaOrg', 'BetaOrg', 'GammaOrg' ]
171
+
172
+ // Examples A4 to A6: join from the snapshot of a checkpoint, anchored in the trust anchor (10.5): its checkpoint signed as
173
+ // the roaming network operator by the holders the trust anchor requires; a snapshot of the head instead by the
174
+ // attestations it carries. Then take the commits since one by one, each checked as section 5.2 says: its change set
175
+ // signed by its owners, fitting the head, leading to the ETags of the commit. Signed commits and checkpoints are taken
176
+ // only with a verifier of their signatures.
177
+ let trust = version.trust;
178
+ const history = History.fromSnapshot(checkpointSnapshotJSON, {
179
+ trustAnchor: TrustAnchor.parse(qrCodeText),
180
+ verifyCommit: (commit, before, after) => {
181
+ trust.verifyOperatorSignatures(commit.signatures, (s, key) => commit.verifySignature(s, key), 'the commit');
182
+ trust = trust.followedBy(after); // the keys its operator states anew, signed by those trusted
183
+ },
184
+ verifyCheckpoint: checkpoint => verifyCheckpointSignatures(checkpoint, trust, { witnesses, requiredWitnesses: 2 }),
185
+ });
186
+
187
+ for (const commit of commitsSince)
188
+ history.append(commit);
189
+
190
+ // Example A11: the final checkpoint closes the epoch; the next one starts from its version, its genesis the one every
191
+ // server makes, SHA-512 identifications and ML-DSA signatures from then on.
192
+ history.takeCheckpoint(finalCheckpointJSON);
193
+ const epoch2 = history.startNextEpoch();
194
+ epoch2.root.commit.id; // the published genesis of epoch 2, 128 hexadecimal digits
195
+ ```
196
+
197
+
198
+ ## What is ported, and what is not yet
199
+
200
+ | | |
201
+ |---|---|
202
+ | Canonical JSON, deterministic CBOR (Styx) | complete, including Styx's string escaping and decimals as `System.Decimal` holds them |
203
+ | Keys, thumbprints, signatures | EC2, OKP and AKP COSE keys, read in the words of WWCP_POI; all signature algorithms of section 4; the key usages `infrastructure`, `tariffs`, `priceStatements`, `models`, `software`, `certificates`, `payments`, `party` |
204
+ | Trust anchors | QR code (`WWCP/TA-2:` and Base45 of CBOR), URL and JSON forms, read and written; key statements followed by succession |
205
+ | Roaming networks | ETags `json`, `cbor`, `effectiveTree`; the roaming network operator, its key statement and certificates; the keys of every party and of the roaming network operator as WWCP_POI reads them, none twice, none both a signing and an identity key, no identity key naming usages; the identifications unique within the network (grid connection points, energy meters, parking entities, transparency software releases and certificates, energy meter templates and groups, groups of operators and providers, offers), each once, compared as the C# types compare them; hardware revisions, firmware versions and serial numbers as WWCP_POI reads them, not empty, no whitespace at either end, those of a model each once; certificates as WWCP_POI reads them, for exactly one model, with its versions and releases; the conformity assessment bodies issuing them as WWCP_POI reads them, a notified body number of four digits for one body only, the legislations they are notified for neither empty nor twice, in their ordinal order, a certificate only under one of them; the content as WWCP_POI writes it (SPECIFICATION.md 2): what it takes and writes otherwise is refused, as written again it would be another text than the one hashed (ordered lists out of their order or with untrimmed texts, lists it leaves out given empty or writes also empty left out, properties given as null, embedded objects naming a JSON-LD context, texts in languages not as Styx writes them or empty, timestamps not UTC to the millisecond, entities without their lastChange, parties without their name, identifications given as numbers, properties of keys and of EV Taler merchants and exchanges it does not read, their base URLs and currencies not as it writes them); the roaming network operator, its key statement and certificates, the tree hash algorithms and the identifications of entities in the words of WWCP_POI.; of every entity what its type in WWCP_POI3 neither writes nor reads, and values it writes otherwise (trimmed, with a scheme, in its case, coordinates as doubles), in the words of its written form since 3de830a ("the content leaves it out", "the content holds it as …"). Not yet: properties of another JSON type or missing, the formats of identifications, and the references, which WWCP_POI refuses |
206
+ | Effective members (9.2) | EVSEs, charging stations, charging pools, charging tariffs, energy meters. Parking spaces and energy meters of grid connection points taking templates are refused, not calculated |
207
+ | Proofs (8.4), attestations (8.5) | complete, with the manufacturers and models of the station and its meters, the transparency software releases and certificates of the meters; subject attestations do not count for a whole proof |
208
+ | Change sets (4) | `Add`, `Remove`, `UpdateProperty`, `RemoveProperty` of parties (charging station operators, e-mobility providers, grid and parking operators, charging station and energy meter manufacturers, transparency software manufacturers, conformity assessment bodies), the roaming network and its operator, of charging pools, stations, EVSEs, tariffs, templates and groups of charging station operators, energy meter templates and groups, models of manufacturers, transparency software releases and certificates, with their owners' signatures, quorum and proof of possession, and the properties a change set may change: not the version of a release, also not by removing it and adding it again; the `notifiedBodyNumber` and `notifiedFor` of a conformity assessment body by the roaming network operator only. Element changes, preconditions, offers and the entities of providers, grid and parking operators are refused, not applied. The references the C# implementation checks after every change set (a manufacturer, model, release or certificate a station, meter or certificate names) are not checked again: the `afterETags` of the change set pin the version it leads to. The identifications unique within the network are, as in every version |
209
+ | Histories (5, 10, 11) | linear histories from snapshots, anchored in the trust anchor as a peer of WWCP_POI starts (10.5): a snapshot of a checkpoint by the checkpoint, signed as the roaming network operator, a snapshot of the head by its attestations, none without a trust anchor unless taken unanchored explicitly; commits, checkpoints, their lines and signatures, signed ones only with a verifier; proofs of commits, archive segments; epochs: their profiles, the final checkpoint, the genesis of the next epoch, its first checkpoint. Merges are not taken yet; no peer, no server |
210
+ | Price statements (12) | statements, chains, checkpoint trees and proofs in every epoch |
211
+ | Runtime status (12.5) | status updates, frames of the WebSocket, the runtime status of a snapshot. No WebSocket client: the app brings its own |
212
+ | QI-Digital documents | a proof of concept of WWCP_POI, as example A14 uses it: digital calibration certificates, digital certificates of conformity of a model or of the quality system of a manufacturer, digital declarations of conformity, read in the words of WWCP_POI and checked against a roaming network (the keys of their issuers, the models, the charging stations and energy meters as they are at runtime) and against each other (a declaration and the certificates it names, a certificate and the meter it covers). Their signature input is what WWCP_POI writes again of what it read, numbers without trailing zeros. Read and checked, not signed |
213
+ | EV Taler | phase 1 of WWCP_POI's concept of paying for charging with GNU Taler: the merchant of a charging station operator (`evTaler`), read and refused as WWCP_POI reads it, changed by a key that may sign `payments`; payment options an open set of names, none twice whatever its case; the payment option EVTaler of a charging pool or template only with a merchant; the EV Taler tariff of an EVSE and the exchanges in its currency, from the proof of its station (`evTalerTariffOf`, `evTalerExchangesAt`, `paymentOptionName`). Nothing here takes or makes a payment |
214
+
215
+ Whatever is not ported is refused with a `POIError` saying so, but for the references named above: the library never
216
+ takes as checked what it can not check.
217
+ And whatever it calculated otherwise than the C# implementation would end in other ETags or identifications, and be
218
+ refused as well.
219
+
220
+
221
+ ## Local authorization and shared trust contexts
222
+
223
+ An app that lets its users import a trust anchor before they trust its keys does not hand that anchor to a verifier: a
224
+ verifier given a `TrustAnchor` trusts every identity key it pins. The app evaluates a `TrustContext` instead, from the
225
+ identity keys it trusts locally:
226
+
227
+ ```ts
228
+ import { ChargingStationProof, RoamingNetworkOperator, TrustContext, verifyChargingStation } from '@open-charging-cloud/wwcp-poi';
229
+
230
+ // The key statement and the identity keys of the operator as the proof gives them: trusted only once evaluated.
231
+ const proof = ChargingStationProof.parse(proofJSON);
232
+ const operator = RoamingNetworkOperator.parse(proof.roamingNetwork['operator'], anchor.id);
233
+
234
+ if (operator.keyStatement !== undefined) {
235
+
236
+ const result = TrustContext.evaluate(anchor, operator.keyStatement, {
237
+ trustedIdentityKeyIds: locallyAuthorizedKeyIds,
238
+ otherIdentityKeys: operator.identityKeys,
239
+ at: Date.now()
240
+ });
241
+
242
+ // Show signatures, signers, quorum and validity apart. canTrustKeys allows importing
243
+ // validly signed data even without a trusted quorum.
244
+ if (result.context !== undefined) {
245
+ const station = verifyChargingStation(result.context, proof, { evseId: 'DE*GEF*E1*1' });
246
+ }
247
+
248
+ }
249
+ ```
250
+
251
+ `TrustContext.evaluate` keeps of the holders of the trust anchor the identity keys the app trusts, and checks every
252
+ signature of the key statement and that its key was valid when the statement was signed, that the statement is valid at
253
+ `at`, that it names the holders of the trusted keys as the trust anchor does, and that trusted keys of as many holders
254
+ as the larger of both quorums signed it. Two trusted keys of one holder still count once. A context exists only when all
255
+ of that holds; `TrustContext.create` evaluates the same and throws otherwise.
256
+
257
+ The context binds what is verified with it to the roaming network, its operator and the signed content of the key
258
+ statement, which a statement embedded without its JSON-LD context gives as well. Another key statement, even one signed
259
+ genuinely, needs another evaluation: a context does not follow the keys of the operator to their successors by itself.
260
+ Other valid signatures, or the same in another order, do not change the content; every signature given and the trusted
261
+ quorum are checked again.
262
+
263
+ `OperatorTrust.of` and `fromOperatorJSON`, `verifyVersion`, `verifyChargingStation`, `ChargingStationProof.verify` and
264
+ `verifyCheckpoint` take a context in place of a trust anchor. A context is evaluated for one time: `at` is that time or
265
+ left out, another time needs another context. Given a trust anchor, they verify as before, trusting every key it pins.
266
+ The other APIs taking a trust anchor, among them `verifyEvidence` and `POIClient`, take only one the app has authorized:
267
+ they are no way around its local policy.
268
+
269
+ The app keeps storing and showing its decisions, choosing keys and excluding them locally. It reads its current
270
+ decisions consistently and creates a context from them for every verification: a context is no authority to store, nor
271
+ to use again after a key was withdrawn or deleted or the policy changed, and does not see what changes after it was
272
+ created. The library needs no IndexedDB, localStorage, network, DOM, Node or anything of an app.
273
+
274
+ Removing a pinned key from the local roots removes the trust it started with. A key statement signed by the quorum may
275
+ still name it among its identity keys and so delegate to it: excluding a key locally is no global revocation or deny
276
+ list. Revocation and verified succession need policies of their own. Unknown imports, invalid evidence and expired data
277
+ never give an authorized context.
278
+
279
+
280
+ ## Tests
281
+
282
+ ```bash
283
+ npm run verify
284
+ ```
285
+
286
+ `verify` runs what CI runs: the type checks (as Node and as a browser sees `src/`), ESLint with the rules of
287
+ ChargyCore.TS, and `npm test`, which is
288
+
289
+ - `npm run test:node`: the tests below, in Node;
290
+ - `npm run build` and `npm run test:bundle`: what `dist/` holds, as an app gets it: the bundle imports only the
291
+ dependencies `package.json` names and neither `node:*` modules, `Buffer` nor `process`; an app imports it by the name
292
+ of the package in plain Node and gets its declared types with TypeScript (`nodenext` and `bundler`); `npm pack`
293
+ publishes nothing but `dist/` and the documents;
294
+ - `npm run test:browser`: a trust anchor, a commit, a station proof of ML-DSA attestations, a price and the evidence of
295
+ a session, checked in Chromium (Vitest browser mode with Playwright; once: `npx playwright install chromium`).
296
+
297
+ GitHub Actions run it on every push and pull request (`.github/workflows/ci.yml`): Ubuntu on Node 22.18, the minimum
298
+ `package.json` names, and on Node 26, Windows on Node 24. Every night (`.github/workflows/nightly.yml`) the same suite
299
+ runs again, and, informational, with the dependencies the lockfile does not pin, with the newest releases of
300
+ `@vanaheimr/cose` and `@vanaheimr/metrological-cbor`, and as `npm audit`. The C# implementation as it is today is checked
301
+ every night by [WWCPPOIConformanceTests](https://github.com/OpenChargingCloud/WWCPPOIConformanceTests): its nightly run
302
+ holds WWCP_POI master and this library to the same golden vectors, and runs the C# examples of WWCP_POI master anew,
303
+ checked with `examples/validate-examples.ts` of this repository, which turns red the day WWCP_POI moves on and this port
304
+ needs work.
305
+
306
+ The test vectors are in `tests/vectors`, vendored from
307
+ [WWCPPOIConformanceTests](https://github.com/OpenChargingCloud/WWCPPOIConformanceTests), where they are generated -
308
+ never edit them here, and its `VendoredCopiesTests` fail when these copies drift:
309
+
310
+ - `tests/vectors/poi-*.json`, the golden vectors: canonical JSON and CBOR, Base64 and Base45, the examples of section 6
311
+ of the specification, station proofs of its example network signed with its keys against a trust anchor, what to
312
+ calculate of the documents of the run below, documents to refuse, the QI-Digital documents of example A14 of that run
313
+ taken and refused as WWCP_POI checks them, and small histories WWCPPOIConformanceTests signs itself, replayed step by
314
+ step (the format: the
315
+ [README of the vectors](https://github.com/OpenChargingCloud/WWCPPOIConformanceTests/blob/master/test-vectors/README.md));
316
+ - `tests/vectors/csharp-33447ac`, what one run of the C# examples of WWCP_POI at commit `33447ac` published (public data
317
+ only; its output is `csharp-output.log` there), and in `proofs/`, what `proofs/MakeProofs.cs.txt` made of the same
318
+ history with the C# implementation: proofs of charging stations of epoch 2, the commits of epoch 2 up to the head, and
319
+ the ECDSA twin of a price statement.
320
+
321
+ - `tests/conformance.test.ts`: the golden vectors, as the C# implementation runs them in WWCPPOIConformanceTests.
322
+ - `tests/canonical.test.ts`, `tests/specification.test.ts`: the test vectors of Styx and of section 6 of the
323
+ specification.
324
+ - `tests/exampleA1.test.ts`, `tests/examples.test.ts`: the examples A1 to A9, epoch 1, the manufacturers and the models
325
+ they state among them.
326
+ - `tests/evidence.test.ts`: example A10, the evidence a driver's app keeps, and ECDSA twins of price statements.
327
+ - `tests/exampleA11.test.ts`: example A11, epoch 2, followed with the old trust anchor and joined with the new one.
328
+ - `tests/exampleA12.test.ts`: example A12, the runtime status and the commit pushed over the WebSocket.
329
+ - `tests/exampleA13.test.ts`: example A13, transparency software manufacturers, conformity assessment bodies, and who
330
+ checks the meter.
331
+ - `tests/qidigital.test.ts`: example A14, the QI-Digital documents of a meter as WWCP_POI signed them, and others signed
332
+ with the keys of section 6 of the specification, to change them at will.
333
+ - `tests/chargingStation.test.ts`, `tests/client.test.ts`: proofs of charging stations of epoch 2, as an app checks them.
334
+ - `tests/replay.test.ts`: the whole history of `QA`, both epochs, commit by commit, with every checkpoint.
335
+ - `tests/build/bundle.test.js`, `tests/browser/browser.test.ts`: the bundle and the browser, as above.
336
+
337
+ To check a run of your own of the C# examples, with its new keys:
338
+
339
+ ```bash
340
+ npx tsx examples/validate-examples.ts D:\Coding\OpenChargingCloud\WWCP_POI\examples\csharp
341
+ ```
342
+
343
+
344
+ ## Shared trust and session verification in 0.2.0
345
+
346
+ These APIs were previously supplied in the unpublished 0.1.2-dev.0 candidate. WWCP_POI3 has them as well
347
+ (`WWCP_POI3/Verifier`), except for the local journal of observed versions, which is the app's. The versioned historical
348
+ and session-price evidence, compact trust delegation and publication-receipt profiles remain experimental; they do not
349
+ change the strict standard Evidence schema. See [Migration](CHANGELOG.md#migration) before upgrading a consumer.
350
+
351
+ ### Meter-key association
352
+
353
+ `verifyMeterKey(context, proof, { evseId, meterSerialNumber, keyFormat: 'spki-der' })` re-verifies the original proof
354
+ with a locally authorized `TrustContext`, selects only that EVSE's directly associated meter and requires an exact
355
+ serial match. Multiple keys require an explicit `keyId`; it never searches unrelated meters or tries keys until one
356
+ verifies. No fallback to CPO/RNO signing keys is performed.
357
+
358
+ The first pilot supports public uncompressed ECDSA P-256 measurement keys. `parseMeasurementPublicKey(value, 'spki-der'
359
+ | 'cose-key')` requires an explicit format: it checks SHA-256(SPKI) or the deterministic COSE key thumbprint as
360
+ appropriate, validates the point and produces a canonical COSE key/material ID. It retains the original ID and its
361
+ derivation scheme. Unsupported algorithms and ambiguous associations fail explicitly rather than becoming trusted keys.
362
+
363
+ The result carries RN/RNO, EVSE, station, meter, original/canonical key identity, commit/revision/root, KeyStatement
364
+ digest, evaluation time and a conservative expiry bounded by the counted attestations and their certified keys. It is a
365
+ current-snapshot observation, not historical key-rotation/installation evidence or a permanent authorization token.
366
+ Reconstruct local authorization for each operation. Future station-owned measurement signers and additional key formats
367
+ need explicit association rules; they are not guessed by this pilot.
368
+
369
+ The portable public test vector `tests/vectors/meter-session.json` originates from ChargyWebApp commit 97da24d
370
+ (`tests/fixtures/POISession`); its source manifest is `meter-session-meta.json`. It contains public demonstration keys
371
+ only.
372
+
373
+
374
+ ### Historical meter evidence
375
+
376
+ `parseHistoricalMeterEvidence` and `verifyHistoricalMeterEvidence(currentContext, value, request, policy?)` keep current
377
+ local authorization separate from historical validity. The request supplies the actual original reading times, EVSE,
378
+ meter serial and explicit key format. Expired historical statements and operational certificates are checked at those
379
+ times; each later checkpoint uses its own supplied operator and signature times. The output is an immutable per-time
380
+ binding for Core, not a serialized trust token.
381
+
382
+ The versioned `historicalMeterEvidence/v1` envelope is a TS-library extension, not a change to the existing strict
383
+ Evidence schema. It contains `EVSEId` and `versions`, each with `chargingStationProof`, optional `commitProof`, and
384
+ optional `checkpoints` entries shaped as `{checkpoint, operator}`. Missing inclusion stays visible; malformed supplied
385
+ inclusion fails. Prices are not required just to verify a meter assignment. Only caller-authorized witnesses/revocations
386
+ count. `invalidFrom` compromise times apply to signature times or measurement-key use; ordinary expiry does not
387
+ retroactively invalidate a valid historical signature. An unauthorized current quorum still blocks all checks. Identity
388
+ succession not connected to today's authorized pins fails closed.
389
+
390
+ The claim is RNO attestation at the signed measurement times and optionally checkpoint inclusion. Independently
391
+ established actual time, sole/latest membership, complete owner-signed change replay and checkpoint continuity are not
392
+ inferred. A checkpoint obtained later does not prove it was public before charging. No full network archive is
393
+ downloaded. See the app's illustrated historical-session guide for integration.
394
+
395
+
396
+ `HistoricalTrustContext.create(anchor, savedReferenceStatement, {trustedIdentityKeyIds, otherIdentityKeys?, now?})`
397
+ authorizes historical checks from fresh caller-supplied local decisions. It verifies the saved reference at its own
398
+ signed time; expiry today does not require a server or a newly issued statement. Its one-minute validity bound is an
399
+ operation deadline, not historical document validity. Recreate it for each check and reread local authorization after
400
+ async work. Current `TrustContext` remains the separate path for fresh/current station checks. Unknown identity
401
+ succession still fails.
402
+
403
+
404
+ ### Session-bound prices
405
+
406
+ `verifySessionPriceEvidence` verifies the attested station at the original start, CPO PriceStatements with existing
407
+ generic algorithms (including Ed25519 and ML-DSA), usage/validity, entity/tariff references, supplied chains and
408
+ explicit selection. `SessionPriceAgreement` binds the exact original start document SHA-256, meter/EVSE, time, tariff,
409
+ statement digest, mode and binding/profile in a CPO signature. This is a CPO assertion, not customer consent. Unknown
410
+ agreements never authorize themselves. The experimental `chargy-gross-rates/v1` profile fixes unconditional gross rates
411
+ at start. EMP contracts, variable bindings and unsupported restrictions stay unproven.
412
+
413
+ Checkpoint authority is shared with historical meter verification. Price inclusion, `after` links/continuity, selected
414
+ statement identity and related checkpoint forks are checked separately. A later checkpoint never proves pre-start
415
+ publication, independent time or sole/latest offer. Original decimals are preserved. There is no P-256-only restriction
416
+ on CPO price/assignment signatures. See the app's price guide.
417
+
418
+
419
+ ### Locally observed versions
420
+
421
+ `compareObservedVersions` is a pure, shared policy for monotonic current selection and persistent authenticated
422
+ contradictions. Metadata projection helpers for station proofs, checkpoint positions/root revisions and PriceStatements
423
+ do not authenticate anything: call the usual role, quorum, subject and validity verifiers before recording them. The
424
+ caller owns durable storage and must compare and replace the state atomically. The app uses a separate IndexedDB journal
425
+ scoped to the exact local anchor policy and authenticated RN/RNO identity.
426
+
427
+ Current selection rejects known rollbacks, unresolved forks, missing checkpoint, price or live predecessors, and
428
+ unverified epoch changes. Historical inspection allows older coordinates and never advances current heads. Authenticated
429
+ gaps and rejected conflicting originals remain negative knowledge. A rejected gap cannot advance its head by
430
+ resubmitting the same tip. Station snapshots permit revision jumps without claiming commit ancestry. Limits are 4096
431
+ points and 128 conflicts; storage must fail closed rather than silently evict history. These local rules establish
432
+ neither global freshness nor independently authorized witnesses.
433
+
434
+
435
+ ### CPO envelope authority and compact trust succession
436
+
437
+ `verifyChargingDocumentKeys` re-verifies the original station/EVSE proof, exact CPO, document-time validity and the
438
+ explicit `chargeTransparencyRecords` usage (or an unrestricted party key). It returns normalized COSE material for the
439
+ shared Core envelope verifier; a price-only key does not authorize document signing. This is independent of the
440
+ P-256/OCMF meter pilot.
441
+
442
+ A transition names the policy it continues by `trustPolicyId(anchor)`: the SHA-256 of the canonical JSON of `{network,
443
+ holders, minConsensus, urls}`, the URLs as a WHATWG URL serializes them without trailing slashes, in their ordinal order,
444
+ as WWCP_POI3 calculates it. `TrustTransition` and `VerifiedTrustTransition` implement an experimental compact delegation
445
+ certificate, signed by both old authorized and target identity holder quorums, binding exact policies, statements,
446
+ sources and predecessor. It is not History owner/change-set replay and does not misuse `followedBy`.
447
+ `applyVerifiedTrustTransition` preserves negative journal knowledge while adopting explicit policy delegation and exact
448
+ final-checkpoint/genesis/first-checkpoint epoch evidence. Unverified epoch jumps and quorum reductions fail closed.
449
+ `TrustContext.withEpoch` carries verified profiles into subsequent proof checks; historical contexts can carry a
450
+ reverified authority timeline. These objects are recreated from local decisions and original evidence, never serialized
451
+ green flags. See the app's illustrated trust-succession guide and genuine public fixtures.
452
+
453
+ ### Independent price publication receipts
454
+
455
+ `parseWitnessPublicationPolicy` parses separately authorized organization, network, key and clock decisions. Parsing a
456
+ recommendation grants no trust. Pass the accepted policy as `publicationPolicy` to `verifySessionPriceEvidence`; its
457
+ optional `publicationReceipts` and `witnessCandidates` do not authorize themselves. `PricePublicationReceipt` signs a
458
+ versioned public-retrieval assertion for the exact selected price/EVSE/network/checkpoint and public source. The
459
+ experimental key usage is `witnessPublications`; existing usage restrictions still apply.
460
+
461
+ The result `publicationBeforeStart: 'witnessed-before-signed-start'` requires the separate distinct-organization quorum,
462
+ valid original signatures and inclusion, and every counted latest signing bound (including locally accepted clock error)
463
+ strictly before the original signed start. Several keys/aliases of one approved organization count once; reused key
464
+ material cannot create another organization. Known RNO/CPO key material is excluded from independent witnessing. Actual
465
+ clock accuracy and organizational independence remain explicit caller trust assumptions. The physical charging start,
466
+ global latest/sole offer, customer consent and final billing are not independently proven. This is not an RFC 3161
467
+ implementation.
468
+
469
+ `tests/vectors/price-publication.json` contains public Ed25519/ML-DSA-65 vectors and negative timing, scope, signature
470
+ and duplicate-organization cases. The caller must reread current policy across asynchronous work and persist original
471
+ receipts, never serialized green verdicts. No production witness service is implied.
472
+
473
+ Publication receipts additionally bind `priceStatementSHA256` (the complete original CPO-signed object including its
474
+ signature value) and `tariffSHA256` (the exact selected tariff including its currency). The ordinary statement digest
475
+ excludes its signature value and cannot alone prove that a signed offer, rather than its draft, was retrieved.
476
+ `pricePublicationOfferHashes` computes these bindings consistently for generators and callers. Neither alternate
477
+ signature encodings nor changed tariff definitions reuse another original's receipt.
478
+
479
+
480
+ ## Publishing
481
+
482
+ Version 0.2.0 is prepared locally. Preparation does not publish to npm, create a release tag or change a consumer's
483
+ pinned development package. Validate the source and inspect the package before approving a release:
484
+
485
+ ```bash
486
+ npm ci
487
+ npx playwright install chromium
488
+ npm run verify
489
+ npm run examples
490
+ npm pack --dry-run
491
+ npm pack
492
+ ```
493
+
494
+ After release approval, replace the prepared-release markers in this README and the changelog with the actual release
495
+ status and date, rebuild and check the final archive. Commit the reviewed release files, then tag and push that exact
496
+ commit to each configured remote:
497
+
498
+ ```bash
499
+ git tag -s v0.2.0 -m "v0.2.0"
500
+ git push origin master v0.2.0
501
+ git push git1 master v0.2.0
502
+ git push git2 master v0.2.0
503
+ ```
504
+
505
+ Publish the inspected archive only after that separate approval:
506
+
507
+ ```bash
508
+ npm login
509
+ npm whoami
510
+ npm publish ./open-charging-cloud-wwcp-poi-0.2.0.tgz --access public
511
+ ```
512
+
513
+ After publication, consumers can replace their local POI development archive with
514
+ `@open-charging-cloud/wwcp-poi@^0.2.0`, refresh their lockfile and verify a clean install. Their separate ChargyCore
515
+ candidate still needs its own release; publishing POI alone does not replace it.
516
+
517
+
518
+ ## License
519
+
520
+ [GNU Affero General Public License v3.0](LICENSE), as WWCP_POI.
521
+
522
+ Copyright 2014-2026 GraphDefined GmbH