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 { 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 { 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 { 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 { 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 { 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 { 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 { // 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; } }