zatca-xml-ts
Version:
An implementation of Saudi Arabia ZATCA's E-Invoicing requirements, processes, and standards.
205 lines (204 loc) • 9.9 kB
JavaScript
"use strict";
/**
* This module requires OpenSSL to be installed on the system.
* Using an OpenSSL In order to generate secp256k1 key pairs, a CSR and sign it.
* I was unable to find a working library that supports the named curve `secp256k1` and do not want to implement my own JS based crypto.
* Any crypto expert contributions to move away from OpenSSL to JS will be appreciated.
*/
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.EGS = void 0;
const child_process_1 = require("child_process");
const uuid_1 = require("uuid");
const fs_1 = __importDefault(require("fs"));
const csr_template_1 = __importDefault(require("../templates/csr_template"));
const api_1 = __importDefault(require("../api"));
const OpenSSL = (cmd) => {
return new Promise((resolve, reject) => {
try {
const command = (0, child_process_1.spawn)("openssl", cmd);
let result = "";
command.stdout.on("data", (data) => {
result += data.toString();
});
command.on("close", (code) => {
return resolve(result);
});
command.on("error", (error) => {
return reject(error);
});
}
catch (error) {
reject(error);
}
});
};
// Generate a secp256k1 key pair
// https://techdocs.akamai.com/iot-token-access-control/docs/generate-ecdsa-keys
// openssl ecparam -name secp256k1 -genkey -noout -out ec-secp256k1-priv-key.pem
const generateSecp256k1KeyPair = async () => {
try {
const result = await OpenSSL(["ecparam", "-name", "secp256k1", "-genkey"]);
if (!result.includes("-----BEGIN EC PRIVATE KEY-----"))
throw new Error("Error no private key found in OpenSSL output.");
let private_key = `-----BEGIN EC PRIVATE KEY-----${result.split("-----BEGIN EC PRIVATE KEY-----")[1]}`.trim();
return private_key;
}
catch (error) {
throw error;
}
};
// Generate a signed ecdsaWithSHA256 CSR
// 2.2.2 Profile specification of the Cryptographic Stamp identifiers. & CSR field contents / RDNs.
const generateCSR = async (egs_info, production, solution_name) => {
if (!egs_info.private_key)
throw new Error("EGS has no private key");
// This creates a temporary private file, and csr config file to pass to OpenSSL in order to create and sign the CSR.
// * In terms of security, this is very bad as /tmp can be accessed by all users. a simple watcher by unauthorized user can retrieve the keys.
// Better change it to some protected dir.
const private_key_file = `${process.env.TEMP_FOLDER ?? "/tmp/"}${(0, uuid_1.v4)()}.pem`;
const csr_config_file = `${process.env.TEMP_FOLDER ?? "/tmp/"}${(0, uuid_1.v4)()}.cnf`;
fs_1.default.writeFileSync(private_key_file, egs_info.private_key);
fs_1.default.writeFileSync(csr_config_file, (0, csr_template_1.default)({
egs_model: egs_info.model,
egs_serial_number: egs_info.uuid,
solution_name: solution_name,
vat_number: egs_info.VAT_number,
branch_location: `${egs_info.location?.building} ${egs_info.location?.street}, ${egs_info.location?.city}`,
branch_industry: egs_info.branch_industry,
branch_name: egs_info.branch_name,
taxpayer_name: egs_info.VAT_name,
taxpayer_provided_id: egs_info.custom_id,
production: production
}));
const cleanUp = () => {
fs_1.default.unlink(private_key_file, () => { });
fs_1.default.unlink(csr_config_file, () => { });
};
try {
const result = await OpenSSL(["req", "-new", "-sha256", "-key", private_key_file, "-config", csr_config_file]);
if (!result.includes("-----BEGIN CERTIFICATE REQUEST-----"))
throw new Error("Error no CSR found in OpenSSL output.");
let csr = `-----BEGIN CERTIFICATE REQUEST-----${result.split("-----BEGIN CERTIFICATE REQUEST-----")[1]}`.trim();
cleanUp();
return csr;
}
catch (error) {
cleanUp();
throw error;
}
};
class EGS {
egs_info;
api;
constructor(egs_info, env = "development") {
this.egs_info = egs_info;
this.api = new api_1.default(env);
}
/**
* @returns EGSUnitInfo
*/
get() {
return this.egs_info;
}
/**
* Sets/Updates an EGS info field.
* @param egs_info Partial<EGSUnitInfo>
*/
set(egs_info) {
this.egs_info = { ...this.egs_info, ...egs_info };
}
/**
* Generates a new secp256k1 Public/Private key pair for the EGS.
* Also generates and signs a new CSR.
* `Note`: This functions uses OpenSSL thus requires it to be installed on whatever system the package is running in.
* @param production Boolean CSR or Compliance CSR
* @param solution_name String name of solution generating certs.
* @returns Promise void on success, throws error on fail.
*/
async generateNewKeysAndCSR(production, solution_name) {
try {
const new_private_key = await generateSecp256k1KeyPair();
this.egs_info.private_key = new_private_key;
const new_csr = await generateCSR(this.egs_info, production, solution_name);
this.egs_info.csr = new_csr;
}
catch (error) {
throw error;
}
}
/**
* Generates a new compliance certificate through ZATCA API.
* @param OTP String Tax payer provided from Fatoora portal to link to this EGS.
* @returns Promise String compliance request id on success to be used in production CSID request, throws error on fail.
*/
async issueComplianceCertificate(OTP) {
if (!this.egs_info.csr)
throw new Error("EGS needs to generate a CSR first.");
const issued_data = await this.api.compliance().issueCertificate(this.egs_info.csr, OTP);
this.egs_info.compliance_certificate = issued_data.issued_certificate;
this.egs_info.compliance_api_secret = issued_data.api_secret;
return issued_data.request_id;
}
/**
* Generates a new production certificate through ZATCA API.
* @param compliance_request_id String compliance request ID generated from compliance CSID request.
* @returns Promise String request id on success, throws error on fail.
*/
async issueProductionCertificate(compliance_request_id) {
if (!this.egs_info.compliance_certificate || !this.egs_info.compliance_api_secret)
throw new Error("EGS is missing a certificate/private key/api secret to request a production certificate.");
const issued_data = await this.api.production(this.egs_info.compliance_certificate, this.egs_info.compliance_api_secret).issueCertificate(compliance_request_id);
this.egs_info.production_certificate = issued_data.issued_certificate;
this.egs_info.production_api_secret = issued_data.api_secret;
return issued_data.request_id;
}
/**
* Checks Invoice compliance with ZATCA API.
* @param signed_invoice_string String.
* @param invoice_hash String.
* @returns Promise compliance data on success, throws error on fail.
*/
async checkInvoiceCompliance(signed_invoice_string, invoice_hash) {
if (!this.egs_info.compliance_certificate || !this.egs_info.compliance_api_secret)
throw new Error("EGS is missing a certificate/private key/api secret to check the invoice compliance.");
return await this.api.compliance(this.egs_info.compliance_certificate, this.egs_info.compliance_api_secret).checkInvoiceCompliance(signed_invoice_string, invoice_hash, this.egs_info.uuid);
}
/**
* Reports invoice with ZATCA API.
* @param signed_invoice_string String.
* @param invoice_hash String.
* @returns Promise reporting data on success, throws error on fail.
*/
async reportInvoice(signed_invoice_string, invoice_hash) {
if (!this.egs_info.production_certificate || !this.egs_info.production_api_secret)
throw new Error("EGS is missing a certificate/private key/api secret to report the invoice.");
return await this.api.production(this.egs_info.production_certificate, this.egs_info.production_api_secret).reportInvoice(signed_invoice_string, invoice_hash, this.egs_info.uuid);
}
/**
* Reports invoice with ZATCA API.
* @param signed_invoice_string String.
* @param invoice_hash String.
* @returns Promise reporting data on success, throws error on fail.
*/
async clearanceInvoice(signed_invoice_string, invoice_hash) {
if (!this.egs_info.production_certificate || !this.egs_info.production_api_secret)
throw new Error("EGS is missing a certificate/private key/api secret to report the invoice.");
return await this.api.production(this.egs_info.production_certificate, this.egs_info.production_api_secret).clearanceInvoice(signed_invoice_string, invoice_hash, this.egs_info.uuid);
}
/**
* Signs a given invoice using the EGS certificate and keypairs.
* @param invoice Invoice to sign
* @param production Boolean production or compliance certificate.
* @returns Promise void on success (signed_invoice_string: string, invoice_hash: string, qr: string), throws error on fail.
*/
signInvoice(invoice, production) {
const certificate = production ? this.egs_info.production_certificate : this.egs_info.compliance_certificate;
if (!certificate || !this.egs_info.private_key)
throw new Error("EGS is missing a certificate/private key to sign the invoice.");
return invoice.sign(certificate, this.egs_info.private_key);
}
}
exports.EGS = EGS;