shouldIncludePublicKey,
type SignedSampledValueResult,
type SigningConfig,
+ validateSigningPrerequisites,
} from '../OCPPSignedMeterValueUtils.js'
import { OCPP16Constants } from './OCPP16Constants.js'
import { buildOCPP16SampledValue, buildSignedOCPP16SampledValue } from './OCPP16RequestBuilders.js'
chargingStation,
connectorId
)
- const signedResult = OCPP16ServiceUtils.buildSignedSampledValue(
- signingCfg,
- meterStart ?? 0,
- OCPP16MeterValueContext.TRANSACTION_BEGIN,
- transactionId,
- publicKeySentInTransaction,
- meterValue.timestamp
- )
- meterValue.sampledValue.push(signedResult.sampledValue)
- if (signedResult.publicKeyIncluded && connectorStatus != null) {
- connectorStatus.publicKeySentInTransaction = true
+ if (signingCfg != null) {
+ const signedResult = OCPP16ServiceUtils.buildSignedSampledValue(
+ signingCfg,
+ meterStart ?? 0,
+ OCPP16MeterValueContext.TRANSACTION_BEGIN,
+ transactionId,
+ publicKeySentInTransaction,
+ meterValue.timestamp
+ )
+ meterValue.sampledValue.push(signedResult.sampledValue)
+ if (signedResult.publicKeyIncluded && connectorStatus != null) {
+ connectorStatus.publicKeySentInTransaction = true
+ }
}
}
return meterValue
chargingStation,
connectorId
)
- const signedResult = OCPP16ServiceUtils.buildSignedSampledValue(
- signingCfg,
- meterStop ?? 0,
- OCPP16MeterValueContext.TRANSACTION_END,
- transactionId,
- publicKeySentInTransaction,
- meterValue.timestamp
- )
- meterValue.sampledValue.push(signedResult.sampledValue)
- if (signedResult.publicKeyIncluded && connectorStatus != null) {
- connectorStatus.publicKeySentInTransaction = true
+ if (signingCfg != null) {
+ const signedResult = OCPP16ServiceUtils.buildSignedSampledValue(
+ signingCfg,
+ meterStop ?? 0,
+ OCPP16MeterValueContext.TRANSACTION_END,
+ transactionId,
+ publicKeySentInTransaction,
+ meterValue.timestamp
+ )
+ meterValue.sampledValue.push(signedResult.sampledValue)
+ if (signedResult.publicKeyIncluded && connectorStatus != null) {
+ connectorStatus.publicKeySentInTransaction = true
+ }
}
}
return meterValue
chargingStation,
connectorId
)
- const signedResult = OCPP16ServiceUtils.buildSignedSampledValue(
- signingCfg,
- energyWh,
- OCPP16MeterValueContext.SAMPLE_PERIODIC,
- transactionId,
- publicKeySentInTransaction,
- (meterValue as OCPP16MeterValue).timestamp
- )
- ;(meterValue as OCPP16MeterValue).sampledValue.push(signedResult.sampledValue)
- if (signedResult.publicKeyIncluded) {
- connectorStatus.publicKeySentInTransaction = true
+ if (signingCfg != null) {
+ const signedResult = OCPP16ServiceUtils.buildSignedSampledValue(
+ signingCfg,
+ energyWh,
+ OCPP16MeterValueContext.SAMPLE_PERIODIC,
+ transactionId,
+ publicKeySentInTransaction,
+ (meterValue as OCPP16MeterValue).timestamp
+ )
+ ;(meterValue as OCPP16MeterValue).sampledValue.push(signedResult.sampledValue)
+ if (signedResult.publicKeyIncluded) {
+ connectorStatus.publicKeySentInTransaction = true
+ }
}
}
chargingStation.ocppRequestService
private static readSigningConfigForConnector (
chargingStation: ChargingStation,
connectorId: number
- ): SigningConfig {
+ ): SigningConfig | undefined {
+ const publicKeyHex = getConfigurationKey(
+ chargingStation,
+ `${OCPP16VendorParametersKey.MeterPublicKey}${connectorId.toString()}`
+ )?.value
+ const configuredSigningMethod = getConfigurationKey(
+ chargingStation,
+ OCPP16VendorParametersKey.SigningMethod
+ )?.value as SigningMethodEnumType | undefined
+
+ const prerequisiteResult = validateSigningPrerequisites(publicKeyHex, configuredSigningMethod)
+ if (!prerequisiteResult.enabled) {
+ logger.warn(
+ `${chargingStation.logPrefix()} OCPP16ServiceUtils.readSigningConfigForConnector: Signed meter values disabled for connector ${connectorId.toString()}: ${prerequisiteResult.reason}`
+ )
+ return undefined
+ }
+
return {
meterSerialNumber: chargingStation.stationInfo?.meterSerialNumber ?? 'SIMULATOR',
- publicKeyHex: getConfigurationKey(
- chargingStation,
- `${OCPP16VendorParametersKey.MeterPublicKey}${connectorId.toString()}`
- )?.value,
+ publicKeyHex,
publicKeyWithSignedMeterValue: parsePublicKeyWithSignedMeterValue(
getConfigurationKey(
chargingStation,
OCPP16VendorParametersKey.PublicKeyWithSignedMeterValue
)?.value
),
- signingMethod: getConfigurationKey(chargingStation, OCPP16VendorParametersKey.SigningMethod)
- ?.value as SigningMethodEnumType | undefined,
+ signingMethod: prerequisiteResult.signingMethod,
}
}
}
import {
parsePublicKeyWithSignedMeterValue,
type SampledValueSigningConfig,
+ validateSigningPrerequisites,
} from './OCPPSignedMeterValueUtils.js'
const moduleName = 'OCPPServiceUtils'
chargingStation,
buildConfigKey(OCPP20ComponentName.FiscalMetering, VendorParametersKey.PublicKey)
)?.value
- const signingMethod = getConfigurationKey(
+ const configuredSigningMethod = getConfigurationKey(
chargingStation,
buildConfigKey(OCPP20ComponentName.FiscalMetering, VendorParametersKey.SigningMethod)
)?.value as SigningMethodEnumType | undefined
- signingConfig = {
- enabled: true,
- meterSerialNumber: chargingStation.stationInfo.meterSerialNumber ?? 'UNKNOWN',
+
+ const prerequisiteResult = validateSigningPrerequisites(
publicKeyHex,
- publicKeySentInTransaction:
- chargingStation.getConnectorStatus(connectorId)?.publicKeySentInTransaction ??
- false,
- publicKeyWithSignedMeterValue: parsePublicKeyWithSignedMeterValue(
- publicKeyWithSignedMeterValueStr
- ),
- signingMethod,
- transactionId,
+ configuredSigningMethod
+ )
+ if (prerequisiteResult.enabled) {
+ signingConfig = {
+ enabled: true,
+ meterSerialNumber: chargingStation.stationInfo.meterSerialNumber ?? 'UNKNOWN',
+ publicKeyHex,
+ publicKeySentInTransaction:
+ chargingStation.getConnectorStatus(connectorId)?.publicKeySentInTransaction ??
+ false,
+ publicKeyWithSignedMeterValue: parsePublicKeyWithSignedMeterValue(
+ publicKeyWithSignedMeterValueStr
+ ),
+ signingMethod: prerequisiteResult.signingMethod,
+ transactionId,
+ }
+ } else {
+ logger.warn(
+ `${chargingStation.logPrefix()} ${moduleName}.buildMeterValue: Signed meter values disabled: ${prerequisiteResult.reason}`
+ )
}
}
}
import {
PublicKeyWithSignedMeterValueEnumType,
type SampledValue,
- type SigningMethodEnumType,
+ SigningMethodEnumType,
} from '../../types/index.js'
export interface SampledValueSigningConfig extends SigningConfig {
signingMethod?: SigningMethodEnumType
}
+// EC curve OID hex → SigningMethodEnumType (OCA Application Note Table 12)
+const EC_CURVE_OID_MAP = new Map<string, SigningMethodEnumType>([
+ ['06052b8104000a', SigningMethodEnumType.ECDSA_secp256k1_SHA256],
+ ['06052b8104001f', SigningMethodEnumType.ECDSA_secp192k1_SHA256],
+ ['06052b81040022', SigningMethodEnumType.ECDSA_secp384r1_SHA256],
+ ['06082a8648ce3d030101', SigningMethodEnumType.ECDSA_secp192r1_SHA256],
+ ['06082a8648ce3d030107', SigningMethodEnumType.ECDSA_secp256r1_SHA256],
+ ['06092b240303020801010b', SigningMethodEnumType.ECDSA_brainpool384r1_SHA256],
+ ['06092b2403030208010107', SigningMethodEnumType.ECDSA_brainpool256r1_SHA256],
+])
+
+export const deriveSigningMethodFromPublicKeyHex = (
+ publicKeyHex: string
+): SigningMethodEnumType | undefined => {
+ const hex = publicKeyHex.toLowerCase().replace(/[^0-9a-f]/g, '')
+ for (const [oid, method] of EC_CURVE_OID_MAP) {
+ if (hex.includes(oid)) {
+ return method
+ }
+ }
+ return undefined
+}
+
+export interface SigningPrerequisiteResult {
+ enabled: false
+ reason: string
+}
+
+export interface SigningPrerequisiteSuccess {
+ enabled: true
+ signingMethod: SigningMethodEnumType
+}
+
+export const validateSigningPrerequisites = (
+ publicKeyHex: string | undefined,
+ configuredSigningMethod: SigningMethodEnumType | undefined
+): SigningPrerequisiteResult | SigningPrerequisiteSuccess => {
+ if (publicKeyHex == null || publicKeyHex.length === 0) {
+ return { enabled: false, reason: 'Public key is not configured' }
+ }
+
+ const derivedMethod = deriveSigningMethodFromPublicKeyHex(publicKeyHex)
+
+ if (derivedMethod == null) {
+ return {
+ enabled: false,
+ reason: 'Cannot derive EC curve from public key hex — unsupported or malformed ASN.1 key',
+ }
+ }
+
+ if (configuredSigningMethod != null && configuredSigningMethod !== derivedMethod) {
+ return {
+ enabled: false,
+ reason:
+ `SigningMethod mismatch: configured '${configuredSigningMethod}' ` +
+ `but public key uses '${derivedMethod}'`,
+ }
+ }
+
+ return { enabled: true, signingMethod: configuredSigningMethod ?? derivedMethod }
+}
+
const PUBLIC_KEY_WITH_SIGNED_METER_VALUE_VALUES = new Set<string>(
Object.values(PublicKeyWithSignedMeterValueEnumType)
)
import assert from 'node:assert/strict'
import { describe, it } from 'node:test'
-import { shouldIncludePublicKey } from '../../../src/charging-station/ocpp/OCPPSignedMeterValueUtils.js'
-import { PublicKeyWithSignedMeterValueEnumType } from '../../../src/types/index.js'
+import {
+ deriveSigningMethodFromPublicKeyHex,
+ shouldIncludePublicKey,
+ validateSigningPrerequisites,
+} from '../../../src/charging-station/ocpp/OCPPSignedMeterValueUtils.js'
+import {
+ PublicKeyWithSignedMeterValueEnumType,
+ SigningMethodEnumType,
+} from '../../../src/types/index.js'
+import { TEST_PUBLIC_KEY_HEX } from '../ChargingStationTestConstants.js'
await describe('SignedMeterValueUtils', async () => {
await describe('PublicKeyWithSignedMeterValueEnumType', async () => {
)
})
})
+
+ await describe('deriveSigningMethodFromPublicKeyHex', async () => {
+ await it('should derive secp256k1 from OCA spec §5.3 public key', () => {
+ assert.strictEqual(
+ deriveSigningMethodFromPublicKeyHex(TEST_PUBLIC_KEY_HEX),
+ SigningMethodEnumType.ECDSA_secp256k1_SHA256
+ )
+ })
+
+ await it('should derive secp256r1 from key with OID 06082a8648ce3d030107', () => {
+ const secp256r1Key = '3059301306072a8648ce3d020106082a8648ce3d03010703420004abcd'
+ assert.strictEqual(
+ deriveSigningMethodFromPublicKeyHex(secp256r1Key),
+ SigningMethodEnumType.ECDSA_secp256r1_SHA256
+ )
+ })
+
+ await it('should handle mixed case hex', () => {
+ assert.strictEqual(
+ deriveSigningMethodFromPublicKeyHex(TEST_PUBLIC_KEY_HEX.toUpperCase()),
+ SigningMethodEnumType.ECDSA_secp256k1_SHA256
+ )
+ })
+
+ await it('should return undefined for unrecognized key', () => {
+ assert.strictEqual(deriveSigningMethodFromPublicKeyHex('deadbeef'), undefined)
+ })
+
+ await it('should return undefined for empty string', () => {
+ assert.strictEqual(deriveSigningMethodFromPublicKeyHex(''), undefined)
+ })
+ })
+
+ await describe('validateSigningPrerequisites', async () => {
+ await it('should return enabled with derived method when no configured method', () => {
+ const result = validateSigningPrerequisites(TEST_PUBLIC_KEY_HEX, undefined)
+ assert.strictEqual(result.enabled, true)
+ assert.strictEqual(
+ (result as { signingMethod: SigningMethodEnumType }).signingMethod,
+ SigningMethodEnumType.ECDSA_secp256k1_SHA256
+ )
+ })
+
+ await it('should return enabled when configured method matches key curve', () => {
+ const result = validateSigningPrerequisites(
+ TEST_PUBLIC_KEY_HEX,
+ SigningMethodEnumType.ECDSA_secp256k1_SHA256
+ )
+ assert.strictEqual(result.enabled, true)
+ })
+
+ await it('should return disabled when public key is undefined', () => {
+ const result = validateSigningPrerequisites(undefined, undefined)
+ assert.strictEqual(result.enabled, false)
+ assert.ok((result as { reason: string }).reason.includes('Public key'))
+ })
+
+ await it('should return disabled when public key is empty', () => {
+ const result = validateSigningPrerequisites('', undefined)
+ assert.strictEqual(result.enabled, false)
+ })
+
+ await it('should return disabled when key has unrecognized curve', () => {
+ const result = validateSigningPrerequisites('deadbeef', undefined)
+ assert.strictEqual(result.enabled, false)
+ assert.ok((result as { reason: string }).reason.includes('Cannot derive'))
+ })
+
+ await it('should return disabled when configured method mismatches key curve', () => {
+ const result = validateSigningPrerequisites(
+ TEST_PUBLIC_KEY_HEX,
+ SigningMethodEnumType.ECDSA_secp256r1_SHA256
+ )
+ assert.strictEqual(result.enabled, false)
+ assert.ok((result as { reason: string }).reason.includes('mismatch'))
+ })
+ })
})