@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,188 @@
|
|
|
1
|
+
import JSZip from 'jszip';
|
|
2
|
+
import { normalizeSignatureContainerLimits } from './limits.js';
|
|
3
|
+
import { inspectZipCentralDirectory } from './zipPreflight.js';
|
|
4
|
+
const ASICS_MIME = 'application/vnd.etsi.asic-s+zip';
|
|
5
|
+
const ASICE_MIME = 'application/vnd.etsi.asic-e+zip';
|
|
6
|
+
const METADATA_PREFIX = 'META-INF/';
|
|
7
|
+
const getExtension = (name) => {
|
|
8
|
+
const basename = name.split('/').pop() || '';
|
|
9
|
+
const dot = basename.lastIndexOf('.');
|
|
10
|
+
return dot > 0 ? basename.slice(dot + 1).toLowerCase() : '';
|
|
11
|
+
};
|
|
12
|
+
const signatureKind = (name) => {
|
|
13
|
+
const extension = getExtension(name);
|
|
14
|
+
if (['p7s', 'p7m', 'p7b', 'p7c', 'pkcs7', 'cms', 'cmsc'].includes(extension))
|
|
15
|
+
return 'cades';
|
|
16
|
+
if (['tst', 'tsr', 'tsq', 'tsd'].includes(extension))
|
|
17
|
+
return 'timestamp';
|
|
18
|
+
if (extension === 'ers')
|
|
19
|
+
return 'evidence-record';
|
|
20
|
+
if (extension === 'jws')
|
|
21
|
+
return 'jws';
|
|
22
|
+
if (extension === 'xml')
|
|
23
|
+
return 'xades-or-xml';
|
|
24
|
+
return 'unknown';
|
|
25
|
+
};
|
|
26
|
+
const contentTypeForName = (name) => {
|
|
27
|
+
const extension = getExtension(name);
|
|
28
|
+
return {
|
|
29
|
+
pdf: 'application/pdf',
|
|
30
|
+
xml: 'application/xml',
|
|
31
|
+
json: 'application/json',
|
|
32
|
+
txt: 'text/plain',
|
|
33
|
+
html: 'text/html',
|
|
34
|
+
htm: 'text/html',
|
|
35
|
+
png: 'image/png',
|
|
36
|
+
jpg: 'image/jpeg',
|
|
37
|
+
jpeg: 'image/jpeg',
|
|
38
|
+
gif: 'image/gif',
|
|
39
|
+
docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
|
40
|
+
xlsx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
|
|
41
|
+
}[extension];
|
|
42
|
+
};
|
|
43
|
+
const safeArchiveReference = (value) => {
|
|
44
|
+
if (!value ||
|
|
45
|
+
value.startsWith('#') ||
|
|
46
|
+
value.includes('\u0000') ||
|
|
47
|
+
/^[A-Za-z][A-Za-z0-9+.-]*:/u.test(value))
|
|
48
|
+
return undefined;
|
|
49
|
+
let decoded;
|
|
50
|
+
try {
|
|
51
|
+
decoded = decodeURIComponent(value);
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
|
56
|
+
if (decoded.startsWith('/') || decoded.includes('\\'))
|
|
57
|
+
return undefined;
|
|
58
|
+
const parts = decoded.split('/');
|
|
59
|
+
if (parts.some((part) => part === '..' || part === '.'))
|
|
60
|
+
return undefined;
|
|
61
|
+
return decoded.normalize('NFC');
|
|
62
|
+
};
|
|
63
|
+
/** Linear, non-validating extraction of same-container URI metadata only. */
|
|
64
|
+
const collectXmlReferences = (data, maxXmlBytes) => {
|
|
65
|
+
if (data.byteLength > maxXmlBytes)
|
|
66
|
+
return [];
|
|
67
|
+
const text = new TextDecoder('utf-8', { fatal: false }).decode(data);
|
|
68
|
+
const references = [];
|
|
69
|
+
let cursor = 0;
|
|
70
|
+
while (cursor < text.length && references.length < 256) {
|
|
71
|
+
const uri = text.indexOf('URI', cursor);
|
|
72
|
+
if (uri < 0)
|
|
73
|
+
break;
|
|
74
|
+
cursor = uri + 3;
|
|
75
|
+
while (cursor < text.length && /\s/u.test(text[cursor]))
|
|
76
|
+
cursor += 1;
|
|
77
|
+
if (text[cursor] !== '=')
|
|
78
|
+
continue;
|
|
79
|
+
cursor += 1;
|
|
80
|
+
while (cursor < text.length && /\s/u.test(text[cursor]))
|
|
81
|
+
cursor += 1;
|
|
82
|
+
const quote = text[cursor];
|
|
83
|
+
if (quote !== '"' && quote !== "'")
|
|
84
|
+
continue;
|
|
85
|
+
const end = text.indexOf(quote, cursor + 1);
|
|
86
|
+
if (end < 0 || end - cursor > 4096)
|
|
87
|
+
break;
|
|
88
|
+
const safe = safeArchiveReference(text.slice(cursor + 1, end));
|
|
89
|
+
if (safe && !references.includes(safe))
|
|
90
|
+
references.push(safe);
|
|
91
|
+
cursor = end + 1;
|
|
92
|
+
}
|
|
93
|
+
return references;
|
|
94
|
+
};
|
|
95
|
+
const extractChecked = async (zip, entry) => {
|
|
96
|
+
const file = zip.file(entry.name);
|
|
97
|
+
if (!file)
|
|
98
|
+
throw new Error(`ASiC entry ${entry.name} disappeared after ZIP validation.`);
|
|
99
|
+
const data = await file.async('uint8array');
|
|
100
|
+
if (data.byteLength !== entry.uncompressedSize) {
|
|
101
|
+
throw new Error(`ASiC entry ${entry.name} inflated to an unexpected size.`);
|
|
102
|
+
}
|
|
103
|
+
return data;
|
|
104
|
+
};
|
|
105
|
+
export const inspectAsicContainer = async (input, requestedLimits) => {
|
|
106
|
+
const limits = normalizeSignatureContainerLimits(requestedLimits);
|
|
107
|
+
const bytes = input instanceof Uint8Array ? input : new Uint8Array(input);
|
|
108
|
+
const directory = inspectZipCentralDirectory(bytes, limits);
|
|
109
|
+
const fileEntries = directory.entries.filter((entry) => !entry.directory);
|
|
110
|
+
const mimetypeEntry = fileEntries.find((entry) => entry.name === 'mimetype');
|
|
111
|
+
if (!mimetypeEntry ||
|
|
112
|
+
mimetypeEntry.localHeaderOffset !== 0 ||
|
|
113
|
+
mimetypeEntry.compressionMethod !== 0) {
|
|
114
|
+
throw new Error('ASiC requires an uncompressed first-entry mimetype file.');
|
|
115
|
+
}
|
|
116
|
+
const zip = await JSZip.loadAsync(bytes, { checkCRC32: true, createFolders: false });
|
|
117
|
+
const mimetypeBytes = await extractChecked(zip, mimetypeEntry);
|
|
118
|
+
const mediaType = new TextDecoder('ascii', { fatal: true }).decode(mimetypeBytes);
|
|
119
|
+
if (mediaType !== ASICS_MIME && mediaType !== ASICE_MIME) {
|
|
120
|
+
throw new Error(`Unsupported ASiC mimetype ${JSON.stringify(mediaType)}.`);
|
|
121
|
+
}
|
|
122
|
+
const documentEntries = fileEntries.filter((entry) => entry.name !== 'mimetype' && !entry.name.startsWith(METADATA_PREFIX));
|
|
123
|
+
const metadataEntries = fileEntries.filter((entry) => entry.name.startsWith(METADATA_PREFIX));
|
|
124
|
+
if (documentEntries.length > limits.maxDocuments)
|
|
125
|
+
throw new Error(`ASiC document count exceeds ${limits.maxDocuments}.`);
|
|
126
|
+
const signatureEntries = metadataEntries.filter((entry) => {
|
|
127
|
+
const basename = entry.name.slice(METADATA_PREFIX.length).toLowerCase();
|
|
128
|
+
return (basename.includes('signature') ||
|
|
129
|
+
basename.includes('timestamp') ||
|
|
130
|
+
['p7s', 'p7m', 'xml', 'tst', 'tsr', 'ers', 'jws'].includes(getExtension(entry.name)));
|
|
131
|
+
});
|
|
132
|
+
if (signatureEntries.length > limits.maxSignatureMembers)
|
|
133
|
+
throw new Error(`ASiC signature-member count exceeds ${limits.maxSignatureMembers}.`);
|
|
134
|
+
const documents = [];
|
|
135
|
+
for (const entry of documentEntries) {
|
|
136
|
+
documents.push({
|
|
137
|
+
name: entry.name,
|
|
138
|
+
compressedSize: entry.compressedSize,
|
|
139
|
+
uncompressedSize: entry.uncompressedSize,
|
|
140
|
+
mediaType: contentTypeForName(entry.name),
|
|
141
|
+
data: await extractChecked(zip, entry)
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
const signatures = [];
|
|
145
|
+
for (const entry of signatureEntries) {
|
|
146
|
+
const data = await extractChecked(zip, entry);
|
|
147
|
+
signatures.push({
|
|
148
|
+
name: entry.name,
|
|
149
|
+
compressedSize: entry.compressedSize,
|
|
150
|
+
uncompressedSize: entry.uncompressedSize,
|
|
151
|
+
data,
|
|
152
|
+
kind: signatureKind(entry.name),
|
|
153
|
+
referencedDocuments: getExtension(entry.name) === 'xml'
|
|
154
|
+
? collectXmlReferences(data, limits.maxXmlBytes).filter((reference) => documents.some((document) => document.name === reference))
|
|
155
|
+
: []
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
const signatureNames = new Set(signatureEntries.map((entry) => entry.name));
|
|
159
|
+
const metadata = metadataEntries
|
|
160
|
+
.filter((entry) => !signatureNames.has(entry.name))
|
|
161
|
+
.map((entry) => ({
|
|
162
|
+
name: entry.name,
|
|
163
|
+
compressedSize: entry.compressedSize,
|
|
164
|
+
uncompressedSize: entry.uncompressedSize,
|
|
165
|
+
mediaType: contentTypeForName(entry.name)
|
|
166
|
+
}));
|
|
167
|
+
const kind = mediaType === ASICS_MIME ? 'ASiC-S' : 'ASiC-E';
|
|
168
|
+
const warnings = [
|
|
169
|
+
'ZIP paths, entry sizes, compression ratios, CRCs and aggregate inflation were checked before extracted content was exposed.',
|
|
170
|
+
'No URI, certificate URL, revocation endpoint or package reference is fetched automatically.',
|
|
171
|
+
'Package parsing and member mapping do not establish certificate trust, policy compliance or legal validity.'
|
|
172
|
+
];
|
|
173
|
+
if (kind === 'ASiC-S' && documents.length !== 1)
|
|
174
|
+
warnings.push(`ASiC-S normally carries one document; this package contains ${documents.length}.`);
|
|
175
|
+
if (!signatures.length)
|
|
176
|
+
warnings.push('No signature or timestamp member was detected under META-INF.');
|
|
177
|
+
return {
|
|
178
|
+
kind,
|
|
179
|
+
mediaType,
|
|
180
|
+
sourceSize: bytes.byteLength,
|
|
181
|
+
entryCount: fileEntries.length,
|
|
182
|
+
totalUncompressedBytes: directory.totalUncompressedBytes,
|
|
183
|
+
documents,
|
|
184
|
+
signatures,
|
|
185
|
+
metadata,
|
|
186
|
+
warnings
|
|
187
|
+
};
|
|
188
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { type SignatureContainerLimits } from './limits.js';
|
|
2
|
+
import type { AsicInspection } from './types.js';
|
|
3
|
+
export declare class SignatureContainerWorkerClient {
|
|
4
|
+
private readonly workerFactory?;
|
|
5
|
+
private worker?;
|
|
6
|
+
private readonly pending;
|
|
7
|
+
constructor(workerFactory?: (() => Worker) | undefined);
|
|
8
|
+
private ensureWorker;
|
|
9
|
+
private failWorker;
|
|
10
|
+
inspectAsic(input: ArrayBuffer, requestedLimits?: Partial<SignatureContainerLimits>): Promise<AsicInspection>;
|
|
11
|
+
dispose(): void;
|
|
12
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { normalizeSignatureContainerLimits } from './limits.js';
|
|
2
|
+
let nextContainerRequestId = 0;
|
|
3
|
+
export class SignatureContainerWorkerClient {
|
|
4
|
+
constructor(workerFactory) {
|
|
5
|
+
this.workerFactory = workerFactory;
|
|
6
|
+
this.pending = new Map();
|
|
7
|
+
}
|
|
8
|
+
ensureWorker() {
|
|
9
|
+
if (this.worker)
|
|
10
|
+
return this.worker;
|
|
11
|
+
if (typeof Worker === 'undefined')
|
|
12
|
+
throw new Error('ASiC inspection requires Web Worker support.');
|
|
13
|
+
const worker = this.workerFactory
|
|
14
|
+
? this.workerFactory()
|
|
15
|
+
: new Worker(new URL('../container.worker.js', import.meta.url), {
|
|
16
|
+
type: 'module',
|
|
17
|
+
name: 'file-viewer-signature-container'
|
|
18
|
+
});
|
|
19
|
+
worker.addEventListener('message', (event) => {
|
|
20
|
+
const pending = this.pending.get(event.data.id);
|
|
21
|
+
if (!pending)
|
|
22
|
+
return;
|
|
23
|
+
clearTimeout(pending.timer);
|
|
24
|
+
this.pending.delete(event.data.id);
|
|
25
|
+
if (event.data.ok)
|
|
26
|
+
pending.resolve(event.data.result);
|
|
27
|
+
else
|
|
28
|
+
pending.reject(Object.assign(new Error(event.data.error.message), { code: event.data.error.code }));
|
|
29
|
+
});
|
|
30
|
+
worker.addEventListener('error', (event) => this.failWorker(new Error(event.message || 'ASiC Worker failed.')));
|
|
31
|
+
worker.addEventListener('messageerror', () => this.failWorker(new Error('ASiC Worker returned an unreadable response.')));
|
|
32
|
+
this.worker = worker;
|
|
33
|
+
return worker;
|
|
34
|
+
}
|
|
35
|
+
failWorker(error) {
|
|
36
|
+
var _a;
|
|
37
|
+
(_a = this.worker) === null || _a === void 0 ? void 0 : _a.terminate();
|
|
38
|
+
this.worker = undefined;
|
|
39
|
+
for (const pending of this.pending.values()) {
|
|
40
|
+
clearTimeout(pending.timer);
|
|
41
|
+
pending.reject(error);
|
|
42
|
+
}
|
|
43
|
+
this.pending.clear();
|
|
44
|
+
}
|
|
45
|
+
inspectAsic(input, requestedLimits) {
|
|
46
|
+
if (this.pending.size >= 2)
|
|
47
|
+
return Promise.reject(new Error('ASiC Worker request limit exceeded.'));
|
|
48
|
+
const limits = normalizeSignatureContainerLimits(requestedLimits);
|
|
49
|
+
if (input.byteLength === 0 || input.byteLength > limits.maxContainerBytes) {
|
|
50
|
+
return Promise.reject(new Error(`ASiC input exceeds the ${limits.maxContainerBytes}-byte boundary.`));
|
|
51
|
+
}
|
|
52
|
+
const worker = this.ensureWorker();
|
|
53
|
+
const id = `signature-container-${++nextContainerRequestId}`;
|
|
54
|
+
const transferable = input.slice(0);
|
|
55
|
+
return new Promise((resolve, reject) => {
|
|
56
|
+
const timer = setTimeout(() => {
|
|
57
|
+
this.failWorker(new DOMException('ASiC inspection timed out.', 'TimeoutError'));
|
|
58
|
+
}, limits.maxWorkerMs);
|
|
59
|
+
this.pending.set(id, { resolve, reject, timer });
|
|
60
|
+
worker.postMessage({
|
|
61
|
+
id,
|
|
62
|
+
type: 'inspect-asic',
|
|
63
|
+
input: transferable,
|
|
64
|
+
limits
|
|
65
|
+
}, [transferable]);
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
dispose() {
|
|
69
|
+
this.failWorker(new DOMException('ASiC Worker was terminated.', 'AbortError'));
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { SignatureContainerLimits } from './limits.js';
|
|
2
|
+
import type { AsicInspection } from './types.js';
|
|
3
|
+
export interface AsicWorkerRequest {
|
|
4
|
+
id: string;
|
|
5
|
+
type: 'inspect-asic';
|
|
6
|
+
input: ArrayBuffer;
|
|
7
|
+
limits: SignatureContainerLimits;
|
|
8
|
+
}
|
|
9
|
+
export type AsicWorkerResponse = {
|
|
10
|
+
id: string;
|
|
11
|
+
ok: true;
|
|
12
|
+
result: AsicInspection;
|
|
13
|
+
} | {
|
|
14
|
+
id: string;
|
|
15
|
+
ok: false;
|
|
16
|
+
error: {
|
|
17
|
+
code: 'invalid-input' | 'unsafe-archive' | 'internal-parser-error';
|
|
18
|
+
message: string;
|
|
19
|
+
};
|
|
20
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { InspectJwsOptions, JwsInspection } from './types.js';
|
|
2
|
+
export declare const isProbablyJws: (bytes: Uint8Array, filename?: string) => boolean;
|
|
3
|
+
export declare const inspectJws: (input: ArrayBuffer | Uint8Array | string, options?: InspectJwsOptions) => Promise<JwsInspection>;
|