mirror of
https://github.com/containers/skopeo.git
synced 2025-07-18 16:51:41 +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
|
package signature
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/sha256"
|
|
||||||
"encoding/hex"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
"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,
|
// SignDockerManifest returns a signature for manifest as the specified dockerReference,
|
||||||
// using mech and keyIdentity.
|
// using mech and keyIdentity.
|
||||||
func SignDockerManifest(manifest []byte, dockerReference string, mech SigningMechanism, keyIdentity string) ([]byte, error) {
|
func SignDockerManifest(manifest []byte, dockerReference string, mech SigningMechanism, keyIdentity string) ([]byte, error) {
|
||||||
manifestDigest, err := dockerManifestDigest(manifest)
|
manifestDigest, err := dockerutils.ManifestDigest(manifest)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -84,7 +28,7 @@ func SignDockerManifest(manifest []byte, dockerReference string, mech SigningMec
|
|||||||
// using mech.
|
// using mech.
|
||||||
func VerifyDockerManifestSignature(unverifiedSignature, unverifiedManifest []byte,
|
func VerifyDockerManifestSignature(unverifiedSignature, unverifiedManifest []byte,
|
||||||
expectedDockerReference string, mech SigningMechanism, expectedKeyIdentity string) (*Signature, error) {
|
expectedDockerReference string, mech SigningMechanism, expectedKeyIdentity string) (*Signature, error) {
|
||||||
expectedManifestDigest, err := dockerManifestDigest(unverifiedManifest)
|
expectedManifestDigest, err := dockerutils.ManifestDigest(unverifiedManifest)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,6 @@ package signature
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"path/filepath"
|
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/projectatomic/skopeo/signature/fixtures"
|
"github.com/projectatomic/skopeo/signature/fixtures"
|
||||||
@ -10,53 +9,6 @@ import (
|
|||||||
"github.com/stretchr/testify/require"
|
"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) {
|
func TestSignDockerManifest(t *testing.T) {
|
||||||
mech, err := newGPGSigningMechanismInDirectory(testGPGHomeDirectory)
|
mech, err := newGPGSigningMechanismInDirectory(testGPGHomeDirectory)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
@ -3,8 +3,6 @@ package fixtures
|
|||||||
const (
|
const (
|
||||||
// TestImageManifestDigest is the Docker manifest digest of "image.manifest.json"
|
// TestImageManifestDigest is the Docker manifest digest of "image.manifest.json"
|
||||||
TestImageManifestDigest = "sha256:20bf21ed457b390829cdbeec8795a7bea1626991fda603e0d01b4e7f60427e55"
|
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 is the Docker image reference signed in "image.signature"
|
||||||
TestImageSignatureReference = "testing/manifest"
|
TestImageSignatureReference = "testing/manifest"
|
||||||
// TestKeyFingerprint is the fingerprint of the private key in this directory.
|
// TestKeyFingerprint is the fingerprint of the private key in this directory.
|
||||||
|
Loading…
Reference in New Issue
Block a user