diff --git a/dockerutils/fixtures/info.go b/dockerutils/fixtures/info.go new file mode 100644 index 00000000..1ee7e09d --- /dev/null +++ b/dockerutils/fixtures/info.go @@ -0,0 +1,8 @@ +package fixtures + +const ( + // TestV2S2ManifestDigest is the Docker manifest digest of "v2s2.manifest.json" + TestV2S2ManifestDigest = "sha256:20bf21ed457b390829cdbeec8795a7bea1626991fda603e0d01b4e7f60427e55" + // TestV2S1ManifestDigest is the Docker manifest digest of "v2s1.manifest.json" + TestV2S1ManifestDigest = "sha256:077594da70fc17ec2c93cfa4e6ed1fcc26992851fb2c71861338aaf4aa9e41b1" +) diff --git a/dockerutils/fixtures/non-json.manifest.json b/dockerutils/fixtures/non-json.manifest.json new file mode 100644 index 00000000..f8927212 Binary files /dev/null and b/dockerutils/fixtures/non-json.manifest.json differ diff --git a/signature/fixtures/unknown-version.manifest.json b/dockerutils/fixtures/unknown-version.manifest.json similarity index 100% rename from signature/fixtures/unknown-version.manifest.json rename to dockerutils/fixtures/unknown-version.manifest.json diff --git a/dockerutils/fixtures/v2s1-invalid-signatures.manifest.json b/dockerutils/fixtures/v2s1-invalid-signatures.manifest.json new file mode 100644 index 00000000..8dfefd4e --- /dev/null +++ b/dockerutils/fixtures/v2s1-invalid-signatures.manifest.json @@ -0,0 +1,11 @@ +{ + "schemaVersion": 1, + "name": "mitr/buxybox", + "tag": "latest", + "architecture": "amd64", + "fsLayers": [ + ], + "history": [ + ], + "signatures": 1 +} diff --git a/signature/fixtures/v1s1.manifest.json b/dockerutils/fixtures/v2s1.manifest.json similarity index 100% rename from signature/fixtures/v1s1.manifest.json rename to dockerutils/fixtures/v2s1.manifest.json diff --git a/dockerutils/fixtures/v2s2.manifest.json b/dockerutils/fixtures/v2s2.manifest.json new file mode 100644 index 00000000..198da23f --- /dev/null +++ b/dockerutils/fixtures/v2s2.manifest.json @@ -0,0 +1,26 @@ +{ + "schemaVersion": 2, + "mediaType": "application/vnd.docker.distribution.manifest.v2+json", + "config": { + "mediaType": "application/vnd.docker.container.image.v1+json", + "size": 7023, + "digest": "sha256:b5b2b2c507a0944348e0303114d8d93aaaa081732b86451d9bce1f432a537bc7" + }, + "layers": [ + { + "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip", + "size": 32654, + "digest": "sha256:e692418e4cbaf90ca69d05a66403747baa33ee08806650b51fab815ad7fc331f" + }, + { + "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip", + "size": 16724, + "digest": "sha256:3c3a4604a545cdc127456d94e421cd355bca5b528f4a9c1905b15da2eb4a4c6b" + }, + { + "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip", + "size": 73109, + "digest": "sha256:ec4b8955958665577945c89419d1af06b5f7636b4ac3da7f12184802ad867736" + } + ] +} \ No newline at end of file diff --git a/signature/fixtures/v2s2nomime.manifest.json b/dockerutils/fixtures/v2s2nomime.manifest.json similarity index 100% rename from signature/fixtures/v2s2nomime.manifest.json rename to dockerutils/fixtures/v2s2nomime.manifest.json diff --git a/dockerutils/manifest.go b/dockerutils/manifest.go new file mode 100644 index 00000000..464c129d --- /dev/null +++ b/dockerutils/manifest.go @@ -0,0 +1,65 @@ +package dockerutils + +import ( + "crypto/sha256" + "encoding/hex" + "encoding/json" + + "github.com/docker/libtrust" +) + +// FIXME: Should we just use docker/distribution and docker/docker implementations directly? + +// A string representing a Docker manifest MIME type +type manifestMIMEType string + +const ( + dockerV2Schema1MIMEType manifestMIMEType = "application/vnd.docker.distribution.manifest.v1+json" + dockerV2Schema2MIMEType manifestMIMEType = "application/vnd.docker.distribution.manifest.v2+json" +) + +// guessManifestMIMEType guesses MIME type of a manifest and returns it _if it is recognized_, or "" if unknown or unrecognized. +// FIXME? We should, in general, prefer out-of-band MIME type instead of blindly parsing the manifest, +// but we may not have such metadata available (e.g. when the manifest is a local file). +func guessManifestMIMEType(manifest []byte) manifestMIMEType { + // A subset of manifest fields; the rest is silently ignored by json.Unmarshal. + // Also docker/distribution/manifest.Versioned. + meta := struct { + MediaType string `json:"mediaType"` + SchemaVersion int `json:"schemaVersion"` + }{} + if err := json.Unmarshal(manifest, &meta); err != nil { + return "" + } + + switch meta.MediaType { + case string(dockerV2Schema2MIMEType): // A recognized type. + return manifestMIMEType(meta.MediaType) + } + switch meta.SchemaVersion { + case 1: + return dockerV2Schema1MIMEType + case 2: // Really should not happen, meta.MediaType should have been set. But given the data, this is our best guess. + return dockerV2Schema2MIMEType + } + return "" +} + +// ManifestDigest returns the a digest of a docker manifest, with any necessary implied transformations like stripping v1s1 signatures. +func ManifestDigest(manifest []byte) (string, error) { + if guessManifestMIMEType(manifest) == dockerV2Schema1MIMEType { + sig, err := libtrust.ParsePrettySignature(manifest, "signatures") + if err != nil { + return "", err + } + manifest, err = sig.Payload() + if err != nil { + // Coverage: This should never happen, libtrust's Payload() can fail only if joseBase64UrlDecode() fails, on a string + // that libtrust itself has josebase64UrlEncode()d + return "", err + } + } + + hash := sha256.Sum256(manifest) + return "sha256:" + hex.EncodeToString(hash[:]), nil +} diff --git a/dockerutils/manifest_test.go b/dockerutils/manifest_test.go new file mode 100644 index 00000000..5f325c0d --- /dev/null +++ b/dockerutils/manifest_test.go @@ -0,0 +1,58 @@ +package dockerutils + +import ( + "io/ioutil" + "path/filepath" + "testing" + + "github.com/projectatomic/skopeo/dockerutils/fixtures" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestGuessManifestMIMEType(t *testing.T) { + cases := []struct { + path string + mimeType manifestMIMEType + }{ + {"v2s2.manifest.json", dockerV2Schema2MIMEType}, + {"v2s1.manifest.json", dockerV2Schema1MIMEType}, + {"v2s1-invalid-signatures.manifest.json", dockerV2Schema1MIMEType}, + {"v2s2nomime.manifest.json", dockerV2Schema2MIMEType}, // It is unclear whether this one is legal, but we should guess v2s2 if anything at all. + {"unknown-version.manifest.json", ""}, + {"non-json.manifest.json", ""}, // Not a manifest (nor JSON) at all + } + + for _, c := range cases { + manifest, err := ioutil.ReadFile(filepath.Join("fixtures", c.path)) + require.NoError(t, err) + mimeType := guessManifestMIMEType(manifest) + assert.Equal(t, c.mimeType, mimeType) + } +} + +func TestManifestDigest(t *testing.T) { + cases := []struct { + path string + digest string + }{ + {"v2s2.manifest.json", fixtures.TestV2S2ManifestDigest}, + {"v2s1.manifest.json", fixtures.TestV2S1ManifestDigest}, + } + for _, c := range cases { + manifest, err := ioutil.ReadFile(filepath.Join("fixtures", c.path)) + require.NoError(t, err) + digest, err := ManifestDigest(manifest) + require.NoError(t, err) + assert.Equal(t, c.digest, digest) + } + + manifest, err := ioutil.ReadFile("fixtures/v2s1-invalid-signatures.manifest.json") + require.NoError(t, err) + digest, err := ManifestDigest(manifest) + assert.Error(t, err) + + digest, err = ManifestDigest([]byte{}) + require.NoError(t, err) + assert.Equal(t, "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", digest) +} diff --git a/signature/docker.go b/signature/docker.go index 6bfed67b..386289dd 100644 --- a/signature/docker.go +++ b/signature/docker.go @@ -3,71 +3,15 @@ package signature import ( - "crypto/sha256" - "encoding/hex" - "encoding/json" "fmt" - "github.com/docker/libtrust" + "github.com/projectatomic/skopeo/dockerutils" ) -// A string representing a Docker manifest MIME type -type manifestMIMEType string - -const ( - dockerV2Schema1MIMEType manifestMIMEType = "application/vnd.docker.distribution.manifest.v1+json" - dockerV2Schema2MIMEType manifestMIMEType = "application/vnd.docker.distribution.manifest.v2+json" -) - -// guessManifestMIMEType guesses MIME type of a manifest and returns it _if it is recognized_, or "" if unknown or unrecognized. -// FIXME? This should ideally use out-of-band MIME type instead of blindly parsing the manifest, -// but we may not have such metadata available (e.g. when the manifest is a local file). -func guessManifestMIMEType(manifest []byte) manifestMIMEType { - // A subset of manifest fields; the rest is silently ignored by json.Unmarshal. - // Also docker/distribution/manifest.Versioned. - meta := struct { - MediaType string `json:"mediaType"` - SchemaVersion int `json:"schemaVersion"` - }{} - if err := json.Unmarshal(manifest, &meta); err != nil { - return "" - } - - switch meta.MediaType { - case string(dockerV2Schema2MIMEType): // A recognized type. - return manifestMIMEType(meta.MediaType) - } - switch meta.SchemaVersion { - case 1: - return dockerV2Schema1MIMEType - case 2: // Really should not happen, meta.MediaType should have been set. But given the data, this is our best guess. - return dockerV2Schema2MIMEType - } - return "" -} - -func dockerManifestDigest(manifest []byte) (string, error) { - if guessManifestMIMEType(manifest) == dockerV2Schema1MIMEType { - sig, err := libtrust.ParsePrettySignature(manifest, "signatures") - if err != nil { - return "", err - } - manifest, err = sig.Payload() - if err != nil { - // Coverage: This should never happen, libtrust's Payload() can fail only if joseBase64UrlDecode() fails, on a string - // that libtrust itself has josebase64UrlEncode()d - return "", err - } - } - - hash := sha256.Sum256(manifest) - return "sha256:" + hex.EncodeToString(hash[:]), nil -} - // SignDockerManifest returns a signature for manifest as the specified dockerReference, // using mech and keyIdentity. func SignDockerManifest(manifest []byte, dockerReference string, mech SigningMechanism, keyIdentity string) ([]byte, error) { - manifestDigest, err := dockerManifestDigest(manifest) + manifestDigest, err := dockerutils.ManifestDigest(manifest) if err != nil { return nil, err } @@ -84,7 +28,7 @@ func SignDockerManifest(manifest []byte, dockerReference string, mech SigningMec // using mech. func VerifyDockerManifestSignature(unverifiedSignature, unverifiedManifest []byte, expectedDockerReference string, mech SigningMechanism, expectedKeyIdentity string) (*Signature, error) { - expectedManifestDigest, err := dockerManifestDigest(unverifiedManifest) + expectedManifestDigest, err := dockerutils.ManifestDigest(unverifiedManifest) if err != nil { return nil, err } diff --git a/signature/docker_test.go b/signature/docker_test.go index 616f47dc..205d74ff 100644 --- a/signature/docker_test.go +++ b/signature/docker_test.go @@ -2,7 +2,6 @@ package signature import ( "io/ioutil" - "path/filepath" "testing" "github.com/projectatomic/skopeo/signature/fixtures" @@ -10,53 +9,6 @@ import ( "github.com/stretchr/testify/require" ) -func TestGuessManifestMIMEType(t *testing.T) { - cases := []struct { - path string - mimeType manifestMIMEType - }{ - {"image.manifest.json", dockerV2Schema2MIMEType}, - {"v1s1.manifest.json", dockerV2Schema1MIMEType}, - {"v1s1-invalid-signatures.manifest.json", dockerV2Schema1MIMEType}, - {"v2s2nomime.manifest.json", dockerV2Schema2MIMEType}, // It is unclear whether this one is legal, but we should guess v2s2 if anything at all. - {"unknown-version.manifest.json", ""}, - {"image.signature", ""}, // Not a manifest (nor JSON) at all - } - - for _, c := range cases { - manifest, err := ioutil.ReadFile(filepath.Join("fixtures", c.path)) - require.NoError(t, err) - mimeType := guessManifestMIMEType(manifest) - assert.Equal(t, c.mimeType, mimeType) - } -} - -func TestDockerManifestDigest(t *testing.T) { - cases := []struct { - path string - digest string - }{ - {"image.manifest.json", fixtures.TestImageManifestDigest}, - {"v1s1.manifest.json", fixtures.TestV1S1ManifestDigest}, - } - for _, c := range cases { - manifest, err := ioutil.ReadFile(filepath.Join("fixtures", c.path)) - require.NoError(t, err) - digest, err := dockerManifestDigest(manifest) - require.NoError(t, err) - assert.Equal(t, c.digest, digest) - } - - manifest, err := ioutil.ReadFile("fixtures/v1s1-invalid-signatures.manifest.json") - require.NoError(t, err) - digest, err := dockerManifestDigest(manifest) - assert.Error(t, err) - - digest, err = dockerManifestDigest([]byte{}) - require.NoError(t, err) - assert.Equal(t, "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", digest) -} - func TestSignDockerManifest(t *testing.T) { mech, err := newGPGSigningMechanismInDirectory(testGPGHomeDirectory) require.NoError(t, err) diff --git a/signature/fixtures/info.go b/signature/fixtures/info.go index 74b849e5..fedbde77 100644 --- a/signature/fixtures/info.go +++ b/signature/fixtures/info.go @@ -3,8 +3,6 @@ package fixtures const ( // TestImageManifestDigest is the Docker manifest digest of "image.manifest.json" TestImageManifestDigest = "sha256:20bf21ed457b390829cdbeec8795a7bea1626991fda603e0d01b4e7f60427e55" - // TestV1S1ManifestDigest is the Docker manifest digest of "v1s1.manifest.json" - TestV1S1ManifestDigest = "sha256:077594da70fc17ec2c93cfa4e6ed1fcc26992851fb2c71861338aaf4aa9e41b1" // TestImageSignatureReference is the Docker image reference signed in "image.signature" TestImageSignatureReference = "testing/manifest" // TestKeyFingerprint is the fingerprint of the private key in this directory.