UNPKG

@citrineos/util

Version:

The OCPP util module which supplies helpful utilities like cache and queue connectors, etc.

135 lines 6.67 kB
import { sequelize } from '@citrineos/data'; import { OCPP2_0_1, SignedMeterValuesConfig } from '@citrineos/base'; import { Logger } from 'tslog'; import * as crypto from 'node:crypto'; import { stringToArrayBuffer } from 'pvutils'; /** * Util to process and validate signed meter values. */ export class SignedMeterValuesUtil { _fileStorage; _logger; _chargingStationSecurityInfoRepository; _signedMeterValuesConfiguration; /** * @param {IFileStorage} [fileStorage] - The `fileStorage` allows access to the configured file storage. * * @param {BootstrapConfig & SystemConfig} config - The `config` contains the current system configuration settings. * * @param {Logger<ILogObj>} [logger] - The `logger` represents an instance of {@link Logger<ILogObj>}. * */ constructor(fileStorage, config, logger) { this._fileStorage = fileStorage; this._logger = logger; this._chargingStationSecurityInfoRepository = new sequelize.SequelizeChargingStationSecurityInfoRepository(config, logger); this._signedMeterValuesConfiguration = config.modules.transactions.signedMeterValuesConfiguration; } /** * Checks the validity of a meter value. * * If a meter value is unsigned, it is valid. * * If a meter value is signed, it is valid if: * - SignedMeterValuesConfig is configured * AND * - The incoming signed meter value's signing method matches the configured signing method * AND * - The incoming signed meter value's public key is empty but there is a public key stored for that charging station * OR * - The incoming signed meter value's public key isn't empty and it matches the configured public key * * @param stationId - The charging station the meter values belong to * @param meterValues - The list of meter values */ async validateMeterValues(tenantId, stationId, meterValues) { for (const meterValue of meterValues) { for (const sampledValue of meterValue.sampledValue) { if (sampledValue.signedMeterValue) { const validMeterValues = await this.validateSignedSampledValue(tenantId, stationId, sampledValue.signedMeterValue); if (!validMeterValues) { return false; } } } } return true; } async validateSignedSampledValue(tenantId, stationId, signedMeterValue) { if (signedMeterValue.publicKey && signedMeterValue.publicKey.length > 0) { const incomingPublicKeyIsValid = await this.validateSignedMeterValueSignature(signedMeterValue); if (this._signedMeterValuesConfiguration && incomingPublicKeyIsValid) { await this._chargingStationSecurityInfoRepository.readOrCreateChargingStationInfo(tenantId, stationId, this._signedMeterValuesConfiguration.publicKeyFileId); return true; } else { return false; } } else { const chargingStationPublicKeyFileId = await this._chargingStationSecurityInfoRepository.readChargingStationPublicKeyFileId(tenantId, stationId); return await this.validateSignedMeterValueSignature(signedMeterValue, chargingStationPublicKeyFileId); } } async validateSignedMeterValueSignature(signedMeterValue, publicKeyFileId) { const incomingPublicKeyString = signedMeterValue.publicKey; const signingMethod = signedMeterValue.signingMethod; if (!this._signedMeterValuesConfiguration?.publicKeyFileId) { this._logger.warn('Invalid signature because public key is missing from system config.'); return false; } if (publicKeyFileId && publicKeyFileId !== this._signedMeterValuesConfiguration?.publicKeyFileId) { this._logger.warn('Invalid signature because incoming public key does not match configured public key.'); return false; } if (!publicKeyFileId && incomingPublicKeyString.length === 0) { this._logger.warn('Invalid signature because no configured public key and incoming signed meter values has no public key.'); return false; } if (this._signedMeterValuesConfiguration?.signingMethod !== signingMethod) { this._logger.warn('Invalid signature because incoming signing method does not match configured signing method.'); return false; } const configuredPublicKey = this.formatKey(await this._fileStorage.getFile(this._signedMeterValuesConfiguration.publicKeyFileId)); if (incomingPublicKeyString.length > 0) { const signedMeterValuePublicKey = Buffer.from(signedMeterValue.publicKey, 'base64').toString(); if (configuredPublicKey !== signedMeterValuePublicKey) { return false; } } switch (signingMethod) { case 'RSASSA-PKCS1-v1_5': return await this.validateRsaSignature(configuredPublicKey, signingMethod, signedMeterValue.encodingMethod, signedMeterValue.signedMeterData); default: this._logger.warn(`${signingMethod} is not supported for Signed Meter Values.`); return false; } } async validateRsaSignature(configuredPublicKey, signingMethod, encodingMethod, signatureData) { try { const cryptoPublicKey = await crypto.subtle.importKey('spki', stringToArrayBuffer(atob(configuredPublicKey)), { name: signingMethod, hash: encodingMethod }, true, ['verify']); const signatureBuffer = Buffer.from(signatureData, 'base64'); // For now, we only care that the signature could be read, regardless of the value in the signature. await crypto.subtle.verify(signingMethod, cryptoPublicKey, signatureBuffer, signatureBuffer); return true; } catch (e) { const errorMessage = e instanceof DOMException ? e.message : JSON.stringify(e); this._logger.warn(`Error decrypting public key or verifying signature from Signed Meter Value. Error: ${errorMessage}`); return false; } } formatKey(key) { if (!key) { throw new Error('Public key file is missing.'); } return key .replace('-----BEGIN PUBLIC KEY-----', '') .replace('-----END PUBLIC KEY-----', '') .replace(/(\r\n|\n|\r)/gm, ''); } } //# sourceMappingURL=SignedMeterValuesUtil.js.map