@open-charging-cloud/chargy-core 0.13.1 → 0.14.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/CHANGELOG.md CHANGED
@@ -7,6 +7,50 @@ While the version number is below 1.0.0, breaking changes are released in minor
7
7
  versions and are always listed first below.
8
8
 
9
9
 
10
+ ## [0.14.0] - 2026-08-30
11
+
12
+ ### Added
13
+
14
+ - **The signatures over a whole document are verified.** A charge transparency
15
+ live link may be signed as a whole by its operator, and that signature is what
16
+ ties the transport URLs and the listed public keys to whoever signed them.
17
+ Until now the `signatures` array was carried around but never read. Every
18
+ entry is checked now: the properties it excludes are removed, what remains is
19
+ canonicalized and read as UTF-8, the key is resolved by the `keyId` the entry
20
+ names - following the document's own `keyIdGeneration` - and the signature is
21
+ verified with it. ECDSA over P-256, P-384 and P-521, Ed25519, Ed448 and
22
+ ML-DSA-44/65/87 are understood.
23
+
24
+ A key id is defined over the canonical `SubjectPublicKeyInfo` form of a key,
25
+ no matter which form the document stores the key in. A key stored as bare key
26
+ material - as Ed25519 keys usually are - is therefore wrapped before its id is
27
+ computed: hashing the raw bytes would yield a different id for the same key,
28
+ and the signature would look as though no key of the document had signed it.
29
+
30
+ - **`verifyDocumentSignatures()` and `collectDocumentPublicKeys()`** do this for
31
+ any JSON document, for callers who want to check one without going through
32
+ `DetectAndConvertContentFormat()`. Neither throws: a document that is
33
+ unsigned, signed by an unknown key or signed badly is reported as such.
34
+
35
+ ### Changed
36
+
37
+ - **A live link says what its signatures did, and stays usable either way.**
38
+ `IChargeTransparencyLiveLink.signatureVerification` carries the outcome per
39
+ signature, and anything short of "all valid" also adds a `warning`. None of it
40
+ is fatal: an unsigned document, an unknown key and even a signature that
41
+ demonstrably does not match are all warnings, never a reason to refuse the
42
+ document. Its transports still work, and its signed meter values carry their
43
+ own signatures, which are verified separately. The warnings are graded by what
44
+ they actually say - that nothing was claimed (unsigned), that the claim cannot
45
+ be judged here (unknown key, unsupported algorithm, malformed), or that the
46
+ claim is demonstrably false (the signature does not match).
47
+
48
+ Verification runs before anything is added to the document, because the
49
+ signatures cover every property but their own: a timestamp defaulted into
50
+ `created` first would become part of what is verified and would turn a good
51
+ signature into a bad one.
52
+
53
+
10
54
  ## [0.13.0] - 2026-08-28
11
55
 
12
56
  ### Breaking
package/README.md CHANGED
@@ -207,7 +207,7 @@ npx playwright install chromium
207
207
  ## Publishing
208
208
 
209
209
  ```bash
210
- npm version 0.13.1 --no-git-tag-version
210
+ npm version 0.14.0 --no-git-tag-version
211
211
  npm run verify
212
212
  npm pack --dry-run
213
213
  npm pack
@@ -0,0 +1,70 @@
1
+ import * as chargyLib from './interfaces/chargyLib';
2
+ /** How one signature over a whole document came out. */
3
+ export type DocumentSignatureStatus =
4
+ /** The key is known and the signature matches the signed data. */
5
+ "validSignature" |
6
+ /** Everything resolved, but the signature does not match the signed data. */
7
+ "invalidSignature" |
8
+ /** No public key of the document carries the key id the signature names. */
9
+ "unknownPublicKey" |
10
+ /** The algorithm or one of the encodings is not supported here. */
11
+ "unsupportedAlgorithm" |
12
+ /** The entry is not a well-formed signature at all. */
13
+ "malformed";
14
+ /** The outcome of one entry of the "signatures" array. */
15
+ export interface IDocumentSignatureResult {
16
+ /** Position within the "signatures" array. */
17
+ index: number;
18
+ /** The key id the signature names, when it names one. */
19
+ keyId?: string;
20
+ /** The algorithm the signature names, when it names one. */
21
+ algorithm?: string;
22
+ status: DocumentSignatureStatus;
23
+ /** Technical detail for developers; never localized, never shown as-is. */
24
+ details?: string;
25
+ }
26
+ /** How a document as a whole came out. */
27
+ export type DocumentSignaturesStatus =
28
+ /** No "signatures" array, or an empty one: nothing to verify. */
29
+ "unsigned" |
30
+ /** Every signature verified. */
31
+ "allValid" |
32
+ /** At least one verified and at least one did not. */
33
+ "someValid" |
34
+ /** Signatures are present, but not one of them verified. */
35
+ "noneValid";
36
+ export interface IDocumentSignaturesResult {
37
+ status: DocumentSignaturesStatus;
38
+ signatures: Array<IDocumentSignatureResult>;
39
+ validCount: number;
40
+ }
41
+ /** A public key as a document lists it. */
42
+ export interface IDocumentPublicKey {
43
+ algorithm: string;
44
+ encodings: Array<string>;
45
+ value: string;
46
+ keyUsage: Array<string>;
47
+ }
48
+ /**
49
+ * Every public key a document lists, in the order they appear.
50
+ *
51
+ * The keys of a charge transparency live link live in two places: those of the
52
+ * operator, and the one of the energy meter of the EVSE. A key that does not
53
+ * carry the three things needed to use it - an algorithm, an encodings pipeline
54
+ * and a value - is skipped rather than guessed at.
55
+ */
56
+ export declare function collectDocumentPublicKeys(Document: chargyLib.JSONObject): Array<IDocumentPublicKey>;
57
+ /**
58
+ * Verifies every signature a document carries over itself.
59
+ *
60
+ * A document without signatures is reported as "unsigned" rather than as a
61
+ * failure: whether that is acceptable depends on the document, and saying so is
62
+ * the caller's job. Nothing here throws.
63
+ *
64
+ * The document must be the one as it was read. Verification covers every
65
+ * property except those the signatures exclude, so anything added to the object
66
+ * beforehand - a default timestamp, a verification result - changes the bytes
67
+ * that are verified and makes a good signature look bad.
68
+ */
69
+ export declare function verifyDocumentSignatures(Document: chargyLib.JSONObject): IDocumentSignaturesResult;
70
+ //# sourceMappingURL=DocumentSignatures.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"DocumentSignatures.d.ts","sourceRoot":"","sources":["../src/DocumentSignatures.ts"],"names":[],"mappings":"AA2BA,OAAO,KAAK,SAAS,MAAyB,wBAAwB,CAAA;AAQtE,wDAAwD;AACxD,MAAM,MAAM,uBAAuB;AAE/B,kEAAkE;AAClE,gBAAgB;AAEhB,6EAA6E;AAC7E,kBAAkB;AAElB,4EAA4E;AAC5E,kBAAkB;AAElB,mEAAmE;AACnE,sBAAsB;AAEtB,uDAAuD;AACvD,WAAW,CAAC;AAGhB,0DAA0D;AAC1D,MAAM,WAAW,wBAAwB;IAErC,8CAA8C;IAC9C,KAAK,EAAQ,MAAM,CAAC;IAEpB,yDAAyD;IACzD,KAAK,CAAC,EAAO,MAAM,CAAC;IAEpB,4DAA4D;IAC5D,SAAS,CAAC,EAAG,MAAM,CAAC;IAEpB,MAAM,EAAO,uBAAuB,CAAC;IAErC,2EAA2E;IAC3E,OAAO,CAAC,EAAK,MAAM,CAAC;CAEvB;AAGD,0CAA0C;AAC1C,MAAM,MAAM,wBAAwB;AAEhC,iEAAiE;AACjE,UAAU;AAEV,gCAAgC;AAChC,UAAU;AAEV,sDAAsD;AACtD,WAAW;AAEX,4DAA4D;AAC5D,WAAW,CAAC;AAGhB,MAAM,WAAW,yBAAyB;IACtC,MAAM,EAAO,wBAAwB,CAAC;IACtC,UAAU,EAAG,KAAK,CAAC,wBAAwB,CAAC,CAAC;IAC7C,UAAU,EAAG,MAAM,CAAC;CACvB;AAGD,2CAA2C;AAC3C,MAAM,WAAW,kBAAkB;IAC/B,SAAS,EAAG,MAAM,CAAC;IACnB,SAAS,EAAG,KAAK,CAAC,MAAM,CAAC,CAAC;IAC1B,KAAK,EAAO,MAAM,CAAC;IACnB,QAAQ,EAAI,KAAK,CAAC,MAAM,CAAC,CAAC;CAC7B;AA8RD;;;;;;;GAOG;AACH,wBAAgB,yBAAyB,CAAC,QAAQ,EAAE,SAAS,CAAC,UAAU,GAAG,KAAK,CAAC,kBAAkB,CAAC,CA4CnG;AAiID;;;;;;;;;;;GAWG;AACH,wBAAgB,wBAAwB,CAAC,QAAQ,EAAE,SAAS,CAAC,UAAU,GAAG,yBAAyB,CAyBlG"}
@@ -5,7 +5,7 @@ import { ed448ph, ed448 } from '@noble/curves/ed448.js';
5
5
  import { p521, p384, p256 } from '@noble/curves/nist.js';
6
6
  import { secp256k1 } from '@noble/curves/secp256k1.js';
7
7
  import { ml_dsa87, ml_dsa65, ml_dsa44 } from '@noble/post-quantum/ml-dsa.js';
8
- import { sha256 as sha256$1 } from '@noble/hashes/sha2.js';
8
+ import { sha256 as sha256$1, sha512 as sha512$1, sha384 as sha384$1 } from '@noble/hashes/sha2.js';
9
9
  import { fileTypeFromBuffer } from 'file-type';
10
10
  import isURL from 'is-url-superb';
11
11
  import jsQR from 'jsqr';
@@ -3073,10 +3073,10 @@ var NobleCompatibleCurve = class {
3073
3073
  keyFromPublic(publicKey, encoding) {
3074
3074
  if (encoding.toLowerCase() !== "hex")
3075
3075
  throw new TypeError("Only hexadecimal public keys are supported by the compatibility API.");
3076
- const publicKeyBytes = normalizeSEC1PublicKey(publicKey, this.coordinateLength);
3077
- if (!this.suite.isValidPublicKey(publicKeyBytes))
3076
+ const publicKeyBytes2 = normalizeSEC1PublicKey(publicKey, this.coordinateLength);
3077
+ if (!this.suite.isValidPublicKey(publicKeyBytes2))
3078
3078
  throw new TypeError("Public key is not a valid point on the selected curve.");
3079
- return new NobleCompatiblePublicKey(this.suite, publicKeyBytes, this.coordinateLength);
3079
+ return new NobleCompatiblePublicKey(this.suite, publicKeyBytes2, this.coordinateLength);
3080
3080
  }
3081
3081
  };
3082
3082
  var suites = {
@@ -5910,7 +5910,7 @@ async function signJSONMessage(JSONMessage, KeyPairs, options) {
5910
5910
  continue;
5911
5911
  const publicKeyEncoding = isRawSignatureAlgorithm(algorithm) ? "raw" : "sec1";
5912
5912
  const signatureEncoding = options?.signatureEncoding ?? suite.signatureEncoding;
5913
- const publicKeyBytes = KeyPair.publicKey ?? suite.getPublicKey(KeyPair.privateKey);
5913
+ const publicKeyBytes2 = KeyPair.publicKey ?? suite.getPublicKey(KeyPair.privateKey);
5914
5914
  const signatureBytes = suite.sign(
5915
5915
  plainText,
5916
5916
  KeyPair.privateKey,
@@ -5920,8 +5920,8 @@ async function signJSONMessage(JSONMessage, KeyPairs, options) {
5920
5920
  algorithm,
5921
5921
  publicKeyEncoding,
5922
5922
  signatureEncoding,
5923
- publicKey: bytesToBase64(publicKeyBytes),
5924
- publicKeyHEX: bytesToHex(publicKeyBytes),
5923
+ publicKey: bytesToBase64(publicKeyBytes2),
5924
+ publicKeyHEX: bytesToHex(publicKeyBytes2),
5925
5925
  signature: bytesToBase64(signatureBytes),
5926
5926
  signatureHEX: bytesToHex(signatureBytes)
5927
5927
  };
@@ -5932,7 +5932,7 @@ async function signJSONMessage(JSONMessage, KeyPairs, options) {
5932
5932
  const signatureIsValid = suite.verify(
5933
5933
  plainText,
5934
5934
  signatureBytes,
5935
- publicKeyBytes,
5935
+ publicKeyBytes2,
5936
5936
  signatureOptions(options, signatureEncoding)
5937
5937
  );
5938
5938
  if (!signatureIsValid)
@@ -5975,11 +5975,11 @@ async function verifyJSONSignatureResult(JSONMessage, signature, options) {
5975
5975
  const resolvedOptions = resolveSignOptions(options);
5976
5976
  const algorithm = signature.algorithm ?? resolvedOptions.algorithm;
5977
5977
  const suite = getSignatureSuite(algorithm);
5978
- const publicKeyBytes = hexToBytes(signature.publicKeyHEX);
5978
+ const publicKeyBytes2 = hexToBytes(signature.publicKeyHEX);
5979
5979
  const signatureBytes = hexToBytes(signature.signatureHEX);
5980
5980
  const signatureEncoding = signature.signatureEncoding ?? (isRawSignatureAlgorithm(algorithm) ? "raw" : "der");
5981
5981
  try {
5982
- const publicKeyIsValid = suite.isValidPublicKey(publicKeyBytes);
5982
+ const publicKeyIsValid = suite.isValidPublicKey(publicKeyBytes2);
5983
5983
  if (!publicKeyIsValid)
5984
5984
  return verificationResult(
5985
5985
  "InvalidPublicKey" /* InvalidPublicKey */,
@@ -5995,7 +5995,7 @@ async function verifyJSONSignatureResult(JSONMessage, signature, options) {
5995
5995
  const signatureIsValid = suite.verify(
5996
5996
  plainText,
5997
5997
  signatureBytes,
5998
- publicKeyBytes,
5998
+ publicKeyBytes2,
5999
5999
  signatureVerificationOptions(signature, options, signatureEncoding)
6000
6000
  );
6001
6001
  return signatureIsValid ? verificationResult("True" /* True */) : verificationResult(
@@ -8324,10 +8324,10 @@ var OCMF = class {
8324
8324
  try {
8325
8325
  if (typeof PublicKey !== "string")
8326
8326
  throw new TypeError("EdDSA and ML-DSA public keys must use a raw string encoding.");
8327
- const publicKeyBytes = decodeRawOCMFBytes(PublicKey, PublicKeyEncoding);
8327
+ const publicKeyBytes2 = decodeRawOCMFBytes(PublicKey, PublicKeyEncoding);
8328
8328
  const signatureBytes = OCMFJSONDocument.signatureBytes;
8329
8329
  const rawPayload = OCMFJSONDocument.rawPayload;
8330
- if (!signatureSuite.isValidPublicKey(publicKeyBytes))
8330
+ if (!signatureSuite.isValidPublicKey(publicKeyBytes2))
8331
8331
  throw new TypeError("Public key is invalid for the selected signature algorithm.");
8332
8332
  if (signatureBytes == null)
8333
8333
  throw new TypeError("Missing raw signature bytes.");
@@ -8336,7 +8336,7 @@ var OCMF = class {
8336
8336
  const signatureValid = signatureSuite.verify(
8337
8337
  new TextEncoder().encode(rawPayload),
8338
8338
  signatureBytes,
8339
- publicKeyBytes
8339
+ publicKeyBytes2
8340
8340
  );
8341
8341
  if (!signatureValid)
8342
8342
  this.AddValidationError(OCMFJSONDocument, "Verification_SignatureMismatch");
@@ -8353,7 +8353,7 @@ var OCMF = class {
8353
8353
  if (curve === null)
8354
8354
  throw new Error(`Unsupported ECC curve '${OCMFJSONDocument.signature.SA ?? ""}'!`);
8355
8355
  OCMFJSONDocument.publicKey ??= PublicKey;
8356
- let publicKeyBytes = null;
8356
+ let publicKeyBytes2 = null;
8357
8357
  if (typeof OCMFJSONDocument.publicKey === "string") {
8358
8358
  const ECPoint = this.chargy.asn1.define("ECPoint", function() {
8359
8359
  this.seq().obj(
@@ -8367,36 +8367,36 @@ var OCMF = class {
8367
8367
  if (PublicKeyEncoding != null && PublicKeyEncoding.length > 0) {
8368
8368
  switch (PublicKeyEncoding.toLowerCase()) {
8369
8369
  case "hex":
8370
- publicKeyBytes = Buffer.from(OCMFJSONDocument.publicKey, "hex");
8370
+ publicKeyBytes2 = Buffer.from(OCMFJSONDocument.publicKey, "hex");
8371
8371
  break;
8372
8372
  case "base32":
8373
- publicKeyBytes = Buffer.from(this.chargy.base32Decode(OCMFJSONDocument.publicKey, "RFC4648"));
8373
+ publicKeyBytes2 = Buffer.from(this.chargy.base32Decode(OCMFJSONDocument.publicKey, "RFC4648"));
8374
8374
  break;
8375
8375
  case "base64":
8376
- publicKeyBytes = Buffer.from(OCMFJSONDocument.publicKey, "base64");
8376
+ publicKeyBytes2 = Buffer.from(OCMFJSONDocument.publicKey, "base64");
8377
8377
  break;
8378
8378
  }
8379
8379
  }
8380
- if (publicKeyBytes == null) {
8380
+ if (publicKeyBytes2 == null) {
8381
8381
  const hexRegex = /^[0-9A-Fa-f]+$/;
8382
8382
  const base32Regex = /^(?:[A-Z2-7]{8})*(?:[A-Z2-7]{2}={6}|[A-Z2-7]{4}={4}|[A-Z2-7]{5}={3}|[A-Z2-7]{7}=)?$/;
8383
8383
  const base64Regex = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/;
8384
8384
  if (hexRegex.test(OCMFJSONDocument.publicKey)) {
8385
8385
  PublicKeyEncoding = "hex";
8386
- publicKeyBytes = Buffer.from(OCMFJSONDocument.publicKey, "hex");
8386
+ publicKeyBytes2 = Buffer.from(OCMFJSONDocument.publicKey, "hex");
8387
8387
  } else if (base32Regex.test(OCMFJSONDocument.publicKey)) {
8388
8388
  PublicKeyEncoding = "base32";
8389
- publicKeyBytes = Buffer.from(this.chargy.base32Decode(OCMFJSONDocument.publicKey, "RFC4648"));
8389
+ publicKeyBytes2 = Buffer.from(this.chargy.base32Decode(OCMFJSONDocument.publicKey, "RFC4648"));
8390
8390
  } else if (base64Regex.test(OCMFJSONDocument.publicKey)) {
8391
8391
  PublicKeyEncoding = "base64";
8392
- publicKeyBytes = Buffer.from(OCMFJSONDocument.publicKey, "base64");
8392
+ publicKeyBytes2 = Buffer.from(OCMFJSONDocument.publicKey, "base64");
8393
8393
  }
8394
8394
  }
8395
- if (publicKeyBytes == null) {
8395
+ if (publicKeyBytes2 == null) {
8396
8396
  OCMFJSONDocument.validationStatus = "UnknownPublicKeyFormat" /* UnknownPublicKeyFormat */;
8397
8397
  return OCMFJSONDocument.validationStatus;
8398
8398
  }
8399
- const decodedPublicKeyASN1 = ECPoint.decode(publicKeyBytes, "der");
8399
+ const decodedPublicKeyASN1 = ECPoint.decode(publicKeyBytes2, "der");
8400
8400
  const publicKeyASN1 = decodedPublicKeyASN1;
8401
8401
  const coordinates = publicKeyASN1.pubKey.data.subarray(1);
8402
8402
  const halfLength = coordinates.length / 2;
@@ -12329,6 +12329,259 @@ var validationRules_default = {
12329
12329
  }
12330
12330
  }
12331
12331
  };
12332
+ var defaultKeyIdGeneration = ["SubjectPublicKeyInfo", "DER", "SHA-256", "hex"];
12333
+ var defaultExcludedProperties = ["signatures"];
12334
+ var canonicalJSONEncodings = ["JSON", "JCS", "UTF-8"];
12335
+ var subjectPublicKeyInfoHeaders = {
12336
+ "EdDSA-Ed25519": "302A300506032B6570032100",
12337
+ "EdDSA-Ed448": "3043300506032B6571033A00",
12338
+ "ECDSA-secp256r1": "3059301306072A8648CE3D020106082A8648CE3D030107034200",
12339
+ "ECDSA-secp384r1": "3076301006072A8648CE3D020106052B81040022036200",
12340
+ "ECDSA-secp521r1": "30819B301006072A8648CE3D020106052B8104002303818600"
12341
+ };
12342
+ function readDERLength2(bytes, offset) {
12343
+ const first = bytes[offset];
12344
+ if (first === void 0)
12345
+ throw new Error("Truncated ASN.1 length!");
12346
+ if (first < 128)
12347
+ return { length: first, size: 1 };
12348
+ const count = first & 127;
12349
+ if (count === 0 || count > 4)
12350
+ throw new Error("Unsupported ASN.1 length!");
12351
+ let length = 0;
12352
+ for (let index = 1; index <= count; index++) {
12353
+ const byte = bytes[offset + index];
12354
+ if (byte === void 0)
12355
+ throw new Error("Truncated ASN.1 length!");
12356
+ length = length * 256 + byte;
12357
+ }
12358
+ return { length, size: count + 1 };
12359
+ }
12360
+ function subjectPublicKeyInfoKeyMaterial(spki) {
12361
+ if (spki[0] !== 48)
12362
+ throw new Error("A SubjectPublicKeyInfo must start with a SEQUENCE!");
12363
+ let index = 1 + readDERLength2(spki, 1).size;
12364
+ if (spki[index] !== 48)
12365
+ throw new Error("The SubjectPublicKeyInfo does not start with an AlgorithmIdentifier!");
12366
+ const algorithmLength = readDERLength2(spki, index + 1);
12367
+ index += 1 + algorithmLength.size + algorithmLength.length;
12368
+ if (spki[index] !== 3)
12369
+ throw new Error("The SubjectPublicKeyInfo does not contain a BIT STRING!");
12370
+ const keyLength = readDERLength2(spki, index + 1);
12371
+ const start = index + 1 + keyLength.size;
12372
+ if (spki[start] !== 0)
12373
+ throw new Error("The SubjectPublicKeyInfo BIT STRING has unused bits!");
12374
+ const keyMaterial = spki.subarray(start + 1, start + keyLength.length);
12375
+ if (keyMaterial.length === 0)
12376
+ throw new Error("The SubjectPublicKeyInfo contains no key material!");
12377
+ return keyMaterial;
12378
+ }
12379
+ function wrapInSubjectPublicKeyInfo(keyMaterial, algorithm) {
12380
+ const header = subjectPublicKeyInfoHeaders[algorithm];
12381
+ if (header === void 0)
12382
+ throw new Error("Cannot wrap a raw " + algorithm + " key into a SubjectPublicKeyInfo!");
12383
+ const headerBytes = hexToBytes(header);
12384
+ const spki = new Uint8Array(headerBytes.length + keyMaterial.length);
12385
+ spki.set(headerBytes, 0);
12386
+ spki.set(keyMaterial, headerBytes.length);
12387
+ return spki;
12388
+ }
12389
+ function decodeText(value, encoding) {
12390
+ switch (encoding) {
12391
+ case "hex":
12392
+ return hexToBytes(value);
12393
+ case "base64":
12394
+ return base64ToBytes(value);
12395
+ default:
12396
+ throw new Error("Unsupported text encoding: " + String(encoding));
12397
+ }
12398
+ }
12399
+ function encodeText(bytes, encoding) {
12400
+ switch (encoding) {
12401
+ case "hex":
12402
+ return bytesToHex(bytes).toUpperCase();
12403
+ case "base64":
12404
+ return bytesToBase64(bytes);
12405
+ default:
12406
+ return null;
12407
+ }
12408
+ }
12409
+ function publicKeyBytes(publicKey, structure) {
12410
+ const storedStructure = publicKey.encodings[0];
12411
+ const storedBytes = decodeText(publicKey.value, publicKey.encodings[publicKey.encodings.length - 1]);
12412
+ if (storedStructure === structure)
12413
+ return storedBytes;
12414
+ if (storedStructure === "SubjectPublicKeyInfo" && structure === "raw")
12415
+ return subjectPublicKeyInfoKeyMaterial(storedBytes);
12416
+ if (storedStructure === "raw" && structure === "SubjectPublicKeyInfo")
12417
+ return wrapInSubjectPublicKeyInfo(storedBytes, publicKey.algorithm);
12418
+ throw new Error("Cannot represent a " + String(storedStructure) + " key as " + structure + "!");
12419
+ }
12420
+ function computeKeyId(publicKey, keyIdGeneration) {
12421
+ let index = 0;
12422
+ const structure = keyIdGeneration[index++];
12423
+ if (structure === void 0)
12424
+ return null;
12425
+ if (structure === "SubjectPublicKeyInfo" && keyIdGeneration[index] === "DER")
12426
+ index++;
12427
+ let bytes;
12428
+ try {
12429
+ bytes = publicKeyBytes(publicKey, structure);
12430
+ } catch {
12431
+ return null;
12432
+ }
12433
+ for (; index < keyIdGeneration.length; index++) {
12434
+ const step = keyIdGeneration[index];
12435
+ if (step === void 0)
12436
+ return null;
12437
+ switch (step) {
12438
+ case "SHA-256":
12439
+ bytes = sha256$1(bytes);
12440
+ break;
12441
+ case "SHA-384":
12442
+ bytes = sha384$1(bytes);
12443
+ break;
12444
+ case "SHA-512":
12445
+ bytes = sha512$1(bytes);
12446
+ break;
12447
+ // A pipeline ends with the text encoding of the id.
12448
+ default:
12449
+ return encodeText(bytes, step);
12450
+ }
12451
+ }
12452
+ return null;
12453
+ }
12454
+ function nobleSignatureAlgorithm(algorithm) {
12455
+ switch (algorithm) {
12456
+ case "ECDSA-secp256r1-SHA256":
12457
+ return "ECDSA-P256";
12458
+ case "ECDSA-secp384r1-SHA384":
12459
+ return "ECDSA-P384";
12460
+ case "ECDSA-secp521r1-SHA512":
12461
+ return "ECDSA-P521";
12462
+ case "EdDSA-Ed25519":
12463
+ return "Ed25519";
12464
+ case "EdDSA-Ed448":
12465
+ return "Ed448";
12466
+ case "ML-DSA-44":
12467
+ return "ML-DSA-44";
12468
+ case "ML-DSA-65":
12469
+ return "ML-DSA-65";
12470
+ case "ML-DSA-87":
12471
+ return "ML-DSA-87";
12472
+ default:
12473
+ return null;
12474
+ }
12475
+ }
12476
+ function asStringArray(value) {
12477
+ return Array.isArray(value) && value.every((entry) => typeof entry === "string") ? value : void 0;
12478
+ }
12479
+ function collectDocumentPublicKeys(Document) {
12480
+ const publicKeys = new Array();
12481
+ const collectFrom = (candidates) => {
12482
+ if (!Array.isArray(candidates))
12483
+ return;
12484
+ for (const candidate of candidates) {
12485
+ const entry = asJSONObject(candidate);
12486
+ if (entry === void 0)
12487
+ continue;
12488
+ const algorithm = asString(entry["algorithm"]);
12489
+ const value = asString(entry["value"]);
12490
+ const encodings = asStringArray(entry["encodings"]);
12491
+ const keyUsage = asStringArray(entry["keyUsage"]) ?? [];
12492
+ if (algorithm === void 0 || algorithm === "" || value === void 0 || value === "" || encodings === void 0 || encodings.length === 0) {
12493
+ continue;
12494
+ }
12495
+ publicKeys.push({ algorithm, encodings, value, keyUsage });
12496
+ }
12497
+ };
12498
+ const chargingStation = asJSONObject(Document["chargingStation"]);
12499
+ const evse = asJSONObject(chargingStation?.["EVSE"]);
12500
+ collectFrom(asJSONObject(Document["chargingStationOperator"])?.["publicKeys"]);
12501
+ collectFrom(asJSONObject(evse?.["energyMeter"])?.["publicKeys"]);
12502
+ return publicKeys;
12503
+ }
12504
+ function verifyDocumentSignature(Document, Signature, Index, PublicKeys, KeyIdGeneration) {
12505
+ const entry = asJSONObject(Signature);
12506
+ if (entry === void 0)
12507
+ return { index: Index, status: "malformed", details: "The signature is not a JSON object." };
12508
+ const keyId = asString(entry["keyId"]);
12509
+ const algorithm = asString(entry["algorithm"]);
12510
+ const value = asString(entry["value"]);
12511
+ const encodings = asStringArray(entry["encodings"]);
12512
+ const signedData = asJSONObject(entry["signedData"]);
12513
+ const described = (status, details) => ({
12514
+ index: Index,
12515
+ ...keyId !== void 0 ? { keyId } : {},
12516
+ ...algorithm !== void 0 ? { algorithm } : {},
12517
+ status,
12518
+ ...details !== void 0 ? { details } : {}
12519
+ });
12520
+ if (keyId === void 0 || keyId === "" || algorithm === void 0 || algorithm === "" || value === void 0 || value === "" || encodings === void 0 || encodings.length === 0) {
12521
+ return described("malformed", "A signature needs a keyId, an algorithm, encodings and a value.");
12522
+ }
12523
+ const signedEncodings = asStringArray(signedData?.["encodings"]) ?? canonicalJSONEncodings;
12524
+ if (signedEncodings.length !== canonicalJSONEncodings.length || !signedEncodings.every((step, position) => step === canonicalJSONEncodings[position])) {
12525
+ return described(
12526
+ "unsupportedAlgorithm",
12527
+ "Only [ " + canonicalJSONEncodings.join(", ") + " ] signed data is understood here."
12528
+ );
12529
+ }
12530
+ const excludedProperties = asStringArray(signedData?.["excludedProperties"]) ?? defaultExcludedProperties;
12531
+ if (!excludedProperties.includes("signatures"))
12532
+ excludedProperties.push("signatures");
12533
+ let signedBytes;
12534
+ try {
12535
+ signedBytes = canonicalJSONBytes(
12536
+ Object.fromEntries(
12537
+ Object.entries(Document).filter(([property]) => !excludedProperties.includes(property))
12538
+ )
12539
+ );
12540
+ } catch (exception) {
12541
+ return described(
12542
+ "malformed",
12543
+ "The signed properties could not be canonicalized: " + (exception instanceof Error ? exception.message : String(exception))
12544
+ );
12545
+ }
12546
+ const suiteAlgorithm = nobleSignatureAlgorithm(algorithm);
12547
+ if (suiteAlgorithm === null)
12548
+ return described("unsupportedAlgorithm", "Unsupported signature algorithm: " + algorithm);
12549
+ const wantedKeyId = keyId.toUpperCase();
12550
+ const publicKey = PublicKeys.find((candidate) => computeKeyId(candidate, KeyIdGeneration)?.toUpperCase() === wantedKeyId);
12551
+ if (publicKey === void 0)
12552
+ return described("unknownPublicKey", "No public key of this document has the key id " + keyId + ".");
12553
+ try {
12554
+ const suite = getSignatureSuite(suiteAlgorithm);
12555
+ const publicKeyBytes_ = publicKeyBytes(publicKey, "raw");
12556
+ const signatureBytes = decodeText(value, encodings[encodings.length - 1]);
12557
+ if (!suite.isValidPublicKey(publicKeyBytes_))
12558
+ return described("malformed", "The public key is not valid for " + algorithm + ".");
12559
+ return described(suite.verify(signedBytes, signatureBytes, publicKeyBytes_) ? "validSignature" : "invalidSignature");
12560
+ } catch (exception) {
12561
+ return described(
12562
+ "malformed",
12563
+ exception instanceof Error ? exception.message : String(exception)
12564
+ );
12565
+ }
12566
+ }
12567
+ function verifyDocumentSignatures(Document) {
12568
+ const signatures = Document["signatures"];
12569
+ if (!Array.isArray(signatures) || signatures.length === 0)
12570
+ return { status: "unsigned", signatures: [], validCount: 0 };
12571
+ const keyIdGeneration = asStringArray(Document["keyIdGeneration"]) ?? defaultKeyIdGeneration;
12572
+ const publicKeys = collectDocumentPublicKeys(Document);
12573
+ const results = signatures.map(
12574
+ (signature, index) => verifyDocumentSignature(Document, signature, index, publicKeys, keyIdGeneration)
12575
+ );
12576
+ const validCount = results.filter((result) => result.status === "validSignature").length;
12577
+ return {
12578
+ status: validCount === 0 ? "noneValid" : validCount === results.length ? "allValid" : "someValid",
12579
+ signatures: results,
12580
+ validCount
12581
+ };
12582
+ }
12583
+
12584
+ // src/chargy.ts
12332
12585
  function isPdfAttachment(value) {
12333
12586
  if (!isMandatoryJSONObject(value))
12334
12587
  return false;
@@ -13265,6 +13518,7 @@ var Chargy = class {
13265
13518
  throw new Error("Parsed JSON content is not a JSON object!");
13266
13519
  const JSONContext = JSONContent["@context"];
13267
13520
  if (IsAChargeTransparencyLiveLink(JSONContent)) {
13521
+ this.verifyLiveLinkSignatures(JSONContent);
13268
13522
  JSONContent.created ??= (/* @__PURE__ */ new Date()).toISOString();
13269
13523
  processedFile.result = JSONContent;
13270
13524
  } else if (JSONContent["format"] === "ptb")
@@ -13434,6 +13688,56 @@ var Chargy = class {
13434
13688
  return IsAChargeTransparencyRecord(verifiedCTR) ? verifiedCTR : void 0;
13435
13689
  }
13436
13690
  //#endregion
13691
+ //#region (private) verifyLiveLinkSignatures(LiveLink)
13692
+ // A live link may be signed as a whole by the operator, which is what ties
13693
+ // the transport URLs and the listed public keys to whoever signed them.
13694
+ // Those signatures are verified whenever the document carries any.
13695
+ //
13696
+ // Nothing here rejects a document. An unsigned live link, an unknown key or
13697
+ // even a signature that does not match is reported as a warning and the
13698
+ // document stays usable: its transports still work, and its signed meter
13699
+ // values carry their own signatures, which are verified separately. What a
13700
+ // reader makes of the warning is the reader's decision.
13701
+ //
13702
+ // Must run before anything is added to the document - see the note at the
13703
+ // call site.
13704
+ verifyLiveLinkSignatures(LiveLink) {
13705
+ const result = verifyDocumentSignatures(LiveLink);
13706
+ const warnings = new Array();
13707
+ const warn = (messageKey, level) => {
13708
+ const message = this.GetMultilanguageText(messageKey);
13709
+ if (!warnings.some((warning) => warning.message === message))
13710
+ warnings.push({ level, message });
13711
+ };
13712
+ if (result.status === "unsigned")
13713
+ warn("DocumentSignature_Missing", "low" /* low */);
13714
+ for (const signature of result.signatures) {
13715
+ switch (signature.status) {
13716
+ case "validSignature":
13717
+ break;
13718
+ // A signature that demonstrably does not match its document is
13719
+ // the one case that says something is actually wrong.
13720
+ case "invalidSignature":
13721
+ warn("DocumentSignature_Mismatch", "high" /* high */);
13722
+ break;
13723
+ // The rest mean "cannot be judged here", which is a weaker
13724
+ // statement than "is wrong".
13725
+ case "unknownPublicKey":
13726
+ warn("DocumentSignature_UnknownPublicKey", "medium" /* medium */);
13727
+ break;
13728
+ case "unsupportedAlgorithm":
13729
+ warn("DocumentSignature_UnsupportedAlgorithm", "medium" /* medium */);
13730
+ break;
13731
+ case "malformed":
13732
+ warn("DocumentSignature_Malformed", "medium" /* medium */);
13733
+ break;
13734
+ }
13735
+ }
13736
+ LiveLink.signatureVerification = result;
13737
+ if (warnings.length > 0)
13738
+ LiveLink.warnings = [...LiveLink.warnings ?? [], ...warnings];
13739
+ }
13740
+ //#endregion
13437
13741
  //#region (private) collectLiveLinkMeterValueKeys(LiveLink)
13438
13742
  // Every public key of the document that is allowed to sign meter values.
13439
13743
  // A charging session is regularly signed by more than one of them: the
@@ -13813,6 +14117,6 @@ buffer/index.js:
13813
14117
  *)
13814
14118
  */
13815
14119
 
13816
- export { ACrypt, Alfen, AlfenCrypt01, BSMCrypt01, CanonicalJSONError, ChargeIT, ChargePoint, ChargePointCrypt01, IChargeTransparencyLiveLink_exports as ChargeTransparencyLiveLink, ChargeTransparencyLiveLinkContext, IChargeTransparencyRecord_exports as ChargeTransparencyRecord, Chargy, chargyInterfaces_exports as ChargyInterfaces, Clone, CloneCTR, ConcatenateBuffers, CreateDiv, CreateDiv2, CreateError, CreateWarning, CryptoAlgorithms, CryptoHashAlgorithms, DayOfWeek, DisplayPrefixes, EDL40, EDL40Crypt01, EDL40ValidationError, EDL40_OBIS, EDL40_SESSION_CONTEXT, EDL40_SIGNATURE_CONTEXT, EMHCrypt01, ErrorLevel, GDFCrypt01, IECCurves, IEncoding, InformationRelevance, InformationRelevanceToString, IsAChargeTransparencyLiveLink, IsAChargeTransparencyRecord, IsAPublicKey, IsAPublicKeyLookup, IsAPublicKeySignature, IsAPublicKeyXY, IsASessionCryptoResult, IsAURL, IsNullOrEmpty, IsValidURL, JSONSignatureVerificationStatus, MENNEKES_EDL40_OBIS, MENNEKES_EDL40_XMLNS, Mennekes, MennekesCrypt01, OBIS2Hex, OBIS2MeasurementName, OBIS_RegExpr, OCMF, OCMFBonnTariffParseError, OCMFTransactionTypes, OCMF_SIGNATURE_ALGORITHMS, OCMFv1_x, OCPI, OIDInfo, PCDF, PCDFCrypt01, PCDFParseError, PCDFValidationError, PCDF_FIELD_ORDER, PCDF_PREFIX, PTB, ParseJSON_LD, PublicKeyFormats, IPublicKeyInfo_exports as PublicKeyInfo, SAFEXML, SessionVerificationResult, SetHex, SetInt8, SetText, SetText_withLength, SetTimestamp, SetTimestamp32, SetUInt32, SetUInt32_withCode, SetUInt64, SetUInt64D, SignMessage, SignatureFormats, IURL_exports as SimpleURL, TimeStatusTypes, URLContext, UTC2human, VerificationResult, VerifyJSONMessageSignatures, WarningLevel, WhenNullOrEmpty, XMLContainer, asJSONArray, asJSONObject, asNumber, asString, base64ToBytes, buf2hex, buildEDL40Signature, buildIsaSignature, buildMennekesSignatureData, bytesToBase64, bytesToHex, canParseEDL40, canonicalJSONBytes, canonicalJSONStringify, cleanHex, closeFullscreen, createCompatibleCurve, createHexString, createLegacyP192Curve, dateToMennekesLocalEpochSeconds, decodeSmlMessages, extractMennekesChargingProcesses, findEntryByObis, findGetListRes, firstKey, firstValue, generateSignatureKeyPair, getArrayElement, getArrayLikeElement, getDirectChildByLocalName, getDirectChildrenByLocalName, getElementsByLocalName, getFirstArrayElement, getInt16Bytes, getInt32Bytes, getInt64Bytes, getInt8Bytes, getLastArrayElement, getOCMFSignatureDisplay, getSignatureSuite, getTrimmedTextContent, hashFile, hex2bin, hex32, hexToArrayBuffer, hexToBytes, intFromBytes, isEncodedValue, isGeoLocation, isI18NString, isICryptoResult, isIFileInfo, isISessionCryptoResult1, isISessionCryptoResult2, isJSONLDObject, isMandatoryArrayOfStrings, isMandatoryBoolean, isMandatoryDecimal, isMandatoryJSONArray, isMandatoryJSONObject, isMandatoryNumber, isMandatoryString, isMandatoryURL, isOIDInfo, isObject, isOptionalArrayOfStrings, isOptionalDecimal, isOptionalJSONArray, isOptionalJSONArrayError, isOptionalJSONArrayOk, isOptionalJSONObject, isOptionalNumber, isOptionalString, isOptionalStringArray, isOptionalStringOrOIDInfo, isOptionalURL, isPCDFText, isPublicKeySubject, isString, isStringArray, isStringOrOIDInfo, isStringOrStringArray, isaListNameContext, jsonPrettyPrinter, measurementName2human, meterTimeZone, normalizePCDFPublicKeyHex, normalizeXMLText, ocmfBonnTariffToChargingTariff, openFullscreen, pad, parseAndVerifyJSONSignatures, parseDescription, parseEDL40, parseHexString, parseMennekesXMLDocument, parseNumber, parseOBIS, parseOCMFBonnTariffText, parsePCDFDocument, parsePCDFPublicKey, parsePCDFSignature, parseSmlTime, parseUTC, readQRCodeTextFromImage, readQRCodeTextFromImageData, readTLV, secp224k1, setUILocale, sha256, sha256____, sha384, sha384____, sha512, sha512____, signJSONMessage, signMessage, stripPCDFControlCharacters, stripTransport, time2human, timeZoneOffsetMinutes, toArrayBuffer, toSessionVerificationResults, toUint8Array, transformEDL40Status, tryParseOCMFBonnTariffText, unquotePCDFText, validatePCDFFields, verifyEDL40Document, verifyJSONMessageSignatureResults, verifyJSONMessageSignatures, verifyJSONSignature, verifyJSONSignatureResult, verifyPCDFDocument };
14120
+ export { ACrypt, Alfen, AlfenCrypt01, BSMCrypt01, CanonicalJSONError, ChargeIT, ChargePoint, ChargePointCrypt01, IChargeTransparencyLiveLink_exports as ChargeTransparencyLiveLink, ChargeTransparencyLiveLinkContext, IChargeTransparencyRecord_exports as ChargeTransparencyRecord, Chargy, chargyInterfaces_exports as ChargyInterfaces, Clone, CloneCTR, ConcatenateBuffers, CreateDiv, CreateDiv2, CreateError, CreateWarning, CryptoAlgorithms, CryptoHashAlgorithms, DayOfWeek, DisplayPrefixes, EDL40, EDL40Crypt01, EDL40ValidationError, EDL40_OBIS, EDL40_SESSION_CONTEXT, EDL40_SIGNATURE_CONTEXT, EMHCrypt01, ErrorLevel, GDFCrypt01, IECCurves, IEncoding, InformationRelevance, InformationRelevanceToString, IsAChargeTransparencyLiveLink, IsAChargeTransparencyRecord, IsAPublicKey, IsAPublicKeyLookup, IsAPublicKeySignature, IsAPublicKeyXY, IsASessionCryptoResult, IsAURL, IsNullOrEmpty, IsValidURL, JSONSignatureVerificationStatus, MENNEKES_EDL40_OBIS, MENNEKES_EDL40_XMLNS, Mennekes, MennekesCrypt01, OBIS2Hex, OBIS2MeasurementName, OBIS_RegExpr, OCMF, OCMFBonnTariffParseError, OCMFTransactionTypes, OCMF_SIGNATURE_ALGORITHMS, OCMFv1_x, OCPI, OIDInfo, PCDF, PCDFCrypt01, PCDFParseError, PCDFValidationError, PCDF_FIELD_ORDER, PCDF_PREFIX, PTB, ParseJSON_LD, PublicKeyFormats, IPublicKeyInfo_exports as PublicKeyInfo, SAFEXML, SessionVerificationResult, SetHex, SetInt8, SetText, SetText_withLength, SetTimestamp, SetTimestamp32, SetUInt32, SetUInt32_withCode, SetUInt64, SetUInt64D, SignMessage, SignatureFormats, IURL_exports as SimpleURL, TimeStatusTypes, URLContext, UTC2human, VerificationResult, VerifyJSONMessageSignatures, WarningLevel, WhenNullOrEmpty, XMLContainer, asJSONArray, asJSONObject, asNumber, asString, base64ToBytes, buf2hex, buildEDL40Signature, buildIsaSignature, buildMennekesSignatureData, bytesToBase64, bytesToHex, canParseEDL40, canonicalJSONBytes, canonicalJSONStringify, cleanHex, closeFullscreen, collectDocumentPublicKeys, createCompatibleCurve, createHexString, createLegacyP192Curve, dateToMennekesLocalEpochSeconds, decodeSmlMessages, extractMennekesChargingProcesses, findEntryByObis, findGetListRes, firstKey, firstValue, generateSignatureKeyPair, getArrayElement, getArrayLikeElement, getDirectChildByLocalName, getDirectChildrenByLocalName, getElementsByLocalName, getFirstArrayElement, getInt16Bytes, getInt32Bytes, getInt64Bytes, getInt8Bytes, getLastArrayElement, getOCMFSignatureDisplay, getSignatureSuite, getTrimmedTextContent, hashFile, hex2bin, hex32, hexToArrayBuffer, hexToBytes, intFromBytes, isEncodedValue, isGeoLocation, isI18NString, isICryptoResult, isIFileInfo, isISessionCryptoResult1, isISessionCryptoResult2, isJSONLDObject, isMandatoryArrayOfStrings, isMandatoryBoolean, isMandatoryDecimal, isMandatoryJSONArray, isMandatoryJSONObject, isMandatoryNumber, isMandatoryString, isMandatoryURL, isOIDInfo, isObject, isOptionalArrayOfStrings, isOptionalDecimal, isOptionalJSONArray, isOptionalJSONArrayError, isOptionalJSONArrayOk, isOptionalJSONObject, isOptionalNumber, isOptionalString, isOptionalStringArray, isOptionalStringOrOIDInfo, isOptionalURL, isPCDFText, isPublicKeySubject, isString, isStringArray, isStringOrOIDInfo, isStringOrStringArray, isaListNameContext, jsonPrettyPrinter, measurementName2human, meterTimeZone, normalizePCDFPublicKeyHex, normalizeXMLText, ocmfBonnTariffToChargingTariff, openFullscreen, pad, parseAndVerifyJSONSignatures, parseDescription, parseEDL40, parseHexString, parseMennekesXMLDocument, parseNumber, parseOBIS, parseOCMFBonnTariffText, parsePCDFDocument, parsePCDFPublicKey, parsePCDFSignature, parseSmlTime, parseUTC, readQRCodeTextFromImage, readQRCodeTextFromImageData, readTLV, secp224k1, setUILocale, sha256, sha256____, sha384, sha384____, sha512, sha512____, signJSONMessage, signMessage, stripPCDFControlCharacters, stripTransport, time2human, timeZoneOffsetMinutes, toArrayBuffer, toSessionVerificationResults, toUint8Array, transformEDL40Status, tryParseOCMFBonnTariffText, unquotePCDFText, validatePCDFFields, verifyDocumentSignatures, verifyEDL40Document, verifyJSONMessageSignatureResults, verifyJSONMessageSignatures, verifyJSONSignature, verifyJSONSignatureResult, verifyPCDFDocument };
13817
14121
  //# sourceMappingURL=index.js.map
13818
14122
  //# sourceMappingURL=index.js.map