diff --git a/docker/utils/manifest.go b/docker/utils/manifest.go index ac943458..be9a86ed 100644 --- a/docker/utils/manifest.go +++ b/docker/utils/manifest.go @@ -64,3 +64,16 @@ func ManifestDigest(manifest []byte) (string, error) { hash := sha256.Sum256(manifest) return "sha256:" + hex.EncodeToString(hash[:]), nil } + +// ManifestMatchesDigest returns true iff the manifest matches expectedDigest. +// Error may be set if this returns false. +// Note that this is not doing ConstantTimeCompare; by the time we get here, the cryptographic signature must already have been verified, +// or we are not using a cryptographic channel and the attacker can modify the digest along with the manifest blob. +func ManifestMatchesDigest(manifest []byte, expectedDigest string) (bool, error) { + // This should eventually support various digest types. + actualDigest, err := ManifestDigest(manifest) + if err != nil { + return false, err + } + return expectedDigest == actualDigest, nil +} diff --git a/docker/utils/manifest_test.go b/docker/utils/manifest_test.go index ca4c7c6d..68e78ac4 100644 --- a/docker/utils/manifest_test.go +++ b/docker/utils/manifest_test.go @@ -1,6 +1,8 @@ package utils import ( + "crypto/sha256" + "encoding/hex" "io/ioutil" "path/filepath" "testing" @@ -56,3 +58,43 @@ func TestManifestDigest(t *testing.T) { require.NoError(t, err) assert.Equal(t, "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", digest) } + +func TestManifestMatchesDigest(t *testing.T) { + cases := []struct { + path string + digest string + result bool + }{ + // Success + {"v2s2.manifest.json", TestV2S2ManifestDigest, true}, + {"v2s1.manifest.json", TestV2S1ManifestDigest, true}, + // No match (switched s1/s2) + {"v2s2.manifest.json", TestV2S1ManifestDigest, false}, + {"v2s1.manifest.json", TestV2S2ManifestDigest, false}, + // Unrecognized algorithm + {"v2s2.manifest.json", "md5:2872f31c5c1f62a694fbd20c1e85257c", false}, + // Mangled format + {"v2s2.manifest.json", TestV2S2ManifestDigest + "abc", false}, + {"v2s2.manifest.json", TestV2S2ManifestDigest[:20], false}, + {"v2s2.manifest.json", "", false}, + } + for _, c := range cases { + manifest, err := ioutil.ReadFile(filepath.Join("fixtures", c.path)) + require.NoError(t, err) + res, err := ManifestMatchesDigest(manifest, c.digest) + require.NoError(t, err) + assert.Equal(t, c.result, res) + } + + manifest, err := ioutil.ReadFile("fixtures/v2s1-invalid-signatures.manifest.json") + require.NoError(t, err) + // Even a correct SHA256 hash is rejected if we can't strip the JSON signature. + hash := sha256.Sum256(manifest) + res, err := ManifestMatchesDigest(manifest, "sha256:"+hex.EncodeToString(hash[:])) + assert.False(t, res) + assert.Error(t, err) + + res, err = ManifestMatchesDigest([]byte{}, "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855") + assert.True(t, res) + assert.NoError(t, err) +}