From 90bb3ab86e864f29c4dbc34749c8725b6c8ed789 Mon Sep 17 00:00:00 2001 From: =?utf8?q?J=C3=A9r=C3=B4me=20Benoit?= Date: Tue, 7 Apr 2026 20:20:34 +0200 Subject: [PATCH] fix(ocpp): populate signingMethod field and add SigningMethodEnumType enum - Fix signingMethod being empty string in SignedMeterValueType payload - Add SigningMethodEnumType enum per OCA Application Note Table 12 and OCPP 2.1 Appendix 7.4 with all 7 standardized values - Propagate FiscalMetering.SigningMethod config variable to the signed meter data generator with fallback to ECDSA-secp256r1-SHA256 - Add OCPP 1.6 vendor key SigningMethod and read it in readSigningConfigForConnector - Add MeterPublicKey and SigningMethod mappings to OCPP2_PARAMETER_KEY_MAP - Use SigningMethodEnumType in all interfaces and type definitions - Update README vendor-specific keys documentation --- README.md | 1 + src/charging-station/ConfigurationKeyUtils.ts | 8 ++++++++ .../ocpp/1.6/OCPP16ServiceUtils.ts | 7 ++++++- .../ocpp/2.0/OCPP20RequestBuilders.ts | 3 ++- .../ocpp/2.0/OCPP20VariableRegistry.ts | 3 ++- src/charging-station/ocpp/OCPPServiceUtils.ts | 7 ++++++- .../ocpp/OCPPSignedMeterDataGenerator.ts | 19 +++++++++++++------ .../ocpp/OCPPSignedMeterValueUtils.ts | 7 ++++++- src/types/index.ts | 1 + src/types/ocpp/1.6/Configuration.ts | 1 + src/types/ocpp/1.6/MeterValues.ts | 4 +++- src/types/ocpp/2.0/MeterValues.ts | 4 +++- src/types/ocpp/Configuration.ts | 10 ++++++++++ .../ocpp/1.6/OCPP16SignedMeterValues.test.ts | 3 ++- .../ocpp/2.0/OCPP20SignedMeterValues.test.ts | 11 +++++++++-- .../ocpp/OCPPSignedMeterDataGenerator.test.ts | 10 +++++++--- tests/types/ocpp/1.6/MeterValues.test.ts | 13 ++++++++++--- .../1.6/OCPP16VendorParametersKey.test.ts | 4 ++++ 18 files changed, 94 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index e7cdadc9..45715636 100644 --- a/README.md +++ b/README.md @@ -681,6 +681,7 @@ All kind of OCPP parameters are supported in charging station configuration or c - :white_check_mark: SampledDataSignReadings (type: boolean) (units: -) **(vendor-specific)** - :white_check_mark: SampledDataSignStartedReadings (type: boolean) (units: -) **(vendor-specific)** - :white_check_mark: SampledDataSignUpdatedReadings (type: boolean) (units: -) **(vendor-specific)** +- :white_check_mark: SigningMethod (type: string) (units: -) **(vendor-specific)** - :white_check_mark: StartTxnSampledData (type: memberlist) (units: -) **(vendor-specific)** ### Version 2.0.x diff --git a/src/charging-station/ConfigurationKeyUtils.ts b/src/charging-station/ConfigurationKeyUtils.ts index f040cf5f..3d1080c3 100644 --- a/src/charging-station/ConfigurationKeyUtils.ts +++ b/src/charging-station/ConfigurationKeyUtils.ts @@ -84,6 +84,10 @@ const OCPP2_PARAMETER_KEY_MAP = new Map = { [buildRegistryKey(OCPP20ComponentName.FiscalMetering, OCPP20VendorVariableName.SigningMethod)]: { component: OCPP20ComponentName.FiscalMetering, dataType: DataEnumType.string, - defaultValue: 'ECDSA-secp256r1-SHA256', + defaultValue: SigningMethodEnumType.ECDSA_secp256r1_SHA256, description: 'Method used to create the digital signature for signed meter values. See OCA Application Note v1.0 Table 12 for valid values.', mutability: MutabilityEnumType.ReadOnly, diff --git a/src/charging-station/ocpp/OCPPServiceUtils.ts b/src/charging-station/ocpp/OCPPServiceUtils.ts index da9c0c91..a1bbf3e2 100644 --- a/src/charging-station/ocpp/OCPPServiceUtils.ts +++ b/src/charging-station/ocpp/OCPPServiceUtils.ts @@ -6,7 +6,7 @@ import { readFileSync } from 'node:fs' import { dirname, join } from 'node:path' import { fileURLToPath } from 'node:url' -import type { BootReasonEnumType } from '../../types/index.js' +import type { BootReasonEnumType, SigningMethodEnumType } from '../../types/index.js' import { buildConfigKey } from '../../charging-station/ConfigurationKeyUtils.js' import { type ChargingStation, getConfigurationKey } from '../../charging-station/index.js' @@ -969,6 +969,10 @@ export const buildMeterValue = ( chargingStation, buildConfigKey(OCPP20ComponentName.FiscalMetering, VendorParametersKey.PublicKey) )?.value + const signingMethod = getConfigurationKey( + chargingStation, + buildConfigKey(OCPP20ComponentName.FiscalMetering, VendorParametersKey.SigningMethod) + )?.value as SigningMethodEnumType | undefined signingConfig = { enabled: true, meterSerialNumber: chargingStation.stationInfo.meterSerialNumber ?? 'UNKNOWN', @@ -979,6 +983,7 @@ export const buildMeterValue = ( publicKeyWithSignedMeterValue: parsePublicKeyWithSignedMeterValue( publicKeyWithSignedMeterValueStr ), + signingMethod, transactionId, } } diff --git a/src/charging-station/ocpp/OCPPSignedMeterDataGenerator.ts b/src/charging-station/ocpp/OCPPSignedMeterDataGenerator.ts index c9f2dc84..6474a25f 100644 --- a/src/charging-station/ocpp/OCPPSignedMeterDataGenerator.ts +++ b/src/charging-station/ocpp/OCPPSignedMeterDataGenerator.ts @@ -1,13 +1,18 @@ import { createHash } from 'node:crypto' -import { type JsonObject, MeterValueContext, MeterValueUnit } from '../../types/index.js' +import { + type JsonObject, + MeterValueContext, + MeterValueUnit, + SigningMethodEnumType, +} from '../../types/index.js' import { roundTo } from '../../utils/index.js' export interface SignedMeterData extends JsonObject { encodingMethod: string publicKey: string signedMeterData: string - signingMethod: string + signingMethod: SigningMethodEnumType } export interface SignedMeterDataParams { @@ -19,7 +24,7 @@ export interface SignedMeterDataParams { transactionId: number | string } -const SIGNING_METHOD = 'ECDSA-secp256r1-SHA256' +const DEFAULT_SIGNING_METHOD = SigningMethodEnumType.ECDSA_secp256r1_SHA256 const ENCODING_METHOD = 'OCMF' const contextToTxCode = (context: MeterValueContext): string => { @@ -39,8 +44,10 @@ export const buildPublicKeyValue = (hexKey: string): string => { export const generateSignedMeterData = ( params: SignedMeterDataParams, - publicKeyHex?: string + publicKeyHex?: string, + signingMethod?: SigningMethodEnumType ): SignedMeterData => { + const resolvedSigningMethod = signingMethod ?? DEFAULT_SIGNING_METHOD const txCode = contextToTxCode(params.context) const meterValueKwh = params.meterValueUnit === MeterValueUnit.KILO_WATT_HOUR @@ -68,12 +75,12 @@ export const generateSignedMeterData = ( const simulatedSignature = createHash('sha256').update(JSON.stringify(ocmfPayload)).digest('hex') - const ocmfString = `OCMF|${JSON.stringify(ocmfPayload)}|{"SA":"${SIGNING_METHOD}","SD":"${simulatedSignature}"}` + const ocmfString = `OCMF|${JSON.stringify(ocmfPayload)}|{"SA":"${resolvedSigningMethod}","SD":"${simulatedSignature}"}` return { encodingMethod: ENCODING_METHOD, publicKey: publicKeyHex != null ? buildPublicKeyValue(publicKeyHex) : '', signedMeterData: Buffer.from(ocmfString).toString('base64'), - signingMethod: '', + signingMethod: resolvedSigningMethod, } } diff --git a/src/charging-station/ocpp/OCPPSignedMeterValueUtils.ts b/src/charging-station/ocpp/OCPPSignedMeterValueUtils.ts index 48d4867c..599df8bb 100644 --- a/src/charging-station/ocpp/OCPPSignedMeterValueUtils.ts +++ b/src/charging-station/ocpp/OCPPSignedMeterValueUtils.ts @@ -1,5 +1,9 @@ import { BaseError } from '../../exception/index.js' -import { PublicKeyWithSignedMeterValueEnumType, type SampledValue } from '../../types/index.js' +import { + PublicKeyWithSignedMeterValueEnumType, + type SampledValue, + type SigningMethodEnumType, +} from '../../types/index.js' export interface SampledValueSigningConfig extends SigningConfig { enabled: boolean @@ -17,6 +21,7 @@ export interface SigningConfig { meterSerialNumber: string publicKeyHex?: string publicKeyWithSignedMeterValue: PublicKeyWithSignedMeterValueEnumType + signingMethod?: SigningMethodEnumType } const PUBLIC_KEY_WITH_SIGNED_METER_VALUE_VALUES = new Set( diff --git a/src/types/index.ts b/src/types/index.ts index bd253b7c..f39ba634 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -341,6 +341,7 @@ export { ConnectorPhaseRotation, type OCPPConfigurationKey, PublicKeyWithSignedMeterValueEnumType, + SigningMethodEnumType, StandardParametersKey, SupportedFeatureProfiles, VendorParametersKey, diff --git a/src/types/ocpp/1.6/Configuration.ts b/src/types/ocpp/1.6/Configuration.ts index 29e50af0..0ba44b9f 100644 --- a/src/types/ocpp/1.6/Configuration.ts +++ b/src/types/ocpp/1.6/Configuration.ts @@ -63,5 +63,6 @@ export enum OCPP16VendorParametersKey { SampledDataSignReadings = 'SampledDataSignReadings', SampledDataSignStartedReadings = 'SampledDataSignStartedReadings', SampledDataSignUpdatedReadings = 'SampledDataSignUpdatedReadings', + SigningMethod = 'SigningMethod', StartTxnSampledData = 'StartTxnSampledData', } diff --git a/src/types/ocpp/1.6/MeterValues.ts b/src/types/ocpp/1.6/MeterValues.ts index de4461f3..021f9764 100644 --- a/src/types/ocpp/1.6/MeterValues.ts +++ b/src/types/ocpp/1.6/MeterValues.ts @@ -1,6 +1,8 @@ import type { EmptyObject } from '../../EmptyObject.js' import type { JsonObject } from '../../JsonType.js' +import { type SigningMethodEnumType } from '../Configuration.js' + export enum OCPP16MeterValueContext { INTERRUPTION_BEGIN = 'Interruption.Begin', INTERRUPTION_END = 'Interruption.End', @@ -109,5 +111,5 @@ export interface OCPP16SignedMeterValue extends JsonObject { encodingMethod: string publicKey: string signedMeterData: string - signingMethod: string + signingMethod: '' | SigningMethodEnumType } diff --git a/src/types/ocpp/2.0/MeterValues.ts b/src/types/ocpp/2.0/MeterValues.ts index b3715dbf..fc34a546 100644 --- a/src/types/ocpp/2.0/MeterValues.ts +++ b/src/types/ocpp/2.0/MeterValues.ts @@ -2,6 +2,8 @@ import type { EmptyObject } from '../../EmptyObject.js' import type { JsonObject } from '../../JsonType.js' import type { CustomDataType, OCPP20UnitEnumType } from './Common.js' +import { type SigningMethodEnumType } from '../Configuration.js' + export enum OCPP20LocationEnumType { Body = 'Body', Cable = 'Cable', @@ -92,7 +94,7 @@ export interface OCPP20SignedMeterValue extends JsonObject { encodingMethod: string // maxLength: 50 publicKey: string // Base64 encoded, maxLength: 2500 signedMeterData: string // Base64 encoded, maxLength: 2500 - signingMethod: string // maxLength: 50 + signingMethod: '' | SigningMethodEnumType // maxLength: 50 } export interface OCPP20UnitOfMeasure extends JsonObject { diff --git a/src/types/ocpp/Configuration.ts b/src/types/ocpp/Configuration.ts index 90a3bab3..991e1fb0 100644 --- a/src/types/ocpp/Configuration.ts +++ b/src/types/ocpp/Configuration.ts @@ -28,6 +28,16 @@ export enum PublicKeyWithSignedMeterValueEnumType { OncePerTransaction = 'OncePerTransaction', } +export enum SigningMethodEnumType { + ECDSA_brainpool256r1_SHA256 = 'ECDSA-brainpool256r1-SHA256', + ECDSA_brainpool384r1_SHA256 = 'ECDSA-brainpool384r1-SHA256', + ECDSA_secp192k1_SHA256 = 'ECDSA-secp192k1-SHA256', + ECDSA_secp192r1_SHA256 = 'ECDSA-secp192r1-SHA256', + ECDSA_secp256k1_SHA256 = 'ECDSA-secp256k1-SHA256', + ECDSA_secp256r1_SHA256 = 'ECDSA-secp256r1-SHA256', + ECDSA_secp384r1_SHA256 = 'ECDSA-secp384r1-SHA256', +} + export type ConfigurationKeyType = StandardParametersKey | string | VendorParametersKey export interface OCPPConfigurationKey extends JsonObject { diff --git a/tests/charging-station/ocpp/1.6/OCPP16SignedMeterValues.test.ts b/tests/charging-station/ocpp/1.6/OCPP16SignedMeterValues.test.ts index 958758b4..d58bf8aa 100644 --- a/tests/charging-station/ocpp/1.6/OCPP16SignedMeterValues.test.ts +++ b/tests/charging-station/ocpp/1.6/OCPP16SignedMeterValues.test.ts @@ -23,6 +23,7 @@ import { type OCPP16SignedMeterValue, OCPP16VendorParametersKey, OCPPVersion, + SigningMethodEnumType, } from '../../../../src/types/index.js' import { standardCleanup, withMockTimers } from '../../../helpers/TestLifecycleHelpers.js' import { createMockChargingStation } from '../../ChargingStationTestUtils.js' @@ -296,7 +297,7 @@ await describe('OCPP 1.6 — Signed MeterValues', async () => { assert.strictEqual(typeof parsed.signedMeterData, 'string') assert.strictEqual(typeof parsed.publicKey, 'string') assert.strictEqual(parsed.encodingMethod, 'OCMF') - assert.strictEqual(parsed.signingMethod, '') + assert.strictEqual(parsed.signingMethod, SigningMethodEnumType.ECDSA_secp256r1_SHA256) }) }) diff --git a/tests/charging-station/ocpp/2.0/OCPP20SignedMeterValues.test.ts b/tests/charging-station/ocpp/2.0/OCPP20SignedMeterValues.test.ts index aa1a4ca3..7be581b7 100644 --- a/tests/charging-station/ocpp/2.0/OCPP20SignedMeterValues.test.ts +++ b/tests/charging-station/ocpp/2.0/OCPP20SignedMeterValues.test.ts @@ -21,6 +21,7 @@ import { OCPPVersion, PublicKeyWithSignedMeterValueEnumType, type SampledValueTemplate, + SigningMethodEnumType, } from '../../../../src/types/index.js' import { Constants } from '../../../../src/utils/index.js' import { standardCleanup } from '../../../helpers/TestLifecycleHelpers.js' @@ -63,7 +64,10 @@ await describe('OCPP 2.0 Signed Meter Values', async () => { ) assert.ok(sampledValue.signedMeterValue != null) - assert.strictEqual(sampledValue.signedMeterValue.signingMethod, '') + assert.strictEqual( + sampledValue.signedMeterValue.signingMethod, + SigningMethodEnumType.ECDSA_secp256r1_SHA256 + ) assert.strictEqual(sampledValue.signedMeterValue.encodingMethod, 'OCMF') }) @@ -238,7 +242,10 @@ await describe('OCPP 2.0 Signed Meter Values', async () => { ) as OCPP20SampledValue | undefined assert.notStrictEqual(energySampledValue, undefined) assert.ok(energySampledValue?.signedMeterValue != null) - assert.strictEqual(energySampledValue.signedMeterValue.signingMethod, '') + assert.strictEqual( + energySampledValue.signedMeterValue.signingMethod, + SigningMethodEnumType.ECDSA_secp256r1_SHA256 + ) assert.strictEqual(energySampledValue.signedMeterValue.encodingMethod, 'OCMF') }) diff --git a/tests/charging-station/ocpp/OCPPSignedMeterDataGenerator.test.ts b/tests/charging-station/ocpp/OCPPSignedMeterDataGenerator.test.ts index bd3537b9..aae72c44 100644 --- a/tests/charging-station/ocpp/OCPPSignedMeterDataGenerator.test.ts +++ b/tests/charging-station/ocpp/OCPPSignedMeterDataGenerator.test.ts @@ -19,7 +19,11 @@ import { generateSignedMeterData, type SignedMeterDataParams, } from '../../../src/charging-station/ocpp/OCPPSignedMeterDataGenerator.js' -import { MeterValueContext, MeterValueUnit } from '../../../src/types/index.js' +import { + MeterValueContext, + MeterValueUnit, + SigningMethodEnumType, +} from '../../../src/types/index.js' const DEFAULT_PARAMS: SignedMeterDataParams = { context: MeterValueContext.SAMPLE_PERIODIC, @@ -55,10 +59,10 @@ await describe('SignedMeterDataGenerator', async () => { assert.ok(decoded.startsWith('OCMF|')) }) - await it('should set signingMethod to empty string when included in signedMeterData', () => { + await it('should set signingMethod to ECDSA-secp256r1-SHA256', () => { const result = generateSignedMeterData(DEFAULT_PARAMS) - assert.strictEqual(result.signingMethod, '') + assert.strictEqual(result.signingMethod, SigningMethodEnumType.ECDSA_secp256r1_SHA256) }) await it('should set encodingMethod to OCMF', () => { diff --git a/tests/types/ocpp/1.6/MeterValues.test.ts b/tests/types/ocpp/1.6/MeterValues.test.ts index 068817f5..34e3c70e 100644 --- a/tests/types/ocpp/1.6/MeterValues.test.ts +++ b/tests/types/ocpp/1.6/MeterValues.test.ts @@ -6,7 +6,11 @@ import assert from 'node:assert/strict' import { describe, it } from 'node:test' -import { OCPP16MeterValueFormat, type OCPP16SignedMeterValue } from '../../../../src/types/index.js' +import { + OCPP16MeterValueFormat, + type OCPP16SignedMeterValue, + SigningMethodEnumType, +} from '../../../../src/types/index.js' await describe('OCPP 1.6 meter value types', async () => { await describe('OCPP16MeterValueFormat', async () => { @@ -25,10 +29,13 @@ await describe('OCPP 1.6 meter value types', async () => { encodingMethod: 'OCMF', publicKey: 'b2NhOmJhc2UxNjphc24xOmZha2VrZXk=', // cspell:disable-line signedMeterData: 'T0NNRnx7fXxmYWtlc2lnbmF0dXJl', // cspell:disable-line - signingMethod: 'ECDSA-secp256r1-SHA256', + signingMethod: SigningMethodEnumType.ECDSA_secp256r1_SHA256, } assert.strictEqual(signedMeterValue.encodingMethod, 'OCMF') - assert.strictEqual(signedMeterValue.signingMethod, 'ECDSA-secp256r1-SHA256') + assert.strictEqual( + signedMeterValue.signingMethod, + SigningMethodEnumType.ECDSA_secp256r1_SHA256 + ) assert.ok(signedMeterValue.publicKey.length > 0) assert.ok(signedMeterValue.signedMeterData.length > 0) }) diff --git a/tests/types/ocpp/1.6/OCPP16VendorParametersKey.test.ts b/tests/types/ocpp/1.6/OCPP16VendorParametersKey.test.ts index 186a74ae..5bebf2ea 100644 --- a/tests/types/ocpp/1.6/OCPP16VendorParametersKey.test.ts +++ b/tests/types/ocpp/1.6/OCPP16VendorParametersKey.test.ts @@ -52,6 +52,10 @@ await describe('OCPP16VendorParametersKey', async () => { ) }) + await it('should have SigningMethod key', () => { + assert.strictEqual(OCPP16VendorParametersKey.SigningMethod, 'SigningMethod') + }) + await it('should have StartTxnSampledData key', () => { assert.strictEqual(OCPP16VendorParametersKey.StartTxnSampledData, 'StartTxnSampledData') }) -- 2.43.0