@road-labs/ocmf 0.0.2 → 0.0.4
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/LICENSE +22 -0
- package/README.md +6 -0
- package/package.json +13 -3
- package/jest.config.js +0 -11
- package/src/errors.ts +0 -3
- package/src/index.ts +0 -8
- package/src/parse.ts +0 -42
- package/src/signature.ts +0 -72
- package/src/signer.ts +0 -50
- package/src/types.ts +0 -144
- package/src/verifier.ts +0 -83
- package/test/parse.spec.ts +0 -67
- package/test/signer.spec.ts +0 -81
- package/test/verifier.spec.ts +0 -111
- package/tsconfig.json +0 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
Copyright (c) 2025 Road B.V.
|
|
2
|
+
|
|
3
|
+
MIT License
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
|
6
|
+
a copy of this software and associated documentation files (the
|
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
|
11
|
+
the following conditions:
|
|
12
|
+
|
|
13
|
+
The above copyright notice and this permission notice shall be
|
|
14
|
+
included in all copies or substantial portions of the Software.
|
|
15
|
+
|
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
package/README.md
ADDED
package/package.json
CHANGED
|
@@ -1,10 +1,20 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@road-labs/ocmf",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.4",
|
|
4
4
|
"main": "build/cjs/index.js",
|
|
5
5
|
"module": "build/es2022/index.js",
|
|
6
6
|
"types": "build/types/index.d.ts",
|
|
7
|
-
"
|
|
7
|
+
"files": [
|
|
8
|
+
"build/**/*",
|
|
9
|
+
"README.md"
|
|
10
|
+
],
|
|
11
|
+
"keywords": [
|
|
12
|
+
"ocmf"
|
|
13
|
+
],
|
|
14
|
+
"bugs": {
|
|
15
|
+
"url": "https://github.com/road-labs/ocmf-js/issues"
|
|
16
|
+
},
|
|
17
|
+
"homepage": "https://github.com/road-labs/ocmf-js/tree/main/packages/ocmf#readme",
|
|
8
18
|
"author": "",
|
|
9
19
|
"license": "MIT",
|
|
10
20
|
"description": "",
|
|
@@ -20,7 +30,7 @@
|
|
|
20
30
|
"test-commons": "0.0.1"
|
|
21
31
|
},
|
|
22
32
|
"dependencies": {
|
|
23
|
-
"@road-labs/ocmf-crypto": "0.0.
|
|
33
|
+
"@road-labs/ocmf-crypto": "0.0.4"
|
|
24
34
|
},
|
|
25
35
|
"scripts": {
|
|
26
36
|
"clear": "rimraf build",
|
package/jest.config.js
DELETED
package/src/errors.ts
DELETED
package/src/index.ts
DELETED
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
export * from './types';
|
|
2
|
-
export * from './parse';
|
|
3
|
-
export * from './signature';
|
|
4
|
-
export * from './errors';
|
|
5
|
-
export * from './verifier';
|
|
6
|
-
export * from './signer';
|
|
7
|
-
export { default as Signer } from './signer';
|
|
8
|
-
export { default as Verifier } from './verifier';
|
package/src/parse.ts
DELETED
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
import { Header, SignedData } from './types';
|
|
2
|
-
|
|
3
|
-
interface Sections {
|
|
4
|
-
header: Header;
|
|
5
|
-
payloadData: string;
|
|
6
|
-
signature: string;
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
export function decomposeValue(value: string): Sections {
|
|
10
|
-
const parts = value.split('|');
|
|
11
|
-
if (parts.length !== 3) {
|
|
12
|
-
throw new Error('Signed data must be in OCMF|data|signature format');
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
const headerSection = parts[0];
|
|
16
|
-
const payloadDataSection = parts[1];
|
|
17
|
-
const signatureSection = parts[2];
|
|
18
|
-
|
|
19
|
-
if (headerSection !== 'OCMF') {
|
|
20
|
-
throw new Error('OCMF header required');
|
|
21
|
-
}
|
|
22
|
-
if (payloadDataSection.length === 0) {
|
|
23
|
-
throw new Error('Payload section cannot be empty');
|
|
24
|
-
}
|
|
25
|
-
if (signatureSection.length === 0) {
|
|
26
|
-
throw new Error('Signature section cannot be empty');
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
return {
|
|
30
|
-
header: headerSection,
|
|
31
|
-
payloadData: payloadDataSection,
|
|
32
|
-
signature: signatureSection,
|
|
33
|
-
};
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
export function parseSections(sections: Sections): SignedData {
|
|
37
|
-
return {
|
|
38
|
-
header: sections.header,
|
|
39
|
-
payloadData: JSON.parse(sections.payloadData),
|
|
40
|
-
signature: JSON.parse(sections.signature),
|
|
41
|
-
};
|
|
42
|
-
}
|
package/src/signature.ts
DELETED
|
@@ -1,72 +0,0 @@
|
|
|
1
|
-
import { Curve, Hash } from '@road-labs/ocmf-crypto';
|
|
2
|
-
|
|
3
|
-
export type SignatureMethodId =
|
|
4
|
-
| 'ECDSA-brainpool256r1-SHA256'
|
|
5
|
-
| 'ECDSA-brainpool384r1-SHA256'
|
|
6
|
-
| 'ECDSA-secp192k1-SHA256'
|
|
7
|
-
| 'ECDSA-secp192r1-SHA256'
|
|
8
|
-
| 'ECDSA-secp256k1-SHA256'
|
|
9
|
-
| 'ECDSA-secp256r1-SHA256'
|
|
10
|
-
| 'ECDSA-secp384r1-SHA256';
|
|
11
|
-
|
|
12
|
-
export type SignatureMethod = {
|
|
13
|
-
id: SignatureMethodId;
|
|
14
|
-
algorithm: string;
|
|
15
|
-
curve: Curve;
|
|
16
|
-
hash: Hash;
|
|
17
|
-
};
|
|
18
|
-
|
|
19
|
-
const signatureMethods: SignatureMethod[] = [
|
|
20
|
-
{
|
|
21
|
-
id: 'ECDSA-secp192r1-SHA256',
|
|
22
|
-
algorithm: 'ECDSA',
|
|
23
|
-
curve: 'secp192r1',
|
|
24
|
-
hash: 'SHA-256',
|
|
25
|
-
},
|
|
26
|
-
{
|
|
27
|
-
id: 'ECDSA-secp192k1-SHA256',
|
|
28
|
-
algorithm: 'ECDSA',
|
|
29
|
-
curve: 'secp192k1',
|
|
30
|
-
hash: 'SHA-256',
|
|
31
|
-
},
|
|
32
|
-
{
|
|
33
|
-
id: 'ECDSA-secp256r1-SHA256',
|
|
34
|
-
algorithm: 'ECDSA',
|
|
35
|
-
curve: 'secp256r1',
|
|
36
|
-
hash: 'SHA-256',
|
|
37
|
-
},
|
|
38
|
-
{
|
|
39
|
-
id: 'ECDSA-secp256k1-SHA256',
|
|
40
|
-
algorithm: 'ECDSA',
|
|
41
|
-
curve: 'secp256k1',
|
|
42
|
-
hash: 'SHA-256',
|
|
43
|
-
},
|
|
44
|
-
{
|
|
45
|
-
id: 'ECDSA-secp384r1-SHA256',
|
|
46
|
-
algorithm: 'ECDSA',
|
|
47
|
-
curve: 'secp384r1',
|
|
48
|
-
hash: 'SHA-256',
|
|
49
|
-
},
|
|
50
|
-
{
|
|
51
|
-
id: 'ECDSA-brainpool256r1-SHA256',
|
|
52
|
-
algorithm: 'ECDSA',
|
|
53
|
-
curve: 'brainpool256r1',
|
|
54
|
-
hash: 'SHA-256',
|
|
55
|
-
},
|
|
56
|
-
{
|
|
57
|
-
id: 'ECDSA-brainpool384r1-SHA256',
|
|
58
|
-
algorithm: 'ECDSA',
|
|
59
|
-
curve: 'brainpool384r1',
|
|
60
|
-
hash: 'SHA-256',
|
|
61
|
-
},
|
|
62
|
-
];
|
|
63
|
-
|
|
64
|
-
export function signatureMethodFromId(id: SignatureMethodId): SignatureMethod {
|
|
65
|
-
const signatureMethod = signatureMethods.find(
|
|
66
|
-
(signature) => signature.id === id
|
|
67
|
-
);
|
|
68
|
-
if (!signatureMethod) {
|
|
69
|
-
throw new Error(`Unsupported signature method: ${id}`);
|
|
70
|
-
}
|
|
71
|
-
return signatureMethod;
|
|
72
|
-
}
|
package/src/signer.ts
DELETED
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
import { signatureMethodFromId, SignatureMethodId } from './signature';
|
|
2
|
-
import { Header, PayloadData, Signature } from './types';
|
|
3
|
-
import { CryptoAdapter, EcPrivateKey } from '@road-labs/ocmf-crypto';
|
|
4
|
-
import { CurveMismatchError } from './errors';
|
|
5
|
-
|
|
6
|
-
export default class Signer {
|
|
7
|
-
constructor(private readonly crypto: CryptoAdapter) {}
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* @param payload - Meter data payload to be signed
|
|
11
|
-
* @param privateKey - Private key to use for signing
|
|
12
|
-
* @param signatureMethodId - Signing method. Note: the curve indicated by the id must match that of the private key
|
|
13
|
-
*/
|
|
14
|
-
public async sign(
|
|
15
|
-
payload: PayloadData,
|
|
16
|
-
privateKey: EcPrivateKey,
|
|
17
|
-
signatureMethodId: SignatureMethodId
|
|
18
|
-
): Promise<string> {
|
|
19
|
-
const signatureMethod = signatureMethodFromId(signatureMethodId);
|
|
20
|
-
if (signatureMethod.curve !== privateKey.getCurve()) {
|
|
21
|
-
throw new CurveMismatchError(
|
|
22
|
-
`Expected ${privateKey.getCurve()}, actual ${signatureMethod.curve}`
|
|
23
|
-
);
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
const payloadDataSegment = JSON.stringify(payload);
|
|
27
|
-
const textEncoder = new TextEncoder();
|
|
28
|
-
const payloadDataSegmentBytes = textEncoder.encode(payloadDataSegment);
|
|
29
|
-
|
|
30
|
-
const signature = await this.crypto.sign(
|
|
31
|
-
payloadDataSegmentBytes,
|
|
32
|
-
privateKey,
|
|
33
|
-
signatureMethod.hash,
|
|
34
|
-
'sigvalue-der'
|
|
35
|
-
);
|
|
36
|
-
|
|
37
|
-
const signatureSegment = JSON.stringify({
|
|
38
|
-
SA: signatureMethod.id,
|
|
39
|
-
SD: bytesToHex(signature).toUpperCase(),
|
|
40
|
-
} as Signature);
|
|
41
|
-
|
|
42
|
-
return [Header, payloadDataSegment, signatureSegment].join('|');
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
function bytesToHex(bytes: Uint8Array): string {
|
|
47
|
-
return Array.from(bytes)
|
|
48
|
-
.map((x) => x.toString(16).padStart(2, '0'))
|
|
49
|
-
.join('');
|
|
50
|
-
}
|
package/src/types.ts
DELETED
|
@@ -1,144 +0,0 @@
|
|
|
1
|
-
import { SignatureMethodId } from './signature';
|
|
2
|
-
|
|
3
|
-
export interface SignedData {
|
|
4
|
-
header: Header;
|
|
5
|
-
payloadData: PayloadData;
|
|
6
|
-
signature: Signature;
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
export type Header = 'OCMF';
|
|
10
|
-
export const Header: Header = 'OCMF';
|
|
11
|
-
|
|
12
|
-
type Iso15118UserAssignment = 'ISO15118_NONE' | 'ISO15118_PNC';
|
|
13
|
-
type PlmnUserAssignment = 'PLMN_NONE' | 'PLMN_RING' | 'PLMN_SMS';
|
|
14
|
-
type ChargingPointAssignment = 'EVSEID' | 'CBIDC';
|
|
15
|
-
|
|
16
|
-
type RfidUserAssignment =
|
|
17
|
-
| 'RFID_NONE'
|
|
18
|
-
| 'RFID_PLAIN'
|
|
19
|
-
| 'RFID_RELATED'
|
|
20
|
-
| 'RFID_PSK';
|
|
21
|
-
|
|
22
|
-
type OcppUserAssignment =
|
|
23
|
-
| 'OCPP_NONE'
|
|
24
|
-
| 'OCPP_RS'
|
|
25
|
-
| 'OCPP_AUTH'
|
|
26
|
-
| 'OCPP_RS_TLS'
|
|
27
|
-
| 'OCPP_AUTH_TLS'
|
|
28
|
-
| 'OCPP_CACHE'
|
|
29
|
-
| 'OCPP_WHITELIST'
|
|
30
|
-
| 'OCPP_CERTIFIED';
|
|
31
|
-
|
|
32
|
-
type IdentificationLevel =
|
|
33
|
-
| 'NONE'
|
|
34
|
-
| 'HEARSAY'
|
|
35
|
-
| 'TRUSTED'
|
|
36
|
-
| 'VERIFIED'
|
|
37
|
-
| 'CERTIFIED'
|
|
38
|
-
| 'SECURE'
|
|
39
|
-
| 'MISMATCH'
|
|
40
|
-
| 'INVALID'
|
|
41
|
-
| 'OUTDATED'
|
|
42
|
-
| 'UNKNOWN';
|
|
43
|
-
|
|
44
|
-
type IdentificationType =
|
|
45
|
-
| 'NONE'
|
|
46
|
-
| 'DENIED'
|
|
47
|
-
| 'UNDEFINED'
|
|
48
|
-
| 'ISO14443'
|
|
49
|
-
| 'ISO15693'
|
|
50
|
-
| 'EMAID'
|
|
51
|
-
| 'EVCCID'
|
|
52
|
-
| 'EVCOID'
|
|
53
|
-
| 'ISO7812'
|
|
54
|
-
| 'CARD_TXN_NR'
|
|
55
|
-
| 'CENTRAL'
|
|
56
|
-
| 'CENTRAL_1'
|
|
57
|
-
| 'CENTRAL_2'
|
|
58
|
-
| 'LOCAL'
|
|
59
|
-
| 'LOCAL_1'
|
|
60
|
-
| 'LOCAL_2'
|
|
61
|
-
| 'PHONE_NUMBER'
|
|
62
|
-
| 'KEY_CODE';
|
|
63
|
-
|
|
64
|
-
type MeterReadingReason =
|
|
65
|
-
| 'B' // Begin of transaction
|
|
66
|
-
| 'C' // Charging
|
|
67
|
-
| 'X' // Exception
|
|
68
|
-
| 'E' // End of transaction
|
|
69
|
-
| 'L' // End of transaction, terminated locally
|
|
70
|
-
| 'R' // End of transaction, terminated remotely
|
|
71
|
-
| 'A' // End of transaction, due to abort
|
|
72
|
-
| 'P' // End of transaction, due to power failure
|
|
73
|
-
| 'S' // Suspended
|
|
74
|
-
| 'T'; // Tariff change
|
|
75
|
-
|
|
76
|
-
type MeterStatus =
|
|
77
|
-
| 'N' // NOT_PRESENT
|
|
78
|
-
| 'G' // OK
|
|
79
|
-
| 'T' // TIMEOUT
|
|
80
|
-
| 'D' // DISCONNECTED
|
|
81
|
-
| 'R' // NOT_FOUND
|
|
82
|
-
| 'M' // MANIPULATED
|
|
83
|
-
| 'X' // EXCHANGED
|
|
84
|
-
| 'I' // INCOMPATIBLE
|
|
85
|
-
| 'O' // OUT_OF_RANGE
|
|
86
|
-
| 'S' // SUBSTITUTE
|
|
87
|
-
| 'E' // OTHER_ERROR
|
|
88
|
-
| 'F'; // READ_ERROR
|
|
89
|
-
|
|
90
|
-
// This should be more restrictive if we can find a way
|
|
91
|
-
type UserAssignment =
|
|
92
|
-
| RfidUserAssignment
|
|
93
|
-
| OcppUserAssignment
|
|
94
|
-
| Iso15118UserAssignment
|
|
95
|
-
| PlmnUserAssignment;
|
|
96
|
-
|
|
97
|
-
type UnitType = 'kWh' | 'Wh' | 'mOhm' | 'uOhm';
|
|
98
|
-
|
|
99
|
-
export interface PayloadData {
|
|
100
|
-
FV?: string; // Format Version
|
|
101
|
-
GI?: string; // Gateway Identification
|
|
102
|
-
GS?: string; // Gateway Serial
|
|
103
|
-
GV?: string; // Gateway Version
|
|
104
|
-
PG: string; // Pagination
|
|
105
|
-
MV?: string; // Meter Vendor
|
|
106
|
-
MM?: string; // Meter Model
|
|
107
|
-
MS: string; // Meter Serial
|
|
108
|
-
MF?: string; // Meter Firmware
|
|
109
|
-
IS: boolean; // Identification Status
|
|
110
|
-
IL?: IdentificationLevel; // Identification Level
|
|
111
|
-
IF?: UserAssignment[]; // Identification Flags
|
|
112
|
-
IT: IdentificationType; // Identification Type
|
|
113
|
-
ID?: string; // Identification Data
|
|
114
|
-
TT?: string; // Tariff Text
|
|
115
|
-
CF?: string; // Charge Controller Firmware Version
|
|
116
|
-
LC?: {
|
|
117
|
-
// Loss Compensation
|
|
118
|
-
LN?: string; // Naming
|
|
119
|
-
LI?: number; // Identification
|
|
120
|
-
LR: number; // Cable Resistance
|
|
121
|
-
LU: Extract<UnitType, 'mOhm' | 'uOhm'>; // Resistance Unit
|
|
122
|
-
};
|
|
123
|
-
CT?: ChargingPointAssignment; // Charge Point Identification Type
|
|
124
|
-
CI?: string; // Charge Point Identification
|
|
125
|
-
RD: {
|
|
126
|
-
// Readings
|
|
127
|
-
TM: string; // Time
|
|
128
|
-
TX?: MeterReadingReason; // Transaction
|
|
129
|
-
RV: number; // Reading Value
|
|
130
|
-
RI?: string; // Reading Identification
|
|
131
|
-
RU: UnitType; // Reading Unit
|
|
132
|
-
RT?: string; // Reading Current Type
|
|
133
|
-
CL?: number; // Cumulated Loss
|
|
134
|
-
EF?: string; // Error Flag
|
|
135
|
-
ST: MeterStatus; // Status
|
|
136
|
-
}[];
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
export interface Signature {
|
|
140
|
-
SA: SignatureMethodId;
|
|
141
|
-
SE?: 'hex';
|
|
142
|
-
SM?: 'application/x-der';
|
|
143
|
-
SD: string;
|
|
144
|
-
}
|
package/src/verifier.ts
DELETED
|
@@ -1,83 +0,0 @@
|
|
|
1
|
-
import { SignedData } from './types';
|
|
2
|
-
import { signatureMethodFromId } from './signature';
|
|
3
|
-
import { decomposeValue, parseSections } from './parse';
|
|
4
|
-
import { CryptoAdapter, EcPublicKey } from '@road-labs/ocmf-crypto';
|
|
5
|
-
import {
|
|
6
|
-
CurveMismatchError,
|
|
7
|
-
UnknownSignatureEncoding,
|
|
8
|
-
UnknownSignatureMimeType,
|
|
9
|
-
} from './errors';
|
|
10
|
-
|
|
11
|
-
export interface ParseAndVerifyResult {
|
|
12
|
-
verified: boolean;
|
|
13
|
-
value?: SignedData; // Only included if the payload was verified
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export default class Verifier {
|
|
17
|
-
constructor(private readonly crypto: CryptoAdapter) {}
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* @param rawSignedData - The full OCMF signed meter data payload as sent by the charging station
|
|
21
|
-
* @param publicKey - The public key for verifying the OCMF signed meter data payload
|
|
22
|
-
* @return Result of verification. Note: if the payload was not verified, the parsed value is not included.
|
|
23
|
-
*/
|
|
24
|
-
async parseAndVerify(
|
|
25
|
-
rawSignedData: string,
|
|
26
|
-
publicKey: EcPublicKey
|
|
27
|
-
): Promise<ParseAndVerifyResult> {
|
|
28
|
-
const sections = decomposeValue(rawSignedData);
|
|
29
|
-
const signedData = parseSections(sections);
|
|
30
|
-
const { signature } = signedData;
|
|
31
|
-
|
|
32
|
-
const signatureMethod = signatureMethodFromId(signature.SA);
|
|
33
|
-
if (signatureMethod.curve !== publicKey.getCurve()) {
|
|
34
|
-
throw new CurveMismatchError(
|
|
35
|
-
`Expected ${publicKey.getCurve()}, actual ${signatureMethod.curve}`
|
|
36
|
-
);
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
if (signature.SE && signature.SE !== 'hex') {
|
|
40
|
-
throw new UnknownSignatureEncoding(
|
|
41
|
-
'Only hex encoded signatures are supported'
|
|
42
|
-
);
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
if (signature.SM && signature.SM !== 'application/x-der') {
|
|
46
|
-
throw new UnknownSignatureMimeType(
|
|
47
|
-
'Only application/x-der encoded signatures are supported'
|
|
48
|
-
);
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
const textEncoder = new TextEncoder();
|
|
52
|
-
const payloadDataSegment = textEncoder.encode(sections.payloadData);
|
|
53
|
-
const signatureBytes = hexToBytes(signature.SD);
|
|
54
|
-
|
|
55
|
-
const verified = await this.crypto.verify(
|
|
56
|
-
signatureBytes,
|
|
57
|
-
payloadDataSegment,
|
|
58
|
-
publicKey,
|
|
59
|
-
signatureMethod.hash,
|
|
60
|
-
'sigvalue-der'
|
|
61
|
-
);
|
|
62
|
-
|
|
63
|
-
return {
|
|
64
|
-
verified,
|
|
65
|
-
value: verified ? signedData : undefined,
|
|
66
|
-
};
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
function hexToBytes(hex: string): Uint8Array {
|
|
71
|
-
if (
|
|
72
|
-
hex.length === 0 ||
|
|
73
|
-
hex.length % 2 !== 0 ||
|
|
74
|
-
!hex.match(/^[A-Za-f0-9]+$/)
|
|
75
|
-
) {
|
|
76
|
-
throw new Error('Invalid hex string');
|
|
77
|
-
}
|
|
78
|
-
const bytes = hex.match(/.{2}/g)?.map((byte) => parseInt(byte, 16));
|
|
79
|
-
if (!bytes) {
|
|
80
|
-
throw new Error('Failed to map hex string');
|
|
81
|
-
}
|
|
82
|
-
return new Uint8Array(bytes);
|
|
83
|
-
}
|
package/test/parse.spec.ts
DELETED
|
@@ -1,67 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from '@jest/globals';
|
|
2
|
-
import { decomposeValue, parseSections } from '../src';
|
|
3
|
-
|
|
4
|
-
describe('decomposeValue', () => {
|
|
5
|
-
it('returns decomposed sections for a valid value', () => {
|
|
6
|
-
const { header, payloadData, signature } = decomposeValue(
|
|
7
|
-
'OCMF|data|signature'
|
|
8
|
-
);
|
|
9
|
-
expect(header).toEqual('OCMF');
|
|
10
|
-
expect(payloadData).toEqual('data');
|
|
11
|
-
expect(signature).toEqual('signature');
|
|
12
|
-
});
|
|
13
|
-
|
|
14
|
-
it.each([
|
|
15
|
-
{
|
|
16
|
-
name: 'too few segments',
|
|
17
|
-
value: 'OCMF|data',
|
|
18
|
-
},
|
|
19
|
-
{
|
|
20
|
-
name: 'wrong header segment',
|
|
21
|
-
value: 'FOO|data|signature',
|
|
22
|
-
},
|
|
23
|
-
{
|
|
24
|
-
name: 'empty data payload segment',
|
|
25
|
-
value: 'OCMF||signature',
|
|
26
|
-
},
|
|
27
|
-
{
|
|
28
|
-
name: 'empty signature segment',
|
|
29
|
-
value: 'OCMF|data|',
|
|
30
|
-
},
|
|
31
|
-
])('throws when $name', ({ value }) => {
|
|
32
|
-
expect(() => decomposeValue(value)).toThrow();
|
|
33
|
-
});
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
describe('parseSections', () => {
|
|
37
|
-
it('parses the sections', () => {
|
|
38
|
-
const { header, payloadData, signature } = parseSections({
|
|
39
|
-
header: 'OCMF',
|
|
40
|
-
payloadData: '{"FV":"1.0"}',
|
|
41
|
-
signature: '{"SD":"ABC"}',
|
|
42
|
-
});
|
|
43
|
-
expect(header).toEqual('OCMF');
|
|
44
|
-
expect(payloadData).toEqual({ FV: '1.0' });
|
|
45
|
-
expect(signature).toEqual({ SD: 'ABC' });
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
it('throws on invalid json in the data section', () => {
|
|
49
|
-
expect(() =>
|
|
50
|
-
parseSections({
|
|
51
|
-
header: 'OCMF',
|
|
52
|
-
payloadData: '{',
|
|
53
|
-
signature: '{"SD":"ABC"}',
|
|
54
|
-
})
|
|
55
|
-
).toThrow();
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
it('throws on invalid json in the signature section', () => {
|
|
59
|
-
expect(() =>
|
|
60
|
-
parseSections({
|
|
61
|
-
header: 'OCMF',
|
|
62
|
-
payloadData: '{"FV":"1.0"}',
|
|
63
|
-
signature: '{',
|
|
64
|
-
})
|
|
65
|
-
).toThrow();
|
|
66
|
-
});
|
|
67
|
-
});
|
package/test/signer.spec.ts
DELETED
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
import { beforeEach, describe, expect, it } from '@jest/globals';
|
|
2
|
-
import { CryptoAdapter, EcPrivateKey } from '@road-labs/ocmf-crypto';
|
|
3
|
-
import { mock, MockProxy } from 'jest-mock-extended';
|
|
4
|
-
import { CurveMismatchError, PayloadData, Signer } from '../src';
|
|
5
|
-
import { hexToBytes } from 'test-commons';
|
|
6
|
-
|
|
7
|
-
const payload: PayloadData = {
|
|
8
|
-
FV: '1.0',
|
|
9
|
-
GI: 'TEST',
|
|
10
|
-
GS: '1234567',
|
|
11
|
-
GV: '1.0.0',
|
|
12
|
-
PG: 'T1',
|
|
13
|
-
MV: 'MV',
|
|
14
|
-
MM: 'MM',
|
|
15
|
-
MS: '1234567',
|
|
16
|
-
MF: '1.0',
|
|
17
|
-
IS: true,
|
|
18
|
-
IL: 'VERIFIED',
|
|
19
|
-
IF: ['RFID_PLAIN', 'OCPP_RS_TLS'],
|
|
20
|
-
IT: 'ISO14443',
|
|
21
|
-
ID: '79A94A26862469',
|
|
22
|
-
CI: 'CSONE',
|
|
23
|
-
CT: 'CBIDC',
|
|
24
|
-
RD: [
|
|
25
|
-
{
|
|
26
|
-
TM: '2025-06-14T08:44:54,562+0100 S',
|
|
27
|
-
TX: 'B',
|
|
28
|
-
RV: 0.75727,
|
|
29
|
-
RI: '1-b',
|
|
30
|
-
RU: 'kWh',
|
|
31
|
-
ST: 'G',
|
|
32
|
-
},
|
|
33
|
-
{
|
|
34
|
-
TM: '2025-06-14T11:44:54,562+0100 S',
|
|
35
|
-
TX: 'E',
|
|
36
|
-
RV: 3.12961,
|
|
37
|
-
RI: '1-b',
|
|
38
|
-
RU: 'kWh',
|
|
39
|
-
ST: 'G',
|
|
40
|
-
},
|
|
41
|
-
],
|
|
42
|
-
};
|
|
43
|
-
const hexPayloadData =
|
|
44
|
-
'7b224656223a22312e30222c224749223a2254455354222c224753223a2231323334353637222c224756223a22312e302e30222c225047223a225431222c224d56223a224d56222c224d4d223a224d4d222c224d53223a2231323334353637222c224d46223a22312e30222c224953223a747275652c22494c223a225645524946494544222c224946223a5b22524649445f504c41494e222c224f4350505f52535f544c53225d2c224954223a2249534f3134343433222c224944223a223739413934413236383632343639222c224349223a2243534f4e45222c224354223a224342494443222c225244223a5b7b22544d223a22323032352d30362d31345430383a34343a35342c3536322b303130302053222c225458223a2242222c225256223a302e37353732372c225249223a22312d62222c225255223a226b5768222c225354223a2247227d2c7b22544d223a22323032352d30362d31345431313a34343a35342c3536322b303130302053222c225458223a2245222c225256223a332e31323936312c225249223a22312d62222c225255223a226b5768222c225354223a2247227d5d7d';
|
|
45
|
-
|
|
46
|
-
describe('Signer', () => {
|
|
47
|
-
let privateKey: MockProxy<EcPrivateKey>;
|
|
48
|
-
let crypto: MockProxy<CryptoAdapter>;
|
|
49
|
-
let signer: Signer;
|
|
50
|
-
|
|
51
|
-
beforeEach(() => {
|
|
52
|
-
privateKey = mock<EcPrivateKey>();
|
|
53
|
-
crypto = mock<CryptoAdapter>();
|
|
54
|
-
signer = new Signer(crypto);
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
it('throws if the private key curve does not align with the signature', async () => {
|
|
58
|
-
privateKey.getCurve.mockReturnValue('secp192r1');
|
|
59
|
-
await expect(() =>
|
|
60
|
-
signer.sign(payload, privateKey, 'ECDSA-secp256r1-SHA256')
|
|
61
|
-
).rejects.toThrow(CurveMismatchError);
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
it('signs via the crypto adapter and returns a composed value', async () => {
|
|
65
|
-
privateKey.getCurve.mockReturnValue('secp256r1');
|
|
66
|
-
crypto.sign.mockReturnValue(new Uint8Array([0x01, 0x02, 0x03, 0x0a]));
|
|
67
|
-
const actual = await signer.sign(
|
|
68
|
-
payload,
|
|
69
|
-
privateKey,
|
|
70
|
-
'ECDSA-secp256r1-SHA256'
|
|
71
|
-
);
|
|
72
|
-
const expected = `OCMF|{"FV":"1.0","GI":"TEST","GS":"1234567","GV":"1.0.0","PG":"T1","MV":"MV","MM":"MM","MS":"1234567","MF":"1.0","IS":true,"IL":"VERIFIED","IF":["RFID_PLAIN","OCPP_RS_TLS"],"IT":"ISO14443","ID":"79A94A26862469","CI":"CSONE","CT":"CBIDC","RD":[{"TM":"2025-06-14T08:44:54,562+0100 S","TX":"B","RV":0.75727,"RI":"1-b","RU":"kWh","ST":"G"},{"TM":"2025-06-14T11:44:54,562+0100 S","TX":"E","RV":3.12961,"RI":"1-b","RU":"kWh","ST":"G"}]}|{"SA":"ECDSA-secp256r1-SHA256","SD":"0102030A"}`;
|
|
73
|
-
expect(actual).toEqual(expected);
|
|
74
|
-
expect(crypto.sign).toHaveBeenCalledWith(
|
|
75
|
-
hexToBytes(hexPayloadData),
|
|
76
|
-
privateKey,
|
|
77
|
-
'SHA-256',
|
|
78
|
-
'sigvalue-der'
|
|
79
|
-
);
|
|
80
|
-
});
|
|
81
|
-
});
|
package/test/verifier.spec.ts
DELETED
|
@@ -1,111 +0,0 @@
|
|
|
1
|
-
import { beforeEach, describe, expect, it } from '@jest/globals';
|
|
2
|
-
import { CryptoAdapter, EcPublicKey } from '@road-labs/ocmf-crypto';
|
|
3
|
-
import { mock, MockProxy } from 'jest-mock-extended';
|
|
4
|
-
import {
|
|
5
|
-
CurveMismatchError,
|
|
6
|
-
UnknownSignatureEncoding,
|
|
7
|
-
UnknownSignatureMimeType,
|
|
8
|
-
Verifier,
|
|
9
|
-
} from '../src';
|
|
10
|
-
import { hexToBytes } from 'test-commons';
|
|
11
|
-
|
|
12
|
-
const signedData = `OCMF|{"FV":"1.0","GI":"TEST","GS":"1234567","GV":"1.0.0","PG":"T1","MV":"MV","MM":"MM","MS":"1234567","MF":"1.0","IS":true,"IL":"VERIFIED","IF":["RFID_PLAIN","OCPP_RS_TLS"],"IT":"ISO14443","ID":"79A94A26862469","CI":"CSONE","CT":"CBIDC","RD":[{"TM":"2025-06-14T08:44:54,562+0100 S","TX":"B","RV":0.75727,"RI":"1-b","RU":"kWh","ST":"G"},{"TM":"2025-06-14T11:44:54,562+0100 S","TX":"E","RV":3.12961,"RI":"1-b","RU":"kWh","ST":"G"}]}|{"SA":"ECDSA-secp256r1-SHA256","SD":"0102030A"}`;
|
|
13
|
-
const hexPayloadData =
|
|
14
|
-
'7b224656223a22312e30222c224749223a2254455354222c224753223a2231323334353637222c224756223a22312e302e30222c225047223a225431222c224d56223a224d56222c224d4d223a224d4d222c224d53223a2231323334353637222c224d46223a22312e30222c224953223a747275652c22494c223a225645524946494544222c224946223a5b22524649445f504c41494e222c224f4350505f52535f544c53225d2c224954223a2249534f3134343433222c224944223a223739413934413236383632343639222c224349223a2243534f4e45222c224354223a224342494443222c225244223a5b7b22544d223a22323032352d30362d31345430383a34343a35342c3536322b303130302053222c225458223a2242222c225256223a302e37353732372c225249223a22312d62222c225255223a226b5768222c225354223a2247227d2c7b22544d223a22323032352d30362d31345431313a34343a35342c3536322b303130302053222c225458223a2245222c225256223a332e31323936312c225249223a22312d62222c225255223a226b5768222c225354223a2247227d5d7d';
|
|
15
|
-
|
|
16
|
-
describe('Verifier', () => {
|
|
17
|
-
let publicKey: MockProxy<EcPublicKey>;
|
|
18
|
-
let crypto: MockProxy<CryptoAdapter>;
|
|
19
|
-
let verifier: Verifier;
|
|
20
|
-
|
|
21
|
-
beforeEach(() => {
|
|
22
|
-
publicKey = mock<EcPublicKey>();
|
|
23
|
-
crypto = mock<CryptoAdapter>();
|
|
24
|
-
verifier = new Verifier(crypto);
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
it('throws if the private key curve does not align with the signature', async () => {
|
|
28
|
-
publicKey.getCurve.mockReturnValue('secp192r1');
|
|
29
|
-
await expect(() =>
|
|
30
|
-
verifier.parseAndVerify(signedData, publicKey)
|
|
31
|
-
).rejects.toThrow(CurveMismatchError);
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
it('throws if the signature is encoded with an unknown type', async () => {
|
|
35
|
-
publicKey.getCurve.mockReturnValue('secp256r1');
|
|
36
|
-
const signedData = `OCMF|{"FV":"1.0"}|{"SE":"foo","SA":"ECDSA-secp256r1-SHA256","SD":"0102030A"}`;
|
|
37
|
-
|
|
38
|
-
await expect(() =>
|
|
39
|
-
verifier.parseAndVerify(signedData, publicKey)
|
|
40
|
-
).rejects.toThrow(UnknownSignatureEncoding);
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
it('throws if the signature is encoded with an unknown mime type', async () => {
|
|
44
|
-
publicKey.getCurve.mockReturnValue('secp256r1');
|
|
45
|
-
const signedData = `OCMF|{"FV":"1.0"}|{"SM":"foo","SA":"ECDSA-secp256r1-SHA256","SD":"0102030A"}`;
|
|
46
|
-
|
|
47
|
-
await expect(() =>
|
|
48
|
-
verifier.parseAndVerify(signedData, publicKey)
|
|
49
|
-
).rejects.toThrow(UnknownSignatureMimeType);
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
it('signs via the crypto adapter and returns a composed value', async () => {
|
|
53
|
-
publicKey.getCurve.mockReturnValue('secp256r1');
|
|
54
|
-
crypto.verify.mockReturnValue(true);
|
|
55
|
-
const actual = await verifier.parseAndVerify(signedData, publicKey);
|
|
56
|
-
const expected = {
|
|
57
|
-
verified: true,
|
|
58
|
-
value: {
|
|
59
|
-
header: 'OCMF',
|
|
60
|
-
payloadData: {
|
|
61
|
-
FV: '1.0',
|
|
62
|
-
GI: 'TEST',
|
|
63
|
-
GS: '1234567',
|
|
64
|
-
GV: '1.0.0',
|
|
65
|
-
PG: 'T1',
|
|
66
|
-
MV: 'MV',
|
|
67
|
-
MM: 'MM',
|
|
68
|
-
MS: '1234567',
|
|
69
|
-
MF: '1.0',
|
|
70
|
-
IS: true,
|
|
71
|
-
IL: 'VERIFIED',
|
|
72
|
-
IF: ['RFID_PLAIN', 'OCPP_RS_TLS'],
|
|
73
|
-
IT: 'ISO14443',
|
|
74
|
-
ID: '79A94A26862469',
|
|
75
|
-
CI: 'CSONE',
|
|
76
|
-
CT: 'CBIDC',
|
|
77
|
-
RD: [
|
|
78
|
-
{
|
|
79
|
-
TM: '2025-06-14T08:44:54,562+0100 S',
|
|
80
|
-
TX: 'B',
|
|
81
|
-
RV: 0.75727,
|
|
82
|
-
RI: '1-b',
|
|
83
|
-
RU: 'kWh',
|
|
84
|
-
ST: 'G',
|
|
85
|
-
},
|
|
86
|
-
{
|
|
87
|
-
TM: '2025-06-14T11:44:54,562+0100 S',
|
|
88
|
-
TX: 'E',
|
|
89
|
-
RV: 3.12961,
|
|
90
|
-
RI: '1-b',
|
|
91
|
-
RU: 'kWh',
|
|
92
|
-
ST: 'G',
|
|
93
|
-
},
|
|
94
|
-
],
|
|
95
|
-
},
|
|
96
|
-
signature: {
|
|
97
|
-
SA: 'ECDSA-secp256r1-SHA256',
|
|
98
|
-
SD: '0102030A',
|
|
99
|
-
},
|
|
100
|
-
},
|
|
101
|
-
};
|
|
102
|
-
expect(actual).toEqual(expected);
|
|
103
|
-
expect(crypto.verify).toHaveBeenCalledWith(
|
|
104
|
-
hexToBytes('0102030A'),
|
|
105
|
-
hexToBytes(hexPayloadData),
|
|
106
|
-
publicKey,
|
|
107
|
-
'SHA-256',
|
|
108
|
-
'sigvalue-der'
|
|
109
|
-
);
|
|
110
|
-
});
|
|
111
|
-
});
|
package/tsconfig.json
DELETED