mirror of
https://github.com/containers/skopeo.git
synced 2025-07-17 00:02:03 +00:00
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:
parent
7a7dd84818
commit
23899acadd
8
dockerutils/fixtures/info.go
Normal file
8
dockerutils/fixtures/info.go
Normal 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"
|
||||
)
|
BIN
dockerutils/fixtures/non-json.manifest.json
Normal file
BIN
dockerutils/fixtures/non-json.manifest.json
Normal file
Binary file not shown.
11
dockerutils/fixtures/v2s1-invalid-signatures.manifest.json
Normal file
11
dockerutils/fixtures/v2s1-invalid-signatures.manifest.json
Normal file
@ -0,0 +1,11 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"name": "mitr/buxybox",
|
||||
"tag": "latest",
|
||||
"architecture": "amd64",
|
||||
"fsLayers": [
|
||||
],
|
||||
"history": [
|
||||
],
|
||||
"signatures": 1
|
||||
}
|
26
dockerutils/fixtures/v2s2.manifest.json
Normal file
26
dockerutils/fixtures/v2s2.manifest.json
Normal 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
65
dockerutils/manifest.go
Normal 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
|
||||
}
|
58
dockerutils/manifest_test.go
Normal file
58
dockerutils/manifest_test.go
Normal 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)
|
||||
}
|
@ -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
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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.
|
||||
|
Loading…
Reference in New Issue
Block a user