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