diff --git a/src/releases.ts b/src/releases.ts index 7115c06..d34ba83 100644 --- a/src/releases.ts +++ b/src/releases.ts @@ -16,7 +16,6 @@ import { objectKeyFromArtifactUrl, streamToString, toSemverRange, - verifyHash, } from "./helpers"; import { z, ZodError } from "zod"; import { @@ -686,39 +685,19 @@ export const RetrieveLatestSystemRecovery = cachedRedirect( recovery.file, ); - const [firmwareFile, hashFile] = await Promise.all([ - // TODO: store file hash using custom header to avoid extra request - s3Client.send( - new GetObjectCommand({ - Bucket: bucketName, - Key: artifactPath, - }), - ), - s3Client.send( - new GetObjectCommand({ - Bucket: bucketName, - Key: `${artifactPath}.sha256`, - }), - ), - ]); - - if (!firmwareFile.Body || !hashFile.Body) { - throw new NotFoundError( - `Recovery image or hash file not found for version ${latestVersion}`, - ); + if (!(await s3ObjectExists(artifactPath))) { + throw new NotFoundError(`Recovery image not found for version ${latestVersion}`); } - await verifyHash(firmwareFile, hashFile, "recovery image hash does not match"); - - console.log("recovery image hash matches", latestVersion); - return `${baseUrl}/${artifactPath}`; }, ); /** - * 302 to the newest over-the-air artifact of one kind for the requested SKU, - * after verifying the object against its .sha256 sibling. The product table + * 302 to the newest over-the-air artifact of one kind for the requested SKU. + * Integrity is checked at publish time (the .sha256 sidecar is written by the + * release script, the sync script verifies hash and signature); here the + * object only has to exist. The product table * says which prefix holds it, so the same URL serves every product. Used by * build tooling (rv1106-system pulls the app binary into the system image) * and by flashing scripts. @@ -763,30 +742,10 @@ function latestArtifactRedirect(kind: OtaKind) { artifact.file, ); - const [artifactFile, hashFile] = await Promise.all([ - s3Client.send( - new GetObjectCommand({ - Bucket: bucketName, - Key: artifactPath, - }), - ), - s3Client.send( - new GetObjectCommand({ - Bucket: bucketName, - Key: `${artifactPath}.sha256`, - }), - ), - ]); - - if (!artifactFile.Body || !hashFile.Body) { - throw new NotFoundError( - `${prefix} artifact or hash file not found for version ${latestVersion}`, - ); + if (!(await s3ObjectExists(artifactPath))) { + throw new NotFoundError(`${prefix} artifact not found for version ${latestVersion}`); } - await verifyHash(artifactFile, hashFile, `${prefix} hash does not match`); - - console.log(`${prefix} hash matches`, latestVersion); return `${baseUrl}/${artifactPath}`; }, ); diff --git a/test/releases.test.ts b/test/releases.test.ts index 2294de9..cc32725 100644 --- a/test/releases.test.ts +++ b/test/releases.test.ts @@ -134,6 +134,9 @@ function mockS3LegacyVersionWithContent( Contents: [], }); + // Legacy artifact exists (HeadObjectCommand for the existence check) + s3Mock.on(HeadObjectCommand, { Key: `${prefix}/${version}/${fileName}` }).resolves({}); + // Mock legacy file path with content s3Mock.on(GetObjectCommand, { Key: `${prefix}/${version}/${fileName}` }).resolves({ Body: createAsyncIterable(content) as any, @@ -929,25 +932,6 @@ describe("RetrieveLatestApp S3 redirect handler", () => { ); }); - it("should throw InternalServerError when hash does not match", async () => { - const req = createMockRequest({}); - const res = createMockResponse(); - - s3Mock.on(ListObjectsV2Command, { Prefix: "app/" }).resolves({ - CommonPrefixes: [{ Prefix: "app/1.0.0/" }], - }); - - mockS3LegacyVersionWithContent( - "app", - "1.0.0", - "jetkvm_app", - "actual-content", - "wrong-hash-value", - ); - - await expect(RetrieveLatestApp(req, res)).rejects.toThrow(InternalServerError); - }); - it("should throw NotFoundError when app file is missing", async () => { const req = createMockRequest({}); const res = createMockResponse(); @@ -961,12 +945,9 @@ describe("RetrieveLatestApp S3 redirect handler", () => { Contents: [], }); - s3Mock.on(GetObjectCommand, { Key: "app/1.0.0/jetkvm_app" }).resolves({ - Body: undefined, - }); - s3Mock.on(GetObjectCommand, { Key: "app/1.0.0/jetkvm_app.sha256" }).resolves({ - Body: createAsyncIterable("some-hash") as any, - }); + s3Mock + .on(HeadObjectCommand, { Key: "app/1.0.0/jetkvm_app" }) + .rejects({ name: "NotFound", $metadata: { httpStatusCode: 404 } }); await expect(RetrieveLatestApp(req, res)).rejects.toThrow(NotFoundError); }); @@ -1304,28 +1285,7 @@ describe("RetrieveLatestSystemRecovery S3 redirect handler", () => { ); }); - it("should throw InternalServerError when hash does not match", async () => { - const req = createMockRequest({}); - const res = createMockResponse(); - - s3Mock.on(ListObjectsV2Command, { Prefix: "system/" }).resolves({ - CommonPrefixes: [{ Prefix: "system/1.0.0/" }], - }); - - mockS3LegacyVersionWithContent( - "system", - "1.0.0", - "update.img", - "actual-content", - "mismatched-hash", - ); - - await expect(RetrieveLatestSystemRecovery(req, res)).rejects.toThrow( - InternalServerError, - ); - }); - - it("should throw NotFoundError when recovery image or hash file is missing", async () => { + it("should throw NotFoundError when recovery image is missing", async () => { const req = createMockRequest({}); const res = createMockResponse(); @@ -1338,12 +1298,9 @@ describe("RetrieveLatestSystemRecovery S3 redirect handler", () => { Contents: [], }); - s3Mock.on(GetObjectCommand, { Key: "system/1.0.0/update.img" }).resolves({ - Body: undefined, - }); - s3Mock.on(GetObjectCommand, { Key: "system/1.0.0/update.img.sha256" }).resolves({ - Body: undefined, - }); + s3Mock + .on(HeadObjectCommand, { Key: "system/1.0.0/update.img" }) + .rejects({ name: "NotFound", $metadata: { httpStatusCode: 404 } }); await expect(RetrieveLatestSystemRecovery(req, res)).rejects.toThrow(NotFoundError); });