405 lines
11 KiB
TypeScript
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;
|
|
}
|
|
} |