Skip to content

Commit d346c06

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 63be353 commit d346c06

File tree

3 files changed

+103
-26
lines changed

3 files changed

+103
-26
lines changed

image/internal/manifest/manifest.go

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -107,23 +107,39 @@ 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

126-
return digest.FromBytes(manifest), nil
128+
// Digest returns the 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+
return DigestWithAlgorithm(manifest, digest.Canonical)
132+
}
133+
134+
// DigestWithAlgorithm returns the digest of a docker manifest using the specified algorithm,
135+
// with any necessary implied transformations like stripping v1s1 signatures.
136+
// This is publicly visible as c/image/manifest.DigestWithAlgorithm.
137+
func DigestWithAlgorithm(manifest []byte, algo digest.Algorithm) (digest.Digest, error) {
138+
manifest, err := stripManifestSignature(manifest)
139+
if err != nil {
140+
return "", err
141+
}
142+
return algo.FromBytes(manifest), nil
127143
}
128144

129145
// MatchesDigest returns true iff the manifest matches expectedDigest.

image/internal/manifest/manifest_test.go

Lines changed: 75 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -46,31 +46,86 @@ func TestGuessMIMEType(t *testing.T) {
4646
}
4747
}
4848

49+
var digestTestCases = []struct {
50+
path string
51+
expectedSHA256 digest.Digest
52+
expectedSHA512 digest.Digest
53+
shouldFail bool
54+
}{
55+
{
56+
path: "v2s2.manifest.json",
57+
expectedSHA256: "sha256:20bf21ed457b390829cdbeec8795a7bea1626991fda603e0d01b4e7f60427e55",
58+
expectedSHA512: "sha512:50763a72163eef344fc0b58ec5a2676ceeddfa46b547475013778f3de5c0c1a75e18c947db36483e4622c1d46a908aa26649e6b0ac22514b8100889f74ed2b8c",
59+
},
60+
{
61+
path: "v2s1.manifest.json",
62+
expectedSHA256: "sha256:7364fea9d84ee548ab67d4c46c6006289800c98de3fbf8c0a97138dfcc23f000",
63+
expectedSHA512: "sha512:987d6df9aca32adc296bd0698cc7407f12605b4d5e8f0de2ca5d0c43f22d894082e96fb0a02c0f659db3bb8314912dd0a1fcb5cb421c04cd5cb468ad3829d9f7",
64+
},
65+
{
66+
path: "v2s1-unsigned.manifest.json",
67+
expectedSHA256: "sha256:7364fea9d84ee548ab67d4c46c6006289800c98de3fbf8c0a97138dfcc23f000",
68+
expectedSHA512: "sha512:987d6df9aca32adc296bd0698cc7407f12605b4d5e8f0de2ca5d0c43f22d894082e96fb0a02c0f659db3bb8314912dd0a1fcb5cb421c04cd5cb468ad3829d9f7",
69+
},
70+
{
71+
path: "",
72+
expectedSHA256: "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
73+
expectedSHA512: "sha512:cf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce47d0d13c5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da3e",
74+
},
75+
{
76+
path: "v2s1-invalid-signatures.manifest.json",
77+
shouldFail: true,
78+
},
79+
}
80+
4981
func TestDigest(t *testing.T) {
50-
cases := []struct {
51-
path string
52-
expectedDigest digest.Digest
53-
}{
54-
{"v2s2.manifest.json", TestDockerV2S2ManifestDigest},
55-
{"v2s1.manifest.json", TestDockerV2S1ManifestDigest},
56-
{"v2s1-unsigned.manifest.json", TestDockerV2S1UnsignedManifestDigest},
57-
}
58-
for _, c := range cases {
59-
manifest, err := os.ReadFile(filepath.Join("testdata", c.path))
60-
require.NoError(t, err)
82+
for _, c := range digestTestCases {
83+
var manifest []byte
84+
var err error
85+
if c.path == "" {
86+
manifest = []byte{}
87+
} else {
88+
manifest, err = os.ReadFile(filepath.Join("testdata", c.path))
89+
require.NoError(t, err)
90+
}
91+
6192
actualDigest, err := Digest(manifest)
62-
require.NoError(t, err)
63-
assert.Equal(t, c.expectedDigest, actualDigest)
93+
if c.shouldFail {
94+
assert.Error(t, err, c.path)
95+
} else {
96+
require.NoError(t, err, c.path)
97+
assert.Equal(t, c.expectedSHA256, actualDigest, c.path)
98+
}
6499
}
100+
}
65101

66-
manifest, err := os.ReadFile("testdata/v2s1-invalid-signatures.manifest.json")
67-
require.NoError(t, err)
68-
_, err = Digest(manifest)
69-
assert.Error(t, err)
102+
func TestDigestWithAlgorithm(t *testing.T) {
103+
for _, c := range digestTestCases {
104+
var manifest []byte
105+
var err error
106+
if c.path == "" {
107+
manifest = []byte{}
108+
} else {
109+
manifest, err = os.ReadFile(filepath.Join("testdata", c.path))
110+
require.NoError(t, err)
111+
}
70112

71-
actualDigest, err := Digest([]byte{})
72-
require.NoError(t, err)
73-
assert.Equal(t, digest.Digest(digestSha256EmptyTar), actualDigest)
113+
sha256Digest, err := DigestWithAlgorithm(manifest, digest.SHA256)
114+
if c.shouldFail {
115+
assert.Error(t, err, c.path)
116+
} else {
117+
require.NoError(t, err, c.path)
118+
assert.Equal(t, c.expectedSHA256, sha256Digest, c.path)
119+
}
120+
121+
sha512Digest, err := DigestWithAlgorithm(manifest, digest.SHA512)
122+
if c.shouldFail {
123+
assert.Error(t, err, c.path)
124+
} else {
125+
require.NoError(t, err, c.path)
126+
assert.Equal(t, c.expectedSHA512, sha512Digest, c.path)
127+
}
128+
}
74129
}
75130

76131
func TestMatchesDigest(t *testing.T) {

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)