Files
skr/ts/skr.security.ts
Juergen Kunz 73b46f7857
Some checks failed
Default (tags) / security (push) Successful in 48s
Default (tags) / test (push) Failing after 4m3s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
feat(invoice): add e-invoice support with XRechnung/ZUGFeRD and advanced export features
2025-08-12 12:37:01 +00:00

405 lines
11 KiB
TypeScript

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