Skip to content

Commit bdbac34

Browse files
committed
image/manifest: Add DigestWithAlgorithm function
Add a new `manifest.DigestWithAlgorithm` function that allows computing the digest of a manifest using a specified algorithm (e.g., SHA256, SHA512) while properly handling v2s1 signed manifest signature stripping. This addresses the need for skopeo's `--manifest-digest` flag to support different digest algorithms while correctly handling all manifest types, particularly Docker v2s1 signed manifests that require signature stripping before digest computation. Signed-off-by: Lokesh Mandvekar <[email protected]>
1 parent 9cd39ec commit bdbac34

File tree

3 files changed

+90
-5
lines changed

3 files changed

+90
-5
lines changed

image/internal/manifest/manifest.go

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -107,25 +107,45 @@ func GuessMIMEType(manifest []byte) string {
107107
return ""
108108
}
109109

110-
// Digest returns the a digest of a docker manifest, with any necessary implied transformations like stripping v1s1 signatures.
111-
// This is publicly visible as c/image/manifest.Digest.
112-
func Digest(manifest []byte) (digest.Digest, error) {
110+
// stripManifestSignature strips v1s1 signatures from a manifest if present.
111+
// Returns the manifest bytes (either the original or the unsigned payload).
112+
func stripManifestSignature(manifest []byte) ([]byte, error) {
113113
if GuessMIMEType(manifest) == DockerV2Schema1SignedMediaType {
114114
sig, err := libtrust.ParsePrettySignature(manifest, "signatures")
115115
if err != nil {
116-
return "", err
116+
return nil, err
117117
}
118118
manifest, err = sig.Payload()
119119
if err != nil {
120120
// Coverage: This should never happen, libtrust's Payload() can fail only if joseBase64UrlDecode() fails, on a string
121121
// that libtrust itself has josebase64UrlEncode()d
122-
return "", err
122+
return nil, err
123123
}
124124
}
125+
return manifest, nil
126+
}
125127

128+
// Digest returns the a digest of a docker manifest, with any necessary implied transformations like stripping v1s1 signatures.
129+
// This is publicly visible as c/image/manifest.Digest.
130+
func Digest(manifest []byte) (digest.Digest, error) {
131+
manifest, err := stripManifestSignature(manifest)
132+
if err != nil {
133+
return "", err
134+
}
126135
return digest.FromBytes(manifest), nil
127136
}
128137

138+
// DigestWithAlgorithm returns the digest of a docker manifest using the specified algorithm,
139+
// with any necessary implied transformations like stripping v1s1 signatures.
140+
// This is publicly visible as c/image/manifest.DigestWithAlgorithm.
141+
func DigestWithAlgorithm(manifest []byte, algo digest.Algorithm) (digest.Digest, error) {
142+
manifest, err := stripManifestSignature(manifest)
143+
if err != nil {
144+
return "", err
145+
}
146+
return algo.FromBytes(manifest), nil
147+
}
148+
129149
// MatchesDigest returns true iff the manifest matches expectedDigest.
130150
// Error may be set if this returns false.
131151
// Note that this is not doing ConstantTimeCompare; by the time we get here, the cryptographic signature must already have been verified,

image/internal/manifest/manifest_test.go

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,65 @@ func TestDigest(t *testing.T) {
7373
assert.Equal(t, digest.Digest(digestSha256EmptyTar), actualDigest)
7474
}
7575

76+
func TestDigestWithAlgorithm(t *testing.T) {
77+
sha256Cases := []struct {
78+
path string
79+
expectedDigest digest.Digest
80+
}{
81+
{"v2s2.manifest.json", TestDockerV2S2ManifestDigest},
82+
{"v2s1.manifest.json", TestDockerV2S1ManifestDigest},
83+
{"v2s1-unsigned.manifest.json", TestDockerV2S1UnsignedManifestDigest},
84+
}
85+
for _, c := range sha256Cases {
86+
manifest, err := os.ReadFile(filepath.Join("testdata", c.path))
87+
require.NoError(t, err)
88+
actualDigest, err := DigestWithAlgorithm(manifest, digest.SHA256)
89+
require.NoError(t, err)
90+
assert.Equal(t, c.expectedDigest, actualDigest, c.path)
91+
defaultDigest, err := Digest(manifest)
92+
require.NoError(t, err)
93+
assert.Equal(t, defaultDigest, actualDigest, c.path)
94+
}
95+
96+
// Test with SHA512
97+
for _, c := range sha256Cases {
98+
manifest, err := os.ReadFile(filepath.Join("testdata", c.path))
99+
require.NoError(t, err)
100+
actualDigest, err := DigestWithAlgorithm(manifest, digest.SHA512)
101+
require.NoError(t, err)
102+
assert.Equal(t, digest.SHA512, actualDigest.Algorithm())
103+
sha256Digest, err := DigestWithAlgorithm(manifest, digest.SHA256)
104+
require.NoError(t, err)
105+
assert.NotEqual(t, sha256Digest, actualDigest, c.path)
106+
}
107+
108+
// Test that v2s1 signed manifest signature stripping works with different algorithms
109+
manifest, err := os.ReadFile("testdata/v2s1.manifest.json")
110+
require.NoError(t, err)
111+
unsignedManifest, err := os.ReadFile("testdata/v2s1-unsigned.manifest.json")
112+
require.NoError(t, err)
113+
114+
// Both signed and unsigned should produce the same digest for each algorithm
115+
for _, algo := range []digest.Algorithm{digest.SHA256, digest.SHA512} {
116+
signedDigest, err := DigestWithAlgorithm(manifest, algo)
117+
require.NoError(t, err)
118+
unsignedDigest, err := DigestWithAlgorithm(unsignedManifest, algo)
119+
require.NoError(t, err)
120+
assert.Equal(t, unsignedDigest, signedDigest, "algorithm: %s", algo)
121+
}
122+
123+
manifest, err = os.ReadFile("testdata/v2s1-invalid-signatures.manifest.json")
124+
require.NoError(t, err)
125+
_, err = DigestWithAlgorithm(manifest, digest.SHA256)
126+
assert.Error(t, err)
127+
_, err = DigestWithAlgorithm(manifest, digest.SHA512)
128+
assert.Error(t, err)
129+
130+
actualDigest, err := DigestWithAlgorithm([]byte{}, digest.SHA256)
131+
require.NoError(t, err)
132+
assert.Equal(t, digest.Digest(digestSha256EmptyTar), actualDigest)
133+
}
134+
76135
func TestMatchesDigest(t *testing.T) {
77136
cases := []struct {
78137
path string

image/manifest/manifest.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,12 @@ func Digest(manifestBlob []byte) (digest.Digest, error) {
113113
return manifest.Digest(manifestBlob)
114114
}
115115

116+
// DigestWithAlgorithm returns the digest of a docker manifest using the specified algorithm,
117+
// with any necessary implied transformations like stripping v1s1 signatures.
118+
func DigestWithAlgorithm(manifestBlob []byte, algo digest.Algorithm) (digest.Digest, error) {
119+
return manifest.DigestWithAlgorithm(manifestBlob, algo)
120+
}
121+
116122
// MatchesDigest returns true iff the manifest matches expectedDigest.
117123
// Error may be set if this returns false.
118124
// Note that this is not doing ConstantTimeCompare; by the time we get here, the cryptographic signature must already have been verified,

0 commit comments

Comments
 (0)