@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/CHANGELOG.md +498 -454
- package/README.md +522 -362
- package/dist/history/commit.d.ts.map +1 -1
- package/dist/history/epoch.d.ts +2 -0
- package/dist/history/epoch.d.ts.map +1 -1
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1404 -278
- package/dist/index.js.map +1 -1
- package/dist/keys.d.ts +2 -0
- package/dist/keys.d.ts.map +1 -1
- package/dist/proofs/stationProof.d.ts +2 -0
- package/dist/proofs/stationProof.d.ts.map +1 -1
- package/dist/proofs/trustContext.d.ts +6 -0
- package/dist/proofs/trustContext.d.ts.map +1 -1
- package/dist/verifier/chargingStation.d.ts +5 -0
- package/dist/verifier/chargingStation.d.ts.map +1 -1
- package/dist/verifier/documentKeys.d.ts +27 -0
- package/dist/verifier/documentKeys.d.ts.map +1 -0
- package/dist/verifier/historicalMeasurements.d.ts +96 -0
- package/dist/verifier/historicalMeasurements.d.ts.map +1 -0
- package/dist/verifier/measurementKeys.d.ts +48 -0
- package/dist/verifier/measurementKeys.d.ts.map +1 -0
- package/dist/verifier/observedVersions.d.ts +70 -0
- package/dist/verifier/observedVersions.d.ts.map +1 -0
- package/dist/verifier/pricePublication.d.ts +74 -0
- package/dist/verifier/pricePublication.d.ts.map +1 -0
- package/dist/verifier/sessionPrices.d.ts +75 -0
- package/dist/verifier/sessionPrices.d.ts.map +1 -0
- package/dist/verifier/trustTransitions.d.ts +67 -0
- package/dist/verifier/trustTransitions.d.ts.map +1 -0
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { encode, decode, cbor } from '@vanaheimr/metrological-cbor';
|
|
2
2
|
import { sha512, sha384, sha256 } from '@noble/hashes/sha2.js';
|
|
3
|
-
import { algorithmByName, verifyWith, CoseKey, KEY_TYPE_AKP, KEY_TYPE_OKP, KEY_TYPE_EC2 } from '@vanaheimr/cose';
|
|
3
|
+
import { algorithmByName, verifyWith, CoseKey, KEY_TYPE_AKP, KEY_TYPE_OKP, KEY_TYPE_EC2, DerReader, contentsOf, Tag, derObjectIdentifier, derBitString, isOnCurve, CoseCurves } from '@vanaheimr/cose';
|
|
4
4
|
|
|
5
5
|
// src/errors.ts
|
|
6
6
|
var POIError = class extends Error {
|
|
@@ -170,7 +170,7 @@ function sameValue(left, right) {
|
|
|
170
170
|
return false;
|
|
171
171
|
const leftKeys = Object.keys(left);
|
|
172
172
|
const rightKeys = Object.keys(right);
|
|
173
|
-
return leftKeys.length === rightKeys.length && leftKeys.every((
|
|
173
|
+
return leftKeys.length === rightKeys.length && leftKeys.every((key2) => Object.prototype.hasOwnProperty.call(right, key2) && sameValue(left[key2], right[key2]));
|
|
174
174
|
}
|
|
175
175
|
return left === right;
|
|
176
176
|
}
|
|
@@ -520,12 +520,12 @@ function cborToJson(value) {
|
|
|
520
520
|
return value.items.map(cborToJson);
|
|
521
521
|
case "map": {
|
|
522
522
|
const object = {};
|
|
523
|
-
for (const [
|
|
524
|
-
if (
|
|
523
|
+
for (const [key2, item] of value.entries) {
|
|
524
|
+
if (key2.type !== "text")
|
|
525
525
|
throw new POIError("A CBOR map with keys other than texts has no JSON representation!");
|
|
526
|
-
if (Object.prototype.hasOwnProperty.call(object,
|
|
527
|
-
throw new POIError(`A CBOR map names '${
|
|
528
|
-
set(object,
|
|
526
|
+
if (Object.prototype.hasOwnProperty.call(object, key2.value))
|
|
527
|
+
throw new POIError(`A CBOR map names '${key2.value}' twice!`);
|
|
528
|
+
set(object, key2.value, cborToJson(item));
|
|
529
529
|
}
|
|
530
530
|
return object;
|
|
531
531
|
}
|
|
@@ -817,22 +817,22 @@ function signatureAlgorithm(name) {
|
|
|
817
817
|
throw new POIError(`Unknown or noncanonical signature algorithm '${name}'!`);
|
|
818
818
|
return algorithm;
|
|
819
819
|
}
|
|
820
|
-
function publicKeyBytes(
|
|
820
|
+
function publicKeyBytes(key2, algorithm) {
|
|
821
821
|
switch (algorithm.family) {
|
|
822
822
|
case "ecdsa":
|
|
823
|
-
if (
|
|
823
|
+
if (key2.keyType !== KEY_TYPE_EC2 || key2.x === null || key2.y === null)
|
|
824
824
|
throw new POIError(`The algorithm '${algorithm.name}' needs an elliptic curve key with x and y!`);
|
|
825
|
-
return Uint8Array.of(4, ...
|
|
825
|
+
return Uint8Array.of(4, ...key2.x, ...key2.y);
|
|
826
826
|
case "eddsa":
|
|
827
|
-
if (
|
|
827
|
+
if (key2.keyType !== KEY_TYPE_OKP || key2.x === null)
|
|
828
828
|
throw new POIError(`The algorithm '${algorithm.name}' needs an octet key pair!`);
|
|
829
|
-
return
|
|
829
|
+
return key2.x;
|
|
830
830
|
case "mldsa":
|
|
831
|
-
if (
|
|
831
|
+
if (key2.keyType !== KEY_TYPE_AKP || key2.pub === null)
|
|
832
832
|
throw new POIError(`The algorithm '${algorithm.name}' needs an algorithm key pair!`);
|
|
833
|
-
if (
|
|
834
|
-
throw new POIError(`The algorithm '${algorithm.name}' differs from the one of its key, '${
|
|
835
|
-
return
|
|
833
|
+
if (key2.algorithm?.name !== algorithm.name)
|
|
834
|
+
throw new POIError(`The algorithm '${algorithm.name}' differs from the one of its key, '${key2.algorithm?.name ?? "none"}'!`);
|
|
835
|
+
return key2.pub;
|
|
836
836
|
default:
|
|
837
837
|
throw new POIError(`'${algorithm.name}' is no signature algorithm!`);
|
|
838
838
|
}
|
|
@@ -852,19 +852,21 @@ function verifySignature(algorithmName, publicKey, input, value) {
|
|
|
852
852
|
if (signature.length === 0)
|
|
853
853
|
throw new POIError("The signature value must be nonempty canonical standard Base64!");
|
|
854
854
|
const keyBytes = publicKeyBytes(publicKey, algorithm);
|
|
855
|
-
let
|
|
855
|
+
let valid2;
|
|
856
856
|
try {
|
|
857
|
-
|
|
857
|
+
valid2 = verifyWith(algorithm, publicKey.curve, input, signature, keyBytes);
|
|
858
858
|
} catch (e) {
|
|
859
859
|
throw new POIError(`The signature is invalid: ${e instanceof Error ? e.message : String(e)}`);
|
|
860
860
|
}
|
|
861
|
-
if (!
|
|
861
|
+
if (!valid2)
|
|
862
862
|
throw new POIError("The signature is invalid!");
|
|
863
863
|
}
|
|
864
864
|
var KeyUsages = {
|
|
865
865
|
Infrastructure: "infrastructure",
|
|
866
|
+
ChargeTransparencyRecords: "chargeTransparencyRecords",
|
|
866
867
|
Tariffs: "tariffs",
|
|
867
868
|
PriceStatements: "priceStatements",
|
|
869
|
+
WitnessPublications: "witnessPublications",
|
|
868
870
|
Models: "models",
|
|
869
871
|
Software: "software",
|
|
870
872
|
Certificates: "certificates",
|
|
@@ -886,9 +888,9 @@ var isDeterministicCbor = (bytes) => {
|
|
|
886
888
|
}
|
|
887
889
|
};
|
|
888
890
|
var POIPublicKey = class _POIPublicKey {
|
|
889
|
-
constructor(id,
|
|
891
|
+
constructor(id, key2, keyBytes, notBefore, notAfter, holder, usages, json) {
|
|
890
892
|
this.id = id;
|
|
891
|
-
this.key =
|
|
893
|
+
this.key = key2;
|
|
892
894
|
this.keyBytes = keyBytes;
|
|
893
895
|
this.notBefore = notBefore;
|
|
894
896
|
this.notAfter = notAfter;
|
|
@@ -932,20 +934,20 @@ var POIPublicKey = class _POIPublicKey {
|
|
|
932
934
|
if (id === void 0 || keyText === void 0)
|
|
933
935
|
throw new POIError("A public key needs its identification and its key!");
|
|
934
936
|
let keyBytes;
|
|
935
|
-
let
|
|
937
|
+
let key2;
|
|
936
938
|
try {
|
|
937
939
|
keyBytes = fromBase64(keyText);
|
|
938
940
|
} catch {
|
|
939
941
|
throw new POIError(isDotNetBase64(keyText) ? "A public key must be given as canonical Base64!" : `Invalid public key: ${DOTNET_BAD_BASE64}`);
|
|
940
942
|
}
|
|
941
943
|
try {
|
|
942
|
-
|
|
944
|
+
key2 = CoseKey.parse(keyBytes);
|
|
943
945
|
} catch (e) {
|
|
944
946
|
throw new POIError(`Invalid COSE key: ${e instanceof Error ? e.message : String(e)}`);
|
|
945
947
|
}
|
|
946
948
|
if (!isDeterministicCbor(keyBytes))
|
|
947
949
|
throw new POIError(`Invalid COSE key: Invalid deterministic CBOR: the key of the public key '${id}' is not written as the deterministic CBOR of its COSE key!`);
|
|
948
|
-
if (
|
|
950
|
+
if (key2.isPrivate)
|
|
949
951
|
throw new POIError("A public key must not contain private parts!");
|
|
950
952
|
const notBefore = optionalTimestamp(json, "notBefore", `The notBefore of the key '${id}'`);
|
|
951
953
|
const notAfter = optionalTimestamp(json, "notAfter", `The notAfter of the key '${id}'`);
|
|
@@ -973,13 +975,13 @@ var POIPublicKey = class _POIPublicKey {
|
|
|
973
975
|
throw new POIError("Invalid public key: A public key must not expire before it becomes valid!");
|
|
974
976
|
let thumbprint;
|
|
975
977
|
try {
|
|
976
|
-
thumbprint = toHex(
|
|
978
|
+
thumbprint = toHex(key2.thumbprint("sha256"));
|
|
977
979
|
} catch (e) {
|
|
978
980
|
throw new POIError(`Invalid public key: ${e instanceof Error ? e.message : String(e)}`);
|
|
979
981
|
}
|
|
980
982
|
if (thumbprint !== id)
|
|
981
983
|
throw new POIError(`The identification '${id}' of a public key is not its thumbprint '${thumbprint}'!`);
|
|
982
|
-
return new _POIPublicKey(id,
|
|
984
|
+
return new _POIPublicKey(id, key2, keyBytes, notBefore, notAfter, holder, usages, json);
|
|
983
985
|
}
|
|
984
986
|
/** Reads an array of keys, refusing a key given twice. */
|
|
985
987
|
static parseAll(value, what) {
|
|
@@ -988,7 +990,7 @@ var POIPublicKey = class _POIPublicKey {
|
|
|
988
990
|
if (!isArray(value))
|
|
989
991
|
throw new POIError(`The ${what} must be a JSON array!`);
|
|
990
992
|
const keys = value.map((item) => _POIPublicKey.parse(item, `A key of the ${what}`));
|
|
991
|
-
if (new Set(keys.map((
|
|
993
|
+
if (new Set(keys.map((key2) => key2.id)).size !== keys.length)
|
|
992
994
|
throw new POIError(`A key of the ${what} must not be given twice!`);
|
|
993
995
|
return keys;
|
|
994
996
|
}
|
|
@@ -1004,16 +1006,16 @@ function parseETag(value, hexOnly = false) {
|
|
|
1004
1006
|
throw new POIError(`Unknown ETag format '${format}'!`);
|
|
1005
1007
|
if (!isHashAlgorithm(algorithm))
|
|
1006
1008
|
throw new POIError(`Unknown ETag hash algorithm '${algorithm}'!`);
|
|
1007
|
-
let
|
|
1009
|
+
let digest2;
|
|
1008
1010
|
if (encoding === "hex")
|
|
1009
|
-
|
|
1011
|
+
digest2 = fromHex(text, true);
|
|
1010
1012
|
else if (encoding === "base64" && !hexOnly)
|
|
1011
|
-
|
|
1013
|
+
digest2 = fromBase64(text);
|
|
1012
1014
|
else
|
|
1013
1015
|
throw new POIError(hexOnly ? `An ETag of a change set, commit or checkpoint must be written in hex, not in '${encoding}'!` : `Unknown ETag encoding '${encoding}'!`);
|
|
1014
|
-
if (
|
|
1015
|
-
throw new POIError(`A ${algorithm} ETag has ${DIGEST_LENGTH[algorithm]} bytes, not ${
|
|
1016
|
-
return { format, algorithm, digest };
|
|
1016
|
+
if (digest2.length !== DIGEST_LENGTH[algorithm])
|
|
1017
|
+
throw new POIError(`A ${algorithm} ETag has ${DIGEST_LENGTH[algorithm]} bytes, not ${digest2.length}!`);
|
|
1018
|
+
return { format, algorithm, digest: digest2 };
|
|
1017
1019
|
}
|
|
1018
1020
|
function parseETags(value, hexOnly = false, what = "The ETags") {
|
|
1019
1021
|
if (!isArray(value))
|
|
@@ -1141,18 +1143,18 @@ var groupId = (letters) => {
|
|
|
1141
1143
|
return match === null ? ignoringCase(text) : `${ignoringCase(match[1] ?? "")}*${ignoringCase(match[2] ?? "")}*${letters}${ignoringCase(match[3] ?? "")}`;
|
|
1142
1144
|
};
|
|
1143
1145
|
};
|
|
1144
|
-
function twice(entities,
|
|
1146
|
+
function twice(entities, key2) {
|
|
1145
1147
|
const seen = /* @__PURE__ */ new Set();
|
|
1146
1148
|
for (const entity of entities) {
|
|
1147
|
-
const id =
|
|
1149
|
+
const id = key2(idOf(entity));
|
|
1148
1150
|
if (seen.has(id))
|
|
1149
1151
|
return entity;
|
|
1150
1152
|
seen.add(id);
|
|
1151
1153
|
}
|
|
1152
1154
|
return void 0;
|
|
1153
1155
|
}
|
|
1154
|
-
var givenTwice = (what, entities,
|
|
1155
|
-
const duplicate = twice(entities,
|
|
1156
|
+
var givenTwice = (what, entities, key2) => {
|
|
1157
|
+
const duplicate = twice(entities, key2);
|
|
1156
1158
|
return duplicate === void 0 ? void 0 : `The ${what} '${idOf(duplicate)}' must not be given twice in the roaming network!`;
|
|
1157
1159
|
};
|
|
1158
1160
|
var childrenOf = (parties, property) => parties.flatMap((party) => objectsOf(get(party, property)));
|
|
@@ -1440,20 +1442,20 @@ function applyTemplates(member, templates, rules) {
|
|
|
1440
1442
|
for (const [property, rule] of Object.entries(rules.unions)) {
|
|
1441
1443
|
const own = get(member, property);
|
|
1442
1444
|
const elements = isArray(own) ? [...own] : [];
|
|
1443
|
-
const
|
|
1444
|
-
const taken = new Set(elements.map(
|
|
1445
|
+
const key2 = rule.key ?? byJSON;
|
|
1446
|
+
const taken = new Set(elements.map(key2));
|
|
1445
1447
|
for (const template of templates) {
|
|
1446
1448
|
const values = get(template, property);
|
|
1447
1449
|
if (isArray(values)) {
|
|
1448
1450
|
for (const element of values)
|
|
1449
|
-
if (!taken.has(
|
|
1450
|
-
taken.add(
|
|
1451
|
+
if (!taken.has(key2(element))) {
|
|
1452
|
+
taken.add(key2(element));
|
|
1451
1453
|
elements.push(clone(element));
|
|
1452
1454
|
}
|
|
1453
1455
|
}
|
|
1454
1456
|
}
|
|
1455
1457
|
if (rule.order === "ordinal") {
|
|
1456
|
-
const sortBy = rule.sortBy ??
|
|
1458
|
+
const sortBy = rule.sortBy ?? key2;
|
|
1457
1459
|
elements.sort((left, right) => ordinal(sortBy(left), sortBy(right)));
|
|
1458
1460
|
}
|
|
1459
1461
|
if (elements.length > 0)
|
|
@@ -1713,8 +1715,8 @@ function instantOf(text) {
|
|
|
1713
1715
|
const date = /* @__PURE__ */ new Date(0);
|
|
1714
1716
|
date.setUTCFullYear(Number(year), Number(month) - 1, Number(day));
|
|
1715
1717
|
date.setUTCHours(Number(hour), Number(minute), Number(second), Number(digits.slice(0, 3)));
|
|
1716
|
-
const
|
|
1717
|
-
return
|
|
1718
|
+
const valid2 = Number(year) >= 1 && date.getUTCFullYear() === Number(year) && date.getUTCMonth() === Number(month) - 1 && date.getUTCDate() === Number(day) && date.getUTCHours() === Number(hour) && date.getUTCMinutes() === Number(minute) && date.getUTCSeconds() === Number(second) && Number(offsetMinutes) <= 59 && offset <= 14 * 60;
|
|
1719
|
+
return valid2 ? [date.getTime() - (sign === "-" ? -offset : offset) * 6e4, Number(digits.slice(3))] : void 0;
|
|
1718
1720
|
}
|
|
1719
1721
|
var invalidInstant = (json, property) => {
|
|
1720
1722
|
const value = get(json, property);
|
|
@@ -1832,9 +1834,9 @@ function invalidConstruction(certificate, id) {
|
|
|
1832
1834
|
return `The certificate '${id}' of an energy meter model names no energy meters! (Parameter 'EnergyMeters')`;
|
|
1833
1835
|
const models = /* @__PURE__ */ new Map();
|
|
1834
1836
|
for (const meter of energyMeters) {
|
|
1835
|
-
const
|
|
1836
|
-
const model = models.get(
|
|
1837
|
-
models.set(
|
|
1837
|
+
const key2 = `${idKey(idAt(meter, "manufacturerId"))}\0${idKey(idAt(meter, "modelId"))}`;
|
|
1838
|
+
const model = models.get(key2);
|
|
1839
|
+
models.set(key2, { first: model?.first ?? meter, count: (model?.count ?? 0) + 1 });
|
|
1838
1840
|
}
|
|
1839
1841
|
const twiceNamed = [...models.values()].find((model) => model.count > 1)?.first;
|
|
1840
1842
|
return twiceNamed === void 0 ? void 0 : `The energy meter model '${idAt(twiceNamed, "modelId")}' of '${idAt(twiceNamed, "manufacturerId")}' must not be given twice in the certificate '${id}'! (Parameter 'EnergyMeters')`;
|
|
@@ -2104,12 +2106,12 @@ function optionalI18N(json, property, description) {
|
|
|
2104
2106
|
}
|
|
2105
2107
|
function i18nOf(value, prefix) {
|
|
2106
2108
|
const texts2 = [];
|
|
2107
|
-
for (const [
|
|
2108
|
-
const language =
|
|
2109
|
+
for (const [key2, text] of Object.entries(value)) {
|
|
2110
|
+
const language = key2.toLowerCase();
|
|
2109
2111
|
if (!LANGUAGES.has(language))
|
|
2110
|
-
throw new POIError(`${prefix}'${
|
|
2112
|
+
throw new POIError(`${prefix}'${key2}' is no language!`);
|
|
2111
2113
|
if (typeof text !== "string")
|
|
2112
|
-
throw new POIError(`${prefix}the text in '${
|
|
2114
|
+
throw new POIError(`${prefix}the text in '${key2}' must be a text!`);
|
|
2113
2115
|
if (texts2.some(([other]) => other === language))
|
|
2114
2116
|
throw new POIError(`${prefix}the language '${language}' is given twice!`);
|
|
2115
2117
|
texts2.push([language, text]);
|
|
@@ -2134,8 +2136,8 @@ var ignoringCase2 = (text) => Array.from(text, (character) => {
|
|
|
2134
2136
|
}).join("");
|
|
2135
2137
|
var sameId = (left, right) => left !== void 0 && right !== void 0 && ignoringCase2(left) === ignoringCase2(right);
|
|
2136
2138
|
var sameText = (left, right) => left === void 0 ? right === void 0 : sameId(left, right);
|
|
2137
|
-
function distinct(values,
|
|
2138
|
-
if (new Set(values.map(
|
|
2139
|
+
function distinct(values, key2, description) {
|
|
2140
|
+
if (new Set(values.map(key2)).size !== values.length)
|
|
2139
2141
|
throw new POIError(`An ${description} must not be given twice!`);
|
|
2140
2142
|
return [...values];
|
|
2141
2143
|
}
|
|
@@ -2215,8 +2217,8 @@ function urlOf(json, property, description) {
|
|
|
2215
2217
|
}
|
|
2216
2218
|
function lenientI18N(value) {
|
|
2217
2219
|
const texts2 = /* @__PURE__ */ new Map();
|
|
2218
|
-
for (const [
|
|
2219
|
-
const language = trimmed(
|
|
2220
|
+
for (const [key2, text] of Object.entries(value)) {
|
|
2221
|
+
const language = trimmed(key2).toLowerCase();
|
|
2220
2222
|
if (typeof text === "string" && (LANGUAGES.has(language) || /^[+-]?\d+$/u.test(language)))
|
|
2221
2223
|
texts2.set(language, text);
|
|
2222
2224
|
}
|
|
@@ -2389,9 +2391,9 @@ function keysOf(party, property, description) {
|
|
|
2389
2391
|
if (!isObject(item))
|
|
2390
2392
|
throw new POIError(`Invalid ${description} '${compactJSON(item)}'!`);
|
|
2391
2393
|
try {
|
|
2392
|
-
const
|
|
2393
|
-
onlyProperties(item, KEY_PROPERTIES, `The public key '${
|
|
2394
|
-
return
|
|
2394
|
+
const key2 = POIPublicKey.parse(item);
|
|
2395
|
+
onlyProperties(item, KEY_PROPERTIES, `The public key '${key2.id}'`);
|
|
2396
|
+
return key2;
|
|
2395
2397
|
} catch (e) {
|
|
2396
2398
|
throw new POIError(`Invalid ${description} '${compactJSON(item)}': ${e instanceof Error ? e.message : String(e)}`);
|
|
2397
2399
|
}
|
|
@@ -2402,16 +2404,16 @@ function partyKeysOf(party, name, kind) {
|
|
|
2402
2404
|
const identityKeys = keysOf(party, "identityKeys", "identity key");
|
|
2403
2405
|
const invalid = (message) => new POIError(`The given JSON representation of ${kind} is invalid: ${message}`);
|
|
2404
2406
|
for (const [keys, description] of [[signingKeys, "signing key"], [identityKeys, "identity key"]]) {
|
|
2405
|
-
const ids = keys.map((
|
|
2407
|
+
const ids = keys.map((key2) => key2.id);
|
|
2406
2408
|
if (new Set(ids).size !== ids.length)
|
|
2407
2409
|
throw invalid(`An ${description} of ${name} must not be given twice!`);
|
|
2408
2410
|
}
|
|
2409
|
-
const signingIds = signingKeys.map((
|
|
2411
|
+
const signingIds = signingKeys.map((key2) => key2.id).sort(ordinal);
|
|
2410
2412
|
const identities = [...identityKeys].sort((a, b) => ordinal(a.id, b.id));
|
|
2411
|
-
const both = signingIds.find((id) => identityKeys.some((
|
|
2413
|
+
const both = signingIds.find((id) => identityKeys.some((key2) => key2.id === id));
|
|
2412
2414
|
if (both !== void 0)
|
|
2413
2415
|
throw invalid(`The key '${both}' of ${name} must not be both a signing and an identity key!`);
|
|
2414
|
-
const withUsages = identities.find((
|
|
2416
|
+
const withUsages = identities.find((key2) => key2.usages.length > 0);
|
|
2415
2417
|
if (withUsages !== void 0)
|
|
2416
2418
|
throw invalid(`The identity key '${withUsages.id}' of ${name} names no usages: they are of signing keys!`);
|
|
2417
2419
|
return { signingKeys, identityKeys };
|
|
@@ -3449,23 +3451,23 @@ function asRead(value, kind) {
|
|
|
3449
3451
|
if (!isObject(value))
|
|
3450
3452
|
return value;
|
|
3451
3453
|
const known = PROPERTIES_OF[kind];
|
|
3452
|
-
return Object.fromEntries(Object.entries(value).filter(([
|
|
3454
|
+
return Object.fromEntries(Object.entries(value).filter(([key2]) => known === void 0 || known.includes(key2)).map(([key2, element]) => [key2, PROPERTIES_OF[key2] === void 0 ? element : asRead(element, key2)]));
|
|
3453
3455
|
}
|
|
3454
3456
|
function notWritten(value, property, path, of) {
|
|
3455
3457
|
const eachOnce = of === "new value";
|
|
3456
|
-
const join = (at,
|
|
3458
|
+
const join = (at, key2) => at === "" ? key2 : `${at}.${key2}`;
|
|
3457
3459
|
const holdsAs = (at, written) => ({ path: at, reason: `the content holds it as '${contentText(written)}'` });
|
|
3458
|
-
const leftOut = (
|
|
3460
|
+
const leftOut = (key2, element, kind, keepsEmptyName) => {
|
|
3459
3461
|
if (element === null)
|
|
3460
3462
|
return "a property without a value is left out";
|
|
3461
|
-
if (isArray(element) && element.length === 0 && LEFT_OUT_WHEN_EMPTY.has(
|
|
3463
|
+
if (isArray(element) && element.length === 0 && LEFT_OUT_WHEN_EMPTY.has(key2) && !(key2 === "identityKeys" && kind === "keyStatement"))
|
|
3462
3464
|
return "a list without elements is left out";
|
|
3463
|
-
if (TEXTS_IN_LANGUAGES.has(
|
|
3465
|
+
if (TEXTS_IN_LANGUAGES.has(key2) && isObject(element) && languagesOf(element).length === 0 && !(key2 === "name" && keepsEmptyName))
|
|
3464
3466
|
return "the content leaves it out";
|
|
3465
|
-
if (
|
|
3467
|
+
if (key2 === "dataLicenseIds")
|
|
3466
3468
|
return "the content leaves it out";
|
|
3467
3469
|
const known = kind === void 0 ? void 0 : PROPERTIES_OF[kind];
|
|
3468
|
-
if (known !== void 0 && !known.includes(
|
|
3470
|
+
if (known !== void 0 && !known.includes(key2))
|
|
3469
3471
|
return "the content leaves it out";
|
|
3470
3472
|
return void 0;
|
|
3471
3473
|
};
|
|
@@ -3476,7 +3478,7 @@ function notWritten(value, property, path, of) {
|
|
|
3476
3478
|
if (name !== void 0 && ORDERED_TEXTS.has(name) && item.some((element) => typeof element === "string" && trimmed(element) !== element))
|
|
3477
3479
|
return { path: at, reason: "the texts of its elements are trimmed" };
|
|
3478
3480
|
const keys = orderKeys(item, name);
|
|
3479
|
-
if (keys?.some((
|
|
3481
|
+
if (keys?.some((key2, index) => index > 0 && ordinal(keys[index - 1] ?? "", key2) >= (eachOnce ? 0 : 1)) === true)
|
|
3480
3482
|
return { path: at, reason: eachOnce ? "its elements are in the order the content holds them in, each once" : "its elements are in the order the content holds them in" };
|
|
3481
3483
|
for (const [index, element] of item.entries()) {
|
|
3482
3484
|
const elementAt = `${at}[${String(index)}]`;
|
|
@@ -3497,22 +3499,22 @@ function notWritten(value, property, path, of) {
|
|
|
3497
3499
|
const keepsEmptyName = name === void 0 && holder === "chargingStationOperators";
|
|
3498
3500
|
if (name !== void 0 && TEXTS_IN_LANGUAGES.has(name) && [...Object.keys(item)].sort(ordinal).join("\0") !== languagesOf(item).sort(ordinal).join("\0"))
|
|
3499
3501
|
return { path: at, reason: "a text in languages names each language as Styx writes it, in lower case, with a text" };
|
|
3500
|
-
for (const [
|
|
3501
|
-
if (
|
|
3502
|
+
for (const [key2, element] of Object.entries(item)) {
|
|
3503
|
+
if (key2 === "@context") {
|
|
3502
3504
|
if (root)
|
|
3503
3505
|
continue;
|
|
3504
3506
|
return { path: at, reason: "embedded, an object names no JSON-LD context" };
|
|
3505
3507
|
}
|
|
3506
|
-
const elementAt = join(at,
|
|
3507
|
-
const omitted2 = leftOut(
|
|
3508
|
+
const elementAt = join(at, key2);
|
|
3509
|
+
const omitted2 = leftOut(key2, element, kind, keepsEmptyName);
|
|
3508
3510
|
if (omitted2 !== void 0)
|
|
3509
3511
|
return { path: elementAt, reason: omitted2 };
|
|
3510
|
-
if (typeof element === "string" && TIMESTAMP_PROPERTIES.has(
|
|
3512
|
+
if (typeof element === "string" && TIMESTAMP_PROPERTIES.has(key2) && !TIMESTAMP3.test(element))
|
|
3511
3513
|
return { path: elementAt, reason: "a timestamp is UTC to the millisecond, yyyy-MM-ddTHH:mm:ss.fffZ" };
|
|
3512
|
-
const written =
|
|
3514
|
+
const written = key2 === "customData" ? void 0 : writtenValueOf(key2, element, kind);
|
|
3513
3515
|
if (written !== void 0 && contentText(written) !== contentText(element))
|
|
3514
3516
|
return holdsAs(elementAt, written);
|
|
3515
|
-
const found = check2(element,
|
|
3517
|
+
const found = check2(element, key2, kind, elementAt);
|
|
3516
3518
|
if (found !== void 0)
|
|
3517
3519
|
return found;
|
|
3518
3520
|
}
|
|
@@ -3537,10 +3539,10 @@ function notWrittenAsHeld(content) {
|
|
|
3537
3539
|
return value.map(unknownLicense).find((found2) => found2 !== void 0);
|
|
3538
3540
|
if (!isObject(value))
|
|
3539
3541
|
return void 0;
|
|
3540
|
-
for (const [
|
|
3541
|
-
if (
|
|
3542
|
+
for (const [key2, element] of Object.entries(value)) {
|
|
3543
|
+
if (key2 === "customData")
|
|
3542
3544
|
continue;
|
|
3543
|
-
const unknown =
|
|
3545
|
+
const unknown = key2 === "dataLicenseIds" && isArray(element) ? element.find((id) => typeof id === "string" && !KNOWN_DATA_LICENSES.has(id)) : void 0;
|
|
3544
3546
|
if (unknown !== void 0)
|
|
3545
3547
|
return `The data license '${asText(unknown)}' is not well-known, so it must be given completely!`;
|
|
3546
3548
|
const found2 = unknownLicense(element);
|
|
@@ -3719,8 +3721,8 @@ function prunedDigest(entity, kind, algorithm, path = "$") {
|
|
|
3719
3721
|
return { digest: leafHash(algorithm, canonicalJSONBytes(skeletonJSON)), attested };
|
|
3720
3722
|
}
|
|
3721
3723
|
function prunedTree(roamingNetwork, algorithm) {
|
|
3722
|
-
const { digest, attested } = prunedDigest(roamingNetwork, "roamingNetwork", algorithm);
|
|
3723
|
-
return { root:
|
|
3724
|
+
const { digest: digest2, attested } = prunedDigest(roamingNetwork, "roamingNetwork", algorithm);
|
|
3725
|
+
return { root: digest2, attested };
|
|
3724
3726
|
}
|
|
3725
3727
|
|
|
3726
3728
|
// src/proofs/keyStatement.ts
|
|
@@ -3788,9 +3790,9 @@ var KeyStatement = class _KeyStatement {
|
|
|
3788
3790
|
return { algorithm, keyId, value: value2 };
|
|
3789
3791
|
});
|
|
3790
3792
|
onlyProperties(json, ["@context", "roamingNetworkId", "roamingNetworkOperatorId", "identityKeys", "minConsensus", "signedAt", "validUntil", "signatures"], "A key statement");
|
|
3791
|
-
if (identityKeys.some((
|
|
3793
|
+
if (identityKeys.some((key2, index) => {
|
|
3792
3794
|
const before = identityKeys[index - 1];
|
|
3793
|
-
return before !== void 0 && before.id >=
|
|
3795
|
+
return before !== void 0 && before.id >= key2.id;
|
|
3794
3796
|
}))
|
|
3795
3797
|
throw new POIError("The identity keys of a key statement are written in the order of their @ids!");
|
|
3796
3798
|
if (validUntil < signedAt)
|
|
@@ -3803,7 +3805,7 @@ var KeyStatement = class _KeyStatement {
|
|
|
3803
3805
|
"@context": KEY_STATEMENT_CONTEXT,
|
|
3804
3806
|
roamingNetworkId: this.roamingNetworkId,
|
|
3805
3807
|
roamingNetworkOperatorId: this.roamingNetworkOperatorId,
|
|
3806
|
-
identityKeys: this.identityKeys.map((
|
|
3808
|
+
identityKeys: this.identityKeys.map((key2) => without(key2.json, "@context")),
|
|
3807
3809
|
minConsensus: this.minConsensus,
|
|
3808
3810
|
signedAt: toTimestamp(this.signedAt),
|
|
3809
3811
|
validUntil: toTimestamp(this.validUntil)
|
|
@@ -3826,8 +3828,8 @@ var KeyStatement = class _KeyStatement {
|
|
|
3826
3828
|
/** The `@id`s of its identity keys per holder, as it names their holders: what a trust anchor of it pins (8.6). */
|
|
3827
3829
|
get holders() {
|
|
3828
3830
|
const holders = /* @__PURE__ */ new Map();
|
|
3829
|
-
for (const
|
|
3830
|
-
holders.set(
|
|
3831
|
+
for (const key2 of this.identityKeys)
|
|
3832
|
+
holders.set(key2.holderOrUnnamed, [...holders.get(key2.holderOrUnnamed) ?? [], key2.id]);
|
|
3831
3833
|
return [...holders.values()];
|
|
3832
3834
|
}
|
|
3833
3835
|
/**
|
|
@@ -3880,7 +3882,7 @@ var KeyStatement = class _KeyStatement {
|
|
|
3880
3882
|
if (only2 !== void 0 && this.signatures.length === 1 && required2 === 1 && !trusted.has(only2.keyId))
|
|
3881
3883
|
throw new POIError(`The key statement is signed by the key '${only2.keyId}', which is not trusted!`);
|
|
3882
3884
|
for (const signature of this.signatures) {
|
|
3883
|
-
const identityKey = keys.find((
|
|
3885
|
+
const identityKey = keys.find((key2) => key2.id === signature.keyId);
|
|
3884
3886
|
if (identityKey === void 0)
|
|
3885
3887
|
throw new POIError(`The identity key '${signature.keyId}' that signed the key statement is not given!`);
|
|
3886
3888
|
if (!identityKey.isValidAt(this.signedAt))
|
|
@@ -3894,7 +3896,7 @@ var KeyStatement = class _KeyStatement {
|
|
|
3894
3896
|
throw new POIError(`The key statement is signed by trusted identity keys of ${holders.size} holder${holders.size === 1 ? "" : "s"}, not of ${required2}!`);
|
|
3895
3897
|
if (options.at !== void 0 && !this.isValidAt(options.at))
|
|
3896
3898
|
throw new POIError(`The key statement is not valid at ${toTimestamp(options.at)}: it was signed at ${toTimestamp(this.signedAt)} and is valid until ${toTimestamp(this.validUntil)}!`);
|
|
3897
|
-
const trustedThen = new Map(this.identityKeys.map((
|
|
3899
|
+
const trustedThen = new Map(this.identityKeys.map((key2) => [key2.id, key2.holderOrUnnamed]));
|
|
3898
3900
|
for (const [id, holder] of trusted)
|
|
3899
3901
|
if (!trustedThen.has(id))
|
|
3900
3902
|
trustedThen.set(id, nameOfHolder(holder));
|
|
@@ -3904,7 +3906,7 @@ var KeyStatement = class _KeyStatement {
|
|
|
3904
3906
|
|
|
3905
3907
|
// src/proofs/signingKeyCertificate.ts
|
|
3906
3908
|
var SIGNING_KEY_CERTIFICATE_CONTEXT = CONTEXT_PREFIX + "roamingNetwork/signingKeyCertificate/v2";
|
|
3907
|
-
var describeHolder = (
|
|
3909
|
+
var describeHolder = (key2) => key2.holder !== void 0 ? `the holder '${key2.holder}'` : "no named holder";
|
|
3908
3910
|
var SigningKeyCertificate = class _SigningKeyCertificate {
|
|
3909
3911
|
constructor(roamingNetworkOperatorId, signingKey, signedAt, algorithm, keyId, value, json) {
|
|
3910
3912
|
this.roamingNetworkOperatorId = roamingNetworkOperatorId;
|
|
@@ -3965,7 +3967,7 @@ var SigningKeyCertificate = class _SigningKeyCertificate {
|
|
|
3965
3967
|
* where trusted keys are given, of the same holder as the signing key, valid at `signedAt`. Returns that identity key.
|
|
3966
3968
|
*/
|
|
3967
3969
|
verify(identityKeys, trustedIdentityKeyIds) {
|
|
3968
|
-
const identityKey = identityKeys.find((
|
|
3970
|
+
const identityKey = identityKeys.find((key2) => key2.id === this.keyId);
|
|
3969
3971
|
if (identityKey === void 0)
|
|
3970
3972
|
throw new POIError(`The identity key '${this.keyId}' that certified the signing key '${this.signingKey.id}' is not given!`);
|
|
3971
3973
|
if (trustedIdentityKeyIds !== void 0 && !trustedIdentityKeyIds.has(this.keyId))
|
|
@@ -3988,7 +3990,7 @@ var ROAMING_NETWORK_CONTEXT = CONTEXT_PREFIX + "roamingNetwork/v1";
|
|
|
3988
3990
|
var KeyStatementError = class extends POIError {
|
|
3989
3991
|
};
|
|
3990
3992
|
var sameId2 = (left, right) => ignoringCase(trimmed(left)) === ignoringCase(trimmed(right));
|
|
3991
|
-
var holdersOf = (keys) => new Set(keys.map((
|
|
3993
|
+
var holdersOf = (keys) => new Set(keys.map((key2) => key2.holderOrUnnamed));
|
|
3992
3994
|
var RoamingNetworkOperator = class _RoamingNetworkOperator {
|
|
3993
3995
|
constructor(id, identityKeys, minConsensus, keyStatement, signingKeyCertificates, json) {
|
|
3994
3996
|
this.id = id;
|
|
@@ -4046,7 +4048,7 @@ var RoamingNetworkOperator = class _RoamingNetworkOperator {
|
|
|
4046
4048
|
certified.set(certificate.signingKey.id, certificate.signingKey);
|
|
4047
4049
|
}
|
|
4048
4050
|
const signingKeys = [...certified.values()];
|
|
4049
|
-
const both = [...signingKeys].sort((a, b) => a.id < b.id ? -1 : a.id > b.id ? 1 : 0).find((
|
|
4051
|
+
const both = [...signingKeys].sort((a, b) => a.id < b.id ? -1 : a.id > b.id ? 1 : 0).find((key2) => identityKeys.some((identityKey) => identityKey.id === key2.id));
|
|
4050
4052
|
if (both !== void 0)
|
|
4051
4053
|
throw invalid(`The key '${both.id}' of the roaming network operator '${id}' must not be both a signing and an identity key!`);
|
|
4052
4054
|
for (const [kind, keys] of [["signing", signingKeys], ["identity", identityKeys]]) {
|
|
@@ -4075,15 +4077,15 @@ var RoamingNetworkOperator = class _RoamingNetworkOperator {
|
|
|
4075
4077
|
if (keyStatement !== void 0) {
|
|
4076
4078
|
if (!sameId2(keyStatement.roamingNetworkOperatorId, id))
|
|
4077
4079
|
throw new KeyStatementError(`The key statement of the roaming network operator '${id}' is one of '${keyStatement.roamingNetworkOperatorId}'!`);
|
|
4078
|
-
const unknown = keyStatement.identityKeys.find((
|
|
4080
|
+
const unknown = keyStatement.identityKeys.find((key2) => !identityKeys.some((identityKey) => sameValue(without(asRead(identityKey.json, "identityKeys"), "@context"), without(key2.json, "@context"))));
|
|
4079
4081
|
if (unknown !== void 0)
|
|
4080
4082
|
throw new KeyStatementError(`The key statement of the roaming network operator '${id}' names the identity key '${unknown.id}', which is not one of its identity keys!`);
|
|
4081
4083
|
if (keyStatement.minConsensus !== minConsensus)
|
|
4082
4084
|
throw new KeyStatementError(`The key statement of the roaming network operator '${id}' states the minConsensus ${keyStatement.minConsensus}, not its ${minConsensus}!`);
|
|
4083
4085
|
try {
|
|
4084
4086
|
const holders = /* @__PURE__ */ new Map();
|
|
4085
|
-
for (const
|
|
4086
|
-
holders.set(
|
|
4087
|
+
for (const key2 of identityKeys)
|
|
4088
|
+
holders.set(key2.holderOrUnnamed, [...holders.get(key2.holderOrUnnamed) ?? [], key2.id]);
|
|
4087
4089
|
keyStatement.verifyHolders([...holders.values()], identityKeys, { minConsensus });
|
|
4088
4090
|
} catch (e) {
|
|
4089
4091
|
throw new KeyStatementError(minConsensus > 1 ? `The key statement of the roaming network operator '${id}' is not signed by identity keys of ${minConsensus} of its holders: ${e instanceof Error ? e.message : String(e)}` : `The key statement of the roaming network operator '${id}' is not signed by one of its identity keys: ${e instanceof Error ? e.message : String(e)}`);
|
|
@@ -4222,6 +4224,91 @@ var RoamingNetwork = class _RoamingNetwork {
|
|
|
4222
4224
|
}
|
|
4223
4225
|
};
|
|
4224
4226
|
|
|
4227
|
+
// src/history/epoch.ts
|
|
4228
|
+
var EPOCH_PROFILES_CONTEXT = CONTEXT_PREFIX + "roamingNetwork/epochProfiles/v1";
|
|
4229
|
+
var EPOCH_CONTEXT = CONTEXT_PREFIX + "roamingNetwork/epoch/v1";
|
|
4230
|
+
var EPOCH_LINK_CONTEXT = CONTEXT_PREFIX + "roamingNetwork/epochLink/v1";
|
|
4231
|
+
var STYX_CANONICAL_JSON = "styx-canonical-json-v1";
|
|
4232
|
+
var COMMIT_CONTEXT = CONTEXT_PREFIX + "roamingNetwork/commit/v3";
|
|
4233
|
+
var CHECKPOINT_CONTEXT = CONTEXT_PREFIX + "roamingNetwork/checkpoint/v1";
|
|
4234
|
+
var PRICE_STATEMENT_CONTEXT = CONTEXT_PREFIX + "roamingNetwork/priceStatement/v1";
|
|
4235
|
+
var EpochProfiles = class _EpochProfiles {
|
|
4236
|
+
constructor(identificationHash, signatureAlgorithms) {
|
|
4237
|
+
this.identificationHash = identificationHash;
|
|
4238
|
+
this.signatureAlgorithms = signatureAlgorithms;
|
|
4239
|
+
}
|
|
4240
|
+
identificationHash;
|
|
4241
|
+
signatureAlgorithms;
|
|
4242
|
+
/** The profiles of the first epoch: SHA-256 identifications, every signature algorithm. */
|
|
4243
|
+
static FIRST = new _EpochProfiles("sha256", []);
|
|
4244
|
+
/** Reads profiles as `RoamingNetworkEpochProfiles.TryParse` does, refusing those this library can not calculate. */
|
|
4245
|
+
static parse(value) {
|
|
4246
|
+
const json = requireObject(value, "The profiles of an epoch");
|
|
4247
|
+
onlyProperties(json, ["@context", "canonicalization", "identificationHash", "commit", "checkpoint", "priceStatement", "signatureAlgorithms"], "The profiles of an epoch");
|
|
4248
|
+
if (get(json, "@context") !== void 0 && get(json, "@context") !== EPOCH_PROFILES_CONTEXT)
|
|
4249
|
+
throw new POIError(`The JSON-LD context of the profiles of an epoch must be '${EPOCH_PROFILES_CONTEXT}'!`);
|
|
4250
|
+
if (get(json, "canonicalization") !== STYX_CANONICAL_JSON)
|
|
4251
|
+
throw new POIError(`The canonicalization of an epoch must be '${STYX_CANONICAL_JSON}', the one this library calculates!`);
|
|
4252
|
+
const identificationHash = get(json, "identificationHash");
|
|
4253
|
+
if (!isHashAlgorithm(identificationHash))
|
|
4254
|
+
throw new POIError("The identification hash of an epoch must be sha256, sha384 or sha512!");
|
|
4255
|
+
if (get(json, "commit") !== COMMIT_CONTEXT || get(json, "checkpoint") !== CHECKPOINT_CONTEXT || get(json, "priceStatement") !== PRICE_STATEMENT_CONTEXT)
|
|
4256
|
+
throw new POIError(`The JSON-LD contexts of commits, checkpoints and price statements of an epoch must be '${COMMIT_CONTEXT}', '${CHECKPOINT_CONTEXT}' and '${PRICE_STATEMENT_CONTEXT}', the ones this library calculates!`);
|
|
4257
|
+
const algorithmsJSON = get(json, "signatureAlgorithms");
|
|
4258
|
+
const algorithms = [];
|
|
4259
|
+
if (algorithmsJSON !== void 0) {
|
|
4260
|
+
if (!isArray(algorithmsJSON) || !algorithmsJSON.every((name) => typeof name === "string"))
|
|
4261
|
+
throw new POIError("The signature algorithms of an epoch must be a JSON array of COSE names!");
|
|
4262
|
+
for (const name of algorithmsJSON) {
|
|
4263
|
+
signatureAlgorithm(name);
|
|
4264
|
+
if (!algorithms.includes(name))
|
|
4265
|
+
algorithms.push(name);
|
|
4266
|
+
}
|
|
4267
|
+
}
|
|
4268
|
+
return new _EpochProfiles(identificationHash, algorithms.sort());
|
|
4269
|
+
}
|
|
4270
|
+
/** Whether the epoch accepts signatures of the algorithm. */
|
|
4271
|
+
allowsSignatureAlgorithm(algorithm) {
|
|
4272
|
+
return this.signatureAlgorithms.length === 0 ? SIGNATURE_ALGORITHMS.includes(algorithm) : this.signatureAlgorithms.includes(algorithm);
|
|
4273
|
+
}
|
|
4274
|
+
toJSON() {
|
|
4275
|
+
return {
|
|
4276
|
+
canonicalization: STYX_CANONICAL_JSON,
|
|
4277
|
+
identificationHash: this.identificationHash,
|
|
4278
|
+
commit: COMMIT_CONTEXT,
|
|
4279
|
+
checkpoint: CHECKPOINT_CONTEXT,
|
|
4280
|
+
priceStatement: PRICE_STATEMENT_CONTEXT,
|
|
4281
|
+
signatureAlgorithms: [...this.signatureAlgorithms]
|
|
4282
|
+
};
|
|
4283
|
+
}
|
|
4284
|
+
toString() {
|
|
4285
|
+
return `${this.identificationHash}, ${this.signatureAlgorithms.length === 0 ? "every signature algorithm" : this.signatureAlgorithms.join(", ")}`;
|
|
4286
|
+
}
|
|
4287
|
+
};
|
|
4288
|
+
var FIRST_EPOCH = { number: 1, profiles: EpochProfiles.FIRST };
|
|
4289
|
+
function parseEpoch(value, what = "An epoch") {
|
|
4290
|
+
const json = requireObject(value, what);
|
|
4291
|
+
onlyProperties(json, ["@context", "epoch", "profiles", "previous", "previousProfiles"], what);
|
|
4292
|
+
if (get(json, "@context") !== void 0 && get(json, "@context") !== EPOCH_CONTEXT)
|
|
4293
|
+
throw new POIError(`The JSON-LD context of the epoch must be '${EPOCH_CONTEXT}'!`);
|
|
4294
|
+
return {
|
|
4295
|
+
number: requireInteger(json, "epoch", 1, 4294967295, `The number of ${what.toLowerCase()}`),
|
|
4296
|
+
profiles: EpochProfiles.parse(get(json, "profiles"))
|
|
4297
|
+
};
|
|
4298
|
+
}
|
|
4299
|
+
var sameEpoch = (left, right) => left.number === right.number && left.profiles.identificationHash === right.profiles.identificationHash && left.profiles.signatureAlgorithms.join() === right.profiles.signatureAlgorithms.join();
|
|
4300
|
+
function parseEpochLink(value) {
|
|
4301
|
+
const json = requireObject(value, "The epoch before");
|
|
4302
|
+
onlyProperties(json, ["@context", "epoch", "checkpointId", "eTags"], "The epoch before");
|
|
4303
|
+
if (get(json, "@context") !== void 0 && get(json, "@context") !== EPOCH_LINK_CONTEXT)
|
|
4304
|
+
throw new POIError(`The JSON-LD context of the epoch before must be '${EPOCH_LINK_CONTEXT}'!`);
|
|
4305
|
+
return {
|
|
4306
|
+
epoch: requireInteger(json, "epoch", 1, 4294967295, "The number of the epoch before"),
|
|
4307
|
+
checkpointId: requireString(json, "checkpointId", "The final checkpoint of the epoch before"),
|
|
4308
|
+
eTags: parseETags(get(json, "eTags"), true, "The ETags of the epoch before")
|
|
4309
|
+
};
|
|
4310
|
+
}
|
|
4311
|
+
|
|
4225
4312
|
// src/proofs/trustAnchor.ts
|
|
4226
4313
|
var TRUST_ANCHOR_CONTEXT = CONTEXT_PREFIX + "roamingNetwork/trustAnchor/v2";
|
|
4227
4314
|
var TRUST_ANCHOR_QR_PREFIX = "WWCP/TA-2:";
|
|
@@ -4312,7 +4399,7 @@ var TrustAnchor = class _TrustAnchor {
|
|
|
4312
4399
|
const keys = requireArray(get(json, "keys"), "The keys of a trust anchor").map((holder) => {
|
|
4313
4400
|
if (typeof holder === "string")
|
|
4314
4401
|
return holder;
|
|
4315
|
-
if (Array.isArray(holder) && holder.length > 0 && holder.every((
|
|
4402
|
+
if (Array.isArray(holder) && holder.length > 0 && holder.every((key2) => typeof key2 === "string"))
|
|
4316
4403
|
return holder;
|
|
4317
4404
|
throw new POIError("A trust anchor names the identity keys of the roaming network operator by their @ids, per holder!");
|
|
4318
4405
|
});
|
|
@@ -4326,12 +4413,12 @@ var TrustAnchor = class _TrustAnchor {
|
|
|
4326
4413
|
if (value.type !== "map")
|
|
4327
4414
|
throw new POIError("The CBOR representation of a trust anchor must be a map!");
|
|
4328
4415
|
const values = /* @__PURE__ */ new Map();
|
|
4329
|
-
for (const [
|
|
4330
|
-
if (
|
|
4416
|
+
for (const [key2, item] of value.entries) {
|
|
4417
|
+
if (key2.type !== "text")
|
|
4331
4418
|
throw new POIError("The CBOR representation of a trust anchor must be a map with text keys!");
|
|
4332
|
-
if (values.has(
|
|
4333
|
-
throw new POIError(`The CBOR representation of a trust anchor names '${
|
|
4334
|
-
values.set(
|
|
4419
|
+
if (values.has(key2.value))
|
|
4420
|
+
throw new POIError(`The CBOR representation of a trust anchor names '${key2.value}' twice!`);
|
|
4421
|
+
values.set(key2.value, item);
|
|
4335
4422
|
}
|
|
4336
4423
|
const context = values.get("@context");
|
|
4337
4424
|
if (context !== void 0 && !(context.type === "text" && context.value === TRUST_ANCHOR_CONTEXT))
|
|
@@ -4345,10 +4432,10 @@ var TrustAnchor = class _TrustAnchor {
|
|
|
4345
4432
|
const keys = values.get("keys");
|
|
4346
4433
|
if (keys?.type !== "array")
|
|
4347
4434
|
throw new POIError("The identity keys of a trust anchor must be an array, one entry per holder!");
|
|
4348
|
-
const thumbprint = (
|
|
4349
|
-
if (
|
|
4435
|
+
const thumbprint = (key2) => {
|
|
4436
|
+
if (key2.type !== "bytes" || key2.value.length !== 32)
|
|
4350
4437
|
throw new POIError("The identity keys of a trust anchor must be their SHA-256 thumbprints, 32 bytes each, per holder!");
|
|
4351
|
-
return toHex(
|
|
4438
|
+
return toHex(key2.value);
|
|
4352
4439
|
};
|
|
4353
4440
|
const holders = keys.items.map((holder) => holder.type === "array" ? holder.items.map(thumbprint) : thumbprint(holder));
|
|
4354
4441
|
let quorum = 1;
|
|
@@ -4389,7 +4476,7 @@ var TrustAnchor = class _TrustAnchor {
|
|
|
4389
4476
|
[cbor.text("@id"), cbor.text(this.id)],
|
|
4390
4477
|
[cbor.text("@context"), cbor.text(TRUST_ANCHOR_CONTEXT)],
|
|
4391
4478
|
[cbor.text("urls"), cbor.array(this.urls.map((url) => cbor.text(url)))],
|
|
4392
|
-
[cbor.text("keys"), cbor.array(this.holders.map((keys) => keys.length === 1 ? cbor.bytes(fromHex(required(keys[0], "The key of a holder"), true)) : cbor.array(keys.map((
|
|
4479
|
+
[cbor.text("keys"), cbor.array(this.holders.map((keys) => keys.length === 1 ? cbor.bytes(fromHex(required(keys[0], "The key of a holder"), true)) : cbor.array(keys.map((key2) => cbor.bytes(fromHex(key2, true))))))]
|
|
4393
4480
|
];
|
|
4394
4481
|
if (Object.keys(this.name).length > 0)
|
|
4395
4482
|
entries.push([cbor.text("name"), cbor.map(Object.entries(this.name).map(([language, text]) => [cbor.text(language), cbor.text(text)]))]);
|
|
@@ -4448,7 +4535,7 @@ function checkTrustAnchor(urls, holders, minConsensus) {
|
|
|
4448
4535
|
throw new POIError(`The URLs of a trust anchor name one roaming network, not '${networkId}' and '${urlNetworkId}'!`);
|
|
4449
4536
|
}
|
|
4450
4537
|
const keys = holders.flat();
|
|
4451
|
-
if (keys.length === 0 || holders.some((holder) => holder.length === 0) || keys.some((
|
|
4538
|
+
if (keys.length === 0 || holders.some((holder) => holder.length === 0) || keys.some((key2) => !/^[0-9a-f]{64}$/.test(key2)) || new Set(keys).size !== keys.length)
|
|
4452
4539
|
throw new POIError("A trust anchor names at least one identity key by its @id, 64 lowercase hexadecimal digits, each once, and every holder with at least one!");
|
|
4453
4540
|
if (minConsensus < 1 || minConsensus > holders.length)
|
|
4454
4541
|
throw new POIError(`The minConsensus ${minConsensus} of a trust anchor needs identity keys of as many holders, not of ${holders.length}!`);
|
|
@@ -4457,8 +4544,11 @@ function checkTrustAnchor(urls, holders, minConsensus) {
|
|
|
4457
4544
|
|
|
4458
4545
|
// src/proofs/trustContext.ts
|
|
4459
4546
|
var TrustContext = class _TrustContext {
|
|
4460
|
-
constructor(anchor, holders, statement, evaluatedAt) {
|
|
4547
|
+
constructor(anchor, holders, statement, evaluatedAt, epoch = FIRST_EPOCH, epochs = [FIRST_EPOCH]) {
|
|
4548
|
+
this.statement = statement;
|
|
4461
4549
|
this.evaluatedAt = evaluatedAt;
|
|
4550
|
+
this.epoch = epoch;
|
|
4551
|
+
this.epochs = epochs;
|
|
4462
4552
|
this.anchor = frozenAnchor(anchor);
|
|
4463
4553
|
this.trustedHolders = Object.freeze(holders.map((holder) => Object.freeze([...holder])));
|
|
4464
4554
|
this.authorizedAnchor = frozenAnchor(TrustAnchor.create(
|
|
@@ -4476,7 +4566,10 @@ var TrustContext = class _TrustContext {
|
|
|
4476
4566
|
this.validUntil = statement.validUntil;
|
|
4477
4567
|
Object.freeze(this);
|
|
4478
4568
|
}
|
|
4569
|
+
statement;
|
|
4479
4570
|
evaluatedAt;
|
|
4571
|
+
epoch;
|
|
4572
|
+
epochs;
|
|
4480
4573
|
anchor;
|
|
4481
4574
|
authorizedAnchor;
|
|
4482
4575
|
trustedHolders;
|
|
@@ -4485,6 +4578,10 @@ var TrustContext = class _TrustContext {
|
|
|
4485
4578
|
keyStatementDigest;
|
|
4486
4579
|
requiredConsensus;
|
|
4487
4580
|
validUntil;
|
|
4581
|
+
/** Attach only profiles established by a verified transition; the bootstrap keeps the first epoch. */
|
|
4582
|
+
static withEpoch(reference, epoch, epochs = [epoch]) {
|
|
4583
|
+
return new _TrustContext(reference.anchor, reference.trustedHolders, reference.statement, reference.evaluatedAt, epoch, Object.freeze([...epochs]));
|
|
4584
|
+
}
|
|
4488
4585
|
static create(anchor, statement, options) {
|
|
4489
4586
|
const result = _TrustContext.evaluate(anchor, statement, options);
|
|
4490
4587
|
if (result.context === void 0)
|
|
@@ -4507,11 +4604,11 @@ var TrustContext = class _TrustContext {
|
|
|
4507
4604
|
let verificationError;
|
|
4508
4605
|
try {
|
|
4509
4606
|
for (const signature of statement.signatures) {
|
|
4510
|
-
const
|
|
4511
|
-
if (
|
|
4607
|
+
const key2 = pool.find((key3) => key3.id === signature.keyId);
|
|
4608
|
+
if (key2 === void 0)
|
|
4512
4609
|
throw new POIError("The public key of a signer was not provided");
|
|
4513
|
-
statement.verifySignature(signature,
|
|
4514
|
-
if (!
|
|
4610
|
+
statement.verifySignature(signature, key2);
|
|
4611
|
+
if (!key2.isValidAt(statement.signedAt))
|
|
4515
4612
|
keysValid = false;
|
|
4516
4613
|
}
|
|
4517
4614
|
} catch (error) {
|
|
@@ -4657,10 +4754,10 @@ var Attestation = class _Attestation {
|
|
|
4657
4754
|
return canonicalJSONBytes(json);
|
|
4658
4755
|
}
|
|
4659
4756
|
/** Verifies the signature with a trusted key, refusing one by another key. */
|
|
4660
|
-
verifySignature(
|
|
4661
|
-
if (this.keyId !==
|
|
4757
|
+
verifySignature(key2) {
|
|
4758
|
+
if (this.keyId !== key2.id)
|
|
4662
4759
|
throw new POIError("The attestation is signed by another key.");
|
|
4663
|
-
verifySignature(this.algorithm,
|
|
4760
|
+
verifySignature(this.algorithm, key2.key, this.signingBytes(), this.value);
|
|
4664
4761
|
}
|
|
4665
4762
|
/** Whether it is valid at the given point in time: signed then or before, and not after its `validUntil`. */
|
|
4666
4763
|
isValidAt(time) {
|
|
@@ -4744,10 +4841,13 @@ var ChargingStationProof = class _ChargingStationProof {
|
|
|
4744
4841
|
const { eTag, attested } = this.calculate();
|
|
4745
4842
|
const { keyStatement, required: required2, signingKeyOf } = this.certifiedSigningKeys(basis, time);
|
|
4746
4843
|
const certifiedSigningKeyOf = (attestation) => signingKeyOf(attestation.keyId, attestation.signedAt);
|
|
4844
|
+
let validUntil = keyStatement.validUntil;
|
|
4747
4845
|
const counted = [];
|
|
4748
4846
|
const holders = /* @__PURE__ */ new Set();
|
|
4749
4847
|
const refused = [];
|
|
4848
|
+
if (basis instanceof TrustContext && basis.epoch.number > 1 && this.commitId.length !== { sha256: 64, sha384: 96, sha512: 128 }[basis.epoch.profiles.identificationHash]) throw new POIError("The station commit identification does not match its verified epoch profile");
|
|
4750
4849
|
for (const attestation of this.attestations) {
|
|
4850
|
+
if (basis instanceof TrustContext && (!basis.epoch.profiles.allowsSignatureAlgorithm(attestation.algorithm) || attestation.signedAt < (basis.epoch.startedAt ?? -Infinity))) continue;
|
|
4751
4851
|
if (attestation.roamingNetworkId !== networkId || attestation.commitId !== this.commitId || attestation.revision !== this.revision || attestation.eTag.algorithm !== this.hashAlgorithm)
|
|
4752
4852
|
continue;
|
|
4753
4853
|
if (attestation.subject !== void 0 || !bytesEqual(attestation.eTag.digest, eTag.digest)) {
|
|
@@ -4774,11 +4874,12 @@ var ChargingStationProof = class _ChargingStationProof {
|
|
|
4774
4874
|
continue;
|
|
4775
4875
|
}
|
|
4776
4876
|
counted.push(attestation);
|
|
4877
|
+
validUntil = Math.min(validUntil, attestation.validUntil, certified.validUntil);
|
|
4777
4878
|
holders.add(certified.holder);
|
|
4778
4879
|
}
|
|
4779
4880
|
if (holders.size < required2)
|
|
4780
4881
|
throw new POIError(`Valid attestations of trusted keys of ${holders.size} holder${holders.size === 1 ? "" : "s"} attest the root ${etagToText(eTag)} of the proof, not of ${required2}!` + (refused.length > 0 ? ` (${refused.join("; ")})` : ""));
|
|
4781
|
-
return { eTag, attested, attestations: counted, attestedBy: [...holders].sort(), keyStatement };
|
|
4882
|
+
return { eTag, attested, attestations: counted, attestedBy: [...holders].sort(), keyStatement, validUntil };
|
|
4782
4883
|
}
|
|
4783
4884
|
/**
|
|
4784
4885
|
* Verifies a checkpoint against a trust anchor with this proof of the version of its commit (10.5), as
|
|
@@ -4850,8 +4951,12 @@ var ChargingStationProof = class _ChargingStationProof {
|
|
|
4850
4951
|
if (certificate.signingKey.id !== keyId || certificate.roamingNetworkOperatorId !== operatorId || !certificate.signingKey.isValidAt(signedAt) || !certificate.signingKey.isValidAt(time) || !certificate.signingKey.maySign(KeyUsages.Party))
|
|
4851
4952
|
continue;
|
|
4852
4953
|
try {
|
|
4853
|
-
certificate.verify(identityKeys, trustedIds);
|
|
4854
|
-
return {
|
|
4954
|
+
const identity = certificate.verify(identityKeys, trustedIds);
|
|
4955
|
+
return {
|
|
4956
|
+
key: certificate.signingKey,
|
|
4957
|
+
holder: required(holderOf.get(certificate.keyId), "The holder of a trusted identity key"),
|
|
4958
|
+
validUntil: Math.min(certificate.signingKey.notAfter ?? Infinity, identity.notAfter ?? Infinity)
|
|
4959
|
+
};
|
|
4855
4960
|
} catch {
|
|
4856
4961
|
continue;
|
|
4857
4962
|
}
|
|
@@ -4877,7 +4982,7 @@ var ChargingStationProof = class _ChargingStationProof {
|
|
|
4877
4982
|
};
|
|
4878
4983
|
|
|
4879
4984
|
// src/proofs/priceStatement.ts
|
|
4880
|
-
var
|
|
4985
|
+
var PRICE_STATEMENT_CONTEXT2 = CONTEXT_PREFIX + "roamingNetwork/priceStatement/v1";
|
|
4881
4986
|
var PRICE_STATEMENT_PROOF_CONTEXT = CONTEXT_PREFIX + "roamingNetwork/priceStatementProof/v1";
|
|
4882
4987
|
var PRICE_STATEMENT_ENTITY_TYPES = ["ChargingPool", "ChargingStation", "EVSE"];
|
|
4883
4988
|
var MAX_PRICE_STATEMENT_SEQUENCE = Number.MAX_SAFE_INTEGER;
|
|
@@ -4919,7 +5024,7 @@ var PriceStatement = class _PriceStatement {
|
|
|
4919
5024
|
json;
|
|
4920
5025
|
static parse(value) {
|
|
4921
5026
|
const given2 = requireObject(value, "A price statement");
|
|
4922
|
-
checkContext(given2,
|
|
5027
|
+
checkContext(given2, PRICE_STATEMENT_CONTEXT2, "a price statement");
|
|
4923
5028
|
onlyProperties(given2, [
|
|
4924
5029
|
"@context",
|
|
4925
5030
|
"roamingNetworkId",
|
|
@@ -4938,7 +5043,7 @@ var PriceStatement = class _PriceStatement {
|
|
|
4938
5043
|
"keyId",
|
|
4939
5044
|
"value"
|
|
4940
5045
|
], "A price statement");
|
|
4941
|
-
const json = { ...given2, "@context":
|
|
5046
|
+
const json = { ...given2, "@context": PRICE_STATEMENT_CONTEXT2 };
|
|
4942
5047
|
const entityType = requireString(json, "entityType", "The entity type of a price statement");
|
|
4943
5048
|
if (!PRICE_STATEMENT_ENTITY_TYPES.includes(entityType))
|
|
4944
5049
|
throw new POIError(`A price statement is one of a ChargingPool, a ChargingStation or an EVSE, not of a '${entityType}'!`);
|
|
@@ -5013,19 +5118,19 @@ var PriceStatement = class _PriceStatement {
|
|
|
5013
5118
|
throw new POIError(`The price statement is one of the roaming network '${this.roamingNetworkId}', not of '${roamingNetworkId}'!`);
|
|
5014
5119
|
if (this.signer !== operatorId || !this.entityId.startsWith(operatorId + "*"))
|
|
5015
5120
|
throw new POIError(`The price statement of the ${this.entityType} '${this.entityId}' is not signed by its operator '${operatorId}'!`);
|
|
5016
|
-
const
|
|
5017
|
-
if (
|
|
5121
|
+
const key2 = signingKeys.find((signingKey) => signingKey.id === this.keyId);
|
|
5122
|
+
if (key2 === void 0)
|
|
5018
5123
|
throw new POIError(`The key '${this.keyId}' is no signing key of the charging station operator '${operatorId}'!`);
|
|
5019
|
-
if (!
|
|
5020
|
-
throw new POIError(`The signing key '${
|
|
5021
|
-
if (!
|
|
5022
|
-
throw new POIError(`The signing key '${
|
|
5124
|
+
if (!key2.isValidAt(this.signedAt))
|
|
5125
|
+
throw new POIError(`The signing key '${key2.id}' of the charging station operator '${operatorId}' was not valid when the price statement was signed!`);
|
|
5126
|
+
if (!key2.maySign(KeyUsages.PriceStatements))
|
|
5127
|
+
throw new POIError(`The signing key '${key2.id}' of the charging station operator '${operatorId}' may not sign 'priceStatements', only '${key2.usages.join(", ")}'!`);
|
|
5023
5128
|
try {
|
|
5024
|
-
verifySignature(this.algorithm,
|
|
5129
|
+
verifySignature(this.algorithm, key2.key, this.signingBytes(), this.value);
|
|
5025
5130
|
} catch (e) {
|
|
5026
5131
|
throw new POIError(`The signature of the price statement is invalid: ${e instanceof Error ? e.message : String(e)}`);
|
|
5027
5132
|
}
|
|
5028
|
-
return
|
|
5133
|
+
return key2;
|
|
5029
5134
|
}
|
|
5030
5135
|
/** Whether it is valid at the given point in time: from `validFrom`, until `validUntil` where it names one. */
|
|
5031
5136
|
isValidAt(time) {
|
|
@@ -5121,91 +5226,6 @@ var PriceStatementProof = class _PriceStatementProof {
|
|
|
5121
5226
|
}
|
|
5122
5227
|
};
|
|
5123
5228
|
|
|
5124
|
-
// src/history/epoch.ts
|
|
5125
|
-
var EPOCH_PROFILES_CONTEXT = CONTEXT_PREFIX + "roamingNetwork/epochProfiles/v1";
|
|
5126
|
-
var EPOCH_CONTEXT = CONTEXT_PREFIX + "roamingNetwork/epoch/v1";
|
|
5127
|
-
var EPOCH_LINK_CONTEXT = CONTEXT_PREFIX + "roamingNetwork/epochLink/v1";
|
|
5128
|
-
var STYX_CANONICAL_JSON = "styx-canonical-json-v1";
|
|
5129
|
-
var COMMIT_CONTEXT = CONTEXT_PREFIX + "roamingNetwork/commit/v3";
|
|
5130
|
-
var CHECKPOINT_CONTEXT = CONTEXT_PREFIX + "roamingNetwork/checkpoint/v1";
|
|
5131
|
-
var PRICE_STATEMENT_CONTEXT2 = CONTEXT_PREFIX + "roamingNetwork/priceStatement/v1";
|
|
5132
|
-
var EpochProfiles = class _EpochProfiles {
|
|
5133
|
-
constructor(identificationHash, signatureAlgorithms) {
|
|
5134
|
-
this.identificationHash = identificationHash;
|
|
5135
|
-
this.signatureAlgorithms = signatureAlgorithms;
|
|
5136
|
-
}
|
|
5137
|
-
identificationHash;
|
|
5138
|
-
signatureAlgorithms;
|
|
5139
|
-
/** The profiles of the first epoch: SHA-256 identifications, every signature algorithm. */
|
|
5140
|
-
static FIRST = new _EpochProfiles("sha256", []);
|
|
5141
|
-
/** Reads profiles as `RoamingNetworkEpochProfiles.TryParse` does, refusing those this library can not calculate. */
|
|
5142
|
-
static parse(value) {
|
|
5143
|
-
const json = requireObject(value, "The profiles of an epoch");
|
|
5144
|
-
onlyProperties(json, ["@context", "canonicalization", "identificationHash", "commit", "checkpoint", "priceStatement", "signatureAlgorithms"], "The profiles of an epoch");
|
|
5145
|
-
if (get(json, "@context") !== void 0 && get(json, "@context") !== EPOCH_PROFILES_CONTEXT)
|
|
5146
|
-
throw new POIError(`The JSON-LD context of the profiles of an epoch must be '${EPOCH_PROFILES_CONTEXT}'!`);
|
|
5147
|
-
if (get(json, "canonicalization") !== STYX_CANONICAL_JSON)
|
|
5148
|
-
throw new POIError(`The canonicalization of an epoch must be '${STYX_CANONICAL_JSON}', the one this library calculates!`);
|
|
5149
|
-
const identificationHash = get(json, "identificationHash");
|
|
5150
|
-
if (!isHashAlgorithm(identificationHash))
|
|
5151
|
-
throw new POIError("The identification hash of an epoch must be sha256, sha384 or sha512!");
|
|
5152
|
-
if (get(json, "commit") !== COMMIT_CONTEXT || get(json, "checkpoint") !== CHECKPOINT_CONTEXT || get(json, "priceStatement") !== PRICE_STATEMENT_CONTEXT2)
|
|
5153
|
-
throw new POIError(`The JSON-LD contexts of commits, checkpoints and price statements of an epoch must be '${COMMIT_CONTEXT}', '${CHECKPOINT_CONTEXT}' and '${PRICE_STATEMENT_CONTEXT2}', the ones this library calculates!`);
|
|
5154
|
-
const algorithmsJSON = get(json, "signatureAlgorithms");
|
|
5155
|
-
const algorithms = [];
|
|
5156
|
-
if (algorithmsJSON !== void 0) {
|
|
5157
|
-
if (!isArray(algorithmsJSON) || !algorithmsJSON.every((name) => typeof name === "string"))
|
|
5158
|
-
throw new POIError("The signature algorithms of an epoch must be a JSON array of COSE names!");
|
|
5159
|
-
for (const name of algorithmsJSON) {
|
|
5160
|
-
signatureAlgorithm(name);
|
|
5161
|
-
if (!algorithms.includes(name))
|
|
5162
|
-
algorithms.push(name);
|
|
5163
|
-
}
|
|
5164
|
-
}
|
|
5165
|
-
return new _EpochProfiles(identificationHash, algorithms.sort());
|
|
5166
|
-
}
|
|
5167
|
-
/** Whether the epoch accepts signatures of the algorithm. */
|
|
5168
|
-
allowsSignatureAlgorithm(algorithm) {
|
|
5169
|
-
return this.signatureAlgorithms.length === 0 ? SIGNATURE_ALGORITHMS.includes(algorithm) : this.signatureAlgorithms.includes(algorithm);
|
|
5170
|
-
}
|
|
5171
|
-
toJSON() {
|
|
5172
|
-
return {
|
|
5173
|
-
canonicalization: STYX_CANONICAL_JSON,
|
|
5174
|
-
identificationHash: this.identificationHash,
|
|
5175
|
-
commit: COMMIT_CONTEXT,
|
|
5176
|
-
checkpoint: CHECKPOINT_CONTEXT,
|
|
5177
|
-
priceStatement: PRICE_STATEMENT_CONTEXT2,
|
|
5178
|
-
signatureAlgorithms: [...this.signatureAlgorithms]
|
|
5179
|
-
};
|
|
5180
|
-
}
|
|
5181
|
-
toString() {
|
|
5182
|
-
return `${this.identificationHash}, ${this.signatureAlgorithms.length === 0 ? "every signature algorithm" : this.signatureAlgorithms.join(", ")}`;
|
|
5183
|
-
}
|
|
5184
|
-
};
|
|
5185
|
-
var FIRST_EPOCH = { number: 1, profiles: EpochProfiles.FIRST };
|
|
5186
|
-
function parseEpoch(value, what = "An epoch") {
|
|
5187
|
-
const json = requireObject(value, what);
|
|
5188
|
-
onlyProperties(json, ["@context", "epoch", "profiles", "previous", "previousProfiles"], what);
|
|
5189
|
-
if (get(json, "@context") !== void 0 && get(json, "@context") !== EPOCH_CONTEXT)
|
|
5190
|
-
throw new POIError(`The JSON-LD context of the epoch must be '${EPOCH_CONTEXT}'!`);
|
|
5191
|
-
return {
|
|
5192
|
-
number: requireInteger(json, "epoch", 1, 4294967295, `The number of ${what.toLowerCase()}`),
|
|
5193
|
-
profiles: EpochProfiles.parse(get(json, "profiles"))
|
|
5194
|
-
};
|
|
5195
|
-
}
|
|
5196
|
-
var sameEpoch = (left, right) => left.number === right.number && left.profiles.identificationHash === right.profiles.identificationHash && left.profiles.signatureAlgorithms.join() === right.profiles.signatureAlgorithms.join();
|
|
5197
|
-
function parseEpochLink(value) {
|
|
5198
|
-
const json = requireObject(value, "The epoch before");
|
|
5199
|
-
onlyProperties(json, ["@context", "epoch", "checkpointId", "eTags"], "The epoch before");
|
|
5200
|
-
if (get(json, "@context") !== void 0 && get(json, "@context") !== EPOCH_LINK_CONTEXT)
|
|
5201
|
-
throw new POIError(`The JSON-LD context of the epoch before must be '${EPOCH_LINK_CONTEXT}'!`);
|
|
5202
|
-
return {
|
|
5203
|
-
epoch: requireInteger(json, "epoch", 1, 4294967295, "The number of the epoch before"),
|
|
5204
|
-
checkpointId: requireString(json, "checkpointId", "The final checkpoint of the epoch before"),
|
|
5205
|
-
eTags: parseETags(get(json, "eTags"), true, "The ETags of the epoch before")
|
|
5206
|
-
};
|
|
5207
|
-
}
|
|
5208
|
-
|
|
5209
5229
|
// src/history/changeSet.ts
|
|
5210
5230
|
var CHANGE_SET_CONTEXT = CONTEXT_PREFIX + "roamingNetwork/changeSet/v3";
|
|
5211
5231
|
var MERGE_CHANGE_SET_ID_PREFIX = "merge-";
|
|
@@ -5223,7 +5243,7 @@ function checkRebasedFrom(json, id) {
|
|
|
5223
5243
|
throw new POIError(`The rebasedFrom of the change set '${id}' must be an array of the change sets it was rebased from!`);
|
|
5224
5244
|
rebasedFrom.forEach((entry, index) => {
|
|
5225
5245
|
const malformed = `Entry ${index} of the rebasedFrom of the change set '${id}' must name the @id, the beforeETags and the afterETags of a change set it was rebased from!`;
|
|
5226
|
-
if (!isObject(entry) || Object.keys(entry).some((
|
|
5246
|
+
if (!isObject(entry) || Object.keys(entry).some((key2) => key2 !== "@id" && key2 !== "beforeETags" && key2 !== "afterETags"))
|
|
5227
5247
|
throw new POIError(malformed);
|
|
5228
5248
|
const namedId = get(entry, "@id");
|
|
5229
5249
|
const beforeETags = get(entry, "beforeETags");
|
|
@@ -5366,10 +5386,10 @@ var ChangeSet = class _ChangeSet {
|
|
|
5366
5386
|
});
|
|
5367
5387
|
}
|
|
5368
5388
|
/** Verifies one signature with the given key. */
|
|
5369
|
-
verifySignature(signature,
|
|
5370
|
-
if (signature.keyId !==
|
|
5389
|
+
verifySignature(signature, key2) {
|
|
5390
|
+
if (signature.keyId !== key2.id)
|
|
5371
5391
|
throw new POIError("The signature key ID does not match the key ID.");
|
|
5372
|
-
verifySignature(signature.algorithm,
|
|
5392
|
+
verifySignature(signature.algorithm, key2.key, this.signingBytes(signature), signature.value);
|
|
5373
5393
|
}
|
|
5374
5394
|
};
|
|
5375
5395
|
|
|
@@ -5473,8 +5493,9 @@ var Commit = class _Commit {
|
|
|
5473
5493
|
throw new POIError("The change set must be a JSON object!");
|
|
5474
5494
|
if (parentIds.length === 0 !== (changeSet === void 0))
|
|
5475
5495
|
throw new POIError(`The commit '${id}' must have parents and a change set, or neither, as a genesis commit!`);
|
|
5476
|
-
|
|
5477
|
-
|
|
5496
|
+
for (const format of ["json", "cbor", "effectiveTree"])
|
|
5497
|
+
if (!eTags.some((eTag) => eTag.format === format))
|
|
5498
|
+
throw new POIError(`The commit '${id}' names no ${format} ETag: a commit requires the json, cbor and effectiveTree ETags of its version!`);
|
|
5478
5499
|
const signaturesJSON = get(json, "signatures");
|
|
5479
5500
|
const signatures = signaturesJSON === void 0 ? [] : requireArray(signaturesJSON, "The signatures of a commit").map((signature) => parseRoleSignature(signature, "A signature of a commit"));
|
|
5480
5501
|
const identificationHash = hashAlgorithmOfIdentification(id);
|
|
@@ -5531,10 +5552,10 @@ var Commit = class _Commit {
|
|
|
5531
5552
|
return roleSignatureBytes(COMMIT_SIGNATURE_CONTEXT, signature, "commit", this.signedContent());
|
|
5532
5553
|
}
|
|
5533
5554
|
/** Verifies one signature with a trusted key, refusing one by another key. */
|
|
5534
|
-
verifySignature(signature,
|
|
5535
|
-
if (signature.keyId !==
|
|
5555
|
+
verifySignature(signature, key2) {
|
|
5556
|
+
if (signature.keyId !== key2.id)
|
|
5536
5557
|
throw new POIError("The signature key ID does not match the trusted key ID.");
|
|
5537
|
-
verifySignature(signature.algorithm,
|
|
5558
|
+
verifySignature(signature.algorithm, key2.key, this.signingBytes(signature), signature.value);
|
|
5538
5559
|
}
|
|
5539
5560
|
/** The description of its change set, in its first language. */
|
|
5540
5561
|
get description() {
|
|
@@ -6188,17 +6209,17 @@ function ownerOf(content, change, index, addedEnergyMeterChildren) {
|
|
|
6188
6209
|
}
|
|
6189
6210
|
const energyMeterChild = ENERGY_METER_CHILD_TYPES[change.entityType];
|
|
6190
6211
|
if (energyMeterChild !== void 0) {
|
|
6191
|
-
const
|
|
6212
|
+
const key2 = `${change.entityType}:${change.entityId.toUpperCase()}`;
|
|
6192
6213
|
let owner;
|
|
6193
6214
|
if (change.kind === "Add") {
|
|
6194
6215
|
if (change.parentEntityType !== "ChargingStationOperator" && change.parentEntityType !== "GridOperator" || change.parentEntityId === void 0)
|
|
6195
6216
|
throw new POIError(`Change ${index}: adding the ${change.entityType} '${change.entityId}' needs its ChargingStationOperator or GridOperator as its parent!`);
|
|
6196
6217
|
const party = partyNamed(content, change.parentEntityType, change.parentEntityId);
|
|
6197
6218
|
owner = { type: change.parentEntityType, id: asText(party?.["@id"] ?? change.parentEntityId) };
|
|
6198
|
-
addedEnergyMeterChildren.set(
|
|
6219
|
+
addedEnergyMeterChildren.set(key2, owner);
|
|
6199
6220
|
} else {
|
|
6200
6221
|
const found = energyMeterChildOf(content, energyMeterChild, change.entityId);
|
|
6201
|
-
owner = addedEnergyMeterChildren.get(
|
|
6222
|
+
owner = addedEnergyMeterChildren.get(key2) ?? (found === void 0 ? void 0 : { type: found.partyType, id: asText(found.party["@id"]) });
|
|
6202
6223
|
}
|
|
6203
6224
|
if (owner === void 0)
|
|
6204
6225
|
throw new POIError(`Change ${index}: the ${change.entityType} '${change.entityId}' belongs to no charging station operator and no grid operator!`);
|
|
@@ -6265,40 +6286,40 @@ function checkOwnership(before, changeSet) {
|
|
|
6265
6286
|
if (keys === void 0)
|
|
6266
6287
|
return;
|
|
6267
6288
|
const known = change.entityType === "RoamingNetworkOperator" ? rnoIdentity : parties.get(`${change.entityType}:${change.entityId}`)?.identityKeys ?? [];
|
|
6268
|
-
for (const
|
|
6269
|
-
if (!known.some((knownKey) => knownKey.id ===
|
|
6270
|
-
newIdentityKeys.push(
|
|
6289
|
+
for (const key2 of POIPublicKey.parseAll(keys, `identity keys of change ${index}`))
|
|
6290
|
+
if (!known.some((knownKey) => knownKey.id === key2.id))
|
|
6291
|
+
newIdentityKeys.push(key2);
|
|
6271
6292
|
});
|
|
6272
6293
|
const allKeys = /* @__PURE__ */ new Map();
|
|
6273
|
-
for (const
|
|
6274
|
-
allKeys.set(
|
|
6294
|
+
for (const key2 of [...rnoSigning, ...rnoIdentity, ...[...parties.values()].flatMap((party) => [...party.signingKeys, ...party.identityKeys]), ...newIdentityKeys])
|
|
6295
|
+
allKeys.set(key2.id, key2);
|
|
6275
6296
|
const partyKeys = [
|
|
6276
|
-
...rno === void 0 ? [] : [...rnoSigning, ...rnoIdentity].map((
|
|
6277
|
-
...[...parties.values()].flatMap((party) => [...party.signingKeys, ...party.identityKeys].map((
|
|
6297
|
+
...rno === void 0 ? [] : [...rnoSigning, ...rnoIdentity].map((key2) => ({ party: `RoamingNetworkOperator '${rno.id}'`, key: key2 })),
|
|
6298
|
+
...[...parties.values()].flatMap((party) => [...party.signingKeys, ...party.identityKeys].map((key2) => ({ party: `${party.type} '${party.id}'`, key: key2 })))
|
|
6278
6299
|
];
|
|
6279
|
-
const
|
|
6300
|
+
const valid2 = /* @__PURE__ */ new Set();
|
|
6280
6301
|
changeSet.signatures.forEach((signature, index) => {
|
|
6281
|
-
const
|
|
6282
|
-
if (
|
|
6302
|
+
const key2 = allKeys.get(signature.keyId);
|
|
6303
|
+
if (key2 === void 0)
|
|
6283
6304
|
throw new POIError(`Signature ${index} is by the key '${signature.keyId}', which is no key of a party of this roaming network!`);
|
|
6284
6305
|
for (const partyKey of partyKeys.filter((item) => item.key.id === signature.keyId))
|
|
6285
6306
|
if (!partyKey.key.isValidAt(createdAt))
|
|
6286
6307
|
throw new POIError(`Signature ${index}: the key '${partyKey.key.id}' of the ${partyKey.party} is not valid at ${toTimestamp(createdAt)}!`);
|
|
6287
6308
|
try {
|
|
6288
|
-
changeSet.verifySignature(signature,
|
|
6309
|
+
changeSet.verifySignature(signature, key2);
|
|
6289
6310
|
} catch (e) {
|
|
6290
6311
|
throw new POIError(`Signature ${index} ('${signature.keyId}') is invalid: ${e instanceof Error ? e.message : String(e)}`);
|
|
6291
6312
|
}
|
|
6292
|
-
|
|
6313
|
+
valid2.add(signature.keyId);
|
|
6293
6314
|
});
|
|
6294
|
-
for (const
|
|
6295
|
-
if (!
|
|
6296
|
-
throw new POIError(`The new identity key '${
|
|
6297
|
-
const signedValid = (
|
|
6315
|
+
for (const key2 of newIdentityKeys)
|
|
6316
|
+
if (!valid2.has(key2.id))
|
|
6317
|
+
throw new POIError(`The new identity key '${key2.id}' did not sign the change set: no proof of possession!`);
|
|
6318
|
+
const signedValid = (key2) => valid2.has(key2.id) && key2.isValidAt(createdAt);
|
|
6298
6319
|
const rnoHolders = (keys, ...usages) => new Set([
|
|
6299
|
-
...keys !== "identity" ? rnoSigning.filter((
|
|
6320
|
+
...keys !== "identity" ? rnoSigning.filter((key2) => [KeyUsages.Party, ...usages].some((usage) => key2.maySign(usage))) : [],
|
|
6300
6321
|
...keys !== "signing" ? rnoIdentity : []
|
|
6301
|
-
].filter(signedValid).map((
|
|
6322
|
+
].filter(signedValid).map((key2) => key2.holderOrUnnamed));
|
|
6302
6323
|
changeSet.changes.forEach((change, index) => {
|
|
6303
6324
|
checkPartyChange(change, index);
|
|
6304
6325
|
const owner = ownerOf(before.content, change, index, addedEnergyMeterChildren);
|
|
@@ -6313,7 +6334,7 @@ function checkOwnership(before, changeSet) {
|
|
|
6313
6334
|
const party = parties.get(`${owner.type}:${owner.id}`);
|
|
6314
6335
|
if (party === void 0)
|
|
6315
6336
|
throw new POIError(`${describeChange(change, index)}: the ${owner.type} '${owner.id}' does not exist!`);
|
|
6316
|
-
const bySigningKey = owner.keys !== "identity" && party.signingKeys.some((
|
|
6337
|
+
const bySigningKey = owner.keys !== "identity" && party.signingKeys.some((key2) => key2.maySign(owner.usage) && signedValid(key2));
|
|
6317
6338
|
const byIdentityKey = owner.keys !== "signing" && party.identityKeys.some(signedValid);
|
|
6318
6339
|
const byRecovery = owner.keys !== "signing" && rnoHolders("identity").size >= minConsensus && rnoIdentity.some(signedValid);
|
|
6319
6340
|
const byRNO = owner.keys === "signing" && party.signingKeys.length === 0 && rnoHolders("signing", owner.usage).size >= minConsensus;
|
|
@@ -6406,8 +6427,8 @@ function checkKeyHolders(before, after) {
|
|
|
6406
6427
|
const keysOf2 = (network) => {
|
|
6407
6428
|
const keys = /* @__PURE__ */ new Map();
|
|
6408
6429
|
const add = (type, id, partyKeys) => {
|
|
6409
|
-
for (const
|
|
6410
|
-
keys.set(`${type}:${id}:${
|
|
6430
|
+
for (const key2 of partyKeys)
|
|
6431
|
+
keys.set(`${type}:${id}:${key2.id}`, { party: `${type} '${id}'`, key: key2 });
|
|
6411
6432
|
};
|
|
6412
6433
|
if (network.operator !== void 0)
|
|
6413
6434
|
add("RoamingNetworkOperator", network.operator.id, [...network.operator.identityKeys, ...network.operator.signingKeyCertificates.map((certificate) => certificate.signingKey)]);
|
|
@@ -6416,10 +6437,10 @@ function checkKeyHolders(before, after) {
|
|
|
6416
6437
|
return keys;
|
|
6417
6438
|
};
|
|
6418
6439
|
const holders = keysOf2(before);
|
|
6419
|
-
for (const [where, { party, key }] of keysOf2(after)) {
|
|
6440
|
+
for (const [where, { party, key: key2 }] of keysOf2(after)) {
|
|
6420
6441
|
const previous = holders.get(where);
|
|
6421
|
-
if (previous !== void 0 && previous.key.holder !==
|
|
6422
|
-
throw new POIError(`The key '${
|
|
6442
|
+
if (previous !== void 0 && previous.key.holder !== key2.holder)
|
|
6443
|
+
throw new POIError(`The key '${key2.id}' of the ${party} keeps its holder ${previous.key.holder === void 0 ? "none" : `'${previous.key.holder}'`}, not ${key2.holder === void 0 ? "none" : `'${key2.holder}'`}!`);
|
|
6423
6444
|
}
|
|
6424
6445
|
}
|
|
6425
6446
|
|
|
@@ -6555,10 +6576,10 @@ var Checkpoint = class _Checkpoint {
|
|
|
6555
6576
|
return roleSignatureBytes(CHECKPOINT_SIGNATURE_CONTEXT, signature, "checkpoint", this.signedContent());
|
|
6556
6577
|
}
|
|
6557
6578
|
/** Verifies one signature with a trusted key, refusing one by another key. */
|
|
6558
|
-
verifySignature(signature,
|
|
6559
|
-
if (signature.keyId !==
|
|
6579
|
+
verifySignature(signature, key2) {
|
|
6580
|
+
if (signature.keyId !== key2.id)
|
|
6560
6581
|
throw new POIError("The signature key ID does not match the trusted key ID.");
|
|
6561
|
-
verifySignature(signature.algorithm,
|
|
6582
|
+
verifySignature(signature.algorithm, key2.key, this.signingBytes(signature), signature.value);
|
|
6562
6583
|
}
|
|
6563
6584
|
/** Whether the checkpoint names the given commit with its revision and ETags. */
|
|
6564
6585
|
isOf(commit) {
|
|
@@ -7125,11 +7146,11 @@ var QIDigitalDocument = class {
|
|
|
7125
7146
|
return canonicalJSONBytes(this.signatureInput());
|
|
7126
7147
|
}
|
|
7127
7148
|
/** Verifies its signature with the given public key: by that key, with a canonical algorithm and value (`VerifySignature`). */
|
|
7128
|
-
verifySignature(
|
|
7149
|
+
verifySignature(key2) {
|
|
7129
7150
|
const signature = this.signature;
|
|
7130
7151
|
if (signature === void 0)
|
|
7131
7152
|
throw new POIError(`${this.documentText} is not signed.`);
|
|
7132
|
-
if (signature.keyId !==
|
|
7153
|
+
if (signature.keyId !== key2.id)
|
|
7133
7154
|
throw new POIError(`The key ID of ${this.documentText} does not match the expected key ID.`);
|
|
7134
7155
|
try {
|
|
7135
7156
|
signatureAlgorithm(signature.algorithm);
|
|
@@ -7144,7 +7165,7 @@ var QIDigitalDocument = class {
|
|
|
7144
7165
|
}
|
|
7145
7166
|
if (value.length === 0)
|
|
7146
7167
|
throw new POIError("The value must contain nonempty canonical standard Base64.");
|
|
7147
|
-
verifySignature(signature.algorithm,
|
|
7168
|
+
verifySignature(signature.algorithm, key2.key, this.signingBytes(), signature.value);
|
|
7148
7169
|
}
|
|
7149
7170
|
/**
|
|
7150
7171
|
* Verifies its signature with the one of the given signing keys of its issuer it names, valid when it was signed, and
|
|
@@ -7154,7 +7175,7 @@ var QIDigitalDocument = class {
|
|
|
7154
7175
|
const signature = this.signature;
|
|
7155
7176
|
if (signature === void 0)
|
|
7156
7177
|
throw new POIError(`${capitalized(this.documentText)} is not signed!`);
|
|
7157
|
-
const signingKey = signingKeys.find((
|
|
7178
|
+
const signingKey = signingKeys.find((key2) => key2.id === signature.keyId);
|
|
7158
7179
|
if (signingKey === void 0)
|
|
7159
7180
|
throw new POIError(`The key '${signature.keyId}' of ${this.documentText} is no signing key of ${this.issuerText}!`);
|
|
7160
7181
|
if (!signingKey.isValidAt(signature.signedAt))
|
|
@@ -8429,7 +8450,7 @@ var OperatorTrust = class _OperatorTrust {
|
|
|
8429
8450
|
for (const [id, holder] of this.identityKeyHolders)
|
|
8430
8451
|
known.set(holder, [...known.get(holder) ?? [], id]);
|
|
8431
8452
|
const { signedBy, holderOf } = statement.verifyHolders([...known.values()], operator.identityKeys, { minConsensus: this.requiredConsensus, at: timeOf(at) });
|
|
8432
|
-
const trusted = /* @__PURE__ */ new Set([...this.anchor.keys, ...statement.identityKeys.map((
|
|
8453
|
+
const trusted = /* @__PURE__ */ new Set([...this.anchor.keys, ...statement.identityKeys.map((key2) => key2.id)]);
|
|
8433
8454
|
return new _OperatorTrust(
|
|
8434
8455
|
this.anchor,
|
|
8435
8456
|
operator,
|
|
@@ -8492,11 +8513,11 @@ var OperatorTrust = class _OperatorTrust {
|
|
|
8492
8513
|
verifyOperatorSignatures(signatures, verify, what) {
|
|
8493
8514
|
const signedBy = /* @__PURE__ */ new Set();
|
|
8494
8515
|
for (const signature of signatures.filter((signature2) => signature2.role === SignatureRoles.RoamingNetworkOperator)) {
|
|
8495
|
-
const
|
|
8516
|
+
const key2 = this.signingKeyOf(signature);
|
|
8496
8517
|
const holder = this.holderOf(signature);
|
|
8497
|
-
if (
|
|
8518
|
+
if (key2 === void 0 || holder === void 0)
|
|
8498
8519
|
throw new POIError(`the key '${signature.keyId}' is no signing key a trusted identity key of its holder certifies`);
|
|
8499
|
-
verify(signature,
|
|
8520
|
+
verify(signature, key2);
|
|
8500
8521
|
signedBy.add(holder);
|
|
8501
8522
|
}
|
|
8502
8523
|
if (signedBy.size < this.requiredConsensus)
|
|
@@ -8531,8 +8552,8 @@ function verifyVersion(basis, networkJSON, commitJSON, at) {
|
|
|
8531
8552
|
if (commit.roamingNetworkId !== network.id || !network.matchesCommitETags(commit.eTags))
|
|
8532
8553
|
throw new POIError("the commit does not lead to the roaming network");
|
|
8533
8554
|
const trust = OperatorTrust.of(basis, network, at);
|
|
8534
|
-
const signedBy = trust.verifyOperatorSignatures(commit.signatures, (signature,
|
|
8535
|
-
commit.verifySignature(signature,
|
|
8555
|
+
const signedBy = trust.verifyOperatorSignatures(commit.signatures, (signature, key2) => {
|
|
8556
|
+
commit.verifySignature(signature, key2);
|
|
8536
8557
|
}, "the commit");
|
|
8537
8558
|
return {
|
|
8538
8559
|
network,
|
|
@@ -8691,6 +8712,10 @@ function verifyChargingStation(anchor, proof, options = {}) {
|
|
|
8691
8712
|
revision: parsed.revision,
|
|
8692
8713
|
eTag: verified.eTag,
|
|
8693
8714
|
attestedBy: verified.attestedBy,
|
|
8715
|
+
verification: {
|
|
8716
|
+
at: anchor instanceof TrustContext ? anchor.evaluatedAt : timeOf(options.at),
|
|
8717
|
+
validUntil: verified.validUntil
|
|
8718
|
+
},
|
|
8694
8719
|
warnings
|
|
8695
8720
|
};
|
|
8696
8721
|
}
|
|
@@ -8709,7 +8734,7 @@ function verifyPriceStatements(station, statements, options = {}) {
|
|
|
8709
8734
|
const signedWith = /* @__PURE__ */ new Map();
|
|
8710
8735
|
for (const item of list) {
|
|
8711
8736
|
const statement = PriceStatement.parse(item);
|
|
8712
|
-
const
|
|
8737
|
+
const key2 = `${statement.entityType}:${statement.entityId}`;
|
|
8713
8738
|
const entity = entities.get(statement.chain);
|
|
8714
8739
|
if (entity === void 0)
|
|
8715
8740
|
throw new POIError(`The price statement of the ${statement.entityType} '${statement.entityId}' is none of the station, its pool or its EVSEs!`);
|
|
@@ -8721,23 +8746,23 @@ function verifyPriceStatements(station, statements, options = {}) {
|
|
|
8721
8746
|
if (!entity.tariffs.has(asText(price["chargingTariffId"])))
|
|
8722
8747
|
throw new POIError(`The price statement of the EVSE '${statement.entityId}' names the charging tariff '${asText(price["chargingTariffId"])}', which is not one of its own!`);
|
|
8723
8748
|
}
|
|
8724
|
-
const known = byEntity.get(
|
|
8749
|
+
const known = byEntity.get(key2) ?? [];
|
|
8725
8750
|
if (!known.some((other) => other.digestHex() === statement.digestHex()))
|
|
8726
|
-
byEntity.set(
|
|
8751
|
+
byEntity.set(key2, [...known, statement]);
|
|
8727
8752
|
}
|
|
8728
8753
|
const current = /* @__PURE__ */ new Map();
|
|
8729
|
-
for (const [
|
|
8754
|
+
for (const [key2, chain] of byEntity) {
|
|
8730
8755
|
chain.sort((left, right) => left.sequence - right.sequence);
|
|
8731
8756
|
if (new Set(chain.map((statement) => statement.sequence)).size !== chain.length)
|
|
8732
|
-
throw new POIError(`Two price statements of one sequence of '${
|
|
8757
|
+
throw new POIError(`Two price statements of one sequence of '${key2}': a split view!`);
|
|
8733
8758
|
chain.forEach((statement, index) => {
|
|
8734
8759
|
const before = chain[index - 1];
|
|
8735
8760
|
if (before !== void 0 && statement.sequence === before.sequence + 1)
|
|
8736
8761
|
verifyPriceStatementChain([before, statement]);
|
|
8737
8762
|
});
|
|
8738
|
-
const
|
|
8739
|
-
if (
|
|
8740
|
-
current.set(
|
|
8763
|
+
const valid2 = [...chain].reverse().find((statement) => statement.validFrom <= time);
|
|
8764
|
+
if (valid2?.isValidAt(time) === true && signedWith.get(valid2.digestHex())?.isValidAt(time) === true)
|
|
8765
|
+
current.set(key2, { statement: valid2, digest: valid2.digestHex() });
|
|
8741
8766
|
}
|
|
8742
8767
|
return current;
|
|
8743
8768
|
}
|
|
@@ -8780,14 +8805,14 @@ function verifyCheckpointSignatures(checkpoint, trust, options = {}) {
|
|
|
8780
8805
|
const ignored = [];
|
|
8781
8806
|
for (const signature of checkpoint.signatures) {
|
|
8782
8807
|
if (signature.role === SignatureRoles.RoamingNetworkOperator) {
|
|
8783
|
-
const
|
|
8808
|
+
const key2 = trust.signingKeyOf(signature);
|
|
8784
8809
|
const holder = trust.holderOf(signature);
|
|
8785
|
-
if (
|
|
8810
|
+
if (key2 === void 0 || holder === void 0) {
|
|
8786
8811
|
ignored.push(`the key '${signature.keyId}' is no signing key a trusted identity key of its holder certifies`);
|
|
8787
8812
|
continue;
|
|
8788
8813
|
}
|
|
8789
8814
|
try {
|
|
8790
|
-
checkpoint.verifySignature(signature,
|
|
8815
|
+
checkpoint.verifySignature(signature, key2);
|
|
8791
8816
|
signedBy.add(holder);
|
|
8792
8817
|
} catch (e) {
|
|
8793
8818
|
ignored.push(`the signature by '${signature.keyId}' is invalid: ${e instanceof Error ? e.message : String(e)}`);
|
|
@@ -9008,11 +9033,11 @@ var RuntimeStatus = class _RuntimeStatus {
|
|
|
9008
9033
|
}
|
|
9009
9034
|
/** Takes a status update: `Updated` unless the status it changes is a later one already, `Outdated` then. */
|
|
9010
9035
|
apply(update) {
|
|
9011
|
-
const
|
|
9012
|
-
const current = this.entries.get(
|
|
9036
|
+
const key2 = _RuntimeStatus.key(update.entityType, update.entityId, update.kind);
|
|
9037
|
+
const current = this.entries.get(key2);
|
|
9013
9038
|
if (current !== void 0 && current.timestamp > update.timestamp)
|
|
9014
9039
|
return "Outdated";
|
|
9015
|
-
this.entries.set(
|
|
9040
|
+
this.entries.set(key2, { value: update.value, timestamp: update.timestamp });
|
|
9016
9041
|
return "Updated";
|
|
9017
9042
|
}
|
|
9018
9043
|
get(entityType, entityId2, kind = "status") {
|
|
@@ -9022,7 +9047,1108 @@ var RuntimeStatus = class _RuntimeStatus {
|
|
|
9022
9047
|
return this.entries.size;
|
|
9023
9048
|
}
|
|
9024
9049
|
};
|
|
9050
|
+
var MeasurementKeyError = class extends POIError {
|
|
9051
|
+
constructor(code, message) {
|
|
9052
|
+
super(message);
|
|
9053
|
+
this.code = code;
|
|
9054
|
+
}
|
|
9055
|
+
code;
|
|
9056
|
+
};
|
|
9057
|
+
function parseMeasurementPublicKey(value, format) {
|
|
9058
|
+
if (!["spki-der", "cose-key"].includes(format))
|
|
9059
|
+
throw new MeasurementKeyError("unsupported-key", "Unknown measurement-key format");
|
|
9060
|
+
const json = requireObject(value, "A measurement public key");
|
|
9061
|
+
const id = requireString(json, "@id");
|
|
9062
|
+
const bytes = fromBase64(requireString(json, "key"));
|
|
9063
|
+
let cose;
|
|
9064
|
+
let idScheme;
|
|
9065
|
+
try {
|
|
9066
|
+
if (format === "spki-der") {
|
|
9067
|
+
const document = new DerReader(bytes, "Measurement SubjectPublicKeyInfo");
|
|
9068
|
+
const sequence = contentsOf(document.read(Tag.sequence), "SubjectPublicKeyInfo");
|
|
9069
|
+
document.expectEnd();
|
|
9070
|
+
const algorithm = contentsOf(sequence.read(Tag.sequence), "AlgorithmIdentifier");
|
|
9071
|
+
const oid = derObjectIdentifier(algorithm.read(Tag.objectIdentifier), "Public key algorithm");
|
|
9072
|
+
const curve = derObjectIdentifier(algorithm.read(Tag.objectIdentifier), "Public key curve");
|
|
9073
|
+
algorithm.expectEnd();
|
|
9074
|
+
if (oid !== "1.2.840.10045.2.1" || curve !== "1.2.840.10045.3.1.7")
|
|
9075
|
+
throw new MeasurementKeyError("unsupported-key", "The measurement-key pilot supports ECDSA P-256 only");
|
|
9076
|
+
const point = derBitString(sequence.read(Tag.bitString), "Measurement public point");
|
|
9077
|
+
sequence.expectEnd();
|
|
9078
|
+
if (point.length !== 65 || point[0] !== 4 || !isOnCurve(CoseCurves.P256, point))
|
|
9079
|
+
throw new MeasurementKeyError("invalid-key", "A measurement key needs a valid uncompressed P-256 point");
|
|
9080
|
+
cose = CoseKey.fromCoordinates(CoseCurves.P256, point.slice(1, 33), point.slice(33));
|
|
9081
|
+
idScheme = "sha256-spki";
|
|
9082
|
+
if (id !== toHex(hash("sha256", bytes))) throw new MeasurementKeyError("invalid-key", "The measurement SPKI hash does not match its ID");
|
|
9083
|
+
} else {
|
|
9084
|
+
cose = CoseKey.parse(bytes);
|
|
9085
|
+
if (cose.isPrivate || cose.curve?.id !== CoseCurves.P256.id || cose.x?.length !== 32 || cose.y?.length !== 32)
|
|
9086
|
+
throw new MeasurementKeyError("unsupported-key", "A public P-256 COSE measurement key is required");
|
|
9087
|
+
const point = Uint8Array.from([4, ...cose.x, ...cose.y]);
|
|
9088
|
+
if (!isOnCurve(CoseCurves.P256, point)) throw new MeasurementKeyError("invalid-key", "The measurement point is not on P-256");
|
|
9089
|
+
if (toBase64(cose.toBytes()) !== toBase64(bytes) || id !== toHex(cose.thumbprint()))
|
|
9090
|
+
throw new MeasurementKeyError("invalid-key", "The COSE key must be deterministic and match its thumbprint");
|
|
9091
|
+
idScheme = "sha256-cose-key-thumbprint";
|
|
9092
|
+
cose = CoseKey.fromCoordinates(CoseCurves.P256, cose.x, cose.y);
|
|
9093
|
+
}
|
|
9094
|
+
} catch (error) {
|
|
9095
|
+
if (error instanceof MeasurementKeyError) throw error;
|
|
9096
|
+
throw new MeasurementKeyError("invalid-key", error instanceof Error ? error.message : "Invalid measurement key");
|
|
9097
|
+
}
|
|
9098
|
+
return Object.freeze({
|
|
9099
|
+
id,
|
|
9100
|
+
idScheme,
|
|
9101
|
+
canonicalId: toHex(cose.thumbprint()),
|
|
9102
|
+
sourceFormat: format,
|
|
9103
|
+
value: toBase64(cose.toBytes()),
|
|
9104
|
+
encodings: Object.freeze(["COSE_Key", "CBOR", "base64"]),
|
|
9105
|
+
algorithm: "ECDSA-secp256r1"
|
|
9106
|
+
});
|
|
9107
|
+
}
|
|
9108
|
+
function verifyMeterKey(context, proof, request) {
|
|
9109
|
+
if (!(context instanceof TrustContext)) throw new POIError("Meter-key authorization requires a TrustContext");
|
|
9110
|
+
const station = verifyChargingStation(context, proof, { evseId: request.evseId });
|
|
9111
|
+
if (station.verification.validUntil < context.evaluatedAt)
|
|
9112
|
+
throw new POIError("The certified-key observation is no longer current");
|
|
9113
|
+
const evse = station.evses.find((item) => item["@id"] === request.evseId);
|
|
9114
|
+
const meter = evse?.["energyMeter"];
|
|
9115
|
+
if (!isObject(meter)) throw new MeasurementKeyError("missing-meter", "The selected EVSE supplies no directly associated energy meter");
|
|
9116
|
+
const meterId = requireString(meter, "@id");
|
|
9117
|
+
const serial = requireString(meter, "serialNumber");
|
|
9118
|
+
if (request.meterSerialNumber.trim() === "" || serial !== request.meterSerialNumber || request.meterId !== void 0 && meterId !== request.meterId)
|
|
9119
|
+
throw new MeasurementKeyError("identity-mismatch", "The signed meter identity does not match the selected EVSE meter");
|
|
9120
|
+
const all = meter["publicKeys"];
|
|
9121
|
+
const keys = Array.isArray(all) ? all.filter((item) => request.keyId === void 0 || isObject(item) && item["@id"] === request.keyId) : [];
|
|
9122
|
+
if (keys.length !== 1) throw new MeasurementKeyError("ambiguous-key", "Select exactly one explicitly identified meter key; no trial verification across keys");
|
|
9123
|
+
const key2 = parseMeasurementPublicKey(keys[0] ?? null, request.keyFormat);
|
|
9124
|
+
return Object.freeze({
|
|
9125
|
+
evseId: request.evseId,
|
|
9126
|
+
chargingStationId: requireString(station.chargingStation, "@id"),
|
|
9127
|
+
meterId,
|
|
9128
|
+
meterSerialNumber: serial,
|
|
9129
|
+
key: key2,
|
|
9130
|
+
evidence: Object.freeze({
|
|
9131
|
+
roamingNetworkId: context.roamingNetworkId,
|
|
9132
|
+
roamingNetworkOperatorId: context.roamingNetworkOperatorId,
|
|
9133
|
+
commitId: station.commitId,
|
|
9134
|
+
revision: station.revision,
|
|
9135
|
+
treeRoot: etagToText(station.eTag),
|
|
9136
|
+
keyStatementDigest: context.keyStatementDigest,
|
|
9137
|
+
verifiedAt: station.verification.at,
|
|
9138
|
+
validUntil: station.verification.validUntil,
|
|
9139
|
+
association: "current-snapshot"
|
|
9140
|
+
})
|
|
9141
|
+
});
|
|
9142
|
+
}
|
|
9143
|
+
|
|
9144
|
+
// src/verifier/historicalMeasurements.ts
|
|
9145
|
+
var HISTORICAL_METER_EVIDENCE_CONTEXT = "https://open.charging.cloud/contexts/wwcp/historicalMeterEvidence/v1";
|
|
9146
|
+
function parseHistoricalMeterEvidence(value) {
|
|
9147
|
+
const json = requireObject(value, "Historical meter evidence");
|
|
9148
|
+
onlyProperties(json, ["@context", "EVSEId", "versions"], "Historical meter evidence");
|
|
9149
|
+
if (json["@context"] !== HISTORICAL_METER_EVIDENCE_CONTEXT) throw new POIError("Unsupported historical meter evidence");
|
|
9150
|
+
const versions = requireArray(json["versions"], "Historical station versions").map((item) => {
|
|
9151
|
+
const version = requireObject(item, "A historical station version");
|
|
9152
|
+
onlyProperties(version, ["chargingStationProof", "commitProof", "checkpoints"], "A historical station version");
|
|
9153
|
+
return {
|
|
9154
|
+
proof: requireObject(version["chargingStationProof"], "A historical station proof"),
|
|
9155
|
+
...version["commitProof"] === void 0 ? {} : { commitProof: version["commitProof"] },
|
|
9156
|
+
checkpoints: (version["checkpoints"] === void 0 ? [] : requireArray(version["checkpoints"], "Historical checkpoints")).map((item2) => {
|
|
9157
|
+
const entry = requireObject(item2, "A checkpoint with its signing authority");
|
|
9158
|
+
onlyProperties(entry, ["checkpoint", "operator"], "A historical checkpoint entry");
|
|
9159
|
+
return { checkpoint: requireObject(entry["checkpoint"], "A checkpoint"), operator: requireObject(entry["operator"], "Its operator") };
|
|
9160
|
+
})
|
|
9161
|
+
};
|
|
9162
|
+
});
|
|
9163
|
+
if (versions.length === 0) throw new POIError("Historical station evidence is empty");
|
|
9164
|
+
return { evseId: requireString(json, "EVSEId"), versions };
|
|
9165
|
+
}
|
|
9166
|
+
function historicalOperatorContext(current, operator, at) {
|
|
9167
|
+
const json = requireObject(operator, "A historical roaming network operator");
|
|
9168
|
+
const statement = KeyStatement.parse(json["keyStatement"]);
|
|
9169
|
+
if (statement.roamingNetworkId !== current.roamingNetworkId || statement.roamingNetworkOperatorId !== current.roamingNetworkOperatorId || json["@id"] !== current.roamingNetworkOperatorId)
|
|
9170
|
+
throw new POIError("The historical statement belongs to another authorized network/operator");
|
|
9171
|
+
const authority = current instanceof HistoricalTrustContext ? current.authorityAt(at) : void 0;
|
|
9172
|
+
const anchor = authority?.authorizedAnchor ?? current.authorizedAnchor;
|
|
9173
|
+
const created = TrustContext.create(anchor, statement, {
|
|
9174
|
+
trustedIdentityKeyIds: anchor.keys,
|
|
9175
|
+
otherIdentityKeys: POIPublicKey.parseAll(json["identityKeys"], "Historical operator identities"),
|
|
9176
|
+
at
|
|
9177
|
+
});
|
|
9178
|
+
const context = TrustContext.withEpoch(created, authority?.epoch ?? current.epoch, current.epochs);
|
|
9179
|
+
if (authority === void 0 && context.requiredConsensus < current.requiredConsensus) throw new POIError("Historical verification must not weaken the current local quorum");
|
|
9180
|
+
return context;
|
|
9181
|
+
}
|
|
9182
|
+
var HistoricalTrustContext = class _HistoricalTrustContext {
|
|
9183
|
+
constructor(reference, evaluatedAt, authorities = []) {
|
|
9184
|
+
this.evaluatedAt = evaluatedAt;
|
|
9185
|
+
this.authorities = authorities;
|
|
9186
|
+
this.epoch = reference.epoch;
|
|
9187
|
+
this.epochs = reference.epochs;
|
|
9188
|
+
this.authorizedAnchor = reference.authorizedAnchor;
|
|
9189
|
+
this.roamingNetworkId = reference.roamingNetworkId;
|
|
9190
|
+
this.roamingNetworkOperatorId = reference.roamingNetworkOperatorId;
|
|
9191
|
+
this.requiredConsensus = reference.requiredConsensus;
|
|
9192
|
+
this.keyStatementDigest = reference.keyStatementDigest;
|
|
9193
|
+
this.trustedHolders = reference.trustedHolders;
|
|
9194
|
+
this.validUntil = evaluatedAt + 6e4;
|
|
9195
|
+
Object.freeze(this);
|
|
9196
|
+
}
|
|
9197
|
+
evaluatedAt;
|
|
9198
|
+
authorities;
|
|
9199
|
+
authorizedAnchor;
|
|
9200
|
+
roamingNetworkId;
|
|
9201
|
+
roamingNetworkOperatorId;
|
|
9202
|
+
requiredConsensus;
|
|
9203
|
+
keyStatementDigest;
|
|
9204
|
+
/** An operation deadline, not historical document validity or a lease on local trust. */
|
|
9205
|
+
validUntil;
|
|
9206
|
+
trustedHolders;
|
|
9207
|
+
epoch;
|
|
9208
|
+
epochs;
|
|
9209
|
+
/** Contexts must be reconstructed from current local roots and verified transition certificates, never serialized success flags. */
|
|
9210
|
+
static fromVerifiedAuthorities(reference, authorities, now) {
|
|
9211
|
+
if (!Number.isFinite(now) || authorities.length === 0 || authorities.some((item) => item.context.roamingNetworkId !== reference.roamingNetworkId || item.context.roamingNetworkOperatorId !== reference.roamingNetworkOperatorId || !Number.isFinite(item.from) || item.until <= item.from)) throw new POIError("Invalid historical authority lineage");
|
|
9212
|
+
return new _HistoricalTrustContext(reference, now, authorities);
|
|
9213
|
+
}
|
|
9214
|
+
authorityAt(at) {
|
|
9215
|
+
if (this.authorities.length === 0) return void 0;
|
|
9216
|
+
const matches = this.authorities.filter((item) => at >= item.from && at < item.until);
|
|
9217
|
+
if (matches.length !== 1) throw new POIError("No unambiguous authorized policy covers this historical time");
|
|
9218
|
+
return matches[0]?.context;
|
|
9219
|
+
}
|
|
9220
|
+
static create(anchor, reference, options) {
|
|
9221
|
+
const now = options.now ?? Date.now();
|
|
9222
|
+
if (!Number.isFinite(now) || reference.signedAt > now) throw new POIError("Invalid historical authorization/reference time");
|
|
9223
|
+
const verified = TrustContext.create(anchor, reference, {
|
|
9224
|
+
trustedIdentityKeyIds: options.trustedIdentityKeyIds,
|
|
9225
|
+
...options.otherIdentityKeys === void 0 ? {} : { otherIdentityKeys: options.otherIdentityKeys },
|
|
9226
|
+
at: reference.signedAt
|
|
9227
|
+
});
|
|
9228
|
+
return new _HistoricalTrustContext(verified, now);
|
|
9229
|
+
}
|
|
9230
|
+
};
|
|
9231
|
+
function verifyHistoricalMeterEvidence(current, value, request, policy = {}) {
|
|
9232
|
+
if (!(current instanceof TrustContext) && !(current instanceof HistoricalTrustContext)) throw new POIError("Historical verification requires current local authorization");
|
|
9233
|
+
const requiredWitnesses = policy.requiredWitnesses ?? 0;
|
|
9234
|
+
if (!Number.isSafeInteger(requiredWitnesses) || requiredWitnesses < 0) throw new POIError("Invalid historical witness policy");
|
|
9235
|
+
for (const revocation of policy.revocations ?? []) if (!Number.isFinite(revocation.invalidFrom)) throw new POIError("Invalid compromise time");
|
|
9236
|
+
const revoked = (id, at) => (policy.revocations ?? []).some((item) => item.keyId === id && at >= item.invalidFrom);
|
|
9237
|
+
const evidence = parseHistoricalMeterEvidence(value);
|
|
9238
|
+
if (evidence.evseId !== request.evseId) throw new POIError("Historical evidence belongs to another EVSE");
|
|
9239
|
+
const times = [...new Set(request.measurementTimes)];
|
|
9240
|
+
if (times.length === 0 || times.some((time) => !Number.isFinite(time) || time > current.evaluatedAt))
|
|
9241
|
+
throw new POIError("Historical verification needs finite, non-future original measurement times");
|
|
9242
|
+
const versions = evidence.versions.map((version) => ({ ...version, parsed: ChargingStationProof.parse(version.proof) }));
|
|
9243
|
+
const used = /* @__PURE__ */ new Map();
|
|
9244
|
+
const bindings = [];
|
|
9245
|
+
for (const time of times) {
|
|
9246
|
+
const matches = [];
|
|
9247
|
+
const errors = [];
|
|
9248
|
+
for (const version2 of versions) {
|
|
9249
|
+
try {
|
|
9250
|
+
const operator = version2.parsed.roamingNetwork["operator"];
|
|
9251
|
+
const context = historicalOperatorContext(current, operator ?? null, time);
|
|
9252
|
+
const binding2 = verifyMeterKey(context, version2.proof, request);
|
|
9253
|
+
const verified = version2.parsed.verify(context);
|
|
9254
|
+
const operatorJSON = requireObject(operator, "The historical operator");
|
|
9255
|
+
const trust = OperatorTrust.fromOperatorJSON(context, operatorJSON);
|
|
9256
|
+
const holders = /* @__PURE__ */ new Set();
|
|
9257
|
+
for (const attestation of verified.attestations) {
|
|
9258
|
+
const certificate = trust.operator.signingKeyCertificates.find((item) => item.signingKey.id === attestation.keyId);
|
|
9259
|
+
if (certificate !== void 0 && !revoked(attestation.keyId, attestation.signedAt) && !revoked(certificate.keyId, attestation.signedAt)) {
|
|
9260
|
+
const holder = trust.holderOf(attestation);
|
|
9261
|
+
if (holder !== void 0) holders.add(holder);
|
|
9262
|
+
}
|
|
9263
|
+
}
|
|
9264
|
+
if (holders.size < context.requiredConsensus || revoked(binding2.key.id, time) || revoked(binding2.key.canonicalId, time))
|
|
9265
|
+
throw new POIError("Historical keys are affected by a caller-authorized compromise");
|
|
9266
|
+
matches.push({ binding: binding2, version: version2 });
|
|
9267
|
+
} catch (error) {
|
|
9268
|
+
errors.push(error instanceof Error ? error.message : String(error));
|
|
9269
|
+
}
|
|
9270
|
+
}
|
|
9271
|
+
if (matches.length === 0) throw new POIError(`No historical EVSE/meter/key coverage at ${new Date(time).toISOString()}: ${errors.join("; ")}`);
|
|
9272
|
+
const identities = new Set(matches.map(({ binding: binding2 }) => `${binding2.meterId}:${binding2.key.canonicalId}`));
|
|
9273
|
+
if (identities.size !== 1) throw new POIError("Conflicting historical key assignments cover the same measurement time");
|
|
9274
|
+
const selected = matches.sort((a, b) => a.binding.evidence.commitId.localeCompare(b.binding.evidence.commitId))[0];
|
|
9275
|
+
if (selected === void 0) throw new POIError("Missing historical selection");
|
|
9276
|
+
const { binding, version } = selected;
|
|
9277
|
+
used.set(binding.evidence.commitId, version);
|
|
9278
|
+
bindings.push(Object.freeze({ ...binding, evidence: Object.freeze({
|
|
9279
|
+
...binding.evidence,
|
|
9280
|
+
verifiedAt: current.evaluatedAt,
|
|
9281
|
+
validUntil: current.validUntil,
|
|
9282
|
+
keyStatementDigest: current.keyStatementDigest,
|
|
9283
|
+
historicalStatementDigest: binding.evidence.keyStatementDigest,
|
|
9284
|
+
attestationValidUntil: binding.evidence.validUntil,
|
|
9285
|
+
measurementTimes: Object.freeze([time]),
|
|
9286
|
+
association: "historical-attestation"
|
|
9287
|
+
}) }));
|
|
9288
|
+
}
|
|
9289
|
+
const heads = /* @__PURE__ */ new Map();
|
|
9290
|
+
const publication = [];
|
|
9291
|
+
for (const [commitId, version] of used) {
|
|
9292
|
+
if (version.commitProof === void 0) {
|
|
9293
|
+
if (version.checkpoints.length > 0 || policy.requirePublication === true || requiredWitnesses > 0)
|
|
9294
|
+
throw new POIError("Required historical commit inclusion evidence is missing");
|
|
9295
|
+
publication.push({ commitId, publication: "missing", witnessedBy: [] });
|
|
9296
|
+
continue;
|
|
9297
|
+
}
|
|
9298
|
+
const proof = CheckpointProof.parse(version.commitProof);
|
|
9299
|
+
const entries = version.checkpoints.map((entry2) => ({ ...entry2, parsed: Checkpoint.parse(entry2.checkpoint) }));
|
|
9300
|
+
const entry = entries.find((item) => item.parsed.id === proof.checkpointId);
|
|
9301
|
+
if (entry === void 0) throw new POIError("The checkpoint of the historical commit proof is missing");
|
|
9302
|
+
const checkpoint = entry.parsed;
|
|
9303
|
+
if (checkpoint.roamingNetworkId !== current.roamingNetworkId || checkpoint.createdAt > current.evaluatedAt)
|
|
9304
|
+
throw new POIError("Foreign or future historical checkpoint");
|
|
9305
|
+
const coordinate2 = `${checkpoint.epoch}:${checkpoint.sequence}`;
|
|
9306
|
+
if (heads.has(coordinate2) && heads.get(coordinate2) !== checkpoint.id) throw new POIError("Conflicting historical checkpoint heads");
|
|
9307
|
+
heads.set(coordinate2, checkpoint.id);
|
|
9308
|
+
const checked = verifyAuthorizedCheckpoint(current, checkpoint, entry.operator, policy);
|
|
9309
|
+
const witnesses = checked.witnessedBy;
|
|
9310
|
+
proof.verify(checkpoint);
|
|
9311
|
+
version.parsed.verifyAgainstCommitProof(proof);
|
|
9312
|
+
publication.push({
|
|
9313
|
+
commitId,
|
|
9314
|
+
publication: witnesses.length > 0 ? "witnessed-checkpoint" : "rno-checkpoint",
|
|
9315
|
+
checkpointId: checkpoint.id,
|
|
9316
|
+
witnessedBy: [...witnesses]
|
|
9317
|
+
});
|
|
9318
|
+
}
|
|
9319
|
+
return Object.freeze({
|
|
9320
|
+
bindings: Object.freeze(bindings),
|
|
9321
|
+
versions: Object.freeze(publication.map((item) => Object.freeze(item))),
|
|
9322
|
+
checkedAt: current.evaluatedAt,
|
|
9323
|
+
timeBasis: "signed-measurement-timestamps",
|
|
9324
|
+
independentTime: "not-proven",
|
|
9325
|
+
latestVersion: "not-proven"
|
|
9326
|
+
});
|
|
9327
|
+
}
|
|
9328
|
+
function verifyAuthorizedCheckpoint(current, checkpoint, operator, policy = {}) {
|
|
9329
|
+
if (!(current instanceof TrustContext) && !(current instanceof HistoricalTrustContext)) throw new POIError("Checkpoint verification requires current local authority");
|
|
9330
|
+
for (const item of policy.revocations ?? []) if (!Number.isFinite(item.invalidFrom)) throw new POIError("Invalid checkpoint compromise time");
|
|
9331
|
+
const requiredWitnesses = policy.requiredWitnesses ?? 0;
|
|
9332
|
+
if (!Number.isSafeInteger(requiredWitnesses) || requiredWitnesses < 0) throw new POIError("Invalid checkpoint witness policy");
|
|
9333
|
+
if (checkpoint.roamingNetworkId !== current.roamingNetworkId || checkpoint.createdAt > current.evaluatedAt) throw new POIError("Foreign or future checkpoint");
|
|
9334
|
+
const revoked = (id, at) => (policy.revocations ?? []).some((item) => item.keyId === id && at >= item.invalidFrom);
|
|
9335
|
+
const profile = current.epochs.find((epoch) => epoch.number === checkpoint.epoch)?.profiles;
|
|
9336
|
+
if (profile === void 0) throw new POIError("The checkpoint epoch has no verified transition from the local policy");
|
|
9337
|
+
const holders = /* @__PURE__ */ new Set();
|
|
9338
|
+
const witnesses = /* @__PURE__ */ new Set();
|
|
9339
|
+
const witnessKeys = /* @__PURE__ */ new Set();
|
|
9340
|
+
let required2 = current instanceof HistoricalTrustContext ? current.authorityAt(checkpoint.createdAt)?.requiredConsensus ?? current.requiredConsensus : current.requiredConsensus;
|
|
9341
|
+
for (const signature of checkpoint.signatures) {
|
|
9342
|
+
if (!profile.allowsSignatureAlgorithm(signature.algorithm)) continue;
|
|
9343
|
+
if (signature.signedAt < checkpoint.createdAt || signature.signedAt > current.evaluatedAt || revoked(signature.keyId, signature.signedAt)) continue;
|
|
9344
|
+
try {
|
|
9345
|
+
if (signature.role === "roamingNetworkOperator") {
|
|
9346
|
+
const context = historicalOperatorContext(current, operator, signature.signedAt);
|
|
9347
|
+
required2 = Math.max(required2, context.requiredConsensus);
|
|
9348
|
+
const trust = OperatorTrust.fromOperatorJSON(context, operator);
|
|
9349
|
+
const certificate = trust.operator.signingKeyCertificates.find((item) => item.signingKey.id === signature.keyId);
|
|
9350
|
+
const key2 = trust.signingKeyOf(signature);
|
|
9351
|
+
const holder = trust.holderOf(signature);
|
|
9352
|
+
if (certificate === void 0 || revoked(certificate.keyId, signature.signedAt) || key2 === void 0 || holder === void 0) continue;
|
|
9353
|
+
checkpoint.verifySignature(signature, key2);
|
|
9354
|
+
holders.add(holder);
|
|
9355
|
+
} else if (signature.role === "witness") {
|
|
9356
|
+
const witness = policy.witnesses?.find((item) => item.witness === signature.signer && item.key.id === signature.keyId);
|
|
9357
|
+
if (witness === void 0 || !witness.key.isValidAt(signature.signedAt) || !witness.key.maySign("party") || witnessKeys.has(witness.key.id)) continue;
|
|
9358
|
+
checkpoint.verifySignature(signature, witness.key);
|
|
9359
|
+
witnesses.add(witness.witness);
|
|
9360
|
+
witnessKeys.add(witness.key.id);
|
|
9361
|
+
}
|
|
9362
|
+
} catch {
|
|
9363
|
+
}
|
|
9364
|
+
}
|
|
9365
|
+
if (holders.size < required2 || witnesses.size < requiredWitnesses) throw new POIError("Historical checkpoint lacks the locally required RNO/witness quorum");
|
|
9366
|
+
return { checkpoint, signedBy: [...holders], witnessedBy: [...witnesses] };
|
|
9367
|
+
}
|
|
9368
|
+
|
|
9369
|
+
// src/verifier/pricePublication.ts
|
|
9370
|
+
var PRICE_PUBLICATION_RECEIPT_CONTEXT = "https://open.charging.cloud/contexts/wwcp/pricePublicationReceipt/v1";
|
|
9371
|
+
var WITNESS_PUBLICATION_POLICY_CONTEXT = "https://open.charging.cloud/contexts/wwcp/witnessPublicationPolicy/v1";
|
|
9372
|
+
function pricePublicationOfferHashes(statement, tariff) {
|
|
9373
|
+
return { priceStatementSHA256: toHex(hash("sha256", canonicalJSONBytes(statement.json))), tariffSHA256: toHex(hash("sha256", canonicalJSONBytes(tariff))) };
|
|
9374
|
+
}
|
|
9375
|
+
function identifier(json, property) {
|
|
9376
|
+
const text = requireString(json, property);
|
|
9377
|
+
if (text.trim() !== text || text.length === 0 || text.length > 512) throw new POIError("Invalid publication identity");
|
|
9378
|
+
for (let index = 0; index < text.length; index++) if (text.charCodeAt(index) < 32) throw new POIError("Invalid publication identity");
|
|
9379
|
+
return text;
|
|
9380
|
+
}
|
|
9381
|
+
function publicPublicationURL(text) {
|
|
9382
|
+
const url = new URL(text);
|
|
9383
|
+
if (url.protocol !== "https:" || url.username !== "" || url.password !== "" || url.hash !== "" || text.length > 2048) throw new POIError("Publication sources require public HTTPS without credentials or fragments");
|
|
9384
|
+
const host = url.hostname.toLowerCase().replace(/^\[|\]$/gu, "");
|
|
9385
|
+
if (host === "localhost" || host.endsWith(".localhost") || host.endsWith(".local") || host === "::1" || host.includes(":") || /^\d+(\.\d+){3}$/u.test(host) || !host.includes(".")) throw new POIError("Publication sources require a public DNS hostname");
|
|
9386
|
+
return url.href;
|
|
9387
|
+
}
|
|
9388
|
+
function parseWitnessPublicationPolicy(value) {
|
|
9389
|
+
const json = requireObject(value, "Witness publication policy");
|
|
9390
|
+
onlyProperties(json, ["@context", "requiredOrganizations", "witnesses"], "Witness publication policy");
|
|
9391
|
+
if (json["@context"] !== WITNESS_PUBLICATION_POLICY_CONTEXT) throw new POIError("Unsupported witness publication policy");
|
|
9392
|
+
const requiredOrganizations = requireInteger(json, "requiredOrganizations", 1, 16);
|
|
9393
|
+
const entries = requireArray(json["witnesses"], "Publication witnesses");
|
|
9394
|
+
if (entries.length > 32 || canonicalJSONBytes(json).length > 1024 * 1024) throw new POIError("Witness policy exceeds its capacity");
|
|
9395
|
+
const witnesses = [], ids = /* @__PURE__ */ new Set(), material = /* @__PURE__ */ new Map();
|
|
9396
|
+
for (const entry of entries) {
|
|
9397
|
+
const item = requireObject(entry, "A publication witness");
|
|
9398
|
+
onlyProperties(item, ["id", "organization", "role", "networks", "keys", "notBefore", "notAfter", "clockErrorMs", "endpoints"], "A publication witness");
|
|
9399
|
+
if (item["role"] !== "price-publication-and-time") throw new POIError("A witness needs the explicit publication and time role");
|
|
9400
|
+
const id = identifier(item, "id"), organization = identifier(item, "organization");
|
|
9401
|
+
if (ids.has(id)) throw new POIError("Duplicate witness identity");
|
|
9402
|
+
ids.add(id);
|
|
9403
|
+
const networks = requireArray(item["networks"], "Witness network scopes");
|
|
9404
|
+
if (networks.length === 0 || networks.length > 32 || networks.some((value2) => typeof value2 !== "string" || value2.length === 0 || value2 === "*") || new Set(networks).size !== networks.length) throw new POIError("Explicit witness network scopes required");
|
|
9405
|
+
const keys = POIPublicKey.parseAll(item["keys"], "Witness publication keys");
|
|
9406
|
+
if (keys.length === 0 || keys.length > 8 || new Set(keys.map((key2) => key2.id)).size !== keys.length) throw new POIError("Invalid witness key list");
|
|
9407
|
+
for (const key2 of keys) {
|
|
9408
|
+
if (!key2.maySign("witnessPublications")) throw new POIError("Witness key usage does not permit publication receipts");
|
|
9409
|
+
const fingerprint = toHex(key2.key.thumbprint()), before = material.get(fingerprint);
|
|
9410
|
+
if (before !== void 0 && before !== organization) throw new POIError("The same witness key cannot represent different organizations");
|
|
9411
|
+
material.set(fingerprint, organization);
|
|
9412
|
+
}
|
|
9413
|
+
const notBefore = requireTimestamp(item, "notBefore"), notAfter = requireTimestamp(item, "notAfter");
|
|
9414
|
+
if (notAfter <= notBefore) throw new POIError("Invalid witness validity interval");
|
|
9415
|
+
const clockErrorMs = requireInteger(item, "clockErrorMs", 0, 3e5);
|
|
9416
|
+
const endpoints = requireArray(item["endpoints"], "Witness receipt endpoints");
|
|
9417
|
+
if (endpoints.length > 4 || endpoints.some((url) => typeof url !== "string")) throw new POIError("Invalid witness receipt endpoints");
|
|
9418
|
+
witnesses.push(Object.freeze({
|
|
9419
|
+
id,
|
|
9420
|
+
organization,
|
|
9421
|
+
networks: Object.freeze(networks),
|
|
9422
|
+
keys: Object.freeze(keys),
|
|
9423
|
+
notBefore,
|
|
9424
|
+
notAfter,
|
|
9425
|
+
clockErrorMs,
|
|
9426
|
+
endpoints: Object.freeze(endpoints.map(publicPublicationURL)),
|
|
9427
|
+
json: item
|
|
9428
|
+
}));
|
|
9429
|
+
}
|
|
9430
|
+
return Object.freeze({ requiredOrganizations, witnesses: Object.freeze(witnesses), json });
|
|
9431
|
+
}
|
|
9432
|
+
var PricePublicationReceipt = class _PricePublicationReceipt {
|
|
9433
|
+
constructor(json, observedAt, signedAt) {
|
|
9434
|
+
this.json = json;
|
|
9435
|
+
this.observedAt = observedAt;
|
|
9436
|
+
this.signedAt = signedAt;
|
|
9437
|
+
}
|
|
9438
|
+
json;
|
|
9439
|
+
observedAt;
|
|
9440
|
+
signedAt;
|
|
9441
|
+
static parse(value) {
|
|
9442
|
+
const json = requireObject(value, "Price publication receipt");
|
|
9443
|
+
onlyProperties(json, ["@context", "assertion", "roamingNetworkId", "EVSEId", "priceStatementDigest", "priceStatementSHA256", "tariffSHA256", "checkpointId", "witnessId", "publicSourceURL", "observedAt", "signedAt", "algorithm", "keyId", "value"], "Price publication receipt");
|
|
9444
|
+
if (json["@context"] !== PRICE_PUBLICATION_RECEIPT_CONTEXT || json["assertion"] !== "public-price-retrieval") throw new POIError("Unsupported publication receipt assertion");
|
|
9445
|
+
for (const field of ["roamingNetworkId", "EVSEId", "witnessId", "algorithm", "keyId"]) identifier(json, field);
|
|
9446
|
+
const signature = requireString(json, "value");
|
|
9447
|
+
if (signature.length === 0 || signature.length > 131072) throw new POIError("Invalid publication signature size");
|
|
9448
|
+
if (!isLowerHex(requireString(json, "priceStatementDigest"), 32) || !/^(?:[0-9a-f]{64}|[0-9a-f]{96}|[0-9a-f]{128})$/u.test(requireString(json, "checkpointId"))) throw new POIError("Invalid publication receipt digest");
|
|
9449
|
+
for (const field of ["priceStatementSHA256", "tariffSHA256"]) if (!isLowerHex(requireString(json, field), 32)) throw new POIError("Invalid signed-price or tariff digest");
|
|
9450
|
+
publicPublicationURL(requireString(json, "publicSourceURL"));
|
|
9451
|
+
const observedAt = requireTimestamp(json, "observedAt"), signedAt = requireTimestamp(json, "signedAt");
|
|
9452
|
+
if (signedAt < observedAt || signedAt - observedAt > 6e4) throw new POIError("A publication receipt must be signed within one minute after observation");
|
|
9453
|
+
return new _PricePublicationReceipt(json, observedAt, signedAt);
|
|
9454
|
+
}
|
|
9455
|
+
signingBytes() {
|
|
9456
|
+
const { value: _, ...signed } = this.json;
|
|
9457
|
+
return canonicalJSONBytes(signed);
|
|
9458
|
+
}
|
|
9459
|
+
digest() {
|
|
9460
|
+
return toHex(hash("sha256", canonicalJSONBytes(this.json)));
|
|
9461
|
+
}
|
|
9462
|
+
};
|
|
9463
|
+
function verifyPricePublication(receipts, policy, subject) {
|
|
9464
|
+
if (!Number.isFinite(subject.chargingStarted) || !Number.isFinite(subject.now) || subject.chargingStarted > subject.now || receipts.length > 64) throw new POIError("Invalid publication evaluation context");
|
|
9465
|
+
const accepted = policy === void 0 ? void 0 : parseWitnessPublicationPolicy(policy.json);
|
|
9466
|
+
const organizations = /* @__PURE__ */ new Set(), rows = [];
|
|
9467
|
+
for (const raw of receipts) {
|
|
9468
|
+
let receipt;
|
|
9469
|
+
try {
|
|
9470
|
+
receipt = PricePublicationReceipt.parse(raw);
|
|
9471
|
+
const json = receipt.json, witness = accepted?.witnesses.find((item) => item.id === json["witnessId"]);
|
|
9472
|
+
if (witness?.networks.includes(subject.roamingNetworkId) !== true) {
|
|
9473
|
+
rows.push({ witnessId: requireString(json, "witnessId"), status: "untrusted", detail: "No locally authorized witness for this network" });
|
|
9474
|
+
continue;
|
|
9475
|
+
}
|
|
9476
|
+
const key2 = witness.keys.find((item) => item.id === json["keyId"]);
|
|
9477
|
+
if (key2 === void 0) throw new POIError("The receipt key is not authorized for this witness");
|
|
9478
|
+
if ((subject.excludedIdentities ?? []).includes(witness.id) || (subject.excludedIdentities ?? []).includes(witness.organization) || (subject.excludedKeyMaterial ?? []).includes(toHex(key2.key.thumbprint()))) throw new POIError("An RNO/CPO authority cannot count as an independent publication witness");
|
|
9479
|
+
const lower = receipt.observedAt - witness.clockErrorMs, upper = receipt.signedAt + witness.clockErrorMs;
|
|
9480
|
+
if (lower < witness.notBefore || upper >= witness.notAfter || !key2.isValidAt(lower) || !key2.isValidAt(upper)) throw new POIError("Witness/key does not cover the observation and signing interval");
|
|
9481
|
+
if (receipt.signedAt > subject.now || json["roamingNetworkId"] !== subject.roamingNetworkId || json["EVSEId"] !== subject.evseId || json["priceStatementDigest"] !== subject.statement.digestHex()) throw new POIError("Foreign, substituted or future publication receipt");
|
|
9482
|
+
const hashes = pricePublicationOfferHashes(subject.statement, subject.tariff);
|
|
9483
|
+
if (json["priceStatementSHA256"] !== hashes.priceStatementSHA256 || json["tariffSHA256"] !== hashes.tariffSHA256) throw new POIError("Receipt does not bind the exact original CPO signature and selected tariff definition/currency");
|
|
9484
|
+
const checkpoint = subject.checkpoints.find((item) => item.id === json["checkpointId"]);
|
|
9485
|
+
if (checkpoint === void 0 || checkpoint.createdAt > receipt.observedAt + witness.clockErrorMs || subject.statement.signedAt > receipt.observedAt + witness.clockErrorMs) throw new POIError("Receipt needs the exact authorized price checkpoint and a consistent observation time");
|
|
9486
|
+
verifySignature(requireString(json, "algorithm"), key2.key, receipt.signingBytes(), requireString(json, "value"));
|
|
9487
|
+
const common = { witnessId: witness.id, organization: witness.organization, keyId: key2.id, receiptDigest: receipt.digest(), latestPublicationAt: upper, sourceURL: requireString(json, "publicSourceURL") };
|
|
9488
|
+
if (upper >= subject.chargingStarted) rows.push({ ...common, status: "after-start", detail: "The latest observation/signing time including the accepted clock error is not before the signed start" });
|
|
9489
|
+
else if (organizations.has(witness.organization)) rows.push({ ...common, status: "duplicate-organization", detail: "Only one vote per independently approved organization" });
|
|
9490
|
+
else {
|
|
9491
|
+
organizations.add(witness.organization);
|
|
9492
|
+
rows.push({ ...common, status: "counted", detail: "Public retrieval of this exact included offer witnessed before its signed charging start" });
|
|
9493
|
+
}
|
|
9494
|
+
} catch (error) {
|
|
9495
|
+
rows.push({ witnessId: typeof receipt?.json["witnessId"] === "string" ? receipt.json["witnessId"] : "", status: "invalid", detail: error instanceof Error ? error.message : String(error) });
|
|
9496
|
+
}
|
|
9497
|
+
}
|
|
9498
|
+
const requiredOrganizations = accepted?.requiredOrganizations ?? 2;
|
|
9499
|
+
return Object.freeze({
|
|
9500
|
+
status: organizations.size >= requiredOrganizations ? "witnessed-before-signed-start" : rows.some((row) => row.status === "invalid") ? "invalid" : "not-proven",
|
|
9501
|
+
requiredOrganizations,
|
|
9502
|
+
organizations: Object.freeze([...organizations].sort()),
|
|
9503
|
+
timeBasis: "trusted-witness-clocks-versus-signed-start",
|
|
9504
|
+
actualStartTime: "not-independently-proven",
|
|
9505
|
+
receipts: Object.freeze(rows)
|
|
9506
|
+
});
|
|
9507
|
+
}
|
|
9508
|
+
|
|
9509
|
+
// src/verifier/sessionPrices.ts
|
|
9510
|
+
var SESSION_PRICE_EVIDENCE_CONTEXT = "https://open.charging.cloud/contexts/wwcp/sessionPriceEvidence/v1";
|
|
9511
|
+
var SESSION_PRICE_AGREEMENT_CONTEXT = "https://open.charging.cloud/contexts/wwcp/sessionPriceAgreement/v1";
|
|
9512
|
+
var SIMPLE_GROSS_PRICE_PROFILE = "chargy-gross-rates/v1";
|
|
9513
|
+
var SessionPriceAgreement = class _SessionPriceAgreement {
|
|
9514
|
+
constructor(json, chargingStarted, signedAt) {
|
|
9515
|
+
this.json = json;
|
|
9516
|
+
this.chargingStarted = chargingStarted;
|
|
9517
|
+
this.signedAt = signedAt;
|
|
9518
|
+
}
|
|
9519
|
+
json;
|
|
9520
|
+
chargingStarted;
|
|
9521
|
+
signedAt;
|
|
9522
|
+
static parse(value) {
|
|
9523
|
+
const json = requireObject(value, "A session price assignment");
|
|
9524
|
+
onlyProperties(json, ["@context", "roamingNetworkId", "EVSEId", "meterSerialNumber", "chargingStarted", "startDocumentSHA256", "chargingMode", "chargingTariffId", "priceStatementDigest", "priceBinding", "pricingProfile", "role", "signer", "signedAt", "algorithm", "keyId", "value"], "A session price assignment");
|
|
9525
|
+
if (json["@context"] !== SESSION_PRICE_AGREEMENT_CONTEXT || json["role"] !== "chargingStationOperator") throw new POIError("Unsupported price assignment context/role");
|
|
9526
|
+
for (const field of ["roamingNetworkId", "EVSEId", "meterSerialNumber", "chargingMode", "chargingTariffId", "priceBinding", "pricingProfile", "signer", "algorithm", "keyId", "value"]) requireString(json, field);
|
|
9527
|
+
for (const field of ["startDocumentSHA256", "priceStatementDigest"]) if (!isLowerHex(requireString(json, field), 32)) throw new POIError("Invalid price assignment digest");
|
|
9528
|
+
return new _SessionPriceAgreement(json, requireTimestamp(json, "chargingStarted"), requireTimestamp(json, "signedAt"));
|
|
9529
|
+
}
|
|
9530
|
+
signingBytes() {
|
|
9531
|
+
const { value: _, ...signed } = this.json;
|
|
9532
|
+
return canonicalJSONBytes(signed);
|
|
9533
|
+
}
|
|
9534
|
+
verify(station) {
|
|
9535
|
+
if (this.json["roamingNetworkId"] !== station.roamingNetwork.id || this.json["signer"] !== station.operator["@id"]) throw new POIError("Foreign price assignment authority");
|
|
9536
|
+
const key2 = station.operatorSigningKeys.find((item) => item.id === this.json["keyId"]);
|
|
9537
|
+
if (key2 === void 0 || !key2.isValidAt(this.signedAt) || !key2.isValidAt(this.chargingStarted) || !key2.maySign("priceStatements")) throw new POIError("Unauthorized/expired price assignment key");
|
|
9538
|
+
verifySignature(requireString(this.json, "algorithm"), key2.key, this.signingBytes(), requireString(this.json, "value"));
|
|
9539
|
+
}
|
|
9540
|
+
};
|
|
9541
|
+
function parseSessionPriceEvidence(value) {
|
|
9542
|
+
const json = requireObject(value, "Session price evidence");
|
|
9543
|
+
onlyProperties(json, ["@context", "EVSEId", "chargingStationProof", "priceStatements", "agreement", "priceStatementProofs", "checkpoints", "publicationReceipts", "witnessCandidates"], "Session price evidence");
|
|
9544
|
+
if (json["@context"] !== SESSION_PRICE_EVIDENCE_CONTEXT) throw new POIError("Unsupported session price evidence");
|
|
9545
|
+
const publicationReceipts = json["publicationReceipts"] === void 0 ? [] : requireArray(json["publicationReceipts"], "Publication receipts");
|
|
9546
|
+
const witnessCandidates = json["witnessCandidates"] === void 0 ? [] : requireArray(json["witnessCandidates"], "Untrusted witness recommendations");
|
|
9547
|
+
if (publicationReceipts.length > 64 || witnessCandidates.length > 32) throw new POIError("Too many publication receipts or recommendations");
|
|
9548
|
+
return {
|
|
9549
|
+
publicationReceipts,
|
|
9550
|
+
witnessCandidates,
|
|
9551
|
+
evseId: requireString(json, "EVSEId"),
|
|
9552
|
+
proof: requireObject(json["chargingStationProof"], "The priced station proof"),
|
|
9553
|
+
priceStatements: requireArray(json["priceStatements"], "Price statements"),
|
|
9554
|
+
...json["agreement"] === void 0 ? {} : { agreement: json["agreement"] },
|
|
9555
|
+
priceProofs: json["priceStatementProofs"] === void 0 ? [] : requireArray(json["priceStatementProofs"], "Price inclusion proofs"),
|
|
9556
|
+
checkpoints: (json["checkpoints"] === void 0 ? [] : requireArray(json["checkpoints"], "Price checkpoints")).map((item) => {
|
|
9557
|
+
const entry = requireObject(item, "A price checkpoint entry");
|
|
9558
|
+
onlyProperties(entry, ["checkpoint", "operator"], "A price checkpoint entry");
|
|
9559
|
+
return { checkpoint: requireObject(entry["checkpoint"], "A price checkpoint"), operator: requireObject(entry["operator"], "The checkpoint authority") };
|
|
9560
|
+
})
|
|
9561
|
+
};
|
|
9562
|
+
}
|
|
9563
|
+
function grossRates(currency, elements) {
|
|
9564
|
+
if (elements.length !== 1) return void 0;
|
|
9565
|
+
const element = elements[0];
|
|
9566
|
+
if (element === void 0 || Object.keys(element).some((key2) => key2 !== "priceComponents" && key2 !== "restrictions")) return void 0;
|
|
9567
|
+
if (element["restrictions"] !== void 0 && (!isObject(element["restrictions"]) || Object.keys(element["restrictions"]).length > 0)) return void 0;
|
|
9568
|
+
const components = element["priceComponents"];
|
|
9569
|
+
if (!Array.isArray(components) || components.length === 0) return void 0;
|
|
9570
|
+
const rates = { currency };
|
|
9571
|
+
for (const raw of components) {
|
|
9572
|
+
if (!isObject(raw) || Object.keys(raw).some((key2) => !["type", "price", "stepSize"].includes(key2))) return void 0;
|
|
9573
|
+
const type = raw["type"], value = raw["price"], step = raw["stepSize"];
|
|
9574
|
+
if (!isNumber(value) || Decimal.parse(asText(value)).mantissa < 0n || typeof step !== "number" || !Number.isSafeInteger(step) || step < 1) return void 0;
|
|
9575
|
+
const field = type === "ENERGY" ? "energyPerKWh" : type === "FLAT" ? "flatFee" : type === "TIME" ? "timePerHour" : void 0;
|
|
9576
|
+
if (field === void 0 || rates[field] !== void 0) return void 0;
|
|
9577
|
+
rates[field] = asText(value);
|
|
9578
|
+
}
|
|
9579
|
+
return rates;
|
|
9580
|
+
}
|
|
9581
|
+
function verifySessionPriceEvidence(current, value, request, policy = {}) {
|
|
9582
|
+
if (!(current instanceof TrustContext) && !(current instanceof HistoricalTrustContext)) throw new POIError("Session prices require current local authorization");
|
|
9583
|
+
if (!Number.isFinite(request.chargingStarted) || request.chargingStarted > current.evaluatedAt || !isLowerHex(request.startDocumentSHA256, 32)) throw new POIError("Invalid original price context");
|
|
9584
|
+
for (const item of policy.revocations ?? []) if (!Number.isFinite(item.invalidFrom)) throw new POIError("Invalid compromise time");
|
|
9585
|
+
const evidence = parseSessionPriceEvidence(value);
|
|
9586
|
+
if (evidence.evseId !== request.evseId) throw new POIError("Price evidence belongs to another EVSE");
|
|
9587
|
+
const proof = ChargingStationProof.parse(evidence.proof), context = historicalOperatorContext(current, proof.roamingNetwork["operator"] ?? null, request.chargingStarted);
|
|
9588
|
+
const station = verifyChargingStation(context, evidence.proof, { evseId: request.evseId }), diagnostics = [];
|
|
9589
|
+
const base = { station, publicationBeforeStart: "not-proven", customerConsent: "not-proven", latestOrSoleOffer: "not-proven", checkedAt: current.evaluatedAt, checkpointIds: [], witnessedBy: [] };
|
|
9590
|
+
const fail = (signatures, applicability2, message) => ({ ...base, signatures, applicability: applicability2, agreement: "missing", publication: "missing", chainGaps: false, diagnostics: [message] });
|
|
9591
|
+
if (evidence.priceStatements.length === 0) return fail("missing", "missing", "No signed price statement supplied");
|
|
9592
|
+
let statements;
|
|
9593
|
+
try {
|
|
9594
|
+
statements = evidence.priceStatements.map((item) => PriceStatement.parse(item));
|
|
9595
|
+
for (const statement of statements) {
|
|
9596
|
+
statement.verify(station.roamingNetwork.id, asText(station.operator["@id"]), station.operatorSigningKeys);
|
|
9597
|
+
if (statement.signedAt > current.evaluatedAt || (policy.revocations ?? []).some((item) => item.keyId === statement.keyId && statement.signedAt >= item.invalidFrom)) throw new POIError("Future/revoked CPO price signature");
|
|
9598
|
+
}
|
|
9599
|
+
} catch (error) {
|
|
9600
|
+
return fail("invalid", "invalid", error instanceof Error ? error.message : String(error));
|
|
9601
|
+
}
|
|
9602
|
+
let active;
|
|
9603
|
+
try {
|
|
9604
|
+
active = verifyPriceStatements(station, [...evidence.priceStatements], { at: request.chargingStarted });
|
|
9605
|
+
} catch (error) {
|
|
9606
|
+
return fail("valid", "invalid", error instanceof Error ? error.message : String(error));
|
|
9607
|
+
}
|
|
9608
|
+
const chainGaps = statements.some((statement) => statement.sequence > 0 && !statements.some((before) => statement.follows(before)));
|
|
9609
|
+
let agreement;
|
|
9610
|
+
if (evidence.agreement !== void 0) {
|
|
9611
|
+
try {
|
|
9612
|
+
agreement = SessionPriceAgreement.parse(evidence.agreement);
|
|
9613
|
+
agreement.verify(station);
|
|
9614
|
+
const a = agreement.json;
|
|
9615
|
+
if (a["EVSEId"] !== request.evseId || a["meterSerialNumber"] !== request.meterSerialNumber || agreement.chargingStarted !== request.chargingStarted || a["startDocumentSHA256"] !== request.startDocumentSHA256 || agreement.signedAt > current.evaluatedAt || (policy.revocations ?? []).some((item) => item.keyId === a["keyId"] && agreement !== void 0 && agreement.signedAt >= item.invalidFrom)) throw new POIError("Price assignment does not match this original session");
|
|
9616
|
+
} catch (error) {
|
|
9617
|
+
return { ...fail("valid", "invalid", error instanceof Error ? error.message : String(error)), agreement: "invalid", chainGaps };
|
|
9618
|
+
}
|
|
9619
|
+
}
|
|
9620
|
+
const tariffs = station.tariffs.get(request.evseId) ?? [];
|
|
9621
|
+
const candidates = [...active.values()].flatMap(({ statement, digest: digest2 }) => statement.prices.flatMap((price) => {
|
|
9622
|
+
const tariff = tariffs.find((item) => item["@id"] === price["chargingTariffId"]);
|
|
9623
|
+
return tariff === void 0 ? [] : [{ statement, digest: digest2, tariff, price }];
|
|
9624
|
+
}));
|
|
9625
|
+
const eligible = agreement === void 0 ? candidates : candidates.filter((item) => item.digest === agreement.json["priceStatementDigest"] && item.tariff["@id"] === agreement.json["chargingTariffId"]);
|
|
9626
|
+
if (eligible.length !== 1) return { ...fail("valid", eligible.length === 0 ? "missing" : "ambiguous", eligible.length === 0 ? "No selected tariff/price is active at charging start" : "Competing price/tariff candidates need a signed selection"), agreement: agreement === void 0 ? "missing" : "invalid", chainGaps };
|
|
9627
|
+
const selected = eligible[0];
|
|
9628
|
+
if (selected === void 0) throw new POIError("Missing selected price");
|
|
9629
|
+
const currency = requireString(selected.tariff, "currency"), elements = requireArray(selected.price["elements"], "Signed price elements").map((item) => requireObject(item, "A signed price element")), rates = grossRates(currency, elements);
|
|
9630
|
+
let applicability = agreement === void 0 ? "unbound" : "bound";
|
|
9631
|
+
if (agreement !== void 0 && (agreement.json["chargingMode"] !== "adhoc" || agreement.json["priceBinding"] !== "fixed-at-start" || agreement.json["pricingProfile"] !== SIMPLE_GROSS_PRICE_PROFILE || rates === void 0)) {
|
|
9632
|
+
applicability = "unsupported";
|
|
9633
|
+
diagnostics.push("EMP/contract terms, variable binding, unknown price profile or conditional elements are not established by this profile");
|
|
9634
|
+
}
|
|
9635
|
+
let publication = "missing";
|
|
9636
|
+
const checkpointIds = [], witnessed = /* @__PURE__ */ new Set();
|
|
9637
|
+
if (evidence.priceProofs.length > 0 || evidence.checkpoints.length > 0) {
|
|
9638
|
+
try {
|
|
9639
|
+
const entries = evidence.checkpoints.map((entry) => ({ ...entry, checked: verifyAuthorizedCheckpoint(current, Checkpoint.parse(entry.checkpoint), entry.operator, policy) })), byId3 = new Map(entries.map((entry) => [entry.checked.checkpoint.id, entry.checked])), heads = /* @__PURE__ */ new Map();
|
|
9640
|
+
for (const checked of [...entries.map((entry) => entry.checked), ...(policy.relatedCheckpoints ?? []).map((entry) => verifyAuthorizedCheckpoint(current, Checkpoint.parse(entry.checkpoint), entry.operator, policy))]) {
|
|
9641
|
+
const cp = checked.checkpoint, coordinate2 = `${cp.epoch}:${cp.sequence}`;
|
|
9642
|
+
if (heads.has(coordinate2) && heads.get(coordinate2) !== cp.id) throw new POIError("Conflicting station/price checkpoint heads");
|
|
9643
|
+
heads.set(coordinate2, cp.id);
|
|
9644
|
+
}
|
|
9645
|
+
let selectedProof = false;
|
|
9646
|
+
for (const raw of evidence.priceProofs) {
|
|
9647
|
+
const p = PriceStatementProof.parse(raw), checked = byId3.get(p.checkpointId);
|
|
9648
|
+
if (checked === void 0) throw new POIError("Missing price checkpoint");
|
|
9649
|
+
p.priceStatement.verify(station.roamingNetwork.id, asText(station.operator["@id"]), station.operatorSigningKeys);
|
|
9650
|
+
if (!statements.some((statement) => statement.digestHex() === p.priceStatement.digestHex())) throw new POIError("Price proof belongs to an unsupplied statement");
|
|
9651
|
+
p.verify(checked.checkpoint);
|
|
9652
|
+
if (p.priceStatement.digestHex() !== selected.digest) continue;
|
|
9653
|
+
selectedProof = true;
|
|
9654
|
+
checkpointIds.push(checked.checkpoint.id);
|
|
9655
|
+
for (const name of checked.witnessedBy) witnessed.add(name);
|
|
9656
|
+
if (p.priceStatement.after !== void 0) {
|
|
9657
|
+
const after = byId3.get(p.priceStatement.after)?.checkpoint;
|
|
9658
|
+
if (after === void 0) {
|
|
9659
|
+
publication = "incomplete";
|
|
9660
|
+
diagnostics.push("Missing checkpoint named by after-link");
|
|
9661
|
+
continue;
|
|
9662
|
+
}
|
|
9663
|
+
if (after.epoch !== checked.checkpoint.epoch || after.sequence >= checked.checkpoint.sequence) throw new POIError("Invalid checkpoint temporal order");
|
|
9664
|
+
verifyCheckpointLine(entries.map((entry) => entry.checked.checkpoint).filter((cp) => cp.epoch === after.epoch && cp.sequence >= after.sequence && cp.sequence <= checked.checkpoint.sequence).sort((a, b) => a.sequence - b.sequence));
|
|
9665
|
+
}
|
|
9666
|
+
}
|
|
9667
|
+
if (publication !== "incomplete") publication = selectedProof ? "valid" : "missing";
|
|
9668
|
+
} catch (error) {
|
|
9669
|
+
publication = "invalid";
|
|
9670
|
+
diagnostics.push(error instanceof Error ? error.message : String(error));
|
|
9671
|
+
}
|
|
9672
|
+
}
|
|
9673
|
+
const operator = proof.roamingNetwork["operator"];
|
|
9674
|
+
const operatorJSON = isObject(operator) ? operator : {};
|
|
9675
|
+
const authorities = [operatorJSON, ...evidence.checkpoints.map((entry) => isObject(entry.operator) ? entry.operator : {})];
|
|
9676
|
+
const partyKeys = [...station.operatorSigningKeys, ...POIPublicKey.parseAll(station.operator["identityKeys"] ?? [], "CPO identity keys")];
|
|
9677
|
+
for (const authority of authorities) {
|
|
9678
|
+
partyKeys.push(...POIPublicKey.parseAll(authority["identityKeys"] ?? [], "RNO identity keys"));
|
|
9679
|
+
const certificates = Array.isArray(authority["signingKeyCertificates"]) ? authority["signingKeyCertificates"] : [];
|
|
9680
|
+
for (const certificate of certificates) if (isObject(certificate) && certificate["signingKey"] !== void 0) partyKeys.push(POIPublicKey.parse(certificate["signingKey"]));
|
|
9681
|
+
}
|
|
9682
|
+
const publicationWitnesses = verifyPricePublication(evidence.publicationReceipts, policy.publicationPolicy, {
|
|
9683
|
+
roamingNetworkId: station.roamingNetwork.id,
|
|
9684
|
+
evseId: request.evseId,
|
|
9685
|
+
statement: selected.statement,
|
|
9686
|
+
tariff: selected.tariff,
|
|
9687
|
+
chargingStarted: request.chargingStarted,
|
|
9688
|
+
now: current.evaluatedAt,
|
|
9689
|
+
checkpoints: publication === "valid" ? evidence.checkpoints.map((entry) => Checkpoint.parse(entry.checkpoint)).filter((cp) => checkpointIds.includes(cp.id)) : [],
|
|
9690
|
+
excludedKeyMaterial: partyKeys.map((key2) => toHex(key2.key.thumbprint())),
|
|
9691
|
+
excludedIdentities: [asText(station.operator["@id"]), ...authorities.map((authority) => asText(authority["@id"]))]
|
|
9692
|
+
});
|
|
9693
|
+
if (policy.requirePublication === true && publication !== "valid") diagnostics.push("Required price publication evidence is incomplete");
|
|
9694
|
+
return {
|
|
9695
|
+
...base,
|
|
9696
|
+
publicationWitnesses,
|
|
9697
|
+
publicationBeforeStart: publicationWitnesses.status,
|
|
9698
|
+
signatures: "valid",
|
|
9699
|
+
applicability,
|
|
9700
|
+
agreement: agreement === void 0 ? "missing" : "valid",
|
|
9701
|
+
publication,
|
|
9702
|
+
chainGaps,
|
|
9703
|
+
diagnostics,
|
|
9704
|
+
checkpointIds,
|
|
9705
|
+
witnessedBy: [...witnessed],
|
|
9706
|
+
selected: { statement: selected.statement.json, digest: selected.digest, tariff: selected.tariff, currency, elements, ...rates === void 0 || agreement?.json["pricingProfile"] !== SIMPLE_GROSS_PRICE_PROFILE ? {} : { rates } }
|
|
9707
|
+
};
|
|
9708
|
+
}
|
|
9709
|
+
|
|
9710
|
+
// src/verifier/observedVersions.ts
|
|
9711
|
+
function emptyObservedVersions() {
|
|
9712
|
+
return { version: 1, points: [], heads: {}, conflicts: [] };
|
|
9713
|
+
}
|
|
9714
|
+
function observedVersionsChecksum(value) {
|
|
9715
|
+
return toHex(hash("sha256", canonicalJSONBytes(value)));
|
|
9716
|
+
}
|
|
9717
|
+
function key(point) {
|
|
9718
|
+
return JSON.stringify([point.kind, point.chain]);
|
|
9719
|
+
}
|
|
9720
|
+
function coordinate(point) {
|
|
9721
|
+
return JSON.stringify([point.kind, point.chain, point.epoch, point.position]);
|
|
9722
|
+
}
|
|
9723
|
+
function digest(value) {
|
|
9724
|
+
return /^(?:[0-9a-f]{64}|[0-9a-f]{96}|[0-9a-f]{128})$/u.test(value);
|
|
9725
|
+
}
|
|
9726
|
+
function isSchema(value) {
|
|
9727
|
+
return value === 1;
|
|
9728
|
+
}
|
|
9729
|
+
function valid(point) {
|
|
9730
|
+
if (!["station", "checkpoint", "price", "live", "policy"].includes(point.kind) || point.chain.length === 0 || point.chain.length > 2048 || point.epoch.length > 128 || !Number.isSafeInteger(point.position) || point.position < 0 || !digest(point.digest) || point.previous !== void 0 && !digest(point.previous) || point.identity !== void 0 && !digest(point.identity) || point.revision !== void 0 && (!Number.isSafeInteger(point.revision) || point.revision < 0) || typeof point.original !== "string" || point.original.length === 0 || point.original.length > 4 * 1024 * 1024)
|
|
9731
|
+
throw new POIError("Invalid observed version");
|
|
9732
|
+
}
|
|
9733
|
+
function compareObservedVersions(before, candidates, mode, observedAt) {
|
|
9734
|
+
if (!isSchema(before.version) || !Number.isFinite(observedAt) || before.points.length > 4096 || before.conflicts.length > 128) throw new POIError("Invalid observed history");
|
|
9735
|
+
for (const point of [...before.points, ...candidates]) valid(point);
|
|
9736
|
+
const points = new Map(before.points.map((point) => [coordinate(point), point])), conflicts = [...before.conflicts], issues = [];
|
|
9737
|
+
const heads = { ...before.heads };
|
|
9738
|
+
const issue = (point, code, detail) => {
|
|
9739
|
+
issues.push({ code, kind: point.kind, chain: point.chain, detail });
|
|
9740
|
+
};
|
|
9741
|
+
const conflict = (point, known, code, detail) => {
|
|
9742
|
+
issue(point, code, detail);
|
|
9743
|
+
if (!conflicts.some((item) => item.code === code && item.kind === point.kind && item.chain === point.chain && item.epoch === point.epoch && item.known === known.digest && item.incoming === point.digest))
|
|
9744
|
+
conflicts.push({ code, kind: point.kind, chain: point.chain, epoch: point.epoch, position: point.position, known: known.digest, incoming: point.digest, original: point.original, observedAt });
|
|
9745
|
+
};
|
|
9746
|
+
for (const point of candidates) {
|
|
9747
|
+
const existing = points.get(coordinate(point));
|
|
9748
|
+
if (existing !== void 0 && existing.digest !== point.digest) {
|
|
9749
|
+
conflict(point, existing, "fork", "Two authenticated digests at the same coordinate");
|
|
9750
|
+
continue;
|
|
9751
|
+
}
|
|
9752
|
+
const identity = [...points.values()].find((item) => key(item) === key(point) && item.identity !== void 0 && point.identity !== void 0 && item.identity !== point.identity);
|
|
9753
|
+
if (identity !== void 0) {
|
|
9754
|
+
conflict(point, identity, "identity", "The identity or pinned document keys of an observed chain changed");
|
|
9755
|
+
continue;
|
|
9756
|
+
}
|
|
9757
|
+
if (point.kind === "live" && point.previous !== void 0) {
|
|
9758
|
+
const sibling = [...points.values()].find((item) => key(item) === key(point) && item.epoch === point.epoch && item.previous === point.previous && item.digest !== point.digest);
|
|
9759
|
+
if (sibling !== void 0) {
|
|
9760
|
+
conflict(point, sibling, "link", "Two authenticated live updates name the same predecessor");
|
|
9761
|
+
continue;
|
|
9762
|
+
}
|
|
9763
|
+
}
|
|
9764
|
+
if (existing === void 0) points.set(coordinate(point), point);
|
|
9765
|
+
}
|
|
9766
|
+
const groups = /* @__PURE__ */ new Map();
|
|
9767
|
+
for (const point of points.values()) {
|
|
9768
|
+
const group = JSON.stringify([point.kind, point.chain, point.epoch]);
|
|
9769
|
+
groups.set(group, [...groups.get(group) ?? [], point]);
|
|
9770
|
+
}
|
|
9771
|
+
for (const group of groups.values()) {
|
|
9772
|
+
if (!candidates.some((candidate) => group.some((point) => key(point) === key(candidate)))) continue;
|
|
9773
|
+
group.sort((a, b) => a.position - b.position);
|
|
9774
|
+
for (let i = 1; i < group.length; i++) {
|
|
9775
|
+
const point = group[i], previous = group[i - 1];
|
|
9776
|
+
if (point === void 0 || previous === void 0 || point.kind === "station") continue;
|
|
9777
|
+
if ((point.kind === "price" || point.kind === "checkpoint") && point.position === previous.position + 1 && point.previous !== previous.digest)
|
|
9778
|
+
conflict(point, previous, "link", "Authenticated adjacent versions contradict their predecessor link");
|
|
9779
|
+
if (point.kind === "checkpoint" && point.previous === previous.digest && point.revision !== void 0 && previous.revision !== void 0 && (point.revision < previous.revision || point.revision === previous.revision && !(point.final === true && point.rootDigest === previous.rootDigest))) conflict(point, previous, "link", "Checkpoint revision does not increase");
|
|
9780
|
+
}
|
|
9781
|
+
}
|
|
9782
|
+
const selected = /* @__PURE__ */ new Map();
|
|
9783
|
+
for (const point of candidates) {
|
|
9784
|
+
const name = key(point), old = selected.get(name);
|
|
9785
|
+
if (old === void 0 || old.epoch === point.epoch && point.position > old.position) selected.set(name, point);
|
|
9786
|
+
}
|
|
9787
|
+
for (const point of selected.values()) {
|
|
9788
|
+
if (conflicts.some((item) => item.kind === point.kind && item.chain === point.chain)) issue(point, "known-fork", "This chain has an unresolved authenticated contradiction");
|
|
9789
|
+
if (mode === "historical") continue;
|
|
9790
|
+
if (new Set(candidates.filter((candidate) => key(candidate) === key(point)).map((candidate) => candidate.epoch)).size > 1) {
|
|
9791
|
+
issue(point, "epoch-transition", "A current selection cannot combine epochs without a verified transition");
|
|
9792
|
+
continue;
|
|
9793
|
+
}
|
|
9794
|
+
const head = before.heads[key(point)];
|
|
9795
|
+
const closed = before.points.find((item) => item.kind === "checkpoint" && item.epoch === point.epoch && item.final === true);
|
|
9796
|
+
if (point.kind === "checkpoint" && closed !== void 0 && point.position > closed.position) {
|
|
9797
|
+
issue(point, "epoch-transition", "This epoch was closed by its final checkpoint");
|
|
9798
|
+
continue;
|
|
9799
|
+
}
|
|
9800
|
+
if (head !== void 0 && head.epoch !== point.epoch) {
|
|
9801
|
+
issue(point, "epoch-transition", "An epoch change requires an explicitly verified transition");
|
|
9802
|
+
continue;
|
|
9803
|
+
}
|
|
9804
|
+
const prior = before.points.filter((item) => key(item) === key(point) && item.epoch === point.epoch).sort((a, b) => b.position - a.position)[0];
|
|
9805
|
+
if (prior !== void 0 && point.position < prior.position) {
|
|
9806
|
+
issue(point, "rollback", `Version ${point.position} is older than locally observed ${prior.position}`);
|
|
9807
|
+
continue;
|
|
9808
|
+
}
|
|
9809
|
+
const anchor = head === void 0 ? prior : before.points.find((item) => key(item) === key(point) && item.epoch === head.epoch && item.digest === head.digest);
|
|
9810
|
+
if (anchor !== void 0 && point.position > anchor.position && point.kind !== "station") {
|
|
9811
|
+
let cursor = point;
|
|
9812
|
+
const visited = /* @__PURE__ */ new Set();
|
|
9813
|
+
while (cursor.digest !== anchor.digest && !visited.has(cursor.digest)) {
|
|
9814
|
+
visited.add(cursor.digest);
|
|
9815
|
+
const parent = [...points.values()].find((item) => key(item) === key(point) && item.epoch === point.epoch && item.digest === cursor?.previous);
|
|
9816
|
+
if (parent === void 0 || parent.position >= cursor.position || point.kind !== "live" && parent.position + 1 !== cursor.position) {
|
|
9817
|
+
cursor = void 0;
|
|
9818
|
+
break;
|
|
9819
|
+
}
|
|
9820
|
+
cursor = parent;
|
|
9821
|
+
}
|
|
9822
|
+
if (cursor?.digest !== anchor.digest) issue(point, "continuity-missing", "Missing authenticated predecessor path to the locally observed head");
|
|
9823
|
+
}
|
|
9824
|
+
}
|
|
9825
|
+
const accepted = issues.length === 0;
|
|
9826
|
+
if (accepted && mode === "current") for (const point of selected.values()) heads[key(point)] = { epoch: point.epoch, position: point.position, digest: point.digest };
|
|
9827
|
+
if (points.size > 4096 || conflicts.length > 128) throw new POIError("Observed history capacity reached; reset requires an explicit local decision");
|
|
9828
|
+
const state = { ...before, version: 1, points: [...points.values()], heads, conflicts };
|
|
9829
|
+
const advanced = candidates.some((point) => before.points.some((old) => key(old) === key(point) && old.epoch === point.epoch && old.position < point.position));
|
|
9830
|
+
return { accepted, status: !accepted ? "rejected" : mode === "historical" ? "historical" : !before.points.some((old) => candidates.some((point) => key(point) === key(old) && point.epoch === old.epoch)) ? "first-seen" : advanced ? "advanced" : "matches", issues, state };
|
|
9831
|
+
}
|
|
9832
|
+
function stationVersionObservation(proof, epoch = "") {
|
|
9833
|
+
return { kind: "station", chain: "network", epoch, position: proof.revision, digest: proof.commitId, original: prettyJSON(proof.json) };
|
|
9834
|
+
}
|
|
9835
|
+
function checkpointVersionObservation(checkpoint) {
|
|
9836
|
+
return {
|
|
9837
|
+
kind: "checkpoint",
|
|
9838
|
+
chain: "network",
|
|
9839
|
+
epoch: String(checkpoint.epoch),
|
|
9840
|
+
position: checkpoint.sequence,
|
|
9841
|
+
revision: checkpoint.revision,
|
|
9842
|
+
digest: checkpoint.id,
|
|
9843
|
+
final: checkpoint.isFinal,
|
|
9844
|
+
rootDigest: checkpoint.commitId,
|
|
9845
|
+
...checkpoint.previousId === void 0 ? {} : { previous: checkpoint.previousId },
|
|
9846
|
+
original: prettyJSON(checkpoint.json)
|
|
9847
|
+
};
|
|
9848
|
+
}
|
|
9849
|
+
function priceVersionObservation(statement) {
|
|
9850
|
+
return {
|
|
9851
|
+
kind: "price",
|
|
9852
|
+
chain: statement.chain,
|
|
9853
|
+
epoch: "",
|
|
9854
|
+
position: statement.sequence,
|
|
9855
|
+
digest: statement.digestHex(),
|
|
9856
|
+
...statement.previous === void 0 ? {} : { previous: statement.previous },
|
|
9857
|
+
original: prettyJSON(statement.json)
|
|
9858
|
+
};
|
|
9859
|
+
}
|
|
9860
|
+
function checkpointRootVersionObservation(checkpoint) {
|
|
9861
|
+
return { kind: "station", chain: "network", epoch: checkpoint.epoch === 1 ? "" : String(checkpoint.epoch), position: checkpoint.revision, digest: checkpoint.commitId, original: prettyJSON(checkpoint.json) };
|
|
9862
|
+
}
|
|
9863
|
+
|
|
9864
|
+
// src/verifier/documentKeys.ts
|
|
9865
|
+
var CHARGING_DOCUMENT_USAGE = "chargeTransparencyRecords";
|
|
9866
|
+
var ALGORITHMS = {
|
|
9867
|
+
1: "ECDSA-secp256r1",
|
|
9868
|
+
2: "ECDSA-secp384r1",
|
|
9869
|
+
3: "ECDSA-secp521r1",
|
|
9870
|
+
6: "EdDSA-Ed25519",
|
|
9871
|
+
7: "EdDSA-Ed448",
|
|
9872
|
+
8: "ECDSA-secp256k1"
|
|
9873
|
+
};
|
|
9874
|
+
function verifyChargingDocumentKeys(context, proof, request) {
|
|
9875
|
+
if (!Number.isFinite(request.at) || request.at > context.evaluatedAt)
|
|
9876
|
+
throw new POIError("Invalid charging-document evaluation time");
|
|
9877
|
+
const parsed = ChargingStationProof.parse(proof);
|
|
9878
|
+
const historical = historicalOperatorContext(context, parsed.roamingNetwork["operator"] ?? null, request.at);
|
|
9879
|
+
const station = verifyChargingStation(historical, parsed, { evseId: request.evseId });
|
|
9880
|
+
if (station.operator["@id"] !== request.operatorId)
|
|
9881
|
+
throw new POIError("The live document CPO is not the operator of the proven EVSE");
|
|
9882
|
+
const keys = station.operatorSigningKeys.filter((key2) => key2.isValidAt(request.at) && key2.maySign(CHARGING_DOCUMENT_USAGE)).map((key2) => {
|
|
9883
|
+
const algorithm = key2.key.curve === null ? key2.key.algorithm?.name : ALGORITHMS[key2.key.curve.id];
|
|
9884
|
+
if (algorithm === void 0)
|
|
9885
|
+
throw new POIError("Unsupported CPO charging-document key algorithm");
|
|
9886
|
+
return { id: key2.id, algorithm, encodings: ["COSE_Key", "CBOR", "base64"], value: requireString(key2.json, "key"), keyUsage: ["signCTRs"] };
|
|
9887
|
+
});
|
|
9888
|
+
if (keys.length === 0)
|
|
9889
|
+
throw new POIError("The proven CPO supplies no valid keys authorized for charging transparency records");
|
|
9890
|
+
return {
|
|
9891
|
+
evseId: request.evseId,
|
|
9892
|
+
chargingStationId: requireString(station.chargingStation, "@id"),
|
|
9893
|
+
operatorId: request.operatorId,
|
|
9894
|
+
at: request.at,
|
|
9895
|
+
commitId: station.commitId,
|
|
9896
|
+
revision: station.revision,
|
|
9897
|
+
keys
|
|
9898
|
+
};
|
|
9899
|
+
}
|
|
9900
|
+
|
|
9901
|
+
// src/verifier/trustTransitions.ts
|
|
9902
|
+
var TRUST_TRANSITION_CONTEXT = "https://open.charging.cloud/contexts/wwcp/trustTransition/v1";
|
|
9903
|
+
var trustStatementDigest = (statement) => toHex(hash("sha256", canonicalJSONBytes(statement.content())));
|
|
9904
|
+
function trustPolicyId(anchor) {
|
|
9905
|
+
return toHex(hash("sha256", canonicalJSONBytes({
|
|
9906
|
+
network: anchor.id,
|
|
9907
|
+
holders: anchor.holders.map((holder) => [...holder]),
|
|
9908
|
+
minConsensus: anchor.minConsensus,
|
|
9909
|
+
urls: anchor.urls.map((url) => new URL(url).href.replace(/\/+$/u, "")).sort()
|
|
9910
|
+
})));
|
|
9911
|
+
}
|
|
9912
|
+
var TrustTransition = class _TrustTransition {
|
|
9913
|
+
constructor(json, sequence, previous, signedAt, validUntil, fromPolicyId, fromStatementDigest, anchor, operator) {
|
|
9914
|
+
this.json = json;
|
|
9915
|
+
this.sequence = sequence;
|
|
9916
|
+
this.previous = previous;
|
|
9917
|
+
this.signedAt = signedAt;
|
|
9918
|
+
this.validUntil = validUntil;
|
|
9919
|
+
this.fromPolicyId = fromPolicyId;
|
|
9920
|
+
this.fromStatementDigest = fromStatementDigest;
|
|
9921
|
+
this.anchor = anchor;
|
|
9922
|
+
this.operator = operator;
|
|
9923
|
+
}
|
|
9924
|
+
json;
|
|
9925
|
+
sequence;
|
|
9926
|
+
previous;
|
|
9927
|
+
signedAt;
|
|
9928
|
+
validUntil;
|
|
9929
|
+
fromPolicyId;
|
|
9930
|
+
fromStatementDigest;
|
|
9931
|
+
anchor;
|
|
9932
|
+
operator;
|
|
9933
|
+
static parse(value) {
|
|
9934
|
+
const json = requireObject(value, "A trust transition");
|
|
9935
|
+
onlyProperties(json, [
|
|
9936
|
+
"@context",
|
|
9937
|
+
"sequence",
|
|
9938
|
+
"previous",
|
|
9939
|
+
"fromPolicyId",
|
|
9940
|
+
"fromStatementDigest",
|
|
9941
|
+
"toAnchor",
|
|
9942
|
+
"toOperator",
|
|
9943
|
+
"signedAt",
|
|
9944
|
+
"validUntil",
|
|
9945
|
+
"checkpoints",
|
|
9946
|
+
"epochChange",
|
|
9947
|
+
"signatures"
|
|
9948
|
+
], "A trust transition");
|
|
9949
|
+
if (json["@context"] !== TRUST_TRANSITION_CONTEXT || new TextEncoder().encode(prettyJSON(json)).length > 2 * 1024 * 1024)
|
|
9950
|
+
throw new POIError("Unsupported or excessive trust transition");
|
|
9951
|
+
const previous = get(json, "previous");
|
|
9952
|
+
const sequence = requireInteger(json, "sequence", 0, 31);
|
|
9953
|
+
if (previous !== void 0 !== sequence > 0 || previous !== void 0 && (typeof previous !== "string" || !/^[0-9a-f]{64}$/u.test(previous)))
|
|
9954
|
+
throw new POIError("Invalid predecessor of a trust transition");
|
|
9955
|
+
const signedAt = requireTimestamp(json, "signedAt");
|
|
9956
|
+
const validUntil = requireTimestamp(json, "validUntil");
|
|
9957
|
+
if (validUntil < signedAt)
|
|
9958
|
+
throw new POIError("Invalid transition validity");
|
|
9959
|
+
return new _TrustTransition(
|
|
9960
|
+
json,
|
|
9961
|
+
sequence,
|
|
9962
|
+
previous,
|
|
9963
|
+
signedAt,
|
|
9964
|
+
validUntil,
|
|
9965
|
+
requireString(json, "fromPolicyId"),
|
|
9966
|
+
requireString(json, "fromStatementDigest"),
|
|
9967
|
+
TrustAnchor.parse(requireString(json, "toAnchor")),
|
|
9968
|
+
requireObject(json["toOperator"], "The target operator")
|
|
9969
|
+
);
|
|
9970
|
+
}
|
|
9971
|
+
/** What it names, but its signatures. */
|
|
9972
|
+
content() {
|
|
9973
|
+
const { signatures: _, ...content } = this.json;
|
|
9974
|
+
return content;
|
|
9975
|
+
}
|
|
9976
|
+
/** The SHA-256 of the canonical JSON of its content: the `previous` of the next transition. */
|
|
9977
|
+
digest() {
|
|
9978
|
+
return toHex(hash("sha256", canonicalJSONBytes(this.content())));
|
|
9979
|
+
}
|
|
9980
|
+
/** The signature input of a signature: its content with the side, the algorithm and the key of the signature. */
|
|
9981
|
+
signingBytes(side, algorithm, keyId) {
|
|
9982
|
+
return canonicalJSONBytes({ ...this.content(), side, algorithm, keyId });
|
|
9983
|
+
}
|
|
9984
|
+
};
|
|
9985
|
+
var VerifiedTrustTransition = class _VerifiedTrustTransition {
|
|
9986
|
+
constructor(source, context, epoch, checkpoints, first, oldEpoch, signedBy) {
|
|
9987
|
+
this.source = source;
|
|
9988
|
+
this.context = context;
|
|
9989
|
+
this.epoch = epoch;
|
|
9990
|
+
this.checkpoints = checkpoints;
|
|
9991
|
+
this.first = first;
|
|
9992
|
+
this.oldEpoch = oldEpoch;
|
|
9993
|
+
this.signedBy = signedBy;
|
|
9994
|
+
}
|
|
9995
|
+
source;
|
|
9996
|
+
context;
|
|
9997
|
+
epoch;
|
|
9998
|
+
checkpoints;
|
|
9999
|
+
first;
|
|
10000
|
+
oldEpoch;
|
|
10001
|
+
signedBy;
|
|
10002
|
+
static verify(basis, fromOperator, transition, options) {
|
|
10003
|
+
if (!Number.isFinite(options.acceptedAt) || options.acceptedAt < transition.signedAt || options.acceptedAt > transition.validUntil || transition.sequence !== options.sequence || transition.previous !== options.previous)
|
|
10004
|
+
throw new POIError("Transition is expired, out of order or not accepted within its signed interval");
|
|
10005
|
+
if (transition.fromPolicyId !== trustPolicyId(basis.anchor) || transition.fromStatementDigest !== basis.keyStatementDigest || transition.anchor.id !== basis.roamingNetworkId || transition.operator["@id"] !== basis.roamingNetworkOperatorId)
|
|
10006
|
+
throw new POIError("Transition does not continue the exact authorized policy/network/operator");
|
|
10007
|
+
const oldOperator = requireObject(fromOperator, "Previous operator");
|
|
10008
|
+
const oldStatement = KeyStatement.parse(oldOperator["keyStatement"]);
|
|
10009
|
+
OperatorTrust.fromOperatorJSON(basis, oldOperator);
|
|
10010
|
+
const nextStatement = KeyStatement.parse(transition.operator["keyStatement"]);
|
|
10011
|
+
const identities = POIPublicKey.parseAll(transition.operator["identityKeys"], "Target identity keys");
|
|
10012
|
+
const next = TrustContext.create(transition.anchor, nextStatement, { trustedIdentityKeyIds: transition.anchor.keys, otherIdentityKeys: identities, at: transition.signedAt });
|
|
10013
|
+
OperatorTrust.fromOperatorJSON(next, transition.operator);
|
|
10014
|
+
if (next.roamingNetworkOperatorId !== basis.roamingNetworkOperatorId || next.requiredConsensus < basis.requiredConsensus)
|
|
10015
|
+
throw new POIError("A trust transition must not silently reduce the required holder quorum");
|
|
10016
|
+
for (const holder of basis.authorizedAnchor.holders)
|
|
10017
|
+
if (transition.anchor.holders.filter((group) => group.some((id) => holder.includes(id))).length > 1)
|
|
10018
|
+
throw new POIError("A transition splits a previously known holder");
|
|
10019
|
+
const oldPool = [...oldStatement.identityKeys, ...POIPublicKey.parseAll(oldOperator["identityKeys"], "Previous identity keys")];
|
|
10020
|
+
const nextPool = [...nextStatement.identityKeys, ...identities];
|
|
10021
|
+
const old = /* @__PURE__ */ new Set();
|
|
10022
|
+
const target = /* @__PURE__ */ new Set();
|
|
10023
|
+
for (const raw of requireArray(transition.json["signatures"], "Transition signatures")) {
|
|
10024
|
+
const signature = requireObject(raw, "A transition signature");
|
|
10025
|
+
onlyProperties(signature, ["side", "algorithm", "keyId", "value"], "A transition signature");
|
|
10026
|
+
const side = requireString(signature, "side");
|
|
10027
|
+
const algorithm = requireString(signature, "algorithm");
|
|
10028
|
+
const id = requireString(signature, "keyId");
|
|
10029
|
+
if (side !== "old" && side !== "new")
|
|
10030
|
+
throw new POIError("Invalid transition signature side");
|
|
10031
|
+
const groups = side === "old" ? basis.trustedHolders : next.trustedHolders;
|
|
10032
|
+
const pool = side === "old" ? oldPool : nextPool;
|
|
10033
|
+
const holder = groups.findIndex((group) => group.includes(id));
|
|
10034
|
+
const key2 = pool.find((key3) => key3.id === id);
|
|
10035
|
+
if (key2?.isValidAt(transition.signedAt) !== true)
|
|
10036
|
+
throw new POIError("Transition signer is not an authorized valid identity key");
|
|
10037
|
+
verifySignature(algorithm, key2.key, transition.signingBytes(side, algorithm, id), requireString(signature, "value"));
|
|
10038
|
+
if (holder >= 0)
|
|
10039
|
+
(side === "old" ? old : target).add(holder);
|
|
10040
|
+
}
|
|
10041
|
+
if (old.size < basis.requiredConsensus || target.size < next.requiredConsensus)
|
|
10042
|
+
throw new POIError("Both old and new holder quorums must sign the trust transition");
|
|
10043
|
+
const checkpoints = (transition.json["checkpoints"] === void 0 ? [] : requireArray(transition.json["checkpoints"], "Transition checkpoints")).map((raw) => {
|
|
10044
|
+
const checkpoint = Checkpoint.parse(raw);
|
|
10045
|
+
verifyAuthorizedCheckpoint(historicalOperatorContext(basis, oldOperator, checkpoint.createdAt), checkpoint, oldOperator);
|
|
10046
|
+
if (checkpoint.epoch !== options.epoch.number)
|
|
10047
|
+
throw new POIError("The transition checkpoint line belongs to another epoch");
|
|
10048
|
+
return checkpoint;
|
|
10049
|
+
});
|
|
10050
|
+
let epoch = options.epoch;
|
|
10051
|
+
let first;
|
|
10052
|
+
if (transition.json["epochChange"] !== void 0) {
|
|
10053
|
+
const change = requireObject(transition.json["epochChange"], "An epoch change");
|
|
10054
|
+
onlyProperties(change, ["genesis", "firstCheckpoint"], "An epoch change");
|
|
10055
|
+
const final = checkpoints.at(-1);
|
|
10056
|
+
if (final?.next === void 0)
|
|
10057
|
+
throw new POIError("An epoch transition needs the signed final checkpoint");
|
|
10058
|
+
if (final.createdAt > transition.signedAt)
|
|
10059
|
+
throw new POIError("The final checkpoint postdates the transition");
|
|
10060
|
+
const genesis = Commit.parse(change["genesis"]);
|
|
10061
|
+
const expected = Commit.genesisOf(basis.roamingNetworkId, final.json["eTags"] ?? null, final.next);
|
|
10062
|
+
first = Checkpoint.parse(change["firstCheckpoint"]);
|
|
10063
|
+
if (genesis.id !== expected.id || !first.isOf(expected) || first.sequence !== 0 || first.epoch !== final.next.number || first.previous?.checkpointId !== final.id || first.previous.epoch !== final.epoch || !sameETags(first.previous.eTags, final.eTags))
|
|
10064
|
+
throw new POIError("The next epoch must start from the exact deterministic genesis and final checkpoint");
|
|
10065
|
+
if ((trustPolicyId(next.anchor) !== trustPolicyId(basis.anchor) || next.keyStatementDigest !== basis.keyStatementDigest) && first.createdAt !== transition.signedAt)
|
|
10066
|
+
throw new POIError("A combined policy/epoch handoff must activate at its first checkpoint time");
|
|
10067
|
+
if (first.createdAt < final.createdAt || first.createdAt > transition.signedAt)
|
|
10068
|
+
throw new POIError("Invalid epoch-transition checkpoint times");
|
|
10069
|
+
const trust = OperatorTrust.fromOperatorJSON(historicalOperatorContext(next, transition.operator, first.createdAt), transition.operator);
|
|
10070
|
+
for (const profile of [options.epoch.profiles, final.next.profiles]) {
|
|
10071
|
+
const signatures = first.signatures.filter((signature) => signature.role === "roamingNetworkOperator" && profile.allowsSignatureAlgorithm(signature.algorithm));
|
|
10072
|
+
trust.verifyOperatorSignatures(signatures, (signature, key2) => first?.verifySignature(signature, key2), "epoch-transition checkpoint");
|
|
10073
|
+
}
|
|
10074
|
+
for (const signature of first.signatures)
|
|
10075
|
+
if (!options.epoch.profiles.allowsSignatureAlgorithm(signature.algorithm) && !final.next.profiles.allowsSignatureAlgorithm(signature.algorithm))
|
|
10076
|
+
throw new POIError("An epoch-transition signature uses neither accepted profile");
|
|
10077
|
+
epoch = { ...final.next, startedAt: first.createdAt };
|
|
10078
|
+
}
|
|
10079
|
+
return new _VerifiedTrustTransition(
|
|
10080
|
+
transition,
|
|
10081
|
+
TrustContext.withEpoch(next, epoch, [...basis.epochs.filter((item) => item.number !== epoch.number), epoch]),
|
|
10082
|
+
epoch,
|
|
10083
|
+
checkpoints,
|
|
10084
|
+
first,
|
|
10085
|
+
options.epoch,
|
|
10086
|
+
{ old: [...old], next: [...target] }
|
|
10087
|
+
);
|
|
10088
|
+
}
|
|
10089
|
+
};
|
|
10090
|
+
function applyVerifiedTrustTransition(before, verified, at) {
|
|
10091
|
+
const id = verified.source.digest();
|
|
10092
|
+
const known = before.transitions?.find((entry) => entry.id === id);
|
|
10093
|
+
const policy = compareObservedVersions(
|
|
10094
|
+
before,
|
|
10095
|
+
[{
|
|
10096
|
+
kind: "policy",
|
|
10097
|
+
chain: "trust",
|
|
10098
|
+
epoch: "",
|
|
10099
|
+
position: verified.source.sequence,
|
|
10100
|
+
digest: id,
|
|
10101
|
+
original: prettyJSON(verified.source.json),
|
|
10102
|
+
...verified.source.previous === void 0 ? {} : { previous: verified.source.previous }
|
|
10103
|
+
}],
|
|
10104
|
+
"historical",
|
|
10105
|
+
at
|
|
10106
|
+
);
|
|
10107
|
+
if (!policy.accepted)
|
|
10108
|
+
return policy;
|
|
10109
|
+
before = policy.state;
|
|
10110
|
+
if (known !== void 0)
|
|
10111
|
+
return { accepted: true, status: "matches", issues: [], state: before };
|
|
10112
|
+
if ((before.transitions?.length ?? 0) !== verified.source.sequence)
|
|
10113
|
+
throw new POIError("The observed journal has another transition predecessor");
|
|
10114
|
+
const oldPoints = verified.checkpoints.flatMap((checkpoint) => [checkpointVersionObservation(checkpoint), checkpointRootVersionObservation(checkpoint)]);
|
|
10115
|
+
const prior = compareObservedVersions(before, oldPoints, "current", at);
|
|
10116
|
+
if (!prior.accepted)
|
|
10117
|
+
return prior;
|
|
10118
|
+
let state = prior.state;
|
|
10119
|
+
if (verified.first !== void 0) {
|
|
10120
|
+
const points = [checkpointVersionObservation(verified.first), checkpointRootVersionObservation(verified.first)];
|
|
10121
|
+
const checked = compareObservedVersions(state, points, "historical", at);
|
|
10122
|
+
if (!checked.accepted)
|
|
10123
|
+
return checked;
|
|
10124
|
+
const heads = { ...checked.state.heads };
|
|
10125
|
+
for (const point of points)
|
|
10126
|
+
heads[JSON.stringify([point.kind, point.chain])] = { epoch: point.epoch, position: point.position, digest: point.digest };
|
|
10127
|
+
state = { ...checked.state, heads };
|
|
10128
|
+
}
|
|
10129
|
+
return {
|
|
10130
|
+
accepted: true,
|
|
10131
|
+
status: "advanced",
|
|
10132
|
+
issues: [],
|
|
10133
|
+
state: {
|
|
10134
|
+
...state,
|
|
10135
|
+
transitions: [
|
|
10136
|
+
...state.transitions ?? [],
|
|
10137
|
+
{
|
|
10138
|
+
id,
|
|
10139
|
+
sequence: verified.source.sequence,
|
|
10140
|
+
fromPolicyId: verified.source.fromPolicyId,
|
|
10141
|
+
toPolicyId: trustPolicyId(verified.source.anchor),
|
|
10142
|
+
fromEpoch: verified.oldEpoch.number,
|
|
10143
|
+
toEpoch: verified.epoch.number,
|
|
10144
|
+
original: prettyJSON(verified.source.json)
|
|
10145
|
+
}
|
|
10146
|
+
]
|
|
10147
|
+
}
|
|
10148
|
+
};
|
|
10149
|
+
}
|
|
10150
|
+
var initialTrustEpoch = () => ({ number: 1, profiles: EpochProfiles.FIRST });
|
|
9025
10151
|
|
|
9026
|
-
export { ATTESTATION_CONTEXT, ArchiveSegment, Attestation, CHANGE_SET_CONTEXT, CHECKPOINT_CONTEXT2 as CHECKPOINT_CONTEXT, CHILD_ARRAYS, COMMIT_CONTEXT2 as COMMIT_CONTEXT, CONTEXT_PREFIX, ChangeSet, ChargingStationProof, Checkpoint, CheckpointProof, Commit, DCCMeasurementResult, DCCResult, DCCStatement, DCC_PERFORMANCE_LOCATIONS, DCOC_MODULES, DDoCNotifiedBody, Decimal, DigitalCalibrationCertificate, DigitalCertificateOfConformity, DigitalDeclarationOfConformity, EPOCH_CONTEXT, EPOCH_LINK_CONTEXT, EPOCH_PROFILES_CONTEXT, EV_TALER_EXCHANGE_CONTEXT, EV_TALER_MERCHANT_CONTEXT, EV_TALER_PAYMENT_MEANS, EV_TALER_PAYMENT_OPTION, EpochProfiles, FIRST_EPOCH, History, KEY_STATEMENT_CONTEXT, KeyStatement, KeyUsages, LIVE_TOPICS, MAX_DECIMAL_SCALE, MAX_PRICE_STATEMENT_SEQUENCE, MERGE_CHANGE_SET_ID_PREFIX, OperatorTrust, POIClient, POIError, POIPublicKey, PROOF_CONTEXT, PriceStatement, PriceStatementProof, QIDigitalDocument, QIDigitalItem, QIDigitalLegislation, QIDigitalManufacturer, QIDigitalModel, QIDigitalPerson, QIDigitalQuantity, QIDigitalSignature, QIDigitalStandard, QI_DIGITAL_CONFORMITIES, ROAMING_NETWORK_CONTEXT, RUNTIME_STATUS_KINDS, RoamingNetwork, RoamingNetworkOperator, RuntimeStatus, SIGNATURE_ALGORITHMS, SIGNING_KEY_CERTIFICATE_CONTEXT, STATUS_UPDATE_CONTEXT, STYX_CANONICAL_JSON, SignatureRoles, SigningKeyCertificate, TRUST_ANCHOR_CONTEXT, TRUST_ANCHOR_QR_PREFIX, TrustAnchor, TrustContext, applyChangeSet, approvesQualitySystem, auditPath, canonicalJSON, canonicalJSONBytes, canonicalString, cborETag, cborToJson, changeSetIdentification, check, checkOwnership, clone, commitsRoot, decodeDeterministicCbor, describeVersion, deterministicCbor, effectiveChargingStationOperator, effectiveContent, effectiveTreeETag, entityDigest, etagToJSON, etagToText, evTalerExchangesAt, evTalerMerchantOf, evTalerTariffOf, fromBase45, fromBase64, fromHex, fromJavaScript, get, hash, isArray, isObject, isTalerPublicKey, jsonETag, jsonToCbor, leafHash, liveRequest, merkleRoot, merkleRootOfLeafHashes, nodeHash, parseArchiveSegmentInfo, parseETag, parseETags, parseEpoch, parseEpochLink, parseEvidence, parseFrame, parseJSON, parseJSONBytes, parseJSONObject, parsePriceStatements, parseStatusUpdate, parseTimestamp, parseTrustedWitnesses, paymentOptionName, prettyJSON, priceStatementChainOf, priceStatementsRoot, prunedTree, qiDigitalContext, rootFromAuditPath, sameETag, sameETags, sameEpoch, sameValue, skeleton, subscribeRequest, toBase45, toBase64, toHex, toTimestamp, verifyChargingStation, verifyCheckpointLine, verifyCheckpointPriceStatements, verifyCheckpointSignatures, verifyEvidence, verifyPriceStatementChain, verifyPriceStatements, verifySegmentLine, verifySignature, verifyVersion };
|
|
10152
|
+
export { ATTESTATION_CONTEXT, ArchiveSegment, Attestation, CHANGE_SET_CONTEXT, CHARGING_DOCUMENT_USAGE, CHECKPOINT_CONTEXT2 as CHECKPOINT_CONTEXT, CHILD_ARRAYS, COMMIT_CONTEXT2 as COMMIT_CONTEXT, CONTEXT_PREFIX, ChangeSet, ChargingStationProof, Checkpoint, CheckpointProof, Commit, DCCMeasurementResult, DCCResult, DCCStatement, DCC_PERFORMANCE_LOCATIONS, DCOC_MODULES, DDoCNotifiedBody, Decimal, DigitalCalibrationCertificate, DigitalCertificateOfConformity, DigitalDeclarationOfConformity, EPOCH_CONTEXT, EPOCH_LINK_CONTEXT, EPOCH_PROFILES_CONTEXT, EV_TALER_EXCHANGE_CONTEXT, EV_TALER_MERCHANT_CONTEXT, EV_TALER_PAYMENT_MEANS, EV_TALER_PAYMENT_OPTION, EpochProfiles, FIRST_EPOCH, HISTORICAL_METER_EVIDENCE_CONTEXT, HistoricalTrustContext, History, KEY_STATEMENT_CONTEXT, KeyStatement, KeyUsages, LIVE_TOPICS, MAX_DECIMAL_SCALE, MAX_PRICE_STATEMENT_SEQUENCE, MERGE_CHANGE_SET_ID_PREFIX, MeasurementKeyError, OperatorTrust, POIClient, POIError, POIPublicKey, PRICE_PUBLICATION_RECEIPT_CONTEXT, PROOF_CONTEXT, PricePublicationReceipt, PriceStatement, PriceStatementProof, QIDigitalDocument, QIDigitalItem, QIDigitalLegislation, QIDigitalManufacturer, QIDigitalModel, QIDigitalPerson, QIDigitalQuantity, QIDigitalSignature, QIDigitalStandard, QI_DIGITAL_CONFORMITIES, ROAMING_NETWORK_CONTEXT, RUNTIME_STATUS_KINDS, RoamingNetwork, RoamingNetworkOperator, RuntimeStatus, SESSION_PRICE_AGREEMENT_CONTEXT, SESSION_PRICE_EVIDENCE_CONTEXT, SIGNATURE_ALGORITHMS, SIGNING_KEY_CERTIFICATE_CONTEXT, SIMPLE_GROSS_PRICE_PROFILE, STATUS_UPDATE_CONTEXT, STYX_CANONICAL_JSON, SessionPriceAgreement, SignatureRoles, SigningKeyCertificate, TRUST_ANCHOR_CONTEXT, TRUST_ANCHOR_QR_PREFIX, TRUST_TRANSITION_CONTEXT, TrustAnchor, TrustContext, TrustTransition, VerifiedTrustTransition, WITNESS_PUBLICATION_POLICY_CONTEXT, applyChangeSet, applyVerifiedTrustTransition, approvesQualitySystem, auditPath, canonicalJSON, canonicalJSONBytes, canonicalString, cborETag, cborToJson, changeSetIdentification, check, checkOwnership, checkpointRootVersionObservation, checkpointVersionObservation, clone, commitsRoot, compareObservedVersions, decodeDeterministicCbor, describeVersion, deterministicCbor, effectiveChargingStationOperator, effectiveContent, effectiveTreeETag, emptyObservedVersions, entityDigest, etagToJSON, etagToText, evTalerExchangesAt, evTalerMerchantOf, evTalerTariffOf, fromBase45, fromBase64, fromHex, fromJavaScript, get, hash, historicalOperatorContext, initialTrustEpoch, isArray, isObject, isTalerPublicKey, jsonETag, jsonToCbor, leafHash, liveRequest, merkleRoot, merkleRootOfLeafHashes, nodeHash, observedVersionsChecksum, parseArchiveSegmentInfo, parseETag, parseETags, parseEpoch, parseEpochLink, parseEvidence, parseFrame, parseHistoricalMeterEvidence, parseJSON, parseJSONBytes, parseJSONObject, parseMeasurementPublicKey, parsePriceStatements, parseSessionPriceEvidence, parseStatusUpdate, parseTimestamp, parseTrustedWitnesses, parseWitnessPublicationPolicy, paymentOptionName, prettyJSON, pricePublicationOfferHashes, priceStatementChainOf, priceStatementsRoot, priceVersionObservation, prunedTree, publicPublicationURL, qiDigitalContext, rootFromAuditPath, sameETag, sameETags, sameEpoch, sameValue, skeleton, stationVersionObservation, subscribeRequest, toBase45, toBase64, toHex, toTimestamp, trustPolicyId, trustStatementDigest, verifyAuthorizedCheckpoint, verifyChargingDocumentKeys, verifyChargingStation, verifyCheckpointLine, verifyCheckpointPriceStatements, verifyCheckpointSignatures, verifyEvidence, verifyHistoricalMeterEvidence, verifyMeterKey, verifyPricePublication, verifyPriceStatementChain, verifyPriceStatements, verifySegmentLine, verifySessionPriceEvidence, verifySignature, verifyVersion };
|
|
9027
10153
|
//# sourceMappingURL=index.js.map
|
|
9028
10154
|
//# sourceMappingURL=index.js.map
|