From ce0a0287952fb2091f40349d9ddef1d855bb28ca Mon Sep 17 00:00:00 2001 From: Elliot Sabitov <> Date: Fri, 18 Oct 2024 15:28:27 -0400 Subject: [PATCH 1/3] feat: wip initial commit for adjusting implementation such that a GetInstalledCertificateIds message is triggered after a successful InstallCertificate response is received from the charger --- 00_Base/src/index.ts | 1 + 00_Base/src/interfaces/repository.ts | 7 ++ 00_Base/src/ocpp/persistence/namespace.ts | 1 + 01_Data/src/index.ts | 3 + 01_Data/src/interfaces/repositories.ts | 61 ++++++++++----- 01_Data/src/layers/sequelize/index.ts | 3 +- .../model/Certificate/Certificate.ts | 5 ++ .../model/Certificate/InstalledCertificate.ts | 55 +++++++++++++ .../sequelize/model/Certificate/index.ts | 1 + .../src/layers/sequelize/repository/Base.ts | 4 + .../repository/InstalledCertificate.ts | 16 ++++ 03_Modules/Certificates/src/module/api.ts | 3 +- 03_Modules/Certificates/src/module/module.ts | 77 ++++++++++++++++++- 13 files changed, 212 insertions(+), 25 deletions(-) create mode 100644 01_Data/src/layers/sequelize/model/Certificate/InstalledCertificate.ts create mode 100644 01_Data/src/layers/sequelize/repository/InstalledCertificate.ts diff --git a/00_Base/src/index.ts b/00_Base/src/index.ts index e95cbc66..9cb38506 100644 --- a/00_Base/src/index.ts +++ b/00_Base/src/index.ts @@ -244,3 +244,4 @@ export { assert, notNull, deepDirectionalEqual } from './assertion/assertion'; export { UnauthorizedError } from './interfaces/api/exception/UnauthorizedError'; export { AuthorizationSecurity } from './interfaces/api/AuthorizationSecurity'; export { Ajv }; +export declare type Constructable = new (...args: any[]) => T; diff --git a/00_Base/src/interfaces/repository.ts b/00_Base/src/interfaces/repository.ts index c7daa439..0121efff 100644 --- a/00_Base/src/interfaces/repository.ts +++ b/00_Base/src/interfaces/repository.ts @@ -63,6 +63,12 @@ export abstract class CrudRepository extends EventEmitter { return result; } + public async bulkCreate(values: T[], namespace?: string): Promise { + const result = await this._bulkCreate(values, namespace); + this.emit('created', result); + return result; + } + /** * Creates a new entry in the database with the specified value and key. * If a namespace is provided, the entry will be created within that namespace. @@ -250,6 +256,7 @@ export abstract class CrudRepository extends EventEmitter { abstract existByQuery(query: object, namespace?: string): Promise; protected abstract _create(value: T, namespace?: string): Promise; + protected abstract _bulkCreate(value: T[], namespace?: string): Promise; protected abstract _createByKey( value: T, diff --git a/00_Base/src/ocpp/persistence/namespace.ts b/00_Base/src/ocpp/persistence/namespace.ts index e34bf855..bcc60482 100644 --- a/00_Base/src/ocpp/persistence/namespace.ts +++ b/00_Base/src/ocpp/persistence/namespace.ts @@ -12,6 +12,7 @@ export enum Namespace { AuthorizationRestrictions = 'AuthorizationRestrictions', BootConfig = 'Boot', Certificate = 'Certificate', + InstalledCertificate = 'InstalledCertificate', CertificateChain = 'CertificateChain', ChargingNeeds = 'ChargingNeeds', ChargingProfile = 'ChargingProfile', diff --git a/01_Data/src/index.ts b/01_Data/src/index.ts index 4ad3ac59..a2296e47 100644 --- a/01_Data/src/index.ts +++ b/01_Data/src/index.ts @@ -4,6 +4,7 @@ // SPDX-License-Identifier: Apache 2.0 import { Transaction as SequelizeTransaction } from 'sequelize'; + export { SequelizeTransaction }; export { IdTokenAdditionalInfo } from './layers/sequelize/model/Authorization/IdTokenAdditionalInfo'; export * as sequelize from './layers/sequelize'; @@ -32,6 +33,7 @@ export { VariableCharacteristics, VariableStatus, Certificate, + InstalledCertificate, CountryNameEnumType, TransactionEvent, IdToken, @@ -47,6 +49,7 @@ export { SequelizeBootRepository, SequelizeCallMessageRepository, SequelizeCertificateRepository, + SequelizeInstalledCertificateRepository, SequelizeChargingProfileRepository, SequelizeChargingStationSecurityInfoRepository, SequelizeDeviceModelRepository, diff --git a/01_Data/src/interfaces/repositories.ts b/01_Data/src/interfaces/repositories.ts index 4cc31527..9aab750d 100644 --- a/01_Data/src/interfaces/repositories.ts +++ b/01_Data/src/interfaces/repositories.ts @@ -4,53 +4,74 @@ // SPDX-License-Identifier: Apache 2.0 import { - MessageInfoType, type AuthorizationData, type BootConfig, type CallAction, + ChargingLimitSourceEnumType, + ChargingProfilePurposeEnumType, + ChargingProfileType, type ChargingStateEnumType, type ComponentType, + CompositeScheduleType, + type CrudRepository, type EventDataType, type EVSEType, type GetVariableResultType, - type CrudRepository, type IdTokenType, + MessageInfoType, + MeterValueType, type MonitoringDataType, + NotifyEVChargingNeedsRequest, type RegistrationStatusEnumType, type ReportDataType, + ReserveNowRequest, type SecurityEventNotificationRequest, type SetMonitoringDataType, type SetMonitoringResultType, type SetVariableDataType, type SetVariableResultType, type StatusInfoType, + StatusNotificationRequest, type TransactionEventRequest, + UpdateEnumType, type VariableAttributeType, type VariableMonitoringType, type VariableType, - ChargingProfileType, - ChargingProfilePurposeEnumType, - NotifyEVChargingNeedsRequest, - ChargingLimitSourceEnumType, - CompositeScheduleType, - StatusNotificationRequest, - ReserveNowRequest, - MeterValueType, - UpdateEnumType, } from '@citrineos/base'; import { type AuthorizationQuerystring } from './queries/Authorization'; -import { CallMessage, ChargingStationSecurityInfo, ChargingStationSequence, ChargingStationSequenceType, CompositeSchedule, MeterValue, type Transaction, VariableCharacteristics } from '../layers/sequelize'; -import { type VariableAttribute } from '../layers/sequelize'; +import { + type Authorization, + type Boot, + CallMessage, + type Certificate, + ChargingNeeds, + ChargingProfile, + type ChargingStation, + ChargingStationSecurityInfo, + ChargingStationSequence, + ChargingStationSequenceType, + type Component, + CompositeSchedule, + type EventData, + Evse, + type Location, + MessageInfo, + MeterValue, + Reservation, + type SecurityEvent, + Subscription, + Tariff, + type Transaction, + type Variable, + type VariableAttribute, + VariableCharacteristics, + type VariableMonitoring, +} from '../layers/sequelize'; import { type AuthorizationRestrictions, type VariableAttributeQuerystring } from '.'; -import { type Authorization, type Boot, type Certificate, ChargingNeeds, type ChargingStation, type Component, type EventData, Evse, type Location, type SecurityEvent, type Variable, type VariableMonitoring } from '../layers/sequelize'; -import { MessageInfo } from '../layers/sequelize'; -import { Subscription } from '../layers/sequelize'; -import { Tariff } from '../layers/sequelize'; import { TariffQueryString } from './queries/Tariff'; -import { ChargingProfile } from '../layers/sequelize'; -import { Reservation } from '../layers/sequelize'; import { LocalListVersion } from '../layers/sequelize/model/Authorization/LocalListVersion'; import { SendLocalList } from '../layers/sequelize/model/Authorization/SendLocalList'; +import { InstalledCertificate } from '../layers/sequelize/model/Certificate/InstalledCertificate'; export interface IAuthorizationRepository extends CrudRepository { createOrUpdateByQuerystring: (value: AuthorizationData, query: AuthorizationQuerystring) => Promise; @@ -170,6 +191,8 @@ export interface ICertificateRepository extends CrudRepository { createOrUpdateCertificate(certificate: Certificate): Promise; } +export interface IInstalledCertificateRepository extends CrudRepository {} + export interface IChargingProfileRepository extends CrudRepository { createOrUpdateChargingProfile(chargingProfile: ChargingProfileType, stationId: string, evseId?: number | null, chargingLimitSource?: ChargingLimitSourceEnumType, isActive?: boolean): Promise; createChargingNeeds(chargingNeeds: NotifyEVChargingNeedsRequest, stationId: string): Promise; diff --git a/01_Data/src/layers/sequelize/index.ts b/01_Data/src/layers/sequelize/index.ts index 268b8dd0..8ad0539e 100644 --- a/01_Data/src/layers/sequelize/index.ts +++ b/01_Data/src/layers/sequelize/index.ts @@ -14,7 +14,7 @@ export { ChargingStationSequence, ChargingStationSequenceType } from './model/Ch export { MessageInfo } from './model/MessageInfo'; export { Tariff } from './model/Tariff'; export { Subscription } from './model/Subscription'; -export { Certificate, SignatureAlgorithmEnumType, CountryNameEnumType } from './model/Certificate'; +export { Certificate, SignatureAlgorithmEnumType, CountryNameEnumType, InstalledCertificate } from './model/Certificate'; export { ChargingProfile, ChargingNeeds, ChargingSchedule, CompositeSchedule, SalesTariff } from './model/ChargingProfile'; export { CallMessage } from './model/CallMessage'; export { Reservation } from './model/Reservation'; @@ -34,6 +34,7 @@ export { SequelizeMessageInfoRepository } from './repository/MessageInfo'; export { SequelizeTariffRepository } from './repository/Tariff'; export { SequelizeSubscriptionRepository } from './repository/Subscription'; export { SequelizeCertificateRepository } from './repository/Certificate'; +export { SequelizeInstalledCertificateRepository } from './repository/InstalledCertificate'; export { SequelizeChargingProfileRepository } from './repository/ChargingProfile'; export { SequelizeCallMessageRepository } from './repository/CallMessage'; export { SequelizeReservationRepository } from './repository/Reservation'; diff --git a/01_Data/src/layers/sequelize/model/Certificate/Certificate.ts b/01_Data/src/layers/sequelize/model/Certificate/Certificate.ts index 9d9f3d3b..c01417eb 100644 --- a/01_Data/src/layers/sequelize/model/Certificate/Certificate.ts +++ b/01_Data/src/layers/sequelize/model/Certificate/Certificate.ts @@ -26,6 +26,11 @@ export class Certificate extends Model { }) declare issuerName: string; + @Column({ + type: DataType.STRING, + }) + declare issuerKey: string; + @Column(DataType.STRING) declare organizationName: string; diff --git a/01_Data/src/layers/sequelize/model/Certificate/InstalledCertificate.ts b/01_Data/src/layers/sequelize/model/Certificate/InstalledCertificate.ts new file mode 100644 index 00000000..930c3881 --- /dev/null +++ b/01_Data/src/layers/sequelize/model/Certificate/InstalledCertificate.ts @@ -0,0 +1,55 @@ +import { GetCertificateIdUseEnumType, Namespace } from '@citrineos/base'; +import { Column, DataType, ForeignKey, Model, Table } from 'sequelize-typescript'; +import { ChargingStation } from '../Location'; + +@Table +export class InstalledCertificate extends Model { + static readonly MODEL_NAME: string = Namespace.InstalledCertificate; + + @ForeignKey(() => ChargingStation) + @Column({ + type: DataType.STRING(36), + allowNull: false, + }) + declare stationId: string; + + @Column({ + type: DataType.STRING, + allowNull: false, + }) + declare hashAlgorithm: string; + + @Column({ + type: DataType.STRING, + allowNull: false, + }) + declare issuerNameHash: string; + + @Column({ + type: DataType.STRING, + allowNull: false, + }) + declare issuerKeyHash: string; + + @Column({ + type: DataType.STRING, + allowNull: false, + }) + declare serialNumber: string; + + @Column({ + type: DataType.ENUM('V2GRootCertificate', 'MORootCertificate', 'CSMSRootCertificate', 'V2GCertificateChain', 'ManufacturerRootCertificate'), + allowNull: false, + }) + declare certificateType: GetCertificateIdUseEnumType; + + constructor(stationId: string, hashAlgorithm: string, issuerNameHash: string, issuerKeyHash: string, serialNumber: string, certificateType: GetCertificateIdUseEnumType) { + super(); + this.stationId = stationId; + this.hashAlgorithm = hashAlgorithm; + this.issuerNameHash = issuerNameHash; + this.issuerKeyHash = issuerKeyHash; + this.serialNumber = serialNumber; + this.certificateType = certificateType; + } +} diff --git a/01_Data/src/layers/sequelize/model/Certificate/index.ts b/01_Data/src/layers/sequelize/model/Certificate/index.ts index 9a6c4c78..ff4d9ced 100644 --- a/01_Data/src/layers/sequelize/model/Certificate/index.ts +++ b/01_Data/src/layers/sequelize/model/Certificate/index.ts @@ -3,6 +3,7 @@ // SPDX-License-Identifier: Apache 2.0 export { Certificate } from './Certificate'; +export { InstalledCertificate } from './InstalledCertificate'; export const enum SignatureAlgorithmEnumType { RSA = 'SHA256withRSA', diff --git a/01_Data/src/layers/sequelize/repository/Base.ts b/01_Data/src/layers/sequelize/repository/Base.ts index 46184b52..ac5a7b5f 100644 --- a/01_Data/src/layers/sequelize/repository/Base.ts +++ b/01_Data/src/layers/sequelize/repository/Base.ts @@ -60,6 +60,10 @@ export class SequelizeRepository> extends CrudReposito return await value.save(); } + protected async _bulkCreate(values: T[], clazz: Model): Promise { + return await (clazz as unknown as ModelStatic).bulkCreate(values as any); + } + protected async _createByKey(value: T, key: string): Promise { const primaryKey = this.s.models[this.namespace].primaryKeyAttribute; value.setDataValue(primaryKey, key); diff --git a/01_Data/src/layers/sequelize/repository/InstalledCertificate.ts b/01_Data/src/layers/sequelize/repository/InstalledCertificate.ts new file mode 100644 index 00000000..1347ffc4 --- /dev/null +++ b/01_Data/src/layers/sequelize/repository/InstalledCertificate.ts @@ -0,0 +1,16 @@ +// Copyright Contributors to the CitrineOS Project +// +// SPDX-License-Identifier: Apache 2.0 + +import { SequelizeRepository } from './Base'; +import { IInstalledCertificateRepository } from '../../../interfaces'; +import { SystemConfig } from '@citrineos/base'; +import { Sequelize } from 'sequelize-typescript'; +import { ILogObj, Logger } from 'tslog'; +import { InstalledCertificate } from '../model/Certificate/InstalledCertificate'; + +export class SequelizeInstalledCertificateRepository extends SequelizeRepository implements IInstalledCertificateRepository { + constructor(config: SystemConfig, logger?: Logger, sequelizeInstance?: Sequelize) { + super(config, InstalledCertificate.MODEL_NAME, logger, sequelizeInstance); + } +} diff --git a/03_Modules/Certificates/src/module/api.ts b/03_Modules/Certificates/src/module/api.ts index e52f6faf..daf73533 100644 --- a/03_Modules/Certificates/src/module/api.ts +++ b/03_Modules/Certificates/src/module/api.ts @@ -22,6 +22,7 @@ import { Namespace, WebsocketServerConfig, } from '@citrineos/base'; +import jsrsasign, { KEYUTIL } from 'jsrsasign'; import { FastifyInstance, FastifyRequest } from 'fastify'; import { ILogObj, Logger } from 'tslog'; import { ICertificatesModuleApi } from './interface'; @@ -46,7 +47,6 @@ import { } from '@citrineos/data'; import fs from 'fs'; import moment from 'moment'; -import jsrsasign from 'jsrsasign'; const enum PemType { Root = 'Root', @@ -605,6 +605,7 @@ export class CertificatesModuleApi const certObj = new jsrsasign.X509(); certObj.readCertPEM(certPem); certificateEntity.issuerName = certObj.getIssuerString(); + certificateEntity.issuerKey = KEYUTIL.getPEM(certObj.getPublicKey()); return await this._module.certificateRepository.createOrUpdateCertificate( certificateEntity, ); diff --git a/03_Modules/Certificates/src/module/module.ts b/03_Modules/Certificates/src/module/module.ts index c846032d..4672ee17 100644 --- a/03_Modules/Certificates/src/module/module.ts +++ b/03_Modules/Certificates/src/module/module.ts @@ -8,6 +8,7 @@ import { AsHandler, AttributeEnumType, CallAction, + CertificateHashDataChainType, CertificateSignedRequest, CertificateSignedResponse, CertificateSigningUseEnumType, @@ -20,6 +21,7 @@ import { GetCertificateStatusEnumType, GetCertificateStatusRequest, GetCertificateStatusResponse, + GetInstalledCertificateIdsRequest, GetInstalledCertificateIdsResponse, HandlerProperties, ICache, @@ -27,7 +29,9 @@ import { IMessageHandler, IMessageSender, InstallCertificateResponse, + InstallCertificateStatusEnumType, Iso15118EVCertificateStatusEnumType, + Namespace, SignCertificateRequest, SignCertificateResponse, SystemConfig, @@ -35,7 +39,9 @@ import { import { ICertificateRepository, IDeviceModelRepository, + IInstalledCertificateRepository, ILocationRepository, + InstalledCertificate, sequelize, } from '@citrineos/data'; import { @@ -81,6 +87,7 @@ export class CertificatesModule extends AbstractModule { protected _deviceModelRepository: IDeviceModelRepository; protected _certificateRepository: ICertificateRepository; + protected _installedCertificateRepository: IInstalledCertificateRepository; protected _locationRepository: ILocationRepository; protected _certificateAuthorityService: CertificateAuthorityService; @@ -153,6 +160,8 @@ export class CertificatesModule extends AbstractModule { this._certificateRepository = certificateRepository || new sequelize.SequelizeCertificateRepository(config, logger); + this._installedCertificateRepository = + new sequelize.SequelizeInstalledCertificateRepository(config, logger); this._locationRepository = locationRepository || new sequelize.SequelizeLocationRepository(config, logger); @@ -320,19 +329,79 @@ export class CertificatesModule extends AbstractModule { } @AsHandler(CallAction.GetInstalledCertificateIds) - protected _handleGetInstalledCertificateIds( + protected async _handleGetInstalledCertificateIds( message: IMessage, props?: HandlerProperties, - ): void { + ): Promise { this._logger.debug('GetInstalledCertificateIds received:', message, props); + const certificateHashDataList: CertificateHashDataChainType[] = + message.payload.certificateHashDataChain!; + // persist installed certificate information + if (certificateHashDataList && certificateHashDataList.length > 0) { + // delete previous hashes for station + try { + await this._installedCertificateRepository.deleteAllByQuery({ + where: { + stationId: message.context.stationId, + }, + }); + } catch (error: any) { + this._logger.error( + 'GetInstalledCertificateIds failed to delete previous certificates', + error, + ); + } + // save new hashes + const records = certificateHashDataList.map( + (certificateHashDataWrap: CertificateHashDataChainType) => { + const certificateHashData = + certificateHashDataWrap.certificateHashData; + const certificateType = certificateHashDataWrap.certificateType; + return new InstalledCertificate( + message.context.stationId, + certificateHashData.hashAlgorithm, + certificateHashData.issuerNameHash, + certificateHashData.issuerKeyHash, + certificateHashData.serialNumber, + certificateType, + ); + }, + ); + const response = await this._installedCertificateRepository.bulkCreate( + records, + Namespace.InstalledCertificate, + ); + if (response.length === records.length) { + console.log( + 'Successfully updated installed certificate information for station', + message.context.stationId, + ); + } + } } @AsHandler(CallAction.InstallCertificate) - protected _handleInstallCertificate( + protected async _handleInstallCertificate( message: IMessage, props?: HandlerProperties, - ): void { + ): Promise { this._logger.debug('InstallCertificate received:', message, props); + // if response is successful, then trigger the get installed certificates ids + if (message.payload.status === InstallCertificateStatusEnumType.Accepted) { + try { + await this.sendCall( + message.context.stationId, + message.context.tenantId, + CallAction.GetInstalledCertificateIds, + {} as GetInstalledCertificateIdsRequest, + ); + } catch (e: any) { + console.error( + `There was an error sending GetInstalledCertificateIds message to the charger after installing certificate ${e.message}`, + e, + ); + } + } } private async _verifySignCertRequest( From 93693a5525bdde702e3f0397278369011e995c27 Mon Sep 17 00:00:00 2001 From: Elliot Sabitov <> Date: Fri, 18 Oct 2024 23:11:19 -0400 Subject: [PATCH 2/3] feat: handling Not Found or empty certificate set in GetInstalledCertificateIds handler and adjusting delete handler to re-trigger getInstalledCertificateIds so that we can keep latest state in sync with DB --- 00_Base/src/interfaces/repository.ts | 7 +- .../model/Certificate/InstalledCertificate.ts | 10 --- .../src/layers/sequelize/repository/Base.ts | 4 +- 01_Data/src/layers/sequelize/util.ts | 2 + 03_Modules/Certificates/src/module/module.ts | 68 ++++++++++++++++--- 5 files changed, 66 insertions(+), 25 deletions(-) diff --git a/00_Base/src/interfaces/repository.ts b/00_Base/src/interfaces/repository.ts index 0121efff..7dc2edbc 100644 --- a/00_Base/src/interfaces/repository.ts +++ b/00_Base/src/interfaces/repository.ts @@ -63,8 +63,11 @@ export abstract class CrudRepository extends EventEmitter { return result; } - public async bulkCreate(values: T[], namespace?: string): Promise { - const result = await this._bulkCreate(values, namespace); + public async bulkCreate( + values: T[], + clazz: any, // todo: should Model be in base so it can be used here? + ): Promise { + const result = await this._bulkCreate(values, clazz); this.emit('created', result); return result; } diff --git a/01_Data/src/layers/sequelize/model/Certificate/InstalledCertificate.ts b/01_Data/src/layers/sequelize/model/Certificate/InstalledCertificate.ts index 930c3881..a8524d65 100644 --- a/01_Data/src/layers/sequelize/model/Certificate/InstalledCertificate.ts +++ b/01_Data/src/layers/sequelize/model/Certificate/InstalledCertificate.ts @@ -42,14 +42,4 @@ export class InstalledCertificate extends Model { allowNull: false, }) declare certificateType: GetCertificateIdUseEnumType; - - constructor(stationId: string, hashAlgorithm: string, issuerNameHash: string, issuerKeyHash: string, serialNumber: string, certificateType: GetCertificateIdUseEnumType) { - super(); - this.stationId = stationId; - this.hashAlgorithm = hashAlgorithm; - this.issuerNameHash = issuerNameHash; - this.issuerKeyHash = issuerKeyHash; - this.serialNumber = serialNumber; - this.certificateType = certificateType; - } } diff --git a/01_Data/src/layers/sequelize/repository/Base.ts b/01_Data/src/layers/sequelize/repository/Base.ts index ac5a7b5f..52537746 100644 --- a/01_Data/src/layers/sequelize/repository/Base.ts +++ b/01_Data/src/layers/sequelize/repository/Base.ts @@ -60,8 +60,8 @@ export class SequelizeRepository> extends CrudReposito return await value.save(); } - protected async _bulkCreate(values: T[], clazz: Model): Promise { - return await (clazz as unknown as ModelStatic).bulkCreate(values as any); + protected async _bulkCreate(values: T[]): Promise { + return await (this.s.models[this.namespace] as ModelStatic).bulkCreate(values as any); } protected async _createByKey(value: T, key: string): Promise { diff --git a/01_Data/src/layers/sequelize/util.ts b/01_Data/src/layers/sequelize/util.ts index 0579ab1a..c1199a78 100644 --- a/01_Data/src/layers/sequelize/util.ts +++ b/01_Data/src/layers/sequelize/util.ts @@ -26,6 +26,7 @@ import { Evse, IdToken, IdTokenInfo, + InstalledCertificate, Location, MeterValue, Reservation, @@ -117,6 +118,7 @@ export class DefaultSequelizeInstance { Boot, CallMessage, Certificate, + InstalledCertificate, ChargingNeeds, ChargingProfile, ChargingSchedule, diff --git a/03_Modules/Certificates/src/module/module.ts b/03_Modules/Certificates/src/module/module.ts index 4672ee17..0a86a13d 100644 --- a/03_Modules/Certificates/src/module/module.ts +++ b/03_Modules/Certificates/src/module/module.ts @@ -13,6 +13,7 @@ import { CertificateSignedResponse, CertificateSigningUseEnumType, DeleteCertificateResponse, + DeleteCertificateStatusEnumType, ErrorCode, EventGroup, GenericStatusEnumType, @@ -23,6 +24,7 @@ import { GetCertificateStatusResponse, GetInstalledCertificateIdsRequest, GetInstalledCertificateIdsResponse, + GetInstalledCertificateStatusEnumType, HandlerProperties, ICache, IMessage, @@ -321,11 +323,27 @@ export class CertificatesModule extends AbstractModule { } @AsHandler(CallAction.DeleteCertificate) - protected _handleDeleteCertificate( + protected async _handleDeleteCertificate( message: IMessage, props?: HandlerProperties, - ): void { + ): Promise { this._logger.debug('DeleteCertificate received:', message, props); + // if response is successful, then trigger the get installed certificates ids + if (message.payload.status === DeleteCertificateStatusEnumType.Accepted) { + try { + await this.sendCall( + message.context.stationId, + message.context.tenantId, + CallAction.GetInstalledCertificateIds, + {} as GetInstalledCertificateIdsRequest, + ); + } catch (e: any) { + console.error( + `There was an error sending GetInstalledCertificateIds message to the charger after installing certificate ${e.message}`, + e, + ); + } + } } @AsHandler(CallAction.GetInstalledCertificateIds) @@ -357,26 +375,54 @@ export class CertificatesModule extends AbstractModule { const certificateHashData = certificateHashDataWrap.certificateHashData; const certificateType = certificateHashDataWrap.certificateType; - return new InstalledCertificate( - message.context.stationId, - certificateHashData.hashAlgorithm, - certificateHashData.issuerNameHash, - certificateHashData.issuerKeyHash, - certificateHashData.serialNumber, - certificateType, - ); + return { + stationId: message.context.stationId, + hashAlgorithm: certificateHashData.hashAlgorithm, + issuerNameHash: certificateHashData.issuerNameHash, + issuerKeyHash: certificateHashData.issuerKeyHash, + serialNumber: certificateHashData.serialNumber, + certificateType: certificateType, + } as InstalledCertificate; }, ); + this._logger.info('Attempting to save', records); const response = await this._installedCertificateRepository.bulkCreate( records, Namespace.InstalledCertificate, ); if (response.length === records.length) { - console.log( + this._logger.info( 'Successfully updated installed certificate information for station', message.context.stationId, ); } + } else if ( + message.payload.status === GetInstalledCertificateStatusEnumType.NotFound + ) { + // charger has no certs + this._logger.info( + 'No certs found on charger, will delete for station', + message.context.stationId, + ); + try { + await this._installedCertificateRepository.deleteAllByQuery( + { + where: { + stationId: message.context.stationId, + }, + }, + Namespace.InstalledCertificate, + ); + this._logger.info( + 'Successfully deleted any previously installed certs for station', + message.context.stationId, + ); + } catch (error: any) { + this._logger.error( + `GetInstalledCertificateIds failed to delete previous certificates: ${error.message}`, + error, + ); + } } } From d5342bc1fb490747002e241b3ae1b822a9e13941 Mon Sep 17 00:00:00 2001 From: Elliot Sabitov <> Date: Fri, 1 Nov 2024 13:12:37 -0400 Subject: [PATCH 3/3] chore: addressing PR feedback, removing issuerKey, using CertificateHashDataType and HashAlgorithmEnumType to better match spec, and adjusting module to not automatically trigger GetInstalledCertificateIds, and when GetInstalledCertificateIds payload comes in from the charger, deleting only installed certificate records with matching certificate types. --- .../model/Certificate/Certificate.ts | 5 - .../model/Certificate/InstalledCertificate.ts | 6 +- 03_Modules/Certificates/src/module/api.ts | 3 +- 03_Modules/Certificates/src/module/module.ts | 108 ++++++------------ 4 files changed, 37 insertions(+), 85 deletions(-) diff --git a/01_Data/src/layers/sequelize/model/Certificate/Certificate.ts b/01_Data/src/layers/sequelize/model/Certificate/Certificate.ts index c01417eb..9d9f3d3b 100644 --- a/01_Data/src/layers/sequelize/model/Certificate/Certificate.ts +++ b/01_Data/src/layers/sequelize/model/Certificate/Certificate.ts @@ -26,11 +26,6 @@ export class Certificate extends Model { }) declare issuerName: string; - @Column({ - type: DataType.STRING, - }) - declare issuerKey: string; - @Column(DataType.STRING) declare organizationName: string; diff --git a/01_Data/src/layers/sequelize/model/Certificate/InstalledCertificate.ts b/01_Data/src/layers/sequelize/model/Certificate/InstalledCertificate.ts index a8524d65..ea2cefab 100644 --- a/01_Data/src/layers/sequelize/model/Certificate/InstalledCertificate.ts +++ b/01_Data/src/layers/sequelize/model/Certificate/InstalledCertificate.ts @@ -1,9 +1,9 @@ -import { GetCertificateIdUseEnumType, Namespace } from '@citrineos/base'; +import { CertificateHashDataType, GetCertificateIdUseEnumType, HashAlgorithmEnumType, Namespace } from '@citrineos/base'; import { Column, DataType, ForeignKey, Model, Table } from 'sequelize-typescript'; import { ChargingStation } from '../Location'; @Table -export class InstalledCertificate extends Model { +export class InstalledCertificate extends Model implements CertificateHashDataType { static readonly MODEL_NAME: string = Namespace.InstalledCertificate; @ForeignKey(() => ChargingStation) @@ -17,7 +17,7 @@ export class InstalledCertificate extends Model { type: DataType.STRING, allowNull: false, }) - declare hashAlgorithm: string; + declare hashAlgorithm: HashAlgorithmEnumType; @Column({ type: DataType.STRING, diff --git a/03_Modules/Certificates/src/module/api.ts b/03_Modules/Certificates/src/module/api.ts index daf73533..a8639b8c 100644 --- a/03_Modules/Certificates/src/module/api.ts +++ b/03_Modules/Certificates/src/module/api.ts @@ -22,7 +22,7 @@ import { Namespace, WebsocketServerConfig, } from '@citrineos/base'; -import jsrsasign, { KEYUTIL } from 'jsrsasign'; +import jsrsasign from 'jsrsasign'; import { FastifyInstance, FastifyRequest } from 'fastify'; import { ILogObj, Logger } from 'tslog'; import { ICertificatesModuleApi } from './interface'; @@ -605,7 +605,6 @@ export class CertificatesModuleApi const certObj = new jsrsasign.X509(); certObj.readCertPEM(certPem); certificateEntity.issuerName = certObj.getIssuerString(); - certificateEntity.issuerKey = KEYUTIL.getPEM(certObj.getPublicKey()); return await this._module.certificateRepository.createOrUpdateCertificate( certificateEntity, ); diff --git a/03_Modules/Certificates/src/module/module.ts b/03_Modules/Certificates/src/module/module.ts index 0a86a13d..50580b0c 100644 --- a/03_Modules/Certificates/src/module/module.ts +++ b/03_Modules/Certificates/src/module/module.ts @@ -13,7 +13,6 @@ import { CertificateSignedResponse, CertificateSigningUseEnumType, DeleteCertificateResponse, - DeleteCertificateStatusEnumType, ErrorCode, EventGroup, GenericStatusEnumType, @@ -22,22 +21,20 @@ import { GetCertificateStatusEnumType, GetCertificateStatusRequest, GetCertificateStatusResponse, - GetInstalledCertificateIdsRequest, GetInstalledCertificateIdsResponse, - GetInstalledCertificateStatusEnumType, HandlerProperties, ICache, IMessage, IMessageHandler, IMessageSender, InstallCertificateResponse, - InstallCertificateStatusEnumType, Iso15118EVCertificateStatusEnumType, Namespace, SignCertificateRequest, SignCertificateResponse, SystemConfig, } from '@citrineos/base'; +import { Op } from 'sequelize'; import { ICertificateRepository, IDeviceModelRepository, @@ -328,22 +325,6 @@ export class CertificatesModule extends AbstractModule { props?: HandlerProperties, ): Promise { this._logger.debug('DeleteCertificate received:', message, props); - // if response is successful, then trigger the get installed certificates ids - if (message.payload.status === DeleteCertificateStatusEnumType.Accepted) { - try { - await this.sendCall( - message.context.stationId, - message.context.tenantId, - CallAction.GetInstalledCertificateIds, - {} as GetInstalledCertificateIdsRequest, - ); - } catch (e: any) { - console.error( - `There was an error sending GetInstalledCertificateIds message to the charger after installing certificate ${e.message}`, - e, - ); - } - } } @AsHandler(CallAction.GetInstalledCertificateIds) @@ -357,18 +338,10 @@ export class CertificatesModule extends AbstractModule { // persist installed certificate information if (certificateHashDataList && certificateHashDataList.length > 0) { // delete previous hashes for station - try { - await this._installedCertificateRepository.deleteAllByQuery({ - where: { - stationId: message.context.stationId, - }, - }); - } catch (error: any) { - this._logger.error( - 'GetInstalledCertificateIds failed to delete previous certificates', - error, - ); - } + await this.deleteExistingMatchingCertificateHashes( + message.context.stationId, + certificateHashDataList, + ); // save new hashes const records = certificateHashDataList.map( (certificateHashDataWrap: CertificateHashDataChainType) => { @@ -396,33 +369,6 @@ export class CertificatesModule extends AbstractModule { message.context.stationId, ); } - } else if ( - message.payload.status === GetInstalledCertificateStatusEnumType.NotFound - ) { - // charger has no certs - this._logger.info( - 'No certs found on charger, will delete for station', - message.context.stationId, - ); - try { - await this._installedCertificateRepository.deleteAllByQuery( - { - where: { - stationId: message.context.stationId, - }, - }, - Namespace.InstalledCertificate, - ); - this._logger.info( - 'Successfully deleted any previously installed certs for station', - message.context.stationId, - ); - } catch (error: any) { - this._logger.error( - `GetInstalledCertificateIds failed to delete previous certificates: ${error.message}`, - error, - ); - } } } @@ -432,22 +378,6 @@ export class CertificatesModule extends AbstractModule { props?: HandlerProperties, ): Promise { this._logger.debug('InstallCertificate received:', message, props); - // if response is successful, then trigger the get installed certificates ids - if (message.payload.status === InstallCertificateStatusEnumType.Accepted) { - try { - await this.sendCall( - message.context.stationId, - message.context.tenantId, - CallAction.GetInstalledCertificateIds, - {} as GetInstalledCertificateIdsRequest, - ); - } catch (e: any) { - console.error( - `There was an error sending GetInstalledCertificateIds message to the charger after installing certificate ${e.message}`, - e, - ); - } - } } private async _verifySignCertRequest( @@ -511,4 +441,32 @@ export class CertificatesModule extends AbstractModule { `Verified SignCertRequest for station ${stationId} successfully.`, ); } + + private async deleteExistingMatchingCertificateHashes( + stationId: string, + certificateHashDataList: CertificateHashDataChainType[], + ) { + try { + const certificateTypes = certificateHashDataList.map( + (certificateHashData) => { + return certificateHashData.certificateType; + }, + ); + if (certificateTypes && certificateTypes.length > 0) { + await this._installedCertificateRepository.deleteAllByQuery({ + where: { + stationId, + certificateType: { + [Op.in]: certificateTypes, + }, + }, + }); + } + } catch (error: any) { + this._logger.error( + 'GetInstalledCertificateIds failed to delete previous certificates', + error, + ); + } + } }