Skip to content

Commit b7fc7d9

Browse files
ralphbeanclaude
andcommitted
Add org.opencontainers.image.title annotation to attestation layers
Add the org.opencontainers.image.title annotation to layer descriptors in attestation manifests to enable tools like 'oras pull' to download attestation bundles with meaningful, collision-free filenames. The annotation format is {algorithm}-{hex}.sigstore.json where the hyphen separator ensures cross-platform filename compatibility, particularly for Windows which forbids colons in filenames. Changes: - Add Annotations field to layer descriptors in WriteReferrer - Update tests to verify annotation is set correctly - Document the optional layer annotation in BUNDLE_SPEC.md Fixes #4497 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]> Signed-off-by: Ralph Bean <[email protected]>
1 parent f9a9a0b commit b7fc7d9

File tree

3 files changed

+79
-4
lines changed

3 files changed

+79
-4
lines changed

pkg/oci/remote/write.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -277,6 +277,9 @@ func WriteReferrer(d name.Digest, artifactType string, layers []v1.Layer, annota
277277
MediaType: mediaType,
278278
Digest: layerDigest,
279279
Size: layerSize,
280+
Annotations: map[string]string{
281+
"org.opencontainers.image.title": fmt.Sprintf("%s-%s.sigstore.json", layerDigest.Algorithm, layerDigest.Hex),
282+
},
280283
}
281284
}
282285

pkg/oci/remote/write_test.go

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,24 @@ func TestWriteAttestationNewBundleFormat(t *testing.T) {
251251
if refManifest.Annotations["dev.sigstore.bundle.predicateType"] != predicateType {
252252
t.Errorf("Expected predicateType annotation to be %s, got %s", predicateType, refManifest.Annotations["dev.sigstore.bundle.predicateType"])
253253
}
254+
255+
// Verify the layer has the org.opencontainers.image.title annotation
256+
if len(refManifest.Layers) == 0 {
257+
t.Fatal("Expected at least one layer in manifest")
258+
}
259+
layer := refManifest.Layers[0]
260+
if layer.Annotations == nil {
261+
t.Fatal("Expected layer to have annotations, but Annotations is nil")
262+
}
263+
title, ok := layer.Annotations["org.opencontainers.image.title"]
264+
if !ok {
265+
t.Error("Expected layer to have 'org.opencontainers.image.title' annotation, but it was not found")
266+
}
267+
// Verify the title format matches {algorithm}-{hex}.sigstore.json
268+
expectedTitle := fmt.Sprintf("%s-%s.sigstore.json", layer.Digest.Algorithm, layer.Digest.Hex)
269+
if title != expectedTitle {
270+
t.Errorf("Expected layer title to be %s, got %s", expectedTitle, title)
271+
}
254272
}
255273

256274
func TestWriteAttestationsReferrer(t *testing.T) {
@@ -333,7 +351,22 @@ func TestWriteAttestationsReferrer(t *testing.T) {
333351

334352
// Verify we have at least one layer
335353
if len(refManifest.Layers) == 0 {
336-
t.Error("Expected at least one layer in manifest")
354+
t.Fatal("Expected at least one layer in manifest")
355+
}
356+
// Verify each layer has the org.opencontainers.image.title annotation
357+
for i, layer := range refManifest.Layers {
358+
if layer.Annotations == nil {
359+
t.Fatalf("Expected layer %d to have annotations, but Annotations is nil", i)
360+
}
361+
title, ok := layer.Annotations["org.opencontainers.image.title"]
362+
if !ok {
363+
t.Errorf("Expected layer %d to have 'org.opencontainers.image.title' annotation, but it was not found", i)
364+
}
365+
// Verify the title format matches {algorithm}-{hex}.sigstore.json
366+
expectedTitle := fmt.Sprintf("%s-%s.sigstore.json", layer.Digest.Algorithm, layer.Digest.Hex)
367+
if title != expectedTitle {
368+
t.Errorf("Expected layer %d title to be %s, got %s", i, expectedTitle, title)
369+
}
337370
}
338371
}
339372

@@ -411,6 +444,24 @@ func TestWriteReferrer(t *testing.T) {
411444
t.Errorf("Expected 1 layer, got %d", len(refManifest.Layers))
412445
}
413446

447+
// Verify the layer has the org.opencontainers.image.title annotation
448+
if len(refManifest.Layers) == 0 {
449+
t.Fatal("Expected at least one layer in manifest")
450+
}
451+
layer := refManifest.Layers[0]
452+
if layer.Annotations == nil {
453+
t.Fatal("Expected layer to have annotations, but Annotations is nil")
454+
}
455+
title, ok := layer.Annotations["org.opencontainers.image.title"]
456+
if !ok {
457+
t.Error("Expected layer to have 'org.opencontainers.image.title' annotation, but it was not found")
458+
}
459+
// Verify the title format matches {algorithm}-{hex}.sigstore.json
460+
expectedTitle := fmt.Sprintf("%s-%s.sigstore.json", layer.Digest.Algorithm, layer.Digest.Hex)
461+
if title != expectedTitle {
462+
t.Errorf("Expected layer title to be %s, got %s", expectedTitle, title)
463+
}
464+
414465
// Verify the subject is set
415466
if refManifest.Subject == nil {
416467
t.Error("Expected Subject to be set")

specs/BUNDLE_SPEC.md

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,10 @@ Content-Type: application/vnd.oci.image.manifest.v1+json
6565
{
6666
"digest": "sha256:cafed00d...",
6767
"mediaType": "application/vnd.dev.sigstore.bundle.v0.3+json",
68-
"size": 4971
68+
"size": 4971,
69+
"annotations": {
70+
"org.opencontainers.image.title": "sha256-cafed00d.sigstore.json"
71+
}
6972
}
7073
],
7174
"subject": {
@@ -184,7 +187,10 @@ GET /v2/foo/manifests/sha256:badf00d..
184187
{
185188
"digest": "sha256:cafed00d...",
186189
"mediaType": "application/vnd.dev.sigstore.bundle.v0.3+json",
187-
"size": 4971
190+
"size": 4971,
191+
"annotations": {
192+
"org.opencontainers.image.title": "sha256-cafed00d.sigstore.json"
193+
}
188194
}
189195
],
190196
"subject": {
@@ -249,6 +255,18 @@ when it was created:
249255
the pre-defined annotation keys identified in the
250256
[OCI spec](https://github.com/opencontainers/image-spec/blob/main/annotations.md#pre-defined-annotation-keys)).
251257

258+
### Layer Annotations
259+
260+
In addition to manifest-level annotations, individual layer descriptors within
261+
the bundle manifest may optionally include the `org.opencontainers.image.title`
262+
annotation to provide a meaningful filename for the attestation bundle:
263+
264+
- `org.opencontainers.image.title` (optional) - A suggested filename for the
265+
layer content, formatted as `{digest-algorithm}-{digest-hex}.sigstore.json`.
266+
The hyphen separator (rather than colon) ensures the filename is valid across
267+
all platforms, including Windows. This enables tools like `oras pull` to save
268+
attestation bundles with collision-free, human-readable filenames.
269+
252270
These annotations should be included as part of the bundle manifest:
253271

254272
```json
@@ -270,7 +288,10 @@ These annotations should be included as part of the bundle manifest:
270288
{
271289
"digest": "sha256:cafed00d...",
272290
"mediaType": "application/vnd.dev.sigstore.bundle.v0.3+json",
273-
"size": 4971
291+
"size": 4971,
292+
"annotations": {
293+
"org.opencontainers.image.title": "sha256-cafed00d.sigstore.json"
294+
}
274295
}
275296
],
276297
"subject": {

0 commit comments

Comments
 (0)