@citrineos/util
Version:
The OCPP util module which supplies helpful utilities like cache and queue connectors, etc.
233 lines • 11.5 kB
JavaScript
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