@wpelevator/wp-release 0.4.0 → 0.4.1
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/README.md +2 -2
- package/dist/commands/verify.js +92 -46
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -134,7 +134,7 @@ wp-release verify wp-content/plugins/my-plugin --checksums my-plugin.1.2.0.check
|
|
|
134
134
|
- Manifest paths outside the package folder (absolute or with `..`) are rejected.
|
|
135
135
|
- `--zip <file|url>` checks the files of a ZIP instead of a directory. The ZIP can be a URL, such as the one that WordPress.org builds for a release, and its single root folder is found automatically (or set `--root-folder`). The ZIP is extracted to a temporary directory after the same safety checks as `deploy-svn --from-zip`.
|
|
136
136
|
- The version in the manifest must match the `Version` header of the checked files, so that an older release with its own valid manifest can't pass as a newer one.
|
|
137
|
-
- A manifest can't list itself. A copy of the manifest and of its `.sig` inside the checked files, at the root with the same names as the `--checksums` file or at the path of `--embedded`, is skipped when comparing files, but it must be identical to the `--checksums` file. See [Manifest inside the plugin](#manifest-inside-the-plugin).
|
|
137
|
+
- A manifest can't list itself. A copy of the manifest and of its `.sig` inside the checked files, at the root with the same names as the `--checksums` file or at the path of `--embedded`, is skipped when comparing files, but it must be identical to the `--checksums` file and its `.sig`. A path that the manifest lists is never skipped, and a file that isn't a matching copy is reported as not in the checksums. See [Manifest inside the plugin](#manifest-inside-the-plugin).
|
|
138
138
|
|
|
139
139
|
A checksums manifest proves only that the files match the manifest. With `--public-key <key>`, `verify` first checks `<checksums>.sig` against the base64 Ed25519 public key and fails with exit code `2` before comparing any file when the signature is missing or invalid, so that the manifest itself can be trusted. See [Sign](#sign).
|
|
140
140
|
|
|
@@ -158,7 +158,7 @@ wp-release deploy-svn build --trunk --svn-tag 1.2.0
|
|
|
158
158
|
--checksums build/checksums.json --public-key "$PUBLIC_KEY"
|
|
159
159
|
```
|
|
160
160
|
|
|
161
|
-
- The manifest can have any name and location, for example `--output build/includes/signatures/checksums.json`, and missing folders are created. Tell `verify` where the shipped copy is with `--embedded <path>`, like `--embedded includes/signatures/checksums.json`.
|
|
161
|
+
- The manifest can have any name and location, for example `--output build/includes/signatures/checksums.json`, and missing folders are created. Tell `verify` where the shipped copy is with `--embedded <path>`, like `--embedded includes/signatures/checksums.json`. Both the `--checksums` file and its `.sig` must then exist, both copies must be shipped, and they're skipped when comparing files and must be identical to the originals. The manifest can't list the embedded path. Without `--embedded`, `verify` looks for the manifest name at the root, and skips a file only when it exists, the matching original exists and the manifest doesn't list it. Otherwise it's checked like any other file, so a listed file that has the name of the manifest can't be deleted unnoticed, and a shipped `.sig` without an original is reported as not in the checksums.
|
|
162
162
|
|
|
163
163
|
The public key has to come from somewhere other than the plugin that it checks, because someone who can overwrite the plugin files can also replace a key and a signature inside them. A copy of the manifest also doesn't prevent a downgrade to an older release that was signed too, so the installing side has to enforce a minimum version.
|
|
164
164
|
|
package/dist/commands/verify.js
CHANGED
|
@@ -99,33 +99,20 @@ function getEmbeddedPath(checksumsPath, embedded) {
|
|
|
99
99
|
(isAbsolute(embedded) ||
|
|
100
100
|
embedded.includes('\\') ||
|
|
101
101
|
path.startsWith('../') ||
|
|
102
|
-
'.'
|
|
102
|
+
['.', '..'].includes(path))) {
|
|
103
103
|
throw new ValidationError(`--embedded must be a path inside the plugin with forward slashes, got "${embedded}".`);
|
|
104
104
|
}
|
|
105
105
|
return embedded || path.endsWith('.json') ? path : null;
|
|
106
106
|
}
|
|
107
107
|
/**
|
|
108
|
-
*
|
|
109
|
-
* directory with the given ones. A release that ships the manifest and its
|
|
110
|
-
* signature with the plugin files must ship the same bytes that were
|
|
111
|
-
* signed, otherwise a verifier on the site would check something else. The
|
|
112
|
-
* copies are not part of the manifest, since a manifest can't list itself,
|
|
113
|
-
* so they're skipped when comparing the files.
|
|
114
|
-
*
|
|
115
|
-
* Without an explicit path, the copies are expected at the root with the
|
|
116
|
-
* name of the manifest, and are checked only when they exist. With an
|
|
117
|
-
* explicit path they must exist.
|
|
108
|
+
* Get the manifest and signature pairs to look for inside the checked files.
|
|
118
109
|
*
|
|
119
|
-
* @param
|
|
120
|
-
* @param
|
|
121
|
-
* @
|
|
122
|
-
* @param input.embedded Explicit path of the embedded manifest.
|
|
123
|
-
* @return The copies that were found to differ or be missing, and their paths.
|
|
110
|
+
* @param checksumsPath Manifest path.
|
|
111
|
+
* @param path Relative path of the embedded manifest, if any.
|
|
112
|
+
* @return Pairs of the originals and their copies.
|
|
124
113
|
*/
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
const path = getEmbeddedPath(checksumsPath, embedded);
|
|
128
|
-
const pairs = path
|
|
114
|
+
function getCopyPairs(checksumsPath, path) {
|
|
115
|
+
return path
|
|
129
116
|
? [
|
|
130
117
|
{ original: checksumsPath, copy: path },
|
|
131
118
|
{
|
|
@@ -134,28 +121,82 @@ async function checkEmbeddedCopies(input) {
|
|
|
134
121
|
},
|
|
135
122
|
]
|
|
136
123
|
: [];
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Check that an explicit --embedded path can be verified: the manifest can't
|
|
127
|
+
* list its own copies, and the originals to compare with must exist.
|
|
128
|
+
*
|
|
129
|
+
* @param pairs Pairs of the originals and their copies.
|
|
130
|
+
* @param manifest Checksums manifest.
|
|
131
|
+
* @throws {ValidationError} If a copy is listed or an original is missing.
|
|
132
|
+
*/
|
|
133
|
+
function checkExplicitPairs(pairs, manifest) {
|
|
134
|
+
pairs.forEach(({ original, copy }) => {
|
|
135
|
+
if (Object.hasOwn(manifest.files, copy)) {
|
|
136
|
+
throw new ValidationError(`The checksums list ${copy}, but a manifest can't list its own embedded copy.`);
|
|
137
|
+
}
|
|
138
|
+
if (!existsSync(original)) {
|
|
139
|
+
throw new ValidationError(`--embedded needs ${original} to compare ${copy} with, but it doesn't exist.`);
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Check that two paths are the same file or have the same bytes.
|
|
145
|
+
*
|
|
146
|
+
* @param original Path of the original.
|
|
147
|
+
* @param file Path of the copy.
|
|
148
|
+
* @return Whether the contents are identical.
|
|
149
|
+
*/
|
|
150
|
+
async function hasSameContents(original, file) {
|
|
151
|
+
return (resolve(original) === resolve(file) ||
|
|
152
|
+
(await readFile(original)).equals(await readFile(file)));
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Compare the copies of the manifest and its signature inside the checked
|
|
156
|
+
* directory with the given ones. A release that ships the manifest and its
|
|
157
|
+
* signature with the plugin files must ship the same bytes that were
|
|
158
|
+
* signed, otherwise a verifier on the site would check something else. The
|
|
159
|
+
* copies are not part of the manifest, since a manifest can't list itself,
|
|
160
|
+
* so they're skipped when comparing the files.
|
|
161
|
+
*
|
|
162
|
+
* With an explicit path both originals and both copies must exist. Without
|
|
163
|
+
* one, the copies are expected at the root with the name of the manifest,
|
|
164
|
+
* and a file is treated as a copy only when it exists, the matching
|
|
165
|
+
* original exists and the manifest doesn't list it. Any other file is
|
|
166
|
+
* checked like the rest of the files.
|
|
167
|
+
*
|
|
168
|
+
* @param input Directory, manifest and embedded path.
|
|
169
|
+
* @param input.dir Directory to check.
|
|
170
|
+
* @param input.manifest Checksums manifest.
|
|
171
|
+
* @param input.checksumsPath Manifest path.
|
|
172
|
+
* @param input.embedded Explicit path of the embedded manifest.
|
|
173
|
+
* @return The copies that were found to differ or be missing, and the paths of the existing copies.
|
|
174
|
+
* @throws {ValidationError} If an explicit path can't be verified.
|
|
175
|
+
*/
|
|
176
|
+
async function checkEmbeddedCopies(input) {
|
|
177
|
+
const { dir, manifest, checksumsPath, embedded } = input;
|
|
178
|
+
const pairs = getCopyPairs(checksumsPath, getEmbeddedPath(checksumsPath, embedded));
|
|
179
|
+
if (embedded) {
|
|
180
|
+
checkExplicitPairs(pairs, manifest);
|
|
181
|
+
}
|
|
182
|
+
const found = pairs.filter(({ original, copy }) => existsSync(join(dir, copy)) &&
|
|
183
|
+
(embedded ||
|
|
184
|
+
(existsSync(original) &&
|
|
185
|
+
!Object.hasOwn(manifest.files, copy))));
|
|
186
|
+
const results = await Promise.all(found.map(async ({ original, copy }) => ({
|
|
187
|
+
copy,
|
|
188
|
+
isSame: await hasSameContents(original, join(dir, copy)),
|
|
189
|
+
})));
|
|
151
190
|
return {
|
|
152
|
-
paths:
|
|
191
|
+
paths: found.map(({ copy }) => copy),
|
|
153
192
|
differing: results
|
|
154
|
-
.filter((
|
|
155
|
-
.map((
|
|
156
|
-
missing:
|
|
157
|
-
|
|
158
|
-
|
|
193
|
+
.filter(({ isSame }) => !isSame)
|
|
194
|
+
.map(({ copy }) => copy),
|
|
195
|
+
missing: embedded
|
|
196
|
+
? pairs
|
|
197
|
+
.filter(({ copy }) => !existsSync(join(dir, copy)))
|
|
198
|
+
.map(({ copy }) => copy)
|
|
199
|
+
: [],
|
|
159
200
|
};
|
|
160
201
|
}
|
|
161
202
|
/**
|
|
@@ -193,6 +234,7 @@ async function verifyTarget(input) {
|
|
|
193
234
|
const { dir } = target;
|
|
194
235
|
const copies = await checkEmbeddedCopies({
|
|
195
236
|
dir,
|
|
237
|
+
manifest,
|
|
196
238
|
checksumsPath,
|
|
197
239
|
embedded,
|
|
198
240
|
});
|
|
@@ -248,11 +290,13 @@ Options:
|
|
|
248
290
|
--root-folder <name> Folder in the ZIP (default: the one folder that
|
|
249
291
|
every entry is in)
|
|
250
292
|
--embedded <path> Where the manifest and its .sig are shipped inside the
|
|
251
|
-
files, like includes/checksums.json
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
293
|
+
files, like includes/checksums.json. Both the
|
|
294
|
+
--checksums file and its .sig must exist, both
|
|
295
|
+
copies must be shipped, and they must be identical
|
|
296
|
+
to them. Without this option, a file with the name
|
|
297
|
+
of the --checksums file at the root (and its .sig)
|
|
298
|
+
is a copy only when it exists, the matching original
|
|
299
|
+
exists and the manifest doesn't list it
|
|
256
300
|
--public-key <key> Base64 Ed25519 public key. Checks <checksums>.sig, as
|
|
257
301
|
written by "wp-release sign", before comparing any
|
|
258
302
|
file, and fails when it's missing or invalid
|
|
@@ -270,7 +314,9 @@ ignore file inside the checked directory can't hide added files. Use
|
|
|
270
314
|
|
|
271
315
|
A manifest can't list itself. A copy of the manifest and of its .sig inside
|
|
272
316
|
the checked files is therefore skipped, but it must be identical to the
|
|
273
|
-
--checksums file.
|
|
317
|
+
--checksums file. A path that the manifest lists is never skipped, and a
|
|
318
|
+
file that isn't a matching copy is reported as extra. See --embedded for
|
|
319
|
+
where the copies are looked for.`,
|
|
274
320
|
options: {
|
|
275
321
|
checksums: { type: 'string' },
|
|
276
322
|
zip: { type: 'string' },
|
package/package.json
CHANGED