filterTypes?: InstallCertificateUseEnumType[],
hashAlgorithm?: HashAlgorithmEnumType
): GetInstalledCertificatesResult | Promise<GetInstalledCertificatesResult>
+ isChargingStationCertificateHash(
+ stationHashId: string,
+ certificateHashData: CertificateHashDataType,
+ hashAlgorithm?: HashAlgorithmEnumType
+ ): boolean | Promise<boolean>
storeCertificate(
stationHashId: string,
certType: CertificateSigningUseEnumType | InstallCertificateUseEnumType,
return { certificateHashDataChain }
}
+ /**
+ * Checks whether the given certificate hash data matches a ChargingStationCertificate
+ * stored on disk for the specified station.
+ * @param stationHashId - Charging station unique identifier
+ * @param certificateHashData - Certificate hash data to check against stored CS certificates
+ * @param hashAlgorithm - Optional hash algorithm override (defaults to certificateHashData.hashAlgorithm)
+ * @returns true if the hash matches a stored ChargingStationCertificate, false otherwise
+ */
+ public async isChargingStationCertificateHash (
+ stationHashId: string,
+ certificateHashData: CertificateHashDataType,
+ hashAlgorithm?: HashAlgorithmEnumType
+ ): Promise<boolean> {
+ try {
+ const certFilePath = this.getCertificatePath(
+ stationHashId,
+ CertificateSigningUseEnumType.ChargingStationCertificate,
+ ''
+ )
+ // getCertificatePath returns basePath/ChargingStationCertificate/.pem
+ // We need the directory: basePath/ChargingStationCertificate
+ const dirPath = resolve(certFilePath, '..')
+
+ if (!(await this.pathExists(dirPath))) {
+ return false
+ }
+
+ const allFiles = await readdir(dirPath)
+ const pemFiles = allFiles.filter(f => f.endsWith('.pem'))
+
+ for (const file of pemFiles) {
+ const filePath = join(dirPath, file)
+ this.validateCertificatePath(filePath, OCPP20CertificateManager.BASE_CERT_PATH)
+ try {
+ const pemData = await readFile(filePath, 'utf8')
+ const hashData = this.computeCertificateHash(
+ pemData,
+ hashAlgorithm ?? certificateHashData.hashAlgorithm
+ )
+ if (
+ hashData.serialNumber === certificateHashData.serialNumber &&
+ hashData.issuerNameHash === certificateHashData.issuerNameHash &&
+ hashData.issuerKeyHash === certificateHashData.issuerKeyHash
+ ) {
+ return true
+ }
+ } catch {
+ continue
+ }
+ }
+ } catch {
+ // Ignore errors — treat as "not a CS cert"
+ }
+ return false
+ }
+
/**
* Stores a PEM certificate to the filesystem
* @param stationHashId - Charging station unique identifier
try {
// M04.FR.06: Check if the certificate to delete is a ChargingStationCertificate
- // If so, reject the deletion request
- // Get all installed certificates using the request's hashAlgorithm for consistent comparison
- const installedCerts = chargingStation.certificateManager.getInstalledCertificates(
+ const isCSCertResult = chargingStation.certificateManager.isChargingStationCertificateHash(
chargingStation.stationInfo?.hashId ?? '',
- undefined,
- certificateHashData.hashAlgorithm
+ certificateHashData
)
+ const isCSCert = isCSCertResult instanceof Promise ? await isCSCertResult : isCSCertResult
- const installedCertsResult =
- installedCerts instanceof Promise ? await installedCerts : installedCerts
-
- 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
- ) {
- logger.warn(
- `${chargingStation.logPrefix()} ${moduleName}.handleRequestDeleteCertificate: Attempted to delete ChargingStationCertificate (M04.FR.06)`
- )
- return {
- status: DeleteCertificateStatusEnumType.Failed,
- statusInfo: {
- additionalInfo: 'ChargingStationCertificate cannot be deleted (M04.FR.06)',
- reasonCode: ReasonCodeEnumType.NotSupported,
- },
- }
+ if (isCSCert) {
+ logger.warn(
+ `${chargingStation.logPrefix()} ${moduleName}.handleRequestDeleteCertificate: Attempted to delete ChargingStationCertificate (M04.FR.06)`
+ )
+ return {
+ status: DeleteCertificateStatusEnumType.Failed,
+ statusInfo: {
+ additionalInfo: 'ChargingStationCertificate cannot be deleted (M04.FR.06)',
+ reasonCode: ReasonCodeEnumType.NotSupported,
+ },
}
}
import { OCPP20IncomingRequestService } from '../../../../src/charging-station/ocpp/2.0/OCPP20IncomingRequestService.js'
import {
DeleteCertificateStatusEnumType,
- GetCertificateIdUseEnumType,
HashAlgorithmEnumType,
type OCPP20DeleteCertificateRequest,
type OCPP20DeleteCertificateResponse,
import { TEST_CHARGING_STATION_BASE_NAME } from '../../ChargingStationTestConstants.js'
import { createMockChargingStation } from '../../ChargingStationTestUtils.js'
import {
- createMockCertificateHashDataChain,
createMockCertificateManager,
createStationWithCertificateManager,
} from './OCPP20TestUtils.js'
await describe('M04.FR.06 - ChargingStationCertificate Protection', async () => {
await it('should reject deletion of ChargingStationCertificate', async () => {
- const chargingStationCertHash = createMockCertificateHashDataChain(
- GetCertificateIdUseEnumType.V2GCertificateChain,
- 'CHARGING_STATION_CERT_SERIAL'
- )
-
stationWithCertManager.certificateManager = createMockCertificateManager({
- getInstalledCertificatesResult: [chargingStationCertHash],
+ isChargingStationCertificateHashResult: true,
})
const request: OCPP20DeleteCertificateRequest = {
- certificateHashData: chargingStationCertHash.certificateHashData,
+ certificateHashData: VALID_CERTIFICATE_HASH_DATA,
}
const response: OCPP20DeleteCertificateResponse =
await it('should allow deletion of non-ChargingStationCertificate when no ChargingStationCertificate exists', async () => {
stationWithCertManager.certificateManager = createMockCertificateManager({
deleteCertificateResult: { status: DeleteCertificateStatusEnumType.Accepted },
- getInstalledCertificatesResult: [],
+ isChargingStationCertificateHashResult: false,
})
const request: OCPP20DeleteCertificateRequest = {
getInstalledCertificatesError?: Error
/** Result to return from getInstalledCertificates (default: []) */
getInstalledCertificatesResult?: CertificateHashDataChainType[]
+ /** Result to return from isChargingStationCertificateHash (default: false) */
+ isChargingStationCertificateHashResult?: boolean
/** Error to throw when storeCertificate is called */
storeCertificateError?: Error
/** Result to return from storeCertificate (default: { success: true }) */
certificateHashDataChain: options.getInstalledCertificatesResult ?? [],
}
}),
+ isChargingStationCertificateHash: mock.fn(() => {
+ return options.isChargingStationCertificateHashResult ?? false
+ }),
storeCertificate: mock.fn(() => {
if (options.storeCertificateError != null) {
throw options.storeCertificateError