feat(invoice): add e-invoice support with XRechnung/ZUGFeRD and advanced export features
This commit is contained in:
405
ts/skr.security.ts
Normal file
405
ts/skr.security.ts
Normal file
@@ -0,0 +1,405 @@
|
||||
import * as plugins from './plugins.js';
|
||||
import * as path from 'path';
|
||||
import * as crypto from 'crypto';
|
||||
import * as https from 'https';
|
||||
|
||||
export interface ISigningOptions {
|
||||
certificatePem?: string;
|
||||
privateKeyPem?: string;
|
||||
privateKeyPassphrase?: string;
|
||||
timestampServerUrl?: string;
|
||||
includeTimestamp?: boolean;
|
||||
}
|
||||
|
||||
export interface ISignatureResult {
|
||||
signature: string;
|
||||
signatureFormat: 'CAdES-B' | 'CAdES-T' | 'CAdES-LT';
|
||||
signingTime: string;
|
||||
certificateChain?: string[];
|
||||
timestampToken?: string;
|
||||
timestampTime?: string;
|
||||
}
|
||||
|
||||
export interface ITimestampResponse {
|
||||
token: string;
|
||||
time: string;
|
||||
serverUrl: string;
|
||||
hashAlgorithm: string;
|
||||
}
|
||||
|
||||
export class SecurityManager {
|
||||
private options: ISigningOptions;
|
||||
private logger: plugins.smartlog.ConsoleLog;
|
||||
|
||||
constructor(options: ISigningOptions = {}) {
|
||||
this.options = {
|
||||
timestampServerUrl: options.timestampServerUrl || 'http://timestamp.digicert.com',
|
||||
includeTimestamp: options.includeTimestamp !== false,
|
||||
...options
|
||||
};
|
||||
this.logger = new plugins.smartlog.ConsoleLog();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a CAdES-B (Basic) signature for data
|
||||
*/
|
||||
public async createCadesSignature(
|
||||
data: Buffer | string,
|
||||
certificatePem?: string,
|
||||
privateKeyPem?: string
|
||||
): Promise<ISignatureResult> {
|
||||
const cert = certificatePem || this.options.certificatePem;
|
||||
const key = privateKeyPem || this.options.privateKeyPem;
|
||||
|
||||
if (!cert || !key) {
|
||||
throw new Error('Certificate and private key are required for signing');
|
||||
}
|
||||
|
||||
try {
|
||||
// Parse certificate and key
|
||||
const certificate = plugins.nodeForge.pki.certificateFromPem(cert);
|
||||
const privateKey = this.options.privateKeyPassphrase
|
||||
? plugins.nodeForge.pki.decryptRsaPrivateKey(key, this.options.privateKeyPassphrase)
|
||||
: plugins.nodeForge.pki.privateKeyFromPem(key);
|
||||
|
||||
// Create PKCS#7 signed data (CMS)
|
||||
const p7 = plugins.nodeForge.pkcs7.createSignedData();
|
||||
|
||||
// Add content
|
||||
if (typeof data === 'string') {
|
||||
p7.content = plugins.nodeForge.util.createBuffer(data, 'utf8');
|
||||
} else {
|
||||
p7.content = plugins.nodeForge.util.createBuffer(data.toString('latin1'));
|
||||
}
|
||||
|
||||
// Add certificate
|
||||
p7.addCertificate(certificate);
|
||||
|
||||
// Add signer
|
||||
p7.addSigner({
|
||||
key: privateKey,
|
||||
certificate: certificate,
|
||||
digestAlgorithm: plugins.nodeForge.pki.oids.sha256,
|
||||
authenticatedAttributes: [
|
||||
{
|
||||
type: plugins.nodeForge.pki.oids.contentType,
|
||||
value: plugins.nodeForge.pki.oids.data
|
||||
},
|
||||
{
|
||||
type: plugins.nodeForge.pki.oids.messageDigest
|
||||
},
|
||||
{
|
||||
type: plugins.nodeForge.pki.oids.signingTime,
|
||||
value: new Date().toISOString()
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
// Sign the data
|
||||
p7.sign({ detached: true });
|
||||
|
||||
// Convert to PEM
|
||||
const pem = plugins.nodeForge.pkcs7.messageToPem(p7);
|
||||
|
||||
// Extract base64 signature
|
||||
const signature = pem
|
||||
.replace(/-----BEGIN PKCS7-----/, '')
|
||||
.replace(/-----END PKCS7-----/, '')
|
||||
.replace(/\r?\n/g, '');
|
||||
|
||||
const result: ISignatureResult = {
|
||||
signature: signature,
|
||||
signatureFormat: 'CAdES-B',
|
||||
signingTime: new Date().toISOString(),
|
||||
certificateChain: [cert]
|
||||
};
|
||||
|
||||
// Add timestamp if requested
|
||||
if (this.options.includeTimestamp && this.options.timestampServerUrl) {
|
||||
try {
|
||||
const timestampResponse = await this.requestTimestamp(signature);
|
||||
result.timestampToken = timestampResponse.token;
|
||||
result.timestampTime = timestampResponse.time;
|
||||
result.signatureFormat = 'CAdES-T';
|
||||
} catch (error) {
|
||||
this.logger.log('warn', `Failed to obtain timestamp: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to create CAdES signature: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Requests an RFC 3161 timestamp from a TSA
|
||||
*/
|
||||
public async requestTimestamp(dataHash: string | Buffer): Promise<ITimestampResponse> {
|
||||
try {
|
||||
// Create hash of the data
|
||||
let hash: Buffer;
|
||||
if (typeof dataHash === 'string') {
|
||||
hash = crypto.createHash('sha256').update(dataHash).digest();
|
||||
} else {
|
||||
hash = crypto.createHash('sha256').update(dataHash).digest();
|
||||
}
|
||||
|
||||
// Create timestamp request (simplified - in production use proper ASN.1 encoding)
|
||||
const tsRequest = this.createTimestampRequest(hash);
|
||||
|
||||
// Send request to TSA
|
||||
const response = await this.sendTimestampRequest(tsRequest);
|
||||
|
||||
return {
|
||||
token: response.toString('base64'),
|
||||
time: new Date().toISOString(),
|
||||
serverUrl: this.options.timestampServerUrl!,
|
||||
hashAlgorithm: 'sha256'
|
||||
};
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to obtain timestamp: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a timestamp request (simplified version)
|
||||
*/
|
||||
private createTimestampRequest(hash: Buffer): Buffer {
|
||||
// In production, use proper ASN.1 encoding library
|
||||
// This is a simplified placeholder
|
||||
const request = {
|
||||
version: 1,
|
||||
messageImprint: {
|
||||
hashAlgorithm: { algorithm: '2.16.840.1.101.3.4.2.1' }, // SHA-256 OID
|
||||
hashedMessage: hash
|
||||
},
|
||||
reqPolicy: null,
|
||||
nonce: crypto.randomBytes(8),
|
||||
certReq: true
|
||||
};
|
||||
|
||||
// Convert to DER-encoded ASN.1 (simplified)
|
||||
return Buffer.from(JSON.stringify(request));
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends timestamp request to TSA server
|
||||
*/
|
||||
private async sendTimestampRequest(request: Buffer): Promise<Buffer> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const url = new URL(this.options.timestampServerUrl!);
|
||||
|
||||
const options = {
|
||||
hostname: url.hostname,
|
||||
port: url.port || 443,
|
||||
path: url.pathname,
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/timestamp-query',
|
||||
'Content-Length': request.length
|
||||
}
|
||||
};
|
||||
|
||||
const req = https.request(options, (res) => {
|
||||
const chunks: Buffer[] = [];
|
||||
|
||||
res.on('data', (chunk) => chunks.push(chunk));
|
||||
res.on('end', () => {
|
||||
const response = Buffer.concat(chunks);
|
||||
if (res.statusCode === 200) {
|
||||
resolve(response);
|
||||
} else {
|
||||
reject(new Error(`TSA server returned status ${res.statusCode}`));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
req.on('error', reject);
|
||||
req.write(request);
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies a CAdES signature
|
||||
*/
|
||||
public async verifyCadesSignature(
|
||||
data: Buffer | string,
|
||||
signature: string,
|
||||
certificatePem?: string
|
||||
): Promise<boolean> {
|
||||
try {
|
||||
// Add PEM headers if not present
|
||||
let pemSignature = signature;
|
||||
if (!signature.includes('BEGIN PKCS7')) {
|
||||
pemSignature = `-----BEGIN PKCS7-----\n${signature}\n-----END PKCS7-----`;
|
||||
}
|
||||
|
||||
// Parse the PKCS#7 message
|
||||
const p7 = plugins.nodeForge.pkcs7.messageFromPem(pemSignature);
|
||||
|
||||
// Prepare content for verification
|
||||
let content: plugins.nodeForge.util.ByteStringBuffer;
|
||||
if (typeof data === 'string') {
|
||||
content = plugins.nodeForge.util.createBuffer(data, 'utf8');
|
||||
} else {
|
||||
content = plugins.nodeForge.util.createBuffer(data.toString('latin1'));
|
||||
}
|
||||
|
||||
// Verify the signature
|
||||
const verified = (p7 as any).verify({
|
||||
content: content,
|
||||
detached: true
|
||||
});
|
||||
|
||||
return verified;
|
||||
} catch (error) {
|
||||
this.logger.log('error', `Signature verification failed: ${error}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a self-signed certificate for testing
|
||||
*/
|
||||
public async generateSelfSignedCertificate(
|
||||
commonName: string = 'SKR Export System',
|
||||
validDays: number = 365
|
||||
): Promise<{ certificate: string; privateKey: string }> {
|
||||
const keys = plugins.nodeForge.pki.rsa.generateKeyPair(2048);
|
||||
const cert = plugins.nodeForge.pki.createCertificate();
|
||||
|
||||
cert.publicKey = keys.publicKey;
|
||||
cert.serialNumber = '01';
|
||||
cert.validity.notBefore = new Date();
|
||||
cert.validity.notAfter = new Date();
|
||||
cert.validity.notAfter.setDate(cert.validity.notAfter.getDate() + validDays);
|
||||
|
||||
const attrs = [
|
||||
{ name: 'commonName', value: commonName },
|
||||
{ name: 'countryName', value: 'DE' },
|
||||
{ name: 'organizationName', value: 'SKR Export System' },
|
||||
{ shortName: 'OU', value: 'Accounting' }
|
||||
];
|
||||
|
||||
cert.setSubject(attrs);
|
||||
cert.setIssuer(attrs);
|
||||
|
||||
cert.setExtensions([
|
||||
{
|
||||
name: 'basicConstraints',
|
||||
cA: true
|
||||
},
|
||||
{
|
||||
name: 'keyUsage',
|
||||
keyCertSign: true,
|
||||
digitalSignature: true,
|
||||
nonRepudiation: true,
|
||||
keyEncipherment: true,
|
||||
dataEncipherment: true
|
||||
},
|
||||
{
|
||||
name: 'extKeyUsage',
|
||||
serverAuth: true,
|
||||
clientAuth: true,
|
||||
codeSigning: true,
|
||||
emailProtection: true,
|
||||
timeStamping: true
|
||||
},
|
||||
{
|
||||
name: 'nsCertType',
|
||||
client: true,
|
||||
server: true,
|
||||
email: true,
|
||||
objsign: true,
|
||||
sslCA: true,
|
||||
emailCA: true,
|
||||
objCA: true
|
||||
},
|
||||
{
|
||||
name: 'subjectAltName',
|
||||
altNames: [
|
||||
{ type: 2, value: commonName }
|
||||
]
|
||||
}
|
||||
]);
|
||||
|
||||
// Self-sign certificate
|
||||
cert.sign(keys.privateKey, plugins.nodeForge.md.sha256.create());
|
||||
|
||||
// Convert to PEM
|
||||
const certificatePem = plugins.nodeForge.pki.certificateToPem(cert);
|
||||
const privateKeyPem = plugins.nodeForge.pki.privateKeyToPem(keys.privateKey);
|
||||
|
||||
return {
|
||||
certificate: certificatePem,
|
||||
privateKey: privateKeyPem
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a detached signature file
|
||||
*/
|
||||
public async createDetachedSignature(
|
||||
dataPath: string,
|
||||
outputPath: string
|
||||
): Promise<void> {
|
||||
const data = await plugins.smartfile.fs.toBuffer(dataPath);
|
||||
const signature = await this.createCadesSignature(data);
|
||||
|
||||
const signatureData = {
|
||||
signature: signature.signature,
|
||||
format: signature.signatureFormat,
|
||||
signingTime: signature.signingTime,
|
||||
timestamp: signature.timestampToken,
|
||||
timestampTime: signature.timestampTime,
|
||||
algorithm: 'SHA256withRSA',
|
||||
signedFile: path.basename(dataPath)
|
||||
};
|
||||
|
||||
await plugins.smartfile.memory.toFs(
|
||||
JSON.stringify(signatureData, null, 2),
|
||||
outputPath
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies a detached signature file
|
||||
*/
|
||||
public async verifyDetachedSignature(
|
||||
dataPath: string,
|
||||
signaturePath: string
|
||||
): Promise<boolean> {
|
||||
try {
|
||||
const data = await plugins.smartfile.fs.toBuffer(dataPath);
|
||||
const signatureJson = await plugins.smartfile.fs.toStringSync(signaturePath);
|
||||
const signatureData = JSON.parse(signatureJson);
|
||||
|
||||
return await this.verifyCadesSignature(data, signatureData.signature);
|
||||
} catch (error) {
|
||||
this.logger.log('error', `Failed to verify detached signature: ${error}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds Long-Term Validation (LTV) information
|
||||
*/
|
||||
public async addLtvInformation(
|
||||
signature: ISignatureResult,
|
||||
ocspResponse?: Buffer,
|
||||
crlData?: Buffer
|
||||
): Promise<ISignatureResult> {
|
||||
// Add OCSP response and CRL data for long-term validation
|
||||
const ltv = {
|
||||
...signature,
|
||||
signatureFormat: 'CAdES-LT' as const,
|
||||
ocsp: ocspResponse?.toString('base64'),
|
||||
crl: crlData?.toString('base64'),
|
||||
ltvTime: new Date().toISOString()
|
||||
};
|
||||
|
||||
return ltv;
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user