Skip to content

Commit 88abb83

Browse files
committed
Validate asset hashes before uploading
1 parent 3bf63b3 commit 88abb83

File tree

4 files changed

+154
-2
lines changed

4 files changed

+154
-2
lines changed

scripts/releases/upload-release-assets-for-dotslash.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212

1313
const {
1414
validateAndParseDotSlashFile,
15+
validateDotSlashArtifactData,
1516
processDotSlashFileInPlace,
1617
} = require('./utils/dotslash-utils');
1718
const {REPO_ROOT} = require('../shared/consts');
@@ -104,6 +105,7 @@ async function uploadReleaseAssetsForDotSlash(
104105
providers,
105106
// NOTE: We mostly ignore suggestedFilename in favour of reading the actual asset URLs
106107
suggestedFilename,
108+
artifactInfo,
107109
) => {
108110
let upstreamUrl, targetReleaseAssetInfo;
109111
for (const provider of providers) {
@@ -170,6 +172,10 @@ async function uploadReleaseAssetsForDotSlash(
170172
// NOTE: Using curl because we have seen issues with fetch() on GHA
171173
// and the Meta CDN. ¯\_(ツ)_/¯
172174
const {data, headers} = await getWithCurl(upstreamUrl);
175+
console.log(
176+
`[${targetReleaseAssetInfo.name}] Validating download...`,
177+
);
178+
await validateDotSlashArtifactData(data, artifactInfo);
173179
if (dryRun) {
174180
console.log(
175181
`[${targetReleaseAssetInfo.name}] Dry run: Not uploading to release.`,

scripts/releases/utils/__tests__/__snapshots__/dotslash-utils-test.js.snap

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,11 @@ Array [
6666
},
6767
],
6868
"test-linux-x86_64.tar.gz",
69+
Object {
70+
"digest": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
71+
"hash": "sha256",
72+
"size": 0,
73+
},
6974
],
7075
Array [
7176
Array [
@@ -79,6 +84,19 @@ Array [
7984
},
8085
],
8186
"test-macos-aarch64.zip",
87+
Object {
88+
"digest": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
89+
"hash": "sha256",
90+
"size": 0,
91+
},
8292
],
8393
]
8494
`;
95+
96+
exports[`validateDotSlashArtifactData blake3 failure on digest mismatch 1`] = `"blake3 mismatch: expected 2623f14eac39a9cc7b211cda9c52bcb9949ccd63aed4040a6a1a9f5f9b9431fa, got af1349b9f5f9a1a6a0404dea36dcc9499bcb25c9adc112b7cc9a93cae41f3262"`;
97+
98+
exports[`validateDotSlashArtifactData blake3 failure on size mismatch 1`] = `"size mismatch: expected 1, got 0"`;
99+
100+
exports[`validateDotSlashArtifactData sha256 failure on digest mismatch 1`] = `"sha256 mismatch: expected 558b2587b199594ac439b9464e14ea72429bf6998c4fbfa941c1cf89244c0b3e, got e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"`;
101+
102+
exports[`validateDotSlashArtifactData sha256 failure on size mismatch 1`] = `"size mismatch: expected 1, got 0"`;

scripts/releases/utils/__tests__/dotslash-utils-test.js

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ const {
1616
processDotSlashFileInPlace,
1717
dangerouslyResignGeneratedFile,
1818
validateAndParseDotSlashFile,
19+
validateDotSlashArtifactData,
1920
} = require('../dotslash-utils');
2021

2122
jest.useRealTimers();
@@ -172,3 +173,77 @@ describe('dangerouslyResignGeneratedFile', () => {
172173
}`);
173174
});
174175
});
176+
177+
describe('validateDotSlashArtifactData', () => {
178+
test('blake3 success', async () => {
179+
await expect(
180+
validateDotSlashArtifactData(Buffer.from([]), {
181+
hash: 'blake3',
182+
digest:
183+
'af1349b9f5f9a1a6a0404dea36dcc9499bcb25c9adc112b7cc9a93cae41f3262',
184+
size: 0,
185+
}),
186+
).resolves.toBeUndefined();
187+
});
188+
189+
test('blake3 failure on size mismatch', async () => {
190+
await expect(
191+
validateDotSlashArtifactData(Buffer.from([]), {
192+
hash: 'blake3',
193+
digest:
194+
'af1349b9f5f9a1a6a0404dea36dcc9499bcb25c9adc112b7cc9a93cae41f3262',
195+
size: 1,
196+
}),
197+
).rejects.toThrowErrorMatchingSnapshot();
198+
});
199+
200+
test('blake3 failure on digest mismatch', async () => {
201+
await expect(
202+
validateDotSlashArtifactData(Buffer.from([]), {
203+
hash: 'blake3',
204+
digest:
205+
'af1349b9f5f9a1a6a0404dea36dcc9499bcb25c9adc112b7cc9a93cae41f3262'
206+
.split('')
207+
.reverse()
208+
.join(''),
209+
size: 0,
210+
}),
211+
).rejects.toThrowErrorMatchingSnapshot();
212+
});
213+
214+
test('sha256 success', async () => {
215+
await expect(
216+
validateDotSlashArtifactData(Buffer.from([]), {
217+
hash: 'sha256',
218+
digest:
219+
'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855',
220+
size: 0,
221+
}),
222+
).resolves.toBeUndefined();
223+
});
224+
225+
test('sha256 failure on size mismatch', async () => {
226+
await expect(
227+
validateDotSlashArtifactData(Buffer.from([]), {
228+
hash: 'sha256',
229+
digest:
230+
'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855',
231+
size: 1,
232+
}),
233+
).rejects.toThrowErrorMatchingSnapshot();
234+
});
235+
236+
test('sha256 failure on digest mismatch', async () => {
237+
await expect(
238+
validateDotSlashArtifactData(Buffer.from([]), {
239+
hash: 'sha256',
240+
digest:
241+
'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855'
242+
.split('')
243+
.reverse()
244+
.join(''),
245+
size: 0,
246+
}),
247+
).rejects.toThrowErrorMatchingSnapshot();
248+
});
249+
});

scripts/releases/utils/dotslash-utils.js

Lines changed: 55 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ const {parse, modify, applyEdits} = require('jsonc-parser');
1515
const signedsource = require('signedsource');
1616
const dotslash = require('@motizilberman/dotslash');
1717
const execFile = require('util').promisify(require('child_process').execFile);
18+
const os = require('os');
19+
const path = require('path');
1820

1921
/*::
2022
type DotSlashProvider = {
@@ -27,6 +29,14 @@ type DotSlashProvider = {
2729
name: string,
2830
};
2931
32+
type DotSlashPlatformSpec = {
33+
providers: DotSlashProvider[],
34+
hash: 'blake3' | 'sha256',
35+
digest: string,
36+
size: number,
37+
...
38+
}
39+
3040
type JSONCFormattingOptions = {
3141
tabSize?: number,
3242
insertSpaces?: boolean,
@@ -36,6 +46,11 @@ type JSONCFormattingOptions = {
3646
type DotSlashProvidersTransformFn = (
3747
providers: $ReadOnlyArray<DotSlashProvider>,
3848
suggestedFilename: string,
49+
artifactInfo: $ReadOnlyArray<{
50+
hash: 'blake3' | 'sha256',
51+
digest: string,
52+
size: number,
53+
}>
3954
) => ?$ReadOnlyArray<DotSlashProvider>;
4055
*/
4156

@@ -82,13 +97,17 @@ async function processDotSlashFileInPlace(
8297
splitShebangFromContents(originalContents);
8398
const json = parse(originalContentsJson);
8499
let intermediateContentsJson = originalContentsJson;
85-
for (const [platform, platformSpec] of Object.entries(json.platforms)) {
100+
for (const [platform, platformSpec] of Object.entries(json.platforms) /*::
101+
as $ReadOnlyArray<[string, DotSlashPlatformSpec]>
102+
*/) {
86103
const providers = platformSpec.providers;
87104
const suggestedFilename =
88105
`${sanitizeFileNameComponent(json.name)}-${platform}` +
89106
(platformSpec.format ? `.${platformSpec.format}` : '');
107+
const {hash, digest, size} = platformSpec;
90108
const newProviders =
91-
transformProviders(providers, suggestedFilename) ?? providers;
109+
transformProviders(providers, suggestedFilename, {hash, digest, size}) ??
110+
providers;
92111
if (newProviders !== providers) {
93112
const edits = modify(
94113
intermediateContentsJson,
@@ -126,9 +145,43 @@ async function dangerouslyResignGeneratedFile(
126145
await fs.writeFile(filename, newContents);
127146
}
128147

148+
async function validateDotSlashArtifactData(
149+
data /*: Buffer */,
150+
platformSpec /*: $ReadOnly<{
151+
digest: string,
152+
hash: 'blake3' | 'sha256',
153+
size: number,
154+
...
155+
}> */,
156+
) /*: Promise<void> */ {
157+
const {digest: expectedDigest, hash, size} = platformSpec;
158+
if (data.length !== size) {
159+
throw new Error(`size mismatch: expected ${size}, got ${data.length}`);
160+
}
161+
const hashFunction = hash === 'blake3' ? 'b3sum' : 'sha256';
162+
163+
const tempDir = await fs.mkdtemp(
164+
path.join(os.tmpdir(), 'validate-artifact-hash-'),
165+
);
166+
try {
167+
const tempFile = path.join(tempDir, 'data');
168+
await fs.writeFile(tempFile, data);
169+
const {stdout} = await execFile(dotslash, ['--', hashFunction, tempFile]);
170+
const actualDigest = stdout.trim();
171+
if (actualDigest !== expectedDigest) {
172+
throw new Error(
173+
`${hash} mismatch: expected ${expectedDigest}, got ${actualDigest}`,
174+
);
175+
}
176+
} finally {
177+
await fs.rm(tempDir, {recursive: true, force: true});
178+
}
179+
}
180+
129181
module.exports = {
130182
DEFAULT_FORMATTING_OPTIONS,
131183
processDotSlashFileInPlace,
132184
dangerouslyResignGeneratedFile,
133185
validateAndParseDotSlashFile,
186+
validateDotSlashArtifactData,
134187
};

0 commit comments

Comments
 (0)