@wpelevator/wp-release 0.12.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.
Files changed (78) hide show
  1. package/README.md +150 -19
  2. package/dist/changeset-config.d.ts +91 -5
  3. package/dist/changeset-config.js +108 -11
  4. package/dist/changeset-dependents.d.ts +19 -8
  5. package/dist/changeset-dependents.js +96 -34
  6. package/dist/changeset-files.d.ts +6 -5
  7. package/dist/changeset-files.js +7 -5
  8. package/dist/changeset-packages.d.ts +55 -8
  9. package/dist/changeset-packages.js +85 -14
  10. package/dist/changeset-prompt.d.ts +119 -0
  11. package/dist/changeset-prompt.js +203 -0
  12. package/dist/changeset-release.d.ts +43 -5
  13. package/dist/changeset-release.js +99 -16
  14. package/dist/changesets.d.ts +6 -2
  15. package/dist/changesets.js +18 -12
  16. package/dist/cli.d.ts +7 -1
  17. package/dist/cli.js +51 -14
  18. package/dist/commands/args.d.ts +0 -21
  19. package/dist/commands/args.js +1 -35
  20. package/dist/commands/changeset-add.d.ts +5 -2
  21. package/dist/commands/changeset-add.js +109 -9
  22. package/dist/commands/changeset-args.d.ts +24 -5
  23. package/dist/commands/changeset-args.js +57 -14
  24. package/dist/commands/changeset-status.d.ts +3 -1
  25. package/dist/commands/changeset-status.js +2 -1
  26. package/dist/commands/changeset-tag.d.ts +1 -1
  27. package/dist/commands/changeset-tag.js +6 -3
  28. package/dist/commands/changeset-version.d.ts +7 -1
  29. package/dist/commands/changeset-version.js +17 -4
  30. package/dist/commands/checksums.js +2 -2
  31. package/dist/commands/command.d.ts +5 -0
  32. package/dist/commands/deploy-git.d.ts +24 -4
  33. package/dist/commands/deploy-git.js +57 -3
  34. package/dist/commands/deploy-svn.d.ts +24 -4
  35. package/dist/commands/deploy-svn.js +57 -2
  36. package/dist/commands/keygen.d.ts +3 -0
  37. package/dist/commands/keygen.js +6 -1
  38. package/dist/commands/package-deploys.d.ts +86 -0
  39. package/dist/commands/package-deploys.js +113 -0
  40. package/dist/commands/sign.d.ts +19 -1
  41. package/dist/commands/sign.js +47 -12
  42. package/dist/commands/verify.d.ts +3 -0
  43. package/dist/commands/verify.js +43 -24
  44. package/dist/commands/zip.d.ts +12 -8
  45. package/dist/commands/zip.js +135 -78
  46. package/dist/ed25519.d.ts +25 -0
  47. package/dist/ed25519.js +44 -0
  48. package/dist/file-selection.d.ts +21 -0
  49. package/dist/file-selection.js +35 -0
  50. package/dist/git-deploy.d.ts +32 -0
  51. package/dist/git-deploy.js +57 -1
  52. package/dist/git.d.ts +17 -0
  53. package/dist/git.js +23 -0
  54. package/dist/index.d.ts +13 -5
  55. package/dist/index.js +7 -3
  56. package/dist/minisign.d.ts +41 -0
  57. package/dist/minisign.js +128 -0
  58. package/dist/package-deploy.d.ts +133 -0
  59. package/dist/package-deploy.js +323 -0
  60. package/dist/package-zip.d.ts +52 -0
  61. package/dist/package-zip.js +78 -0
  62. package/dist/release-repository.d.ts +18 -5
  63. package/dist/release-repository.js +51 -11
  64. package/dist/release-tags.d.ts +3 -1
  65. package/dist/release-tags.js +5 -2
  66. package/dist/signing.d.ts +64 -9
  67. package/dist/signing.js +113 -40
  68. package/dist/ssh-signature.d.ts +43 -0
  69. package/dist/ssh-signature.js +156 -0
  70. package/dist/summary.d.ts +3 -3
  71. package/dist/summary.js +2 -2
  72. package/dist/tag-format.d.ts +23 -14
  73. package/dist/tag-format.js +26 -40
  74. package/dist/version-files.d.ts +8 -8
  75. package/dist/version-files.js +22 -18
  76. package/dist/versions.d.ts +28 -3
  77. package/dist/versions.js +84 -9
  78. package/package.json +2 -2
package/dist/signing.d.ts CHANGED
@@ -1,4 +1,8 @@
1
1
  import type { KeyObject } from 'node:crypto';
2
+ /**
3
+ * Extension of the signature files in the format that WordPress core's
4
+ * verify_file_signature() checks.
5
+ */
2
6
  export declare const SIGNATURE_EXTENSION = ".sig";
3
7
  /**
4
8
  * Environment variable with the base64 Ed25519 signing key.
@@ -7,11 +11,12 @@ export declare const SIGNING_KEY_ENV = "WP_RELEASE_SIGNING_KEY";
7
11
  /**
8
12
  * Generate an Ed25519 key pair in the base64 formats used by wp-release.
9
13
  *
10
- * @return Private seed for WP_RELEASE_SIGNING_KEY and public verification key.
14
+ * @return Private seed for WP_RELEASE_SIGNING_KEY, the base64 public key and the public key of each signature format.
11
15
  */
12
16
  export declare function generateSigningKeyPair(): {
13
17
  privateKey: string;
14
18
  publicKey: string;
19
+ publicKeys: Record<SignatureFormat, string>;
15
20
  };
16
21
  /**
17
22
  * Get an Ed25519 private key from the base64 format that sodium and the
@@ -24,11 +29,13 @@ export declare function generateSigningKeyPair(): {
24
29
  */
25
30
  export declare function parseSigningKey(encoded: string): KeyObject;
26
31
  /**
27
- * Get an Ed25519 public key from its base64 form.
32
+ * Get an Ed25519 public key from the base64 32-byte key, a minisign public
33
+ * key or an OpenSSH ssh-ed25519 public key. They're forms of the same key,
34
+ * so any of them checks the signatures of every format.
28
35
  *
29
- * @param encoded Base64 32-byte public key.
36
+ * @param encoded Public key.
30
37
  * @return Public key.
31
- * @throws {ValidationError} If the key has an unsupported length.
38
+ * @throws {ValidationError} If the key isn't in one of the forms.
32
39
  */
33
40
  export declare function parsePublicKey(encoded: string): KeyObject;
34
41
  /**
@@ -55,24 +62,72 @@ export declare function getFileDigest(path: string): Promise<Buffer>;
55
62
  * @return Base64 signature.
56
63
  */
57
64
  export declare function signFile(path: string, privateKey: KeyObject): Promise<string>;
65
+ /** Names of the signature formats. */
66
+ export type SignatureFormat = 'wordpress' | 'minisign' | 'ssh';
67
+ /** How a signature format writes and checks signature files. */
68
+ export interface SignatureFormatHandler {
69
+ /** Extension of the signature file, appended to the signed file name. */
70
+ extension: string;
71
+ /** Get the contents of the signature file of a file. */
72
+ create: (path: string, privateKey: KeyObject) => Promise<string>;
73
+ /** Check a file against its signature file. */
74
+ verify: (path: string, signaturePath: string, publicKey: KeyObject) => Promise<boolean>;
75
+ /** Get the public key in the form that the verifiers of the format take. */
76
+ getPublicKey: (key: KeyObject) => string;
77
+ }
78
+ export declare const DEFAULT_SIGNATURE_FORMAT: SignatureFormat;
79
+ /**
80
+ * Signature formats, all Ed25519 signatures by the same key:
81
+ *
82
+ * - wordpress: base64 signature of the SHA-384 digest in <file>.sig, which
83
+ * WordPress core's verify_file_signature() checks.
84
+ * - minisign: <file>.minisig, which `minisign -V` checks.
85
+ * - ssh: armored SSH signature in <file>.sshsig, which `ssh-keygen -Y verify`
86
+ * checks with the "file" namespace.
87
+ */
88
+ export declare const SIGNATURE_FORMATS: Record<SignatureFormat, SignatureFormatHandler>;
89
+ /**
90
+ * Check that a value is the name of a signature format.
91
+ *
92
+ * @param value Value to check.
93
+ * @return Whether it's a signature format.
94
+ */
95
+ export declare function isSignatureFormat(value: string): value is SignatureFormat;
96
+ /**
97
+ * Get the signature file path of a file.
98
+ *
99
+ * @param path File path.
100
+ * @param format Signature format.
101
+ * @return Signature file path.
102
+ */
103
+ export declare function getSignaturePath(path: string, format?: SignatureFormat): string;
104
+ /**
105
+ * Get the public key of a key in the form of each signature format.
106
+ *
107
+ * @param key Private or public key.
108
+ * @return Public keys by format.
109
+ */
110
+ export declare function getPublicKeys(key: KeyObject): Record<SignatureFormat, string>;
58
111
  /**
59
- * Write the signature of a file to a .sig file next to it.
112
+ * Write the signature of a file to a signature file next to it.
60
113
  *
61
114
  * @param path File path.
62
115
  * @param privateKey Private key from parseSigningKey().
63
116
  * @param force Whether to overwrite an existing signature file.
117
+ * @param format Signature format.
64
118
  * @return Signature file path.
65
119
  * @throws {ValidationError} If the signature file exists and force isn't set.
66
120
  */
67
- export declare function writeSignature(path: string, privateKey: KeyObject, force?: boolean): Promise<string>;
121
+ export declare function writeSignature(path: string, privateKey: KeyObject, force?: boolean, format?: SignatureFormat): Promise<string>;
68
122
  /**
69
- * Check a file against a .sig file, which holds one base64 signature per
70
- * line. The file is valid when any of the signatures match, so that a file
123
+ * Check a file against a signature file. A wordpress .sig file can hold one
124
+ * signature per line, and is valid when any of them match, so that a file
71
125
  * can carry the signatures of a key rotation window.
72
126
  *
73
127
  * @param path File path.
74
128
  * @param signaturePath Signature file path.
75
129
  * @param publicKey Public key from parsePublicKey().
130
+ * @param format Signature format.
76
131
  * @return Whether a signature matches.
77
132
  */
78
- export declare function verifyFileSignature(path: string, signaturePath: string, publicKey: KeyObject): Promise<boolean>;
133
+ export declare function verifyFileSignature(path: string, signaturePath: string, publicKey: KeyObject, format?: SignatureFormat): Promise<boolean>;
package/dist/signing.js CHANGED
@@ -1,7 +1,13 @@
1
- import { createHash, createPrivateKey, generateKeyPairSync, createPublicKey, sign, verify, } from 'node:crypto';
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 verification key.
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 its base64 form.
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 Base64 32-byte public key.
72
+ * @param encoded Public key.
67
73
  * @return Public key.
68
- * @throws {ValidationError} If the key has an unsupported length.
74
+ * @throws {ValidationError} If the key isn't in one of the forms.
69
75
  */
70
76
  export function parsePublicKey(encoded) {
71
- const bytes = decodeKey(encoded);
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 createPublicKey({
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
- const der = createPublicKey(privateKey).export({
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
- const hash = createHash('sha384');
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
- * Write the signature of a file to a .sig file next to it.
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 + SIGNATURE_EXTENSION;
130
- const signature = await signFile(path, privateKey);
131
- await writeFile(signaturePath, signature + '\n', {
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, which holds one base64 signature per
142
- * line. The file is valid when any of the signatures match, so that a file
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
- const digest = await getFileDigest(path);
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 { ZipResult } from './commands/zip.ts';
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: ZipResult): string;
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 key.
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 key.
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.publicKey)}`,
148
+ ...result.formats.map((format) => `Public key (${format}): ${formatCode(result.publicKeys[format])}`),
149
149
  ]);
150
150
  }
151
151
  /**
@@ -1,20 +1,29 @@
1
- import type { ConfiguredPackage } from './changeset-packages.ts';
1
+ import type { ConfiguredDeployPackage } from './changeset-packages.ts';
2
2
  /**
3
- * Format the tag of a package release.
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 Tag format with {name}, {slug}, {path}, {type} and {version}.
6
- * @param pkg Configured package.
7
- * @param version Released version, empty for the tag prefix of the package.
8
- * @return Tag name.
12
+ * @param format Template with {name}, {path} and {version}.
13
+ * @param pkg Configured package, with or without a version.
14
+ * @param version Released version, empty when there's none.
15
+ * @return Formatted text.
9
16
  */
10
- export declare function formatTag(format: string, pkg: ConfiguredPackage, version: string): string;
17
+ export declare function formatTemplate(format: string, pkg: Pick<ConfiguredDeployPackage, 'name' | 'path'>, version: string): string;
11
18
  /**
12
- * Format the tag prefix of a package before its name is known, for packages
13
- * versioned by their git tags.
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 pattern Tag pattern of the version source.
16
- * @param packagePath Package path relative to the package root.
17
- * @param tagType Configured {type} of the package tags.
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 formatTemplatePrefix(pattern: string, packagePath: string, tagType?: string): string;
29
+ export declare function formatTag(format: string, pkg: Pick<ConfiguredDeployPackage, 'name' | 'path'>, version: string): string;