549 lines
16 KiB
TypeScript
549 lines
16 KiB
TypeScript
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
|
import * as plugins from '../plugins.js';
|
|
import { EInvoice } from '../../../ts/index.js';
|
|
import { CorpusLoader } from '../../helpers/corpus.loader.js';
|
|
import { PerformanceTracker as StaticPerformanceTracker } from '../performance.tracker.js';
|
|
import { rgb } from 'pdf-lib';
|
|
|
|
// PDF-10: Verify digital signature validation and preservation
|
|
// This test ensures signed PDFs are handled correctly
|
|
|
|
// Simple instance-based performance tracker for this test
|
|
class SimplePerformanceTracker {
|
|
private measurements: Map<string, number[]> = new Map();
|
|
private name: string;
|
|
|
|
constructor(name: string) {
|
|
this.name = name;
|
|
}
|
|
|
|
addMeasurement(key: string, time: number): void {
|
|
if (!this.measurements.has(key)) {
|
|
this.measurements.set(key, []);
|
|
}
|
|
this.measurements.get(key)!.push(time);
|
|
}
|
|
|
|
getAverageTime(): number {
|
|
let total = 0;
|
|
let count = 0;
|
|
for (const times of this.measurements.values()) {
|
|
for (const time of times) {
|
|
total += time;
|
|
count++;
|
|
}
|
|
}
|
|
return count > 0 ? total / count : 0;
|
|
}
|
|
|
|
printSummary(): void {
|
|
console.log(`\n${this.name} - Performance Summary:`);
|
|
for (const [key, times] of this.measurements) {
|
|
const avg = times.reduce((a, b) => a + b, 0) / times.length;
|
|
const min = Math.min(...times);
|
|
const max = Math.max(...times);
|
|
console.log(` ${key}: avg=${avg.toFixed(2)}ms, min=${min.toFixed(2)}ms, max=${max.toFixed(2)}ms (${times.length} runs)`);
|
|
}
|
|
console.log(` Overall average: ${this.getAverageTime().toFixed(2)}ms`);
|
|
}
|
|
}
|
|
|
|
const performanceTracker = new SimplePerformanceTracker('PDF-10: PDF Signature Validation');
|
|
|
|
tap.test('PDF-10: Detect Signed PDFs', async () => {
|
|
const startTime = performance.now();
|
|
|
|
const { PDFDocument } = plugins;
|
|
|
|
// Create a PDF that simulates signature structure
|
|
const pdfDoc = await PDFDocument.create();
|
|
const page = pdfDoc.addPage([595, 842]);
|
|
page.drawText('Digitally Signed Invoice', {
|
|
x: 50,
|
|
y: 750,
|
|
size: 20
|
|
});
|
|
|
|
// Add signature placeholder
|
|
page.drawRectangle({
|
|
x: 400,
|
|
y: 50,
|
|
width: 150,
|
|
height: 75,
|
|
borderColor: rgb(0, 0, 0),
|
|
borderWidth: 1
|
|
});
|
|
page.drawText('Digital Signature', {
|
|
x: 420,
|
|
y: 85,
|
|
size: 10
|
|
});
|
|
page.drawText('[Signed Document]', {
|
|
x: 420,
|
|
y: 65,
|
|
size: 8
|
|
});
|
|
|
|
// Add invoice XML
|
|
const xmlContent = `<?xml version="1.0" encoding="UTF-8"?>
|
|
<Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2">
|
|
<ID>SIGNED-001</ID>
|
|
<IssueDate>2025-01-25</IssueDate>
|
|
<DocumentCurrencyCode>EUR</DocumentCurrencyCode>
|
|
<DigitalSignatureAttachment>
|
|
<ExternalReference>
|
|
<URI>signature.p7s</URI>
|
|
<DocumentHash>SHA256:abc123...</DocumentHash>
|
|
</ExternalReference>
|
|
</DigitalSignatureAttachment>
|
|
</Invoice>`;
|
|
|
|
await pdfDoc.attach(
|
|
Buffer.from(xmlContent, 'utf8'),
|
|
'invoice.xml',
|
|
{
|
|
mimeType: 'application/xml',
|
|
description: 'Signed invoice data'
|
|
}
|
|
);
|
|
|
|
// Note: pdf-lib doesn't support actual digital signatures
|
|
// Real signature would require specialized libraries
|
|
|
|
const pdfBytes = await pdfDoc.save();
|
|
|
|
// Test signature detection
|
|
const einvoice = await EInvoice.fromPdf(pdfBytes);
|
|
|
|
console.log('Created PDF with signature placeholder');
|
|
|
|
const elapsed = performance.now() - startTime;
|
|
performanceTracker.addMeasurement('detect-signed', elapsed);
|
|
});
|
|
|
|
tap.test('PDF-10: Signature Metadata Structure', async () => {
|
|
const startTime = performance.now();
|
|
|
|
// Simulate signature metadata that might be found in signed PDFs
|
|
const signatureMetadata = {
|
|
signer: {
|
|
name: 'John Doe',
|
|
email: 'john.doe@company.com',
|
|
organization: 'ACME Corporation',
|
|
organizationUnit: 'Finance Department'
|
|
},
|
|
certificate: {
|
|
issuer: 'GlobalSign CA',
|
|
serialNumber: '01:23:45:67:89:AB:CD:EF',
|
|
validFrom: '2024-01-01T00:00:00Z',
|
|
validTo: '2026-01-01T00:00:00Z',
|
|
algorithm: 'SHA256withRSA'
|
|
},
|
|
timestamp: {
|
|
time: '2025-01-25T10:30:00Z',
|
|
authority: 'GlobalSign TSA',
|
|
hash: 'SHA256'
|
|
},
|
|
signatureDetails: {
|
|
reason: 'Invoice Approval',
|
|
location: 'Munich, Germany',
|
|
contactInfo: '+49 89 12345678'
|
|
}
|
|
};
|
|
|
|
const { PDFDocument } = plugins;
|
|
const pdfDoc = await PDFDocument.create();
|
|
|
|
// Add metadata as document properties
|
|
pdfDoc.setTitle('Signed Invoice 2025-001');
|
|
pdfDoc.setAuthor(signatureMetadata.signer.name);
|
|
pdfDoc.setSubject(`Signed by ${signatureMetadata.signer.organization}`);
|
|
pdfDoc.setKeywords(['signed', 'verified', 'invoice']);
|
|
pdfDoc.setCreator('EInvoice Signature System');
|
|
|
|
const page = pdfDoc.addPage();
|
|
page.drawText('Invoice with Signature Metadata', { x: 50, y: 750, size: 18 });
|
|
|
|
// Display signature info on page
|
|
let yPosition = 650;
|
|
page.drawText('Digital Signature Information:', { x: 50, y: yPosition, size: 14 });
|
|
yPosition -= 30;
|
|
|
|
page.drawText(`Signed by: ${signatureMetadata.signer.name}`, { x: 70, y: yPosition, size: 10 });
|
|
yPosition -= 20;
|
|
page.drawText(`Organization: ${signatureMetadata.signer.organization}`, { x: 70, y: yPosition, size: 10 });
|
|
yPosition -= 20;
|
|
page.drawText(`Date: ${signatureMetadata.timestamp.time}`, { x: 70, y: yPosition, size: 10 });
|
|
yPosition -= 20;
|
|
page.drawText(`Certificate: ${signatureMetadata.certificate.issuer}`, { x: 70, y: yPosition, size: 10 });
|
|
yPosition -= 20;
|
|
page.drawText(`Reason: ${signatureMetadata.signatureDetails.reason}`, { x: 70, y: yPosition, size: 10 });
|
|
|
|
const pdfBytes = await pdfDoc.save();
|
|
console.log('Created PDF with signature metadata structure');
|
|
|
|
const elapsed = performance.now() - startTime;
|
|
performanceTracker.addMeasurement('signature-metadata', elapsed);
|
|
});
|
|
|
|
tap.test('PDF-10: Multiple Signatures Handling', async () => {
|
|
const startTime = performance.now();
|
|
|
|
const { PDFDocument } = plugins;
|
|
const pdfDoc = await PDFDocument.create();
|
|
const page = pdfDoc.addPage();
|
|
|
|
page.drawText('Multi-Signature Invoice', { x: 50, y: 750, size: 20 });
|
|
|
|
// Simulate multiple signature fields
|
|
const signatures = [
|
|
{
|
|
name: 'Creator Signature',
|
|
signer: 'Invoice System',
|
|
date: '2025-01-25T09:00:00Z',
|
|
position: { x: 50, y: 150 }
|
|
},
|
|
{
|
|
name: 'Approval Signature',
|
|
signer: 'Finance Manager',
|
|
date: '2025-01-25T10:00:00Z',
|
|
position: { x: 220, y: 150 }
|
|
},
|
|
{
|
|
name: 'Verification Signature',
|
|
signer: 'Auditor',
|
|
date: '2025-01-25T11:00:00Z',
|
|
position: { x: 390, y: 150 }
|
|
}
|
|
];
|
|
|
|
// Draw signature boxes
|
|
signatures.forEach(sig => {
|
|
page.drawRectangle({
|
|
x: sig.position.x,
|
|
y: sig.position.y,
|
|
width: 150,
|
|
height: 80,
|
|
borderColor: rgb(0, 0, 0),
|
|
borderWidth: 1
|
|
});
|
|
|
|
page.drawText(sig.name, {
|
|
x: sig.position.x + 10,
|
|
y: sig.position.y + 60,
|
|
size: 10
|
|
});
|
|
|
|
page.drawText(sig.signer, {
|
|
x: sig.position.x + 10,
|
|
y: sig.position.y + 40,
|
|
size: 8
|
|
});
|
|
|
|
page.drawText(sig.date, {
|
|
x: sig.position.x + 10,
|
|
y: sig.position.y + 20,
|
|
size: 8
|
|
});
|
|
});
|
|
|
|
// Add invoice with signature references
|
|
const xmlContent = `<?xml version="1.0" encoding="UTF-8"?>
|
|
<Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2">
|
|
<ID>MULTI-SIG-001</ID>
|
|
<Signature>
|
|
<ID>SIG-1</ID>
|
|
<SignatoryParty>
|
|
<PartyName><Name>Invoice System</Name></PartyName>
|
|
</SignatoryParty>
|
|
</Signature>
|
|
<Signature>
|
|
<ID>SIG-2</ID>
|
|
<SignatoryParty>
|
|
<PartyName><Name>Finance Manager</Name></PartyName>
|
|
</SignatoryParty>
|
|
</Signature>
|
|
<Signature>
|
|
<ID>SIG-3</ID>
|
|
<SignatoryParty>
|
|
<PartyName><Name>Auditor</Name></PartyName>
|
|
</SignatoryParty>
|
|
</Signature>
|
|
</Invoice>`;
|
|
|
|
await pdfDoc.attach(
|
|
Buffer.from(xmlContent, 'utf8'),
|
|
'invoice.xml',
|
|
{ mimeType: 'application/xml' }
|
|
);
|
|
|
|
const pdfBytes = await pdfDoc.save();
|
|
console.log('Created PDF with multiple signature placeholders');
|
|
|
|
const elapsed = performance.now() - startTime;
|
|
performanceTracker.addMeasurement('multiple-signatures', elapsed);
|
|
});
|
|
|
|
tap.test('PDF-10: Signature Validation Status', async () => {
|
|
const startTime = performance.now();
|
|
|
|
// Simulate different signature validation statuses
|
|
const validationStatuses = [
|
|
{ status: 'VALID', color: rgb(0, 0.5, 0), message: 'Signature Valid' },
|
|
{ status: 'INVALID', color: rgb(0.8, 0, 0), message: 'Signature Invalid' },
|
|
{ status: 'UNKNOWN', color: rgb(0.5, 0.5, 0), message: 'Signature Unknown' },
|
|
{ status: 'EXPIRED', color: rgb(0.8, 0.4, 0), message: 'Certificate Expired' }
|
|
];
|
|
|
|
const { PDFDocument } = plugins;
|
|
|
|
for (const valStatus of validationStatuses) {
|
|
const pdfDoc = await PDFDocument.create();
|
|
const page = pdfDoc.addPage();
|
|
|
|
page.drawText(`Invoice - Signature ${valStatus.status}`, {
|
|
x: 50,
|
|
y: 750,
|
|
size: 20
|
|
});
|
|
|
|
// Draw status indicator
|
|
page.drawRectangle({
|
|
x: 450,
|
|
y: 740,
|
|
width: 100,
|
|
height: 30,
|
|
color: valStatus.color,
|
|
borderColor: rgb(0, 0, 0),
|
|
borderWidth: 1
|
|
});
|
|
|
|
page.drawText(valStatus.message, {
|
|
x: 460,
|
|
y: 750,
|
|
size: 10,
|
|
color: rgb(1, 1, 1)
|
|
});
|
|
|
|
const xmlContent = `<?xml version="1.0" encoding="UTF-8"?>
|
|
<Invoice>
|
|
<ID>SIG-${valStatus.status}</ID>
|
|
<SignatureValidation>
|
|
<Status>${valStatus.status}</Status>
|
|
<Message>${valStatus.message}</Message>
|
|
</SignatureValidation>
|
|
</Invoice>`;
|
|
|
|
await pdfDoc.attach(
|
|
Buffer.from(xmlContent, 'utf8'),
|
|
'invoice.xml',
|
|
{ mimeType: 'application/xml' }
|
|
);
|
|
|
|
const pdfBytes = await pdfDoc.save();
|
|
console.log(`Created PDF with signature status: ${valStatus.status}`);
|
|
}
|
|
|
|
const elapsed = performance.now() - startTime;
|
|
performanceTracker.addMeasurement('validation-status', elapsed);
|
|
});
|
|
|
|
tap.test('PDF-10: Signature Preservation During Operations', async () => {
|
|
const startTime = performance.now();
|
|
|
|
const { PDFDocument } = plugins;
|
|
|
|
// Create original "signed" PDF
|
|
const originalPdf = await PDFDocument.create();
|
|
originalPdf.setTitle('Original Signed Document');
|
|
originalPdf.setAuthor('Original Signer');
|
|
originalPdf.setSubject('This document has been digitally signed');
|
|
|
|
const page = originalPdf.addPage();
|
|
page.drawText('Original Signed Invoice', { x: 50, y: 750, size: 20 });
|
|
|
|
// Add signature visual
|
|
page.drawRectangle({
|
|
x: 400,
|
|
y: 50,
|
|
width: 150,
|
|
height: 75,
|
|
borderColor: rgb(0, 0.5, 0),
|
|
borderWidth: 2
|
|
});
|
|
page.drawText('[OK] Digitally Signed', {
|
|
x: 420,
|
|
y: 85,
|
|
size: 12,
|
|
color: rgb(0, 0.5, 0)
|
|
});
|
|
|
|
const originalBytes = await originalPdf.save();
|
|
|
|
// Process through EInvoice
|
|
// Add new XML while preserving signature
|
|
const xmlContent = `<?xml version="1.0" encoding="UTF-8"?>
|
|
<Invoice>
|
|
<ID>PRESERVE-SIG-001</ID>
|
|
<Note>Added to signed document</Note>
|
|
</Invoice>`;
|
|
|
|
try {
|
|
const einvoice = await EInvoice.fromPdf(originalBytes);
|
|
|
|
// In a real implementation, this would need to preserve signatures
|
|
console.log('Note: Adding content to signed PDFs typically invalidates signatures');
|
|
console.log('Incremental updates would be needed to preserve signature validity');
|
|
} catch (error) {
|
|
console.log('Signature preservation challenge:', error.message);
|
|
}
|
|
|
|
const elapsed = performance.now() - startTime;
|
|
performanceTracker.addMeasurement('signature-preservation', elapsed);
|
|
});
|
|
|
|
tap.test('PDF-10: Timestamp Validation', async () => {
|
|
const startTime = performance.now();
|
|
|
|
const { PDFDocument } = plugins;
|
|
const pdfDoc = await PDFDocument.create();
|
|
|
|
const page = pdfDoc.addPage();
|
|
page.drawText('Time-stamped Invoice', { x: 50, y: 750, size: 20 });
|
|
|
|
// Simulate timestamp information
|
|
const timestamps = [
|
|
{
|
|
type: 'Document Creation',
|
|
time: '2025-01-25T09:00:00Z',
|
|
authority: 'Internal TSA'
|
|
},
|
|
{
|
|
type: 'Signature Timestamp',
|
|
time: '2025-01-25T10:30:00Z',
|
|
authority: 'Qualified TSA Provider'
|
|
},
|
|
{
|
|
type: 'Archive Timestamp',
|
|
time: '2025-01-25T11:00:00Z',
|
|
authority: 'Long-term Archive TSA'
|
|
}
|
|
];
|
|
|
|
let yPos = 650;
|
|
page.drawText('Timestamp Information:', { x: 50, y: yPos, size: 14 });
|
|
|
|
timestamps.forEach(ts => {
|
|
yPos -= 30;
|
|
page.drawText(`${ts.type}:`, { x: 70, y: yPos, size: 10 });
|
|
yPos -= 20;
|
|
page.drawText(`Time: ${ts.time}`, { x: 90, y: yPos, size: 9 });
|
|
yPos -= 15;
|
|
page.drawText(`TSA: ${ts.authority}`, { x: 90, y: yPos, size: 9 });
|
|
});
|
|
|
|
const xmlContent = `<?xml version="1.0" encoding="UTF-8"?>
|
|
<Invoice>
|
|
<ID>TIMESTAMP-001</ID>
|
|
<Timestamps>
|
|
${timestamps.map(ts => `
|
|
<Timestamp type="${ts.type}">
|
|
<Time>${ts.time}</Time>
|
|
<Authority>${ts.authority}</Authority>
|
|
</Timestamp>`).join('')}
|
|
</Timestamps>
|
|
</Invoice>`;
|
|
|
|
await pdfDoc.attach(
|
|
Buffer.from(xmlContent, 'utf8'),
|
|
'invoice.xml',
|
|
{ mimeType: 'application/xml' }
|
|
);
|
|
|
|
const pdfBytes = await pdfDoc.save();
|
|
console.log('Created PDF with timestamp information');
|
|
|
|
const elapsed = performance.now() - startTime;
|
|
performanceTracker.addMeasurement('timestamp-validation', elapsed);
|
|
});
|
|
|
|
tap.test('PDF-10: Corpus Signed PDF Detection', async () => {
|
|
const startTime = performance.now();
|
|
let signedCount = 0;
|
|
let processedCount = 0;
|
|
const signatureIndicators: string[] = [];
|
|
|
|
// Get PDF files from different categories
|
|
const categories = ['ZUGFERD_V1_CORRECT', 'ZUGFERD_V2_CORRECT', 'ZUGFERD_V2_FAIL', 'UNSTRUCTURED'] as const;
|
|
const allPdfFiles: Array<{ path: string; size: number }> = [];
|
|
|
|
for (const category of categories) {
|
|
try {
|
|
const files = await CorpusLoader.loadCategory(category);
|
|
const pdfFiles = files.filter(f => f.path.toLowerCase().endsWith('.pdf'));
|
|
allPdfFiles.push(...pdfFiles);
|
|
} catch (error) {
|
|
console.log(`Could not load category ${category}: ${error.message}`);
|
|
}
|
|
}
|
|
|
|
// Check PDFs for signature indicators
|
|
const sampleSize = Math.min(50, allPdfFiles.length);
|
|
const sample = allPdfFiles.slice(0, sampleSize);
|
|
|
|
for (const file of sample) {
|
|
try {
|
|
const content = await CorpusLoader.loadFile(file.path);
|
|
|
|
// Look for signature indicators in PDF content
|
|
const pdfString = content.toString('binary');
|
|
const indicators = [
|
|
'/Type /Sig',
|
|
'/ByteRange',
|
|
'/SubFilter',
|
|
'/adbe.pkcs7',
|
|
'/ETSI.CAdES',
|
|
'SignatureField',
|
|
'DigitalSignature'
|
|
];
|
|
|
|
let hasSignature = false;
|
|
for (const indicator of indicators) {
|
|
if (pdfString.includes(indicator)) {
|
|
hasSignature = true;
|
|
if (!signatureIndicators.includes(indicator)) {
|
|
signatureIndicators.push(indicator);
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (hasSignature) {
|
|
signedCount++;
|
|
console.log(`Potential signed PDF: ${file.path}`);
|
|
}
|
|
|
|
processedCount++;
|
|
} catch (error) {
|
|
console.log(`Error checking ${file.path}:`, error.message);
|
|
}
|
|
}
|
|
|
|
console.log(`Corpus signature analysis (${processedCount} PDFs):`);
|
|
console.log(`- PDFs with signature indicators: ${signedCount}`);
|
|
console.log('Signature indicators found:', signatureIndicators);
|
|
|
|
const elapsed = performance.now() - startTime;
|
|
performanceTracker.addMeasurement('corpus-signed-pdfs', elapsed);
|
|
});
|
|
|
|
tap.test('PDF-10: Performance Summary', async () => {
|
|
// Print performance summary
|
|
performanceTracker.printSummary();
|
|
|
|
// Performance assertions
|
|
const avgTime = performanceTracker.getAverageTime();
|
|
expect(avgTime).toBeLessThan(300); // Signature operations should be reasonably fast
|
|
});
|
|
|
|
tap.start(); |