@wpelevator/wp-release 0.13.0 → 0.14.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/README.md +67 -20
- package/dist/changeset-config.d.ts +8 -6
- package/dist/changeset-config.js +7 -5
- package/dist/changeset-packages.d.ts +12 -2
- package/dist/changeset-packages.js +13 -1
- package/dist/changeset-release.d.ts +5 -2
- package/dist/changeset-release.js +12 -6
- package/dist/cli.js +33 -9
- package/dist/commands/args.d.ts +0 -21
- package/dist/commands/args.js +1 -35
- package/dist/commands/changeset-add.d.ts +1 -1
- package/dist/commands/changeset-add.js +3 -2
- package/dist/commands/changeset-status.d.ts +1 -1
- package/dist/commands/changeset-status.js +2 -1
- package/dist/commands/changeset-tag.d.ts +1 -1
- package/dist/commands/changeset-tag.js +4 -2
- package/dist/commands/changeset-version.d.ts +1 -1
- package/dist/commands/changeset-version.js +2 -1
- package/dist/commands/checksums.js +2 -2
- package/dist/commands/command.d.ts +2 -0
- package/dist/commands/deploy-git.d.ts +1 -1
- package/dist/commands/deploy-git.js +2 -1
- package/dist/commands/deploy-svn.d.ts +1 -1
- package/dist/commands/deploy-svn.js +2 -1
- package/dist/commands/keygen.d.ts +3 -0
- package/dist/commands/keygen.js +6 -1
- package/dist/commands/package-deploys.d.ts +8 -0
- package/dist/commands/package-deploys.js +5 -0
- package/dist/commands/sign.d.ts +19 -1
- package/dist/commands/sign.js +47 -12
- package/dist/commands/verify.d.ts +3 -0
- package/dist/commands/verify.js +43 -24
- package/dist/commands/zip.d.ts +12 -8
- package/dist/commands/zip.js +135 -78
- package/dist/ed25519.d.ts +25 -0
- package/dist/ed25519.js +44 -0
- package/dist/file-selection.d.ts +21 -0
- package/dist/file-selection.js +35 -0
- package/dist/git.d.ts +17 -0
- package/dist/git.js +23 -0
- package/dist/index.d.ts +9 -4
- package/dist/index.js +6 -3
- package/dist/minisign.d.ts +41 -0
- package/dist/minisign.js +128 -0
- package/dist/package-deploy.d.ts +28 -0
- package/dist/package-deploy.js +41 -6
- package/dist/package-zip.d.ts +52 -0
- package/dist/package-zip.js +78 -0
- package/dist/release-repository.d.ts +4 -3
- package/dist/release-repository.js +5 -4
- package/dist/signing.d.ts +64 -9
- package/dist/signing.js +113 -40
- package/dist/ssh-signature.d.ts +43 -0
- package/dist/ssh-signature.js +156 -0
- package/dist/summary.d.ts +3 -3
- package/dist/summary.js +2 -2
- package/dist/tag-format.d.ts +21 -12
- package/dist/tag-format.js +25 -39
- package/dist/version-files.d.ts +6 -7
- package/dist/version-files.js +10 -11
- package/package.json +2 -2
package/dist/signing.js
CHANGED
|
@@ -1,7 +1,13 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { createReadStream } from 'node:fs';
|
|
1
|
+
import { createPrivateKey, generateKeyPairSync, sign, verify, } from 'node:crypto';
|
|
3
2
|
import { readFile, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { PUBLIC_KEY_BYTES, createRawPublicKey, digestFile, getRawPublicKey, } from "./ed25519.js";
|
|
4
4
|
import { ValidationError } from "./errors.js";
|
|
5
|
+
import { MINISIGN_EXTENSION, createMinisignSignature, getMinisignPublicKey, parseMinisignPublicKey, verifyMinisignSignature, } from "./minisign.js";
|
|
6
|
+
import { SSH_SIGNATURE_EXTENSION, createSshSignature, getSshPublicKey, parseSshPublicKey, verifySshSignature, } from "./ssh-signature.js";
|
|
7
|
+
/**
|
|
8
|
+
* Extension of the signature files in the format that WordPress core's
|
|
9
|
+
* verify_file_signature() checks.
|
|
10
|
+
*/
|
|
5
11
|
export const SIGNATURE_EXTENSION = '.sig';
|
|
6
12
|
/**
|
|
7
13
|
* Environment variable with the base64 Ed25519 signing key.
|
|
@@ -9,15 +15,12 @@ export const SIGNATURE_EXTENSION = '.sig';
|
|
|
9
15
|
export const SIGNING_KEY_ENV = 'WP_RELEASE_SIGNING_KEY';
|
|
10
16
|
const SEED_BYTES = 32;
|
|
11
17
|
const SECRET_KEY_BYTES = 64;
|
|
12
|
-
const PUBLIC_KEY_BYTES = 32;
|
|
13
18
|
// DER prefix of a PKCS#8 Ed25519 private key, followed by the 32-byte seed.
|
|
14
19
|
const PKCS8_PREFIX = Buffer.from('302e020100300506032b657004220420', 'hex');
|
|
15
|
-
// DER prefix of a SPKI Ed25519 public key, followed by the 32-byte key.
|
|
16
|
-
const SPKI_PREFIX = Buffer.from('302a300506032b6570032100', 'hex');
|
|
17
20
|
/**
|
|
18
21
|
* Generate an Ed25519 key pair in the base64 formats used by wp-release.
|
|
19
22
|
*
|
|
20
|
-
* @return Private seed for WP_RELEASE_SIGNING_KEY and public
|
|
23
|
+
* @return Private seed for WP_RELEASE_SIGNING_KEY, the base64 public key and the public key of each signature format.
|
|
21
24
|
*/
|
|
22
25
|
export function generateSigningKeyPair() {
|
|
23
26
|
const { privateKey, publicKey } = generateKeyPairSync('ed25519');
|
|
@@ -29,6 +32,7 @@ export function generateSigningKeyPair() {
|
|
|
29
32
|
return {
|
|
30
33
|
privateKey: Buffer.from(privateJwk.d, 'base64url').toString('base64'),
|
|
31
34
|
publicKey: Buffer.from(publicJwk.x, 'base64url').toString('base64'),
|
|
35
|
+
publicKeys: getPublicKeys(publicKey),
|
|
32
36
|
};
|
|
33
37
|
}
|
|
34
38
|
/**
|
|
@@ -61,22 +65,22 @@ export function parseSigningKey(encoded) {
|
|
|
61
65
|
});
|
|
62
66
|
}
|
|
63
67
|
/**
|
|
64
|
-
* Get an Ed25519 public key from
|
|
68
|
+
* Get an Ed25519 public key from the base64 32-byte key, a minisign public
|
|
69
|
+
* key or an OpenSSH ssh-ed25519 public key. They're forms of the same key,
|
|
70
|
+
* so any of them checks the signatures of every format.
|
|
65
71
|
*
|
|
66
|
-
* @param encoded
|
|
72
|
+
* @param encoded Public key.
|
|
67
73
|
* @return Public key.
|
|
68
|
-
* @throws {ValidationError} If the key
|
|
74
|
+
* @throws {ValidationError} If the key isn't in one of the forms.
|
|
69
75
|
*/
|
|
70
76
|
export function parsePublicKey(encoded) {
|
|
71
|
-
const bytes =
|
|
77
|
+
const bytes = parseSshPublicKey(encoded) ??
|
|
78
|
+
parseMinisignPublicKey(encoded) ??
|
|
79
|
+
decodeKey(encoded);
|
|
72
80
|
if (PUBLIC_KEY_BYTES !== bytes.length) {
|
|
73
|
-
throw new ValidationError(`The public key must be a base64 ${PUBLIC_KEY_BYTES}-byte Ed25519 key, got ${bytes.length} bytes.`);
|
|
81
|
+
throw new ValidationError(`The public key must be a base64 ${PUBLIC_KEY_BYTES}-byte Ed25519 key, a minisign public key or an ssh-ed25519 public key, got ${bytes.length} bytes.`);
|
|
74
82
|
}
|
|
75
|
-
return
|
|
76
|
-
key: Buffer.concat([SPKI_PREFIX, bytes]),
|
|
77
|
-
format: 'der',
|
|
78
|
-
type: 'spki',
|
|
79
|
-
});
|
|
83
|
+
return createRawPublicKey(bytes);
|
|
80
84
|
}
|
|
81
85
|
/**
|
|
82
86
|
* Get the base64 public key for a private key, in the format that
|
|
@@ -86,11 +90,7 @@ export function parsePublicKey(encoded) {
|
|
|
86
90
|
* @return Base64 public key.
|
|
87
91
|
*/
|
|
88
92
|
export function getPublicKey(privateKey) {
|
|
89
|
-
|
|
90
|
-
format: 'der',
|
|
91
|
-
type: 'spki',
|
|
92
|
-
});
|
|
93
|
-
return der.subarray(SPKI_PREFIX.length).toString('base64');
|
|
93
|
+
return getRawPublicKey(privateKey).toString('base64');
|
|
94
94
|
}
|
|
95
95
|
/**
|
|
96
96
|
* Get the raw SHA-384 digest of a file, which is what WordPress core's
|
|
@@ -100,11 +100,7 @@ export function getPublicKey(privateKey) {
|
|
|
100
100
|
* @return Digest bytes.
|
|
101
101
|
*/
|
|
102
102
|
export async function getFileDigest(path) {
|
|
103
|
-
|
|
104
|
-
for await (const chunk of createReadStream(path)) {
|
|
105
|
-
hash.update(chunk);
|
|
106
|
-
}
|
|
107
|
-
return hash.digest();
|
|
103
|
+
return digestFile(path, 'sha384');
|
|
108
104
|
}
|
|
109
105
|
/**
|
|
110
106
|
* Sign a file with a detached Ed25519 signature of its raw SHA-384 digest.
|
|
@@ -117,18 +113,99 @@ export async function signFile(path, privateKey) {
|
|
|
117
113
|
return sign(null, await getFileDigest(path), privateKey).toString('base64');
|
|
118
114
|
}
|
|
119
115
|
/**
|
|
120
|
-
*
|
|
116
|
+
* Check a file against a .sig file, which holds one base64 signature per
|
|
117
|
+
* line. The file is valid when any of the signatures match, so that a file
|
|
118
|
+
* can carry the signatures of a key rotation window.
|
|
119
|
+
*
|
|
120
|
+
* @param path File path.
|
|
121
|
+
* @param signaturePath Signature file path.
|
|
122
|
+
* @param publicKey Public key.
|
|
123
|
+
* @return Whether a signature matches.
|
|
124
|
+
*/
|
|
125
|
+
async function verifyWordPressSignature(path, signaturePath, publicKey) {
|
|
126
|
+
const digest = await getFileDigest(path);
|
|
127
|
+
const signatures = (await readFile(signaturePath, 'utf8'))
|
|
128
|
+
.split('\n')
|
|
129
|
+
.map((line) => line.trim())
|
|
130
|
+
.filter(Boolean);
|
|
131
|
+
return signatures.some((signature) => verify(null, digest, publicKey, Buffer.from(signature, 'base64')));
|
|
132
|
+
}
|
|
133
|
+
export const DEFAULT_SIGNATURE_FORMAT = 'wordpress';
|
|
134
|
+
/**
|
|
135
|
+
* Signature formats, all Ed25519 signatures by the same key:
|
|
136
|
+
*
|
|
137
|
+
* - wordpress: base64 signature of the SHA-384 digest in <file>.sig, which
|
|
138
|
+
* WordPress core's verify_file_signature() checks.
|
|
139
|
+
* - minisign: <file>.minisig, which `minisign -V` checks.
|
|
140
|
+
* - ssh: armored SSH signature in <file>.sshsig, which `ssh-keygen -Y verify`
|
|
141
|
+
* checks with the "file" namespace.
|
|
142
|
+
*/
|
|
143
|
+
export const SIGNATURE_FORMATS = {
|
|
144
|
+
wordpress: {
|
|
145
|
+
extension: SIGNATURE_EXTENSION,
|
|
146
|
+
create: async (path, privateKey) => (await signFile(path, privateKey)) + '\n',
|
|
147
|
+
verify: verifyWordPressSignature,
|
|
148
|
+
getPublicKey,
|
|
149
|
+
},
|
|
150
|
+
minisign: {
|
|
151
|
+
extension: MINISIGN_EXTENSION,
|
|
152
|
+
create: createMinisignSignature,
|
|
153
|
+
verify: verifyMinisignSignature,
|
|
154
|
+
getPublicKey: getMinisignPublicKey,
|
|
155
|
+
},
|
|
156
|
+
ssh: {
|
|
157
|
+
extension: SSH_SIGNATURE_EXTENSION,
|
|
158
|
+
create: createSshSignature,
|
|
159
|
+
verify: verifySshSignature,
|
|
160
|
+
getPublicKey: getSshPublicKey,
|
|
161
|
+
},
|
|
162
|
+
};
|
|
163
|
+
/**
|
|
164
|
+
* Check that a value is the name of a signature format.
|
|
165
|
+
*
|
|
166
|
+
* @param value Value to check.
|
|
167
|
+
* @return Whether it's a signature format.
|
|
168
|
+
*/
|
|
169
|
+
export function isSignatureFormat(value) {
|
|
170
|
+
return Object.hasOwn(SIGNATURE_FORMATS, value);
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Get the signature file path of a file.
|
|
174
|
+
*
|
|
175
|
+
* @param path File path.
|
|
176
|
+
* @param format Signature format.
|
|
177
|
+
* @return Signature file path.
|
|
178
|
+
*/
|
|
179
|
+
export function getSignaturePath(path, format = DEFAULT_SIGNATURE_FORMAT) {
|
|
180
|
+
return path + SIGNATURE_FORMATS[format].extension;
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Get the public key of a key in the form of each signature format.
|
|
184
|
+
*
|
|
185
|
+
* @param key Private or public key.
|
|
186
|
+
* @return Public keys by format.
|
|
187
|
+
*/
|
|
188
|
+
export function getPublicKeys(key) {
|
|
189
|
+
return {
|
|
190
|
+
wordpress: SIGNATURE_FORMATS.wordpress.getPublicKey(key),
|
|
191
|
+
minisign: SIGNATURE_FORMATS.minisign.getPublicKey(key),
|
|
192
|
+
ssh: SIGNATURE_FORMATS.ssh.getPublicKey(key),
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Write the signature of a file to a signature file next to it.
|
|
121
197
|
*
|
|
122
198
|
* @param path File path.
|
|
123
199
|
* @param privateKey Private key from parseSigningKey().
|
|
124
200
|
* @param force Whether to overwrite an existing signature file.
|
|
201
|
+
* @param format Signature format.
|
|
125
202
|
* @return Signature file path.
|
|
126
203
|
* @throws {ValidationError} If the signature file exists and force isn't set.
|
|
127
204
|
*/
|
|
128
|
-
export async function writeSignature(path, privateKey, force = false) {
|
|
129
|
-
const signaturePath = path
|
|
130
|
-
const signature = await
|
|
131
|
-
await writeFile(signaturePath, signature
|
|
205
|
+
export async function writeSignature(path, privateKey, force = false, format = DEFAULT_SIGNATURE_FORMAT) {
|
|
206
|
+
const signaturePath = getSignaturePath(path, format);
|
|
207
|
+
const signature = await SIGNATURE_FORMATS[format].create(path, privateKey);
|
|
208
|
+
await writeFile(signaturePath, signature, {
|
|
132
209
|
flag: force ? 'w' : 'wx',
|
|
133
210
|
}).catch((error) => {
|
|
134
211
|
throw 'EEXIST' === error.code
|
|
@@ -138,20 +215,16 @@ export async function writeSignature(path, privateKey, force = false) {
|
|
|
138
215
|
return signaturePath;
|
|
139
216
|
}
|
|
140
217
|
/**
|
|
141
|
-
* Check a file against a .sig file
|
|
142
|
-
* line
|
|
218
|
+
* Check a file against a signature file. A wordpress .sig file can hold one
|
|
219
|
+
* signature per line, and is valid when any of them match, so that a file
|
|
143
220
|
* can carry the signatures of a key rotation window.
|
|
144
221
|
*
|
|
145
222
|
* @param path File path.
|
|
146
223
|
* @param signaturePath Signature file path.
|
|
147
224
|
* @param publicKey Public key from parsePublicKey().
|
|
225
|
+
* @param format Signature format.
|
|
148
226
|
* @return Whether a signature matches.
|
|
149
227
|
*/
|
|
150
|
-
export async function verifyFileSignature(path, signaturePath, publicKey) {
|
|
151
|
-
|
|
152
|
-
const signatures = (await readFile(signaturePath, 'utf8'))
|
|
153
|
-
.split('\n')
|
|
154
|
-
.map((line) => line.trim())
|
|
155
|
-
.filter(Boolean);
|
|
156
|
-
return signatures.some((signature) => verify(null, digest, publicKey, Buffer.from(signature, 'base64')));
|
|
228
|
+
export async function verifyFileSignature(path, signaturePath, publicKey, format = DEFAULT_SIGNATURE_FORMAT) {
|
|
229
|
+
return SIGNATURE_FORMATS[format].verify(path, signaturePath, publicKey);
|
|
157
230
|
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { KeyObject } from 'node:crypto';
|
|
2
|
+
export declare const SSH_SIGNATURE_EXTENSION = ".sshsig";
|
|
3
|
+
/**
|
|
4
|
+
* Namespace of the signatures, which `ssh-keygen -Y verify -n` must match,
|
|
5
|
+
* so that a signature for files can't be used as one for something else.
|
|
6
|
+
*/
|
|
7
|
+
export declare const SSH_SIGNATURE_NAMESPACE = "file";
|
|
8
|
+
/**
|
|
9
|
+
* Get the OpenSSH public key of a key, the line of an authorized_keys or
|
|
10
|
+
* allowed_signers file.
|
|
11
|
+
*
|
|
12
|
+
* @param key Private or public key.
|
|
13
|
+
* @return OpenSSH public key.
|
|
14
|
+
*/
|
|
15
|
+
export declare function getSshPublicKey(key: KeyObject): string;
|
|
16
|
+
/**
|
|
17
|
+
* Get the raw public key from an OpenSSH Ed25519 public key line, with or
|
|
18
|
+
* without a comment.
|
|
19
|
+
*
|
|
20
|
+
* @param encoded OpenSSH public key.
|
|
21
|
+
* @return Public key bytes, or null when it isn't an OpenSSH Ed25519 key.
|
|
22
|
+
*/
|
|
23
|
+
export declare function parseSshPublicKey(encoded: string): Buffer | null;
|
|
24
|
+
/**
|
|
25
|
+
* Create an SSH signature of a file, the armored format that
|
|
26
|
+
* `ssh-keygen -Y sign` writes, with the "file" namespace and a SHA-512
|
|
27
|
+
* digest.
|
|
28
|
+
*
|
|
29
|
+
* @param path File path.
|
|
30
|
+
* @param privateKey Private key.
|
|
31
|
+
* @return Contents of the signature file.
|
|
32
|
+
*/
|
|
33
|
+
export declare function createSshSignature(path: string, privateKey: KeyObject): Promise<string>;
|
|
34
|
+
/**
|
|
35
|
+
* Check a file against an SSH signature file. The signature must be made by
|
|
36
|
+
* the public key, for the "file" namespace, with a SHA-512 or SHA-256 digest.
|
|
37
|
+
*
|
|
38
|
+
* @param path File path.
|
|
39
|
+
* @param signaturePath Signature file path.
|
|
40
|
+
* @param publicKey Public key.
|
|
41
|
+
* @return Whether the signature matches.
|
|
42
|
+
*/
|
|
43
|
+
export declare function verifySshSignature(path: string, signaturePath: string, publicKey: KeyObject): Promise<boolean>;
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { sign, verify } from 'node:crypto';
|
|
2
|
+
import { readFile } from 'node:fs/promises';
|
|
3
|
+
import { PUBLIC_KEY_BYTES, digestFile, getRawPublicKey } from "./ed25519.js";
|
|
4
|
+
export const SSH_SIGNATURE_EXTENSION = '.sshsig';
|
|
5
|
+
/**
|
|
6
|
+
* Namespace of the signatures, which `ssh-keygen -Y verify -n` must match,
|
|
7
|
+
* so that a signature for files can't be used as one for something else.
|
|
8
|
+
*/
|
|
9
|
+
export const SSH_SIGNATURE_NAMESPACE = 'file';
|
|
10
|
+
const KEY_TYPE = 'ssh-ed25519';
|
|
11
|
+
const MAGIC = Buffer.from('SSHSIG');
|
|
12
|
+
const VERSION = 1;
|
|
13
|
+
const HASH_ALGORITHM = 'sha512';
|
|
14
|
+
const ARMOR_BEGIN = '-----BEGIN SSH SIGNATURE-----';
|
|
15
|
+
const ARMOR_END = '-----END SSH SIGNATURE-----';
|
|
16
|
+
const ARMOR_LINE_LENGTH = 70;
|
|
17
|
+
/**
|
|
18
|
+
* Encode a value as an SSH wire format string: its length as a 32-bit
|
|
19
|
+
* big-endian number, followed by its bytes.
|
|
20
|
+
*
|
|
21
|
+
* @param value Bytes or text.
|
|
22
|
+
* @return Encoded string.
|
|
23
|
+
*/
|
|
24
|
+
function encodeString(value) {
|
|
25
|
+
const bytes = Buffer.from(value);
|
|
26
|
+
const length = Buffer.alloc(4);
|
|
27
|
+
length.writeUInt32BE(bytes.length);
|
|
28
|
+
return Buffer.concat([length, bytes]);
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Read the SSH wire format strings that follow each other in a buffer.
|
|
32
|
+
*
|
|
33
|
+
* @param bytes Encoded strings.
|
|
34
|
+
* @return Decoded strings, or null when the buffer isn't a list of strings.
|
|
35
|
+
*/
|
|
36
|
+
function decodeStrings(bytes) {
|
|
37
|
+
const strings = [];
|
|
38
|
+
let offset = 0;
|
|
39
|
+
while (offset + 4 <= bytes.length) {
|
|
40
|
+
const end = offset + 4 + bytes.readUInt32BE(offset);
|
|
41
|
+
strings.push(bytes.subarray(offset + 4, end));
|
|
42
|
+
offset = end;
|
|
43
|
+
}
|
|
44
|
+
return offset === bytes.length ? strings : null;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Encode an Ed25519 public key as an SSH public key blob.
|
|
48
|
+
*
|
|
49
|
+
* @param publicKey Public key bytes.
|
|
50
|
+
* @return Public key blob.
|
|
51
|
+
*/
|
|
52
|
+
function encodePublicKey(publicKey) {
|
|
53
|
+
return Buffer.concat([
|
|
54
|
+
encodeString(KEY_TYPE),
|
|
55
|
+
encodeString(publicKey),
|
|
56
|
+
]);
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Get the data that an SSH signature signs: the namespace, the hash
|
|
60
|
+
* algorithm and the digest of the file.
|
|
61
|
+
*
|
|
62
|
+
* @param digest File digest.
|
|
63
|
+
* @param namespace Signature namespace.
|
|
64
|
+
* @param hashAlgorithm Hash algorithm of the digest.
|
|
65
|
+
* @return Signed data.
|
|
66
|
+
*/
|
|
67
|
+
function getSignedData(digest, namespace, hashAlgorithm) {
|
|
68
|
+
return Buffer.concat([
|
|
69
|
+
MAGIC,
|
|
70
|
+
encodeString(namespace),
|
|
71
|
+
encodeString(''),
|
|
72
|
+
encodeString(hashAlgorithm),
|
|
73
|
+
encodeString(digest),
|
|
74
|
+
]);
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Get the OpenSSH public key of a key, the line of an authorized_keys or
|
|
78
|
+
* allowed_signers file.
|
|
79
|
+
*
|
|
80
|
+
* @param key Private or public key.
|
|
81
|
+
* @return OpenSSH public key.
|
|
82
|
+
*/
|
|
83
|
+
export function getSshPublicKey(key) {
|
|
84
|
+
return `${KEY_TYPE} ${encodePublicKey(getRawPublicKey(key)).toString('base64')}`;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Get the raw public key from an OpenSSH Ed25519 public key line, with or
|
|
88
|
+
* without a comment.
|
|
89
|
+
*
|
|
90
|
+
* @param encoded OpenSSH public key.
|
|
91
|
+
* @return Public key bytes, or null when it isn't an OpenSSH Ed25519 key.
|
|
92
|
+
*/
|
|
93
|
+
export function parseSshPublicKey(encoded) {
|
|
94
|
+
const [type, blob = ''] = encoded.trim().split(/\s+/);
|
|
95
|
+
const [blobType, publicKey] = decodeStrings(Buffer.from(blob, 'base64')) ?? [];
|
|
96
|
+
const isSsh = KEY_TYPE === type &&
|
|
97
|
+
KEY_TYPE === blobType?.toString() &&
|
|
98
|
+
PUBLIC_KEY_BYTES === publicKey?.length;
|
|
99
|
+
return isSsh && publicKey ? publicKey : null;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Create an SSH signature of a file, the armored format that
|
|
103
|
+
* `ssh-keygen -Y sign` writes, with the "file" namespace and a SHA-512
|
|
104
|
+
* digest.
|
|
105
|
+
*
|
|
106
|
+
* @param path File path.
|
|
107
|
+
* @param privateKey Private key.
|
|
108
|
+
* @return Contents of the signature file.
|
|
109
|
+
*/
|
|
110
|
+
export async function createSshSignature(path, privateKey) {
|
|
111
|
+
const signature = sign(null, getSignedData(await digestFile(path, HASH_ALGORITHM), SSH_SIGNATURE_NAMESPACE, HASH_ALGORITHM), privateKey);
|
|
112
|
+
const version = Buffer.alloc(4);
|
|
113
|
+
version.writeUInt32BE(VERSION);
|
|
114
|
+
const encoded = Buffer.concat([
|
|
115
|
+
MAGIC,
|
|
116
|
+
version,
|
|
117
|
+
encodeString(encodePublicKey(getRawPublicKey(privateKey))),
|
|
118
|
+
encodeString(SSH_SIGNATURE_NAMESPACE),
|
|
119
|
+
encodeString(''),
|
|
120
|
+
encodeString(HASH_ALGORITHM),
|
|
121
|
+
encodeString(Buffer.concat([
|
|
122
|
+
encodeString(KEY_TYPE),
|
|
123
|
+
encodeString(signature),
|
|
124
|
+
])),
|
|
125
|
+
]).toString('base64');
|
|
126
|
+
const lines = encoded.match(new RegExp(`.{1,${ARMOR_LINE_LENGTH}}`, 'g'));
|
|
127
|
+
return [ARMOR_BEGIN, ...(lines ?? []), ARMOR_END, ''].join('\n');
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Check a file against an SSH signature file. The signature must be made by
|
|
131
|
+
* the public key, for the "file" namespace, with a SHA-512 or SHA-256 digest.
|
|
132
|
+
*
|
|
133
|
+
* @param path File path.
|
|
134
|
+
* @param signaturePath Signature file path.
|
|
135
|
+
* @param publicKey Public key.
|
|
136
|
+
* @return Whether the signature matches.
|
|
137
|
+
*/
|
|
138
|
+
export async function verifySshSignature(path, signaturePath, publicKey) {
|
|
139
|
+
const armored = (await readFile(signaturePath, 'utf8')).trim();
|
|
140
|
+
const bytes = Buffer.from(armored.slice(ARMOR_BEGIN.length, -ARMOR_END.length), 'base64');
|
|
141
|
+
const [signer, namespace, reserved, hashAlgorithm = Buffer.alloc(0), signatureBlob = Buffer.alloc(0),] = decodeStrings(bytes.subarray(MAGIC.length + 4)) ?? [];
|
|
142
|
+
const [signatureType, signature] = decodeStrings(signatureBlob) ?? [];
|
|
143
|
+
const isValidShape = armored.startsWith(ARMOR_BEGIN) &&
|
|
144
|
+
armored.endsWith(ARMOR_END) &&
|
|
145
|
+
bytes.length > MAGIC.length + 4 &&
|
|
146
|
+
MAGIC.equals(bytes.subarray(0, MAGIC.length)) &&
|
|
147
|
+
VERSION === bytes.readUInt32BE(MAGIC.length) &&
|
|
148
|
+
signer?.equals(encodePublicKey(getRawPublicKey(publicKey))) &&
|
|
149
|
+
SSH_SIGNATURE_NAMESPACE === namespace?.toString() &&
|
|
150
|
+
0 === reserved?.length &&
|
|
151
|
+
['sha512', 'sha256'].includes(hashAlgorithm.toString()) &&
|
|
152
|
+
KEY_TYPE === signatureType?.toString() &&
|
|
153
|
+
undefined !== signature;
|
|
154
|
+
return (Boolean(isValidShape) &&
|
|
155
|
+
verify(null, getSignedData(await digestFile(path, hashAlgorithm.toString()), SSH_SIGNATURE_NAMESPACE, hashAlgorithm.toString()), publicKey, signature ?? Buffer.alloc(0)));
|
|
156
|
+
}
|
package/dist/summary.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { ChecksumsResult } from './commands/checksums.ts';
|
|
2
2
|
import type { SignResult } from './commands/sign.ts';
|
|
3
|
-
import type {
|
|
3
|
+
import type { ZipPackageResult } from './package-zip.ts';
|
|
4
4
|
import type { ChangesetTagResult } from './changeset-release.ts';
|
|
5
5
|
import type { PackageRelease } from './changesets.ts';
|
|
6
6
|
import type { DeployOptions } from './deploy-options.ts';
|
|
@@ -76,7 +76,7 @@ export declare function formatFile(context: SummaryContext, file: string, line?:
|
|
|
76
76
|
* @param result ZIP and manifest paths and the number of files.
|
|
77
77
|
* @return Markdown summary.
|
|
78
78
|
*/
|
|
79
|
-
export declare function getZipSummary(context: SummaryContext, result:
|
|
79
|
+
export declare function getZipSummary(context: SummaryContext, result: ZipPackageResult): string;
|
|
80
80
|
/**
|
|
81
81
|
* Get the summary of a checksums manifest.
|
|
82
82
|
*
|
|
@@ -89,7 +89,7 @@ export declare function getChecksumsSummary(context: SummaryContext, result: Che
|
|
|
89
89
|
* Get the summary of signed files.
|
|
90
90
|
*
|
|
91
91
|
* @param context Workspace directory.
|
|
92
|
-
* @param result Signature paths and the public
|
|
92
|
+
* @param result Signature paths and the public keys of the formats.
|
|
93
93
|
* @return Markdown summary.
|
|
94
94
|
*/
|
|
95
95
|
export declare function getSignSummary(context: SummaryContext, result: SignResult): string;
|
package/dist/summary.js
CHANGED
|
@@ -138,14 +138,14 @@ export function getChecksumsSummary(context, result) {
|
|
|
138
138
|
* Get the summary of signed files.
|
|
139
139
|
*
|
|
140
140
|
* @param context Workspace directory.
|
|
141
|
-
* @param result Signature paths and the public
|
|
141
|
+
* @param result Signature paths and the public keys of the formats.
|
|
142
142
|
* @return Markdown summary.
|
|
143
143
|
*/
|
|
144
144
|
export function getSignSummary(context, result) {
|
|
145
145
|
return joinBlocks([
|
|
146
146
|
'### Signatures',
|
|
147
147
|
formatList(result.signatures.map((path) => formatFile(context, path))),
|
|
148
|
-
`Public key: ${formatCode(result.
|
|
148
|
+
...result.formats.map((format) => `Public key (${format}): ${formatCode(result.publicKeys[format])}`),
|
|
149
149
|
]);
|
|
150
150
|
}
|
|
151
151
|
/**
|
package/dist/tag-format.d.ts
CHANGED
|
@@ -1,20 +1,29 @@
|
|
|
1
1
|
import type { ConfiguredDeployPackage } from './changeset-packages.ts';
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
3
|
+
* Tag format of the package releases, like plugins/alpha/1.2.0 in a
|
|
4
|
+
* monorepo, or 1.2.0 for a package at the repository root.
|
|
5
|
+
*/
|
|
6
|
+
export declare const DEFAULT_TAG_FORMAT = "{path}/{version}";
|
|
7
|
+
/**
|
|
8
|
+
* Fill in a template of a package with {name}, {path} and {version}, like a
|
|
9
|
+
* deploy template. A package at the root of the repository has an empty
|
|
10
|
+
* {path}.
|
|
4
11
|
*
|
|
5
|
-
* @param format
|
|
12
|
+
* @param format Template with {name}, {path} and {version}.
|
|
6
13
|
* @param pkg Configured package, with or without a version.
|
|
7
|
-
* @param version Released version, empty
|
|
8
|
-
* @return
|
|
14
|
+
* @param version Released version, empty when there's none.
|
|
15
|
+
* @return Formatted text.
|
|
9
16
|
*/
|
|
10
|
-
export declare function
|
|
17
|
+
export declare function formatTemplate(format: string, pkg: Pick<ConfiguredDeployPackage, 'name' | 'path'>, version: string): string;
|
|
11
18
|
/**
|
|
12
|
-
* Format the tag
|
|
13
|
-
*
|
|
19
|
+
* Format the tag of a package release, or its tag prefix without a version.
|
|
20
|
+
* Empty placeholders leave no repeated or leading slashes, so a package at
|
|
21
|
+
* the repository root gets 1.2.0 for {path}/{version}. A trailing slash is
|
|
22
|
+
* kept, since it separates the tag prefix from the version.
|
|
14
23
|
*
|
|
15
|
-
* @param
|
|
16
|
-
* @param
|
|
17
|
-
* @param
|
|
18
|
-
* @return Tag prefix.
|
|
24
|
+
* @param format Tag format with {name}, {path} and {version}.
|
|
25
|
+
* @param pkg Configured package, with or without a version.
|
|
26
|
+
* @param version Released version, empty for the tag prefix of the package.
|
|
27
|
+
* @return Tag name or prefix.
|
|
19
28
|
*/
|
|
20
|
-
export declare function
|
|
29
|
+
export declare function formatTag(format: string, pkg: Pick<ConfiguredDeployPackage, 'name' | 'path'>, version: string): string;
|
package/dist/tag-format.js
CHANGED
|
@@ -1,51 +1,37 @@
|
|
|
1
|
-
import path from 'node:path';
|
|
2
1
|
/**
|
|
3
|
-
*
|
|
2
|
+
* Tag format of the package releases, like plugins/alpha/1.2.0 in a
|
|
3
|
+
* monorepo, or 1.2.0 for a package at the repository root.
|
|
4
|
+
*/
|
|
5
|
+
export const DEFAULT_TAG_FORMAT = '{path}/{version}';
|
|
6
|
+
/**
|
|
7
|
+
* Fill in a template of a package with {name}, {path} and {version}, like a
|
|
8
|
+
* deploy template. A package at the root of the repository has an empty
|
|
9
|
+
* {path}.
|
|
4
10
|
*
|
|
5
|
-
* @param format
|
|
11
|
+
* @param format Template with {name}, {path} and {version}.
|
|
6
12
|
* @param pkg Configured package, with or without a version.
|
|
7
|
-
* @param version Released version, empty
|
|
8
|
-
* @return
|
|
13
|
+
* @param version Released version, empty when there's none.
|
|
14
|
+
* @return Formatted text.
|
|
9
15
|
*/
|
|
10
|
-
export function
|
|
11
|
-
const type = pkg.group.tagType ?? defaultTagType(pkg.path, pkg.type);
|
|
16
|
+
export function formatTemplate(format, pkg, version) {
|
|
12
17
|
return format
|
|
13
18
|
.replaceAll('{name}', pkg.name)
|
|
14
|
-
.replaceAll('{
|
|
15
|
-
? path.basename(pkg.directory)
|
|
16
|
-
: path.basename(pkg.path))
|
|
17
|
-
.replaceAll('{path}', pkg.path)
|
|
18
|
-
.replaceAll('{type}', type)
|
|
19
|
+
.replaceAll('{path}', '.' === pkg.path ? '' : pkg.path)
|
|
19
20
|
.replaceAll('{version}', version);
|
|
20
21
|
}
|
|
21
22
|
/**
|
|
22
|
-
* Format the tag
|
|
23
|
-
*
|
|
23
|
+
* Format the tag of a package release, or its tag prefix without a version.
|
|
24
|
+
* Empty placeholders leave no repeated or leading slashes, so a package at
|
|
25
|
+
* the repository root gets 1.2.0 for {path}/{version}. A trailing slash is
|
|
26
|
+
* kept, since it separates the tag prefix from the version.
|
|
24
27
|
*
|
|
25
|
-
* @param
|
|
26
|
-
* @param
|
|
27
|
-
* @param
|
|
28
|
-
* @return Tag prefix.
|
|
28
|
+
* @param format Tag format with {name}, {path} and {version}.
|
|
29
|
+
* @param pkg Configured package, with or without a version.
|
|
30
|
+
* @param version Released version, empty for the tag prefix of the package.
|
|
31
|
+
* @return Tag name or prefix.
|
|
29
32
|
*/
|
|
30
|
-
export function
|
|
31
|
-
return
|
|
32
|
-
.
|
|
33
|
-
.
|
|
34
|
-
.replaceAll('{name}', packagePath.split('/').at(-1) ?? packagePath)
|
|
35
|
-
.replaceAll('{slug}', packagePath.split('/').at(-1) ?? packagePath)
|
|
36
|
-
.replaceAll('{version}', '');
|
|
37
|
-
}
|
|
38
|
-
function defaultTagType(packagePath, type) {
|
|
39
|
-
if ('.' === packagePath && type) {
|
|
40
|
-
return type;
|
|
41
|
-
}
|
|
42
|
-
const first = packagePath.split('/')[0] ?? 'package';
|
|
43
|
-
const irregular = {
|
|
44
|
-
actions: 'action',
|
|
45
|
-
plugins: 'plugin',
|
|
46
|
-
themes: 'theme',
|
|
47
|
-
php: 'php',
|
|
48
|
-
js: 'js',
|
|
49
|
-
};
|
|
50
|
-
return irregular[first] ?? first.replace(/s$/, '');
|
|
33
|
+
export function formatTag(format, pkg, version) {
|
|
34
|
+
return formatTemplate(format, pkg, version)
|
|
35
|
+
.replace(/\/{2,}/g, '/')
|
|
36
|
+
.replace(/^\/+/, '');
|
|
51
37
|
}
|
package/dist/version-files.d.ts
CHANGED
|
@@ -1,19 +1,18 @@
|
|
|
1
1
|
import type { PackageVersionSource, PackageVersionTarget } from './changeset-config.ts';
|
|
2
|
-
import type { ConfiguredPackage } from './changeset-packages.ts';
|
|
2
|
+
import type { ConfiguredPackage, ConfiguredDeployPackage } from './changeset-packages.ts';
|
|
3
3
|
import type { FileJournal } from './file-journal.ts';
|
|
4
4
|
import type { ReleaseRepository } from './release-repository.ts';
|
|
5
5
|
/**
|
|
6
6
|
* Read the current version of a package from its configured source.
|
|
7
7
|
*
|
|
8
|
-
* @param repository
|
|
9
|
-
* @param directory
|
|
10
|
-
* @param descriptor
|
|
11
|
-
* @param
|
|
12
|
-
* @param tagType Configured {type} of the package tags.
|
|
8
|
+
* @param repository Repository, for versions stored as git tags.
|
|
9
|
+
* @param directory Package directory.
|
|
10
|
+
* @param descriptor Version source.
|
|
11
|
+
* @param pkg Package name and path, for the tags of a git-tag source.
|
|
13
12
|
* @return Current version.
|
|
14
13
|
* @throws {ValidationError} If the version is missing.
|
|
15
14
|
*/
|
|
16
|
-
export declare function readVersion(repository: ReleaseRepository, directory: string, descriptor: PackageVersionSource,
|
|
15
|
+
export declare function readVersion(repository: ReleaseRepository, directory: string, descriptor: PackageVersionSource, pkg: Pick<ConfiguredDeployPackage, 'name' | 'path'>): Promise<string>;
|
|
17
16
|
/**
|
|
18
17
|
* Write a version to a configured version target.
|
|
19
18
|
*
|