]> Piment Noir Git Repositories - e-mobility-charging-stations-simulator.git/commitdiff
fix(ocpp20): remediate 4 conformance findings from cross-audit
authorJérôme Benoit <jerome.benoit@sap.com>
Sun, 15 Mar 2026 20:04:35 +0000 (21:04 +0100)
committerJérôme Benoit <jerome.benoit@sap.com>
Sun, 15 Mar 2026 20:04:35 +0000 (21:04 +0100)
- 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

src/charging-station/ocpp/2.0/OCPP20IncomingRequestService.ts
tests/charging-station/ocpp/2.0/OCPP20IncomingRequestService-DeleteCertificate.test.ts

index 50ca5287d26767e0b4e1108c5f87c87392268aac..a52418ef3b79364e59c76be877e0a041068aa328 100644 (file)
@@ -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,
index 83a8339c48154d730ebafcba92ebea3f6739c775..d0b8dc2ae97c46a28a432e0e103e76a5c865eb29 100644 (file)
@@ -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'
       )