Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion src/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,13 @@ export class InternalError extends Response {

export class ManifestError extends Response {
constructor(
code: "MANIFEST_INVALID" | "BLOB_UNKNOWN" | "MANIFEST_UNVERIFIED" | "TAG_INVALID" | "NAME_INVALID",
code:
| "MANIFEST_INVALID"
| "BLOB_UNKNOWN"
| "MANIFEST_UNVERIFIED"
| "TAG_INVALID"
| "NAME_INVALID"
| "DIGEST_INVALID",
message: string,
detail: Record<string, string> = {},
) {
Expand Down
11 changes: 11 additions & 0 deletions src/registry/r2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -509,6 +509,17 @@ export class R2Registry implements Registry {
shaWriter.close();
const digest = await sha256.digest;
const digestStr = hexToDigest(digest);

// A reference containing ":" addresses the manifest by digest (an OCI tag never contains ":").
// The submitted content must hash to exactly that digest; a mismatched or malformed digest
// reference is a client error (400 DIGEST_INVALID), not a tag to store under the wrong key.
if (reference.includes(":") && reference !== digestStr) {
const message = isValidDigest(reference)
? `provided digest ${reference} does not match content digest ${digestStr}`
: `invalid digest reference ${reference}`;
return { response: new ManifestError("DIGEST_INVALID", message) };
}

const text = await blob.text();
let manifestJSON: unknown;
try {
Expand Down
52 changes: 52 additions & 0 deletions test/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2380,3 +2380,55 @@ test("docker.io", () => {
}
}
});

describe("manifest PUT digest validation", () => {
test("PUT manifest by a mismatched digest is rejected 400 DIGEST_INVALID (not stored)", async () => {
const name = "manifestdigest/mismatch";
const manifest = await generateManifest(name);
const data = JSON.stringify(manifest);
const wrongDigest = "sha256:" + "0".repeat(64);
const res = await fetch(
createRequest("PUT", `/v2/${name}/manifests/${wrongDigest}`, new Blob([data]).stream(), {
"Content-Type": "application/vnd.oci.image.manifest.v1+json",
}),
);
expect(res.status).toBe(400);
const body = (await res.json()) as { errors: { code: string }[] };
expect(body.errors[0].code).toBe("DIGEST_INVALID");
// Must NOT have been stored under the wrong digest key.
const get = await fetch(createRequest("GET", `/v2/${name}/manifests/${wrongDigest}`, null));
expect(get.status).toBe(404);
});

test("PUT manifest by a malformed digest reference is rejected 400 DIGEST_INVALID", async () => {
const name = "manifestdigest/malformed";
const manifest = await generateManifest(name);
const res = await fetch(
createRequest(
"PUT",
`/v2/${name}/manifests/sha256:baddigeststring`,
new Blob([JSON.stringify(manifest)]).stream(),
{
"Content-Type": "application/vnd.oci.image.manifest.v1+json",
},
),
);
expect(res.status).toBe(400);
const body = (await res.json()) as { errors: { code: string }[] };
expect(body.errors[0].code).toBe("DIGEST_INVALID");
});

test("PUT manifest by its correct digest still succeeds (201)", async () => {
const name = "manifestdigest/correct";
const manifest = await generateManifest(name);
const data = JSON.stringify(manifest);
const digest = await getSHA256(data);
const res = await fetch(
createRequest("PUT", `/v2/${name}/manifests/${digest}`, new Blob([data]).stream(), {
"Content-Type": "application/vnd.oci.image.manifest.v1+json",
}),
);
expect(res.status).toBe(201);
expect(res.headers.get("docker-content-digest")).toEqual(digest);
});
});