UNPKG

@citrineos/util

Version:

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

233 lines 11.5 kB
import { OCPP2_0_1 } from '@citrineos/base'; import { Hubject } from './client/hubject.js'; import { Acme } from './client/acme.js'; import { Logger } from 'tslog'; import * as pkijs from 'pkijs'; import { Certificate } from 'pkijs'; import jsrsasign, { KJUR, X509 } from 'jsrsasign'; import moment from 'moment'; import { createPemBlock, dateTimeFormat, extractCertificateArrayFromEncodedString, extractEncodedContentFromCSR, parseCertificateChainPem, sendOCSPRequest, } from './CertificateUtil.js'; import { Crypto } from '@peculiar/webcrypto'; var OCSPRequest = jsrsasign.KJUR.asn1.ocsp.OCSPRequest; var Request = jsrsasign.KJUR.asn1.ocsp.Request; const cryptoEngine = new pkijs.CryptoEngine({ crypto: new Crypto(), }); pkijs.setEngine('crypto', cryptoEngine); export class CertificateAuthorityService { _v2gClient; _chargingStationClient; _logger; _cache; _config; constructor(config, cache, logger, chargingStationClient, v2gClient) { this._config = config; this._cache = cache; this._logger = logger ? logger.getSubLogger({ name: this.constructor.name }) : new Logger({ name: this.constructor.name }); this._chargingStationClient = chargingStationClient || this._instantiateChargingStationClient(); this._v2gClient = v2gClient || this._instantiateV2GClient(); } /** * Retrieves the certificate chain for V2G- and Charging Station certificates. * * @param {string} csrString - The Certificate Signing Request string. * @param {string} stationId - The station identifier. * @param {CertificateSigningUseEnumType} [certificateType] - The type of certificate to retrieve. * @return {Promise<string>} The certificate chain without the root certificate. */ async getCertificateChain(csrString, stationId, certificateType) { this._logger.info(`Getting certificate chain for certificateType: ${certificateType} and stationId: ${stationId}`); switch (certificateType) { case OCPP2_0_1.CertificateSigningUseEnumType.V2GCertificate: { const signedCert = await this._v2gClient.getSignedCertificate(extractEncodedContentFromCSR(csrString)); const caCerts = await this._v2gClient.getCACertificates(); return this._createCertificateChainWithoutRootCA(signedCert, caCerts); } case OCPP2_0_1.CertificateSigningUseEnumType.ChargingStationCertificate: { return await this._chargingStationClient.getCertificateChain(csrString); } default: { throw new Error(`Unsupported certificate type: ${certificateType}`); } } } async signedSubCaCertificateByExternalCA(csrString) { return await this._chargingStationClient.signCertificateByExternalCA(csrString); } async getSignedContractData(iso15118SchemaVersion, exiRequest) { return await this._v2gClient.getSignedContractData(iso15118SchemaVersion, exiRequest); } async getRootCACertificateFromExternalCA(certificateType) { switch (certificateType) { case OCPP2_0_1.InstallCertificateUseEnumType.V2GRootCertificate: { const caCerts = await this._v2gClient.getCACertificates(); const rootCACert = extractCertificateArrayFromEncodedString(caCerts).pop(); if (rootCACert) { return createPemBlock(Buffer.from(rootCACert.toSchema().toBER(false)).toString('base64')); } else { throw new Error(`V2GRootCertificate not found from ${caCerts}`); } } case OCPP2_0_1.InstallCertificateUseEnumType.CSMSRootCertificate: return await this._chargingStationClient.getRootCACertificate(); default: throw new Error(`Certificate type: ${certificateType} not implemented.`); } } updateSecurityCertChainKeyMap(serverId, certificateChain, privateKey) { this._chargingStationClient.updateCertificateChainKeyMap(serverId, certificateChain, privateKey); } /* * Validate the certificate chain using real time OCSP check. * * @param certificateChainPem - certificate chain pem * @return AuthorizeCertificateStatusEnumType */ async validateCertificateChainPem(certificateChainPem) { const certificatePems = parseCertificateChainPem(certificateChainPem); this._logger.debug(`Found ${certificatePems.length} certificates in chain.`); if (certificatePems.length < 1) { return OCPP2_0_1.AuthorizeCertificateStatusEnumType.NoCertificateAvailable; } try { // Find the root certificate of the certificate chain const rootCerts = await this._v2gClient.getRootCertificates(); const lastCertInChain = new X509(); lastCertInChain.readCertPEM(certificatePems[certificatePems.length - 1]); let rootCertPem; for (const rootCert of rootCerts) { const root = new X509(); root.readCertPEM(rootCert); if (root.getSubjectString() === lastCertInChain.getIssuerString() && root.getExtSubjectKeyIdentifier().kid.hex === lastCertInChain.getExtAuthorityKeyIdentifier().kid.hex) { rootCertPem = rootCert; break; } } if (!rootCertPem) { this._logger.error(`Cannot find root certificate for certificate ${lastCertInChain}`); return OCPP2_0_1.AuthorizeCertificateStatusEnumType.NoCertificateAvailable; } else { certificatePems.push(rootCertPem); } // OCSP validation for each certificate for (let i = 0; i < certificatePems.length - 1; i++) { const subjectCert = new X509(); subjectCert.readCertPEM(certificatePems[i]); this._logger.debug(`Subject Certificate: ${subjectCert.getInfo()}`); const notAfter = moment(subjectCert.getNotAfter(), dateTimeFormat); if (notAfter.isBefore(moment())) { return OCPP2_0_1.AuthorizeCertificateStatusEnumType.CertificateExpired; } const ocspUrls = subjectCert.getExtAIAInfo()?.ocsp; if (ocspUrls && ocspUrls.length > 0) { const ocspRequest = new OCSPRequest({ reqList: [ { issuerCert: certificatePems[i + 1], subjectCert: certificatePems[i], }, ], }); this._logger.debug(`OCSP response URL: ${ocspUrls[0]}`); const ocspResponse = KJUR.asn1.ocsp.OCSPUtil.getOCSPResponseInfo(await sendOCSPRequest(ocspRequest, ocspUrls[0])); const certStatus = ocspResponse.certStatus; if (certStatus === 'revoked') { return OCPP2_0_1.AuthorizeCertificateStatusEnumType.CertificateRevoked; } else if (certStatus !== 'good') { return OCPP2_0_1.AuthorizeCertificateStatusEnumType.NoCertificateAvailable; } } else { this._logger.error(`Certificate ${certificatePems[i]} has no OCSP URL.`); return OCPP2_0_1.AuthorizeCertificateStatusEnumType.CertChainError; } } } catch (error) { this._logger.error(`Failed to validate certificate chain: ${error}`); return OCPP2_0_1.AuthorizeCertificateStatusEnumType.NoCertificateAvailable; } return OCPP2_0_1.AuthorizeCertificateStatusEnumType.Accepted; } async validateCertificateHashData(ocspRequestData) { for (const reqData of ocspRequestData) { const ocspRequest = new Request({ alg: reqData.hashAlgorithm, keyhash: reqData.issuerKeyHash, namehash: reqData.issuerNameHash, serial: reqData.serialNumber, }); this._logger.debug(`OCSP request: ${JSON.stringify(ocspRequest)}`); try { const ocspResponse = KJUR.asn1.ocsp.OCSPUtil.getOCSPResponseInfo(await sendOCSPRequest(ocspRequest, reqData.responderURL)); // Cert statuses: good, revoked, unknown // source: https://kjur.github.io/jsrsasign/api/symbols/KJUR.asn1.ocsp.OCSPUtil.html#.getOCSPResponseInfo const certStatus = ocspResponse.certStatus; if (certStatus === 'revoked') { return OCPP2_0_1.AuthorizeCertificateStatusEnumType.CertificateRevoked; } else if (certStatus !== 'good') { return OCPP2_0_1.AuthorizeCertificateStatusEnumType.NoCertificateAvailable; } } catch (error) { this._logger.error(`Failed to fetch OCSP response: ${error}`); return OCPP2_0_1.AuthorizeCertificateStatusEnumType.NoCertificateAvailable; } } return OCPP2_0_1.AuthorizeCertificateStatusEnumType.Accepted; } /** * Create a certificate chain including leaf and sub CA certificates except for the root certificate. * * @param {string} signedCert - The leaf certificate. * @param {string} caCerts - CA certificates. * @return {string} The certificate chain pem. */ _createCertificateChainWithoutRootCA(signedCert, caCerts) { let certificateChain = ''; // Add Cert const leafRaw = extractCertificateArrayFromEncodedString(signedCert)[0]; if (leafRaw) { certificateChain += createPemBlock(Buffer.from(leafRaw.toSchema().toBER(false)).toString('base64')); } else { throw new Error(`Cannot extract leaf certificate from the pem: ${signedCert}`); } // Add Chain without Root CA Certificate const chainWithoutRoot = extractCertificateArrayFromEncodedString(caCerts).slice(0, -1); chainWithoutRoot.forEach((certItem) => { const cert = certItem; certificateChain += createPemBlock(Buffer.from(cert.toSchema().toBER(false)).toString('base64')); }); return certificateChain; } _instantiateV2GClient() { switch (this._config.util.certificateAuthority.v2gCA.name) { case 'hubject': { return new Hubject(this._config, this._cache, this._logger); } default: { throw new Error(`Unsupported V2G CA: ${this._config.util.certificateAuthority.v2gCA.name}`); } } } _instantiateChargingStationClient() { switch (this._config.util.certificateAuthority.chargingStationCA.name) { case 'acme': { return new Acme(this._config, this._logger); } default: { throw new Error(`Unsupported Charging Station CA: ${this._config.util.certificateAuthority.chargingStationCA.name}`); } } } } //# sourceMappingURL=CertificateAuthority.js.map