UNPKG

@citrineos/util

Version:

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

328 lines 12.5 kB
// SPDX-FileCopyrightText: 2025 Contributors to the CitrineOS Project // // SPDX-License-Identifier: Apache-2.0 import * as pkijs from 'pkijs'; import { CertificationRequest } from 'pkijs'; import * as asn1js from 'asn1js'; import { fromBER } from 'asn1js'; import { Certificate, CountryNameEnumType, SignatureAlgorithmEnumType } from '@citrineos/data'; import jsrsasign from 'jsrsasign'; import { fromBase64, stringToArrayBuffer } from 'pvutils'; import moment from 'moment'; import { Logger } from 'tslog'; var KJUR = jsrsasign.KJUR; var OCSPRequest = jsrsasign.KJUR.asn1.ocsp.OCSPRequest; var Request = jsrsasign.KJUR.asn1.ocsp.Request; var X509 = jsrsasign.X509; var KEYUTIL = jsrsasign.KEYUTIL; export const dateTimeFormat = 'YYMMDDHHmmssZ'; export function getValidityTimeString(time) { return time.utc().format('YYMMDDHHmmss').concat('Z'); } export function createPemBlock(content) { return `-----BEGIN CERTIFICATE-----\n${content}\n-----END CERTIFICATE-----\n`; } /* * Parse the certificate chain and extract certificates * @param pem - certificate chain pem containing multiple certificate blocks * @return array of certificate pem blocks */ export function parseCertificateChainPem(pem) { const certs = []; const beginMarker = '-----BEGIN CERTIFICATE-----'; const endMarker = '-----END CERTIFICATE-----'; let startIndex = pem.indexOf(beginMarker); while (startIndex !== -1) { const endIndex = pem.indexOf(endMarker, startIndex + beginMarker.length); if (endIndex === -1) { break; } certs.push(pem.substring(startIndex, endIndex + endMarker.length)); startIndex = pem.indexOf(beginMarker, endIndex + endMarker.length); } return certs; } /** * Decode the pem and extract certificates * @param pem - base64 encoded certificate chain string without header and footer * @return array of pkijs.CertificateSetItem */ export function extractCertificateArrayFromEncodedString(pem) { try { const cmsSignedBuffer = Buffer.from(pem, 'base64'); const asn1 = asn1js.fromBER(cmsSignedBuffer); const cmsContent = new pkijs.ContentInfo({ schema: asn1.result }); const cmsSigned = new pkijs.SignedData({ schema: cmsContent.content }); if (cmsSigned.certificates && cmsSigned.certificates.length > 0) { return cmsSigned.certificates; } else { return []; } } catch (e) { throw new Error(`Failed to extract certificate ${pem} due to ${e}`); } } /** * extracts the base64-encoded content from a pem encoded csr * @param csrPem * @private * @return {string} The parsed CSR or the original CSR if it cannot be parsed */ export function extractEncodedContentFromCSR(csrPem) { return csrPem .replace(/-----BEGIN CERTIFICATE REQUEST-----/, '') .replace(/-----END CERTIFICATE REQUEST-----/, '') .replace(/[\r\n]/g, ''); } /** * Generate certificate and its private key * * @param certificateEntity - the certificate * @param logger - the logger * @param issuerKeyPem - the issuer private key * @param issuerCertPem - the issuer certificate * * @return generated certificate pem and its private key pem */ export function generateCertificate(certificateEntity, logger, issuerKeyPem, issuerCertPem) { // Generate a key pair let keyPair; logger.debug(`Private key signAlgorithm: ${certificateEntity.signatureAlgorithm}`); if (certificateEntity.signatureAlgorithm === SignatureAlgorithmEnumType.RSA) { keyPair = jsrsasign.KEYUTIL.generateKeypair('RSA', certificateEntity.keyLength ? certificateEntity.keyLength : 2048); } else { keyPair = jsrsasign.KEYUTIL.generateKeypair('EC', 'secp256r1'); } const privateKeyPem = jsrsasign.KEYUTIL.getPEM(keyPair.prvKeyObj, 'PKCS8PRV'); const publicKeyPem = jsrsasign.KEYUTIL.getPEM(keyPair.pubKeyObj); logger.debug(`Created publicKeyPem: ${publicKeyPem}`); let issuerCertObj; if (issuerCertPem) { issuerCertObj = new X509(); issuerCertObj.readCertPEM(issuerCertPem); } // Prepare certificate attributes let subjectNotAfter = certificateEntity.validBefore ? moment(certificateEntity.validBefore) : moment().add(1, 'year'); const subjectString = `/CN=${certificateEntity.commonName}/O=${certificateEntity.organizationName}/C=${certificateEntity.countryName}`; let issuerParam = { str: subjectString }; if (issuerCertObj) { const issuerNotAfter = moment(issuerCertObj.getNotAfter(), dateTimeFormat); if (subjectNotAfter.isAfter(issuerNotAfter)) { subjectNotAfter = issuerNotAfter; } issuerParam = { str: issuerCertObj.getSubjectString() }; } // Prepare certificate extensions const keyUsages = ['digitalSignature', 'keyCertSign', 'crlSign']; if (!certificateEntity.isCA) { keyUsages.push('keyEncipherment'); } let basicConstraints = { extname: 'basicConstraints', critical: true, cA: certificateEntity.isCA, }; if (certificateEntity.pathLen) { basicConstraints = { extname: 'basicConstraints', cA: certificateEntity.isCA, pathLen: certificateEntity.pathLen, }; } const extensions = [ basicConstraints, { extname: 'keyUsage', critical: true, names: keyUsages }, { extname: 'subjectKeyIdentifier', kid: publicKeyPem }, ]; if (issuerCertObj) { extensions.push({ extname: 'authorityKeyIdentifier', kid: issuerCertPem, isscert: issuerCertPem, }); } // Prepare certificate sign parameters const signAlgorithm = certificateEntity.signatureAlgorithm === SignatureAlgorithmEnumType.RSA ? SignatureAlgorithmEnumType.RSA : SignatureAlgorithmEnumType.ECDSA; logger.debug(`Certificate SignAlgorithm: ${signAlgorithm}`); const caKey = issuerKeyPem ? issuerKeyPem : privateKeyPem; // Generate certificate const certificate = new KJUR.asn1.x509.Certificate({ version: 3, serial: { int: moment().valueOf() }, notbefore: getValidityTimeString(moment()), notafter: getValidityTimeString(subjectNotAfter), issuer: issuerParam, subject: { str: subjectString }, sbjpubkey: keyPair.pubKeyObj, ext: extensions, sigalg: signAlgorithm, cakey: caKey, }); return [certificate.getPEM(), privateKeyPem]; } /** * Create a signed certificate for the provided CSR using the issuer certificate, and its private key. * * @param csrPem - The CSR that need to be signed. * @param issuerCertPem - The issuer certificate. * @param issuerPrivateKeyPem - The issuer private key. * @return {KJUR.asn1.x509.Certificate} The signed certificate. */ export function createSignedCertificateFromCSR(csrPem, issuerCertPem, issuerPrivateKeyPem) { const csrObj = jsrsasign.KJUR.asn1.csr.CSRUtil.getParam(csrPem); const issuerCertObj = new X509(); issuerCertObj.readCertPEM(issuerCertPem); let subjectNotAfter = moment().add(1, 'year'); const issuerNotAfter = moment(issuerCertObj.getNotAfter(), dateTimeFormat); if (subjectNotAfter.isAfter(issuerNotAfter)) { subjectNotAfter = issuerNotAfter; } let extensions; if (csrObj.extreq) { extensions = csrObj.extreq; } else { extensions = [ { extname: 'basicConstraints', cA: false }, { extname: 'keyUsage', critical: true, names: ['digitalSignature', 'keyEncipherment'], }, ]; } extensions.push({ extname: 'subjectKeyIdentifier', kid: csrObj.sbjpubkey }); extensions.push({ extname: 'authorityKeyIdentifier', kid: issuerCertPem, isscert: issuerCertPem, }); return new KJUR.asn1.x509.Certificate({ version: 3, serial: { int: moment().valueOf() }, issuer: { str: issuerCertObj.getSubjectString() }, subject: { str: csrObj.subject.str }, notbefore: getValidityTimeString(moment()), notafter: getValidityTimeString(subjectNotAfter), sbjpubkey: csrObj.sbjpubkey, ext: extensions, sigalg: csrObj.sigalg, cakey: issuerPrivateKeyPem, }); } export async function sendOCSPRequest(ocspRequest, responderURL) { const response = await fetch(responderURL, { method: 'POST', headers: { 'Content-Type': 'application/ocsp-request', Accept: 'application/ocsp-response', }, body: ocspRequest.getEncodedHex(), }); if (!response.ok) { throw new Error(`Failed to fetch OCSP response from ${responderURL}: ${response.status} with error: ${await response.text()}`); } return await response.text(); } export function parseCSRForVerification(csrPem) { const certificateBuffer = stringToArrayBuffer(fromBase64(extractEncodedContentFromCSR(csrPem))); const asn1 = fromBER(certificateBuffer); if (asn1.offset === -1) { throw new Error('Failed to parse CSR: invalid ASN.1 BER encoding'); } return new CertificationRequest({ schema: asn1.result }); } export function generateCSR(certificate) { let keyPair; if (certificate.signatureAlgorithm === SignatureAlgorithmEnumType.RSA) { keyPair = KEYUTIL.generateKeypair('RSA', certificate.keyLength ? certificate.keyLength : 2048); } else { keyPair = KEYUTIL.generateKeypair('EC', 'secp256r1'); } const privateKeyPem = jsrsasign.KEYUTIL.getPEM(keyPair.prvKeyObj, 'PKCS8PRV'); const publicKeyPem = jsrsasign.KEYUTIL.getPEM(keyPair.pubKeyObj); let basicConstraintParam; if (certificate.pathLen) { basicConstraintParam = { cA: certificate.isCA, pathLen: certificate.pathLen, }; } else { basicConstraintParam = { cA: certificate.isCA }; } const csr = new KJUR.asn1.csr.CertificationRequest({ subject: { str: `/CN=${certificate.commonName}/O=${certificate.organizationName}/C=${certificate.countryName}`, }, sbjpubkey: publicKeyPem, extreq: [ { extname: 'basicConstraints', array: [basicConstraintParam] }, { extname: 'keyUsage', array: [ { names: ['digitalSignature', 'keyEncipherment', 'keyCertSign', 'crlSign'], }, ], }, ], sigalg: certificate.signatureAlgorithm ? certificate.signatureAlgorithm : SignatureAlgorithmEnumType.ECDSA, sbjprvkey: privateKeyPem, }); return [csr.getPEM(), privateKeyPem]; } export const parseX509Date = (date) => { if (/^\d{14}Z$/.test(date)) { // GeneralizedTime: YYYYMMDDHHMMSSZ return moment.utc(date, 'YYYYMMDDHHmmss[Z]', true).toDate(); } else if (/^\d{12}Z$/.test(date)) { // UTCTime: YYMMDDHHMMSSZ (YY interpreted as 1950-2049) return moment.utc(date, 'YYMMDDHHmmss[Z]', true).toDate(); } else { console.error(`Invalid X.509 date format: ${date}`); return null; } }; export const extractCertificateDetails = (pemString) => { try { const cert = new jsrsasign.X509(); cert.readCertPEM(pemString); // Extract details const serialNumber = parseInt(cert.getSerialNumberHex()); const issuerName = cert.getIssuerString(); const organizationName = cert.getSubjectString().match(/\/O=([^/]+)/)?.[1] || null; const commonName = cert.getSubjectString().match(/\/CN=([^/]+)/)?.[1] || null; const countryName = (cert.getSubjectString().match(/\/C=([^/]+)/)?.[1] || null); const notAfter = cert.getNotAfter(); const validBefore = parseX509Date(notAfter); const signatureAlgorithm = cert.getSignatureAlgorithmField(); return { serialNumber, issuerName, organizationName, commonName, countryName, validBefore, signatureAlgorithm, }; } catch (error) { console.error('Error extracting certificate details:', error); throw new Error('Invalid PEM format or unsupported certificate'); } }; //# sourceMappingURL=CertificateUtil.js.map