2016-07-20 09:46:01 +00:00
package manifest
import (
"encoding/json"
2017-10-25 03:07:20 +00:00
"fmt"
2016-07-20 09:46:01 +00:00
2017-10-25 03:07:20 +00:00
"github.com/containers/image/types"
2016-07-20 09:46:01 +00:00
"github.com/docker/libtrust"
2016-10-17 13:53:40 +00:00
"github.com/opencontainers/go-digest"
2016-09-17 13:50:35 +00:00
imgspecv1 "github.com/opencontainers/image-spec/specs-go/v1"
2016-07-20 09:46:01 +00:00
)
// FIXME: Should we just use docker/distribution and docker/docker implementations directly?
// FIXME(runcom, mitr): should we havea mediatype pkg??
const (
2016-09-17 13:50:35 +00:00
// DockerV2Schema1MediaType MIME type represents Docker manifest schema 1
DockerV2Schema1MediaType = "application/vnd.docker.distribution.manifest.v1+json"
// DockerV2Schema1MediaType MIME type represents Docker manifest schema 1 with a JWS signature
DockerV2Schema1SignedMediaType = "application/vnd.docker.distribution.manifest.v1+prettyjws"
// DockerV2Schema2MediaType MIME type represents Docker manifest schema 2
DockerV2Schema2MediaType = "application/vnd.docker.distribution.manifest.v2+json"
2016-11-22 19:32:10 +00:00
// DockerV2Schema2ConfigMediaType is the MIME type used for schema 2 config blobs.
DockerV2Schema2ConfigMediaType = "application/vnd.docker.container.image.v1+json"
// DockerV2Schema2LayerMediaType is the MIME type used for schema 2 layers.
DockerV2Schema2LayerMediaType = "application/vnd.docker.image.rootfs.diff.tar.gzip"
2016-09-17 13:50:35 +00:00
// DockerV2ListMediaType MIME type represents Docker manifest schema 2 list
DockerV2ListMediaType = "application/vnd.docker.distribution.manifest.list.v2+json"
2016-11-22 19:32:10 +00:00
// DockerV2Schema2ForeignLayerMediaType is the MIME type used for schema 2 foreign layers.
DockerV2Schema2ForeignLayerMediaType = "application/vnd.docker.image.rootfs.foreign.diff.tar.gzip"
2016-07-20 09:46:01 +00:00
)
2016-09-17 13:50:35 +00:00
// DefaultRequestedManifestMIMETypes is a list of MIME types a types.ImageSource
// should request from the backend unless directed otherwise.
var DefaultRequestedManifestMIMETypes = [ ] string {
imgspecv1 . MediaTypeImageManifest ,
DockerV2Schema2MediaType ,
DockerV2Schema1SignedMediaType ,
DockerV2Schema1MediaType ,
2017-09-13 17:01:06 +00:00
// DockerV2ListMediaType, // FIXME: Restore this ASAP
2016-09-17 13:50:35 +00:00
}
2017-10-25 03:07:20 +00:00
// Manifest is an interface for parsing, modifying image manifests in isolation.
// Callers can either use this abstract interface without understanding the details of the formats,
// or instantiate a specific implementation (e.g. manifest.OCI1) and access the public members
// directly.
//
// See types.Image for functionality not limited to manifests, including format conversions and config parsing.
// This interface is similar to, but not strictly equivalent to, the equivalent methods in types.Image.
type Manifest interface {
// ConfigInfo returns a complete BlobInfo for the separate config object, or a BlobInfo{Digest:""} if there isn't a separate object.
ConfigInfo ( ) types . BlobInfo
// LayerInfos returns a list of BlobInfos of layers referenced by this image, in order (the root layer first, and then successive layered layers).
// The Digest field is guaranteed to be provided; Size may be -1.
// WARNING: The list may contain duplicates, and they are semantically relevant.
LayerInfos ( ) [ ] types . BlobInfo
// UpdateLayerInfos replaces the original layers with the specified BlobInfos (size+digest+urls), in order (the root layer first, and then successive layered layers)
UpdateLayerInfos ( layerInfos [ ] types . BlobInfo ) error
// Inspect returns various information for (skopeo inspect) parsed from the manifest,
// incorporating information from a configuration blob returned by configGetter, if
// the underlying image format is expected to include a configuration blob.
Inspect ( configGetter func ( types . BlobInfo ) ( [ ] byte , error ) ) ( * types . ImageInspectInfo , error )
// Serialize returns the manifest in a blob format.
// NOTE: Serialize() does not in general reproduce the original blob if this object was loaded from one, even if no modifications were made!
Serialize ( ) ( [ ] byte , error )
}
2016-07-20 09:46:01 +00:00
// GuessMIMEType 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 GuessMIMEType ( manifest [ ] byte ) string {
// 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" `
Signatures interface { } ` json:"signatures" `
} { }
if err := json . Unmarshal ( manifest , & meta ) ; err != nil {
return ""
}
switch meta . MediaType {
2017-05-17 17:18:35 +00:00
case DockerV2Schema2MediaType , DockerV2ListMediaType : // A recognized type.
2016-07-20 09:46:01 +00:00
return meta . MediaType
}
2016-09-17 13:50:35 +00:00
// this is the only way the function can return DockerV2Schema1MediaType, and recognizing that is essential for stripping the JWS signatures = computing the correct manifest digest.
2016-07-20 09:46:01 +00:00
switch meta . SchemaVersion {
case 1 :
if meta . Signatures != nil {
2016-09-17 13:50:35 +00:00
return DockerV2Schema1SignedMediaType
2016-07-20 09:46:01 +00:00
}
2016-09-17 13:50:35 +00:00
return DockerV2Schema1MediaType
2017-05-17 17:18:35 +00:00
case 2 :
// best effort to understand if this is an OCI image since mediaType
// isn't in the manifest for OCI anymore
// for docker v2s2 meta.MediaType should have been set. But given the data, this is our best guess.
ociMan := struct {
Config struct {
MediaType string ` json:"mediaType" `
} ` json:"config" `
Layers [ ] imgspecv1 . Descriptor ` json:"layers" `
} { }
if err := json . Unmarshal ( manifest , & ociMan ) ; err != nil {
return ""
}
if ociMan . Config . MediaType == imgspecv1 . MediaTypeImageConfig && len ( ociMan . Layers ) != 0 {
return imgspecv1 . MediaTypeImageManifest
}
ociIndex := struct {
Manifests [ ] imgspecv1 . Descriptor ` json:"manifests" `
} { }
if err := json . Unmarshal ( manifest , & ociIndex ) ; err != nil {
return ""
}
if len ( ociIndex . Manifests ) != 0 && ociIndex . Manifests [ 0 ] . MediaType == imgspecv1 . MediaTypeImageManifest {
return imgspecv1 . MediaTypeImageIndex
}
2016-09-17 13:50:35 +00:00
return DockerV2Schema2MediaType
2016-07-20 09:46:01 +00:00
}
return ""
}
// Digest returns the a digest of a docker manifest, with any necessary implied transformations like stripping v1s1 signatures.
2016-11-22 19:32:10 +00:00
func Digest ( manifest [ ] byte ) ( digest . Digest , error ) {
2016-09-17 13:50:35 +00:00
if GuessMIMEType ( manifest ) == DockerV2Schema1SignedMediaType {
2016-07-20 09:46:01 +00:00
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
}
}
2016-11-22 19:32:10 +00:00
return digest . FromBytes ( manifest ) , nil
2016-07-20 09:46:01 +00:00
}
// MatchesDigest 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.
2016-11-22 19:32:10 +00:00
func MatchesDigest ( manifest [ ] byte , expectedDigest digest . Digest ) ( bool , error ) {
2016-07-20 09:46:01 +00:00
// This should eventually support various digest types.
actualDigest , err := Digest ( manifest )
if err != nil {
return false , err
}
return expectedDigest == actualDigest , nil
}
2016-11-22 19:32:10 +00:00
// AddDummyV2S1Signature adds an JWS signature with a temporary key (i.e. useless) to a v2s1 manifest.
// This is useful to make the manifest acceptable to a Docker Registry (even though nothing needs or wants the JWS signature).
func AddDummyV2S1Signature ( manifest [ ] byte ) ( [ ] byte , error ) {
key , err := libtrust . GenerateECP256PrivateKey ( )
if err != nil {
return nil , err // Coverage: This can fail only if rand.Reader fails.
}
js , err := libtrust . NewJSONSignature ( manifest )
if err != nil {
return nil , err
}
if err := js . Sign ( key ) ; err != nil { // Coverage: This can fail basically only if rand.Reader fails.
return nil , err
}
return js . PrettySignature ( "signatures" )
}
2017-10-25 03:07:20 +00:00
// NormalizedMIMEType returns the effective MIME type of a manifest MIME type returned by a server,
// centralizing various workarounds.
func NormalizedMIMEType ( input string ) string {
switch input {
// "application/json" is a valid v2s1 value per https://github.com/docker/distribution/blob/master/docs/spec/manifest-v2-1.md .
// This works for now, when nothing else seems to return "application/json"; if that were not true, the mapping/detection might
// need to happen within the ImageSource.
case "application/json" :
return DockerV2Schema1SignedMediaType
case DockerV2Schema1MediaType , DockerV2Schema1SignedMediaType ,
imgspecv1 . MediaTypeImageManifest ,
DockerV2Schema2MediaType ,
DockerV2ListMediaType :
return input
default :
// If it's not a recognized manifest media type, or we have failed determining the type, we'll try one last time
// to deserialize using v2s1 as per https://github.com/docker/distribution/blob/master/manifests.go#L108
// and https://github.com/docker/distribution/blob/master/manifest/schema1/manifest.go#L50
//
// Crane registries can also return "text/plain", or pretty much anything else depending on a file extension “recognized” in the tag.
// This makes no real sense, but it happens
// because requests for manifests are
// redirected to a content distribution
// network which is configured that way. See https://bugzilla.redhat.com/show_bug.cgi?id=1389442
return DockerV2Schema1SignedMediaType
}
}
// FromBlob returns a Manifest instance for the specified manifest blob and the corresponding MIME type
func FromBlob ( manblob [ ] byte , mt string ) ( Manifest , error ) {
switch NormalizedMIMEType ( mt ) {
case DockerV2Schema1MediaType , DockerV2Schema1SignedMediaType :
return Schema1FromManifest ( manblob )
case imgspecv1 . MediaTypeImageManifest :
return OCI1FromManifest ( manblob )
case DockerV2Schema2MediaType :
return Schema2FromManifest ( manblob )
case DockerV2ListMediaType :
return nil , fmt . Errorf ( "Treating manifest lists as individual manifests is not implemented" )
default : // Note that this may not be reachable, NormalizedMIMEType has a default for unknown values.
return nil , fmt . Errorf ( "Unimplemented manifest MIME type %s" , mt )
}
}