]> Piment Noir Git Repositories - e-mobility-charging-stations-simulator.git/commitdiff
fix(ocpp): populate signingMethod field and add SigningMethodEnumType enum
authorJérôme Benoit <jerome.benoit@sap.com>
Tue, 7 Apr 2026 18:20:34 +0000 (20:20 +0200)
committerJérôme Benoit <jerome.benoit@sap.com>
Tue, 7 Apr 2026 18:20:34 +0000 (20:20 +0200)
- 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

18 files changed:
README.md
src/charging-station/ConfigurationKeyUtils.ts
src/charging-station/ocpp/1.6/OCPP16ServiceUtils.ts
src/charging-station/ocpp/2.0/OCPP20RequestBuilders.ts
src/charging-station/ocpp/2.0/OCPP20VariableRegistry.ts
src/charging-station/ocpp/OCPPServiceUtils.ts
src/charging-station/ocpp/OCPPSignedMeterDataGenerator.ts
src/charging-station/ocpp/OCPPSignedMeterValueUtils.ts
src/types/index.ts
src/types/ocpp/1.6/Configuration.ts
src/types/ocpp/1.6/MeterValues.ts
src/types/ocpp/2.0/MeterValues.ts
src/types/ocpp/Configuration.ts
tests/charging-station/ocpp/1.6/OCPP16SignedMeterValues.test.ts
tests/charging-station/ocpp/2.0/OCPP20SignedMeterValues.test.ts
tests/charging-station/ocpp/OCPPSignedMeterDataGenerator.test.ts
tests/types/ocpp/1.6/MeterValues.test.ts
tests/types/ocpp/1.6/OCPP16VendorParametersKey.test.ts

index e7cdadc9267e041bedb9bfcb2f5b9a272e64948b..457156360a663217486d620be2f83511b5bb1553 100644 (file)
--- 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
index f040cf5f46744d51f86e508f4e72b4705586ba24..3d1080c3d38a6cbbbd8c45a078f3150de765b0b5 100644 (file)
@@ -84,6 +84,10 @@ const OCPP2_PARAMETER_KEY_MAP = new Map<ConfigurationKeyType, ConfigurationKeyTy
     VendorParametersKey.AlignedDataSignUpdatedReadings,
     buildConfigKey(OCPP20ComponentName.AlignedDataCtrlr, VendorParametersKey.SignUpdatedReadings),
   ],
+  [
+    VendorParametersKey.MeterPublicKey,
+    buildConfigKey(OCPP20ComponentName.FiscalMetering, VendorParametersKey.PublicKey),
+  ],
   [
     VendorParametersKey.PublicKeyWithSignedMeterValue,
     buildConfigKey(
@@ -103,6 +107,10 @@ const OCPP2_PARAMETER_KEY_MAP = new Map<ConfigurationKeyType, ConfigurationKeyTy
     VendorParametersKey.SampledDataSignUpdatedReadings,
     buildConfigKey(OCPP20ComponentName.SampledDataCtrlr, VendorParametersKey.SignUpdatedReadings),
   ],
+  [
+    VendorParametersKey.SigningMethod,
+    buildConfigKey(OCPP20ComponentName.FiscalMetering, VendorParametersKey.SigningMethod),
+  ],
   [
     VendorParametersKey.StartTxnSampledData,
     buildConfigKey(OCPP20ComponentName.SampledDataCtrlr, StandardParametersKey.TxStartedMeasurands),
index e5c2d33f346e6ac2c58a31fa9a7d544ad11099f6..2168e4c0ceb00baf1fc4b56e273a6aa66d99b971 100644 (file)
@@ -8,6 +8,8 @@ import {
   isWithinInterval,
 } from 'date-fns'
 
+import type { SigningMethodEnumType } from '../../../types/index.js'
+
 import {
   type ChargingStation,
   getConfigurationKey,
@@ -947,7 +949,8 @@ export class OCPP16ServiceUtils {
         timestamp,
         transactionId,
       },
-      includePublicKey ? signingConfig.publicKeyHex : undefined
+      includePublicKey ? signingConfig.publicKeyHex : undefined,
+      signingConfig.signingMethod
     )
     return {
       publicKeyIncluded: includePublicKey && signingConfig.publicKeyHex != null,
@@ -1047,6 +1050,8 @@ export class OCPP16ServiceUtils {
           OCPP16VendorParametersKey.PublicKeyWithSignedMeterValue
         )?.value
       ),
+      signingMethod: getConfigurationKey(chargingStation, OCPP16VendorParametersKey.SigningMethod)
+        ?.value as SigningMethodEnumType | undefined,
     }
   }
 }
index 80e1a4fb9204fc5dff4a86619db511bdf1762f9f..e8024a9213b2314c04af8f4078d4ec3b1bb4032a 100644 (file)
@@ -94,7 +94,8 @@ export function buildOCPP20SampledValue (
     sampledValue.signedMeterValue = {
       ...generateSignedMeterData(
         signedMeterDataParams,
-        includePublicKey ? signingConfig.publicKeyHex : undefined
+        includePublicKey ? signingConfig.publicKeyHex : undefined,
+        signingConfig.signingMethod
       ),
     }
     publicKeyIncluded = includePublicKey && signingConfig.publicKeyHex != null
index bc39b2a5a2d7af2b455b541141739f53cb6ff287..f66af2f82ab1cf47794cd3b0837fd16b19f69299 100644 (file)
@@ -21,6 +21,7 @@ import {
   PersistenceEnumType,
   PublicKeyWithSignedMeterValueEnumType,
   ReasonCodeEnumType,
+  SigningMethodEnumType,
   type VariableName,
 } from '../../../types/index.js'
 import {
@@ -1087,7 +1088,7 @@ export const VARIABLE_REGISTRY: Record<string, VariableMetadata> = {
   [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,
index da9c0c9191d739347e78909096c7971ab0a2c99b..a1bbf3e207a1d7c14a42a0b4b09f2bb340cee0eb 100644 (file)
@@ -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,
             }
           }
index c9f2dc8436847ab831cc9a582e824364dbe0c8c2..6474a25fe375637e90fd1ac420dec716b34e9f08 100644 (file)
@@ -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,
   }
 }
index 48d4867c5a55d25fcf230786e2069893c83d24f2..599df8bbefb9abfe814d37edacd971b04ac63003 100644 (file)
@@ -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<string>(
index bd253b7cff999f051d9d011d5e5a5be02e44b63b..f39ba6341b4a7dd134ed92eb922d2deaadc3c77a 100644 (file)
@@ -341,6 +341,7 @@ export {
   ConnectorPhaseRotation,
   type OCPPConfigurationKey,
   PublicKeyWithSignedMeterValueEnumType,
+  SigningMethodEnumType,
   StandardParametersKey,
   SupportedFeatureProfiles,
   VendorParametersKey,
index 29e50af0fb99e4ed93cdab9fc9367e9211fd1592..0ba44b9f9e8bbd4c7911d256b5ee61e739e23754 100644 (file)
@@ -63,5 +63,6 @@ export enum OCPP16VendorParametersKey {
   SampledDataSignReadings = 'SampledDataSignReadings',
   SampledDataSignStartedReadings = 'SampledDataSignStartedReadings',
   SampledDataSignUpdatedReadings = 'SampledDataSignUpdatedReadings',
+  SigningMethod = 'SigningMethod',
   StartTxnSampledData = 'StartTxnSampledData',
 }
index de4461f3f68998c3167226e615149e2097497b15..021f97649ee30f09e15769759bf01586d5bbbcd5 100644 (file)
@@ -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
 }
index b3715dbfd2382f943091f953922ff1fa627d8fb6..fc34a54656d04068da6adca4765e2885cf3fd0e5 100644 (file)
@@ -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 {
index 90a3bab37f4bb10a3c8a32711bbcd6bbd10e73b4..991e1fb02149864589bb176f8ad674887640d0b5 100644 (file)
@@ -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 {
index 958758b498a50fde9c8c4d888c5dd0d91302fa06..d58bf8aa1179530a55f1e3b54a2204382dc605d1 100644 (file)
@@ -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)
     })
   })
 
index aa1a4ca3ed5855c67bf471b46c94878e50c9e6bf..7be581b73e52f6943161161a38643a4d86aea0cb 100644 (file)
@@ -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')
     })
 
index bd3537b9cb74f7188d970228e3a7cea378745903..aae72c44aa99be92858515c1fb86d2020f025020 100644 (file)
@@ -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', () => {
index 068817f54fd22809fac8374a99d411ba2b022f9d..34e3c70eccb541af31505041f844f6f75c5f6d1f 100644 (file)
@@ -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)
     })
index 186a74aede2ab975b518d0f9fe1cbcf9426400e9..5bebf2ea53ee8ea3ab238b086a733853dc073fa9 100644 (file)
@@ -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')
   })