2026-01-04 19:08:50 +05:30
|
|
|
import { createIcons, icons } from 'lucide';
|
|
|
|
|
import { showAlert, showLoader, hideLoader } from '../ui.js';
|
|
|
|
|
import { readFileAsArrayBuffer, formatBytes } from '../utils/helpers.js';
|
2026-01-05 14:57:30 +05:30
|
|
|
import { validatePdfSignatures } from './validate-signature-pdf.js';
|
2026-01-04 19:08:50 +05:30
|
|
|
import forge from 'node-forge';
|
2026-01-05 14:57:30 +05:30
|
|
|
import { SignatureValidationResult, ValidateSignatureState } from '@/types';
|
2026-01-04 19:08:50 +05:30
|
|
|
|
|
|
|
|
const state: ValidateSignatureState = {
|
2026-04-17 23:40:24 +05:30
|
|
|
pdfFile: null,
|
|
|
|
|
pdfBytes: null,
|
|
|
|
|
results: [],
|
|
|
|
|
trustedCertFile: null,
|
|
|
|
|
trustedCert: null,
|
2026-01-04 19:08:50 +05:30
|
|
|
};
|
|
|
|
|
|
|
|
|
|
function getElement<T extends HTMLElement>(id: string): T | null {
|
2026-04-17 23:40:24 +05:30
|
|
|
return document.getElementById(id) as T | null;
|
2026-01-04 19:08:50 +05:30
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function resetState(): void {
|
2026-04-17 23:40:24 +05:30
|
|
|
state.pdfFile = null;
|
|
|
|
|
state.pdfBytes = null;
|
|
|
|
|
state.results = [];
|
2026-01-04 19:08:50 +05:30
|
|
|
|
2026-04-17 23:40:24 +05:30
|
|
|
const fileDisplayArea = getElement<HTMLDivElement>('file-display-area');
|
|
|
|
|
if (fileDisplayArea) fileDisplayArea.innerHTML = '';
|
2026-01-04 19:08:50 +05:30
|
|
|
|
2026-04-17 23:40:24 +05:30
|
|
|
const resultsSection = getElement<HTMLDivElement>('results-section');
|
|
|
|
|
if (resultsSection) resultsSection.classList.add('hidden');
|
2026-01-04 19:08:50 +05:30
|
|
|
|
2026-04-17 23:40:24 +05:30
|
|
|
const resultsContainer = getElement<HTMLDivElement>('results-container');
|
|
|
|
|
if (resultsContainer) resultsContainer.innerHTML = '';
|
2026-01-04 19:08:50 +05:30
|
|
|
|
2026-04-17 23:40:24 +05:30
|
|
|
const fileInput = getElement<HTMLInputElement>('file-input');
|
|
|
|
|
if (fileInput) fileInput.value = '';
|
2026-01-04 19:08:50 +05:30
|
|
|
|
2026-04-17 23:40:24 +05:30
|
|
|
const customCertSection = getElement<HTMLDivElement>('custom-cert-section');
|
|
|
|
|
if (customCertSection) customCertSection.classList.add('hidden');
|
2026-01-04 19:08:50 +05:30
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function resetCertState(): void {
|
2026-04-17 23:40:24 +05:30
|
|
|
state.trustedCertFile = null;
|
|
|
|
|
state.trustedCert = null;
|
2026-01-04 19:08:50 +05:30
|
|
|
|
2026-04-17 23:40:24 +05:30
|
|
|
const certDisplayArea = getElement<HTMLDivElement>('cert-display-area');
|
|
|
|
|
if (certDisplayArea) certDisplayArea.innerHTML = '';
|
2026-01-04 19:08:50 +05:30
|
|
|
|
2026-04-17 23:40:24 +05:30
|
|
|
const certInput = getElement<HTMLInputElement>('cert-input');
|
|
|
|
|
if (certInput) certInput.value = '';
|
2026-01-04 19:08:50 +05:30
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function initializePage(): void {
|
2026-04-17 23:40:24 +05:30
|
|
|
createIcons({ icons });
|
|
|
|
|
|
|
|
|
|
const fileInput = getElement<HTMLInputElement>('file-input');
|
|
|
|
|
const dropZone = getElement<HTMLDivElement>('drop-zone');
|
|
|
|
|
const backBtn = getElement<HTMLButtonElement>('back-to-tools');
|
|
|
|
|
const certInput = getElement<HTMLInputElement>('cert-input');
|
|
|
|
|
const certDropZone = getElement<HTMLDivElement>('cert-drop-zone');
|
|
|
|
|
|
|
|
|
|
if (fileInput) {
|
|
|
|
|
fileInput.addEventListener('change', handlePdfUpload);
|
|
|
|
|
fileInput.addEventListener('click', () => {
|
|
|
|
|
fileInput.value = '';
|
|
|
|
|
});
|
|
|
|
|
}
|
2026-01-04 19:08:50 +05:30
|
|
|
|
2026-04-17 23:40:24 +05:30
|
|
|
if (dropZone) {
|
|
|
|
|
dropZone.addEventListener('dragover', (e) => {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
dropZone.classList.add('bg-gray-700');
|
|
|
|
|
});
|
2026-01-04 19:08:50 +05:30
|
|
|
|
2026-04-17 23:40:24 +05:30
|
|
|
dropZone.addEventListener('dragleave', () => {
|
|
|
|
|
dropZone.classList.remove('bg-gray-700');
|
|
|
|
|
});
|
2026-01-04 19:08:50 +05:30
|
|
|
|
2026-04-17 23:40:24 +05:30
|
|
|
dropZone.addEventListener('drop', (e) => {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
dropZone.classList.remove('bg-gray-700');
|
|
|
|
|
const droppedFiles = e.dataTransfer?.files;
|
|
|
|
|
if (droppedFiles && droppedFiles.length > 0) {
|
|
|
|
|
handlePdfFile(droppedFiles[0]);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
2026-01-04 19:08:50 +05:30
|
|
|
|
2026-04-17 23:40:24 +05:30
|
|
|
if (certInput) {
|
|
|
|
|
certInput.addEventListener('change', handleCertUpload);
|
|
|
|
|
certInput.addEventListener('click', () => {
|
|
|
|
|
certInput.value = '';
|
|
|
|
|
});
|
|
|
|
|
}
|
2026-01-04 19:08:50 +05:30
|
|
|
|
2026-04-17 23:40:24 +05:30
|
|
|
if (certDropZone) {
|
|
|
|
|
certDropZone.addEventListener('dragover', (e) => {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
certDropZone.classList.add('bg-gray-700');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
certDropZone.addEventListener('dragleave', () => {
|
|
|
|
|
certDropZone.classList.remove('bg-gray-700');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
certDropZone.addEventListener('drop', (e) => {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
certDropZone.classList.remove('bg-gray-700');
|
|
|
|
|
const droppedFiles = e.dataTransfer?.files;
|
|
|
|
|
if (droppedFiles && droppedFiles.length > 0) {
|
|
|
|
|
handleCertFile(droppedFiles[0]);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (backBtn) {
|
|
|
|
|
backBtn.addEventListener('click', () => {
|
|
|
|
|
window.location.href = import.meta.env.BASE_URL;
|
|
|
|
|
});
|
|
|
|
|
}
|
2026-01-04 19:08:50 +05:30
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function handlePdfUpload(e: Event): void {
|
2026-04-17 23:40:24 +05:30
|
|
|
const input = e.target as HTMLInputElement;
|
|
|
|
|
if (input.files && input.files.length > 0) {
|
|
|
|
|
handlePdfFile(input.files[0]);
|
|
|
|
|
}
|
2026-01-04 19:08:50 +05:30
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function handlePdfFile(file: File): Promise<void> {
|
2026-04-17 23:40:24 +05:30
|
|
|
if (
|
|
|
|
|
file.type !== 'application/pdf' &&
|
|
|
|
|
!file.name.toLowerCase().endsWith('.pdf')
|
|
|
|
|
) {
|
|
|
|
|
showAlert('Invalid File', 'Please select a PDF file.');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
resetState();
|
|
|
|
|
state.pdfFile = file;
|
|
|
|
|
state.pdfBytes = new Uint8Array(
|
|
|
|
|
(await readFileAsArrayBuffer(file)) as ArrayBuffer
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
updatePdfDisplay();
|
|
|
|
|
|
|
|
|
|
const customCertSection = getElement<HTMLDivElement>('custom-cert-section');
|
|
|
|
|
if (customCertSection) customCertSection.classList.remove('hidden');
|
|
|
|
|
createIcons({ icons });
|
|
|
|
|
|
|
|
|
|
await validateSignatures();
|
2026-01-04 19:08:50 +05:30
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function updatePdfDisplay(): void {
|
2026-04-17 23:40:24 +05:30
|
|
|
const fileDisplayArea = getElement<HTMLDivElement>('file-display-area');
|
|
|
|
|
if (!fileDisplayArea || !state.pdfFile) return;
|
2026-01-04 19:08:50 +05:30
|
|
|
|
2026-04-17 23:40:24 +05:30
|
|
|
fileDisplayArea.innerHTML = '';
|
2026-01-04 19:08:50 +05:30
|
|
|
|
2026-04-17 23:40:24 +05:30
|
|
|
const fileDiv = document.createElement('div');
|
|
|
|
|
fileDiv.className =
|
|
|
|
|
'flex items-center justify-between bg-gray-700 p-3 rounded-lg';
|
2026-01-04 19:08:50 +05:30
|
|
|
|
2026-04-17 23:40:24 +05:30
|
|
|
const infoContainer = document.createElement('div');
|
|
|
|
|
infoContainer.className = 'flex flex-col flex-1 min-w-0';
|
2026-01-04 19:08:50 +05:30
|
|
|
|
2026-04-17 23:40:24 +05:30
|
|
|
const nameSpan = document.createElement('div');
|
|
|
|
|
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
|
|
|
|
|
nameSpan.textContent = state.pdfFile.name;
|
2026-01-04 19:08:50 +05:30
|
|
|
|
2026-04-17 23:40:24 +05:30
|
|
|
const metaSpan = document.createElement('div');
|
|
|
|
|
metaSpan.className = 'text-xs text-gray-400';
|
|
|
|
|
metaSpan.textContent = formatBytes(state.pdfFile.size);
|
2026-01-04 19:08:50 +05:30
|
|
|
|
2026-04-17 23:40:24 +05:30
|
|
|
infoContainer.append(nameSpan, metaSpan);
|
2026-01-04 19:08:50 +05:30
|
|
|
|
2026-04-17 23:40:24 +05:30
|
|
|
const removeBtn = document.createElement('button');
|
|
|
|
|
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
|
|
|
|
|
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
|
|
|
|
|
removeBtn.onclick = () => resetState();
|
2026-01-04 19:08:50 +05:30
|
|
|
|
2026-04-17 23:40:24 +05:30
|
|
|
fileDiv.append(infoContainer, removeBtn);
|
|
|
|
|
fileDisplayArea.appendChild(fileDiv);
|
|
|
|
|
createIcons({ icons });
|
2026-01-04 19:08:50 +05:30
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function handleCertUpload(e: Event): void {
|
2026-04-17 23:40:24 +05:30
|
|
|
const input = e.target as HTMLInputElement;
|
|
|
|
|
if (input.files && input.files.length > 0) {
|
|
|
|
|
handleCertFile(input.files[0]);
|
|
|
|
|
}
|
2026-01-04 19:08:50 +05:30
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function handleCertFile(file: File): Promise<void> {
|
2026-04-17 23:40:24 +05:30
|
|
|
const validExtensions = ['.pem', '.crt', '.cer', '.der'];
|
|
|
|
|
const hasValidExtension = validExtensions.some((ext) =>
|
|
|
|
|
file.name.toLowerCase().endsWith(ext)
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (!hasValidExtension) {
|
|
|
|
|
showAlert(
|
|
|
|
|
'Invalid Certificate',
|
|
|
|
|
'Please select a .pem, .crt, .cer, or .der certificate file.'
|
|
|
|
|
);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
resetCertState();
|
|
|
|
|
state.trustedCertFile = file;
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const content = await file.text();
|
|
|
|
|
|
|
|
|
|
if (content.includes('-----BEGIN CERTIFICATE-----')) {
|
|
|
|
|
state.trustedCert = forge.pki.certificateFromPem(content);
|
|
|
|
|
} else {
|
|
|
|
|
const bytes = new Uint8Array(
|
|
|
|
|
(await readFileAsArrayBuffer(file)) as ArrayBuffer
|
|
|
|
|
);
|
|
|
|
|
const derString = String.fromCharCode.apply(null, Array.from(bytes));
|
|
|
|
|
const asn1 = forge.asn1.fromDer(derString);
|
|
|
|
|
state.trustedCert = forge.pki.certificateFromAsn1(asn1);
|
2026-01-04 19:08:50 +05:30
|
|
|
}
|
|
|
|
|
|
2026-04-17 23:40:24 +05:30
|
|
|
updateCertDisplay();
|
|
|
|
|
|
|
|
|
|
if (state.pdfBytes) {
|
|
|
|
|
await validateSignatures();
|
2026-01-04 19:08:50 +05:30
|
|
|
}
|
2026-04-17 23:40:24 +05:30
|
|
|
} catch (error) {
|
|
|
|
|
console.error('Error parsing certificate:', error);
|
|
|
|
|
showAlert('Invalid Certificate', 'Failed to parse the certificate file.');
|
|
|
|
|
resetCertState();
|
|
|
|
|
}
|
2026-01-04 19:08:50 +05:30
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function updateCertDisplay(): void {
|
2026-04-17 23:40:24 +05:30
|
|
|
const certDisplayArea = getElement<HTMLDivElement>('cert-display-area');
|
|
|
|
|
if (!certDisplayArea || !state.trustedCertFile || !state.trustedCert) return;
|
2026-01-04 19:08:50 +05:30
|
|
|
|
2026-04-17 23:40:24 +05:30
|
|
|
certDisplayArea.innerHTML = '';
|
2026-01-04 19:08:50 +05:30
|
|
|
|
2026-04-17 23:40:24 +05:30
|
|
|
const certDiv = document.createElement('div');
|
|
|
|
|
certDiv.className =
|
|
|
|
|
'flex items-center justify-between bg-gray-700 p-3 rounded-lg';
|
2026-01-04 19:08:50 +05:30
|
|
|
|
2026-04-17 23:40:24 +05:30
|
|
|
const infoContainer = document.createElement('div');
|
|
|
|
|
infoContainer.className = 'flex flex-col flex-1 min-w-0';
|
2026-01-04 19:08:50 +05:30
|
|
|
|
2026-04-17 23:40:24 +05:30
|
|
|
const nameSpan = document.createElement('div');
|
|
|
|
|
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
|
2026-01-04 19:08:50 +05:30
|
|
|
|
2026-04-17 23:40:24 +05:30
|
|
|
const cn = state.trustedCert.subject.getField('CN');
|
|
|
|
|
nameSpan.textContent = (cn?.value as string) || state.trustedCertFile.name;
|
2026-01-04 19:08:50 +05:30
|
|
|
|
2026-04-17 23:40:24 +05:30
|
|
|
const metaSpan = document.createElement('div');
|
|
|
|
|
metaSpan.className = 'text-xs text-green-400';
|
|
|
|
|
metaSpan.innerHTML =
|
|
|
|
|
'<i data-lucide="check-circle" class="inline w-3 h-3 mr-1"></i>Trusted certificate loaded';
|
2026-01-04 19:08:50 +05:30
|
|
|
|
2026-04-17 23:40:24 +05:30
|
|
|
infoContainer.append(nameSpan, metaSpan);
|
2026-01-04 19:08:50 +05:30
|
|
|
|
2026-04-17 23:40:24 +05:30
|
|
|
const removeBtn = document.createElement('button');
|
|
|
|
|
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
|
|
|
|
|
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
|
|
|
|
|
removeBtn.onclick = async () => {
|
|
|
|
|
resetCertState();
|
|
|
|
|
if (state.pdfBytes) {
|
|
|
|
|
await validateSignatures();
|
|
|
|
|
}
|
|
|
|
|
};
|
2026-01-04 19:08:50 +05:30
|
|
|
|
2026-04-17 23:40:24 +05:30
|
|
|
certDiv.append(infoContainer, removeBtn);
|
|
|
|
|
certDisplayArea.appendChild(certDiv);
|
|
|
|
|
createIcons({ icons });
|
2026-01-04 19:08:50 +05:30
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function validateSignatures(): Promise<void> {
|
2026-04-17 23:40:24 +05:30
|
|
|
if (!state.pdfBytes) return;
|
|
|
|
|
|
|
|
|
|
showLoader('Analyzing signatures...');
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
state.results = await validatePdfSignatures(
|
|
|
|
|
state.pdfBytes,
|
|
|
|
|
state.trustedCert ?? undefined
|
|
|
|
|
);
|
|
|
|
|
displayResults();
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('Validation error:', error);
|
|
|
|
|
showAlert(
|
|
|
|
|
'Error',
|
|
|
|
|
'Failed to validate signatures. The file may be corrupted.'
|
|
|
|
|
);
|
|
|
|
|
} finally {
|
|
|
|
|
hideLoader();
|
|
|
|
|
}
|
2026-01-04 19:08:50 +05:30
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function displayResults(): void {
|
2026-04-17 23:40:24 +05:30
|
|
|
const resultsSection = getElement<HTMLDivElement>('results-section');
|
|
|
|
|
const resultsContainer = getElement<HTMLDivElement>('results-container');
|
2026-01-04 19:08:50 +05:30
|
|
|
|
2026-04-17 23:40:24 +05:30
|
|
|
if (!resultsSection || !resultsContainer) return;
|
2026-01-04 19:08:50 +05:30
|
|
|
|
2026-04-17 23:40:24 +05:30
|
|
|
resultsContainer.innerHTML = '';
|
|
|
|
|
resultsSection.classList.remove('hidden');
|
2026-01-04 19:08:50 +05:30
|
|
|
|
2026-04-17 23:40:24 +05:30
|
|
|
if (state.results.length === 0) {
|
|
|
|
|
resultsContainer.innerHTML = `
|
2026-01-04 19:08:50 +05:30
|
|
|
<div class="bg-gray-700 rounded-lg p-6 text-center border border-gray-600">
|
|
|
|
|
<i data-lucide="file-x" class="w-12 h-12 mx-auto mb-4 text-gray-400"></i>
|
|
|
|
|
<h3 class="text-lg font-semibold text-white mb-2">No Signatures Found</h3>
|
|
|
|
|
<p class="text-gray-400">This PDF does not contain any digital signatures.</p>
|
|
|
|
|
</div>
|
|
|
|
|
`;
|
2026-04-17 23:40:24 +05:30
|
|
|
createIcons({ icons });
|
|
|
|
|
return;
|
|
|
|
|
}
|
2026-01-04 19:08:50 +05:30
|
|
|
|
2026-04-17 23:40:24 +05:30
|
|
|
const summaryDiv = document.createElement('div');
|
|
|
|
|
summaryDiv.className =
|
|
|
|
|
'mb-4 p-3 bg-gray-700 rounded-lg border border-gray-600';
|
2026-01-04 19:08:50 +05:30
|
|
|
|
2026-04-17 23:40:24 +05:30
|
|
|
const validCount = state.results.filter(
|
|
|
|
|
(r) => r.isValid && !r.isExpired
|
|
|
|
|
).length;
|
|
|
|
|
const trustVerified = state.trustedCert
|
|
|
|
|
? state.results.filter((r) => r.isTrusted).length
|
|
|
|
|
: 0;
|
2026-01-04 19:08:50 +05:30
|
|
|
|
2026-04-17 23:40:24 +05:30
|
|
|
let summaryHtml = `
|
2026-01-04 19:08:50 +05:30
|
|
|
<p class="text-gray-300">
|
|
|
|
|
<span class="font-semibold text-white">${state.results.length}</span>
|
|
|
|
|
signature${state.results.length > 1 ? 's' : ''} found
|
|
|
|
|
<span class="text-gray-500">•</span>
|
|
|
|
|
<span class="${validCount === state.results.length ? 'text-green-400' : 'text-yellow-400'}">${validCount} valid</span>
|
|
|
|
|
</p>
|
|
|
|
|
`;
|
|
|
|
|
|
2026-04-17 23:40:24 +05:30
|
|
|
if (state.trustedCert) {
|
|
|
|
|
summaryHtml += `
|
2026-01-04 19:08:50 +05:30
|
|
|
<p class="text-xs text-gray-400 mt-1">
|
|
|
|
|
<i data-lucide="shield-check" class="inline w-3 h-3 mr-1"></i>
|
|
|
|
|
Trust verification: ${trustVerified}/${state.results.length} signatures verified against custom certificate
|
|
|
|
|
</p>
|
|
|
|
|
`;
|
2026-04-17 23:40:24 +05:30
|
|
|
}
|
2026-01-04 19:08:50 +05:30
|
|
|
|
2026-04-17 23:40:24 +05:30
|
|
|
summaryDiv.innerHTML = summaryHtml;
|
|
|
|
|
resultsContainer.appendChild(summaryDiv);
|
2026-01-04 19:08:50 +05:30
|
|
|
|
2026-04-17 23:40:24 +05:30
|
|
|
state.results.forEach((result, index) => {
|
|
|
|
|
const card = createSignatureCard(result, index);
|
|
|
|
|
resultsContainer.appendChild(card);
|
|
|
|
|
});
|
2026-01-04 19:08:50 +05:30
|
|
|
|
2026-04-17 23:40:24 +05:30
|
|
|
createIcons({ icons });
|
2026-01-04 19:08:50 +05:30
|
|
|
}
|
|
|
|
|
|
2026-04-17 23:40:24 +05:30
|
|
|
function createSignatureCard(
|
|
|
|
|
result: SignatureValidationResult,
|
|
|
|
|
index: number
|
|
|
|
|
): HTMLElement {
|
|
|
|
|
const card = document.createElement('div');
|
|
|
|
|
card.className = 'bg-gray-700 rounded-lg p-4 border border-gray-600 mb-4';
|
|
|
|
|
|
|
|
|
|
let statusColor = 'text-green-400';
|
|
|
|
|
let statusIcon = 'check-circle';
|
|
|
|
|
let statusText = 'Valid Signature';
|
|
|
|
|
|
|
|
|
|
if (!result.isValid) {
|
|
|
|
|
if (result.cryptoVerificationStatus === 'unsupported') {
|
|
|
|
|
statusColor = 'text-yellow-400';
|
|
|
|
|
statusIcon = 'alert-triangle';
|
|
|
|
|
statusText = 'Unverified — Unsupported Signature Algorithm';
|
|
|
|
|
} else {
|
|
|
|
|
statusColor = 'text-red-400';
|
|
|
|
|
statusIcon = 'x-circle';
|
|
|
|
|
statusText =
|
|
|
|
|
result.cryptoVerified === false
|
|
|
|
|
? 'Invalid — Cryptographic Verification Failed'
|
|
|
|
|
: 'Invalid Signature';
|
2026-01-04 19:08:50 +05:30
|
|
|
}
|
2026-04-17 23:40:24 +05:30
|
|
|
} else if (result.usesInsecureDigest) {
|
|
|
|
|
statusColor = 'text-red-400';
|
|
|
|
|
statusIcon = 'x-circle';
|
|
|
|
|
statusText = 'Insecure Digest (MD5 / SHA-1)';
|
|
|
|
|
} else if (result.isExpired) {
|
|
|
|
|
statusColor = 'text-yellow-400';
|
|
|
|
|
statusIcon = 'alert-triangle';
|
|
|
|
|
statusText = 'Certificate Expired';
|
|
|
|
|
} else if (result.isSelfSigned) {
|
|
|
|
|
statusColor = 'text-yellow-400';
|
|
|
|
|
statusIcon = 'alert-triangle';
|
|
|
|
|
statusText = 'Self-Signed Certificate';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const formatDate = (date: Date) => {
|
|
|
|
|
if (!date || date.getTime() === 0) return 'Unknown';
|
|
|
|
|
return date.toLocaleDateString(undefined, {
|
|
|
|
|
year: 'numeric',
|
|
|
|
|
month: 'short',
|
|
|
|
|
day: 'numeric',
|
|
|
|
|
hour: '2-digit',
|
|
|
|
|
minute: '2-digit',
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let trustBadge = '';
|
|
|
|
|
if (state.trustedCert) {
|
|
|
|
|
if (result.isTrusted) {
|
|
|
|
|
trustBadge =
|
|
|
|
|
'<span class="text-xs bg-green-900 text-green-300 px-2 py-1 rounded ml-2"><i data-lucide="shield-check" class="inline w-3 h-3 mr-1"></i>Trusted</span>';
|
|
|
|
|
} else {
|
|
|
|
|
trustBadge =
|
|
|
|
|
'<span class="text-xs bg-gray-600 text-gray-300 px-2 py-1 rounded ml-2"><i data-lucide="shield-x" class="inline w-3 h-3 mr-1"></i>Not in trust chain</span>';
|
2026-01-04 19:08:50 +05:30
|
|
|
}
|
2026-04-17 23:40:24 +05:30
|
|
|
}
|
2026-01-04 19:08:50 +05:30
|
|
|
|
2026-04-17 23:40:24 +05:30
|
|
|
card.innerHTML = `
|
2026-01-04 19:08:50 +05:30
|
|
|
<div class="flex items-start justify-between mb-4">
|
|
|
|
|
<div class="flex items-center gap-3">
|
|
|
|
|
<i data-lucide="${statusIcon}" class="w-6 h-6 ${statusColor}"></i>
|
|
|
|
|
<div>
|
|
|
|
|
<h3 class="font-semibold text-white">Signature ${index + 1}</h3>
|
|
|
|
|
<p class="text-sm ${statusColor}">${statusText}</p>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="flex items-center">
|
2026-04-17 23:40:24 +05:30
|
|
|
${
|
|
|
|
|
result.coverageStatus === 'full'
|
|
|
|
|
? '<span class="text-xs bg-green-900 text-green-300 px-2 py-1 rounded">Full Coverage</span>'
|
|
|
|
|
: result.coverageStatus === 'partial'
|
|
|
|
|
? '<span class="text-xs bg-yellow-900 text-yellow-300 px-2 py-1 rounded">Partial Coverage</span>'
|
|
|
|
|
: ''
|
|
|
|
|
}${trustBadge}
|
2026-01-04 19:08:50 +05:30
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="space-y-3 text-sm">
|
|
|
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
|
|
|
<div>
|
|
|
|
|
<p class="text-gray-400">Signed By</p>
|
|
|
|
|
<p class="text-white font-medium">${escapeHtml(result.signerName)}</p>
|
|
|
|
|
${result.signerOrg ? `<p class="text-gray-400 text-xs">${escapeHtml(result.signerOrg)}</p>` : ''}
|
|
|
|
|
${result.signerEmail ? `<p class="text-gray-400 text-xs">${escapeHtml(result.signerEmail)}</p>` : ''}
|
|
|
|
|
</div>
|
|
|
|
|
<div>
|
|
|
|
|
<p class="text-gray-400">Issuer</p>
|
|
|
|
|
<p class="text-white font-medium">${escapeHtml(result.issuer)}</p>
|
|
|
|
|
${result.issuerOrg ? `<p class="text-gray-400 text-xs">${escapeHtml(result.issuerOrg)}</p>` : ''}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
2026-04-17 23:40:24 +05:30
|
|
|
${
|
|
|
|
|
result.signatureDate
|
|
|
|
|
? `
|
2026-01-04 19:08:50 +05:30
|
|
|
<div>
|
|
|
|
|
<p class="text-gray-400">Signed On</p>
|
|
|
|
|
<p class="text-white">${formatDate(result.signatureDate)}</p>
|
|
|
|
|
</div>
|
2026-04-17 23:40:24 +05:30
|
|
|
`
|
|
|
|
|
: ''
|
|
|
|
|
}
|
2026-01-04 19:08:50 +05:30
|
|
|
|
|
|
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
|
|
|
<div>
|
|
|
|
|
<p class="text-gray-400">Valid From</p>
|
|
|
|
|
<p class="text-white">${formatDate(result.validFrom)}</p>
|
|
|
|
|
</div>
|
|
|
|
|
<div>
|
|
|
|
|
<p class="text-gray-400">Valid Until</p>
|
|
|
|
|
<p class="${result.isExpired ? 'text-red-400' : 'text-white'}">${formatDate(result.validTo)}</p>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
2026-04-17 23:40:24 +05:30
|
|
|
${
|
|
|
|
|
result.reason
|
|
|
|
|
? `
|
2026-01-04 19:08:50 +05:30
|
|
|
<div>
|
|
|
|
|
<p class="text-gray-400">Reason</p>
|
|
|
|
|
<p class="text-white">${escapeHtml(result.reason)}</p>
|
|
|
|
|
</div>
|
2026-04-17 23:40:24 +05:30
|
|
|
`
|
|
|
|
|
: ''
|
|
|
|
|
}
|
2026-01-04 19:08:50 +05:30
|
|
|
|
2026-04-17 23:40:24 +05:30
|
|
|
${
|
|
|
|
|
result.location
|
|
|
|
|
? `
|
2026-01-04 19:08:50 +05:30
|
|
|
<div>
|
|
|
|
|
<p class="text-gray-400">Location</p>
|
|
|
|
|
<p class="text-white">${escapeHtml(result.location)}</p>
|
|
|
|
|
</div>
|
2026-04-17 23:40:24 +05:30
|
|
|
`
|
|
|
|
|
: ''
|
|
|
|
|
}
|
2026-01-04 19:08:50 +05:30
|
|
|
|
|
|
|
|
<details class="mt-2">
|
|
|
|
|
<summary class="cursor-pointer text-indigo-400 hover:text-indigo-300 text-sm">
|
|
|
|
|
Technical Details
|
|
|
|
|
</summary>
|
|
|
|
|
<div class="mt-2 p-3 bg-gray-800 rounded text-xs space-y-1">
|
|
|
|
|
<p><span class="text-gray-400">Serial Number:</span> <span class="text-gray-300 font-mono">${escapeHtml(result.serialNumber)}</span></p>
|
|
|
|
|
<p><span class="text-gray-400">Digest Algorithm:</span> <span class="text-gray-300">${escapeHtml(result.algorithms.digest)}</span></p>
|
|
|
|
|
<p><span class="text-gray-400">Signature Algorithm:</span> <span class="text-gray-300">${escapeHtml(result.algorithms.signature)}</span></p>
|
|
|
|
|
${result.errorMessage ? `<p class="text-red-400">Error: ${escapeHtml(result.errorMessage)}</p>` : ''}
|
2026-04-17 23:40:24 +05:30
|
|
|
${result.unsupportedAlgorithmReason ? `<p class="text-yellow-300">${escapeHtml(result.unsupportedAlgorithmReason)}</p>` : ''}
|
2026-01-04 19:08:50 +05:30
|
|
|
</div>
|
|
|
|
|
</details>
|
|
|
|
|
</div>
|
|
|
|
|
`;
|
|
|
|
|
|
2026-04-17 23:40:24 +05:30
|
|
|
return card;
|
2026-01-04 19:08:50 +05:30
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function escapeHtml(str: string): string {
|
2026-04-17 23:40:24 +05:30
|
|
|
const div = document.createElement('div');
|
|
|
|
|
div.textContent = str;
|
|
|
|
|
return div.innerHTML;
|
2026-01-04 19:08:50 +05:30
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (document.readyState === 'loading') {
|
2026-04-17 23:40:24 +05:30
|
|
|
document.addEventListener('DOMContentLoaded', initializePage);
|
2026-01-04 19:08:50 +05:30
|
|
|
} else {
|
2026-04-17 23:40:24 +05:30
|
|
|
initializePage();
|
2026-01-04 19:08:50 +05:30
|
|
|
}
|