einvoice/test/suite/einvoice_pdf-operations/test.pdf-10.signature-validation.ts
2025-05-28 10:15:48 +00:00

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();