Create a new subpackage "dockerutils", starting with manifest computation

Move the manifest computation (with v2s1 signature stripping) out of
skopeo/signature into a separate package; it is necessary in the
OpenShift client as well, unrelated to signatures.

Other Docker-specific utilities, like getting a list of layer blobsums
from a manifest, may be also moved here in the future.
This commit is contained in:
Miloslav Trmač 2016-04-21 16:42:23 +02:00
parent 7a7dd84818
commit 23899acadd
12 changed files with 171 additions and 109 deletions

View File

@ -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"
)

Binary file not shown.

View File

@ -0,0 +1,11 @@
{
"schemaVersion": 1,
"name": "mitr/buxybox",
"tag": "latest",
"architecture": "amd64",
"fsLayers": [
],
"history": [
],
"signatures": 1
}

View File

@ -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"
}
]
}

65
dockerutils/manifest.go Normal file
View File

@ -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
}

View File

@ -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)
}

View File

@ -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
}

View File

@ -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)

View File

@ -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.