diff --git a/src/minisign.ts b/src/minisign.ts index eadad8a..476e443 100644 --- a/src/minisign.ts +++ b/src/minisign.ts @@ -1,5 +1,25 @@ /** - * Ported from: https://github.com/mlugg/setup-zig/blob/main/main.js (MIT) + * Ported from: https://github.com/mlugg/setup-zig/blob/main/minisign.js + * + * Copyright Matthew Lugg + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. */ import sodium from "libsodium-wrappers"; @@ -9,6 +29,8 @@ export interface Key { key: Buffer; } +export const ready = sodium.ready; + // Parse a minisign key represented as a base64 string. // Throws exceptions on invalid keys. export function parseKey(keyString: string): Key { @@ -31,16 +53,19 @@ export interface Signature { algorithm: Buffer; keyID: Buffer; signature: Buffer; + trustedComment: Buffer; + globalSignature: Buffer; } // Parse a buffer containing the contents of a minisign signature file. // Throws exceptions on invalid signature files. export function parseSignature(sigBuf: Buffer): Signature { const untrustedHeader = Buffer.from("untrusted comment: "); + const trustedHeader = Buffer.from("trusted comment: "); // Validate untrusted comment header, and skip if (!sigBuf.subarray(0, untrustedHeader.byteLength).equals(untrustedHeader)) { - throw new Error("file format not recognised"); + throw new Error("invalid minisign signature: bad untrusted comment header"); } sigBuf = sigBuf.subarray(untrustedHeader.byteLength); @@ -57,36 +82,59 @@ export function parseSignature(sigBuf: Buffer): Signature { const keyID = sigInfo.subarray(2, 10); const signature = sigInfo.subarray(10); - // We don't look at the trusted comment or global signature, so we're done. + // Validate trusted comment header, and skip + if (!sigBuf.subarray(0, trustedHeader.byteLength).equals(trustedHeader)) { + throw new Error("invalid minisign signature: bad trusted comment header"); + } + sigBuf = sigBuf.subarray(trustedHeader.byteLength); + + // Read and skip trusted comment + const trustedCommentEnd = sigBuf.indexOf("\n"); + const trustedComment = sigBuf.subarray(0, trustedCommentEnd); + sigBuf = sigBuf.subarray(trustedCommentEnd + 1); + + // Read and skip global signature; handle missing trailing newline, just in case + let globalSigEnd = sigBuf.indexOf("\n"); + if (globalSigEnd === -1) globalSigEnd = sigBuf.length; + const globalSig = Buffer.from(sigBuf.subarray(0, globalSigEnd).toString(), "base64"); + sigBuf = sigBuf.subarray(sigInfoEnd + 1); // this might be length+1, but that's allowed + + // Validate that all data has been consumed + if (sigBuf.length !== 0) { + throw new Error("invalid minisign signature: trailing bytes"); + } return { algorithm: algorithm, keyID: keyID, signature: signature, + trustedComment: trustedComment, + globalSignature: globalSig, }; } -// Given a parsed key, parsed signature file, and raw file content, verifies the -// signature. Does not throw. Returns 'true' if the signature is valid for this -// file, 'false' otherwise. +// Given a parsed key, parsed signature file, and raw file content, verifies the signature, +// including the global signature (hence validating the trusted comment). Does not throw. +// Returns 'true' if the signature is valid for this file, 'false' otherwise. export function verifySignature(pubkey: Key, signature: Signature, fileContent: Buffer) { + if (!signature.keyID.equals(pubkey.id)) { + return false; + } + let signedContent; if (signature.algorithm.equals(Buffer.from("ED"))) { signedContent = sodium.crypto_generichash(sodium.crypto_generichash_BYTES_MAX, fileContent); } else { signedContent = fileContent; } - - if (!signature.keyID.equals(pubkey.id)) { + if (!sodium.crypto_sign_verify_detached(signature.signature, signedContent, pubkey.key)) { return false; } - if (!sodium.crypto_sign_verify_detached(signature.signature, signedContent, pubkey.key)) { + const globalSignedContent = Buffer.concat([signature.signature, signature.trustedComment]); + if (!sodium.crypto_sign_verify_detached(signature.globalSignature, globalSignedContent, pubkey.key)) { return false; } - // Since we don't use the trusted comment, we don't bother verifying the global signature. - // If we were to start using the trusted comment for any purpose, we must add this. - return true; } diff --git a/src/versionManager.ts b/src/versionManager.ts index 78d60ce..ba55ed9 100644 --- a/src/versionManager.ts +++ b/src/versionManager.ts @@ -209,22 +209,27 @@ async function installFromMirror( progress.report({ message: "Verifying Signature..." }); + await minisign.ready; + const signature = minisign.parseSignature(signatureData); if (!minisign.verifySignature(config.minisignKey, signature, artifactData)) { - try { - await vscode.workspace.fs.delete(installDir, { recursive: true, useTrash: false }); - } catch {} throw new Error(`signature verification failed for '${artifactUrl.toString()}'`); } + const match = /^timestamp:\d+\s+file:([^\s]+)\s+hashed$/.test(signature.trustedComment.toString()); + if (!match) { + throw new Error(`filename verification failed for '${artifactUrl.toString()}'`); + } + + progress.report({ message: "Extracting..." }); + try { await vscode.workspace.fs.delete(installDir, { recursive: true, useTrash: false }); } catch {} - await vscode.workspace.fs.createDirectory(installDir); - await vscode.workspace.fs.writeFile(tarballUri, artifactData); - progress.report({ message: "Extracting..." }); try { + await vscode.workspace.fs.createDirectory(installDir); + await vscode.workspace.fs.writeFile(tarballUri, artifactData); await execFile(tarPath, ["-xf", tarballUri.fsPath, "-C", installDir.fsPath].concat(config.extraTarArgs), { signal: abortController.signal, timeout: 60000, // 60 seconds