CH-J Server Managerspráva serverů přes SSH
Menu
Vydané zdroje

CH-J Server Manager

Procházejte adresáře a soubory konkrétního vydání aplikace.

Stáhnout zdroje ZIP
CH-J Proprietary Software License 1.14

Zdroje jsou zveřejněny pod CH-J Proprietary Software License 1.14. Jejich dostupnost nemění licenční podmínky ani neposkytuje další oprávnění.

10,7 KB · 296 řádkůStáhnout soubor
1"use strict";
3const fs = require("node:fs");
4const { Readable } = require("node:stream");
5const openpgp = require("openpgp");
7const TRUSTED_PRIMARY_FINGERPRINT = "0D92778AD8ECF85C80E3924848F2433AD9CDF453";
8const MAX_PUBLIC_KEY_BYTES = 128 * 1024;
9const MAX_SIGNATURE_BYTES = 128 * 1024;
10const DEFAULT_VERIFICATION_TIMEOUT_MS = 2 * 60 * 1000;
12class SignatureVerificationError extends Error {
13 constructor(message, code = "UPDATE_SIGNATURE_INVALID", options = {}) {
14 super(message, options);
15 this.name = "SignatureVerificationError";
16 this.code = code;
17 }
20function normalizeFingerprint(value) {
21 return String(value || "").replace(/[^0-9a-f]/gi, "").toUpperCase();
24function assertRegularFile(filePath, label, maxBytes) {
25 let stat;
26 try {
27 stat = fs.lstatSync(filePath);
28 } catch (error) {
29 throw new SignatureVerificationError(`${label} is missing.`, "UPDATE_SIGNATURE_FILE_MISSING", { cause: error });
30 }
31 if (stat.isSymbolicLink() || !stat.isFile()) {
32 throw new SignatureVerificationError(`${label} must be a regular file.`, "UPDATE_SIGNATURE_UNSAFE_FILE");
33 }
34 if (stat.size <= 0) {
35 throw new SignatureVerificationError(`${label} is empty.`, "UPDATE_SIGNATURE_EMPTY");
36 }
37 if (maxBytes && stat.size > maxBytes) {
38 throw new SignatureVerificationError(`${label} is too large.`, "UPDATE_SIGNATURE_TOO_LARGE");
39 }
40 return stat;
43function withTimeout(operation, timeoutMs, onTimeout) {
44 let timer;
45 const timeout = new Promise((_, reject) => {
46 timer = setTimeout(() => {
47 try { onTimeout?.(); } catch {}
48 reject(new SignatureVerificationError(
49 "OpenPGP verification timed out.",
50 "UPDATE_SIGNATURE_TIMEOUT"
51 ));
52 }, timeoutMs);
53 timer.unref?.();
54 });
55 return Promise.race([Promise.resolve(operation), timeout]).finally(() => clearTimeout(timer));
58function packetFromVerificationResult(signatureResult) {
59 return Promise.resolve(signatureResult.signature).then((signature) => signature?.packets?.[0] || null);
62class OpenPgpVerifier {
63 constructor(options = {}) {
64 this.publicKeyPath = options.publicKeyPath;
65 this.trustedPrimaryFingerprint = normalizeFingerprint(
66 options.trustedPrimaryFingerprint || TRUSTED_PRIMARY_FINGERPRINT
67 );
68 this.logger = options.logger;
69 this.verificationTimeoutMs = Number(options.verificationTimeoutMs || DEFAULT_VERIFICATION_TIMEOUT_MS);
70 this.openpgp = options.openpgpImpl || openpgp;
72 if (!this.publicKeyPath) throw new Error("A bundled OpenPGP public key path is required.");
73 if (!/^[0-9A-F]{40}$/.test(this.trustedPrimaryFingerprint)) {
74 throw new Error("The trusted primary OpenPGP fingerprint is invalid.");
75 }
76 if (!Number.isFinite(this.verificationTimeoutMs) || this.verificationTimeoutMs <= 0) {
77 throw new Error("The OpenPGP verification timeout is invalid.");
78 }
79 }
81 async verifyFile(artifactPath, signaturePath) {
82 this.logger?.info("OpenPGP update verification started.", {
83 artifact: String(artifactPath || "").split(/[\\/]/).pop() || null
84 });
85 let source;
86 try {
87 assertRegularFile(artifactPath, "Update artifact");
88 assertRegularFile(signaturePath, "Detached OpenPGP signature", MAX_SIGNATURE_BYTES);
89 assertRegularFile(this.publicKeyPath, "Bundled OpenPGP public key", MAX_PUBLIC_KEY_BYTES);
91 const operation = (async () => {
92 const armoredKey = fs.readFileSync(this.publicKeyPath, "utf8");
93 let publicKey;
94 try {
95 publicKey = await this.openpgp.readKey({ armoredKey });
96 } catch (error) {
97 throw new SignatureVerificationError(
98 "Bundled OpenPGP public key is malformed.",
99 "UPDATE_PUBLIC_KEY_INVALID",
100 { cause: error }
101 );
102 }
103 if (publicKey.isPrivate?.()) {
104 throw new SignatureVerificationError("Bundled OpenPGP key contains private key material.");
105 }
107 const actualPrimaryFingerprint = normalizeFingerprint(publicKey.getFingerprint());
108 this.logger?.info("Bundled OpenPGP public key loaded.", {
109 primaryFingerprint: actualPrimaryFingerprint,
110 trustedPrimaryFingerprint: this.trustedPrimaryFingerprint
111 });
112 if (actualPrimaryFingerprint !== this.trustedPrimaryFingerprint) {
113 throw new SignatureVerificationError(
114 "Bundled OpenPGP primary fingerprint does not match the trust anchor.",
115 "UPDATE_PUBLIC_KEY_FINGERPRINT_MISMATCH"
116 );
117 }
119 const verificationDate = new Date();
120 try {
121 await publicKey.verifyPrimaryKey(verificationDate);
122 } catch (error) {
123 throw new SignatureVerificationError(
124 "Bundled OpenPGP primary key is revoked, expired, or invalid.",
125 "UPDATE_PUBLIC_KEY_INVALID",
126 { cause: error }
127 );
128 }
129 this.logger?.info("Trusted OpenPGP primary fingerprint verified.", {
130 trustedPrimaryFingerprint: this.trustedPrimaryFingerprint
131 });
133 const armoredSignature = fs.readFileSync(signaturePath, "utf8");
134 let signature;
135 try {
136 signature = await this.openpgp.readSignature({ armoredSignature });
137 } catch (error) {
138 throw new SignatureVerificationError(
139 "Detached OpenPGP signature is malformed.",
140 "UPDATE_SIGNATURE_MALFORMED",
141 { cause: error }
142 );
143 }
145 source = fs.createReadStream(artifactPath);
146 const message = await this.openpgp.createMessage({ binary: Readable.toWeb(source) });
147 let verification;
148 try {
149 verification = await this.openpgp.verify({
150 message,
151 signature,
152 verificationKeys: publicKey,
153 expectSigned: true,
154 format: "binary",
155 date: verificationDate
156 });
157 } catch (error) {
158 throw new SignatureVerificationError(
159 "Detached OpenPGP signature verification failed.",
160 "UPDATE_SIGNATURE_INVALID",
161 { cause: error }
162 );
163 }
165 if (!Array.isArray(verification.signatures) || verification.signatures.length === 0) {
166 throw new SignatureVerificationError("Detached OpenPGP signature has no signer.");
167 }
169 // OpenPGP.js verifies streaming messages while their data is consumed.
170 try {
171 for await (const _chunk of verification.data) {}
172 } catch (error) {
173 throw new SignatureVerificationError(
174 "Detached OpenPGP signature is not cryptographically valid.",
175 "UPDATE_SIGNATURE_INVALID",
176 { cause: error }
177 );
178 }
180 const signers = [];
181 for (const result of verification.signatures) {
182 try {
183 await result.verified;
184 } catch (error) {
185 throw new SignatureVerificationError(
186 "Detached OpenPGP signature is not cryptographically valid.",
187 "UPDATE_SIGNATURE_INVALID",
188 { cause: error }
189 );
190 }
192 let signingKey;
193 try {
194 signingKey = await publicKey.getSigningKey(result.keyID, verificationDate);
195 } catch (error) {
196 throw new SignatureVerificationError(
197 "The signature was not made by a valid signing subkey of the trusted primary key.",
198 "UPDATE_SIGNATURE_UNTRUSTED_SIGNER",
199 { cause: error }
200 );
201 }
203 const signingFingerprint = normalizeFingerprint(signingKey.getFingerprint());
204 if (!signingFingerprint || signingFingerprint === actualPrimaryFingerprint) {
205 throw new SignatureVerificationError(
206 "Updates must be signed by a valid signing subkey of the trusted primary key.",
207 "UPDATE_SIGNATURE_NOT_SIGNING_SUBKEY"
208 );
209 }
211 const matchingSubkeys = publicKey.getSubkeys(result.keyID);
212 const signingSubkey = matchingSubkeys.find(
213 (subkey) => normalizeFingerprint(subkey.getFingerprint()) === signingFingerprint
214 );
215 if (!signingSubkey) {
216 throw new SignatureVerificationError(
217 "The signing subkey is not bound to the trusted primary key.",
218 "UPDATE_SIGNATURE_UNTRUSTED_SIGNER"
219 );
220 }
222 let bindingSignature;
223 try {
224 bindingSignature = await signingSubkey.verify(verificationDate);
225 } catch (error) {
226 throw new SignatureVerificationError(
227 "The signing subkey is revoked, expired, or has an invalid binding.",
228 "UPDATE_SIGNING_SUBKEY_INVALID",
229 { cause: error }
230 );
231 }
232 const keyFlags = bindingSignature?.keyFlags || [];
233 const hasSigningCapability = [...keyFlags].some(
234 (flags) => (flags & this.openpgp.enums.keyFlags.signData) !== 0
235 );
236 if (!hasSigningCapability) {
237 throw new SignatureVerificationError(
238 "The OpenPGP subkey does not have signing capability.",
239 "UPDATE_SIGNING_SUBKEY_CAPABILITY_INVALID"
240 );
241 }
243 const signaturePacket = await packetFromVerificationResult(result);
244 signers.push({
245 fingerprint: signingFingerprint,
246 createdAt: signaturePacket?.created instanceof Date
247 ? signaturePacket.created.toISOString()
248 : null
249 });
250 }
252 this.logger?.info("OpenPGP update signature verified.", {
253 result: "valid",
254 trustedPrimaryFingerprint: this.trustedPrimaryFingerprint,
255 signingFingerprints: signers.map((signer) => signer.fingerprint)
256 });
257 return {
258 valid: true,
259 primaryFingerprint: actualPrimaryFingerprint,
260 signingFingerprints: signers.map((signer) => signer.fingerprint),
261 signatureCreatedAt: signers[0]?.createdAt || null
262 };
263 })();
265 return await withTimeout(operation, this.verificationTimeoutMs, () => source?.destroy());
266 } catch (error) {
267 const failure = error instanceof SignatureVerificationError
268 ? error
269 : new SignatureVerificationError(
270 "OpenPGP verification failed unexpectedly.",
271 "UPDATE_SIGNATURE_ERROR",
272 { cause: error }
273 );
274 this.logger?.warn("OpenPGP update verification failed.", {
275 result: "invalid",
276 code: failure.code,
277 reason: failure.message,
278 technicalReason: failure.cause?.message || null,
279 trustedPrimaryFingerprint: this.trustedPrimaryFingerprint
280 });
281 throw failure;
282 } finally {
283 source?.destroy();
284 }
285 }
288module.exports = {
289 DEFAULT_VERIFICATION_TIMEOUT_MS,
290 MAX_PUBLIC_KEY_BYTES,
291 MAX_SIGNATURE_BYTES,
292 OpenPgpVerifier,
293 SignatureVerificationError,
294 TRUSTED_PRIMARY_FINGERPRINT,
295 normalizeFingerprint
296};

SHA-256: 145a1fb7fabb0ffbfda72fd7503c6583c1e061dd9e0080a1c4c24961d2fce299

SHA-256 archivu: 5ac91caf4fa32a6fdb114f2430deed486fbe7489d5eea343d1f034169fafb5e0