···217217 }
218218 }
219219220220- // Store the validated manifest
220220+ // Calculate digest first (will be used for storage and header)
221221+ let digest = sha256::digest(bytes.as_ref());
222222+223223+ // Store the validated manifest by the requested reference (tag or digest)
221224 let success = storage::write_manifest_bytes(&org, &repo, &reference, &bytes).await;
222225 if !success {
223226 return response::manifest_invalid("failed to write manifest");
224227 }
225228229229+ // If reference is a tag (not a digest), also store by digest for retrieval by digest
230230+ // This allows manifests to be retrieved both by tag and by content-addressable digest
231231+ // Note: We store without "sha256:" prefix to match how GET strips the prefix
232232+ if !reference.starts_with("sha256:") {
233233+ storage::write_manifest_bytes(&org, &repo, &digest, &bytes).await;
234234+ }
235235+226236 metrics::MANIFEST_UPLOADS_TOTAL.inc();
227237228238 Response::builder()
···231241 "Location",
232242 format!("/v2/{}/{}/manifests/{}", org, repo, reference),
233243 )
244244+ .header("Docker-Content-Digest", format!("sha256:{}", digest))
234245 .body(Body::empty())
235246 .expect("Failed to build response")
236247}
···167167 let entry = entry?;
168168 if entry.path().is_file() {
169169 if let Some(filename) = entry.file_name().to_str() {
170170- // Filter out digest references (start with sha256:)
170170+ // Filter out digest references (64-char hex strings or sha256: prefixed)
171171 // Only include tag names
172172- if !filename.starts_with("sha256:") {
172172+ let is_digest = filename.starts_with("sha256:")
173173+ || (filename.len() == 64 && filename.chars().all(|c| c.is_ascii_hexdigit()));
174174+175175+ if !is_digest {
173176 tags.push(filename.to_string());
174177 }
175178 }