From: Jérôme Benoit Date: Sun, 15 Mar 2026 20:04:35 +0000 (+0100) Subject: fix(ocpp20): remediate 4 conformance findings from cross-audit X-Git-Tag: ocpp-server@v3.1.0~13 X-Git-Url: https://git.piment-noir.org/?a=commitdiff_plain;h=4266b93a48795234d27f2a980cc3cde0b80977ed;p=e-mobility-charging-stations-simulator.git fix(ocpp20): remediate 4 conformance findings from cross-audit - M04.FR.06: Narrow DeleteCertificate guard to V2GCertificateChain type only, allowing legitimate deletion of root certificates (CSMSRoot, ManufacturerRoot, MORootCert, V2GRoot) - B09.FR.05: Use InvalidConfSlot reasonCode per errata 2025-09 when configurationSlot is not in NetworkConfigurationPriority list - B09.FR.04/FR.31: Check AllowSecurityProfileDowngrade variable before rejecting downgrades — allow 3→2 when true, never allow to profile 1 (errata 2025-09 §2.12) - L01.FR.06: Wait for active transactions to end before commencing firmware installation via polling loop in lifecycle simulation --- diff --git a/src/charging-station/ocpp/2.0/OCPP20IncomingRequestService.ts b/src/charging-station/ocpp/2.0/OCPP20IncomingRequestService.ts index 50ca5287..a52418ef 100644 --- a/src/charging-station/ocpp/2.0/OCPP20IncomingRequestService.ts +++ b/src/charging-station/ocpp/2.0/OCPP20IncomingRequestService.ts @@ -1611,6 +1611,7 @@ export class OCPP20IncomingRequestService extends OCPPIncomingRequestService { for (const certChain of installedCertsResult.certificateHashDataChain) { const certHash = certChain.certificateHashData if ( + certChain.certificateType === GetCertificateIdUseEnumType.V2GCertificateChain && certHash.serialNumber === certificateHashData.serialNumber && certHash.issuerNameHash === certificateHashData.issuerNameHash && certHash.issuerKeyHash === certificateHashData.issuerKeyHash @@ -2201,15 +2202,29 @@ export class OCPP20IncomingRequestService extends OCPPIncomingRequestService { const currentSecurityProfile = Number(currentSecurityProfileResults[0]?.attributeValue ?? '0') const newSecurityProfile = commandPayload.connectionData.securityProfile if (newSecurityProfile < currentSecurityProfile) { - logger.warn( - `${chargingStation.logPrefix()} ${moduleName}.handleRequestSetNetworkProfile: Rejected security profile downgrade: ${newSecurityProfile.toString()} < ${currentSecurityProfile.toString()}` - ) - return { - status: SetNetworkProfileStatusEnumType.Rejected, - statusInfo: { - additionalInfo: `Security profile downgrade not allowed: current=${currentSecurityProfile.toString()}, requested=${newSecurityProfile.toString()}`, - reasonCode: ReasonCodeEnumType.NoSecurityDowngrade, + // B09.FR.04 (errata 2025-09): Check AllowSecurityProfileDowngrade before rejecting + const allowDowngradeResults = variableManager.getVariables(chargingStation, [ + { + attributeType: AttributeEnumType.Actual, + component: { name: OCPP20ComponentName.SecurityCtrlr }, + variable: { name: 'AllowSecurityProfileDowngrade' }, }, + ]) + const allowDowngrade = allowDowngradeResults[0]?.attributeValue?.toLowerCase() === 'true' + + // B09.FR.31 (errata 2025-09 §2.12): Allow downgrade when AllowSecurityProfileDowngrade=true + // but NEVER allow downgrade to profile 1 + if (!allowDowngrade || newSecurityProfile <= 1) { + logger.warn( + `${chargingStation.logPrefix()} ${moduleName}.handleRequestSetNetworkProfile: Rejected security profile downgrade: ${newSecurityProfile.toString()} < ${currentSecurityProfile.toString()}` + ) + return { + status: SetNetworkProfileStatusEnumType.Rejected, + statusInfo: { + additionalInfo: `Security profile downgrade not allowed: current=${currentSecurityProfile.toString()}, requested=${newSecurityProfile.toString()}`, + reasonCode: ReasonCodeEnumType.NoSecurityDowngrade, + }, + } } } @@ -2231,7 +2246,7 @@ export class OCPP20IncomingRequestService extends OCPPIncomingRequestService { status: SetNetworkProfileStatusEnumType.Rejected, statusInfo: { additionalInfo: `Configuration slot ${commandPayload.configurationSlot.toString()} is not in NetworkConfigurationPriority list`, - reasonCode: ReasonCodeEnumType.InvalidNetworkConf, + reasonCode: ReasonCodeEnumType.InvalidConfSlot, }, } } @@ -3480,6 +3495,20 @@ export class OCPP20IncomingRequestService extends OCPPIncomingRequestService { } } + // L01.FR.06: Wait for active transactions to end before installing + while ( + !checkAborted() && + [...chargingStation.evses].some( + ([evseId, evse]) => evseId > 0 && this.hasEvseActiveTransactions(evse) + ) + ) { + logger.debug( + `${chargingStation.logPrefix()} ${moduleName}.simulateFirmwareUpdateLifecycle: Waiting for active transactions to end before installing (L01.FR.06)` + ) + await sleep(5000) + } + if (checkAborted()) return + await this.sendFirmwareStatusNotification( chargingStation, FirmwareStatusEnumType.Installing, diff --git a/tests/charging-station/ocpp/2.0/OCPP20IncomingRequestService-DeleteCertificate.test.ts b/tests/charging-station/ocpp/2.0/OCPP20IncomingRequestService-DeleteCertificate.test.ts index 83a8339c..d0b8dc2a 100644 --- a/tests/charging-station/ocpp/2.0/OCPP20IncomingRequestService-DeleteCertificate.test.ts +++ b/tests/charging-station/ocpp/2.0/OCPP20IncomingRequestService-DeleteCertificate.test.ts @@ -270,7 +270,7 @@ await describe('I04 - DeleteCertificate', async () => { await describe('M04.FR.06 - ChargingStationCertificate Protection', async () => { await it('should reject deletion of ChargingStationCertificate', async () => { const chargingStationCertHash = createMockCertificateHashDataChain( - GetCertificateIdUseEnumType.CSMSRootCertificate, + GetCertificateIdUseEnumType.V2GCertificateChain, 'CHARGING_STATION_CERT_SERIAL' )