myinvois-sdk
Version:
TypeScript SDK for interacting with the Malaysia e-invoicing system (MyInvois) API
290 lines (289 loc) • 11.2 kB
JavaScript
;
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.CertificateHandler = void 0;
const fs = __importStar(require("fs"));
const crypto = __importStar(require("crypto"));
const forge = __importStar(require("node-forge"));
const util_1 = require("util");
const readFileAsync = (0, util_1.promisify)(fs.readFile);
/**
* Handles certificate operations for document signing
*/
class CertificateHandler {
/**
* Creates a new certificate handler
* @param config The certificate configuration
*/
constructor(config) {
this.certificates = [];
this.privateKey = null;
this.config = config;
}
/**
* Initializes the certificate handler by loading the certificate chain and private key
*/
async initialize() {
await this.loadCertificateChain();
await this.loadPrivateKey();
}
/**
* Formats a serial number
* @param serialNumber The serial number to format
* @returns The formatted serial number
*/
formatSerialNumber(serialNumber) {
// Remove any colons, spaces, and convert to uppercase
const cleanHex = serialNumber.replace(/[:\s]/g, '').toUpperCase();
// Convert from hex to decimal string
// Using BigInt because the serial number might be too large for regular Number
return BigInt('0x' + cleanHex).toString();
}
/**
* Loads the certificate chain from the certificate path
*/
async loadCertificateChain() {
try {
const certPem = await readFileAsync(this.config.certificatePath, 'utf8');
// Split the PEM file into individual certificates
const certRegex = /(-----BEGIN CERTIFICATE-----[\s\S]*?-----END CERTIFICATE-----)/g;
const matches = certPem.match(certRegex);
if (!matches) {
throw new Error('No certificates found in the file');
}
// Process each certificate in the chain
this.certificates = matches.map(certString => {
const cert = new crypto.X509Certificate(certString);
return {
subject: cert.subject,
issuer: cert.issuer,
serialNumber: cert.serialNumber,
validFrom: new Date(cert.validFrom),
validTo: new Date(cert.validTo),
raw: cert.raw
};
});
console.log(`Found ${this.certificates.length} certificates in chain`);
this.validateCertificateChain();
}
catch (error) {
console.error('Failed to load certificate chain:', error);
throw error;
}
}
/**
* Validates the certificate chain
*/
validateCertificateChain() {
for (let i = 0; i < this.certificates.length - 1; i++) {
const current = this.certificates[i];
const issuer = this.certificates[i + 1];
// For LHDN certificates, we need to normalize the DN strings before comparison
const normalizedIssuer = this.normalizeDN(current.issuer);
const normalizedSubject = this.normalizeDN(issuer.subject);
// Verify the certificate chain
if (normalizedIssuer !== normalizedSubject) {
console.error('Chain break detected!');
console.error(`Certificate ${i} issuer doesn't match certificate ${i + 1} subject`);
console.error('Expected: ' + normalizedIssuer);
console.error('Found: ' + normalizedSubject);
throw new Error(`Certificate chain broken between certificate ${i} and ${i + 1}`);
}
// Verify certificate dates
const now = new Date();
if (now < current.validFrom || now > current.validTo) {
throw new Error(`Certificate ${i} is not currently valid (${current.validFrom} to ${current.validTo})`);
}
}
console.log('Certificate chain validation successful');
}
/**
* Normalizes a distinguished name for comparison
* @param dn The distinguished name to normalize
* @returns The normalized distinguished name
*/
normalizeDN(dn) {
// Split DN into components
const parts = dn.split(',').map(part => part.trim());
// Sort the components to ensure consistent ordering
parts.sort();
// Normalize spaces and case
return parts
.map(part => {
const [key, ...values] = part.split('=');
return `${key.trim().toUpperCase()}=${values.join('=').trim()}`;
})
.join(',');
}
/**
* Gets the signing certificate
* @returns The signing certificate
*/
getSigningCertificate() {
if (this.certificates.length === 0) {
throw new Error('Certificates not loaded. Call initialize() first.');
}
return this.certificates[0];
}
/**
* Gets the intermediate certificate
* @returns The intermediate certificate
*/
getIntermediateCertificate() {
if (this.certificates.length < 2) {
throw new Error('Intermediate certificate not found in chain');
}
return this.certificates[1];
}
/**
* Gets the root certificate
* @returns The root certificate
*/
getRootCertificate() {
if (this.certificates.length === 0) {
throw new Error('Certificates not loaded. Call initialize() first.');
}
return this.certificates[this.certificates.length - 1];
}
/**
* Formats a distinguished name according to LHDN format
* @param dn The distinguished name to format
* @returns The formatted distinguished name
*/
formatDistinguishedName(dn) {
// Split on newlines and commas
const parts = dn.split(/[\n,]/)
.map(part => part.trim())
.filter(part => part.length > 0); // Remove empty parts
// Create a map of DN components
const dnMap = new Map();
parts.forEach(part => {
const [key, ...values] = part.split('=');
dnMap.set(key.trim().toUpperCase(), values.join('=').trim());
});
// Construct the DN in the required order with proper formatting
const orderedParts = [];
// Order: CN, OU, O, C (matching LHDN's format)
if (dnMap.has('CN'))
orderedParts.push(`CN=${dnMap.get('CN')}`);
if (dnMap.has('OU'))
orderedParts.push(`OU=${dnMap.get('OU')}`);
if (dnMap.has('O'))
orderedParts.push(`O=${dnMap.get('O')}`);
if (dnMap.has('C'))
orderedParts.push(`C=${dnMap.get('C')}`);
// Join with ", " to match LHDN's format
return orderedParts.join(', ');
}
/**
* Generates certificate information for document signing
* @returns The certificate information
*/
generateCertificateInfo() {
const signingCert = this.getSigningCertificate();
// Format the issuer name to match the LHDN format
const formattedIssuerName = this.formatDistinguishedName(signingCert.issuer);
return {
X509Certificate: [
{
"_": signingCert.raw.toString('base64')
}
],
X509SubjectName: [
{
"_": this.formatDistinguishedName(signingCert.subject)
}
],
X509IssuerSerial: [
{
X509IssuerName: [
{
"_": formattedIssuerName
}
],
X509SerialNumber: [
{
"_": this.formatSerialNumber(signingCert.serialNumber)
}
]
}
]
};
}
/**
* Generates a certificate hash for document signing
* @returns The certificate hash
*/
generateCertificateHash() {
const signingCert = this.getSigningCertificate();
const certificatePem = forge.pki.certificateFromPem(`-----BEGIN CERTIFICATE-----\n${signingCert.raw.toString('base64')}\n-----END CERTIFICATE-----`);
const derBytes = forge.asn1.toDer(forge.pki.certificateToAsn1(certificatePem)).getBytes();
return crypto.createHash('sha256').update(derBytes, 'binary').digest('base64');
}
/**
* Loads the private key
*/
async loadPrivateKey() {
try {
const keyContent = await readFileAsync(this.config.privateKeyPath, 'utf8');
if (!this.config.privateKeyPassphrase) {
throw new Error('Private key passphrase is not set');
}
this.privateKey = forge.pki.decryptRsaPrivateKey(keyContent, this.config.privateKeyPassphrase);
if (!this.privateKey) {
throw new Error('Failed to decrypt private key');
}
}
catch (error) {
console.error('Failed to read or decrypt private key:', error);
throw new Error('Failed to load private key');
}
}
/**
* Signs data with the private key
* @param data The data to sign
* @returns The signature
*/
signData(data) {
if (!this.privateKey) {
throw new Error('Private key not loaded. Call initialize() first.');
}
const md = forge.md.sha256.create();
md.update(data, 'utf8');
const signature = this.privateKey.sign(md);
return forge.util.encode64(signature);
}
}
exports.CertificateHandler = CertificateHandler;