@file-viewer/renderer-signature 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +160 -0
- package/README.en.md +87 -0
- package/README.md +89 -0
- package/THIRD_PARTY_LICENSES.json +3867 -0
- package/THIRD_PARTY_NOTICES.md +10 -0
- package/dist/container.worker.d.ts +1 -0
- package/dist/container.worker.js +39 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +53 -0
- package/dist/inspect.d.ts +7 -0
- package/dist/inspect.js +4 -0
- package/dist/openpgp/client.d.ts +16 -0
- package/dist/openpgp/client.js +148 -0
- package/dist/openpgp/formatDetection.d.ts +3 -0
- package/dist/openpgp/formatDetection.js +45 -0
- package/dist/openpgp/types.d.ts +72 -0
- package/dist/openpgp/types.js +1 -0
- package/dist/rpgp-wasm/rpgp_wrapper.d.ts +43 -0
- package/dist/rpgp-wasm/rpgp_wrapper.js +578 -0
- package/dist/rpgp-wasm/rpgp_wrapper_bg.wasm +0 -0
- package/dist/rpgp-wasm/rpgp_wrapper_bg.wasm.d.ts +10 -0
- package/dist/signature.d.ts +24 -0
- package/dist/signature.js +1040 -0
- package/dist/signature.worker.d.ts +1 -0
- package/dist/signature.worker.js +108 -0
- package/dist/signatureAsn1.d.ts +150 -0
- package/dist/signatureAsn1.js +1486 -0
- package/dist/structured/asic.d.ts +3 -0
- package/dist/structured/asic.js +188 -0
- package/dist/structured/containerClient.d.ts +12 -0
- package/dist/structured/containerClient.js +71 -0
- package/dist/structured/containerProtocol.d.ts +20 -0
- package/dist/structured/containerProtocol.js +1 -0
- package/dist/structured/jws.d.ts +3 -0
- package/dist/structured/jws.js +382 -0
- package/dist/structured/limits.d.ts +20 -0
- package/dist/structured/limits.js +41 -0
- package/dist/structured/types.d.ts +53 -0
- package/dist/structured/types.js +1 -0
- package/dist/structured/zipPreflight.d.ts +14 -0
- package/dist/structured/zipPreflight.js +173 -0
- package/dist/workerProtocol.d.ts +40 -0
- package/dist/workerProtocol.js +1 -0
- package/file-viewer.capability.json +56 -0
- package/package.json +106 -0
|
@@ -0,0 +1,1040 @@
|
|
|
1
|
+
import { disposeFileViewerRendered, normalizeFileViewerUiDensity } from '@file-viewer/core';
|
|
2
|
+
import { inspectEvidenceRecord, inspectSignatureContainer } from './signatureAsn1.js';
|
|
3
|
+
import { isProbablyOpenPgp } from './openpgp/formatDetection.js';
|
|
4
|
+
import { SignatureContainerWorkerClient } from './structured/containerClient.js';
|
|
5
|
+
import { normalizeSignatureContainerLimits } from './structured/limits.js';
|
|
6
|
+
import { inspectJws, isProbablyJws } from './structured/jws.js';
|
|
7
|
+
const DEFAULT_MAX_CONTAINER_SIZE = 64 * 1024 * 1024;
|
|
8
|
+
const DEFAULT_MAX_NESTED_PREVIEW_SIZE = 32 * 1024 * 1024;
|
|
9
|
+
const signatureStyle = `
|
|
10
|
+
.signature-shell{box-sizing:border-box;width:100%;height:100%;min-width:0;min-height:0;overflow:auto;background:#edf2f7;color:#172033;font-family:Aptos,'Segoe UI','PingFang SC','Microsoft YaHei',sans-serif}
|
|
11
|
+
.signature-shell *{box-sizing:border-box}
|
|
12
|
+
.signature-header{position:sticky;top:0;z-index:3;display:flex;align-items:flex-start;justify-content:space-between;gap:16px;padding:16px 18px;border-bottom:1px solid rgba(23,32,51,.09);background:rgba(255,255,255,.94);backdrop-filter:blur(12px)}
|
|
13
|
+
.signature-title{min-width:0}.signature-title small{display:block;color:#24765a;font-size:11px;font-weight:900;letter-spacing:.08em}.signature-title h2{margin:3px 0 0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:19px}.signature-title p{margin:5px 0 0;color:#64748b;font-size:12px}.signature-phase{flex:0 0 auto;padding:7px 10px;border-radius:999px;background:#e7f8ef;color:#166534;font-size:11px;font-weight:900}
|
|
14
|
+
.signature-content{display:grid;grid-template-columns:minmax(260px,340px) minmax(0,1fr);gap:14px;padding:14px}
|
|
15
|
+
.signature-sidebar,.signature-main{min-width:0;display:flex;flex-direction:column;gap:12px}
|
|
16
|
+
.signature-card{min-width:0;padding:14px;border:1px solid rgba(23,32,51,.08);border-radius:14px;background:#fff;box-shadow:0 8px 24px rgba(23,32,51,.05)}
|
|
17
|
+
.signature-card h3{margin:0 0 10px;font-size:14px}.signature-card h4{margin:13px 0 7px;font-size:12px}.signature-card p{margin:6px 0;color:#526174;font-size:12px;line-height:1.55}.signature-card code{font-family:'SFMono-Regular',Consolas,monospace;font-size:11px;overflow-wrap:anywhere}
|
|
18
|
+
.signature-grid{display:grid;grid-template-columns:minmax(110px,auto) minmax(0,1fr);gap:7px 10px;margin:0}.signature-grid dt{color:#708095;font-size:11px;font-weight:800}.signature-grid dd{min-width:0;margin:0;overflow-wrap:anywhere;font-size:12px}
|
|
19
|
+
.signature-warning{padding:10px 11px;border-radius:10px;background:#fff7e8;color:#854d0e;font-size:11px;line-height:1.5}.signature-warning+.signature-warning{margin-top:7px}
|
|
20
|
+
.signature-status-list{display:flex;flex-wrap:wrap;gap:6px}.signature-status{display:inline-flex;align-items:center;gap:5px;padding:5px 8px;border-radius:999px;background:#eef2f7;color:#475569;font-size:10px;font-weight:900}.signature-status[data-state='valid']{background:#dcfce7;color:#166534}.signature-status[data-state='invalid']{background:#fee2e2;color:#991b1b}.signature-status[data-state='pending']{background:#fef3c7;color:#92400e}
|
|
21
|
+
.signature-item{padding:11px;border:1px solid rgba(23,32,51,.08);border-radius:11px;background:#f8fafc}.signature-item+.signature-item{margin-top:9px}.signature-item-head{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:8px}.signature-item-head strong{font-size:12px}.signature-item-head span{color:#64748b;font-size:10px}
|
|
22
|
+
.signature-original{display:flex;flex-direction:column;gap:9px}.signature-file{width:100%;font-size:12px}.signature-file::file-selector-button{margin-right:9px;padding:7px 10px;border:0;border-radius:8px;background:#1f7a58;color:#fff;font:inherit;font-weight:800;cursor:pointer}.signature-original-state{min-height:18px;color:#64748b;font-size:11px}
|
|
23
|
+
.signature-preview{min-height:360px;overflow:hidden;padding:0}.signature-preview-head{display:flex;align-items:center;justify-content:space-between;gap:10px;padding:11px 14px;border-bottom:1px solid rgba(23,32,51,.08)}.signature-preview-head strong{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:13px}.signature-preview-target{min-height:340px;height:min(65vh,720px);overflow:auto;background:#f8fafc}.signature-preview-empty{height:100%;min-height:340px;display:flex;align-items:center;justify-content:center;padding:28px;color:#64748b;text-align:center;font-size:12px;line-height:1.6}
|
|
24
|
+
.signature-openpgp-note{padding:10px 11px;border-radius:10px;background:#eef6ff;color:#1e3a5f;font-size:11px;line-height:1.5}.signature-key-input{display:flex;flex-direction:column;gap:8px}
|
|
25
|
+
.signature-error{padding:14px;border:1px solid #fecaca;border-radius:12px;background:#fff1f2;color:#991b1b;font-size:12px;line-height:1.55}
|
|
26
|
+
.signature-loading{display:flex;align-items:center;gap:10px;padding:15px;color:#526174;font-size:12px}.signature-spinner{width:20px;height:20px;border:2px solid rgba(31,122,88,.18);border-top-color:#1f7a58;border-radius:999px;animation:signature-spin .8s linear infinite}
|
|
27
|
+
.signature-shell[data-viewer-density='compact'] .signature-header{padding:9px 11px}.signature-shell[data-viewer-density='compact'] .signature-content{gap:8px;padding:8px}.signature-shell[data-viewer-density='compact'] .signature-card{padding:10px;border-radius:10px}
|
|
28
|
+
[data-viewer-theme='dark'] .signature-shell{background:#111827;color:#e5edf7}[data-viewer-theme='dark'] .signature-header,[data-viewer-theme='dark'] .signature-card{background:#182233;border-color:rgba(255,255,255,.08)}[data-viewer-theme='dark'] .signature-item,[data-viewer-theme='dark'] .signature-preview-target{background:#101827;border-color:rgba(255,255,255,.08)}[data-viewer-theme='dark'] .signature-title p,[data-viewer-theme='dark'] .signature-card p,[data-viewer-theme='dark'] .signature-grid dt,[data-viewer-theme='dark'] .signature-item-head span,[data-viewer-theme='dark'] .signature-original-state,[data-viewer-theme='dark'] .signature-preview-empty{color:#9eabc0}
|
|
29
|
+
@media(max-width:860px){.signature-content{grid-template-columns:1fr}.signature-header{position:relative}.signature-preview-target{height:520px}}
|
|
30
|
+
@keyframes signature-spin{to{transform:rotate(360deg)}}
|
|
31
|
+
`;
|
|
32
|
+
const createElement = (documentRef, tag, className, text) => {
|
|
33
|
+
const element = documentRef.createElement(tag);
|
|
34
|
+
if (className)
|
|
35
|
+
element.className = className;
|
|
36
|
+
if (text !== undefined)
|
|
37
|
+
element.textContent = text;
|
|
38
|
+
return element;
|
|
39
|
+
};
|
|
40
|
+
const formatBytes = (value) => {
|
|
41
|
+
if (value < 1024)
|
|
42
|
+
return `${value} B`;
|
|
43
|
+
if (value < 1024 * 1024)
|
|
44
|
+
return `${(value / 1024).toFixed(1)} KiB`;
|
|
45
|
+
return `${(value / (1024 * 1024)).toFixed(1)} MiB`;
|
|
46
|
+
};
|
|
47
|
+
const valueOrDash = (value) => value === undefined || value === null || value === '' ? '—' : String(value);
|
|
48
|
+
const appendDefinitionList = (documentRef, parent, rows) => {
|
|
49
|
+
const list = createElement(documentRef, 'dl', 'signature-grid');
|
|
50
|
+
rows.forEach(([label, value]) => {
|
|
51
|
+
list.append(createElement(documentRef, 'dt', undefined, label), createElement(documentRef, 'dd', undefined, valueOrDash(value)));
|
|
52
|
+
});
|
|
53
|
+
parent.append(list);
|
|
54
|
+
};
|
|
55
|
+
const statusBadge = (documentRef, label, value, pendingLabel) => {
|
|
56
|
+
const state = value === true ? 'valid' : value === false ? 'invalid' : 'pending';
|
|
57
|
+
const text = value === true
|
|
58
|
+
? `${label}: valid`
|
|
59
|
+
: value === false
|
|
60
|
+
? `${label}: invalid`
|
|
61
|
+
: `${label}: ${pendingLabel}`;
|
|
62
|
+
const element = createElement(documentRef, 'span', 'signature-status', text);
|
|
63
|
+
element.dataset.state = state;
|
|
64
|
+
return element;
|
|
65
|
+
};
|
|
66
|
+
const renderSigner = (documentRef, signer) => {
|
|
67
|
+
const item = createElement(documentRef, 'article', 'signature-item');
|
|
68
|
+
const head = createElement(documentRef, 'div', 'signature-item-head');
|
|
69
|
+
head.append(createElement(documentRef, 'strong', undefined, `Signer ${signer.index + 1}`), createElement(documentRef, 'span', undefined, signer.certificateIndex === undefined
|
|
70
|
+
? 'certificate not matched'
|
|
71
|
+
: `certificate ${signer.certificateIndex + 1}`));
|
|
72
|
+
const statuses = createElement(documentRef, 'div', 'signature-status-list');
|
|
73
|
+
statuses.append(statusBadge(documentRef, 'Signature', signer.cryptographicSignatureValid, 'not verified'), statusBadge(documentRef, 'Content digest', signer.digestMatches, 'original required'));
|
|
74
|
+
item.append(head, statuses);
|
|
75
|
+
appendDefinitionList(documentRef, item, [
|
|
76
|
+
['Signer identifier', signer.sid],
|
|
77
|
+
['Digest algorithm', signer.digestAlgorithm],
|
|
78
|
+
['Signature algorithm', signer.signatureAlgorithm],
|
|
79
|
+
['Signing time', signer.signingTime],
|
|
80
|
+
['Signed content type', signer.contentTypeOid],
|
|
81
|
+
['Signed message digest', signer.messageDigest],
|
|
82
|
+
['CAdES signingCertificateV2', signer.signingCertificateV2 ? 'Present' : 'Not detected'],
|
|
83
|
+
['Signature timestamp tokens', signer.signatureTimestampTokens],
|
|
84
|
+
['Verification note', signer.verificationError]
|
|
85
|
+
]);
|
|
86
|
+
return item;
|
|
87
|
+
};
|
|
88
|
+
const renderCertificate = (documentRef, certificate) => {
|
|
89
|
+
const item = createElement(documentRef, 'article', 'signature-item');
|
|
90
|
+
const head = createElement(documentRef, 'div', 'signature-item-head');
|
|
91
|
+
head.append(createElement(documentRef, 'strong', undefined, `Certificate ${certificate.index + 1}`), createElement(documentRef, 'span', undefined, `X.509 v${certificate.version || '?'}`));
|
|
92
|
+
item.append(head);
|
|
93
|
+
appendDefinitionList(documentRef, item, [
|
|
94
|
+
['Subject', certificate.subject],
|
|
95
|
+
['Issuer', certificate.issuer],
|
|
96
|
+
['Serial number', certificate.serialNumber],
|
|
97
|
+
['Valid from', certificate.notBefore],
|
|
98
|
+
['Valid until', certificate.notAfter],
|
|
99
|
+
['Public key', certificate.publicKeyAlgorithm],
|
|
100
|
+
['Public key curve', certificate.publicKeyCurveOid],
|
|
101
|
+
['Certificate signature', certificate.signatureAlgorithm],
|
|
102
|
+
['Subject key identifier', certificate.subjectKeyIdentifier],
|
|
103
|
+
['SHA-256 fingerprint', certificate.fingerprintSha256]
|
|
104
|
+
]);
|
|
105
|
+
return item;
|
|
106
|
+
};
|
|
107
|
+
const renderTimestamp = (documentRef, timestamp) => {
|
|
108
|
+
const card = createElement(documentRef, 'section', 'signature-card');
|
|
109
|
+
card.append(createElement(documentRef, 'h3', undefined, 'RFC 3161 timestamp'));
|
|
110
|
+
const statuses = createElement(documentRef, 'div', 'signature-status-list');
|
|
111
|
+
statuses.append(statusBadge(documentRef, 'Message imprint', timestamp.messageImprintMatchesOriginal, 'original required'));
|
|
112
|
+
card.append(statuses);
|
|
113
|
+
appendDefinitionList(documentRef, card, [
|
|
114
|
+
['Version', timestamp.version],
|
|
115
|
+
['Policy OID', timestamp.policyOid],
|
|
116
|
+
['Serial number', timestamp.serialNumber],
|
|
117
|
+
['Generation time', timestamp.generationTime],
|
|
118
|
+
['Message imprint algorithm', timestamp.messageImprintAlgorithm],
|
|
119
|
+
['Message imprint', timestamp.messageImprint],
|
|
120
|
+
['Nonce', timestamp.nonce],
|
|
121
|
+
[
|
|
122
|
+
'TSA certificate requested',
|
|
123
|
+
timestamp.certReq === undefined ? undefined : timestamp.certReq ? 'Yes' : 'No'
|
|
124
|
+
],
|
|
125
|
+
['Ordering', timestamp.ordering],
|
|
126
|
+
[
|
|
127
|
+
'Accuracy',
|
|
128
|
+
timestamp.accuracy
|
|
129
|
+
? [
|
|
130
|
+
timestamp.accuracy.seconds === undefined ? '' : `${timestamp.accuracy.seconds}s`,
|
|
131
|
+
timestamp.accuracy.millis === undefined ? '' : `${timestamp.accuracy.millis}ms`,
|
|
132
|
+
timestamp.accuracy.micros === undefined ? '' : `${timestamp.accuracy.micros}µs`
|
|
133
|
+
]
|
|
134
|
+
.filter(Boolean)
|
|
135
|
+
.join(' ')
|
|
136
|
+
: undefined
|
|
137
|
+
],
|
|
138
|
+
['Timestamp authority', timestamp.tsa]
|
|
139
|
+
]);
|
|
140
|
+
return card;
|
|
141
|
+
};
|
|
142
|
+
const getFilenameExtension = (filename) => {
|
|
143
|
+
const basename = filename.split(/[\\/]/).pop() || filename;
|
|
144
|
+
const dot = basename.lastIndexOf('.');
|
|
145
|
+
return dot > 0 && dot < basename.length - 1 ? basename.slice(dot + 1).toLowerCase() : '';
|
|
146
|
+
};
|
|
147
|
+
const stripContainerExtension = (filename) => filename.replace(/\.(?:p7m|p7s|p7b|p7c|pkcs7|cms|cmsc|tst|tsq|tsr|tsd)$/i, '');
|
|
148
|
+
const sniffNestedExtension = (bytes) => {
|
|
149
|
+
const head = bytes.subarray(0, Math.min(bytes.byteLength, 512));
|
|
150
|
+
const ascii = String.fromCharCode(...head);
|
|
151
|
+
if (ascii.startsWith('%PDF-'))
|
|
152
|
+
return 'pdf';
|
|
153
|
+
if (head[0] === 0x89 && ascii.slice(1, 4) === 'PNG')
|
|
154
|
+
return 'png';
|
|
155
|
+
if (head[0] === 0xff && head[1] === 0xd8)
|
|
156
|
+
return 'jpg';
|
|
157
|
+
if (ascii.startsWith('GIF87a') || ascii.startsWith('GIF89a'))
|
|
158
|
+
return 'gif';
|
|
159
|
+
if (ascii.startsWith('PK\u0003\u0004'))
|
|
160
|
+
return 'zip';
|
|
161
|
+
const text = new TextDecoder('utf-8', { fatal: false }).decode(head).trimStart();
|
|
162
|
+
if (text.startsWith('<?xml') || text.startsWith('<'))
|
|
163
|
+
return 'xml';
|
|
164
|
+
if (text.startsWith('{') || text.startsWith('['))
|
|
165
|
+
return 'json';
|
|
166
|
+
const printable = head.length === 0 ||
|
|
167
|
+
Array.from(head).filter((value) => value === 9 || value === 10 || value === 13 || (value >= 32 && value < 127)).length /
|
|
168
|
+
head.length >
|
|
169
|
+
0.92;
|
|
170
|
+
return printable ? 'txt' : 'bin';
|
|
171
|
+
};
|
|
172
|
+
const resolveNestedIdentity = (inspection, sourceFilename, originalFilename, preferOriginal = false) => {
|
|
173
|
+
if (preferOriginal && originalFilename) {
|
|
174
|
+
return {
|
|
175
|
+
filename: originalFilename,
|
|
176
|
+
extension: getFilenameExtension(originalFilename) || 'bin'
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
const stripped = stripContainerExtension(sourceFilename);
|
|
180
|
+
const extension = getFilenameExtension(stripped) ||
|
|
181
|
+
(inspection.embeddedContent ? sniffNestedExtension(inspection.embeddedContent) : 'bin');
|
|
182
|
+
const filename = getFilenameExtension(stripped) ? stripped : `signed-content.${extension}`;
|
|
183
|
+
return { filename, extension };
|
|
184
|
+
};
|
|
185
|
+
const readBoundedBlob = async (blob, limit, label) => {
|
|
186
|
+
if (blob.size > limit)
|
|
187
|
+
throw new Error(`${label} exceeds the ${formatBytes(limit)} limit.`);
|
|
188
|
+
const result = await blob.arrayBuffer();
|
|
189
|
+
if (result.byteLength > limit)
|
|
190
|
+
throw new Error(`${label} exceeds the ${formatBytes(limit)} limit.`);
|
|
191
|
+
return result;
|
|
192
|
+
};
|
|
193
|
+
const readConfiguredOriginal = async (options, limit) => {
|
|
194
|
+
if (!(options === null || options === void 0 ? void 0 : options.originalContent)) {
|
|
195
|
+
return undefined;
|
|
196
|
+
}
|
|
197
|
+
if (options.originalContent instanceof ArrayBuffer) {
|
|
198
|
+
if (options.originalContent.byteLength > limit)
|
|
199
|
+
throw new Error(`Original content exceeds the ${formatBytes(limit)} limit.`);
|
|
200
|
+
return options.originalContent;
|
|
201
|
+
}
|
|
202
|
+
return readBoundedBlob(options.originalContent, limit, 'Original content');
|
|
203
|
+
};
|
|
204
|
+
const readConfiguredOpenPgpKeys = async (options, limit) => {
|
|
205
|
+
const keys = (options === null || options === void 0 ? void 0 : options.openPgpPublicKeys) || [];
|
|
206
|
+
if (keys.length > 64)
|
|
207
|
+
throw new Error('At most 64 OpenPGP verification keys may be supplied.');
|
|
208
|
+
const result = await Promise.all(keys.map((key) => {
|
|
209
|
+
if (key instanceof ArrayBuffer) {
|
|
210
|
+
if (key.byteLength > limit)
|
|
211
|
+
throw new Error(`OpenPGP verification key exceeds the ${formatBytes(limit)} limit.`);
|
|
212
|
+
return key;
|
|
213
|
+
}
|
|
214
|
+
return readBoundedBlob(key, limit, 'OpenPGP verification key');
|
|
215
|
+
}));
|
|
216
|
+
const aggregate = result.reduce((sum, value) => sum + value.byteLength, 0);
|
|
217
|
+
if (aggregate > limit)
|
|
218
|
+
throw new Error(`OpenPGP verification keys exceed the aggregate ${formatBytes(limit)} limit.`);
|
|
219
|
+
return result;
|
|
220
|
+
};
|
|
221
|
+
const renderOpenPgpKey = (documentRef, key) => {
|
|
222
|
+
const item = createElement(documentRef, 'article', 'signature-item');
|
|
223
|
+
const head = createElement(documentRef, 'div', 'signature-item-head');
|
|
224
|
+
head.append(createElement(documentRef, 'strong', undefined, `${key.kind === 'private' ? 'Private-key block' : 'Public key'}`), createElement(documentRef, 'span', undefined, key.keyId || 'key id unavailable'));
|
|
225
|
+
item.append(head);
|
|
226
|
+
appendDefinitionList(documentRef, item, [
|
|
227
|
+
['Fingerprint', key.fingerprint],
|
|
228
|
+
['Version', key.version],
|
|
229
|
+
['Algorithm', key.algorithm],
|
|
230
|
+
['Created', key.createdAt],
|
|
231
|
+
['User IDs', key.userIds.join('; ')],
|
|
232
|
+
['Subkeys', key.subkeys.length]
|
|
233
|
+
]);
|
|
234
|
+
if (key.kind === 'private') {
|
|
235
|
+
item.append(createElement(documentRef, 'div', 'signature-warning', 'Only public metadata is exposed. Secret MPIs, private scalars and passphrases never cross the WASM boundary.'));
|
|
236
|
+
}
|
|
237
|
+
return item;
|
|
238
|
+
};
|
|
239
|
+
const renderOpenPgpSignature = (documentRef, signature, index) => {
|
|
240
|
+
const item = createElement(documentRef, 'article', 'signature-item');
|
|
241
|
+
const head = createElement(documentRef, 'div', 'signature-item-head');
|
|
242
|
+
head.append(createElement(documentRef, 'strong', undefined, `Signature ${index + 1}`), createElement(documentRef, 'span', undefined, signature.signatureType || 'OpenPGP'));
|
|
243
|
+
const statuses = createElement(documentRef, 'div', 'signature-status-list');
|
|
244
|
+
statuses.append(statusBadge(documentRef, 'Cryptographic signature', signature.cryptographicValid, 'public key or original required'));
|
|
245
|
+
item.append(head, statuses);
|
|
246
|
+
appendDefinitionList(documentRef, item, [
|
|
247
|
+
['Hash algorithm', signature.hashAlgorithm],
|
|
248
|
+
['Public-key algorithm', signature.publicKeyAlgorithm],
|
|
249
|
+
['Created', signature.createdAt],
|
|
250
|
+
['Expires', signature.expiresAt],
|
|
251
|
+
['Issuer key IDs', signature.issuerKeyIds.join(', ')],
|
|
252
|
+
['Issuer fingerprints', signature.issuerFingerprints.join(', ')],
|
|
253
|
+
['Verification key fingerprint', signature.verificationKeyFingerprint],
|
|
254
|
+
['Verification key ID', signature.verificationKeyId],
|
|
255
|
+
['Verification note', signature.verificationError]
|
|
256
|
+
]);
|
|
257
|
+
return item;
|
|
258
|
+
};
|
|
259
|
+
export default async function renderSignature(buffer, target, type, context) {
|
|
260
|
+
var _a, _b, _c;
|
|
261
|
+
const documentRef = target.ownerDocument;
|
|
262
|
+
const options = (_a = context === null || context === void 0 ? void 0 : context.options) === null || _a === void 0 ? void 0 : _a.signature;
|
|
263
|
+
const filename = (context === null || context === void 0 ? void 0 : context.filename) || `signature.${type || 'p7m'}`;
|
|
264
|
+
const requestedMaxContainer = typeof (options === null || options === void 0 ? void 0 : options.maxContainerSize) === 'number' &&
|
|
265
|
+
Number.isSafeInteger(options.maxContainerSize) &&
|
|
266
|
+
options.maxContainerSize > 0
|
|
267
|
+
? options.maxContainerSize
|
|
268
|
+
: DEFAULT_MAX_CONTAINER_SIZE;
|
|
269
|
+
const containerLimits = normalizeSignatureContainerLimits({
|
|
270
|
+
...options === null || options === void 0 ? void 0 : options.containerLimits,
|
|
271
|
+
maxContainerBytes: requestedMaxContainer
|
|
272
|
+
});
|
|
273
|
+
const maxContainerSize = containerLimits.maxContainerBytes;
|
|
274
|
+
const requestedNestedPreviewSize = typeof (options === null || options === void 0 ? void 0 : options.maxNestedPreviewSize) === 'number' &&
|
|
275
|
+
Number.isSafeInteger(options.maxNestedPreviewSize) &&
|
|
276
|
+
options.maxNestedPreviewSize > 0
|
|
277
|
+
? options.maxNestedPreviewSize
|
|
278
|
+
: DEFAULT_MAX_NESTED_PREVIEW_SIZE;
|
|
279
|
+
const maxNestedPreviewSize = Math.min(requestedNestedPreviewSize, containerLimits.maxEntryBytes);
|
|
280
|
+
if (buffer.byteLength > maxContainerSize) {
|
|
281
|
+
throw new Error(`The cryptographic container (${formatBytes(buffer.byteLength)}) exceeds the ${formatBytes(maxContainerSize)} parsing limit.`);
|
|
282
|
+
}
|
|
283
|
+
const style = createElement(documentRef, 'style');
|
|
284
|
+
style.textContent = signatureStyle;
|
|
285
|
+
const root = createElement(documentRef, 'section', 'signature-shell');
|
|
286
|
+
root.dataset.viewerDensity = normalizeFileViewerUiDensity((_c = (_b = context === null || context === void 0 ? void 0 : context.options) === null || _b === void 0 ? void 0 : _b.ui) === null || _c === void 0 ? void 0 : _c.density);
|
|
287
|
+
target.replaceChildren(style, root);
|
|
288
|
+
let originalContent = await readConfiguredOriginal(options, maxNestedPreviewSize);
|
|
289
|
+
let originalFilename = options === null || options === void 0 ? void 0 : options.originalFilename;
|
|
290
|
+
let openPgpPublicKeys = await readConfiguredOpenPgpKeys(options, Math.min(maxNestedPreviewSize, 8 * 1024 * 1024));
|
|
291
|
+
let jwsVerificationKeys = [...((options === null || options === void 0 ? void 0 : options.jwsVerificationKeys) || [])];
|
|
292
|
+
let openPgpClient;
|
|
293
|
+
let containerClient;
|
|
294
|
+
let nestedRendered;
|
|
295
|
+
let disposed = false;
|
|
296
|
+
let renderSequence = 0;
|
|
297
|
+
const clearNested = async () => {
|
|
298
|
+
await disposeFileViewerRendered(nestedRendered);
|
|
299
|
+
nestedRendered = undefined;
|
|
300
|
+
};
|
|
301
|
+
const renderInspection = async () => {
|
|
302
|
+
var _a, _b;
|
|
303
|
+
const sequence = ++renderSequence;
|
|
304
|
+
await clearNested();
|
|
305
|
+
root.replaceChildren(createElement(documentRef, 'div', 'signature-loading'));
|
|
306
|
+
const loading = root.firstElementChild;
|
|
307
|
+
loading.append(createElement(documentRef, 'span', 'signature-spinner'), createElement(documentRef, 'span', undefined, 'Inspecting signature and timestamp structures locally…'));
|
|
308
|
+
try {
|
|
309
|
+
const sourceBytes = new Uint8Array(buffer);
|
|
310
|
+
const extension = getFilenameExtension(filename) || (type || '').toLowerCase();
|
|
311
|
+
if (['asics', 'scs', 'asice', 'sce'].includes(extension)) {
|
|
312
|
+
containerClient || (containerClient = new SignatureContainerWorkerClient((options === null || options === void 0 ? void 0 : options.workerFactory) ? () => options.workerFactory('container') : undefined));
|
|
313
|
+
const inspection = await containerClient.inspectAsic(buffer, containerLimits);
|
|
314
|
+
if (disposed || sequence !== renderSequence)
|
|
315
|
+
return;
|
|
316
|
+
const signatureResults = [];
|
|
317
|
+
for (const member of inspection.signatures) {
|
|
318
|
+
let result = 'Parsed metadata only';
|
|
319
|
+
let document;
|
|
320
|
+
try {
|
|
321
|
+
if (member.kind === 'cades' && member.data) {
|
|
322
|
+
const candidates = member.referencedDocuments.length
|
|
323
|
+
? inspection.documents.filter((candidate) => member.referencedDocuments.includes(candidate.name))
|
|
324
|
+
: inspection.documents.length === 1
|
|
325
|
+
? inspection.documents
|
|
326
|
+
: [];
|
|
327
|
+
let cms = await inspectSignatureContainer(member.data, {
|
|
328
|
+
sourceFilename: member.name
|
|
329
|
+
});
|
|
330
|
+
if (cms.detached && candidates.length) {
|
|
331
|
+
for (const candidate of candidates.slice(0, 16)) {
|
|
332
|
+
if (!candidate.data)
|
|
333
|
+
continue;
|
|
334
|
+
const checked = await inspectSignatureContainer(member.data, {
|
|
335
|
+
sourceFilename: member.name,
|
|
336
|
+
originalContent: candidate.data
|
|
337
|
+
});
|
|
338
|
+
if (checked.signers.some((signer) => signer.digestMatches === true && signer.cryptographicSignatureValid === true)) {
|
|
339
|
+
cms = checked;
|
|
340
|
+
document = candidate.name;
|
|
341
|
+
break;
|
|
342
|
+
}
|
|
343
|
+
cms = checked;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
const checked = cms.signers.filter((signer) => signer.cryptographicSignatureValid !== undefined);
|
|
347
|
+
result = checked.length
|
|
348
|
+
? checked.every((signer) => signer.cryptographicSignatureValid === true)
|
|
349
|
+
? 'Cryptographic signature valid'
|
|
350
|
+
: 'Cryptographic signature invalid'
|
|
351
|
+
: cms.detached
|
|
352
|
+
? 'Original document mapping required'
|
|
353
|
+
: 'Signature not checked';
|
|
354
|
+
}
|
|
355
|
+
else if (member.kind === 'timestamp' && member.data) {
|
|
356
|
+
const timestamp = await inspectSignatureContainer(member.data, {
|
|
357
|
+
sourceFilename: member.name
|
|
358
|
+
});
|
|
359
|
+
result = timestamp.timestamp ? 'Timestamp structure parsed' : timestamp.detectedFormat;
|
|
360
|
+
}
|
|
361
|
+
else if (member.kind === 'evidence-record' && member.data) {
|
|
362
|
+
const evidence = await inspectEvidenceRecord(member.data);
|
|
363
|
+
result = `${evidence.archiveTimestamps.length} archive timestamp(s) parsed`;
|
|
364
|
+
}
|
|
365
|
+
else if (member.kind === 'jws' && member.data) {
|
|
366
|
+
const jws = await inspectJws(member.data, {
|
|
367
|
+
verificationKeys: jwsVerificationKeys,
|
|
368
|
+
limits: containerLimits
|
|
369
|
+
});
|
|
370
|
+
result = jws.signatures.some((item) => item.cryptographicValid === false)
|
|
371
|
+
? 'JWS signature invalid'
|
|
372
|
+
: jws.signatures.length &&
|
|
373
|
+
jws.signatures.every((item) => item.cryptographicValid === true)
|
|
374
|
+
? 'JWS signatures valid'
|
|
375
|
+
: 'JWS parsed; verification key or payload required';
|
|
376
|
+
}
|
|
377
|
+
else if (member.kind === 'xades-or-xml') {
|
|
378
|
+
result = member.referencedDocuments.length
|
|
379
|
+
? `XML signature metadata references ${member.referencedDocuments.length} document(s)`
|
|
380
|
+
: 'XML signature metadata parsed; canonicalization not verified';
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
catch (error) {
|
|
384
|
+
result = `Inspection failed: ${error instanceof Error ? error.message : String(error)}`;
|
|
385
|
+
}
|
|
386
|
+
signatureResults.push({ name: member.name, result, document });
|
|
387
|
+
}
|
|
388
|
+
root.replaceChildren();
|
|
389
|
+
const header = createElement(documentRef, 'header', 'signature-header');
|
|
390
|
+
const title = createElement(documentRef, 'div', 'signature-title');
|
|
391
|
+
title.append(createElement(documentRef, 'small', undefined, 'ASSOCIATED SIGNATURE CONTAINER'), createElement(documentRef, 'h2', undefined, filename), createElement(documentRef, 'p', undefined, `${inspection.kind} · ${inspection.mediaType}`));
|
|
392
|
+
header.append(title, createElement(documentRef, 'span', 'signature-phase', 'OPT-IN · BOUNDED ZIP WORKER'));
|
|
393
|
+
const content = createElement(documentRef, 'div', 'signature-content');
|
|
394
|
+
const sidebar = createElement(documentRef, 'aside', 'signature-sidebar');
|
|
395
|
+
const main = createElement(documentRef, 'main', 'signature-main');
|
|
396
|
+
const summary = createElement(documentRef, 'section', 'signature-card');
|
|
397
|
+
summary.append(createElement(documentRef, 'h3', undefined, 'ASiC package'));
|
|
398
|
+
appendDefinitionList(documentRef, summary, [
|
|
399
|
+
['Type', inspection.kind],
|
|
400
|
+
['Source size', formatBytes(inspection.sourceSize)],
|
|
401
|
+
['Entries', inspection.entryCount],
|
|
402
|
+
['Inflated total', formatBytes(inspection.totalUncompressedBytes)],
|
|
403
|
+
['Documents', inspection.documents.length],
|
|
404
|
+
['Signature/evidence members', inspection.signatures.length]
|
|
405
|
+
]);
|
|
406
|
+
sidebar.append(summary);
|
|
407
|
+
const boundaries = createElement(documentRef, 'section', 'signature-card');
|
|
408
|
+
boundaries.append(createElement(documentRef, 'h3', undefined, 'Security and validation boundaries'));
|
|
409
|
+
inspection.warnings.forEach((warning) => boundaries.append(createElement(documentRef, 'div', 'signature-warning', warning)));
|
|
410
|
+
sidebar.append(boundaries);
|
|
411
|
+
if (signatureResults.length) {
|
|
412
|
+
const signatureCard = createElement(documentRef, 'section', 'signature-card');
|
|
413
|
+
signatureCard.append(createElement(documentRef, 'h3', undefined, 'Signatures and evidence'));
|
|
414
|
+
signatureResults.forEach((item) => {
|
|
415
|
+
const row = createElement(documentRef, 'article', 'signature-item');
|
|
416
|
+
row.append(createElement(documentRef, 'strong', undefined, item.name));
|
|
417
|
+
appendDefinitionList(documentRef, row, [
|
|
418
|
+
['Result', item.result],
|
|
419
|
+
['Matched document', item.document]
|
|
420
|
+
]);
|
|
421
|
+
signatureCard.append(row);
|
|
422
|
+
});
|
|
423
|
+
main.append(signatureCard);
|
|
424
|
+
}
|
|
425
|
+
const previewCard = createElement(documentRef, 'section', 'signature-card signature-preview');
|
|
426
|
+
const previewHead = createElement(documentRef, 'div', 'signature-preview-head');
|
|
427
|
+
const previewTitle = createElement(documentRef, 'strong', undefined, 'Contained document preview');
|
|
428
|
+
const previewStatus = createElement(documentRef, 'span', 'signature-status');
|
|
429
|
+
const previewTarget = createElement(documentRef, 'div', 'signature-preview-target');
|
|
430
|
+
previewHead.append(previewTitle, previewStatus);
|
|
431
|
+
previewCard.append(previewHead, previewTarget);
|
|
432
|
+
const chooser = createElement(documentRef, 'section', 'signature-card');
|
|
433
|
+
chooser.append(createElement(documentRef, 'h3', undefined, 'Contained documents'));
|
|
434
|
+
const chooserList = createElement(documentRef, 'div', 'signature-status-list');
|
|
435
|
+
chooser.append(chooserList);
|
|
436
|
+
sidebar.append(chooser);
|
|
437
|
+
main.append(previewCard);
|
|
438
|
+
content.append(sidebar, main);
|
|
439
|
+
root.append(header, content);
|
|
440
|
+
const openDocument = async (member) => {
|
|
441
|
+
const selection = ++renderSequence;
|
|
442
|
+
await clearNested();
|
|
443
|
+
if (disposed || selection !== renderSequence)
|
|
444
|
+
return;
|
|
445
|
+
previewTarget.replaceChildren();
|
|
446
|
+
previewTitle.textContent = `Contained document · ${member.name}`;
|
|
447
|
+
previewStatus.textContent = formatBytes(member.uncompressedSize);
|
|
448
|
+
if (!member.data || member.data.byteLength > maxNestedPreviewSize) {
|
|
449
|
+
previewTarget.append(createElement(documentRef, 'div', 'signature-preview-empty', 'This contained document exceeds the configured nested-preview boundary.'));
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
const nestedExtension = getFilenameExtension(member.name) || sniffNestedExtension(member.data);
|
|
453
|
+
if (!(context === null || context === void 0 ? void 0 : context.renderNestedBuffer) || nestedExtension === 'bin') {
|
|
454
|
+
previewTarget.append(createElement(documentRef, 'div', 'signature-preview-empty', 'No compatible nested renderer is installed for this contained document.'));
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
nestedRendered = await context.renderNestedBuffer(member.data.buffer.slice(member.data.byteOffset, member.data.byteOffset + member.data.byteLength), nestedExtension, previewTarget, { ...context, filename: member.name, url: undefined, streamUrl: undefined });
|
|
458
|
+
};
|
|
459
|
+
inspection.documents.forEach((member, index) => {
|
|
460
|
+
const button = createElement(documentRef, 'button', 'signature-status', member.name);
|
|
461
|
+
button.type = 'button';
|
|
462
|
+
button.addEventListener('click', () => {
|
|
463
|
+
void openDocument(member);
|
|
464
|
+
});
|
|
465
|
+
chooserList.append(button);
|
|
466
|
+
if (index === 0)
|
|
467
|
+
void openDocument(member);
|
|
468
|
+
});
|
|
469
|
+
if (!inspection.documents.length) {
|
|
470
|
+
previewTarget.append(createElement(documentRef, 'div', 'signature-preview-empty', 'No contained document was found.'));
|
|
471
|
+
}
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
474
|
+
if (extension === 'ers') {
|
|
475
|
+
const inspection = await inspectEvidenceRecord(buffer, {
|
|
476
|
+
originalContent
|
|
477
|
+
});
|
|
478
|
+
if (disposed || sequence !== renderSequence)
|
|
479
|
+
return;
|
|
480
|
+
root.replaceChildren();
|
|
481
|
+
const header = createElement(documentRef, 'header', 'signature-header');
|
|
482
|
+
const title = createElement(documentRef, 'div', 'signature-title');
|
|
483
|
+
title.append(createElement(documentRef, 'small', undefined, 'ARCHIVAL EVIDENCE'), createElement(documentRef, 'h2', undefined, filename), createElement(documentRef, 'p', undefined, inspection.detectedFormat));
|
|
484
|
+
header.append(title, createElement(documentRef, 'span', 'signature-phase', 'RFC 4998 · STRUCTURAL + BOUNDED'));
|
|
485
|
+
const content = createElement(documentRef, 'div', 'signature-content');
|
|
486
|
+
const sidebar = createElement(documentRef, 'aside', 'signature-sidebar');
|
|
487
|
+
const main = createElement(documentRef, 'main', 'signature-main');
|
|
488
|
+
const summary = createElement(documentRef, 'section', 'signature-card');
|
|
489
|
+
summary.append(createElement(documentRef, 'h3', undefined, 'Evidence record'));
|
|
490
|
+
appendDefinitionList(documentRef, summary, [
|
|
491
|
+
['Version', inspection.version],
|
|
492
|
+
['Digest algorithms', inspection.digestAlgorithms.join(', ')],
|
|
493
|
+
['Archive timestamp chains', inspection.archiveTimestampChains],
|
|
494
|
+
['Archive timestamps', inspection.archiveTimestamps.length],
|
|
495
|
+
['Original supplied', inspection.originalContentSupplied ? 'Yes' : 'No'],
|
|
496
|
+
[
|
|
497
|
+
'First evidence digest',
|
|
498
|
+
inspection.originalEvidenceMatches === undefined
|
|
499
|
+
? 'Not checked'
|
|
500
|
+
: inspection.originalEvidenceMatches
|
|
501
|
+
? 'Matches'
|
|
502
|
+
: 'Does not match'
|
|
503
|
+
],
|
|
504
|
+
['Full archival validation', 'Not performed']
|
|
505
|
+
]);
|
|
506
|
+
sidebar.append(summary);
|
|
507
|
+
const boundaries = createElement(documentRef, 'section', 'signature-card');
|
|
508
|
+
boundaries.append(createElement(documentRef, 'h3', undefined, 'Validation boundaries'));
|
|
509
|
+
inspection.warnings.forEach((warning) => boundaries.append(createElement(documentRef, 'div', 'signature-warning', warning)));
|
|
510
|
+
sidebar.append(boundaries);
|
|
511
|
+
const originalCard = createElement(documentRef, 'section', 'signature-card');
|
|
512
|
+
originalCard.append(createElement(documentRef, 'h3', undefined, 'Protected original content'));
|
|
513
|
+
const input = createElement(documentRef, 'input', 'signature-file');
|
|
514
|
+
input.type = 'file';
|
|
515
|
+
input.setAttribute('aria-label', 'Choose EvidenceRecord protected content');
|
|
516
|
+
input.addEventListener('change', () => {
|
|
517
|
+
var _a;
|
|
518
|
+
const file = (_a = input.files) === null || _a === void 0 ? void 0 : _a[0];
|
|
519
|
+
if (!file)
|
|
520
|
+
return;
|
|
521
|
+
void readBoundedBlob(file, maxNestedPreviewSize, 'EvidenceRecord protected content').then((next) => {
|
|
522
|
+
originalContent = next;
|
|
523
|
+
originalFilename = file.name;
|
|
524
|
+
return renderInspection();
|
|
525
|
+
});
|
|
526
|
+
});
|
|
527
|
+
originalCard.append(input);
|
|
528
|
+
sidebar.append(originalCard);
|
|
529
|
+
const timestamps = createElement(documentRef, 'section', 'signature-card');
|
|
530
|
+
timestamps.append(createElement(documentRef, 'h3', undefined, 'Archive timestamps'));
|
|
531
|
+
inspection.archiveTimestamps.forEach((timestamp) => {
|
|
532
|
+
var _a;
|
|
533
|
+
const item = createElement(documentRef, 'article', 'signature-item');
|
|
534
|
+
item.append(createElement(documentRef, 'strong', undefined, `Chain ${timestamp.chainIndex + 1} · timestamp ${timestamp.index + 1}`));
|
|
535
|
+
appendDefinitionList(documentRef, item, [
|
|
536
|
+
['Digest algorithm', timestamp.digestAlgorithm],
|
|
537
|
+
['Reduced hash-tree nodes', timestamp.reducedHashTreeNodes],
|
|
538
|
+
['Generation time', (_a = timestamp.timestamp) === null || _a === void 0 ? void 0 : _a.generationTime],
|
|
539
|
+
['Timestamp signatures', timestamp.timestampSignerCount],
|
|
540
|
+
[
|
|
541
|
+
'Timestamp signature check',
|
|
542
|
+
timestamp.timestampSignaturesValid === undefined
|
|
543
|
+
? 'Not checked'
|
|
544
|
+
: timestamp.timestampSignaturesValid
|
|
545
|
+
? 'Valid'
|
|
546
|
+
: 'Invalid'
|
|
547
|
+
],
|
|
548
|
+
[
|
|
549
|
+
'Evidence digest vs original',
|
|
550
|
+
timestamp.evidenceDigestMatchesOriginal === undefined
|
|
551
|
+
? 'Not checked'
|
|
552
|
+
: timestamp.evidenceDigestMatchesOriginal
|
|
553
|
+
? 'Matches'
|
|
554
|
+
: 'Does not match'
|
|
555
|
+
]
|
|
556
|
+
]);
|
|
557
|
+
timestamps.append(item);
|
|
558
|
+
});
|
|
559
|
+
main.append(timestamps);
|
|
560
|
+
content.append(sidebar, main);
|
|
561
|
+
root.append(header, content);
|
|
562
|
+
return;
|
|
563
|
+
}
|
|
564
|
+
if (isProbablyJws(sourceBytes, filename)) {
|
|
565
|
+
const inspection = await inspectJws(buffer, {
|
|
566
|
+
detachedPayload: originalContent,
|
|
567
|
+
verificationKeys: jwsVerificationKeys,
|
|
568
|
+
limits: containerLimits
|
|
569
|
+
});
|
|
570
|
+
if (disposed || sequence !== renderSequence)
|
|
571
|
+
return;
|
|
572
|
+
root.replaceChildren();
|
|
573
|
+
const header = createElement(documentRef, 'header', 'signature-header');
|
|
574
|
+
const title = createElement(documentRef, 'div', 'signature-title');
|
|
575
|
+
title.append(createElement(documentRef, 'small', undefined, 'JSON WEB SIGNATURE'), createElement(documentRef, 'h2', undefined, filename), createElement(documentRef, 'p', undefined, `${inspection.serialization} · ${inspection.signatures.length} signature(s)`));
|
|
576
|
+
header.append(title, createElement(documentRef, 'span', 'signature-phase', 'RFC 7515 · PUBLIC-KEY VERIFY'));
|
|
577
|
+
const content = createElement(documentRef, 'div', 'signature-content');
|
|
578
|
+
const sidebar = createElement(documentRef, 'aside', 'signature-sidebar');
|
|
579
|
+
const main = createElement(documentRef, 'main', 'signature-main');
|
|
580
|
+
const summary = createElement(documentRef, 'section', 'signature-card');
|
|
581
|
+
summary.append(createElement(documentRef, 'h3', undefined, 'JWS summary'));
|
|
582
|
+
appendDefinitionList(documentRef, summary, [
|
|
583
|
+
['Serialization', inspection.serialization],
|
|
584
|
+
['Detached payload', inspection.detached ? 'Yes' : 'No'],
|
|
585
|
+
['Base64url payload', inspection.payloadEncoded ? 'Yes' : 'No'],
|
|
586
|
+
[
|
|
587
|
+
'Payload size',
|
|
588
|
+
inspection.payload ? formatBytes(inspection.payload.byteLength) : 'Not supplied'
|
|
589
|
+
],
|
|
590
|
+
['Signatures', inspection.signatures.length],
|
|
591
|
+
['Verification keys', jwsVerificationKeys.length]
|
|
592
|
+
]);
|
|
593
|
+
sidebar.append(summary);
|
|
594
|
+
const boundaries = createElement(documentRef, 'section', 'signature-card');
|
|
595
|
+
boundaries.append(createElement(documentRef, 'h3', undefined, 'Security and trust boundary'));
|
|
596
|
+
inspection.warnings.forEach((warning) => boundaries.append(createElement(documentRef, 'div', 'signature-warning', warning)));
|
|
597
|
+
sidebar.append(boundaries);
|
|
598
|
+
if (inspection.detached) {
|
|
599
|
+
const originalCard = createElement(documentRef, 'section', 'signature-card');
|
|
600
|
+
originalCard.append(createElement(documentRef, 'h3', undefined, 'Detached payload'));
|
|
601
|
+
const input = createElement(documentRef, 'input', 'signature-file');
|
|
602
|
+
input.type = 'file';
|
|
603
|
+
input.addEventListener('change', () => {
|
|
604
|
+
var _a;
|
|
605
|
+
const file = (_a = input.files) === null || _a === void 0 ? void 0 : _a[0];
|
|
606
|
+
if (!file)
|
|
607
|
+
return;
|
|
608
|
+
void readBoundedBlob(file, maxNestedPreviewSize, 'JWS detached payload').then((next) => {
|
|
609
|
+
originalContent = next;
|
|
610
|
+
originalFilename = file.name;
|
|
611
|
+
return renderInspection();
|
|
612
|
+
});
|
|
613
|
+
});
|
|
614
|
+
originalCard.append(input);
|
|
615
|
+
sidebar.append(originalCard);
|
|
616
|
+
}
|
|
617
|
+
const keyCard = createElement(documentRef, 'section', 'signature-card');
|
|
618
|
+
keyCard.append(createElement(documentRef, 'h3', undefined, 'Public JWK verification keys'));
|
|
619
|
+
const keyInput = createElement(documentRef, 'input', 'signature-file');
|
|
620
|
+
keyInput.type = 'file';
|
|
621
|
+
keyInput.multiple = true;
|
|
622
|
+
keyInput.accept = '.jwk,.json,application/json';
|
|
623
|
+
keyInput.addEventListener('change', () => {
|
|
624
|
+
const files = Array.from(keyInput.files || []).slice(0, 64);
|
|
625
|
+
void Promise.all(files.map((file) => readBoundedBlob(file, 1024 * 1024, 'JWK file'))).then((values) => {
|
|
626
|
+
const keys = [];
|
|
627
|
+
for (const value of values) {
|
|
628
|
+
const parsed = JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(value));
|
|
629
|
+
const candidates = 'keys' in parsed && Array.isArray(parsed.keys)
|
|
630
|
+
? parsed.keys
|
|
631
|
+
: [parsed];
|
|
632
|
+
for (const key of candidates) {
|
|
633
|
+
if (!key || typeof key !== 'object' || 'd' in key || 'k' in key)
|
|
634
|
+
throw new Error('Only public asymmetric JWKs are accepted.');
|
|
635
|
+
keys.push({ key, kid: key.kid });
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
jwsVerificationKeys = keys.slice(0, 64);
|
|
639
|
+
return renderInspection();
|
|
640
|
+
});
|
|
641
|
+
});
|
|
642
|
+
keyCard.append(keyInput);
|
|
643
|
+
sidebar.append(keyCard);
|
|
644
|
+
const signatures = createElement(documentRef, 'section', 'signature-card');
|
|
645
|
+
signatures.append(createElement(documentRef, 'h3', undefined, 'Signatures'));
|
|
646
|
+
inspection.signatures.forEach((signature) => {
|
|
647
|
+
const item = createElement(documentRef, 'article', 'signature-item');
|
|
648
|
+
item.append(createElement(documentRef, 'strong', undefined, `Signature ${signature.index + 1}`));
|
|
649
|
+
const status = createElement(documentRef, 'div', 'signature-status-list');
|
|
650
|
+
status.append(statusBadge(documentRef, 'Cryptographic signature', signature.cryptographicValid, inspection.detached && !inspection.payload
|
|
651
|
+
? 'payload required'
|
|
652
|
+
: 'public key required'));
|
|
653
|
+
item.append(status);
|
|
654
|
+
appendDefinitionList(documentRef, item, [
|
|
655
|
+
['Algorithm', signature.algorithm],
|
|
656
|
+
['Key ID', signature.keyId],
|
|
657
|
+
['Signature bytes', signature.signatureBytes],
|
|
658
|
+
['JAdES-related properties', signature.jadesProperties.join(', ')],
|
|
659
|
+
['Verification note', signature.verificationError],
|
|
660
|
+
['Protected header', JSON.stringify(signature.protectedHeader)],
|
|
661
|
+
['Unprotected header', JSON.stringify(signature.unprotectedHeader)]
|
|
662
|
+
]);
|
|
663
|
+
signatures.append(item);
|
|
664
|
+
});
|
|
665
|
+
main.append(signatures);
|
|
666
|
+
const previewCard = createElement(documentRef, 'section', 'signature-card signature-preview');
|
|
667
|
+
const previewTarget = createElement(documentRef, 'div', 'signature-preview-target');
|
|
668
|
+
const sniffedPayloadExtension = inspection.payload
|
|
669
|
+
? sniffNestedExtension(inspection.payload)
|
|
670
|
+
: 'bin';
|
|
671
|
+
const payloadExtension = originalFilename
|
|
672
|
+
? getFilenameExtension(originalFilename) || sniffedPayloadExtension
|
|
673
|
+
: sniffedPayloadExtension;
|
|
674
|
+
const payloadName = originalFilename || `jws-payload.${payloadExtension}`;
|
|
675
|
+
const previewHead = createElement(documentRef, 'div', 'signature-preview-head');
|
|
676
|
+
previewHead.append(createElement(documentRef, 'strong', undefined, `Payload preview · ${payloadName}`), createElement(documentRef, 'span', 'signature-status', inspection.payload ? formatBytes(inspection.payload.byteLength) : 'payload unavailable'));
|
|
677
|
+
previewCard.append(previewHead, previewTarget);
|
|
678
|
+
main.append(previewCard);
|
|
679
|
+
content.append(sidebar, main);
|
|
680
|
+
root.append(header, content);
|
|
681
|
+
if (inspection.payload &&
|
|
682
|
+
inspection.payload.byteLength <= maxNestedPreviewSize &&
|
|
683
|
+
(context === null || context === void 0 ? void 0 : context.renderNestedBuffer) &&
|
|
684
|
+
payloadExtension !== 'bin') {
|
|
685
|
+
nestedRendered = await context.renderNestedBuffer(inspection.payload.buffer.slice(inspection.payload.byteOffset, inspection.payload.byteOffset + inspection.payload.byteLength), payloadExtension, previewTarget, { ...context, filename: payloadName, url: undefined, streamUrl: undefined });
|
|
686
|
+
}
|
|
687
|
+
else {
|
|
688
|
+
previewTarget.append(createElement(documentRef, 'div', 'signature-preview-empty', inspection.payload
|
|
689
|
+
? 'No compatible nested renderer is installed for this payload.'
|
|
690
|
+
: 'Supply the detached payload to preview and verify it.'));
|
|
691
|
+
}
|
|
692
|
+
return;
|
|
693
|
+
}
|
|
694
|
+
if (isProbablyOpenPgp(sourceBytes, filename, type)) {
|
|
695
|
+
const { OpenPgpWorkerClient } = await import('./openpgp/client.js');
|
|
696
|
+
openPgpClient || (openPgpClient = new OpenPgpWorkerClient((options === null || options === void 0 ? void 0 : options.workerFactory) ? () => options.workerFactory('openpgp') : undefined));
|
|
697
|
+
const inspection = await openPgpClient.inspect(buffer, options === null || options === void 0 ? void 0 : options.openPgpLimits, openPgpPublicKeys);
|
|
698
|
+
let verification;
|
|
699
|
+
if (inspection.classification === 'detached-signature' &&
|
|
700
|
+
originalContent &&
|
|
701
|
+
openPgpPublicKeys.length) {
|
|
702
|
+
verification = await openPgpClient.verifyDetached(originalContent, buffer, openPgpPublicKeys, options === null || options === void 0 ? void 0 : options.openPgpLimits);
|
|
703
|
+
}
|
|
704
|
+
if (disposed || sequence !== renderSequence)
|
|
705
|
+
return;
|
|
706
|
+
root.replaceChildren();
|
|
707
|
+
const header = createElement(documentRef, 'header', 'signature-header');
|
|
708
|
+
const title = createElement(documentRef, 'div', 'signature-title');
|
|
709
|
+
title.append(createElement(documentRef, 'small', undefined, 'OPENPGP · rPGP / WASM'), createElement(documentRef, 'h2', undefined, filename), createElement(documentRef, 'p', undefined, inspection.classification));
|
|
710
|
+
header.append(title, createElement(documentRef, 'span', 'signature-phase', 'OPTIONAL · LAZY WORKER / WASM'));
|
|
711
|
+
const content = createElement(documentRef, 'div', 'signature-content');
|
|
712
|
+
const sidebar = createElement(documentRef, 'aside', 'signature-sidebar');
|
|
713
|
+
const main = createElement(documentRef, 'main', 'signature-main');
|
|
714
|
+
const summary = createElement(documentRef, 'section', 'signature-card');
|
|
715
|
+
summary.append(createElement(documentRef, 'h3', undefined, 'OpenPGP summary'));
|
|
716
|
+
appendDefinitionList(documentRef, summary, [
|
|
717
|
+
['Classification', inspection.classification],
|
|
718
|
+
['ASCII armored', inspection.armored ? 'Yes' : 'No'],
|
|
719
|
+
['Armor type', inspection.armorType],
|
|
720
|
+
['Encrypted', inspection.encrypted ? 'Yes' : 'No'],
|
|
721
|
+
[
|
|
722
|
+
'Integrity protected',
|
|
723
|
+
inspection.integrityProtected === undefined
|
|
724
|
+
? undefined
|
|
725
|
+
: inspection.integrityProtected
|
|
726
|
+
? 'Yes'
|
|
727
|
+
: 'No'
|
|
728
|
+
],
|
|
729
|
+
['Symmetric algorithm', inspection.symmetricAlgorithm],
|
|
730
|
+
['AEAD mode', inspection.aeadMode],
|
|
731
|
+
['Recipients', inspection.recipients.join(', ')],
|
|
732
|
+
['Compressed', inspection.compressed ? 'Yes' : 'No'],
|
|
733
|
+
['Packet count', inspection.packetCount],
|
|
734
|
+
['Packet types', inspection.packetTypes.join(', ')],
|
|
735
|
+
['Keys', inspection.keys.length],
|
|
736
|
+
['Signatures', inspection.signatures.length]
|
|
737
|
+
]);
|
|
738
|
+
summary.append(createElement(documentRef, 'div', 'signature-openpgp-note', 'OpenPGP parsing runs browser-local inside a dedicated Worker backed by rPGP WebAssembly. OpenPGP.js and GnuPG are not used.'));
|
|
739
|
+
sidebar.append(summary);
|
|
740
|
+
const warnings = createElement(documentRef, 'section', 'signature-card');
|
|
741
|
+
warnings.append(createElement(documentRef, 'h3', undefined, 'Security boundaries'));
|
|
742
|
+
[
|
|
743
|
+
...inspection.warnings,
|
|
744
|
+
'Parsing does not imply signature verification, key trust, identity trust, or legal validity.',
|
|
745
|
+
'Private-key decryption, signing, key generation and message decryption are intentionally excluded from this inspection boundary.'
|
|
746
|
+
].forEach((warning) => warnings.append(createElement(documentRef, 'div', 'signature-warning', warning)));
|
|
747
|
+
sidebar.append(warnings);
|
|
748
|
+
if (inspection.classification === 'detached-signature') {
|
|
749
|
+
const verifyCard = createElement(documentRef, 'section', 'signature-card');
|
|
750
|
+
verifyCard.append(createElement(documentRef, 'h3', undefined, 'Detached signature verification'));
|
|
751
|
+
const statusList = createElement(documentRef, 'div', 'signature-status-list');
|
|
752
|
+
statusList.append(statusBadge(documentRef, 'Cryptographic signature', verification === null || verification === void 0 ? void 0 : verification.valid, originalContent
|
|
753
|
+
? openPgpPublicKeys.length
|
|
754
|
+
? 'not verified'
|
|
755
|
+
: 'public key required'
|
|
756
|
+
: 'original required'));
|
|
757
|
+
verifyCard.append(statusList);
|
|
758
|
+
appendDefinitionList(documentRef, verifyCard, [
|
|
759
|
+
['Result', verification === null || verification === void 0 ? void 0 : verification.status],
|
|
760
|
+
['Key fingerprint', verification === null || verification === void 0 ? void 0 : verification.keyFingerprint],
|
|
761
|
+
['Key ID', verification === null || verification === void 0 ? void 0 : verification.keyId],
|
|
762
|
+
['Verification note', verification === null || verification === void 0 ? void 0 : verification.error]
|
|
763
|
+
]);
|
|
764
|
+
const originalBox = createElement(documentRef, 'div', 'signature-original');
|
|
765
|
+
const originalInput = createElement(documentRef, 'input', 'signature-file');
|
|
766
|
+
originalInput.type = 'file';
|
|
767
|
+
originalInput.setAttribute('aria-label', 'Choose original OpenPGP signed content');
|
|
768
|
+
const originalInputState = createElement(documentRef, 'div', 'signature-original-state', originalContent
|
|
769
|
+
? `${originalFilename || 'Host-provided original'} · ${formatBytes(originalContent.byteLength)}`
|
|
770
|
+
: 'Original content is required.');
|
|
771
|
+
originalInput.addEventListener('change', () => {
|
|
772
|
+
var _a;
|
|
773
|
+
const file = (_a = originalInput.files) === null || _a === void 0 ? void 0 : _a[0];
|
|
774
|
+
if (!file)
|
|
775
|
+
return;
|
|
776
|
+
void readBoundedBlob(file, maxNestedPreviewSize, 'OpenPGP original content')
|
|
777
|
+
.then((next) => {
|
|
778
|
+
originalContent = next;
|
|
779
|
+
originalFilename = file.name;
|
|
780
|
+
return renderInspection();
|
|
781
|
+
})
|
|
782
|
+
.catch((error) => {
|
|
783
|
+
originalInputState.textContent =
|
|
784
|
+
error instanceof Error ? error.message : String(error);
|
|
785
|
+
});
|
|
786
|
+
});
|
|
787
|
+
originalBox.append(originalInput, originalInputState);
|
|
788
|
+
const keyBox = createElement(documentRef, 'div', 'signature-key-input');
|
|
789
|
+
const keyInput = createElement(documentRef, 'input', 'signature-file');
|
|
790
|
+
keyInput.type = 'file';
|
|
791
|
+
keyInput.multiple = true;
|
|
792
|
+
keyInput.setAttribute('aria-label', 'Choose OpenPGP public verification keys');
|
|
793
|
+
const keyInputState = createElement(documentRef, 'div', 'signature-original-state', openPgpPublicKeys.length
|
|
794
|
+
? `${openPgpPublicKeys.length} verification key file(s) supplied.`
|
|
795
|
+
: 'Supply one or more public keys; no keyring lookup is performed.');
|
|
796
|
+
keyInput.addEventListener('change', () => {
|
|
797
|
+
const files = Array.from(keyInput.files || []).slice(0, 64);
|
|
798
|
+
void Promise.all(files.map((file) => readBoundedBlob(file, 8 * 1024 * 1024, 'OpenPGP public key')))
|
|
799
|
+
.then((keys) => {
|
|
800
|
+
const aggregate = keys.reduce((sum, value) => sum + value.byteLength, 0);
|
|
801
|
+
if (aggregate > 32 * 1024 * 1024)
|
|
802
|
+
throw new Error('OpenPGP public keys exceed the 32 MiB aggregate boundary.');
|
|
803
|
+
openPgpPublicKeys = keys;
|
|
804
|
+
return renderInspection();
|
|
805
|
+
})
|
|
806
|
+
.catch((error) => {
|
|
807
|
+
keyInputState.textContent = error instanceof Error ? error.message : String(error);
|
|
808
|
+
});
|
|
809
|
+
});
|
|
810
|
+
keyBox.append(keyInput, keyInputState);
|
|
811
|
+
verifyCard.append(originalBox, keyBox);
|
|
812
|
+
sidebar.append(verifyCard);
|
|
813
|
+
}
|
|
814
|
+
if (inspection.keys.length) {
|
|
815
|
+
const keysCard = createElement(documentRef, 'section', 'signature-card');
|
|
816
|
+
keysCard.append(createElement(documentRef, 'h3', undefined, 'Key metadata'));
|
|
817
|
+
inspection.keys.forEach((key) => keysCard.append(renderOpenPgpKey(documentRef, key)));
|
|
818
|
+
main.append(keysCard);
|
|
819
|
+
}
|
|
820
|
+
if (inspection.signatures.length) {
|
|
821
|
+
const signaturesCard = createElement(documentRef, 'section', 'signature-card');
|
|
822
|
+
signaturesCard.append(createElement(documentRef, 'h3', undefined, 'Signature metadata'));
|
|
823
|
+
inspection.signatures.forEach((signature, index) => signaturesCard.append(renderOpenPgpSignature(documentRef, signature, index)));
|
|
824
|
+
main.append(signaturesCard);
|
|
825
|
+
}
|
|
826
|
+
const previewCard = createElement(documentRef, 'section', 'signature-card signature-preview');
|
|
827
|
+
const previewHead = createElement(documentRef, 'div', 'signature-preview-head');
|
|
828
|
+
const previewTarget = createElement(documentRef, 'div', 'signature-preview-target');
|
|
829
|
+
const literal = inspection.literalData;
|
|
830
|
+
const literalBytes = literal === null || literal === void 0 ? void 0 : literal.data;
|
|
831
|
+
const literalFilename = (literal === null || literal === void 0 ? void 0 : literal.filename) || 'openpgp-literal-data.bin';
|
|
832
|
+
const literalExtension = getFilenameExtension(literalFilename) ||
|
|
833
|
+
(literalBytes ? sniffNestedExtension(literalBytes) : 'bin');
|
|
834
|
+
previewHead.append(createElement(documentRef, 'strong', undefined, `Literal-data preview · ${literalFilename}`), createElement(documentRef, 'span', 'signature-status', literalBytes ? formatBytes(literalBytes.byteLength) : 'not extracted'));
|
|
835
|
+
previewCard.append(previewHead, previewTarget);
|
|
836
|
+
main.append(previewCard);
|
|
837
|
+
if (!literalBytes) {
|
|
838
|
+
previewTarget.append(createElement(documentRef, 'div', 'signature-preview-empty', inspection.encrypted
|
|
839
|
+
? 'Encrypted OpenPGP data is inspection-only; automatic decryption is not exposed.'
|
|
840
|
+
: inspection.compressed
|
|
841
|
+
? 'Compressed OpenPGP data did not expose literal content within the configured nesting and output limits.'
|
|
842
|
+
: 'No safely extractable literal data is available.'));
|
|
843
|
+
}
|
|
844
|
+
else if (literalBytes.byteLength > maxNestedPreviewSize) {
|
|
845
|
+
previewTarget.append(createElement(documentRef, 'div', 'signature-preview-empty', 'Extracted literal data exceeds the nested-preview limit.'));
|
|
846
|
+
}
|
|
847
|
+
else if (!(context === null || context === void 0 ? void 0 : context.renderNestedBuffer) || literalExtension === 'bin') {
|
|
848
|
+
previewTarget.append(createElement(documentRef, 'div', 'signature-preview-empty', 'Literal data was extracted, but no compatible nested renderer is available.'));
|
|
849
|
+
}
|
|
850
|
+
else {
|
|
851
|
+
nestedRendered = await context.renderNestedBuffer(literalBytes.buffer.slice(literalBytes.byteOffset, literalBytes.byteOffset + literalBytes.byteLength), literalExtension, previewTarget, { ...context, filename: literalFilename, url: undefined, streamUrl: undefined });
|
|
852
|
+
}
|
|
853
|
+
content.append(sidebar, main);
|
|
854
|
+
root.append(header, content);
|
|
855
|
+
return;
|
|
856
|
+
}
|
|
857
|
+
const inspection = await inspectSignatureContainer(buffer, {
|
|
858
|
+
sourceFilename: filename,
|
|
859
|
+
extensionHint: type,
|
|
860
|
+
originalContent
|
|
861
|
+
});
|
|
862
|
+
if (disposed || sequence !== renderSequence) {
|
|
863
|
+
return;
|
|
864
|
+
}
|
|
865
|
+
root.replaceChildren();
|
|
866
|
+
const header = createElement(documentRef, 'header', 'signature-header');
|
|
867
|
+
const title = createElement(documentRef, 'div', 'signature-title');
|
|
868
|
+
title.append(createElement(documentRef, 'small', undefined, 'CRYPTOGRAPHIC CONTAINER'), createElement(documentRef, 'h2', undefined, filename), createElement(documentRef, 'p', undefined, inspection.detectedFormat));
|
|
869
|
+
header.append(title, createElement(documentRef, 'span', 'signature-phase', 'CMS · RFC 3161 · RFC 5544 · BOUNDED'));
|
|
870
|
+
const content = createElement(documentRef, 'div', 'signature-content');
|
|
871
|
+
const sidebar = createElement(documentRef, 'aside', 'signature-sidebar');
|
|
872
|
+
const main = createElement(documentRef, 'main', 'signature-main');
|
|
873
|
+
const summary = createElement(documentRef, 'section', 'signature-card');
|
|
874
|
+
summary.append(createElement(documentRef, 'h3', undefined, 'Container summary'));
|
|
875
|
+
appendDefinitionList(documentRef, summary, [
|
|
876
|
+
['Detected format', inspection.detectedFormat],
|
|
877
|
+
['Size', formatBytes(inspection.sourceSize)],
|
|
878
|
+
['CMS content type', inspection.contentType],
|
|
879
|
+
['Signed content type', inspection.signedContentType],
|
|
880
|
+
['Digest algorithms', inspection.digestAlgorithms.join(', ')],
|
|
881
|
+
[
|
|
882
|
+
'Encapsulated content',
|
|
883
|
+
inspection.embeddedContent ? formatBytes(inspection.embeddedContent.byteLength) : 'No'
|
|
884
|
+
],
|
|
885
|
+
['Detached', inspection.detached ? 'Yes' : 'No'],
|
|
886
|
+
['Signers', inspection.signers.length],
|
|
887
|
+
['Certificates', inspection.certificates.length],
|
|
888
|
+
['CRLs', inspection.crlCount]
|
|
889
|
+
]);
|
|
890
|
+
sidebar.append(summary);
|
|
891
|
+
const boundaries = createElement(documentRef, 'section', 'signature-card');
|
|
892
|
+
boundaries.append(createElement(documentRef, 'h3', undefined, 'Validation boundaries'));
|
|
893
|
+
inspection.warnings.forEach((warning) => boundaries.append(createElement(documentRef, 'div', 'signature-warning', warning)));
|
|
894
|
+
sidebar.append(boundaries);
|
|
895
|
+
if (inspection.timestampResponse) {
|
|
896
|
+
const response = createElement(documentRef, 'section', 'signature-card');
|
|
897
|
+
response.append(createElement(documentRef, 'h3', undefined, 'Timestamp response'));
|
|
898
|
+
appendDefinitionList(documentRef, response, [
|
|
899
|
+
['Status', inspection.timestampResponse.statusLabel],
|
|
900
|
+
['Status code', inspection.timestampResponse.status],
|
|
901
|
+
['Status text', inspection.timestampResponse.statusText.join('; ')],
|
|
902
|
+
['Failure info', inspection.timestampResponse.failureInfo]
|
|
903
|
+
]);
|
|
904
|
+
sidebar.append(response);
|
|
905
|
+
}
|
|
906
|
+
if (inspection.timestampedData) {
|
|
907
|
+
const tsd = createElement(documentRef, 'section', 'signature-card');
|
|
908
|
+
tsd.append(createElement(documentRef, 'h3', undefined, 'RFC 5544 TimeStampedData'));
|
|
909
|
+
appendDefinitionList(documentRef, tsd, [
|
|
910
|
+
['Data URI', inspection.timestampedData.dataUri],
|
|
911
|
+
['Filename', inspection.timestampedData.filename],
|
|
912
|
+
['Media type', inspection.timestampedData.mediaType],
|
|
913
|
+
[
|
|
914
|
+
'Metadata hash protected',
|
|
915
|
+
inspection.timestampedData.hashProtected === undefined
|
|
916
|
+
? undefined
|
|
917
|
+
: inspection.timestampedData.hashProtected
|
|
918
|
+
? 'Yes'
|
|
919
|
+
: 'No'
|
|
920
|
+
],
|
|
921
|
+
['Temporal evidence entries', inspection.timestampedData.temporalEvidenceEntries]
|
|
922
|
+
]);
|
|
923
|
+
sidebar.append(tsd);
|
|
924
|
+
}
|
|
925
|
+
if (inspection.requiresOriginalContent ||
|
|
926
|
+
((_a = inspection.timestamp) === null || _a === void 0 ? void 0 : _a.messageImprint) ||
|
|
927
|
+
inspection.kind === 'timestamped-data') {
|
|
928
|
+
const originalCard = createElement(documentRef, 'section', 'signature-card');
|
|
929
|
+
originalCard.append(createElement(documentRef, 'h3', undefined, 'Original content'));
|
|
930
|
+
const originalBox = createElement(documentRef, 'div', 'signature-original');
|
|
931
|
+
const input = createElement(documentRef, 'input', 'signature-file');
|
|
932
|
+
input.type = 'file';
|
|
933
|
+
input.setAttribute('aria-label', 'Choose original content');
|
|
934
|
+
const originalState = createElement(documentRef, 'div', 'signature-original-state', originalContent
|
|
935
|
+
? `${originalFilename || 'Host-provided content'} · ${formatBytes(originalContent.byteLength)}`
|
|
936
|
+
: 'Choose the original file to compare its digest locally.');
|
|
937
|
+
input.addEventListener('change', () => {
|
|
938
|
+
var _a;
|
|
939
|
+
const file = (_a = input.files) === null || _a === void 0 ? void 0 : _a[0];
|
|
940
|
+
if (!file)
|
|
941
|
+
return;
|
|
942
|
+
void readBoundedBlob(file, maxNestedPreviewSize, 'Original content')
|
|
943
|
+
.then((nextBuffer) => {
|
|
944
|
+
originalContent = nextBuffer;
|
|
945
|
+
originalFilename = file.name;
|
|
946
|
+
return renderInspection();
|
|
947
|
+
})
|
|
948
|
+
.catch((error) => {
|
|
949
|
+
originalState.textContent = error instanceof Error ? error.message : String(error);
|
|
950
|
+
});
|
|
951
|
+
});
|
|
952
|
+
originalBox.append(input, originalState);
|
|
953
|
+
originalCard.append(originalBox);
|
|
954
|
+
sidebar.append(originalCard);
|
|
955
|
+
}
|
|
956
|
+
if (inspection.signers.length) {
|
|
957
|
+
const signers = createElement(documentRef, 'section', 'signature-card');
|
|
958
|
+
signers.append(createElement(documentRef, 'h3', undefined, 'Signers'));
|
|
959
|
+
inspection.signers.forEach((signer) => signers.append(renderSigner(documentRef, signer)));
|
|
960
|
+
main.append(signers);
|
|
961
|
+
}
|
|
962
|
+
if (inspection.timestamp) {
|
|
963
|
+
main.append(renderTimestamp(documentRef, inspection.timestamp));
|
|
964
|
+
}
|
|
965
|
+
if (inspection.certificates.length) {
|
|
966
|
+
const certificates = createElement(documentRef, 'section', 'signature-card');
|
|
967
|
+
certificates.append(createElement(documentRef, 'h3', undefined, 'Included certificates'));
|
|
968
|
+
inspection.certificates.forEach((certificate) => certificates.append(renderCertificate(documentRef, certificate)));
|
|
969
|
+
main.append(certificates);
|
|
970
|
+
}
|
|
971
|
+
const previewCard = createElement(documentRef, 'section', 'signature-card signature-preview');
|
|
972
|
+
const previewHead = createElement(documentRef, 'div', 'signature-preview-head');
|
|
973
|
+
const previewTarget = createElement(documentRef, 'div', 'signature-preview-target');
|
|
974
|
+
const timestampContainer = inspection.kind === 'timestamp-request' ||
|
|
975
|
+
inspection.kind === 'timestamp-response' ||
|
|
976
|
+
inspection.kind === 'timestamp-token' ||
|
|
977
|
+
inspection.kind === 'timestamped-data';
|
|
978
|
+
const previewUsesOriginal = Boolean(originalContent && (inspection.detached || timestampContainer));
|
|
979
|
+
const previewSource = previewUsesOriginal ? originalContent : inspection.embeddedContent;
|
|
980
|
+
const previewBytes = previewSource
|
|
981
|
+
? previewSource instanceof Uint8Array
|
|
982
|
+
? previewSource
|
|
983
|
+
: new Uint8Array(previewSource)
|
|
984
|
+
: undefined;
|
|
985
|
+
const identity = inspection.kind === 'timestamped-data' &&
|
|
986
|
+
((_b = inspection.timestampedData) === null || _b === void 0 ? void 0 : _b.filename) &&
|
|
987
|
+
!previewUsesOriginal
|
|
988
|
+
? {
|
|
989
|
+
filename: inspection.timestampedData.filename,
|
|
990
|
+
extension: getFilenameExtension(inspection.timestampedData.filename) ||
|
|
991
|
+
sniffNestedExtension(previewBytes || new Uint8Array())
|
|
992
|
+
}
|
|
993
|
+
: resolveNestedIdentity(inspection, filename, originalFilename, previewUsesOriginal);
|
|
994
|
+
previewHead.append(createElement(documentRef, 'strong', undefined, `Document preview · ${identity.filename}`), createElement(documentRef, 'span', 'signature-status', previewBytes ? formatBytes(previewBytes.byteLength) : 'content unavailable'));
|
|
995
|
+
previewCard.append(previewHead, previewTarget);
|
|
996
|
+
main.append(previewCard);
|
|
997
|
+
content.append(sidebar, main);
|
|
998
|
+
root.append(header, content);
|
|
999
|
+
if (!previewBytes) {
|
|
1000
|
+
previewTarget.append(createElement(documentRef, 'div', 'signature-preview-empty', inspection.requiresOriginalContent
|
|
1001
|
+
? 'Supply the original content to compare its digest or timestamp message imprint and open it through the nested renderer.'
|
|
1002
|
+
: 'The container does not include previewable document content.'));
|
|
1003
|
+
}
|
|
1004
|
+
else if (previewBytes.byteLength > maxNestedPreviewSize) {
|
|
1005
|
+
previewTarget.append(createElement(documentRef, 'div', 'signature-preview-empty', `Nested preview is disabled because ${formatBytes(previewBytes.byteLength)} exceeds the ${formatBytes(maxNestedPreviewSize)} limit.`));
|
|
1006
|
+
}
|
|
1007
|
+
else if (!(context === null || context === void 0 ? void 0 : context.renderNestedBuffer) || identity.extension === 'bin') {
|
|
1008
|
+
previewTarget.append(createElement(documentRef, 'div', 'signature-preview-empty', 'The content was extracted, but no compatible nested renderer is available for this file type.'));
|
|
1009
|
+
}
|
|
1010
|
+
else {
|
|
1011
|
+
nestedRendered = await context.renderNestedBuffer(previewBytes.buffer.slice(previewBytes.byteOffset, previewBytes.byteOffset + previewBytes.byteLength), identity.extension, previewTarget, {
|
|
1012
|
+
...context,
|
|
1013
|
+
filename: identity.filename,
|
|
1014
|
+
url: undefined,
|
|
1015
|
+
streamUrl: undefined
|
|
1016
|
+
});
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
catch (error) {
|
|
1020
|
+
if (disposed || sequence !== renderSequence) {
|
|
1021
|
+
return;
|
|
1022
|
+
}
|
|
1023
|
+
root.replaceChildren(createElement(documentRef, 'div', 'signature-error', error instanceof Error ? error.message : String(error)));
|
|
1024
|
+
}
|
|
1025
|
+
};
|
|
1026
|
+
await renderInspection();
|
|
1027
|
+
return {
|
|
1028
|
+
$el: root,
|
|
1029
|
+
async unmount() {
|
|
1030
|
+
disposed = true;
|
|
1031
|
+
renderSequence += 1;
|
|
1032
|
+
openPgpClient === null || openPgpClient === void 0 ? void 0 : openPgpClient.dispose();
|
|
1033
|
+
openPgpClient = undefined;
|
|
1034
|
+
containerClient === null || containerClient === void 0 ? void 0 : containerClient.dispose();
|
|
1035
|
+
containerClient = undefined;
|
|
1036
|
+
await clearNested();
|
|
1037
|
+
target.replaceChildren();
|
|
1038
|
+
}
|
|
1039
|
+
};
|
|
1040
|
+
}
|