1 | "use strict";
|
2 |
|
3 | Object.defineProperty(exports, "__esModule", {
|
4 | value: true
|
5 | });
|
6 | exports.Signature = void 0;
|
7 | var _plistDom = require("@shockpkg/plist-dom");
|
8 | var _timestamper = require("./security/timestamper.js");
|
9 | var _sha = require("./hasher/sha1.js");
|
10 | var _sha2 = require("./hasher/sha256.js");
|
11 | const templates = [['certificate', '<X509Certificate>{0}</X509Certificate>'], ['crl', '<X509CRL>{0}</X509CRL>'], ['fileReference', ['<Reference URI="{0}">', '<DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"></DigestMethod>', '<DigestValue>{1}</DigestValue>', '</Reference>'].join('')], ['packageManifest', '<Manifest xmlns="http://www.w3.org/2000/09/xmldsig#" Id="PackageContents">{0}</Manifest>'], ['PackageSignature', ['<signatures>', ' <Signature xmlns="http://www.w3.org/2000/09/xmldsig#" Id="PackageSignature">', ' <SignedInfo>', ' <CanonicalizationMethod Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315"/>', ' <SignatureMethod Algorithm="http://www.w3.org/TR/xmldsig-core#rsa-sha1"/>', ' <Reference URI="#PackageContents">', ' <Transforms>', ' <Transform Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315"/>', ' </Transforms>', ' <DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>', ' <DigestValue>{0}</DigestValue>', ' </Reference>', ' </SignedInfo>',
|
12 |
|
13 | ' <SignatureValue Id="PackageSignatureValue">{1}</SignatureValue>', ' <KeyInfo>', ' <X509Data>', ' {2}', ' </X509Data>', ' </KeyInfo>', ' <Object>', ' <Manifest Id="PackageContents">', ' {3}', ' </Manifest>', ' </Object>', ' {4}', ' </Signature>', '</signatures>', ''].join('\n')], ['SignedInfo', ['<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">', '<CanonicalizationMethod Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315"></CanonicalizationMethod>', '<SignatureMethod Algorithm="http://www.w3.org/TR/xmldsig-core#rsa-sha1"></SignatureMethod>', '<Reference URI="#PackageContents">', '<Transforms>', '<Transform Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315"></Transform>', '</Transforms>', '<DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"></DigestMethod>', '<DigestValue>{0}</DigestValue>', '</Reference>', '</SignedInfo>'].join('')], ['timestamp', ['<Object xmlns:xades="http://uri.etsi.org/01903/v1.1.1#" > ', ' <xades:QualifyingProperties>', ' <xades:UnsignedProperties > ', ' <xades:UnsignedSignatureProperties>', ' <xades:SignatureTimeStamp>', ' \t <xades:HashDataInfo uri="{0}">', ' \t <Transforms>', ' \t <Transform Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315"/>', ' </Transforms>', ' <xades:EncapsulatedTimeStamp>', ' {1}', ' </xades:EncapsulatedTimeStamp> \t', ' \t </xades:HashDataInfo> \t', ' </xades:SignatureTimeStamp>', ' </xades:UnsignedSignatureProperties> ', ' </xades:UnsignedProperties>', ' </xades:QualifyingProperties>', '</Object>'].join('\n')], ['SignatureValue', '<SignatureValue xmlns="http://www.w3.org/2000/09/xmldsig#" Id="PackageSignatureValue">{0}</SignatureValue>']];
|
14 |
|
15 |
|
16 |
|
17 |
|
18 | class Signature {
|
19 | |
20 |
|
21 |
|
22 | certificate = null;
|
23 |
|
24 | |
25 |
|
26 |
|
27 | privateKey = null;
|
28 |
|
29 | |
30 |
|
31 |
|
32 | timestampUrl = null;
|
33 |
|
34 | |
35 |
|
36 |
|
37 | timestampUriSignature = false;
|
38 |
|
39 | |
40 |
|
41 |
|
42 | timestampUriPackage = true;
|
43 |
|
44 | |
45 |
|
46 |
|
47 | _templates = new Map(templates);
|
48 |
|
49 | |
50 |
|
51 |
|
52 | _packageManifest = [];
|
53 |
|
54 | |
55 |
|
56 |
|
57 | _manifestDigest = null;
|
58 |
|
59 | |
60 |
|
61 |
|
62 | _signedInfo = null;
|
63 |
|
64 | |
65 |
|
66 |
|
67 | _signature = null;
|
68 |
|
69 | |
70 |
|
71 |
|
72 | _keyInfo = null;
|
73 |
|
74 | |
75 |
|
76 |
|
77 | _timestamp = null;
|
78 |
|
79 | |
80 |
|
81 |
|
82 | constructor() {}
|
83 |
|
84 | |
85 |
|
86 |
|
87 | defaults() {
|
88 | this.certificate = null;
|
89 | this.privateKey = null;
|
90 | this.timestampUrl = null;
|
91 | this.timestampUriSignature = false;
|
92 | this.timestampUriPackage = true;
|
93 | }
|
94 |
|
95 | |
96 |
|
97 |
|
98 | reset() {
|
99 | this._packageManifest = [];
|
100 | this._manifestDigest = null;
|
101 | this._signedInfo = null;
|
102 | this._signature = null;
|
103 | this._keyInfo = null;
|
104 | this._timestamp = null;
|
105 | }
|
106 |
|
107 | |
108 |
|
109 |
|
110 |
|
111 |
|
112 |
|
113 | addFile(uri, data) {
|
114 | if (this._signedInfo || this._manifestDigest) {
|
115 | throw new Error('Cannot call after: digest');
|
116 | }
|
117 | const digestB64 = this._base64Encode(this._hashSha256(data));
|
118 |
|
119 |
|
120 | const uriEncoded = uri.replace(/&/g, '&');
|
121 | this._packageManifest.push(this._templated('fileReference', [uriEncoded, digestB64]));
|
122 | }
|
123 |
|
124 | |
125 |
|
126 |
|
127 | digest() {
|
128 | if (this._signedInfo || this._manifestDigest) {
|
129 | throw new Error('Already called');
|
130 | }
|
131 | const manifest = this._templated('packageManifest', [this._packageManifest.join('')]);
|
132 | const digest = this._hashSha256(new TextEncoder().encode(manifest));
|
133 | const signed = this._templated('SignedInfo', [this._base64Encode(digest)]);
|
134 | this._manifestDigest = digest;
|
135 | this._signedInfo = signed;
|
136 | }
|
137 |
|
138 | |
139 |
|
140 |
|
141 | sign() {
|
142 | if (this._signature || this._keyInfo !== null) {
|
143 | throw new Error('Already called');
|
144 | }
|
145 | const signedInfo = this._signedInfo;
|
146 | if (!signedInfo) {
|
147 | throw new Error('Must call after: digest');
|
148 | }
|
149 | const {
|
150 | privateKey: keyPrivate
|
151 | } = this;
|
152 | if (!keyPrivate) {
|
153 | throw new Error('Private key not set');
|
154 | }
|
155 | const keyInfo = this._buildKeyInfo();
|
156 | const signature = keyPrivate.sign(new TextEncoder().encode(signedInfo), 'sha1');
|
157 | this._signature = signature;
|
158 | this._keyInfo = keyInfo;
|
159 | }
|
160 |
|
161 | |
162 |
|
163 |
|
164 | async timestamp() {
|
165 | if (this._timestamp) {
|
166 | throw new Error('Already called');
|
167 | }
|
168 | const signature = this._signature;
|
169 | if (!signature) {
|
170 | throw new Error('Must call after: sign');
|
171 | }
|
172 | const {
|
173 | timestampUrl
|
174 | } = this;
|
175 | if (!timestampUrl) {
|
176 | throw new Error('Timestamp URL not set');
|
177 | }
|
178 | const message = this._templated('SignatureValue', [this._base64Encode(signature)]);
|
179 | const timestamper = this._createSecurityTimestamper(timestampUrl);
|
180 | const timestamp = await timestamper.timestamp(this._hashSha1(new TextEncoder().encode(message)), 'sha1');
|
181 | this._timestamp = timestamp;
|
182 | }
|
183 |
|
184 | |
185 |
|
186 |
|
187 |
|
188 |
|
189 | encode() {
|
190 | const signature = this._signature;
|
191 | if (!signature) {
|
192 | throw new Error('Must call after: sign');
|
193 | }
|
194 | const manifestDigest = this._manifestDigest;
|
195 | const keyInfo = this._keyInfo;
|
196 | if (!manifestDigest || keyInfo === null) {
|
197 | throw new Error('Internal error');
|
198 | }
|
199 | const timestamp = this._timestamp ? this._createTimestampXml() : '';
|
200 | return new TextEncoder().encode(this._templated('PackageSignature', [this._base64Encode(manifestDigest), this._base64Encode(signature), keyInfo, this._packageManifest.join(''), timestamp]));
|
201 | }
|
202 |
|
203 | |
204 |
|
205 |
|
206 |
|
207 |
|
208 | _getTimestampDataReferenceUris() {
|
209 | const r = [];
|
210 | if (this.timestampUriSignature) {
|
211 | r.push('SignatureValue');
|
212 | }
|
213 | if (this.timestampUriPackage) {
|
214 | r.push('#PackageSignatureValue');
|
215 | }
|
216 | return r;
|
217 | }
|
218 |
|
219 | |
220 |
|
221 |
|
222 |
|
223 |
|
224 |
|
225 |
|
226 | _templated(name, values) {
|
227 | const template = this._templates.get(name);
|
228 | if (!template) {
|
229 | throw new Error(`Unknown template name: ${name}`);
|
230 | }
|
231 | return template.replace(/\{(\d+)\}/g, (str, index) => {
|
232 | const i = +index;
|
233 | if (i >= values.length) {
|
234 | throw new Error(`Index out of range: ${i} > ${values.length}`);
|
235 | }
|
236 | return values[i];
|
237 | });
|
238 | }
|
239 |
|
240 | |
241 |
|
242 |
|
243 |
|
244 |
|
245 |
|
246 | _createSecurityTimestamper(url) {
|
247 | return new _timestamper.SecurityTimestamper(url);
|
248 | }
|
249 |
|
250 | |
251 |
|
252 |
|
253 |
|
254 |
|
255 | _createHasherSha1() {
|
256 | return new _sha.HasherSha1();
|
257 | }
|
258 |
|
259 | |
260 |
|
261 |
|
262 |
|
263 |
|
264 | _createHasherSha256() {
|
265 | return new _sha2.HasherSha256();
|
266 | }
|
267 |
|
268 | |
269 |
|
270 |
|
271 |
|
272 |
|
273 |
|
274 | _hashSha1(data) {
|
275 | const hasher = this._createHasherSha1();
|
276 | hasher.update(data);
|
277 | return hasher.digest();
|
278 | }
|
279 |
|
280 | |
281 |
|
282 |
|
283 |
|
284 |
|
285 |
|
286 | _hashSha256(data) {
|
287 | const hasher = this._createHasherSha256();
|
288 | hasher.update(data);
|
289 | return hasher.digest();
|
290 | }
|
291 |
|
292 | |
293 |
|
294 |
|
295 |
|
296 |
|
297 |
|
298 |
|
299 |
|
300 | _base64Encode(data, chunk = 76, delimit = '\n') {
|
301 | const chunks = [];
|
302 | for (let b64 = (0, _plistDom.base64Encode)(data); b64; b64 = b64.substring(chunk)) {
|
303 | chunks.push(b64.substring(0, chunk));
|
304 | }
|
305 | return chunks.join(delimit);
|
306 | }
|
307 |
|
308 | |
309 |
|
310 |
|
311 |
|
312 |
|
313 | _createTimestampXml() {
|
314 | const timestamp = this._timestamp;
|
315 | if (!timestamp) {
|
316 | throw new Error('Internal error');
|
317 | }
|
318 | const timestampBase64 = this._base64Encode(timestamp);
|
319 | const result = [];
|
320 | for (const uri of this._getTimestampDataReferenceUris()) {
|
321 | result.push(this._templated('timestamp', [uri, timestampBase64]));
|
322 | }
|
323 | return result.join('\n');
|
324 | }
|
325 |
|
326 | |
327 |
|
328 |
|
329 |
|
330 |
|
331 | _buildKeyInfo() {
|
332 | const {
|
333 | certchain,
|
334 | crlValidationCerts,
|
335 | crls
|
336 | } = this._buildAndVerifyCertChain();
|
337 | const out = [];
|
338 | for (const data of certchain) {
|
339 | out.push(this._templated('certificate', [this._base64Encode(data)]));
|
340 | }
|
341 | if (crls.length) {
|
342 | for (const data of crlValidationCerts) {
|
343 | out.push(this._templated('certificate', [this._base64Encode(data)]));
|
344 | }
|
345 | }
|
346 | for (const data of crls) {
|
347 | out.push(this._templated('crl', [this._base64Encode(data)]));
|
348 | }
|
349 | return out.join('');
|
350 | }
|
351 |
|
352 | |
353 |
|
354 |
|
355 |
|
356 |
|
357 | _buildAndVerifyCertChain() {
|
358 | const {
|
359 | certificate
|
360 | } = this;
|
361 | if (!certificate) {
|
362 | throw new Error('Certificate not set');
|
363 | }
|
364 |
|
365 |
|
366 | const certchain = [];
|
367 | const crlValidationCerts = [];
|
368 | const crls = [];
|
369 |
|
370 |
|
371 | certchain.push(certificate.encodeCertchain());
|
372 | return {
|
373 | certchain,
|
374 | crlValidationCerts,
|
375 | crls
|
376 | };
|
377 | }
|
378 | }
|
379 | exports.Signature = Signature;
|
380 |
|
\ | No newline at end of file |